From 7aff2e1d279a4e442b32f49ca0a0eca065355787 Mon Sep 17 00:00:00 2001 From: murilo ijanc Date: Wed, 25 Mar 2026 02:07:37 -0300 Subject: Initial commit: tesseras-paste decentralized pastebin DHT-backed encrypted pastebin with two binaries (tp/tpd), XChaCha20-Poly1305 encryption, content-addressed storage, and Unix socket + HTTP interfaces. --- src/base58.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/base58.rs (limited to 'src/base58.rs') diff --git a/src/base58.rs b/src/base58.rs new file mode 100644 index 0000000..c412bc3 --- /dev/null +++ b/src/base58.rs @@ -0,0 +1,153 @@ +//! Bitcoin-style Base58 encoding/decoding. +//! +//! No external dependencies. Uses the standard Bitcoin +//! alphabet (no 0, O, I, l to avoid ambiguity). + +const ALPHABET: &[u8; 58] = + b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +/// Decode table: ASCII byte → base58 value (255 = invalid). +const DECODE: [u8; 128] = { + let mut table = [255u8; 128]; + let mut i = 0; + while i < 58 { + table[ALPHABET[i] as usize] = i as u8; + i += 1; + } + table +}; + +/// Encode bytes to Base58 string. +pub fn encode(input: &[u8]) -> String { + if input.is_empty() { + return String::new(); + } + + // Count leading zeros + let leading_zeros = input.iter().take_while(|&&b| b == 0).count(); + + // Convert to base58 via repeated division + let mut digits: Vec = Vec::with_capacity(input.len() * 2); + for &byte in input { + let mut carry = byte as u32; + for d in &mut digits { + carry += (*d as u32) << 8; + *d = (carry % 58) as u8; + carry /= 58; + } + while carry > 0 { + digits.push((carry % 58) as u8); + carry /= 58; + } + } + + let mut result = String::with_capacity(leading_zeros + digits.len()); + for _ in 0..leading_zeros { + result.push('1'); + } + for &d in digits.iter().rev() { + result.push(ALPHABET[d as usize] as char); + } + result +} + +/// Decode a Base58 string to bytes. +pub fn decode(input: &str) -> Option> { + if input.is_empty() { + return Some(Vec::new()); + } + + // Count leading '1's (representing zero bytes) + let leading_ones = input.chars().take_while(|&c| c == '1').count(); + + // Convert from base58 via repeated multiplication + let mut bytes: Vec = Vec::with_capacity(input.len()); + for c in input.chars() { + let c = c as usize; + if c >= 128 { + return None; + } + let val = DECODE[c]; + if val == 255 { + return None; + } + let mut carry = val as u32; + for b in &mut bytes { + carry += (*b as u32) * 58; + *b = (carry & 0xFF) as u8; + carry >>= 8; + } + while carry > 0 { + bytes.push((carry & 0xFF) as u8); + carry >>= 8; + } + } + + let mut result = vec![0; leading_ones]; + result.extend(bytes.iter().rev()); + Some(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encode_empty() { + assert_eq!(encode(b""), ""); + } + + #[test] + fn encode_hello() { + assert_eq!(encode(b"Hello World"), "JxF12TrwUP45BMd"); + } + + #[test] + fn roundtrip() { + let data = b"tesseras-paste test data"; + let encoded = encode(data); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn roundtrip_binary() { + let data: Vec = (0..=255).collect(); + let encoded = encode(&data); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn leading_zeros() { + let data = [0, 0, 0, 1, 2, 3]; + let encoded = encode(&data); + assert!(encoded.starts_with("111")); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn decode_invalid_char() { + assert!(decode("invalid0char").is_none()); + } + + #[test] + fn decode_empty() { + assert_eq!(decode("").unwrap(), Vec::::new()); + } + + #[test] + fn known_vector() { + // SHA-256 of "test" in base58 + let hash: [u8; 32] = { + use tesseras_dht::sha2::{Digest, Sha256}; + let mut h = Sha256::new(); + h.update(b"test"); + h.finalize().into() + }; + let encoded = encode(&hash); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded, hash); + } +} -- cgit v1.2.3