Skip to main content

grit_lib/hash/
mod.rs

1//! Incremental hashing for Git object ids and file checksums.
2//!
3//! Parallel batch helpers: [`hash_objects_parallel`], [`par_hash_with`], and [`Parallelism`].
4//!
5//! This module is the single owner of the `sha1`, `sha2`, and `sha1dc` crates in
6//! `grit-lib`.
7//!
8//! **Object ids** name Git objects. SHA-1 ids are computed with [`sha1dc`] (collision
9//! detection, standard digest mode). [`ObjectHasher::finalize`], [`hash_object`], and
10//! [`HashAlgo::digest`] return [`Result`] and surface [`Error::HashCollision`] on
11//! chosen-prefix collisions. Canonical store bytes are `"<kind> <len>\\0<payload>"`;
12//! use [`HashAlgo::digest`] when hashing raw bytes that should become an [`ObjectId`]
13//! (for example verifying loose object headers).
14//!
15//! **Checksums** detect corruption in pack, index, MIDX, commit-graph, and related
16//! file trailers. They use the same digest algorithms but are not object identities.
17//! Use [`ChecksumHasher`] / [`HashAlgo::checksum_digest`], [`HashingWriter`], and
18//! [`verify_trailer`].
19
20use std::io::{self, Write};
21
22use sha1::{Digest as Sha1Digest, Sha1};
23use sha1dc::Hasher as Sha1dcHasher;
24use sha2::Sha256;
25
26use crate::error::{Error, Result};
27use crate::objects::{HashAlgo, ObjectId, ObjectKind};
28
29mod parallel;
30
31pub use parallel::{
32    hash_objects_parallel, index_parallelism_from_config, par_hash_with, parallel_hash_worthwhile,
33    try_par_hash_with, try_par_hash_with_force, ParallelHashError, Parallelism, PAR_HASH_MIN_ITEMS,
34    PAR_HASH_MIN_TOTAL_BYTES,
35};
36
37/// Which implementation the `sha1` / `sha2` dependency selects for `algo` on this CPU.
38///
39/// Values mirror the `cpufeatures` dispatch inside those crates for the digests
40/// Grit uses (SHA-1 and SHA-256). On x86_64, SHA-256 in `sha2` 0.11 only selects
41/// SHA-NI or portable code (AVX2 is used for SHA-512, not SHA-256, so there is no
42/// separate AVX2 backend variant here).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Backend {
45    /// x86_64 SHA-NI (`sha` target feature) — used for SHA-1 and SHA-256 when available.
46    X86ShaNi,
47    /// AArch64 SHA2 crypto extensions.
48    Aarch64Sha,
49    /// Portable software implementation.
50    Portable,
51}
52
53impl Backend {
54    /// Stable lowercase name for scripts and JSON (`x86_sha_ni`, `portable`, …).
55    #[must_use]
56    pub const fn as_str(self) -> &'static str {
57        match self {
58            Self::X86ShaNi => "x86_sha_ni",
59            Self::Aarch64Sha => "aarch64_sha",
60            Self::Portable => "portable",
61        }
62    }
63}
64
65impl std::fmt::Display for Backend {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.write_str(self.as_str())
68    }
69}
70
71/// Report the hashing backend used for object-id digests on this CPU.
72///
73/// SHA-1 object ids go through [`sha1dc`] (hardware SHA-1 when available). SHA-256
74/// uses the `sha2` crate. File checksums use plain accelerated SHA-1 from the `sha1`
75/// crate, not sha1dc.
76///
77/// Detection uses the same target features as those crates (`sha` on x86_64, `sha2` on
78/// AArch64). `algo` is accepted for a stable signature; both supported algorithms share
79/// the same backend on a given CPU.
80#[must_use]
81pub fn backend(algo: HashAlgo) -> Backend {
82    let _ = algo;
83    #[cfg(target_arch = "x86_64")]
84    {
85        if std::arch::is_x86_feature_detected!("sha") {
86            Backend::X86ShaNi
87        } else {
88            Backend::Portable
89        }
90    }
91    #[cfg(target_arch = "aarch64")]
92    {
93        if std::arch::is_aarch64_feature_detected!("sha2") {
94            Backend::Aarch64Sha
95        } else {
96            Backend::Portable
97        }
98    }
99    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
100    {
101        Backend::Portable
102    }
103}
104/// Maximum length of a Git object header (`"<kind> <decimal-len>\\0"`) on the stack.
105///
106/// Longest kind name is `"commit"` (6 bytes) plus space, up to 20 decimal digits
107/// for the payload length, plus the trailing NUL.
108const OBJECT_HEADER_BUF_LEN: usize = 6 + 1 + 20 + 1;
109
110/// Incremental hasher for Git file checksums (pack/index/MIDX/commit-graph trailers).
111///
112/// Checksums use the same SHA-1 or SHA-256 digests as object ids but name file
113/// integrity, not [`ObjectId`] values. Prefer this type anywhere a trailer or
114/// non-object content hash is computed or verified.
115#[derive(Debug, Clone)]
116pub enum ChecksumHasher {
117    /// SHA-1 (20-byte digests).
118    Sha1(Sha1),
119    /// SHA-256 (32-byte digests).
120    Sha256(Sha256),
121}
122
123impl ChecksumHasher {
124    /// Create a fresh checksum hasher for `algo`.
125    #[must_use]
126    pub fn new(algo: HashAlgo) -> Self {
127        algo.checksum_hasher()
128    }
129
130    /// The hash algorithm this hasher produces.
131    #[must_use]
132    pub fn algo(self: &ChecksumHasher) -> HashAlgo {
133        match self {
134            Self::Sha1(_) => HashAlgo::Sha1,
135            Self::Sha256(_) => HashAlgo::Sha256,
136        }
137    }
138
139    /// Feed more bytes into the digest.
140    pub fn update(&mut self, data: &[u8]) {
141        match self {
142            Self::Sha1(h) => h.update(data),
143            Self::Sha256(h) => h.update(data),
144        }
145    }
146
147    /// Finish hashing and return the raw digest bytes.
148    #[must_use]
149    pub fn finalize(self) -> Vec<u8> {
150        match self {
151            Self::Sha1(h) => h.finalize().to_vec(),
152            Self::Sha256(h) => h.finalize().to_vec(),
153        }
154    }
155
156    /// Write the raw digest into `out` and return the number of bytes written.
157    ///
158    /// # Panics
159    ///
160    /// Panics when `out` is shorter than the digest length for this algorithm.
161    #[must_use]
162    pub fn finalize_into(self, out: &mut [u8]) -> usize {
163        let digest = self.finalize();
164        let len = digest.len();
165        assert!(
166            out.len() >= len,
167            "checksum digest buffer too short: need {len} bytes, have {}",
168            out.len()
169        );
170        out[..len].copy_from_slice(&digest);
171        len
172    }
173}
174
175impl Write for ChecksumHasher {
176    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
177        self.update(buf);
178        Ok(buf.len())
179    }
180
181    fn flush(&mut self) -> io::Result<()> {
182        Ok(())
183    }
184}
185
186/// Incremental hasher for Git's supported object-id algorithms.
187#[derive(Debug, Clone)]
188pub enum ObjectHasher {
189    /// SHA-1 (20-byte digests) with sha1dc collision detection.
190    Sha1(Sha1dcHasher),
191    /// SHA-256 (32-byte digests).
192    Sha256(Sha256),
193}
194
195impl ObjectHasher {
196    /// Create a fresh hasher for `algo`.
197    #[must_use]
198    pub fn new(algo: HashAlgo) -> Self {
199        algo.hasher()
200    }
201
202    /// The hash algorithm this hasher produces.
203    #[must_use]
204    pub fn algo(self: &ObjectHasher) -> HashAlgo {
205        match self {
206            Self::Sha1(_) => HashAlgo::Sha1,
207            Self::Sha256(_) => HashAlgo::Sha256,
208        }
209    }
210
211    /// Feed more bytes into the digest.
212    pub fn update(&mut self, data: &[u8]) {
213        match self {
214            Self::Sha1(h) => h.update(data),
215            Self::Sha256(h) => h.update(data),
216        }
217    }
218
219    /// Create a hasher that already consumed the Git object header for `kind` and `len`.
220    ///
221    /// The header is written into a stack buffer (no heap allocation).
222    #[must_use]
223    pub fn for_object(algo: HashAlgo, kind: ObjectKind, len: u64) -> Self {
224        let mut hasher = Self::new(algo);
225        let mut header = [0u8; OBJECT_HEADER_BUF_LEN];
226        let header_len = write_object_header(&mut header, kind, len);
227        hasher.update(&header[..header_len]);
228        hasher
229    }
230
231    /// Finish hashing and return the object id.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`Error::HashCollision`] when sha1dc detects a chosen-prefix collision.
236    pub fn finalize(self) -> Result<ObjectId> {
237        match self {
238            Self::Sha1(h) => match h.finalize() {
239                Ok(digest) => Ok(object_id_from_sha1_digest(&digest)),
240                Err(collision) => Err(collision_to_error(collision)),
241            },
242            Self::Sha256(h) => Ok(ObjectId::from_bytes(h.finalize().as_slice())
243                .unwrap_or_else(|_| unreachable!("SHA-256 digest is 32 bytes"))),
244        }
245    }
246
247    /// Write the raw digest into `out` and return the number of bytes written.
248    ///
249    /// # Errors
250    ///
251    /// Returns [`Error::InvalidObjectId`] when `out` is shorter than the digest
252    /// length for this algorithm (a diagnostic string names the required size).
253    pub fn finalize_into(self, out: &mut [u8]) -> Result<usize> {
254        match self {
255            Self::Sha1(h) => {
256                let oid = match h.finalize() {
257                    Ok(digest) => object_id_from_sha1_digest(&digest),
258                    Err(collision) => return Err(collision_to_error(collision)),
259                };
260                let bytes = oid.as_bytes();
261                if out.len() < bytes.len() {
262                    return Err(Error::InvalidObjectId(format!(
263                        "digest buffer too short: need {} bytes, have {}",
264                        bytes.len(),
265                        out.len()
266                    )));
267                }
268                out[..bytes.len()].copy_from_slice(bytes);
269                Ok(bytes.len())
270            }
271            Self::Sha256(h) => {
272                let digest = h.finalize();
273                let len = digest.len();
274                if out.len() < len {
275                    return Err(Error::InvalidObjectId(format!(
276                        "digest buffer too short: need {len} bytes, have {}",
277                        out.len()
278                    )));
279                }
280                out[..len].copy_from_slice(&digest);
281                Ok(len)
282            }
283        }
284    }
285}
286
287impl Write for ObjectHasher {
288    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
289        self.update(buf);
290        Ok(buf.len())
291    }
292
293    fn flush(&mut self) -> io::Result<()> {
294        Ok(())
295    }
296}
297
298impl HashAlgo {
299    /// Return a fresh incremental checksum hasher for this algorithm.
300    #[must_use]
301    pub fn checksum_hasher(self) -> ChecksumHasher {
302        match self {
303            Self::Sha1 => ChecksumHasher::Sha1(Sha1::new()),
304            Self::Sha256 => ChecksumHasher::Sha256(Sha256::new()),
305        }
306    }
307
308    /// Hash a byte slice with no Git object header and return a file checksum.
309    #[must_use]
310    pub fn checksum_digest(self, data: &[u8]) -> Vec<u8> {
311        let mut hasher = self.checksum_hasher();
312        hasher.update(data);
313        hasher.finalize()
314    }
315
316    /// Return a fresh incremental hasher for this algorithm.
317    #[must_use]
318    pub fn hasher(self) -> ObjectHasher {
319        match self {
320            Self::Sha1 => ObjectHasher::Sha1(Sha1dcHasher::new()),
321            Self::Sha256 => ObjectHasher::Sha256(Sha256::new()),
322        }
323    }
324
325    /// Hash a byte slice with no Git object header.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`Error::HashCollision`] when sha1dc detects a chosen-prefix collision.
330    pub fn digest(self, data: &[u8]) -> Result<ObjectId> {
331        let mut hasher = self.hasher();
332        hasher.update(data);
333        hasher.finalize()
334    }
335
336    /// Hash canonical Git store bytes for `kind` and `data`.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`Error::HashCollision`] when sha1dc detects a chosen-prefix collision.
341    pub fn hash_object(self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
342        hash_object(self, kind, data)
343    }
344}
345
346/// Hash canonical Git store bytes for `kind` and `data`.
347///
348/// Equivalent to [`HashAlgo::hash_object`] but accepts the algorithm as the
349/// first argument.
350///
351/// Detection runs over the bytes that form the object id (header and payload), as in
352/// Git. A payload that only collides when hashed on its own, such as the SHAttered
353/// PDFs stored as blobs in Git's own history, is an ordinary object.
354///
355/// # Errors
356///
357/// Returns [`Error::HashCollision`] when sha1dc detects a chosen-prefix collision.
358pub fn hash_object(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
359    let len = u64::try_from(data.len()).unwrap_or(u64::MAX);
360    let mut hasher = ObjectHasher::for_object(algo, kind, len);
361    hasher.update(data);
362    hasher.finalize()
363}
364
365/// Incremental Git object-id hash (header plus payload chunks).
366///
367/// Use this instead of constructing [`ObjectHasher`] outside the hash module.
368pub struct ObjectPayloadHasher {
369    inner: ObjectHasher,
370}
371
372impl ObjectPayloadHasher {
373    /// Start hashing canonical store bytes for `kind` with payload length `len`.
374    #[must_use]
375    pub fn new(algo: HashAlgo, kind: ObjectKind, len: u64) -> Self {
376        Self {
377            inner: ObjectHasher::for_object(algo, kind, len),
378        }
379    }
380
381    /// Feed more decompressed payload bytes.
382    pub fn update(&mut self, chunk: &[u8]) {
383        self.inner.update(chunk);
384    }
385
386    /// Finish and return the object id.
387    ///
388    /// # Errors
389    ///
390    /// Returns [`Error::HashCollision`] when sha1dc detects a chosen-prefix collision.
391    pub fn finish(self) -> Result<ObjectId> {
392        self.inner.finalize()
393    }
394}
395
396fn object_id_from_sha1_digest(digest: &sha1dc::Digest) -> ObjectId {
397    ObjectId::from_bytes(digest.as_ref())
398        .unwrap_or_else(|_| unreachable!("SHA-1 digest is 20 bytes"))
399}
400
401fn collision_to_error(collision: sha1dc::Collision) -> Error {
402    Error::HashCollision {
403        oid: object_id_from_sha1_digest(&collision.digest()),
404    }
405}
406
407/// A writer that forwards bytes to an inner sink while accumulating a checksum.
408///
409/// Call [`Self::finish`] to append the raw digest trailer to the inner writer
410/// and obtain the digest as an [`ObjectId`].
411pub struct HashingWriter<W: Write> {
412    inner: W,
413    hasher: ChecksumHasher,
414}
415
416impl<W: Write> HashingWriter<W> {
417    /// Wrap `inner`, hashing every byte written through this adapter.
418    #[must_use]
419    pub fn new(algo: HashAlgo, inner: W) -> Self {
420        Self {
421            inner,
422            hasher: algo.checksum_hasher(),
423        }
424    }
425
426    /// Borrow the inner writer without finishing the checksum.
427    #[must_use]
428    pub fn get_ref(&self) -> &W {
429        &self.inner
430    }
431
432    /// Mutably borrow the inner writer without finishing the checksum.
433    pub fn get_mut(&mut self) -> &mut W {
434        &mut self.inner
435    }
436
437    /// Append the checksum trailer to the inner writer and return it with the digest.
438    ///
439    /// # Errors
440    ///
441    /// Propagates I/O errors from writing the trailer to `inner`.
442    pub fn finish(mut self) -> io::Result<(W, ObjectId)> {
443        let digest_bytes = self.hasher.finalize();
444        self.inner.write_all(&digest_bytes)?;
445        let digest = ObjectId::from_bytes(&digest_bytes)
446            .unwrap_or_else(|_| unreachable!("checksum digest length matches object id for algo"));
447        Ok((self.inner, digest))
448    }
449}
450
451impl<W: Write> Write for HashingWriter<W> {
452    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
453        let n = self.inner.write(buf)?;
454        if n > 0 {
455            self.hasher.update(&buf[..n]);
456        }
457        Ok(n)
458    }
459
460    fn flush(&mut self) -> io::Result<()> {
461        self.inner.flush()
462    }
463}
464
465/// Checksum at the end of `bytes` did not match a digest of the leading body.
466#[derive(Debug, Clone, PartialEq, Eq)]
467pub struct TrailerMismatch {
468    /// Digest computed over the body (everything before the trailer).
469    pub expected: Vec<u8>,
470    /// Trailing bytes found in `bytes`.
471    pub found: Vec<u8>,
472}
473
474impl std::fmt::Display for TrailerMismatch {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        write!(
477            f,
478            "trailer checksum mismatch (expected {}, found {})",
479            hex::encode(&self.expected),
480            hex::encode(&self.found)
481        )
482    }
483}
484
485impl std::error::Error for TrailerMismatch {}
486
487/// Verify that `bytes` ends with the hash of the preceding body.
488///
489/// # Errors
490///
491/// Returns [`TrailerMismatch`] when the trailer does not match, or when `bytes`
492/// is shorter than one digest for `algo`.
493pub fn verify_trailer(algo: HashAlgo, bytes: &[u8]) -> std::result::Result<(), TrailerMismatch> {
494    let trailer_len = algo.len();
495    if bytes.len() < trailer_len {
496        return Err(TrailerMismatch {
497            expected: Vec::new(),
498            found: bytes.to_vec(),
499        });
500    }
501    let split = bytes.len() - trailer_len;
502    let (body, trailer) = bytes.split_at(split);
503    let expected = algo.checksum_digest(body);
504    if expected.as_slice() == trailer {
505        Ok(())
506    } else {
507        Err(TrailerMismatch {
508            expected,
509            found: trailer.to_vec(),
510        })
511    }
512}
513
514/// Write `"<kind> <len>\\0"` into `buf` and return the number of bytes used.
515fn write_object_header(buf: &mut [u8], kind: ObjectKind, len: u64) -> usize {
516    let kind_bytes = kind.as_str().as_bytes();
517    let mut pos = 0;
518    buf[pos..pos + kind_bytes.len()].copy_from_slice(kind_bytes);
519    pos += kind_bytes.len();
520    buf[pos] = b' ';
521    pos += 1;
522    pos += write_decimal(&mut buf[pos..], len);
523    buf[pos] = 0;
524    pos + 1
525}
526
527fn write_decimal(out: &mut [u8], mut n: u64) -> usize {
528    if n == 0 {
529        out[0] = b'0';
530        return 1;
531    }
532    let mut tmp = [0u8; 20];
533    let mut digits = 0usize;
534    while n > 0 {
535        tmp[digits] = b'0' + (n % 10) as u8;
536        digits += 1;
537        n /= 10;
538    }
539    for i in 0..digits {
540        out[i] = tmp[digits - 1 - i];
541    }
542    digits
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use std::io::Cursor;
549
550    fn hex_oid(_algo: HashAlgo, hex: &str) -> ObjectId {
551        ObjectId::from_hex(hex).expect("test vector hex")
552    }
553
554    fn assert_digest(algo: HashAlgo, data: &[u8], expected_hex: &str) {
555        let got = algo.digest(data).expect("digest");
556        assert_eq!(got, hex_oid(algo, expected_hex), "digest mismatch");
557    }
558
559    fn assert_checksum_digest(algo: HashAlgo, data: &[u8], expected_hex: &str) {
560        let got = algo.checksum_digest(data);
561        assert_eq!(hex::encode(&got), expected_hex, "checksum digest mismatch");
562    }
563
564    #[test]
565    fn checksum_hasher_nist_vectors_sha1_sha256() {
566        assert_checksum_digest(
567            HashAlgo::Sha1,
568            b"",
569            "da39a3ee5e6b4b0d3255bfef95601890afd80709",
570        );
571        assert_checksum_digest(
572            HashAlgo::Sha1,
573            b"abc",
574            "a9993e364706816aba3e25717850c26c9cd0d89d",
575        );
576        assert_checksum_digest(
577            HashAlgo::Sha256,
578            b"",
579            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
580        );
581        assert_checksum_digest(
582            HashAlgo::Sha256,
583            b"abc",
584            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
585        );
586    }
587
588    #[test]
589    fn checksum_hasher_matches_object_hasher_digest() {
590        let data: Vec<u8> = (0..512).map(|i| (i % 251) as u8).collect();
591        for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
592            let mut checksum = algo.checksum_hasher();
593            checksum.update(&data);
594            let checksum_bytes = checksum.finalize();
595            assert_eq!(
596                checksum_bytes,
597                algo.digest(&data).expect("digest").as_bytes()
598            );
599        }
600    }
601
602    #[test]
603    fn hashing_writer_uses_checksum_hasher() {
604        let body = b"streaming checksum body";
605        let mut buf = Vec::new();
606        {
607            let mut w = HashingWriter::new(HashAlgo::Sha256, Cursor::new(&mut buf));
608            w.write_all(body).unwrap();
609            let (_, digest) = w.finish().unwrap();
610            assert_eq!(
611                digest.as_bytes(),
612                HashAlgo::Sha256.checksum_digest(body).as_slice()
613            );
614        }
615        verify_trailer(HashAlgo::Sha256, &buf).expect("trailer valid");
616    }
617
618    #[test]
619    fn sha1_nist_vectors() {
620        assert_digest(
621            HashAlgo::Sha1,
622            b"",
623            "da39a3ee5e6b4b0d3255bfef95601890afd80709",
624        );
625        assert_digest(
626            HashAlgo::Sha1,
627            b"abc",
628            "a9993e364706816aba3e25717850c26c9cd0d89d",
629        );
630        assert_digest(
631            HashAlgo::Sha1,
632            b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
633            "84983e441c3bd26ebaae4aa1f95129e5e54670f1",
634        );
635        let million_a = vec![b'a'; 1_000_000];
636        assert_digest(
637            HashAlgo::Sha1,
638            &million_a,
639            "34aa973cd4c4daa4f61eeb2bdbad27316534016f",
640        );
641    }
642
643    #[test]
644    fn sha256_nist_vectors() {
645        assert_digest(
646            HashAlgo::Sha256,
647            b"",
648            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
649        );
650        assert_digest(
651            HashAlgo::Sha256,
652            b"abc",
653            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
654        );
655        assert_digest(
656            HashAlgo::Sha256,
657            b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
658            "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
659        );
660        let million_a = vec![b'a'; 1_000_000];
661        assert_digest(
662            HashAlgo::Sha256,
663            &million_a,
664            "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0",
665        );
666    }
667
668    #[test]
669    fn incremental_matches_oneshot_random_splits() {
670        let data: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
671        for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
672            let oneshot = algo.digest(&data).expect("digest");
673            let mut state = 0xdead_beef_u64;
674            let mut pos = 0usize;
675            let mut hasher = algo.hasher();
676            while pos < data.len() {
677                state = state
678                    .wrapping_mul(6364136223846793005)
679                    .wrapping_add(1442695040888963407);
680                let remaining = data.len() - pos;
681                let chunk = if remaining == 1 {
682                    1
683                } else {
684                    (state as usize % remaining).max(1)
685                };
686                hasher.update(&data[pos..pos + chunk]);
687                pos += chunk;
688            }
689            assert_eq!(hasher.finalize().expect("finalize"), oneshot);
690        }
691    }
692
693    #[test]
694    fn object_header_encoding() {
695        let kinds = [
696            (ObjectKind::Blob, "blob"),
697            (ObjectKind::Tree, "tree"),
698            (ObjectKind::Commit, "commit"),
699            (ObjectKind::Tag, "tag"),
700        ];
701        let lens = [0_u64, 1, 4_294_967_297];
702        let cases: Vec<(ObjectKind, u64, String)> = kinds
703            .iter()
704            .flat_map(|&(kind, name)| {
705                lens.iter()
706                    .map(move |&len| (kind, len, format!("{name} {len}\0")))
707            })
708            .collect();
709        for (kind, len, expect) in &cases {
710            let mut buf = [0u8; OBJECT_HEADER_BUF_LEN];
711            let n = write_object_header(&mut buf, *kind, *len);
712            assert_eq!(
713                &buf[..n],
714                expect.as_bytes(),
715                "header for {kind:?} len {len}"
716            );
717        }
718    }
719
720    #[test]
721    fn hash_object_all_kinds() {
722        for kind in [
723            ObjectKind::Blob,
724            ObjectKind::Tree,
725            ObjectKind::Commit,
726            ObjectKind::Tag,
727        ] {
728            for payload in [b"" as &[u8], b"x"] {
729                let mut hasher =
730                    ObjectHasher::for_object(HashAlgo::Sha1, kind, payload.len() as u64);
731                hasher.update(payload);
732                assert_eq!(
733                    hash_object(HashAlgo::Sha1, kind, payload).expect("hash"),
734                    hasher.finalize().expect("finalize")
735                );
736            }
737        }
738    }
739
740    /// Inner writer that accepts at most `max` bytes per `write` call.
741    struct PartialWrite {
742        max: usize,
743        buf: Vec<u8>,
744        fail: bool,
745    }
746
747    impl Write for PartialWrite {
748        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
749            if self.fail {
750                return Err(io::Error::new(
751                    io::ErrorKind::Other,
752                    "simulated write failure",
753                ));
754            }
755            let n = buf.len().min(self.max);
756            self.buf.extend_from_slice(&buf[..n]);
757            Ok(n)
758        }
759
760        fn flush(&mut self) -> io::Result<()> {
761            Ok(())
762        }
763    }
764
765    #[test]
766    fn hashing_writer_partial_write_checksum() {
767        let inner = PartialWrite {
768            max: 2,
769            buf: Vec::new(),
770            fail: false,
771        };
772        let mut w = HashingWriter::new(HashAlgo::Sha1, inner);
773        let n = w.write(b"abcdef").expect("write");
774        assert_eq!(n, 2);
775        assert_eq!(w.get_ref().buf, b"ab");
776        let (_, digest) = w.finish().expect("finish");
777        assert_eq!(
778            digest.as_bytes(),
779            HashAlgo::Sha1.checksum_digest(b"ab").as_slice()
780        );
781        assert_ne!(
782            digest.as_bytes(),
783            HashAlgo::Sha1.checksum_digest(b"abcdef").as_slice()
784        );
785    }
786
787    #[test]
788    fn hashing_writer_write_error_does_not_update_checksum() {
789        let inner = PartialWrite {
790            max: 64,
791            buf: Vec::new(),
792            fail: true,
793        };
794        let mut w = HashingWriter::new(HashAlgo::Sha1, inner);
795        assert!(w.write(b"abcdef").is_err());
796        w.get_mut().fail = false;
797        w.write_all(b"ab").expect("write");
798        let (_, digest) = w.finish().expect("finish");
799        assert_eq!(
800            digest.as_bytes(),
801            HashAlgo::Sha1.checksum_digest(b"ab").as_slice()
802        );
803    }
804
805    #[test]
806    fn hashing_writer_trailer_matches_digest() {
807        let body = b"index payload bytes";
808        let mut buf = Vec::new();
809        {
810            let mut w = HashingWriter::new(HashAlgo::Sha1, Cursor::new(&mut buf));
811            w.write_all(body).unwrap();
812            let (_, digest) = w.finish().unwrap();
813            assert_eq!(
814                digest.as_bytes(),
815                HashAlgo::Sha1.checksum_digest(body).as_slice()
816            );
817            assert_eq!(&buf[body.len()..], digest.as_bytes());
818        }
819        verify_trailer(HashAlgo::Sha1, &buf).expect("trailer valid");
820    }
821
822    #[test]
823    fn verify_trailer_mismatch_is_typed() {
824        let body = b"payload";
825        let mut file = body.to_vec();
826        file.extend_from_slice(&[0u8; 20]);
827        let err = verify_trailer(HashAlgo::Sha1, &file).unwrap_err();
828        assert!(matches!(err, TrailerMismatch { .. }));
829        assert_ne!(err.expected, err.found);
830    }
831
832    #[test]
833    fn finalize_into_and_write_trait() {
834        let mut hasher = HashAlgo::Sha256.hasher();
835        hasher.write_all(b"via write trait").unwrap();
836        let mut out = [0u8; 32];
837        let n = hasher.finalize_into(&mut out).unwrap();
838        assert_eq!(n, 32);
839        assert_eq!(
840            HashAlgo::Sha256.digest(b"via write trait").expect("digest"),
841            ObjectId::from_bytes(&out).unwrap()
842        );
843    }
844
845    #[test]
846    fn backend_matches_cpu() {
847        let expected = {
848            #[cfg(target_arch = "x86_64")]
849            {
850                if std::arch::is_x86_feature_detected!("sha") {
851                    Backend::X86ShaNi
852                } else {
853                    Backend::Portable
854                }
855            }
856            #[cfg(target_arch = "aarch64")]
857            {
858                if std::arch::is_aarch64_feature_detected!("sha2") {
859                    Backend::Aarch64Sha
860                } else {
861                    Backend::Portable
862                }
863            }
864            #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
865            {
866                Backend::Portable
867            }
868        };
869        assert_eq!(super::backend(HashAlgo::Sha1), expected);
870    }
871
872    #[test]
873    fn sha1_object_hasher_matches_sha1_crate_random() {
874        let mut state = 0x9e37_79b9_u64;
875        for len in [0_usize, 1, 63, 64, 65, 127, 128, 129, 512, 4096, 8192] {
876            let mut data = vec![0_u8; len];
877            for b in &mut data {
878                state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
879                *b = (state >> 32) as u8;
880            }
881            let mut plain = Sha1::new();
882            plain.update(&data);
883            let expected = plain.finalize();
884            let mut hasher = ObjectHasher::new(HashAlgo::Sha1);
885            hasher.update(&data);
886            assert_eq!(
887                hasher.finalize().expect("finalize").as_bytes(),
888                expected.as_slice(),
889                "len {len}"
890            );
891        }
892    }
893
894    #[test]
895    fn finalize_detects_shattered_prefix() {
896        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data");
897        for name in ["shattered-1-prefix.bin", "shattered-2-prefix.bin"] {
898            let bytes = std::fs::read(dir.join(name)).expect("fixture");
899            let mut hasher = ObjectHasher::new(HashAlgo::Sha1);
900            hasher.update(&bytes);
901            let err = hasher.finalize().expect_err("SHAttered block must collide");
902            assert!(matches!(err, Error::HashCollision { .. }));
903            assert!(matches!(
904                HashAlgo::Sha1.digest(&bytes),
905                Err(Error::HashCollision { .. })
906            ));
907
908            // As a blob, the header shifts the colliding blocks: the object id is an
909            // ordinary SHA-1, exactly what Git computes for these files.
910            let mut plain = Sha1::new();
911            plain.update(format!("blob {}\0", bytes.len()).as_bytes());
912            plain.update(&bytes);
913            let oid = hash_object(HashAlgo::Sha1, ObjectKind::Blob, &bytes)
914                .expect("blob holding a SHAttered prefix is not a collision");
915            assert_eq!(oid.as_bytes(), plain.finalize().as_slice());
916        }
917    }
918
919    #[test]
920    fn sha256_finalize_never_collides() {
921        let data: Vec<u8> = (0..1024).map(|i| (i % 251) as u8).collect();
922        let mut hasher = ObjectHasher::new(HashAlgo::Sha256);
923        hasher.update(&data);
924        hasher
925            .finalize()
926            .expect("SHA-256 has no collision detection");
927    }
928
929    #[test]
930    fn sha256_backend_matches_sha2_dispatcher() {
931        #[cfg(target_arch = "x86_64")]
932        {
933            let got = super::backend(HashAlgo::Sha256);
934            if std::arch::is_x86_feature_detected!("sha") {
935                assert_eq!(got, Backend::X86ShaNi);
936            } else {
937                assert_eq!(got, Backend::Portable);
938            }
939        }
940    }
941}