ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ip.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 ip.cpp
6 * @brief pc_ip implementation: RFC 4291 text parsing, RFC 5952 canonical formatting, scope
7 * classification. Pure, hand-rolled (no stdlib parsing), host-identical.
8 *
9 * The public entry points (pc_ip_parse / pc_ip_format / pc_ip_classify) are thin: the work is
10 * split into small single-purpose helpers in the anonymous namespace below - one per concern
11 * (parse a hextet, assemble the 16 bytes, find the zero run to compress, classify one family).
12 */
13
15#include <string.h>
16
17namespace
18{
19// -------------------------------------------------------------------------------------------
20// Parsing helpers (text -> bytes)
21// -------------------------------------------------------------------------------------------
22
23/** Value of one hex digit, or -1 if @p c is not a hex digit. */
24int hexval(char c)
25{
26 if (c >= '0' && c <= '9')
27 {
28 return c - '0';
29 }
30 if (c >= 'a' && c <= 'f')
31 {
32 return c - 'a' + 10;
33 }
34 if (c >= 'A' && c <= 'F')
35 {
36 return c - 'A' + 10;
37 }
38 return -1;
39}
40
41/** Parse the dotted-quad in s[0, len) into out[4]. Rejects empty octets, > 3 digits, or > 255. */
42bool parse_v4(const char *s, size_t len, uint8_t out[4])
43{
44 int oct = 0;
45 int val = -1;
46 int digits = 0;
47 for (size_t i = 0; i < len; i++)
48 {
49 char c = s[i];
50 if (c == '.') // octet separator: commit the octet in progress
51 {
52 if (digits == 0 || oct >= 3)
53 {
54 return false;
55 }
56 out[oct++] = (uint8_t)val;
57 val = -1;
58 digits = 0;
59 }
60 else if (c >= '0' && c <= '9')
61 {
62 if (digits >= 3)
63 {
64 return false;
65 }
66 val = (val < 0 ? 0 : val) * 10 + (c - '0');
67 if (val > 255)
68 {
69 return false;
70 }
71 digits++;
72 }
73 else
74 {
75 return false;
76 }
77 }
78 if (oct != 3 || digits == 0) // exactly four octets, the last one non-empty
79 {
80 return false;
81 }
82 out[3] = (uint8_t)val;
83 return true;
84}
85
86/** Parse one 1-4 digit IPv6 hextet in s[0, len) into *out. False if empty / too long / non-hex. */
87bool parse_hextet(const char *s, size_t len, uint16_t *out)
88{
89 if (len == 0 || len > 4)
90 {
91 return false;
92 }
93 int v = 0;
94 for (size_t i = 0; i < len; i++)
95 {
96 int h = hexval(s[i]);
97 if (h < 0)
98 {
99 return false;
100 }
101 v = (v << 4) | h;
102 }
103 *out = (uint16_t)v;
104 return true;
105}
106
107/**
108 * Place the @p nhead hextets that preceded "::" and the @p ntail that followed it into the 16
109 * output bytes (big-endian), zero-filling the gap the "::" stands for: head fills from the left,
110 * tail from the right.
111 */
112void assemble_v6(const uint16_t *head, int nhead, const uint16_t *tail, int ntail, uint8_t out[16])
113{
114 uint16_t g[8];
115 memset(g, 0, sizeof(g));
116 for (int k = 0; k < nhead; k++)
117 {
118 g[k] = head[k];
119 }
120 for (int k = 0; k < ntail; k++)
121 {
122 g[8 - ntail + k] = tail[k];
123 }
124 for (int k = 0; k < 8; k++)
125 {
126 out[2 * k] = (uint8_t)(g[k] >> 8);
127 out[2 * k + 1] = (uint8_t)(g[k] & 0xFF);
128 }
129}
130
131/**
132 * Parse an IPv6 text address (RFC 4291 §2.2) into out[16].
133 *
134 * The address is up to eight 16-bit hextets separated by ':'. A single "::" may stand in for one
135 * or more all-zero hextets: the groups before it fill a head list from the left, those after fill
136 * a tail list from the right, and assemble_v6() zero-fills the middle. The last token may be a
137 * dotted-quad (an embedded IPv4 tail such as ::ffff:1.2.3.4), which expands to two hextets.
138 */
139bool parse_v6(const char *s, size_t len, uint8_t out[16])
140{
141 uint16_t head[8];
142 uint16_t tail[8];
143 int nhead = 0;
144 int ntail = 0;
145 bool seen_dc = false; // have we passed the "::" yet?
146 uint16_t *cur = head; // the list currently being filled (head, then tail after "::")
147 int *ncur = &nhead;
148
149 size_t i = 0;
150 // len >= 1 is never false here: the only caller (pc_ip_parse) rejects an empty string before
151 // calling parse_v6, and colon (which gates this call) requires at least one ':' character.
152 if (len >= 1 && s[0] == ':') // GCOVR_EXCL_BR_LINE a leading "::" (the address opens in the zero gap)
153 {
154 if (len < 2 || s[1] != ':')
155 {
156 return false; // a lone leading ':' is illegal
157 }
158 seen_dc = true;
159 cur = tail;
160 ncur = &ntail;
161 i = 2;
162 if (i == len) // the whole address is "::"
163 {
164 memset(out, 0, 16);
165 return true;
166 }
167 }
168
169 while (i < len)
170 {
171 // Span this token up to the next ':' (noting a '.' -> it is an embedded-v4 tail).
172 size_t j = i;
173 bool has_dot = false;
174 while (j < len && s[j] != ':')
175 {
176 if (s[j] == '.')
177 {
178 has_dot = true;
179 }
180 j++;
181 }
182 size_t tlen = j - i;
183
184 if (has_dot) // embedded IPv4 -> two hextets; it must be the final token
185 {
186 uint8_t q[4];
187 if (!parse_v4(s + i, tlen, q) || *ncur > 6 || j != len)
188 {
189 return false;
190 }
191 cur[(*ncur)++] = (uint16_t)((q[0] << 8) | q[1]);
192 cur[(*ncur)++] = (uint16_t)((q[2] << 8) | q[3]);
193 break;
194 }
195
196 uint16_t hx;
197 if (!parse_hextet(s + i, tlen, &hx) || *ncur >= 8)
198 {
199 return false;
200 }
201 cur[(*ncur)++] = hx;
202 i = j;
203
204 // Consume the separator: "::" switches us to the tail list, a single ":" just continues.
205 if (i < len)
206 {
207 if (i + 1 < len && s[i + 1] == ':') // "::"
208 {
209 if (seen_dc)
210 {
211 return false; // only one "::" is allowed
212 }
213 seen_dc = true;
214 cur = tail;
215 ncur = &ntail;
216 i += 2;
217 if (i == len)
218 {
219 break; // trailing "::"
220 }
221 }
222 else // single ':'
223 {
224 i += 1;
225 if (i == len)
226 {
227 return false; // a trailing lone ':' is illegal
228 }
229 }
230 }
231 }
232
233 // Without "::" all eight hextets must be present; with it, the gap stands for >= 1 group.
234 int total = nhead + ntail;
235 if (seen_dc ? (total > 7) : (total != 8))
236 {
237 return false;
238 }
239
240 assemble_v6(head, nhead, tail, ntail, out);
241 return true;
242}
243
244// -------------------------------------------------------------------------------------------
245// Formatting helpers (bytes -> text)
246// -------------------------------------------------------------------------------------------
247
248/** Append the decimal form of @p v (0-255) at @p o. Returns the digit count (1-3). */
249size_t put_u8(uint8_t v, char *o)
250{
251 size_t n = 0;
252 if (v >= 100)
253 {
254 o[n++] = (char)('0' + v / 100);
255 }
256 if (v >= 10)
257 {
258 o[n++] = (char)('0' + (v / 10) % 10);
259 }
260 o[n++] = (char)('0' + v % 10);
261 return n;
262}
263
264/** Format the four bytes at @p b as "a.b.c.d" into @p out (NUL-terminated). 0 if it won't fit. */
265size_t format_v4(const uint8_t *b, char *out, size_t cap)
266{
267 char tmp[16];
268 size_t n = 0;
269 for (int k = 0; k < 4; k++)
270 {
271 if (k)
272 {
273 tmp[n++] = '.';
274 }
275 n += put_u8(b[k], tmp + n);
276 }
277 if (n + 1 > cap)
278 {
279 return 0;
280 }
281 memcpy(out, tmp, n);
282 out[n] = '\0';
283 return n;
284}
285
286/** Append @p v as lower-case hex with no leading zeros at @p o. Returns the digit count (1-4). */
287size_t put_hex16(uint16_t v, char *o)
288{
289 static const char H[] = "0123456789abcdef";
290 char t[4];
291 int n = 0;
292 do
293 {
294 t[n++] = H[v & 0xF];
295 v >>= 4;
296 } while (v);
297 for (int k = 0; k < n; k++) // reverse into place (we built it least-significant first)
298 {
299 o[k] = t[n - 1 - k];
300 }
301 return (size_t)n;
302}
303
304/**
305 * Find the longest run of >= 2 zero hextets to compress to "::" (leftmost wins a tie,
306 * RFC 5952 §4.2.3). Writes the run start to *start (-1 if none qualifies) and its length to *len.
307 */
308void longest_zero_run(const uint16_t g[8], int *start, int *len)
309{
310 int best_start = -1;
311 int best_len = 0;
312 int cur_start = -1;
313 int cur_len = 0;
314 for (int k = 0; k < 8; k++)
315 {
316 if (g[k] == 0)
317 {
318 if (cur_start < 0)
319 {
320 cur_start = k;
321 }
322 cur_len++;
323 if (cur_len > best_len)
324 {
325 best_len = cur_len;
326 best_start = cur_start;
327 }
328 }
329 else
330 {
331 cur_start = -1;
332 cur_len = 0;
333 }
334 }
335 *start = (best_len >= 2) ? best_start : -1;
336 *len = best_len;
337}
338
339// -------------------------------------------------------------------------------------------
340// Classification helpers
341// -------------------------------------------------------------------------------------------
342
343/** True if the 16 v6 bytes carry the ::ffff:0:0/96 IPv4-mapped prefix (RFC 4291 §2.5.5.2). */
344bool is_v4_mapped_bytes(const uint8_t *b)
345{
346 for (int k = 0; k < 10; k++)
347 {
348 if (b[k])
349 {
350 return false;
351 }
352 }
353 return b[10] == 0xff && b[11] == 0xff;
354}
355
356/** Classify the four v4 bytes at @p b. */
357pc_ip_scope classify_v4(const uint8_t *b)
358{
359 if (b[0] == 0 && b[1] == 0 && b[2] == 0 && b[3] == 0)
360 {
361 return pc_ip_scope::PC_IP_SCOPE_UNSPECIFIED; // 0.0.0.0
362 }
363 if (b[0] == 127)
364 {
365 return pc_ip_scope::PC_IP_SCOPE_LOOPBACK; // 127/8
366 }
367 if (b[0] == 169 && b[1] == 254)
368 {
369 return pc_ip_scope::PC_IP_SCOPE_LINK_LOCAL; // 169.254/16
370 }
371 if (b[0] == 10 || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) || (b[0] == 192 && b[1] == 168))
372 {
373 return pc_ip_scope::PC_IP_SCOPE_PRIVATE; // RFC 1918
374 }
375 if (b[0] >= 224 && b[0] <= 239)
376 {
378 }
380}
381
382/** Classify the sixteen v6 bytes at @p b (v4-mapped addresses defer to their embedded v4). */
383pc_ip_scope classify_v6(const uint8_t *b)
384{
385 bool allzero = true;
386 for (int k = 0; k < 16; k++)
387 {
388 if (b[k])
389 {
390 allzero = false;
391 break;
392 }
393 }
394 if (allzero)
395 {
397 }
398
399 bool loopback = (b[15] == 1);
400 for (int k = 0; k < 15 && loopback; k++)
401 {
402 if (b[k])
403 {
404 loopback = false;
405 }
406 }
407 if (loopback)
408 {
410 }
411
412 if (is_v4_mapped_bytes(b))
413 {
414 return classify_v4(b + 12); // ::ffff:a.b.c.d takes the v4 scope
415 }
416 if (b[0] == 0xff)
417 {
418 return pc_ip_scope::PC_IP_SCOPE_MULTICAST; // ff00::/8
419 }
420 if (b[0] == 0xfe && (b[1] & 0xc0) == 0x80)
421 {
422 return pc_ip_scope::PC_IP_SCOPE_LINK_LOCAL; // fe80::/10
423 }
424 if ((b[0] & 0xfe) == 0xfc)
425 {
426 return pc_ip_scope::PC_IP_SCOPE_PRIVATE; // fc00::/7 (unique-local)
427 }
429}
430} // namespace
431
432// -------------------------------------------------------------------------------------------
433// Public API
434// -------------------------------------------------------------------------------------------
435
436bool pc_ip_parse(const char *s, pc_ip *out)
437{
438 if (!s || !out)
439 {
440 return false;
441 }
442 // A ':' means it is v6; a '.' (and no ':') means v4. Bound the scan to a legal length.
443 size_t len = 0;
444 bool colon = false;
445 bool dot = false;
446 while (s[len])
447 {
448 if (s[len] == ':')
449 {
450 colon = true;
451 }
452 else if (s[len] == '.')
453 {
454 dot = true;
455 }
456 if (++len > 45)
457 {
458 return false; // longer than any legal textual address
459 }
460 }
461 if (len == 0)
462 {
463 return false;
464 }
465
466 memset(out->bytes, 0, 16);
467 if (colon)
468 {
469 if (!parse_v6(s, len, out->bytes))
470 {
471 return false;
472 }
474 return true;
475 }
476 if (dot)
477 {
478 if (!parse_v4(s, len, out->bytes))
479 {
480 return false;
481 }
483 return true;
484 }
485 return false;
486}
487
488size_t pc_ip_format(const pc_ip *ip, char *out, size_t cap)
489{
490 if (!ip || !out || cap == 0)
491 {
492 return 0;
493 }
495 {
496 return format_v4(ip->bytes, out, cap);
497 }
499 {
500 return 0;
501 }
502
503 // IPv4-mapped addresses print with a dotted tail: ::ffff:a.b.c.d (RFC 5952 §5).
504 if (pc_ip_is_v4_mapped(ip))
505 {
506 char tail[16];
507 size_t tn = format_v4(ip->bytes + 12, tail, sizeof(tail));
508 // tn == 0 is never true here: tail is a fixed 16 bytes, and the longest possible v4 text
509 // ("255.255.255.255", 15 chars + NUL) always fits, so format_v4 can't fail on this call.
510 if (tn == 0 || 7 + tn + 1 > cap) // GCOVR_EXCL_BR_LINE
511 {
512 return 0;
513 }
514 memcpy(out, "::ffff:", 7);
515 memcpy(out + 7, tail, tn);
516 out[7 + tn] = '\0';
517 return 7 + tn;
518 }
519
520 uint16_t g[8];
521 for (int k = 0; k < 8; k++)
522 {
523 g[k] = (uint16_t)((ip->bytes[2 * k] << 8) | ip->bytes[2 * k + 1]);
524 }
525
526 int zs;
527 int zl;
528 longest_zero_run(g, &zs, &zl);
529
530 // Emit the hextets, replacing the [zs, zs+zl) run with "::" (inet_ntop6-style colon placement).
531 char tmp[PC_IP_STR_MAX];
532 size_t n = 0;
533 for (int k = 0; k < 8; k++)
534 {
535 if (zs != -1 && k >= zs && k < zs + zl)
536 {
537 if (k == zs)
538 {
539 tmp[n++] = ':'; // one colon here; the pair around the run forms "::"
540 }
541 continue;
542 }
543 if (k != 0)
544 {
545 tmp[n++] = ':';
546 }
547 n += put_hex16(g[k], tmp + n);
548 }
549 if (zs != -1 && zs + zl == 8)
550 {
551 tmp[n++] = ':'; // the run reached the end: close the trailing "::"
552 }
553
554 if (n + 1 > cap)
555 {
556 return 0;
557 }
558 memcpy(out, tmp, n);
559 out[n] = '\0';
560 return n;
561}
562
564{
565 return ip && ip->family == pc_ip_family::PC_IP_V6 && is_v4_mapped_bytes(ip->bytes);
566}
567
569{
570 if (!ip)
571 {
573 }
575 {
576 return classify_v4(ip->bytes);
577 }
579 {
580 return classify_v6(ip->bytes);
581 }
583}
584
585bool pc_ip_equal(const pc_ip *a, const pc_ip *b)
586{
587 if (!a || !b || a->family != b->family)
588 {
589 return false;
590 }
591 int n = 0;
593 {
594 n = 4;
595 }
596 else if (a->family == pc_ip_family::PC_IP_V6)
597 {
598 n = 16;
599 }
600 if (n == 0)
601 {
602 return true; // both the same non-address family (pc_ip_family::PC_IP_NONE)
603 }
604 return memcmp(a->bytes, b->bytes, (size_t)n) == 0;
605}
606
607pc_ip pc_ip_from_v4_octets(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
608{
609 pc_ip ip;
610 memset(&ip, 0, sizeof(ip));
612 ip.bytes[0] = a;
613 ip.bytes[1] = b;
614 ip.bytes[2] = c;
615 ip.bytes[3] = d;
616 return ip;
617}
618
619pc_ip pc_ip_from_v6_bytes(const uint8_t bytes[16])
620{
621 pc_ip ip;
623 memcpy(ip.bytes, bytes, 16);
624 return ip;
625}
626
627uint32_t pc_ip_to_v4_be(const pc_ip *ip)
628{
629 if (!ip)
630 {
631 return 0;
632 }
633 const uint8_t *b = ip->bytes;
635 {
636 return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3];
637 }
638 if (pc_ip_is_v4_mapped(ip))
639 {
640 return ((uint32_t)b[12] << 24) | ((uint32_t)b[13] << 16) | ((uint32_t)b[14] << 8) | b[15];
641 }
642 return 0;
643}
644
646{
647 if (!ip || ip->family == pc_ip_family::PC_IP_NONE)
648 {
649 return true;
650 }
651 int n = (ip->family == pc_ip_family::PC_IP_V4) ? 4 : 16;
652 for (int i = 0; i < n; i++)
653 {
654 if (ip->bytes[i])
655 {
656 return false;
657 }
658 }
659 return true;
660}
661
662bool pc_ip_prefix_match(const pc_ip *addr, const pc_ip *net, uint8_t prefix_len)
663{
664 if (!addr || !net || addr->family != net->family)
665 {
666 return false;
667 }
668 int bits = (addr->family == pc_ip_family::PC_IP_V4) ? 32 : (addr->family == pc_ip_family::PC_IP_V6 ? 128 : 0);
669 if (bits == 0 || prefix_len > bits)
670 {
671 return false;
672 }
673 int whole = prefix_len / 8; // bytes that must match exactly
674 for (int i = 0; i < whole; i++)
675 {
676 if (addr->bytes[i] != net->bytes[i])
677 {
678 return false;
679 }
680 }
681 int rem = prefix_len % 8; // leftover high bits in the next byte
682 if (rem)
683 {
684 uint8_t mask = (uint8_t)(0xFF << (8 - rem));
685 if ((addr->bytes[whole] & mask) != (net->bytes[whole] & mask))
686 {
687 return false;
688 }
689 }
690 return true;
691}
pc_ip pc_ip_from_v4_octets(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
Build a v4 pc_ip from four octets (a.b.c.d).
Definition ip.cpp:607
bool pc_ip_equal(const pc_ip *a, const pc_ip *b)
True if a and b are the same family and address.
Definition ip.cpp:585
bool pc_ip_prefix_match(const pc_ip *addr, const pc_ip *net, uint8_t prefix_len)
CIDR containment: is addr inside the net / prefix_len block?
Definition ip.cpp:662
size_t pc_ip_format(const pc_ip *ip, char *out, size_t cap)
Format ip into out as its RFC 5952 canonical text.
Definition ip.cpp:488
pc_ip pc_ip_from_v6_bytes(const uint8_t bytes[16])
Build a v6 pc_ip from 16 address bytes in network (big-endian) order.
Definition ip.cpp:619
bool pc_ip_parse(const char *s, pc_ip *out)
Parse an IPv4 or IPv6 textual address (RFC 4291 §2.2) into out.
Definition ip.cpp:436
pc_ip_scope pc_ip_classify(const pc_ip *ip)
Classify ip into a pc_ip_scope.
Definition ip.cpp:568
bool pc_ip_is_unspecified(const pc_ip *ip)
True if ip is empty (pc_ip_family::PC_IP_NONE) or the all-zero unspecified address (0....
Definition ip.cpp:645
uint32_t pc_ip_to_v4_be(const pc_ip *ip)
The v4 address as a big-endian (network-order) uint32 (a<<24 | b<<16 | c<<8 | d).
Definition ip.cpp:627
bool pc_ip_is_v4_mapped(const pc_ip *ip)
True if ip is an IPv4-mapped IPv6 address (::ffff:a.b.c.d, RFC 4291 §2.5.5.2).
Definition ip.cpp:563
Layer 3 (Network) - a family-tagged IP address (IPv4 or IPv6) with RFC-faithful text parsing,...
pc_ip_scope
Address scope, in rough order of reachability (used for allow/deny policy + logging).
Definition ip.h:41
@ PC_IP_SCOPE_GLOBAL
globally routable unicast
@ PC_IP_SCOPE_LINK_LOCAL
169.254.0.0/16 / fe80::/10
@ PC_IP_SCOPE_LOOPBACK
127.0.0.0/8 / ::1
@ PC_IP_SCOPE_MULTICAST
224.0.0.0/4 / ff00::/8
@ PC_IP_SCOPE_PRIVATE
RFC1918 (10/8, 172.16/12, 192.168/16) / ULA fc00::/7.
@ PC_IP_SCOPE_UNSPECIFIED
0.0.0.0 / ::
@ PC_IP_NONE
empty / unparsed
@ PC_IP_V6
IPv6 (bytes[0..15])
@ PC_IP_V4
IPv4 (bytes[0..3])
#define PC_IP_STR_MAX
Longest text an pc_ip_format can produce, including the NUL (RFC 5952 v4-mapped).
Definition ip.h:58
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
pc_ip_family family
address family tag
Definition ip.h:53
uint8_t bytes[16]
network order; v4 uses the first 4
Definition ip.h:54