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 #[cfg_attr(
1946 not(feature = "full-tests"),
1947 ignore = "starts system git or is heavy; run with --features full-tests"
1948 )]
1949 #[test]
1950 fn test_unpack_objects_blobs() {
1951 use tempfile::TempDir;
1952 let tmp = TempDir::new().unwrap();
1953 let objects_dir = tmp.path().join("objects");
1954 std::fs::create_dir_all(&objects_dir).unwrap();
1955 let odb = Odb::new(&objects_dir);
1956
1957 let pack = make_pack(&[
1958 (ObjectKind::Blob, b"hello\n"),
1959 (ObjectKind::Blob, b"world\n"),
1960 ]);
1961
1962 let opts = UnpackOptions::default();
1963 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
1964 assert_eq!(count, 2);
1965
1966 let oid1 = HashAlgo::Sha1
1968 .hash_object(ObjectKind::Blob, b"hello\n")
1969 .expect("hash");
1970 let oid2 = HashAlgo::Sha1
1971 .hash_object(ObjectKind::Blob, b"world\n")
1972 .expect("hash");
1973 let obj1 = odb.read(&oid1).unwrap();
1974 let obj2 = odb.read(&oid2).unwrap();
1975 assert_eq!(obj1.data, b"hello\n");
1976 assert_eq!(obj2.data, b"world\n");
1977 }
1978
1979 #[cfg_attr(
1980 not(feature = "full-tests"),
1981 ignore = "starts system git or is heavy; run with --features full-tests"
1982 )]
1983 #[test]
1984 fn test_unpack_objects_empty_tree() {
1985 use tempfile::TempDir;
1986 let tmp = TempDir::new().unwrap();
1987 let objects_dir = tmp.path().join("objects");
1988 std::fs::create_dir_all(&objects_dir).unwrap();
1989 let odb = Odb::new(&objects_dir);
1990
1991 let pack = make_pack(&[(ObjectKind::Tree, b"")]);
1992 let opts = UnpackOptions::default();
1993 assert_eq!(
1994 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
1995 1
1996 );
1997 let oid = HashAlgo::Sha1
1998 .hash_object(ObjectKind::Tree, b"")
1999 .expect("hash");
2000 assert!(odb.exists(&oid));
2001 let loose = objects_dir
2002 .join(oid.loose_prefix())
2003 .join(oid.loose_suffix());
2004 assert!(
2005 loose.is_file(),
2006 "empty tree must be materialized as a loose object during unpack"
2007 );
2008 }
2009
2010 #[cfg_attr(
2011 not(feature = "full-tests"),
2012 ignore = "starts system git or is heavy; run with --features full-tests"
2013 )]
2014 #[test]
2015 fn test_strict_skips_gitlink_tree_entries() {
2016 use crate::index::{MODE_GITLINK, MODE_REGULAR};
2017 use crate::objects::{serialize_tree, TreeEntry};
2018
2019 let submodule_oid = ObjectId::from_hex(&"7f".repeat(20)).unwrap();
2022
2023 let tree_data = serialize_tree(&[TreeEntry {
2025 mode: MODE_GITLINK,
2026 name: b"sub".to_vec(),
2027 oid: submodule_oid,
2028 }]);
2029 let tree_oid = HashAlgo::Sha1
2030 .hash_object(ObjectKind::Tree, &tree_data)
2031 .expect("hash");
2032
2033 let mut pack = HashMap::new();
2036 pack.insert(tree_oid, (ObjectKind::Tree, tree_data.clone()));
2037 assert!(strict_verify_packed_references(None, &pack).is_ok());
2038
2039 let bad_tree = serialize_tree(&[TreeEntry {
2042 mode: MODE_REGULAR,
2043 name: b"file".to_vec(),
2044 oid: ObjectId::from_hex(&"ab".repeat(20)).unwrap(),
2045 }]);
2046 let bad_oid = HashAlgo::Sha1
2047 .hash_object(ObjectKind::Tree, &bad_tree)
2048 .expect("hash");
2049 let mut bad_pack = HashMap::new();
2050 bad_pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2051 assert!(matches!(
2052 strict_verify_packed_references(None, &bad_pack),
2053 Err(Error::CorruptObject(_))
2054 ));
2055 }
2056
2057 struct ChunkedReader<'a> {
2059 data: &'a [u8],
2060 pos: usize,
2061 max_len: usize,
2062 }
2063
2064 impl io::Read for ChunkedReader<'_> {
2065 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2066 if self.pos >= self.data.len() {
2067 return Ok(0);
2068 }
2069 let take = (self.data.len() - self.pos)
2070 .min(self.max_len)
2071 .min(buf.len());
2072 buf[..take].copy_from_slice(&self.data[self.pos..self.pos + take]);
2073 self.pos += take;
2074 Ok(take)
2075 }
2076 }
2077
2078 #[cfg_attr(
2079 not(feature = "full-tests"),
2080 ignore = "starts system git or is heavy; run with --features full-tests"
2081 )]
2082 #[test]
2083 fn test_unpack_objects_chunked_read_matches_full_buffer() {
2084 use tempfile::TempDir;
2085 let pack = make_pack(&[(ObjectKind::Blob, b"chunked-stream")]);
2086 let opts = UnpackOptions::default();
2087 let oid = HashAlgo::Sha1
2088 .hash_object(ObjectKind::Blob, b"chunked-stream")
2089 .expect("hash");
2090
2091 let tmp = TempDir::new().unwrap();
2092 let objects_dir = tmp.path().join("objects");
2093 std::fs::create_dir_all(&objects_dir).unwrap();
2094 let odb = Odb::new(&objects_dir);
2095 assert_eq!(
2096 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2097 1
2098 );
2099 assert!(odb.exists(&oid));
2100
2101 let tmp2 = TempDir::new().unwrap();
2102 let objects_dir2 = tmp2.path().join("objects");
2103 std::fs::create_dir_all(&objects_dir2).unwrap();
2104 let odb2 = Odb::new(&objects_dir2);
2105 let mut chunked = ChunkedReader {
2106 data: pack.as_slice(),
2107 pos: 0,
2108 max_len: 8,
2109 };
2110 assert_eq!(unpack_objects(&mut chunked, &odb2, &opts).unwrap(), 1);
2111 assert!(odb2.exists(&oid));
2112 }
2113
2114 #[cfg_attr(
2115 not(feature = "full-tests"),
2116 ignore = "starts system git or is heavy; run with --features full-tests"
2117 )]
2118 #[test]
2119 fn test_unpack_objects_dry_run_writes_nothing() {
2120 use tempfile::TempDir;
2121 let tmp = TempDir::new().unwrap();
2122 let objects_dir = tmp.path().join("objects");
2123 std::fs::create_dir_all(&objects_dir).unwrap();
2124 let odb = Odb::new(&objects_dir);
2125
2126 let pack = make_pack(&[(ObjectKind::Blob, b"test content")]);
2127
2128 let opts = UnpackOptions {
2129 dry_run: true,
2130 quiet: true,
2131 strict: false,
2132 allowed_missing: Default::default(),
2133 allow_promisor_missing_references: false,
2134 max_input_bytes: None,
2135 ..Default::default()
2136 };
2137 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
2138 assert_eq!(count, 1);
2139
2140 let oid = HashAlgo::Sha1
2142 .hash_object(ObjectKind::Blob, b"test content")
2143 .expect("hash");
2144 assert!(!odb.exists(&oid));
2145 }
2146
2147 #[cfg_attr(
2148 not(feature = "full-tests"),
2149 ignore = "starts system git or is heavy; run with --features full-tests"
2150 )]
2151 #[test]
2152 fn test_unpack_objects_bad_signature() {
2153 use tempfile::TempDir;
2154 let tmp = TempDir::new().unwrap();
2155 let objects_dir = tmp.path().join("objects");
2156 std::fs::create_dir_all(&objects_dir).unwrap();
2157 let odb = Odb::new(&objects_dir);
2158
2159 let mut bad = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00".to_vec();
2160 bad.extend_from_slice(&[0u8; 20]);
2161 let opts = UnpackOptions::default();
2162 let err = unpack_objects(&mut bad.as_slice(), &odb, &opts).unwrap_err();
2163 assert!(err.to_string().contains("invalid signature"));
2164 }
2165
2166 #[cfg_attr(
2167 not(feature = "full-tests"),
2168 ignore = "starts system git or is heavy; run with --features full-tests"
2169 )]
2170 #[test]
2171 fn test_unpack_objects_checksum_mismatch() {
2172 use tempfile::TempDir;
2173 let tmp = TempDir::new().unwrap();
2174 let objects_dir = tmp.path().join("objects");
2175 std::fs::create_dir_all(&objects_dir).unwrap();
2176 let odb = Odb::new(&objects_dir);
2177
2178 let mut pack = make_pack(&[(ObjectKind::Blob, b"data")]);
2179 let n = pack.len();
2181 pack[n - 1] ^= 0xFF;
2182
2183 let opts = UnpackOptions::default();
2184 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2185 assert!(err.to_string().contains("checksum"));
2186 }
2187
2188 #[test]
2189 fn test_apply_delta_source_size_mismatch() {
2190 let base = b"hi";
2191 let delta = [3u8, 2u8, 2u8, b'h', b'i']; let err = apply_delta(base, &delta).unwrap_err();
2193 assert!(err.to_string().contains("source size"));
2194 }
2195
2196 #[test]
2197 fn pack_index_many_blobs_without_retaining_all_payloads() {
2198 const N: usize = 1500;
2199 let blobs: Vec<(ObjectKind, Vec<u8>)> = (0..N)
2200 .map(|i| (ObjectKind::Blob, format!("blob-{i}\n").into_bytes()))
2201 .collect();
2202 let blob_refs: Vec<(ObjectKind, &[u8])> =
2203 blobs.iter().map(|(k, d)| (*k, d.as_slice())).collect();
2204 let pack = make_pack(&blob_refs);
2205 let tmp = tempfile::tempdir().expect("tempdir");
2206 let odb = Odb::new(tmp.path());
2207 let started = std::time::Instant::now();
2208 let records = pack_index_records_from_bytes(&pack, &odb).expect("index records");
2209 let elapsed = started.elapsed();
2210 assert_eq!(records.len(), N, "one index row per object");
2211 assert!(
2212 elapsed.as_secs() < 30,
2213 "indexing {N} blobs took {:?}; expected linear scan without full retention blow-up",
2214 elapsed
2215 );
2216 }
2217
2218 fn pack_with_ref_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, ObjectId, ObjectId) {
2219 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2220
2221 let delta = crate::delta_encode::encode_lcp_delta(base, target).expect("delta");
2222 let base_oid = HashAlgo::Sha1
2223 .hash_object(ObjectKind::Blob, base)
2224 .expect("hash");
2225 let target_oid = HashAlgo::Sha1
2226 .hash_object(ObjectKind::Blob, target)
2227 .expect("hash");
2228 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2229 pb.add_full(PK::Blob, base);
2230 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2231 (pb.build().bytes, base_oid, target_oid)
2232 }
2233
2234 #[cfg_attr(
2235 not(feature = "full-tests"),
2236 ignore = "starts system git or is heavy; run with --features full-tests"
2237 )]
2238 #[test]
2239 fn test_unpack_objects_resolves_ref_and_ofs_deltas() {
2240 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2241
2242 let base = b"base payload";
2243 let target = b"base payload-extended";
2244 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2245 let base_oid = HashAlgo::Sha1
2246 .hash_object(ObjectKind::Blob, base)
2247 .expect("hash");
2248
2249 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2250 let base_idx = pb.add_full(PK::Blob, base);
2251 pb.add_ofs_delta(base_idx, &delta, delta.len());
2252 let ofs_pack = pb.build().bytes;
2253
2254 let tmp = tempfile::tempdir().unwrap();
2255 let odb = Odb::new(tmp.path());
2256 let opts = UnpackOptions::default();
2257 assert_eq!(
2258 unpack_objects(&mut ofs_pack.as_slice(), &odb, &opts).unwrap(),
2259 2
2260 );
2261 let target_oid = HashAlgo::Sha1
2262 .hash_object(ObjectKind::Blob, target)
2263 .expect("hash");
2264 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2265
2266 let (ref_pack, _, ref_target_oid) = pack_with_ref_delta(base, target);
2267 let tmp2 = tempfile::tempdir().unwrap();
2268 let odb2 = Odb::new(tmp2.path());
2269 assert_eq!(
2270 unpack_objects(&mut ref_pack.as_slice(), &odb2, &opts).unwrap(),
2271 2
2272 );
2273 assert_eq!(odb2.read(&ref_target_oid).unwrap().data, target);
2274 assert!(odb2.exists(&base_oid));
2275 }
2276
2277 #[cfg_attr(
2278 not(feature = "full-tests"),
2279 ignore = "starts system git or is heavy; run with --features full-tests"
2280 )]
2281 #[test]
2282 fn test_unpack_objects_ref_delta_base_from_loose() {
2283 let base = b"loose-base";
2284 let target = b"loose-base-v2";
2285 let (_, base_oid, target_oid) = pack_with_ref_delta(base, target);
2286
2287 let tmp = tempfile::tempdir().unwrap();
2288 let odb = Odb::new(tmp.path());
2289 odb.write_local(ObjectKind::Blob, base).unwrap();
2290 let delta_only = {
2292 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2293 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2294 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2295 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2296 pb.build().bytes
2297 };
2298 let pack = delta_only;
2299
2300 let opts = UnpackOptions::default();
2301 assert_eq!(
2302 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2303 1
2304 );
2305 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2306 }
2307
2308 #[cfg_attr(
2309 not(feature = "full-tests"),
2310 ignore = "starts system git or is heavy; run with --features full-tests"
2311 )]
2312 #[test]
2313 fn test_unpack_objects_strict_commit_tree_tag() {
2314 use crate::objects::{serialize_commit, serialize_tag, CommitData, TagData};
2315
2316 let tree_data = b"";
2317 let tree_oid = HashAlgo::Sha1
2318 .hash_object(ObjectKind::Tree, tree_data)
2319 .expect("hash");
2320 let commit = CommitData {
2321 tree: tree_oid,
2322 parents: vec![],
2323 author: "A <a@b.com>".into(),
2324 committer: "A <a@b.com>".into(),
2325 author_raw: Vec::new(),
2326 committer_raw: Vec::new(),
2327 encoding: None,
2328 message: "msg".into(),
2329 raw_message: None,
2330 extra_headers: Vec::new(),
2331 };
2332 let commit_data = serialize_commit(&commit);
2333 let commit_oid = HashAlgo::Sha1
2334 .hash_object(ObjectKind::Commit, &commit_data)
2335 .expect("hash");
2336 let tag = TagData {
2337 object: commit_oid,
2338 object_type: "commit".into(),
2339 tag: "v1".into(),
2340 tagger: Some("T <t@e.com>".into()),
2341 extra_headers: Vec::new(),
2342 message: "annotated".into(),
2343 };
2344 let tag_data = serialize_tag(&tag);
2345 let pack = make_pack(&[
2346 (ObjectKind::Tree, tree_data),
2347 (ObjectKind::Commit, &commit_data),
2348 (ObjectKind::Tag, &tag_data),
2349 ]);
2350
2351 let tmp = tempfile::tempdir().unwrap();
2352 let odb = Odb::new(tmp.path());
2353 let opts = UnpackOptions {
2354 strict: true,
2355 ..Default::default()
2356 };
2357 assert_eq!(
2358 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2359 3
2360 );
2361 assert!(odb.exists(&commit_oid));
2362 }
2363
2364 #[cfg_attr(
2365 not(feature = "full-tests"),
2366 ignore = "starts system git or is heavy; run with --features full-tests"
2367 )]
2368 #[test]
2369 fn test_unpack_objects_strict_shallow_skips_missing_parent() {
2370 use crate::objects::{serialize_commit, CommitData};
2371
2372 let tree_oid = HashAlgo::Sha1
2373 .hash_object(ObjectKind::Tree, b"")
2374 .expect("hash");
2375 let missing_parent = ObjectId::from_hex(&"cc".repeat(20)).unwrap();
2376 let commit = CommitData {
2377 tree: tree_oid,
2378 parents: vec![missing_parent],
2379 author: "A <a@b.com>".into(),
2380 committer: "A <a@b.com>".into(),
2381 author_raw: Vec::new(),
2382 committer_raw: Vec::new(),
2383 encoding: None,
2384 message: "msg".into(),
2385 raw_message: None,
2386 extra_headers: Vec::new(),
2387 };
2388 let commit_data = serialize_commit(&commit);
2389 let commit_oid = HashAlgo::Sha1
2390 .hash_object(ObjectKind::Commit, &commit_data)
2391 .expect("hash");
2392 let pack = make_pack(&[(ObjectKind::Tree, b""), (ObjectKind::Commit, &commit_data)]);
2393
2394 let tmp = tempfile::tempdir().unwrap();
2395 let odb = Odb::new(tmp.path());
2396 let mut shallow = HashSet::new();
2397 shallow.insert(commit_oid);
2398 let opts = UnpackOptions {
2399 strict: true,
2400 shallow_boundaries: shallow,
2401 ..Default::default()
2402 };
2403 assert_eq!(
2404 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2405 2
2406 );
2407 }
2408
2409 #[cfg_attr(
2410 not(feature = "full-tests"),
2411 ignore = "starts system git or is heavy; run with --features full-tests"
2412 )]
2413 #[test]
2414 fn test_unpack_objects_max_input_bytes() {
2415 let pack = make_pack(&[(ObjectKind::Blob, b"small")]);
2416 let tmp = tempfile::tempdir().unwrap();
2417 let odb = Odb::new(tmp.path());
2418 let opts = UnpackOptions {
2419 max_input_bytes: Some(4),
2420 ..Default::default()
2421 };
2422 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2423 assert!(err.to_string().contains("maximum allowed size"));
2424 }
2425
2426 #[cfg_attr(
2427 not(feature = "full-tests"),
2428 ignore = "starts system git or is heavy; run with --features full-tests"
2429 )]
2430 #[test]
2431 fn test_unpack_objects_unresolved_ref_delta() {
2432 let missing = ObjectId::from_hex(&"dd".repeat(20)).unwrap();
2433 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2434 let delta = [0u8, 1u8, 1u8, b'x'];
2435 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2436 pb.add_ref_delta(missing.as_bytes(), &delta, delta.len());
2437 let pack = pb.build().bytes;
2438
2439 let tmp = tempfile::tempdir().unwrap();
2440 let odb = Odb::new(tmp.path());
2441 let err =
2442 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
2443 assert!(err.to_string().contains("could not be resolved"));
2444 }
2445
2446 #[cfg_attr(
2447 not(feature = "full-tests"),
2448 ignore = "starts system git or is heavy; run with --features full-tests"
2449 )]
2450 #[test]
2451 fn missing_ref_delta_base_is_typed_on_every_ingest_path() {
2452 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2453 let delta = [0u8, 1u8, 1u8, b'x'];
2454 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2455 pb.add_ref_delta(&[0xdd; 20], &delta, delta.len());
2456 pb.add_ref_delta(&[0xee; 20], &delta, delta.len());
2457 let pack = pb.build().bytes;
2458 let tmp = tempfile::tempdir().unwrap();
2459 let odb = Odb::new(tmp.path());
2460
2461 let unpacked = unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default());
2462 assert!(
2463 matches!(unpacked, Err(Error::UnresolvedDeltas { count: 2 })),
2464 "{unpacked:?}"
2465 );
2466 let mapped = pack_bytes_to_object_map(&pack, &odb);
2467 assert!(
2468 matches!(mapped, Err(Error::UnresolvedDeltas { count: 2 })),
2469 "{mapped:?}"
2470 );
2471 let indexed = pack_index_records_from_bytes(&pack, &odb);
2472 assert!(
2473 matches!(indexed, Err(Error::UnresolvedDeltas { count: 2 })),
2474 "{indexed:?}"
2475 );
2476 assert!(pack_is_thin(&pack, HashAlgo::Sha1));
2477 }
2478
2479 #[test]
2480 fn test_apply_delta_into_matches_apply_delta() {
2481 let base = b"abcdef";
2482 let mut delta = Vec::new();
2483 delta.push(6u8);
2484 delta.push(3u8);
2485 delta.push(0x91u8);
2486 delta.push(2u8);
2487 delta.push(3u8);
2488
2489 let expected = apply_delta(base, &delta).unwrap();
2490 let mut out = Vec::new();
2491 apply_delta_into(&mut out, base, &delta).unwrap();
2492 assert_eq!(out, expected);
2493 }
2494
2495 #[test]
2496 fn test_pack_bytes_to_object_map_ref_delta() {
2497 let base = b"map-base";
2498 let target = b"map-base-2";
2499 let (pack, _, target_oid) = pack_with_ref_delta(base, target);
2500 let tmp = tempfile::tempdir().unwrap();
2501 let odb = Odb::new(tmp.path());
2502 let map = pack_bytes_to_object_map(&pack, &odb).unwrap();
2503 assert_eq!(map.get(&target_oid).unwrap().data, target);
2504 }
2505
2506 #[test]
2507 fn test_pack_is_thin_and_non_thin() {
2508 let base = b"thin";
2509 let target = b"thin-pack";
2510 let (full, base_oid, _) = pack_with_ref_delta(base, target);
2511 assert!(!pack_is_thin(&full, HashAlgo::Sha1));
2512
2513 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2514 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2515 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2516 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2517 let thin = pb.build().bytes;
2518 assert!(pack_is_thin(&thin, HashAlgo::Sha1));
2519 }
2520
2521 #[cfg_attr(
2522 not(feature = "full-tests"),
2523 ignore = "starts system git or is heavy; run with --features full-tests"
2524 )]
2525 #[test]
2526 fn test_unpack_zero_byte_blob_via_pack_builder() {
2527 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2528 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2529 pb.add_full(PK::Blob, b"");
2530 let pack = pb.build().bytes;
2531 let tmp = tempfile::tempdir().unwrap();
2532 let odb = Odb::new(tmp.path());
2533 assert_eq!(
2534 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
2535 1
2536 );
2537 let oid = HashAlgo::Sha1
2538 .hash_object(ObjectKind::Blob, b"")
2539 .expect("hash");
2540 assert!(odb.exists(&oid));
2541 }
2542
2543 #[test]
2544 fn test_strict_verify_rejects_missing_tree_reference() {
2545 let tree_oid = HashAlgo::Sha1
2546 .hash_object(ObjectKind::Tree, b"")
2547 .expect("hash");
2548 let missing = ObjectId::from_hex(&"ee".repeat(20)).unwrap();
2549 let bad_tree = crate::objects::serialize_tree(&[crate::objects::TreeEntry {
2550 mode: crate::index::MODE_REGULAR,
2551 name: b"f".to_vec(),
2552 oid: missing,
2553 }]);
2554 let bad_oid = HashAlgo::Sha1
2555 .hash_object(ObjectKind::Tree, &bad_tree)
2556 .expect("hash");
2557 let mut pack = HashMap::new();
2558 pack.insert(tree_oid, (ObjectKind::Tree, b"".to_vec()));
2559 pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2560 assert!(strict_verify_packed_references(None, &pack).is_err());
2561 }
2562
2563 #[cfg_attr(
2564 not(feature = "full-tests"),
2565 ignore = "starts system git or is heavy; run with --features full-tests"
2566 )]
2567 #[test]
2568 fn test_unpack_large_blob_materializes_on_disk() {
2569 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
2570 let pack = make_pack(&[(ObjectKind::Blob, &big)]);
2571 let tmp = tempfile::tempdir().unwrap();
2572 let odb = Odb::new(tmp.path());
2573 assert_eq!(
2574 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
2575 1
2576 );
2577 let oid = HashAlgo::Sha1
2578 .hash_object(ObjectKind::Blob, &big)
2579 .expect("hash");
2580 assert_eq!(odb.read(&oid).unwrap().data, big);
2581 }
2582
2583 #[cfg_attr(
2584 not(feature = "full-tests"),
2585 ignore = "starts system git or is heavy; run with --features full-tests"
2586 )]
2587 #[test]
2588 fn truncated_zlib_after_complete_payload_does_not_hang() {
2589 use std::time::{Duration, Instant};
2590
2591 let payload = vec![0u8; 4096];
2592 let mut pack = make_pack(&[(ObjectKind::Blob, &payload)]);
2593 pack.truncate(pack.len() - 21);
2594 let digest = HashAlgo::Sha1.checksum_digest(&pack);
2595 pack.extend_from_slice(&digest);
2596
2597 let tmp = tempfile::tempdir().unwrap();
2598 let odb = Odb::new(tmp.path());
2599 let started = Instant::now();
2600 let err =
2601 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
2602 assert!(
2603 started.elapsed() < Duration::from_secs(2),
2604 "unpack hung on truncated zlib"
2605 );
2606 let msg = err.to_string();
2607 assert!(
2608 msg.contains("truncated") || msg.contains("zlib") || msg.contains("mismatch"),
2609 "unexpected error: {msg}"
2610 );
2611 }
2612
2613 #[test]
2614 fn strict_dot_special_does_not_reload_streamed_blobs() {
2615 use std::cell::Cell;
2616
2617 let tmp = tempfile::tempdir().unwrap();
2618 let odb = Odb::new(tmp.path());
2619 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
2620 let big_oid = HashAlgo::Sha1
2621 .hash_object(ObjectKind::Blob, &big)
2622 .expect("hash");
2623 let tree_oid = HashAlgo::Sha1
2624 .hash_object(ObjectKind::Tree, b"")
2625 .expect("hash");
2626
2627 struct CountingSink<'a> {
2628 inner: &'a Odb,
2629 reads: Cell<usize>,
2630 }
2631
2632 impl UnpackSink for CountingSink<'_> {
2633 fn hash_algo(&self) -> HashAlgo {
2634 Odb::hash_algo(self.inner)
2635 }
2636
2637 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
2638 if dry_run {
2639 return self.inner.hash(kind, data);
2640 }
2641 self.inner.write(kind, data)
2642 }
2643
2644 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
2645 self.reads.set(self.reads.get() + 1);
2646 self.inner.read_stored(oid)
2647 }
2648
2649 fn contains(&self, oid: &ObjectId) -> Result<bool> {
2650 Odb::contains(self.inner, oid)
2651 }
2652 }
2653
2654 let mut by_oid = HashMap::new();
2655 by_oid.insert(big_oid, PackedObjectEntry::BlobOnDisk { oid: big_oid });
2656 by_oid.insert(
2657 tree_oid,
2658 PackedObjectEntry::InMemory {
2659 kind: ObjectKind::Tree,
2660 data: Vec::new(),
2661 },
2662 );
2663
2664 let sink = CountingSink {
2665 inner: &odb,
2666 reads: Cell::new(0),
2667 };
2668 verify_packed_dot_special_entries(&by_oid, &sink).expect("dot-special walk");
2669 assert_eq!(
2670 sink.reads.get(),
2671 0,
2672 "must not read streamed blobs back for strict fsck"
2673 );
2674 }
2675}