1use std::collections::BTreeMap;
2
3use thiserror::Error;
4
5use crate::encoding::{Decodable, DecodeError, Encodable};
6use crate::module::registry::ModuleDecoderRegistry;
7
8const RFC4648: [u8; 32] = *b"0123456789abcdefghijklmnopqrstuv";
10
11pub const FEDIMINT_PREFIX: &str = "fedimint";
14
15pub fn encode(input: &[u8]) -> String {
17 let mut output = Vec::with_capacity(((8 * input.len()) / 5) + 1);
18
19 let mut buffer = 0;
20 let mut bits = 0;
21
22 for byte in input {
23 buffer |= (*byte as usize) << bits;
24 bits += 8;
25
26 while bits >= 5 {
27 output.push(RFC4648[buffer & 0b11111]);
28
29 buffer >>= 5;
30 bits -= 5;
31 }
32 }
33
34 if bits > 0 {
35 output.push(RFC4648[buffer & 0b11111]);
36 }
37
38 String::from_utf8(output).unwrap()
39}
40
41pub fn decode(input: &str) -> Result<Vec<u8>, Base32DecodeError> {
44 let decode_table = RFC4648
45 .iter()
46 .enumerate()
47 .map(|(i, c)| (*c, i))
48 .collect::<BTreeMap<u8, usize>>();
49
50 let mut output = Vec::with_capacity(((5 * input.len()) / 8) + 1);
51
52 let mut buffer = 0;
53 let mut bits = 0;
54
55 for (index, ch) in input.char_indices() {
56 let value = ch
57 .is_ascii()
58 .then(|| decode_table.get(&(ch as u8)).copied())
59 .flatten()
60 .ok_or(Base32DecodeError::InvalidCharacter { ch, index })?;
61
62 buffer |= value << bits;
63 bits += 5;
64
65 while bits >= 8 {
66 output.push((buffer & 0xFF) as u8);
67
68 buffer >>= 8;
69 bits -= 8;
70 }
71 }
72
73 Ok(output)
74}
75
76pub fn encode_prefixed<T: Encodable>(prefix: &str, encodable: &T) -> String {
77 encode_prefixed_bytes(prefix, &encodable.consensus_encode_to_vec())
78}
79
80pub fn encode_prefixed_bytes(prefix: &str, bytes: &[u8]) -> String {
81 format!("{prefix}{}", encode(bytes))
82}
83
84pub fn decode_prefixed<T: Decodable>(prefix: &str, s: &str) -> Result<T, PrefixedDecodeError> {
85 Ok(T::consensus_decode_whole(
86 &decode_prefixed_bytes(prefix, s)?,
87 &ModuleDecoderRegistry::default(),
88 )?)
89}
90
91pub fn decode_prefixed_bytes(prefix: &str, s: &str) -> Result<Vec<u8>, PrefixedDecodeError> {
92 let s = s.to_lowercase();
93 if !s.starts_with(prefix) {
94 return Err(PrefixedDecodeError::InvalidPrefix {
95 expected: prefix.to_owned(),
96 });
97 }
98 Ok(decode(&s[prefix.len()..])?)
99}
100
101#[derive(Debug, Error, Clone, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum Base32DecodeError {
105 #[error("Invalid base32 character {ch:?} at byte index {index}")]
112 InvalidCharacter { ch: char, index: usize },
113}
114
115#[derive(Debug, Error)]
121#[non_exhaustive]
122pub enum PrefixedDecodeError {
123 #[error("Invalid prefix, expected '{expected}'")]
125 InvalidPrefix { expected: String },
126 #[error("Invalid base32 payload: {0}")]
128 Base32Decode(#[from] Base32DecodeError),
129 #[error("Invalid consensus encoding in base32 payload: {0}")]
131 ConsensusDecode(#[from] DecodeError),
132}
133
134#[test]
135fn test_base_32_roundtrip() {
136 const TEST_PREFIX: &str = "test";
137 let data: [u8; 10] = [0x50, 0xAB, 0x3F, 0x77, 0x01, 0xCD, 0x55, 0xFE, 0x10, 0x99];
138
139 for n in 1..10 {
140 let bytes = data[0..n].to_vec();
141
142 assert_eq!(decode(&encode(&bytes)).unwrap(), bytes);
143
144 assert_eq!(
145 decode_prefixed::<Vec<u8>>(TEST_PREFIX, &encode_prefixed(TEST_PREFIX, &bytes)).unwrap(),
146 bytes
147 );
148
149 assert_eq!(
150 decode_prefixed::<Vec<u8>>(
151 TEST_PREFIX,
152 &encode_prefixed(TEST_PREFIX, &bytes).to_ascii_uppercase()
153 )
154 .unwrap(),
155 bytes
156 );
157 }
158}
159
160#[test]
161fn decode_reports_invalid_character_position() {
162 assert_eq!(
163 decode("ab!c"),
164 Err(Base32DecodeError::InvalidCharacter { ch: '!', index: 2 })
165 );
166}
167
168#[test]
169fn decode_escapes_invalid_character_in_message() {
170 let err = decode("a\nb").expect_err("a newline is not in the base32 alphabet");
171
172 assert_eq!(
173 err,
174 Base32DecodeError::InvalidCharacter { ch: '\n', index: 1 }
175 );
176 assert_eq!(
177 err.to_string(),
178 "Invalid base32 character '\\n' at byte index 1"
179 );
180}
181
182#[test]
183fn decode_prefixed_bytes_rejects_wrong_prefix() {
184 assert!(matches!(
185 decode_prefixed_bytes("fed", "xyz00"),
186 Err(PrefixedDecodeError::InvalidPrefix { expected }) if expected == "fed"
187 ));
188}