irontide_session/i2p/
destination.rs1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct I2pDestination {
18 #[serde(with = "serde_bytes")]
20 bytes: Vec<u8>,
21}
22
23const I2P_BASE64_CHARS: &[u8; 64] =
25 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
26
27impl I2pDestination {
28 #[must_use]
30 pub fn from_bytes(bytes: Vec<u8>) -> Self {
31 Self { bytes }
32 }
33
34 #[must_use]
36 pub fn as_bytes(&self) -> &[u8] {
37 &self.bytes
38 }
39
40 #[must_use]
42 pub fn to_base64(&self) -> String {
43 i2p_base64_encode(&self.bytes)
44 }
45
46 pub fn from_base64(s: &str) -> Result<Self, I2pDestinationError> {
52 let bytes = i2p_base64_decode(s)?;
53 if bytes.is_empty() {
54 return Err(I2pDestinationError::Empty);
55 }
56 Ok(Self { bytes })
57 }
58
59 #[must_use]
64 pub fn to_b32_address(&self) -> String {
65 let hash = irontide_core::sha256(&self.bytes);
66 let mut out = String::with_capacity(52);
67 base32_encode_lower(hash.as_bytes(), &mut out);
68 format!("{out}.b32.i2p")
69 }
70
71 #[must_use]
73 pub fn len(&self) -> usize {
74 self.bytes.len()
75 }
76
77 #[must_use]
79 pub fn is_empty(&self) -> bool {
80 self.bytes.is_empty()
81 }
82}
83
84impl fmt::Debug for I2pDestination {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 let b64 = self.to_base64();
87 if b64.len() > 16 {
88 write!(
89 f,
90 "I2pDestination({}...{} bytes)",
91 &b64[..16],
92 self.bytes.len()
93 )
94 } else {
95 write!(f, "I2pDestination({b64})")
96 }
97 }
98}
99
100impl fmt::Display for I2pDestination {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}", self.to_base64())
103 }
104}
105
106#[derive(Debug, Clone, thiserror::Error)]
108pub enum I2pDestinationError {
109 #[error("invalid Base64 character at position {0}")]
111 InvalidBase64(
112 usize,
114 ),
115 #[error("empty destination")]
117 Empty,
118}
119
120pub(crate) fn i2p_base64_encode(data: &[u8]) -> String {
124 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
125
126 for chunk in data.chunks(3) {
127 let b0 = u32::from(chunk[0]);
128 let b1 = if chunk.len() > 1 {
129 u32::from(chunk[1])
130 } else {
131 0
132 };
133 let b2 = if chunk.len() > 2 {
134 u32::from(chunk[2])
135 } else {
136 0
137 };
138 let triple = (b0 << 16) | (b1 << 8) | b2;
139
140 result.push(I2P_BASE64_CHARS[((triple >> 18) & 0x3F) as usize] as char);
141 result.push(I2P_BASE64_CHARS[((triple >> 12) & 0x3F) as usize] as char);
142
143 if chunk.len() > 1 {
144 result.push(I2P_BASE64_CHARS[((triple >> 6) & 0x3F) as usize] as char);
145 } else {
146 result.push('=');
147 }
148
149 if chunk.len() > 2 {
150 result.push(I2P_BASE64_CHARS[(triple & 0x3F) as usize] as char);
151 } else {
152 result.push('=');
153 }
154 }
155
156 result
157}
158
159#[allow(
161 clippy::many_single_char_names,
162 reason = "base64 decoding variables a/b/c/d follow the standard naming convention"
163)]
164pub(crate) fn i2p_base64_decode(s: &str) -> Result<Vec<u8>, I2pDestinationError> {
165 fn char_to_val(c: u8, pos: usize) -> Result<u32, I2pDestinationError> {
166 match c {
167 b'A'..=b'Z' => Ok(u32::from(c - b'A')),
168 b'a'..=b'z' => Ok(u32::from(c - b'a' + 26)),
169 b'0'..=b'9' => Ok(u32::from(c - b'0' + 52)),
170 b'-' => Ok(62),
171 b'~' => Ok(63),
172 b'=' => Ok(0), _ => Err(I2pDestinationError::InvalidBase64(pos)),
174 }
175 }
176
177 let bytes = s.as_bytes();
178 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
179
180 for (chunk_idx, chunk) in bytes.chunks(4).enumerate() {
181 if chunk.len() < 4 {
182 if !chunk.is_empty() {
184 return Err(I2pDestinationError::InvalidBase64(chunk_idx * 4));
185 }
186 break;
187 }
188
189 let base = chunk_idx * 4;
190 let a = char_to_val(chunk[0], base)?;
191 let b = char_to_val(chunk[1], base + 1)?;
192 let c = char_to_val(chunk[2], base + 2)?;
193 let d = char_to_val(chunk[3], base + 3)?;
194
195 let triple = (a << 18) | (b << 12) | (c << 6) | d;
196
197 result.push(((triple >> 16) & 0xFF) as u8);
198 if chunk[2] != b'=' {
199 result.push(((triple >> 8) & 0xFF) as u8);
200 }
201 if chunk[3] != b'=' {
202 result.push((triple & 0xFF) as u8);
203 }
204 }
205
206 Ok(result)
207}
208
209fn base32_encode_lower(data: &[u8], out: &mut String) {
211 const ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
212 let mut bits: u64 = 0;
213 let mut num_bits: u32 = 0;
214
215 for &byte in data {
216 bits = (bits << 8) | u64::from(byte);
217 num_bits += 8;
218 while num_bits >= 5 {
219 num_bits -= 5;
220 out.push(ALPHABET[((bits >> num_bits) & 0x1F) as usize] as char);
221 }
222 }
223
224 if num_bits > 0 {
225 out.push(ALPHABET[((bits << (5 - num_bits)) & 0x1F) as usize] as char);
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn i2p_base64_roundtrip() {
235 let data = vec![0u8; 516]; let encoded = i2p_base64_encode(&data);
237 let decoded = i2p_base64_decode(&encoded).unwrap();
238 assert_eq!(decoded, data);
239 }
240
241 #[test]
242 fn i2p_base64_alphabet_differs_from_standard() {
243 let data = vec![0xFF, 0xFE, 0xFD]; let encoded = i2p_base64_encode(&data);
246 assert!(!encoded.contains('+'));
247 assert!(!encoded.contains('/'));
248 }
249
250 #[test]
251 fn i2p_base64_decode_invalid_char() {
252 let err = i2p_base64_decode("AAAA+AAA").unwrap_err();
253 assert!(matches!(err, I2pDestinationError::InvalidBase64(_)));
254 }
255
256 #[test]
257 fn i2p_base64_known_vector() {
258 let data = b"hello";
261 let encoded = i2p_base64_encode(data);
262 assert_eq!(encoded, "aGVsbG8=");
263 let decoded = i2p_base64_decode(&encoded).unwrap();
264 assert_eq!(decoded, data);
265 }
266
267 #[test]
268 fn destination_from_base64_roundtrip() {
269 let raw = vec![42u8; 516];
270 let dest = I2pDestination::from_bytes(raw.clone());
271 let b64 = dest.to_base64();
272 let parsed = I2pDestination::from_base64(&b64).unwrap();
273 assert_eq!(parsed.as_bytes(), raw.as_slice());
274 assert_eq!(parsed, dest);
275 }
276
277 #[test]
278 fn destination_from_base64_empty_rejected() {
279 let err = I2pDestination::from_base64("").unwrap_err();
280 assert!(matches!(err, I2pDestinationError::Empty));
281 }
282
283 #[test]
284 fn destination_debug_truncated() {
285 let dest = I2pDestination::from_bytes(vec![0; 516]);
286 let dbg = format!("{dest:?}");
287 assert!(dbg.contains("I2pDestination("));
288 assert!(dbg.contains("..."));
289 assert!(dbg.contains("516 bytes"));
290 }
291
292 #[test]
293 fn destination_display_is_base64() {
294 let dest = I2pDestination::from_bytes(vec![1, 2, 3]);
295 let display = format!("{dest}");
296 let base64 = dest.to_base64();
297 assert_eq!(display, base64);
298 }
299
300 #[test]
301 fn destination_b32_address() {
302 let dest = I2pDestination::from_bytes(vec![0u8; 516]);
303 let b32 = dest.to_b32_address();
304 assert!(b32.ends_with(".b32.i2p"));
305 let host = b32.strip_suffix(".b32.i2p").unwrap();
307 assert_eq!(host.len(), 52);
308 }
309
310 #[test]
311 fn destination_hash_and_eq() {
312 use std::collections::HashSet;
313 let a = I2pDestination::from_bytes(vec![1, 2, 3]);
314 let b = I2pDestination::from_bytes(vec![1, 2, 3]);
315 let c = I2pDestination::from_bytes(vec![4, 5, 6]);
316 assert_eq!(a, b);
317 assert_ne!(a, c);
318
319 let mut set = HashSet::new();
320 set.insert(a);
321 set.insert(b); set.insert(c);
323 assert_eq!(set.len(), 2);
324 }
325
326 #[test]
327 fn destination_serde_roundtrip() {
328 let dest = I2pDestination::from_bytes(vec![7u8; 100]);
329 let json = serde_json::to_string(&dest).unwrap();
330 let parsed: I2pDestination = serde_json::from_str(&json).unwrap();
331 assert_eq!(parsed, dest);
332 }
333
334 #[test]
335 fn base32_encode_known_vector() {
336 let hash = irontide_core::sha256(&[0u8; 32]);
338 let mut out = String::new();
339 base32_encode_lower(hash.as_bytes(), &mut out);
340 assert_eq!(out.len(), 52); assert!(
343 out.chars()
344 .all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c))
345 );
346 }
347}