1use std::collections::BTreeMap;
2
3use thiserror::Error;
4
5use crate::encoding::{Decodable, DecodeError, Encodable};
6use crate::module::registry::ModuleDecoderRegistry;
7use crate::util::FmtCompact as _;
8
9const RFC4648: [u8; 32] = *b"0123456789abcdefghijklmnopqrstuv";
11
12pub const FEDIMINT_PREFIX: &str = "fedimint";
15
16pub fn encode(input: &[u8]) -> String {
18 let mut output = Vec::with_capacity(((8 * input.len()) / 5) + 1);
19
20 let mut buffer = 0;
21 let mut bits = 0;
22
23 for byte in input {
24 buffer |= (*byte as usize) << bits;
25 bits += 8;
26
27 while bits >= 5 {
28 output.push(RFC4648[buffer & 0b11111]);
29
30 buffer >>= 5;
31 bits -= 5;
32 }
33 }
34
35 if bits > 0 {
36 output.push(RFC4648[buffer & 0b11111]);
37 }
38
39 String::from_utf8(output).unwrap()
40}
41
42pub fn decode(input: &str) -> Result<Vec<u8>, Base32DecodeError> {
45 let decode_table = RFC4648
46 .iter()
47 .enumerate()
48 .map(|(i, c)| (*c, i))
49 .collect::<BTreeMap<u8, usize>>();
50
51 let mut output = Vec::with_capacity(((5 * input.len()) / 8) + 1);
52
53 let mut buffer = 0;
54 let mut bits = 0;
55
56 for (index, ch) in input.char_indices() {
57 let value = ch
58 .is_ascii()
59 .then(|| decode_table.get(&(ch as u8)).copied())
60 .flatten()
61 .ok_or(Base32DecodeError::InvalidCharacter { ch, index })?;
62
63 buffer |= value << bits;
64 bits += 5;
65
66 while bits >= 8 {
67 output.push((buffer & 0xFF) as u8);
68
69 buffer >>= 8;
70 bits -= 8;
71 }
72 }
73
74 Ok(output)
75}
76
77pub fn encode_prefixed<T: Encodable>(prefix: &str, encodable: &T) -> String {
78 encode_prefixed_bytes(prefix, &encodable.consensus_encode_to_vec())
79}
80
81pub fn encode_prefixed_bytes(prefix: &str, bytes: &[u8]) -> String {
82 format!("{prefix}{}", encode(bytes))
83}
84
85pub fn decode_prefixed<T: Decodable>(prefix: &str, s: &str) -> Result<T, PrefixedDecodeError> {
86 Ok(T::consensus_decode_whole(
87 &decode_prefixed_bytes(prefix, s)?,
88 &ModuleDecoderRegistry::default(),
89 )?)
90}
91
92pub fn decode_prefixed_bytes(prefix: &str, s: &str) -> Result<Vec<u8>, PrefixedDecodeError> {
93 let s = s.to_lowercase();
94 if !s.starts_with(prefix) {
95 return Err(PrefixedDecodeError::InvalidPrefix {
96 expected: prefix.to_owned(),
97 });
98 }
99 Ok(decode(&s[prefix.len()..])?)
100}
101
102#[derive(Debug, Error, Clone, Eq, PartialEq)]
104#[non_exhaustive]
105pub enum Base32DecodeError {
106 #[error("Invalid base32 character {ch:?} at byte index {index}")]
113 InvalidCharacter { ch: char, index: usize },
114}
115
116#[derive(Debug, Error)]
122#[non_exhaustive]
123pub enum PrefixedDecodeError {
124 #[error("Invalid prefix, expected '{expected}'")]
126 InvalidPrefix { expected: String },
127 #[error("Invalid base32 payload: {0}")]
129 Base32Decode(Base32DecodeError),
130 #[error("Invalid consensus encoding in base32 payload: {}", .0.fmt_compact())]
132 ConsensusDecode(DecodeError),
133}
134
135impl From<DecodeError> for PrefixedDecodeError {
136 fn from(source: DecodeError) -> Self {
137 Self::ConsensusDecode(source)
138 }
139}
140
141impl From<Base32DecodeError> for PrefixedDecodeError {
142 fn from(source: Base32DecodeError) -> Self {
143 Self::Base32Decode(source)
144 }
145}
146
147#[test]
148fn test_base_32_roundtrip() {
149 const TEST_PREFIX: &str = "test";
150 let data: [u8; 10] = [0x50, 0xAB, 0x3F, 0x77, 0x01, 0xCD, 0x55, 0xFE, 0x10, 0x99];
151
152 for n in 1..10 {
153 let bytes = data[0..n].to_vec();
154
155 assert_eq!(decode(&encode(&bytes)).unwrap(), bytes);
156
157 assert_eq!(
158 decode_prefixed::<Vec<u8>>(TEST_PREFIX, &encode_prefixed(TEST_PREFIX, &bytes)).unwrap(),
159 bytes
160 );
161
162 assert_eq!(
163 decode_prefixed::<Vec<u8>>(
164 TEST_PREFIX,
165 &encode_prefixed(TEST_PREFIX, &bytes).to_ascii_uppercase()
166 )
167 .unwrap(),
168 bytes
169 );
170 }
171}
172
173#[test]
174fn decode_reports_invalid_character_position() {
175 assert_eq!(
176 decode("ab!c"),
177 Err(Base32DecodeError::InvalidCharacter { ch: '!', index: 2 })
178 );
179}
180
181#[test]
182fn decode_escapes_invalid_character_in_message() {
183 let err = decode("a\nb").expect_err("a newline is not in the base32 alphabet");
184
185 assert_eq!(
186 err,
187 Base32DecodeError::InvalidCharacter { ch: '\n', index: 1 }
188 );
189 assert_eq!(
190 err.to_string(),
191 "Invalid base32 character '\\n' at byte index 1"
192 );
193}
194
195#[test]
196fn decode_prefixed_bytes_rejects_wrong_prefix() {
197 assert!(matches!(
198 decode_prefixed_bytes("fed", "xyz00"),
199 Err(PrefixedDecodeError::InvalidPrefix { expected }) if expected == "fed"
200 ));
201}
202
203#[test]
204fn decode_prefixed_reports_the_whole_decode_chain() {
205 use std::str::FromStr;
206
207 use crate::encoding::Encodable;
208 use crate::invite_code::InviteCode;
209
210 let invite_code_str = "fed11qgqpu8rhwden5te0vejkg6tdd9h8gepwd4cxcumxv4jzuen0duhsqqfqh6nl7sgk72caxfx8khtfnn8y436q3nhyrkev3qp8ugdhdllnh86qmp42pm";
214 let invite = InviteCode::from_str(invite_code_str).expect("valid invite code");
215 let bytes = invite.consensus_encode_to_vec();
216 let encoded = encode_prefixed_bytes(FEDIMINT_PREFIX, &bytes[..bytes.len() - 1]);
217
218 let err =
219 decode_prefixed::<InviteCode>(FEDIMINT_PREFIX, &encoded).expect_err("payload is truncated");
220 let text = err.to_string();
221 let err_fmt_compact = err.fmt_compact().to_string();
222 let PrefixedDecodeError::ConsensusDecode(inner) = err else {
223 panic!("a truncated payload is a decode error: {err:?}");
224 };
225
226 assert_eq!(
227 text,
228 format!(
229 "Invalid consensus encoding in base32 payload: {}",
230 inner.fmt_compact()
231 )
232 );
233 assert_ne!(inner.fmt_compact().to_string(), inner.to_string());
234 assert_eq!(
235 err_fmt_compact, text,
236 "no source, so nothing is printed twice"
237 );
238}