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_with_options(kind, &data, WriteOptions::default());
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 match unpack_into(
515 reader,
516 &StoreSink { store, bases },
517 opts,
518 progress,
519 cancel,
520 None,
521 ) {
522 Ok(unpacked) => {
523 store.flush_for_publish()?;
524 Ok(unpacked.into_ingested())
525 }
526 Err(err) => {
527 let _ = store.rollback();
528 Err(err)
529 }
530 }
531}
532
533pub(crate) fn ingest_pack_into_odb(
536 reader: &mut dyn Read,
537 odb: &Odb,
538 opts: &UnpackOptions,
539 progress: Option<&mut dyn ProgressSink>,
540 cancel: Option<&dyn Cancel>,
541) -> Result<IngestedObjects> {
542 match unpack_into(reader, odb, opts, progress, cancel, None) {
543 Ok(unpacked) => {
544 odb.flush_for_publish()?;
545 Ok(unpacked.into_ingested())
546 }
547 Err(err) => {
548 let _ = odb.rollback();
549 Err(err)
550 }
551 }
552}
553
554struct Unpacked {
556 count: usize,
557 by_oid: HashMap<ObjectId, PackedObjectEntry>,
558 object_counts: ObjectKindCounts,
559 pack_bytes: u64,
560}
561
562impl Unpacked {
563 fn into_ingested(self) -> IngestedObjects {
564 IngestedObjects {
565 object_ids: self.by_oid.into_keys().collect(),
566 object_counts: self.object_counts,
567 pack_bytes: self.pack_bytes,
568 }
569 }
570}
571
572fn poll_unpack_cancel(
573 progress: &mut Option<&mut dyn ProgressSink>,
574 cancel: Option<&dyn Cancel>,
575) -> Result<()> {
576 let Some(cancel) = cancel else {
577 return Ok(());
578 };
579 match progress {
580 Some(p) => poll_cancel(&mut **p, cancel),
581 None => poll_cancel(&mut NullProgress, cancel),
582 }
583}
584
585fn unpack_into<S: UnpackSink + ?Sized>(
586 reader: &mut dyn Read,
587 sink: &S,
588 opts: &UnpackOptions,
589 mut progress: Option<&mut dyn ProgressSink>,
590 cancel: Option<&dyn Cancel>,
591 promised: Option<PromisedReference<'_>>,
592) -> Result<Unpacked> {
593 const MAX_STREAM_BLOB_BYTES: usize = 1024 * 1024;
595
596 let algo = sink.hash_algo();
597 let mut rd = StreamingPackReader::new(reader, opts.max_input_bytes, algo);
598 let mut base_cache = UnpackBaseByteCache::new(unpack_base_cache_byte_limit());
599
600 let sig = rd.read_exact_n(4)?;
602 if sig != b"PACK" {
603 return Err(Error::CorruptObject(
604 "not a pack stream: invalid signature".to_owned(),
605 ));
606 }
607 let version = rd.read_u32_be()?;
608 if version != 2 && version != 3 {
609 return Err(Error::CorruptObject(format!(
610 "unsupported pack version {version}"
611 )));
612 }
613 let nr_objects = rd.read_u32_be()? as usize;
614
615 let mut by_offset: HashMap<usize, PackedObjectEntry> = HashMap::new();
616 let mut by_oid: HashMap<ObjectId, PackedObjectEntry> = HashMap::new();
617
618 let mut pending: Vec<PendingDelta> = Vec::new();
619 let mut count = 0usize;
620 let mut object_counts = ObjectKindCounts::default();
621
622 if let Some(p) = progress.as_mut() {
623 p.start(ProgressPhase::IndexingObjects, Some(nr_objects as u64));
624 }
625 for _ in 0..nr_objects {
626 poll_unpack_cancel(&mut progress, cancel)?;
627 let obj_offset = rd.stream_pos();
628 let (type_code, size) = rd.read_type_size()?;
629
630 match type_code {
631 1..=4 => {
632 let kind = type_code_to_kind(type_code)?;
633 if !opts.dry_run
634 && size > MAX_STREAM_BLOB_BYTES
635 && kind == ObjectKind::Blob
636 && sink.supports_streaming_inflate()
637 {
638 let oid = sink.write_inflated_from_pack(kind, size, &mut rd, false)?;
639 let entry = PackedObjectEntry::Stored { kind, oid };
640 by_offset.insert(obj_offset, entry.clone());
641 by_oid.insert(oid, entry);
642 } else {
643 let data = rd.decompress(size)?;
644 let oid = sink.write(kind, &data, opts.dry_run)?;
645 record_resolved_object(
646 &mut by_offset,
647 &mut by_oid,
648 obj_offset,
649 oid,
650 kind,
651 data,
652 opts,
653 MAX_STREAM_BLOB_BYTES,
654 &mut base_cache,
655 )?;
656 }
657 object_counts.add(kind);
658 count += 1;
659 }
660 6 => {
661 let neg = rd.read_ofs_neg_offset()?;
662 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
663 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
664 })?;
665 let delta_data = rd.decompress(size)?;
666 let delta = PendingDelta {
667 offset: obj_offset,
668 base_oid: None,
669 base_offset: Some(base_offset),
670 delta_data,
671 };
672 test_note_pending_delta_bytes(&pending, base_cache.bytes_used());
673 if let Some((oid, kind, data)) = resolve_pending_delta(
674 &delta,
675 &by_offset,
676 &by_oid,
677 sink,
678 opts,
679 MAX_STREAM_BLOB_BYTES,
680 &mut base_cache,
681 )? {
682 record_resolved_object(
683 &mut by_offset,
684 &mut by_oid,
685 delta.offset,
686 oid,
687 kind,
688 data,
689 opts,
690 MAX_STREAM_BLOB_BYTES,
691 &mut base_cache,
692 )?;
693 object_counts.add(kind);
694 count += 1;
695 } else {
696 pending.push(delta);
697 }
698 }
699 7 => {
700 let base_bytes = rd.read_exact_n(algo.len())?;
701 let base_oid = ObjectId::from_bytes(&base_bytes)?;
702 let delta_data = rd.decompress(size)?;
703 let delta = PendingDelta {
704 offset: obj_offset,
705 base_oid: Some(base_oid),
706 base_offset: None,
707 delta_data,
708 };
709 test_note_pending_delta_bytes(&pending, base_cache.bytes_used());
710 if let Some((oid, kind, data)) = resolve_pending_delta(
711 &delta,
712 &by_offset,
713 &by_oid,
714 sink,
715 opts,
716 MAX_STREAM_BLOB_BYTES,
717 &mut base_cache,
718 )? {
719 record_resolved_object(
720 &mut by_offset,
721 &mut by_oid,
722 delta.offset,
723 oid,
724 kind,
725 data,
726 opts,
727 MAX_STREAM_BLOB_BYTES,
728 &mut base_cache,
729 )?;
730 object_counts.add(kind);
731 count += 1;
732 } else {
733 pending.push(delta);
734 }
735 }
736 other => {
737 return Err(Error::CorruptObject(format!(
738 "unknown packed-object type {other}"
739 )))
740 }
741 }
742 if let Some(p) = progress.as_mut() {
743 p.inc(1);
744 }
745 }
746 if let Some(p) = progress.as_mut() {
747 p.finish();
748 }
749
750 let digest = rd.finalize_hasher();
751 let trailing = rd.read_trailer()?;
752 if digest != trailing {
753 return Err(Error::CorruptObject(
754 "pack trailing checksum mismatch".to_owned(),
755 ));
756 }
757 let pack_bytes = rd.stream_pos() as u64;
758
759 let mut remaining = pending;
760 if let (Some(p), false) = (progress.as_mut(), remaining.is_empty()) {
761 p.start(ProgressPhase::ResolvingDeltas, Some(remaining.len() as u64));
762 }
763 let resolving = !remaining.is_empty();
764 loop {
765 if remaining.is_empty() {
766 break;
767 }
768 poll_unpack_cancel(&mut progress, cancel)?;
769 test_note_pending_delta_bytes(&remaining, base_cache.bytes_used());
770 let before = remaining.len();
771 let mut still_pending: Vec<PendingDelta> = Vec::new();
772
773 for delta in remaining {
774 if let Some((oid, kind, data)) = resolve_pending_delta(
775 &delta,
776 &by_offset,
777 &by_oid,
778 sink,
779 opts,
780 MAX_STREAM_BLOB_BYTES,
781 &mut base_cache,
782 )? {
783 record_resolved_object(
784 &mut by_offset,
785 &mut by_oid,
786 delta.offset,
787 oid,
788 kind,
789 data,
790 opts,
791 MAX_STREAM_BLOB_BYTES,
792 &mut base_cache,
793 )?;
794 object_counts.add(kind);
795 count += 1;
796 if let Some(p) = progress.as_mut() {
797 p.inc(1);
798 }
799 } else {
800 still_pending.push(delta);
801 }
802 }
803
804 remaining = still_pending;
805 if remaining.len() == before {
806 return Err(Error::UnresolvedDeltas {
807 count: u64::try_from(remaining.len()).unwrap_or(u64::MAX),
808 });
809 }
810 }
811 if let (Some(p), true) = (progress.as_mut(), resolving) {
812 p.finish();
813 }
814
815 if opts.strict {
816 verify_packed_dot_special_entries(&by_oid, sink)?;
817 strict_verify_packed_references_map(
818 sink,
819 &by_oid,
820 &opts.allowed_missing,
821 opts.allow_promisor_missing_references,
822 &opts.shallow_boundaries,
823 promised,
824 )?;
825 }
826
827 Ok(Unpacked {
828 count,
829 by_oid,
830 object_counts,
831 pack_bytes,
832 })
833}
834
835#[derive(Debug, Clone)]
837enum PackedObjectEntry {
838 DryRun { kind: ObjectKind, data: Vec<u8> },
839 Stored { kind: ObjectKind, oid: ObjectId },
840}
841
842fn packed_entry_kind(entry: &PackedObjectEntry) -> ObjectKind {
843 match entry {
844 PackedObjectEntry::DryRun { kind, .. } | PackedObjectEntry::Stored { kind, .. } => *kind,
845 }
846}
847
848#[allow(clippy::too_many_arguments)]
849fn record_resolved_object(
850 by_offset: &mut HashMap<usize, PackedObjectEntry>,
851 by_oid: &mut HashMap<ObjectId, PackedObjectEntry>,
852 pack_offset: usize,
853 oid: ObjectId,
854 kind: ObjectKind,
855 data: Vec<u8>,
856 opts: &UnpackOptions,
857 max_stream_blob: usize,
858 cache: &mut UnpackBaseByteCache,
859) -> Result<()> {
860 let entry =
861 packed_entry_after_write(kind, data, oid, opts, max_stream_blob, pack_offset, cache);
862 by_offset.insert(pack_offset, entry.clone());
863 by_oid.insert(oid, entry);
864 Ok(())
865}
866
867fn resolve_pending_delta<S: UnpackSink + ?Sized>(
868 delta: &PendingDelta,
869 by_offset: &HashMap<usize, PackedObjectEntry>,
870 by_oid: &HashMap<ObjectId, PackedObjectEntry>,
871 sink: &S,
872 opts: &UnpackOptions,
873 max_stream_blob: usize,
874 cache: &mut UnpackBaseByteCache,
875) -> Result<Option<(ObjectId, ObjectKind, Vec<u8>)>> {
876 let Some((base_kind, base_data)) =
877 delta_base_bytes(delta, by_offset, by_oid, sink, opts, cache)?
878 else {
879 return Ok(None);
880 };
881 let result = apply_delta(base_data.as_ref(), &delta.delta_data)?;
882 let oid = sink.write(base_kind, &result, opts.dry_run)?;
883 if !opts.dry_run && (base_kind != ObjectKind::Blob || result.len() <= max_stream_blob) {
884 cache.put(delta.offset, base_kind, result.clone());
885 }
886 Ok(Some((oid, base_kind, result)))
887}
888
889fn delta_base_bytes<'a, S: UnpackSink + ?Sized>(
890 delta: &PendingDelta,
891 by_offset: &'a HashMap<usize, PackedObjectEntry>,
892 by_oid: &'a HashMap<ObjectId, PackedObjectEntry>,
893 sink: &S,
894 opts: &UnpackOptions,
895 cache: &mut UnpackBaseByteCache,
896) -> Result<Option<(ObjectKind, Cow<'a, [u8]>)>> {
897 if let Some(base_off) = delta.base_offset {
898 if let Some((kind, data)) = cache.get(base_off) {
899 return Ok(Some((kind, Cow::Owned(data))));
900 }
901 let Some(entry) = by_offset.get(&base_off) else {
902 return Ok(None);
903 };
904 return entry_object_bytes(entry, sink, Some(base_off), cache).map(Some);
905 }
906 if let Some(ref base_id) = delta.base_oid {
907 if let Some(entry) = by_oid.get(base_id) {
908 return entry_object_bytes(entry, sink, None, cache).map(Some);
909 }
910 if !opts.dry_run {
911 if let Some(obj) = sink.read_stored(base_id)? {
912 return Ok(Some((obj.kind, Cow::Owned(obj.data))));
913 }
914 }
915 }
916 Ok(None)
917}
918
919fn verify_packed_dot_special_entries<S: UnpackSink + ?Sized>(
920 by_oid: &HashMap<ObjectId, PackedObjectEntry>,
921 sink: &S,
922) -> Result<()> {
923 let mut tracker = DotFsckTracker::default();
924 let mut keys: Vec<ObjectId> = by_oid.keys().copied().collect();
925 keys.sort();
926 for oid in keys {
927 let entry = &by_oid[&oid];
928 if matches!(
929 entry,
930 PackedObjectEntry::Stored {
931 kind: ObjectKind::Blob,
932 ..
933 } | PackedObjectEntry::DryRun {
934 kind: ObjectKind::Blob,
935 ..
936 }
937 ) {
938 continue;
939 }
940 let (kind, data) = entry_payload_for_strict(entry, sink, &oid)?;
941 if kind == ObjectKind::Tree {
942 for di in tracker.on_tree(&oid, &data)? {
943 if di.is_error_severity() {
944 return Err(Error::CorruptObject(di.format_line()));
945 }
946 }
947 }
948 for di in tracker.on_object(&oid, kind, &data) {
949 if di.is_error_severity() {
950 return Err(Error::CorruptObject(di.format_line()));
951 }
952 }
953 }
954 let pending_gm: Vec<ObjectId> = tracker
955 .gitmodules_found
956 .difference(&tracker.gitmodules_done)
957 .copied()
958 .collect();
959 let pending_ga: Vec<ObjectId> = tracker
960 .gitattributes_found
961 .difference(&tracker.gitattributes_done)
962 .copied()
963 .collect();
964 for oid in pending_gm {
965 tracker.gitmodules_done.insert(oid);
966 let Some(entry) = by_oid.get(&oid) else {
967 continue;
968 };
969 let (kind, data) = dot_special_blob_bytes(entry, sink, &oid)?;
970 for di in gitmodules::fsck_dot_special_object(
971 &oid,
972 kind,
973 &data,
974 &tracker.gitmodules_found,
975 &tracker.gitattributes_found,
976 ) {
977 if di.is_error_severity() {
978 return Err(Error::CorruptObject(di.format_line()));
979 }
980 }
981 }
982 for oid in pending_ga {
983 if tracker.gitattributes_done.contains(&oid) {
984 continue;
985 }
986 tracker.gitattributes_done.insert(oid);
987 let Some(entry) = by_oid.get(&oid) else {
988 continue;
989 };
990 let (kind, data) = dot_special_blob_bytes(entry, sink, &oid)?;
991 for di in gitmodules::fsck_dot_special_object(
992 &oid,
993 kind,
994 &data,
995 &tracker.gitmodules_found,
996 &tracker.gitattributes_found,
997 ) {
998 if di.is_error_severity() {
999 return Err(Error::CorruptObject(di.format_line()));
1000 }
1001 }
1002 }
1003 Ok(())
1004}
1005
1006fn dot_special_blob_bytes<S: UnpackSink + ?Sized>(
1007 entry: &PackedObjectEntry,
1008 sink: &S,
1009 id: &ObjectId,
1010) -> Result<(ObjectKind, Vec<u8>)> {
1011 entry_payload_for_strict(entry, sink, id)
1012}
1013
1014fn entry_payload_for_strict<S: UnpackSink + ?Sized>(
1015 entry: &PackedObjectEntry,
1016 sink: &S,
1017 id: &ObjectId,
1018) -> Result<(ObjectKind, Vec<u8>)> {
1019 match entry {
1020 PackedObjectEntry::DryRun { kind, data } => Ok((*kind, data.clone())),
1021 PackedObjectEntry::Stored { kind, oid } => {
1022 debug_assert_eq!(oid, id);
1023 let Some(obj) = sink.read_stored(id)? else {
1024 return Err(Error::CorruptObject(format!("missing object {id}")));
1025 };
1026 Ok((*kind, obj.data))
1027 }
1028 }
1029}
1030
1031fn packed_entry_after_write(
1032 kind: ObjectKind,
1033 data: Vec<u8>,
1034 oid: ObjectId,
1035 opts: &UnpackOptions,
1036 max_stream_blob: usize,
1037 _pack_offset: usize,
1038 _cache: &mut UnpackBaseByteCache,
1039) -> PackedObjectEntry {
1040 let _ = max_stream_blob;
1041 if opts.dry_run {
1042 return PackedObjectEntry::DryRun { kind, data };
1043 }
1044 PackedObjectEntry::Stored { kind, oid }
1045}
1046
1047fn entry_object_bytes<'a, S: UnpackSink + ?Sized>(
1048 entry: &'a PackedObjectEntry,
1049 sink: &S,
1050 pack_offset: Option<usize>,
1051 cache: &mut UnpackBaseByteCache,
1052) -> Result<(ObjectKind, Cow<'a, [u8]>)> {
1053 match entry {
1054 PackedObjectEntry::DryRun { kind, data } => Ok((*kind, Cow::Borrowed(data.as_slice()))),
1055 PackedObjectEntry::Stored { kind, oid } => {
1056 if let Some(off) = pack_offset {
1057 if let Some((k, data)) = cache.get(off) {
1058 return Ok((k, Cow::Owned(data)));
1059 }
1060 }
1061 let obj = sink
1062 .read_stored(oid)?
1063 .ok_or_else(|| Error::ObjectNotFound(oid.to_hex()))?;
1064 if let Some(off) = pack_offset {
1065 cache.put(off, *kind, obj.data.clone());
1066 }
1067 Ok((*kind, Cow::Owned(obj.data)))
1068 }
1069 }
1070}
1071
1072fn strict_verify_packed_references_map<S: UnpackSink + ?Sized>(
1073 sink: &S,
1074 pack: &HashMap<ObjectId, PackedObjectEntry>,
1075 allowed_missing: &HashSet<ObjectId>,
1076 allow_promisor_missing_references: bool,
1077 shallow_boundaries: &HashSet<ObjectId>,
1078 promised: Option<PromisedReference<'_>>,
1079) -> Result<()> {
1080 let resolves = |target: &ObjectId, referrer: &ObjectId| -> Result<bool> {
1081 if strict_ref_resolves_map(
1082 target,
1083 pack,
1084 sink,
1085 allowed_missing,
1086 allow_promisor_missing_references,
1087 )? {
1088 return Ok(true);
1089 }
1090 promised.map_or(Ok(false), |promised| promised(target, referrer))
1091 };
1092 for (oid, entry) in pack {
1093 let kind = packed_entry_kind(entry);
1094 if kind == ObjectKind::Blob {
1095 continue;
1096 }
1097 let (_, data) = entry_payload_for_strict(entry, sink, oid)?;
1098 match kind {
1099 ObjectKind::Tree => {
1100 for e in parse_tree(&data)? {
1101 if e.mode == MODE_GITLINK {
1106 continue;
1107 }
1108 if !resolves(&e.oid, oid)? {
1109 return Err(Error::CorruptObject(format!(
1110 "strict: missing object {} referenced by tree",
1111 e.oid.to_hex()
1112 )));
1113 }
1114 }
1115 }
1116 ObjectKind::Commit => {
1117 let c = parse_commit(&data)?;
1118 if !resolves(&c.tree, oid)? {
1119 return Err(Error::CorruptObject(format!(
1120 "strict: missing tree {} referenced by commit",
1121 c.tree.to_hex()
1122 )));
1123 }
1124 if shallow_boundaries.contains(oid) {
1128 continue;
1129 }
1130 for p in &c.parents {
1131 if !resolves(p, oid)? {
1132 return Err(Error::CorruptObject(format!(
1133 "strict: missing parent {} referenced by commit",
1134 p.to_hex()
1135 )));
1136 }
1137 }
1138 }
1139 ObjectKind::Tag => {
1140 let t = parse_tag(&data)?;
1141 if !resolves(&t.object, oid)? {
1142 return Err(Error::CorruptObject(format!(
1143 "strict: missing object {} referenced by tag",
1144 t.object.to_hex()
1145 )));
1146 }
1147 }
1148 ObjectKind::Blob => {}
1149 }
1150 }
1151 Ok(())
1152}
1153
1154fn strict_ref_resolves_map<S: UnpackSink + ?Sized>(
1155 oid: &ObjectId,
1156 pack: &HashMap<ObjectId, PackedObjectEntry>,
1157 sink: &S,
1158 allowed_missing: &HashSet<ObjectId>,
1159 allow_promisor_missing_references: bool,
1160) -> Result<bool> {
1161 if pack.contains_key(oid) || allowed_missing.contains(oid) || allow_promisor_missing_references
1162 {
1163 return Ok(true);
1164 }
1165 sink.contains(oid)
1166}
1167
1168fn strict_ref_resolves(
1169 oid: &ObjectId,
1170 pack: &std::collections::HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1171 odb: Option<&Odb>,
1172) -> Result<bool> {
1173 if pack.contains_key(oid) {
1174 return Ok(true);
1175 }
1176 odb.map_or(Ok(false), |o| o.contains(oid))
1177}
1178
1179pub fn strict_verify_packed_references(
1190 odb: Option<&Odb>,
1191 pack: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1192) -> Result<()> {
1193 for (kind, data) in pack.values() {
1194 match kind {
1195 ObjectKind::Tree => {
1196 for e in parse_tree(data)? {
1197 if e.mode == MODE_GITLINK {
1202 continue;
1203 }
1204 if !strict_ref_resolves(&e.oid, pack, odb)? {
1205 return Err(Error::CorruptObject(format!(
1206 "strict: missing object {} referenced by tree",
1207 e.oid.to_hex()
1208 )));
1209 }
1210 }
1211 }
1212 ObjectKind::Commit => {
1213 let c = parse_commit(data)?;
1214 if !strict_ref_resolves(&c.tree, pack, odb)? {
1215 return Err(Error::CorruptObject(format!(
1216 "strict: missing tree {} referenced by commit",
1217 c.tree.to_hex()
1218 )));
1219 }
1220 for p in &c.parents {
1221 if !strict_ref_resolves(p, pack, odb)? {
1222 return Err(Error::CorruptObject(format!(
1223 "strict: missing parent {} referenced by commit",
1224 p.to_hex()
1225 )));
1226 }
1227 }
1228 }
1229 ObjectKind::Tag => {
1230 let t = parse_tag(data)?;
1231 if !strict_ref_resolves(&t.object, pack, odb)? {
1232 return Err(Error::CorruptObject(format!(
1233 "strict: missing object {} referenced by tag",
1234 t.object.to_hex()
1235 )));
1236 }
1237 }
1238 ObjectKind::Blob => {}
1239 }
1240 }
1241 Ok(())
1242}
1243
1244pub fn pack_is_thin(data: &[u8], algo: HashAlgo) -> bool {
1251 pack_is_thin_inner(data, algo).unwrap_or(false)
1252}
1253
1254fn pack_is_thin_inner(data: &[u8], algo: HashAlgo) -> Result<bool> {
1255 use crate::pack_zlib::skip_zlib_at;
1256
1257 let mut rd = PackReader::new(data);
1258 if rd.read_exact(4)? != b"PACK" {
1259 return Ok(false);
1260 }
1261 let _version = rd.read_u32_be()?;
1262 let nr_objects = rd.read_u32_be()? as usize;
1263
1264 let mut ref_delta_bases: Vec<ObjectId> = Vec::new();
1265 let mut saw_ref_delta = false;
1266
1267 for _ in 0..nr_objects {
1268 let obj_offset = rd.pos;
1269 let (type_code, size) = rd.read_type_size()?;
1270 match type_code {
1271 1..=4 => {
1272 let zlib_offset = rd.pos;
1273 let consumed = skip_zlib_at(data, zlib_offset, size)?;
1274 rd.pos = zlib_offset + consumed;
1275 let _ = obj_offset;
1276 }
1277 6 => {
1278 let neg = rd.read_ofs_neg_offset()?;
1279 let _base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
1280 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
1281 })?;
1282 let zlib_offset = rd.pos;
1283 let consumed = skip_zlib_at(data, zlib_offset, size)?;
1284 rd.pos = zlib_offset + consumed;
1285 }
1286 7 => {
1287 saw_ref_delta = true;
1288 let base_bytes = rd.read_exact(algo.len())?;
1289 let base_oid = ObjectId::from_bytes(base_bytes)?;
1290 ref_delta_bases.push(base_oid);
1291 let zlib_offset = rd.pos;
1292 let consumed = skip_zlib_at(data, zlib_offset, size)?;
1293 rd.pos = zlib_offset + consumed;
1294 }
1295 _ => return Ok(false),
1296 }
1297 }
1298
1299 if !saw_ref_delta {
1300 return Ok(false);
1301 }
1302
1303 pack_is_thin_with_ofs_deltas(data, algo, ref_delta_bases)
1304}
1305
1306fn pack_is_thin_with_ofs_deltas(
1308 data: &[u8],
1309 algo: HashAlgo,
1310 ref_delta_bases: Vec<ObjectId>,
1311) -> Result<bool> {
1312 let tmp = tempfile::tempdir().map_err(Error::Io)?;
1313 let git_dir = tmp.path().join(".git");
1314 std::fs::create_dir_all(git_dir.join("objects")).map_err(Error::Io)?;
1315 if algo == HashAlgo::Sha256 {
1316 std::fs::write(
1317 git_dir.join("config"),
1318 "[extensions]\n\tobjectformat = sha256\n",
1319 )
1320 .map_err(Error::Io)?;
1321 }
1322 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
1323 match crate::pack_index_build::build_pack_index_records(
1324 data,
1325 &odb,
1326 crate::hash::Parallelism::resolve(Some(1)),
1327 false,
1328 ) {
1329 Ok(records) => {
1330 let in_pack: HashSet<ObjectId> = records.into_iter().map(|r| r.oid).collect();
1331 Ok(ref_delta_bases.iter().any(|b| !in_pack.contains(b)))
1332 }
1333 Err(Error::UnresolvedDeltas { .. }) => Ok(true),
1334 Err(e) => Err(e),
1335 }
1336}
1337
1338pub fn pack_bytes_to_object_map(data: &[u8], odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
1350 build_pack_object_map(PackReader::new(data), odb)
1351}
1352
1353#[derive(Debug, Clone)]
1359pub struct PackIndexRecord {
1360 pub oid: ObjectId,
1362 pub offset: u64,
1364 pub crc32: u32,
1366 pub kind: ObjectKind,
1368}
1369
1370pub fn pack_index_records_from_bytes(data: &[u8], odb: &Odb) -> Result<Vec<PackIndexRecord>> {
1380 pack_index_records_with_threads(
1381 data,
1382 odb,
1383 crate::config::ConfigSet::pack_index_parallelism_for_git_dir(
1384 &crate::environment::Environment::empty(),
1385 odb.config_git_dir(),
1386 ),
1387 )
1388}
1389
1390pub fn pack_index_records_with_threads(
1392 data: &[u8],
1393 odb: &Odb,
1394 parallelism: crate::hash::Parallelism,
1395) -> Result<Vec<PackIndexRecord>> {
1396 crate::pack_index_build::build_pack_index_records(data, odb, parallelism, true)
1397}
1398
1399pub fn pack_index_records_with_threads_with_progress(
1402 data: &[u8],
1403 odb: &Odb,
1404 parallelism: crate::hash::Parallelism,
1405 progress: Option<&mut dyn crate::progress::ProgressSink>,
1406) -> Result<Vec<PackIndexRecord>> {
1407 crate::pack_index_build::build_pack_index_records_with_progress(
1408 data,
1409 odb,
1410 parallelism,
1411 true,
1412 progress,
1413 None,
1414 )
1415}
1416
1417fn build_pack_object_map(mut rd: PackReader<'_>, odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
1418 let algo = odb.hash_algo();
1419 let sig = rd.read_exact(4)?;
1420 if sig != b"PACK" {
1421 return Err(Error::CorruptObject(
1422 "not a pack stream: invalid signature".to_owned(),
1423 ));
1424 }
1425 let version = rd.read_u32_be()?;
1426 if version != 2 && version != 3 {
1427 return Err(Error::CorruptObject(format!(
1428 "unsupported pack version {version}"
1429 )));
1430 }
1431 let nr_objects = rd.read_u32_be()? as usize;
1432
1433 let mut by_offset: HashMap<usize, (ObjectKind, Vec<u8>)> = HashMap::new();
1434 let mut by_oid: HashMap<ObjectId, (ObjectKind, Vec<u8>)> = HashMap::new();
1435 let mut pending: Vec<PendingDelta> = Vec::new();
1436
1437 fn base_from_pack_or_odb(
1438 by_oid: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
1439 odb: &Odb,
1440 id: &ObjectId,
1441 ) -> Option<(ObjectKind, Vec<u8>)> {
1442 if let Some(e) = by_oid.get(id) {
1443 return Some(e.clone());
1444 }
1445 odb.read(id).ok().map(|o| (o.kind, o.data))
1446 }
1447
1448 for _ in 0..nr_objects {
1449 let obj_offset = rd.pos;
1450 let (type_code, size) = rd.read_type_size()?;
1451
1452 match type_code {
1453 1..=4 => {
1454 let kind = type_code_to_kind(type_code)?;
1455 let data = rd.decompress(size)?;
1456 let oid = odb.hash(kind, &data)?;
1457 by_offset.insert(obj_offset, (kind, data.clone()));
1458 by_oid.insert(oid, (kind, data));
1459 }
1460 6 => {
1461 let neg = rd.read_ofs_neg_offset()?;
1462 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
1463 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
1464 })?;
1465 let delta_data = rd.decompress(size)?;
1466 pending.push(PendingDelta {
1467 offset: obj_offset,
1468 base_oid: None,
1469 base_offset: Some(base_offset),
1470 delta_data,
1471 });
1472 }
1473 7 => {
1474 let base_bytes = rd.read_exact(algo.len())?;
1475 let base_oid = ObjectId::from_bytes(base_bytes)?;
1476 let delta_data = rd.decompress(size)?;
1477 pending.push(PendingDelta {
1478 offset: obj_offset,
1479 base_oid: Some(base_oid),
1480 base_offset: None,
1481 delta_data,
1482 });
1483 }
1484 other => {
1485 return Err(Error::CorruptObject(format!(
1486 "unknown packed-object type {other}"
1487 )))
1488 }
1489 }
1490 }
1491
1492 let consumed = rd.pos;
1493 {
1494 let mut hasher = ChecksumHasher::new(algo);
1495 hasher.update(&rd.data[..consumed]);
1496 let digest = hasher.finalize();
1497 let trailing = rd.read_exact(algo.len())?;
1498 if digest.as_slice() != trailing {
1499 return Err(Error::CorruptObject(
1500 "pack trailing checksum mismatch".to_owned(),
1501 ));
1502 }
1503 }
1504
1505 let mut remaining = pending;
1506 loop {
1507 if remaining.is_empty() {
1508 break;
1509 }
1510 let before = remaining.len();
1511 let mut still_pending: Vec<PendingDelta> = Vec::new();
1512
1513 for delta in remaining {
1514 let base = if let Some(base_off) = delta.base_offset {
1515 by_offset.get(&base_off).cloned()
1516 } else if let Some(ref base_id) = delta.base_oid {
1517 base_from_pack_or_odb(&by_oid, odb, base_id)
1518 } else {
1519 None
1520 };
1521
1522 if let Some((base_kind, base_data)) = base {
1523 let result = apply_delta(&base_data, &delta.delta_data)?;
1524 let oid = odb.hash(base_kind, &result)?;
1525 by_offset.insert(delta.offset, (base_kind, result.clone()));
1526 by_oid.insert(oid, (base_kind, result));
1527 } else {
1528 still_pending.push(delta);
1529 }
1530 }
1531
1532 remaining = still_pending;
1533 if remaining.len() == before {
1534 return Err(Error::UnresolvedDeltas {
1535 count: u64::try_from(remaining.len()).unwrap_or(u64::MAX),
1536 });
1537 }
1538 }
1539
1540 Ok(by_oid
1541 .into_iter()
1542 .map(|(oid, (kind, data))| (oid, Object::new(kind, data)))
1543 .collect())
1544}
1545
1546pub(crate) fn type_code_to_kind(code: u8) -> Result<ObjectKind> {
1548 match code {
1549 1 => Ok(ObjectKind::Commit),
1550 2 => Ok(ObjectKind::Tree),
1551 3 => Ok(ObjectKind::Blob),
1552 4 => Ok(ObjectKind::Tag),
1553 _ => Err(Error::CorruptObject(format!(
1554 "type code {code} is not a regular object type"
1555 ))),
1556 }
1557}
1558
1559pub(crate) struct PackReader<'a> {
1561 data: &'a [u8],
1562 pub(crate) pos: usize,
1563}
1564
1565impl<'a> PackReader<'a> {
1566 pub(crate) fn new(data: &'a [u8]) -> Self {
1567 Self { data, pos: 0 }
1568 }
1569
1570 pub(crate) fn read_exact(&mut self, n: usize) -> Result<&[u8]> {
1573 if self.pos + n > self.data.len() {
1574 return Err(Error::CorruptObject(format!(
1575 "pack stream truncated: need {n} bytes at offset {}",
1576 self.pos
1577 )));
1578 }
1579 let slice = &self.data[self.pos..self.pos + n];
1580 self.pos += n;
1581 Ok(slice)
1582 }
1583
1584 pub(crate) fn read_byte(&mut self) -> Result<u8> {
1586 if self.pos >= self.data.len() {
1587 return Err(Error::CorruptObject(
1588 "unexpected end of pack stream".to_owned(),
1589 ));
1590 }
1591 let b = self.data[self.pos];
1592 self.pos += 1;
1593 Ok(b)
1594 }
1595
1596 pub(crate) fn read_u32_be(&mut self) -> Result<u32> {
1598 let bytes = self.read_exact(4)?;
1599 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
1600 Error::CorruptObject("u32 read failed".to_owned())
1601 })?))
1602 }
1603
1604 pub(crate) fn read_type_size(&mut self) -> Result<(u8, usize)> {
1609 let c = self.read_byte()?;
1610 let type_code = (c >> 4) & 0x7;
1611 let mut size = (c & 0x0f) as usize;
1612 let mut shift = 4u32;
1613 let mut cur = c;
1614 while cur & 0x80 != 0 {
1615 cur = self.read_byte()?;
1616 size |= ((cur & 0x7f) as usize) << shift;
1617 shift += 7;
1618 }
1619 Ok((type_code, size))
1620 }
1621
1622 pub(crate) fn read_ofs_neg_offset(&mut self) -> Result<usize> {
1627 let mut c = self.read_byte()?;
1628 let mut value = (c & 0x7f) as usize;
1629 while c & 0x80 != 0 {
1630 c = self.read_byte()?;
1631 value = (value + 1) << 7 | (c & 0x7f) as usize;
1632 }
1633 Ok(value)
1634 }
1635
1636 pub(crate) fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
1641 let (out, consumed) = decompress_zlib_at(self.data, self.pos, expected_size)?;
1642 self.pos += consumed;
1643 Ok(out)
1644 }
1645}
1646
1647pub(crate) fn decompress_zlib_at_into(
1649 pack: &[u8],
1650 offset: usize,
1651 expected_size: usize,
1652 out: &mut Vec<u8>,
1653) -> Result<usize> {
1654 out.clear();
1655 out.reserve(expected_size);
1656 let slice = pack.get(offset..).ok_or_else(|| {
1657 Error::CorruptObject(format!(
1658 "pack stream truncated: need zlib at offset {offset}"
1659 ))
1660 })?;
1661 let mut decoder = ZlibDecoder::new(slice);
1662 decoder
1663 .read_to_end(out)
1664 .map_err(|e| Error::Zlib(e.to_string()))?;
1665 if out.len() != expected_size {
1666 return Err(Error::CorruptObject(format!(
1667 "decompressed {} bytes but expected {}",
1668 out.len(),
1669 expected_size
1670 )));
1671 }
1672 Ok(decoder.total_in() as usize)
1673}
1674
1675pub(crate) fn decompress_zlib_at(
1677 pack: &[u8],
1678 offset: usize,
1679 expected_size: usize,
1680) -> Result<(Vec<u8>, usize)> {
1681 let mut out = Vec::new();
1682 let consumed = decompress_zlib_at_into(pack, offset, expected_size, &mut out)?;
1683 Ok((out, consumed))
1684}
1685
1686fn io_to_corrupt_eof(e: io::Error, stream_pos: usize, context: &str) -> Error {
1687 if e.kind() == io::ErrorKind::UnexpectedEof {
1688 Error::CorruptObject(format!(
1689 "pack stream truncated ({context}) at offset {stream_pos}"
1690 ))
1691 } else {
1692 Error::Io(e)
1693 }
1694}
1695
1696struct StreamingPackReader<'a> {
1702 inner: &'a mut dyn Read,
1703 pack_hasher: ChecksumHasher,
1704 stream_pos: usize,
1705 max_input_bytes: Option<u64>,
1706 pending: Vec<u8>,
1709}
1710
1711impl<'a> StreamingPackReader<'a> {
1712 fn new(inner: &'a mut dyn Read, max_input_bytes: Option<u64>, algo: HashAlgo) -> Self {
1713 Self {
1714 inner,
1715 pack_hasher: ChecksumHasher::new(algo),
1716 stream_pos: 0,
1717 max_input_bytes,
1718 pending: Vec::new(),
1719 }
1720 }
1721
1722 fn stream_pos(&self) -> usize {
1723 self.stream_pos
1724 }
1725
1726 fn enforce_max_input(&self) -> Result<()> {
1727 if let Some(limit) = self.max_input_bytes {
1728 let pos = u64::try_from(self.stream_pos)
1729 .map_err(|_| Error::CorruptObject("pack stream position overflow".to_owned()))?;
1730 if pos > limit {
1731 return Err(Error::CorruptObject(
1732 "pack exceeds maximum allowed size".to_owned(),
1733 ));
1734 }
1735 }
1736 Ok(())
1737 }
1738
1739 fn read_from_source(&mut self, buf: &mut [u8]) -> Result<usize> {
1741 let n = if !self.pending.is_empty() {
1742 let take = buf.len().min(self.pending.len());
1743 buf[..take].copy_from_slice(&self.pending[..take]);
1744 self.pending.drain(..take);
1745 take
1746 } else {
1747 self.inner.read(buf).map_err(Error::Io)?
1748 };
1749 if n > 0 {
1750 self.pack_hasher.update(&buf[..n]);
1751 self.stream_pos += n;
1752 self.enforce_max_input()?;
1753 }
1754 Ok(n)
1755 }
1756
1757 fn read_byte(&mut self) -> Result<u8> {
1758 let mut b = [0u8; 1];
1759 let n = self.read_from_source(&mut b)?;
1760 if n == 0 {
1761 return Err(Error::CorruptObject(format!(
1762 "pack stream truncated (read byte) at offset {}",
1763 self.stream_pos
1764 )));
1765 }
1766 Ok(b[0])
1767 }
1768
1769 fn read_exact_n(&mut self, n: usize) -> Result<Vec<u8>> {
1770 let mut v = vec![0u8; n];
1771 let mut got = 0usize;
1772 while got < n {
1773 let m = self.read_from_source(&mut v[got..n])?;
1774 if m == 0 {
1775 return Err(Error::CorruptObject(format!(
1776 "pack stream truncated (read exact) at offset {}",
1777 self.stream_pos
1778 )));
1779 }
1780 got += m;
1781 }
1782 Ok(v)
1783 }
1784
1785 fn read_u32_be(&mut self) -> Result<u32> {
1786 let mut b = [0u8; 4];
1787 let mut got = 0usize;
1788 while got < 4 {
1789 let m = self.read_from_source(&mut b[got..4])?;
1790 if m == 0 {
1791 return Err(Error::CorruptObject(format!(
1792 "pack stream truncated (read u32) at offset {}",
1793 self.stream_pos
1794 )));
1795 }
1796 got += m;
1797 }
1798 Ok(u32::from_be_bytes(b))
1799 }
1800
1801 fn read_type_size(&mut self) -> Result<(u8, usize)> {
1802 let c = self.read_byte()?;
1803 let type_code = (c >> 4) & 0x7;
1804 let mut size = (c & 0x0f) as usize;
1805 let mut shift = 4u32;
1806 let mut cur = c;
1807 while cur & 0x80 != 0 {
1808 cur = self.read_byte()?;
1809 size |= ((cur & 0x7f) as usize) << shift;
1810 shift += 7;
1811 }
1812 Ok((type_code, size))
1813 }
1814
1815 fn read_ofs_neg_offset(&mut self) -> Result<usize> {
1816 let mut c = self.read_byte()?;
1817 let mut value = (c & 0x7f) as usize;
1818 while c & 0x80 != 0 {
1819 c = self.read_byte()?;
1820 value = (value + 1) << 7 | (c & 0x7f) as usize;
1821 }
1822 Ok(value)
1823 }
1824
1825 pub(crate) fn decompress_into(
1836 &mut self,
1837 expected_size: usize,
1838 mut chunk_sink: impl FnMut(&[u8]) -> Result<()>,
1839 ) -> Result<()> {
1840 if expected_size == 0 {
1841 const CHUNK: usize = 64 * 1024;
1842 let mut scratch = [0u8; CHUNK];
1843 loop {
1844 let mut cursor = std::io::Cursor::new(self.pending.as_slice());
1845 let mut z = ZlibDecoder::new(&mut cursor);
1846 let mut sink = [0u8; 1];
1847 match z.read(&mut sink) {
1848 Ok(0) => {
1849 let consumed = z.total_in() as usize;
1850 if consumed > self.pending.len() {
1851 return Err(Error::CorruptObject(
1852 "zlib total_in exceeds pending buffer".to_owned(),
1853 ));
1854 }
1855 if consumed == 0 {
1856 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1857 if n == 0 {
1858 return Err(Error::CorruptObject(format!(
1859 "pack stream truncated (zlib) at offset {}",
1860 self.stream_pos
1861 )));
1862 }
1863 self.pending.extend_from_slice(&scratch[..n]);
1864 continue;
1865 }
1866 self.pack_hasher.update(&self.pending[..consumed]);
1867 self.stream_pos += consumed;
1868 self.pending.drain(..consumed);
1869 self.enforce_max_input()?;
1870 return Ok(());
1871 }
1872 Ok(_) => {
1873 return Err(Error::CorruptObject(
1874 "0-byte packed object inflated to non-empty output".to_owned(),
1875 ));
1876 }
1877 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1878 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1879 if n == 0 {
1880 return Err(Error::CorruptObject(format!(
1881 "pack stream truncated (zlib) at offset {}",
1882 self.stream_pos
1883 )));
1884 }
1885 self.pending.extend_from_slice(&scratch[..n]);
1886 }
1887 Err(e) => return Err(Error::Zlib(e.to_string())),
1888 }
1889 }
1890 }
1891
1892 const CHUNK: usize = 64 * 1024;
1893 let mut scratch = [0u8; CHUNK];
1894 let mut scratch_out = [0u8; CHUNK];
1895
1896 let mut z = Decompress::new(true);
1897 let mut produced = 0usize;
1898 let mut eof = false;
1899 loop {
1900 if self.pending.is_empty() && !eof {
1901 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1902 if n == 0 {
1903 eof = true;
1904 } else {
1905 self.pending.extend_from_slice(&scratch[..n]);
1906 }
1907 }
1908
1909 let flush = if eof && self.pending.is_empty() {
1910 FlushDecompress::Finish
1911 } else {
1912 FlushDecompress::None
1913 };
1914
1915 let before_in = z.total_in();
1916 let before_out = z.total_out();
1917 let status = z
1918 .decompress(self.pending.as_slice(), &mut scratch_out, flush)
1919 .map_err(|e| Error::Zlib(e.to_string()))?;
1920 let consumed = (z.total_in() - before_in) as usize;
1921 if consumed > self.pending.len() {
1922 return Err(Error::CorruptObject(
1923 "zlib consumed more than pending buffer".to_owned(),
1924 ));
1925 }
1926 self.pack_hasher.update(&self.pending[..consumed]);
1927 self.stream_pos += consumed;
1928 self.pending.drain(..consumed);
1929 self.enforce_max_input()?;
1930 let chunk_len = (z.total_out() - before_out) as usize;
1931 if chunk_len > 0 {
1932 let chunk = &scratch_out[..chunk_len];
1933 let take = chunk.len().min(expected_size - produced);
1934 chunk_sink(&chunk[..take])?;
1935 produced += take;
1936 if take < chunk.len() {
1937 return Err(Error::CorruptObject(format!(
1938 "decompressed size mismatch: got {}, want {expected_size}",
1939 produced + (chunk.len() - take)
1940 )));
1941 }
1942 }
1943
1944 match status {
1945 Status::StreamEnd => {
1946 if produced != expected_size {
1947 return Err(Error::CorruptObject(format!(
1948 "decompressed size mismatch: got {produced}, want {expected_size}"
1949 )));
1950 }
1951 return Ok(());
1952 }
1953 Status::Ok | Status::BufError => {
1954 if consumed == 0 && !eof {
1955 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1956 if n == 0 {
1957 eof = true;
1958 } else {
1959 self.pending.extend_from_slice(&scratch[..n]);
1960 }
1961 } else if eof && self.pending.is_empty() && consumed == 0 {
1962 return Err(Error::CorruptObject(format!(
1963 "pack stream truncated (zlib) at offset {}",
1964 self.stream_pos
1965 )));
1966 }
1967 }
1968 }
1969 }
1970 }
1971
1972 fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
1973 let mut out = vec![0u8; expected_size];
1974 let mut pos = 0usize;
1975 self.decompress_into(expected_size, |chunk| {
1976 out[pos..pos + chunk.len()].copy_from_slice(chunk);
1977 pos += chunk.len();
1978 Ok(())
1979 })?;
1980 Ok(out)
1981 }
1982
1983 fn finalize_hasher(&self) -> Vec<u8> {
1985 self.pack_hasher.clone().finalize()
1986 }
1987
1988 fn read_trailer(&mut self) -> Result<Vec<u8>> {
1990 let hash_len = self.pack_hasher.algo().len();
1991 let mut b = vec![0u8; hash_len];
1992 if self.pending.len() >= hash_len {
1993 b.copy_from_slice(&self.pending[..hash_len]);
1994 self.pending.drain(..hash_len);
1995 self.stream_pos += hash_len;
1996 self.enforce_max_input()?;
1997 return Ok(b);
1998 }
1999 let tail = self.pending.len();
2000 if tail > 0 {
2001 b[..tail].copy_from_slice(&self.pending[..]);
2002 self.pending.clear();
2003 }
2004 self.inner
2005 .read_exact(&mut b[tail..])
2006 .map_err(|e| io_to_corrupt_eof(e, self.stream_pos, "trailer"))?;
2007 self.stream_pos += hash_len;
2008 self.enforce_max_input()?;
2009 Ok(b)
2010 }
2011}
2012
2013pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
2027 let mut result = Vec::new();
2028 apply_delta_into(&mut result, base, delta)?;
2029 Ok(result)
2030}
2031
2032pub fn apply_delta_into(out: &mut Vec<u8>, base: &[u8], delta: &[u8]) -> Result<()> {
2036 let mut pos = 0usize;
2037
2038 let src_size = read_delta_varint(delta, &mut pos)?;
2039 if src_size != base.len() {
2040 return Err(Error::CorruptObject(format!(
2041 "delta source size {src_size} != base size {}",
2042 base.len()
2043 )));
2044 }
2045 let dest_size = read_delta_varint(delta, &mut pos)?;
2046 out.clear();
2047 if out.capacity() < dest_size {
2048 out.reserve(dest_size - out.capacity());
2049 }
2050
2051 while pos < delta.len() {
2052 let cmd = delta[pos];
2053 pos += 1;
2054 if cmd == 0 {
2055 return Err(Error::CorruptObject(
2056 "reserved opcode 0 in delta stream".to_owned(),
2057 ));
2058 }
2059 if cmd & 0x80 != 0 {
2060 let mut offset = 0usize;
2063 let mut size = 0usize;
2064
2065 macro_rules! maybe_read_byte {
2066 ($flag:expr, $shift:expr, $target:expr) => {
2067 if cmd & $flag != 0 {
2068 let b = *delta.get(pos).ok_or_else(|| {
2069 Error::CorruptObject("truncated delta COPY operand".to_owned())
2070 })?;
2071 pos += 1;
2072 $target |= (b as usize) << $shift;
2073 }
2074 };
2075 }
2076
2077 maybe_read_byte!(0x01, 0, offset);
2078 maybe_read_byte!(0x02, 8, offset);
2079 maybe_read_byte!(0x04, 16, offset);
2080 maybe_read_byte!(0x08, 24, offset);
2081 maybe_read_byte!(0x10, 0, size);
2082 maybe_read_byte!(0x20, 8, size);
2083 maybe_read_byte!(0x40, 16, size);
2084
2085 if size == 0 {
2086 size = 0x10000;
2087 }
2088
2089 let end = offset.checked_add(size).ok_or_else(|| {
2090 Error::CorruptObject("delta COPY range overflows usize".to_owned())
2091 })?;
2092 let chunk = base.get(offset..end).ok_or_else(|| {
2093 Error::CorruptObject(format!(
2094 "delta COPY [{offset},{end}) out of range (base is {} bytes)",
2095 base.len()
2096 ))
2097 })?;
2098 out.extend_from_slice(chunk);
2099 } else {
2100 let n = cmd as usize;
2102 let chunk = delta
2103 .get(pos..pos + n)
2104 .ok_or_else(|| Error::CorruptObject("truncated delta INSERT data".to_owned()))?;
2105 out.extend_from_slice(chunk);
2106 pos += n;
2107 }
2108 }
2109
2110 if out.len() != dest_size {
2111 return Err(Error::CorruptObject(format!(
2112 "delta produced {} bytes but expected {dest_size}",
2113 out.len()
2114 )));
2115 }
2116
2117 Ok(())
2118}
2119
2120pub(crate) fn apply_delta_prefix_into(
2130 out: &mut Vec<u8>,
2131 base: &[u8],
2132 delta: &[u8],
2133 max_out: usize,
2134 prefix_sufficient: impl Fn(&[u8]) -> bool,
2135) -> Result<()> {
2136 let mut pos = 0usize;
2137
2138 let src_size = read_delta_varint(delta, &mut pos)?;
2139 if src_size != base.len() {
2140 return Err(Error::CorruptObject(format!(
2141 "delta source size {src_size} != base size {}",
2142 base.len()
2143 )));
2144 }
2145 let dest_size = read_delta_varint(delta, &mut pos)?;
2146 out.clear();
2147 if out.capacity() < max_out.min(dest_size) {
2148 out.reserve(max_out.min(dest_size) - out.capacity());
2149 }
2150
2151 let mut result_len = 0usize;
2152 let mut done = false;
2153
2154 while pos < delta.len() {
2155 let cmd = delta[pos];
2156 pos += 1;
2157 if cmd == 0 {
2158 return Err(Error::CorruptObject(
2159 "reserved opcode 0 in delta stream".to_owned(),
2160 ));
2161 }
2162 if cmd & 0x80 != 0 {
2163 let mut offset = 0usize;
2164 let mut size = 0usize;
2165
2166 macro_rules! maybe_read_byte {
2167 ($flag:expr, $shift:expr, $target:expr) => {
2168 if cmd & $flag != 0 {
2169 let b = *delta.get(pos).ok_or_else(|| {
2170 Error::CorruptObject("truncated delta COPY operand".to_owned())
2171 })?;
2172 pos += 1;
2173 $target |= (b as usize) << $shift;
2174 }
2175 };
2176 }
2177
2178 maybe_read_byte!(0x01, 0, offset);
2179 maybe_read_byte!(0x02, 8, offset);
2180 maybe_read_byte!(0x04, 16, offset);
2181 maybe_read_byte!(0x08, 24, offset);
2182 maybe_read_byte!(0x10, 0, size);
2183 maybe_read_byte!(0x20, 8, size);
2184 maybe_read_byte!(0x40, 16, size);
2185
2186 if size == 0 {
2187 size = 0x10000;
2188 }
2189
2190 let end = offset.checked_add(size).ok_or_else(|| {
2191 Error::CorruptObject("delta COPY range overflows usize".to_owned())
2192 })?;
2193 let chunk = base.get(offset..end).ok_or_else(|| {
2194 Error::CorruptObject(format!(
2195 "delta COPY [{offset},{end}) out of range (base is {} bytes)",
2196 base.len()
2197 ))
2198 })?;
2199 result_len = result_len
2200 .checked_add(size)
2201 .ok_or_else(|| Error::CorruptObject("delta result length overflow".to_owned()))?;
2202 if !done {
2203 let room = max_out.saturating_sub(out.len());
2204 if room == 0 {
2205 if prefix_sufficient(out) {
2206 done = true;
2207 }
2208 } else if chunk.len() <= room {
2209 out.extend_from_slice(chunk);
2210 if out.len() >= max_out && prefix_sufficient(out) {
2211 done = true;
2212 out.truncate(max_out);
2213 }
2214 } else {
2215 out.extend_from_slice(&chunk[..room]);
2216 if prefix_sufficient(out) {
2217 done = true;
2218 }
2219 }
2220 }
2221 } else {
2222 let n = cmd as usize;
2223 let chunk = delta
2224 .get(pos..pos + n)
2225 .ok_or_else(|| Error::CorruptObject("truncated delta INSERT data".to_owned()))?;
2226 pos += n;
2227 result_len = result_len
2228 .checked_add(n)
2229 .ok_or_else(|| Error::CorruptObject("delta result length overflow".to_owned()))?;
2230 if !done {
2231 let room = max_out.saturating_sub(out.len());
2232 if room == 0 {
2233 if prefix_sufficient(out) {
2234 done = true;
2235 }
2236 } else if chunk.len() <= room {
2237 out.extend_from_slice(chunk);
2238 if out.len() >= max_out && prefix_sufficient(out) {
2239 done = true;
2240 out.truncate(max_out);
2241 }
2242 } else {
2243 out.extend_from_slice(&chunk[..room]);
2244 if prefix_sufficient(out) {
2245 done = true;
2246 }
2247 }
2248 }
2249 }
2250 }
2251
2252 if result_len != dest_size {
2253 return Err(Error::CorruptObject(format!(
2254 "delta produced {result_len} bytes but expected {dest_size}"
2255 )));
2256 }
2257
2258 Ok(())
2259}
2260
2261pub(crate) fn delta_uncompressed_result_size_if_complete(data: &[u8]) -> Result<Option<usize>> {
2267 let mut pos = 0;
2268 let Some(_src_size) = read_delta_varint_if_complete(data, &mut pos)? else {
2269 return Ok(None);
2270 };
2271 read_delta_varint_if_complete(data, &mut pos)
2272}
2273
2274fn read_delta_varint_if_complete(data: &[u8], pos: &mut usize) -> Result<Option<usize>> {
2275 let start = *pos;
2276 let mut value = 0usize;
2277 let mut shift = 0u32;
2278 while *pos < data.len() {
2279 let b = data[*pos];
2280 *pos += 1;
2281 value |= ((b & 0x7f) as usize) << shift;
2282 shift += 7;
2283 if b & 0x80 == 0 {
2284 return Ok(Some(value));
2285 }
2286 if shift > 35 {
2287 return Err(Error::CorruptObject(
2288 "delta varint exceeds supported width".to_owned(),
2289 ));
2290 }
2291 }
2292 *pos = start;
2293 Ok(None)
2294}
2295
2296fn read_delta_varint(data: &[u8], pos: &mut usize) -> Result<usize> {
2300 let mut value = 0usize;
2301 let mut shift = 0u32;
2302 loop {
2303 let b = *data
2304 .get(*pos)
2305 .ok_or_else(|| Error::CorruptObject("truncated delta varint".to_owned()))?;
2306 *pos += 1;
2307 value |= ((b & 0x7f) as usize) << shift;
2308 shift += 7;
2309 if b & 0x80 == 0 {
2310 break;
2311 }
2312 }
2313 Ok(value)
2314}
2315
2316#[cfg(test)]
2317mod tests {
2318 #![allow(clippy::vec_init_then_push)]
2319 use super::*;
2320
2321 fn make_pack(objects: &[(ObjectKind, &[u8])]) -> Vec<u8> {
2324 use flate2::write::ZlibEncoder;
2325 use std::io::Write;
2326
2327 let mut entries: Vec<Vec<u8>> = Vec::new();
2328 for (kind, data) in objects {
2329 let type_code: u8 = match kind {
2330 ObjectKind::Commit => 1,
2331 ObjectKind::Tree => 2,
2332 ObjectKind::Blob => 3,
2333 ObjectKind::Tag => 4,
2334 };
2335 let mut header = Vec::new();
2337 let mut size = data.len();
2338 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
2339 size >>= 4;
2340 if size > 0 {
2341 header.push(first | 0x80);
2342 while size > 0 {
2343 let b = (size & 0x7f) as u8;
2344 size >>= 7;
2345 header.push(if size > 0 { b | 0x80 } else { b });
2346 }
2347 } else {
2348 header.push(first);
2349 }
2350 let mut enc = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
2352 enc.write_all(data).unwrap();
2353 let compressed = enc.finish().unwrap();
2354 let mut entry = header;
2355 entry.extend_from_slice(&compressed);
2356 entries.push(entry);
2357 }
2358
2359 let mut pack = Vec::new();
2361 pack.extend_from_slice(b"PACK");
2362 pack.extend_from_slice(&2u32.to_be_bytes());
2363 pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
2364 for entry in &entries {
2365 pack.extend_from_slice(entry);
2366 }
2367 pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
2368 pack
2369 }
2370
2371 #[test]
2372 fn test_apply_delta_simple() {
2373 let base = b"hello";
2375 let mut delta = Vec::new();
2376 delta.push(5u8);
2378 delta.push(11u8);
2380 delta.push(0x80 | 0x01 | 0x10); delta.push(0u8); delta.push(5u8); delta.push(6u8);
2387 delta.extend_from_slice(b" world");
2388
2389 let result = apply_delta(base, &delta).unwrap();
2390 assert_eq!(result, b"hello world");
2391 }
2392
2393 #[test]
2394 fn test_apply_delta_prefix_into_stops_after_prefix() {
2395 let base = b"author A <a@b.com> 1 +0000\n\nbody";
2396 let mut delta = Vec::new();
2397 delta.push(base.len() as u8);
2398 delta.push(base.len() as u8);
2399 delta.push(0x80 | 0x01 | 0x10);
2400 delta.push(0u8);
2401 delta.push(base.len() as u8);
2402
2403 let mut out = Vec::new();
2404 apply_delta_prefix_into(&mut out, base, &delta, 32, |buf| {
2405 buf.windows(7).any(|w| w == b"author ")
2406 })
2407 .unwrap();
2408 assert!(out.len() <= 32);
2409 assert!(out.starts_with(b"author A"));
2410 assert_eq!(apply_delta(base, &delta).unwrap(), base);
2411 }
2412
2413 #[test]
2414 fn test_apply_delta_insert_only() {
2415 let base = b"";
2416 let mut delta = Vec::new();
2417 delta.push(0u8); delta.push(5u8); delta.push(5u8); delta.extend_from_slice(b"hello");
2421
2422 let result = apply_delta(base, &delta).unwrap();
2423 assert_eq!(result, b"hello");
2424 }
2425
2426 #[test]
2427 fn test_apply_delta_copy_only() {
2428 let base = b"abcdef";
2429 let mut delta = Vec::new();
2430 delta.push(6u8); delta.push(3u8); delta.push(0x91u8);
2435 delta.push(2u8); delta.push(3u8); let result = apply_delta(base, &delta).unwrap();
2439 assert_eq!(result, b"cde");
2440 }
2441
2442 #[test]
2443 fn test_apply_delta_size_zero_means_65536() {
2444 let base = vec![0xABu8; 65536];
2446 let mut delta = Vec::new();
2447 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);
2453 delta.push(0x80 | ((65536 >> 7) & 0x7f) as u8);
2454 delta.push(((65536 >> 14) & 0x7f) as u8);
2455 delta.push(0x80u8);
2458
2459 let result = apply_delta(&base, &delta).unwrap();
2460 assert_eq!(result.len(), 65536);
2461 assert!(result.iter().all(|&b| b == 0xAB));
2462 }
2463
2464 #[cfg_attr(
2465 not(feature = "full-tests"),
2466 ignore = "starts system git or is heavy; run with --features full-tests"
2467 )]
2468 #[test]
2469 fn test_unpack_objects_blobs() {
2470 use tempfile::TempDir;
2471 let tmp = TempDir::new().unwrap();
2472 let objects_dir = tmp.path().join("objects");
2473 std::fs::create_dir_all(&objects_dir).unwrap();
2474 let odb = Odb::new(&objects_dir);
2475
2476 let pack = make_pack(&[
2477 (ObjectKind::Blob, b"hello\n"),
2478 (ObjectKind::Blob, b"world\n"),
2479 ]);
2480
2481 let opts = UnpackOptions::default();
2482 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
2483 assert_eq!(count, 2);
2484
2485 let oid1 = HashAlgo::Sha1
2487 .hash_object(ObjectKind::Blob, b"hello\n")
2488 .expect("hash");
2489 let oid2 = HashAlgo::Sha1
2490 .hash_object(ObjectKind::Blob, b"world\n")
2491 .expect("hash");
2492 let obj1 = odb.read(&oid1).unwrap();
2493 let obj2 = odb.read(&oid2).unwrap();
2494 assert_eq!(obj1.data, b"hello\n");
2495 assert_eq!(obj2.data, b"world\n");
2496 }
2497
2498 #[cfg_attr(
2499 not(feature = "full-tests"),
2500 ignore = "starts system git or is heavy; run with --features full-tests"
2501 )]
2502 #[test]
2503 fn test_unpack_objects_empty_tree() {
2504 use tempfile::TempDir;
2505 let tmp = TempDir::new().unwrap();
2506 let objects_dir = tmp.path().join("objects");
2507 std::fs::create_dir_all(&objects_dir).unwrap();
2508 let odb = Odb::new(&objects_dir);
2509
2510 let pack = make_pack(&[(ObjectKind::Tree, b"")]);
2511 let opts = UnpackOptions::default();
2512 assert_eq!(
2513 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2514 1
2515 );
2516 let oid = HashAlgo::Sha1
2517 .hash_object(ObjectKind::Tree, b"")
2518 .expect("hash");
2519 assert!(odb.exists(&oid));
2520 let loose = objects_dir
2521 .join(oid.loose_prefix())
2522 .join(oid.loose_suffix());
2523 assert!(
2524 loose.is_file(),
2525 "empty tree must be materialized as a loose object during unpack"
2526 );
2527 }
2528
2529 #[cfg_attr(
2530 not(feature = "full-tests"),
2531 ignore = "starts system git or is heavy; run with --features full-tests"
2532 )]
2533 #[test]
2534 fn test_strict_skips_gitlink_tree_entries() {
2535 use crate::index::{MODE_GITLINK, MODE_REGULAR};
2536 use crate::objects::{serialize_tree, TreeEntry};
2537
2538 let submodule_oid = ObjectId::from_hex(&"7f".repeat(20)).unwrap();
2541
2542 let tree_data = serialize_tree(&[TreeEntry {
2544 mode: MODE_GITLINK,
2545 name: b"sub".to_vec(),
2546 oid: submodule_oid,
2547 }]);
2548 let tree_oid = HashAlgo::Sha1
2549 .hash_object(ObjectKind::Tree, &tree_data)
2550 .expect("hash");
2551
2552 let mut pack = HashMap::new();
2555 pack.insert(tree_oid, (ObjectKind::Tree, tree_data.clone()));
2556 assert!(strict_verify_packed_references(None, &pack).is_ok());
2557
2558 let bad_tree = serialize_tree(&[TreeEntry {
2561 mode: MODE_REGULAR,
2562 name: b"file".to_vec(),
2563 oid: ObjectId::from_hex(&"ab".repeat(20)).unwrap(),
2564 }]);
2565 let bad_oid = HashAlgo::Sha1
2566 .hash_object(ObjectKind::Tree, &bad_tree)
2567 .expect("hash");
2568 let mut bad_pack = HashMap::new();
2569 bad_pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
2570 assert!(matches!(
2571 strict_verify_packed_references(None, &bad_pack),
2572 Err(Error::CorruptObject(_))
2573 ));
2574 }
2575
2576 struct ChunkedReader<'a> {
2578 data: &'a [u8],
2579 pos: usize,
2580 max_len: usize,
2581 }
2582
2583 impl io::Read for ChunkedReader<'_> {
2584 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2585 if self.pos >= self.data.len() {
2586 return Ok(0);
2587 }
2588 let take = (self.data.len() - self.pos)
2589 .min(self.max_len)
2590 .min(buf.len());
2591 buf[..take].copy_from_slice(&self.data[self.pos..self.pos + take]);
2592 self.pos += take;
2593 Ok(take)
2594 }
2595 }
2596
2597 #[cfg_attr(
2598 not(feature = "full-tests"),
2599 ignore = "starts system git or is heavy; run with --features full-tests"
2600 )]
2601 #[test]
2602 fn test_unpack_objects_chunked_read_matches_full_buffer() {
2603 use tempfile::TempDir;
2604 let pack = make_pack(&[(ObjectKind::Blob, b"chunked-stream")]);
2605 let opts = UnpackOptions::default();
2606 let oid = HashAlgo::Sha1
2607 .hash_object(ObjectKind::Blob, b"chunked-stream")
2608 .expect("hash");
2609
2610 let tmp = TempDir::new().unwrap();
2611 let objects_dir = tmp.path().join("objects");
2612 std::fs::create_dir_all(&objects_dir).unwrap();
2613 let odb = Odb::new(&objects_dir);
2614 assert_eq!(
2615 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2616 1
2617 );
2618 assert!(odb.exists(&oid));
2619
2620 let tmp2 = TempDir::new().unwrap();
2621 let objects_dir2 = tmp2.path().join("objects");
2622 std::fs::create_dir_all(&objects_dir2).unwrap();
2623 let odb2 = Odb::new(&objects_dir2);
2624 let mut chunked = ChunkedReader {
2625 data: pack.as_slice(),
2626 pos: 0,
2627 max_len: 8,
2628 };
2629 assert_eq!(unpack_objects(&mut chunked, &odb2, &opts).unwrap(), 1);
2630 assert!(odb2.exists(&oid));
2631 }
2632
2633 #[cfg_attr(
2634 not(feature = "full-tests"),
2635 ignore = "starts system git or is heavy; run with --features full-tests"
2636 )]
2637 #[test]
2638 fn test_unpack_objects_dry_run_writes_nothing() {
2639 use tempfile::TempDir;
2640 let tmp = TempDir::new().unwrap();
2641 let objects_dir = tmp.path().join("objects");
2642 std::fs::create_dir_all(&objects_dir).unwrap();
2643 let odb = Odb::new(&objects_dir);
2644
2645 let pack = make_pack(&[(ObjectKind::Blob, b"test content")]);
2646
2647 let opts = UnpackOptions {
2648 dry_run: true,
2649 quiet: true,
2650 strict: false,
2651 allowed_missing: Default::default(),
2652 allow_promisor_missing_references: false,
2653 max_input_bytes: None,
2654 ..Default::default()
2655 };
2656 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
2657 assert_eq!(count, 1);
2658
2659 let oid = HashAlgo::Sha1
2661 .hash_object(ObjectKind::Blob, b"test content")
2662 .expect("hash");
2663 assert!(!odb.exists(&oid));
2664 }
2665
2666 #[cfg_attr(
2667 not(feature = "full-tests"),
2668 ignore = "starts system git or is heavy; run with --features full-tests"
2669 )]
2670 #[test]
2671 fn test_unpack_objects_bad_signature() {
2672 use tempfile::TempDir;
2673 let tmp = TempDir::new().unwrap();
2674 let objects_dir = tmp.path().join("objects");
2675 std::fs::create_dir_all(&objects_dir).unwrap();
2676 let odb = Odb::new(&objects_dir);
2677
2678 let mut bad = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00".to_vec();
2679 bad.extend_from_slice(&[0u8; 20]);
2680 let opts = UnpackOptions::default();
2681 let err = unpack_objects(&mut bad.as_slice(), &odb, &opts).unwrap_err();
2682 assert!(err.to_string().contains("invalid signature"));
2683 }
2684
2685 #[cfg_attr(
2686 not(feature = "full-tests"),
2687 ignore = "starts system git or is heavy; run with --features full-tests"
2688 )]
2689 #[test]
2690 fn test_unpack_objects_checksum_mismatch() {
2691 use tempfile::TempDir;
2692 let tmp = TempDir::new().unwrap();
2693 let objects_dir = tmp.path().join("objects");
2694 std::fs::create_dir_all(&objects_dir).unwrap();
2695 let odb = Odb::new(&objects_dir);
2696
2697 let mut pack = make_pack(&[(ObjectKind::Blob, b"data")]);
2698 let n = pack.len();
2700 pack[n - 1] ^= 0xFF;
2701
2702 let opts = UnpackOptions::default();
2703 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
2704 assert!(err.to_string().contains("checksum"));
2705 }
2706
2707 #[test]
2708 fn test_apply_delta_source_size_mismatch() {
2709 let base = b"hi";
2710 let delta = [3u8, 2u8, 2u8, b'h', b'i']; let err = apply_delta(base, &delta).unwrap_err();
2712 assert!(err.to_string().contains("source size"));
2713 }
2714
2715 #[test]
2716 fn unpack_many_objects_does_not_retain_all_payloads() {
2717 let _lock = TEST_UNPACK_RETAIN_LOCK.lock().expect("retain lock");
2718 const N: usize = 2500;
2719 let blobs: Vec<(ObjectKind, Vec<u8>)> = (0..N)
2720 .map(|i| (ObjectKind::Blob, format!("blob-{i}\n").into_bytes()))
2721 .collect();
2722 let blob_refs: Vec<(ObjectKind, &[u8])> =
2723 blobs.iter().map(|(k, d)| (*k, d.as_slice())).collect();
2724 let pack = make_pack(&blob_refs);
2725 let tmp = tempfile::tempdir().expect("tempdir");
2726 let odb = Odb::new(tmp.path());
2727 let cache_budget = 256 * 1024;
2728 test_set_unpack_base_cache_byte_limit(cache_budget);
2729 let count =
2730 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).expect("unpack");
2731 assert_eq!(count, N);
2732 assert!(
2733 test_peak_unpack_retained_bytes() <= cache_budget + 128 * 1024,
2734 "peak retained {} exceeds cache budget {}",
2735 test_peak_unpack_retained_bytes(),
2736 cache_budget
2737 );
2738 test_set_unpack_base_cache_byte_limit(0);
2739 }
2740
2741 #[test]
2742 fn unpack_many_deltified_blobs_respects_base_cache_budget() {
2743 let _lock = TEST_UNPACK_RETAIN_LOCK.lock().expect("retain lock");
2744 use crate::objects::{serialize_commit, CommitData};
2745 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2746
2747 const N_DELTAS: usize = 350;
2748 const BLOB_SIZE: usize = 48 * 1024;
2749 let base: Vec<u8> = (0..BLOB_SIZE).map(|i| (i % 251) as u8).collect();
2750 let tree_oid = HashAlgo::Sha1
2751 .hash_object(ObjectKind::Tree, b"")
2752 .expect("hash");
2753 let commit = CommitData {
2754 tree: tree_oid,
2755 parents: vec![],
2756 author: "A <a@b.com>".into(),
2757 committer: "A <a@b.com>".into(),
2758 author_raw: Vec::new(),
2759 committer_raw: Vec::new(),
2760 encoding: None,
2761 message: "m".into(),
2762 raw_message: None,
2763 extra_headers: Vec::new(),
2764 };
2765 let commit_data = serialize_commit(&commit);
2766
2767 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2768 pb.add_full(PK::Tree, b"");
2769 pb.add_full(PK::Commit, &commit_data);
2770 let base_idx = pb.add_full(PK::Blob, &base);
2771 for i in 0..N_DELTAS {
2772 let mut variant = base.clone();
2773 variant[BLOB_SIZE - 1] = (i % 256) as u8;
2774 let delta = crate::delta_encode::encode_lcp_delta(&base, &variant).expect("delta");
2775 pb.add_ofs_delta(base_idx, &delta, delta.len());
2776 }
2777 let pack = pb.build().bytes;
2778
2779 let tmp = tempfile::tempdir().expect("tempdir");
2780 let odb = Odb::new(tmp.path());
2781 let cache_budget = 512 * 1024;
2782 test_set_unpack_base_cache_byte_limit(cache_budget);
2783 let opts = UnpackOptions {
2784 strict: true,
2785 ..Default::default()
2786 };
2787 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).expect("unpack");
2788 assert_eq!(count, N_DELTAS + 3);
2789 assert!(
2790 test_peak_unpack_retained_bytes() <= cache_budget + 256 * 1024,
2791 "peak retained {} exceeds cache budget {}",
2792 test_peak_unpack_retained_bytes(),
2793 cache_budget
2794 );
2795 test_set_unpack_base_cache_byte_limit(0);
2796 }
2797
2798 #[test]
2799 fn pack_index_many_blobs_without_retaining_all_payloads() {
2800 const N: usize = 1500;
2801 let blobs: Vec<(ObjectKind, Vec<u8>)> = (0..N)
2802 .map(|i| (ObjectKind::Blob, format!("blob-{i}\n").into_bytes()))
2803 .collect();
2804 let blob_refs: Vec<(ObjectKind, &[u8])> =
2805 blobs.iter().map(|(k, d)| (*k, d.as_slice())).collect();
2806 let pack = make_pack(&blob_refs);
2807 let tmp = tempfile::tempdir().expect("tempdir");
2808 let odb = Odb::new(tmp.path());
2809 let started = std::time::Instant::now();
2810 let records = pack_index_records_from_bytes(&pack, &odb).expect("index records");
2811 let elapsed = started.elapsed();
2812 assert_eq!(records.len(), N, "one index row per object");
2813 assert!(
2814 elapsed.as_secs() < 30,
2815 "indexing {N} blobs took {:?}; expected linear scan without full retention blow-up",
2816 elapsed
2817 );
2818 }
2819
2820 fn pack_with_ref_delta(base: &[u8], target: &[u8]) -> (Vec<u8>, ObjectId, ObjectId) {
2821 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2822
2823 let delta = crate::delta_encode::encode_lcp_delta(base, target).expect("delta");
2824 let base_oid = HashAlgo::Sha1
2825 .hash_object(ObjectKind::Blob, base)
2826 .expect("hash");
2827 let target_oid = HashAlgo::Sha1
2828 .hash_object(ObjectKind::Blob, target)
2829 .expect("hash");
2830 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2831 pb.add_full(PK::Blob, base);
2832 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2833 (pb.build().bytes, base_oid, target_oid)
2834 }
2835
2836 #[cfg_attr(
2837 not(feature = "full-tests"),
2838 ignore = "starts system git or is heavy; run with --features full-tests"
2839 )]
2840 #[test]
2841 fn test_unpack_objects_resolves_ref_and_ofs_deltas() {
2842 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
2843
2844 let base = b"base payload";
2845 let target = b"base payload-extended";
2846 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2847 let base_oid = HashAlgo::Sha1
2848 .hash_object(ObjectKind::Blob, base)
2849 .expect("hash");
2850
2851 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2852 let base_idx = pb.add_full(PK::Blob, base);
2853 pb.add_ofs_delta(base_idx, &delta, delta.len());
2854 let ofs_pack = pb.build().bytes;
2855
2856 let tmp = tempfile::tempdir().unwrap();
2857 let odb = Odb::new(tmp.path());
2858 let opts = UnpackOptions::default();
2859 assert_eq!(
2860 unpack_objects(&mut ofs_pack.as_slice(), &odb, &opts).unwrap(),
2861 2
2862 );
2863 let target_oid = HashAlgo::Sha1
2864 .hash_object(ObjectKind::Blob, target)
2865 .expect("hash");
2866 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2867
2868 let (ref_pack, _, ref_target_oid) = pack_with_ref_delta(base, target);
2869 let tmp2 = tempfile::tempdir().unwrap();
2870 let odb2 = Odb::new(tmp2.path());
2871 assert_eq!(
2872 unpack_objects(&mut ref_pack.as_slice(), &odb2, &opts).unwrap(),
2873 2
2874 );
2875 assert_eq!(odb2.read(&ref_target_oid).unwrap().data, target);
2876 assert!(odb2.exists(&base_oid));
2877 }
2878
2879 #[cfg_attr(
2880 not(feature = "full-tests"),
2881 ignore = "starts system git or is heavy; run with --features full-tests"
2882 )]
2883 #[test]
2884 fn test_unpack_objects_ref_delta_base_from_loose() {
2885 let base = b"loose-base";
2886 let target = b"loose-base-v2";
2887 let (_, base_oid, target_oid) = pack_with_ref_delta(base, target);
2888
2889 let tmp = tempfile::tempdir().unwrap();
2890 let odb = Odb::new(tmp.path());
2891 odb.write_local(ObjectKind::Blob, base).unwrap();
2892 let delta_only = {
2894 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
2895 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
2896 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
2897 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
2898 pb.build().bytes
2899 };
2900 let pack = delta_only;
2901
2902 let opts = UnpackOptions::default();
2903 assert_eq!(
2904 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2905 1
2906 );
2907 assert_eq!(odb.read(&target_oid).unwrap().data, target);
2908 }
2909
2910 #[cfg_attr(
2911 not(feature = "full-tests"),
2912 ignore = "starts system git or is heavy; run with --features full-tests"
2913 )]
2914 #[test]
2915 fn test_unpack_objects_strict_commit_tree_tag() {
2916 use crate::objects::{serialize_commit, serialize_tag, CommitData, TagData};
2917
2918 let tree_data = b"";
2919 let tree_oid = HashAlgo::Sha1
2920 .hash_object(ObjectKind::Tree, tree_data)
2921 .expect("hash");
2922 let commit = CommitData {
2923 tree: tree_oid,
2924 parents: vec![],
2925 author: "A <a@b.com>".into(),
2926 committer: "A <a@b.com>".into(),
2927 author_raw: Vec::new(),
2928 committer_raw: Vec::new(),
2929 encoding: None,
2930 message: "msg".into(),
2931 raw_message: None,
2932 extra_headers: Vec::new(),
2933 };
2934 let commit_data = serialize_commit(&commit);
2935 let commit_oid = HashAlgo::Sha1
2936 .hash_object(ObjectKind::Commit, &commit_data)
2937 .expect("hash");
2938 let tag = TagData {
2939 object: commit_oid,
2940 object_type: "commit".into(),
2941 tag: "v1".into(),
2942 tagger: Some("T <t@e.com>".into()),
2943 extra_headers: Vec::new(),
2944 message: "annotated".into(),
2945 };
2946 let tag_data = serialize_tag(&tag);
2947 let pack = make_pack(&[
2948 (ObjectKind::Tree, tree_data),
2949 (ObjectKind::Commit, &commit_data),
2950 (ObjectKind::Tag, &tag_data),
2951 ]);
2952
2953 let tmp = tempfile::tempdir().unwrap();
2954 let odb = Odb::new(tmp.path());
2955 let opts = UnpackOptions {
2956 strict: true,
2957 ..Default::default()
2958 };
2959 assert_eq!(
2960 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
2961 3
2962 );
2963 assert!(odb.exists(&commit_oid));
2964 }
2965
2966 #[cfg_attr(
2967 not(feature = "full-tests"),
2968 ignore = "starts system git or is heavy; run with --features full-tests"
2969 )]
2970 #[test]
2971 fn test_unpack_objects_strict_shallow_skips_missing_parent() {
2972 use crate::objects::{serialize_commit, CommitData};
2973
2974 let tree_oid = HashAlgo::Sha1
2975 .hash_object(ObjectKind::Tree, b"")
2976 .expect("hash");
2977 let missing_parent = ObjectId::from_hex(&"cc".repeat(20)).unwrap();
2978 let commit = CommitData {
2979 tree: tree_oid,
2980 parents: vec![missing_parent],
2981 author: "A <a@b.com>".into(),
2982 committer: "A <a@b.com>".into(),
2983 author_raw: Vec::new(),
2984 committer_raw: Vec::new(),
2985 encoding: None,
2986 message: "msg".into(),
2987 raw_message: None,
2988 extra_headers: Vec::new(),
2989 };
2990 let commit_data = serialize_commit(&commit);
2991 let commit_oid = HashAlgo::Sha1
2992 .hash_object(ObjectKind::Commit, &commit_data)
2993 .expect("hash");
2994 let pack = make_pack(&[(ObjectKind::Tree, b""), (ObjectKind::Commit, &commit_data)]);
2995
2996 let tmp = tempfile::tempdir().unwrap();
2997 let odb = Odb::new(tmp.path());
2998 let mut shallow = HashSet::new();
2999 shallow.insert(commit_oid);
3000 let opts = UnpackOptions {
3001 strict: true,
3002 shallow_boundaries: shallow,
3003 ..Default::default()
3004 };
3005 assert_eq!(
3006 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
3007 2
3008 );
3009 }
3010
3011 #[cfg_attr(
3012 not(feature = "full-tests"),
3013 ignore = "starts system git or is heavy; run with --features full-tests"
3014 )]
3015 #[test]
3016 fn test_unpack_objects_max_input_bytes() {
3017 let pack = make_pack(&[(ObjectKind::Blob, b"small")]);
3018 let tmp = tempfile::tempdir().unwrap();
3019 let odb = Odb::new(tmp.path());
3020 let opts = UnpackOptions {
3021 max_input_bytes: Some(4),
3022 ..Default::default()
3023 };
3024 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
3025 assert!(err.to_string().contains("maximum allowed size"));
3026 }
3027
3028 #[cfg_attr(
3029 not(feature = "full-tests"),
3030 ignore = "starts system git or is heavy; run with --features full-tests"
3031 )]
3032 #[test]
3033 fn test_unpack_objects_unresolved_ref_delta() {
3034 let missing = ObjectId::from_hex(&"dd".repeat(20)).unwrap();
3035 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
3036 let delta = [0u8, 1u8, 1u8, b'x'];
3037 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
3038 pb.add_ref_delta(missing.as_bytes(), &delta, delta.len());
3039 let pack = pb.build().bytes;
3040
3041 let tmp = tempfile::tempdir().unwrap();
3042 let odb = Odb::new(tmp.path());
3043 let err =
3044 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
3045 assert!(err.to_string().contains("could not be resolved"));
3046 }
3047
3048 #[cfg_attr(
3049 not(feature = "full-tests"),
3050 ignore = "starts system git or is heavy; run with --features full-tests"
3051 )]
3052 #[test]
3053 fn missing_ref_delta_base_is_typed_on_every_ingest_path() {
3054 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
3055 let delta = [0u8, 1u8, 1u8, b'x'];
3056 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
3057 pb.add_ref_delta(&[0xdd; 20], &delta, delta.len());
3058 pb.add_ref_delta(&[0xee; 20], &delta, delta.len());
3059 let pack = pb.build().bytes;
3060 let tmp = tempfile::tempdir().unwrap();
3061 let odb = Odb::new(tmp.path());
3062
3063 let unpacked = unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default());
3064 assert!(
3065 matches!(unpacked, Err(Error::UnresolvedDeltas { count: 2 })),
3066 "{unpacked:?}"
3067 );
3068 let mapped = pack_bytes_to_object_map(&pack, &odb);
3069 assert!(
3070 matches!(mapped, Err(Error::UnresolvedDeltas { count: 2 })),
3071 "{mapped:?}"
3072 );
3073 let indexed = pack_index_records_from_bytes(&pack, &odb);
3074 assert!(
3075 matches!(indexed, Err(Error::UnresolvedDeltas { count: 2 })),
3076 "{indexed:?}"
3077 );
3078 assert!(pack_is_thin(&pack, HashAlgo::Sha1));
3079 }
3080
3081 #[test]
3082 fn test_apply_delta_into_matches_apply_delta() {
3083 let base = b"abcdef";
3084 let mut delta = Vec::new();
3085 delta.push(6u8);
3086 delta.push(3u8);
3087 delta.push(0x91u8);
3088 delta.push(2u8);
3089 delta.push(3u8);
3090
3091 let expected = apply_delta(base, &delta).unwrap();
3092 let mut out = Vec::new();
3093 apply_delta_into(&mut out, base, &delta).unwrap();
3094 assert_eq!(out, expected);
3095 }
3096
3097 #[test]
3098 fn test_pack_bytes_to_object_map_ref_delta() {
3099 let base = b"map-base";
3100 let target = b"map-base-2";
3101 let (pack, _, target_oid) = pack_with_ref_delta(base, target);
3102 let tmp = tempfile::tempdir().unwrap();
3103 let odb = Odb::new(tmp.path());
3104 let map = pack_bytes_to_object_map(&pack, &odb).unwrap();
3105 assert_eq!(map.get(&target_oid).unwrap().data, target);
3106 }
3107
3108 #[test]
3109 fn test_pack_is_thin_and_non_thin() {
3110 let base = b"thin";
3111 let target = b"thin-pack";
3112 let (full, base_oid, _) = pack_with_ref_delta(base, target);
3113 assert!(!pack_is_thin(&full, HashAlgo::Sha1));
3114
3115 let delta = crate::delta_encode::encode_lcp_delta(base, target).unwrap();
3116 use grit_test_support::objects::{HashAlgo as FixtureAlgo, PackBuilder};
3117 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
3118 pb.add_ref_delta(base_oid.as_bytes(), &delta, delta.len());
3119 let thin = pb.build().bytes;
3120 assert!(pack_is_thin(&thin, HashAlgo::Sha1));
3121 }
3122
3123 #[cfg_attr(
3124 not(feature = "full-tests"),
3125 ignore = "starts system git or is heavy; run with --features full-tests"
3126 )]
3127 #[test]
3128 fn test_unpack_zero_byte_blob_via_pack_builder() {
3129 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
3130 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
3131 pb.add_full(PK::Blob, b"");
3132 let pack = pb.build().bytes;
3133 let tmp = tempfile::tempdir().unwrap();
3134 let odb = Odb::new(tmp.path());
3135 assert_eq!(
3136 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
3137 1
3138 );
3139 let oid = HashAlgo::Sha1
3140 .hash_object(ObjectKind::Blob, b"")
3141 .expect("hash");
3142 assert!(odb.exists(&oid));
3143 }
3144
3145 #[test]
3146 fn test_strict_verify_rejects_missing_tree_reference() {
3147 let tree_oid = HashAlgo::Sha1
3148 .hash_object(ObjectKind::Tree, b"")
3149 .expect("hash");
3150 let missing = ObjectId::from_hex(&"ee".repeat(20)).unwrap();
3151 let bad_tree = crate::objects::serialize_tree(&[crate::objects::TreeEntry {
3152 mode: crate::index::MODE_REGULAR,
3153 name: b"f".to_vec(),
3154 oid: missing,
3155 }]);
3156 let bad_oid = HashAlgo::Sha1
3157 .hash_object(ObjectKind::Tree, &bad_tree)
3158 .expect("hash");
3159 let mut pack = HashMap::new();
3160 pack.insert(tree_oid, (ObjectKind::Tree, b"".to_vec()));
3161 pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
3162 assert!(strict_verify_packed_references(None, &pack).is_err());
3163 }
3164
3165 #[cfg_attr(
3166 not(feature = "full-tests"),
3167 ignore = "starts system git or is heavy; run with --features full-tests"
3168 )]
3169 #[test]
3170 fn test_unpack_large_blob_materializes_on_disk() {
3171 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
3172 let pack = make_pack(&[(ObjectKind::Blob, &big)]);
3173 let tmp = tempfile::tempdir().unwrap();
3174 let odb = Odb::new(tmp.path());
3175 assert_eq!(
3176 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap(),
3177 1
3178 );
3179 let oid = HashAlgo::Sha1
3180 .hash_object(ObjectKind::Blob, &big)
3181 .expect("hash");
3182 assert_eq!(odb.read(&oid).unwrap().data, big);
3183 }
3184
3185 #[cfg_attr(
3186 not(feature = "full-tests"),
3187 ignore = "starts system git or is heavy; run with --features full-tests"
3188 )]
3189 #[test]
3190 fn truncated_zlib_after_complete_payload_does_not_hang() {
3191 use std::time::{Duration, Instant};
3192
3193 let payload = vec![0u8; 4096];
3194 let mut pack = make_pack(&[(ObjectKind::Blob, &payload)]);
3195 pack.truncate(pack.len() - 21);
3196 let digest = HashAlgo::Sha1.checksum_digest(&pack);
3197 pack.extend_from_slice(&digest);
3198
3199 let tmp = tempfile::tempdir().unwrap();
3200 let odb = Odb::new(tmp.path());
3201 let started = Instant::now();
3202 let err =
3203 unpack_objects(&mut pack.as_slice(), &odb, &UnpackOptions::default()).unwrap_err();
3204 assert!(
3205 started.elapsed() < Duration::from_secs(2),
3206 "unpack hung on truncated zlib"
3207 );
3208 let msg = err.to_string();
3209 assert!(
3210 msg.contains("truncated") || msg.contains("zlib") || msg.contains("mismatch"),
3211 "unexpected error: {msg}"
3212 );
3213 }
3214
3215 #[test]
3216 fn strict_dot_special_does_not_reload_streamed_blobs() {
3217 use std::cell::Cell;
3218
3219 let tmp = tempfile::tempdir().unwrap();
3220 let odb = Odb::new(tmp.path());
3221 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
3222 let big_oid = HashAlgo::Sha1
3223 .hash_object(ObjectKind::Blob, &big)
3224 .expect("hash");
3225 let tree_oid = HashAlgo::Sha1
3226 .hash_object(ObjectKind::Tree, b"")
3227 .expect("hash");
3228
3229 struct CountingSink<'a> {
3230 inner: &'a Odb,
3231 reads: Cell<usize>,
3232 }
3233
3234 impl UnpackSink for CountingSink<'_> {
3235 fn hash_algo(&self) -> HashAlgo {
3236 Odb::hash_algo(self.inner)
3237 }
3238
3239 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
3240 if dry_run {
3241 return self.inner.hash(kind, data);
3242 }
3243 self.inner.write(kind, data)
3244 }
3245
3246 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
3247 self.reads.set(self.reads.get() + 1);
3248 self.inner.read_stored(oid)
3249 }
3250
3251 fn contains(&self, oid: &ObjectId) -> Result<bool> {
3252 Odb::contains(self.inner, oid)
3253 }
3254 }
3255
3256 let mut by_oid = HashMap::new();
3257 by_oid.insert(
3258 big_oid,
3259 PackedObjectEntry::Stored {
3260 kind: ObjectKind::Blob,
3261 oid: big_oid,
3262 },
3263 );
3264 by_oid.insert(
3265 tree_oid,
3266 PackedObjectEntry::DryRun {
3267 kind: ObjectKind::Tree,
3268 data: Vec::new(),
3269 },
3270 );
3271
3272 let sink = CountingSink {
3273 inner: &odb,
3274 reads: Cell::new(0),
3275 };
3276 verify_packed_dot_special_entries(&by_oid, &sink).expect("dot-special walk");
3277 assert_eq!(
3278 sink.reads.get(),
3279 0,
3280 "must not read streamed blobs back for strict fsck"
3281 );
3282 }
3283
3284 #[test]
3285 fn strict_verify_does_not_reload_streamed_blobs() {
3286 use std::cell::Cell;
3287
3288 use crate::index::MODE_REGULAR;
3289 use crate::objects::{serialize_commit, serialize_tree, CommitData, TreeEntry};
3290
3291 let tmp = tempfile::tempdir().unwrap();
3292 let odb = Odb::new(tmp.path());
3293 let big: Vec<u8> = vec![b'x'; 2 * 1024 * 1024 + 1];
3294 let big_oid = odb.write(ObjectKind::Blob, &big).expect("write blob");
3295
3296 let tree_data = serialize_tree(&[TreeEntry {
3297 mode: MODE_REGULAR,
3298 name: b"big.bin".to_vec(),
3299 oid: big_oid,
3300 }]);
3301 let tree_oid = HashAlgo::Sha1
3302 .hash_object(ObjectKind::Tree, &tree_data)
3303 .expect("hash");
3304 let commit = CommitData {
3305 tree: tree_oid,
3306 parents: vec![],
3307 author: "A <a@b.com>".into(),
3308 committer: "A <a@b.com>".into(),
3309 author_raw: Vec::new(),
3310 committer_raw: Vec::new(),
3311 encoding: None,
3312 message: "m".into(),
3313 raw_message: None,
3314 extra_headers: Vec::new(),
3315 };
3316 let commit_data = serialize_commit(&commit);
3317 let commit_oid = HashAlgo::Sha1
3318 .hash_object(ObjectKind::Commit, &commit_data)
3319 .expect("hash");
3320
3321 struct CountingSink<'a> {
3322 inner: &'a Odb,
3323 reads: Cell<usize>,
3324 }
3325
3326 impl UnpackSink for CountingSink<'_> {
3327 fn hash_algo(&self) -> HashAlgo {
3328 Odb::hash_algo(self.inner)
3329 }
3330
3331 fn write(&self, kind: ObjectKind, data: &[u8], dry_run: bool) -> Result<ObjectId> {
3332 if dry_run {
3333 return self.inner.hash(kind, data);
3334 }
3335 self.inner.write(kind, data)
3336 }
3337
3338 fn read_stored(&self, oid: &ObjectId) -> Result<Option<Object>> {
3339 self.reads.set(self.reads.get() + 1);
3340 self.inner.read_stored(oid)
3341 }
3342
3343 fn contains(&self, oid: &ObjectId) -> Result<bool> {
3344 Odb::contains(self.inner, oid)
3345 }
3346 }
3347
3348 let mut by_oid = HashMap::new();
3349 by_oid.insert(
3350 big_oid,
3351 PackedObjectEntry::Stored {
3352 kind: ObjectKind::Blob,
3353 oid: big_oid,
3354 },
3355 );
3356 by_oid.insert(
3357 tree_oid,
3358 PackedObjectEntry::DryRun {
3359 kind: ObjectKind::Tree,
3360 data: tree_data,
3361 },
3362 );
3363 by_oid.insert(
3364 commit_oid,
3365 PackedObjectEntry::DryRun {
3366 kind: ObjectKind::Commit,
3367 data: commit_data,
3368 },
3369 );
3370
3371 let sink = CountingSink {
3372 inner: &odb,
3373 reads: Cell::new(0),
3374 };
3375 strict_verify_packed_references_map(
3376 &sink,
3377 &by_oid,
3378 &HashSet::new(),
3379 false,
3380 &HashSet::new(),
3381 None,
3382 )
3383 .expect("strict connectivity");
3384 assert_eq!(
3385 sink.reads.get(),
3386 0,
3387 "strict verify must not read streamed blob payloads"
3388 );
3389
3390 let missing = ObjectId::from_hex(&"ab".repeat(20)).unwrap();
3391 let bad_tree = serialize_tree(&[TreeEntry {
3392 mode: MODE_REGULAR,
3393 name: b"missing".to_vec(),
3394 oid: missing,
3395 }]);
3396 let bad_tree_oid = HashAlgo::Sha1
3397 .hash_object(ObjectKind::Tree, &bad_tree)
3398 .expect("hash");
3399 let mut bad = HashMap::new();
3400 bad.insert(
3401 bad_tree_oid,
3402 PackedObjectEntry::DryRun {
3403 kind: ObjectKind::Tree,
3404 data: bad_tree,
3405 },
3406 );
3407 let sink2 = CountingSink {
3408 inner: &odb,
3409 reads: Cell::new(0),
3410 };
3411 assert!(matches!(
3412 strict_verify_packed_references_map(
3413 &sink2,
3414 &bad,
3415 &HashSet::new(),
3416 false,
3417 &HashSet::new(),
3418 None,
3419 ),
3420 Err(Error::CorruptObject(_))
3421 ));
3422 assert_eq!(sink2.reads.get(), 0);
3423 }
3424}