Coverage Report

Created: 2026-08-13 06:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/base64ct-1.6.0/src/encoder.rs
Line
Count
Source
1
//! Buffered Base64 encoder.
2
3
use crate::{
4
    Encoding,
5
    Error::{self, InvalidLength},
6
    LineEnding, MIN_LINE_WIDTH,
7
};
8
use core::{cmp, marker::PhantomData, str};
9
10
#[cfg(feature = "std")]
11
use std::io;
12
13
#[cfg(doc)]
14
use crate::{Base64, Base64Unpadded};
15
16
/// Stateful Base64 encoder with support for buffered, incremental encoding.
17
///
18
/// The `E` type parameter can be any type which impls [`Encoding`] such as
19
/// [`Base64`] or [`Base64Unpadded`].
20
pub struct Encoder<'o, E: Encoding> {
21
    /// Output buffer.
22
    output: &'o mut [u8],
23
24
    /// Cursor within the output buffer.
25
    position: usize,
26
27
    /// Block buffer used for non-block-aligned data.
28
    block_buffer: BlockBuffer,
29
30
    /// Configuration and state for line-wrapping the output at a specified
31
    /// column.
32
    line_wrapper: Option<LineWrapper>,
33
34
    /// Phantom parameter for the Base64 encoding in use.
35
    encoding: PhantomData<E>,
36
}
37
38
impl<'o, E: Encoding> Encoder<'o, E> {
39
    /// Create a new encoder which writes output to the given byte slice.
40
    ///
41
    /// Output constructed using this method is not line-wrapped.
42
0
    pub fn new(output: &'o mut [u8]) -> Result<Self, Error> {
43
0
        if output.is_empty() {
44
0
            return Err(InvalidLength);
45
0
        }
46
47
0
        Ok(Self {
48
0
            output,
49
0
            position: 0,
50
0
            block_buffer: BlockBuffer::default(),
51
0
            line_wrapper: None,
52
0
            encoding: PhantomData,
53
0
        })
54
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::new
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::new
55
56
    /// Create a new encoder which writes line-wrapped output to the given byte
57
    /// slice.
58
    ///
59
    /// Output will be wrapped at the specified interval, using the provided
60
    /// line ending. Use [`LineEnding::default()`] to use the conventional line
61
    /// ending for the target OS.
62
    ///
63
    /// Minimum allowed line width is 4.
64
0
    pub fn new_wrapped(
65
0
        output: &'o mut [u8],
66
0
        width: usize,
67
0
        ending: LineEnding,
68
0
    ) -> Result<Self, Error> {
69
0
        let mut encoder = Self::new(output)?;
70
0
        encoder.line_wrapper = Some(LineWrapper::new(width, ending)?);
71
0
        Ok(encoder)
72
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::new_wrapped
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::new_wrapped
73
74
    /// Encode the provided buffer as Base64, writing it to the output buffer.
75
    ///
76
    /// # Returns
77
    /// - `Ok(bytes)` if the expected amount of data was read
78
    /// - `Err(Error::InvalidLength)` if there is insufficient space in the output buffer
79
0
    pub fn encode(&mut self, mut input: &[u8]) -> Result<(), Error> {
80
        // If there's data in the block buffer, fill it
81
0
        if !self.block_buffer.is_empty() {
82
0
            self.process_buffer(&mut input)?;
83
0
        }
84
85
0
        while !input.is_empty() {
86
            // Attempt to encode a stride of block-aligned data
87
0
            let in_blocks = input.len() / 3;
88
0
            let out_blocks = self.remaining().len() / 4;
89
0
            let mut blocks = cmp::min(in_blocks, out_blocks);
90
91
            // When line wrapping, cap the block-aligned stride at near/at line length
92
0
            if let Some(line_wrapper) = &self.line_wrapper {
93
0
                line_wrapper.wrap_blocks(&mut blocks)?;
94
0
            }
95
96
0
            if blocks > 0 {
97
0
                let len = blocks.checked_mul(3).ok_or(InvalidLength)?;
98
0
                let (in_aligned, in_rem) = input.split_at(len);
99
0
                input = in_rem;
100
0
                self.perform_encode(in_aligned)?;
101
0
            }
102
103
            // If there's remaining non-aligned data, fill the block buffer
104
0
            if !input.is_empty() {
105
0
                self.process_buffer(&mut input)?;
106
0
            }
107
        }
108
109
0
        Ok(())
110
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::encode
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::encode
111
112
    /// Get the position inside of the output buffer where the write cursor
113
    /// is currently located.
114
0
    pub fn position(&self) -> usize {
115
0
        self.position
116
0
    }
117
118
    /// Finish encoding data, returning the resulting Base64 as a `str`.
119
0
    pub fn finish(self) -> Result<&'o str, Error> {
120
0
        self.finish_with_remaining().map(|(base64, _)| base64)
121
0
    }
122
123
    /// Finish encoding data, returning the resulting Base64 as a `str`
124
    /// along with the remaining space in the output buffer.
125
0
    pub fn finish_with_remaining(mut self) -> Result<(&'o str, &'o mut [u8]), Error> {
126
0
        if !self.block_buffer.is_empty() {
127
0
            let buffer_len = self.block_buffer.position;
128
0
            let block = self.block_buffer.bytes;
129
0
            self.perform_encode(&block[..buffer_len])?;
130
0
        }
131
132
0
        let (base64, remaining) = self.output.split_at_mut(self.position);
133
0
        Ok((str::from_utf8(base64)?, remaining))
134
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::finish_with_remaining
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::finish_with_remaining
135
136
    /// Borrow the remaining data in the buffer.
137
0
    fn remaining(&mut self) -> &mut [u8] {
138
0
        &mut self.output[self.position..]
139
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::remaining
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::remaining
140
141
    /// Fill the block buffer with data, consuming and encoding it when the
142
    /// buffer is full.
143
0
    fn process_buffer(&mut self, input: &mut &[u8]) -> Result<(), Error> {
144
0
        self.block_buffer.fill(input)?;
145
146
0
        if self.block_buffer.is_full() {
147
0
            let block = self.block_buffer.take();
148
0
            self.perform_encode(&block)?;
149
0
        }
150
151
0
        Ok(())
152
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::process_buffer
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::process_buffer
153
154
    /// Perform Base64 encoding operation.
155
0
    fn perform_encode(&mut self, input: &[u8]) -> Result<usize, Error> {
156
0
        let mut len = E::encode(input, self.remaining())?.as_bytes().len();
157
158
        // Insert newline characters into the output as needed
159
0
        if let Some(line_wrapper) = &mut self.line_wrapper {
160
0
            line_wrapper.insert_newlines(&mut self.output[self.position..], &mut len)?;
161
0
        }
162
163
0
        self.position = self.position.checked_add(len).ok_or(InvalidLength)?;
164
0
        Ok(len)
165
0
    }
Unexecuted instantiation: <base64ct::encoder::Encoder<base64ct::alphabet::standard::Base64>>::perform_encode
Unexecuted instantiation: <base64ct::encoder::Encoder<_>>::perform_encode
166
}
167
168
#[cfg(feature = "std")]
169
impl<'o, E: Encoding> io::Write for Encoder<'o, E> {
170
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
171
        self.encode(buf)?;
172
        Ok(buf.len())
173
    }
174
175
    fn flush(&mut self) -> io::Result<()> {
176
        // TODO(tarcieri): return an error if there's still data remaining in the buffer?
177
        Ok(())
178
    }
179
}
180
181
/// Base64 encode buffer for a 1-block output.
182
///
183
/// This handles a partial block of data, i.e. data which hasn't been
184
#[derive(Clone, Default, Debug)]
185
struct BlockBuffer {
186
    /// 3 decoded bytes to be encoded to a 4-byte Base64-encoded input.
187
    bytes: [u8; Self::SIZE],
188
189
    /// Position within the buffer.
190
    position: usize,
191
}
192
193
impl BlockBuffer {
194
    /// Size of the buffer in bytes: 3-bytes of unencoded input which
195
    /// Base64 encode to 4-bytes of output.
196
    const SIZE: usize = 3;
197
198
    /// Fill the remaining space in the buffer with the input data.
199
0
    fn fill(&mut self, input: &mut &[u8]) -> Result<(), Error> {
200
0
        let remaining = Self::SIZE.checked_sub(self.position).ok_or(InvalidLength)?;
201
0
        let len = cmp::min(input.len(), remaining);
202
0
        self.bytes[self.position..][..len].copy_from_slice(&input[..len]);
203
0
        self.position = self.position.checked_add(len).ok_or(InvalidLength)?;
204
0
        *input = &input[len..];
205
0
        Ok(())
206
0
    }
207
208
    /// Take the output buffer, resetting the position to 0.
209
0
    fn take(&mut self) -> [u8; Self::SIZE] {
210
0
        debug_assert!(self.is_full());
211
0
        let result = self.bytes;
212
0
        *self = Default::default();
213
0
        result
214
0
    }
215
216
    /// Is the buffer empty?
217
0
    fn is_empty(&self) -> bool {
218
0
        self.position == 0
219
0
    }
220
221
    /// Is the buffer full?
222
0
    fn is_full(&self) -> bool {
223
0
        self.position == Self::SIZE
224
0
    }
225
}
226
227
/// Helper for wrapping Base64 at a given line width.
228
#[derive(Debug)]
229
struct LineWrapper {
230
    /// Number of bytes remaining in the current line.
231
    remaining: usize,
232
233
    /// Column at which Base64 should be wrapped.
234
    width: usize,
235
236
    /// Newline characters to use at the end of each line.
237
    ending: LineEnding,
238
}
239
240
impl LineWrapper {
241
    /// Create a new linewrapper.
242
0
    fn new(width: usize, ending: LineEnding) -> Result<Self, Error> {
243
0
        if width < MIN_LINE_WIDTH {
244
0
            return Err(InvalidLength);
245
0
        }
246
247
0
        Ok(Self {
248
0
            remaining: width,
249
0
            width,
250
0
            ending,
251
0
        })
252
0
    }
253
254
    /// Wrap the number of blocks to encode near/at EOL.
255
0
    fn wrap_blocks(&self, blocks: &mut usize) -> Result<(), Error> {
256
0
        if blocks.checked_mul(4).ok_or(InvalidLength)? >= self.remaining {
257
0
            *blocks = self.remaining / 4;
258
0
        }
259
260
0
        Ok(())
261
0
    }
262
263
    /// Insert newlines into the output buffer as needed.
264
0
    fn insert_newlines(&mut self, mut buffer: &mut [u8], len: &mut usize) -> Result<(), Error> {
265
0
        let mut buffer_len = *len;
266
267
0
        if buffer_len <= self.remaining {
268
0
            self.remaining = self
269
0
                .remaining
270
0
                .checked_sub(buffer_len)
271
0
                .ok_or(InvalidLength)?;
272
273
0
            return Ok(());
274
0
        }
275
276
0
        buffer = &mut buffer[self.remaining..];
277
0
        buffer_len = buffer_len
278
0
            .checked_sub(self.remaining)
279
0
            .ok_or(InvalidLength)?;
280
281
        // The `wrap_blocks` function should ensure the buffer is no larger than a Base64 block
282
0
        debug_assert!(buffer_len <= 4, "buffer too long: {}", buffer_len);
283
284
        // Ensure space in buffer to add newlines
285
0
        let buffer_end = buffer_len
286
0
            .checked_add(self.ending.len())
287
0
            .ok_or(InvalidLength)?;
288
289
0
        if buffer_end >= buffer.len() {
290
0
            return Err(InvalidLength);
291
0
        }
292
293
        // Shift the buffer contents to make space for the line ending
294
0
        for i in (0..buffer_len).rev() {
295
0
            buffer[i.checked_add(self.ending.len()).ok_or(InvalidLength)?] = buffer[i];
296
        }
297
298
0
        buffer[..self.ending.len()].copy_from_slice(self.ending.as_bytes());
299
0
        *len = (*len).checked_add(self.ending.len()).ok_or(InvalidLength)?;
300
0
        self.remaining = self.width.checked_sub(buffer_len).ok_or(InvalidLength)?;
301
302
0
        Ok(())
303
0
    }
304
}
305
306
#[cfg(test)]
307
mod tests {
308
    use crate::{alphabet::Alphabet, test_vectors::*, Base64, Base64Unpadded, Encoder, LineEnding};
309
310
    #[test]
311
    fn encode_padded() {
312
        encode_test::<Base64>(PADDED_BIN, PADDED_BASE64, None);
313
    }
314
315
    #[test]
316
    fn encode_unpadded() {
317
        encode_test::<Base64Unpadded>(UNPADDED_BIN, UNPADDED_BASE64, None);
318
    }
319
320
    #[test]
321
    fn encode_multiline_padded() {
322
        encode_test::<Base64>(MULTILINE_PADDED_BIN, MULTILINE_PADDED_BASE64, Some(70));
323
    }
324
325
    #[test]
326
    fn encode_multiline_unpadded() {
327
        encode_test::<Base64Unpadded>(MULTILINE_UNPADDED_BIN, MULTILINE_UNPADDED_BASE64, Some(70));
328
    }
329
330
    #[test]
331
    fn no_trailing_newline_when_aligned() {
332
        let mut buffer = [0u8; 64];
333
        let mut encoder = Encoder::<Base64>::new_wrapped(&mut buffer, 64, LineEnding::LF).unwrap();
334
        encoder.encode(&[0u8; 48]).unwrap();
335
336
        // Ensure no newline character is present in this case
337
        assert_eq!(
338
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
339
            encoder.finish().unwrap()
340
        );
341
    }
342
343
    /// Core functionality of an encoding test.
344
    fn encode_test<V: Alphabet>(input: &[u8], expected: &str, wrapped: Option<usize>) {
345
        let mut buffer = [0u8; 1024];
346
347
        for chunk_size in 1..input.len() {
348
            let mut encoder = match wrapped {
349
                Some(line_width) => {
350
                    Encoder::<V>::new_wrapped(&mut buffer, line_width, LineEnding::LF)
351
                }
352
                None => Encoder::<V>::new(&mut buffer),
353
            }
354
            .unwrap();
355
356
            for chunk in input.chunks(chunk_size) {
357
                encoder.encode(chunk).unwrap();
358
            }
359
360
            assert_eq!(expected, encoder.finish().unwrap());
361
        }
362
    }
363
}