nx_ipaddr.nx source
↩ module page · 348 lines · 11404 B
1// ipaddr.nx -- IPv4 + IPv6 address parse / format.
2//
3// Used by: net.nx (connect / bind with human-readable addresses),
4// X.509 subjectAltName IP entries, HTTP Host header validation,
5// access-control lists.
6//
7// IPv4: dotted quad "a.b.c.d" where each octet is 0..255.
8// Packed as network-order u32 (big-endian): a in high byte.
9//
10// IPv6: colon-separated "1234:abcd:..." with optional "::" run-
11// length compression. 16-byte value (8 x u16 hextets).
12// Parser handles both full + compressed forms; emitter
13// always produces compressed canonical form (RFC 5952).
14//
15// Invariants:
16// IP1 All decoders are bounds-checked; malformed input returns
17// a negative error code, never garbage.
18// IP2 Encoders never overflow the output buffer -- caller
19// guarantees >= 16 chars for IPv4 ("255.255.255.255\0"),
20// >= 40 chars for IPv6 (":ffff:ffff:...:ffff\0").
21// IP3 Round-trip exact for canonical forms. Compressed IPv6
22// that uses "::" is normalised on re-format; the byte-level
23// value round-trips even if the string doesn't.
24// IP4 No IPv4-mapped IPv6 (::ffff:a.b.c.d) handling today.
25// That form is RFC 4291 but complicates parsing; callers
26// needing it can decompose manually.
27
28// nx_safety_envelope:
29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
30// sil_target: SIL1
31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
32// verdict: NOT_YET_EVALUATED
33
34import "nx_syscalls.nx"
35
36const IPADDR_ERR_FORMAT: i64 = -1
37const IPADDR_ERR_RANGE: i64 = -2
38
39// ---- IPv4 --------------------------------------------------------
40
41// Parse "a.b.c.d" into a 32-bit integer (a in high byte = network
42// byte order). Returns the u32 value, or negative IPADDR_ERR_*.
43func ipv4_parse(s: *u8, n: i64) -> i64 {
44 var pos: i64 = 0
45 var octets: i64 = 0
46 var result: i64 = 0
47 var octet_val: i64 = 0
48 var octet_digits: i64 = 0
49 while pos < n {
50 let c: i64 = s[pos]
51 if c == 0x2E { // '.'
52 if octet_digits == 0 { return IPADDR_ERR_FORMAT }
53 if octet_val > 255 { return IPADDR_ERR_RANGE }
54 result = (result << 8) | octet_val
55 octets = octets + 1
56 if octets >= 4 { return IPADDR_ERR_FORMAT }
57 octet_val = 0
58 octet_digits = 0
59 pos = pos + 1
60 } else {
61 if c >= 0x30 {
62 if c <= 0x39 {
63 octet_val = octet_val * 10 + (c - 0x30)
64 octet_digits = octet_digits + 1
65 if octet_digits > 3 { return IPADDR_ERR_FORMAT }
66 pos = pos + 1
67 } else { return IPADDR_ERR_FORMAT }
68 } else { return IPADDR_ERR_FORMAT }
69 }
70 }
71 // Final octet.
72 if octet_digits == 0 { return IPADDR_ERR_FORMAT }
73 if octet_val > 255 { return IPADDR_ERR_RANGE }
74 if octets != 3 { return IPADDR_ERR_FORMAT }
75 result = (result << 8) | octet_val
76 return result & 0xFFFFFFFF
77}
78
79// Forward declaration for ip_emit_dec (defined after ipv4_format).
80func ip_emit_dec(out: *u8, off: i64, n: i64) -> i64;
81
82// Format a 32-bit IPv4 address as "a.b.c.d". Returns bytes written
83// (4..15). Caller provides >= 16 bytes of space.
84func ipv4_format(ip: i64, out: *u8) -> i64 {
85 let a: i64 = (ip >> 24) & 0xFF
86 let b: i64 = (ip >> 16) & 0xFF
87 let c: i64 = (ip >> 8) & 0xFF
88 let d: i64 = ip & 0xFF
89 var pos: i64 = 0
90 pos = pos + ip_emit_dec(out, pos, a)
91 out[pos] = 0x2E; pos = pos + 1
92 pos = pos + ip_emit_dec(out, pos, b)
93 out[pos] = 0x2E; pos = pos + 1
94 pos = pos + ip_emit_dec(out, pos, c)
95 out[pos] = 0x2E; pos = pos + 1
96 pos = pos + ip_emit_dec(out, pos, d)
97 return pos
98}
99
100// Emit 0..255 as decimal. Returns digits written (1..3).
101func ip_emit_dec(out: *u8, off: i64, n: i64) -> i64 {
102 if n < 10 {
103 out[off] = 0x30 + n
104 return 1
105 }
106 if n < 100 {
107 out[off] = 0x30 + (n / 10)
108 out[off + 1] = 0x30 + (n % 10)
109 return 2
110 }
111 out[off] = 0x30 + (n / 100)
112 out[off + 1] = 0x30 + ((n / 10) % 10)
113 out[off + 2] = 0x30 + (n % 10)
114 return 3
115}
116
117// ---- IPv6 --------------------------------------------------------
118//
119// 16 bytes packed into 8 hextets, big-endian. Output buffer is
120// caller-owned 16 bytes; each pair of bytes [0-1], [2-3], etc. is
121// one hextet in network byte order.
122
123// Parse one hextet from 1-4 hex digits starting at s[pos]. On
124// success writes value to *out_val and advances *pos past the
125// digits. Returns 0 / negative on error.
126func ipv6_parse_hex(s: *u8, n: i64, pos: *i64, out_val: *i64) -> i64 {
127 var p: i64 = *pos
128 var val: i64 = 0
129 var digits: i64 = 0
130 while p < n {
131 let c: i64 = s[p]
132 var d: i64 = -1
133 if c >= 0x30 { if c <= 0x39 { d = c - 0x30 } }
134 if c >= 0x41 { if c <= 0x46 { d = c - 0x41 + 10 } }
135 if c >= 0x61 { if c <= 0x66 { d = c - 0x61 + 10 } }
136 if d < 0 {
137 if digits == 0 { return IPADDR_ERR_FORMAT }
138 *out_val = val
139 *pos = p
140 return 0
141 }
142 val = (val << 4) | d
143 digits = digits + 1
144 if digits > 4 { return IPADDR_ERR_FORMAT }
145 p = p + 1
146 }
147 if digits == 0 { return IPADDR_ERR_FORMAT }
148 *out_val = val
149 *pos = p
150 return 0
151}
152
153// Parse an IPv6 address into the caller-supplied 16-byte buffer.
154// Handles the "::" run-length compression. Returns 0 on success
155// or negative IPADDR_ERR_*.
156func ipv6_parse(s: *u8, n: i64, out: *u8) -> i64 {
157 var pos: i64 = 0
158 // left[] holds hextets before "::" (or all of them if no "::").
159 let left_raw: *u8 = sys_mmap(64)
160 let left: *i64 = left_raw as *i64
161 // right[] holds hextets after "::".
162 let right_raw: *u8 = sys_mmap(64)
163 let right: *i64 = right_raw as *i64
164 var n_left: i64 = 0
165 var n_right: i64 = 0
166 var seen_double_colon: i64 = 0
167
168 // Leading "::" handled explicitly.
169 if pos + 1 < n {
170 if s[pos] == 0x3A {
171 if s[pos + 1] == 0x3A {
172 seen_double_colon = 1
173 pos = pos + 2
174 }
175 }
176 }
177
178 // Parse hextets.
179 while pos < n {
180 let val_p: *i64 = sys_mmap(16) as *i64
181 let pos_p: *i64 = sys_mmap(16) as *i64
182 *pos_p = pos
183 let rc: i64 = ipv6_parse_hex(s, n, pos_p, val_p)
184 if rc < 0 { return rc }
185 pos = *pos_p
186 if seen_double_colon == 0 {
187 left[n_left] = *val_p
188 n_left = n_left + 1
189 } else {
190 right[n_right] = *val_p
191 n_right = n_right + 1
192 }
193 if pos >= n { pos = n }
194 if pos < n {
195 if s[pos] == 0x3A {
196 if pos + 1 < n {
197 if s[pos + 1] == 0x3A {
198 if seen_double_colon == 1 { return IPADDR_ERR_FORMAT }
199 seen_double_colon = 1
200 pos = pos + 2
201 } else {
202 pos = pos + 1
203 }
204 } else {
205 pos = pos + 1
206 }
207 } else {
208 return IPADDR_ERR_FORMAT
209 }
210 }
211 }
212
213 let total: i64 = n_left + n_right
214 if seen_double_colon == 0 {
215 if total != 8 { return IPADDR_ERR_FORMAT }
216 } else {
217 if total > 8 { return IPADDR_ERR_FORMAT }
218 }
219
220 // Compose 16 bytes.
221 var i: i64 = 0
222 while i < 16 { out[i] = 0; i = i + 1 }
223 i = 0
224 while i < n_left {
225 out[i * 2] = (left[i] >> 8) & 0xFF
226 out[i * 2 + 1] = left[i] & 0xFF
227 i = i + 1
228 }
229 let right_start: i64 = 8 - n_right
230 i = 0
231 while i < n_right {
232 out[(right_start + i) * 2] = (right[i] >> 8) & 0xFF
233 out[(right_start + i) * 2 + 1] = right[i] & 0xFF
234 i = i + 1
235 }
236 return 0
237}
238
239// Format an IPv6 address (16 bytes) into its canonical RFC 5952
240// compressed form. Rules (RFC 5952 ยง4):
241// - Lowercase hex
242// - Suppress leading zeros in each hextet
243// - Replace the LONGEST run of consecutive all-zero hextets
244// with "::" (ties: leftmost run)
245// - "::" is used only once
246// - At least two consecutive zero hextets required for "::";
247// a single zero hextet is still written as "0"
248// Returns bytes written to `out`.
249func ipv6_format(bytes: *u8, out: *u8) -> i64 {
250 // Extract the 8 hextets.
251 let h_raw: *u8 = sys_mmap(64)
252 let h: *i64 = h_raw as *i64
253 var i: i64 = 0
254 while i < 8 {
255 h[i] = (bytes[i * 2] << 8) | bytes[i * 2 + 1]
256 i = i + 1
257 }
258
259 // Find the longest run of consecutive zero hextets. Must be
260 // >= 2 to qualify for "::" compression.
261 var best_start: i64 = -1
262 var best_len: i64 = 0
263 var run_start: i64 = -1
264 var run_len: i64 = 0
265 i = 0
266 while i < 8 {
267 if h[i] == 0 {
268 if run_start < 0 { run_start = i }
269 run_len = run_len + 1
270 if run_len > best_len {
271 best_start = run_start
272 best_len = run_len
273 }
274 } else {
275 run_start = -1
276 run_len = 0
277 }
278 i = i + 1
279 }
280 if best_len < 2 { best_start = -1 }
281
282 // Emit each hextet; substitute "::" at the compressed run.
283 var pos: i64 = 0
284 i = 0
285 while i < 8 {
286 if i == best_start {
287 // Emit "::" and skip the run.
288 if i == 0 { out[pos] = 0x3A; pos = pos + 1 }
289 out[pos] = 0x3A
290 pos = pos + 1
291 i = i + best_len
292 } else {
293 if i > 0 {
294 out[pos] = 0x3A
295 pos = pos + 1
296 }
297 // Emit hextet as 1..4 lowercase hex digits, no leading zeros.
298 let v: i64 = h[i]
299 if v == 0 {
300 out[pos] = 0x30
301 pos = pos + 1
302 } else {
303 var emitted: i64 = 0
304 var shift: i64 = 12
305 while shift >= 0 {
306 let nib: i64 = (v >> shift) & 0xF
307 if emitted == 0 {
308 if nib != 0 {
309 var c: i64 = 0x30 + nib
310 if nib > 9 { c = 0x61 + nib - 10 }
311 out[pos] = c
312 pos = pos + 1
313 emitted = 1
314 }
315 } else {
316 var c: i64 = 0x30 + nib
317 if nib > 9 { c = 0x61 + nib - 10 }
318 out[pos] = c
319 pos = pos + 1
320 }
321 shift = shift - 4
322 }
323 }
324 i = i + 1
325 }
326 }
327 return pos
328}
329
330// Compile-only smoke.
331func main() -> i64 {
332 // IPv4 round-trip: 192.168.1.1 -> 0xC0A80101 -> "192.168.1.1".
333 let v4: i64 = ipv4_parse("192.168.1.1", 11)
334 if v4 != 0xC0A80101 { return 1 }
335 let out: *u8 = sys_mmap(32)
336 let n: i64 = ipv4_format(v4, out)
337 if n != 11 { return 2 }
338 if out[0] != 0x31 { return 3 } // '1'
339 if out[3] != 0x2E { return 4 } // '.'
340
341 // IPv6 parse: "::1" (loopback) = 15 zero bytes + 0x01.
342 let v6: *u8 = sys_mmap(16)
343 let rc: i64 = ipv6_parse("::1", 3, v6)
344 if rc != 0 { return 5 }
345 if v6[15] != 0x01 { return 6 }
346 if v6[0] != 0 { return 7 }
347 return 0
348}