lt_rs/
info_hash.rs

1use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
2
3use crate::ffi::ffi;
4
5pub struct Sha1Hash([u8; 20]);
6
7impl From<[u8; 20]> for Sha1Hash {
8    fn from(value: [u8; 20]) -> Self {
9        Sha1Hash(value)
10    }
11}
12
13#[derive(Debug, Clone, Copy)]
14pub enum InfoHash {
15    V1([u8; 20]),
16    V2([u8; 32]),
17}
18
19impl InfoHash {
20    pub fn as_base64(&self) -> String {
21        match self {
22            InfoHash::V1(v1) => BASE64_URL_SAFE_NO_PAD.encode(v1),
23            InfoHash::V2(v2) => BASE64_URL_SAFE_NO_PAD.encode(v2),
24        }
25    }
26
27    // Convert hex string to byte array
28    fn from_hex<const N: usize>(hex: &str) -> Result<[u8; N], ()> {
29        if hex.len() != N * 2 {
30            return Err(());
31        }
32        let mut bytes = [0u8; N];
33        for i in 0..N {
34            let hi = hex_char_to_val(hex.as_bytes()[i * 2])?;
35            let lo = hex_char_to_val(hex.as_bytes()[i * 2 + 1])?;
36            bytes[i] = (hi << 4) | lo;
37        }
38        Ok(bytes)
39    }
40
41    // Decode base32 (RFC4648, no padding)
42    fn from_base32(input: &str) -> Result<[u8; 20], ()> {
43        let mut bits = 0u64;
44        let mut bit_count = 0;
45        let mut out = [0u8; 20];
46        let mut byte_index = 0;
47
48        for c in input.chars() {
49            let val = base32_char_to_val(c)?;
50            bits = (bits << 5) | (val as u64);
51            bit_count += 5;
52
53            while bit_count >= 8 {
54                bit_count -= 8;
55                if byte_index >= 20 {
56                    return Err(()); // too long
57                }
58                out[byte_index] = ((bits >> bit_count) & 0xFF) as u8;
59                byte_index += 1;
60            }
61        }
62
63        if byte_index != 20 {
64            return Err(()); // too short
65        }
66
67        Ok(out)
68    }
69
70    pub fn from_magnet(magnet_uri: &str) -> Result<Self, ()> {
71        let xt_start = magnet_uri.find("xt=urn:").ok_or(())?;
72        let xt = &magnet_uri[xt_start + 7..];
73        let end = xt.find('&').unwrap_or(xt.len());
74        let urn = &xt[..end];
75
76        if let Some(rest) = urn.strip_prefix("btih:") {
77            if rest.len() == 40 {
78                let bytes = Self::from_hex::<20>(rest)?;
79                return Ok(InfoHash::V1(bytes));
80            } else if rest.len() == 32 {
81                let bytes = Self::from_base32(rest)?;
82                return Ok(InfoHash::V1(bytes));
83            } else {
84                return Err(());
85            }
86        } else if let Some(rest) = urn.strip_prefix("btmh:") {
87            if let Some(rest) = rest.strip_prefix("1220") {
88                let bytes = Self::from_hex::<32>(rest)?;
89                return Ok(InfoHash::V2(bytes));
90            } else {
91                return Err(());
92            }
93        }
94
95        Err(())
96    }
97}
98
99// Convert single hex char to value
100fn hex_char_to_val(c: u8) -> Result<u8, ()> {
101    match c {
102        b'0'..=b'9' => Ok(c - b'0'),
103        b'a'..=b'f' => Ok(c - b'a' + 10),
104        b'A'..=b'F' => Ok(c - b'A' + 10),
105        _ => Err(()),
106    }
107}
108
109// Convert single base32 char to value
110fn base32_char_to_val(c: char) -> Result<u8, ()> {
111    match c {
112        'A'..='Z' => Ok((c as u8) - b'A'),
113        '2'..='7' => Ok((c as u8) - b'2' + 26),
114        _ => Err(()),
115    }
116}
117
118impl std::hash::Hash for InfoHash {
119    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
120        match self {
121            InfoHash::V1(v1) => v1.hash(state),
122            InfoHash::V2(v2) => v2.hash(state),
123        }
124    }
125}
126
127impl PartialEq for InfoHash {
128    fn eq(&self, other: &Self) -> bool {
129        match (self, other) {
130            (InfoHash::V1(lhs), InfoHash::V1(rhs)) => lhs == rhs,
131            (InfoHash::V2(lhs), InfoHash::V2(rhs)) => lhs == rhs,
132            _ => false,
133        }
134    }
135}
136
137impl Eq for InfoHash {}
138
139impl From<ffi::InfoHashCpp> for InfoHash {
140    fn from(value: ffi::InfoHashCpp) -> Self {
141        match value.version {
142            1 => InfoHash::V1(value.inner[..20].try_into().unwrap()),
143            2 => InfoHash::V2(value.inner),
144            _ => unreachable!(),
145        }
146    }
147}