Skip to main content

fastbase91_core/
decode.rs

1use core::fmt;
2
3use crate::tables::DECODE_TABLE;
4use crate::OutputTooSmall;
5
6/// Decoder policy options.
7#[non_exhaustive]
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct DecodeOptions {
10    /// Whether bytes outside the basE91 alphabet should be rejected.
11    pub reject_non_alphabet: bool,
12}
13
14impl DecodeOptions {
15    /// Creates lenient options that ignore bytes outside the basE91 alphabet.
16    pub const fn new() -> Self {
17        Self {
18            reject_non_alphabet: false,
19        }
20    }
21}
22
23impl Default for DecodeOptions {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29/// Error returned by the decoding APIs.
30#[non_exhaustive]
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum DecodeError {
33    /// The caller-provided output storage cannot hold the result.
34    OutputTooSmall(OutputTooSmall),
35    /// A strict decode encountered a byte outside the alphabet.
36    InvalidByte { byte: u8, offset: usize },
37    /// The allocator could not reserve the requested storage.
38    #[cfg(feature = "alloc")]
39    AllocationFailed,
40}
41
42impl fmt::Display for DecodeError {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::OutputTooSmall(error) => error.fmt(formatter),
46            Self::InvalidByte { byte, offset } => {
47                write!(formatter, "invalid byte 0x{byte:02x} at offset {offset}")
48            }
49            #[cfg(feature = "alloc")]
50            Self::AllocationFailed => formatter.write_str("decoding allocation failed"),
51        }
52    }
53}
54
55impl core::error::Error for DecodeError {}
56
57/// Returns a decoded-length upper bound for `input_len` bytes.
58///
59/// The bound holds for every reachable decoder state and every input, and
60/// includes the possible one-byte tail returned by [`Decoder::finish`]. In the
61/// worst case, an existing pending symbol makes the number of symbols at most
62/// `input_len + 1`. The resulting bound is monotone in that symbol count, so it
63/// completes `ceil(input_len / 2)` pairs of at most 14 bits and has a tail
64/// exactly when `input_len` is even. The quotient and remainder calculation
65/// avoids forming `14 * input_len`.
66///
67/// `None` means that the mathematical bound does not fit in `usize`. A `Some`
68/// result can still exceed `isize::MAX`, so allocating APIs perform that
69/// separate platform-capacity check.
70pub const fn max_decoded_len(input_len: usize) -> Option<usize> {
71    let pairs = input_len / 2 + input_len % 2;
72    let prefix = match (pairs / 4).checked_mul(7) {
73        Some(value) => value,
74        None => return None,
75    };
76    let remaining_bytes = ((pairs % 4) * 14).div_ceil(8);
77    let tail = if input_len % 2 == 0 { 1 } else { 0 };
78    match prefix.checked_add(remaining_bytes) {
79        Some(value) => value.checked_add(tail),
80        None => None,
81    }
82}
83
84/// Stateful standard basE91 decoder.
85///
86/// A call to [`finish`](Self::finish) ends the current message and consumes the
87/// decoder, so the same stream cannot be continued afterwards. The consuming
88/// contract is enforced by the type system: the decoder is not `Copy`, so
89/// reusing it after `finish` fails to compile.
90///
91/// ```compile_fail
92/// use fastbase91_core::{DecodeOptions, Decoder};
93/// let mut decoder = Decoder::new(DecodeOptions::new());
94/// let mut output = [0_u8; 4];
95/// let _tail = decoder.finish();
96/// // `finish` consumed the decoder; decoding more must not compile.
97/// let _ = decoder.update(b"AA", &mut output);
98/// ```
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct Decoder {
101    options: DecodeOptions,
102    pending: Option<u8>,
103    queue: u32,
104    nbits: u8,
105}
106
107impl Decoder {
108    /// Creates a decoder with the requested policy.
109    pub const fn new(options: DecodeOptions) -> Self {
110        Self {
111            options,
112            pending: None,
113            queue: 0,
114            nbits: 0,
115        }
116    }
117
118    /// Decodes a chunk into caller-provided storage.
119    ///
120    /// Capacity is checked first using a content-independent upper bound; on
121    /// [`OutputTooSmall`](DecodeError::OutputTooSmall) neither the decoder state
122    /// nor the output is changed. In strict mode the first byte outside the
123    /// alphabet stops decoding with [`InvalidByte`](DecodeError::InvalidByte),
124    /// whose `offset` is the 0-based index of that byte within this `input`; the
125    /// decoder state is left unchanged, but bytes already written to `output`
126    /// are unspecified and must be discarded.
127    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
128        let required = self.update_capacity(input.len());
129        if output.len() < required {
130            return Err(DecodeError::OutputTooSmall(OutputTooSmall::new(required)));
131        }
132
133        if self.options.reject_non_alphabet {
134            self.update_strict(input, output)
135        } else {
136            Ok(self.update_lenient(input, output))
137        }
138    }
139
140    // Fast path: lenient never needs the byte offset, so drop the enumerate/early-exit
141    // that constrains the loop. u32 sentinel (u32::MAX = no pending) + u32 nbits in
142    // locals; the Decoder struct stays Option<u8>/u8 (O(1) convert in/out per call).
143    fn update_lenient(&mut self, input: &[u8], output: &mut [u8]) -> usize {
144        let mut queue = self.queue;
145        let mut nbits = u32::from(self.nbits);
146        let mut val = match self.pending {
147            Some(v) => u32::from(v),
148            None => u32::MAX,
149        };
150        let mut written = 0;
151        for &byte in input {
152            let d = u32::from(DECODE_TABLE[usize::from(byte)]);
153            if d == 91 {
154                continue;
155            }
156            if val == u32::MAX {
157                val = d;
158            } else {
159                let combined = val + d * 91;
160                val = u32::MAX;
161                queue |= combined << nbits;
162                nbits += if combined & 8191 > 88 { 13 } else { 14 };
163                // pair adds 13-14 bits => nbits >= 13: 1 output byte, maybe a 2nd.
164                output[written] = queue as u8;
165                written += 1;
166                queue >>= 8;
167                nbits -= 8;
168                if nbits >= 8 {
169                    output[written] = queue as u8;
170                    written += 1;
171                    queue >>= 8;
172                    nbits -= 8;
173                }
174            }
175        }
176        self.queue = queue;
177        self.nbits = nbits as u8;
178        self.pending = if val == u32::MAX {
179            None
180        } else {
181            Some(val as u8)
182        };
183        written
184    }
185
186    // Strict path: identical behaviour to the old strict branch (offset + InvalidByte
187    // + transactional state). Since reject_non_alphabet is known true here, any
188    // non-alphabet byte errors (the old `continue` was unreachable in strict mode).
189    fn update_strict(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
190        let mut working = self.clone();
191        let mut written = 0;
192        for (offset, &byte) in input.iter().enumerate() {
193            let value = DECODE_TABLE[usize::from(byte)];
194            if value == 91 {
195                return Err(DecodeError::InvalidByte { byte, offset });
196            }
197            if let Some(first) = working.pending.take() {
198                let combined = u32::from(first) + u32::from(value) * 91;
199                working.queue |= combined << working.nbits;
200                working.nbits += if combined & 8191 > 88 { 13 } else { 14 };
201                while working.nbits > 7 {
202                    output[written] = working.queue as u8;
203                    written += 1;
204                    working.queue >>= 8;
205                    working.nbits -= 8;
206                }
207            } else {
208                working.pending = Some(value);
209            }
210        }
211        *self = working;
212        Ok(written)
213    }
214
215    /// Returns the final decoded byte, if an unpaired symbol remains.
216    pub fn finish(self) -> Result<Option<u8>, DecodeError> {
217        Ok(self
218            .pending
219            .map(|first| (self.queue | (u32::from(first) << self.nbits)) as u8))
220    }
221
222    pub(crate) fn decode_into(
223        mut self,
224        input: &[u8],
225        output: &mut [u8],
226    ) -> Result<usize, DecodeError> {
227        let required = one_shot_capacity(input.len());
228        if output.len() < required {
229            return Err(DecodeError::OutputTooSmall(OutputTooSmall::new(required)));
230        }
231
232        let written = self.update(input, output)?;
233        if let Some(byte) = self.finish()? {
234            output[written] = byte;
235            Ok(written + 1)
236        } else {
237            Ok(written)
238        }
239    }
240
241    fn update_capacity(&self, input_len: usize) -> usize {
242        let pairs = input_len / 2
243            + if self.pending.is_some() {
244                input_len % 2
245            } else {
246                0
247            };
248        let prefix = (pairs / 4).saturating_mul(7);
249        let remaining_bits = usize::from(self.nbits) + (pairs % 4) * 14;
250        prefix.saturating_add(remaining_bits / 8)
251    }
252}
253
254const fn one_shot_capacity(input_len: usize) -> usize {
255    let pairs = input_len / 2;
256    let prefix = (pairs / 4).saturating_mul(7);
257    let remaining_bytes = ((pairs % 4) * 14) / 8;
258    prefix
259        .saturating_add(remaining_bytes)
260        .saturating_add(input_len % 2)
261}
262
263#[cfg(feature = "alloc")]
264pub fn decode(input: &[u8], options: DecodeOptions) -> Result<alloc::vec::Vec<u8>, DecodeError> {
265    let capacity = max_decoded_len(input.len()).ok_or(DecodeError::AllocationFailed)?;
266    if capacity > isize::MAX as usize {
267        return Err(DecodeError::AllocationFailed);
268    }
269
270    let mut output = alloc::vec::Vec::new();
271    output
272        .try_reserve_exact(capacity)
273        .map_err(|_| DecodeError::AllocationFailed)?;
274    output.resize(capacity, 0);
275    let written = Decoder::new(options).decode_into(input, &mut output)?;
276    output.truncate(written);
277    Ok(output)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn all_abstract_state_classes_are_reachable_and_shifts_fit_u32() {
286        let mut reachable = [[false; 8]; 2];
287        reachable[0][0] = true;
288
289        for _ in 0..32 {
290            let mut next = reachable;
291            for nbits in 0..=7 {
292                if reachable[0][nbits] {
293                    next[1][nbits] = true;
294                }
295                if reachable[1][nbits] {
296                    next[0][(nbits + 13) % 8] = true;
297                    next[0][(nbits + 14) % 8] = true;
298                }
299            }
300            reachable = next;
301        }
302
303        assert!(reachable.into_iter().flatten().all(|state| state));
304        for nbits in 0..=7 {
305            let shifted = 8280_u32 << nbits;
306            assert!(shifted < (1_u32 << 21));
307        }
308    }
309
310    #[test]
311    fn update_capacity_matches_wide_formula() {
312        for pending in [None, Some(0)] {
313            for nbits in 0..=7 {
314                let decoder = Decoder {
315                    options: DecodeOptions::new(),
316                    pending,
317                    queue: 0,
318                    nbits,
319                };
320                for input_len in [0, 1, 2, 7, 8, 9, usize::MAX] {
321                    let pairs = input_len as u128 / 2
322                        + u128::from(pending.is_some()) * (input_len as u128 % 2);
323                    let wide = (u128::from(nbits) + 14 * pairs) / 8;
324                    let expected = usize::try_from(wide).unwrap_or(usize::MAX);
325                    assert_eq!(decoder.update_capacity(input_len), expected);
326                }
327            }
328        }
329    }
330}