PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
util.cpp
Go to the documentation of this file.
1
29
30#include "profinet/util.h"
31
32#include <arpa/inet.h>
33#include <fcntl.h>
34#include <net/if.h>
35#include <netdb.h>
36#include <netinet/in.h>
37#include <sys/ioctl.h>
38#include <sys/socket.h>
39#include <sys/types.h>
40#include <unistd.h>
41
42#include <algorithm>
43#include <ranges>
44
45#include "profinet/exceptions.h"
46
47extern "C"
48{
49#include <linux/if_packet.h>
50}
51
52#include <cerrno>
53#include <cstdio>
54#include <cstring>
55#include <sstream>
56#include <utility>
57
58namespace profinet
59{
60// =============================================================================
61// Byte Conversion Utilities
62// =============================================================================
63std::string Hex4(std::uint16_t value)
64{
65 return std::format("0x{:04X}", value);
66}
67
68std::string Hex2(std::uint8_t value)
69{
70 return std::format("0x{:02X}", value);
71}
72
73std::vector<std::uint8_t> ToVec(const std::string& input)
74{
75 return {input.begin(), input.end()};
76}
77
78std::string HexFallback(const char* widthPrefix, std::uint32_t value, int digitWidth)
79{
80 // {} -> The prefix string
81 // {:0{}X} -> Hexadecimal (X), zero-padded (0), with a dynamic width ({})
82 return std::format("{}(0x{:0{}X})", widthPrefix, value, digitWidth);
83}
84
85// =============================================================================
86// Address Conversion Utilities
87// =============================================================================
88
89std::size_t SkipVlanTags(std::span<const std::uint8_t> frame) noexcept
90{
91 std::size_t offset = 12;
92
93 while (frame.size() >= offset + 4)
94 {
95 // Read 16-bit TPID in network byte order (big-endian)
96 const std::uint16_t tpid = (static_cast<std::uint16_t>(frame[offset]) << 8) | frame[offset + 1];
97
98 // 802.1Q (0x8100), 802.1ad (0x88A8), 802.1Q double-tagging legacy (0x9100)
99 if (tpid == 0x8100 || tpid == 0x88A8 || tpid == 0x9100)
100 {
101 offset += 4;
102 }
103 else
104 {
105 break;
106 }
107 }
108
109 return offset;
110}
111
112std::string ToHex(const std::uint8_t* data, std::size_t len)
113{
114 std::string out;
115 out.reserve(len * 2);
116 static const char* digits = "0123456789abcdef";
117 for (std::size_t i = 0; i < len; ++i)
118 {
119 out.push_back(digits[data[i] >> 4]);
120 out.push_back(digits[data[i] & 0x0F]);
121 }
122 return out;
123}
124
125std::string ToHex(const std::vector<std::uint8_t>& data)
126{
127 return ToHex(data.data(), data.size());
128}
129
130MacAddress String2Mac(const std::string& macStr)
131{
132 // Strict structural constants for a standard MAC address
133 static constexpr std::size_t expectedStringLength = 17;
134 static constexpr std::size_t singleByteHexLength = 2;
135 static constexpr int hexBase = 16;
136
137 // Explicit array index positions for delimiters
138 static constexpr std::array<std::size_t, 5> delimiterIndices = {2, 5, 8, 11, 14};
139
140 // Enforce strict total length check
141 if (macStr.size() != expectedStringLength)
142 {
143 throw InvalidMACError(std::format("Invalid MAC address length: '{}'", macStr));
144 }
145
146 // Validate format
147 // We loop through indices from 0 to 16
148 bool isValidStructure = true;
149 for (std::size_t i = 0; i < expectedStringLength; ++i)
150 {
151 // Check if current index 'i' matches one of our delimiter positions
152 const bool isDelimiterIdx = std::ranges::any_of(delimiterIndices,
153 [i](std::size_t d)
154 { return d == i; });
155
156 if (isDelimiterIdx)
157 {
158 if (macStr[i] != ':')
159 {
160 isValidStructure = false;
161 break;
162 }
163 }
164 else
165 {
166 if (std::isxdigit(static_cast<unsigned char>(macStr[i])) == 0)
167 {
168 isValidStructure = false;
169 break;
170 }
171 }
172 }
173
174 if (!isValidStructure)
175 {
176 throw InvalidMACError(std::format("Invalid MAC address characters or format: '{}'", macStr));
177 }
178
179 MacAddress mac{};
180 std::string_view macView{macStr};
181
182 // 3. Parse the hex pairs directly into the destination array using string_view
183 static constexpr std::size_t hexPairStride = 3; // Length of "xx:" segment
184 for (std::size_t i = 0; i < macAddressLength; ++i)
185 {
186 const std::size_t offset = i * hexPairStride;
187 const std::string_view hexPair = macView.substr(offset, singleByteHexLength);
188
189 // std::stoul is fully safe here because we pre-validated all digits above
190 mac.at(i) = static_cast<std::uint8_t>(std::stoul(std::string(hexPair), nullptr, hexBase));
191 }
192
193 return mac;
194}
195
196std::string Mac2String(const MacAddress& mac)
197{
198 return Mac2String(mac.data(), mac.size());
199}
200
201std::string Mac2String(const std::uint8_t* macBytes, std::size_t len)
202{
203 if (len != 6)
204 {
205 throw InvalidMACError("MAC address must be 6 bytes, got " + std::to_string(len));
206 }
207 // Extract the formatted MAC address into a dedicated string variable
208 const std::string macStr = std::format("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
209 macBytes[0], macBytes[1], macBytes[2],
210 macBytes[3], macBytes[4], macBytes[5]);
211 return macStr;
212}
213
214std::string String2Ip(std::span<const std::uint8_t> ipBytes)
215{
216 if (ipBytes.size() < 4)
217 {
218 throw InvalidIPError("IP address must be at least 4 bytes, got " + std::to_string(ipBytes.size()));
219 }
220
221 return std::to_string(ipBytes[0]) + "." + std::to_string(ipBytes[1]) + "." +
222 std::to_string(ipBytes[2]) + "." + std::to_string(ipBytes[3]);
223}
224
225std::string String2Ip(const std::uint8_t* ipBytes, std::size_t len)
226{
227 return String2Ip(std::span<const std::uint8_t>(ipBytes, len));
228}
229
230std::array<std::uint8_t, 4> Ip2String(const std::string& ipStr)
231{
232 if (ipStr.empty())
233 {
234 throw InvalidIPError("IP address cannot be empty");
235 }
236
237 in_addr addr{};
238 if (::inet_pton(AF_INET, ipStr.c_str(), &addr) != 1)
239 {
240 throw InvalidIPError("Invalid IP address: '" + ipStr + "'");
241 }
242 std::array<std::uint8_t, 4> out{};
243 std::memcpy(out.data(), &addr.s_addr, 4);
244 return out;
245}
246
247std::string DecodeBytes(const std::uint8_t* data, std::size_t len)
248{
249 std::size_t end = len;
250 while (end > 0 && data[end - 1] == 0)
251 {
252 --end;
253 }
254 // NOLINTNEXTLINE(modernize-return-braced-init-list)
255 return std::string(reinterpret_cast<const char*>(data), end);
256}
257
258std::string DecodeBytes(const std::vector<std::uint8_t>& data)
259{
260 return DecodeBytes(data.data(), data.size());
261}
262
263// =============================================================================
264// Socket Utilities (Linux AF_PACKET)
265// =============================================================================
266
267MacAddress GetMac(const std::string& ifname)
268{
269 if (ifname.empty())
270 {
271 throw SocketError("Interface name cannot be empty");
272 }
273
274 const int s = ::socket(AF_INET, SOCK_DGRAM, 0);
275 if (s < 0)
276 {
277 throw SocketError("Failed to get MAC address for '" + ifname + "': " + std::strerror(errno));
278 }
279
280 ifreq ifr{};
281 std::strncpy(ifr.ifr_name, ifname.c_str(), IFNAMSIZ - 1);
282 if (::ioctl(s, SIOCGIFHWADDR, &ifr) < 0)
283 {
284 const int err = errno;
285 ::close(s);
286 throw SocketError("Failed to get MAC address for '" + ifname + "': " + std::strerror(err));
287 }
288 ::close(s);
289
290 MacAddress mac{};
291 std::memcpy(mac.data(), ifr.ifr_hwaddr.sa_data, 6);
292 return mac;
293}
294
295EthernetSocket::EthernetSocket(const std::string& interface, std::uint16_t ethertype)
296{
297 if (interface.empty())
298 {
299 throw SocketError("Interface name cannot be empty");
300 }
301
302 const std::uint16_t proto = ethertype != 0 ? ethertype : static_cast<std::uint16_t>(ETH_P_ALL);
303
304 fd = ::socket(AF_PACKET, SOCK_RAW, htons(proto));
305 if (fd < 0)
306 {
307 if (errno == EPERM || errno == EACCES)
308 {
309 throw PermissionDeniedError("Root privileges required for raw socket access: " +
310 std::string(std::strerror(errno)));
311 }
312 throw SocketError("Failed to create socket on '" + interface + "': " + std::strerror(errno));
313 }
314
315 const unsigned int ifindex = if_nametoindex(interface.c_str());
316 if (ifindex == 0)
317 {
318 const int err = errno;
319 ::close(fd);
320 fd = -1;
321 throw SocketError("Failed to create socket on '" + interface + "': " + std::strerror(err));
322 }
323
324 sockaddr_ll addr{};
325 addr.sll_family = AF_PACKET;
326 addr.sll_protocol = htons(proto);
327 addr.sll_ifindex = static_cast<int>(ifindex);
328
329 if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
330 {
331 const int err = errno;
332 ::close(fd);
333 fd = -1;
334 throw SocketError("Failed to create socket on '" + interface + "': " + std::strerror(err));
335 }
336}
337
339{
340 if (fd >= 0)
341 {
342 ::close(fd);
343 }
344}
345
347 : fd(other.fd)
348{
349 other.fd = -1;
350}
351
353{
354 if (this != &other)
355 {
356 if (fd >= 0)
357 {
358 ::close(fd);
359 }
360 fd = other.fd;
361 other.fd = -1;
362 }
363 return *this;
364}
365
366void EthernetSocket::Send(const std::vector<std::uint8_t>& frame) const
367{
368 const ssize_t n = ::send(fd, frame.data(), frame.size(), 0);
369 if (n < 0 || static_cast<std::size_t>(n) != frame.size())
370 {
371 throw SocketError(std::string("Failed to send frame: ") + std::strerror(errno));
372 }
373}
374
375void EthernetSocket::SetTimeout(std::chrono::milliseconds timeout) const
376{
377 timeval tv{};
378 tv.tv_sec = static_cast<time_t>(timeout.count() / 1000);
379 tv.tv_usec = static_cast<suseconds_t>((timeout.count() % 1000) * 1000);
380 if (::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
381 {
382 throw SocketError(std::string("Failed to set socket timeout: ") + std::strerror(errno));
383 }
384}
385
386std::vector<std::uint8_t> EthernetSocket::Recv() const
387{
388 std::vector<std::uint8_t> buf(MAX_ETHERNET_FRAME);
389 const ssize_t n = ::recv(fd, buf.data(), buf.size(), 0);
390 if (n < 0)
391 {
392 if (errno == EAGAIN || errno == EWOULDBLOCK)
393 {
394 return {}; // timeout
395 }
396 throw SocketError(std::string("Socket error during receive: ") + std::strerror(errno));
397 }
398 buf.resize(static_cast<std::size_t>(n));
399 return buf;
400}
401
402UdpSocket::UdpSocket(const std::string& host, std::uint16_t port,
403 std::chrono::duration<double> timeout)
404{
405 addrinfo hints{};
406 hints.ai_family = AF_INET;
407 hints.ai_socktype = SOCK_DGRAM;
408 addrinfo* result = nullptr;
409
410 const std::string portStr = std::to_string(port);
411 if (::getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result) != 0 || result == nullptr)
412 {
413 throw SocketError("Failed to create UDP socket to " + host + ":" + portStr);
414 }
415
416 fd = ::socket(result->ai_family, result->ai_socktype, result->ai_protocol);
417 if (fd < 0)
418 {
419 ::freeaddrinfo(result);
420 throw SocketError("Failed to create UDP socket to " + host + ":" + portStr + ": " +
421 std::strerror(errno));
422 }
423
424 timeval tv{};
425 tv.tv_sec = static_cast<time_t>(timeout.count());
426 static constexpr int microsecondsPerSecond = 1'000'000;
427 tv.tv_usec = static_cast<suseconds_t>((timeout.count() - static_cast<double>(tv.tv_sec)) * microsecondsPerSecond);
428 ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
429
430 if (::connect(fd, result->ai_addr, result->ai_addrlen) < 0)
431 {
432 const int err = errno;
433 ::freeaddrinfo(result);
434 ::close(fd);
435 fd = -1;
436 throw SocketError("Failed to create UDP socket to " + host + ":" + portStr + ": " +
437 std::strerror(err));
438 }
439 ::freeaddrinfo(result);
440}
441
443{
444 if (fd >= 0)
445 {
446 ::close(fd);
447 }
448}
449
451 : fd(other.fd)
452{
453 other.fd = -1;
454}
455
457{
458 if (this != &other)
459 {
460 if (fd >= 0)
461 {
462 ::close(fd);
463 }
464 fd = other.fd;
465 other.fd = -1;
466 }
467 return *this;
468}
469
470void UdpSocket::Send(const std::vector<std::uint8_t>& data) const
471{
472 const ssize_t n = ::send(fd, data.data(), data.size(), 0);
473 if (n < 0 || static_cast<std::size_t>(n) != data.size())
474 {
475 throw SocketError(std::string("Failed to send UDP datagram: ") + std::strerror(errno));
476 }
477}
478
479std::vector<std::uint8_t> UdpSocket::Recv(std::size_t maxLen) const
480{
481 std::vector<std::uint8_t> buf(maxLen);
482 const ssize_t n = ::recv(fd, buf.data(), buf.size(), 0);
483 if (n < 0)
484 {
485 if (errno == EAGAIN || errno == EWOULDBLOCK)
486 {
487 return {};
488 }
489 throw SocketError(std::string("Socket error during UDP receive: ") + std::strerror(errno));
490 }
491 buf.resize(static_cast<std::size_t>(n));
492 return buf;
493}
494std::filesystem::path GetExecutableDirectory()
495{
496 constexpr std::string_view processLink = "/proc/self/exe";
497
498 try
499 {
500 // resolve a symlink to its absolute path
501 if (std::filesystem::is_symlink(processLink) || std::filesystem::exists(processLink))
502 {
503 return std::filesystem::read_symlink(processLink).parent_path();
504 }
505 }
506 catch (const std::filesystem::filesystem_error& e)
507 {
508 // std::cerr << "Filesystem error: " << e.what() << '\n';
509 throw std::runtime_error("Filesystem error: " + std::string(e.what()));
510 }
511 throw std::runtime_error("Critical error: /proc/self/exe not available on this system");
512}
513} // namespace profinet
A minimal RAII wrapper around a Linux AF_PACKET raw socket bound to an interface.
Definition util.h:196
int fd
Underlying socket file descriptor, or -1 if closed/moved-from.
Definition util.h:248
EthernetSocket & operator=(const EthernetSocket &)=delete
Not copyable (owns a raw socket file descriptor).
void SetTimeout(std::chrono::milliseconds timeout) const override
Set the receive timeout.
Definition util.cpp:375
EthernetSocket(const std::string &interface, std::uint16_t ethertype=0)
Bind a raw socket to an interface.
Definition util.cpp:295
void Send(const std::vector< std::uint8_t > &frame) const override
Send a raw Ethernet frame.
Definition util.cpp:366
std::vector< std::uint8_t > Recv() const override
Receive up to MAX_ETHERNET_FRAME bytes.
Definition util.cpp:386
~EthernetSocket()
Close the underlying socket.
Definition util.cpp:338
Invalid IP address format.
Definition exceptions.h:651
Invalid MAC address format.
Definition exceptions.h:639
Insufficient permissions for raw socket.
Definition exceptions.h:675
Socket operation error.
Definition exceptions.h:663
A connected UDP socket with a timeout.
Definition util.h:254
~UdpSocket()
Close the underlying socket.
Definition util.cpp:442
std::vector< std::uint8_t > Recv(std::size_t maxLen=defaultReceiveBufferSize) const
Receive a UDP datagram from the connected peer.
Definition util.cpp:479
UdpSocket(const std::string &host, std::uint16_t port, std::chrono::duration< double > timeout=std::chrono::duration< double >(defaultTimeout))
Resolve a host and connect a UDP socket to it.
Definition util.cpp:402
void Send(const std::vector< std::uint8_t > &data) const
Send a UDP datagram to the connected peer.
Definition util.cpp:470
int fd
Underlying socket file descriptor, or -1 if closed/moved-from.
Definition util.h:305
UdpSocket & operator=(const UdpSocket &)=delete
Not copyable (owns a socket file descriptor).
Declares exceptions and error types used by the PROFINET IO controller stack.
std::string Hex2(std::uint8_t value)
Format an 8-bit unsigned integer as a 2-digit hexadecimal string.
Definition util.cpp:68
std::vector< std::uint8_t > ToVec(const std::string &input)
Convert a string into a vector of raw bytes.
Definition util.cpp:73
std::string String2Ip(std::span< const std::uint8_t > ipBytes)
Format raw IPv4 address bytes as a dotted string.
Definition util.cpp:214
std::array< std::uint8_t, macAddressLength > MacAddress
A 6-byte Ethernet MAC address.
Definition util.h:67
std::array< std::uint8_t, 4 > Ip2String(const std::string &ipStr)
Parse a dotted-decimal IPv4 address string.
Definition util.cpp:230
std::filesystem::path GetExecutableDirectory()
Retrieves the absolute directory path of the currently running executable.
Definition util.cpp:494
MacAddress String2Mac(const std::string &macStr)
Parse a colon-separated MAC address string.
Definition util.cpp:130
constexpr std::size_t MAX_ETHERNET_FRAME
Maximum PROFINET Ethernet frame size in bytes (including VLAN tag headroom).
Definition util.h:30
std::size_t SkipVlanTags(std::span< const std::uint8_t > frame) noexcept
Return the byte offset of the real EtherType in a raw Ethernet frame.
Definition util.cpp:89
MacAddress GetMac(const std::string &ifname)
Get the MAC address of a network interface.
Definition util.cpp:267
constexpr int macAddressLength
Constant lenght of a mac address.
Definition util.h:41
std::string ToHex(const std::uint8_t *data, std::size_t len)
Hex-encode a byte buffer.
Definition util.cpp:112
std::string Hex4(std::uint16_t value)
Format a 16-bit unsigned integer as a 4-digit hexadecimal string.
Definition util.cpp:63
std::string HexFallback(const char *widthPrefix, std::uint32_t value, int digitWidth)
Format a hexadecimal string with a custom prefix and dynamic width.
Definition util.cpp:78
constexpr int ETH_P_ALL
Linux AF_PACKET protocol value matching every EtherType.
Definition util.h:38
std::string Mac2String(const MacAddress &mac)
Format a MAC address as a colon-separated string.
Definition util.cpp:196
std::string DecodeBytes(const std::uint8_t *data, std::size_t len)
Decode bytes to a UTF-8 string, stripping trailing NUL bytes.
Definition util.cpp:247
Declares Linux Ethernet, addressing, timing, and utility helpers used by the PROFINET stack.