Skip to main content

onionlink_core/
util.rs

1use std::net::{Ipv4Addr, ToSocketAddrs};
2use std::time::Duration;
3
4use base64::engine::general_purpose::STANDARD;
5use base64::Engine;
6
7use crate::error::{ensure, err, Error, Result};
8
9pub type Bytes = Vec<u8>;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct HostPort {
13    pub host: String,
14    pub port: u16,
15}
16
17pub fn split_ws(s: &str) -> Vec<&str> {
18    s.split_whitespace().collect()
19}
20
21pub fn lower(s: impl AsRef<str>) -> String {
22    s.as_ref().to_ascii_lowercase()
23}
24
25pub fn put_u16(b: &mut Bytes, v: u16) {
26    b.extend_from_slice(&v.to_be_bytes());
27}
28
29pub fn put_u32(b: &mut Bytes, v: u32) {
30    b.extend_from_slice(&v.to_be_bytes());
31}
32
33pub fn put_u64(b: &mut Bytes, v: u64) {
34    b.extend_from_slice(&v.to_be_bytes());
35}
36
37pub fn read_u16(b: &[u8], off: usize) -> Result<u16> {
38    ensure(off + 2 <= b.len(), "short u16")?;
39    Ok(u16::from_be_bytes([b[off], b[off + 1]]))
40}
41
42pub fn read_u32(b: &[u8], off: usize) -> Result<u32> {
43    ensure(off + 4 <= b.len(), "short u32")?;
44    Ok(u32::from_be_bytes([
45        b[off],
46        b[off + 1],
47        b[off + 2],
48        b[off + 3],
49    ]))
50}
51
52pub fn from_string(s: impl AsRef<str>) -> Bytes {
53    s.as_ref().as_bytes().to_vec()
54}
55
56pub fn to_string_lossy(b: &[u8]) -> String {
57    String::from_utf8_lossy(b).into_owned()
58}
59
60pub fn hex(b: &[u8]) -> String {
61    const HEX: &[u8; 16] = b"0123456789ABCDEF";
62    let mut out = String::with_capacity(b.len() * 2);
63    for &c in b {
64        out.push(HEX[(c >> 4) as usize] as char);
65        out.push(HEX[(c & 0x0f) as usize] as char);
66    }
67    out
68}
69
70pub fn parse_hostport(s: &str, default_port: u16) -> Result<HostPort> {
71    ensure(!s.is_empty(), "empty host")?;
72    let (host, port) = if let Some(rest) = s.strip_prefix('[') {
73        let close = rest
74            .find(']')
75            .ok_or_else(|| Error::new("bad IPv6 host:port"))?;
76        let host = rest[..close].to_string();
77        let after = &rest[close + 1..];
78        let port = if after.is_empty() {
79            default_port
80        } else {
81            ensure(after.starts_with(':'), "bad IPv6 port separator")?;
82            after[1..].parse::<u16>()?
83        };
84        (host, port)
85    } else if let Some(colon) = s.rfind(':') {
86        if s[..colon].contains(':') {
87            (s.to_string(), default_port)
88        } else {
89            (s[..colon].to_string(), s[colon + 1..].parse::<u16>()?)
90        }
91    } else {
92        (s.to_string(), default_port)
93    };
94    ensure(!host.is_empty(), "missing host")?;
95    ensure(port != 0, "missing port")?;
96    Ok(HostPort { host, port })
97}
98
99pub fn base64_decode(s: &str) -> Result<Bytes> {
100    let mut cleaned: String = s.chars().filter(|c| !c.is_ascii_whitespace()).collect();
101    let padding = (4 - cleaned.len() % 4) % 4;
102    for _ in 0..padding {
103        cleaned.push('=');
104    }
105    Ok(STANDARD.decode(cleaned)?)
106}
107
108pub fn base64_encode_unpadded(b: &[u8]) -> String {
109    let mut out = STANDARD.encode(b);
110    while out.ends_with('=') {
111        out.pop();
112    }
113    out
114}
115
116pub fn base32_decode_onion(s: &str) -> Result<Bytes> {
117    let mut s = lower(s);
118    if s.len() > 6 && s.ends_with(".onion") {
119        s.truncate(s.len() - 6);
120    }
121    ensure(
122        s.len() == 56,
123        "v3 onion address must have 56 base32 characters",
124    )?;
125    let mut bits = 0;
126    let mut acc = 0u32;
127    let mut out = Bytes::new();
128    for ch in s.bytes() {
129        let v = match ch {
130            b'a'..=b'z' => ch - b'a',
131            b'2'..=b'7' => ch - b'2' + 26,
132            _ => return err("invalid base32 character in onion address"),
133        } as u32;
134        acc = (acc << 5) | v;
135        bits += 5;
136        while bits >= 8 {
137            bits -= 8;
138            out.push(((acc >> bits) & 0xff) as u8);
139        }
140    }
141    ensure(out.len() == 35, "invalid v3 onion address length")?;
142    Ok(out)
143}
144
145pub fn duration_from_timeout_ms(timeout_ms: i32) -> Result<Duration> {
146    ensure(timeout_ms >= 0, "timeout must be non-negative")?;
147    Ok(Duration::from_millis(timeout_ms as u64))
148}
149
150pub fn resolve_socket_addrs(host: &str, port: u16) -> Result<Vec<std::net::SocketAddr>> {
151    let addrs: Vec<_> = (host, port).to_socket_addrs()?.collect();
152    ensure(
153        !addrs.is_empty(),
154        format!("getaddrinfo failed for {host}: no address found"),
155    )?;
156    Ok(addrs)
157}
158
159pub fn ipv4_to_link_bytes(ip: &str, port: u16) -> Result<Bytes> {
160    let addr: Ipv4Addr = ip
161        .parse()
162        .map_err(|_| Error::new("relay has non-IPv4 address"))?;
163    let mut out = addr.octets().to_vec();
164    put_u16(&mut out, port);
165    Ok(out)
166}