#[must_use]
pub fn aton(text: &[u8]) -> Option<[u8; 4]> {
let mut parts = Vec::new();
for field in core::str::from_utf8(text).ok()?.split('.') {
let (digits, radix) = match field.as_bytes() {
[b'0', b'x' | b'X', rest @ ..] => (rest, 16),
[b'0', rest @ ..] if !rest.is_empty() => (rest, 8),
other => (other, 10),
};
let digits = core::str::from_utf8(digits).ok()?;
parts.push(u32::from_str_radix(digits, radix).ok()?);
if parts.len() > 4 {
return None;
}
}
let leading = parts.len().checked_sub(1)?;
let mut address: u32 = 0;
for (index, part) in parts.iter().enumerate() {
let width = if index == leading {
32 - 8 * leading
} else {
8
};
if width < 32 && *part >= 1 << width {
return None;
}
address |= part << (32 - 8 * index - width);
}
Some(address.to_be_bytes())
}
#[must_use]
pub fn ntoa(packed: [u8; 4]) -> String {
format!("{}.{}.{}.{}", packed[0], packed[1], packed[2], packed[3])
}
fn strict_octet(field: &[u8]) -> Option<u8> {
if field.is_empty() || field.len() > 3 || (field.len() > 1 && field[0] == b'0') {
return None;
}
let mut value: u32 = 0;
for digit in field {
value = value * 10 + u32::from(digit.checked_sub(b'0').filter(|d| *d < 10)?);
}
u8::try_from(value).ok()
}
#[must_use]
pub fn pton_v4(text: &[u8]) -> Option<[u8; 4]> {
let mut address = [0u8; 4];
let mut fields = text.split(|b| *b == b'.');
for slot in &mut address {
*slot = strict_octet(fields.next()?)?;
}
fields.next().is_none().then_some(address)
}
#[must_use]
pub fn pton_v6(text: &[u8]) -> Option<[u8; 16]> {
let (head, tail) = match text.iter().position(|b| *b == b'.') {
None => (text, None),
Some(_) => {
let colon = text.iter().rposition(|b| *b == b':')?;
let head_end = colon + usize::from(text[..colon].ends_with(b":"));
(&text[..head_end], Some(pton_v4(&text[colon + 1..])?))
}
};
let groups_wanted = if tail.is_some() { 6 } else { 8 };
let (before, after) = match find_double_colon(head)? {
None => (head, None),
Some(at) => (&head[..at], Some(&head[at + 2..])),
};
let leading = hex_groups(before, groups_wanted)?;
let trailing = match after {
None => Vec::new(),
Some(rest) => hex_groups(rest, groups_wanted - leading.len())?,
};
let elided = groups_wanted - leading.len() - trailing.len();
if (after.is_none() && elided != 0) || (after.is_some() && elided == 0) {
return None;
}
let mut address = [0u8; 16];
let mut out = address.iter_mut();
for group in leading
.iter()
.chain(core::iter::repeat_n(&0u16, elided))
.chain(trailing.iter())
{
*out.next()? = (group >> 8) as u8;
*out.next()? = *group as u8;
}
if let Some(quad) = tail {
address[12..].copy_from_slice(&quad);
}
Some(address)
}
fn find_double_colon(text: &[u8]) -> Option<Option<usize>> {
let mut found = None;
let mut index = 0;
while index + 1 < text.len() {
if text[index] == b':' && text[index + 1] == b':' {
if found.is_some() {
return None;
}
found = Some(index);
index += 1;
}
index += 1;
}
Some(found)
}
fn hex_groups(text: &[u8], limit: usize) -> Option<Vec<u16>> {
if text.is_empty() {
return Some(Vec::new());
}
let mut groups = Vec::new();
for field in text.split(|b| *b == b':') {
if field.is_empty() || field.len() > 4 || groups.len() == limit {
return None;
}
let mut value: u16 = 0;
for digit in field {
value = value * 16 + u16::from((*digit as char).to_digit(16)? as u8);
}
groups.push(value);
}
Some(groups)
}
#[must_use]
pub fn ntop_v6(packed: &[u8]) -> String {
let group = |i: usize| (u16::from(packed[2 * i]) << 8) | u16::from(packed[2 * i + 1]);
let text = if packed[..12] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff] {
format!(
"{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{}.{}.{}.{}",
group(0),
group(1),
group(2),
group(3),
group(4),
group(5),
packed[12],
packed[13],
packed[14],
packed[15]
)
} else {
(0..8)
.map(|i| format!("{:x}", group(i)))
.collect::<Vec<_>>()
.join(":")
};
let bytes = text.as_bytes();
let (mut best, mut longest) = (0, 2);
for start in 0..bytes.len() {
if start != 0 && bytes[start] != b':' {
continue;
}
let run = bytes[start..]
.iter()
.take_while(|b| **b == b':' || **b == b'0')
.count();
if run > longest + usize::from(best == 0) {
(best, longest) = (start, run);
}
}
if longest <= 3 {
return text;
}
format!("{}::{}", &text[..best], &text[best + longest..])
}
#[cfg(test)]
mod tests {
use super::*;
use core::fmt::Write;
#[test]
fn pton_v6_reads_the_shared_grammar() {
let cases: &[(&str, Option<&str>)] = &[
("::", Some("00000000000000000000000000000000")),
("::1", Some("00000000000000000000000000000001")),
("1::", Some("00010000000000000000000000000000")),
("::192.0.2.1", Some("000000000000000000000000c0000201")),
(
"2001:db8::192.0.2.1",
Some("20010db80000000000000000c0000201"),
),
("::ffff:192.0.2.1", Some("00000000000000000000ffffc0000201")),
(
"0:0:0:0:0:ffff:192.0.2.1",
Some("00000000000000000000ffffc0000201"),
),
("1::2:192.0.2.1", Some("000100000000000000000002c0000201")),
(
"1:2:3:4:5:6:1.2.3.4",
Some("00010002000300040005000601020304"),
),
("1:2:3:4:5:6:7:8", Some("00010002000300040005000600070008")),
("fe80::1", Some("fe800000000000000000000000000001")),
("1:2:3:4:5:6:7::", Some("00010002000300040005000600070000")),
("::1:2:3:4:5:6:7", Some("00000001000200030004000500060007")),
("1::2::3", None),
(":::192.0.2.1", None),
("::192.0.2.1.5", None),
("::1.2.3", None),
("1:2:3:4:5:6:7:8:9", None),
("1:2:3:4:5:6:7", None),
("1.2.3.4", None),
("::01.2.3.4", None),
("::00001", None),
("::%eth0", None),
("", None),
(":", None),
];
for (text, want) in cases {
let got = pton_v6(text.as_bytes()).map(|bytes| {
bytes.iter().fold(String::new(), |mut output, byte| {
write!(output, "{byte:02x}").unwrap();
output
})
});
assert_eq!(got.as_deref(), *want, "inet_pton(AF_INET6, {text:?})");
}
}
#[test]
fn pton_v4_reads_four_strict_octets() {
for (text, want) in [
("1.2.3.4", Some([1, 2, 3, 4])),
("255.255.255.255", Some([255, 255, 255, 255])),
("0.0.0.0", Some([0, 0, 0, 0])),
("01.2.3.4", None),
("1.2.3", None),
("1.2.3.4.5", None),
("1.2.3.256", None),
("1.2.3.", None),
] {
assert_eq!(
pton_v4(text.as_bytes()),
want,
"inet_pton(AF_INET, {text:?})"
);
}
}
#[test]
fn aton_is_the_lenient_parser() {
for (text, want) in [
("127.1", Some([127, 0, 0, 1])),
("1.2.3.4", Some([1, 2, 3, 4])),
("16909060", Some([1, 2, 3, 4])),
("0x7f.1", Some([127, 0, 0, 1])),
("0177.0.0.1", Some([127, 0, 0, 1])),
("1.2.3.4.5", None),
("256.1.1.1", None),
("1.2.3.4.", None),
("", None),
] {
assert_eq!(aton(text.as_bytes()), want, "inet_aton({text:?})");
}
assert_eq!(ntoa([1, 2, 3, 4]), "1.2.3.4");
}
#[test]
fn ntop_v6_round_trips_and_compresses_the_longest_run() {
for (packed, want) in [
("00000000000000000000000000000000", "::"),
("00000000000000000000000000000001", "::1"),
("20010db8000000000000000000000001", "2001:db8::1"),
("20010000000100000000000200030004", "2001:0:1::2:3:4"),
("00000000000100000000000200030004", "::1:0:0:2:3:4"),
("000000000000000000000000c0000201", "::c000:201"),
("00000000000000000000ffffc0000201", "::ffff:192.0.2.1"),
("fe800000000000000000000000000001", "fe80::1"),
] {
let bytes: Vec<u8> = (0..16)
.map(|i| u8::from_str_radix(&packed[2 * i..2 * i + 2], 16).unwrap())
.collect();
let text = ntop_v6(&bytes);
assert_eq!(text, want, "inet_ntop({packed})");
assert_eq!(
pton_v6(text.as_bytes())
.as_ref()
.map(|address| &address[..]),
Some(&bytes[..]),
"round trip of {text}"
);
}
}
}