1use super::{Canonicalization, Signature};
8use crate::common::{
9 crypto::HashContext,
10 headers::{HeaderStream, Writable, Writer},
11};
12
13pub struct BodyHasher<H> {
18 hasher: H,
19 canonicalization: Canonicalization,
20 body_length_limit: u64,
21 bytes_hashed: u64,
22 state: CanonicalState,
23 done: bool,
24}
25
26impl<H: Writer> BodyHasher<H> {
27 pub fn new(hasher: H, canonicalization: Canonicalization, body_length_limit: u64) -> Self {
34 Self {
35 hasher,
36 canonicalization,
37 body_length_limit,
38 bytes_hashed: 0,
39 state: CanonicalState::new(),
40 done: false,
41 }
42 }
43
44 pub fn write(&mut self, chunk: &[u8]) {
49 if self.done {
50 return;
51 }
52
53 let chunk = if self.body_length_limit > 0 {
55 let remaining = self.body_length_limit.saturating_sub(self.bytes_hashed);
56 if remaining == 0 {
57 return;
58 }
59 &chunk[..remaining.min(chunk.len() as u64) as usize]
60 } else {
61 chunk
62 };
63
64 self.bytes_hashed += chunk.len() as u64;
65 self.state
66 .write(self.canonicalization, chunk, &mut self.hasher);
67 }
68
69 pub fn finish(mut self) -> (H, u64)
75 where
76 H: HashContext,
77 {
78 if !self.done {
79 self.done = true;
80 self.state.finish(self.canonicalization, &mut self.hasher);
81 }
82 (self.hasher, self.bytes_hashed)
83 }
84}
85
86const CRLF_RUN_MAX: usize = 32;
87const CRLF_RUN: [u8; CRLF_RUN_MAX * 2] = {
88 let mut run = [b'\r'; CRLF_RUN_MAX * 2];
89 let mut pos = 1;
90 while pos < run.len() {
91 run[pos] = b'\n';
92 pos += 2;
93 }
94 run
95};
96const SWAR_ONES: u64 = 0x0101_0101_0101_0101;
97const SWAR_HIGH: u64 = 0x8080_8080_8080_8080;
98const MEMCHR_MIN_LEN: usize = 16;
99
100#[inline(always)]
101fn write_crlf_run(writer: &mut impl Writer, count: usize) {
102 if count == 1 {
103 writer.write(b"\r\n");
104 } else if count > 1 {
105 let mut left = count;
106 while left != 0 {
107 let take = left.min(CRLF_RUN_MAX);
108 writer.write(&CRLF_RUN[..take * 2]);
109 left -= take;
110 }
111 }
112}
113
114#[inline(always)]
115fn find_line_break(haystack: &[u8]) -> Option<usize> {
116 use memchr::memchr2;
117
118 if haystack.len() >= MEMCHR_MIN_LEN {
119 memchr2(b'\r', b'\n', haystack)
120 } else {
121 haystack.iter().position(|&ch| ch == b'\r' || ch == b'\n')
122 }
123}
124
125#[inline(always)]
126fn find_wsp_or_break(haystack: &[u8]) -> Option<usize> {
127 let (words, tail) = haystack.as_chunks::<8>();
128
129 for (index, word) in words.iter().enumerate() {
130 let value = u64::from_le_bytes(*word);
131 let found = value.wrapping_sub(SWAR_ONES * 0x21) & !value & SWAR_HIGH;
132 if found != 0 {
133 return Some(index * 8 + (found.trailing_zeros() / 8) as usize);
134 }
135 }
136
137 tail.iter()
138 .position(|&ch| ch <= b' ')
139 .map(|pos| words.len() * 8 + pos)
140}
141
142fn simple_run_end(chunk: &[u8]) -> usize {
143 let mut offset = 0;
144
145 while let Some(pos) = find_line_break(&chunk[offset..]) {
146 let start = offset + pos;
147 match &chunk[start..] {
148 [b'\r', b'\n', next, ..] if *next != b'\r' && *next != b'\n' => offset = start + 2,
149 _ => return start,
150 }
151 }
152
153 chunk.len()
154}
155
156fn relaxed_run_end(chunk: &[u8]) -> usize {
157 let mut offset = 0;
158
159 while let Some(pos) = find_wsp_or_break(&chunk[offset..]) {
160 let start = offset + pos;
161 match &chunk[start..] {
162 [b' ', next, ..] if start != 0 && *next > b' ' => offset = start + 2,
163 [b'\r', b'\n', next, ..] if start != 0 && *next > b' ' => offset = start + 2,
164 [b'\t' | b'\n' | b'\r' | b' ', ..] => return start,
165 _ => offset = start + 1,
166 }
167 }
168
169 chunk.len()
170}
171
172struct CanonicalState {
173 crlf_seq: usize,
174 last_ch: u8,
175 is_empty: bool,
176}
177
178impl CanonicalState {
179 fn new() -> Self {
180 CanonicalState {
181 crlf_seq: 0,
182 last_ch: 0,
183 is_empty: true,
184 }
185 }
186
187 fn write(
188 &mut self,
189 canonicalization: Canonicalization,
190 chunk: &[u8],
191 writer: &mut impl Writer,
192 ) {
193 match canonicalization {
194 Canonicalization::Relaxed => self.write_relaxed(chunk, writer),
195 Canonicalization::Simple => self.write_simple(chunk, writer),
196 }
197 }
198
199 fn finish(&mut self, canonicalization: Canonicalization, writer: &mut impl Writer) {
200 match canonicalization {
201 Canonicalization::Relaxed => {
202 if !self.is_empty {
203 writer.write(b"\r\n");
204 }
205 }
206 Canonicalization::Simple => {
207 writer.write(b"\r\n");
208 }
209 }
210 }
211
212 #[inline(always)]
213 fn flush_breaks(&mut self, writer: &mut impl Writer) {
214 if self.crlf_seq != 0 {
215 write_crlf_run(writer, self.crlf_seq);
216 self.crlf_seq = 0;
217 }
218 }
219
220 fn write_simple(&mut self, chunk: &[u8], writer: &mut impl Writer) {
221 let mut rest = chunk;
222
223 while !rest.is_empty() {
224 let (run, tail) = rest.split_at(simple_run_end(rest));
225
226 if !run.is_empty() {
227 self.flush_breaks(writer);
228 writer.write(run);
229 self.is_empty = false;
230 }
231
232 let mut consumed = 0;
233 let mut breaks = self.crlf_seq;
234
235 for &ch in tail {
236 match ch {
237 b'\n' => breaks += 1,
238 b'\r' => {}
239 _ => break,
240 }
241 consumed += 1;
242 }
243
244 self.crlf_seq = breaks;
245 rest = &tail[consumed..];
246 }
247 }
248
249 fn write_relaxed(&mut self, chunk: &[u8], writer: &mut impl Writer) {
250 let mut rest = chunk;
251
252 while !rest.is_empty() {
253 let (run, tail) = rest.split_at(relaxed_run_end(rest));
254
255 if let Some(&last_ch) = run.last() {
256 self.flush_breaks(writer);
257 if self.last_ch == b' ' || self.last_ch == b'\t' {
258 writer.write(b" ");
259 }
260 writer.write(run);
261 self.is_empty = false;
262 self.last_ch = last_ch;
263 }
264
265 let mut consumed = 0;
266 let mut last_ch = self.last_ch;
267 let mut breaks = self.crlf_seq;
268 let mut pending = 0;
269 let mut has_wsp = false;
270
271 for &ch in tail {
272 match ch {
273 b'\n' => pending += 1,
274 b' ' | b'\t' => {
275 breaks += pending;
276 pending = 0;
277 has_wsp = true;
278 }
279 b'\r' => {}
280 _ => break,
281 }
282 consumed += 1;
283 last_ch = ch;
284 }
285
286 if consumed != 0 {
287 if has_wsp {
288 write_crlf_run(writer, breaks);
289 self.crlf_seq = pending;
290 self.is_empty = false;
291 } else {
292 self.crlf_seq = breaks + pending;
293 }
294 self.last_ch = last_ch;
295 }
296
297 rest = &tail[consumed..];
298 }
299 }
300}
301
302pub struct CanonicalBody<'a> {
303 canonicalization: Canonicalization,
304 body: &'a [u8],
305}
306
307impl Writable for CanonicalBody<'_> {
308 fn write(self, hasher: &mut impl Writer) {
309 let mut state = CanonicalState::new();
310 state.write(self.canonicalization, self.body, hasher);
311 state.finish(self.canonicalization, hasher);
312 }
313}
314
315impl Canonicalization {
316 pub fn canonicalize_headers<'a>(
317 &self,
318 headers: impl Iterator<Item = (&'a [u8], &'a [u8])>,
319 hasher: &mut impl Writer,
320 ) {
321 match self {
322 Canonicalization::Relaxed => {
323 for (name, value) in headers {
324 write_relaxed_name(name, hasher);
325 write_relaxed_value(value, hasher);
326 }
327 }
328 Canonicalization::Simple => {
329 for (name, value) in headers {
330 hasher.write(name);
331 hasher.write(b":");
332 hasher.write(value);
333 }
334 }
335 }
336 }
337
338 pub fn canonical_headers<'a>(
339 &self,
340 headers: Vec<(&'a [u8], &'a [u8])>,
341 ) -> CanonicalHeaders<'a> {
342 CanonicalHeaders {
343 canonicalization: *self,
344 headers,
345 }
346 }
347
348 pub fn canonical_body<'a>(&self, body: &'a [u8], l: u64) -> CanonicalBody<'a> {
349 CanonicalBody {
350 canonicalization: *self,
351 body: if l == 0 {
352 body
353 } else {
354 &body[..l.min(body.len() as u64) as usize]
355 },
356 }
357 }
358
359 pub fn serialize_name(&self, writer: &mut impl Writer) {
360 writer.write(match self {
361 Canonicalization::Relaxed => b"relaxed",
362 Canonicalization::Simple => b"simple",
363 });
364 }
365}
366
367impl Signature {
368 pub fn canonicalize<'x>(
369 &self,
370 mut message: impl HeaderStream<'x>,
371 ) -> (usize, CanonicalHeaders<'x>, Vec<String>, CanonicalBody<'x>) {
372 let mut headers = Vec::with_capacity(self.h.len());
373 let mut found_headers = FoundHeaders::default();
374 let mut signed_headers = Vec::with_capacity(self.h.len());
375
376 while let Some((name, value)) = message.next_header() {
377 if let Some(pos) = self
378 .h
379 .iter()
380 .position(|header| name.eq_ignore_ascii_case(header.as_bytes()))
381 {
382 headers.push((name, value));
383 found_headers.insert(pos);
384 signed_headers.push(std::str::from_utf8(name).unwrap().into());
385 }
386 }
387
388 let body = message.body();
389 let body_len = body.len();
390 let canonical_headers = self.ch.canonical_headers(headers);
391 let canonical_body = self.cb.canonical_body(body, u64::MAX);
392
393 signed_headers.reverse();
395 for (pos, header) in self.h.iter().enumerate() {
396 if !found_headers.contains(pos) {
397 signed_headers.push(header.to_string());
398 }
399 }
400
401 (body_len, canonical_headers, signed_headers, canonical_body)
402 }
403}
404
405pub struct CanonicalHeaders<'a> {
406 canonicalization: Canonicalization,
407 headers: Vec<(&'a [u8], &'a [u8])>,
408}
409
410impl Writable for CanonicalHeaders<'_> {
411 fn write(self, writer: &mut impl Writer) {
412 self.canonicalization
413 .canonicalize_headers(self.headers.into_iter().rev(), writer)
414 }
415}
416
417const LANE_ONES: u64 = 0x0101_0101_0101_0101;
418const LANE_HIGH: u64 = 0x8080_8080_8080_8080;
419const NAME_BUF_LEN: usize = 64;
420
421#[inline(always)]
422const fn zero_lanes(word: u64) -> u64 {
423 word.wrapping_sub(LANE_ONES) & !word & LANE_HIGH
424}
425
426#[inline(always)]
427const fn whitespace_lanes(word: u64) -> u64 {
428 zero_lanes(word ^ (LANE_ONES * 0x09))
429 | zero_lanes(word ^ (LANE_ONES * 0x0a))
430 | zero_lanes(word ^ (LANE_ONES * 0x0c))
431 | zero_lanes(word ^ (LANE_ONES * 0x0d))
432 | zero_lanes(word ^ (LANE_ONES * 0x20))
433}
434
435#[inline(always)]
436pub(crate) fn find_whitespace(bytes: &[u8]) -> Option<usize> {
437 let (words, tail) = bytes.as_chunks::<8>();
438 for (index, word) in words.iter().enumerate() {
439 let lanes = whitespace_lanes(u64::from_le_bytes(*word));
440 if lanes != 0 {
441 return Some(index * 8 + (lanes.trailing_zeros() / 8) as usize);
442 }
443 }
444
445 tail.iter()
446 .position(u8::is_ascii_whitespace)
447 .map(|offset| words.len() * 8 + offset)
448}
449
450pub(crate) struct SpacedToken<'a> {
451 pub spaces: &'a [u8],
452 pub token: &'a [u8],
453 pub spaces_and_token: &'a [u8],
454}
455
456pub(crate) struct SpacedTokens<'a> {
457 rest: &'a [u8],
458}
459
460impl<'a> SpacedTokens<'a> {
461 #[inline(always)]
462 pub(crate) fn new(bytes: &'a [u8]) -> Self {
463 Self { rest: bytes }
464 }
465}
466
467impl<'a> Iterator for SpacedTokens<'a> {
468 type Item = SpacedToken<'a>;
469
470 #[inline(always)]
471 fn next(&mut self) -> Option<Self::Item> {
472 let start = self.rest.iter().position(|ch| !ch.is_ascii_whitespace())?;
473 let end = find_whitespace(&self.rest[start..]).map_or(self.rest.len(), |len| start + len);
474 let (spaces_and_token, rest) = self.rest.split_at_checked(end)?;
475 let (spaces, token) = spaces_and_token.split_at_checked(start)?;
476 self.rest = rest;
477
478 Some(SpacedToken {
479 spaces,
480 token,
481 spaces_and_token,
482 })
483 }
484}
485
486#[inline(always)]
487fn fill_lowercase(buf: &mut [u8; NAME_BUF_LEN], name: &[u8]) -> usize {
488 let mut len = 0;
489 for (slot, ch) in buf
490 .iter_mut()
491 .zip(name.iter().filter(|ch| !ch.is_ascii_whitespace()))
492 {
493 *slot = ch.to_ascii_lowercase();
494 len += 1;
495 }
496 len
497}
498
499pub(crate) fn write_relaxed_name(name: &[u8], writer: &mut impl Writer) {
500 let mut buf = [0u8; NAME_BUF_LEN];
501 let mut rest = name;
502
503 while rest.len() >= NAME_BUF_LEN {
504 let (chunk, tail) = rest.split_at(NAME_BUF_LEN - 1);
505 let len = fill_lowercase(&mut buf, chunk);
506 writer.write(buf.get(..len).unwrap_or_default());
507 rest = tail;
508 }
509
510 let len = fill_lowercase(&mut buf, rest);
511 if let Some(slot) = buf.get_mut(len) {
512 *slot = b':';
513 }
514 writer.write(buf.get(..len + 1).unwrap_or_default());
515}
516
517fn write_relaxed_value(value: &[u8], writer: &mut impl Writer) {
518 let mut tokens = SpacedTokens::new(value);
519
520 if let Some(first) = tokens.next() {
521 writer.write(first.token);
522
523 for token in tokens {
524 if token.spaces == b" " {
525 writer.write(token.spaces_and_token);
526 } else {
527 if matches!(token.spaces.last(), Some(b' ' | b'\t')) {
528 writer.write(b" ");
529 }
530 writer.write(token.token);
531 }
532 }
533 }
534
535 if value.last() == Some(&b'\n') {
536 writer.write(b"\r\n");
537 }
538}
539
540#[derive(Default)]
541pub(crate) struct FoundHeaders {
542 inline: u64,
543 spilled: Vec<u64>,
544}
545
546impl FoundHeaders {
547 pub(crate) fn insert(&mut self, position: usize) {
548 match position.checked_sub(u64::BITS as usize) {
549 None => self.inline |= 1 << position,
550 Some(offset) => {
551 let word = offset / u64::BITS as usize;
552 if self.spilled.len() <= word {
553 self.spilled.resize(word + 1, 0);
554 }
555 if let Some(slot) = self.spilled.get_mut(word) {
556 *slot |= 1 << (offset % u64::BITS as usize);
557 }
558 }
559 }
560 }
561
562 pub(crate) fn contains(&self, position: usize) -> bool {
563 match position.checked_sub(u64::BITS as usize) {
564 None => self.inline & (1 << position) != 0,
565 Some(offset) => self
566 .spilled
567 .get(offset / u64::BITS as usize)
568 .is_some_and(|word| word & (1 << (offset % u64::BITS as usize)) != 0),
569 }
570 }
571}
572
573#[cfg(test)]
574mod test {
575 use super::{BodyHasher, CanonicalBody, CanonicalHeaders};
576 use crate::{
577 common::{
578 crypto::{HashContext, HashImpl, Sha256},
579 headers::{HeaderIterator, Writable},
580 },
581 dkim::Canonicalization,
582 };
583 use mail_builder::encoders::Base64Encoder;
584
585 #[test]
586 #[allow(clippy::needless_collect)]
587 fn dkim_canonicalize() {
588 for (message, (relaxed_headers, relaxed_body), (simple_headers, simple_body)) in [
589 (
590 concat!(
591 "A: X\r\n",
592 "B : Y\t\r\n",
593 "\tZ \r\n",
594 "\r\n",
595 " C \r\n",
596 "D \t E\r\n"
597 ),
598 (
599 concat!("a:X\r\n", "b:Y Z\r\n",),
600 concat!(" C\r\n", "D E\r\n"),
601 ),
602 ("A: X\r\nB : Y\t\r\n\tZ \r\n", " C \r\nD \t E\r\n"),
603 ),
604 (
605 concat!(
606 " From : John\tdoe <jdoe@domain.com>\t\r\n",
607 "SUB JECT:\ttest \t \r\n\r\n",
608 " body \t \r\n",
609 "\r\n",
610 "\r\n",
611 ),
612 (
613 concat!("from:John doe <jdoe@domain.com>\r\n", "subject:test\r\n"),
614 " body\r\n",
615 ),
616 (
617 concat!(
618 " From : John\tdoe <jdoe@domain.com>\t\r\n",
619 "SUB JECT:\ttest \t \r\n"
620 ),
621 " body \t \r\n",
622 ),
623 ),
624 (
625 "H: value\t\r\n\r\n",
626 ("h:value\r\n", ""),
627 ("H: value\t\r\n", "\r\n"),
628 ),
629 (
630 "\tx\t: \t\t\tz\r\n\r\nabc",
631 ("x:z\r\n", "abc\r\n"),
632 ("\tx\t: \t\t\tz\r\n", "abc\r\n"),
633 ),
634 (
635 "Subject: hello\r\n\r\n\r\n",
636 ("subject:hello\r\n", ""),
637 ("Subject: hello\r\n", "\r\n"),
638 ),
639 ] {
640 let mut header_iterator = HeaderIterator::new(message.as_bytes());
641 let parsed_headers = (&mut header_iterator).collect::<Vec<_>>();
642 let raw_body = header_iterator
643 .body_offset()
644 .map(|pos| &message.as_bytes()[pos..])
645 .unwrap_or_default();
646
647 for (canonicalization, expected_headers, expected_body) in [
648 (Canonicalization::Relaxed, relaxed_headers, relaxed_body),
649 (Canonicalization::Simple, simple_headers, simple_body),
650 ] {
651 let mut headers = Vec::new();
652 CanonicalHeaders {
653 canonicalization,
654 headers: parsed_headers.iter().cloned().rev().collect(),
655 }
656 .write(&mut headers);
657 assert_eq!(expected_headers, String::from_utf8(headers).unwrap());
658
659 let mut body = Vec::new();
660 CanonicalBody {
661 canonicalization,
662 body: raw_body,
663 }
664 .write(&mut body);
665 assert_eq!(expected_body, String::from_utf8(body).unwrap());
666 }
667 }
668
669 for (canonicalization, hash) in [
671 (
672 Canonicalization::Relaxed,
673 "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
674 ),
675 (
676 Canonicalization::Simple,
677 "frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=",
678 ),
679 ] {
680 for body in ["\r\n", ""] {
681 let mut hasher = Sha256::hasher();
682 CanonicalBody {
683 canonicalization,
684 body: body.as_bytes(),
685 }
686 .write(&mut hasher);
687
688 assert_eq!(
689 String::from_utf8(
690 Base64Encoder::new()
691 .encode(hasher.complete().as_ref())
692 .unwrap()
693 )
694 .unwrap(),
695 hash,
696 );
697 }
698 }
699 }
700
701 #[test]
702 fn body_hasher_matches_canonical_body() {
703 for (body, canonicalization) in [
705 (" C \r\nD \t E\r\n", Canonicalization::Relaxed),
706 (" C \r\nD \t E\r\n", Canonicalization::Simple),
707 (" body \t \r\n\r\n\r\n", Canonicalization::Relaxed),
708 (" body \t \r\n\r\n\r\n", Canonicalization::Simple),
709 ("", Canonicalization::Relaxed),
710 ("", Canonicalization::Simple),
711 ("\r\n", Canonicalization::Relaxed),
712 ("\r\n", Canonicalization::Simple),
713 ("abc", Canonicalization::Relaxed),
714 ("abc", Canonicalization::Simple),
715 ("hello world\r\n", Canonicalization::Relaxed),
716 ("hello world\r\n", Canonicalization::Simple),
717 ] {
718 let mut expected_hasher = Sha256::hasher();
720 CanonicalBody {
721 canonicalization,
722 body: body.as_bytes(),
723 }
724 .write(&mut expected_hasher);
725 let expected_hash = expected_hasher.complete();
726
727 let mut body_hasher = BodyHasher::new(Sha256::hasher(), canonicalization, 0);
729 body_hasher.write(body.as_bytes());
730 let (actual_hasher, _) = body_hasher.finish();
731 let actual_hash = actual_hasher.complete();
732
733 assert_eq!(
734 expected_hash.as_ref(),
735 actual_hash.as_ref(),
736 "BodyHasher (single chunk) mismatch for body {:?} with {:?} canonicalization",
737 body,
738 canonicalization
739 );
740 }
741 }
742
743 #[test]
744 fn body_hasher_chunked_matches_single() {
745 let body = " C \r\nD \t E\r\nMore content here\r\n\r\n";
747
748 for canonicalization in [Canonicalization::Relaxed, Canonicalization::Simple] {
749 let mut single_hasher = BodyHasher::new(Sha256::hasher(), canonicalization, 0);
751 single_hasher.write(body.as_bytes());
752 let (single_result, single_len) = single_hasher.finish();
753 let single_hash = single_result.complete();
754
755 for chunk_size in [1, 2, 3, 5, 7, 10] {
757 let mut chunked_hasher = BodyHasher::new(Sha256::hasher(), canonicalization, 0);
758 for chunk in body.as_bytes().chunks(chunk_size) {
759 chunked_hasher.write(chunk);
760 }
761 let (chunked_result, chunked_len) = chunked_hasher.finish();
762 let chunked_hash = chunked_result.complete();
763
764 assert_eq!(
765 single_hash.as_ref(),
766 chunked_hash.as_ref(),
767 "Chunked (size {}) mismatch for {:?} canonicalization",
768 chunk_size,
769 canonicalization
770 );
771 assert_eq!(single_len, chunked_len);
772 }
773 }
774 }
775
776 #[test]
777 fn body_hasher_length_limit() {
778 let body = "Hello World! This is a test body.\r\n";
779
780 for canonicalization in [Canonicalization::Relaxed, Canonicalization::Simple] {
781 let mut limited_hasher = BodyHasher::new(Sha256::hasher(), canonicalization, 10);
783 limited_hasher.write(body.as_bytes());
784 let (limited_result, limited_len) = limited_hasher.finish();
785 let limited_hash = limited_result.complete();
786
787 let mut expected_hasher = Sha256::hasher();
789 CanonicalBody {
790 canonicalization,
791 body: &body.as_bytes()[..10],
792 }
793 .write(&mut expected_hasher);
794 let expected_hash = expected_hasher.complete();
795
796 assert_eq!(
797 expected_hash.as_ref(),
798 limited_hash.as_ref(),
799 "Body length limit mismatch for {:?} canonicalization",
800 canonicalization
801 );
802 assert_eq!(limited_len, 10);
803 }
804 }
805
806 #[test]
807 fn body_hasher_split_crlf() {
808 let body = "Line1\r\nLine2\r\n";
810
811 for canonicalization in [Canonicalization::Relaxed, Canonicalization::Simple] {
812 let mut single_hasher = BodyHasher::new(Sha256::hasher(), canonicalization, 0);
814 single_hasher.write(body.as_bytes());
815 let (single_result, _) = single_hasher.finish();
816 let single_hash = single_result.complete();
817
818 let mut split_hasher = BodyHasher::new(Sha256::hasher(), canonicalization, 0);
820 split_hasher.write(b"Line1\r");
821 split_hasher.write(b"\nLine2\r");
822 split_hasher.write(b"\n");
823 let (split_result, _) = split_hasher.finish();
824 let split_hash = split_result.complete();
825
826 assert_eq!(
827 single_hash.as_ref(),
828 split_hash.as_ref(),
829 "Split CRLF mismatch for {:?} canonicalization",
830 canonicalization
831 );
832 }
833 }
834}