1use std::borrow::Cow;
14use std::collections::{HashMap, HashSet};
15use std::io::{self, Read};
16
17use crate::error::{Error, Result};
18use crate::gitmodules::{self, DotFsckTracker};
19use crate::hash::{ChecksumHasher, ObjectPayloadHasher};
20use crate::index::MODE_GITLINK;
21use crate::index_pack::ObjectKindCounts;
22use crate::objects::{parse_commit, parse_tag, parse_tree, HashAlgo, Object, ObjectId, ObjectKind};
23use crate::odb::store::{ObjectStore, WritableObjectStore};
24use crate::odb::{Odb, WriteOptions};
25use crate::progress::{poll_cancel, Cancel, NullProgress, ProgressPhase, ProgressSink};
26use flate2::read::ZlibDecoder;
27use flate2::{Decompress, FlushDecompress, Status};
28
29#[derive(Debug, Default)]
31pub struct UnpackOptions {
32 pub dry_run: bool,
34 pub quiet: bool,
36 pub strict: bool,
38 pub allowed_missing: HashSet<ObjectId>,
40 pub allow_promisor_missing_references: bool,
42 pub max_input_bytes: Option<u64>,
47 pub shallow_boundaries: HashSet<ObjectId>,
53}
54
55struct PendingDelta {
57 offset: usize,
60 base_oid: Option<ObjectId>,
62 base_offset: Option<usize>,
64 delta_data: Vec<u8>,
66}
67
68#[derive(Debug, Clone, Default)]
70pub struct IngestedObjects {
71 pub object_ids: HashSet<ObjectId>,
73 pub object_counts: ObjectKindCounts,
75 pub pack_bytes: u64,
77}
78
79trait UnpackSink {
81 fn hash_algo(&self) -> HashAlgo;
82
83 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId>;
85
86 fn supports_streaming_inflate(&self) -> bool {
88 false
89 }
90
91 fn write_inflated_from_pack(
93 &self,
94 kind: ObjectKind,
95 size: usize,
96 rd: &mut StreamingPackReader<'_>,
97 dry_run: bool,
98 ) -> Result<ObjectId> {
99 let _ = (kind, size, rd, dry_run);
100 Err(Error::UnsupportedObjectStore {
101 operation: "write_inflated_from_pack",
102 })
103 }
104
105 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>>;
107
108 fn contains(&self, oid: &ObjectId) -> Result<bool>;
110}
111
112impl UnpackSink for Odb {
113 fn hash_algo(&self) -> HashAlgo {
114 Odb::hash_algo(self)
115 }
116
117 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
118 if dry_run {
119 return self.hash(kind, data);
120 }
121 if !self.uses_files_primary() {
122 return Odb::write(self, kind, data);
123 }
124 self.write_local(kind, data)
127 }
128
129 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
130 match self.read(oid) {
131 Ok(obj) => Ok(Some(obj)),
132 Err(Error::ObjectNotFound(_)) => Ok(None),
133 Err(e) => Err(e),
134 }
135 }
136
137 fn contains(&self, oid: &ObjectId) -> Result<bool> {
138 Odb::contains(self, oid)
139 }
140
141 fn supports_streaming_inflate(&self) -> bool {
142 self.uses_files_primary()
143 }
144
145 fn write_inflated_from_pack(
146 &self,
147 kind: ObjectKind,
148 size: usize,
149 rd: &mut StreamingPackReader<'_>,
150 dry_run: bool,
151 ) -> Result<ObjectId> {
152 let size_u64 = u64::try_from(size).map_err(|_| {
153 Error::CorruptObject(format!("object size {size} exceeds address space"))
154 })?;
155 if dry_run {
156 let mut hasher = ObjectPayloadHasher::new(self.hash_algo(), kind, size_u64);
157 rd.decompress_into(size, |chunk| {
158 hasher.update(chunk);
159 Ok(())
160 })?;
161 return hasher.finish();
162 }
163 if !self.uses_files_primary() {
164 let data = rd.decompress(size)?;
165 return self.write_local(kind, &data);
166 }
167 self.write_local_stream_with(kind, size_u64, |chunk_sink| {
168 rd.decompress_into(size, chunk_sink)
169 })
170 }
171}
172
173struct StoreSink<'a> {
175 store: &'a dyn WritableObjectStore,
176 bases: &'a dyn ObjectStore,
177}
178
179impl UnpackSink for StoreSink<'_> {
180 fn hash_algo(&self) -> HashAlgo {
181 self.store.hash_algo()
182 }
183
184 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
185 if dry_run {
186 return crate::hash::hash_object(self.store.hash_algo(), kind, data);
187 }
188 self.store.write(kind, data, WriteOptions::default())
189 }
190
191 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
192 if let Some(obj) = self.store.read(oid)? {
193 return Ok(Some(obj));
194 }
195 self.bases.read(oid)
196 }
197
198 fn contains(&self, oid: &ObjectId) -> Result<bool> {
199 Ok(self.store.contains(oid)? || self.bases.contains(oid)?)
200 }
201}
202
203pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> Result<usize> {
223 unpack_into(reader, odb, opts, None, None).map(|unpacked| unpacked.count)
224}
225
226pub fn ingest_pack_into(
293 reader: &mut dyn Read,
294 store: &dyn WritableObjectStore,
295 bases: &dyn ObjectStore,
296 opts: &UnpackOptions,
297) -> Result<IngestedObjects> {
298 ingest_pack_into_with_progress(reader, store, bases, opts, None, None)
299}
300
301pub fn ingest_pack_into_with_progress(
309 reader: &mut dyn Read,
310 store: &dyn WritableObjectStore,
311 bases: &dyn ObjectStore,
312 opts: &UnpackOptions,
313 progress: Option<&mut dyn ProgressSink>,
314 cancel: Option<&dyn Cancel>,
315) -> Result<IngestedObjects> {
316 unpack_into(reader, &StoreSink { store, bases }, opts, progress, cancel)
317 .map(Unpacked::into_ingested)
318}
319
320pub(crate) fn ingest_pack_into_odb(
323 reader: &mut dyn Read,
324 odb: &Odb,
325 opts: &UnpackOptions,
326 progress: Option<&mut dyn ProgressSink>,
327 cancel: Option<&dyn Cancel>,
328) -> Result<IngestedObjects> {
329 unpack_into(reader, odb, opts, progress, cancel).map(Unpacked::into_ingested)
330}
331
332struct Unpacked {
334 count: usize,
335 by_oid: HashMap<ObjectId, PackedObjectEntry>,
336 object_counts: ObjectKindCounts,
337 pack_bytes: u64,
338}
339
340impl Unpacked {
341 fn into_ingested(self) -> IngestedObjects {
342 IngestedObjects {
343 object_ids: self.by_oid.into_keys().collect(),
344 object_counts: self.object_counts,
345 pack_bytes: self.pack_bytes,
346 }
347 }
348}
349
350fn poll_unpack_cancel(
351 progress: &mut Option<&mut dyn ProgressSink>,
352 cancel: Option<&dyn Cancel>,
353) -> Result<()> {
354 let Some(cancel) = cancel else {
355 return Ok(());
356 };
357 match progress {
358 Some(p) => poll_cancel(&mut **p, cancel),
359 None => poll_cancel(&mut NullProgress, cancel),
360 }
361}
362
363fn unpack_into<S: UnpackSink + ?Sized>(
364 reader: &mut dyn Read,
365 sink: &S,
366 opts: &UnpackOptions,
367 mut progress: Option<&mut dyn ProgressSink>,
368 cancel: Option<&dyn Cancel>,
369) -> Result<Unpacked> {
370 const MAX_RETAIN_BYTES: usize = 1024 * 1024;
374
375 let algo = sink.hash_algo();
376 let mut rd = StreamingPackReader::new(reader, opts.max_input_bytes, algo);
377
378 let sig = rd.read_exact_n(4)?;
380 if sig != b"PACK" {
381 return Err(Error::CorruptObject(
382 "not a pack stream: invalid signature".to_owned(),
383 ));
384 }
385 let version = rd.read_u32_be()?;
386 if version != 2 && version != 3 {
387 return Err(Error::CorruptObject(format!(
388 "unsupported pack version {version}"
389 )));
390 }
391 let nr_objects = rd.read_u32_be()? as usize;
392
393 let mut by_offset: HashMap<usize, PackedObjectEntry> = HashMap::new();
395 let mut by_oid: HashMap<ObjectId, PackedObjectEntry> = HashMap::new();
397
398 let mut pending: Vec<PendingDelta> = Vec::new();
399 let mut count = 0usize;
400 let mut object_counts = ObjectKindCounts::default();
401
402 if let Some(p) = progress.as_mut() {
403 p.start(ProgressPhase::IndexingObjects, Some(nr_objects as u64));
404 }
405 for _ in 0..nr_objects {
406 poll_unpack_cancel(&mut progress, cancel)?;
407 let obj_offset = rd.stream_pos();
408 let (type_code, size) = rd.read_type_size()?;
409
410 match type_code {
411 1..=4 => {
412 let kind = type_code_to_kind(type_code)?;
413 let (oid, entry) = if !opts.dry_run
414 && size > MAX_RETAIN_BYTES
415 && kind == ObjectKind::Blob
416 && sink.supports_streaming_inflate()
417 {
418 let oid = sink.write_inflated_from_pack(kind, size, &mut rd, false)?;
419 (oid, PackedObjectEntry::BlobOnDisk { oid })
420 } else {
421 let data = rd.decompress(size)?;
422 let oid = sink.write(kind, &data, opts.dry_run)?;
423 (
424 oid,
425 packed_entry_after_write(kind, data, oid, opts, MAX_RETAIN_BYTES),
426 )
427 };
428 by_offset.insert(obj_offset, entry.clone());
429 by_oid.insert(oid, entry);
430 object_counts.add(kind);
431 count += 1;
432 }
433 6 => {
434 let neg = rd.read_ofs_neg_offset()?;
436 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
437 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
438 })?;
439 let delta_data = rd.decompress(size)?;
440 pending.push(PendingDelta {
441 offset: obj_offset,
442 base_oid: None,
443 base_offset: Some(base_offset),
444 delta_data,
445 });
446 }
447 7 => {
448 let base_bytes = rd.read_exact_n(algo.len())?;
450 let base_oid = ObjectId::from_bytes(&base_bytes)?;
451 let delta_data = rd.decompress(size)?;
452 pending.push(PendingDelta {
453 offset: obj_offset,
454 base_oid: Some(base_oid),
455 base_offset: None,
456 delta_data,
457 });
458 }
459 other => {
460 return Err(Error::CorruptObject(format!(
461 "unknown packed-object type {other}"
462 )))
463 }
464 }
465 if let Some(p) = progress.as_mut() {
466 p.inc(1);
467 }
468 }
469 if let Some(p) = progress.as_mut() {
470 p.finish();
471 }
472
473 let digest = rd.finalize_hasher();
475 let trailing = rd.read_trailer()?;
476 if digest != trailing {
477 return Err(Error::CorruptObject(
478 "pack trailing checksum mismatch".to_owned(),
479 ));
480 }
481 let pack_bytes = rd.stream_pos() as u64;
482
483 let mut remaining = pending;
486 if let (Some(p), false) = (progress.as_mut(), remaining.is_empty()) {
487 p.start(ProgressPhase::ResolvingDeltas, Some(remaining.len() as u64));
488 }
489 let resolving = !remaining.is_empty();
490 loop {
491 if remaining.is_empty() {
492 break;
493 }
494 poll_unpack_cancel(&mut progress, cancel)?;
495 let before = remaining.len();
496 let mut still_pending: Vec<PendingDelta> = Vec::new();
497
498 for delta in remaining {
499 let base_res: Option<(ObjectKind, Cow<'_, [u8]>)> =
500 if let Some(base_off) = delta.base_offset {
501 match by_offset.get(&base_off) {
502 Some(e) => Some((e.kind(), entry_object_bytes(e, sink)?)),
503 None => None,
504 }
505 } else if let Some(ref base_id) = delta.base_oid {
506 if let Some(e) = by_oid.get(base_id) {
507 Some((e.kind(), entry_object_bytes(e, sink)?))
508 } else if !opts.dry_run {
509 sink.read_stored(base_id)?
510 .map(|obj| (obj.kind, Cow::Owned(obj.data)))
511 } else {
512 None
513 }
514 } else {
515 None
516 };
517
518 match base_res {
519 Some((base_kind, base_data)) => {
520 let result = apply_delta(base_data.as_ref(), &delta.delta_data)?;
521 let oid = sink.write(base_kind, &result, opts.dry_run)?;
522 let new_entry =
523 packed_entry_after_write(base_kind, result, oid, opts, MAX_RETAIN_BYTES);
524 by_offset.insert(delta.offset, new_entry.clone());
525 by_oid.insert(oid, new_entry);
526 object_counts.add(base_kind);
527 count += 1;
528 if let Some(p) = progress.as_mut() {
529 p.inc(1);
530 }
531 }
532 None => still_pending.push(delta),
533 }
534 }
535
536 remaining = still_pending;
537 if remaining.len() == before {
538 return Err(Error::UnresolvedDeltas {
539 count: u64::try_from(remaining.len()).unwrap_or(u64::MAX),
540 });
541 }
542 }
543 if let (Some(p), true) = (progress.as_mut(), resolving) {
544 p.finish();
545 }
546
547 if opts.strict {
548 verify_packed_dot_special_entries(&by_oid, sink)?;
549 strict_verify_packed_references_map(
550 sink,
551 &by_oid,
552 &opts.allowed_missing,
553 opts.allow_promisor_missing_references,
554 &opts.shallow_boundaries,
555 )?;
556 }
557
558 Ok(Unpacked {
559 count,
560 by_oid,
561 object_counts,
562 pack_bytes,
563 })
564}
565
566#[derive(Debug, Clone)]
568enum PackedObjectEntry {
569 InMemory { kind: ObjectKind, data: Vec<u8> },
570 BlobOnDisk { oid: ObjectId },
571}
572
573impl PackedObjectEntry {
574 fn kind(&self) -> ObjectKind {
575 match self {
576 PackedObjectEntry::InMemory { kind, .. } => *kind,
577 PackedObjectEntry::BlobOnDisk { .. } => ObjectKind::Blob,
578 }
579 }
580}
581
582fn verify_packed_dot_special_entries<S: UnpackSink + ?Sized>(
583 by_oid: &HashMap<ObjectId, PackedObjectEntry>,
584 sink: &S,
585) -> Result<()> {
586 let mut tracker = DotFsckTracker::default();
587 let mut keys: Vec<ObjectId> = by_oid.keys().copied().collect();
588 keys.sort();
589 for oid in keys {
590 let entry = &by_oid[&oid];
591 let PackedObjectEntry::InMemory { kind, data } = entry else {
592 continue;
593 };
594 if *kind == ObjectKind::Tree {
595 for di in tracker.on_tree(&oid, data)? {
596 if di.is_error_severity() {
597 return Err(Error::CorruptObject(di.format_line()));
598 }
599 }
600 }
601 for di in tracker.on_object(&oid, *kind, data) {
602 if di.is_error_severity() {
603 return Err(Error::CorruptObject(di.format_line()));
604 }
605 }
606 }
607 let pending_gm: Vec<ObjectId> = tracker
608 .gitmodules_found
609 .difference(&tracker.gitmodules_done)
610 .copied()
611 .collect();
612 let pending_ga: Vec<ObjectId> = tracker
613 .gitattributes_found
614 .difference(&tracker.gitattributes_done)
615 .copied()
616 .collect();
617 for oid in pending_gm {
618 tracker.gitmodules_done.insert(oid);
619 let Some(entry) = by_oid.get(&oid) else {
620 continue;
621 };
622 let (kind, data) = dot_special_blob_bytes(entry, sink, &oid)?;
623 for di in gitmodules::fsck_dot_special_object(
624 &oid,
625 kind,
626 &data,
627 &tracker.gitmodules_found,
628 &tracker.gitattributes_found,
629 ) {
630 if di.is_error_severity() {
631 return Err(Error::CorruptObject(di.format_line()));
632 }
633 }
634 }
635 for oid in pending_ga {
636 if tracker.gitattributes_done.contains(&oid) {
637 continue;
638 }
639 tracker.gitattributes_done.insert(oid);
640 let Some(entry) = by_oid.get(&oid) else {
641 continue;
642 };
643 let (kind, data) = dot_special_blob_bytes(entry, sink, &oid)?;
644 for di in gitmodules::fsck_dot_special_object(
645 &oid,
646 kind,
647 &data,
648 &tracker.gitmodules_found,
649 &tracker.gitattributes_found,
650 ) {
651 if di.is_error_severity() {
652 return Err(Error::CorruptObject(di.format_line()));
653 }
654 }
655 }
656 Ok(())
657}
658
659fn dot_special_blob_bytes<S: UnpackSink + ?Sized>(
660 entry: &PackedObjectEntry,
661 sink: &S,
662 id: &ObjectId,
663) -> Result<(ObjectKind, Vec<u8>)> {
664 match entry {
665 PackedObjectEntry::InMemory { kind, data, .. } => Ok((*kind, data.clone())),
666 PackedObjectEntry::BlobOnDisk { oid } => {
667 debug_assert_eq!(oid, id);
668 let Some(obj) = sink.read_stored(id)? else {
669 return Err(Error::CorruptObject(format!(
670 "missing .gitmodules/.gitattributes blob {id}"
671 )));
672 };
673 Ok((obj.kind, obj.data))
674 }
675 }
676}
677
678fn packed_entry_after_write(
679 kind: ObjectKind,
680 data: Vec<u8>,
681 oid: ObjectId,
682 opts: &UnpackOptions,
683 max_retain: usize,
684) -> PackedObjectEntry {
685 if !opts.dry_run && kind == ObjectKind::Blob && data.len() > max_retain {
686 PackedObjectEntry::BlobOnDisk { oid }
687 } else {
688 PackedObjectEntry::InMemory { kind, data }
689 }
690}
691
692fn entry_object_bytes<'a, S: UnpackSink + ?Sized>(
693 entry: &'a PackedObjectEntry,
694 sink: &S,
695) -> Result<Cow<'a, [u8]>> {
696 match entry {
697 PackedObjectEntry::InMemory { data, .. } => Ok(Cow::Borrowed(data.as_slice())),
698 PackedObjectEntry::BlobOnDisk { oid } => sink
699 .read_stored(oid)?
700 .map(|obj| Cow::Owned(obj.data))
701 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex())),
702 }
703}
704
705fn strict_verify_packed_references_map<S: UnpackSink + ?Sized>(
706 sink: &S,
707 pack: &HashMap<ObjectId, PackedObjectEntry>,
708 allowed_missing: &HashSet<ObjectId>,
709 allow_promisor_missing_references: bool,
710 shallow_boundaries: &HashSet<ObjectId>,
711) -> Result<()> {
712 for (oid, entry) in pack {
713 match entry {
714 PackedObjectEntry::BlobOnDisk { .. } => {}
715 PackedObjectEntry::InMemory { kind, data } => match kind {
716 ObjectKind::Tree => {
717 for e in parse_tree(data)? {
718 if e.mode == MODE_GITLINK {
723 continue;
724 }
725 if !strict_ref_resolves_map(
726 &e.oid,
727 pack,
728 sink,
729 allowed_missing,
730 allow_promisor_missing_references,
731 )? {
732 return Err(Error::CorruptObject(format!(
733 "strict: missing object {} referenced by tree",
734 e.oid.to_hex()
735 )));
736 }
737 }
738 }
739 ObjectKind::Commit => {
740 let c = parse_commit(data)?;
741 if !strict_ref_resolves_map(
742 &c.tree,
743 pack,
744 sink,
745 allowed_missing,
746 allow_promisor_missing_references,
747 )? {
748 return Err(Error::CorruptObject(format!(
749 "strict: missing tree {} referenced by commit",
750 c.tree.to_hex()
751 )));
752 }
753 if shallow_boundaries.contains(oid) {
757 continue;
758 }
759 for p in &c.parents {
760 if !strict_ref_resolves_map(
761 p,
762 pack,
763 sink,
764 allowed_missing,
765 allow_promisor_missing_references,
766 )? {
767 return Err(Error::CorruptObject(format!(
768 "strict: missing parent {} referenced by commit",
769 p.to_hex()
770 )));
771 }
772 }
773 }
774 ObjectKind::Tag => {
775 let t = parse_tag(data)?;
776 if !strict_ref_resolves_map(
777 &t.object,
778 pack,
779 sink,
780 allowed_missing,
781 allow_promisor_missing_references,
782 )? {
783 return Err(Error::CorruptObject(format!(
784 "strict: missing object {} referenced by tag",
785 t.object.to_hex()
786 )));
787 }
788 }
789 ObjectKind::Blob => {}
790 },
791 }
792 }
793 Ok(())
794}
795
796fn strict_ref_resolves_map<S: UnpackSink + ?Sized>(
797 oid: &ObjectId,
798 pack: &HashMap<ObjectId, PackedObjectEntry>,
799 sink: &S,
800 allowed_missing: &HashSet<ObjectId>,
801 allow_promisor_missing_references: bool,
802) -> Result<bool> {
803 if pack.contains_key(oid) || allowed_missing.contains(oid) || allow_promisor_missing_references
804 {
805 return Ok(true);
806 }
807 sink.contains(oid)
808}
809
810fn strict_ref_resolves(
811 oid: &ObjectId,
812 pack: &std::collections::HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
813 odb: Option<&Odb>,
814) -> Result<bool> {
815 if pack.contains_key(oid) {
816 return Ok(true);
817 }
818 odb.map_or(Ok(false), |o| o.contains(oid))
819}
820
821pub fn strict_verify_packed_references(
832 odb: Option<&Odb>,
833 pack: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
834) -> Result<()> {
835 for (kind, data) in pack.values() {
836 match kind {
837 ObjectKind::Tree => {
838 for e in parse_tree(data)? {
839 if e.mode == MODE_GITLINK {
844 continue;
845 }
846 if !strict_ref_resolves(&e.oid, pack, odb)? {
847 return Err(Error::CorruptObject(format!(
848 "strict: missing object {} referenced by tree",
849 e.oid.to_hex()
850 )));
851 }
852 }
853 }
854 ObjectKind::Commit => {
855 let c = parse_commit(data)?;
856 if !strict_ref_resolves(&c.tree, pack, odb)? {
857 return Err(Error::CorruptObject(format!(
858 "strict: missing tree {} referenced by commit",
859 c.tree.to_hex()
860 )));
861 }
862 for p in &c.parents {
863 if !strict_ref_resolves(p, pack, odb)? {
864 return Err(Error::CorruptObject(format!(
865 "strict: missing parent {} referenced by commit",
866 p.to_hex()
867 )));
868 }
869 }
870 }
871 ObjectKind::Tag => {
872 let t = parse_tag(data)?;
873 if !strict_ref_resolves(&t.object, pack, odb)? {
874 return Err(Error::CorruptObject(format!(
875 "strict: missing object {} referenced by tag",
876 t.object.to_hex()
877 )));
878 }
879 }
880 ObjectKind::Blob => {}
881 }
882 }
883 Ok(())
884}
885
886pub fn pack_is_thin(data: &[u8], algo: HashAlgo) -> bool {
893 pack_is_thin_inner(data, algo).unwrap_or(false)
894}
895
896fn pack_is_thin_inner(data: &[u8], algo: HashAlgo) -> Result<bool> {
897 use crate::pack_zlib::skip_zlib_at;
898
899 let mut rd = PackReader::new(data);
900 if rd.read_exact(4)? != b"PACK" {
901 return Ok(false);
902 }
903 let _version = rd.read_u32_be()?;
904 let nr_objects = rd.read_u32_be()? as usize;
905
906 let mut ref_delta_bases: Vec<ObjectId> = Vec::new();
907 let mut saw_ref_delta = false;
908
909 for _ in 0..nr_objects {
910 let obj_offset = rd.pos;
911 let (type_code, size) = rd.read_type_size()?;
912 match type_code {
913 1..=4 => {
914 let zlib_offset = rd.pos;
915 let consumed = skip_zlib_at(data, zlib_offset, size)?;
916 rd.pos = zlib_offset + consumed;
917 let _ = obj_offset;
918 }
919 6 => {
920 let neg = rd.read_ofs_neg_offset()?;
921 let _base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
922 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
923 })?;
924 let zlib_offset = rd.pos;
925 let consumed = skip_zlib_at(data, zlib_offset, size)?;
926 rd.pos = zlib_offset + consumed;
927 }
928 7 => {
929 saw_ref_delta = true;
930 let base_bytes = rd.read_exact(algo.len())?;
931 let base_oid = ObjectId::from_bytes(base_bytes)?;
932 ref_delta_bases.push(base_oid);
933 let zlib_offset = rd.pos;
934 let consumed = skip_zlib_at(data, zlib_offset, size)?;
935 rd.pos = zlib_offset + consumed;
936 }
937 _ => return Ok(false),
938 }
939 }
940
941 if !saw_ref_delta {
942 return Ok(false);
943 }
944
945 pack_is_thin_with_ofs_deltas(data, algo, ref_delta_bases)
946}
947
948fn pack_is_thin_with_ofs_deltas(
950 data: &[u8],
951 algo: HashAlgo,
952 ref_delta_bases: Vec<ObjectId>,
953) -> Result<bool> {
954 let tmp = tempfile::tempdir().map_err(Error::Io)?;
955 let git_dir = tmp.path().join(".git");
956 std::fs::create_dir_all(git_dir.join("objects")).map_err(Error::Io)?;
957 if algo == HashAlgo::Sha256 {
958 std::fs::write(
959 git_dir.join("config"),
960 "[extensions]\n\tobjectformat = sha256\n",
961 )
962 .map_err(Error::Io)?;
963 }
964 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
965 match crate::pack_index_build::build_pack_index_records(
966 data,
967 &odb,
968 crate::hash::Parallelism::resolve(Some(1)),
969 false,
970 ) {
971 Ok(records) => {
972 let in_pack: HashSet<ObjectId> = records.into_iter().map(|r| r.oid).collect();
973 Ok(ref_delta_bases.iter().any(|b| !in_pack.contains(b)))
974 }
975 Err(Error::UnresolvedDeltas { .. }) => Ok(true),
976 Err(e) => Err(e),
977 }
978}
979
980pub fn pack_bytes_to_object_map(data: &[u8], odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
992 build_pack_object_map(PackReader::new(data), odb)
993}
994
995#[derive(Debug, Clone)]
1001pub struct PackIndexRecord {
1002 pub oid: ObjectId,
1004 pub offset: u64,
1006 pub crc32: u32,
1008 pub kind: ObjectKind,
1010}
1011
1012pub fn pack_index_records_from_bytes(data: &[u8], odb: &Odb) -> Result<Vec<PackIndexRecord>> {
1022 pack_index_records_with_threads(
1023 data,
1024 odb,
1025 crate::config::ConfigSet::pack_index_parallelism_for_git_dir(
1026 &crate::environment::Environment::empty(),
1027 odb.config_git_dir(),
1028 ),
1029 )
1030}
1031
1032pub fn pack_index_records_with_threads(
1034 data: &[u8],
1035 odb: &Odb,
1036 parallelism: crate::hash::Parallelism,
1037) -> Result<Vec<PackIndexRecord>> {
1038 crate::pack_index_build::build_pack_index_records(data, odb, parallelism, true)
1039}
1040
1041pub fn pack_index_records_with_threads_with_progress(
1044 data: &[u8],
1045 odb: &Odb,
1046 parallelism: crate::hash::Parallelism,
1047 progress: Option<&mut dyn crate::progress::ProgressSink>,
1048) -> Result<Vec<PackIndexRecord>> {
1049 crate::pack_index_build::build_pack_index_records_with_progress(
1050 data,
1051 odb,
1052 parallelism,
1053 true,
1054 progress,
1055 None,
1056 )
1057}
1058
1059fn build_pack_object_map(mut rd: PackReader<'_>, odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
1060 let algo = odb.hash_algo();
1061 let sig = rd.read_exact(4)?;
1062 if sig != b"PACK" {
1063 return Err(Error::CorruptObject(
1064 "not a pack stream: invalid signature".to_owned(),
1065 ));
1066 }
1067 let version = rd.read_u32_be()?;
1068 if version != 2 && version != 3 {
1069 return Err(Error::CorruptObject(format!(
1070 "unsupported pack version {version}"
1071 )));
1072 }
1073 let nr_objects = rd.read_u32_be()? as usize;
1074
1075 let mut by_offset: HashMap<usize, (ObjectKind, Vec<u8>)> = HashMap::new();
1076 let mut by_oid: HashMap<ObjectId, (ObjectKind, Vec<u8>)> = HashMap::new();
1077 let mut pending: Vec<PendingDelta> = Vec::new();
1078
1079 fn base_from_pack_or_odb(
1080 by_oid: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1081 odb: &Odb,
1082 id: &ObjectId,
1083 ) -> Option<(ObjectKind, Vec<u8>)> {
1084 if let Some(e) = by_oid.get(id) {
1085 return Some(e.clone());
1086 }
1087 odb.read(id).ok().map(|o| (o.kind, o.data))
1088 }
1089
1090 for _ in 0..nr_objects {
1091 let obj_offset = rd.pos;
1092 let (type_code, size) = rd.read_type_size()?;
1093
1094 match type_code {
1095 1..=4 => {
1096 let kind = type_code_to_kind(type_code)?;
1097 let data = rd.decompress(size)?;
1098 let oid = odb.hash(kind, &data)?;
1099 by_offset.insert(obj_offset, (kind, data.clone()));
1100 by_oid.insert(oid, (kind, data));
1101 }
1102 6 => {
1103 let neg = rd.read_ofs_neg_offset()?;
1104 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
1105 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
1106 })?;
1107 let delta_data = rd.decompress(size)?;
1108 pending.push(PendingDelta {
1109 offset: obj_offset,
1110 base_oid: None,
1111 base_offset: Some(base_offset),
1112 delta_data,
1113 });
1114 }
1115 7 => {
1116 let base_bytes = rd.read_exact(algo.len())?;
1117 let base_oid = ObjectId::from_bytes(base_bytes)?;
1118 let delta_data = rd.decompress(size)?;
1119 pending.push(PendingDelta {
1120 offset: obj_offset,
1121 base_oid: Some(base_oid),
1122 base_offset: None,
1123 delta_data,
1124 });
1125 }
1126 other => {
1127 return Err(Error::CorruptObject(format!(
1128 "unknown packed-object type {other}"
1129 )))
1130 }
1131 }
1132 }
1133
1134 let consumed = rd.pos;
1135 {
1136 let mut hasher = ChecksumHasher::new(algo);
1137 hasher.update(&rd.data[..consumed]);
1138 let digest = hasher.finalize();
1139 let trailing = rd.read_exact(algo.len())?;
1140 if digest.as_slice() != trailing {
1141 return Err(Error::CorruptObject(
1142 "pack trailing checksum mismatch".to_owned(),
1143 ));
1144 }
1145 }
1146
1147 let mut remaining = pending;
1148 loop {
1149 if remaining.is_empty() {
1150 break;
1151 }
1152 let before = remaining.len();
1153 let mut still_pending: Vec<PendingDelta> = Vec::new();
1154
1155 for delta in remaining {
1156 let base = if let Some(base_off) = delta.base_offset {
1157 by_offset.get(&base_off).cloned()
1158 } else if let Some(ref base_id) = delta.base_oid {
1159 base_from_pack_or_odb(&by_oid, odb, base_id)
1160 } else {
1161 None
1162 };
1163
1164 if let Some((base_kind, base_data)) = base {
1165 let result = apply_delta(&base_data, &delta.delta_data)?;
1166 let oid = odb.hash(base_kind, &result)?;
1167 by_offset.insert(delta.offset, (base_kind, result.clone()));
1168 by_oid.insert(oid, (base_kind, result));
1169 } else {
1170 still_pending.push(delta);
1171 }
1172 }
1173
1174 remaining = still_pending;
1175 if remaining.len() == before {
1176 return Err(Error::UnresolvedDeltas {
1177 count: u64::try_from(remaining.len()).unwrap_or(u64::MAX),
1178 });
1179 }
1180 }
1181
1182 Ok(by_oid
1183 .into_iter()
1184 .map(|(oid, (kind, data))| (oid, Object::new(kind, data)))
1185 .collect())
1186}
1187
1188pub(crate) fn type_code_to_kind(code: u8) -> Result<ObjectKind> {
1190 match code {
1191 1 => Ok(ObjectKind::Commit),
1192 2 => Ok(ObjectKind::Tree),
1193 3 => Ok(ObjectKind::Blob),
1194 4 => Ok(ObjectKind::Tag),
1195 _ => Err(Error::CorruptObject(format!(
1196 "type code {code} is not a regular object type"
1197 ))),
1198 }
1199}
1200
1201pub(crate) struct PackReader<'a> {
1203 data: &'a [u8],
1204 pub(crate) pos: usize,
1205}
1206
1207impl<'a> PackReader<'a> {
1208 pub(crate) fn new(data: &'a [u8]) -> Self {
1209 Self { data, pos: 0 }
1210 }
1211
1212 pub(crate) fn read_exact(&mut self, n: usize) -> Result<&[u8]> {
1215 if self.pos + n > self.data.len() {
1216 return Err(Error::CorruptObject(format!(
1217 "pack stream truncated: need {n} bytes at offset {}",
1218 self.pos
1219 )));
1220 }
1221 let slice = &self.data[self.pos..self.pos + n];
1222 self.pos += n;
1223 Ok(slice)
1224 }
1225
1226 pub(crate) fn read_byte(&mut self) -> Result<u8> {
1228 if self.pos >= self.data.len() {
1229 return Err(Error::CorruptObject(
1230 "unexpected end of pack stream".to_owned(),
1231 ));
1232 }
1233 let b = self.data[self.pos];
1234 self.pos += 1;
1235 Ok(b)
1236 }
1237
1238 pub(crate) fn read_u32_be(&mut self) -> Result<u32> {
1240 let bytes = self.read_exact(4)?;
1241 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
1242 Error::CorruptObject("u32 read failed".to_owned())
1243 })?))
1244 }
1245
1246 pub(crate) fn read_type_size(&mut self) -> Result<(u8, usize)> {
1251 let c = self.read_byte()?;
1252 let type_code = (c >> 4) & 0x7;
1253 let mut size = (c & 0x0f) as usize;
1254 let mut shift = 4u32;
1255 let mut cur = c;
1256 while cur & 0x80 != 0 {
1257 cur = self.read_byte()?;
1258 size |= ((cur & 0x7f) as usize) << shift;
1259 shift += 7;
1260 }
1261 Ok((type_code, size))
1262 }
1263
1264 pub(crate) fn read_ofs_neg_offset(&mut self) -> Result<usize> {
1269 let mut c = self.read_byte()?;
1270 let mut value = (c & 0x7f) as usize;
1271 while c & 0x80 != 0 {
1272 c = self.read_byte()?;
1273 value = (value + 1) << 7 | (c & 0x7f) as usize;
1274 }
1275 Ok(value)
1276 }
1277
1278 pub(crate) fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
1283 let (out, consumed) = decompress_zlib_at(self.data, self.pos, expected_size)?;
1284 self.pos += consumed;
1285 Ok(out)
1286 }
1287}
1288
1289pub(crate) fn decompress_zlib_at_into(
1291 pack: &[u8],
1292 offset: usize,
1293 expected_size: usize,
1294 out: &mut Vec<u8>,
1295) -> Result<usize> {
1296 out.clear();
1297 out.reserve(expected_size);
1298 let slice = pack.get(offset..).ok_or_else(|| {
1299 Error::CorruptObject(format!(
1300 "pack stream truncated: need zlib at offset {offset}"
1301 ))
1302 })?;
1303 let mut decoder = ZlibDecoder::new(slice);
1304 decoder
1305 .read_to_end(out)
1306 .map_err(|e| Error::Zlib(e.to_string()))?;
1307 if out.len() != expected_size {
1308 return Err(Error::CorruptObject(format!(
1309 "decompressed {} bytes but expected {}",
1310 out.len(),
1311 expected_size
1312 )));
1313 }
1314 Ok(decoder.total_in() as usize)
1315}
1316
1317pub(crate) fn decompress_zlib_at(
1319 pack: &[u8],
1320 offset: usize,
1321 expected_size: usize,
1322) -> Result<(Vec<u8>, usize)> {
1323 let mut out = Vec::new();
1324 let consumed = decompress_zlib_at_into(pack, offset, expected_size, &mut out)?;
1325 Ok((out, consumed))
1326}
1327
1328fn io_to_corrupt_eof(e: io::Error, stream_pos: usize, context: &str) -> Error {
1329 if e.kind() == io::ErrorKind::UnexpectedEof {
1330 Error::CorruptObject(format!(
1331 "pack stream truncated ({context}) at offset {stream_pos}"
1332 ))
1333 } else {
1334 Error::Io(e)
1335 }
1336}
1337
1338struct StreamingPackReader<'a> {
1344 inner: &'a mut dyn Read,
1345 pack_hasher: ChecksumHasher,
1346 stream_pos: usize,
1347 max_input_bytes: Option<u64>,
1348 pending: Vec<u8>,
1351}
1352
1353impl<'a> StreamingPackReader<'a> {
1354 fn new(inner: &'a mut dyn Read, max_input_bytes: Option<u64>, algo: HashAlgo) -> Self {
1355 Self {
1356 inner,
1357 pack_hasher: ChecksumHasher::new(algo),
1358 stream_pos: 0,
1359 max_input_bytes,
1360 pending: Vec::new(),
1361 }
1362 }
1363
1364 fn stream_pos(&self) -> usize {
1365 self.stream_pos
1366 }
1367
1368 fn enforce_max_input(&self) -> Result<()> {
1369 if let Some(limit) = self.max_input_bytes {
1370 let pos = u64::try_from(self.stream_pos)
1371 .map_err(|_| Error::CorruptObject("pack stream position overflow".to_owned()))?;
1372 if pos > limit {
1373 return Err(Error::CorruptObject(
1374 "pack exceeds maximum allowed size".to_owned(),
1375 ));
1376 }
1377 }
1378 Ok(())
1379 }
1380
1381 fn read_from_source(&mut self, buf: &mut [u8]) -> Result<usize> {
1383 let n = if !self.pending.is_empty() {
1384 let take = buf.len().min(self.pending.len());
1385 buf[..take].copy_from_slice(&self.pending[..take]);
1386 self.pending.drain(..take);
1387 take
1388 } else {
1389 self.inner.read(buf).map_err(Error::Io)?
1390 };
1391 if n > 0 {
1392 self.pack_hasher.update(&buf[..n]);
1393 self.stream_pos += n;
1394 self.enforce_max_input()?;
1395 }
1396 Ok(n)
1397 }
1398
1399 fn read_byte(&mut self) -> Result<u8> {
1400 let mut b = [0u8; 1];
1401 let n = self.read_from_source(&mut b)?;
1402 if n == 0 {
1403 return Err(Error::CorruptObject(format!(
1404 "pack stream truncated (read byte) at offset {}",
1405 self.stream_pos
1406 )));
1407 }
1408 Ok(b[0])
1409 }
1410
1411 fn read_exact_n(&mut self, n: usize) -> Result<Vec<u8>> {
1412 let mut v = vec![0u8; n];
1413 let mut got = 0usize;
1414 while got < n {
1415 let m = self.read_from_source(&mut v[got..n])?;
1416 if m == 0 {
1417 return Err(Error::CorruptObject(format!(
1418 "pack stream truncated (read exact) at offset {}",
1419 self.stream_pos
1420 )));
1421 }
1422 got += m;
1423 }
1424 Ok(v)
1425 }
1426
1427 fn read_u32_be(&mut self) -> Result<u32> {
1428 let mut b = [0u8; 4];
1429 let mut got = 0usize;
1430 while got < 4 {
1431 let m = self.read_from_source(&mut b[got..4])?;
1432 if m == 0 {
1433 return Err(Error::CorruptObject(format!(
1434 "pack stream truncated (read u32) at offset {}",
1435 self.stream_pos
1436 )));
1437 }
1438 got += m;
1439 }
1440 Ok(u32::from_be_bytes(b))
1441 }
1442
1443 fn read_type_size(&mut self) -> Result<(u8, usize)> {
1444 let c = self.read_byte()?;
1445 let type_code = (c >> 4) & 0x7;
1446 let mut size = (c & 0x0f) as usize;
1447 let mut shift = 4u32;
1448 let mut cur = c;
1449 while cur & 0x80 != 0 {
1450 cur = self.read_byte()?;
1451 size |= ((cur & 0x7f) as usize) << shift;
1452 shift += 7;
1453 }
1454 Ok((type_code, size))
1455 }
1456
1457 fn read_ofs_neg_offset(&mut self) -> Result<usize> {
1458 let mut c = self.read_byte()?;
1459 let mut value = (c & 0x7f) as usize;
1460 while c & 0x80 != 0 {
1461 c = self.read_byte()?;
1462 value = (value + 1) << 7 | (c & 0x7f) as usize;
1463 }
1464 Ok(value)
1465 }
1466
1467 pub(crate) fn decompress_into(
1478 &mut self,
1479 expected_size: usize,
1480 mut chunk_sink: impl FnMut(&[u8]) -> Result<()>,
1481 ) -> Result<()> {
1482 if expected_size == 0 {
1483 const CHUNK: usize = 64 * 1024;
1484 let mut scratch = [0u8; CHUNK];
1485 loop {
1486 let mut cursor = std::io::Cursor::new(self.pending.as_slice());
1487 let mut z = ZlibDecoder::new(&mut cursor);
1488 let mut sink = [0u8; 1];
1489 match z.read(&mut sink) {
1490 Ok(0) => {
1491 let consumed = z.total_in() as usize;
1492 if consumed > self.pending.len() {
1493 return Err(Error::CorruptObject(
1494 "zlib total_in exceeds pending buffer".to_owned(),
1495 ));
1496 }
1497 if consumed == 0 {
1498 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1499 if n == 0 {
1500 return Err(Error::CorruptObject(format!(
1501 "pack stream truncated (zlib) at offset {}",
1502 self.stream_pos
1503 )));
1504 }
1505 self.pending.extend_from_slice(&scratch[..n]);
1506 continue;
1507 }
1508 self.pack_hasher.update(&self.pending[..consumed]);
1509 self.stream_pos += consumed;
1510 self.pending.drain(..consumed);
1511 self.enforce_max_input()?;
1512 return Ok(());
1513 }
1514 Ok(_) => {
1515 return Err(Error::CorruptObject(
1516 "0-byte packed object inflated to non-empty output".to_owned(),
1517 ));
1518 }
1519 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1520 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1521 if n == 0 {
1522 return Err(Error::CorruptObject(format!(
1523 "pack stream truncated (zlib) at offset {}",
1524 self.stream_pos
1525 )));
1526 }
1527 self.pending.extend_from_slice(&scratch[..n]);
1528 }
1529 Err(e) => return Err(Error::Zlib(e.to_string())),
1530 }
1531 }
1532 }
1533
1534 const CHUNK: usize = 64 * 1024;
1535 let mut scratch = [0u8; CHUNK];
1536 let mut scratch_out = [0u8; CHUNK];
1537
1538 let mut z = Decompress::new(true);
1539 let mut produced = 0usize;
1540 let mut eof = false;
1541 loop {
1542 if self.pending.is_empty() && !eof {
1543 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1544 if n == 0 {
1545 eof = true;
1546 } else {
1547 self.pending.extend_from_slice(&scratch[..n]);
1548 }
1549 }
1550
1551 let flush = if eof && self.pending.is_empty() {
1552 FlushDecompress::Finish
1553 } else {
1554 FlushDecompress::None
1555 };
1556
1557 let before_in = z.total_in();
1558 let before_out = z.total_out();
1559 let status = z
1560 .decompress(self.pending.as_slice(), &mut scratch_out, flush)
1561 .map_err(|e| Error::Zlib(e.to_string()))?;
1562 let consumed = (z.total_in() - before_in) as usize;
1563 if consumed > self.pending.len() {
1564 return Err(Error::CorruptObject(
1565 "zlib consumed more than pending buffer".to_owned(),
1566 ));
1567 }
1568 self.pack_hasher.update(&self.pending[..consumed]);
1569 self.stream_pos += consumed;
1570 self.pending.drain(..consumed);
1571 self.enforce_max_input()?;
1572 let chunk_len = (z.total_out() - before_out) as usize;
1573 if chunk_len > 0 {
1574 let chunk = &scratch_out[..chunk_len];
1575 let take = chunk.len().min(expected_size - produced);
1576 chunk_sink(&chunk[..take])?;
1577 produced += take;
1578 if take < chunk.len() {
1579 return Err(Error::CorruptObject(format!(
1580 "decompressed size mismatch: got {}, want {expected_size}",
1581 produced + (chunk.len() - take)
1582 )));
1583 }
1584 }
1585
1586 match status {
1587 Status::StreamEnd => {
1588 if produced != expected_size {
1589 return Err(Error::CorruptObject(format!(
1590 "decompressed size mismatch: got {produced}, want {expected_size}"
1591 )));
1592 }
1593 return Ok(());
1594 }
1595 Status::Ok | Status::BufError => {
1596 if consumed == 0 && !eof {
1597 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1598 if n == 0 {
1599 eof = true;
1600 } else {
1601 self.pending.extend_from_slice(&scratch[..n]);
1602 }
1603 } else if eof && self.pending.is_empty() && consumed == 0 {
1604 return Err(Error::CorruptObject(format!(
1605 "pack stream truncated (zlib) at offset {}",
1606 self.stream_pos
1607 )));
1608 }
1609 }
1610 }
1611 }
1612 }
1613
1614 fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
1615 let mut out = vec![0u8; expected_size];
1616 let mut pos = 0usize;
1617 self.decompress_into(expected_size, |chunk| {
1618 out[pos..pos + chunk.len()].copy_from_slice(chunk);
1619 pos += chunk.len();
1620 Ok(())
1621 })?;
1622 Ok(out)
1623 }
1624
1625 fn finalize_hasher(&self) -> Vec<u8> {
1627 self.pack_hasher.clone().finalize()
1628 }
1629
1630 fn read_trailer(&mut self) -> Result<Vec<u8>> {
1632 let hash_len = self.pack_hasher.algo().len();
1633 let mut b = vec![0u8; hash_len];
1634 if self.pending.len() >= hash_len {
1635 b.copy_from_slice(&self.pending[..hash_len]);
1636 self.pending.drain(..hash_len);
1637 self.stream_pos += hash_len;
1638 self.enforce_max_input()?;
1639 return Ok(b);
1640 }
1641 let tail = self.pending.len();
1642 if tail > 0 {
1643 b[..tail].copy_from_slice(&self.pending[..]);
1644 self.pending.clear();
1645 }
1646 self.inner
1647 .read_exact(&mut b[tail..])
1648 .map_err(|e| io_to_corrupt_eof(e, self.stream_pos, "trailer"))?;
1649 self.stream_pos += hash_len;
1650 self.enforce_max_input()?;
1651 Ok(b)
1652 }
1653}
1654
1655pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
1669 let mut result = Vec::new();
1670 apply_delta_into(&mut result, base, delta)?;
1671 Ok(result)
1672}
1673
1674pub fn apply_delta_into(out: &mut Vec<u8>, base: &[u8], delta: &[u8]) -> Result<()> {
1678 let mut pos = 0usize;
1679
1680 let src_size = read_delta_varint(delta, &mut pos)?;
1681 if src_size != base.len() {
1682 return Err(Error::CorruptObject(format!(
1683 "delta source size {src_size} != base size {}",
1684 base.len()
1685 )));
1686 }
1687 let dest_size = read_delta_varint(delta, &mut pos)?;
1688 out.clear();
1689 if out.capacity() < dest_size {
1690 out.reserve(dest_size - out.capacity());
1691 }
1692
1693 while pos < delta.len() {
1694 let cmd = delta[pos];
1695 pos += 1;
1696 if cmd == 0 {
1697 return Err(Error::CorruptObject(
1698 "reserved opcode 0 in delta stream".to_owned(),
1699 ));
1700 }
1701 if cmd & 0x80 != 0 {
1702 let mut offset = 0usize;
1705 let mut size = 0usize;
1706
1707 macro_rules! maybe_read_byte {
1708 ($flag:expr, $shift:expr, $target:expr) => {
1709 if cmd & $flag != 0 {
1710 let b = *delta.get(pos).ok_or_else(|| {
1711 Error::CorruptObject("truncated delta COPY operand".to_owned())
1712 })?;
1713 pos += 1;
1714 $target |= (b as usize) << $shift;
1715 }
1716 };
1717 }
1718
1719 maybe_read_byte!(0x01, 0, offset);
1720 maybe_read_byte!(0x02, 8, offset);
1721 maybe_read_byte!(0x04, 16, offset);
1722 maybe_read_byte!(0x08, 24, offset);
1723 maybe_read_byte!(0x10, 0, size);
1724 maybe_read_byte!(0x20, 8, size);
1725 maybe_read_byte!(0x40, 16, size);
1726
1727 if size == 0 {
1728 size = 0x10000;
1729 }
1730
1731 let end = offset.checked_add(size).ok_or_else(|| {
1732 Error::CorruptObject("delta COPY range overflows usize".to_owned())
1733 })?;
1734 let chunk = base.get(offset..end).ok_or_else(|| {
1735 Error::CorruptObject(format!(
1736 "delta COPY [{offset},{end}) out of range (base is {} bytes)",
1737 base.len()
1738 ))
1739 })?;
1740 out.extend_from_slice(chunk);
1741 } else {
1742 let n = cmd as usize;
1744 let chunk = delta
1745 .get(pos..pos + n)
1746 .ok_or_else(|| Error::CorruptObject("truncated delta INSERT data".to_owned()))?;
1747 out.extend_from_slice(chunk);
1748 pos += n;
1749 }
1750 }
1751
1752 if out.len() != dest_size {
1753 return Err(Error::CorruptObject(format!(
1754 "delta produced {} bytes but expected {dest_size}",
1755 out.len()
1756 )));
1757 }
1758
1759 Ok(())
1760}
1761
1762pub(crate) fn delta_uncompressed_result_size_if_complete(data: &[u8]) -> Result<Option<usize>> {
1768 let mut pos = 0;
1769 let Some(_src_size) = read_delta_varint_if_complete(data, &mut pos)? else {
1770 return Ok(None);
1771 };
1772 read_delta_varint_if_complete(data, &mut pos)
1773}
1774
1775fn read_delta_varint_if_complete(data: &[u8], pos: &mut usize) -> Result<Option<usize>> {
1776 let start = *pos;
1777 let mut value = 0usize;
1778 let mut shift = 0u32;
1779 while *pos < data.len() {
1780 let b = data[*pos];
1781 *pos += 1;
1782 value |= ((b & 0x7f) as usize) << shift;
1783 shift += 7;
1784 if b & 0x80 == 0 {
1785 return Ok(Some(value));
1786 }
1787 if shift > 35 {
1788 return Err(Error::CorruptObject(
1789 "delta varint exceeds supported width".to_owned(),
1790 ));
1791 }
1792 }
1793 *pos = start;
1794 Ok(None)
1795}
1796
1797fn read_delta_varint(data: &[u8], pos: &mut usize) -> Result<usize> {
1801 let mut value = 0usize;
1802 let mut shift = 0u32;
1803 loop {
1804 let b = *data
1805 .get(*pos)
1806 .ok_or_else(|| Error::CorruptObject("truncated delta varint".to_owned()))?;
1807 *pos += 1;
1808 value |= ((b & 0x7f) as usize) << shift;
1809 shift += 7;
1810 if b & 0x80 == 0 {
1811 break;
1812 }
1813 }
1814 Ok(value)
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819 #![allow(clippy::vec_init_then_push)]
1820 use super::*;
1821
1822 fn make_pack(objects: &[(ObjectKind, &[u8])]) -> Vec<u8> {
1825 use flate2::write::ZlibEncoder;
1826 use std::io::Write;
1827
1828 let mut entries: Vec<Vec<u8>> = Vec::new();
1829 for (kind, data) in objects {
1830 let type_code: u8 = match kind {
1831 ObjectKind::Commit => 1,
1832 ObjectKind::Tree => 2,
1833 ObjectKind::Blob => 3,
1834 ObjectKind::Tag => 4,
1835 };
1836 let mut header = Vec::new();
1838 let mut size = data.len();
1839 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
1840 size >>= 4;
1841 if size > 0 {
1842 header.push(first | 0x80);
1843 while size > 0 {
1844 let b = (size & 0x7f) as u8;
1845 size >>= 7;
1846 header.push(if size > 0 { b | 0x80 } else { b });
1847 }
1848 } else {
1849 header.push(first);
1850 }
1851 let mut enc = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1853 enc.write_all(data).unwrap();
1854 let compressed = enc.finish().unwrap();
1855 let mut entry = header;
1856 entry.extend_from_slice(&compressed);
1857 entries.push(entry);
1858 }
1859
1860 let mut pack = Vec::new();
1862 pack.extend_from_slice(b"PACK");
1863 pack.extend_from_slice(&2u32.to_be_bytes());
1864 pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
1865 for entry in &entries {
1866 pack.extend_from_slice(entry);
1867 }
1868 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
1869 pack
1870 }
1871
1872 #[test]
1873 fn test_apply_delta_simple() {
1874 let base = b"hello";
1876 let mut delta = Vec::new();
1877 delta.push(5u8);
1879 delta.push(11u8);
1881 delta.push(0x80 | 0x01 | 0x10); delta.push(0u8); delta.push(5u8); delta.push(6u8);
1888 delta.extend_from_slice(b" world");
1889
1890 let result = apply_delta(base, &delta).unwrap();
1891 assert_eq!(result, b"hello world");
1892 }
1893
1894 #[test]
1895 fn test_apply_delta_insert_only() {
1896 let base = b"";
1897 let mut delta = Vec::new();
1898 delta.push(0u8); delta.push(5u8); delta.push(5u8); delta.extend_from_slice(b"hello");
1902
1903 let result = apply_delta(base, &delta).unwrap();
1904 assert_eq!(result, b"hello");
1905 }
1906
1907 #[test]
1908 fn test_apply_delta_copy_only() {
1909 let base = b"abcdef";
1910 let mut delta = Vec::new();
1911 delta.push(6u8); delta.push(3u8); delta.push(0x91u8);
1916 delta.push(2u8); delta.push(3u8); let result = apply_delta(base, &delta).unwrap();
1920 assert_eq!(result, b"cde");
1921 }
1922
1923 #[test]
1924 fn test_apply_delta_size_zero_means_65536() {
1925 let base = vec![0xABu8; 65536];
1927 let mut delta = Vec::new();
1928 delta.push(0x80 | (65536 & 0x7f) as u8); delta.push(0x80 | ((65536 >> 7) & 0x7f) as u8); delta.push(((65536 >> 14) & 0x7f) as u8); delta.push(0x80 | (65536 & 0x7f) as u8);
1934 delta.push(0x80 | ((65536 >> 7) & 0x7f) as u8);
1935 delta.push(((65536 >> 14) & 0x7f) as u8);
1936 delta.push(0x80u8);
1939
1940 let result = apply_delta(&base, &delta).unwrap();
1941 assert_eq!(result.len(), 65536);
1942 assert!(result.iter().all(|&b| b == 0xAB));
1943 }
1944
1945 #[test]
1946 fn test_unpack_objects_blobs() {
1947 use tempfile::TempDir;
1948 let tmp = TempDir::new().unwrap();
1949 let objects_dir = tmp.path().join("objects");
1950 std::fs::create_dir_all(&objects_dir).unwrap();
1951 let odb = Odb::new(&objects_dir);
1952
1953 let pack = make_pack(&[
1954 (ObjectKind::Blob, b"hello\n"),
1955 (ObjectKind::Blob, b"world\n"),
1956 ]);
1957
1958 let opts = UnpackOptions::default();
1959 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
1960 assert_eq!(count, 2);
1961
1962 let oid1 = HashAlgo::Sha1
1964 .hash_object(ObjectKind::Blob, b"hello\n")
1965 .expect("hash");
1966 let oid2 = HashAlgo::Sha1
1967 .hash_object(ObjectKind::Blob, b"world\n")
1968 .expect("hash");
1969 let obj1 = odb.read(&oid1).unwrap();
1970 let obj2 = odb.read(&oid2).unwrap();
1971 assert_eq!(obj1.data, b"hello\n");
1972 assert_eq!(obj2.data, b"world\n");
1973 }
1974
1975 #[test]
1976 fn test_unpack_objects_empty_tree() {
1977 use tempfile::TempDir;
1978 let tmp = TempDir::new().unwrap();
1979 let objects_dir = tmp.path().join("objects");
1980 std::fs::create_dir_all(&objects_dir).unwrap();
1981 let odb = Odb::new(&objects_dir);
1982
1983 let pack = make_pack(&[(ObjectKind::Tree, b"")]);
1984 let opts = UnpackOptions::default();
1985 assert_eq!(
1986 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
1987 1
1988 );
1989 let oid = HashAlgo::Sha1
1990 .hash_object(ObjectKind::Tree, b"")
1991 .expect("hash");
1992 assert!(odb.exists(&oid));
1993 let loose = objects_dir
1994 .join(oid.loose_prefix())
1995 .join(oid.loose_suffix());
1996 assert!(
1997 loose.is_file(),
1998 "empty tree must be materialized as a loose object during unpack"
1999 );
2000 }
2001
2002 #[test]
2003 fn test_strict_skips_gitlink_tree_entries() {
2004 use crate::index::{MODE_GITLINK, MODE_REGULAR};
2005 use crate::objects::{serialize_tree, TreeEntry};
2006
2007 let submodule_oid = ObjectId::from_hex(&"7f".repeat(20)).unwrap();
2010
2011 let tree_data = serialize_tree(&[TreeEntry {
2013 mode: MODE_GITLINK,
2014 name: b"sub".to_vec(),
2015 oid: submodule_oid,
2016 }]);
2017 let tree_oid = HashAlgo::Sha1
2018 .hash_object(ObjectKind::Tree, &tree_data)
2019 .expect("hash");
2020
2021 let mut pack = HashMap::new();
2024 pack.insert(tree_oid, (ObjectKind::Tree, tree_data.clone()));
2025 assert!(strict_verify_packed_references(None, &pack).is_ok());
2026
2027 let bad_tree = serialize_tree(&[TreeEntry {
2030 mode: MODE_REGULAR,
2031 name: b"file".to_vec(),
2032 oid: ObjectId::from_hex(&"ab".repeat(20)).unwrap(),
2033 }]);
2034 let bad_oid = HashAlgo::Sha1
2035 .hash_object(ObjectKind::Tree, &bad_tree)
2036 .expect("hash");
2037 let mut bad_pack = HashMap::new();
2038 bad_pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2039 assert!(matches!(
2040 strict_verify_packed_references(None, &bad_pack),
2041 Err(Error::CorruptObject(_))
2042 ));
2043 }
2044
2045 struct ChunkedReader<'a> {
2047 data: &'a [u8],
2048 pos: usize,
2049 max_len: usize,
2050 }
2051
2052 impl io::Read for ChunkedReader<'_> {
2053 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2054 if self.pos >= self.data.len() {
2055 return Ok(0);
2056 }
2057 let take = (self.data.len() - self.pos)
2058 .min(self.max_len)
2059 .min(buf.len());
2060 buf[..take].copy_from_slice(&self.data[self.pos..self.pos + take]);
2061 self.pos += take;
2062 Ok(take)
2063 }
2064 }
2065
2066 #[test]
2067 fn test_unpack_objects_chunked_read_matches_full_buffer() {
2068 use tempfile::TempDir;
2069 let pack = make_pack(&[(ObjectKind::Blob, b"chunked-stream")]);
2070 let opts = UnpackOptions::default();
2071 let oid = HashAlgo::Sha1
2072 .hash_object(ObjectKind::Blob, b"chunked-stream")
2073 .expect("hash");
2074
2075 let tmp = TempDir::new().unwrap();
2076 let objects_dir = tmp.path().join("objects");
2077 std::fs::create_dir_all(&objects_dir).unwrap();
2078 let odb = Odb::new(&objects_dir);
2079 assert_eq!(
2080 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2081 1
2082 );
2083 assert!(odb.exists(&oid));
2084
2085 let tmp2 = TempDir::new().unwrap();
2086 let objects_dir2 = tmp2.path().join("objects");
2087 std::fs::create_dir_all(&objects_dir2).unwrap();
2088 let odb2 = Odb::new(&objects_dir2);
2089 let mut chunked = ChunkedReader {
2090 data: pack.as_slice(),
2091 pos: 0,
2092 max_len: 8,
2093 };
2094 assert_eq!(unpack_objects(&mut chunked, &odb2, &opts).unwrap(), 1);
2095 assert!(odb2.exists(&oid));
2096 }
2097
2098 #[test]
2099 fn test_unpack_objects_dry_run_writes_nothing() {
2100 use tempfile::TempDir;
2101 let tmp = TempDir::new().unwrap();
2102 let objects_dir = tmp.path().join("objects");
2103 std::fs::create_dir_all(&objects_dir).unwrap();
2104 let odb = Odb::new(&objects_dir);
2105
2106 let pack = make_pack(&[(ObjectKind::Blob, b"test content")]);
2107
2108 let opts = UnpackOptions {
2109 dry_run: true,
2110 quiet: true,
2111 strict: false,
2112 allowed_missing: Default::default(),
2113 allow_promisor_missing_references: false,
2114 max_input_bytes: None,
2115 ..Default::default()
2116 };
2117 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
2118 assert_eq!(count, 1);
2119
2120 let oid = HashAlgo::Sha1
2122 .hash_object(ObjectKind::Blob, b"test content")
2123 .expect("hash");
2124 assert!(!odb.exists(&oid));
2125 }
2126
2127 #[test]
2128 fn test_unpack_objects_bad_signature() {
2129 use tempfile::TempDir;
2130 let tmp = TempDir::new().unwrap();
2131 let objects_dir = tmp.path().join("objects");
2132 std::fs::create_dir_all(&objects_dir).unwrap();
2133 let odb = Odb::new(&objects_dir);
2134
2135 let mut bad = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00".to_vec();
2136 bad.extend_from_slice(&[0u8; 20]);
2137 let opts = UnpackOptions::default();
2138 let err = unpack_objects(&mut bad.as_slice(), &odb, &opts).unwrap_err();
2139 assert!(err.to_string().contains("invalid signature"));
2140 }
2141
2142 #[test]
2143 fn test_unpack_objects_checksum_mismatch() {
2144 use tempfile::TempDir;
2145 let tmp = TempDir::new().unwrap();
2146 let objects_dir = tmp.path().join("objects");
2147 std::fs::create_dir_all(&objects_dir).unwrap();
2148 let odb = Odb::new(&objects_dir);
2149
2150 let mut pack = make_pack(&[(ObjectKind::Blob, b"data")]);
2151 let n = pack.len();
2153 pack[n - 1] ^= 0xFF;
2154
2155 let opts = UnpackOptions::default();
2156 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2157 assert!(err.to_string().contains("checksum"));
2158 }
2159
2160 #[test]
2161 fn test_apply_delta_source_size_mismatch() {
2162 let base = b"hi";
2163 let delta = [3u8, 2u8, 2u8, b'h', b'i']; let err = apply_delta(base, &delta).unwrap_err();
2165 assert!(err.to_string().contains("source size"));
2166 }
2167
2168 #[test]
2169 fn pack_index_many_blobs_without_retaining_all_payloads() {
2170 const N: usize = 1500;
2171 let blobs: Vec<(ObjectKind, Vec<u8>)> = (0..N)
2172 .map(|i| (ObjectKind::Blob, format!("blob-{i}\n").into_bytes()))
2173 .collect();
2174 let blob_refs: Vec<(ObjectKind, &[u8])> =
2175 blobs.iter().map(|(k, d)| (*k, d.as_slice())).collect();
2176 let pack = make_pack(&blob_refs);
2177 let tmp = tempfile::tempdir().expect("tempdir");
2178 let odb = Odb::new(tmp.path());
2179 let started = std::time::Instant::now();
2180 let records = pack_index_records_from_bytes(&pack, &odb).expect("index records");
2181 let elapsed = started.elapsed();
2182 assert_eq!(records.len(), N, "one index row per object");
2183 assert!(
2184 elapsed.as_secs() < 30,
2185 "indexing {N} blobs took {:?}; expected linear scan without full retention blow-up",
2186 elapsed
2187 );
2188 }
2189
2190 fn pack_with_ref_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, ObjectId, ObjectId) {
2191 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2192
2193 let delta = crate::delta_encode::encode_lcp_delta(base, target).expect("delta");
2194 let base_oid = HashAlgo::Sha1
2195 .hash_object(ObjectKind::Blob, base)
2196 .expect("hash");
2197 let target_oid = HashAlgo::Sha1
2198 .hash_object(ObjectKind::Blob, target)
2199 .expect("hash");
2200 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2201 pb.add_full(PK::Blob, base);
2202 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2203 (pb.build().bytes, base_oid, target_oid)
2204 }
2205
2206 #[test]
2207 fn test_unpack_objects_resolves_ref_and_ofs_deltas() {
2208 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2209
2210 let base = b"base payload";
2211 let target = b"base payload-extended";
2212 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2213 let base_oid = HashAlgo::Sha1
2214 .hash_object(ObjectKind::Blob, base)
2215 .expect("hash");
2216
2217 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2218 let base_idx = pb.add_full(PK::Blob, base);
2219 pb.add_ofs_delta(base_idx, &delta, delta.len());
2220 let ofs_pack = pb.build().bytes;
2221
2222 let tmp = tempfile::tempdir().unwrap();
2223 let odb = Odb::new(tmp.path());
2224 let opts = UnpackOptions::default();
2225 assert_eq!(
2226 unpack_objects(&mut ofs_pack.as_slice(), &odb, &opts).unwrap(),
2227 2
2228 );
2229 let target_oid = HashAlgo::Sha1
2230 .hash_object(ObjectKind::Blob, target)
2231 .expect("hash");
2232 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2233
2234 let (ref_pack, _, ref_target_oid) = pack_with_ref_delta(base, target);
2235 let tmp2 = tempfile::tempdir().unwrap();
2236 let odb2 = Odb::new(tmp2.path());
2237 assert_eq!(
2238 unpack_objects(&mut ref_pack.as_slice(), &odb2, &opts).unwrap(),
2239 2
2240 );
2241 assert_eq!(odb2.read(&ref_target_oid).unwrap().data, target);
2242 assert!(odb2.exists(&base_oid));
2243 }
2244
2245 #[test]
2246 fn test_unpack_objects_ref_delta_base_from_loose() {
2247 let base = b"loose-base";
2248 let target = b"loose-base-v2";
2249 let (_, base_oid, target_oid) = pack_with_ref_delta(base, target);
2250
2251 let tmp = tempfile::tempdir().unwrap();
2252 let odb = Odb::new(tmp.path());
2253 odb.write_local(ObjectKind::Blob, base).unwrap();
2254 let delta_only = {
2256 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2257 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2258 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2259 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2260 pb.build().bytes
2261 };
2262 let pack = delta_only;
2263
2264 let opts = UnpackOptions::default();
2265 assert_eq!(
2266 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2267 1
2268 );
2269 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2270 }
2271
2272 #[test]
2273 fn test_unpack_objects_strict_commit_tree_tag() {
2274 use crate::objects::{serialize_commit, serialize_tag, CommitData, TagData};
2275
2276 let tree_data = b"";
2277 let tree_oid = HashAlgo::Sha1
2278 .hash_object(ObjectKind::Tree, tree_data)
2279 .expect("hash");
2280 let commit = CommitData {
2281 tree: tree_oid,
2282 parents: vec![],
2283 author: "A <a@b.com>".into(),
2284 committer: "A <a@b.com>".into(),
2285 author_raw: Vec::new(),
2286 committer_raw: Vec::new(),
2287 encoding: None,
2288 message: "msg".into(),
2289 raw_message: None,
2290 extra_headers: Vec::new(),
2291 };
2292 let commit_data = serialize_commit(&commit);
2293 let commit_oid = HashAlgo::Sha1
2294 .hash_object(ObjectKind::Commit, &commit_data)
2295 .expect("hash");
2296 let tag = TagData {
2297 object: commit_oid,
2298 object_type: "commit".into(),
2299 tag: "v1".into(),
2300 tagger: Some("T <t@e.com>".into()),
2301 extra_headers: Vec::new(),
2302 message: "annotated".into(),
2303 };
2304 let tag_data = serialize_tag(&tag);
2305 let pack = make_pack(&[
2306 (ObjectKind::Tree, tree_data),
2307 (ObjectKind::Commit, &commit_data),
2308 (ObjectKind::Tag, &tag_data),
2309 ]);
2310
2311 let tmp = tempfile::tempdir().unwrap();
2312 let odb = Odb::new(tmp.path());
2313 let opts = UnpackOptions {
2314 strict: true,
2315 ..Default::default()
2316 };
2317 assert_eq!(
2318 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2319 3
2320 );
2321 assert!(odb.exists(&commit_oid));
2322 }
2323
2324 #[test]
2325 fn test_unpack_objects_strict_shallow_skips_missing_parent() {
2326 use crate::objects::{serialize_commit, CommitData};
2327
2328 let tree_oid = HashAlgo::Sha1
2329 .hash_object(ObjectKind::Tree, b"")
2330 .expect("hash");
2331 let missing_parent = ObjectId::from_hex(&"cc".repeat(20)).unwrap();
2332 let commit = CommitData {
2333 tree: tree_oid,
2334 parents: vec![missing_parent],
2335 author: "A <a@b.com>".into(),
2336 committer: "A <a@b.com>".into(),
2337 author_raw: Vec::new(),
2338 committer_raw: Vec::new(),
2339 encoding: None,
2340 message: "msg".into(),
2341 raw_message: None,
2342 extra_headers: Vec::new(),
2343 };
2344 let commit_data = serialize_commit(&commit);
2345 let commit_oid = HashAlgo::Sha1
2346 .hash_object(ObjectKind::Commit, &commit_data)
2347 .expect("hash");
2348 let pack = make_pack(&[(ObjectKind::Tree, b""), (ObjectKind::Commit, &commit_data)]);
2349
2350 let tmp = tempfile::tempdir().unwrap();
2351 let odb = Odb::new(tmp.path());
2352 let mut shallow = HashSet::new();
2353 shallow.insert(commit_oid);
2354 let opts = UnpackOptions {
2355 strict: true,
2356 shallow_boundaries: shallow,
2357 ..Default::default()
2358 };
2359 assert_eq!(
2360 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2361 2
2362 );
2363 }
2364
2365 #[test]
2366 fn test_unpack_objects_max_input_bytes() {
2367 let pack = make_pack(&[(ObjectKind::Blob, b"small")]);
2368 let tmp = tempfile::tempdir().unwrap();
2369 let odb = Odb::new(tmp.path());
2370 let opts = UnpackOptions {
2371 max_input_bytes: Some(4),
2372 ..Default::default()
2373 };
2374 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2375 assert!(err.to_string().contains("maximum allowed size"));
2376 }
2377
2378 #[test]
2379 fn test_unpack_objects_unresolved_ref_delta() {
2380 let missing = ObjectId::from_hex(&"dd".repeat(20)).unwrap();
2381 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2382 let delta = [0u8, 1u8, 1u8, b'x'];
2383 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2384 pb.add_ref_delta(missing.as_bytes(), &delta, delta.len());
2385 let pack = pb.build().bytes;
2386
2387 let tmp = tempfile::tempdir().unwrap();
2388 let odb = Odb::new(tmp.path());
2389 let err =
2390 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
2391 assert!(err.to_string().contains("could not be resolved"));
2392 }
2393
2394 #[test]
2395 fn missing_ref_delta_base_is_typed_on_every_ingest_path() {
2396 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2397 let delta = [0u8, 1u8, 1u8, b'x'];
2398 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2399 pb.add_ref_delta(&[0xdd; 20], &delta, delta.len());
2400 pb.add_ref_delta(&[0xee; 20], &delta, delta.len());
2401 let pack = pb.build().bytes;
2402 let tmp = tempfile::tempdir().unwrap();
2403 let odb = Odb::new(tmp.path());
2404
2405 let unpacked = unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default());
2406 assert!(
2407 matches!(unpacked, Err(Error::UnresolvedDeltas { count: 2 })),
2408 "{unpacked:?}"
2409 );
2410 let mapped = pack_bytes_to_object_map(&pack, &odb);
2411 assert!(
2412 matches!(mapped, Err(Error::UnresolvedDeltas { count: 2 })),
2413 "{mapped:?}"
2414 );
2415 let indexed = pack_index_records_from_bytes(&pack, &odb);
2416 assert!(
2417 matches!(indexed, Err(Error::UnresolvedDeltas { count: 2 })),
2418 "{indexed:?}"
2419 );
2420 assert!(pack_is_thin(&pack, HashAlgo::Sha1));
2421 }
2422
2423 #[test]
2424 fn test_apply_delta_into_matches_apply_delta() {
2425 let base = b"abcdef";
2426 let mut delta = Vec::new();
2427 delta.push(6u8);
2428 delta.push(3u8);
2429 delta.push(0x91u8);
2430 delta.push(2u8);
2431 delta.push(3u8);
2432
2433 let expected = apply_delta(base, &delta).unwrap();
2434 let mut out = Vec::new();
2435 apply_delta_into(&mut out, base, &delta).unwrap();
2436 assert_eq!(out, expected);
2437 }
2438
2439 #[test]
2440 fn test_pack_bytes_to_object_map_ref_delta() {
2441 let base = b"map-base";
2442 let target = b"map-base-2";
2443 let (pack, _, target_oid) = pack_with_ref_delta(base, target);
2444 let tmp = tempfile::tempdir().unwrap();
2445 let odb = Odb::new(tmp.path());
2446 let map = pack_bytes_to_object_map(&pack, &odb).unwrap();
2447 assert_eq!(map.get(&target_oid).unwrap().data, target);
2448 }
2449
2450 #[test]
2451 fn test_pack_is_thin_and_non_thin() {
2452 let base = b"thin";
2453 let target = b"thin-pack";
2454 let (full, base_oid, _) = pack_with_ref_delta(base, target);
2455 assert!(!pack_is_thin(&full, HashAlgo::Sha1));
2456
2457 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2458 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2459 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2460 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2461 let thin = pb.build().bytes;
2462 assert!(pack_is_thin(&thin, HashAlgo::Sha1));
2463 }
2464
2465 #[test]
2466 fn test_unpack_zero_byte_blob_via_pack_builder() {
2467 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2468 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2469 pb.add_full(PK::Blob, b"");
2470 let pack = pb.build().bytes;
2471 let tmp = tempfile::tempdir().unwrap();
2472 let odb = Odb::new(tmp.path());
2473 assert_eq!(
2474 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
2475 1
2476 );
2477 let oid = HashAlgo::Sha1
2478 .hash_object(ObjectKind::Blob, b"")
2479 .expect("hash");
2480 assert!(odb.exists(&oid));
2481 }
2482
2483 #[test]
2484 fn test_strict_verify_rejects_missing_tree_reference() {
2485 let tree_oid = HashAlgo::Sha1
2486 .hash_object(ObjectKind::Tree, b"")
2487 .expect("hash");
2488 let missing = ObjectId::from_hex(&"ee".repeat(20)).unwrap();
2489 let bad_tree = crate::objects::serialize_tree(&[crate::objects::TreeEntry {
2490 mode: crate::index::MODE_REGULAR,
2491 name: b"f".to_vec(),
2492 oid: missing,
2493 }]);
2494 let bad_oid = HashAlgo::Sha1
2495 .hash_object(ObjectKind::Tree, &bad_tree)
2496 .expect("hash");
2497 let mut pack = HashMap::new();
2498 pack.insert(tree_oid, (ObjectKind::Tree, b"".to_vec()));
2499 pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2500 assert!(strict_verify_packed_references(None, &pack).is_err());
2501 }
2502
2503 #[test]
2504 fn test_unpack_large_blob_materializes_on_disk() {
2505 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
2506 let pack = make_pack(&[(ObjectKind::Blob, &big)]);
2507 let tmp = tempfile::tempdir().unwrap();
2508 let odb = Odb::new(tmp.path());
2509 assert_eq!(
2510 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
2511 1
2512 );
2513 let oid = HashAlgo::Sha1
2514 .hash_object(ObjectKind::Blob, &big)
2515 .expect("hash");
2516 assert_eq!(odb.read(&oid).unwrap().data, big);
2517 }
2518
2519 #[test]
2520 fn truncated_zlib_after_complete_payload_does_not_hang() {
2521 use std::time::{Duration, Instant};
2522
2523 let payload = vec![0u8; 4096];
2524 let mut pack = make_pack(&[(ObjectKind::Blob, &payload)]);
2525 pack.truncate(pack.len() - 21);
2526 let digest = HashAlgo::Sha1.checksum_digest(&pack);
2527 pack.extend_from_slice(&digest);
2528
2529 let tmp = tempfile::tempdir().unwrap();
2530 let odb = Odb::new(tmp.path());
2531 let started = Instant::now();
2532 let err =
2533 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
2534 assert!(
2535 started.elapsed() < Duration::from_secs(2),
2536 "unpack hung on truncated zlib"
2537 );
2538 let msg = err.to_string();
2539 assert!(
2540 msg.contains("truncated") || msg.contains("zlib") || msg.contains("mismatch"),
2541 "unexpected error: {msg}"
2542 );
2543 }
2544
2545 #[test]
2546 fn strict_dot_special_does_not_reload_streamed_blobs() {
2547 use std::cell::Cell;
2548
2549 let tmp = tempfile::tempdir().unwrap();
2550 let odb = Odb::new(tmp.path());
2551 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
2552 let big_oid = HashAlgo::Sha1
2553 .hash_object(ObjectKind::Blob, &big)
2554 .expect("hash");
2555 let tree_oid = HashAlgo::Sha1
2556 .hash_object(ObjectKind::Tree, b"")
2557 .expect("hash");
2558
2559 struct CountingSink<'a> {
2560 inner: &'a Odb,
2561 reads: Cell<usize>,
2562 }
2563
2564 impl UnpackSink for CountingSink<'_> {
2565 fn hash_algo(&self) -> HashAlgo {
2566 Odb::hash_algo(self.inner)
2567 }
2568
2569 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
2570 if dry_run {
2571 return self.inner.hash(kind, data);
2572 }
2573 self.inner.write(kind, data)
2574 }
2575
2576 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
2577 self.reads.set(self.reads.get() + 1);
2578 self.inner.read_stored(oid)
2579 }
2580
2581 fn contains(&self, oid: &ObjectId) -> Result<bool> {
2582 Odb::contains(self.inner, oid)
2583 }
2584 }
2585
2586 let mut by_oid = HashMap::new();
2587 by_oid.insert(big_oid, PackedObjectEntry::BlobOnDisk { oid: big_oid });
2588 by_oid.insert(
2589 tree_oid,
2590 PackedObjectEntry::InMemory {
2591 kind: ObjectKind::Tree,
2592 data: Vec::new(),
2593 },
2594 );
2595
2596 let sink = CountingSink {
2597 inner: &odb,
2598 reads: Cell::new(0),
2599 };
2600 verify_packed_dot_special_entries(&by_oid, &sink).expect("dot-special walk");
2601 assert_eq!(
2602 sink.reads.get(),
2603 0,
2604 "must not read streamed blobs back for strict fsck"
2605 );
2606 }
2607}