| 1 | module net |
| 2 | |
| 3 | import os |
| 4 | |
| 5 | // Conformance tests for RFC 5952 (IPv6 text representation). |
| 6 | // |
| 7 | // Each vector is `(input_form, expected_canonical)`. Inputs include |
| 8 | // non-canonical legitimate forms (uppercase, leading zeros, partial |
| 9 | // "::" placement) so that canonical_ipv6() also exercises the parser. |
| 10 | |
| 11 | struct V6Vec { |
| 12 | input string |
| 13 | expected string |
| 14 | } |
| 15 | |
| 16 | const rfc5952_vectors = [ |
| 17 | // -------- §4.1 Leading zeros MUST be suppressed -------- |
| 18 | V6Vec{'2001:0db8:0000:0000:0000:0000:0000:0001', '2001:db8::1'}, |
| 19 | V6Vec{'0000:0000:0000:0000:0000:0000:0000:0000', '::'}, |
| 20 | V6Vec{'0000:0000:0000:0000:0000:0000:0000:0001', '::1'}, |
| 21 | // "A single 16-bit 0000 field MUST be represented as 0." |
| 22 | V6Vec{'2001:0db8:0000:0001:0001:0001:0001:0001', '2001:db8:0:1:1:1:1:1'}, |
| 23 | // -------- §4.2.1 Shorten as much as possible -------- |
| 24 | V6Vec{'2001:db8:0:0:0:0:2:1', '2001:db8::2:1'}, |
| 25 | // Counter-example from the RFC: "2001:db8::0:1 is not acceptable". |
| 26 | V6Vec{'2001:db8:0:0:0:0:0:1', '2001:db8::1'}, |
| 27 | // -------- §4.2.2 "::" MUST NOT shorten just one 16-bit 0 field -------- |
| 28 | // "2001:db8::1:1:1:1:1 is not correct" — single 0, leave as 0. |
| 29 | V6Vec{'2001:db8:0:1:1:1:1:1', '2001:db8:0:1:1:1:1:1'}, |
| 30 | // Several singletons, none shortened: |
| 31 | V6Vec{'1:0:1:0:1:0:1:0', '1:0:1:0:1:0:1:0'}, |
| 32 | V6Vec{'0:1:0:1:0:1:0:1', '0:1:0:1:0:1:0:1'}, |
| 33 | // -------- §4.2.3 Longest run wins; ties → first run -------- |
| 34 | // Three zeros (0,0,0) at positions 1..3 outweighs two zeros at 4..5. |
| 35 | V6Vec{'2001:0:0:0:1:0:0:1', '2001::1:0:0:1'}, |
| 36 | // Tie: two equal pairs (0,0) at positions 2..3 and 5..6 — first wins. |
| 37 | V6Vec{'2001:db8:0:0:1:0:0:1', '2001:db8::1:0:0:1'}, |
| 38 | // Earlier example from §4.2.3 with three trailing zero fields: |
| 39 | V6Vec{'2001:0:0:1:0:0:0:1', '2001:0:0:1::1'}, |
| 40 | // -------- §4.3 Lowercase -------- |
| 41 | V6Vec{'2001:0DB8:AC10:FE01:0000:0000:0000:0000', '2001:db8:ac10:fe01::'}, |
| 42 | V6Vec{'FE80::1', 'fe80::1'}, |
| 43 | V6Vec{'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'}, |
| 44 | // -------- §5 IPv4-mapped (::ffff:0:0/96) → mixed notation -------- |
| 45 | V6Vec{'0:0:0:0:0:ffff:c000:0201', '::ffff:192.0.2.1'}, |
| 46 | V6Vec{'::ffff:192.0.2.1', '::ffff:192.0.2.1'}, |
| 47 | V6Vec{'0000:0000:0000:0000:0000:FFFF:0A00:0001', '::ffff:10.0.0.1'}, |
| 48 | V6Vec{'::ffff:0.0.0.0', '::ffff:0.0.0.0'}, |
| 49 | V6Vec{'::ffff:255.255.255.255', '::ffff:255.255.255.255'}, |
| 50 | // -------- Run at start (>=2) -------- |
| 51 | V6Vec{'0:0:0:0:0:0:0:2', '::2'}, |
| 52 | V6Vec{'0:0:0:0:0:0:1:2', '::1:2'}, |
| 53 | V6Vec{'0:0:c:d:e:f:1:2', '::c:d:e:f:1:2'}, |
| 54 | // -------- Run at end (>=2) -------- |
| 55 | V6Vec{'1::', '1::'}, |
| 56 | V6Vec{'1:0:0:0:0:0:0:0', '1::'}, |
| 57 | V6Vec{'a:b:c:d:e:f:0:0', 'a:b:c:d:e:f::'}, |
| 58 | V6Vec{'2001:db8:ac10:fe01::', '2001:db8:ac10:fe01::'}, |
| 59 | // -------- Run in the middle -------- |
| 60 | V6Vec{'2001:db8::1', '2001:db8::1'}, |
| 61 | V6Vec{'fe80:0:0:0:0:0:0:1', 'fe80::1'}, |
| 62 | // -------- Loopback / unspecified -------- |
| 63 | V6Vec{'::1', '::1'}, |
| 64 | V6Vec{'::', '::'}, |
| 65 | // -------- No zero field at all -------- |
| 66 | V6Vec{'1:2:3:4:5:6:7:8', '1:2:3:4:5:6:7:8'}, |
| 67 | V6Vec{'2001:db8:1:2:3:4:5:6', '2001:db8:1:2:3:4:5:6'}, |
| 68 | // -------- Mixed leading-zero suppression with longer/shorter values -------- |
| 69 | V6Vec{'0001:0023:0456:7890:abcd:00ef:0:1', '1:23:456:7890:abcd:ef:0:1'}, |
| 70 | // -------- Two equal-length non-trivial runs (§4.2.3 tie) -------- |
| 71 | V6Vec{'0:0:1:2:3:4:0:0', '::1:2:3:4:0:0'}, |
| 72 | // -------- Run of length exactly 2 still triggers compression -------- |
| 73 | V6Vec{'1:2:0:0:3:4:5:6', '1:2::3:4:5:6'}, |
| 74 | // -------- Adjacent zero singletons: NEITHER must be compressed when |
| 75 | // a longer run exists -------- |
| 76 | V6Vec{'0:1:0:0:0:1:0:1', '0:1::1:0:1'}, |
| 77 | // -------- Parser robustness -------- |
| 78 | V6Vec{'2001:DB8::AB:CD', '2001:db8::ab:cd'}, |
| 79 | V6Vec{'::ffff:192.000.002.001', '::ffff:192.0.2.1'}, |
| 80 | ]! |
| 81 | |
| 82 | fn test_rfc5952_vectors() { |
| 83 | mut failures := []string{} |
| 84 | for v in rfc5952_vectors { |
| 85 | got := canonical_ipv6(v.input) or { |
| 86 | failures << '${v.input} -> error: ${err}' |
| 87 | continue |
| 88 | } |
| 89 | if got != v.expected { |
| 90 | failures << '${v.input} -> got ${got}, want ${v.expected}' |
| 91 | } |
| 92 | } |
| 93 | if failures.len > 0 { |
| 94 | for f in failures { |
| 95 | eprintln(' FAIL: ${f}') |
| 96 | } |
| 97 | assert false, '${failures.len}/${rfc5952_vectors.len} RFC 5952 vectors failed' |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | fn test_from_bytes_basic() { |
| 102 | zeros := []u8{len: 16} |
| 103 | assert canonical_ipv6_from_bytes(zeros)! == '::' |
| 104 | |
| 105 | mut loop := []u8{len: 16} |
| 106 | loop[15] = 1 |
| 107 | assert canonical_ipv6_from_bytes(loop)! == '::1' |
| 108 | |
| 109 | bytes := [u8(0x20), 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] |
| 110 | assert canonical_ipv6_from_bytes(bytes)! == '2001:db8::1' |
| 111 | |
| 112 | mapped := [u8(0), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 0, 2, 1] |
| 113 | assert canonical_ipv6_from_bytes(mapped)! == '::ffff:192.0.2.1' |
| 114 | } |
| 115 | |
| 116 | fn test_from_bytes_rejects_wrong_length() { |
| 117 | if _ := canonical_ipv6_from_bytes([u8(0)]) { |
| 118 | assert false, 'must reject 1-byte input' |
| 119 | } |
| 120 | if _ := canonical_ipv6_from_bytes([]u8{len: 15}) { |
| 121 | assert false, 'must reject 15-byte input' |
| 122 | } |
| 123 | if _ := canonical_ipv6_from_bytes([]u8{len: 17}) { |
| 124 | assert false, 'must reject 17-byte input' |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | fn test_canonical_is_idempotent() { |
| 129 | for v in rfc5952_vectors { |
| 130 | once := canonical_ipv6(v.input) or { |
| 131 | assert false, 'first pass failed on ${v.input}: ${err}' |
| 132 | continue |
| 133 | } |
| 134 | twice := canonical_ipv6(once) or { |
| 135 | assert false, 'second pass failed on ${once}: ${err}' |
| 136 | continue |
| 137 | } |
| 138 | assert once == twice, 'idempotence broken: ${v.input} -> ${once} -> ${twice}' |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | fn test_parser_rejects_malformed() { |
| 143 | bad := [ |
| 144 | '', |
| 145 | 'gggg::1', // non-hex |
| 146 | '1::2::3', // two "::" |
| 147 | '1:2:3:4:5:6:7:8:9', // too many groups, no "::" |
| 148 | '1:2:3:4:5:6:7', // too few groups, no "::" |
| 149 | '1:2:3:4:5:6:7:8::', // "::" with already 8 groups |
| 150 | '12345::1', // group too long |
| 151 | '1:2:3:4:5:6:1.2.3', // bad dotted quad (3 octets) |
| 152 | '1:2:3:4:5:6:1.2.3.4.5', // bad dotted quad (5 octets) |
| 153 | '::ffff:1.2.3.999', // octet > 255 |
| 154 | '::ffff:1.2.3.', // trailing dot in v4 |
| 155 | ] |
| 156 | for s in bad { |
| 157 | if _ := canonical_ipv6(s) { |
| 158 | assert false, 'parser must reject "${s}"' |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | // test_inet_ntop_corpus loads testdata/ipv6_rfc5952.tsv (113 vectors |
| 164 | // generated by Python's ipaddress.IPv6Address.compressed) and confirms |
| 165 | // our pure-V implementation produces byte-identical output. |
| 166 | fn test_inet_ntop_corpus() { |
| 167 | path := os.join_path(os.dir(@FILE), 'testdata', 'ipv6_rfc5952.tsv') |
| 168 | body := os.read_file(path) or { |
| 169 | assert false, 'cannot read fixture ${path}: ${err}' |
| 170 | return |
| 171 | } |
| 172 | mut total := 0 |
| 173 | mut failed := []string{} |
| 174 | for raw in body.split('\n') { |
| 175 | line := raw.trim_space() |
| 176 | if line.len == 0 { |
| 177 | continue |
| 178 | } |
| 179 | fields := line.split('\t') |
| 180 | if fields.len != 2 { |
| 181 | assert false, 'bad fixture line: ${line}' |
| 182 | continue |
| 183 | } |
| 184 | hex_addr := fields[0] |
| 185 | want := fields[1] |
| 186 | bytes := hex_to_bytes(hex_addr) or { |
| 187 | assert false, 'bad hex: ${hex_addr}' |
| 188 | continue |
| 189 | } |
| 190 | got := canonical_ipv6_from_bytes(bytes) or { |
| 191 | failed << '${hex_addr}: ${err}' |
| 192 | continue |
| 193 | } |
| 194 | if got != want { |
| 195 | failed << '${hex_addr}: got "${got}", want "${want}"' |
| 196 | } |
| 197 | total++ |
| 198 | } |
| 199 | if failed.len > 0 { |
| 200 | for f in failed { |
| 201 | eprintln(' FAIL: ${f}') |
| 202 | } |
| 203 | assert false, '${failed.len}/${total} cross-check vectors failed' |
| 204 | } |
| 205 | assert total >= 100, 'expected >= 100 vectors, got ${total}' |
| 206 | } |
| 207 | |
| 208 | fn hex_to_bytes(s string) ![]u8 { |
| 209 | if s.len % 2 != 0 { |
| 210 | return error('hex_to_bytes: odd length') |
| 211 | } |
| 212 | mut out := []u8{cap: s.len / 2} |
| 213 | for i := 0; i < s.len; i += 2 { |
| 214 | hi := hex_digit(s[i]) or { return error('bad hex at ${i}') } |
| 215 | lo := hex_digit(s[i + 1]) or { return error('bad hex at ${i + 1}') } |
| 216 | out << (hi << 4) | lo |
| 217 | } |
| 218 | return out |
| 219 | } |
| 220 | |
| 221 | // Property: bytes -> canonical -> parse -> bytes round-trip + idempotence. |
| 222 | fn test_property_roundtrip_and_idempotence() { |
| 223 | mut state := u64(0xCAFEBABEDEADBEEF) |
| 224 | for iter in 0 .. 1000 { |
| 225 | mut bytes := []u8{cap: 16} |
| 226 | for _ in 0 .. 16 { |
| 227 | state = state * 6364136223846793005 + 1442695040888963407 |
| 228 | bytes << u8(state >> 56) |
| 229 | } |
| 230 | if iter % 5 == 0 { |
| 231 | run_start := int((state >> 8) & 0x7) |
| 232 | run_len := int((state >> 16) & 0x7) + 1 |
| 233 | for k in 0 .. run_len { |
| 234 | idx := 2 * (run_start + k) |
| 235 | if idx + 1 < bytes.len { |
| 236 | bytes[idx] = 0 |
| 237 | bytes[idx + 1] = 0 |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | canon := canonical_ipv6_from_bytes(bytes) or { |
| 242 | assert false, 'canonical_ipv6_from_bytes failed on iter ${iter}: ${err}' |
| 243 | continue |
| 244 | } |
| 245 | parsed := parse_ipv6_to_bytes(canon) or { |
| 246 | assert false, 'parse_ipv6_to_bytes failed on canonical "${canon}" (iter ${iter}): ${err}' |
| 247 | continue |
| 248 | } |
| 249 | assert parsed == bytes, 'round-trip mismatch on iter ${iter}: bytes=${bytes.hex()}, canon="${canon}", parsed=${parsed.hex()}' |
| 250 | |
| 251 | canon2 := canonical_ipv6(canon) or { |
| 252 | assert false, 'canonical_ipv6 failed on its own output "${canon}" (iter ${iter}): ${err}' |
| 253 | continue |
| 254 | } |
| 255 | assert canon == canon2, 'idempotence broken on iter ${iter}: "${canon}" -> "${canon2}"' |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | fn test_alternative_forms_canonicalize_identically() { |
| 260 | cases := [ |
| 261 | ['2001:db8:0:0:0:0:0:1', '2001:db8::1'], |
| 262 | ['2001:DB8:0000:0000:0000:0000:0000:0001', '2001:db8::1'], |
| 263 | ['2001:0db8::0001', '2001:db8::1'], |
| 264 | ['2001:DB8::1', '2001:db8::1'], |
| 265 | ['0:0:0:0:0:0:0:0', '::'], |
| 266 | ['0000:0000:0000:0000:0000:0000:0000:0000', '::'], |
| 267 | ['0:0:0:0:0:ffff:c0a8:0001', '::ffff:192.168.0.1'], |
| 268 | ['::FFFF:192.168.0.1', '::ffff:192.168.0.1'], |
| 269 | ['::ffff:c0a8:1', '::ffff:192.168.0.1'], |
| 270 | ['fe80:0:0:0:0:0:0:1', 'fe80::1'], |
| 271 | ['FE80::0:0:0:1', 'fe80::1'], |
| 272 | ] |
| 273 | for c in cases { |
| 274 | got := canonical_ipv6(c[0]) or { |
| 275 | assert false, 'canonical_ipv6("${c[0]}") errored: ${err}' |
| 276 | continue |
| 277 | } |
| 278 | assert got == c[1], '"${c[0]}" -> got "${got}", want "${c[1]}"' |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | fn test_well_known_corpus() { |
| 283 | assert canonical_ipv6('::')! == '::' |
| 284 | assert canonical_ipv6('::1')! == '::1' |
| 285 | assert canonical_ipv6('1::')! == '1::' |
| 286 | assert canonical_ipv6('fe80::1')! == 'fe80::1' |
| 287 | assert canonical_ipv6('2001:db8::')! == '2001:db8::' |
| 288 | assert canonical_ipv6('2001:db8:0:0:0:0:0:1')! == '2001:db8::1' |
| 289 | assert canonical_ipv6('2001:db8:0:0:1:0:0:1')! == '2001:db8::1:0:0:1' |
| 290 | // §5 is opt-in for non-::ffff:0:0/96 prefixes; we keep them as hex. |
| 291 | assert canonical_ipv6('64:ff9b::192.0.2.33')! == '64:ff9b::c000:221' |
| 292 | } |
| 293 | |