Skip to main content

mail_auth/dkim/
canonicalize.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use super::{Canonicalization, Signature};
8use crate::common::{
9    crypto::HashContext,
10    headers::{HeaderStream, Writable, Writer},
11};
12
13/// Incremental body hasher for streaming DKIM signing.
14///
15/// This struct allows body content to be fed in chunks while maintaining
16/// the canonicalization state between calls.
17pub 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    /// Creates a new incremental body hasher.
28    ///
29    /// # Arguments
30    /// * `hasher` - The hash context to write canonicalized body to
31    /// * `canonicalization` - The body canonicalization algorithm to use
32    /// * `body_length_limit` - Maximum bytes to hash (0 = unlimited)
33    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    /// Feed a chunk of body data to the hasher.
45    ///
46    /// Data is canonicalized according to the configured algorithm and
47    /// written to the underlying hash context.
48    pub fn write(&mut self, chunk: &[u8]) {
49        if self.done {
50            return;
51        }
52
53        // Apply body length limit if set
54        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    /// Finalize the body hash.
70    ///
71    /// Applies the final canonicalization rules (trailing CRLF handling)
72    /// and returns the completed hash context along with the number of
73    /// body bytes that were processed.
74    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        // Add any missing headers
394        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        // Test empty body hashes
670        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        // Test that BodyHasher produces identical results to CanonicalBody
704        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            // Hash using CanonicalBody
719            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            // Hash using BodyHasher (single chunk)
728            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        // Test that chunked input produces same result as single input
746        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            // Single chunk
750            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            // Multiple chunks - split at various points
756            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            // Hash with limit of 10 bytes
782            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            // Hash the first 10 bytes using CanonicalBody
788            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        // Test that CRLF split across chunks is handled correctly
809        let body = "Line1\r\nLine2\r\n";
810
811        for canonicalization in [Canonicalization::Relaxed, Canonicalization::Simple] {
812            // Single chunk reference
813            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            // Split right in the middle of \r\n
819            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}