| 1 | module net |
| 2 | |
| 3 | // IPv6 text representation per RFC 5952 (August 2010). |
| 4 | // |
| 5 | // Pure-V replacement for libc's inet_ntop on the IPv6 path. inet_ntop |
| 6 | // historically follows the older RFC 1884 / RFC 2373 rules and emits |
| 7 | // the deprecated IPv4-compatible mixed form (`::a.b.c.d`) for any |
| 8 | // address whose upper 96 bits are zero — non-conformant per RFC 5952 |
| 9 | // §5, which restricts the mixed form to recognized IPv4-in-IPv6 |
| 10 | // formats. |
| 11 | // |
| 12 | // Reference: https://www.rfc-editor.org/rfc/rfc5952 |
| 13 | // |
| 14 | // Conformance summary: |
| 15 | // §4.1 Leading zeros MUST be suppressed. ✓ |
| 16 | // §4.2.1 "::" MUST be used to its maximum capability. ✓ |
| 17 | // §4.2.2 "::" MUST NOT shorten just one 16-bit 0 field. ✓ |
| 18 | // §4.2.3 Longest run of zeros wins; ties: first run wins. ✓ |
| 19 | // §4.3 Hex digits a-f MUST be lowercase. ✓ |
| 20 | // §5 IPv4-mapped (::ffff:0:0/96) uses dotted-quad ✓ |
| 21 | // tail; the leading zeros and 0xffff are still |
| 22 | // formatted per §4. Other IPv4-in-IPv6 formats |
| 23 | // (ISATAP, IPv4-translatable) are not detectable |
| 24 | // from the address bits alone and stay in hex. |
| 25 | |
| 26 | // canonical_ipv6_from_bytes formats a 16-byte IPv6 address as the |
| 27 | // canonical text representation defined by RFC 5952. Errors only when |
| 28 | // the input slice is not exactly 16 octets long. |
| 29 | pub fn canonical_ipv6_from_bytes(b []u8) !string { |
| 30 | if b.len != 16 { |
| 31 | return error('canonical_ipv6_from_bytes: need 16 bytes, got ${b.len}') |
| 32 | } |
| 33 | mut groups := [8]u16{} |
| 34 | for i in 0 .. 8 { |
| 35 | groups[i] = (u16(b[2 * i]) << 8) | u16(b[2 * i + 1]) |
| 36 | } |
| 37 | return format_ipv6_groups(groups) |
| 38 | } |
| 39 | |
| 40 | // canonical_ipv6 takes any RFC 4291 legitimate text form (full, |
| 41 | // "::"-compressed, or with an embedded IPv4 dotted-quad tail) and |
| 42 | // returns the RFC 5952 canonical form. |
| 43 | pub fn canonical_ipv6(addr string) !string { |
| 44 | bytes := parse_ipv6_to_bytes(addr)! |
| 45 | return canonical_ipv6_from_bytes(bytes) |
| 46 | } |
| 47 | |
| 48 | // format_ipv6_groups turns 8 u16 groups into the canonical RFC 5952 |
| 49 | // string. Only the IPv4-mapped prefix (::ffff:0:0/96, RFC 4291) |
| 50 | // triggers mixed notation here — other RFC 5952 §5 prefixes |
| 51 | // (IPv4-translatable RFC 6052, ISATAP) require out-of-band knowledge |
| 52 | // and are not assumed. |
| 53 | fn format_ipv6_groups(g [8]u16) string { |
| 54 | if is_ipv4_mapped(g) { |
| 55 | a := g[6] >> 8 |
| 56 | b := g[6] & 0xff |
| 57 | c := g[7] >> 8 |
| 58 | d := g[7] & 0xff |
| 59 | return '::ffff:${a}.${b}.${c}.${d}' |
| 60 | } |
| 61 | |
| 62 | start, length := longest_zero_run(g) |
| 63 | |
| 64 | mut parts := []string{cap: 8} |
| 65 | for v in g { |
| 66 | parts << v.hex() |
| 67 | } |
| 68 | |
| 69 | if length < 2 { |
| 70 | return parts.join(':') |
| 71 | } |
| 72 | |
| 73 | mut out := '' |
| 74 | if start > 0 { |
| 75 | out += parts[..start].join(':') |
| 76 | } |
| 77 | out += '::' |
| 78 | end := start + length |
| 79 | if end < 8 { |
| 80 | out += parts[end..].join(':') |
| 81 | } |
| 82 | return out |
| 83 | } |
| 84 | |
| 85 | // is_ipv4_mapped tests for the ::ffff:0:0/96 prefix (RFC 4291 §2.5.5.2). |
| 86 | fn is_ipv4_mapped(g [8]u16) bool { |
| 87 | return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 && g[5] == 0xffff |
| 88 | } |
| 89 | |
| 90 | // longest_zero_run scans the 8 groups for the longest contiguous run of |
| 91 | // 0x0000 fields and returns (start_index, length). On ties the FIRST |
| 92 | // run wins (RFC 5952 §4.2.3). When no zero is present, length is 0. |
| 93 | fn longest_zero_run(g [8]u16) (int, int) { |
| 94 | mut best_start := 0 |
| 95 | mut best_len := 0 |
| 96 | mut cur_start := 0 |
| 97 | mut cur_len := 0 |
| 98 | for i in 0 .. 8 { |
| 99 | if g[i] == 0 { |
| 100 | if cur_len == 0 { |
| 101 | cur_start = i |
| 102 | } |
| 103 | cur_len++ |
| 104 | if cur_len > best_len { |
| 105 | best_len = cur_len |
| 106 | best_start = cur_start |
| 107 | } |
| 108 | } else { |
| 109 | cur_len = 0 |
| 110 | } |
| 111 | } |
| 112 | return best_start, best_len |
| 113 | } |
| 114 | |
| 115 | // parse_ipv6_to_bytes accepts the legitimate RFC 4291 forms: |
| 116 | // - 8 groups of 1..4 hex digits separated by ':' |
| 117 | // - the same with one "::" run replacing one or more all-zero groups |
| 118 | // - the same with the trailing 32 bits given as dotted-quad IPv4 |
| 119 | // Returns 16 octets in network order. |
| 120 | fn parse_ipv6_to_bytes(s string) ![]u8 { |
| 121 | if s == '' { |
| 122 | return error('parse_ipv6: empty') |
| 123 | } |
| 124 | double_idx := s.index('::') or { -1 } |
| 125 | if s.count('::') > 1 { |
| 126 | return error('parse_ipv6: multiple "::" runs') |
| 127 | } |
| 128 | |
| 129 | head, tail := if double_idx >= 0 { |
| 130 | s[..double_idx], s[double_idx + 2..] |
| 131 | } else { |
| 132 | s, '' |
| 133 | } |
| 134 | |
| 135 | mut head_g := if head == '' { []string{} } else { head.split(':') } |
| 136 | mut tail_g := if tail == '' { []string{} } else { tail.split(':') } |
| 137 | mut v4_bytes := []u8{} |
| 138 | |
| 139 | // Detect dotted-quad tail in the LAST group (of tail when "::" present, |
| 140 | // of head otherwise). |
| 141 | tail_owns_last := double_idx >= 0 |
| 142 | last_group := if tail_owns_last && tail_g.len > 0 { |
| 143 | tail_g[tail_g.len - 1] |
| 144 | } else if !tail_owns_last && head_g.len > 0 { |
| 145 | head_g[head_g.len - 1] |
| 146 | } else { |
| 147 | '' |
| 148 | } |
| 149 | if last_group.contains('.') { |
| 150 | v4_bytes = parse_dotted_quad(last_group)! |
| 151 | if tail_owns_last { |
| 152 | tail_g = tail_g[..tail_g.len - 1].clone() |
| 153 | } else { |
| 154 | head_g = head_g[..head_g.len - 1].clone() |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | mut head_words := []u16{} |
| 159 | for h in head_g { |
| 160 | head_words << parse_hex_group(h)! |
| 161 | } |
| 162 | mut tail_words := []u16{} |
| 163 | for t in tail_g { |
| 164 | tail_words << parse_hex_group(t)! |
| 165 | } |
| 166 | |
| 167 | v4_word_len := if v4_bytes.len == 4 { 2 } else { 0 } |
| 168 | total := head_words.len + tail_words.len + v4_word_len |
| 169 | if double_idx < 0 { |
| 170 | if total != 8 { |
| 171 | return error('parse_ipv6: ${s} must have exactly 8 groups (got ${total})') |
| 172 | } |
| 173 | } else { |
| 174 | if total >= 8 { |
| 175 | return error('parse_ipv6: "::" present but address already has ${total} groups') |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | mut words := []u16{cap: 8} |
| 180 | for w in head_words { |
| 181 | words << w |
| 182 | } |
| 183 | if double_idx >= 0 { |
| 184 | for _ in 0 .. (8 - total) { |
| 185 | words << u16(0) |
| 186 | } |
| 187 | } |
| 188 | for w in tail_words { |
| 189 | words << w |
| 190 | } |
| 191 | if v4_bytes.len == 4 { |
| 192 | words << (u16(v4_bytes[0]) << 8) | u16(v4_bytes[1]) |
| 193 | words << (u16(v4_bytes[2]) << 8) | u16(v4_bytes[3]) |
| 194 | } |
| 195 | |
| 196 | if words.len != 8 { |
| 197 | return error('parse_ipv6: internal: expanded to ${words.len} groups') |
| 198 | } |
| 199 | |
| 200 | mut out := []u8{cap: 16} |
| 201 | for w in words { |
| 202 | out << u8(w >> 8) |
| 203 | out << u8(w & 0xff) |
| 204 | } |
| 205 | return out |
| 206 | } |
| 207 | |
| 208 | fn parse_hex_group(s string) !u16 { |
| 209 | if s == '' || s.len > 4 { |
| 210 | return error('parse_ipv6: bad hex group "${s}"') |
| 211 | } |
| 212 | mut v := u32(0) |
| 213 | for c in s { |
| 214 | d := hex_digit(c) or { return error('parse_ipv6: non-hex char in "${s}"') } |
| 215 | v = (v << 4) | u32(d) |
| 216 | } |
| 217 | return u16(v) |
| 218 | } |
| 219 | |
| 220 | fn hex_digit(c u8) ?u8 { |
| 221 | if c >= `0` && c <= `9` { |
| 222 | return u8(c - `0`) |
| 223 | } |
| 224 | if c >= `a` && c <= `f` { |
| 225 | return u8(c - `a` + 10) |
| 226 | } |
| 227 | if c >= `A` && c <= `F` { |
| 228 | return u8(c - `A` + 10) |
| 229 | } |
| 230 | return none |
| 231 | } |
| 232 | |
| 233 | fn parse_dotted_quad(s string) ![]u8 { |
| 234 | parts := s.split('.') |
| 235 | if parts.len != 4 { |
| 236 | return error('parse_ipv6: bad dotted-quad "${s}"') |
| 237 | } |
| 238 | mut out := []u8{cap: 4} |
| 239 | for p in parts { |
| 240 | if p.len == 0 || p.len > 3 { |
| 241 | return error('parse_ipv6: bad octet "${p}"') |
| 242 | } |
| 243 | mut v := u32(0) |
| 244 | for c in p { |
| 245 | if c < `0` || c > `9` { |
| 246 | return error('parse_ipv6: non-digit in octet "${p}"') |
| 247 | } |
| 248 | v = v * 10 + u32(c - `0`) |
| 249 | } |
| 250 | if v > 255 { |
| 251 | return error('parse_ipv6: octet "${p}" > 255') |
| 252 | } |
| 253 | out << u8(v) |
| 254 | } |
| 255 | return out |
| 256 | } |
| 257 | |