ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
mms.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 mms.cpp
6 * @brief IEC 61850 MMS PDU codec (see mms.h).
7 */
8
10
11#if PC_ENABLE_MMS
12
13#include <string.h>
14
15namespace
16{
17// BER definite-length octet count for a length value < 64 KiB.
18size_t len_octets(size_t len)
19{
20 if (len < 0x80)
21 {
22 return 1;
23 }
24 if (len < 0x100)
25 {
26 return 2;
27 }
28 // Reachable: tlv() calls this on the raw, still-unvalidated val_len (a caller-supplied data_len can be
29 // >=256) and only rejects the total afterwards, so the 3-octet form is computed before the cap check.
30 return 3;
31}
32
33// Write a BER length; returns octets written.
34size_t write_len(uint8_t *p, size_t len)
35{
36 if (len < 0x80)
37 {
38 p[0] = (uint8_t)len;
39 return 1;
40 }
41 if (len < 0x100)
42 {
43 p[0] = 0x81;
44 p[1] = (uint8_t)len;
45 return 2;
46 }
47 // Reachable: the OUTERMOST wrap's value is the assembled body, which is exactly sizeof(body) == 256
48 // octets when the caller's payload fills it, and its cap is the caller's buffer rather than a fixed
49 // 256B scratch - so a 256-octet value with a large enough cap reaches the 3-octet (0x82) form.
50 p[0] = 0x82;
51 p[1] = (uint8_t)(len >> 8);
52 p[2] = (uint8_t)len;
53 return 3;
54}
55
56// Write a full TLV (tag + length + value) at out; returns total length, or 0 on overflow.
57size_t tlv(uint8_t tag, const uint8_t *val, size_t val_len, uint8_t *out, size_t cap)
58{
59 // One source of truth for the length-octet count: the value offset (k) and the total (n) both derive
60 // from it, so k + val_len == n is provable (write_len writes exactly this many octets).
61 size_t k = 1 + len_octets(val_len); // value offset: tag + length octets
62 size_t n = k + val_len; // total = offset + value
63 if (n > cap)
64 {
65 return 0;
66 }
67 out[0] = tag;
68 write_len(out + 1, val_len); // writes exactly len_octets(val_len) octets
69 // Copy length expressed as n - k (== val_len) so the destination bound flows straight from the
70 // n <= cap check above: out + k + (n - k) = out + n <= out + cap, and it reads n - k == val_len bytes
71 // from the val_len-sized val. Provably in bounds; all three callers pass val buffers >= val_len
72 // (data[data_len], scratch[256] with n<=256, idc[5] with idlen<=5). S3519 here explores an infeasible
73 // path (it assumes val_len >= cap - k, e.g. 5 >= 254) that the n <= cap guard rules out.
74 if (n > k)
75 {
76 memcpy(out + k, val, n - k); // NOSONAR - see above: bound proven, analyzer follows an infeasible path
77 }
78 return n;
79}
80
81// Minimal big-endian unsigned INTEGER content (with a leading 0 if the MSB is set), into buf; returns len.
82size_t int_content(uint32_t v, uint8_t *buf)
83{
84 uint8_t tmp[4];
85 size_t t = 0;
86 if (v == 0)
87 {
88 tmp[t++] = 0;
89 }
90 else
91 {
92 for (uint32_t x = v; x; x >>= 8)
93 {
94 tmp[t++] = (uint8_t)x;
95 }
96 }
97 size_t k = 0;
98 if (tmp[t - 1] & 0x80)
99 {
100 buf[k++] = 0x00;
101 }
102 for (size_t i = 0; i < t; i++)
103 {
104 buf[k++] = tmp[t - 1 - i];
105 }
106 return k;
107}
108
109// Decode a BER definite length at pdu[at]. Sets *value and *hdr (octets the length field spans: 1 for
110// short form, 1+nb for long form). Returns false for an out-of-bounds field or an unsupported long form
111// (nb outside 1..2). Only short/2-byte lengths occur in the PDUs this codec accepts.
112bool pc_ber_len(const uint8_t *pdu, size_t len, size_t at, size_t *value, size_t *hdr)
113{
114 if (at >= len)
115 {
116 return false;
117 }
118 size_t v = pdu[at];
119 if (v & 0x80)
120 {
121 size_t nb = v & 0x7F;
122 if (nb == 0 || nb > 2 || at + 1 + nb > len)
123 {
124 return false;
125 }
126 v = 0;
127 for (size_t i = 0; i < nb; i++)
128 {
129 v = (v << 8) | pdu[at + 1 + i];
130 }
131 *hdr = 1 + nb;
132 }
133 else
134 {
135 *hdr = 1;
136 }
137 *value = v;
138 return true;
139}
140} // namespace
141
142size_t pc_mms_read_request(uint32_t invoke_id, const char *item_name, uint8_t *out, size_t cap)
143{
144 if (!out || !item_name)
145 {
146 return 0;
147 }
148 size_t name_len = strnlen(item_name, 128 + 1);
149 if (name_len > 128)
150 {
151 return 0;
152 }
153
154 // Each wrap checks its tlv() result; with name_len capped at 128 (above) and every buffer here 256
155 // bytes, none of these per-wrap overflow guards can fire (the running length maxes ~146). They are
156 // retained as defense-in-depth; the condition AND the return carry GCOVR_EXCL_LINE so neither the
157 // line nor its branches are reported (a marker on the return alone still leaves the branch counted).
158 uint8_t scratch[256];
159 // innermost: objectName VisibleString (0x1A) with the item name.
160 size_t n = tlv(0x1A, (const uint8_t *)item_name, name_len, scratch, sizeof(scratch));
161 if (!n) // GCOVR_EXCL_LINE
162 {
163 return 0; // GCOVR_EXCL_LINE
164 }
165 // A0 name [0]
166 uint8_t a0[256];
167 n = tlv(0xA0, scratch, n, a0, sizeof(a0));
168 if (!n) // GCOVR_EXCL_LINE
169 {
170 return 0; // GCOVR_EXCL_LINE
171 }
172 // 30 SEQUENCE (one VariableSpecification)
173 n = tlv(0x30, a0, n, scratch, sizeof(scratch));
174 if (!n) // GCOVR_EXCL_LINE
175 {
176 return 0; // GCOVR_EXCL_LINE
177 }
178 // A0 listOfVariable [0]
179 n = tlv(0xA0, scratch, n, a0, sizeof(a0));
180 if (!n) // GCOVR_EXCL_LINE
181 {
182 return 0; // GCOVR_EXCL_LINE
183 }
184 // A1 variableAccessSpecification [1]
185 n = tlv(0xA1, a0, n, scratch, sizeof(scratch));
186 if (!n) // GCOVR_EXCL_LINE
187 {
188 return 0; // GCOVR_EXCL_LINE
189 }
190 // A4 read [4]
191 n = tlv(Mms::MMS_SERVICE_READ, scratch, n, a0, sizeof(a0));
192 if (!n) // GCOVR_EXCL_LINE
193 {
194 return 0; // GCOVR_EXCL_LINE
195 }
196
197 // Prepend the invokeID INTEGER, then wrap in the confirmed-request PDU.
198 uint8_t idc[5];
199 size_t idlen = int_content(invoke_id, idc);
200 uint8_t body[256];
201 size_t bn = tlv(Mms::MMS_TAG_INVOKE_ID, idc, idlen, body, sizeof(body));
202 // GCOVR_EXCL_LINE neither half can fire here: bn is 2+idlen (2..7, never 0) and bn+n maxes ~152 << 256
203 // because name_len is capped at 128 above. Unreachable, kept defensively.
204 if (!bn || bn + n > sizeof(body)) // GCOVR_EXCL_LINE
205 {
206 return 0; // GCOVR_EXCL_LINE
207 }
208 memcpy(body + bn, a0, n); // append the A4 read
209 bn += n;
210 return tlv(Mms::MMS_PDU_CONFIRMED_REQUEST, body, bn, out, cap);
211}
212
213size_t pc_mms_read_response(uint32_t invoke_id, const uint8_t *data, size_t data_len, uint8_t *out, size_t cap)
214{
215 if (!out || (data_len && !data))
216 {
217 return 0;
218 }
219 uint8_t scratch[256];
220 // listOfAccessResult SEQUENCE (0xA1 in Read response) wrapping the caller's Data value(s).
221 size_t n = tlv(0xA1, data, data_len, scratch, sizeof(scratch));
222 if (!n)
223 {
224 return 0;
225 }
226 // A4 read response [4]
227 uint8_t svc[256];
228 n = tlv(Mms::MMS_SERVICE_READ, scratch, n, svc, sizeof(svc));
229 if (!n)
230 {
231 return 0;
232 }
233
234 uint8_t idc[5];
235 size_t idlen = int_content(invoke_id, idc);
236 uint8_t body[256];
237 size_t bn = tlv(Mms::MMS_TAG_INVOKE_ID, idc, idlen, body, sizeof(body));
238 // The bn+n overflow half IS reachable (a large caller data_len trips it) and is covered by a test, but
239 // !bn is not: int_content always yields idlen 1..5, so this tlv always writes 2+idlen (2..7) octets into
240 // a 256-byte buffer and can never return 0. gcovr cannot exclude one operand, so the line is excluded.
241 if (!bn || bn + n > sizeof(body)) // GCOVR_EXCL_LINE
242 {
243 return 0;
244 }
245 memcpy(body + bn, svc, n);
246 bn += n;
247 return tlv(Mms::MMS_PDU_CONFIRMED_RESPONSE, body, bn, out, cap);
248}
249
250bool pc_mms_parse(const uint8_t *pdu, size_t len, MmsPdu *out)
251{
252 if (!pdu || !out || len < 2)
253 {
254 return false;
255 }
256 out->pdu_tag = pdu[0];
257 if (out->pdu_tag != Mms::MMS_PDU_CONFIRMED_REQUEST && out->pdu_tag != Mms::MMS_PDU_CONFIRMED_RESPONSE &&
258 out->pdu_tag != Mms::MMS_PDU_CONFIRMED_ERROR)
259 {
260 return false;
261 }
262 // Decode the outer length.
263 size_t off = 1;
264 size_t body_len = 0;
265 size_t lhdr = 0;
266 if (!pc_ber_len(pdu, len, off, &body_len, &lhdr))
267 {
268 return false;
269 }
270 off += lhdr;
271 if (off + body_len > len)
272 {
273 return false;
274 }
275
276 // First inner element must be the invokeID INTEGER.
277 if (off + 2 > len || pdu[off] != Mms::MMS_TAG_INVOKE_ID)
278 {
279 return false;
280 }
281 size_t idlen = pdu[off + 1];
282 if (idlen > 4 || off + 2 + idlen > len)
283 {
284 return false;
285 }
286 uint32_t id = 0;
287 for (size_t i = 0; i < idlen; i++)
288 {
289 id = (id << 8) | pdu[off + 2 + i];
290 }
291 out->invoke_id = id;
292 size_t p = off + 2 + idlen;
293
294 // The next element is the confirmedService (A4 read, A5 write, ...). The p < off + body_len half is
295 // reachable both ways (a PDU carrying only the invokeID takes the else); p < len can never be false
296 // because off + body_len <= len was already checked above, so p < off + body_len implies p < len.
297 // gcovr cannot exclude one operand, so the whole line is excluded.
298 if (p < off + body_len && p < len) // GCOVR_EXCL_LINE
299 {
300 out->service_tag = pdu[p];
301 size_t sp = p + 1;
302 size_t slen = 0;
303 size_t hdr = 0;
304 if (!pc_ber_len(pdu, len, sp, &slen, &hdr))
305 {
306 return false;
307 }
308 if (sp + hdr + slen > len)
309 {
310 return false;
311 }
312 out->service_body = pdu + sp + hdr;
313 out->service_len = slen;
314 }
315 else
316 {
317 out->service_tag = 0;
318 out->service_body = nullptr;
319 out->service_len = 0;
320 }
321 return true;
322}
323
324#endif // PC_ENABLE_MMS
IEC 61850 MMS (Manufacturing Message Specification) PDU codec (PC_ENABLE_MMS).