1use crate::hash::LimitHasher;
2use bstr::ByteSlice;
3use memchr::memmem::Finder;
4use std::sync::LazyLock;
5
6#[derive(PartialEq, Clone, Debug, Copy)]
7pub enum Type {
8 Simple,
9 Relaxed,
10}
11
12impl Type {
13 pub fn canon_name(&self) -> &'static str {
14 match self {
15 Self::Simple => "simple",
16 Self::Relaxed => "relaxed",
17 }
18 }
19
20 pub(crate) fn canon_body(&self, body: &[u8], hasher: &mut LimitHasher) {
21 match self {
22 Self::Simple => body_simple(body, hasher),
23 Self::Relaxed => body_relaxed(body, hasher),
24 }
25 }
26
27 pub(crate) fn canon_header_into(&self, key: &[u8], value: &[u8], out: &mut Vec<u8>) {
28 match self {
29 Self::Simple => canonicalize_header_simple(key, value, out),
30 Self::Relaxed => canonicalize_header_relaxed(key, value, out),
31 }
32 }
33}
34
35fn do_body_simple(mut body: &[u8]) -> &[u8] {
36 if body.is_empty() {
37 return b"\r\n";
38 }
39
40 while body.ends_with(b"\r\n\r\n") {
41 body = &body[..body.len() - 2];
42 }
43
44 body
45}
46
47fn body_simple(body: &[u8], hasher: &mut LimitHasher) {
49 let body = do_body_simple(body);
50 hasher.hash(body);
51}
52
53struct IterLines<'haystack> {
55 haystack: &'haystack [u8],
56 inner: memchr::memmem::FindIter<'haystack, 'static>,
57 start: usize,
58 done: bool,
59}
60
61impl<'haystack> Iterator for IterLines<'haystack> {
62 type Item = &'haystack [u8];
63
64 fn next(&mut self) -> Option<Self::Item> {
65 if self.done {
66 return None;
67 }
68
69 match self.inner.next() {
70 Some(idx) => {
71 let line = &self.haystack[self.start..idx + 2];
72 self.start = idx + 2;
73 Some(line)
74 }
75 None => {
76 self.done = true;
77 let line = &self.haystack[self.start..];
78 if line.is_empty() {
79 None
80 } else {
81 Some(line)
82 }
83 }
84 }
85 }
86}
87
88fn iter_lines(haystack: &'_ [u8]) -> IterLines<'_> {
89 static CRLF: LazyLock<Finder> = LazyLock::new(|| memchr::memmem::Finder::new("\r\n"));
90 IterLines {
91 haystack,
92 inner: CRLF.find_iter(haystack),
93 start: 0,
94 done: false,
95 }
96}
97
98fn body_relaxed(body: &[u8], hasher: &mut LimitHasher) {
102 let mut pending_empty_lines = 0usize;
103
104 for mut line in iter_lines(body) {
105 line = trim_ws_end(line);
107
108 if line.is_empty() {
112 pending_empty_lines += 1;
113 continue;
114 }
115
116 for _ in 0..pending_empty_lines {
117 hasher.hash(b"\r\n");
118 }
119 pending_empty_lines = 0;
120
121 let mut prior = 0;
122 for idx in memchr::memchr2_iter(b' ', b'\t', line) {
124 if prior > 0 && idx == prior {
125 prior = idx + 1;
127 continue;
128 }
129
130 hasher.hash(&line[prior..idx]);
133 hasher.hash(b" ");
135
136 prior = idx + 1;
137 }
138 hasher.hash(&line[prior..]);
140
141 hasher.hash(b"\r\n");
143 }
144}
145
146fn canonicalize_header_simple(key: &[u8], value: &[u8], out: &mut Vec<u8>) {
148 out.extend_from_slice(key);
149 out.extend_from_slice(b": ");
150 out.extend_from_slice(value);
151 out.extend_from_slice(b"\r\n");
152}
153
154fn canonicalize_header_relaxed(key: &[u8], value: &[u8], out: &mut Vec<u8>) {
156 let key = key.to_ascii_lowercase();
157 let key = key.trim_end();
158
159 out.extend_from_slice(key.as_bytes());
160 out.extend_from_slice(b":");
161
162 let value = trim_ws_start(trim_ws_end(value));
163 let mut space_run = false;
164 for &c in value {
165 match c {
166 b'\r' | b'\n' => {}
167 b' ' | b'\t' => {
168 if space_run {
169 continue;
170 }
171 space_run = true;
172 out.push(b' ');
173 }
174 _ => {
175 space_run = false;
176 out.push(c);
177 }
178 }
179 }
180
181 out.extend_from_slice(b"\r\n");
182}
183
184fn trim_ws_start(mut line: &[u8]) -> &[u8] {
185 while let Some(c) = line.first() {
186 match c {
187 b' ' | b'\t' | b'\r' | b'\n' => line = &line[1..],
188 _ => break,
189 }
190 }
191 line
192}
193
194fn trim_ws_end(mut line: &[u8]) -> &[u8] {
195 while let Some(c) = line.last() {
196 match c {
197 b' ' | b'\t' | b'\r' | b'\n' => {
198 line = &line[0..line.len() - 1];
199 }
200 _ => break,
201 }
202 }
203 line
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn header_relaxed(key: &str, value: &[u8]) -> Vec<u8> {
211 let mut result = vec![];
212 canonicalize_header_relaxed(key.as_bytes(), value, &mut result);
213 result
214 }
215
216 #[test]
217 fn test_canonicalize_header_relaxed() {
218 assert_eq!(header_relaxed("SUBJect", b" AbC\r\n"), b"subject:AbC\r\n");
219 assert_eq!(
220 header_relaxed("Subject \t", b"\t Your Name\t \r\n"),
221 b"subject:Your Name\r\n"
222 );
223 assert_eq!(
224 header_relaxed("Subject \t", b"\t Kimi \t \r\n No \t\r\n Na Wa\r\n"),
225 b"subject:Kimi No Na Wa\r\n"
226 );
227 }
228
229 fn body_relaxed(data: &[u8]) -> Vec<u8> {
230 body_relaxed_with_limit(data, usize::MAX)
231 }
232
233 fn body_relaxed_with_limit(data: &[u8], limit: usize) -> Vec<u8> {
234 let mut hasher = LimitHasher {
235 hasher: crate::hash::HashImpl::copy_data(),
236 limit,
237 hashed: 0,
238 };
239 super::body_relaxed(data, &mut hasher);
240 hasher.finalize_bytes()
241 }
242
243 fn reference_body_relaxed(body: &[u8]) -> Vec<u8> {
246 let mut lines = vec![];
247 let mut start = 0;
248
249 while let Some(offset) = body[start..]
250 .windows(2)
251 .position(|window| window == b"\r\n")
252 {
253 let end = start + offset;
254 lines.push(&body[start..end]);
255 start = end + 2;
256 }
257
258 if start < body.len() {
259 lines.push(&body[start..]);
260 }
261
262 let mut canonical_lines = lines
263 .into_iter()
264 .map(|line| {
265 let mut canonical = vec![];
266 let mut pending_wsp = false;
267
268 for &byte in line {
269 match byte {
270 b' ' | b'\t' => pending_wsp = true,
271 _ => {
272 if pending_wsp {
273 canonical.push(b' ');
274 pending_wsp = false;
275 }
276 canonical.push(byte);
277 }
278 }
279 }
280
281 canonical
282 })
283 .collect::<Vec<_>>();
284
285 while canonical_lines.last().is_some_and(Vec::is_empty) {
286 canonical_lines.pop();
287 }
288
289 let mut canonical = canonical_lines
290 .into_iter()
291 .collect::<Vec<_>>()
292 .join(b"\r\n".as_slice());
293 if !canonical.is_empty() {
294 canonical.extend_from_slice(b"\r\n");
295 }
296 canonical
297 }
298
299 fn body_simple(data: &[u8]) -> Vec<u8> {
300 let mut hasher = LimitHasher {
301 hasher: crate::hash::HashImpl::copy_data(),
302 limit: usize::MAX,
303 hashed: 0,
304 };
305 super::body_simple(data, &mut hasher);
306 hasher.finalize_bytes()
307 }
308
309 #[test]
310 fn test_canonicalize_body_relaxed() {
311 assert_eq!(body_relaxed(b""), b"");
312 assert_eq!(body_relaxed(b"\r\n"), b"");
313 assert_eq!(body_relaxed(b"\r\n\r\n"), b"");
314 assert_eq!(body_relaxed(b" \t\r\n\t\r\n"), b"");
315 assert_eq!(body_relaxed(b"hey \r\n"), b"hey\r\n");
316 assert_eq!(body_relaxed(b" C \r\nD \t E\r\n\r\n\r\n"), b" C\r\nD E\r\n");
317 assert_eq!(body_relaxed(b"\r\nhey \t\r\n \t\r\n"), b"\r\nhey\r\n");
318 assert_eq!(
319 body_relaxed(b"hey\r\n \t\r\nthere"),
320 b"hey\r\n\r\nthere\r\n"
321 );
322 }
323
324 #[test]
325 fn test_canonicalize_body_relaxed_against_reference() {
326 let line_variants: &[&[u8]] = &[
327 b"",
328 b"a",
329 b" ",
330 b"\t",
331 b" \t",
332 b"a ",
333 b" a",
334 b"a \t b",
335 b" a\t \tb ",
336 ];
337
338 for line_count in 0..=4u32 {
339 let combination_count = line_variants.len().pow(line_count);
340
341 for combination in 0..combination_count {
342 for terminated in [false, true] {
343 let mut selected = combination;
344 let mut body = vec![];
345
346 for line_index in 0..line_count {
347 if line_index > 0 {
348 body.extend_from_slice(b"\r\n");
349 }
350 body.extend_from_slice(line_variants[selected % line_variants.len()]);
351 selected /= line_variants.len();
352 }
353
354 if terminated {
355 body.extend_from_slice(b"\r\n");
356 }
357
358 assert_eq!(
359 body_relaxed(&body),
360 reference_body_relaxed(&body),
361 "body={body:?}"
362 );
363 }
364 }
365 }
366 }
367
368 #[test]
369 fn test_canonicalize_body_relaxed_respects_length_limit() {
370 let body = b"\r\n C \t\r\n \t\r\nD \t E\r\n\r\n";
371 let canonical = reference_body_relaxed(body);
372
373 for limit in 0..=canonical.len() + 1 {
374 assert_eq!(
375 body_relaxed_with_limit(body, limit),
376 &canonical[..limit.min(canonical.len())],
377 "limit={limit}"
378 );
379 }
380 }
381
382 #[test]
383 fn test_canonicalize_body_simple() {
384 assert_eq!(body_simple(b"\r\n"), b"\r\n");
385 assert_eq!(body_simple(b"hey \r\n"), b"hey \r\n");
386 assert_eq!(
387 body_simple(b" C \r\nD \t E\r\n\r\n\r\n"),
388 b" C \r\nD \t E\r\n"
389 );
390 }
391}