///|
/// The value of a hexadecimal digit, or -1 when the character is not one.
fn hex_value(c : Char) -> Int32 {
let code = c.to_int()
if code >= 48 && code <= 57 {
code - 48
} else if code >= 97 && code <= 102 {
code - 87
} else if code >= 65 && code <= 70 {
code - 55
} else {
-1
}
}
///|
/// The characters between two positions, both counted in characters. Views index
/// bytes, so a zone name with any character outside ASCII would otherwise be cut
/// in the wrong place.
fn string_between(text : StringView, start : Int32, end : Int32) -> String {
let mut out = ""
let mut i = start
while i < end {
out = out + text.get_char(i).unwrap().to_string()
i = i + 1
}
out
}
///|
/// Four dotted decimal fields, ending exactly at `end`.
fn parse_v4_from(
text : StringView,
start : Int32,
end : Int32,
) -> Addr raise Refused {
let mut i = start
let mut value = 0UL
let mut field = 0
while true {
let field_start = i
let mut number = 0
while i < end {
let c = text.get_char(i).unwrap()
if c < '0' || c > '9' {
break
}
if i - field_start == 3 {
raise Refused::Malformed(at=i)
}
number = number * 10 + (c.to_int() - 48)
i = i + 1
}
if i == field_start {
raise Refused::Malformed(at=i)
}
if i - field_start > 1 && text.get_char(field_start).unwrap() == '0' {
// A leading zero would read as octal to some resolvers, so it is refused
// outright rather than guessed at.
raise Refused::Malformed(at=field_start)
}
if number > 255 {
raise Refused::OutOfRange(limit=255, got=number)
}
value = (value << 8) | number.to_uint64()
field = field + 1
if field == 4 {
break
}
if i >= end || text.get_char(i).unwrap() != '.' {
raise Refused::Malformed(at=i)
}
i = i + 1
}
if i != end {
raise Refused::Malformed(at=i)
}
addr_of_u32(value)
}
///|
/// The hex groups of an IPv6 address, with one `::` allowed to stand for the run of
/// zero groups it replaces.
fn parse_v6_body(
text : StringView,
end : Int32,
) -> (UInt64, UInt64) raise Refused {
let groups : Array[Int32] = []
let mut i = 0
let mut gap = -1
if end >= 2 && text.get_char(0).unwrap() == ':' {
if text.get_char(1).unwrap() != ':' {
raise Refused::Malformed(at=0)
}
gap = 0
i = 2
if i >= end {
return (0UL, 0UL)
}
}
while i < end {
let group_start = i
let mut value = 0
while i < end {
let digit = hex_value(text.get_char(i).unwrap())
if digit < 0 {
break
}
if i - group_start == 4 {
raise Refused::Malformed(at=i)
}
value = (value << 4) | digit
i = i + 1
}
if i == group_start {
raise Refused::Malformed(at=i)
}
if i < end && text.get_char(i).unwrap() == '.' {
// The last two groups may be written as one dotted quad instead.
if (gap < 0 && groups.length() != 6) || groups.length() > 6 {
raise Refused::Malformed(at=group_start)
}
let quad = parse_v4_from(text, group_start, end)
groups.push(((quad.lo >> 16) & 0xFFFFUL).to_int())
groups.push((quad.lo & 0xFFFFUL).to_int())
i = end
break
}
groups.push(value)
if i >= end {
break
}
if text.get_char(i).unwrap() != ':' {
raise Refused::Malformed(at=i)
}
i = i + 1
if i >= end {
raise Refused::Malformed(at=i - 1)
}
if text.get_char(i).unwrap() == ':' {
if gap >= 0 {
raise Refused::Malformed(at=i)
}
gap = groups.length()
i = i + 1
if i >= end {
break
}
}
}
let parsed = groups.length()
if gap < 0 {
if parsed != 8 {
raise Refused::Malformed(at=end)
}
} else if parsed > 7 {
// `::` has to stand for at least one group of zeros, or it is not shorter.
raise Refused::Malformed(at=end)
}
let padding = if gap < 0 { 0 } else { 8 - parsed }
let full : Array[Int32] = []
let mut k = 0
while k < 8 {
if gap >= 0 && k >= gap && k < gap + padding {
full.push(0)
} else if gap >= 0 && k >= gap + padding {
full.push(groups[k - padding])
} else {
full.push(groups[k])
}
k = k + 1
}
let hi = (full[0].to_uint64() << 48) |
(full[1].to_uint64() << 32) |
(full[2].to_uint64() << 16) |
full[3].to_uint64()
let lo = (full[4].to_uint64() << 48) |
(full[5].to_uint64() << 32) |
(full[6].to_uint64() << 16) |
full[7].to_uint64()
(hi, lo)
}
///|
fn parse_v6_text(text : StringView, n : Int32) -> Addr raise Refused {
let mut pct = -1
let mut i = 0
while i < n {
if text.get_char(i).unwrap() == '%' {
pct = i
break
}
i = i + 1
}
let zone = if pct < 0 {
None
} else {
if pct + 1 >= n {
raise Refused::Malformed(at=pct + 1)
}
let mut j = pct + 1
while j < n {
if text.get_char(j).unwrap() == '%' {
raise Refused::Malformed(at=j)
}
j = j + 1
}
Some(string_between(text, pct + 1, n))
}
let body_end = if pct < 0 { n } else { pct }
let (hi, lo) = parse_v6_body(text, body_end)
match zone {
None => Addr::v6(hi, lo)
Some(name) => Addr::v6(hi, lo).with_zone(Some(name))
}
}
///|
/// Read an address out of its text form: four dotted decimal fields, or the colon
/// form of RFC 4291 with an optional `%zone`.
pub fn Addr::parse(text : StringView) -> Addr raise Refused {
let n = text.char_length()
let mut i = 0
while i < n {
let c = text.get_char(i).unwrap()
if c == '.' {
return parse_v4_from(text, 0, n)
} else if c == ':' {
return parse_v6_text(text, n)
} else if c == '%' {
// A zone needs an address to sit on.
raise Refused::Malformed(at=i)
}
i = i + 1
}
raise Refused::Malformed(at=0)
}
///|
fn decimal_octets(value : UInt64) -> String {
let a = (value >> 24) & 0xFFUL
let b = (value >> 16) & 0xFFUL
let c = (value >> 8) & 0xFFUL
let d = value & 0xFFUL
a.to_int().to_string() +
"." +
b.to_int().to_string() +
"." +
c.to_int().to_string() +
"." +
d.to_int().to_string()
}
///|
/// One 16-bit group in lower-case hexadecimal, without leading zeros.
fn hex_group(value : Int32) -> String {
let digits = "0123456789abcdef"
let mut out = ""
let mut started = false
let mut shift = 12
while shift >= 0 {
let nibble = (value >> shift) & 0xF
if nibble != 0 || started || shift == 0 {
started = true
out = out + digits.get_char(nibble).unwrap().to_string()
}
shift = shift - 4
}
out
}
///|
/// One 16-bit group as exactly four lower-case hexadecimal digits.
fn hex_group_fixed(value : Int32) -> String {
let digits = "0123456789abcdef"
let mut out = ""
let mut shift = 12
while shift >= 0 {
out = out + digits.get_char((value >> shift) & 0xF).unwrap().to_string()
shift = shift - 4
}
out
}
///|
fn groups_of(addr : Addr) -> Array[Int32] {
[
((addr.hi >> 48) & 0xFFFFUL).to_int(),
((addr.hi >> 32) & 0xFFFFUL).to_int(),
((addr.hi >> 16) & 0xFFFFUL).to_int(),
(addr.hi & 0xFFFFUL).to_int(),
((addr.lo >> 48) & 0xFFFFUL).to_int(),
((addr.lo >> 32) & 0xFFFFUL).to_int(),
((addr.lo >> 16) & 0xFFFFUL).to_int(),
(addr.lo & 0xFFFFUL).to_int(),
]
}
///|
/// The longest run of two or more zero groups, leftmost when two runs tie, as
/// RFC 5952 section 4.2 asks. Returns (start, length), length 0 for no run.
fn zero_run(groups : Array[Int32], count : Int32) -> (Int32, Int32) {
let mut best_start = 0
let mut best = 0
let mut i = 0
while i < count {
if groups[i] == 0 {
let start = i
while i < count && groups[i] == 0 {
i = i + 1
}
if i - start > best {
best = i - start
best_start = start
}
} else {
i = i + 1
}
}
if best < 2 {
(0, 0)
} else {
(best_start, best)
}
}
///|
/// The text form of the address, in the canonical shape of RFC 5952: lower-case
/// hexadecimal, no leading zeros in a group, the longest run of zero groups
/// replaced by `::`, and an IPv4-mapped address written with its four dotted
/// decimal fields at the end.
///
/// ```mbt check
/// test {
/// assert_eq(
/// @moonip.Addr::parse("2001:0DB8:0000:0000:0000:0000:0000:0001").to_string(),
/// "2001:db8::1",
/// )
/// assert_eq(
/// @moonip.Addr::parse("::ffff:192.0.2.1").to_string(),
/// "::ffff:192.0.2.1",
/// )
/// }
/// ```
pub impl Show for Addr with fn to_string(self) {
if self.v4 {
return decimal_octets(u32_of(self))
}
let mapped = self.is_v4_mapped()
let count = if mapped { 6 } else { 8 }
let groups = groups_of(self)
let (run_start, run_length) = zero_run(groups, count)
let mut out = ""
let mut i = 0
while i < count {
if run_length > 0 && i == run_start {
out = out + "::"
i = i + run_length
} else {
if i > 0 && !(run_length > 0 && i == run_start + run_length) {
out = out + ":"
}
out = out + hex_group(groups[i])
i = i + 1
}
}
if mapped {
out = out + ":" + decimal_octets(self.lo & 0xFFFF_FFFFUL)
}
match self.zone {
None => out
Some(name) => out + "%" + name
}
}
///|
pub extend Addr with Show::{to_string, output}
///|
/// Every group written out in full, four hexadecimal digits each, with nothing
/// compressed. Handy for anything that compares text rather than reads it: two
/// addresses are equal exactly when their exploded forms are.
///
/// ```mbt check
/// test {
/// assert_eq(
/// @moonip.Addr::parse("2001:db8::1").exploded(),
/// "2001:0db8:0000:0000:0000:0000:0000:0001",
/// )
/// }
/// ```
pub fn Addr::exploded(self : Addr) -> String {
if self.v4 {
return decimal_octets(u32_of(self))
}
let groups = groups_of(self)
let count = if self.is_v4_mapped() { 6 } else { 8 }
let mut out = ""
let mut i = 0
while i < count {
if i > 0 {
out = out + ":"
}
out = out + hex_group_fixed(groups[i])
i = i + 1
}
if self.is_v4_mapped() {
out = out + ":" + decimal_octets(self.lo & 0xFFFF_FFFFUL)
}
out
}
///|
/// The name this address has in the reverse DNS tree: `4.3.2.1.in-addr.arpa` for
/// IPv4, and the thirty-two nibbles of the address in reverse order under
/// `ip6.arpa` for IPv6 (RFC 3596).
///
/// ```mbt check
/// test {
/// assert_eq(
/// @moonip.Addr::parse("192.0.2.1").reverse_pointer(),
/// "1.2.0.192.in-addr.arpa",
/// )
/// assert_eq(
/// @moonip.Addr::parse("::1").reverse_pointer(),
/// "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
/// )
/// }
/// ```
pub fn Addr::reverse_pointer(self : Addr) -> String {
let digits = "0123456789abcdef"
if self.v4 {
let value = u32_of(self)
let mut out = ""
for shift in [0, 8, 16, 24] {
out = out + ((value >> shift) & 0xFFUL).to_int().to_string() + "."
}
out + "in-addr.arpa"
} else {
let mut out = ""
let groups = groups_of(self)
let mut i = 7
while i >= 0 {
let mut shift = 0
while shift < 16 {
out = out +
digits.get_char((groups[i] >> shift) & 0xF).unwrap().to_string() +
"."
shift = shift + 4
}
i = i - 1
}
out + "ip6.arpa"
}
}