Skip to main content

base64_ng/v2/
chunks.rs

1//! Borrowed input iteration over synthesized encoded quanta.
2
3use super::{
4    ordinary::OneShotError,
5    specifications::{Base64, Codec, CodecSettings, EncodePadding},
6};
7
8/// One synthesized Base64 output chunk.
9///
10/// Complete chunks contain four bytes. The final chunk may contain two or
11/// three bytes for an unpadded codec. This value owns its synthesized bytes;
12/// it is not a zero-copy view into the plaintext input.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct EncodedChunk {
15    bytes: [u8; 4],
16    len: usize,
17}
18
19impl EncodedChunk {
20    /// Returns the initialized encoded bytes in this chunk.
21    #[must_use]
22    pub fn as_bytes(&self) -> &[u8] {
23        &self.bytes[..self.len]
24    }
25
26    /// Returns the initialized encoded bytes as visible ASCII.
27    ///
28    /// Validated alphabets guarantee this succeeds. The `Result` keeps that
29    /// invariant checked without introducing an unsafe UTF-8 conversion.
30    pub fn as_str(&self) -> Result<&str, core::str::Utf8Error> {
31        core::str::from_utf8(self.as_bytes())
32    }
33}
34
35impl core::fmt::Display for EncodedChunk {
36    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        formatter.write_str(self.as_str().map_err(|_| core::fmt::Error)?)
38    }
39}
40
41/// Iterator over synthesized Base64 output chunks for one borrowed input.
42///
43/// The iterator borrows only `input`; it owns a copy of the validated codec
44/// settings. Dropping the codec value after construction therefore does not
45/// invalidate iteration, while the input cannot be dropped or mutated until
46/// this iterator is released.
47#[derive(Clone)]
48pub struct EncodedChunks<'a> {
49    settings: CodecSettings,
50    input: &'a [u8],
51    offset: usize,
52}
53
54impl core::fmt::Debug for EncodedChunks<'_> {
55    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        formatter
57            .debug_struct("EncodedChunks")
58            .field("input_len", &self.input.len())
59            .field("offset", &self.offset)
60            .field("remaining_chunks", &self.remaining_chunks())
61            .finish_non_exhaustive()
62    }
63}
64
65impl<'a> EncodedChunks<'a> {
66    pub(super) const fn new(settings: CodecSettings, input: &'a [u8]) -> Self {
67        Self {
68            settings,
69            input,
70            offset: 0,
71        }
72    }
73
74    fn remaining_chunks(&self) -> usize {
75        self.input.len().saturating_sub(self.offset).div_ceil(3)
76    }
77}
78
79impl Iterator for EncodedChunks<'_> {
80    type Item = EncodedChunk;
81
82    fn next(&mut self) -> Option<Self::Item> {
83        let remaining = self.input.get(self.offset..)?;
84        if remaining.is_empty() {
85            return None;
86        }
87
88        let consumed = remaining.len().min(3);
89        let chunk = encode_chunk(self.settings, &remaining[..consumed]);
90        self.offset += consumed;
91        Some(chunk)
92    }
93
94    fn size_hint(&self) -> (usize, Option<usize>) {
95        let remaining = self.remaining_chunks();
96        (remaining, Some(remaining))
97    }
98}
99
100impl ExactSizeIterator for EncodedChunks<'_> {
101    fn len(&self) -> usize {
102        self.remaining_chunks()
103    }
104}
105
106impl core::iter::FusedIterator for EncodedChunks<'_> {}
107
108impl<S: Codec> Base64<S> {
109    /// Returns a borrowed-input iterator over synthesized encoded chunks.
110    ///
111    /// Length arithmetic is checked before the iterator is returned, so
112    /// iteration itself has no encoding-error path. Empty input yields no
113    /// chunks. Padding, when configured, appears only in the final chunk.
114    pub fn encoded_chunks<'a>(&self, input: &'a [u8]) -> Result<EncodedChunks<'a>, OneShotError> {
115        self.encoded_len(input.len())?;
116        Ok(EncodedChunks::new(self.settings(), input))
117    }
118}
119
120fn encode_chunk(settings: CodecSettings, input: &[u8]) -> EncodedChunk {
121    let alphabet = settings.alphabet().as_array();
122    let first = input[0];
123    let second = input.get(1).copied().unwrap_or(0);
124    let third = input.get(2).copied().unwrap_or(0);
125    let mut bytes = [b'='; 4];
126
127    bytes[0] = alphabet[usize::from(first >> 2)];
128    bytes[1] = alphabet[usize::from(((first & 3) << 4) | (second >> 4))];
129    bytes[2] = alphabet[usize::from(((second & 15) << 2) | (third >> 6))];
130    bytes[3] = alphabet[usize::from(third & 63)];
131
132    let len = match input.len() {
133        3 => 4,
134        1 | 2 if settings.encode_padding() == EncodePadding::Padded => 4,
135        2 => 3,
136        1 => 2,
137        _ => 0,
138    };
139    if settings.encode_padding() == EncodePadding::Padded {
140        if input.len() == 1 {
141            bytes[2] = b'=';
142            bytes[3] = b'=';
143        } else if input.len() == 2 {
144            bytes[3] = b'=';
145        }
146    }
147
148    EncodedChunk { bytes, len }
149}