293 lines
9.7 KiB
V
293 lines
9.7 KiB
V
module net
|
|
|
|
import os
|
|
|
|
// Conformance tests for RFC 5952 (IPv6 text representation).
|
|
//
|
|
// Each vector is `(input_form, expected_canonical)`. Inputs include
|
|
// non-canonical legitimate forms (uppercase, leading zeros, partial
|
|
// "::" placement) so that canonical_ipv6() also exercises the parser.
|
|
|
|
struct V6Vec {
|
|
input string
|
|
expected string
|
|
}
|
|
|
|
const rfc5952_vectors = [
|
|
// -------- §4.1 Leading zeros MUST be suppressed --------
|
|
V6Vec{'2001:0db8:0000:0000:0000:0000:0000:0001', '2001:db8::1'},
|
|
V6Vec{'0000:0000:0000:0000:0000:0000:0000:0000', '::'},
|
|
V6Vec{'0000:0000:0000:0000:0000:0000:0000:0001', '::1'},
|
|
// "A single 16-bit 0000 field MUST be represented as 0."
|
|
V6Vec{'2001:0db8:0000:0001:0001:0001:0001:0001', '2001:db8:0:1:1:1:1:1'},
|
|
// -------- §4.2.1 Shorten as much as possible --------
|
|
V6Vec{'2001:db8:0:0:0:0:2:1', '2001:db8::2:1'},
|
|
// Counter-example from the RFC: "2001:db8::0:1 is not acceptable".
|
|
V6Vec{'2001:db8:0:0:0:0:0:1', '2001:db8::1'},
|
|
// -------- §4.2.2 "::" MUST NOT shorten just one 16-bit 0 field --------
|
|
// "2001:db8::1:1:1:1:1 is not correct" — single 0, leave as 0.
|
|
V6Vec{'2001:db8:0:1:1:1:1:1', '2001:db8:0:1:1:1:1:1'},
|
|
// Several singletons, none shortened:
|
|
V6Vec{'1:0:1:0:1:0:1:0', '1:0:1:0:1:0:1:0'},
|
|
V6Vec{'0:1:0:1:0:1:0:1', '0:1:0:1:0:1:0:1'},
|
|
// -------- §4.2.3 Longest run wins; ties → first run --------
|
|
// Three zeros (0,0,0) at positions 1..3 outweighs two zeros at 4..5.
|
|
V6Vec{'2001:0:0:0:1:0:0:1', '2001::1:0:0:1'},
|
|
// Tie: two equal pairs (0,0) at positions 2..3 and 5..6 — first wins.
|
|
V6Vec{'2001:db8:0:0:1:0:0:1', '2001:db8::1:0:0:1'},
|
|
// Earlier example from §4.2.3 with three trailing zero fields:
|
|
V6Vec{'2001:0:0:1:0:0:0:1', '2001:0:0:1::1'},
|
|
// -------- §4.3 Lowercase --------
|
|
V6Vec{'2001:0DB8:AC10:FE01:0000:0000:0000:0000', '2001:db8:ac10:fe01::'},
|
|
V6Vec{'FE80::1', 'fe80::1'},
|
|
V6Vec{'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'},
|
|
// -------- §5 IPv4-mapped (::ffff:0:0/96) → mixed notation --------
|
|
V6Vec{'0:0:0:0:0:ffff:c000:0201', '::ffff:192.0.2.1'},
|
|
V6Vec{'::ffff:192.0.2.1', '::ffff:192.0.2.1'},
|
|
V6Vec{'0000:0000:0000:0000:0000:FFFF:0A00:0001', '::ffff:10.0.0.1'},
|
|
V6Vec{'::ffff:0.0.0.0', '::ffff:0.0.0.0'},
|
|
V6Vec{'::ffff:255.255.255.255', '::ffff:255.255.255.255'},
|
|
// -------- Run at start (>=2) --------
|
|
V6Vec{'0:0:0:0:0:0:0:2', '::2'},
|
|
V6Vec{'0:0:0:0:0:0:1:2', '::1:2'},
|
|
V6Vec{'0:0:c:d:e:f:1:2', '::c:d:e:f:1:2'},
|
|
// -------- Run at end (>=2) --------
|
|
V6Vec{'1::', '1::'},
|
|
V6Vec{'1:0:0:0:0:0:0:0', '1::'},
|
|
V6Vec{'a:b:c:d:e:f:0:0', 'a:b:c:d:e:f::'},
|
|
V6Vec{'2001:db8:ac10:fe01::', '2001:db8:ac10:fe01::'},
|
|
// -------- Run in the middle --------
|
|
V6Vec{'2001:db8::1', '2001:db8::1'},
|
|
V6Vec{'fe80:0:0:0:0:0:0:1', 'fe80::1'},
|
|
// -------- Loopback / unspecified --------
|
|
V6Vec{'::1', '::1'},
|
|
V6Vec{'::', '::'},
|
|
// -------- No zero field at all --------
|
|
V6Vec{'1:2:3:4:5:6:7:8', '1:2:3:4:5:6:7:8'},
|
|
V6Vec{'2001:db8:1:2:3:4:5:6', '2001:db8:1:2:3:4:5:6'},
|
|
// -------- Mixed leading-zero suppression with longer/shorter values --------
|
|
V6Vec{'0001:0023:0456:7890:abcd:00ef:0:1', '1:23:456:7890:abcd:ef:0:1'},
|
|
// -------- Two equal-length non-trivial runs (§4.2.3 tie) --------
|
|
V6Vec{'0:0:1:2:3:4:0:0', '::1:2:3:4:0:0'},
|
|
// -------- Run of length exactly 2 still triggers compression --------
|
|
V6Vec{'1:2:0:0:3:4:5:6', '1:2::3:4:5:6'},
|
|
// -------- Adjacent zero singletons: NEITHER must be compressed when
|
|
// a longer run exists --------
|
|
V6Vec{'0:1:0:0:0:1:0:1', '0:1::1:0:1'},
|
|
// -------- Parser robustness --------
|
|
V6Vec{'2001:DB8::AB:CD', '2001:db8::ab:cd'},
|
|
V6Vec{'::ffff:192.000.002.001', '::ffff:192.0.2.1'},
|
|
]!
|
|
|
|
fn test_rfc5952_vectors() {
|
|
mut failures := []string{}
|
|
for v in rfc5952_vectors {
|
|
got := canonical_ipv6(v.input) or {
|
|
failures << '${v.input} -> error: ${err}'
|
|
continue
|
|
}
|
|
if got != v.expected {
|
|
failures << '${v.input} -> got ${got}, want ${v.expected}'
|
|
}
|
|
}
|
|
if failures.len > 0 {
|
|
for f in failures {
|
|
eprintln(' FAIL: ${f}')
|
|
}
|
|
assert false, '${failures.len}/${rfc5952_vectors.len} RFC 5952 vectors failed'
|
|
}
|
|
}
|
|
|
|
fn test_from_bytes_basic() {
|
|
zeros := []u8{len: 16}
|
|
assert canonical_ipv6_from_bytes(zeros)! == '::'
|
|
|
|
mut loop := []u8{len: 16}
|
|
loop[15] = 1
|
|
assert canonical_ipv6_from_bytes(loop)! == '::1'
|
|
|
|
bytes := [u8(0x20), 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
|
|
assert canonical_ipv6_from_bytes(bytes)! == '2001:db8::1'
|
|
|
|
mapped := [u8(0), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 0, 2, 1]
|
|
assert canonical_ipv6_from_bytes(mapped)! == '::ffff:192.0.2.1'
|
|
}
|
|
|
|
fn test_from_bytes_rejects_wrong_length() {
|
|
if _ := canonical_ipv6_from_bytes([u8(0)]) {
|
|
assert false, 'must reject 1-byte input'
|
|
}
|
|
if _ := canonical_ipv6_from_bytes([]u8{len: 15}) {
|
|
assert false, 'must reject 15-byte input'
|
|
}
|
|
if _ := canonical_ipv6_from_bytes([]u8{len: 17}) {
|
|
assert false, 'must reject 17-byte input'
|
|
}
|
|
}
|
|
|
|
fn test_canonical_is_idempotent() {
|
|
for v in rfc5952_vectors {
|
|
once := canonical_ipv6(v.input) or {
|
|
assert false, 'first pass failed on ${v.input}: ${err}'
|
|
continue
|
|
}
|
|
twice := canonical_ipv6(once) or {
|
|
assert false, 'second pass failed on ${once}: ${err}'
|
|
continue
|
|
}
|
|
assert once == twice, 'idempotence broken: ${v.input} -> ${once} -> ${twice}'
|
|
}
|
|
}
|
|
|
|
fn test_parser_rejects_malformed() {
|
|
bad := [
|
|
'',
|
|
'gggg::1', // non-hex
|
|
'1::2::3', // two "::"
|
|
'1:2:3:4:5:6:7:8:9', // too many groups, no "::"
|
|
'1:2:3:4:5:6:7', // too few groups, no "::"
|
|
'1:2:3:4:5:6:7:8::', // "::" with already 8 groups
|
|
'12345::1', // group too long
|
|
'1:2:3:4:5:6:1.2.3', // bad dotted quad (3 octets)
|
|
'1:2:3:4:5:6:1.2.3.4.5', // bad dotted quad (5 octets)
|
|
'::ffff:1.2.3.999', // octet > 255
|
|
'::ffff:1.2.3.', // trailing dot in v4
|
|
]
|
|
for s in bad {
|
|
if _ := canonical_ipv6(s) {
|
|
assert false, 'parser must reject "${s}"'
|
|
}
|
|
}
|
|
}
|
|
|
|
// test_inet_ntop_corpus loads testdata/ipv6_rfc5952.tsv (113 vectors
|
|
// generated by Python's ipaddress.IPv6Address.compressed) and confirms
|
|
// our pure-V implementation produces byte-identical output.
|
|
fn test_inet_ntop_corpus() {
|
|
path := os.join_path(os.dir(@FILE), 'testdata', 'ipv6_rfc5952.tsv')
|
|
body := os.read_file(path) or {
|
|
assert false, 'cannot read fixture ${path}: ${err}'
|
|
return
|
|
}
|
|
mut total := 0
|
|
mut failed := []string{}
|
|
for raw in body.split('\n') {
|
|
line := raw.trim_space()
|
|
if line.len == 0 {
|
|
continue
|
|
}
|
|
fields := line.split('\t')
|
|
if fields.len != 2 {
|
|
assert false, 'bad fixture line: ${line}'
|
|
continue
|
|
}
|
|
hex_addr := fields[0]
|
|
want := fields[1]
|
|
bytes := hex_to_bytes(hex_addr) or {
|
|
assert false, 'bad hex: ${hex_addr}'
|
|
continue
|
|
}
|
|
got := canonical_ipv6_from_bytes(bytes) or {
|
|
failed << '${hex_addr}: ${err}'
|
|
continue
|
|
}
|
|
if got != want {
|
|
failed << '${hex_addr}: got "${got}", want "${want}"'
|
|
}
|
|
total++
|
|
}
|
|
if failed.len > 0 {
|
|
for f in failed {
|
|
eprintln(' FAIL: ${f}')
|
|
}
|
|
assert false, '${failed.len}/${total} cross-check vectors failed'
|
|
}
|
|
assert total >= 100, 'expected >= 100 vectors, got ${total}'
|
|
}
|
|
|
|
fn hex_to_bytes(s string) ![]u8 {
|
|
if s.len % 2 != 0 {
|
|
return error('hex_to_bytes: odd length')
|
|
}
|
|
mut out := []u8{cap: s.len / 2}
|
|
for i := 0; i < s.len; i += 2 {
|
|
hi := hex_digit(s[i]) or { return error('bad hex at ${i}') }
|
|
lo := hex_digit(s[i + 1]) or { return error('bad hex at ${i + 1}') }
|
|
out << (hi << 4) | lo
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Property: bytes -> canonical -> parse -> bytes round-trip + idempotence.
|
|
fn test_property_roundtrip_and_idempotence() {
|
|
mut state := u64(0xCAFEBABEDEADBEEF)
|
|
for iter in 0 .. 1000 {
|
|
mut bytes := []u8{cap: 16}
|
|
for _ in 0 .. 16 {
|
|
state = state * 6364136223846793005 + 1442695040888963407
|
|
bytes << u8(state >> 56)
|
|
}
|
|
if iter % 5 == 0 {
|
|
run_start := int((state >> 8) & 0x7)
|
|
run_len := int((state >> 16) & 0x7) + 1
|
|
for k in 0 .. run_len {
|
|
idx := 2 * (run_start + k)
|
|
if idx + 1 < bytes.len {
|
|
bytes[idx] = 0
|
|
bytes[idx + 1] = 0
|
|
}
|
|
}
|
|
}
|
|
canon := canonical_ipv6_from_bytes(bytes) or {
|
|
assert false, 'canonical_ipv6_from_bytes failed on iter ${iter}: ${err}'
|
|
continue
|
|
}
|
|
parsed := parse_ipv6_to_bytes(canon) or {
|
|
assert false, 'parse_ipv6_to_bytes failed on canonical "${canon}" (iter ${iter}): ${err}'
|
|
continue
|
|
}
|
|
assert parsed == bytes, 'round-trip mismatch on iter ${iter}: bytes=${bytes.hex()}, canon="${canon}", parsed=${parsed.hex()}'
|
|
|
|
canon2 := canonical_ipv6(canon) or {
|
|
assert false, 'canonical_ipv6 failed on its own output "${canon}" (iter ${iter}): ${err}'
|
|
continue
|
|
}
|
|
assert canon == canon2, 'idempotence broken on iter ${iter}: "${canon}" -> "${canon2}"'
|
|
}
|
|
}
|
|
|
|
fn test_alternative_forms_canonicalize_identically() {
|
|
cases := [
|
|
['2001:db8:0:0:0:0:0:1', '2001:db8::1'],
|
|
['2001:DB8:0000:0000:0000:0000:0000:0001', '2001:db8::1'],
|
|
['2001:0db8::0001', '2001:db8::1'],
|
|
['2001:DB8::1', '2001:db8::1'],
|
|
['0:0:0:0:0:0:0:0', '::'],
|
|
['0000:0000:0000:0000:0000:0000:0000:0000', '::'],
|
|
['0:0:0:0:0:ffff:c0a8:0001', '::ffff:192.168.0.1'],
|
|
['::FFFF:192.168.0.1', '::ffff:192.168.0.1'],
|
|
['::ffff:c0a8:1', '::ffff:192.168.0.1'],
|
|
['fe80:0:0:0:0:0:0:1', 'fe80::1'],
|
|
['FE80::0:0:0:1', 'fe80::1'],
|
|
]
|
|
for c in cases {
|
|
got := canonical_ipv6(c[0]) or {
|
|
assert false, 'canonical_ipv6("${c[0]}") errored: ${err}'
|
|
continue
|
|
}
|
|
assert got == c[1], '"${c[0]}" -> got "${got}", want "${c[1]}"'
|
|
}
|
|
}
|
|
|
|
fn test_well_known_corpus() {
|
|
assert canonical_ipv6('::')! == '::'
|
|
assert canonical_ipv6('::1')! == '::1'
|
|
assert canonical_ipv6('1::')! == '1::'
|
|
assert canonical_ipv6('fe80::1')! == 'fe80::1'
|
|
assert canonical_ipv6('2001:db8::')! == '2001:db8::'
|
|
assert canonical_ipv6('2001:db8:0:0:0:0:0:1')! == '2001:db8::1'
|
|
assert canonical_ipv6('2001:db8:0:0:1:0:0:1')! == '2001:db8::1:0:0:1'
|
|
// §5 is opt-in for non-::ffff:0:0/96 prefixes; we keep them as hex.
|
|
assert canonical_ipv6('64:ff9b::192.0.2.33')! == '64:ff9b::c000:221'
|
|
}
|