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