Skip to main content

base64_ng/v2/
incremental.rs

1//! Heapless incremental ordinary transforms.
2
3use core::num::NonZeroUsize;
4
5use super::{
6    contracts::{Lifecycle, OperationError, Progress, Step},
7    specifications::{Base64, Codec, CodecSettings, EncodePadding},
8};
9
10const INPUT_QUANTUM: usize = 3;
11const OUTPUT_QUANTUM: usize = 4;
12
13/// Heapless ordinary Base64 encoder state.
14///
15/// The state retains at most two incomplete input bytes and one four-byte
16/// encoded quantum. It intentionally has no cleanup destructor; secret
17/// transforms use a separate state type.
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct EncoderState {
20    settings: CodecSettings,
21    tail: [u8; INPUT_QUANTUM],
22    tail_len: usize,
23    pending: [u8; OUTPUT_QUANTUM],
24    pending_start: usize,
25    pending_len: usize,
26    lifecycle: Lifecycle,
27}
28
29impl EncoderState {
30    pub(crate) const fn new(settings: CodecSettings) -> Self {
31        Self {
32            settings,
33            tail: [0; INPUT_QUANTUM],
34            tail_len: 0,
35            pending: [0; OUTPUT_QUANTUM],
36            pending_start: 0,
37            pending_len: 0,
38            lifecycle: Lifecycle::new(),
39        }
40    }
41
42    /// Accepts an input prefix and writes as much encoded output as fits.
43    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<Step, OperationError> {
44        let span = self.lifecycle.reserve_input(input.len())?;
45        let consumed =
46            planned_input_consumption(self.tail_len, self.pending_len, input.len(), output.len());
47        self.lifecycle.commit_input(span, consumed)?;
48
49        let mut produced = self.drain_pending(output);
50        let mut input_offset = 0;
51        while input_offset < consumed {
52            self.tail[self.tail_len] = input[input_offset];
53            self.tail_len += 1;
54            input_offset += 1;
55
56            if self.tail_len == INPUT_QUANTUM {
57                self.pending = encode_quantum(self.settings, self.tail);
58                self.pending_start = 0;
59                self.pending_len = OUTPUT_QUANTUM;
60                self.tail_len = 0;
61                produced += self.drain_pending(&mut output[produced..]);
62            }
63        }
64
65        let progress = Progress::new(consumed, produced);
66        if self.pending_len != 0 || consumed != input.len() {
67            self.lifecycle.output_full(progress, NonZeroUsize::MIN)
68        } else {
69            self.lifecycle.need_input(progress)
70        }
71    }
72
73    /// Emits the canonical final tail and completes the current message.
74    pub fn finish(&mut self, output: &mut [u8]) -> Result<Step, OperationError> {
75        if self.lifecycle.begin_finish()? {
76            return self.lifecycle.finish(Progress::ZERO);
77        }
78
79        let mut produced = self.drain_pending(output);
80        if self.pending_len == 0 && self.tail_len != 0 {
81            self.pending_len = encode_tail(
82                self.settings,
83                &self.tail[..self.tail_len],
84                &mut self.pending,
85            );
86            self.pending_start = 0;
87            self.tail_len = 0;
88            produced += self.drain_pending(&mut output[produced..]);
89        }
90
91        let progress = Progress::new(0, produced);
92        if self.pending_len == 0 {
93            self.lifecycle.finish(progress)
94        } else {
95            self.lifecycle.output_full(progress, NonZeroUsize::MIN)
96        }
97    }
98
99    /// Resets the state for one unrelated message.
100    pub fn reset(&mut self) {
101        self.tail = [0; INPUT_QUANTUM];
102        self.tail_len = 0;
103        self.pending = [0; OUTPUT_QUANTUM];
104        self.pending_start = 0;
105        self.pending_len = 0;
106        self.lifecycle.reset();
107    }
108
109    /// Clears retained ordinary input and output through the reviewed wipe
110    /// boundary, then resets the state for reuse.
111    ///
112    /// This is explicit best-effort cleanup. It cannot clear bytes already
113    /// returned to a caller or copies retained by an input source, allocator,
114    /// register, cache, swap device, or crash dump.
115    pub fn clear(&mut self) {
116        self.wipe();
117    }
118
119    /// Returns the absolute number of input bytes accepted since reset.
120    #[must_use]
121    pub const fn source_position(&self) -> usize {
122        self.lifecycle.source_position()
123    }
124
125    /// Returns raw input bytes retained until the next complete quantum.
126    #[must_use]
127    pub const fn buffered_input_len(&self) -> usize {
128        self.tail_len
129    }
130
131    /// Clears retained input and output through the reviewed wipe boundary.
132    pub(crate) fn wipe(&mut self) {
133        crate::wipe_bytes(&mut self.tail);
134        crate::wipe_bytes(&mut self.pending);
135        self.tail_len = 0;
136        self.pending_start = 0;
137        self.pending_len = 0;
138        self.lifecycle.reset();
139    }
140
141    fn drain_pending(&mut self, output: &mut [u8]) -> usize {
142        let written = self.pending_len.min(output.len());
143        let pending_end = self.pending_start + written;
144        output[..written].copy_from_slice(&self.pending[self.pending_start..pending_end]);
145        self.pending_start = pending_end;
146        self.pending_len -= written;
147        if self.pending_len == 0 {
148            self.pending_start = 0;
149        }
150        written
151    }
152
153    #[cfg(kani)]
154    pub(crate) const fn proof_invariants(&self) -> bool {
155        self.tail_len < INPUT_QUANTUM
156            && self.pending_start <= OUTPUT_QUANTUM
157            && self.pending_len <= OUTPUT_QUANTUM
158            && self.pending_start + self.pending_len <= OUTPUT_QUANTUM
159    }
160}
161
162impl<S: Codec> Base64<S> {
163    /// Constructs a fresh heapless ordinary encoder.
164    pub fn encoder(&self) -> EncoderState {
165        EncoderState::new(self.settings())
166    }
167}
168
169fn planned_input_consumption(
170    initial_tail: usize,
171    pending: usize,
172    input: usize,
173    output: usize,
174) -> usize {
175    let pending_written = pending.min(output);
176    if pending_written != pending {
177        return 0;
178    }
179
180    let mut available_output = output - pending_written;
181    let mut tail = initial_tail;
182    let mut consumed = 0;
183    while consumed < input {
184        let copied = (INPUT_QUANTUM - tail).min(input - consumed);
185        consumed += copied;
186        tail += copied;
187        if tail != INPUT_QUANTUM {
188            break;
189        }
190
191        tail = 0;
192        let written = OUTPUT_QUANTUM.min(available_output);
193        available_output -= written;
194        if written != OUTPUT_QUANTUM {
195            break;
196        }
197    }
198    consumed
199}
200
201fn encode_quantum(settings: CodecSettings, input: [u8; INPUT_QUANTUM]) -> [u8; OUTPUT_QUANTUM] {
202    let table = settings.alphabet().as_array();
203    [
204        table[usize::from(input[0] >> 2)],
205        table[usize::from(((input[0] & 0x03) << 4) | (input[1] >> 4))],
206        table[usize::from(((input[1] & 0x0f) << 2) | (input[2] >> 6))],
207        table[usize::from(input[2] & 0x3f)],
208    ]
209}
210
211fn encode_tail(settings: CodecSettings, input: &[u8], output: &mut [u8; 4]) -> usize {
212    let table = settings.alphabet().as_array();
213    output[0] = table[usize::from(input[0] >> 2)];
214    output[1] = table[usize::from((input[0] & 0x03) << 4)];
215
216    if input.len() == 1 {
217        if settings.encode_padding() == EncodePadding::Padded {
218            output[2] = b'=';
219            output[3] = b'=';
220            4
221        } else {
222            2
223        }
224    } else {
225        output[1] = table[usize::from(((input[0] & 0x03) << 4) | (input[1] >> 4))];
226        output[2] = table[usize::from((input[1] & 0x0f) << 2)];
227        if settings.encode_padding() == EncodePadding::Padded {
228            output[3] = b'=';
229            4
230        } else {
231            3
232        }
233    }
234}