1use core::num::NonZeroUsize;
4
5#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7pub enum LineEnding {
8 Lf,
10 CrLf,
12}
13
14impl LineEnding {
15 #[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 #[must_use]
26 pub const fn byte_len(self) -> usize {
27 self.as_bytes().len()
28 }
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33#[non_exhaustive]
34pub enum LineWrapError {
35 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52pub struct LineWrap {
53 line_width: NonZeroUsize,
54 line_ending: LineEnding,
55}
56
57impl LineWrap {
58 pub(crate) const MIME_BODY_WRAP: Self = Self {
62 line_width: NonZeroUsize::MIN.saturating_add(75),
63 line_ending: LineEnding::CrLf,
64 };
65
66 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 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 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 #[must_use]
98 pub const fn line_width(self) -> NonZeroUsize {
99 self.line_width
100 }
101
102 #[must_use]
104 pub const fn line_ending(self) -> LineEnding {
105 self.line_ending
106 }
107
108 #[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 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 #[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 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}