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