Skip to main content

base64_ng/v2/
wrapping.rs

1//! Validated line-wrapping policy for the 2.0 codec core.
2
3use core::num::NonZeroUsize;
4
5/// Line ending inserted between encoded body lines.
6#[derive(Clone, Copy, Debug, Eq, Hash, 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 the exact line-ending bytes.
16    #[must_use]
17    pub const fn as_bytes(self) -> &'static [u8] {
18        match self {
19            Self::Lf => b"\n",
20            Self::CrLf => b"\r\n",
21        }
22    }
23
24    /// Returns the line-ending width in bytes.
25    #[must_use]
26    pub const fn byte_len(self) -> usize {
27        self.as_bytes().len()
28    }
29}
30
31/// Failure constructing a line-wrapping policy.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33#[non_exhaustive]
34pub enum LineWrapError {
35    /// A zero-width line would prevent encoder progress.
36    ZeroWidth,
37}
38
39impl core::fmt::Display for LineWrapError {
40    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        match self {
42            Self::ZeroWidth => formatter.write_str("base64 line width must be non-zero"),
43        }
44    }
45}
46
47#[cfg(feature = "std")]
48impl std::error::Error for LineWrapError {}
49
50/// Immutable, always-progressing Base64 body wrapping policy.
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52pub struct LineWrap {
53    line_width: NonZeroUsize,
54    line_ending: LineEnding,
55}
56
57impl LineWrap {
58    /// MIME transfer-body wrapping: 76 columns with CRLF separators.
59    ///
60    /// This is a body-layout value, not a complete MIME parser or profile.
61    pub(crate) const MIME_BODY_WRAP: Self = Self {
62        line_width: NonZeroUsize::MIN.saturating_add(75),
63        line_ending: LineEnding::CrLf,
64    };
65
66    /// PEM body wrapping: 64 columns with LF separators.
67    ///
68    /// This is a body-layout value, not a complete RFC 7468 parser.
69    pub(crate) const PEM_BODY_LF_WRAP: Self = Self {
70        line_width: NonZeroUsize::MIN.saturating_add(63),
71        line_ending: LineEnding::Lf,
72    };
73
74    /// PEM body wrapping: 64 columns with CRLF separators.
75    ///
76    /// This is a body-layout value, not a complete RFC 7468 parser.
77    pub(crate) const PEM_BODY_CRLF_WRAP: Self = Self {
78        line_width: NonZeroUsize::MIN.saturating_add(63),
79        line_ending: LineEnding::CrLf,
80    };
81
82    /// Constructs a validated wrapping policy.
83    pub const fn try_new(
84        line_width: usize,
85        line_ending: LineEnding,
86    ) -> Result<Self, LineWrapError> {
87        match NonZeroUsize::new(line_width) {
88            Some(line_width) => Ok(Self {
89                line_width,
90                line_ending,
91            }),
92            None => Err(LineWrapError::ZeroWidth),
93        }
94    }
95
96    /// Returns the non-zero encoded body width.
97    #[must_use]
98    pub const fn line_width(self) -> NonZeroUsize {
99        self.line_width
100    }
101
102    /// Returns the separator inserted between body lines.
103    #[must_use]
104    pub const fn line_ending(self) -> LineEnding {
105        self.line_ending
106    }
107
108    /// Returns the exact wrapped size without a trailing line ending.
109    #[must_use]
110    pub const fn checked_output_len(self, payload_len: usize) -> Option<usize> {
111        if payload_len == 0 {
112            return Some(0);
113        }
114
115        let breaks = (payload_len - 1) / self.line_width.get();
116        let Some(separator_bytes) = breaks.checked_mul(self.line_ending.byte_len()) else {
117            return None;
118        };
119        payload_len.checked_add(separator_bytes)
120    }
121
122    /// Inserts line endings into an already encoded Base64 body.
123    ///
124    /// The destination is unchanged when it is too small or length arithmetic
125    /// overflows. Successful output never has a trailing line ending.
126    pub fn insert_into(self, payload: &[u8], output: &mut [u8]) -> Option<usize> {
127        let required = self.checked_output_len(payload.len())?;
128        if output.len() < required {
129            return None;
130        }
131
132        let separator = self.line_ending.as_bytes();
133        let width = self.line_width.get();
134        let mut read = 0usize;
135        let mut write = 0usize;
136        let mut column = 0usize;
137        while read < payload.len() {
138            if column == width {
139                let end = write.checked_add(separator.len())?;
140                output[write..end].copy_from_slice(separator);
141                write = end;
142                column = 0;
143            }
144            output[write] = payload[read];
145            read += 1;
146            write += 1;
147            column += 1;
148        }
149        Some(write)
150    }
151
152    /// Validates wrapped body layout and returns its unwrapped payload length.
153    ///
154    /// Interior lines must have exactly the configured width. A final line may
155    /// be shorter, and one final line ending is accepted for compatibility
156    /// with body formats that terminate their last line.
157    #[must_use]
158    pub fn payload_len(self, input: &[u8]) -> Option<usize> {
159        let separator = self.line_ending.as_bytes();
160        let width = self.line_width.get();
161        let mut index = 0usize;
162        let mut column = 0;
163        let mut payload_len = 0;
164
165        while index < input.len() {
166            if starts_with(input, index, separator) {
167                if column == 0 {
168                    return None;
169                }
170                index = index.checked_add(separator.len())?;
171                if index == input.len() {
172                    return Some(payload_len);
173                }
174                if column != width {
175                    return None;
176                }
177                column = 0;
178                continue;
179            }
180
181            if matches!(input[index], b'\r' | b'\n') || column == width {
182                return None;
183            }
184            index += 1;
185            column += 1;
186            payload_len += 1;
187        }
188        Some(payload_len)
189    }
190
191    /// Validates and copies a wrapped body without its line endings.
192    ///
193    /// The destination is unchanged when layout validation fails or the
194    /// destination is too small.
195    pub fn copy_payload_into(self, input: &[u8], output: &mut [u8]) -> Option<usize> {
196        let payload_len = self.payload_len(input)?;
197        if output.len() < payload_len {
198            return None;
199        }
200
201        let separator = self.line_ending.as_bytes();
202        let mut read = 0usize;
203        let mut write = 0usize;
204        while read < input.len() {
205            if starts_with(input, read, separator) {
206                read = read.checked_add(separator.len())?;
207            } else {
208                output[write] = input[read];
209                read += 1;
210                write += 1;
211            }
212        }
213        Some(write)
214    }
215}
216
217fn starts_with(input: &[u8], index: usize, needle: &[u8]) -> bool {
218    let Some(end) = index.checked_add(needle.len()) else {
219        return false;
220    };
221    end <= input.len() && &input[index..end] == needle
222}