Skip to main content

reifydb_type/util/
base58.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{error, fmt, iter};
5const BASE58_CHARS: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
6
7pub fn encode(input: &[u8]) -> String {
8	if input.is_empty() {
9		return String::new();
10	}
11
12	let leading_zeros = input.iter().take_while(|&&b| b == 0).count();
13
14	let mut bytes = input.to_vec();
15	let mut result = Vec::new();
16
17	while !bytes.iter().all(|&b| b == 0) {
18		let mut remainder = 0u32;
19		for byte in bytes.iter_mut() {
20			let value = (remainder << 8) | (*byte as u32);
21			*byte = (value / 58) as u8;
22			remainder = value % 58;
23		}
24		result.push(BASE58_CHARS[remainder as usize]);
25	}
26
27	result.extend(iter::repeat_n(b'1', leading_zeros));
28
29	result.reverse();
30	String::from_utf8(result).unwrap()
31}
32
33pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
34	if input.is_empty() {
35		return Ok(Vec::new());
36	}
37
38	let leading_ones = input.chars().take_while(|&c| c == '1').count();
39
40	let mut bytes: Vec<u8> = Vec::new();
41
42	for ch in input.chars() {
43		let value = char_to_value(ch)?;
44
45		let mut carry = value as u32;
46		for byte in bytes.iter_mut().rev() {
47			let val = (*byte as u32) * 58 + carry;
48			*byte = (val & 0xFF) as u8;
49			carry = val >> 8;
50		}
51
52		while carry > 0 {
53			bytes.insert(0, (carry & 0xFF) as u8);
54			carry >>= 8;
55		}
56	}
57
58	let mut result = vec![0u8; leading_ones];
59	result.extend(bytes);
60
61	Ok(result)
62}
63
64fn char_to_value(ch: char) -> Result<u8, DecodeError> {
65	let byte = ch as u8;
66	BASE58_CHARS.iter().position(|&b| b == byte).map(|pos| pos as u8).ok_or(DecodeError::InvalidCharacter(ch))
67}
68
69#[derive(Debug)]
70pub enum DecodeError {
71	InvalidCharacter(char),
72}
73
74impl fmt::Display for DecodeError {
75	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76		match self {
77			DecodeError::InvalidCharacter(ch) => {
78				write!(f, "Invalid base58 character: '{}'", ch)
79			}
80		}
81	}
82}
83
84impl error::Error for DecodeError {}
85
86#[cfg(test)]
87pub mod tests {
88	use super::*;
89
90	#[test]
91	fn test_encode_empty() {
92		assert_eq!(encode(b""), "");
93	}
94
95	#[test]
96	fn test_encode_hello() {
97		// "Hello" -> "9Ajdvzr"
98		assert_eq!(encode(b"Hello"), "9Ajdvzr");
99	}
100
101	#[test]
102	fn test_encode_hello_world() {
103		// "Hello, World!" -> "72k1xXWG59fYdzSNoA"
104		assert_eq!(encode(b"Hello, World!"), "72k1xXWG59fYdzSNoA");
105	}
106
107	#[test]
108	fn test_encode_leading_zeros() {
109		// Leading zero bytes become leading '1's
110		assert_eq!(encode(&[0, 0, 1]), "112");
111		assert_eq!(encode(&[0, 0, 0]), "111");
112	}
113
114	#[test]
115	fn test_decode_empty() {
116		assert_eq!(decode("").unwrap(), b"");
117	}
118
119	#[test]
120	fn test_decode_hello() {
121		assert_eq!(decode("9Ajdvzr").unwrap(), b"Hello");
122	}
123
124	#[test]
125	fn test_decode_hello_world() {
126		assert_eq!(decode("72k1xXWG59fYdzSNoA").unwrap(), b"Hello, World!");
127	}
128
129	#[test]
130	fn test_decode_leading_ones() {
131		assert_eq!(decode("112").unwrap(), &[0, 0, 1]);
132		assert_eq!(decode("111").unwrap(), &[0, 0, 0]);
133	}
134
135	#[test]
136	fn test_roundtrip() {
137		let data = b"Hello, World! \x00\x01\x02\xFF";
138		let encoded = encode(data);
139		let decoded = decode(&encoded).unwrap();
140		assert_eq!(decoded, data);
141	}
142
143	#[test]
144	fn test_roundtrip_binary() {
145		let data = &[0xde, 0xad, 0xbe, 0xef];
146		let encoded = encode(data);
147		let decoded = decode(&encoded).unwrap();
148		assert_eq!(decoded, data);
149	}
150
151	#[test]
152	fn test_invalid_character() {
153		// '0', 'O', 'I', 'l' are not in base58 alphabet
154		assert!(decode("0").is_err());
155		assert!(decode("O").is_err());
156		assert!(decode("I").is_err());
157		assert!(decode("l").is_err());
158		assert!(decode("invalid!").is_err());
159	}
160}