///|
/// Hex characters for IPv6 serialization
let hex_chars : FixedArray[Char] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
]
///|
/// Represents an IPv6 address as 8 16-bit pieces
struct IPv6(FixedArray[UInt16]) derive(Debug)
///|
/// Serialize IPv6 address per WHATWG URL spec
/// Finds longest run of consecutive zeros for :: compression
pub fn IPv6::to_string(self : IPv6) -> String {
let address = self.0
// Find the longest sequence of consecutive 0 pieces for compression
let mut compress_start : Int? = None
let mut compress_len = 0
let mut current_start : Int? = None
let mut current_len = 0
for i = 0; i < 8; i = i + 1 {
if address[i] == 0 {
if current_start is None {
current_start = Some(i)
current_len = 1
} else {
current_len = current_len + 1
}
} else {
if current_len > compress_len && current_len > 1 {
compress_start = current_start
compress_len = current_len
}
current_start = None
current_len = 0
}
}
// Check at the end
if current_len > compress_len && current_len > 1 {
compress_start = current_start
compress_len = current_len
}
// Build the string
let result = StringBuilder::new()
let mut i = 0
let mut ignore = false // Track if we just wrote :: and should not add another :
while i < 8 {
if compress_start is Some(start) && i == start {
// Always write "::" for zero compression
result.write_string("::")
i = i + compress_len
ignore = true
continue
}
if i > 0 && !ignore {
result.write_char(':')
}
ignore = false
// Write hex value (lowercase, no leading zeros)
let value = address[i].to_int()
if value == 0 {
result.write_char('0')
} else {
let mut started = false
for shift = 12; shift >= 0; shift = shift - 4 {
let digit = (value >> shift) & 0xf
if digit != 0 || started {
result.write_char(hex_chars[digit])
started = true
}
}
}
i = i + 1
}
result.to_string()
}
///|
/// Implement Show trait for IPv6, outputting compressed colon notation
pub impl Show for IPv6 with fn output(self : IPv6, logger : &Logger) -> Unit {
logger.write_string(self.to_string())
}
///|
/// Implement ToJson trait for IPv6, outputting as array of 8 16-bit values
pub impl ToJson for IPv6 with fn to_json(self : IPv6) -> Json {
self.0.to_json()
}
///|
/// Parse an IPv6 address per WHATWG URL spec.
/// Supports :: compression and embedded IPv4 addresses.
pub fn IPv6::parse(input : StringView) -> IPv6 raise ValidationError {
// 1. Let `address` be a new IPv6 address whose pieces are all 0.
let address : FixedArray[UInt16] = FixedArray::make(8, 0)
// 2. Let `piece_index` be `0`.
let mut piece_index : Int = 0
// 3. Let compress be `None`.
let mut compress : Int? = None
// 4. Let `pointer` be a pointer for `input`.
//
// Note: We do not have pointer arithmetic in Moonbit, so we will just
// manipulate the `input` directly in `match`/`loop` expressions below.
// 5. If `c` is U+003A (:), then:
let input = match input {
[':', ':', .. remaining] => {
// 5.3. Increase `piece_index` by 1 and then set compress to `piece_index`.
piece_index = piece_index + 1
compress = Some(piece_index)
// 5.2. Increase `pointer` by 2.
remaining
}
[':', ..] =>
// 5.1. If `remaining` does not start with U+003A (:),
// IPv6-invalid-compression validation error, return failure.
raise IPv6InvalidCompression
input => input
}
// 6. While `c` is ...
for input = input {
match input {
// ... not the EOF code point:
[] => break
// 6.1. If `piece_index` is 8, IPv6-too-many-pieces validation error,
// return failure.
_ if piece_index == 8 => raise IPv6TooManyPieces
// 6.2. If c is U+003A (:), then:
[':', .. remaining] => {
// 6.2.1. If `compress` is non-null, IPv6-multiple-compression validation
// error, return failure.
if compress is Some(_) {
raise IPv6MultipleCompression
}
// 6.2.2. increase pointer and `piece_index` by 1, set `compress` to
// `piece_index`, and then continue.
piece_index = piece_index + 1
compress = Some(piece_index)
continue remaining
}
input => {
// 6.3. Let `value` and `length` be 0.
let mut value : UInt16 = 0
let mut length = 0
// 6.4. While `length` is less than 4 and c is an ASCII hex digit, set
// `value` to `value` × 0x10 + c interpreted as hexadecimal number,
// and increase pointer and `length` by 1.
let remaining = for input = input; length < 4; {
match input {
['0'..='9' as c, .. rest] => {
value = value * 0x10 + (c.to_int().to_uint16() - '0')
length = length + 1
continue rest
}
['a'..='f' as c, .. rest] => {
value = value * 0x10 + (c.to_int().to_uint16() - 'a' + 10)
length = length + 1
continue rest
}
['A'..='F' as c, .. rest] => {
value = value * 0x10 + (c.to_int().to_uint16() - 'A' + 10)
length = length + 1
continue rest
}
_ => break input
}
} nobreak {
input
}
let remaining = match remaining {
[] => remaining
// 6.5. If c is U+002E (.), then:
['.', ..] => {
// 6.5.1. If [length] is 0, IPv4-in-IPv6-invalid-code-point validation
// error, return failure.
if length == 0 {
raise IPv4InIPv6InvalidCodePoint
}
// 6.5.2. Decrease pointer by length.
//
// Note: We does not have pointer arithmetic, so this is achieved by
// using `input` instead of `remaining` directly in the loop
// below.
// 6.5.3. If `piece_index` is greater than 6,
// IPv4-in-IPv6-too-many-pieces validation error, return
// failure.
if piece_index > 6 {
raise IPv4InIPv6TooManyPieces
}
// 6.5.4. Let `numbers_seen` be 0.
let mut numbers_seen = 0
// 6.5.5. While `c` is not ...
for input = input {
match input {
// ... the EOF code point:
[] => break
// 6.5.5.1. Let `ipv4_piece` be `None`.
//
// Note: Here we use `UInt16` directly since we will always
// initialize it in the loop below, and there is no need to
// have it initialized here as an `UInt16?`.
//
// 6.5.5.2. If `numbers_seen` is greater than 0, then:
// 1. If c is U+002E (.), increase pointer and `numbers_seen`
// is less than 4, then increase `pointer` by 1.
// 2. Otherwise, IPv4-in-IPv6-invalid-code-point validation
// error, return failure.
//
// Note: Here we test if the `c` is `.` first even if `numbers_seen`
// is `0`. This is because in the immediately following
// branching we will be testing if `c` is a digit, and raise
// the same error (IPv4-in-IPv6-invalid-code-point) if not.
// Therefore, the code logic here is equivalent to what is
// described in the spec.
['.', .. remaining] =>
if numbers_seen is (1..<4) {
continue remaining
} else {
raise IPv4InIPv6InvalidCodePoint
}
// 6.5.5.4. While `c` is an ASCII digit:
['0'..='9' as c, .. remaining] => {
let mut ipv4_piece : UInt16 = c.to_int().to_uint16() - '0'
// 6.5.5.4. While `c` is an ASCII digit:
let remaining = for remaining = remaining {
match remaining {
['0'..='9' as c, .. remaining] => {
// 6.5.5.4.1. Let `number` be `c` interpreted as decimal number.
let number = c.to_int().to_uint16() - '0'
// 6.5.5.4.2. If `ipv4_piece` is `None`, then set `ipv4_piece`
// │ to `number`.
// │
// │ Note: This is actually done above before entering
// │ this loop.
// │ Otherwise, if `ipv4_piece` is 0, IPv4-in-IPv6-invalid-code-point
// │ validation error, return failure.
if ipv4_piece is 0 {
raise IPv4InIPv6InvalidCodePoint
}
// │ Otherwise, set `ipv4_piece` to `ipv4_piece` × 10 + `number`.
ipv4_piece = ipv4_piece * 10 + number
// 6.5.5.4.3. If `ipv4_piece` is greater than 255, IPv4-in-IPv6-out-of-range-part
// validation error, return failure.
if ipv4_piece > 255 {
raise IPv4InIPv6OutOfRangePart
}
// 6.5.5.4.4. Increase `pointer` by 1.
continue remaining
}
remaining => break remaining
}
}
// 6.5.5.5. Set `address[piece_index]` to
// `address[piece_index]` × 0x100 + `ipv4_piece`.
address[piece_index] = address[piece_index] * 0x100 +
ipv4_piece
// 6.5.5.6. Increase `numbers_seen` by 1.
numbers_seen = numbers_seen + 1
// 6.5.5.7. If `numbers_seen` is 2 or 4, increase `piece_index`
// by 1.
if numbers_seen is (2 | 4) {
piece_index = piece_index + 1
}
continue remaining
}
// 6.5.5.3. If `c` is not an ASCII digit, IPv4-in-IPv6-invalid-code-point
// validation error, return failure.
[_, ..] => raise IPv4InIPv6InvalidCodePoint
}
}
// 6.5.6. If `numbers_seen` is not 4, IPv4-in-IPv6-too-few-parts validation
// error, return failure.
if numbers_seen != 4 {
raise IPv4InIPv6TooFewParts
}
// 6.5.7. Break.
break
}
// 6.6. Otherwise, if `c` is U+003A (:):
// 6.6.2. If `c` is the EOF code point, IPv6-invalid-code-point validation error,
// return failure.
[':'] => raise IPv6InvalidCodePoint
// 6.6.1. Increase `pointer` by 1.
[':', .. remaining] => remaining
// 6.7. Otherwise, if `c` is not the EOF code point, IPv6-invalid-code-point
// validation error, return failure.
[_, ..] => raise IPv6InvalidCodePoint
}
// 6.8. Set `address[piece_index]` to `value`.
address[piece_index] = value
// 6.9. Increase `piece_index` by 1.
piece_index = piece_index + 1
continue remaining
}
}
}
if compress is Some(compress) {
// 7. If `compress` is non-null, then:
// 7.1. Let `swaps` be `piece_index` - `compress`.
let mut swaps = piece_index - compress
// 7.2. Let `piece_index` be `7`.
piece_index = 7
// 7.3. While `swaps` is greater than `0`, swap `address[piece_index]` and
// `address[compress + swaps - 1]`, decrease `piece_index` by `1`, and
// decrease `swaps` by `1`.
while piece_index != 0 && swaps > 0 {
address.swap(piece_index, compress + swaps - 1)
piece_index = piece_index - 1
swaps = swaps - 1
}
} else if piece_index != 8 {
// 8. Otherwise, if `compress` is null and `piece_index` is not `8`,
// IPv6-too-few-pieces validation error, return failure.
raise IPv6TooFewPieces
}
// 9. Return address.
IPv6(address)
}