onionlink-core 0.1.1

Core Tor v3 onion-service client protocol implementation for onionlink
Documentation
use std::net::{Ipv4Addr, ToSocketAddrs};
use std::time::Duration;

use base64::engine::general_purpose::STANDARD;
use base64::Engine;

use crate::error::{ensure, err, Error, Result};

pub type Bytes = Vec<u8>;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostPort {
    pub host: String,
    pub port: u16,
}

pub fn split_ws(s: &str) -> Vec<&str> {
    s.split_whitespace().collect()
}

pub fn lower(s: impl AsRef<str>) -> String {
    s.as_ref().to_ascii_lowercase()
}

pub fn put_u16(b: &mut Bytes, v: u16) {
    b.extend_from_slice(&v.to_be_bytes());
}

pub fn put_u32(b: &mut Bytes, v: u32) {
    b.extend_from_slice(&v.to_be_bytes());
}

pub fn put_u64(b: &mut Bytes, v: u64) {
    b.extend_from_slice(&v.to_be_bytes());
}

pub fn read_u16(b: &[u8], off: usize) -> Result<u16> {
    ensure(off + 2 <= b.len(), "short u16")?;
    Ok(u16::from_be_bytes([b[off], b[off + 1]]))
}

pub fn read_u32(b: &[u8], off: usize) -> Result<u32> {
    ensure(off + 4 <= b.len(), "short u32")?;
    Ok(u32::from_be_bytes([
        b[off],
        b[off + 1],
        b[off + 2],
        b[off + 3],
    ]))
}

pub fn from_string(s: impl AsRef<str>) -> Bytes {
    s.as_ref().as_bytes().to_vec()
}

pub fn to_string_lossy(b: &[u8]) -> String {
    String::from_utf8_lossy(b).into_owned()
}

pub fn hex(b: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789ABCDEF";
    let mut out = String::with_capacity(b.len() * 2);
    for &c in b {
        out.push(HEX[(c >> 4) as usize] as char);
        out.push(HEX[(c & 0x0f) as usize] as char);
    }
    out
}

pub fn parse_hostport(s: &str, default_port: u16) -> Result<HostPort> {
    ensure(!s.is_empty(), "empty host")?;
    let (host, port) = if let Some(rest) = s.strip_prefix('[') {
        let close = rest
            .find(']')
            .ok_or_else(|| Error::new("bad IPv6 host:port"))?;
        let host = rest[..close].to_string();
        let after = &rest[close + 1..];
        let port = if after.is_empty() {
            default_port
        } else {
            ensure(after.starts_with(':'), "bad IPv6 port separator")?;
            after[1..].parse::<u16>()?
        };
        (host, port)
    } else if let Some(colon) = s.rfind(':') {
        if s[..colon].contains(':') {
            (s.to_string(), default_port)
        } else {
            (s[..colon].to_string(), s[colon + 1..].parse::<u16>()?)
        }
    } else {
        (s.to_string(), default_port)
    };
    ensure(!host.is_empty(), "missing host")?;
    ensure(port != 0, "missing port")?;
    Ok(HostPort { host, port })
}

pub fn base64_decode(s: &str) -> Result<Bytes> {
    let mut cleaned: String = s.chars().filter(|c| !c.is_ascii_whitespace()).collect();
    let padding = (4 - cleaned.len() % 4) % 4;
    for _ in 0..padding {
        cleaned.push('=');
    }
    Ok(STANDARD.decode(cleaned)?)
}

pub fn base64_encode_unpadded(b: &[u8]) -> String {
    let mut out = STANDARD.encode(b);
    while out.ends_with('=') {
        out.pop();
    }
    out
}

pub fn base32_decode_onion(s: &str) -> Result<Bytes> {
    let mut s = lower(s);
    if s.len() > 6 && s.ends_with(".onion") {
        s.truncate(s.len() - 6);
    }
    ensure(
        s.len() == 56,
        "v3 onion address must have 56 base32 characters",
    )?;
    let mut bits = 0;
    let mut acc = 0u32;
    let mut out = Bytes::new();
    for ch in s.bytes() {
        let v = match ch {
            b'a'..=b'z' => ch - b'a',
            b'2'..=b'7' => ch - b'2' + 26,
            _ => return err("invalid base32 character in onion address"),
        } as u32;
        acc = (acc << 5) | v;
        bits += 5;
        while bits >= 8 {
            bits -= 8;
            out.push(((acc >> bits) & 0xff) as u8);
        }
    }
    ensure(out.len() == 35, "invalid v3 onion address length")?;
    Ok(out)
}

pub fn duration_from_timeout_ms(timeout_ms: i32) -> Result<Duration> {
    ensure(timeout_ms >= 0, "timeout must be non-negative")?;
    Ok(Duration::from_millis(timeout_ms as u64))
}

pub fn resolve_socket_addrs(host: &str, port: u16) -> Result<Vec<std::net::SocketAddr>> {
    let addrs: Vec<_> = (host, port).to_socket_addrs()?.collect();
    ensure(
        !addrs.is_empty(),
        format!("getaddrinfo failed for {host}: no address found"),
    )?;
    Ok(addrs)
}

pub fn ipv4_to_link_bytes(ip: &str, port: u16) -> Result<Bytes> {
    let addr: Ipv4Addr = ip
        .parse()
        .map_err(|_| Error::new("relay has non-IPv4 address"))?;
    let mut out = addr.octets().to_vec();
    put_u16(&mut out, port);
    Ok(out)
}