code wiki / (root) / nx_ipaddr.nx

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}