1use std::borrow::Cow;
13use std::collections::{HashMap, HashSet};
14use std::io::{self, Read};
15
16use crate::error::{Error, Result};
17use crate::gitmodules::{self, DotFsckTracker};
18use crate::hash::{ChecksumHasher, ObjectPayloadHasher};
19use crate::index::MODE_GITLINK;
20use crate::index_pack::ObjectKindCounts;
21use crate::objects::{parse_commit, parse_tag, parse_tree, HashAlgo, Object, ObjectId, ObjectKind};
22use crate::odb::store::{ObjectStore, WritableObjectStore};
23use crate::odb::{Odb, WriteOptions};
24use crate::progress::{poll_cancel, Cancel, NullProgress, ProgressPhase, ProgressSink};
25use flate2::read::ZlibDecoder;
26use flate2::{Decompress, FlushDecompress, Status};
27
28#[derive(Debug, Default)]
30pub struct UnpackOptions {
31 pub dry_run: bool,
33 pub quiet: bool,
35 pub strict: bool,
37 pub allowed_missing: HashSet<ObjectId>,
39 pub allow_promisor_missing_references: bool,
41 pub max_input_bytes: Option<u64>,
46 pub shallow_boundaries: HashSet<ObjectId>,
52}
53
54struct PendingDelta {
56 offset: usize,
59 base_oid: Option<ObjectId>,
61 base_offset: Option<usize>,
63 delta_data: Vec<u8>,
65}
66
67const UNPACK_DELTA_BASE_BYTE_LIMIT: usize = 96 * 1024 * 1024;
69
70struct UnpackBaseByteCache {
72 byte_limit: usize,
73 bytes_used: usize,
74 entries: HashMap<usize, (ObjectKind, Vec<u8>)>,
75 head: Option<usize>,
76 tail: Option<usize>,
77 links: HashMap<usize, (Option<usize>, Option<usize>)>,
78}
79
80impl UnpackBaseByteCache {
81 fn new(byte_limit: usize) -> Self {
82 Self {
83 byte_limit,
84 bytes_used: 0,
85 entries: HashMap::new(),
86 head: None,
87 tail: None,
88 links: HashMap::new(),
89 }
90 }
91
92 fn unlink(&mut self, key: usize) {
93 let Some((prev, next)) = self.links.remove(&key) else {
94 return;
95 };
96 match prev {
97 Some(p) => {
98 if let Some(link) = self.links.get_mut(&p) {
99 link.1 = next;
100 }
101 }
102 None => self.head = next,
103 }
104 match next {
105 Some(n) => {
106 if let Some(link) = self.links.get_mut(&n) {
107 link.0 = prev;
108 }
109 }
110 None => self.tail = prev,
111 }
112 }
113
114 fn append_tail(&mut self, key: usize) {
115 let prev = self.tail;
116 self.links.insert(key, (prev, None));
117 if let Some(p) = prev {
118 if let Some(link) = self.links.get_mut(&p) {
119 link.1 = Some(key);
120 }
121 } else {
122 self.head = Some(key);
123 }
124 self.tail = Some(key);
125 }
126
127 fn touch(&mut self, key: usize) {
128 self.unlink(key);
129 self.append_tail(key);
130 }
131
132 fn evict_key(&mut self, key: usize) {
133 self.unlink(key);
134 if let Some((_, data)) = self.entries.remove(&key) {
135 self.bytes_used = self.bytes_used.saturating_sub(data.len());
136 }
137 }
138
139 fn get(&mut self, offset: usize) -> Option<(ObjectKind, Vec<u8>)> {
140 let (kind, data) = self.entries.get(&offset).map(|(k, d)| (*k, d.clone()))?;
141 self.touch(offset);
142 Some((kind, data))
143 }
144
145 fn put(&mut self, offset: usize, kind: ObjectKind, data: Vec<u8>) {
146 if self.byte_limit == 0 {
147 return;
148 }
149 let sz = data.len();
150 if sz > self.byte_limit {
151 return;
152 }
153 use std::collections::hash_map::Entry;
154 match self.entries.entry(offset) {
155 Entry::Occupied(mut slot) => {
156 let old_len = slot.get().1.len();
157 slot.insert((kind, data));
158 self.bytes_used = self.bytes_used.saturating_sub(old_len).saturating_add(sz);
159 self.touch(offset);
160 }
161 Entry::Vacant(slot) => {
162 slot.insert((kind, data));
163 self.bytes_used = self.bytes_used.saturating_add(sz);
164 self.append_tail(offset);
165 }
166 }
167 while self.bytes_used > self.byte_limit {
168 let Some(evict) = self.head else {
169 break;
170 };
171 self.evict_key(evict);
172 }
173 test_note_retained_bytes(self.bytes_used);
174 }
175
176 fn bytes_used(&self) -> usize {
177 self.bytes_used
178 }
179}
180
181#[cfg(test)]
182static TEST_UNPACK_CACHE_BYTE_LIMIT: std::sync::atomic::AtomicUsize =
183 std::sync::atomic::AtomicUsize::new(0);
184#[cfg(test)]
185static TEST_UNPACK_RETAINED_PEAK: std::sync::atomic::AtomicUsize =
186 std::sync::atomic::AtomicUsize::new(0);
187#[cfg(test)]
188static TEST_UNPACK_RETAIN_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
189
190fn unpack_base_cache_byte_limit() -> usize {
191 #[cfg(test)]
192 {
193 let override_limit =
194 TEST_UNPACK_CACHE_BYTE_LIMIT.load(std::sync::atomic::Ordering::Relaxed);
195 if override_limit != 0 {
196 return override_limit;
197 }
198 }
199 UNPACK_DELTA_BASE_BYTE_LIMIT
200}
201
202#[cfg(test)]
203pub(crate) fn test_set_unpack_base_cache_byte_limit(bytes: usize) {
204 TEST_UNPACK_CACHE_BYTE_LIMIT.store(bytes, std::sync::atomic::Ordering::Relaxed);
205 TEST_UNPACK_RETAINED_PEAK.store(0, std::sync::atomic::Ordering::Relaxed);
206}
207
208#[cfg(test)]
209pub(crate) fn test_peak_unpack_retained_bytes() -> usize {
210 TEST_UNPACK_RETAINED_PEAK.load(std::sync::atomic::Ordering::Relaxed)
211}
212
213fn test_note_retained_bytes(bytes: usize) {
214 #[cfg(test)]
215 {
216 let mut cur = TEST_UNPACK_RETAINED_PEAK.load(std::sync::atomic::Ordering::Relaxed);
217 while cur < bytes {
218 match TEST_UNPACK_RETAINED_PEAK.compare_exchange_weak(
219 cur,
220 bytes,
221 std::sync::atomic::Ordering::Relaxed,
222 std::sync::atomic::Ordering::Relaxed,
223 ) {
224 Ok(_) => break,
225 Err(v) => cur = v,
226 }
227 }
228 }
229 #[cfg(not(test))]
230 let _ = bytes;
231}
232
233fn test_note_pending_delta_bytes(pending: &[PendingDelta], cache_bytes: usize) {
234 #[cfg(test)]
235 {
236 let pending_bytes: usize = pending.iter().map(|d| d.delta_data.len()).sum();
237 test_note_retained_bytes(cache_bytes.saturating_add(pending_bytes));
238 }
239 let _ = (pending, cache_bytes);
240}
241
242#[derive(Debug, Clone, Default)]
244pub struct IngestedObjects {
245 pub object_ids: HashSet<ObjectId>,
247 pub object_counts: ObjectKindCounts,
249 pub pack_bytes: u64,
251}
252
253trait UnpackSink {
255 fn hash_algo(&self) -> HashAlgo;
256
257 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId>;
259
260 fn supports_streaming_inflate(&self) -> bool {
262 false
263 }
264
265 fn write_inflated_from_pack(
267 &self,
268 kind: ObjectKind,
269 size: usize,
270 rd: &mut StreamingPackReader<'_>,
271 dry_run: bool,
272 ) -> Result<ObjectId> {
273 let _ = (kind, size, rd, dry_run);
274 Err(Error::UnsupportedObjectStore {
275 operation: "write_inflated_from_pack",
276 })
277 }
278
279 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>>;
281
282 fn contains(&self, oid: &ObjectId) -> Result<bool>;
284}
285
286impl UnpackSink for Odb {
287 fn hash_algo(&self) -> HashAlgo {
288 Odb::hash_algo(self)
289 }
290
291 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
292 if dry_run {
293 return self.hash(kind, data);
294 }
295 if !self.uses_files_primary() {
296 return Odb::write(self, kind, data);
297 }
298 self.write_local(kind, data)
301 }
302
303 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
304 match self.read(oid) {
305 Ok(obj) => Ok(Some(obj)),
306 Err(Error::ObjectNotFound(_)) => Ok(None),
307 Err(e) => Err(e),
308 }
309 }
310
311 fn contains(&self, oid: &ObjectId) -> Result<bool> {
312 Odb::contains(self, oid)
313 }
314
315 fn supports_streaming_inflate(&self) -> bool {
316 self.uses_files_primary()
317 }
318
319 fn write_inflated_from_pack(
320 &self,
321 kind: ObjectKind,
322 size: usize,
323 rd: &mut StreamingPackReader<'_>,
324 dry_run: bool,
325 ) -> Result<ObjectId> {
326 let size_u64 = u64::try_from(size).map_err(|_| {
327 Error::CorruptObject(format!("object size {size} exceeds address space"))
328 })?;
329 if dry_run {
330 let mut hasher = ObjectPayloadHasher::new(self.hash_algo(), kind, size_u64);
331 rd.decompress_into(size, |chunk| {
332 hasher.update(chunk);
333 Ok(())
334 })?;
335 return hasher.finish();
336 }
337 if !self.uses_files_primary() {
338 let data = rd.decompress(size)?;
339 return self.write_local(kind, &data);
340 }
341 self.write_local_stream_with(kind, size_u64, |chunk_sink| {
342 rd.decompress_into(size, chunk_sink)
343 })
344 }
345}
346
347struct StoreSink<'a> {
349 store: &'a dyn WritableObjectStore,
350 bases: &'a dyn ObjectStore,
351}
352
353impl UnpackSink for StoreSink<'_> {
354 fn hash_algo(&self) -> HashAlgo {
355 self.store.hash_algo()
356 }
357
358 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
359 if dry_run {
360 return crate::hash::hash_object(self.store.hash_algo(), kind, data);
361 }
362 self.store.write(kind, data, WriteOptions::default())
363 }
364
365 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
366 if let Some(obj) = self.store.read(oid)? {
367 return Ok(Some(obj));
368 }
369 self.bases.read(oid)
370 }
371
372 fn contains(&self, oid: &ObjectId) -> Result<bool> {
373 Ok(self.store.contains(oid)? || self.bases.contains(oid)?)
374 }
375}
376
377pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> Result<usize> {
397 unpack_into(reader, odb, opts, None, None, None).map(|unpacked| unpacked.count)
398}
399
400pub(crate) type PromisedReference<'a> = &'a dyn Fn(&ObjectId, &ObjectId) -> Result<bool>;
405
406pub(crate) fn unpack_objects_with_promises(
415 reader: &mut dyn Read,
416 odb: &Odb,
417 opts: &UnpackOptions,
418 promised: PromisedReference<'_>,
419) -> Result<usize> {
420 unpack_into(reader, odb, opts, None, None, Some(promised)).map(|unpacked| unpacked.count)
421}
422
423pub fn ingest_pack_into(
491 reader: &mut dyn Read,
492 store: &dyn WritableObjectStore,
493 bases: &dyn ObjectStore,
494 opts: &UnpackOptions,
495) -> Result<IngestedObjects> {
496 ingest_pack_into_with_progress(reader, store, bases, opts, None, None)
497}
498
499pub fn ingest_pack_into_with_progress(
507 reader: &mut dyn Read,
508 store: &dyn WritableObjectStore,
509 bases: &dyn ObjectStore,
510 opts: &UnpackOptions,
511 progress: Option<&mut dyn ProgressSink>,
512 cancel: Option<&dyn Cancel>,
513) -> Result<IngestedObjects> {
514 unpack_into(
515 reader,
516 &StoreSink { store, bases },
517 opts,
518 progress,
519 cancel,
520 None,
521 )
522 .map(Unpacked::into_ingested)
523}
524
525pub(crate) fn ingest_pack_into_odb(
528 reader: &mut dyn Read,
529 odb: &Odb,
530 opts: &UnpackOptions,
531 progress: Option<&mut dyn ProgressSink>,
532 cancel: Option<&dyn Cancel>,
533) -> Result<IngestedObjects> {
534 unpack_into(reader, odb, opts, progress, cancel, None).map(Unpacked::into_ingested)
535}
536
537struct Unpacked {
539 count: usize,
540 by_oid: HashMap<ObjectId, PackedObjectEntry>,
541 object_counts: ObjectKindCounts,
542 pack_bytes: u64,
543}
544
545impl Unpacked {
546 fn into_ingested(self) -> IngestedObjects {
547 IngestedObjects {
548 object_ids: self.by_oid.into_keys().collect(),
549 object_counts: self.object_counts,
550 pack_bytes: self.pack_bytes,
551 }
552 }
553}
554
555fn poll_unpack_cancel(
556 progress: &mut Option<&mut dyn ProgressSink>,
557 cancel: Option<&dyn Cancel>,
558) -> Result<()> {
559 let Some(cancel) = cancel else {
560 return Ok(());
561 };
562 match progress {
563 Some(p) => poll_cancel(&mut **p, cancel),
564 None => poll_cancel(&mut NullProgress, cancel),
565 }
566}
567
568fn unpack_into<S: UnpackSink + ?Sized>(
569 reader: &mut dyn Read,
570 sink: &S,
571 opts: &UnpackOptions,
572 mut progress: Option<&mut dyn ProgressSink>,
573 cancel: Option<&dyn Cancel>,
574 promised: Option<PromisedReference<'_>>,
575) -> Result<Unpacked> {
576 const MAX_STREAM_BLOB_BYTES: usize = 1024 * 1024;
578
579 let algo = sink.hash_algo();
580 let mut rd = StreamingPackReader::new(reader, opts.max_input_bytes, algo);
581 let mut base_cache = UnpackBaseByteCache::new(unpack_base_cache_byte_limit());
582
583 let sig = rd.read_exact_n(4)?;
585 if sig != b"PACK" {
586 return Err(Error::CorruptObject(
587 "not a pack stream: invalid signature".to_owned(),
588 ));
589 }
590 let version = rd.read_u32_be()?;
591 if version != 2 && version != 3 {
592 return Err(Error::CorruptObject(format!(
593 "unsupported pack version {version}"
594 )));
595 }
596 let nr_objects = rd.read_u32_be()? as usize;
597
598 let mut by_offset: HashMap<usize, PackedObjectEntry> = HashMap::new();
599 let mut by_oid: HashMap<ObjectId, PackedObjectEntry> = HashMap::new();
600
601 let mut pending: Vec<PendingDelta> = Vec::new();
602 let mut count = 0usize;
603 let mut object_counts = ObjectKindCounts::default();
604
605 if let Some(p) = progress.as_mut() {
606 p.start(ProgressPhase::IndexingObjects, Some(nr_objects as u64));
607 }
608 for _ in 0..nr_objects {
609 poll_unpack_cancel(&mut progress, cancel)?;
610 let obj_offset = rd.stream_pos();
611 let (type_code, size) = rd.read_type_size()?;
612
613 match type_code {
614 1..=4 => {
615 let kind = type_code_to_kind(type_code)?;
616 if !opts.dry_run
617 && size > MAX_STREAM_BLOB_BYTES
618 && kind == ObjectKind::Blob
619 && sink.supports_streaming_inflate()
620 {
621 let oid = sink.write_inflated_from_pack(kind, size, &mut rd, false)?;
622 let entry = PackedObjectEntry::Stored { kind, oid };
623 by_offset.insert(obj_offset, entry.clone());
624 by_oid.insert(oid, entry);
625 } else {
626 let data = rd.decompress(size)?;
627 let oid = sink.write(kind, &data, opts.dry_run)?;
628 record_resolved_object(
629 &mut by_offset,
630 &mut by_oid,
631 obj_offset,
632 oid,
633 kind,
634 data,
635 opts,
636 MAX_STREAM_BLOB_BYTES,
637 &mut base_cache,
638 )?;
639 }
640 object_counts.add(kind);
641 count += 1;
642 }
643 6 => {
644 let neg = rd.read_ofs_neg_offset()?;
645 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
646 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
647 })?;
648 let delta_data = rd.decompress(size)?;
649 let delta = PendingDelta {
650 offset: obj_offset,
651 base_oid: None,
652 base_offset: Some(base_offset),
653 delta_data,
654 };
655 test_note_pending_delta_bytes(&pending, base_cache.bytes_used());
656 if let Some((oid, kind, data)) = resolve_pending_delta(
657 &delta,
658 &by_offset,
659 &by_oid,
660 sink,
661 opts,
662 MAX_STREAM_BLOB_BYTES,
663 &mut base_cache,
664 )? {
665 record_resolved_object(
666 &mut by_offset,
667 &mut by_oid,
668 delta.offset,
669 oid,
670 kind,
671 data,
672 opts,
673 MAX_STREAM_BLOB_BYTES,
674 &mut base_cache,
675 )?;
676 object_counts.add(kind);
677 count += 1;
678 } else {
679 pending.push(delta);
680 }
681 }
682 7 => {
683 let base_bytes = rd.read_exact_n(algo.len())?;
684 let base_oid = ObjectId::from_bytes(&base_bytes)?;
685 let delta_data = rd.decompress(size)?;
686 let delta = PendingDelta {
687 offset: obj_offset,
688 base_oid: Some(base_oid),
689 base_offset: None,
690 delta_data,
691 };
692 test_note_pending_delta_bytes(&pending, base_cache.bytes_used());
693 if let Some((oid, kind, data)) = resolve_pending_delta(
694 &delta,
695 &by_offset,
696 &by_oid,
697 sink,
698 opts,
699 MAX_STREAM_BLOB_BYTES,
700 &mut base_cache,
701 )? {
702 record_resolved_object(
703 &mut by_offset,
704 &mut by_oid,
705 delta.offset,
706 oid,
707 kind,
708 data,
709 opts,
710 MAX_STREAM_BLOB_BYTES,
711 &mut base_cache,
712 )?;
713 object_counts.add(kind);
714 count += 1;
715 } else {
716 pending.push(delta);
717 }
718 }
719 other => {
720 return Err(Error::CorruptObject(format!(
721 "unknown packed-object type {other}"
722 )))
723 }
724 }
725 if let Some(p) = progress.as_mut() {
726 p.inc(1);
727 }
728 }
729 if let Some(p) = progress.as_mut() {
730 p.finish();
731 }
732
733 let digest = rd.finalize_hasher();
734 let trailing = rd.read_trailer()?;
735 if digest != trailing {
736 return Err(Error::CorruptObject(
737 "pack trailing checksum mismatch".to_owned(),
738 ));
739 }
740 let pack_bytes = rd.stream_pos() as u64;
741
742 let mut remaining = pending;
743 if let (Some(p), false) = (progress.as_mut(), remaining.is_empty()) {
744 p.start(ProgressPhase::ResolvingDeltas, Some(remaining.len() as u64));
745 }
746 let resolving = !remaining.is_empty();
747 loop {
748 if remaining.is_empty() {
749 break;
750 }
751 poll_unpack_cancel(&mut progress, cancel)?;
752 test_note_pending_delta_bytes(&remaining, base_cache.bytes_used());
753 let before = remaining.len();
754 let mut still_pending: Vec<PendingDelta> = Vec::new();
755
756 for delta in remaining {
757 if let Some((oid, kind, data)) = resolve_pending_delta(
758 &delta,
759 &by_offset,
760 &by_oid,
761 sink,
762 opts,
763 MAX_STREAM_BLOB_BYTES,
764 &mut base_cache,
765 )? {
766 record_resolved_object(
767 &mut by_offset,
768 &mut by_oid,
769 delta.offset,
770 oid,
771 kind,
772 data,
773 opts,
774 MAX_STREAM_BLOB_BYTES,
775 &mut base_cache,
776 )?;
777 object_counts.add(kind);
778 count += 1;
779 if let Some(p) = progress.as_mut() {
780 p.inc(1);
781 }
782 } else {
783 still_pending.push(delta);
784 }
785 }
786
787 remaining = still_pending;
788 if remaining.len() == before {
789 return Err(Error::UnresolvedDeltas {
790 count: u64::try_from(remaining.len()).unwrap_or(u64::MAX),
791 });
792 }
793 }
794 if let (Some(p), true) = (progress.as_mut(), resolving) {
795 p.finish();
796 }
797
798 if opts.strict {
799 verify_packed_dot_special_entries(&by_oid, sink)?;
800 strict_verify_packed_references_map(
801 sink,
802 &by_oid,
803 &opts.allowed_missing,
804 opts.allow_promisor_missing_references,
805 &opts.shallow_boundaries,
806 promised,
807 )?;
808 }
809
810 Ok(Unpacked {
811 count,
812 by_oid,
813 object_counts,
814 pack_bytes,
815 })
816}
817
818#[derive(Debug, Clone)]
820enum PackedObjectEntry {
821 DryRun { kind: ObjectKind, data: Vec<u8> },
822 Stored { kind: ObjectKind, oid: ObjectId },
823}
824
825fn packed_entry_kind(entry: &PackedObjectEntry) -> ObjectKind {
826 match entry {
827 PackedObjectEntry::DryRun { kind, .. } | PackedObjectEntry::Stored { kind, .. } => *kind,
828 }
829}
830
831#[allow(clippy::too_many_arguments)]
832fn record_resolved_object(
833 by_offset: &mut HashMap<usize, PackedObjectEntry>,
834 by_oid: &mut HashMap<ObjectId, PackedObjectEntry>,
835 pack_offset: usize,
836 oid: ObjectId,
837 kind: ObjectKind,
838 data: Vec<u8>,
839 opts: &UnpackOptions,
840 max_stream_blob: usize,
841 cache: &mut UnpackBaseByteCache,
842) -> Result<()> {
843 let entry =
844 packed_entry_after_write(kind, data, oid, opts, max_stream_blob, pack_offset, cache);
845 by_offset.insert(pack_offset, entry.clone());
846 by_oid.insert(oid, entry);
847 Ok(())
848}
849
850fn resolve_pending_delta<S: UnpackSink + ?Sized>(
851 delta: &PendingDelta,
852 by_offset: &HashMap<usize, PackedObjectEntry>,
853 by_oid: &HashMap<ObjectId, PackedObjectEntry>,
854 sink: &S,
855 opts: &UnpackOptions,
856 max_stream_blob: usize,
857 cache: &mut UnpackBaseByteCache,
858) -> Result<Option<(ObjectId, ObjectKind, Vec<u8>)>> {
859 let Some((base_kind, base_data)) =
860 delta_base_bytes(delta, by_offset, by_oid, sink, opts, cache)?
861 else {
862 return Ok(None);
863 };
864 let result = apply_delta(base_data.as_ref(), &delta.delta_data)?;
865 let oid = sink.write(base_kind, &result, opts.dry_run)?;
866 if !opts.dry_run && (base_kind != ObjectKind::Blob || result.len() <= max_stream_blob) {
867 cache.put(delta.offset, base_kind, result.clone());
868 }
869 Ok(Some((oid, base_kind, result)))
870}
871
872fn delta_base_bytes<'a, S: UnpackSink + ?Sized>(
873 delta: &PendingDelta,
874 by_offset: &'a HashMap<usize, PackedObjectEntry>,
875 by_oid: &'a HashMap<ObjectId, PackedObjectEntry>,
876 sink: &S,
877 opts: &UnpackOptions,
878 cache: &mut UnpackBaseByteCache,
879) -> Result<Option<(ObjectKind, Cow<'a, [u8]>)>> {
880 if let Some(base_off) = delta.base_offset {
881 if let Some((kind, data)) = cache.get(base_off) {
882 return Ok(Some((kind, Cow::Owned(data))));
883 }
884 let Some(entry) = by_offset.get(&base_off) else {
885 return Ok(None);
886 };
887 return entry_object_bytes(entry, sink, Some(base_off), cache).map(Some);
888 }
889 if let Some(ref base_id) = delta.base_oid {
890 if let Some(entry) = by_oid.get(base_id) {
891 return entry_object_bytes(entry, sink, None, cache).map(Some);
892 }
893 if !opts.dry_run {
894 if let Some(obj) = sink.read_stored(base_id)? {
895 return Ok(Some((obj.kind, Cow::Owned(obj.data))));
896 }
897 }
898 }
899 Ok(None)
900}
901
902fn verify_packed_dot_special_entries<S: UnpackSink + ?Sized>(
903 by_oid: &HashMap<ObjectId, PackedObjectEntry>,
904 sink: &S,
905) -> Result<()> {
906 let mut tracker = DotFsckTracker::default();
907 let mut keys: Vec<ObjectId> = by_oid.keys().copied().collect();
908 keys.sort();
909 for oid in keys {
910 let entry = &by_oid[&oid];
911 if matches!(
912 entry,
913 PackedObjectEntry::Stored {
914 kind: ObjectKind::Blob,
915 ..
916 } | PackedObjectEntry::DryRun {
917 kind: ObjectKind::Blob,
918 ..
919 }
920 ) {
921 continue;
922 }
923 let (kind, data) = entry_payload_for_strict(entry, sink, &oid)?;
924 if kind == ObjectKind::Tree {
925 for di in tracker.on_tree(&oid, &data)? {
926 if di.is_error_severity() {
927 return Err(Error::CorruptObject(di.format_line()));
928 }
929 }
930 }
931 for di in tracker.on_object(&oid, kind, &data) {
932 if di.is_error_severity() {
933 return Err(Error::CorruptObject(di.format_line()));
934 }
935 }
936 }
937 let pending_gm: Vec<ObjectId> = tracker
938 .gitmodules_found
939 .difference(&tracker.gitmodules_done)
940 .copied()
941 .collect();
942 let pending_ga: Vec<ObjectId> = tracker
943 .gitattributes_found
944 .difference(&tracker.gitattributes_done)
945 .copied()
946 .collect();
947 for oid in pending_gm {
948 tracker.gitmodules_done.insert(oid);
949 let Some(entry) = by_oid.get(&oid) else {
950 continue;
951 };
952 let (kind, data) = dot_special_blob_bytes(entry, sink, &oid)?;
953 for di in gitmodules::fsck_dot_special_object(
954 &oid,
955 kind,
956 &data,
957 &tracker.gitmodules_found,
958 &tracker.gitattributes_found,
959 ) {
960 if di.is_error_severity() {
961 return Err(Error::CorruptObject(di.format_line()));
962 }
963 }
964 }
965 for oid in pending_ga {
966 if tracker.gitattributes_done.contains(&oid) {
967 continue;
968 }
969 tracker.gitattributes_done.insert(oid);
970 let Some(entry) = by_oid.get(&oid) else {
971 continue;
972 };
973 let (kind, data) = dot_special_blob_bytes(entry, sink, &oid)?;
974 for di in gitmodules::fsck_dot_special_object(
975 &oid,
976 kind,
977 &data,
978 &tracker.gitmodules_found,
979 &tracker.gitattributes_found,
980 ) {
981 if di.is_error_severity() {
982 return Err(Error::CorruptObject(di.format_line()));
983 }
984 }
985 }
986 Ok(())
987}
988
989fn dot_special_blob_bytes<S: UnpackSink + ?Sized>(
990 entry: &PackedObjectEntry,
991 sink: &S,
992 id: &ObjectId,
993) -> Result<(ObjectKind, Vec<u8>)> {
994 entry_payload_for_strict(entry, sink, id)
995}
996
997fn entry_payload_for_strict<S: UnpackSink + ?Sized>(
998 entry: &PackedObjectEntry,
999 sink: &S,
1000 id: &ObjectId,
1001) -> Result<(ObjectKind, Vec<u8>)> {
1002 match entry {
1003 PackedObjectEntry::DryRun { kind, data } => Ok((*kind, data.clone())),
1004 PackedObjectEntry::Stored { kind, oid } => {
1005 debug_assert_eq!(oid, id);
1006 let Some(obj) = sink.read_stored(id)? else {
1007 return Err(Error::CorruptObject(format!("missing object {id}")));
1008 };
1009 Ok((*kind, obj.data))
1010 }
1011 }
1012}
1013
1014fn packed_entry_after_write(
1015 kind: ObjectKind,
1016 data: Vec<u8>,
1017 oid: ObjectId,
1018 opts: &UnpackOptions,
1019 max_stream_blob: usize,
1020 _pack_offset: usize,
1021 _cache: &mut UnpackBaseByteCache,
1022) -> PackedObjectEntry {
1023 let _ = max_stream_blob;
1024 if opts.dry_run {
1025 return PackedObjectEntry::DryRun { kind, data };
1026 }
1027 PackedObjectEntry::Stored { kind, oid }
1028}
1029
1030fn entry_object_bytes<'a, S: UnpackSink + ?Sized>(
1031 entry: &'a PackedObjectEntry,
1032 sink: &S,
1033 pack_offset: Option<usize>,
1034 cache: &mut UnpackBaseByteCache,
1035) -> Result<(ObjectKind, Cow<'a, [u8]>)> {
1036 match entry {
1037 PackedObjectEntry::DryRun { kind, data } => Ok((*kind, Cow::Borrowed(data.as_slice()))),
1038 PackedObjectEntry::Stored { kind, oid } => {
1039 if let Some(off) = pack_offset {
1040 if let Some((k, data)) = cache.get(off) {
1041 return Ok((k, Cow::Owned(data)));
1042 }
1043 }
1044 let obj = sink
1045 .read_stored(oid)?
1046 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))?;
1047 if let Some(off) = pack_offset {
1048 cache.put(off, *kind, obj.data.clone());
1049 }
1050 Ok((*kind, Cow::Owned(obj.data)))
1051 }
1052 }
1053}
1054
1055fn strict_verify_packed_references_map<S: UnpackSink + ?Sized>(
1056 sink: &S,
1057 pack: &HashMap<ObjectId, PackedObjectEntry>,
1058 allowed_missing: &HashSet<ObjectId>,
1059 allow_promisor_missing_references: bool,
1060 shallow_boundaries: &HashSet<ObjectId>,
1061 promised: Option<PromisedReference<'_>>,
1062) -> Result<()> {
1063 let resolves = |target: &ObjectId, referrer: &ObjectId| -> Result<bool> {
1064 if strict_ref_resolves_map(
1065 target,
1066 pack,
1067 sink,
1068 allowed_missing,
1069 allow_promisor_missing_references,
1070 )? {
1071 return Ok(true);
1072 }
1073 promised.map_or(Ok(false), |promised| promised(target, referrer))
1074 };
1075 for (oid, entry) in pack {
1076 let kind = packed_entry_kind(entry);
1077 if kind == ObjectKind::Blob {
1078 continue;
1079 }
1080 let (_, data) = entry_payload_for_strict(entry, sink, oid)?;
1081 match kind {
1082 ObjectKind::Tree => {
1083 for e in parse_tree(&data)? {
1084 if e.mode == MODE_GITLINK {
1089 continue;
1090 }
1091 if !resolves(&e.oid, oid)? {
1092 return Err(Error::CorruptObject(format!(
1093 "strict: missing object {} referenced by tree",
1094 e.oid.to_hex()
1095 )));
1096 }
1097 }
1098 }
1099 ObjectKind::Commit => {
1100 let c = parse_commit(&data)?;
1101 if !resolves(&c.tree, oid)? {
1102 return Err(Error::CorruptObject(format!(
1103 "strict: missing tree {} referenced by commit",
1104 c.tree.to_hex()
1105 )));
1106 }
1107 if shallow_boundaries.contains(oid) {
1111 continue;
1112 }
1113 for p in &c.parents {
1114 if !resolves(p, oid)? {
1115 return Err(Error::CorruptObject(format!(
1116 "strict: missing parent {} referenced by commit",
1117 p.to_hex()
1118 )));
1119 }
1120 }
1121 }
1122 ObjectKind::Tag => {
1123 let t = parse_tag(&data)?;
1124 if !resolves(&t.object, oid)? {
1125 return Err(Error::CorruptObject(format!(
1126 "strict: missing object {} referenced by tag",
1127 t.object.to_hex()
1128 )));
1129 }
1130 }
1131 ObjectKind::Blob => {}
1132 }
1133 }
1134 Ok(())
1135}
1136
1137fn strict_ref_resolves_map<S: UnpackSink + ?Sized>(
1138 oid: &ObjectId,
1139 pack: &HashMap<ObjectId, PackedObjectEntry>,
1140 sink: &S,
1141 allowed_missing: &HashSet<ObjectId>,
1142 allow_promisor_missing_references: bool,
1143) -> Result<bool> {
1144 if pack.contains_key(oid) || allowed_missing.contains(oid) || allow_promisor_missing_references
1145 {
1146 return Ok(true);
1147 }
1148 sink.contains(oid)
1149}
1150
1151fn strict_ref_resolves(
1152 oid: &ObjectId,
1153 pack: &std::collections::HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1154 odb: Option<&Odb>,
1155) -> Result<bool> {
1156 if pack.contains_key(oid) {
1157 return Ok(true);
1158 }
1159 odb.map_or(Ok(false), |o| o.contains(oid))
1160}
1161
1162pub fn strict_verify_packed_references(
1173 odb: Option<&Odb>,
1174 pack: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1175) -> Result<()> {
1176 for (kind, data) in pack.values() {
1177 match kind {
1178 ObjectKind::Tree => {
1179 for e in parse_tree(data)? {
1180 if e.mode == MODE_GITLINK {
1185 continue;
1186 }
1187 if !strict_ref_resolves(&e.oid, pack, odb)? {
1188 return Err(Error::CorruptObject(format!(
1189 "strict: missing object {} referenced by tree",
1190 e.oid.to_hex()
1191 )));
1192 }
1193 }
1194 }
1195 ObjectKind::Commit => {
1196 let c = parse_commit(data)?;
1197 if !strict_ref_resolves(&c.tree, pack, odb)? {
1198 return Err(Error::CorruptObject(format!(
1199 "strict: missing tree {} referenced by commit",
1200 c.tree.to_hex()
1201 )));
1202 }
1203 for p in &c.parents {
1204 if !strict_ref_resolves(p, pack, odb)? {
1205 return Err(Error::CorruptObject(format!(
1206 "strict: missing parent {} referenced by commit",
1207 p.to_hex()
1208 )));
1209 }
1210 }
1211 }
1212 ObjectKind::Tag => {
1213 let t = parse_tag(data)?;
1214 if !strict_ref_resolves(&t.object, pack, odb)? {
1215 return Err(Error::CorruptObject(format!(
1216 "strict: missing object {} referenced by tag",
1217 t.object.to_hex()
1218 )));
1219 }
1220 }
1221 ObjectKind::Blob => {}
1222 }
1223 }
1224 Ok(())
1225}
1226
1227pub fn pack_is_thin(data: &[u8], algo: HashAlgo) -> bool {
1234 pack_is_thin_inner(data, algo).unwrap_or(false)
1235}
1236
1237fn pack_is_thin_inner(data: &[u8], algo: HashAlgo) -> Result<bool> {
1238 use crate::pack_zlib::skip_zlib_at;
1239
1240 let mut rd = PackReader::new(data);
1241 if rd.read_exact(4)? != b"PACK" {
1242 return Ok(false);
1243 }
1244 let _version = rd.read_u32_be()?;
1245 let nr_objects = rd.read_u32_be()? as usize;
1246
1247 let mut ref_delta_bases: Vec<ObjectId> = Vec::new();
1248 let mut saw_ref_delta = false;
1249
1250 for _ in 0..nr_objects {
1251 let obj_offset = rd.pos;
1252 let (type_code, size) = rd.read_type_size()?;
1253 match type_code {
1254 1..=4 => {
1255 let zlib_offset = rd.pos;
1256 let consumed = skip_zlib_at(data, zlib_offset, size)?;
1257 rd.pos = zlib_offset + consumed;
1258 let _ = obj_offset;
1259 }
1260 6 => {
1261 let neg = rd.read_ofs_neg_offset()?;
1262 let _base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
1263 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
1264 })?;
1265 let zlib_offset = rd.pos;
1266 let consumed = skip_zlib_at(data, zlib_offset, size)?;
1267 rd.pos = zlib_offset + consumed;
1268 }
1269 7 => {
1270 saw_ref_delta = true;
1271 let base_bytes = rd.read_exact(algo.len())?;
1272 let base_oid = ObjectId::from_bytes(base_bytes)?;
1273 ref_delta_bases.push(base_oid);
1274 let zlib_offset = rd.pos;
1275 let consumed = skip_zlib_at(data, zlib_offset, size)?;
1276 rd.pos = zlib_offset + consumed;
1277 }
1278 _ => return Ok(false),
1279 }
1280 }
1281
1282 if !saw_ref_delta {
1283 return Ok(false);
1284 }
1285
1286 pack_is_thin_with_ofs_deltas(data, algo, ref_delta_bases)
1287}
1288
1289fn pack_is_thin_with_ofs_deltas(
1291 data: &[u8],
1292 algo: HashAlgo,
1293 ref_delta_bases: Vec<ObjectId>,
1294) -> Result<bool> {
1295 let tmp = tempfile::tempdir().map_err(Error::Io)?;
1296 let git_dir = tmp.path().join(".git");
1297 std::fs::create_dir_all(git_dir.join("objects")).map_err(Error::Io)?;
1298 if algo == HashAlgo::Sha256 {
1299 std::fs::write(
1300 git_dir.join("config"),
1301 "[extensions]\n\tobjectformat = sha256\n",
1302 )
1303 .map_err(Error::Io)?;
1304 }
1305 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
1306 match crate::pack_index_build::build_pack_index_records(
1307 data,
1308 &odb,
1309 crate::hash::Parallelism::resolve(Some(1)),
1310 false,
1311 ) {
1312 Ok(records) => {
1313 let in_pack: HashSet<ObjectId> = records.into_iter().map(|r| r.oid).collect();
1314 Ok(ref_delta_bases.iter().any(|b| !in_pack.contains(b)))
1315 }
1316 Err(Error::UnresolvedDeltas { .. }) => Ok(true),
1317 Err(e) => Err(e),
1318 }
1319}
1320
1321pub fn pack_bytes_to_object_map(data: &[u8], odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
1333 build_pack_object_map(PackReader::new(data), odb)
1334}
1335
1336#[derive(Debug, Clone)]
1342pub struct PackIndexRecord {
1343 pub oid: ObjectId,
1345 pub offset: u64,
1347 pub crc32: u32,
1349 pub kind: ObjectKind,
1351}
1352
1353pub fn pack_index_records_from_bytes(data: &[u8], odb: &Odb) -> Result<Vec<PackIndexRecord>> {
1363 pack_index_records_with_threads(
1364 data,
1365 odb,
1366 crate::config::ConfigSet::pack_index_parallelism_for_git_dir(
1367 &crate::environment::Environment::empty(),
1368 odb.config_git_dir(),
1369 ),
1370 )
1371}
1372
1373pub fn pack_index_records_with_threads(
1375 data: &[u8],
1376 odb: &Odb,
1377 parallelism: crate::hash::Parallelism,
1378) -> Result<Vec<PackIndexRecord>> {
1379 crate::pack_index_build::build_pack_index_records(data, odb, parallelism, true)
1380}
1381
1382pub fn pack_index_records_with_threads_with_progress(
1385 data: &[u8],
1386 odb: &Odb,
1387 parallelism: crate::hash::Parallelism,
1388 progress: Option<&mut dyn crate::progress::ProgressSink>,
1389) -> Result<Vec<PackIndexRecord>> {
1390 crate::pack_index_build::build_pack_index_records_with_progress(
1391 data,
1392 odb,
1393 parallelism,
1394 true,
1395 progress,
1396 None,
1397 )
1398}
1399
1400fn build_pack_object_map(mut rd: PackReader<'_>, odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
1401 let algo = odb.hash_algo();
1402 let sig = rd.read_exact(4)?;
1403 if sig != b"PACK" {
1404 return Err(Error::CorruptObject(
1405 "not a pack stream: invalid signature".to_owned(),
1406 ));
1407 }
1408 let version = rd.read_u32_be()?;
1409 if version != 2 && version != 3 {
1410 return Err(Error::CorruptObject(format!(
1411 "unsupported pack version {version}"
1412 )));
1413 }
1414 let nr_objects = rd.read_u32_be()? as usize;
1415
1416 let mut by_offset: HashMap<usize, (ObjectKind, Vec<u8>)> = HashMap::new();
1417 let mut by_oid: HashMap<ObjectId, (ObjectKind, Vec<u8>)> = HashMap::new();
1418 let mut pending: Vec<PendingDelta> = Vec::new();
1419
1420 fn base_from_pack_or_odb(
1421 by_oid: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1422 odb: &Odb,
1423 id: &ObjectId,
1424 ) -> Option<(ObjectKind, Vec<u8>)> {
1425 if let Some(e) = by_oid.get(id) {
1426 return Some(e.clone());
1427 }
1428 odb.read(id).ok().map(|o| (o.kind, o.data))
1429 }
1430
1431 for _ in 0..nr_objects {
1432 let obj_offset = rd.pos;
1433 let (type_code, size) = rd.read_type_size()?;
1434
1435 match type_code {
1436 1..=4 => {
1437 let kind = type_code_to_kind(type_code)?;
1438 let data = rd.decompress(size)?;
1439 let oid = odb.hash(kind, &data)?;
1440 by_offset.insert(obj_offset, (kind, data.clone()));
1441 by_oid.insert(oid, (kind, data));
1442 }
1443 6 => {
1444 let neg = rd.read_ofs_neg_offset()?;
1445 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
1446 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
1447 })?;
1448 let delta_data = rd.decompress(size)?;
1449 pending.push(PendingDelta {
1450 offset: obj_offset,
1451 base_oid: None,
1452 base_offset: Some(base_offset),
1453 delta_data,
1454 });
1455 }
1456 7 => {
1457 let base_bytes = rd.read_exact(algo.len())?;
1458 let base_oid = ObjectId::from_bytes(base_bytes)?;
1459 let delta_data = rd.decompress(size)?;
1460 pending.push(PendingDelta {
1461 offset: obj_offset,
1462 base_oid: Some(base_oid),
1463 base_offset: None,
1464 delta_data,
1465 });
1466 }
1467 other => {
1468 return Err(Error::CorruptObject(format!(
1469 "unknown packed-object type {other}"
1470 )))
1471 }
1472 }
1473 }
1474
1475 let consumed = rd.pos;
1476 {
1477 let mut hasher = ChecksumHasher::new(algo);
1478 hasher.update(&rd.data[..consumed]);
1479 let digest = hasher.finalize();
1480 let trailing = rd.read_exact(algo.len())?;
1481 if digest.as_slice() != trailing {
1482 return Err(Error::CorruptObject(
1483 "pack trailing checksum mismatch".to_owned(),
1484 ));
1485 }
1486 }
1487
1488 let mut remaining = pending;
1489 loop {
1490 if remaining.is_empty() {
1491 break;
1492 }
1493 let before = remaining.len();
1494 let mut still_pending: Vec<PendingDelta> = Vec::new();
1495
1496 for delta in remaining {
1497 let base = if let Some(base_off) = delta.base_offset {
1498 by_offset.get(&base_off).cloned()
1499 } else if let Some(ref base_id) = delta.base_oid {
1500 base_from_pack_or_odb(&by_oid, odb, base_id)
1501 } else {
1502 None
1503 };
1504
1505 if let Some((base_kind, base_data)) = base {
1506 let result = apply_delta(&base_data, &delta.delta_data)?;
1507 let oid = odb.hash(base_kind, &result)?;
1508 by_offset.insert(delta.offset, (base_kind, result.clone()));
1509 by_oid.insert(oid, (base_kind, result));
1510 } else {
1511 still_pending.push(delta);
1512 }
1513 }
1514
1515 remaining = still_pending;
1516 if remaining.len() == before {
1517 return Err(Error::UnresolvedDeltas {
1518 count: u64::try_from(remaining.len()).unwrap_or(u64::MAX),
1519 });
1520 }
1521 }
1522
1523 Ok(by_oid
1524 .into_iter()
1525 .map(|(oid, (kind, data))| (oid, Object::new(kind, data)))
1526 .collect())
1527}
1528
1529pub(crate) fn type_code_to_kind(code: u8) -> Result<ObjectKind> {
1531 match code {
1532 1 => Ok(ObjectKind::Commit),
1533 2 => Ok(ObjectKind::Tree),
1534 3 => Ok(ObjectKind::Blob),
1535 4 => Ok(ObjectKind::Tag),
1536 _ => Err(Error::CorruptObject(format!(
1537 "type code {code} is not a regular object type"
1538 ))),
1539 }
1540}
1541
1542pub(crate) struct PackReader<'a> {
1544 data: &'a [u8],
1545 pub(crate) pos: usize,
1546}
1547
1548impl<'a> PackReader<'a> {
1549 pub(crate) fn new(data: &'a [u8]) -> Self {
1550 Self { data, pos: 0 }
1551 }
1552
1553 pub(crate) fn read_exact(&mut self, n: usize) -> Result<&[u8]> {
1556 if self.pos + n > self.data.len() {
1557 return Err(Error::CorruptObject(format!(
1558 "pack stream truncated: need {n} bytes at offset {}",
1559 self.pos
1560 )));
1561 }
1562 let slice = &self.data[self.pos..self.pos + n];
1563 self.pos += n;
1564 Ok(slice)
1565 }
1566
1567 pub(crate) fn read_byte(&mut self) -> Result<u8> {
1569 if self.pos >= self.data.len() {
1570 return Err(Error::CorruptObject(
1571 "unexpected end of pack stream".to_owned(),
1572 ));
1573 }
1574 let b = self.data[self.pos];
1575 self.pos += 1;
1576 Ok(b)
1577 }
1578
1579 pub(crate) fn read_u32_be(&mut self) -> Result<u32> {
1581 let bytes = self.read_exact(4)?;
1582 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
1583 Error::CorruptObject("u32 read failed".to_owned())
1584 })?))
1585 }
1586
1587 pub(crate) fn read_type_size(&mut self) -> Result<(u8, usize)> {
1592 let c = self.read_byte()?;
1593 let type_code = (c >> 4) & 0x7;
1594 let mut size = (c & 0x0f) as usize;
1595 let mut shift = 4u32;
1596 let mut cur = c;
1597 while cur & 0x80 != 0 {
1598 cur = self.read_byte()?;
1599 size |= ((cur & 0x7f) as usize) << shift;
1600 shift += 7;
1601 }
1602 Ok((type_code, size))
1603 }
1604
1605 pub(crate) fn read_ofs_neg_offset(&mut self) -> Result<usize> {
1610 let mut c = self.read_byte()?;
1611 let mut value = (c & 0x7f) as usize;
1612 while c & 0x80 != 0 {
1613 c = self.read_byte()?;
1614 value = (value + 1) << 7 | (c & 0x7f) as usize;
1615 }
1616 Ok(value)
1617 }
1618
1619 pub(crate) fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
1624 let (out, consumed) = decompress_zlib_at(self.data, self.pos, expected_size)?;
1625 self.pos += consumed;
1626 Ok(out)
1627 }
1628}
1629
1630pub(crate) fn decompress_zlib_at_into(
1632 pack: &[u8],
1633 offset: usize,
1634 expected_size: usize,
1635 out: &mut Vec<u8>,
1636) -> Result<usize> {
1637 out.clear();
1638 out.reserve(expected_size);
1639 let slice = pack.get(offset..).ok_or_else(|| {
1640 Error::CorruptObject(format!(
1641 "pack stream truncated: need zlib at offset {offset}"
1642 ))
1643 })?;
1644 let mut decoder = ZlibDecoder::new(slice);
1645 decoder
1646 .read_to_end(out)
1647 .map_err(|e| Error::Zlib(e.to_string()))?;
1648 if out.len() != expected_size {
1649 return Err(Error::CorruptObject(format!(
1650 "decompressed {} bytes but expected {}",
1651 out.len(),
1652 expected_size
1653 )));
1654 }
1655 Ok(decoder.total_in() as usize)
1656}
1657
1658pub(crate) fn decompress_zlib_at(
1660 pack: &[u8],
1661 offset: usize,
1662 expected_size: usize,
1663) -> Result<(Vec<u8>, usize)> {
1664 let mut out = Vec::new();
1665 let consumed = decompress_zlib_at_into(pack, offset, expected_size, &mut out)?;
1666 Ok((out, consumed))
1667}
1668
1669fn io_to_corrupt_eof(e: io::Error, stream_pos: usize, context: &str) -> Error {
1670 if e.kind() == io::ErrorKind::UnexpectedEof {
1671 Error::CorruptObject(format!(
1672 "pack stream truncated ({context}) at offset {stream_pos}"
1673 ))
1674 } else {
1675 Error::Io(e)
1676 }
1677}
1678
1679struct StreamingPackReader<'a> {
1685 inner: &'a mut dyn Read,
1686 pack_hasher: ChecksumHasher,
1687 stream_pos: usize,
1688 max_input_bytes: Option<u64>,
1689 pending: Vec<u8>,
1692}
1693
1694impl<'a> StreamingPackReader<'a> {
1695 fn new(inner: &'a mut dyn Read, max_input_bytes: Option<u64>, algo: HashAlgo) -> Self {
1696 Self {
1697 inner,
1698 pack_hasher: ChecksumHasher::new(algo),
1699 stream_pos: 0,
1700 max_input_bytes,
1701 pending: Vec::new(),
1702 }
1703 }
1704
1705 fn stream_pos(&self) -> usize {
1706 self.stream_pos
1707 }
1708
1709 fn enforce_max_input(&self) -> Result<()> {
1710 if let Some(limit) = self.max_input_bytes {
1711 let pos = u64::try_from(self.stream_pos)
1712 .map_err(|_| Error::CorruptObject("pack stream position overflow".to_owned()))?;
1713 if pos > limit {
1714 return Err(Error::CorruptObject(
1715 "pack exceeds maximum allowed size".to_owned(),
1716 ));
1717 }
1718 }
1719 Ok(())
1720 }
1721
1722 fn read_from_source(&mut self, buf: &mut [u8]) -> Result<usize> {
1724 let n = if !self.pending.is_empty() {
1725 let take = buf.len().min(self.pending.len());
1726 buf[..take].copy_from_slice(&self.pending[..take]);
1727 self.pending.drain(..take);
1728 take
1729 } else {
1730 self.inner.read(buf).map_err(Error::Io)?
1731 };
1732 if n > 0 {
1733 self.pack_hasher.update(&buf[..n]);
1734 self.stream_pos += n;
1735 self.enforce_max_input()?;
1736 }
1737 Ok(n)
1738 }
1739
1740 fn read_byte(&mut self) -> Result<u8> {
1741 let mut b = [0u8; 1];
1742 let n = self.read_from_source(&mut b)?;
1743 if n == 0 {
1744 return Err(Error::CorruptObject(format!(
1745 "pack stream truncated (read byte) at offset {}",
1746 self.stream_pos
1747 )));
1748 }
1749 Ok(b[0])
1750 }
1751
1752 fn read_exact_n(&mut self, n: usize) -> Result<Vec<u8>> {
1753 let mut v = vec![0u8; n];
1754 let mut got = 0usize;
1755 while got < n {
1756 let m = self.read_from_source(&mut v[got..n])?;
1757 if m == 0 {
1758 return Err(Error::CorruptObject(format!(
1759 "pack stream truncated (read exact) at offset {}",
1760 self.stream_pos
1761 )));
1762 }
1763 got += m;
1764 }
1765 Ok(v)
1766 }
1767
1768 fn read_u32_be(&mut self) -> Result<u32> {
1769 let mut b = [0u8; 4];
1770 let mut got = 0usize;
1771 while got < 4 {
1772 let m = self.read_from_source(&mut b[got..4])?;
1773 if m == 0 {
1774 return Err(Error::CorruptObject(format!(
1775 "pack stream truncated (read u32) at offset {}",
1776 self.stream_pos
1777 )));
1778 }
1779 got += m;
1780 }
1781 Ok(u32::from_be_bytes(b))
1782 }
1783
1784 fn read_type_size(&mut self) -> Result<(u8, usize)> {
1785 let c = self.read_byte()?;
1786 let type_code = (c >> 4) & 0x7;
1787 let mut size = (c & 0x0f) as usize;
1788 let mut shift = 4u32;
1789 let mut cur = c;
1790 while cur & 0x80 != 0 {
1791 cur = self.read_byte()?;
1792 size |= ((cur & 0x7f) as usize) << shift;
1793 shift += 7;
1794 }
1795 Ok((type_code, size))
1796 }
1797
1798 fn read_ofs_neg_offset(&mut self) -> Result<usize> {
1799 let mut c = self.read_byte()?;
1800 let mut value = (c & 0x7f) as usize;
1801 while c & 0x80 != 0 {
1802 c = self.read_byte()?;
1803 value = (value + 1) << 7 | (c & 0x7f) as usize;
1804 }
1805 Ok(value)
1806 }
1807
1808 pub(crate) fn decompress_into(
1819 &mut self,
1820 expected_size: usize,
1821 mut chunk_sink: impl FnMut(&[u8]) -> Result<()>,
1822 ) -> Result<()> {
1823 if expected_size == 0 {
1824 const CHUNK: usize = 64 * 1024;
1825 let mut scratch = [0u8; CHUNK];
1826 loop {
1827 let mut cursor = std::io::Cursor::new(self.pending.as_slice());
1828 let mut z = ZlibDecoder::new(&mut cursor);
1829 let mut sink = [0u8; 1];
1830 match z.read(&mut sink) {
1831 Ok(0) => {
1832 let consumed = z.total_in() as usize;
1833 if consumed > self.pending.len() {
1834 return Err(Error::CorruptObject(
1835 "zlib total_in exceeds pending buffer".to_owned(),
1836 ));
1837 }
1838 if consumed == 0 {
1839 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1840 if n == 0 {
1841 return Err(Error::CorruptObject(format!(
1842 "pack stream truncated (zlib) at offset {}",
1843 self.stream_pos
1844 )));
1845 }
1846 self.pending.extend_from_slice(&scratch[..n]);
1847 continue;
1848 }
1849 self.pack_hasher.update(&self.pending[..consumed]);
1850 self.stream_pos += consumed;
1851 self.pending.drain(..consumed);
1852 self.enforce_max_input()?;
1853 return Ok(());
1854 }
1855 Ok(_) => {
1856 return Err(Error::CorruptObject(
1857 "0-byte packed object inflated to non-empty output".to_owned(),
1858 ));
1859 }
1860 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1861 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1862 if n == 0 {
1863 return Err(Error::CorruptObject(format!(
1864 "pack stream truncated (zlib) at offset {}",
1865 self.stream_pos
1866 )));
1867 }
1868 self.pending.extend_from_slice(&scratch[..n]);
1869 }
1870 Err(e) => return Err(Error::Zlib(e.to_string())),
1871 }
1872 }
1873 }
1874
1875 const CHUNK: usize = 64 * 1024;
1876 let mut scratch = [0u8; CHUNK];
1877 let mut scratch_out = [0u8; CHUNK];
1878
1879 let mut z = Decompress::new(true);
1880 let mut produced = 0usize;
1881 let mut eof = false;
1882 loop {
1883 if self.pending.is_empty() && !eof {
1884 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1885 if n == 0 {
1886 eof = true;
1887 } else {
1888 self.pending.extend_from_slice(&scratch[..n]);
1889 }
1890 }
1891
1892 let flush = if eof && self.pending.is_empty() {
1893 FlushDecompress::Finish
1894 } else {
1895 FlushDecompress::None
1896 };
1897
1898 let before_in = z.total_in();
1899 let before_out = z.total_out();
1900 let status = z
1901 .decompress(self.pending.as_slice(), &mut scratch_out, flush)
1902 .map_err(|e| Error::Zlib(e.to_string()))?;
1903 let consumed = (z.total_in() - before_in) as usize;
1904 if consumed > self.pending.len() {
1905 return Err(Error::CorruptObject(
1906 "zlib consumed more than pending buffer".to_owned(),
1907 ));
1908 }
1909 self.pack_hasher.update(&self.pending[..consumed]);
1910 self.stream_pos += consumed;
1911 self.pending.drain(..consumed);
1912 self.enforce_max_input()?;
1913 let chunk_len = (z.total_out() - before_out) as usize;
1914 if chunk_len > 0 {
1915 let chunk = &scratch_out[..chunk_len];
1916 let take = chunk.len().min(expected_size - produced);
1917 chunk_sink(&chunk[..take])?;
1918 produced += take;
1919 if take < chunk.len() {
1920 return Err(Error::CorruptObject(format!(
1921 "decompressed size mismatch: got {}, want {expected_size}",
1922 produced + (chunk.len() - take)
1923 )));
1924 }
1925 }
1926
1927 match status {
1928 Status::StreamEnd => {
1929 if produced != expected_size {
1930 return Err(Error::CorruptObject(format!(
1931 "decompressed size mismatch: got {produced}, want {expected_size}"
1932 )));
1933 }
1934 return Ok(());
1935 }
1936 Status::Ok | Status::BufError => {
1937 if consumed == 0 && !eof {
1938 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1939 if n == 0 {
1940 eof = true;
1941 } else {
1942 self.pending.extend_from_slice(&scratch[..n]);
1943 }
1944 } else if eof && self.pending.is_empty() && consumed == 0 {
1945 return Err(Error::CorruptObject(format!(
1946 "pack stream truncated (zlib) at offset {}",
1947 self.stream_pos
1948 )));
1949 }
1950 }
1951 }
1952 }
1953 }
1954
1955 fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
1956 let mut out = vec![0u8; expected_size];
1957 let mut pos = 0usize;
1958 self.decompress_into(expected_size, |chunk| {
1959 out[pos..pos + chunk.len()].copy_from_slice(chunk);
1960 pos += chunk.len();
1961 Ok(())
1962 })?;
1963 Ok(out)
1964 }
1965
1966 fn finalize_hasher(&self) -> Vec<u8> {
1968 self.pack_hasher.clone().finalize()
1969 }
1970
1971 fn read_trailer(&mut self) -> Result<Vec<u8>> {
1973 let hash_len = self.pack_hasher.algo().len();
1974 let mut b = vec![0u8; hash_len];
1975 if self.pending.len() >= hash_len {
1976 b.copy_from_slice(&self.pending[..hash_len]);
1977 self.pending.drain(..hash_len);
1978 self.stream_pos += hash_len;
1979 self.enforce_max_input()?;
1980 return Ok(b);
1981 }
1982 let tail = self.pending.len();
1983 if tail > 0 {
1984 b[..tail].copy_from_slice(&self.pending[..]);
1985 self.pending.clear();
1986 }
1987 self.inner
1988 .read_exact(&mut b[tail..])
1989 .map_err(|e| io_to_corrupt_eof(e, self.stream_pos, "trailer"))?;
1990 self.stream_pos += hash_len;
1991 self.enforce_max_input()?;
1992 Ok(b)
1993 }
1994}
1995
1996pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
2010 let mut result = Vec::new();
2011 apply_delta_into(&mut result, base, delta)?;
2012 Ok(result)
2013}
2014
2015pub fn apply_delta_into(out: &mut Vec<u8>, base: &[u8], delta: &[u8]) -> Result<()> {
2019 let mut pos = 0usize;
2020
2021 let src_size = read_delta_varint(delta, &mut pos)?;
2022 if src_size != base.len() {
2023 return Err(Error::CorruptObject(format!(
2024 "delta source size {src_size} != base size {}",
2025 base.len()
2026 )));
2027 }
2028 let dest_size = read_delta_varint(delta, &mut pos)?;
2029 out.clear();
2030 if out.capacity() < dest_size {
2031 out.reserve(dest_size - out.capacity());
2032 }
2033
2034 while pos < delta.len() {
2035 let cmd = delta[pos];
2036 pos += 1;
2037 if cmd == 0 {
2038 return Err(Error::CorruptObject(
2039 "reserved opcode 0 in delta stream".to_owned(),
2040 ));
2041 }
2042 if cmd & 0x80 != 0 {
2043 let mut offset = 0usize;
2046 let mut size = 0usize;
2047
2048 macro_rules! maybe_read_byte {
2049 ($flag:expr, $shift:expr, $target:expr) => {
2050 if cmd & $flag != 0 {
2051 let b = *delta.get(pos).ok_or_else(|| {
2052 Error::CorruptObject("truncated delta COPY operand".to_owned())
2053 })?;
2054 pos += 1;
2055 $target |= (b as usize) << $shift;
2056 }
2057 };
2058 }
2059
2060 maybe_read_byte!(0x01, 0, offset);
2061 maybe_read_byte!(0x02, 8, offset);
2062 maybe_read_byte!(0x04, 16, offset);
2063 maybe_read_byte!(0x08, 24, offset);
2064 maybe_read_byte!(0x10, 0, size);
2065 maybe_read_byte!(0x20, 8, size);
2066 maybe_read_byte!(0x40, 16, size);
2067
2068 if size == 0 {
2069 size = 0x10000;
2070 }
2071
2072 let end = offset.checked_add(size).ok_or_else(|| {
2073 Error::CorruptObject("delta COPY range overflows usize".to_owned())
2074 })?;
2075 let chunk = base.get(offset..end).ok_or_else(|| {
2076 Error::CorruptObject(format!(
2077 "delta COPY [{offset},{end}) out of range (base is {} bytes)",
2078 base.len()
2079 ))
2080 })?;
2081 out.extend_from_slice(chunk);
2082 } else {
2083 let n = cmd as usize;
2085 let chunk = delta
2086 .get(pos..pos + n)
2087 .ok_or_else(|| Error::CorruptObject("truncated delta INSERT data".to_owned()))?;
2088 out.extend_from_slice(chunk);
2089 pos += n;
2090 }
2091 }
2092
2093 if out.len() != dest_size {
2094 return Err(Error::CorruptObject(format!(
2095 "delta produced {} bytes but expected {dest_size}",
2096 out.len()
2097 )));
2098 }
2099
2100 Ok(())
2101}
2102
2103pub(crate) fn delta_uncompressed_result_size_if_complete(data: &[u8]) -> Result<Option<usize>> {
2109 let mut pos = 0;
2110 let Some(_src_size) = read_delta_varint_if_complete(data, &mut pos)? else {
2111 return Ok(None);
2112 };
2113 read_delta_varint_if_complete(data, &mut pos)
2114}
2115
2116fn read_delta_varint_if_complete(data: &[u8], pos: &mut usize) -> Result<Option<usize>> {
2117 let start = *pos;
2118 let mut value = 0usize;
2119 let mut shift = 0u32;
2120 while *pos < data.len() {
2121 let b = data[*pos];
2122 *pos += 1;
2123 value |= ((b & 0x7f) as usize) << shift;
2124 shift += 7;
2125 if b & 0x80 == 0 {
2126 return Ok(Some(value));
2127 }
2128 if shift > 35 {
2129 return Err(Error::CorruptObject(
2130 "delta varint exceeds supported width".to_owned(),
2131 ));
2132 }
2133 }
2134 *pos = start;
2135 Ok(None)
2136}
2137
2138fn read_delta_varint(data: &[u8], pos: &mut usize) -> Result<usize> {
2142 let mut value = 0usize;
2143 let mut shift = 0u32;
2144 loop {
2145 let b = *data
2146 .get(*pos)
2147 .ok_or_else(|| Error::CorruptObject("truncated delta varint".to_owned()))?;
2148 *pos += 1;
2149 value |= ((b & 0x7f) as usize) << shift;
2150 shift += 7;
2151 if b & 0x80 == 0 {
2152 break;
2153 }
2154 }
2155 Ok(value)
2156}
2157
2158#[cfg(test)]
2159mod tests {
2160 #![allow(clippy::vec_init_then_push)]
2161 use super::*;
2162
2163 fn make_pack(objects: &[(ObjectKind, &[u8])]) -> Vec<u8> {
2166 use flate2::write::ZlibEncoder;
2167 use std::io::Write;
2168
2169 let mut entries: Vec<Vec<u8>> = Vec::new();
2170 for (kind, data) in objects {
2171 let type_code: u8 = match kind {
2172 ObjectKind::Commit => 1,
2173 ObjectKind::Tree => 2,
2174 ObjectKind::Blob => 3,
2175 ObjectKind::Tag => 4,
2176 };
2177 let mut header = Vec::new();
2179 let mut size = data.len();
2180 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
2181 size >>= 4;
2182 if size > 0 {
2183 header.push(first | 0x80);
2184 while size > 0 {
2185 let b = (size & 0x7f) as u8;
2186 size >>= 7;
2187 header.push(if size > 0 { b | 0x80 } else { b });
2188 }
2189 } else {
2190 header.push(first);
2191 }
2192 let mut enc = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
2194 enc.write_all(data).unwrap();
2195 let compressed = enc.finish().unwrap();
2196 let mut entry = header;
2197 entry.extend_from_slice(&compressed);
2198 entries.push(entry);
2199 }
2200
2201 let mut pack = Vec::new();
2203 pack.extend_from_slice(b"PACK");
2204 pack.extend_from_slice(&2u32.to_be_bytes());
2205 pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
2206 for entry in &entries {
2207 pack.extend_from_slice(entry);
2208 }
2209 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
2210 pack
2211 }
2212
2213 #[test]
2214 fn test_apply_delta_simple() {
2215 let base = b"hello";
2217 let mut delta = Vec::new();
2218 delta.push(5u8);
2220 delta.push(11u8);
2222 delta.push(0x80 | 0x01 | 0x10); delta.push(0u8); delta.push(5u8); delta.push(6u8);
2229 delta.extend_from_slice(b" world");
2230
2231 let result = apply_delta(base, &delta).unwrap();
2232 assert_eq!(result, b"hello world");
2233 }
2234
2235 #[test]
2236 fn test_apply_delta_insert_only() {
2237 let base = b"";
2238 let mut delta = Vec::new();
2239 delta.push(0u8); delta.push(5u8); delta.push(5u8); delta.extend_from_slice(b"hello");
2243
2244 let result = apply_delta(base, &delta).unwrap();
2245 assert_eq!(result, b"hello");
2246 }
2247
2248 #[test]
2249 fn test_apply_delta_copy_only() {
2250 let base = b"abcdef";
2251 let mut delta = Vec::new();
2252 delta.push(6u8); delta.push(3u8); delta.push(0x91u8);
2257 delta.push(2u8); delta.push(3u8); let result = apply_delta(base, &delta).unwrap();
2261 assert_eq!(result, b"cde");
2262 }
2263
2264 #[test]
2265 fn test_apply_delta_size_zero_means_65536() {
2266 let base = vec![0xABu8; 65536];
2268 let mut delta = Vec::new();
2269 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);
2275 delta.push(0x80 | ((65536 >> 7) & 0x7f) as u8);
2276 delta.push(((65536 >> 14) & 0x7f) as u8);
2277 delta.push(0x80u8);
2280
2281 let result = apply_delta(&base, &delta).unwrap();
2282 assert_eq!(result.len(), 65536);
2283 assert!(result.iter().all(|&b| b == 0xAB));
2284 }
2285
2286 #[cfg_attr(
2287 not(feature = "full-tests"),
2288 ignore = "starts system git or is heavy; run with --features full-tests"
2289 )]
2290 #[test]
2291 fn test_unpack_objects_blobs() {
2292 use tempfile::TempDir;
2293 let tmp = TempDir::new().unwrap();
2294 let objects_dir = tmp.path().join("objects");
2295 std::fs::create_dir_all(&objects_dir).unwrap();
2296 let odb = Odb::new(&objects_dir);
2297
2298 let pack = make_pack(&[
2299 (ObjectKind::Blob, b"hello\n"),
2300 (ObjectKind::Blob, b"world\n"),
2301 ]);
2302
2303 let opts = UnpackOptions::default();
2304 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
2305 assert_eq!(count, 2);
2306
2307 let oid1 = HashAlgo::Sha1
2309 .hash_object(ObjectKind::Blob, b"hello\n")
2310 .expect("hash");
2311 let oid2 = HashAlgo::Sha1
2312 .hash_object(ObjectKind::Blob, b"world\n")
2313 .expect("hash");
2314 let obj1 = odb.read(&oid1).unwrap();
2315 let obj2 = odb.read(&oid2).unwrap();
2316 assert_eq!(obj1.data, b"hello\n");
2317 assert_eq!(obj2.data, b"world\n");
2318 }
2319
2320 #[cfg_attr(
2321 not(feature = "full-tests"),
2322 ignore = "starts system git or is heavy; run with --features full-tests"
2323 )]
2324 #[test]
2325 fn test_unpack_objects_empty_tree() {
2326 use tempfile::TempDir;
2327 let tmp = TempDir::new().unwrap();
2328 let objects_dir = tmp.path().join("objects");
2329 std::fs::create_dir_all(&objects_dir).unwrap();
2330 let odb = Odb::new(&objects_dir);
2331
2332 let pack = make_pack(&[(ObjectKind::Tree, b"")]);
2333 let opts = UnpackOptions::default();
2334 assert_eq!(
2335 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2336 1
2337 );
2338 let oid = HashAlgo::Sha1
2339 .hash_object(ObjectKind::Tree, b"")
2340 .expect("hash");
2341 assert!(odb.exists(&oid));
2342 let loose = objects_dir
2343 .join(oid.loose_prefix())
2344 .join(oid.loose_suffix());
2345 assert!(
2346 loose.is_file(),
2347 "empty tree must be materialized as a loose object during unpack"
2348 );
2349 }
2350
2351 #[cfg_attr(
2352 not(feature = "full-tests"),
2353 ignore = "starts system git or is heavy; run with --features full-tests"
2354 )]
2355 #[test]
2356 fn test_strict_skips_gitlink_tree_entries() {
2357 use crate::index::{MODE_GITLINK, MODE_REGULAR};
2358 use crate::objects::{serialize_tree, TreeEntry};
2359
2360 let submodule_oid = ObjectId::from_hex(&"7f".repeat(20)).unwrap();
2363
2364 let tree_data = serialize_tree(&[TreeEntry {
2366 mode: MODE_GITLINK,
2367 name: b"sub".to_vec(),
2368 oid: submodule_oid,
2369 }]);
2370 let tree_oid = HashAlgo::Sha1
2371 .hash_object(ObjectKind::Tree, &tree_data)
2372 .expect("hash");
2373
2374 let mut pack = HashMap::new();
2377 pack.insert(tree_oid, (ObjectKind::Tree, tree_data.clone()));
2378 assert!(strict_verify_packed_references(None, &pack).is_ok());
2379
2380 let bad_tree = serialize_tree(&[TreeEntry {
2383 mode: MODE_REGULAR,
2384 name: b"file".to_vec(),
2385 oid: ObjectId::from_hex(&"ab".repeat(20)).unwrap(),
2386 }]);
2387 let bad_oid = HashAlgo::Sha1
2388 .hash_object(ObjectKind::Tree, &bad_tree)
2389 .expect("hash");
2390 let mut bad_pack = HashMap::new();
2391 bad_pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2392 assert!(matches!(
2393 strict_verify_packed_references(None, &bad_pack),
2394 Err(Error::CorruptObject(_))
2395 ));
2396 }
2397
2398 struct ChunkedReader<'a> {
2400 data: &'a [u8],
2401 pos: usize,
2402 max_len: usize,
2403 }
2404
2405 impl io::Read for ChunkedReader<'_> {
2406 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2407 if self.pos >= self.data.len() {
2408 return Ok(0);
2409 }
2410 let take = (self.data.len() - self.pos)
2411 .min(self.max_len)
2412 .min(buf.len());
2413 buf[..take].copy_from_slice(&self.data[self.pos..self.pos + take]);
2414 self.pos += take;
2415 Ok(take)
2416 }
2417 }
2418
2419 #[cfg_attr(
2420 not(feature = "full-tests"),
2421 ignore = "starts system git or is heavy; run with --features full-tests"
2422 )]
2423 #[test]
2424 fn test_unpack_objects_chunked_read_matches_full_buffer() {
2425 use tempfile::TempDir;
2426 let pack = make_pack(&[(ObjectKind::Blob, b"chunked-stream")]);
2427 let opts = UnpackOptions::default();
2428 let oid = HashAlgo::Sha1
2429 .hash_object(ObjectKind::Blob, b"chunked-stream")
2430 .expect("hash");
2431
2432 let tmp = TempDir::new().unwrap();
2433 let objects_dir = tmp.path().join("objects");
2434 std::fs::create_dir_all(&objects_dir).unwrap();
2435 let odb = Odb::new(&objects_dir);
2436 assert_eq!(
2437 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2438 1
2439 );
2440 assert!(odb.exists(&oid));
2441
2442 let tmp2 = TempDir::new().unwrap();
2443 let objects_dir2 = tmp2.path().join("objects");
2444 std::fs::create_dir_all(&objects_dir2).unwrap();
2445 let odb2 = Odb::new(&objects_dir2);
2446 let mut chunked = ChunkedReader {
2447 data: pack.as_slice(),
2448 pos: 0,
2449 max_len: 8,
2450 };
2451 assert_eq!(unpack_objects(&mut chunked, &odb2, &opts).unwrap(), 1);
2452 assert!(odb2.exists(&oid));
2453 }
2454
2455 #[cfg_attr(
2456 not(feature = "full-tests"),
2457 ignore = "starts system git or is heavy; run with --features full-tests"
2458 )]
2459 #[test]
2460 fn test_unpack_objects_dry_run_writes_nothing() {
2461 use tempfile::TempDir;
2462 let tmp = TempDir::new().unwrap();
2463 let objects_dir = tmp.path().join("objects");
2464 std::fs::create_dir_all(&objects_dir).unwrap();
2465 let odb = Odb::new(&objects_dir);
2466
2467 let pack = make_pack(&[(ObjectKind::Blob, b"test content")]);
2468
2469 let opts = UnpackOptions {
2470 dry_run: true,
2471 quiet: true,
2472 strict: false,
2473 allowed_missing: Default::default(),
2474 allow_promisor_missing_references: false,
2475 max_input_bytes: None,
2476 ..Default::default()
2477 };
2478 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
2479 assert_eq!(count, 1);
2480
2481 let oid = HashAlgo::Sha1
2483 .hash_object(ObjectKind::Blob, b"test content")
2484 .expect("hash");
2485 assert!(!odb.exists(&oid));
2486 }
2487
2488 #[cfg_attr(
2489 not(feature = "full-tests"),
2490 ignore = "starts system git or is heavy; run with --features full-tests"
2491 )]
2492 #[test]
2493 fn test_unpack_objects_bad_signature() {
2494 use tempfile::TempDir;
2495 let tmp = TempDir::new().unwrap();
2496 let objects_dir = tmp.path().join("objects");
2497 std::fs::create_dir_all(&objects_dir).unwrap();
2498 let odb = Odb::new(&objects_dir);
2499
2500 let mut bad = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00".to_vec();
2501 bad.extend_from_slice(&[0u8; 20]);
2502 let opts = UnpackOptions::default();
2503 let err = unpack_objects(&mut bad.as_slice(), &odb, &opts).unwrap_err();
2504 assert!(err.to_string().contains("invalid signature"));
2505 }
2506
2507 #[cfg_attr(
2508 not(feature = "full-tests"),
2509 ignore = "starts system git or is heavy; run with --features full-tests"
2510 )]
2511 #[test]
2512 fn test_unpack_objects_checksum_mismatch() {
2513 use tempfile::TempDir;
2514 let tmp = TempDir::new().unwrap();
2515 let objects_dir = tmp.path().join("objects");
2516 std::fs::create_dir_all(&objects_dir).unwrap();
2517 let odb = Odb::new(&objects_dir);
2518
2519 let mut pack = make_pack(&[(ObjectKind::Blob, b"data")]);
2520 let n = pack.len();
2522 pack[n - 1] ^= 0xFF;
2523
2524 let opts = UnpackOptions::default();
2525 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2526 assert!(err.to_string().contains("checksum"));
2527 }
2528
2529 #[test]
2530 fn test_apply_delta_source_size_mismatch() {
2531 let base = b"hi";
2532 let delta = [3u8, 2u8, 2u8, b'h', b'i']; let err = apply_delta(base, &delta).unwrap_err();
2534 assert!(err.to_string().contains("source size"));
2535 }
2536
2537 #[test]
2538 fn unpack_many_objects_does_not_retain_all_payloads() {
2539 let _lock = TEST_UNPACK_RETAIN_LOCK.lock().expect("retain lock");
2540 const N: usize = 2500;
2541 let blobs: Vec<(ObjectKind, Vec<u8>)> = (0..N)
2542 .map(|i| (ObjectKind::Blob, format!("blob-{i}\n").into_bytes()))
2543 .collect();
2544 let blob_refs: Vec<(ObjectKind, &[u8])> =
2545 blobs.iter().map(|(k, d)| (*k, d.as_slice())).collect();
2546 let pack = make_pack(&blob_refs);
2547 let tmp = tempfile::tempdir().expect("tempdir");
2548 let odb = Odb::new(tmp.path());
2549 let cache_budget = 256 * 1024;
2550 test_set_unpack_base_cache_byte_limit(cache_budget);
2551 let count =
2552 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).expect("unpack");
2553 assert_eq!(count, N);
2554 assert!(
2555 test_peak_unpack_retained_bytes() <= cache_budget + 128 * 1024,
2556 "peak retained {} exceeds cache budget {}",
2557 test_peak_unpack_retained_bytes(),
2558 cache_budget
2559 );
2560 test_set_unpack_base_cache_byte_limit(0);
2561 }
2562
2563 #[test]
2564 fn unpack_many_deltified_blobs_respects_base_cache_budget() {
2565 let _lock = TEST_UNPACK_RETAIN_LOCK.lock().expect("retain lock");
2566 use crate::objects::{serialize_commit, CommitData};
2567 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2568
2569 const N_DELTAS: usize = 350;
2570 const BLOB_SIZE: usize = 48 * 1024;
2571 let base: Vec<u8> = (0..BLOB_SIZE).map(|i| (i % 251) as u8).collect();
2572 let tree_oid = HashAlgo::Sha1
2573 .hash_object(ObjectKind::Tree, b"")
2574 .expect("hash");
2575 let commit = CommitData {
2576 tree: tree_oid,
2577 parents: vec![],
2578 author: "A <a@b.com>".into(),
2579 committer: "A <a@b.com>".into(),
2580 author_raw: Vec::new(),
2581 committer_raw: Vec::new(),
2582 encoding: None,
2583 message: "m".into(),
2584 raw_message: None,
2585 extra_headers: Vec::new(),
2586 };
2587 let commit_data = serialize_commit(&commit);
2588
2589 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2590 pb.add_full(PK::Tree, b"");
2591 pb.add_full(PK::Commit, &commit_data);
2592 let base_idx = pb.add_full(PK::Blob, &base);
2593 for i in 0..N_DELTAS {
2594 let mut variant = base.clone();
2595 variant[BLOB_SIZE - 1] = (i % 256) as u8;
2596 let delta = crate::delta_encode::encode_lcp_delta(&base, &variant).expect("delta");
2597 pb.add_ofs_delta(base_idx, &delta, delta.len());
2598 }
2599 let pack = pb.build().bytes;
2600
2601 let tmp = tempfile::tempdir().expect("tempdir");
2602 let odb = Odb::new(tmp.path());
2603 let cache_budget = 512 * 1024;
2604 test_set_unpack_base_cache_byte_limit(cache_budget);
2605 let opts = UnpackOptions {
2606 strict: true,
2607 ..Default::default()
2608 };
2609 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).expect("unpack");
2610 assert_eq!(count, N_DELTAS + 3);
2611 assert!(
2612 test_peak_unpack_retained_bytes() <= cache_budget + 256 * 1024,
2613 "peak retained {} exceeds cache budget {}",
2614 test_peak_unpack_retained_bytes(),
2615 cache_budget
2616 );
2617 test_set_unpack_base_cache_byte_limit(0);
2618 }
2619
2620 #[test]
2621 fn pack_index_many_blobs_without_retaining_all_payloads() {
2622 const N: usize = 1500;
2623 let blobs: Vec<(ObjectKind, Vec<u8>)> = (0..N)
2624 .map(|i| (ObjectKind::Blob, format!("blob-{i}\n").into_bytes()))
2625 .collect();
2626 let blob_refs: Vec<(ObjectKind, &[u8])> =
2627 blobs.iter().map(|(k, d)| (*k, d.as_slice())).collect();
2628 let pack = make_pack(&blob_refs);
2629 let tmp = tempfile::tempdir().expect("tempdir");
2630 let odb = Odb::new(tmp.path());
2631 let started = std::time::Instant::now();
2632 let records = pack_index_records_from_bytes(&pack, &odb).expect("index records");
2633 let elapsed = started.elapsed();
2634 assert_eq!(records.len(), N, "one index row per object");
2635 assert!(
2636 elapsed.as_secs() < 30,
2637 "indexing {N} blobs took {:?}; expected linear scan without full retention blow-up",
2638 elapsed
2639 );
2640 }
2641
2642 fn pack_with_ref_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, ObjectId, ObjectId) {
2643 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2644
2645 let delta = crate::delta_encode::encode_lcp_delta(base, target).expect("delta");
2646 let base_oid = HashAlgo::Sha1
2647 .hash_object(ObjectKind::Blob, base)
2648 .expect("hash");
2649 let target_oid = HashAlgo::Sha1
2650 .hash_object(ObjectKind::Blob, target)
2651 .expect("hash");
2652 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2653 pb.add_full(PK::Blob, base);
2654 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2655 (pb.build().bytes, base_oid, target_oid)
2656 }
2657
2658 #[cfg_attr(
2659 not(feature = "full-tests"),
2660 ignore = "starts system git or is heavy; run with --features full-tests"
2661 )]
2662 #[test]
2663 fn test_unpack_objects_resolves_ref_and_ofs_deltas() {
2664 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2665
2666 let base = b"base payload";
2667 let target = b"base payload-extended";
2668 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2669 let base_oid = HashAlgo::Sha1
2670 .hash_object(ObjectKind::Blob, base)
2671 .expect("hash");
2672
2673 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2674 let base_idx = pb.add_full(PK::Blob, base);
2675 pb.add_ofs_delta(base_idx, &delta, delta.len());
2676 let ofs_pack = pb.build().bytes;
2677
2678 let tmp = tempfile::tempdir().unwrap();
2679 let odb = Odb::new(tmp.path());
2680 let opts = UnpackOptions::default();
2681 assert_eq!(
2682 unpack_objects(&mut ofs_pack.as_slice(), &odb, &opts).unwrap(),
2683 2
2684 );
2685 let target_oid = HashAlgo::Sha1
2686 .hash_object(ObjectKind::Blob, target)
2687 .expect("hash");
2688 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2689
2690 let (ref_pack, _, ref_target_oid) = pack_with_ref_delta(base, target);
2691 let tmp2 = tempfile::tempdir().unwrap();
2692 let odb2 = Odb::new(tmp2.path());
2693 assert_eq!(
2694 unpack_objects(&mut ref_pack.as_slice(), &odb2, &opts).unwrap(),
2695 2
2696 );
2697 assert_eq!(odb2.read(&ref_target_oid).unwrap().data, target);
2698 assert!(odb2.exists(&base_oid));
2699 }
2700
2701 #[cfg_attr(
2702 not(feature = "full-tests"),
2703 ignore = "starts system git or is heavy; run with --features full-tests"
2704 )]
2705 #[test]
2706 fn test_unpack_objects_ref_delta_base_from_loose() {
2707 let base = b"loose-base";
2708 let target = b"loose-base-v2";
2709 let (_, base_oid, target_oid) = pack_with_ref_delta(base, target);
2710
2711 let tmp = tempfile::tempdir().unwrap();
2712 let odb = Odb::new(tmp.path());
2713 odb.write_local(ObjectKind::Blob, base).unwrap();
2714 let delta_only = {
2716 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2717 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2718 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2719 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2720 pb.build().bytes
2721 };
2722 let pack = delta_only;
2723
2724 let opts = UnpackOptions::default();
2725 assert_eq!(
2726 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2727 1
2728 );
2729 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2730 }
2731
2732 #[cfg_attr(
2733 not(feature = "full-tests"),
2734 ignore = "starts system git or is heavy; run with --features full-tests"
2735 )]
2736 #[test]
2737 fn test_unpack_objects_strict_commit_tree_tag() {
2738 use crate::objects::{serialize_commit, serialize_tag, CommitData, TagData};
2739
2740 let tree_data = b"";
2741 let tree_oid = HashAlgo::Sha1
2742 .hash_object(ObjectKind::Tree, tree_data)
2743 .expect("hash");
2744 let commit = CommitData {
2745 tree: tree_oid,
2746 parents: vec![],
2747 author: "A <a@b.com>".into(),
2748 committer: "A <a@b.com>".into(),
2749 author_raw: Vec::new(),
2750 committer_raw: Vec::new(),
2751 encoding: None,
2752 message: "msg".into(),
2753 raw_message: None,
2754 extra_headers: Vec::new(),
2755 };
2756 let commit_data = serialize_commit(&commit);
2757 let commit_oid = HashAlgo::Sha1
2758 .hash_object(ObjectKind::Commit, &commit_data)
2759 .expect("hash");
2760 let tag = TagData {
2761 object: commit_oid,
2762 object_type: "commit".into(),
2763 tag: "v1".into(),
2764 tagger: Some("T <t@e.com>".into()),
2765 extra_headers: Vec::new(),
2766 message: "annotated".into(),
2767 };
2768 let tag_data = serialize_tag(&tag);
2769 let pack = make_pack(&[
2770 (ObjectKind::Tree, tree_data),
2771 (ObjectKind::Commit, &commit_data),
2772 (ObjectKind::Tag, &tag_data),
2773 ]);
2774
2775 let tmp = tempfile::tempdir().unwrap();
2776 let odb = Odb::new(tmp.path());
2777 let opts = UnpackOptions {
2778 strict: true,
2779 ..Default::default()
2780 };
2781 assert_eq!(
2782 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2783 3
2784 );
2785 assert!(odb.exists(&commit_oid));
2786 }
2787
2788 #[cfg_attr(
2789 not(feature = "full-tests"),
2790 ignore = "starts system git or is heavy; run with --features full-tests"
2791 )]
2792 #[test]
2793 fn test_unpack_objects_strict_shallow_skips_missing_parent() {
2794 use crate::objects::{serialize_commit, CommitData};
2795
2796 let tree_oid = HashAlgo::Sha1
2797 .hash_object(ObjectKind::Tree, b"")
2798 .expect("hash");
2799 let missing_parent = ObjectId::from_hex(&"cc".repeat(20)).unwrap();
2800 let commit = CommitData {
2801 tree: tree_oid,
2802 parents: vec![missing_parent],
2803 author: "A <a@b.com>".into(),
2804 committer: "A <a@b.com>".into(),
2805 author_raw: Vec::new(),
2806 committer_raw: Vec::new(),
2807 encoding: None,
2808 message: "msg".into(),
2809 raw_message: None,
2810 extra_headers: Vec::new(),
2811 };
2812 let commit_data = serialize_commit(&commit);
2813 let commit_oid = HashAlgo::Sha1
2814 .hash_object(ObjectKind::Commit, &commit_data)
2815 .expect("hash");
2816 let pack = make_pack(&[(ObjectKind::Tree, b""), (ObjectKind::Commit, &commit_data)]);
2817
2818 let tmp = tempfile::tempdir().unwrap();
2819 let odb = Odb::new(tmp.path());
2820 let mut shallow = HashSet::new();
2821 shallow.insert(commit_oid);
2822 let opts = UnpackOptions {
2823 strict: true,
2824 shallow_boundaries: shallow,
2825 ..Default::default()
2826 };
2827 assert_eq!(
2828 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2829 2
2830 );
2831 }
2832
2833 #[cfg_attr(
2834 not(feature = "full-tests"),
2835 ignore = "starts system git or is heavy; run with --features full-tests"
2836 )]
2837 #[test]
2838 fn test_unpack_objects_max_input_bytes() {
2839 let pack = make_pack(&[(ObjectKind::Blob, b"small")]);
2840 let tmp = tempfile::tempdir().unwrap();
2841 let odb = Odb::new(tmp.path());
2842 let opts = UnpackOptions {
2843 max_input_bytes: Some(4),
2844 ..Default::default()
2845 };
2846 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2847 assert!(err.to_string().contains("maximum allowed size"));
2848 }
2849
2850 #[cfg_attr(
2851 not(feature = "full-tests"),
2852 ignore = "starts system git or is heavy; run with --features full-tests"
2853 )]
2854 #[test]
2855 fn test_unpack_objects_unresolved_ref_delta() {
2856 let missing = ObjectId::from_hex(&"dd".repeat(20)).unwrap();
2857 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2858 let delta = [0u8, 1u8, 1u8, b'x'];
2859 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2860 pb.add_ref_delta(missing.as_bytes(), &delta, delta.len());
2861 let pack = pb.build().bytes;
2862
2863 let tmp = tempfile::tempdir().unwrap();
2864 let odb = Odb::new(tmp.path());
2865 let err =
2866 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
2867 assert!(err.to_string().contains("could not be resolved"));
2868 }
2869
2870 #[cfg_attr(
2871 not(feature = "full-tests"),
2872 ignore = "starts system git or is heavy; run with --features full-tests"
2873 )]
2874 #[test]
2875 fn missing_ref_delta_base_is_typed_on_every_ingest_path() {
2876 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2877 let delta = [0u8, 1u8, 1u8, b'x'];
2878 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2879 pb.add_ref_delta(&[0xdd; 20], &delta, delta.len());
2880 pb.add_ref_delta(&[0xee; 20], &delta, delta.len());
2881 let pack = pb.build().bytes;
2882 let tmp = tempfile::tempdir().unwrap();
2883 let odb = Odb::new(tmp.path());
2884
2885 let unpacked = unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default());
2886 assert!(
2887 matches!(unpacked, Err(Error::UnresolvedDeltas { count: 2 })),
2888 "{unpacked:?}"
2889 );
2890 let mapped = pack_bytes_to_object_map(&pack, &odb);
2891 assert!(
2892 matches!(mapped, Err(Error::UnresolvedDeltas { count: 2 })),
2893 "{mapped:?}"
2894 );
2895 let indexed = pack_index_records_from_bytes(&pack, &odb);
2896 assert!(
2897 matches!(indexed, Err(Error::UnresolvedDeltas { count: 2 })),
2898 "{indexed:?}"
2899 );
2900 assert!(pack_is_thin(&pack, HashAlgo::Sha1));
2901 }
2902
2903 #[test]
2904 fn test_apply_delta_into_matches_apply_delta() {
2905 let base = b"abcdef";
2906 let mut delta = Vec::new();
2907 delta.push(6u8);
2908 delta.push(3u8);
2909 delta.push(0x91u8);
2910 delta.push(2u8);
2911 delta.push(3u8);
2912
2913 let expected = apply_delta(base, &delta).unwrap();
2914 let mut out = Vec::new();
2915 apply_delta_into(&mut out, base, &delta).unwrap();
2916 assert_eq!(out, expected);
2917 }
2918
2919 #[test]
2920 fn test_pack_bytes_to_object_map_ref_delta() {
2921 let base = b"map-base";
2922 let target = b"map-base-2";
2923 let (pack, _, target_oid) = pack_with_ref_delta(base, target);
2924 let tmp = tempfile::tempdir().unwrap();
2925 let odb = Odb::new(tmp.path());
2926 let map = pack_bytes_to_object_map(&pack, &odb).unwrap();
2927 assert_eq!(map.get(&target_oid).unwrap().data, target);
2928 }
2929
2930 #[test]
2931 fn test_pack_is_thin_and_non_thin() {
2932 let base = b"thin";
2933 let target = b"thin-pack";
2934 let (full, base_oid, _) = pack_with_ref_delta(base, target);
2935 assert!(!pack_is_thin(&full, HashAlgo::Sha1));
2936
2937 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2938 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2939 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2940 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2941 let thin = pb.build().bytes;
2942 assert!(pack_is_thin(&thin, HashAlgo::Sha1));
2943 }
2944
2945 #[cfg_attr(
2946 not(feature = "full-tests"),
2947 ignore = "starts system git or is heavy; run with --features full-tests"
2948 )]
2949 #[test]
2950 fn test_unpack_zero_byte_blob_via_pack_builder() {
2951 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2952 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2953 pb.add_full(PK::Blob, b"");
2954 let pack = pb.build().bytes;
2955 let tmp = tempfile::tempdir().unwrap();
2956 let odb = Odb::new(tmp.path());
2957 assert_eq!(
2958 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
2959 1
2960 );
2961 let oid = HashAlgo::Sha1
2962 .hash_object(ObjectKind::Blob, b"")
2963 .expect("hash");
2964 assert!(odb.exists(&oid));
2965 }
2966
2967 #[test]
2968 fn test_strict_verify_rejects_missing_tree_reference() {
2969 let tree_oid = HashAlgo::Sha1
2970 .hash_object(ObjectKind::Tree, b"")
2971 .expect("hash");
2972 let missing = ObjectId::from_hex(&"ee".repeat(20)).unwrap();
2973 let bad_tree = crate::objects::serialize_tree(&[crate::objects::TreeEntry {
2974 mode: crate::index::MODE_REGULAR,
2975 name: b"f".to_vec(),
2976 oid: missing,
2977 }]);
2978 let bad_oid = HashAlgo::Sha1
2979 .hash_object(ObjectKind::Tree, &bad_tree)
2980 .expect("hash");
2981 let mut pack = HashMap::new();
2982 pack.insert(tree_oid, (ObjectKind::Tree, b"".to_vec()));
2983 pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2984 assert!(strict_verify_packed_references(None, &pack).is_err());
2985 }
2986
2987 #[cfg_attr(
2988 not(feature = "full-tests"),
2989 ignore = "starts system git or is heavy; run with --features full-tests"
2990 )]
2991 #[test]
2992 fn test_unpack_large_blob_materializes_on_disk() {
2993 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
2994 let pack = make_pack(&[(ObjectKind::Blob, &big)]);
2995 let tmp = tempfile::tempdir().unwrap();
2996 let odb = Odb::new(tmp.path());
2997 assert_eq!(
2998 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
2999 1
3000 );
3001 let oid = HashAlgo::Sha1
3002 .hash_object(ObjectKind::Blob, &big)
3003 .expect("hash");
3004 assert_eq!(odb.read(&oid).unwrap().data, big);
3005 }
3006
3007 #[cfg_attr(
3008 not(feature = "full-tests"),
3009 ignore = "starts system git or is heavy; run with --features full-tests"
3010 )]
3011 #[test]
3012 fn truncated_zlib_after_complete_payload_does_not_hang() {
3013 use std::time::{Duration, Instant};
3014
3015 let payload = vec![0u8; 4096];
3016 let mut pack = make_pack(&[(ObjectKind::Blob, &payload)]);
3017 pack.truncate(pack.len() - 21);
3018 let digest = HashAlgo::Sha1.checksum_digest(&pack);
3019 pack.extend_from_slice(&digest);
3020
3021 let tmp = tempfile::tempdir().unwrap();
3022 let odb = Odb::new(tmp.path());
3023 let started = Instant::now();
3024 let err =
3025 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
3026 assert!(
3027 started.elapsed() < Duration::from_secs(2),
3028 "unpack hung on truncated zlib"
3029 );
3030 let msg = err.to_string();
3031 assert!(
3032 msg.contains("truncated") || msg.contains("zlib") || msg.contains("mismatch"),
3033 "unexpected error: {msg}"
3034 );
3035 }
3036
3037 #[test]
3038 fn strict_dot_special_does_not_reload_streamed_blobs() {
3039 use std::cell::Cell;
3040
3041 let tmp = tempfile::tempdir().unwrap();
3042 let odb = Odb::new(tmp.path());
3043 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
3044 let big_oid = HashAlgo::Sha1
3045 .hash_object(ObjectKind::Blob, &big)
3046 .expect("hash");
3047 let tree_oid = HashAlgo::Sha1
3048 .hash_object(ObjectKind::Tree, b"")
3049 .expect("hash");
3050
3051 struct CountingSink<'a> {
3052 inner: &'a Odb,
3053 reads: Cell<usize>,
3054 }
3055
3056 impl UnpackSink for CountingSink<'_> {
3057 fn hash_algo(&self) -> HashAlgo {
3058 Odb::hash_algo(self.inner)
3059 }
3060
3061 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
3062 if dry_run {
3063 return self.inner.hash(kind, data);
3064 }
3065 self.inner.write(kind, data)
3066 }
3067
3068 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
3069 self.reads.set(self.reads.get() + 1);
3070 self.inner.read_stored(oid)
3071 }
3072
3073 fn contains(&self, oid: &ObjectId) -> Result<bool> {
3074 Odb::contains(self.inner, oid)
3075 }
3076 }
3077
3078 let mut by_oid = HashMap::new();
3079 by_oid.insert(
3080 big_oid,
3081 PackedObjectEntry::Stored {
3082 kind: ObjectKind::Blob,
3083 oid: big_oid,
3084 },
3085 );
3086 by_oid.insert(
3087 tree_oid,
3088 PackedObjectEntry::DryRun {
3089 kind: ObjectKind::Tree,
3090 data: Vec::new(),
3091 },
3092 );
3093
3094 let sink = CountingSink {
3095 inner: &odb,
3096 reads: Cell::new(0),
3097 };
3098 verify_packed_dot_special_entries(&by_oid, &sink).expect("dot-special walk");
3099 assert_eq!(
3100 sink.reads.get(),
3101 0,
3102 "must not read streamed blobs back for strict fsck"
3103 );
3104 }
3105
3106 #[test]
3107 fn strict_verify_does_not_reload_streamed_blobs() {
3108 use std::cell::Cell;
3109
3110 use crate::index::MODE_REGULAR;
3111 use crate::objects::{serialize_commit, serialize_tree, CommitData, TreeEntry};
3112
3113 let tmp = tempfile::tempdir().unwrap();
3114 let odb = Odb::new(tmp.path());
3115 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
3116 let big_oid = odb.write(ObjectKind::Blob, &big).expect("write blob");
3117
3118 let tree_data = serialize_tree(&[TreeEntry {
3119 mode: MODE_REGULAR,
3120 name: b"big.bin".to_vec(),
3121 oid: big_oid,
3122 }]);
3123 let tree_oid = HashAlgo::Sha1
3124 .hash_object(ObjectKind::Tree, &tree_data)
3125 .expect("hash");
3126 let commit = CommitData {
3127 tree: tree_oid,
3128 parents: vec![],
3129 author: "A <a@b.com>".into(),
3130 committer: "A <a@b.com>".into(),
3131 author_raw: Vec::new(),
3132 committer_raw: Vec::new(),
3133 encoding: None,
3134 message: "m".into(),
3135 raw_message: None,
3136 extra_headers: Vec::new(),
3137 };
3138 let commit_data = serialize_commit(&commit);
3139 let commit_oid = HashAlgo::Sha1
3140 .hash_object(ObjectKind::Commit, &commit_data)
3141 .expect("hash");
3142
3143 struct CountingSink<'a> {
3144 inner: &'a Odb,
3145 reads: Cell<usize>,
3146 }
3147
3148 impl UnpackSink for CountingSink<'_> {
3149 fn hash_algo(&self) -> HashAlgo {
3150 Odb::hash_algo(self.inner)
3151 }
3152
3153 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
3154 if dry_run {
3155 return self.inner.hash(kind, data);
3156 }
3157 self.inner.write(kind, data)
3158 }
3159
3160 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
3161 self.reads.set(self.reads.get() + 1);
3162 self.inner.read_stored(oid)
3163 }
3164
3165 fn contains(&self, oid: &ObjectId) -> Result<bool> {
3166 Odb::contains(self.inner, oid)
3167 }
3168 }
3169
3170 let mut by_oid = HashMap::new();
3171 by_oid.insert(
3172 big_oid,
3173 PackedObjectEntry::Stored {
3174 kind: ObjectKind::Blob,
3175 oid: big_oid,
3176 },
3177 );
3178 by_oid.insert(
3179 tree_oid,
3180 PackedObjectEntry::DryRun {
3181 kind: ObjectKind::Tree,
3182 data: tree_data,
3183 },
3184 );
3185 by_oid.insert(
3186 commit_oid,
3187 PackedObjectEntry::DryRun {
3188 kind: ObjectKind::Commit,
3189 data: commit_data,
3190 },
3191 );
3192
3193 let sink = CountingSink {
3194 inner: &odb,
3195 reads: Cell::new(0),
3196 };
3197 strict_verify_packed_references_map(
3198 &sink,
3199 &by_oid,
3200 &HashSet::new(),
3201 false,
3202 &HashSet::new(),
3203 None,
3204 )
3205 .expect("strict connectivity");
3206 assert_eq!(
3207 sink.reads.get(),
3208 0,
3209 "strict verify must not read streamed blob payloads"
3210 );
3211
3212 let missing = ObjectId::from_hex(&"ab".repeat(20)).unwrap();
3213 let bad_tree = serialize_tree(&[TreeEntry {
3214 mode: MODE_REGULAR,
3215 name: b"missing".to_vec(),
3216 oid: missing,
3217 }]);
3218 let bad_tree_oid = HashAlgo::Sha1
3219 .hash_object(ObjectKind::Tree, &bad_tree)
3220 .expect("hash");
3221 let mut bad = HashMap::new();
3222 bad.insert(
3223 bad_tree_oid,
3224 PackedObjectEntry::DryRun {
3225 kind: ObjectKind::Tree,
3226 data: bad_tree,
3227 },
3228 );
3229 let sink2 = CountingSink {
3230 inner: &odb,
3231 reads: Cell::new(0),
3232 };
3233 assert!(matches!(
3234 strict_verify_packed_references_map(
3235 &sink2,
3236 &bad,
3237 &HashSet::new(),
3238 false,
3239 &HashSet::new(),
3240 None,
3241 ),
3242 Err(Error::CorruptObject(_))
3243 ));
3244 assert_eq!(sink2.reads.get(), 0);
3245 }
3246}