Skip to main content

rs_matter/utils/codec/
base38.rs

1/*
2 *
3 *    Copyright (c) 2023-2025 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! Base38 encoding and decoding functions.
19
20use crate::error::{Error, ErrorCode};
21
22const BASE38_CHARS: [char; 38] = [
23    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
24    'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.',
25];
26
27const UNUSED: u8 = 255;
28
29// map of base38 charater to numeric value
30// subtract 45 from the character, then index into this array, if possible
31const DECODE_BASE38: [u8; 46] = [
32    36,     // '-', =45
33    37,     // '.', =46
34    UNUSED, // '/', =47
35    0,      // '0', =48
36    1,      // '1', =49
37    2,      // '2', =50
38    3,      // '3', =51
39    4,      // '4', =52
40    5,      // '5', =53
41    6,      // '6', =54
42    7,      // '7', =55
43    8,      // '8', =56
44    9,      // '9', =57
45    UNUSED, // ':', =58
46    UNUSED, // ';', =59
47    UNUSED, // '<', =50
48    UNUSED, // '=', =61
49    UNUSED, // '>', =62
50    UNUSED, // '?', =63
51    UNUSED, // '@', =64
52    10,     // 'A', =65
53    11,     // 'B', =66
54    12,     // 'C', =67
55    13,     // 'D', =68
56    14,     // 'E', =69
57    15,     // 'F', =70
58    16,     // 'G', =71
59    17,     // 'H', =72
60    18,     // 'I', =73
61    19,     // 'J', =74
62    20,     // 'K', =75
63    21,     // 'L', =76
64    22,     // 'M', =77
65    23,     // 'N', =78
66    24,     // 'O', =79
67    25,     // 'P', =80
68    26,     // 'Q', =81
69    27,     // 'R', =82
70    28,     // 'S', =83
71    29,     // 'T', =84
72    30,     // 'U', =85
73    31,     // 'V', =86
74    32,     // 'W', =87
75    33,     // 'X', =88
76    34,     // 'Y', =89
77    35,     // 'Z', =90
78];
79
80const RADIX: u32 = BASE38_CHARS.len() as u32;
81
82/// Encode a byte array into a base38 string.
83///
84/// # Arguments
85/// * `bytes` - byte array to encode
86pub fn encode_string<const N: usize>(bytes: &[u8]) -> Result<heapless::String<N>, Error> {
87    let mut string = heapless::String::new();
88    for c in encode(bytes) {
89        string.push(c).map_err(|_| ErrorCode::BufferTooSmall)?;
90    }
91
92    Ok(string)
93}
94
95pub fn encode(bytes: &[u8]) -> impl Iterator<Item = char> + '_ {
96    (0..bytes.len() / 3)
97        .flat_map(move |index| {
98            let offset = index * 3;
99
100            encode_base38(
101                ((bytes[offset + 2] as u32) << 16)
102                    | ((bytes[offset + 1] as u32) << 8)
103                    | (bytes[offset] as u32),
104                5,
105            )
106        })
107        .chain(
108            core::iter::once(bytes.len() % 3).flat_map(move |remainder| {
109                let offset = bytes.len() / 3 * 3;
110
111                match remainder {
112                    2 => encode_base38(
113                        ((bytes[offset + 1] as u32) << 8) | (bytes[offset] as u32),
114                        4,
115                    ),
116                    1 => encode_base38(bytes[offset] as u32, 2),
117                    _ => encode_base38(0, 0),
118                }
119            }),
120        )
121}
122
123pub fn encode_bits(bits: u32, bits_count: u8) -> impl Iterator<Item = char> {
124    assert!(bits_count <= 24);
125
126    let repeat = match bits_count / 8 {
127        3 => 5,
128        2 => 4,
129        1 => 2,
130        _ => unreachable!(),
131    };
132
133    encode_base38(bits, repeat)
134}
135
136fn encode_base38(mut value: u32, repeat: usize) -> impl Iterator<Item = char> {
137    (0..repeat).map(move |_| {
138        let remainder = value % RADIX;
139        let c = BASE38_CHARS[remainder as usize];
140
141        value = (value - remainder) / RADIX;
142
143        c
144    })
145}
146
147pub fn decode_vec<const N: usize>(base38_str: &str) -> Result<heapless::Vec<u8, N>, Error> {
148    let mut vec = heapless::Vec::new();
149
150    for byte in decode(base38_str) {
151        vec.push(byte?).map_err(|_| ErrorCode::BufferTooSmall)?;
152    }
153
154    Ok(vec)
155}
156
157/// Decode a base38-encoded string into a byte slice
158///
159/// # Arguments
160/// * `base38_str` - base38-encoded string to decode
161///
162/// Fails if the string contains invalid characters or if the supplied buffer is too small to fit the decoded data
163pub fn decode(base38_str: &str) -> impl Iterator<Item = Result<u8, Error>> + '_ {
164    let stru = base38_str.as_bytes();
165
166    (0..stru.len() / 5)
167        .flat_map(move |index| {
168            let offset = index * 5;
169            decode_base38(&stru[offset..offset + 5])
170        })
171        .chain({
172            let offset = stru.len() / 5 * 5;
173            decode_base38(&stru[offset..])
174        })
175        .take_while(Result::is_ok)
176}
177
178fn decode_base38(chars: &[u8]) -> impl Iterator<Item = Result<u8, Error>> {
179    let mut value = 0u32;
180    let mut cerr = None;
181
182    let repeat = match chars.len() {
183        5 => 3,
184        4 => 2,
185        2 => 1,
186        0 => 0,
187        _ => -1,
188    };
189
190    if repeat >= 0 {
191        for c in chars.iter().rev() {
192            match decode_char(*c) {
193                Ok(v) => value = value * RADIX + v as u32,
194                Err(err) => {
195                    cerr = Some(err.code());
196                    break;
197                }
198            }
199        }
200    } else {
201        cerr = Some(ErrorCode::InvalidData)
202    }
203
204    (0..repeat)
205        .map(move |_| {
206            if let Some(err) = cerr {
207                Err(err.into())
208            } else {
209                let byte = (value & 0xff) as u8;
210
211                value >>= 8;
212
213                Ok(byte)
214            }
215        })
216        .take_while(Result::is_ok)
217}
218
219fn decode_char(c: u8) -> Result<u8, Error> {
220    if !(45..=90).contains(&c) {
221        Err(ErrorCode::InvalidData)?;
222    }
223
224    let c = DECODE_BASE38[c as usize - 45];
225    if c == UNUSED {
226        Err(ErrorCode::InvalidData)?;
227    }
228
229    Ok(c)
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    const ENCODED: &str = "-MOA57ZU02IT2L2BJ00";
236    const DECODED: [u8; 11] = [
237        0x88, 0xff, 0xa7, 0x91, 0x50, 0x40, 0x00, 0x47, 0x51, 0xdd, 0x02,
238    ];
239
240    #[test]
241    fn can_base38_encode() {
242        assert_eq!(
243            unwrap!(encode_string::<{ ENCODED.len() }>(&DECODED)),
244            ENCODED
245        );
246    }
247
248    #[test]
249    fn can_base38_decode() {
250        assert_eq!(
251            unwrap!(
252                decode_vec::<{ DECODED.len() }>(ENCODED),
253                "Cannot decode base38"
254            ),
255            DECODED
256        );
257    }
258}