1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Backend {
45 X86ShaNi,
47 Aarch64Sha,
49 Portable,
51}
52
53impl Backend {
54 #[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#[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}
104const OBJECT_HEADER_BUF_LEN: usize = 6 + 1 + 20 + 1;
109
110#[derive(Debug, Clone)]
116pub enum ChecksumHasher {
117 Sha1(Sha1),
119 Sha256(Sha256),
121}
122
123impl ChecksumHasher {
124 #[must_use]
126 pub fn new(algo: HashAlgo) -> Self {
127 algo.checksum_hasher()
128 }
129
130 #[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 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 #[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 #[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#[derive(Debug, Clone)]
188pub enum ObjectHasher {
189 Sha1(Sha1dcHasher),
191 Sha256(Sha256),
193}
194
195impl ObjectHasher {
196 #[must_use]
198 pub fn new(algo: HashAlgo) -> Self {
199 algo.hasher()
200 }
201
202 #[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 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 #[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 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 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 #[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 #[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 #[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 pub fn digest(self, data: &[u8]) -> Result<ObjectId> {
331 let mut hasher = self.hasher();
332 hasher.update(data);
333 hasher.finalize()
334 }
335
336 pub fn hash_object(self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
342 hash_object(self, kind, data)
343 }
344}
345
346pub 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
365pub struct ObjectPayloadHasher {
369 inner: ObjectHasher,
370}
371
372impl ObjectPayloadHasher {
373 #[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 pub fn update(&mut self, chunk: &[u8]) {
383 self.inner.update(chunk);
384 }
385
386 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
407pub struct HashingWriter<W: Write> {
412 inner: W,
413 hasher: ChecksumHasher,
414}
415
416impl<W: Write> HashingWriter<W> {
417 #[must_use]
419 pub fn new(algo: HashAlgo, inner: W) -> Self {
420 Self {
421 inner,
422 hasher: algo.checksum_hasher(),
423 }
424 }
425
426 #[must_use]
428 pub fn get_ref(&self) -> &W {
429 &self.inner
430 }
431
432 pub fn get_mut(&mut self) -> &mut W {
434 &mut self.inner
435 }
436
437 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#[derive(Debug, Clone, PartialEq, Eq)]
467pub struct TrailerMismatch {
468 pub expected: Vec<u8>,
470 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
487pub 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
514fn 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 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 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}