ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
enip.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file enip.cpp
6 * @brief EtherNet/IP encapsulation builder + parser (pure, host-tested; constants per Wireshark).
7 */
8
10
11#if PC_ENABLE_ENIP
12
13#include <string.h>
14
15// EtherNet/IP fields are little-endian.
16static size_t put16(uint8_t *p, uint16_t v)
17{
18 p[0] = (uint8_t)(v & 0xFF);
19 p[1] = (uint8_t)(v >> 8);
20 return 2;
21}
22
23static size_t put32(uint8_t *p, uint32_t v)
24{
25 p[0] = (uint8_t)(v & 0xFF);
26 p[1] = (uint8_t)(v >> 8);
27 p[2] = (uint8_t)(v >> 16);
28 p[3] = (uint8_t)(v >> 24);
29 return 4;
30}
31
32static uint16_t get16(const uint8_t *p)
33{
34 return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
35}
36
37static uint32_t get32(const uint8_t *p)
38{
39 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
40}
41
42size_t pc_eip_build(uint8_t *buf, size_t cap, const EipHeader *h, const uint8_t *data, size_t data_len)
43{
44 if (!buf || !h || (data_len && !data) || data_len > 0xFFFF)
45 {
46 return 0;
47 }
48 size_t total = EIP_HEADER_SIZE + data_len;
49 if (total > cap)
50 {
51 return 0;
52 }
53 size_t p = 0;
54 p += put16(buf + p, h->command);
55 p += put16(buf + p, (uint16_t)data_len); // length covers the command data only
56 p += put32(buf + p, h->session_handle);
57 p += put32(buf + p, h->status);
58 memcpy(buf + p, h->sender_context, 8);
59 p += 8;
60 p += put32(buf + p, h->options);
61 if (data_len)
62 {
63 memcpy(buf + p, data, data_len);
64 p += data_len;
65 }
66 return p;
67}
68
69bool pc_eip_parse(const uint8_t *buf, size_t len, EipHeader *out, const uint8_t **data, size_t *data_len)
70{
71 if (!buf || !out || len < EIP_HEADER_SIZE)
72 {
73 return false;
74 }
75 out->command = get16(buf);
76 out->length = get16(buf + 2);
77 out->session_handle = get32(buf + 4);
78 out->status = get32(buf + 8);
79 memcpy(out->sender_context, buf + 12, 8);
80 out->options = get32(buf + 20);
81 if ((size_t)EIP_HEADER_SIZE + out->length > len) // declared data not fully buffered
82 {
83 return false;
84 }
85 if (data)
86 {
87 *data = buf + EIP_HEADER_SIZE;
88 }
89 if (data_len)
90 {
91 *data_len = out->length;
92 }
93 return true;
94}
95
96size_t pc_eip_build_register_session(uint8_t *buf, size_t cap, const uint8_t sender_context[8])
97{
98 EipHeader h;
99 memset(&h, 0, sizeof(h));
100 h.command = EIP_CMD_REGISTER_SESSION;
101 if (sender_context)
102 {
103 memcpy(h.sender_context, sender_context, 8);
104 }
105 uint8_t data[4];
106 put16(data, 1); // protocol version
107 put16(data + 2, 0); // options flags
108 return pc_eip_build(buf, cap, &h, data, sizeof(data));
109}
110
111size_t pc_eip_build_unregister_session(uint8_t *buf, size_t cap, uint32_t session_handle,
112 const uint8_t sender_context[8])
113{
114 EipHeader h;
115 memset(&h, 0, sizeof(h));
116 h.command = EIP_CMD_UNREGISTER_SESSION;
117 h.session_handle = session_handle; // the session to close
118 if (sender_context)
119 {
120 memcpy(h.sender_context, sender_context, 8);
121 }
122 return pc_eip_build(buf, cap, &h, nullptr, 0); // no command-specific data
123}
124
125size_t pc_eip_build_send_rr_data(uint8_t *buf, size_t cap, uint32_t session_handle, const uint8_t sender_context[8],
126 uint16_t timeout, const uint8_t *cip, size_t pc_cip_len)
127{
128 if (!buf || (pc_cip_len && !cip) || pc_cip_len > 0xFFFF)
129 {
130 return 0;
131 }
132 // command data: interface handle(4) + timeout(2) + CPF{ count(2) + null item(4) + unconn item(4+cip) }
133 size_t data_len = 4 + 2 + 2 + 4 + 4 + pc_cip_len;
134 size_t total = EIP_HEADER_SIZE + data_len;
135 if (total > cap || data_len > 0xFFFF)
136 {
137 return 0;
138 }
139
140 // Write the header (length = the command-data length) then the command data straight into
141 // buf - no temp buffer, so a large CIP payload never lands on the stack.
142 EipHeader h;
143 memset(&h, 0, sizeof(h));
144 h.command = EIP_CMD_SEND_RR_DATA;
145 h.session_handle = session_handle;
146 if (sender_context)
147 {
148 memcpy(h.sender_context, sender_context, 8);
149 }
150 // GCOVR_EXCL_START unreachable: total>cap above already proved cap>=40>EIP_HEADER_SIZE, so this
151 // header-only build (data_len=0) can never fail here; the branch on the `if` itself is dead too.
152 if (pc_eip_build(buf, cap, &h, nullptr, 0) == 0) // writes only the 24-octet header, length 0
153 {
154 return 0;
155 }
156 // GCOVR_EXCL_STOP
157 // Patch the length field (offset 2) to the real command-data length.
158 put16(buf + 2, (uint16_t)data_len);
159
160 size_t p = EIP_HEADER_SIZE;
161 p += put32(buf + p, 0); // interface handle (CIP)
162 p += put16(buf + p, timeout); // timeout
163 p += put16(buf + p, 2); // CPF item count
164 p += put16(buf + p, EIP_CPF_NULL);
165 p += put16(buf + p, 0); // null address item length
166 p += put16(buf + p, EIP_CPF_UNCONNECTED_DATA);
167 p += put16(buf + p, (uint16_t)pc_cip_len);
168 if (pc_cip_len)
169 {
170 memcpy(buf + p, cip, pc_cip_len);
171 p += pc_cip_len;
172 }
173 return p;
174}
175
176size_t pc_eip_build_list_identity(uint8_t *buf, size_t cap, const uint8_t sender_context[8])
177{
178 EipHeader h;
179 memset(&h, 0, sizeof(h));
180 h.command = EIP_CMD_LIST_IDENTITY;
181 if (sender_context)
182 {
183 memcpy(h.sender_context, sender_context, 8);
184 }
185 return pc_eip_build(buf, cap, &h, nullptr, 0); // no command-specific data
186}
187
188bool pc_eip_parse_list_identity(const uint8_t *data, size_t data_len, EipIdentity *out)
189{
190 if (!data || !out || data_len < 2) // item count
191 {
192 return false;
193 }
194 uint16_t item_count = get16(data);
195 size_t pos = 2;
196 for (uint16_t i = 0; i < item_count; i++)
197 {
198 if (pos + 4 > data_len)
199 {
200 return false;
201 }
202 uint16_t type = get16(data + pos);
203 uint16_t ilen = get16(data + pos + 2);
204 pos += 4;
205 if (pos + ilen > data_len)
206 {
207 return false;
208 }
209 if (type == EIP_CPF_LIST_IDENTITY)
210 {
211 const uint8_t *it = data + pos;
212 if (ilen <
213 33) // proto(2) + sockaddr(16) + vendor/type/code(6) + rev(2) + status(2) + serial(4) + namelen(1)
214 {
215 return false;
216 }
217 uint8_t name_len = it[32];
218 if ((size_t)ilen < (size_t)34 + name_len) // + the name + the trailing state octet
219 {
220 return false;
221 }
222 out->protocol_version = get16(it);
223 // it[2..17] is the 16-octet CIP socket address (network-order); not reinterpreted here.
224 out->vendor_id = get16(it + 18);
225 out->device_type = get16(it + 20);
226 out->product_code = get16(it + 22);
227 out->revision_major = it[24];
228 out->revision_minor = it[25];
229 out->status = get16(it + 26);
230 out->serial_number = get32(it + 28);
231 out->product_name_len = name_len;
232 out->product_name = (const char *)(it + 33);
233 out->state = it[33 + name_len];
234 return true;
235 }
236 pos += ilen;
237 }
238 return false; // no List Identity item
239}
240
241bool pc_eip_parse_send_rr_data(const uint8_t *data, size_t data_len, const uint8_t **cip, size_t *pc_cip_len)
242{
243 if (!data || data_len < 8) // interface handle(4) + timeout(2) + item count(2)
244 {
245 return false;
246 }
247 size_t pos = 6; // skip interface handle + timeout
248 uint16_t item_count = get16(data + pos);
249 pos += 2;
250 for (uint16_t i = 0; i < item_count; i++)
251 {
252 if (pos + 4 > data_len)
253 {
254 return false;
255 }
256 uint16_t type = get16(data + pos);
257 uint16_t ilen = get16(data + pos + 2);
258 pos += 4;
259 if (pos + ilen > data_len)
260 {
261 return false;
262 }
263 if (type == EIP_CPF_UNCONNECTED_DATA)
264 {
265 if (cip)
266 {
267 *cip = data + pos;
268 }
269 if (pc_cip_len)
270 {
271 *pc_cip_len = ilen;
272 }
273 return true;
274 }
275 pos += ilen;
276 }
277 return false; // no unconnected data item
278}
279
280#endif // PC_ENABLE_ENIP
EtherNet/IP encapsulation codec (PC_ENABLE_ENIP) - zero-heap builder + parser for the ODVA EtherNet/I...