1use std::borrow::Cow;
13use std::collections::{HashMap, HashSet};
14use std::io::{self, Read};
15
16use flate2::read::ZlibDecoder;
17use flate2::{Decompress, FlushDecompress, Status};
18use sha1::{Digest, Sha1};
19use sha2::{Digest as Sha256Digest, Sha256};
20
21use crate::error::{Error, Result};
22use crate::gitmodules;
23use crate::index::MODE_GITLINK;
24use crate::objects::{parse_commit, parse_tag, parse_tree, HashAlgo, Object, ObjectId, ObjectKind};
25use crate::odb::Odb;
26
27#[derive(Clone)]
29enum PackHasher {
30 Sha1(Sha1),
31 Sha256(Sha256),
32}
33
34impl PackHasher {
35 fn new(algo: HashAlgo) -> Self {
36 match algo {
37 HashAlgo::Sha1 => Self::Sha1(Sha1::new()),
38 HashAlgo::Sha256 => Self::Sha256(Sha256::new()),
39 }
40 }
41
42 fn update(&mut self, data: &[u8]) {
43 match self {
44 Self::Sha1(h) => Digest::update(h, data),
45 Self::Sha256(h) => Sha256Digest::update(h, data),
46 }
47 }
48
49 fn finalize(self) -> Vec<u8> {
50 match self {
51 Self::Sha1(h) => h.finalize().to_vec(),
52 Self::Sha256(h) => h.finalize().to_vec(),
53 }
54 }
55
56 fn len(&self) -> usize {
57 match self {
58 Self::Sha1(_) => 20,
59 Self::Sha256(_) => 32,
60 }
61 }
62}
63
64fn hash_object_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> ObjectId {
67 let header = format!("{kind} {}\0", data.len());
68 let mut h = PackHasher::new(algo);
69 h.update(header.as_bytes());
70 h.update(data);
71 #[allow(clippy::expect_used)]
74 ObjectId::from_bytes(&h.finalize()).expect("digest is a valid OID width")
75}
76
77#[derive(Debug, Default)]
79pub struct UnpackOptions {
80 pub dry_run: bool,
82 pub quiet: bool,
84 pub strict: bool,
86 pub allowed_missing: HashSet<ObjectId>,
88 pub allow_promisor_missing_references: bool,
90 pub max_input_bytes: Option<u64>,
95 pub shallow_boundaries: HashSet<ObjectId>,
101}
102
103struct PendingDelta {
105 offset: usize,
108 base_oid: Option<ObjectId>,
110 base_offset: Option<usize>,
112 delta_data: Vec<u8>,
114}
115
116pub fn unpack_objects(reader: &mut dyn Read, odb: &Odb, opts: &UnpackOptions) -> Result<usize> {
131 const MAX_RETAIN_BYTES: usize = 1024 * 1024;
135
136 let algo = odb.hash_algo();
137 let mut rd = StreamingPackReader::new(reader, opts.max_input_bytes, algo);
138
139 let sig = rd.read_exact_n(4)?;
141 if sig != b"PACK" {
142 return Err(Error::CorruptObject(
143 "not a pack stream: invalid signature".to_owned(),
144 ));
145 }
146 let version = rd.read_u32_be()?;
147 if version != 2 && version != 3 {
148 return Err(Error::CorruptObject(format!(
149 "unsupported pack version {version}"
150 )));
151 }
152 let nr_objects = rd.read_u32_be()? as usize;
153
154 let mut by_offset: HashMap<usize, PackedObjectEntry> = HashMap::new();
156 let mut by_oid: HashMap<ObjectId, PackedObjectEntry> = HashMap::new();
158
159 let mut pending: Vec<PendingDelta> = Vec::new();
160 let mut count = 0usize;
161
162 for _ in 0..nr_objects {
163 let obj_offset = rd.stream_pos();
164 let (type_code, size) = rd.read_type_size()?;
165
166 match type_code {
167 1..=4 => {
168 let kind = type_code_to_kind(type_code)?;
169 let data = rd.decompress(size)?;
170 let oid = write_or_hash(kind, &data, odb, opts.dry_run)?;
171 let entry = packed_entry_after_write(kind, data, oid, odb, opts, MAX_RETAIN_BYTES);
172 by_offset.insert(obj_offset, entry.clone());
173 by_oid.insert(oid, entry);
174 count += 1;
175 }
176 6 => {
177 let neg = rd.read_ofs_neg_offset()?;
179 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
180 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
181 })?;
182 let delta_data = rd.decompress(size)?;
183 pending.push(PendingDelta {
184 offset: obj_offset,
185 base_oid: None,
186 base_offset: Some(base_offset),
187 delta_data,
188 });
189 }
190 7 => {
191 let base_bytes = rd.read_exact_n(algo.len())?;
193 let base_oid = ObjectId::from_bytes(&base_bytes)?;
194 let delta_data = rd.decompress(size)?;
195 pending.push(PendingDelta {
196 offset: obj_offset,
197 base_oid: Some(base_oid),
198 base_offset: None,
199 delta_data,
200 });
201 }
202 other => {
203 return Err(Error::CorruptObject(format!(
204 "unknown packed-object type {other}"
205 )))
206 }
207 }
208 }
209
210 let digest = rd.finalize_hasher();
212 let trailing = rd.read_trailer()?;
213 if digest != trailing {
214 return Err(Error::CorruptObject(
215 "pack trailing checksum mismatch".to_owned(),
216 ));
217 }
218
219 let mut remaining = pending;
222 loop {
223 if remaining.is_empty() {
224 break;
225 }
226 let before = remaining.len();
227 let mut still_pending: Vec<PendingDelta> = Vec::new();
228
229 for delta in remaining {
230 let base_res: Option<Result<(ObjectKind, Cow<'_, [u8]>)>> =
231 if let Some(base_off) = delta.base_offset {
232 by_offset
233 .get(&base_off)
234 .map(|e| entry_object_bytes(e, odb).map(|d| (e.kind(), d)))
235 } else if let Some(ref base_id) = delta.base_oid {
236 if let Some(e) = by_oid.get(base_id) {
237 Some(entry_object_bytes(e, odb).map(|d| (e.kind(), d)))
238 } else if !opts.dry_run {
239 odb.read(base_id)
240 .ok()
241 .map(|obj| Ok((obj.kind, Cow::Owned(obj.data))))
242 } else {
243 None
244 }
245 } else {
246 None
247 };
248
249 match base_res {
250 Some(Ok((base_kind, base_data))) => {
251 let result = apply_delta(base_data.as_ref(), &delta.delta_data)?;
252 let oid = write_or_hash(base_kind, &result, odb, opts.dry_run)?;
253 let new_entry = packed_entry_after_write(
254 base_kind,
255 result,
256 oid,
257 odb,
258 opts,
259 MAX_RETAIN_BYTES,
260 );
261 by_offset.insert(delta.offset, new_entry.clone());
262 by_oid.insert(oid, new_entry);
263 count += 1;
264 }
265 Some(Err(e)) => return Err(e),
266 None => still_pending.push(delta),
267 }
268 }
269
270 remaining = still_pending;
271 if remaining.len() == before {
272 return Err(Error::CorruptObject(format!(
273 "{} delta(s) could not be resolved",
274 remaining.len()
275 )));
276 }
277 }
278
279 if opts.strict {
280 let mut dot_fsck_map: HashMap<ObjectId, (ObjectKind, Vec<u8>)> =
281 HashMap::with_capacity(by_oid.len());
282 for (oid, entry) in &by_oid {
283 let kind = entry.kind();
284 let data = match entry {
285 PackedObjectEntry::InMemory { data, .. } => data.clone(),
286 PackedObjectEntry::BlobOnDisk { oid: blob_oid } => odb.read(blob_oid)?.data,
287 };
288 dot_fsck_map.insert(*oid, (kind, data));
289 }
290 gitmodules::verify_packed_dot_special(&dot_fsck_map)?;
291 strict_verify_packed_references_map(
292 Some(odb),
293 &by_oid,
294 &opts.allowed_missing,
295 opts.allow_promisor_missing_references,
296 &opts.shallow_boundaries,
297 )?;
298 }
299
300 Ok(count)
301}
302
303#[derive(Debug, Clone)]
305enum PackedObjectEntry {
306 InMemory { kind: ObjectKind, data: Vec<u8> },
307 BlobOnDisk { oid: ObjectId },
308}
309
310impl PackedObjectEntry {
311 fn kind(&self) -> ObjectKind {
312 match self {
313 PackedObjectEntry::InMemory { kind, .. } => *kind,
314 PackedObjectEntry::BlobOnDisk { .. } => ObjectKind::Blob,
315 }
316 }
317}
318
319fn packed_entry_after_write(
320 kind: ObjectKind,
321 data: Vec<u8>,
322 oid: ObjectId,
323 _odb: &Odb,
324 opts: &UnpackOptions,
325 max_retain: usize,
326) -> PackedObjectEntry {
327 if !opts.dry_run && kind == ObjectKind::Blob && data.len() > max_retain {
328 PackedObjectEntry::BlobOnDisk { oid }
329 } else {
330 PackedObjectEntry::InMemory { kind, data }
331 }
332}
333
334fn entry_object_bytes<'a>(entry: &'a PackedObjectEntry, odb: &Odb) -> Result<Cow<'a, [u8]>> {
335 match entry {
336 PackedObjectEntry::InMemory { data, .. } => Ok(Cow::Borrowed(data.as_slice())),
337 PackedObjectEntry::BlobOnDisk { oid } => Ok(Cow::Owned(odb.read(oid)?.data)),
338 }
339}
340
341fn strict_verify_packed_references_map(
342 odb: Option<&Odb>,
343 pack: &HashMap<ObjectId, PackedObjectEntry>,
344 allowed_missing: &HashSet<ObjectId>,
345 allow_promisor_missing_references: bool,
346 shallow_boundaries: &HashSet<ObjectId>,
347) -> Result<()> {
348 for (oid, entry) in pack {
349 match entry {
350 PackedObjectEntry::BlobOnDisk { .. } => {}
351 PackedObjectEntry::InMemory { kind, data } => match kind {
352 ObjectKind::Tree => {
353 for e in parse_tree(data)? {
354 if e.mode == MODE_GITLINK {
359 continue;
360 }
361 if !strict_ref_resolves_map(
362 &e.oid,
363 pack,
364 odb,
365 allowed_missing,
366 allow_promisor_missing_references,
367 ) {
368 return Err(Error::CorruptObject(format!(
369 "strict: missing object {} referenced by tree",
370 e.oid.to_hex()
371 )));
372 }
373 }
374 }
375 ObjectKind::Commit => {
376 let c = parse_commit(data)?;
377 if !strict_ref_resolves_map(
378 &c.tree,
379 pack,
380 odb,
381 allowed_missing,
382 allow_promisor_missing_references,
383 ) {
384 return Err(Error::CorruptObject(format!(
385 "strict: missing tree {} referenced by commit",
386 c.tree.to_hex()
387 )));
388 }
389 if shallow_boundaries.contains(oid) {
393 continue;
394 }
395 for p in &c.parents {
396 if !strict_ref_resolves_map(
397 p,
398 pack,
399 odb,
400 allowed_missing,
401 allow_promisor_missing_references,
402 ) {
403 return Err(Error::CorruptObject(format!(
404 "strict: missing parent {} referenced by commit",
405 p.to_hex()
406 )));
407 }
408 }
409 }
410 ObjectKind::Tag => {
411 let t = parse_tag(data)?;
412 if !strict_ref_resolves_map(
413 &t.object,
414 pack,
415 odb,
416 allowed_missing,
417 allow_promisor_missing_references,
418 ) {
419 return Err(Error::CorruptObject(format!(
420 "strict: missing object {} referenced by tag",
421 t.object.to_hex()
422 )));
423 }
424 }
425 ObjectKind::Blob => {}
426 },
427 }
428 }
429 Ok(())
430}
431
432fn strict_ref_resolves_map(
433 oid: &ObjectId,
434 pack: &HashMap<ObjectId, PackedObjectEntry>,
435 odb: Option<&Odb>,
436 allowed_missing: &HashSet<ObjectId>,
437 allow_promisor_missing_references: bool,
438) -> bool {
439 pack.contains_key(oid)
440 || allowed_missing.contains(oid)
441 || odb.is_some_and(|o| o.exists(oid))
442 || allow_promisor_missing_references
443}
444
445fn strict_ref_resolves(
446 oid: &ObjectId,
447 pack: &std::collections::HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
448 odb: Option<&Odb>,
449) -> bool {
450 pack.contains_key(oid) || odb.is_some_and(|o| o.exists(oid))
451}
452
453pub fn strict_verify_packed_references(
459 odb: Option<&Odb>,
460 pack: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
461) -> Result<()> {
462 for (kind, data) in pack.values() {
463 match kind {
464 ObjectKind::Tree => {
465 for e in parse_tree(data)? {
466 if e.mode == MODE_GITLINK {
471 continue;
472 }
473 if !strict_ref_resolves(&e.oid, pack, odb) {
474 return Err(Error::CorruptObject(format!(
475 "strict: missing object {} referenced by tree",
476 e.oid.to_hex()
477 )));
478 }
479 }
480 }
481 ObjectKind::Commit => {
482 let c = parse_commit(data)?;
483 if !strict_ref_resolves(&c.tree, pack, odb) {
484 return Err(Error::CorruptObject(format!(
485 "strict: missing tree {} referenced by commit",
486 c.tree.to_hex()
487 )));
488 }
489 for p in &c.parents {
490 if !strict_ref_resolves(p, pack, odb) {
491 return Err(Error::CorruptObject(format!(
492 "strict: missing parent {} referenced by commit",
493 p.to_hex()
494 )));
495 }
496 }
497 }
498 ObjectKind::Tag => {
499 let t = parse_tag(data)?;
500 if !strict_ref_resolves(&t.object, pack, odb) {
501 return Err(Error::CorruptObject(format!(
502 "strict: missing object {} referenced by tag",
503 t.object.to_hex()
504 )));
505 }
506 }
507 ObjectKind::Blob => {}
508 }
509 }
510 Ok(())
511}
512
513pub fn pack_is_thin(data: &[u8], algo: HashAlgo) -> bool {
520 pack_is_thin_inner(data, algo).unwrap_or(false)
521}
522
523fn pack_is_thin_inner(data: &[u8], algo: HashAlgo) -> Result<bool> {
524 let mut rd = PackReader::new(data.to_vec());
525 if rd.read_exact(4)? != b"PACK" {
526 return Ok(false);
527 }
528 let _version = rd.read_u32_be()?;
529 let nr_objects = rd.read_u32_be()? as usize;
530
531 let mut in_pack: HashSet<ObjectId> = HashSet::new();
532 let mut ref_delta_bases: Vec<ObjectId> = Vec::new();
533 for _ in 0..nr_objects {
534 let obj_offset = rd.pos;
535 let (type_code, size) = rd.read_type_size()?;
536 match type_code {
537 1..=4 => {
538 let kind = type_code_to_kind(type_code)?;
539 let obj_data = rd.decompress(size)?;
540 in_pack.insert(hash_object_with(algo, kind, &obj_data));
541 }
542 6 => {
543 let _neg = rd.read_ofs_neg_offset()?;
545 let _ = obj_offset;
546 let _ = rd.decompress(size)?;
547 }
548 7 => {
549 let base_bytes = rd.read_exact(algo.len())?;
550 ref_delta_bases.push(ObjectId::from_bytes(base_bytes)?);
551 let _ = rd.decompress(size)?;
552 }
553 _ => return Ok(false),
554 }
555 }
556 Ok(ref_delta_bases.iter().any(|b| !in_pack.contains(b)))
558}
559
560pub fn pack_bytes_to_object_map(data: &[u8], odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
567 let rd = PackReader::new(data.to_vec());
568 build_pack_object_map(rd, odb)
569}
570
571fn build_pack_object_map(mut rd: PackReader, odb: &Odb) -> Result<HashMap<ObjectId, Object>> {
572 let algo = odb.hash_algo();
573 let sig = rd.read_exact(4)?;
574 if sig != b"PACK" {
575 return Err(Error::CorruptObject(
576 "not a pack stream: invalid signature".to_owned(),
577 ));
578 }
579 let version = rd.read_u32_be()?;
580 if version != 2 && version != 3 {
581 return Err(Error::CorruptObject(format!(
582 "unsupported pack version {version}"
583 )));
584 }
585 let nr_objects = rd.read_u32_be()? as usize;
586
587 let mut by_offset: HashMap<usize, (ObjectKind, Vec<u8>)> = HashMap::new();
588 let mut by_oid: HashMap<ObjectId, (ObjectKind, Vec<u8>)> = HashMap::new();
589 let mut pending: Vec<PendingDelta> = Vec::new();
590
591 fn base_from_pack_or_odb(
592 by_oid: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>,
593 odb: &Odb,
594 id: &ObjectId,
595 ) -> Option<(ObjectKind, Vec<u8>)> {
596 if let Some(e) = by_oid.get(id) {
597 return Some(e.clone());
598 }
599 odb.read(id).ok().map(|o| (o.kind, o.data))
600 }
601
602 for _ in 0..nr_objects {
603 let obj_offset = rd.pos;
604 let (type_code, size) = rd.read_type_size()?;
605
606 match type_code {
607 1..=4 => {
608 let kind = type_code_to_kind(type_code)?;
609 let data = rd.decompress(size)?;
610 let oid = odb.hash(kind, &data);
611 by_offset.insert(obj_offset, (kind, data.clone()));
612 by_oid.insert(oid, (kind, data));
613 }
614 6 => {
615 let neg = rd.read_ofs_neg_offset()?;
616 let base_offset = obj_offset.checked_sub(neg).ok_or_else(|| {
617 Error::CorruptObject("ofs-delta base offset underflow".to_owned())
618 })?;
619 let delta_data = rd.decompress(size)?;
620 pending.push(PendingDelta {
621 offset: obj_offset,
622 base_oid: None,
623 base_offset: Some(base_offset),
624 delta_data,
625 });
626 }
627 7 => {
628 let base_bytes = rd.read_exact(algo.len())?;
629 let base_oid = ObjectId::from_bytes(base_bytes)?;
630 let delta_data = rd.decompress(size)?;
631 pending.push(PendingDelta {
632 offset: obj_offset,
633 base_oid: Some(base_oid),
634 base_offset: None,
635 delta_data,
636 });
637 }
638 other => {
639 return Err(Error::CorruptObject(format!(
640 "unknown packed-object type {other}"
641 )))
642 }
643 }
644 }
645
646 let consumed = rd.pos;
647 {
648 let mut hasher = PackHasher::new(algo);
649 hasher.update(&rd.data[..consumed]);
650 let digest = hasher.finalize();
651 let trailing = rd.read_exact(algo.len())?;
652 if digest.as_slice() != trailing {
653 return Err(Error::CorruptObject(
654 "pack trailing checksum mismatch".to_owned(),
655 ));
656 }
657 }
658
659 let mut remaining = pending;
660 loop {
661 if remaining.is_empty() {
662 break;
663 }
664 let before = remaining.len();
665 let mut still_pending: Vec<PendingDelta> = Vec::new();
666
667 for delta in remaining {
668 let base = if let Some(base_off) = delta.base_offset {
669 by_offset.get(&base_off).cloned()
670 } else if let Some(ref base_id) = delta.base_oid {
671 base_from_pack_or_odb(&by_oid, odb, base_id)
672 } else {
673 None
674 };
675
676 if let Some((base_kind, base_data)) = base {
677 let result = apply_delta(&base_data, &delta.delta_data)?;
678 let oid = odb.hash(base_kind, &result);
679 by_offset.insert(delta.offset, (base_kind, result.clone()));
680 by_oid.insert(oid, (base_kind, result));
681 } else {
682 still_pending.push(delta);
683 }
684 }
685
686 remaining = still_pending;
687 if remaining.len() == before {
688 return Err(Error::CorruptObject(format!(
689 "{} delta(s) could not be resolved",
690 remaining.len()
691 )));
692 }
693 }
694
695 Ok(by_oid
696 .into_iter()
697 .map(|(oid, (kind, data))| (oid, Object::new(kind, data)))
698 .collect())
699}
700
701fn write_or_hash(kind: ObjectKind, data: &[u8], odb: &Odb, dry_run: bool) -> Result<ObjectId> {
704 if dry_run {
705 Ok(odb.hash(kind, data))
706 } else {
707 odb.write_local(kind, data)
710 }
711}
712
713fn type_code_to_kind(code: u8) -> Result<ObjectKind> {
715 match code {
716 1 => Ok(ObjectKind::Commit),
717 2 => Ok(ObjectKind::Tree),
718 3 => Ok(ObjectKind::Blob),
719 4 => Ok(ObjectKind::Tag),
720 _ => Err(Error::CorruptObject(format!(
721 "type code {code} is not a regular object type"
722 ))),
723 }
724}
725
726struct PackReader {
728 data: Vec<u8>,
729 pos: usize,
730}
731
732impl PackReader {
733 fn new(data: Vec<u8>) -> Self {
734 Self { data, pos: 0 }
735 }
736
737 fn read_exact(&mut self, n: usize) -> Result<&[u8]> {
740 if self.pos + n > self.data.len() {
741 return Err(Error::CorruptObject(format!(
742 "pack stream truncated: need {n} bytes at offset {}",
743 self.pos
744 )));
745 }
746 let slice = &self.data[self.pos..self.pos + n];
747 self.pos += n;
748 Ok(slice)
749 }
750
751 fn read_byte(&mut self) -> Result<u8> {
753 if self.pos >= self.data.len() {
754 return Err(Error::CorruptObject(
755 "unexpected end of pack stream".to_owned(),
756 ));
757 }
758 let b = self.data[self.pos];
759 self.pos += 1;
760 Ok(b)
761 }
762
763 fn read_u32_be(&mut self) -> Result<u32> {
765 let bytes = self.read_exact(4)?;
766 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
767 Error::CorruptObject("u32 read failed".to_owned())
768 })?))
769 }
770
771 fn read_type_size(&mut self) -> Result<(u8, usize)> {
776 let c = self.read_byte()?;
777 let type_code = (c >> 4) & 0x7;
778 let mut size = (c & 0x0f) as usize;
779 let mut shift = 4u32;
780 let mut cur = c;
781 while cur & 0x80 != 0 {
782 cur = self.read_byte()?;
783 size |= ((cur & 0x7f) as usize) << shift;
784 shift += 7;
785 }
786 Ok((type_code, size))
787 }
788
789 fn read_ofs_neg_offset(&mut self) -> Result<usize> {
794 let mut c = self.read_byte()?;
795 let mut value = (c & 0x7f) as usize;
796 while c & 0x80 != 0 {
797 c = self.read_byte()?;
798 value = (value + 1) << 7 | (c & 0x7f) as usize;
799 }
800 Ok(value)
801 }
802
803 fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
808 let slice = &self.data[self.pos..];
809 let mut decoder = ZlibDecoder::new(slice);
810 let mut out = Vec::with_capacity(expected_size);
811 decoder
812 .read_to_end(&mut out)
813 .map_err(|e| Error::Zlib(e.to_string()))?;
814 if out.len() != expected_size {
815 return Err(Error::CorruptObject(format!(
816 "decompressed {} bytes but expected {}",
817 out.len(),
818 expected_size
819 )));
820 }
821 self.pos += decoder.total_in() as usize;
822 Ok(out)
823 }
824}
825
826fn io_to_corrupt_eof(e: io::Error, stream_pos: usize, context: &str) -> Error {
827 if e.kind() == io::ErrorKind::UnexpectedEof {
828 Error::CorruptObject(format!(
829 "pack stream truncated ({context}) at offset {stream_pos}"
830 ))
831 } else {
832 Error::Io(e)
833 }
834}
835
836struct StreamingPackReader<'a> {
842 inner: &'a mut dyn Read,
843 pack_hasher: PackHasher,
844 stream_pos: usize,
845 max_input_bytes: Option<u64>,
846 pending: Vec<u8>,
849}
850
851impl<'a> StreamingPackReader<'a> {
852 fn new(inner: &'a mut dyn Read, max_input_bytes: Option<u64>, algo: HashAlgo) -> Self {
853 Self {
854 inner,
855 pack_hasher: PackHasher::new(algo),
856 stream_pos: 0,
857 max_input_bytes,
858 pending: Vec::new(),
859 }
860 }
861
862 fn stream_pos(&self) -> usize {
863 self.stream_pos
864 }
865
866 fn enforce_max_input(&self) -> Result<()> {
867 if let Some(limit) = self.max_input_bytes {
868 let pos = u64::try_from(self.stream_pos)
869 .map_err(|_| Error::CorruptObject("pack stream position overflow".to_owned()))?;
870 if pos > limit {
871 return Err(Error::CorruptObject(
872 "pack exceeds maximum allowed size".to_owned(),
873 ));
874 }
875 }
876 Ok(())
877 }
878
879 fn read_from_source(&mut self, buf: &mut [u8]) -> Result<usize> {
881 let n = if !self.pending.is_empty() {
882 let take = buf.len().min(self.pending.len());
883 buf[..take].copy_from_slice(&self.pending[..take]);
884 self.pending.drain(..take);
885 take
886 } else {
887 self.inner.read(buf).map_err(Error::Io)?
888 };
889 if n > 0 {
890 self.pack_hasher.update(&buf[..n]);
891 self.stream_pos += n;
892 self.enforce_max_input()?;
893 }
894 Ok(n)
895 }
896
897 fn read_byte(&mut self) -> Result<u8> {
898 let mut b = [0u8; 1];
899 let n = self.read_from_source(&mut b)?;
900 if n == 0 {
901 return Err(Error::CorruptObject(format!(
902 "pack stream truncated (read byte) at offset {}",
903 self.stream_pos
904 )));
905 }
906 Ok(b[0])
907 }
908
909 fn read_exact_n(&mut self, n: usize) -> Result<Vec<u8>> {
910 let mut v = vec![0u8; n];
911 let mut got = 0usize;
912 while got < n {
913 let m = self.read_from_source(&mut v[got..n])?;
914 if m == 0 {
915 return Err(Error::CorruptObject(format!(
916 "pack stream truncated (read exact) at offset {}",
917 self.stream_pos
918 )));
919 }
920 got += m;
921 }
922 Ok(v)
923 }
924
925 fn read_u32_be(&mut self) -> Result<u32> {
926 let mut b = [0u8; 4];
927 let mut got = 0usize;
928 while got < 4 {
929 let m = self.read_from_source(&mut b[got..4])?;
930 if m == 0 {
931 return Err(Error::CorruptObject(format!(
932 "pack stream truncated (read u32) at offset {}",
933 self.stream_pos
934 )));
935 }
936 got += m;
937 }
938 Ok(u32::from_be_bytes(b))
939 }
940
941 fn read_type_size(&mut self) -> Result<(u8, usize)> {
942 let c = self.read_byte()?;
943 let type_code = (c >> 4) & 0x7;
944 let mut size = (c & 0x0f) as usize;
945 let mut shift = 4u32;
946 let mut cur = c;
947 while cur & 0x80 != 0 {
948 cur = self.read_byte()?;
949 size |= ((cur & 0x7f) as usize) << shift;
950 shift += 7;
951 }
952 Ok((type_code, size))
953 }
954
955 fn read_ofs_neg_offset(&mut self) -> Result<usize> {
956 let mut c = self.read_byte()?;
957 let mut value = (c & 0x7f) as usize;
958 while c & 0x80 != 0 {
959 c = self.read_byte()?;
960 value = (value + 1) << 7 | (c & 0x7f) as usize;
961 }
962 Ok(value)
963 }
964
965 fn decompress(&mut self, expected_size: usize) -> Result<Vec<u8>> {
975 if expected_size == 0 {
979 const CHUNK: usize = 64 * 1024;
980 let mut scratch = [0u8; CHUNK];
981 loop {
982 let mut cursor = std::io::Cursor::new(self.pending.as_slice());
983 let mut z = ZlibDecoder::new(&mut cursor);
984 let mut sink = [0u8; 1];
985 match z.read(&mut sink) {
986 Ok(0) => {
987 let consumed = z.total_in() as usize;
988 if consumed > self.pending.len() {
989 return Err(Error::CorruptObject(
990 "zlib total_in exceeds pending buffer".to_owned(),
991 ));
992 }
993 if consumed == 0 {
994 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
995 if n == 0 {
996 return Err(Error::CorruptObject(format!(
997 "pack stream truncated (zlib) at offset {}",
998 self.stream_pos
999 )));
1000 }
1001 self.pending.extend_from_slice(&scratch[..n]);
1002 continue;
1003 }
1004 self.pack_hasher.update(&self.pending[..consumed]);
1005 self.stream_pos += consumed;
1006 self.pending.drain(..consumed);
1007 self.enforce_max_input()?;
1008 return Ok(Vec::new());
1009 }
1010 Ok(_) => {
1011 return Err(Error::CorruptObject(
1012 "0-byte packed object inflated to non-empty output".to_owned(),
1013 ));
1014 }
1015 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1016 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1017 if n == 0 {
1018 return Err(Error::CorruptObject(format!(
1019 "pack stream truncated (zlib) at offset {}",
1020 self.stream_pos
1021 )));
1022 }
1023 self.pending.extend_from_slice(&scratch[..n]);
1024 }
1025 Err(e) => return Err(Error::Zlib(e.to_string())),
1026 }
1027 }
1028 }
1029
1030 const CHUNK: usize = 64 * 1024;
1031 let mut scratch = [0u8; CHUNK];
1032
1033 let mut out = vec![0u8; expected_size];
1034 let mut z = Decompress::new(true);
1035 let mut out_pos = 0usize;
1036 let mut eof = false;
1037 loop {
1038 if self.pending.is_empty() && !eof {
1039 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1040 if n == 0 {
1041 eof = true;
1042 } else {
1043 self.pending.extend_from_slice(&scratch[..n]);
1044 }
1045 }
1046
1047 let flush = if eof && self.pending.is_empty() {
1048 FlushDecompress::Finish
1049 } else {
1050 FlushDecompress::None
1051 };
1052
1053 let before_in = z.total_in();
1054 let before_out = z.total_out();
1055 let status = z
1056 .decompress(self.pending.as_slice(), &mut out[out_pos..], flush)
1057 .map_err(|e| Error::Zlib(e.to_string()))?;
1058 let consumed = (z.total_in() - before_in) as usize;
1059 if consumed > self.pending.len() {
1060 return Err(Error::CorruptObject(
1061 "zlib consumed more than pending buffer".to_owned(),
1062 ));
1063 }
1064 self.pack_hasher.update(&self.pending[..consumed]);
1065 self.stream_pos += consumed;
1066 self.pending.drain(..consumed);
1067 self.enforce_max_input()?;
1068 out_pos += (z.total_out() - before_out) as usize;
1069
1070 match status {
1071 Status::StreamEnd => {
1072 if out_pos != expected_size {
1073 return Err(Error::CorruptObject(format!(
1074 "decompressed size mismatch: got {out_pos}, want {expected_size}"
1075 )));
1076 }
1077 return Ok(out);
1078 }
1079 Status::Ok | Status::BufError => {
1080 if consumed == 0 && !eof {
1081 let n = self.inner.read(&mut scratch).map_err(Error::Io)?;
1082 if n == 0 {
1083 eof = true;
1084 } else {
1085 self.pending.extend_from_slice(&scratch[..n]);
1086 }
1087 } else if eof && self.pending.is_empty() && out_pos != expected_size {
1088 return Err(Error::CorruptObject(format!(
1089 "pack stream truncated (zlib) at offset {}",
1090 self.stream_pos
1091 )));
1092 }
1093 }
1094 }
1095 }
1096 }
1097
1098 fn finalize_hasher(&self) -> Vec<u8> {
1100 self.pack_hasher.clone().finalize()
1101 }
1102
1103 fn read_trailer(&mut self) -> Result<Vec<u8>> {
1105 let hash_len = self.pack_hasher.len();
1106 let mut b = vec![0u8; hash_len];
1107 if self.pending.len() >= hash_len {
1108 b.copy_from_slice(&self.pending[..hash_len]);
1109 self.pending.drain(..hash_len);
1110 self.stream_pos += hash_len;
1111 self.enforce_max_input()?;
1112 return Ok(b);
1113 }
1114 let tail = self.pending.len();
1115 if tail > 0 {
1116 b[..tail].copy_from_slice(&self.pending[..]);
1117 self.pending.clear();
1118 }
1119 self.inner
1120 .read_exact(&mut b[tail..])
1121 .map_err(|e| io_to_corrupt_eof(e, self.stream_pos, "trailer"))?;
1122 self.stream_pos += hash_len;
1123 self.enforce_max_input()?;
1124 Ok(b)
1125 }
1126}
1127
1128pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
1142 let mut pos = 0usize;
1143
1144 let src_size = read_delta_varint(delta, &mut pos)?;
1145 if src_size != base.len() {
1146 return Err(Error::CorruptObject(format!(
1147 "delta source size {src_size} != base size {}",
1148 base.len()
1149 )));
1150 }
1151 let dest_size = read_delta_varint(delta, &mut pos)?;
1152 let mut result = Vec::with_capacity(dest_size);
1153
1154 while pos < delta.len() {
1155 let cmd = delta[pos];
1156 pos += 1;
1157 if cmd == 0 {
1158 return Err(Error::CorruptObject(
1159 "reserved opcode 0 in delta stream".to_owned(),
1160 ));
1161 }
1162 if cmd & 0x80 != 0 {
1163 let mut offset = 0usize;
1166 let mut size = 0usize;
1167
1168 macro_rules! maybe_read_byte {
1169 ($flag:expr, $shift:expr, $target:expr) => {
1170 if cmd & $flag != 0 {
1171 let b = *delta.get(pos).ok_or_else(|| {
1172 Error::CorruptObject("truncated delta COPY operand".to_owned())
1173 })?;
1174 pos += 1;
1175 $target |= (b as usize) << $shift;
1176 }
1177 };
1178 }
1179
1180 maybe_read_byte!(0x01, 0, offset);
1181 maybe_read_byte!(0x02, 8, offset);
1182 maybe_read_byte!(0x04, 16, offset);
1183 maybe_read_byte!(0x08, 24, offset);
1184 maybe_read_byte!(0x10, 0, size);
1185 maybe_read_byte!(0x20, 8, size);
1186 maybe_read_byte!(0x40, 16, size);
1187
1188 if size == 0 {
1189 size = 0x10000;
1190 }
1191
1192 let end = offset.checked_add(size).ok_or_else(|| {
1193 Error::CorruptObject("delta COPY range overflows usize".to_owned())
1194 })?;
1195 let chunk = base.get(offset..end).ok_or_else(|| {
1196 Error::CorruptObject(format!(
1197 "delta COPY [{offset},{end}) out of range (base is {} bytes)",
1198 base.len()
1199 ))
1200 })?;
1201 result.extend_from_slice(chunk);
1202 } else {
1203 let n = cmd as usize;
1205 let chunk = delta
1206 .get(pos..pos + n)
1207 .ok_or_else(|| Error::CorruptObject("truncated delta INSERT data".to_owned()))?;
1208 result.extend_from_slice(chunk);
1209 pos += n;
1210 }
1211 }
1212
1213 if result.len() != dest_size {
1214 return Err(Error::CorruptObject(format!(
1215 "delta produced {} bytes but expected {dest_size}",
1216 result.len()
1217 )));
1218 }
1219
1220 Ok(result)
1221}
1222
1223fn read_delta_varint(data: &[u8], pos: &mut usize) -> Result<usize> {
1227 let mut value = 0usize;
1228 let mut shift = 0u32;
1229 loop {
1230 let b = *data
1231 .get(*pos)
1232 .ok_or_else(|| Error::CorruptObject("truncated delta varint".to_owned()))?;
1233 *pos += 1;
1234 value |= ((b & 0x7f) as usize) << shift;
1235 shift += 7;
1236 if b & 0x80 == 0 {
1237 break;
1238 }
1239 }
1240 Ok(value)
1241}
1242
1243#[cfg(test)]
1244mod tests {
1245 use super::*;
1246
1247 fn make_pack(objects: &[(ObjectKind, &[u8])]) -> Vec<u8> {
1250 use flate2::write::ZlibEncoder;
1251 use std::io::Write;
1252
1253 let mut entries: Vec<Vec<u8>> = Vec::new();
1254 for (kind, data) in objects {
1255 let type_code: u8 = match kind {
1256 ObjectKind::Commit => 1,
1257 ObjectKind::Tree => 2,
1258 ObjectKind::Blob => 3,
1259 ObjectKind::Tag => 4,
1260 };
1261 let mut header = Vec::new();
1263 let mut size = data.len();
1264 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
1265 size >>= 4;
1266 if size > 0 {
1267 header.push(first | 0x80);
1268 while size > 0 {
1269 let b = (size & 0x7f) as u8;
1270 size >>= 7;
1271 header.push(if size > 0 { b | 0x80 } else { b });
1272 }
1273 } else {
1274 header.push(first);
1275 }
1276 let mut enc = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1278 enc.write_all(data).unwrap();
1279 let compressed = enc.finish().unwrap();
1280 let mut entry = header;
1281 entry.extend_from_slice(&compressed);
1282 entries.push(entry);
1283 }
1284
1285 let mut pack = Vec::new();
1287 pack.extend_from_slice(b"PACK");
1288 pack.extend_from_slice(&2u32.to_be_bytes());
1289 pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
1290 for entry in &entries {
1291 pack.extend_from_slice(entry);
1292 }
1293 let mut hasher = Sha1::new();
1294 hasher.update(&pack);
1295 let digest = hasher.finalize();
1296 pack.extend_from_slice(digest.as_slice());
1297 pack
1298 }
1299
1300 #[test]
1301 fn test_apply_delta_simple() {
1302 let base = b"hello";
1304 let mut delta = Vec::new();
1305 delta.push(5u8);
1307 delta.push(11u8);
1309 delta.push(0x80 | 0x01 | 0x10); delta.push(0u8); delta.push(5u8); delta.push(6u8);
1316 delta.extend_from_slice(b" world");
1317
1318 let result = apply_delta(base, &delta).unwrap();
1319 assert_eq!(result, b"hello world");
1320 }
1321
1322 #[test]
1323 fn test_apply_delta_insert_only() {
1324 let base = b"";
1325 let mut delta = Vec::new();
1326 delta.push(0u8); delta.push(5u8); delta.push(5u8); delta.extend_from_slice(b"hello");
1330
1331 let result = apply_delta(base, &delta).unwrap();
1332 assert_eq!(result, b"hello");
1333 }
1334
1335 #[test]
1336 fn test_apply_delta_copy_only() {
1337 let base = b"abcdef";
1338 let mut delta = Vec::new();
1339 delta.push(6u8); delta.push(3u8); delta.push(0x91u8);
1344 delta.push(2u8); delta.push(3u8); let result = apply_delta(base, &delta).unwrap();
1348 assert_eq!(result, b"cde");
1349 }
1350
1351 #[test]
1352 fn test_apply_delta_size_zero_means_65536() {
1353 let base = vec![0xABu8; 65536];
1355 let mut delta = Vec::new();
1356 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);
1362 delta.push(0x80 | ((65536 >> 7) & 0x7f) as u8);
1363 delta.push(((65536 >> 14) & 0x7f) as u8);
1364 delta.push(0x80u8);
1367
1368 let result = apply_delta(&base, &delta).unwrap();
1369 assert_eq!(result.len(), 65536);
1370 assert!(result.iter().all(|&b| b == 0xAB));
1371 }
1372
1373 #[test]
1374 fn test_unpack_objects_blobs() {
1375 use tempfile::TempDir;
1376 let tmp = TempDir::new().unwrap();
1377 let objects_dir = tmp.path().join("objects");
1378 std::fs::create_dir_all(&objects_dir).unwrap();
1379 let odb = Odb::new(&objects_dir);
1380
1381 let pack = make_pack(&[
1382 (ObjectKind::Blob, b"hello\n"),
1383 (ObjectKind::Blob, b"world\n"),
1384 ]);
1385
1386 let opts = UnpackOptions::default();
1387 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
1388 assert_eq!(count, 2);
1389
1390 let oid1 = Odb::hash_object_data(ObjectKind::Blob, b"hello\n");
1392 let oid2 = Odb::hash_object_data(ObjectKind::Blob, b"world\n");
1393 let obj1 = odb.read(&oid1).unwrap();
1394 let obj2 = odb.read(&oid2).unwrap();
1395 assert_eq!(obj1.data, b"hello\n");
1396 assert_eq!(obj2.data, b"world\n");
1397 }
1398
1399 #[test]
1400 fn test_unpack_objects_empty_tree() {
1401 use tempfile::TempDir;
1402 let tmp = TempDir::new().unwrap();
1403 let objects_dir = tmp.path().join("objects");
1404 std::fs::create_dir_all(&objects_dir).unwrap();
1405 let odb = Odb::new(&objects_dir);
1406
1407 let pack = make_pack(&[(ObjectKind::Tree, b"")]);
1408 let opts = UnpackOptions::default();
1409 assert_eq!(
1410 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
1411 1
1412 );
1413 let oid = Odb::hash_object_data(ObjectKind::Tree, b"");
1414 assert!(odb.exists(&oid));
1415 let loose = objects_dir
1416 .join(oid.loose_prefix())
1417 .join(oid.loose_suffix());
1418 assert!(
1419 loose.is_file(),
1420 "empty tree must be materialized as a loose object during unpack"
1421 );
1422 }
1423
1424 #[test]
1425 fn test_strict_skips_gitlink_tree_entries() {
1426 use crate::index::{MODE_GITLINK, MODE_REGULAR};
1427 use crate::objects::{serialize_tree, TreeEntry};
1428
1429 let submodule_oid = ObjectId::from_hex(&"7f".repeat(20)).unwrap();
1432
1433 let tree_data = serialize_tree(&[TreeEntry {
1435 mode: MODE_GITLINK,
1436 name: b"sub".to_vec(),
1437 oid: submodule_oid,
1438 }]);
1439 let tree_oid = Odb::hash_object_data(ObjectKind::Tree, &tree_data);
1440
1441 let mut pack = HashMap::new();
1444 pack.insert(tree_oid, (ObjectKind::Tree, tree_data.clone()));
1445 assert!(strict_verify_packed_references(None, &pack).is_ok());
1446
1447 let bad_tree = serialize_tree(&[TreeEntry {
1450 mode: MODE_REGULAR,
1451 name: b"file".to_vec(),
1452 oid: ObjectId::from_hex(&"ab".repeat(20)).unwrap(),
1453 }]);
1454 let bad_oid = Odb::hash_object_data(ObjectKind::Tree, &bad_tree);
1455 let mut bad_pack = HashMap::new();
1456 bad_pack.insert(bad_oid, (ObjectKind::Tree, bad_tree));
1457 assert!(matches!(
1458 strict_verify_packed_references(None, &bad_pack),
1459 Err(Error::CorruptObject(_))
1460 ));
1461 }
1462
1463 struct ChunkedReader<'a> {
1465 data: &'a [u8],
1466 pos: usize,
1467 max_len: usize,
1468 }
1469
1470 impl io::Read for ChunkedReader<'_> {
1471 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1472 if self.pos >= self.data.len() {
1473 return Ok(0);
1474 }
1475 let take = (self.data.len() - self.pos)
1476 .min(self.max_len)
1477 .min(buf.len());
1478 buf[..take].copy_from_slice(&self.data[self.pos..self.pos + take]);
1479 self.pos += take;
1480 Ok(take)
1481 }
1482 }
1483
1484 #[test]
1485 fn test_unpack_objects_chunked_read_matches_full_buffer() {
1486 use tempfile::TempDir;
1487 let pack = make_pack(&[(ObjectKind::Blob, b"chunked-stream")]);
1488 let opts = UnpackOptions::default();
1489 let oid = Odb::hash_object_data(ObjectKind::Blob, b"chunked-stream");
1490
1491 let tmp = TempDir::new().unwrap();
1492 let objects_dir = tmp.path().join("objects");
1493 std::fs::create_dir_all(&objects_dir).unwrap();
1494 let odb = Odb::new(&objects_dir);
1495 assert_eq!(
1496 unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap(),
1497 1
1498 );
1499 assert!(odb.exists(&oid));
1500
1501 let tmp2 = TempDir::new().unwrap();
1502 let objects_dir2 = tmp2.path().join("objects");
1503 std::fs::create_dir_all(&objects_dir2).unwrap();
1504 let odb2 = Odb::new(&objects_dir2);
1505 let mut chunked = ChunkedReader {
1506 data: pack.as_slice(),
1507 pos: 0,
1508 max_len: 8,
1509 };
1510 assert_eq!(unpack_objects(&mut chunked, &odb2, &opts).unwrap(), 1);
1511 assert!(odb2.exists(&oid));
1512 }
1513
1514 #[test]
1515 fn test_unpack_objects_dry_run_writes_nothing() {
1516 use tempfile::TempDir;
1517 let tmp = TempDir::new().unwrap();
1518 let objects_dir = tmp.path().join("objects");
1519 std::fs::create_dir_all(&objects_dir).unwrap();
1520 let odb = Odb::new(&objects_dir);
1521
1522 let pack = make_pack(&[(ObjectKind::Blob, b"test content")]);
1523
1524 let opts = UnpackOptions {
1525 dry_run: true,
1526 quiet: true,
1527 strict: false,
1528 allowed_missing: Default::default(),
1529 allow_promisor_missing_references: false,
1530 max_input_bytes: None,
1531 ..Default::default()
1532 };
1533 let count = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap();
1534 assert_eq!(count, 1);
1535
1536 let oid = Odb::hash_object_data(ObjectKind::Blob, b"test content");
1538 assert!(!odb.exists(&oid));
1539 }
1540
1541 #[test]
1542 fn test_unpack_objects_bad_signature() {
1543 use tempfile::TempDir;
1544 let tmp = TempDir::new().unwrap();
1545 let objects_dir = tmp.path().join("objects");
1546 std::fs::create_dir_all(&objects_dir).unwrap();
1547 let odb = Odb::new(&objects_dir);
1548
1549 let mut bad = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00".to_vec();
1550 bad.extend_from_slice(&[0u8; 20]);
1551 let opts = UnpackOptions::default();
1552 let err = unpack_objects(&mut bad.as_slice(), &odb, &opts).unwrap_err();
1553 assert!(err.to_string().contains("invalid signature"));
1554 }
1555
1556 #[test]
1557 fn test_unpack_objects_checksum_mismatch() {
1558 use tempfile::TempDir;
1559 let tmp = TempDir::new().unwrap();
1560 let objects_dir = tmp.path().join("objects");
1561 std::fs::create_dir_all(&objects_dir).unwrap();
1562 let odb = Odb::new(&objects_dir);
1563
1564 let mut pack = make_pack(&[(ObjectKind::Blob, b"data")]);
1565 let n = pack.len();
1567 pack[n - 1] ^= 0xFF;
1568
1569 let opts = UnpackOptions::default();
1570 let err = unpack_objects(&mut pack.as_slice(), &odb, &opts).unwrap_err();
1571 assert!(err.to_string().contains("checksum"));
1572 }
1573
1574 #[test]
1575 fn test_apply_delta_source_size_mismatch() {
1576 let base = b"hi";
1577 let delta = [3u8, 2u8, 2u8, b'h', b'i']; let err = apply_delta(base, &delta).unwrap_err();
1579 assert!(err.to_string().contains("source size"));
1580 }
1581}