Skip to main content

fastbase91_core/
encode.rs

1use core::fmt;
2
3use crate::tables::{ALPHABET, ENCODE_PAIRS};
4
5/// Returns an encoded-length upper bound for `input_len` bytes.
6///
7/// The bound holds for every reachable encoder state and includes the up-to-two
8/// byte tail returned by [`Encoder::finish`]. Mathematically it is
9/// `2 * ceil((8 * input_len + 13) / 13)`. The quotient/remainder form below
10/// avoids overflowing while forming `8 * input_len`.
11///
12/// `None` means that the mathematical bound does not fit in `usize`. A `Some`
13/// result can still exceed `isize::MAX`, so allocating APIs perform that
14/// separate platform-capacity check.
15pub const fn max_encoded_len(input_len: usize) -> Option<usize> {
16    let quotient = input_len / 13;
17    let remainder = input_len % 13;
18    let prefix = match quotient.checked_mul(16) {
19        Some(value) => value,
20        None => return None,
21    };
22    let remaining_bits = remainder * 8 + 13;
23    let remaining_groups = remaining_bits.div_ceil(13);
24    prefix.checked_add(remaining_groups * 2)
25}
26
27/// Error returned by the fallible encoding APIs.
28#[non_exhaustive]
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum EncodeError {
31    /// The caller-provided output storage cannot hold the result.
32    OutputTooSmall(OutputTooSmall),
33    /// The requested output is not allocatable or allocation failed.
34    #[cfg(feature = "alloc")]
35    AllocationFailed,
36}
37
38impl fmt::Display for EncodeError {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::OutputTooSmall(error) => error.fmt(formatter),
42            #[cfg(feature = "alloc")]
43            Self::AllocationFailed => formatter.write_str("encoding allocation failed"),
44        }
45    }
46}
47
48impl core::error::Error for EncodeError {}
49
50/// Stateful standard basE91 encoder.
51///
52/// A call to [`finish`](Self::finish) ends the current message and consumes the
53/// encoder, so the same stream cannot be continued afterwards. Separately
54/// finished messages must not be concatenated and treated as one basE91 stream.
55///
56/// The consuming contract is enforced by the type system: the encoder is not
57/// `Copy`, so reusing it after `finish` fails to compile.
58///
59/// ```compile_fail
60/// use fastbase91_core::Encoder;
61/// let mut encoder = Encoder::new();
62/// let mut output = [0_u8; 4];
63/// let _tail = encoder.finish();
64/// // `finish` consumed the encoder; encoding more must not compile.
65/// let _ = encoder.update(b"x", &mut output);
66/// ```
67#[derive(Clone, Debug, Default, Eq, PartialEq)]
68pub struct Encoder {
69    queue: u32,
70    nbits: u8,
71}
72
73impl Encoder {
74    /// Creates an empty encoder.
75    pub const fn new() -> Self {
76        Self { queue: 0, nbits: 0 }
77    }
78
79    /// Encodes a chunk into caller-provided storage.
80    ///
81    /// If the output is too small, neither the encoder state nor the output
82    /// slice is changed. Capacity is checked using a content-independent upper
83    /// bound derived from the current bit count and the input length.
84    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, OutputTooSmall> {
85        let required = self.update_capacity(input.len());
86        if output.len() < required {
87            return Err(OutputTooSmall::new(required));
88        }
89
90        // Invariant whenever bytes are ingested: `nbits <= 13` and `queue`
91        // holds exactly those bits (zeros above).
92        let mut queue = u64::from(self.queue);
93        let mut nbits = u32::from(self.nbits);
94        let mut written = 0;
95
96        macro_rules! emit_pair {
97            ($out:expr) => {{
98                let low13 = (queue & 8191) as usize;
99                if low13 > 88 {
100                    queue >>= 13;
101                    nbits -= 13;
102                    $out.copy_from_slice(&ENCODE_PAIRS[low13]);
103                } else {
104                    let value = (queue & 16383) as usize;
105                    queue >>= 14;
106                    nbits -= 14;
107                    $out.copy_from_slice(&ENCODE_PAIRS[value]);
108                }
109            }};
110        }
111
112        // Word-at-a-time path: OR in as many whole input bytes as fit in the
113        // 64-bit queue (6..=8), which leaves 57..=64 bits, always enough for
114        // exactly four pairs (at most 56 bits). Each pair value depends only
115        // on the lowest 13/14 queued bits, so ingesting later bytes early
116        // cannot change it; the emitted symbols and the leftover state are
117        // the same as the byte loop's.
118        let mut pos = 0;
119        while pos + 8 <= input.len() {
120            let word =
121                u64::from_le_bytes(input[pos..pos + 8].try_into().expect("slice of length 8"));
122            let whole_bytes = (64 - nbits) >> 3;
123            let whole_bits = whole_bytes << 3;
124            queue |= (word & (u64::MAX >> (64 - whole_bits))) << nbits;
125            nbits += whole_bits;
126            pos += whole_bytes as usize;
127            let out = &mut output[written..written + 8];
128            emit_pair!(&mut out[0..2]);
129            emit_pair!(&mut out[2..4]);
130            emit_pair!(&mut out[4..6]);
131            emit_pair!(&mut out[6..8]);
132            written += 8;
133        }
134
135        for &byte in &input[pos..] {
136            queue |= u64::from(byte) << nbits;
137            nbits += 8;
138            if nbits <= 13 {
139                continue;
140            }
141            emit_pair!(&mut output[written..written + 2]);
142            written += 2;
143        }
144        // `nbits <= 13` here, so the queue fits the stored width.
145        self.queue = queue as u32;
146        self.nbits = nbits as u8;
147        Ok(written)
148    }
149
150    /// Returns the final encoded tail and its length.
151    #[must_use]
152    pub fn finish(self) -> ([u8; 2], usize) {
153        let mut tail = [0; 2];
154        if self.nbits == 0 {
155            return (tail, 0);
156        }
157
158        tail[0] = ALPHABET[self.queue as usize % 91];
159        if self.nbits > 7 || self.queue > 90 {
160            tail[1] = ALPHABET[self.queue as usize / 91];
161            (tail, 2)
162        } else {
163            (tail, 1)
164        }
165    }
166
167    fn update_capacity(&self, input_len: usize) -> usize {
168        // This is 2 * floor((nbits + 8 * input_len) / 13), split at
169        // 13-byte boundaries so that 8 * input_len is never formed.
170        let quotient = input_len / 13;
171        let remainder = input_len % 13;
172        let prefix = quotient.saturating_mul(16);
173        let remaining_bits = usize::from(self.nbits) + remainder * 8;
174        prefix.saturating_add((remaining_bits / 13) * 2)
175    }
176
177    pub(crate) fn encode_into(
178        mut self,
179        input: &[u8],
180        output: &mut [u8],
181    ) -> Result<usize, OutputTooSmall> {
182        let tail_capacity = if self.nbits == 0 && input.is_empty() {
183            0
184        } else {
185            2
186        };
187        let required = self
188            .update_capacity(input.len())
189            .saturating_add(tail_capacity);
190        if output.len() < required {
191            return Err(OutputTooSmall::new(required));
192        }
193
194        let written = self.update(input, output)?;
195        let (tail, tail_len) = self.finish();
196        let end = written + tail_len;
197        output[written..end].copy_from_slice(&tail[..tail_len]);
198        Ok(end)
199    }
200}
201
202#[cfg(feature = "alloc")]
203/// Encodes `input` as standard basE91 into a newly allocated byte vector.
204///
205/// This function is available when the `alloc` feature is enabled.
206///
207/// # Errors
208///
209/// Returns [`EncodeError::AllocationFailed`] if the output-length upper bound
210/// does not fit in `usize`, exceeds `isize::MAX`, or allocation fails.
211pub fn encode(input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
212    let capacity = max_encoded_len(input.len()).ok_or(EncodeError::AllocationFailed)?;
213    if capacity > isize::MAX as usize {
214        return Err(EncodeError::AllocationFailed);
215    }
216
217    let mut output = alloc::vec::Vec::new();
218    output
219        .try_reserve_exact(capacity)
220        .map_err(|_| EncodeError::AllocationFailed)?;
221    output.resize(capacity, 0);
222    let written = Encoder::new()
223        .encode_into(input, &mut output)
224        .map_err(EncodeError::OutputTooSmall)?;
225    output.truncate(written);
226    Ok(output)
227}
228
229/// A caller-visible output capacity error.
230#[non_exhaustive]
231#[derive(Clone, Copy, Debug, Eq, PartialEq)]
232pub struct OutputTooSmall {
233    required: usize,
234}
235
236impl OutputTooSmall {
237    pub(crate) const fn new(required: usize) -> Self {
238        Self { required }
239    }
240
241    /// Returns the capacity required by the attempted operation.
242    pub const fn required(self) -> usize {
243        self.required
244    }
245}
246
247impl fmt::Display for OutputTooSmall {
248    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249        write!(
250            formatter,
251            "output buffer is too small; need {} bytes",
252            self.required
253        )
254    }
255}
256
257impl core::error::Error for OutputTooSmall {}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn update_capacity_matches_wide_formula_and_saturates() {
265        for nbits in [0, 7, 13] {
266            let encoder = Encoder { queue: 0, nbits };
267            for input_len in [0, 1, 12, 13, 14, usize::MAX] {
268                let wide = 2 * ((u128::from(nbits) + 8 * input_len as u128) / 13);
269                let expected = usize::try_from(wide).unwrap_or(usize::MAX);
270                assert_eq!(encoder.update_capacity(input_len), expected);
271            }
272        }
273    }
274
275    #[test]
276    fn allocation_limit_is_distinct_from_math_overflow() {
277        let bound = max_encoded_len(isize::MAX as usize).expect("bound fits usize");
278        assert!(bound > isize::MAX as usize);
279        assert_eq!(max_encoded_len(usize::MAX), None);
280    }
281}