Skip to main content

base64_ng/
length.rs

1//! Length calculation and line wrapping policy helpers.
2
3use crate::{DecodeError, EncodeError};
4
5/// Line ending used by wrapped Base64 output.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum LineEnding {
8    /// Line feed (`\n`).
9    Lf,
10    /// Carriage return followed by line feed (`\r\n`).
11    CrLf,
12}
13
14impl LineEnding {
15    /// Returns a stable printable identifier for this line ending.
16    #[must_use]
17    pub const fn name(self) -> &'static str {
18        match self {
19            Self::Lf => "LF",
20            Self::CrLf => "CRLF",
21        }
22    }
23
24    /// Returns the text representation of this line ending.
25    #[must_use]
26    pub const fn as_str(self) -> &'static str {
27        match self {
28            Self::Lf => "\n",
29            Self::CrLf => "\r\n",
30        }
31    }
32
33    /// Returns the byte representation of this line ending.
34    #[must_use]
35    pub const fn as_bytes(self) -> &'static [u8] {
36        self.as_str().as_bytes()
37    }
38
39    /// Returns the byte length of this line ending.
40    #[must_use]
41    pub const fn byte_len(self) -> usize {
42        match self {
43            Self::Lf => 1,
44            Self::CrLf => 2,
45        }
46    }
47}
48
49impl core::fmt::Display for LineEnding {
50    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51        formatter.write_str(self.name())
52    }
53}
54
55/// Base64 line wrapping policy.
56///
57/// `line_len` is measured in encoded Base64 bytes, not source input bytes.
58/// Encoders insert line endings between lines and do not append a trailing line
59/// ending after the final line.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct LineWrap {
62    /// Maximum encoded bytes per line.
63    pub line_len: usize,
64    /// Line ending inserted between wrapped lines.
65    pub line_ending: LineEnding,
66}
67
68impl LineWrap {
69    /// MIME-style wrapping: 76 columns with CRLF endings.
70    pub const MIME: Self = Self::new(76, LineEnding::CrLf);
71    /// PEM-style wrapping: 64 columns with LF endings.
72    pub const PEM: Self = Self::new(64, LineEnding::Lf);
73    /// PEM-style wrapping: 64 columns with CRLF endings.
74    pub const PEM_CRLF: Self = Self::new(64, LineEnding::CrLf);
75
76    /// Creates a wrapping policy.
77    ///
78    /// This constructor is intended for fixed, trusted values such as
79    /// compile-time MIME or PEM profile constants. Use [`Self::checked_new`]
80    /// when the line length comes from configuration, network input, file
81    /// metadata, or another untrusted runtime source.
82    ///
83    /// # Panics
84    ///
85    /// Panics when `line_len` is zero. Base64 wrapping requires a non-zero
86    /// encoded line length; accepting zero would make progress impossible for
87    /// wrapped encoders. This constructor is callable at runtime, so do not
88    /// pass attacker-controlled or externally configured values here; use
89    /// [`Self::checked_new`] for those cases.
90    #[must_use]
91    pub const fn new(line_len: usize, line_ending: LineEnding) -> Self {
92        assert!(line_len != 0, "base64 line wrap length must be non-zero");
93        Self {
94            line_len,
95            line_ending,
96        }
97    }
98
99    /// Creates a wrapping policy, returning `None` when the line length is
100    /// invalid.
101    ///
102    /// Base64 line-wrapping requires a non-zero encoded line length. This
103    /// helper is useful when accepting a wrapping policy from configuration or
104    /// another untrusted source.
105    #[must_use]
106    pub const fn checked_new(line_len: usize, line_ending: LineEnding) -> Option<Self> {
107        if line_len == 0 {
108            None
109        } else {
110            Some(Self::new(line_len, line_ending))
111        }
112    }
113
114    /// Returns the maximum encoded bytes per line.
115    #[must_use]
116    pub const fn line_len(self) -> usize {
117        self.line_len
118    }
119
120    /// Returns the line ending inserted between wrapped lines.
121    #[must_use]
122    pub const fn line_ending(self) -> LineEnding {
123        self.line_ending
124    }
125
126    /// Returns whether this wrapping policy can be used by the encoder.
127    #[must_use]
128    pub const fn is_valid(self) -> bool {
129        self.line_len != 0
130    }
131}
132
133impl core::fmt::Display for LineWrap {
134    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        write!(formatter, "{}:{}", self.line_len, self.line_ending.name())
136    }
137}
138
139/// Returns the encoded length for an input length and padding policy.
140///
141/// This function returns [`EncodeError::LengthOverflow`] instead of panicking.
142/// Use [`checked_encoded_len`] when an `Option<usize>` is more convenient.
143///
144/// # Examples
145///
146/// ```
147/// use base64_ng::encoded_len;
148///
149/// assert_eq!(encoded_len(5, true).unwrap(), 8);
150/// assert_eq!(encoded_len(5, false).unwrap(), 7);
151/// assert!(encoded_len(usize::MAX, true).is_err());
152/// ```
153pub const fn encoded_len(input_len: usize, padded: bool) -> Result<usize, EncodeError> {
154    match checked_encoded_len(input_len, padded) {
155        Some(len) => Ok(len),
156        None => Err(EncodeError::LengthOverflow),
157    }
158}
159
160/// Returns the encoded length after applying a line wrapping policy.
161///
162/// The returned length includes inserted line endings but does not include a
163/// trailing line ending after the final encoded line.
164///
165/// # Examples
166///
167/// ```
168/// use base64_ng::{LineEnding, LineWrap, wrapped_encoded_len};
169///
170/// let wrap = LineWrap::new(4, LineEnding::Lf);
171/// assert_eq!(wrapped_encoded_len(5, true, wrap).unwrap(), 9);
172/// ```
173pub const fn wrapped_encoded_len(
174    input_len: usize,
175    padded: bool,
176    wrap: LineWrap,
177) -> Result<usize, EncodeError> {
178    if wrap.line_len == 0 {
179        return Err(EncodeError::InvalidLineWrap { line_len: 0 });
180    }
181
182    let Some(encoded) = checked_encoded_len(input_len, padded) else {
183        return Err(EncodeError::LengthOverflow);
184    };
185    if encoded == 0 {
186        return Ok(0);
187    }
188
189    let breaks = (encoded - 1) / wrap.line_len;
190    let Some(line_ending_bytes) = breaks.checked_mul(wrap.line_ending.byte_len()) else {
191        return Err(EncodeError::LengthOverflow);
192    };
193    match encoded.checked_add(line_ending_bytes) {
194        Some(len) => Ok(len),
195        None => Err(EncodeError::LengthOverflow),
196    }
197}
198
199/// Returns the encoded length after line wrapping, or `None` on overflow or
200/// invalid line wrapping.
201///
202/// The returned length includes inserted line endings but does not include a
203/// trailing line ending after the final encoded line.
204///
205/// # Examples
206///
207/// ```
208/// use base64_ng::{LineEnding, LineWrap, checked_wrapped_encoded_len};
209///
210/// let wrap = LineWrap::new(4, LineEnding::Lf);
211/// assert_eq!(checked_wrapped_encoded_len(5, true, wrap), Some(9));
212/// assert_eq!(LineWrap::checked_new(0, LineEnding::Lf), None);
213/// ```
214#[must_use]
215pub const fn checked_wrapped_encoded_len(
216    input_len: usize,
217    padded: bool,
218    wrap: LineWrap,
219) -> Option<usize> {
220    if wrap.line_len == 0 {
221        return None;
222    }
223
224    let Some(encoded) = checked_encoded_len(input_len, padded) else {
225        return None;
226    };
227    if encoded == 0 {
228        return Some(0);
229    }
230
231    let breaks = (encoded - 1) / wrap.line_len;
232    let Some(line_ending_bytes) = breaks.checked_mul(wrap.line_ending.byte_len()) else {
233        return None;
234    };
235    encoded.checked_add(line_ending_bytes)
236}
237
238/// Returns the encoded length, or `None` if it would overflow `usize`.
239///
240/// # Examples
241///
242/// ```
243/// use base64_ng::checked_encoded_len;
244///
245/// assert_eq!(checked_encoded_len(5, true), Some(8));
246/// assert_eq!(checked_encoded_len(usize::MAX, true), None);
247/// ```
248#[must_use]
249pub const fn checked_encoded_len(input_len: usize, padded: bool) -> Option<usize> {
250    let groups = input_len / 3;
251    if groups > usize::MAX / 4 {
252        return None;
253    }
254    let full = groups * 4;
255    let rem = input_len % 3;
256    if rem == 0 {
257        Some(full)
258    } else if padded {
259        full.checked_add(4)
260    } else {
261        full.checked_add(rem + 1)
262    }
263}
264
265/// Returns the maximum decoded length for an encoded input length.
266///
267/// # Examples
268///
269/// ```
270/// use base64_ng::decoded_capacity;
271///
272/// assert_eq!(decoded_capacity(8), 6);
273/// assert_eq!(decoded_capacity(7), 5);
274/// ```
275#[must_use]
276pub const fn decoded_capacity(encoded_len: usize) -> usize {
277    let rem = encoded_len % 4;
278    encoded_len / 4 * 3
279        + if rem == 2 {
280            1
281        } else if rem == 3 {
282            2
283        } else {
284            0
285        }
286}
287
288/// Returns the exact decoded length implied by input length and padding.
289///
290/// This validates padding placement and impossible lengths, but it does not
291/// validate alphabet membership or non-canonical trailing bits.
292///
293/// # Examples
294///
295/// ```
296/// use base64_ng::decoded_len;
297///
298/// assert_eq!(decoded_len(b"aGVsbG8=", true).unwrap(), 5);
299/// assert_eq!(decoded_len(b"aGVsbG8", false).unwrap(), 5);
300/// ```
301pub fn decoded_len(input: &[u8], padded: bool) -> Result<usize, DecodeError> {
302    if padded {
303        decoded_len_padded(input)
304    } else {
305        decoded_len_unpadded(input)
306    }
307}
308
309pub(crate) fn decoded_len_padded(input: &[u8]) -> Result<usize, DecodeError> {
310    if input.is_empty() {
311        return Ok(0);
312    }
313    if !input.len().is_multiple_of(4) {
314        return Err(DecodeError::InvalidLength);
315    }
316
317    let Some((&last, before_last_prefix)) = input.split_last() else {
318        return Ok(0);
319    };
320    let Some(&before_last) = before_last_prefix.last() else {
321        return Err(DecodeError::InvalidLength);
322    };
323
324    let mut padding = 0;
325    if last == b'=' {
326        padding += 1;
327    }
328    if before_last == b'=' {
329        padding += 1;
330    }
331    if padding == 0
332        && let Some(index) = input.iter().position(|byte| *byte == b'=')
333    {
334        return Err(DecodeError::InvalidPadding { index });
335    }
336    if padding > 0 {
337        let first_pad = input.len() - padding;
338        if let Some(index) = input[..first_pad].iter().position(|byte| *byte == b'=') {
339            return Err(DecodeError::InvalidPadding { index });
340        }
341    }
342    Ok(input.len() / 4 * 3 - padding)
343}
344
345pub(crate) fn decoded_len_unpadded(input: &[u8]) -> Result<usize, DecodeError> {
346    if input.len() % 4 == 1 {
347        return Err(DecodeError::InvalidLength);
348    }
349    if let Some(index) = input.iter().position(|byte| *byte == b'=') {
350        return Err(DecodeError::InvalidPadding { index });
351    }
352    Ok(decoded_capacity(input.len()))
353}