v / vlib / net / ipv6_test.v
292 lines · 274 sloc · 9.72 KB · 5696dc8da95388d0c3906688619582df87628097
Raw
1module net
2
3import 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
11struct V6Vec {
12 input string
13 expected string
14}
15
16const 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
82fn 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
101fn 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
116fn 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
128fn 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
142fn 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.
166fn 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
208fn 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.
222fn 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
259fn 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
282fn 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