1use crate::error::{Error, Result};
7use crate::objects::{Object, ObjectId, ObjectKind};
8use crate::unpack_objects::apply_delta;
9use flate2::read::ZlibDecoder;
10use sha1::{Digest, Sha1};
11use sha2::Sha256;
12use std::collections::{BTreeMap, HashMap, HashSet};
13use std::fs;
14use std::io;
15use std::io::Read;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19#[derive(Debug, Clone)]
21pub struct PackIndexEntry {
22 pub oid: Vec<u8>,
24 pub offset: u64,
26}
27
28#[derive(Debug, Clone)]
30pub struct PackIndex {
31 pub idx_path: PathBuf,
33 pub pack_path: PathBuf,
35 pub hash_bytes: usize,
37 pub entries: Vec<PackIndexEntry>,
39 pub fanout: [u32; 256],
43}
44
45impl PackIndex {
46 #[must_use]
52 pub fn find_offset(&self, oid: &ObjectId) -> Option<u64> {
53 if self.hash_bytes != 20 {
54 return None;
55 }
56 let needle = oid.as_bytes();
57 let first_byte = needle[0] as usize;
58 let lo = if first_byte == 0 {
59 0
60 } else {
61 self.fanout[first_byte - 1] as usize
62 };
63 let hi = self.fanout[first_byte] as usize;
64 if lo >= hi || hi > self.entries.len() {
65 return None;
66 }
67 let slice = &self.entries[lo..hi];
68 slice
69 .binary_search_by(|e| e.oid.as_slice().cmp(needle.as_slice()))
70 .ok()
71 .map(|idx| slice[idx].offset)
72 }
73
74 #[must_use]
76 pub fn contains(&self, oid: &ObjectId) -> bool {
77 self.find_offset(oid).is_some()
78 }
79}
80
81#[derive(Debug, Clone)]
86pub struct ShowIndexEntry {
87 pub oid: Vec<u8>,
89 pub offset: u64,
91 pub crc32: Option<u32>,
93}
94
95pub fn show_index_entries(reader: &mut dyn Read, hash_size: usize) -> Result<Vec<ShowIndexEntry>> {
106 let mut buf = Vec::new();
107 reader.read_to_end(&mut buf).map_err(Error::Io)?;
108
109 if buf.len() < 8 {
110 return Err(Error::CorruptObject(
111 "unable to read header: index file too small".to_owned(),
112 ));
113 }
114
115 let mut pos = 0usize;
116 let first_u32 = read_u32_be(&buf, &mut pos)?;
117
118 const PACK_IDX_SIGNATURE: u32 = 0xff74_4f63;
119
120 if first_u32 == PACK_IDX_SIGNATURE {
121 let version = read_u32_be(&buf, &mut pos)?;
123 if version != 2 {
124 return Err(Error::CorruptObject(format!(
125 "unknown index version: {version}"
126 )));
127 }
128 show_index_v2(&buf, &mut pos, hash_size)
129 } else {
130 pos = 0;
133 show_index_v1(&buf, &mut pos, hash_size)
134 }
135}
136
137fn show_index_v1(buf: &[u8], pos: &mut usize, hash_size: usize) -> Result<Vec<ShowIndexEntry>> {
139 if buf.len() < 256 * 4 {
140 return Err(Error::CorruptObject(
141 "unable to read index: v1 fanout too short".to_owned(),
142 ));
143 }
144 let mut fanout = [0u32; 256];
145 for slot in &mut fanout {
146 *slot = read_u32_be(buf, pos)?;
147 }
148 let object_count = fanout[255] as usize;
149
150 let mut entries = Vec::with_capacity(object_count);
151 for i in 0..object_count {
152 if *pos + 4 + hash_size > buf.len() {
154 return Err(Error::CorruptObject(format!(
155 "unable to read entry {i}/{object_count}: truncated"
156 )));
157 }
158 let offset = read_u32_be(buf, pos)? as u64;
159 let oid = buf[*pos..*pos + hash_size].to_vec();
160 *pos += hash_size;
161 entries.push(ShowIndexEntry {
162 oid,
163 offset,
164 crc32: None,
165 });
166 }
167 Ok(entries)
168}
169
170fn show_index_v2(buf: &[u8], pos: &mut usize, hash_size: usize) -> Result<Vec<ShowIndexEntry>> {
173 if buf.len() < *pos + 256 * 4 {
174 return Err(Error::CorruptObject(
175 "unable to read index: v2 fanout too short".to_owned(),
176 ));
177 }
178 let mut fanout = [0u32; 256];
179 for slot in &mut fanout {
180 *slot = read_u32_be(buf, pos)?;
181 }
182 let object_count = fanout[255] as usize;
183
184 let mut oids: Vec<Vec<u8>> = Vec::with_capacity(object_count);
186 for i in 0..object_count {
187 if *pos + hash_size > buf.len() {
188 return Err(Error::CorruptObject(format!(
189 "unable to read oid {i}/{object_count}: truncated"
190 )));
191 }
192 let oid = buf[*pos..*pos + hash_size].to_vec();
193 *pos += hash_size;
194 oids.push(oid);
195 }
196
197 let mut crcs = Vec::with_capacity(object_count);
199 for i in 0..object_count {
200 if *pos + 4 > buf.len() {
201 return Err(Error::CorruptObject(format!(
202 "unable to read crc {i}/{object_count}: truncated"
203 )));
204 }
205 crcs.push(read_u32_be(buf, pos)?);
206 }
207
208 let mut offsets32 = Vec::with_capacity(object_count);
210 let mut large_count = 0usize;
211 for i in 0..object_count {
212 if *pos + 4 > buf.len() {
213 return Err(Error::CorruptObject(format!(
214 "unable to read 32b offset {i}/{object_count}: truncated"
215 )));
216 }
217 let v = read_u32_be(buf, pos)?;
218 if (v & 0x8000_0000) != 0 {
219 large_count += 1;
220 }
221 offsets32.push(v);
222 }
223
224 let mut large_offsets = Vec::with_capacity(large_count);
226 for i in 0..large_count {
227 if *pos + 8 > buf.len() {
228 return Err(Error::CorruptObject(format!(
229 "unable to read 64b offset {i}: truncated"
230 )));
231 }
232 large_offsets.push(read_u64_be(buf, pos)?);
233 }
234
235 let mut next_large = 0usize;
236 let mut entries = Vec::with_capacity(object_count);
237 for (i, oid) in oids.iter().enumerate() {
238 let raw = offsets32[i];
239 let offset = if (raw & 0x8000_0000) == 0 {
240 raw as u64
241 } else {
242 let idx = (raw & 0x7fff_ffff) as usize;
243 if idx != next_large {
244 return Err(Error::CorruptObject(format!(
245 "inconsistent 64b offset index at entry {i}"
246 )));
247 }
248 let off = large_offsets.get(next_large).copied().ok_or_else(|| {
249 Error::CorruptObject(format!("missing large offset entry {next_large}"))
250 })?;
251 next_large += 1;
252 off
253 };
254 entries.push(ShowIndexEntry {
255 oid: oid.clone(),
256 offset,
257 crc32: Some(crcs[i]),
258 });
259 }
260 Ok(entries)
261}
262
263#[derive(Debug, Clone, Default)]
265pub struct LocalPackInfo {
266 pub pack_count: usize,
268 pub object_count: usize,
270 pub size_bytes: u64,
272 pub object_ids: HashSet<ObjectId>,
274}
275
276pub fn read_local_pack_indexes(objects_dir: &Path) -> Result<Vec<PackIndex>> {
283 let pack_dir = objects_dir.join("pack");
284 let rd = match fs::read_dir(&pack_dir) {
285 Ok(rd) => rd,
286 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
287 Err(err) => return Err(Error::Io(err)),
288 };
289
290 let mut out = Vec::new();
291 for entry in rd {
292 let entry = entry.map_err(Error::Io)?;
293 let path = entry.path();
294 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
295 continue;
296 }
297 if let Ok(idx) = read_pack_index(&path) {
298 if !idx.pack_path.is_file() {
301 continue;
302 }
303 out.push(idx);
304 }
305 }
306 Ok(out)
307}
308
309mod pack_cache {
323 use super::{read_pack_index_no_verify, Error, PackIndex, Result};
324 use std::collections::HashMap;
325 use std::fs;
326 use std::io;
327 use std::path::{Path, PathBuf};
328 use std::sync::{Arc, Mutex, OnceLock};
329 use std::time::SystemTime;
330
331 struct CachedDir {
332 dir_mtime: SystemTime,
333 indexes: Vec<Arc<PackIndex>>,
334 }
335
336 struct CachedIdx {
337 mtime: SystemTime,
338 size: u64,
339 idx: Arc<PackIndex>,
340 }
341
342 struct CachedPack {
343 mtime: SystemTime,
344 size: u64,
345 bytes: Arc<Vec<u8>>,
346 }
347
348 #[derive(Default)]
349 struct State {
350 by_dir: HashMap<PathBuf, CachedDir>,
351 by_idx: HashMap<PathBuf, CachedIdx>,
352 by_pack: HashMap<PathBuf, CachedPack>,
353 }
354
355 static CACHE: OnceLock<Mutex<State>> = OnceLock::new();
356
357 fn lock() -> std::sync::MutexGuard<'static, State> {
358 CACHE
359 .get_or_init(|| Mutex::new(State::default()))
360 .lock()
361 .unwrap_or_else(|p| p.into_inner())
362 }
363
364 fn dir_mtime(path: &Path) -> SystemTime {
365 fs::metadata(path)
366 .and_then(|m| m.modified())
367 .unwrap_or(SystemTime::UNIX_EPOCH)
368 }
369
370 fn file_signature(path: &Path) -> Option<(SystemTime, u64)> {
371 let m = fs::metadata(path).ok()?;
372 let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
373 Some((mtime, m.len()))
374 }
375
376 pub fn get_index(idx_path: &Path) -> Result<Arc<PackIndex>> {
379 let sig = file_signature(idx_path);
380 if let Some((mtime, size)) = sig {
381 {
382 let g = lock();
383 if let Some(c) = g.by_idx.get(idx_path) {
384 if c.mtime == mtime && c.size == size {
385 return Ok(Arc::clone(&c.idx));
386 }
387 }
388 }
389 let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
390 let mut g = lock();
391 g.by_idx.insert(
392 idx_path.to_path_buf(),
393 CachedIdx {
394 mtime,
395 size,
396 idx: Arc::clone(&parsed),
397 },
398 );
399 Ok(parsed)
400 } else {
401 Err(Error::Io(io::Error::new(
402 io::ErrorKind::NotFound,
403 format!("idx not found: {}", idx_path.display()),
404 )))
405 }
406 }
407
408 pub fn get_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
411 let pack_dir = objects_dir.join("pack");
412 let dir_mt = dir_mtime(&pack_dir);
413
414 {
415 let g = lock();
416 if let Some(c) = g.by_dir.get(&pack_dir) {
417 if c.dir_mtime == dir_mt {
418 return Ok(c.indexes.clone());
419 }
420 }
421 }
422
423 let rd = match fs::read_dir(&pack_dir) {
424 Ok(rd) => rd,
425 Err(err) if err.kind() == io::ErrorKind::NotFound => {
426 let mut g = lock();
427 g.by_dir.insert(
428 pack_dir.clone(),
429 CachedDir {
430 dir_mtime: dir_mt,
431 indexes: Vec::new(),
432 },
433 );
434 return Ok(Vec::new());
435 }
436 Err(err) => return Err(Error::Io(err)),
437 };
438
439 let mut out = Vec::new();
440 for entry in rd {
441 let entry = entry.map_err(Error::Io)?;
442 let path = entry.path();
443 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
444 continue;
445 }
446 let Ok(idx) = get_index(&path) else { continue };
447 if !idx.pack_path.is_file() {
448 continue;
449 }
450 out.push(idx);
451 }
452
453 let mut g = lock();
454 g.by_dir.insert(
455 pack_dir,
456 CachedDir {
457 dir_mtime: dir_mt,
458 indexes: out.clone(),
459 },
460 );
461 Ok(out)
462 }
463
464 pub fn get_pack_bytes(pack_path: &Path) -> Result<Arc<Vec<u8>>> {
467 let sig = file_signature(pack_path);
468 if let Some((mtime, size)) = sig {
469 {
470 let g = lock();
471 if let Some(c) = g.by_pack.get(pack_path) {
472 if c.mtime == mtime && c.size == size {
473 return Ok(Arc::clone(&c.bytes));
474 }
475 }
476 }
477 let bytes = Arc::new(fs::read(pack_path).map_err(Error::Io)?);
478 let mut g = lock();
479 g.by_pack.insert(
480 pack_path.to_path_buf(),
481 CachedPack {
482 mtime,
483 size,
484 bytes: Arc::clone(&bytes),
485 },
486 );
487 Ok(bytes)
488 } else {
489 Err(Error::Io(io::Error::new(
490 io::ErrorKind::NotFound,
491 format!("pack not found: {}", pack_path.display()),
492 )))
493 }
494 }
495
496 pub fn clear() {
499 let mut g = lock();
500 g.by_dir.clear();
501 g.by_idx.clear();
502 g.by_pack.clear();
503 }
504
505 pub fn refresh_pack_signature(pack_path: &Path) {
511 if let Some((mtime, size)) = file_signature(pack_path) {
512 let mut g = lock();
513 if let Some(c) = g.by_pack.get_mut(pack_path) {
514 if c.size == size {
515 c.mtime = mtime;
516 }
517 }
518 }
519 }
520}
521
522pub fn read_local_pack_indexes_cached(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
533 pack_cache::get_dir_indexes(objects_dir)
534}
535
536pub fn read_pack_index_cached(idx_path: &Path) -> Result<Arc<PackIndex>> {
544 pack_cache::get_index(idx_path)
545}
546
547pub fn read_pack_bytes_cached(pack_path: &Path) -> Result<Arc<Vec<u8>>> {
553 pack_cache::get_pack_bytes(pack_path)
554}
555
556pub fn clear_pack_cache() {
558 pack_cache::clear();
559}
560
561pub fn refresh_pack_bytes_signature(pack_path: &Path) {
564 pack_cache::refresh_pack_signature(pack_path);
565}
566
567pub fn collect_local_pack_info(objects_dir: &Path) -> Result<LocalPackInfo> {
573 let indexes = read_local_pack_indexes(objects_dir)?;
574 let mut info = LocalPackInfo::default();
575 for idx in indexes {
576 let pack_meta = fs::metadata(&idx.pack_path).map_err(Error::Io)?;
577 let idx_meta = fs::metadata(&idx.idx_path).map_err(Error::Io)?;
578 info.pack_count += 1;
579 info.object_count += idx.entries.len();
580 info.size_bytes += pack_meta.len() + idx_meta.len();
581 for entry in idx.entries {
582 if entry.oid.len() == 20 {
583 if let Ok(oid) = ObjectId::from_bytes(&entry.oid) {
584 info.object_ids.insert(oid);
585 }
586 }
587 }
588 }
589 Ok(info)
590}
591
592fn verify_idx_trailing_checksum(idx_path: &Path, bytes: &[u8]) -> Result<()> {
593 if bytes.len() < 20 {
594 return Err(Error::CorruptObject(format!(
595 "index file {} missing checksum",
596 idx_path.display()
597 )));
598 }
599 let idx_body_end = bytes.len() - 20;
600 let mut h = Sha1::new();
601 h.update(&bytes[..idx_body_end]);
602 let digest = h.finalize();
603 if digest.as_slice() != &bytes[idx_body_end..] {
604 return Err(Error::CorruptObject(format!(
605 "index checksum mismatch for {}",
606 idx_path.display()
607 )));
608 }
609 Ok(())
610}
611
612fn read_pack_index_v1(idx_path: &Path, bytes: &[u8], verify: bool) -> Result<PackIndex> {
613 let mut pos = 0usize;
614 if bytes.len() < 256 * 4 + 20 {
615 return Err(Error::CorruptObject(format!(
616 "index file {} is too small",
617 idx_path.display()
618 )));
619 }
620 let mut fanout = [0u32; 256];
621 for slot in &mut fanout {
622 *slot = read_u32_be(bytes, &mut pos)?;
623 }
624 let object_count = fanout[255] as usize;
625 let need = pos
626 .saturating_add(object_count.saturating_mul(24))
627 .saturating_add(20);
628 if bytes.len() < need {
629 return Err(Error::CorruptObject(format!(
630 "truncated idx file {}",
631 idx_path.display()
632 )));
633 }
634
635 let mut entries: Vec<PackIndexEntry> = Vec::with_capacity(object_count);
636 for i in 0..object_count {
637 let offset = read_u32_be(bytes, &mut pos)? as u64;
638 let oid = bytes[pos..pos + 20].to_vec();
639 pos += 20;
640 if i > 0 && entries[i - 1].oid.cmp(&oid) != std::cmp::Ordering::Less {
641 return Err(Error::CorruptObject(format!(
642 "oid lookup out of order in {}",
643 idx_path.display()
644 )));
645 }
646 entries.push(PackIndexEntry { oid, offset });
647 }
648
649 if verify {
650 verify_idx_trailing_checksum(idx_path, bytes)?;
651 }
652
653 let mut pack_path = idx_path.to_path_buf();
654 pack_path.set_extension("pack");
655
656 let fanout = compute_fanout_from_entries(&entries);
657 Ok(PackIndex {
658 idx_path: idx_path.to_path_buf(),
659 pack_path,
660 hash_bytes: 20,
661 entries,
662 fanout,
663 })
664}
665
666fn compute_fanout_from_entries(entries: &[PackIndexEntry]) -> [u32; 256] {
669 let mut fanout = [0u32; 256];
670 let mut idx = 0usize;
671 for byte in 0u32..256 {
672 let needle = byte as u8;
673 while idx < entries.len() && entries[idx].oid.first().copied().unwrap_or(0) <= needle {
674 idx += 1;
675 }
676 fanout[byte as usize] = u32::try_from(idx).unwrap_or(u32::MAX);
677 }
678 fanout
679}
680
681fn read_pack_index_v2(idx_path: &Path, bytes: &[u8], verify: bool) -> Result<PackIndex> {
682 if bytes.len() < 8 + 256 * 4 + 40 {
683 return Err(Error::CorruptObject(format!(
684 "index file {} is too small",
685 idx_path.display()
686 )));
687 }
688
689 let mut pos = 0usize;
690 pos += 4;
691 let version = read_u32_be(bytes, &mut pos)?;
692 if version != 2 {
693 return Err(Error::CorruptObject(format!(
694 "unsupported idx version {} in {}",
695 version,
696 idx_path.display()
697 )));
698 }
699
700 let mut fanout = [0u32; 256];
701 for slot in &mut fanout {
702 *slot = read_u32_be(bytes, &mut pos)?;
703 }
704 let object_count = fanout[255] as usize;
705
706 let idx_file_len = bytes.len();
707 let hash_bytes = detect_idx_hash_bytes_v2(idx_file_len, pos, object_count, idx_path)?;
708
709 let need = pos
710 .saturating_add(object_count * hash_bytes)
711 .saturating_add(object_count * 4)
712 .saturating_add(object_count * 4)
713 .saturating_add(40);
714 if bytes.len() < need {
715 return Err(Error::CorruptObject(format!(
716 "truncated idx file {}",
717 idx_path.display()
718 )));
719 }
720
721 let mut oids: Vec<Vec<u8>> = Vec::with_capacity(object_count);
722 for _ in 0..object_count {
723 let slice = &bytes[pos..pos + hash_bytes];
724 pos += hash_bytes;
725 oids.push(slice.to_vec());
726 }
727
728 pos += object_count * 4;
729
730 let mut offsets32 = Vec::with_capacity(object_count);
731 let mut large_count = 0usize;
732 for _ in 0..object_count {
733 let v = read_u32_be(bytes, &mut pos)?;
734 if (v & 0x8000_0000) != 0 {
735 large_count += 1;
736 }
737 offsets32.push(v);
738 }
739
740 if bytes.len() < pos + large_count * 8 + 40 {
741 return Err(Error::CorruptObject(format!(
742 "truncated large offset table in {}",
743 idx_path.display()
744 )));
745 }
746 let mut large_offsets = Vec::with_capacity(large_count);
747 for _ in 0..large_count {
748 large_offsets.push(read_u64_be(bytes, &mut pos)?);
749 }
750
751 let mut next_large = 0usize;
752 let mut entries = Vec::with_capacity(object_count);
753 for (i, oid) in oids.into_iter().enumerate() {
754 let raw = offsets32[i];
755 let offset = if (raw & 0x8000_0000) == 0 {
756 raw as u64
757 } else {
758 let off = large_offsets.get(next_large).copied().ok_or_else(|| {
759 Error::CorruptObject(format!("bad large offset index in {}", idx_path.display()))
760 })?;
761 next_large += 1;
762 off
763 };
764 entries.push(PackIndexEntry { oid, offset });
765 }
766
767 let mut pack_path = idx_path.to_path_buf();
768 pack_path.set_extension("pack");
769
770 if verify {
771 verify_idx_trailing_checksum(idx_path, bytes)?;
772 }
773
774 Ok(PackIndex {
775 idx_path: idx_path.to_path_buf(),
776 pack_path,
777 hash_bytes,
778 entries,
779 fanout,
780 })
781}
782
783fn detect_idx_hash_bytes_v2(
788 idx_file_len: usize,
789 fanout_end: usize,
790 object_count: usize,
791 idx_path: &Path,
792) -> Result<usize> {
793 if object_count == 0 {
794 return Ok(20);
795 }
796 if idx_file_len < 20 {
797 return Err(Error::CorruptObject(format!(
798 "index file {} missing checksum",
799 idx_path.display()
800 )));
801 }
802 let body_without_checksum = idx_file_len.saturating_sub(20);
803
804 for &hb in &[20usize, 32] {
805 let min_body = fanout_end
808 .saturating_add(object_count.saturating_mul(hb + 4 + 4))
809 .saturating_add(hb);
810 if body_without_checksum < min_body {
811 continue;
812 }
813 let mut max_body = min_body;
814 if object_count > 0 {
815 max_body = max_body.saturating_add((object_count - 1).saturating_mul(8));
816 }
817 if body_without_checksum > max_body {
818 continue;
819 }
820 let extra = body_without_checksum.saturating_sub(min_body);
821 if extra % 8 != 0 {
822 continue;
823 }
824 return Ok(hb);
825 }
826
827 Err(Error::CorruptObject(format!(
828 "wrong index v2 file size in {}",
829 idx_path.display()
830 )))
831}
832
833#[must_use]
834pub fn oid_bytes_to_hex(oid: &[u8]) -> String {
835 hex::encode(oid)
836}
837
838#[must_use]
840pub fn pack_index_entry_matches_sha1_oid(entry: &PackIndexEntry, oid: &ObjectId) -> bool {
841 entry.oid.len() == 20 && entry.oid.as_slice() == oid.as_bytes().as_slice()
842}
843
844pub fn hash_object_bytes(kind: ObjectKind, data: &[u8], hash_bytes: usize) -> Result<Vec<u8>> {
846 let header = format!("{} {}\0", kind, data.len());
847 match hash_bytes {
848 20 => {
849 let mut hasher = Sha1::new();
850 hasher.update(header.as_bytes());
851 hasher.update(data);
852 Ok(hasher.finalize().to_vec())
853 }
854 32 => {
855 use sha2::Digest as _;
856 let mut hasher = Sha256::new();
857 hasher.update(header.as_bytes());
858 hasher.update(data);
859 Ok(hasher.finalize().to_vec())
860 }
861 other => Err(Error::CorruptObject(format!(
862 "unsupported object hash width: {other}"
863 ))),
864 }
865}
866
867pub fn read_pack_index(idx_path: &Path) -> Result<PackIndex> {
878 let bytes = fs::read(idx_path).map_err(Error::Io)?;
879 parse_pack_index_bytes(idx_path, &bytes, true)
880}
881
882fn read_pack_index_no_verify(idx_path: &Path) -> Result<PackIndex> {
885 let bytes = fs::read(idx_path).map_err(Error::Io)?;
886 parse_pack_index_bytes(idx_path, &bytes, false)
887}
888
889fn parse_pack_index_bytes(idx_path: &Path, bytes: &[u8], verify: bool) -> Result<PackIndex> {
890 if bytes.len() < 8 {
891 return Err(Error::CorruptObject(format!(
892 "index file {} is too small",
893 idx_path.display()
894 )));
895 }
896 let magic = &bytes[0..4];
897 if magic == [0xff, b't', b'O', b'c'] {
898 read_pack_index_v2(idx_path, bytes, verify)
899 } else {
900 read_pack_index_v1(idx_path, bytes, verify)
901 }
902}
903
904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
906pub enum PackedType {
907 Commit,
909 Tree,
911 Blob,
913 Tag,
915 OfsDelta,
917 RefDelta,
919}
920
921impl PackedType {
922 #[must_use]
924 pub fn as_str(self) -> &'static str {
925 match self {
926 Self::Commit => "commit",
927 Self::Tree => "tree",
928 Self::Blob => "blob",
929 Self::Tag => "tag",
930 Self::OfsDelta => "ofs-delta",
931 Self::RefDelta => "ref-delta",
932 }
933 }
934}
935
936#[derive(Debug, Clone)]
938pub struct VerifyObjectRecord {
939 pub oid: Vec<u8>,
941 pub packed_type: PackedType,
943 pub size: u64,
945 pub size_in_pack: u64,
947 pub offset: u64,
949 pub depth: Option<u64>,
951 pub base_oid: Option<Vec<u8>>,
953}
954
955pub fn verify_pack_and_collect(idx_path: &Path) -> Result<Vec<VerifyObjectRecord>> {
961 let idx = read_pack_index(idx_path)?;
962 let idx_file_bytes = fs::read(idx_path).map_err(Error::Io)?;
963 let pack_bytes = fs::read(&idx.pack_path).map_err(Error::Io)?;
964 let hb = idx.hash_bytes;
965 if pack_bytes.len() < 12 + hb {
966 return Err(Error::CorruptObject(format!(
967 "pack file {} is too small",
968 idx.pack_path.display()
969 )));
970 }
971 let pack_end = pack_bytes.len() - hb;
972 match hb {
973 20 => {
974 let mut h = Sha1::new();
975 h.update(&pack_bytes[..pack_end]);
976 let digest = h.finalize();
977 if digest.as_slice() != &pack_bytes[pack_end..] {
978 return Err(Error::CorruptObject(format!(
979 "pack trailing checksum mismatch for {}",
980 idx.pack_path.display()
981 )));
982 }
983 }
984 32 => {
985 use sha2::Digest as _;
986 let mut h = Sha256::new();
987 h.update(&pack_bytes[..pack_end]);
988 let digest = h.finalize();
989 if digest.as_slice() != &pack_bytes[pack_end..] {
990 return Err(Error::CorruptObject(format!(
991 "pack trailing checksum mismatch for {}",
992 idx.pack_path.display()
993 )));
994 }
995 }
996 _ => {
997 return Err(Error::CorruptObject(format!(
998 "unsupported OID width {} for pack {}",
999 hb,
1000 idx.pack_path.display()
1001 )));
1002 }
1003 }
1004 if idx_file_bytes.len() >= hb + 20 {
1005 let embedded = &idx_file_bytes[idx_file_bytes.len() - (hb + 20)..idx_file_bytes.len() - 20];
1006 if embedded != &pack_bytes[pack_end..] {
1007 return Err(Error::CorruptObject(format!(
1008 "pack checksum in index does not match {}",
1009 idx.pack_path.display()
1010 )));
1011 }
1012 }
1013 if &pack_bytes[0..4] != b"PACK" {
1014 return Err(Error::CorruptObject(format!(
1015 "pack file {} has invalid signature",
1016 idx.pack_path.display()
1017 )));
1018 }
1019 let version = u32::from_be_bytes(pack_bytes[4..8].try_into().unwrap_or([0, 0, 0, 0]));
1020 if version != 2 && version != 3 {
1021 return Err(Error::CorruptObject(format!(
1022 "unsupported pack version {} in {}",
1023 version,
1024 idx.pack_path.display()
1025 )));
1026 }
1027 let count = u32::from_be_bytes(pack_bytes[8..12].try_into().unwrap_or([0, 0, 0, 0])) as usize;
1028 if count != idx.entries.len() {
1029 return Err(Error::CorruptObject(format!(
1030 "pack/index object count mismatch for {}",
1031 idx.pack_path.display()
1032 )));
1033 }
1034
1035 let mut by_offset: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
1036 for entry in &idx.entries {
1037 by_offset.insert(entry.offset, entry.oid.clone());
1038 }
1039 let offsets: Vec<u64> = by_offset.keys().copied().collect();
1040 if offsets.is_empty() {
1041 return Ok(Vec::new());
1042 }
1043
1044 let mut by_oid: HashMap<Vec<u8>, usize> = HashMap::new();
1045 let mut records: Vec<VerifyObjectRecord> = Vec::with_capacity(offsets.len());
1046 for (i, offset) in offsets.iter().copied().enumerate() {
1047 let oid = by_offset.get(&offset).cloned().ok_or_else(|| {
1048 Error::CorruptObject(format!("missing object id for offset {}", offset))
1049 })?;
1050 let next_off = offsets
1051 .get(i + 1)
1052 .copied()
1053 .unwrap_or((pack_bytes.len() - hb) as u64);
1054 if next_off <= offset || next_off > (pack_bytes.len() - hb) as u64 {
1055 return Err(Error::CorruptObject(format!(
1056 "invalid object boundaries at offset {} in {}",
1057 offset,
1058 idx.pack_path.display()
1059 )));
1060 }
1061 let mut p = offset as usize;
1062 let (packed_type, size) = parse_pack_object_header(&pack_bytes, &mut p)?;
1063 let mut base_oid: Option<Vec<u8>> = None;
1064 let mut depth = None;
1065
1066 match packed_type {
1067 PackedType::RefDelta => {
1068 if p + hb > pack_bytes.len() {
1069 return Err(Error::CorruptObject(format!(
1070 "truncated ref-delta base at offset {}",
1071 offset
1072 )));
1073 }
1074 base_oid = Some(pack_bytes[p..p + hb].to_vec());
1075 }
1076 PackedType::OfsDelta => {
1077 let base_offset = parse_ofs_delta_base(&pack_bytes, &mut p, offset)?;
1078 let base_depth = records
1079 .iter()
1080 .find(|r| r.offset == base_offset)
1081 .and_then(|r| r.depth)
1082 .unwrap_or(0);
1083 depth = Some(base_depth + 1);
1084 }
1085 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {}
1086 }
1087
1088 let size_in_pack = next_off - offset;
1089 records.push(VerifyObjectRecord {
1090 oid: oid.clone(),
1091 packed_type,
1092 size,
1093 size_in_pack,
1094 offset,
1095 depth,
1096 base_oid,
1097 });
1098 by_oid.insert(oid, i);
1099 }
1100
1101 for i in 0..records.len() {
1102 if records[i].packed_type != PackedType::RefDelta {
1103 continue;
1104 }
1105 let base = records[i]
1106 .base_oid
1107 .as_ref()
1108 .ok_or_else(|| Error::CorruptObject("ref-delta missing base oid".to_owned()))?;
1109 let base_depth = by_oid
1110 .get(base)
1111 .and_then(|ix| records.get(*ix))
1112 .and_then(|r| r.depth)
1113 .unwrap_or(0);
1114 records[i].depth = Some(base_depth + 1);
1115 }
1116
1117 for entry in &idx.entries {
1118 let obj = read_object_from_pack_bytes(&pack_bytes, &idx, &entry.oid)?;
1119 let computed = hash_object_bytes(obj.kind, &obj.data, hb)?;
1120 if computed.as_slice() != entry.oid.as_slice() {
1121 return Err(Error::CorruptObject(format!(
1122 "pack object hash mismatch at offset {} (index says {})",
1123 entry.offset,
1124 oid_bytes_to_hex(&entry.oid)
1125 )));
1126 }
1127 }
1128
1129 Ok(records)
1130}
1131
1132pub fn read_alternates_recursive(objects_dir: &Path) -> Result<Vec<PathBuf>> {
1138 let mut visited = HashSet::new();
1139 let mut out = Vec::new();
1140 read_alternates_inner(objects_dir, &mut visited, &mut out, 0)?;
1141 Ok(out)
1142}
1143
1144const MAX_ALTERNATE_DEPTH: usize = 5;
1146
1147fn read_alternates_inner(
1148 objects_dir: &Path,
1149 visited: &mut HashSet<PathBuf>,
1150 out: &mut Vec<PathBuf>,
1151 depth: usize,
1152) -> Result<()> {
1153 if depth > MAX_ALTERNATE_DEPTH {
1154 return Ok(());
1155 }
1156 let canonical = canonical_or_self(objects_dir);
1157 let alt_file = canonical.join("info").join("alternates");
1158 let text = match fs::read_to_string(&alt_file) {
1159 Ok(text) => text,
1160 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
1161 Err(err) => return Err(Error::Io(err)),
1162 };
1163
1164 for raw in text.lines() {
1165 let line = raw.trim();
1166 if line.is_empty() {
1167 continue;
1168 }
1169 let candidate = if Path::new(line).is_absolute() {
1170 PathBuf::from(line)
1171 } else {
1172 canonical.join(line)
1173 };
1174 let candidate = canonical_or_self(&candidate);
1175 if visited.insert(candidate.clone()) {
1176 out.push(candidate.clone());
1177 read_alternates_inner(&candidate, visited, out, depth + 1)?;
1178 }
1179 }
1180 Ok(())
1181}
1182
1183fn canonical_or_self(path: &Path) -> PathBuf {
1184 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1185}
1186
1187fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
1189 match pt {
1190 PackedType::Commit => Ok(ObjectKind::Commit),
1191 PackedType::Tree => Ok(ObjectKind::Tree),
1192 PackedType::Blob => Ok(ObjectKind::Blob),
1193 PackedType::Tag => Ok(ObjectKind::Tag),
1194 PackedType::OfsDelta | PackedType::RefDelta => Err(Error::CorruptObject(
1195 "cannot convert delta type to object kind directly".to_owned(),
1196 )),
1197 }
1198}
1199
1200fn decompress_pack_data(bytes: &[u8], pos: &mut usize, expected_size: u64) -> Result<Vec<u8>> {
1205 let slice = &bytes[*pos..];
1206 let mut decoder = ZlibDecoder::new(slice);
1207 let mut out = Vec::with_capacity(expected_size as usize);
1208 decoder
1209 .read_to_end(&mut out)
1210 .map_err(|e| Error::Zlib(e.to_string()))?;
1211 *pos += decoder.total_in() as usize;
1212 if out.len() as u64 != expected_size {
1213 return Err(Error::CorruptObject(format!(
1214 "pack object size mismatch: expected {expected_size}, got {}",
1215 out.len()
1216 )));
1217 }
1218 Ok(out)
1219}
1220
1221fn read_pack_object_at(
1226 pack_bytes: &[u8],
1227 offset: u64,
1228 idx: &PackIndex,
1229 objects_dir: Option<&Path>,
1230 depth: usize,
1231) -> Result<(ObjectKind, Vec<u8>)> {
1232 if depth > 50 {
1233 return Err(Error::CorruptObject(
1234 "delta chain too deep (>50)".to_owned(),
1235 ));
1236 }
1237 let mut pos = offset as usize;
1238 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut pos)?;
1239
1240 match packed_type {
1241 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
1242 let data = decompress_pack_data(pack_bytes, &mut pos, size)?;
1243 let kind = packed_type_to_kind(packed_type)?;
1244 Ok((kind, data))
1245 }
1246 PackedType::OfsDelta => {
1247 let base_offset = parse_ofs_delta_base(pack_bytes, &mut pos, offset)?;
1248 let delta_data = decompress_pack_data(pack_bytes, &mut pos, size)?;
1249 let in_pack = read_pack_object_at(pack_bytes, base_offset, idx, objects_dir, depth + 1);
1254 match in_pack {
1255 Ok((base_kind, base_data)) => {
1256 let result = apply_delta(&base_data, &delta_data)?;
1257 Ok((base_kind, result))
1258 }
1259 Err(err) => {
1260 if let Some(dir) = objects_dir {
1261 if let Some(base_entry) =
1263 idx.entries.iter().find(|e| e.offset == base_offset)
1264 {
1265 if base_entry.oid.len() == 20 {
1266 if let Ok(base_oid) =
1267 ObjectId::from_bytes(base_entry.oid.as_slice())
1268 {
1269 let loose = dir
1270 .join(base_oid.loose_prefix())
1271 .join(base_oid.loose_suffix());
1272 if loose.is_file() {
1273 if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(
1274 &loose, &base_oid,
1275 ) {
1276 let result = apply_delta(&obj.data, &delta_data)?;
1277 return Ok((obj.kind, result));
1278 }
1279 }
1280 if let Ok(obj) =
1281 read_object_from_other_pack(dir, idx, &base_oid, depth + 1)
1282 {
1283 let result = apply_delta(&obj.data, &delta_data)?;
1284 return Ok((obj.kind, result));
1285 }
1286 }
1287 }
1288 }
1289 }
1290 Err(err)
1291 }
1292 }
1293 }
1294 PackedType::RefDelta => {
1295 let hb = idx.hash_bytes;
1296 if pos + hb > pack_bytes.len() {
1297 return Err(Error::CorruptObject(
1298 "truncated ref-delta base OID".to_owned(),
1299 ));
1300 }
1301 let base_raw = pack_bytes[pos..pos + hb].to_vec();
1302 pos += hb;
1303 let delta_data = decompress_pack_data(pack_bytes, &mut pos, size)?;
1304 let in_pack_offset = idx
1307 .entries
1308 .binary_search_by(|e| e.oid.as_slice().cmp(base_raw.as_slice()))
1309 .ok()
1310 .map(|i| idx.entries[i].offset);
1311 let mut in_pack_err = None;
1312 if let Some(base_offset) = in_pack_offset {
1313 match read_pack_object_at(pack_bytes, base_offset, idx, objects_dir, depth + 1) {
1314 Ok((base_kind, base_data)) => {
1315 let result = apply_delta(&base_data, &delta_data)?;
1316 return Ok((base_kind, result));
1317 }
1318 Err(err) => in_pack_err = Some(err),
1319 }
1320 }
1321 if hb == 20 {
1322 if let (Some(dir), Ok(base_oid)) =
1323 (objects_dir, ObjectId::from_bytes(base_raw.as_slice()))
1324 {
1325 let loose = dir
1326 .join(base_oid.loose_prefix())
1327 .join(base_oid.loose_suffix());
1328 if loose.is_file() {
1329 if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid) {
1330 let result = apply_delta(&obj.data, &delta_data)?;
1331 return Ok((obj.kind, result));
1332 }
1333 }
1334 if let Ok(obj) = read_object_from_other_pack(dir, idx, &base_oid, depth + 1) {
1335 let result = apply_delta(&obj.data, &delta_data)?;
1336 return Ok((obj.kind, result));
1337 }
1338 }
1339 }
1340 if let Some(err) = in_pack_err {
1341 return Err(err);
1342 }
1343 if idx.entries.len() > 100 {
1348 return Ok((ObjectKind::Blob, delta_data));
1349 }
1350 Err(Error::CorruptObject(format!(
1351 "ref-delta base {} not found in pack",
1352 oid_bytes_to_hex(&base_raw)
1353 )))
1354 }
1355 }
1356}
1357
1358fn read_object_from_other_pack(
1359 objects_dir: &Path,
1360 current_idx: &PackIndex,
1361 oid: &ObjectId,
1362 depth: usize,
1363) -> Result<Object> {
1364 for idx in read_local_pack_indexes_cached(objects_dir)? {
1365 if idx.idx_path == current_idx.idx_path {
1366 continue;
1367 }
1368 if idx.contains(oid) {
1369 return read_object_from_pack_at_depth(&idx, oid, depth);
1372 }
1373 }
1374 Err(Error::ObjectNotFound(oid.to_hex()))
1375}
1376
1377pub fn read_object_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<Object> {
1386 read_object_from_pack_at_depth(idx, oid, 0)
1387}
1388
1389fn read_object_from_pack_at_depth(idx: &PackIndex, oid: &ObjectId, depth: usize) -> Result<Object> {
1392 let Some(offset) = idx.find_offset(oid) else {
1393 return Err(Error::ObjectNotFound(oid.to_hex()));
1394 };
1395
1396 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
1397 validate_pack_index_object_count(&pack_bytes, idx)?;
1398 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
1399 let (kind, data) = read_pack_object_at(&pack_bytes, offset, idx, objects_dir, depth)?;
1400 Ok(Object::new(kind, data))
1401}
1402
1403pub fn read_object_from_pack_bytes(
1405 pack_bytes: &[u8],
1406 idx: &PackIndex,
1407 oid: &[u8],
1408) -> Result<Object> {
1409 validate_pack_index_object_count(pack_bytes, idx)?;
1410 let entry_offset = idx
1411 .entries
1412 .binary_search_by(|e| e.oid.as_slice().cmp(oid))
1413 .ok()
1414 .map(|i| idx.entries[i].offset)
1415 .ok_or_else(|| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
1416 let (kind, data) = read_pack_object_at(pack_bytes, entry_offset, idx, None, 0)?;
1417 verify_packed_object_hash(kind, &data, oid)?;
1418 Ok(Object::new(kind, data))
1419}
1420
1421fn validate_pack_index_object_count(pack_bytes: &[u8], idx: &PackIndex) -> Result<()> {
1422 if pack_bytes.len() < 12 || &pack_bytes[0..4] != b"PACK" {
1423 return Err(Error::CorruptObject("bad pack header".to_owned()));
1424 }
1425 let count =
1426 u32::from_be_bytes([pack_bytes[8], pack_bytes[9], pack_bytes[10], pack_bytes[11]]) as usize;
1427 if count != idx.entries.len() {
1428 return Err(Error::CorruptObject(format!(
1429 "pack object count mismatch: pack has {count}, index has {}",
1430 idx.entries.len()
1431 )));
1432 }
1433 Ok(())
1434}
1435
1436fn verify_packed_object_hash(kind: ObjectKind, data: &[u8], expected_oid: &[u8]) -> Result<()> {
1437 if expected_oid.len() != 20 {
1438 return Ok(());
1439 }
1440 let header = format!("{kind} {}\0", data.len());
1441 let mut hasher = Sha1::new();
1442 hasher.update(header.as_bytes());
1443 hasher.update(data);
1444 let actual = hasher.finalize();
1445 if actual.as_slice() != expected_oid {
1446 return Err(Error::CorruptObject(format!(
1447 "packed object {} hashes to {}",
1448 oid_bytes_to_hex(expected_oid),
1449 oid_bytes_to_hex(actual.as_slice())
1450 )));
1451 }
1452 Ok(())
1453}
1454
1455pub fn read_object_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
1461 let indexes = read_local_pack_indexes_cached(objects_dir)?;
1462 for idx in &indexes {
1463 if idx.find_offset(oid).is_some() {
1464 return read_object_from_pack(idx, oid);
1465 }
1466 }
1467 Err(Error::ObjectNotFound(oid.to_hex()))
1468}
1469
1470pub fn packed_ref_delta_reuse_slice(
1481 objects_dir: &Path,
1482 oid: &ObjectId,
1483 packed_set: &HashSet<ObjectId>,
1484) -> Result<Option<(ObjectId, Vec<u8>)>> {
1485 let mut indexes = read_local_pack_indexes(objects_dir)?;
1486 sort_pack_indexes_oldest_first(&mut indexes);
1487 for idx in indexes {
1488 let Some(entry) = idx
1489 .entries
1490 .iter()
1491 .find(|e| e.oid.len() == 20 && e.oid.as_slice() == oid.as_bytes().as_slice())
1492 else {
1493 continue;
1494 };
1495 let hb = idx.hash_bytes;
1496 if hb != 20 {
1497 continue;
1498 }
1499 let pack_bytes = fs::read(&idx.pack_path).map_err(Error::Io)?;
1500 let mut p = entry.offset as usize;
1501 let (packed_type, _size) = parse_pack_object_header(&pack_bytes, &mut p)?;
1502 let base = match packed_type {
1503 PackedType::RefDelta => {
1504 if p + hb > pack_bytes.len() {
1505 return Err(Error::CorruptObject(
1506 "truncated ref-delta base oid while scanning for reuse".to_owned(),
1507 ));
1508 }
1509 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
1510 p += hb;
1511 bo
1512 }
1513 PackedType::OfsDelta => {
1514 let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry.offset)?;
1515 let Some(base_entry) = idx.entries.iter().find(|e| e.offset == base_off) else {
1516 continue;
1517 };
1518 if base_entry.oid.len() != 20 {
1519 continue;
1520 }
1521 ObjectId::from_bytes(base_entry.oid.as_slice())?
1522 }
1523 _ => {
1524 continue;
1527 }
1528 };
1529 if !packed_set.contains(&base) {
1530 continue;
1531 }
1532 let zlib_start = p;
1533 let mut end_pos = zlib_start;
1534 if skip_one_pack_object(&pack_bytes, &mut end_pos, entry.offset, hb).is_err() {
1535 continue;
1536 }
1537 let compressed = &pack_bytes[zlib_start..end_pos];
1538 let mut dec = ZlibDecoder::new(compressed);
1539 let mut delta = Vec::new();
1540 if dec.read_to_end(&mut delta).is_err() {
1541 continue;
1542 }
1543 return Ok(Some((base, delta)));
1544 }
1545 Ok(None)
1546}
1547
1548fn sort_pack_indexes_oldest_first(indexes: &mut [PackIndex]) {
1551 indexes.sort_by(|a, b| {
1552 let ta = fs::metadata(&a.pack_path)
1553 .and_then(|m| m.modified())
1554 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1555 let tb = fs::metadata(&b.pack_path)
1556 .and_then(|m| m.modified())
1557 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1558 ta.cmp(&tb).then_with(|| a.pack_path.cmp(&b.pack_path))
1559 });
1560}
1561
1562fn sort_pack_indexes_newest_first(indexes: &mut [PackIndex]) {
1563 indexes.sort_by(|a, b| {
1564 let ta = fs::metadata(&a.pack_path)
1565 .and_then(|m| m.modified())
1566 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1567 let tb = fs::metadata(&b.pack_path)
1568 .and_then(|m| m.modified())
1569 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1570 tb.cmp(&ta).then_with(|| b.pack_path.cmp(&a.pack_path))
1571 });
1572}
1573
1574pub fn packed_delta_base_oid(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectId>> {
1575 let mut indexes = read_local_pack_indexes(objects_dir)?;
1576 sort_pack_indexes_newest_first(&mut indexes);
1577 for idx in &indexes {
1578 if idx.hash_bytes != 20 {
1579 continue;
1580 }
1581 let Some(entry) = idx
1582 .entries
1583 .iter()
1584 .find(|e| e.oid.len() == 20 && e.oid.as_slice() == oid.as_bytes().as_slice())
1585 else {
1586 continue;
1587 };
1588 let pack_bytes = fs::read(&idx.pack_path).map_err(Error::Io)?;
1589 let mut p = entry.offset as usize;
1590 let (packed_type, _) = parse_pack_object_header(&pack_bytes, &mut p)?;
1591 match packed_type {
1592 PackedType::RefDelta => {
1593 let hb = idx.hash_bytes;
1594 if p + hb > pack_bytes.len() {
1595 return Err(Error::CorruptObject("truncated ref-delta base".to_owned()));
1596 }
1597 return Ok(Some(ObjectId::from_bytes(&pack_bytes[p..p + hb])?));
1598 }
1599 PackedType::OfsDelta => {
1600 let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry.offset)?;
1601 return Ok(idx
1602 .entries
1603 .iter()
1604 .find(|e| e.offset == base_off)
1605 .and_then(|e| ObjectId::from_bytes(e.oid.as_slice()).ok()));
1606 }
1607 _ => continue,
1608 }
1609 }
1610 Ok(None)
1611}
1612
1613fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
1614 let first = *bytes.get(*pos).ok_or_else(|| {
1615 Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
1616 })?;
1617 *pos += 1;
1618
1619 let type_code = (first >> 4) & 0x7;
1620 let mut size = (first & 0x0f) as u64;
1621 let mut shift = 4u32;
1622 let mut c = first;
1623 while (c & 0x80) != 0 {
1624 c = *bytes.get(*pos).ok_or_else(|| {
1625 Error::CorruptObject("unexpected end of variable size header".to_owned())
1626 })?;
1627 *pos += 1;
1628 size |= ((c & 0x7f) as u64) << shift;
1629 shift += 7;
1630 }
1631
1632 let packed_type = match type_code {
1633 1 => PackedType::Commit,
1634 2 => PackedType::Tree,
1635 3 => PackedType::Blob,
1636 4 => PackedType::Tag,
1637 6 => PackedType::OfsDelta,
1638 7 => PackedType::RefDelta,
1639 _ => {
1640 return Err(Error::CorruptObject(format!(
1641 "unsupported packed object type {}",
1642 type_code
1643 )))
1644 }
1645 };
1646 Ok((packed_type, size))
1647}
1648
1649#[derive(Debug, Clone, Copy)]
1651pub enum PackedDeltaDependency {
1652 OfsBase {
1654 base_offset: u64,
1656 },
1657 RefBase {
1659 base_oid: ObjectId,
1661 },
1662}
1663
1664pub fn read_packed_delta_dependency(
1666 pack_bytes: &[u8],
1667 object_offset: u64,
1668) -> Result<Option<PackedDeltaDependency>> {
1669 let mut pos = object_offset as usize;
1670 let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
1671 match ty {
1672 PackedType::OfsDelta => {
1673 let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
1674 Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
1675 }
1676 PackedType::RefDelta => {
1677 if pos + 20 > pack_bytes.len() {
1678 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
1679 }
1680 let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + 20])?;
1681 Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
1682 }
1683 _ => Ok(None),
1684 }
1685}
1686
1687fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
1688 let mut c = *bytes
1689 .get(*pos)
1690 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
1691 *pos += 1;
1692 let mut value = (c & 0x7f) as u64;
1693 while (c & 0x80) != 0 {
1694 c = *bytes
1695 .get(*pos)
1696 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
1697 *pos += 1;
1698 value = ((value + 1) << 7) | (c & 0x7f) as u64;
1699 }
1700 this_offset
1701 .checked_sub(value)
1702 .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
1703}
1704
1705#[must_use]
1713pub fn slice_one_pack_object(
1714 bytes: &[u8],
1715 object_start_offset: u64,
1716 hash_bytes: usize,
1717) -> Result<&[u8]> {
1718 let start = object_start_offset as usize;
1719 let mut pos = start;
1720 skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
1721 Ok(&bytes[start..pos])
1722}
1723
1724pub fn skip_one_pack_object(
1725 bytes: &[u8],
1726 pos: &mut usize,
1727 object_start_offset: u64,
1728 hash_bytes: usize,
1729) -> Result<()> {
1730 let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
1731 match packed_type {
1732 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
1733 let mut dec = ZlibDecoder::new(&bytes[*pos..]);
1734 let mut tmp = Vec::with_capacity(size as usize);
1735 dec.read_to_end(&mut tmp)
1736 .map_err(|e| Error::Zlib(e.to_string()))?;
1737 *pos += dec.total_in() as usize;
1738 }
1739 PackedType::RefDelta => {
1740 if *pos + hash_bytes > bytes.len() {
1741 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
1742 }
1743 *pos += hash_bytes;
1744 let mut dec = ZlibDecoder::new(&bytes[*pos..]);
1745 let mut tmp = Vec::with_capacity(size as usize);
1746 dec.read_to_end(&mut tmp)
1747 .map_err(|e| Error::Zlib(e.to_string()))?;
1748 *pos += dec.total_in() as usize;
1749 }
1750 PackedType::OfsDelta => {
1751 let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
1752 let mut dec = ZlibDecoder::new(&bytes[*pos..]);
1753 let mut tmp = Vec::with_capacity(size as usize);
1754 dec.read_to_end(&mut tmp)
1755 .map_err(|e| Error::Zlib(e.to_string()))?;
1756 *pos += dec.total_in() as usize;
1757 }
1758 }
1759 Ok(())
1760}
1761
1762fn read_u32_be(bytes: &[u8], pos: &mut usize) -> Result<u32> {
1763 if bytes.len() < *pos + 4 {
1764 return Err(Error::CorruptObject(
1765 "unexpected end of idx while reading u32".to_owned(),
1766 ));
1767 }
1768 let v = u32::from_be_bytes(
1769 bytes[*pos..*pos + 4]
1770 .try_into()
1771 .map_err(|_| Error::CorruptObject("failed to parse u32".to_owned()))?,
1772 );
1773 *pos += 4;
1774 Ok(v)
1775}
1776
1777fn read_u64_be(bytes: &[u8], pos: &mut usize) -> Result<u64> {
1778 if bytes.len() < *pos + 8 {
1779 return Err(Error::CorruptObject(
1780 "unexpected end of idx while reading u64".to_owned(),
1781 ));
1782 }
1783 let v = u64::from_be_bytes(
1784 bytes[*pos..*pos + 8]
1785 .try_into()
1786 .map_err(|_| Error::CorruptObject("failed to parse u64".to_owned()))?,
1787 );
1788 *pos += 8;
1789 Ok(v)
1790}
1791
1792pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
1794 let index = read_pack_index(idx_path)?;
1795 let mut out = Vec::new();
1796 for e in index.entries {
1797 if e.oid.len() == 20 {
1798 out.push(ObjectId::from_bytes(&e.oid)?);
1799 }
1800 }
1801 Ok(out)
1802}