1#![no_std]
8#![deny(missing_docs)]
9#![deny(unsafe_code)]
10#![deny(clippy::all)]
11#![deny(clippy::pedantic)]
12#![deny(clippy::nursery)]
13
14extern crate alloc;
15
16pub mod version;
18
19use alloc::{format, string::String, vec::Vec};
20use sha2::{Digest, Sha256};
21use subtle::ConstantTimeEq;
22use thiserror::Error;
23
24#[derive(Error, Debug, Clone, PartialEq, Eq)]
26pub enum DecodeError {
27 #[error("invalid base58: {0}")]
32 InvalidBase58(String),
33 #[error("decoded data too short")]
36 TooShort,
37 #[error("invalid checksum")]
39 InvalidChecksum,
40 #[error("invalid version byte: expected {expected:#04x}, found {found:#04x}")]
42 InvalidVersion {
43 expected: u8,
45 found: u8,
47 },
48}
49
50#[must_use]
52pub(crate) fn checksum(data: &[u8]) -> [u8; 4] {
53 let hash = Sha256::digest(&Sha256::digest(data)[..]);
54 let mut out = [0u8; 4];
55 out.copy_from_slice(&hash[..4]);
56 out
57}
58
59fn checksum_verify(got: [u8; 4], expected: [u8; 4]) -> bool {
64 got.ct_eq(&expected).into()
65}
66
67#[must_use]
72pub fn encode(version: u8, payload: &[u8]) -> String {
73 let mut buf = Vec::with_capacity(1 + payload.len() + 4);
74 buf.push(version);
75 buf.extend_from_slice(payload);
76 let cs = checksum(&buf);
77 buf.extend_from_slice(&cs);
78 bs58::encode(buf).into_string()
79}
80
81#[must_use = "decoding result must be used"]
88pub fn decode(b58: &str) -> Result<(u8, Vec<u8>), DecodeError> {
89 let mut raw = decode_raw(b58)?;
90 let version = raw[0];
91 raw.drain(..1);
92 Ok((version, raw))
93}
94
95#[must_use = "decoding result must be used"]
102pub fn decode_version(b58: &str, expected: u8) -> Result<Vec<u8>, DecodeError> {
103 let (version, payload) = decode(b58)?;
104 if version != expected {
105 return Err(DecodeError::InvalidVersion {
106 expected,
107 found: version,
108 });
109 }
110 Ok(payload)
111}
112
113#[must_use]
116pub fn encode_raw(raw: &[u8]) -> String {
117 let cs = checksum(raw);
118 let mut buf = Vec::with_capacity(raw.len() + 4);
119 buf.extend_from_slice(raw);
120 buf.extend_from_slice(&cs);
121 bs58::encode(buf).into_string()
122}
123
124#[must_use = "decoding result must be used"]
132pub fn decode_raw(b58: &str) -> Result<Vec<u8>, DecodeError> {
133 let mut bytes = bs58::decode(b58)
134 .into_vec()
135 .map_err(|e| DecodeError::InvalidBase58(format!("{e}")))?;
136 if bytes.len() < 5 {
137 return Err(DecodeError::TooShort);
138 }
139 let data_len = bytes.len() - 4;
140 let got = [
141 bytes[data_len],
142 bytes[data_len + 1],
143 bytes[data_len + 2],
144 bytes[data_len + 3],
145 ];
146 if !checksum_verify(got, checksum(&bytes[..data_len])) {
147 return Err(DecodeError::InvalidChecksum);
148 }
149 bytes.truncate(data_len);
150 Ok(bytes)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
162 fn test_checksum_is_deterministic() {
163 let data = b"hello world";
164 assert_eq!(checksum(data), checksum(data));
165 }
166
167 #[test]
168 fn test_checksum_differs_for_different_data() {
169 assert_ne!(checksum(b"aaa"), checksum(b"bbb"));
170 }
171
172 #[test]
173 fn test_checksum_is_four_bytes() {
174 let cs = checksum(b"any data");
175 assert_eq!(cs.len(), 4);
176 }
177
178 #[test]
179 fn test_checksum_verify_equal() {
180 assert!(checksum_verify(
181 [0xAA, 0xBB, 0xCC, 0xDD],
182 [0xAA, 0xBB, 0xCC, 0xDD]
183 ));
184 }
185
186 #[test]
187 fn test_checksum_verify_rejects_each_byte() {
188 let expected = [0xAA, 0xBB, 0xCC, 0xDD];
189 for i in 0..4 {
190 let mut bad = expected;
191 bad[i] ^= 0x01;
192 assert!(
193 !checksum_verify(bad, expected),
194 "byte {i} flip not detected"
195 );
196 }
197 }
198}