sorug 0.1.0

Ultra-high-performance, zero-copy, WHATWG-compliant URL parser
Documentation
//! Host parsing: domain (IDNA), opaque host, IPv4, IPv6.

use std::borrow::Cow;
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};

use super::percent::{in_c0_encode_set, percent_decode, utf8_percent_encode};
use super::punycode;
use crate::ParseError;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Host<'a> {
    Domain(Cow<'a, str>),
    Ipv4(Ipv4Addr),
    Ipv6(Ipv6Addr),
}

impl fmt::Display for Host<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Domain(d) => f.write_str(d),
            Self::Ipv4(a) => write!(f, "{a}"),
            Self::Ipv6(a) => {
                f.write_str("[")?;
                write_ipv6(a, f)?;
                f.write_str("]")
            }
        }
    }
}

/// Parse a special-URL host (domain / IPv4 / IPv6).
pub(crate) fn parse_host(input: &str) -> Result<Host<'_>, ParseError> {
    if let Some(inner) = input.strip_prefix('[') {
        let Some(ipv6) = inner.strip_suffix(']') else {
            return Err(ParseError::Failure);
        };
        return parse_ipv6(ipv6).map(Host::Ipv6);
    }

    let decoded_cow = percent_decode(input.as_bytes());
    match decoded_cow {
        Cow::Borrowed(bytes) => {
            let decoded = std::str::from_utf8(bytes).map_err(|_| ParseError::Failure)?;
            domain_or_ipv4(decoded)
        }
        Cow::Owned(bytes) => {
            let decoded = std::str::from_utf8(&bytes).map_err(|_| ParseError::Failure)?;
            Ok(match domain_or_ipv4(decoded)? {
                Host::Domain(d) => Host::Domain(Cow::Owned(d.into_owned())),
                Host::Ipv4(a) => Host::Ipv4(a),
                Host::Ipv6(a) => Host::Ipv6(a),
            })
        }
    }
}

fn domain_or_ipv4(decoded: &str) -> Result<Host<'_>, ParseError> {
    let ascii = punycode::to_ascii(decoded).map_err(|_| ParseError::Failure)?;
    if ascii.is_empty() {
        return Err(ParseError::Failure);
    }
    if ends_in_a_number(&ascii) {
        Ok(Host::Ipv4(parse_ipv4(&ascii)?))
    } else {
        Ok(Host::Domain(ascii))
    }
}

/// Parse a non-special (opaque) host.
pub(crate) fn parse_opaque_host(input: &str) -> Result<Host<'static>, ParseError> {
    if let Some(inner) = input.strip_prefix('[') {
        let Some(ipv6) = inner.strip_suffix(']') else {
            return Err(ParseError::Failure);
        };
        return parse_ipv6(ipv6).map(Host::Ipv6);
    }

    if input.bytes().any(|c| is_forbidden_host_code_point(c)) {
        return Err(ParseError::Failure);
    }

    let mut out = String::new();
    utf8_percent_encode(input, in_c0_encode_set, &mut out);
    Ok(Host::Domain(Cow::Owned(out)))
}

#[inline]
fn is_forbidden_host_code_point(c: u8) -> bool {
    matches!(
        c,
        0x00 | b'\t'
            | b'\n'
            | b'\r'
            | b' '
            | b'#'
            | b'/'
            | b':'
            | b'<'
            | b'>'
            | b'?'
            | b'@'
            | b'['
            | b'\\'
            | b']'
            | b'^'
            | b'|'
    )
}

pub(crate) fn ends_in_a_number(input: &str) -> bool {
    let mut parts = input.rsplit('.');
    let last = parts.next().unwrap_or("");
    let last = if last.is_empty() {
        parts.next().unwrap_or("")
    } else {
        last
    };
    if last.is_empty() {
        return false;
    }
    if last.bytes().all(|c| c.is_ascii_digit()) {
        return true;
    }
    parse_ipv4_number(last).is_ok()
}

/// Ok(None) means valid syntax but u32 overflow.
fn parse_ipv4_number(mut input: &str) -> Result<Option<u32>, ()> {
    if input.is_empty() {
        return Err(());
    }
    let mut radix = 10u32;
    if input.starts_with("0x") || input.starts_with("0X") {
        input = &input[2..];
        radix = 16;
    } else if input.len() >= 2 && input.starts_with('0') {
        input = &input[1..];
        radix = 8;
    }
    if input.is_empty() {
        return Ok(Some(0));
    }
    let valid = match radix {
        8 => input.bytes().all(|c| (b'0'..=b'7').contains(&c)),
        10 => input.bytes().all(|c| c.is_ascii_digit()),
        16 => input.bytes().all(|c| c.is_ascii_hexdigit()),
        _ => false,
    };
    if !valid {
        return Err(());
    }
    match u32::from_str_radix(input, radix) {
        Ok(n) => Ok(Some(n)),
        Err(_) => Ok(None),
    }
}

fn parse_ipv4(input: &str) -> Result<Ipv4Addr, ParseError> {
    let mut parts: Vec<&str> = input.split('.').collect();
    if parts.last() == Some(&"") {
        parts.pop();
    }
    if parts.len() > 4 {
        return Err(ParseError::Failure);
    }
    let mut numbers = Vec::with_capacity(parts.len());
    for part in parts {
        match parse_ipv4_number(part) {
            Ok(Some(n)) => numbers.push(n),
            Ok(None) | Err(()) => return Err(ParseError::Failure),
        }
    }
    if numbers.is_empty() {
        return Err(ParseError::Failure);
    }
    let mut ipv4 = numbers.pop().unwrap();
    if ipv4 > u32::MAX >> (8 * numbers.len() as u32) {
        return Err(ParseError::Failure);
    }
    if numbers.iter().any(|&x| x > 255) {
        return Err(ParseError::Failure);
    }
    for (counter, n) in numbers.iter().enumerate() {
        ipv4 += n << (8 * (3 - counter as u32));
    }
    Ok(Ipv4Addr::from(ipv4))
}

fn parse_ipv6(input: &str) -> Result<Ipv6Addr, ParseError> {
    let input = input.as_bytes();
    let len = input.len();
    let mut is_ip_v4 = false;
    let mut pieces = [0u16; 8];
    let mut piece_pointer = 0usize;
    let mut compress_pointer = None;
    let mut i = 0usize;

    if len < 2 {
        return Err(ParseError::Failure);
    }

    if input[0] == b':' {
        if input[1] != b':' {
            return Err(ParseError::Failure);
        }
        i = 2;
        piece_pointer = 1;
        compress_pointer = Some(1);
    }

    while i < len {
        if piece_pointer == 8 {
            return Err(ParseError::Failure);
        }
        if input[i] == b':' {
            if compress_pointer.is_some() {
                return Err(ParseError::Failure);
            }
            i += 1;
            piece_pointer += 1;
            compress_pointer = Some(piece_pointer);
            continue;
        }
        let start = i;
        let end = core::cmp::min(len, start + 4);
        let mut value = 0u16;
        while i < end {
            match (input[i] as char).to_digit(16) {
                Some(digit) => {
                    value = value * 0x10 + digit as u16;
                    i += 1;
                }
                None => break,
            }
        }
        if i < len {
            match input[i] {
                b'.' => {
                    if i == start {
                        return Err(ParseError::Failure);
                    }
                    i = start;
                    if piece_pointer > 6 {
                        return Err(ParseError::Failure);
                    }
                    is_ip_v4 = true;
                }
                b':' => {
                    i += 1;
                    if i == len {
                        return Err(ParseError::Failure);
                    }
                }
                _ => return Err(ParseError::Failure),
            }
        }
        if is_ip_v4 {
            break;
        }
        pieces[piece_pointer] = value;
        piece_pointer += 1;
    }

    if is_ip_v4 {
        if piece_pointer > 6 {
            return Err(ParseError::Failure);
        }
        let mut numbers_seen = 0;
        while i < len {
            if numbers_seen > 0 {
                if numbers_seen < 4 && i < len && input[i] == b'.' {
                    i += 1;
                } else {
                    return Err(ParseError::Failure);
                }
            }
            let mut ipv4_piece = None;
            while i < len {
                let digit = match input[i] {
                    c @ b'0'..=b'9' => c - b'0',
                    _ => break,
                };
                match ipv4_piece {
                    None => ipv4_piece = Some(u16::from(digit)),
                    Some(0) => return Err(ParseError::Failure),
                    Some(ref mut v) => {
                        *v = *v * 10 + u16::from(digit);
                        if *v > 255 {
                            return Err(ParseError::Failure);
                        }
                    }
                }
                i += 1;
            }
            pieces[piece_pointer] = if let Some(v) = ipv4_piece {
                pieces[piece_pointer] * 0x100 + v
            } else {
                return Err(ParseError::Failure);
            };
            numbers_seen += 1;
            if numbers_seen == 2 || numbers_seen == 4 {
                piece_pointer += 1;
            }
        }
        if numbers_seen != 4 {
            return Err(ParseError::Failure);
        }
    }

    if i < len {
        return Err(ParseError::Failure);
    }

    match compress_pointer {
        Some(compress_pointer) => {
            let mut swaps = piece_pointer - compress_pointer;
            piece_pointer = 7;
            while swaps > 0 {
                pieces.swap(piece_pointer, compress_pointer + swaps - 1);
                swaps -= 1;
                piece_pointer -= 1;
            }
        }
        None => {
            if piece_pointer != 8 {
                return Err(ParseError::Failure);
            }
        }
    }

    Ok(Ipv6Addr::new(
        pieces[0], pieces[1], pieces[2], pieces[3], pieces[4], pieces[5], pieces[6], pieces[7],
    ))
}

fn write_ipv6(addr: &Ipv6Addr, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let segments = addr.segments();
    let (compress_start, compress_end) = longest_zero_sequence(&segments);
    let mut i = 0isize;
    while i < 8 {
        if i == compress_start {
            f.write_str(":")?;
            if i == 0 {
                f.write_str(":")?;
            }
            if compress_end < 8 {
                i = compress_end;
            } else {
                break;
            }
        }
        write!(f, "{:x}", segments[i as usize])?;
        if i < 7 {
            f.write_str(":")?;
        }
        i += 1;
    }
    Ok(())
}

fn longest_zero_sequence(pieces: &[u16; 8]) -> (isize, isize) {
    let mut longest = -1;
    let mut longest_length = -1;
    let mut start = -1isize;
    macro_rules! finish_sequence {
        ($end:expr) => {{
            if start >= 0 {
                let length = $end - start;
                if length > longest_length {
                    longest = start;
                    longest_length = length;
                }
            }
        }};
    }
    for i in 0..8isize {
        if pieces[i as usize] == 0 {
            if start < 0 {
                start = i;
            }
        } else {
            finish_sequence!(i);
            start = -1;
        }
    }
    finish_sequence!(8);
    if longest_length < 2 {
        (-1, -2)
    } else {
        (longest, longest + longest_length)
    }
}

#[allow(dead_code)]
pub(crate) fn host_to_cow<'a>(host: &'a Host<'_>) -> Cow<'a, str> {
    match host {
        Host::Domain(d) => Cow::Borrowed(d.as_ref()),
        other => Cow::Owned(other.to_string()),
    }
}