Skip to main content

fedimint_core/
base32.rs

1use std::collections::BTreeMap;
2
3use thiserror::Error;
4
5use crate::encoding::{Decodable, DecodeError, Encodable};
6use crate::module::registry::ModuleDecoderRegistry;
7
8/// Lowercase RFC 4648 Base32hex alphabet (32 characters).
9const RFC4648: [u8; 32] = *b"0123456789abcdefghijklmnopqrstuv";
10
11/// Prefix used for some of the user-facing Base32 encodings in Fedimint to
12/// allow easy identification
13pub const FEDIMINT_PREFIX: &str = "fedimint";
14
15/// Encodes the input bytes as Base32 (hex variant) using lowercase characters
16pub 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
41/// Decodes a base 32 string back to raw bytes. Returns an error
42/// if any invalid character is encountered.
43pub 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/// Failure to decode a raw base 32 string.
102#[derive(Debug, Error, Clone, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum Base32DecodeError {
105    /// A character outside the RFC 4648 base32hex alphabet was encountered.
106    ///
107    /// `index` is a byte offset into the string passed to [`decode`]. When the
108    /// error originates in [`decode_prefixed`] or [`decode_prefixed_bytes`],
109    /// that string is the payload after the prefix was stripped and the input
110    /// was lowercased, not the string the caller supplied.
111    #[error("Invalid base32 character {ch:?} at byte index {index}")]
112    InvalidCharacter { ch: char, index: usize },
113}
114
115/// Failure to decode a prefixed base 32 string into a value.
116///
117/// The byte offset in a [`Base32DecodeError::InvalidCharacter`] source refers
118/// to the payload after the prefix was stripped and the input was lowercased,
119/// not to the string the caller supplied.
120#[derive(Debug, Error)]
121#[non_exhaustive]
122pub enum PrefixedDecodeError {
123    /// The string does not start with the expected prefix.
124    #[error("Invalid prefix, expected '{expected}'")]
125    InvalidPrefix { expected: String },
126    /// The payload is not valid base 32.
127    #[error("Invalid base32 payload: {0}")]
128    Base32Decode(#[from] Base32DecodeError),
129    /// The decoded bytes are not a valid consensus encoding of the target type.
130    #[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}