1use std::collections::{HashMap, HashSet};
13use std::fs;
14use std::io::{BufRead, BufReader, Write};
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18use crate::environment::RepositoryOptions;
19use crate::hash::verify_trailer;
20
21use crate::config::ConfigSet;
22use crate::diagnostics::{DiagnosticSink, NullDiagnostics, Warning};
23use crate::error::{Error, MidxError, Result};
24use crate::fs_publish::hard_link_or_copy;
25use crate::objects::{HashAlgo, ObjectId, ObjectInfo};
26use crate::pack::{read_pack_index_no_verify, PackIndex};
27use crate::pack_bitmap::{MidxBitmapWriter, PackBitmapWriteOptions};
28use crate::pack_map::PackData;
29use crate::pack_rev::append_hashfile_checksum;
30use crate::repo::Repository;
31
32const MIDX_SIGNATURE: u32 = 0x4d49_4458;
33const MIDX_VERSION_V1: u8 = 1;
34const MIDX_VERSION_V2: u8 = 2;
35const HASH_VERSION_SHA1: u8 = 1;
36const HASH_VERSION_SHA256: u8 = 2;
37const MIDX_HEADER_SIZE: usize = 12;
38pub(crate) const MIDX_MIN_FILE_LEN: u64 = (MIDX_HEADER_SIZE + 20) as u64;
39const CHUNK_TOC_ENTRY_SIZE: usize = 12;
40const MIDX_CHUNKID_PACKNAMES: u32 = 0x504e_414d;
41const MIDX_CHUNKID_OIDFANOUT: u32 = 0x4f49_4446;
42const MIDX_CHUNKID_OIDLOOKUP: u32 = 0x4f49_444c;
43const MIDX_CHUNKID_OBJECTOFFSETS: u32 = 0x4f4f_4646;
44const MIDX_CHUNKID_LARGEOFFSETS: u32 = 0x4c4f_4646;
45const MIDX_CHUNKID_REVINDEX: u32 = 0x5249_4458;
46const MIDX_CHUNKID_BITMAPPED_PACKS: u32 = 0x4254_4d50;
47
48const RIDX_SIGNATURE: u32 = 0x5249_4458;
50const RIDX_VERSION: u32 = 1;
51const RIDX_HEADER_SIZE: usize = 12;
52const MIDX_CHUNK_ALIGNMENT: usize = 4;
53
54const MIDX_LARGE_OFFSET_NEEDED: u32 = 0x8000_0000;
56
57struct MidxEntry {
58 oid: ObjectId,
59 pack_id: u32,
60 offset: u64,
61 pack_mtime: std::time::SystemTime,
62}
63
64#[derive(Clone, Default)]
66pub struct WriteMultiPackIndexOptions {
67 pub preferred_pack_idx: Option<u32>,
70 pub preferred_pack_name: Option<String>,
73 pub pack_names_subset_ordered: Option<Vec<String>>,
75 pub write_bitmap: bool,
78 pub incremental: bool,
81 pub write_rev_sidecar: bool,
84 pub version: Option<u8>,
89 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
91}
92
93fn resolve_write_version(requested: Option<u8>, default: u8) -> Result<u8> {
95 match requested {
96 None => Ok(default),
97 Some(v @ (MIDX_VERSION_V1 | MIDX_VERSION_V2)) => Ok(v),
98 Some(v) => Err(MidxError::UnknownWriteVersion {
99 version: i64::from(v),
100 }
101 .into()),
102 }
103}
104
105pub(crate) fn midx_write_version(requested: Option<u8>) -> Result<u8> {
111 resolve_write_version(requested, MIDX_VERSION_V1)
112}
113
114pub(crate) fn midx_version_from_config(config: &ConfigSet) -> Result<Option<u8>> {
123 let Some(version) = config.get_i64_strict("midx.version")? else {
124 return Ok(None);
125 };
126 match u8::try_from(version) {
127 Ok(v @ (MIDX_VERSION_V1 | MIDX_VERSION_V2)) => Ok(Some(v)),
128 _ => Err(MidxError::UnknownWriteVersion { version }.into()),
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
134#[non_exhaustive]
135pub enum MidxBitmapWriteOutcome {
136 #[default]
138 NotRequested,
139 Written,
141 SkippedIncrementalLayer,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
147pub struct WriteMultiPackIndexResult {
148 pub bitmap: MidxBitmapWriteOutcome,
150}
151
152fn normalize_pack_idx_basename(raw: &str) -> Result<String> {
153 let t = raw.trim();
154 let t = std::path::Path::new(t)
155 .file_name()
156 .and_then(|s| s.to_str())
157 .unwrap_or(t);
158 let t = t.strip_prefix("./").unwrap_or(t);
159 if t.ends_with(".idx") {
160 Ok(t.to_string())
161 } else if t.ends_with(".pack") {
162 Ok(format!("{}.idx", t.strip_suffix(".pack").unwrap_or(t)))
163 } else {
164 Ok(format!("{t}.idx"))
165 }
166}
167
168fn read_be_u32(data: &[u8], off: usize) -> Result<u32> {
174 let end = off.checked_add(4).filter(|&e| e <= data.len());
175 let Some(end) = end else {
176 return Err(Error::CorruptObject(
177 "truncated MIDX data reading u32".to_owned(),
178 ));
179 };
180 let bytes: [u8; 4] = data[off..end]
181 .try_into()
182 .map_err(|_| Error::CorruptObject("truncated MIDX data reading u32".to_owned()))?;
183 Ok(u32::from_be_bytes(bytes))
184}
185
186fn read_be_u64(data: &[u8], off: usize) -> Result<u64> {
192 let end = off.checked_add(8).filter(|&e| e <= data.len());
193 let Some(end) = end else {
194 return Err(Error::CorruptObject(
195 "truncated MIDX data reading u64".to_owned(),
196 ));
197 };
198 let bytes: [u8; 8] = data[off..end]
199 .try_into()
200 .map_err(|_| Error::CorruptObject("truncated MIDX data reading u64".to_owned()))?;
201 Ok(u64::from_be_bytes(bytes))
202}
203
204struct MidxFileHeader {
205 num_chunks: u8,
206}
207
208fn parse_midx_header(data: &[u8]) -> Result<(MidxFileHeader, usize, u8)> {
209 if data.len() < MIDX_HEADER_SIZE + 20 {
210 return Err(Error::CorruptObject("midx file too small".to_owned()));
211 }
212 let sig = read_be_u32(data, 0)?;
213 if sig != MIDX_SIGNATURE {
214 return Err(Error::CorruptObject("bad MIDX signature".to_owned()));
215 }
216 let version = data[4];
217 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
218 return Err(Error::CorruptObject(format!(
219 "multi-pack-index version {version} not recognized"
220 )));
221 }
222 let object_hash_bytes = data[5];
223 let num_chunks = data[6];
224 let _num_packs = read_be_u32(data, 8)?;
225 Ok((
226 MidxFileHeader { num_chunks },
227 MIDX_HEADER_SIZE,
228 object_hash_bytes,
229 ))
230}
231
232fn parse_pack_names_blob(pn: &[u8]) -> Result<Vec<String>> {
233 let mut names = Vec::new();
234 let mut start = 0usize;
235 for (i, &b) in pn.iter().enumerate() {
236 if b == 0 && i >= start {
237 if i > start {
238 let s = std::str::from_utf8(&pn[start..i])
239 .map_err(|_| Error::CorruptObject("non-utf8 pack name in MIDX".to_owned()))?;
240 names.push(s.to_string());
241 }
242 start = i + 1;
243 }
244 }
245 Ok(names)
246}
247
248fn cmp_idx_or_pack_name(idx_or_pack_name: &str, idx_name: &str) -> std::cmp::Ordering {
250 let a = idx_or_pack_name.as_bytes();
251 let b = idx_name.as_bytes();
252 let mut i = 0usize;
253 let min = a.len().min(b.len());
254 while i < min && a[i] == b[i] {
255 i += 1;
256 }
257 let suf_a = &a[i..];
258 let suf_b = &b[i..];
259 if suf_b == b"idx" && suf_a == b"pack" {
260 return std::cmp::Ordering::Equal;
261 }
262 suf_a.cmp(suf_b)
263}
264
265fn preferred_pack_index_by_mtime(pack_dir: &Path, names: &[String]) -> Result<Option<usize>> {
266 let mut best: Option<(usize, std::time::SystemTime)> = None;
267 for (i, n) in names.iter().enumerate() {
268 let meta = fs::metadata(pack_dir.join(n)).map_err(Error::Io)?;
269 let mtime = meta.modified().map_err(Error::Io)?;
270 match best {
271 None => best = Some((i, mtime)),
272 Some((_, t)) if mtime < t => best = Some((i, mtime)),
273 _ => {}
274 }
275 }
276 Ok(best.map(|(i, _)| i))
277}
278
279fn midx_d_dir(pack_dir: &Path) -> std::path::PathBuf {
280 pack_dir.join("multi-pack-index.d")
281}
282
283fn chain_file_path(pack_dir: &Path) -> std::path::PathBuf {
284 midx_d_dir(pack_dir).join("multi-pack-index-chain")
285}
286
287fn midx_chain_hash_hex_len(pack_dir: &Path) -> usize {
288 let Some(objects_dir) = pack_dir.parent() else {
289 return HashAlgo::Sha1.hex_len();
290 };
291 if repo_midx_hash_version_for_objects_dir(objects_dir) == HASH_VERSION_SHA256 {
292 HashAlgo::Sha256.hex_len()
293 } else {
294 HashAlgo::Sha1.hex_len()
295 }
296}
297
298fn is_valid_midx_chain_hash_line(t: &str, hex_len: usize) -> bool {
299 t.len() == hex_len && t.chars().all(|c| c.is_ascii_hexdigit())
300}
301
302fn read_chain_layer_hashes(pack_dir: &Path) -> Result<Vec<String>> {
303 let hex_len = midx_chain_hash_hex_len(pack_dir);
304 let path = chain_file_path(pack_dir);
305 let f = fs::File::open(&path).map_err(Error::Io)?;
306 let mut out = Vec::new();
307 for line in BufReader::new(f).lines() {
308 let line = line.map_err(Error::Io)?;
309 let t = line.trim();
310 if t.is_empty() {
311 continue;
312 }
313 if !is_valid_midx_chain_hash_line(t, hex_len) {
314 return Err(Error::CorruptObject(format!(
315 "invalid multi-pack-index chain line: {t}"
316 )));
317 }
318 out.push(t.to_ascii_lowercase());
319 }
320 Ok(out)
321}
322
323fn repo_midx_hash_version(pack_dir: &Path) -> u8 {
331 let Some(objects_dir) = pack_dir.parent() else {
333 return HASH_VERSION_SHA1;
334 };
335 repo_midx_hash_version_for_objects_dir(objects_dir)
336}
337
338fn open_midx_layer_bytes(path: &Path) -> Result<Arc<PackData>> {
340 match PackData::open_with_min_len(path, MIDX_MIN_FILE_LEN) {
341 Ok(b) => Ok(b),
342 Err(Error::CorruptObject(_)) => Ok(PackData::from_owned(
343 std::fs::read(path).map_err(Error::Io)?,
344 )),
345 Err(err) => Err(err),
346 }
347}
348
349pub(crate) mod midx_cache {
352 use super::{
353 midx_chain_layer_paths_newest_first, midx_load_for_read, open_midx_layer_bytes,
354 repo_midx_hash_version_for_objects_dir, MidxLoadResult, MidxReadView,
355 MIDX_LARGE_OFFSET_NEEDED, MIDX_MIN_FILE_LEN,
356 };
357 use crate::diagnostics::{CollectingDiagnostics, DiagnosticSink, Warning};
358 use crate::error::{Error, Result};
359 use crate::objects::{ObjectId, ObjectIdPrefix, ObjectInfo};
360 use crate::pack::{read_object_at, read_pack_index_cached, PackIndex};
361 use crate::pack_map::PackData;
362 use std::collections::HashMap;
363 use std::fs;
364 use std::ops::ControlFlow;
365 use std::path::{Path, PathBuf};
366 use std::sync::atomic::{AtomicBool, Ordering};
367 use std::sync::{Arc, Mutex, OnceLock};
368
369 pub struct PreparedMidxLayer {
371 bytes: Arc<PackData>,
372 fanout: [u32; 256],
373 oidl_off: usize,
374 num_objects: usize,
375 hash_len: usize,
376 ooff_off: usize,
377 loff: Option<(usize, usize)>,
378 pack_dir: PathBuf,
379 pack_names: Vec<String>,
380 pack_indexes: Vec<OnceLock<Option<Arc<PackIndex>>>>,
381 }
382
383 impl PreparedMidxLayer {
384 pub(super) fn prepare(bytes: Arc<PackData>, view: MidxReadView, pack_dir: &Path) -> Self {
385 let data = bytes.as_ref();
386 let hash_len = if data.get(5) == Some(&2) { 32 } else { 20 };
387 let mut fanout = [0u32; 256];
388 for (i, slot) in fanout.iter_mut().enumerate() {
389 *slot = read_be_u32(data, view.oidf_off + i * 4).unwrap_or(0);
390 }
391 Self {
392 fanout,
393 oidl_off: view.oidl_off,
394 ooff_off: view.ooff_off,
395 loff: view.loff,
396 num_objects: view.num_objects,
397 hash_len,
398 pack_dir: pack_dir.to_path_buf(),
399 pack_names: view.pack_names.clone(),
400 pack_indexes: view.pack_names.iter().map(|_| OnceLock::new()).collect(),
401 bytes,
402 }
403 }
404
405 fn open_pack_index(&self, pack_id: u32) -> Option<Arc<PackIndex>> {
406 let i = pack_id as usize;
407 let slot = self.pack_indexes.get(i)?;
408 slot.get_or_init(|| {
409 let idx_path = self.pack_dir.join(self.pack_names.get(i)?);
410 if !idx_path.is_file() {
411 return None;
412 }
413 read_pack_index_cached(&idx_path).ok()
414 })
415 .clone()
416 }
417
418 fn oid_at(&self, pos: usize) -> &[u8] {
419 let base = self.oidl_off + pos * self.hash_len;
420 &self.bytes[base..base + self.hash_len]
421 }
422
423 #[inline(always)]
424 fn find_position(&self, oid: &ObjectId) -> Option<usize> {
425 let needle = oid.as_bytes();
426 if needle.len() != self.hash_len {
427 return None;
428 }
429 let first = needle[0] as usize;
430 let lo = if first == 0 {
431 0
432 } else {
433 self.fanout[first - 1] as usize
434 };
435 let hi = self.fanout[first] as usize;
436 if lo >= hi || hi > self.num_objects {
437 return None;
438 }
439 if hi - lo >= 64 {
440 if let Some(pos) = self.interpolation_search(lo, hi, needle) {
441 return Some(pos);
442 }
443 }
444 self.binary_search(lo, hi, needle)
445 }
446
447 fn binary_search(&self, mut lo: usize, mut hi: usize, needle: &[u8]) -> Option<usize> {
448 while lo < hi {
449 let mid = lo + (hi - lo) / 2;
450 match self.oid_at(mid).cmp(needle) {
451 std::cmp::Ordering::Less => lo = mid + 1,
452 std::cmp::Ordering::Greater => hi = mid,
453 std::cmp::Ordering::Equal => return Some(mid),
454 }
455 }
456 None
457 }
458
459 fn interpolation_search(&self, lo: usize, hi: usize, needle: &[u8]) -> Option<usize> {
460 use crate::pack_index::oid_interp_prefix_u64;
461 let mut lo = lo;
462 let mut hi = hi;
463 while lo < hi && needle >= self.oid_at(lo) && needle <= self.oid_at(hi - 1) {
464 let lo_oid = self.oid_at(lo);
465 let hi_oid = self.oid_at(hi - 1);
466 if lo_oid == hi_oid {
467 return if lo_oid == needle { Some(lo) } else { None };
468 }
469 let span = hi - lo - 1;
470 let num = oid_interp_prefix_u64(needle);
471 let lo_val = oid_interp_prefix_u64(lo_oid);
472 let hi_val = oid_interp_prefix_u64(hi_oid);
473 let guess = if hi_val <= lo_val {
474 lo + span / 2
475 } else {
476 let num = u128::from(num.saturating_sub(lo_val));
477 let den = u128::from(hi_val - lo_val);
478 lo.saturating_add(((num * u128::from(span as u64)) / den) as usize)
479 };
480 let pos = guess.min(hi - 1);
481 match self.oid_at(pos).cmp(needle) {
482 std::cmp::Ordering::Equal => return Some(pos),
483 std::cmp::Ordering::Less => lo = pos + 1,
484 std::cmp::Ordering::Greater => hi = pos,
485 }
486 }
487 None
488 }
489
490 fn validate_packs(&self, diagnostics: &dyn DiagnosticSink) {
491 for idx_name in &self.pack_names {
492 let idx_path = self.pack_dir.join(idx_name);
493 if !idx_path.is_file() {
494 continue;
495 }
496 if crate::pack::read_pack_index_no_verify(&idx_path).is_err() {
497 let mut pack_path = idx_path.clone();
498 pack_path.set_extension("pack");
499 diagnostics.warn(Warning::MidxPackIndexUnavailable {
500 pack: pack_path.display().to_string(),
501 });
502 }
503 }
504 }
505
506 #[inline(always)]
508 pub fn lookup_pack_and_offset(&self, oid: &ObjectId) -> Option<(u32, u64)> {
509 let pos = self.find_position(oid)?;
510 let ob = self.ooff_off + pos * 8;
511 let data = self.bytes.as_ref();
512 let pack_id = read_be_u32(data, ob).ok()?;
513 let raw_off = read_be_u32(data, ob + 4).ok()?;
514 decode_midx_object_offset(data, raw_off, self.loff).map(|offset| (pack_id, offset))
515 }
516
517 #[inline]
518 pub fn read_at(&self, pack_id: u32, offset: u64) -> Result<Option<crate::objects::Object>> {
519 let Some(idx) = self.open_pack_index(pack_id) else {
520 return Ok(None);
521 };
522 match read_object_at(idx.as_ref(), offset) {
523 Ok(obj) => Ok(Some(obj)),
524 Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
525 Err(err) => Err(err),
526 }
527 }
528
529 #[cfg(test)]
530 fn synthetic_test_layer(n: usize) -> Self {
531 let hash_len = 20;
532 let first = 0x42u8;
533 let mut fanout = [0u32; 256];
534 fanout[0x42] = u32::try_from(n).expect("n fits u32");
535 let mut bytes = Vec::with_capacity(n * hash_len);
536 for i in 0..n {
537 let mut h = [0u8; 20];
538 h[0] = first;
539 h[19] = i as u8;
540 bytes.extend_from_slice(&h);
541 }
542 Self {
543 bytes: PackData::from_owned(bytes),
544 fanout,
545 oidl_off: 0,
546 ooff_off: 0,
547 loff: None,
548 num_objects: n,
549 hash_len,
550 pack_dir: PathBuf::from("/nonexistent"),
551 pack_names: vec!["test-pack.idx".into()],
552 pack_indexes: vec![OnceLock::new()],
553 }
554 }
555
556 #[cfg(test)]
557 fn test_find_position(&self, oid: &ObjectId) -> Option<usize> {
558 self.find_position(oid)
559 }
560
561 fn for_each_listed_object(
562 &self,
563 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
564 ) -> Result<()> {
565 for i in 0..self.num_objects {
566 let oid = ObjectId::from_bytes(self.oid_at(i))
567 .map_err(|e| Error::CorruptObject(e.to_string()))?;
568 if f(&oid).is_break() {
569 break;
570 }
571 }
572 Ok(())
573 }
574
575 fn lookup_prefix(
576 &self,
577 prefix: &ObjectIdPrefix,
578 limit: usize,
579 count: &mut usize,
580 out: &mut Vec<ObjectId>,
581 seen: &mut std::collections::HashSet<ObjectId>,
582 ) -> Result<()> {
583 let lo = self.lower_bound_oid(prefix.lowest().as_bytes());
584 let hi = self.upper_bound_oid(prefix.highest().as_bytes());
585 for pos in lo..hi {
586 let digest = self.oid_at(pos);
587 if !prefix.matches_digest(digest) {
588 continue;
589 }
590 let oid = ObjectId::from_bytes(digest)
591 .map_err(|e| Error::CorruptObject(e.to_string()))?;
592 if !seen.insert(oid) {
593 continue;
594 }
595 out.push(oid);
596 *count += 1;
597 if limit != 0 && *count >= limit {
598 break;
599 }
600 }
601 Ok(())
602 }
603
604 fn lower_bound_oid(&self, needle: &[u8]) -> usize {
605 let mut lo = 0usize;
606 let mut hi = self.num_objects;
607 while lo < hi {
608 let mid = lo + (hi - lo) / 2;
609 if self.oid_at(mid) < needle {
610 lo = mid + 1;
611 } else {
612 hi = mid;
613 }
614 }
615 lo
616 }
617
618 fn upper_bound_oid(&self, needle: &[u8]) -> usize {
619 let mut lo = 0usize;
620 let mut hi = self.num_objects;
621 while lo < hi {
622 let mid = lo + (hi - lo) / 2;
623 if self.oid_at(mid) <= needle {
624 lo = mid + 1;
625 } else {
626 hi = mid;
627 }
628 }
629 lo
630 }
631 }
632
633 #[cfg(test)]
634 mod layer_lookup_tests {
635 use super::{read_be_u32, PreparedMidxLayer};
636 use crate::objects::ObjectId;
637
638 #[test]
639 fn interpolation_search_finds_oid_in_wide_fanout_bucket() {
640 let layer = PreparedMidxLayer::synthetic_test_layer(70);
641 let mut h = [0u8; 20];
642 h[0] = 0x42;
643 h[19] = 35;
644 let oid = ObjectId::from_bytes(&h).expect("oid");
645 assert_eq!(layer.test_find_position(&oid), Some(35));
646 }
647
648 #[cfg_attr(
649 not(feature = "full-tests"),
650 ignore = "starts system git or is heavy; run with --features full-tests"
651 )]
652 #[test]
653 fn strict_and_permissive_views_share_validated_backing_until_invalidation() {
654 use crate::diagnostics::NullDiagnostics;
655 use crate::pack_store::PackStore;
656 use std::sync::Arc;
657 let (root, _) = crate::midx::tests::midx_fixture(true).expect("fixture");
658 let objects = root.path().join(".git/objects");
659 let store = Arc::new(PackStore::new(objects.clone()));
660 let (permissive, strict) = PackStore::with_context(Arc::clone(&store), || {
661 (
662 super::prepared_chain(&objects, &NullDiagnostics)
663 .expect("permissive")
664 .expect("chain"),
665 super::prepared_chain_strict(&objects, &NullDiagnostics)
666 .expect("strict")
667 .expect("chain"),
668 )
669 });
670 assert!(Arc::ptr_eq(
671 &permissive.layers[0].bytes,
672 &strict.layers[0].bytes
673 ));
674 store.invalidate_midx();
675 let refreshed = PackStore::with_context(Arc::clone(&store), || {
676 super::prepared_chain_strict(&objects, &NullDiagnostics)
677 .expect("refresh")
678 .expect("chain")
679 });
680 assert!(!Arc::ptr_eq(
681 &refreshed.layers[0].bytes,
682 &strict.layers[0].bytes
683 ));
684 }
685
686 #[cfg_attr(
687 not(feature = "full-tests"),
688 ignore = "starts system git or is heavy; run with --features full-tests"
689 )]
690 #[test]
691 fn shared_layer_replays_load_warnings_for_the_other_view() {
692 use crate::diagnostics::{CollectingDiagnostics, NullDiagnostics, Warning};
693 use crate::midx::{write_multi_pack_index_with_options, WriteMultiPackIndexOptions};
694 use crate::objects::HashAlgo;
695 use crate::pack_store::PackStore;
696 use std::sync::Arc;
697 let (root, _) = crate::midx::tests::midx_fixture(false).expect("fixture");
698 let objects = root.path().join(".git/objects");
699 let pack_dir = objects.join("pack");
700 write_multi_pack_index_with_options(
701 &pack_dir,
702 &WriteMultiPackIndexOptions {
703 preferred_pack_idx: Some(0),
704 ..Default::default()
705 },
706 )
707 .expect("write RIDX");
708 let path = pack_dir.join("multi-pack-index");
709 let mut data = std::fs::read(&path).expect("read");
710 let (off, len) = crate::midx::find_chunk(&data, 12, crate::midx::MIDX_CHUNKID_REVINDEX)
711 .expect("RIDX");
712 assert!(len >= 4);
713 data.drain(off..off + 4);
714 let sentinel = 12 + data[6] as usize * 12;
715 let end = crate::midx::read_be_u64(&data, sentinel + 4).expect("end") - 4;
716 data[sentinel + 4..sentinel + 12].copy_from_slice(&end.to_be_bytes());
717 let payload = data.len() - 20;
718 let hash = HashAlgo::Sha1.checksum_digest(&data[..payload]);
719 data[payload..].copy_from_slice(&hash);
720 std::fs::write(&path, data).expect("write warning fixture");
721 let store = Arc::new(PackStore::new(objects.clone()));
722 PackStore::with_context(store, || {
723 let strict = super::prepared_chain_strict(&objects, &NullDiagnostics)
724 .expect("strict")
725 .expect("chain");
726 let sink = CollectingDiagnostics::new();
727 let permissive = super::prepared_chain(&objects, &sink)
728 .expect("permissive")
729 .expect("chain");
730 assert_eq!(
731 sink.warnings(),
732 vec![
733 Warning::MidxRevIndexWrongSize,
734 Warning::MidxBitmapMissingReverseIndex
735 ]
736 );
737 assert!(Arc::ptr_eq(
738 &strict.layers[0].bytes,
739 &permissive.layers[0].bytes
740 ));
741 });
742 }
743
744 #[cfg_attr(
745 not(feature = "full-tests"),
746 ignore = "starts system git or is heavy; run with --features full-tests"
747 )]
748 #[test]
749 fn concurrent_views_share_one_layer_backing() {
750 use crate::diagnostics::NullDiagnostics;
751 use crate::pack_store::PackStore;
752 use std::sync::{Arc, Barrier};
753 let (root, _) = crate::midx::tests::midx_fixture(true).expect("fixture");
754 let objects = root.path().join(".git/objects");
755 let store = Arc::new(PackStore::new(objects.clone()));
756 let start = Arc::new(Barrier::new(4));
757 let handles: Vec<_> = (0..4)
758 .map(|i| {
759 let store = Arc::clone(&store);
760 let start = Arc::clone(&start);
761 let objects = objects.clone();
762 std::thread::spawn(move || {
763 start.wait();
764 PackStore::with_context(store, || {
765 if i % 2 == 0 {
766 super::prepared_chain(&objects, &NullDiagnostics)
767 } else {
768 super::prepared_chain_strict(&objects, &NullDiagnostics)
769 }
770 })
771 .expect("prepare")
772 .expect("chain")
773 })
774 })
775 .collect();
776 let chains: Vec<_> = handles
777 .into_iter()
778 .map(|h| h.join().expect("thread"))
779 .collect();
780 assert!(chains
781 .iter()
782 .all(|chain| Arc::ptr_eq(&chain.layers[0].bytes, &chains[0].layers[0].bytes)));
783 }
784
785 #[test]
786 fn midx_cache_read_be_u32_truncated() {
787 assert!(read_be_u32(&[0, 1, 2], 0).is_err());
788 }
789 }
790
791 pub struct PreparedMidxChain {
793 layers: Vec<Arc<PreparedMidxLayer>>,
794 packs_validated: AtomicBool,
795 }
796
797 impl PreparedMidxChain {
798 #[inline(always)]
799 pub fn lookup_pack_and_offset(&self, oid: &ObjectId) -> Option<(u32, u64)> {
800 for layer in &self.layers {
801 if let Some(found) = layer.lookup_pack_and_offset(oid) {
802 return Some(found);
803 }
804 }
805 None
806 }
807
808 pub fn oid_listed_in_tip(&self, oid: &ObjectId) -> bool {
809 self.lookup_pack_and_offset(oid).is_some()
810 }
811
812 pub fn try_read_object(
813 &self,
814 oid: &ObjectId,
815 diagnostics: &dyn DiagnosticSink,
816 ) -> Result<Option<crate::objects::Object>> {
817 for layer in &self.layers {
818 let Some((pack_id, offset)) = layer.lookup_pack_and_offset(oid) else {
819 continue;
820 };
821 self.ensure_packs_validated(diagnostics);
822 match layer.read_at(pack_id, offset)? {
823 Some(obj) => return Ok(Some(obj)),
824 None => return Ok(None),
825 }
826 }
827 Ok(None)
828 }
829
830 pub fn try_read_info(
831 &self,
832 oid: &ObjectId,
833 diagnostics: &dyn DiagnosticSink,
834 ) -> Result<Option<ObjectInfo>> {
835 for layer in &self.layers {
836 let Some((pack_id, _offset)) = layer.lookup_pack_and_offset(oid) else {
837 continue;
838 };
839 self.ensure_packs_validated(diagnostics);
840 let Some(idx) = layer.open_pack_index(pack_id) else {
841 return Ok(None);
842 };
843 return crate::pack::read_object_info_from_pack(idx.as_ref(), oid).map(Some);
844 }
845 Ok(None)
846 }
847
848 pub fn ensure_packs_validated(&self, diagnostics: &dyn DiagnosticSink) {
849 if self.packs_validated.swap(true, Ordering::Relaxed) {
850 return;
851 }
852 for layer in &self.layers {
853 layer.validate_packs(diagnostics);
854 }
855 }
856
857 pub(crate) fn covered_pack_basenames(&self) -> std::collections::HashSet<String> {
859 let mut out = std::collections::HashSet::new();
860 for layer in &self.layers {
861 for name in &layer.pack_names {
862 out.insert(name.clone());
863 }
864 }
865 out
866 }
867
868 pub(crate) fn for_each_listed_object(
870 &self,
871 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
872 ) -> Result<()> {
873 use std::collections::HashSet;
874 let mut seen = HashSet::new();
875 for layer in &self.layers {
876 let mut stop = false;
877 layer.for_each_listed_object(&mut |oid| {
878 if !seen.insert(*oid) {
879 return ControlFlow::Continue(());
880 }
881 if f(oid).is_break() {
882 stop = true;
883 return ControlFlow::Break(());
884 }
885 ControlFlow::Continue(())
886 })?;
887 if stop {
888 break;
889 }
890 }
891 Ok(())
892 }
893
894 pub(crate) fn lookup_prefix(
896 &self,
897 prefix: &ObjectIdPrefix,
898 limit: usize,
899 out: &mut Vec<ObjectId>,
900 ) -> Result<()> {
901 use std::collections::HashSet;
902 let mut seen = HashSet::new();
903 let mut count = 0usize;
904 for layer in &self.layers {
905 layer.lookup_prefix(prefix, limit, &mut count, out, &mut seen)?;
906 if limit != 0 && count >= limit {
907 break;
908 }
909 }
910 Ok(())
911 }
912 }
913
914 fn read_be_u32(data: &[u8], off: usize) -> Result<u32> {
915 if off + 4 > data.len() {
916 return Err(Error::CorruptObject("truncated MIDX u32".to_owned()));
917 }
918 Ok(u32::from_be_bytes([
919 data[off],
920 data[off + 1],
921 data[off + 2],
922 data[off + 3],
923 ]))
924 }
925
926 fn read_be_u64(data: &[u8], off: usize) -> Result<u64> {
927 if off + 8 > data.len() {
928 return Err(Error::CorruptObject("truncated MIDX u64".to_owned()));
929 }
930 Ok(u64::from_be_bytes([
931 data[off],
932 data[off + 1],
933 data[off + 2],
934 data[off + 3],
935 data[off + 4],
936 data[off + 5],
937 data[off + 6],
938 data[off + 7],
939 ]))
940 }
941
942 fn decode_midx_object_offset(
943 data: &[u8],
944 raw_off: u32,
945 loff: Option<(usize, usize)>,
946 ) -> Option<u64> {
947 if loff.is_none() {
948 return Some(u64::from(raw_off));
949 }
950 if (raw_off & MIDX_LARGE_OFFSET_NEEDED) != 0 {
951 let idx = (raw_off & !MIDX_LARGE_OFFSET_NEEDED) as usize;
952 let need = (idx + 1) * 8;
953 match loff {
954 Some((loff_off, loff_len)) if loff_len >= need => {
955 read_be_u64(data, loff_off + idx * 8).ok()
956 }
957 _ => None,
958 }
959 } else {
960 Some(u64::from(raw_off))
961 }
962 }
963
964 #[cfg(test)]
965 pub(super) fn decode_midx_object_offset_for_test(
966 data: &[u8],
967 raw_off: u32,
968 loff: Option<(usize, usize)>,
969 ) -> Option<u64> {
970 decode_midx_object_offset(data, raw_off, loff)
971 }
972
973 struct CachedLayer {
974 stamp: (Option<std::time::SystemTime>, u64),
975 prepared: Arc<PreparedMidxLayer>,
976 warnings: Vec<Warning>,
977 }
978
979 #[derive(Default)]
980 pub(crate) struct State {
981 layers: HashMap<PathBuf, CachedLayer>,
982 chains: HashMap<PathBuf, Arc<PreparedMidxChain>>,
983 strict_chains: HashMap<PathBuf, Arc<PreparedMidxChain>>,
984 hot_chain: Option<(PathBuf, Arc<PreparedMidxChain>)>,
985 }
986
987 fn build_chain_impl(
988 state: &mut State,
989 objects_dir: &Path,
990 diagnostics: &dyn DiagnosticSink,
991 require_every_layer: bool,
992 ) -> Result<Option<Arc<PreparedMidxChain>>> {
993 let pack_dir = objects_dir.join("pack");
994 let paths = midx_chain_layer_paths_newest_first(&pack_dir);
995 if paths.is_empty() {
996 return Ok(None);
997 }
998 let path_count = paths.len();
999 let hash_version = repo_midx_hash_version_for_objects_dir(objects_dir);
1000 let mut layers = Vec::with_capacity(path_count);
1001 for path in paths {
1002 #[cfg(test)]
1003 crate::hot_path_test_metrics::record_midx_stamp_stat_for_active_scope();
1004 let metadata = match fs::metadata(&path) {
1005 Ok(meta) => meta,
1006 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1007 if require_every_layer {
1008 return Ok(None);
1009 }
1010 continue;
1011 }
1012 Err(err) => return Err(Error::Io(err)),
1013 };
1014 if metadata.len() < MIDX_MIN_FILE_LEN {
1015 if require_every_layer {
1016 return Ok(None);
1017 }
1018 continue;
1019 }
1020 let stamp = (metadata.modified().ok(), metadata.len());
1021 if let Some(cached) = state.layers.get(&path).filter(|cached| {
1022 stamp.0.is_some()
1023 && cached.stamp == stamp
1024 && cached.prepared.hash_len == if hash_version == 2 { 32 } else { 20 }
1025 }) {
1026 for warning in &cached.warnings {
1027 diagnostics.warn(warning.clone());
1028 }
1029 layers.push(Arc::clone(&cached.prepared));
1030 continue;
1031 }
1032 let bytes = match open_midx_layer_bytes(&path) {
1033 Ok(b) => b,
1034 Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
1035 if require_every_layer {
1036 return Ok(None);
1037 }
1038 continue;
1039 }
1040 Err(Error::CorruptObject(_)) => {
1041 if require_every_layer {
1042 return Ok(None);
1043 }
1044 continue;
1045 }
1046 Err(err) => return Err(err),
1047 };
1048 let collected = CollectingDiagnostics::new();
1051 let loaded = midx_load_for_read(bytes.as_ref(), hash_version, &collected);
1052 let warnings = collected.warnings();
1053 for warning in &warnings {
1054 diagnostics.warn(warning.clone());
1055 }
1056 let view = match loaded {
1057 Ok(MidxLoadResult::Ok(v)) => v,
1058 Ok(MidxLoadResult::Skip) => {
1059 if require_every_layer {
1060 return Ok(None);
1061 }
1062 continue;
1063 }
1064 Err(Error::Midx(_)) if require_every_layer => return Ok(None),
1065 Err(err) => return Err(err),
1066 };
1067 let prepared = Arc::new(PreparedMidxLayer::prepare(bytes, view, &pack_dir));
1068 state.layers.insert(
1069 path,
1070 CachedLayer {
1071 stamp,
1072 prepared: Arc::clone(&prepared),
1073 warnings,
1074 },
1075 );
1076 layers.push(prepared);
1077 }
1078 if layers.is_empty() {
1079 return Ok(None);
1080 }
1081 if require_every_layer && layers.len() != path_count {
1082 return Ok(None);
1083 }
1084 Ok(Some(Arc::new(PreparedMidxChain {
1085 layers,
1086 packs_validated: AtomicBool::new(false),
1087 })))
1088 }
1089
1090 pub fn prepared_chain(
1092 objects_dir: &Path,
1093 diagnostics: &dyn DiagnosticSink,
1094 ) -> Result<Option<Arc<PreparedMidxChain>>> {
1095 let (result, warnings) =
1096 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1097 if let Some((path, chain)) = &state.hot_chain {
1098 if path == objects_dir {
1099 return (Ok(Some(Arc::clone(chain))), Vec::new());
1100 }
1101 }
1102 let pack_dir = objects_dir.join("pack");
1103 if let Some(chain) = state.chains.get(&pack_dir) {
1104 let chain = Arc::clone(chain);
1105 state.hot_chain = Some((objects_dir.to_path_buf(), Arc::clone(&chain)));
1106 return (Ok(Some(chain)), Vec::new());
1107 }
1108 let collected = CollectingDiagnostics::new();
1109 let result = build_chain_impl(state, objects_dir, &collected, false);
1110 if let Ok(Some(chain)) = &result {
1111 state.chains.insert(pack_dir, Arc::clone(chain));
1112 state.hot_chain = Some((objects_dir.to_path_buf(), Arc::clone(chain)));
1113 }
1114 (result, collected.warnings())
1115 });
1116 for warning in warnings {
1118 diagnostics.warn(warning);
1119 }
1120 result
1121 }
1122
1123 pub fn prepared_chain_strict(
1126 objects_dir: &Path,
1127 diagnostics: &dyn DiagnosticSink,
1128 ) -> Result<Option<Arc<PreparedMidxChain>>> {
1129 let (result, warnings) =
1130 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1131 let pack_dir = objects_dir.join("pack");
1132 if let Some(chain) = state.strict_chains.get(&pack_dir) {
1133 return (Ok(Some(Arc::clone(chain))), Vec::new());
1134 }
1135 let collected = CollectingDiagnostics::new();
1136 let result = build_chain_impl(state, objects_dir, &collected, true);
1137 if let Ok(Some(chain)) = &result {
1138 state.strict_chains.insert(pack_dir, Arc::clone(chain));
1139 }
1140 (result, collected.warnings())
1141 });
1142 for warning in warnings {
1143 diagnostics.warn(warning);
1144 }
1145 result
1146 }
1147
1148 pub(super) fn cached_pack_names(path: &Path, metadata: &fs::Metadata) -> Option<Vec<String>> {
1149 let stamp = (metadata.modified().ok(), metadata.len());
1150 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1151 state
1152 .layers
1153 .get(path)
1154 .filter(|cached| stamp.0.is_some() && cached.stamp == stamp)
1155 .map(|cached| cached.prepared.pack_names.clone())
1156 })
1157 }
1158
1159 pub fn evict_pack_dir(pack_dir: &Path) {
1160 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1161 evict_pack_dir_in_state(state, pack_dir);
1162 });
1163 }
1164
1165 pub(crate) fn evict_pack_dir_on(state: &Mutex<State>, pack_dir: &Path) {
1166 let mut state = state.lock().unwrap_or_else(|poison| poison.into_inner());
1167 evict_pack_dir_in_state(&mut state, pack_dir);
1168 }
1169
1170 fn evict_pack_dir_in_state(state: &mut State, pack_dir: &Path) {
1171 if state.hot_chain.as_ref().is_some_and(|(objects_dir, _)| {
1172 objects_dir.join("pack") == pack_dir || pack_dir.starts_with(objects_dir)
1173 }) {
1174 state.hot_chain = None;
1175 }
1176 state.layers.retain(|path, _| !path.starts_with(pack_dir));
1177 state.chains.retain(|path, _| !path.starts_with(pack_dir));
1178 state
1179 .strict_chains
1180 .retain(|path, _| !path.starts_with(pack_dir));
1181 }
1182}
1183
1184pub(crate) use midx_cache::PreparedMidxChain;
1186
1187pub(crate) fn prepared_midx_chain(objects_dir: &Path) -> Result<Option<Arc<PreparedMidxChain>>> {
1188 midx_cache::prepared_chain(objects_dir, &NullDiagnostics)
1189}
1190
1191pub(crate) fn prepared_midx_chain_strict(
1193 objects_dir: &Path,
1194) -> Result<Option<Arc<PreparedMidxChain>>> {
1195 midx_cache::prepared_chain_strict(objects_dir, &NullDiagnostics)
1196}
1197
1198fn midx_chain_layer_paths_newest_first(pack_dir: &Path) -> Vec<PathBuf> {
1200 let root = pack_dir.join("multi-pack-index");
1201 if root.is_file() {
1202 return vec![root];
1203 }
1204 let Ok(hashes) = read_chain_layer_hashes(pack_dir) else {
1205 return Vec::new();
1206 };
1207 if hashes.is_empty() {
1208 return Vec::new();
1209 }
1210 let midx_d = midx_d_dir(pack_dir);
1211 hashes
1212 .iter()
1213 .rev()
1214 .map(|h| midx_d.join(format!("multi-pack-index-{h}.midx")))
1215 .collect()
1216}
1217
1218fn repo_midx_hash_version_for_objects_dir(objects_dir: &Path) -> u8 {
1220 let Some(gitdir) = objects_dir.parent() else {
1221 return HASH_VERSION_SHA1;
1222 };
1223 sniff_objectformat_hash_version(&gitdir.join("config"))
1224}
1225
1226fn sniff_objectformat_hash_version(config_path: &Path) -> u8 {
1228 sniff_objectformat_hash_version_uncached(config_path)
1229}
1230
1231fn sniff_objectformat_hash_version_uncached(config_path: &Path) -> u8 {
1233 let text = if let Some(caches) =
1234 crate::pack_store::PackStore::repo_caches_for_config_path(config_path)
1235 {
1236 if let Some(text) = caches.cached_local_config_text_for_path(config_path) {
1237 text
1238 } else {
1239 let Ok(text) = crate::repo_caches::read_local_config_file(config_path) else {
1240 return HASH_VERSION_SHA1;
1241 };
1242 text
1243 }
1244 } else {
1245 let Ok(text) = crate::repo_caches::read_local_config_file(config_path) else {
1246 return HASH_VERSION_SHA1;
1247 };
1248 text
1249 };
1250 let mut in_extensions = false;
1254 for raw in text.lines() {
1255 let line = raw.trim();
1256 if line.starts_with('[') {
1257 let section = line.trim_start_matches('[').trim_end_matches(']');
1258 let name = section.split_whitespace().next().unwrap_or("");
1259 in_extensions = name.eq_ignore_ascii_case("extensions");
1260 continue;
1261 }
1262 if !in_extensions {
1263 continue;
1264 }
1265 if let Some((key, value)) = line.split_once('=') {
1266 if key.trim().eq_ignore_ascii_case("objectformat")
1267 && value.trim().eq_ignore_ascii_case("sha256")
1268 {
1269 return HASH_VERSION_SHA256;
1270 }
1271 }
1272 }
1273 HASH_VERSION_SHA1
1274}
1275
1276#[must_use]
1278pub fn cached_tip_midx_path(pack_dir: &Path) -> Option<std::path::PathBuf> {
1279 resolve_tip_midx_path(pack_dir)
1280}
1281
1282pub fn evict_midx_read_cache_for_pack_dir(pack_dir: &Path) {
1284 midx_cache::evict_pack_dir(pack_dir);
1285}
1286
1287pub fn resolve_tip_midx_path(pack_dir: &Path) -> Option<std::path::PathBuf> {
1289 let root = pack_dir.join("multi-pack-index");
1290 if root.exists() {
1291 return Some(root);
1292 }
1293 let hashes = read_chain_layer_hashes(pack_dir).ok()?;
1294 let last = hashes.last()?;
1295 Some(midx_d_dir(pack_dir).join(format!("multi-pack-index-{last}.midx")))
1296}
1297
1298pub fn midx_sidecar_path_for_midx_file(midx_path: &Path, ext: &str) -> Result<PathBuf> {
1303 if let Some(name) = midx_path.file_name().and_then(|s| s.to_str()) {
1304 if let Some(hash_part) = name
1305 .strip_prefix("multi-pack-index-")
1306 .and_then(|r| r.strip_suffix(".midx"))
1307 {
1308 let parent = midx_path.parent().ok_or_else(|| {
1309 Error::CorruptObject("MIDX path has no parent directory".to_owned())
1310 })?;
1311 return Ok(parent.join(format!("multi-pack-index-{hash_part}.{ext}")));
1312 }
1313 }
1314 let hash_hex = midx_checksum_hex_from_path(midx_path)?;
1315 let parent = midx_path
1316 .parent()
1317 .ok_or_else(|| Error::CorruptObject("MIDX path has no parent directory".to_owned()))?;
1318 Ok(parent.join(format!("multi-pack-index-{hash_hex}.{ext}")))
1319}
1320
1321#[must_use]
1326pub fn resolve_midx_bitmap_layer(pack_dir: &Path) -> Option<(PathBuf, PathBuf)> {
1327 let is_real_bitmap =
1328 |path: &Path| path.is_file() && path.metadata().ok().is_some_and(|m| m.len() > 32);
1329
1330 if let Ok(chain) = read_chain_layer_hashes(pack_dir) {
1331 if !chain.is_empty() {
1332 let midx_d = midx_d_dir(pack_dir);
1333 for hash in chain.iter().rev() {
1334 let midx_path = midx_d.join(format!("multi-pack-index-{hash}.midx"));
1335 if !midx_path.is_file() {
1336 continue;
1337 }
1338 let bitmap_path = midx_d.join(format!("multi-pack-index-{hash}.bitmap"));
1339 if is_real_bitmap(&bitmap_path) {
1340 return Some((bitmap_path, midx_path));
1341 }
1342 }
1343 }
1344 }
1345
1346 let root = pack_dir.join("multi-pack-index");
1347 if root.is_file() {
1348 let hash_hex = midx_checksum_hex_from_path(&root).ok()?;
1349 let bitmap_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.bitmap"));
1350 if is_real_bitmap(&bitmap_path) {
1351 return Some((bitmap_path, root));
1352 }
1353 }
1354 None
1355}
1356
1357pub fn resolve_midx_layer_path(pack_dir: &Path, checksum: &str) -> Option<std::path::PathBuf> {
1359 let checksum = checksum.to_ascii_lowercase();
1360 if let Ok(hashes) = read_chain_layer_hashes(pack_dir) {
1361 if hashes.contains(&checksum) {
1362 return Some(midx_d_dir(pack_dir).join(format!("multi-pack-index-{checksum}.midx")));
1363 }
1364 }
1365 let root = pack_dir.join("multi-pack-index");
1366 if root.exists() {
1367 if let Ok(hex) = midx_checksum_hex_from_path(&root) {
1368 if hex == checksum {
1369 return Some(root);
1370 }
1371 }
1372 }
1373 None
1374}
1375
1376fn load_midx_file(path: &Path) -> Result<Vec<u8>> {
1377 let data = fs::read(path).map_err(Error::Io)?;
1378 let _ = parse_midx_header(&data)?;
1379 Ok(data)
1380}
1381
1382fn midx_hash_len(data: &[u8]) -> usize {
1385 if data.len() > 5 && data[5] == 2 {
1386 32
1387 } else {
1388 20
1389 }
1390}
1391
1392fn oids_and_packs_from_midx_data(data: &[u8]) -> Result<(HashSet<ObjectId>, Vec<String>)> {
1393 let hash_len = midx_hash_len(data);
1394 let (_, hdr_end, _) = parse_midx_header(data)?;
1395 let (pn_off, pn_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1396 let pack_names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
1397 let (_ooff_off, ooff_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1398 let (oidl_off, oidl_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
1399 let num_objects = ooff_len / 8;
1400 if oidl_len != num_objects * hash_len {
1401 return Err(Error::CorruptObject(
1402 "MIDX oid-lookup size mismatch".to_owned(),
1403 ));
1404 }
1405 let mut oids = HashSet::with_capacity(num_objects);
1406 for i in 0..num_objects {
1407 let start = oidl_off + i * hash_len;
1408 let oid = ObjectId::from_bytes(&data[start..start + hash_len])?;
1409 oids.insert(oid);
1410 }
1411 Ok((oids, pack_names))
1412}
1413
1414fn collect_incremental_base(pack_dir: &Path) -> Result<(HashSet<ObjectId>, HashSet<String>)> {
1415 let mut oids = HashSet::new();
1416 let mut packs = HashSet::new();
1417 let root = pack_dir.join("multi-pack-index");
1418 let chain_path = chain_file_path(pack_dir);
1419 if chain_path.exists() {
1420 for h in read_chain_layer_hashes(pack_dir)? {
1421 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{h}.midx"));
1422 let data = load_midx_file(&p)?;
1423 let (layer_oids, names) = oids_and_packs_from_midx_data(&data)?;
1424 oids.extend(layer_oids);
1425 for n in names {
1426 packs.insert(n);
1427 }
1428 }
1429 return Ok((oids, packs));
1430 }
1431 if root.exists() {
1432 let data = load_midx_file(&root)?;
1433 let (o, names) = oids_and_packs_from_midx_data(&data)?;
1434 oids = o;
1435 for n in names {
1436 packs.insert(n);
1437 }
1438 }
1439 Ok((oids, packs))
1440}
1441
1442fn midx_trailing_hash_len(data: &[u8]) -> Result<usize> {
1443 if data.len() < MIDX_HEADER_SIZE {
1444 return Err(Error::CorruptObject(
1445 "midx file too small for header".to_owned(),
1446 ));
1447 }
1448 HashAlgo::try_from(data[5]).map(|a| a.len()).map_err(|e| {
1449 Error::CorruptObject(format!(
1450 "multi-pack-index hash version {} not recognized",
1451 e.0
1452 ))
1453 })
1454}
1455
1456fn midx_checksum_hex_from_path(path: &Path) -> Result<String> {
1457 let data = fs::read(path).map_err(Error::Io)?;
1458 let hash_len = midx_trailing_hash_len(&data)?;
1459 if data.len() < hash_len {
1460 return Err(Error::CorruptObject(
1461 "midx too small for checksum".to_owned(),
1462 ));
1463 }
1464 let hash = &data[data.len() - hash_len..];
1465 Ok(hex::encode(hash))
1466}
1467
1468fn link_root_midx_into_chain(pack_dir: &Path, root_checksum_hex: &str) -> Result<()> {
1469 let midx_d = midx_d_dir(pack_dir);
1470 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
1471 let dst_midx = midx_d.join(format!("multi-pack-index-{root_checksum_hex}.midx"));
1472 hard_link_or_copy(&pack_dir.join("multi-pack-index"), &dst_midx)?;
1473 let exts = ["bitmap", "rev"];
1474 for ext in exts {
1475 let src = pack_dir.join(format!("multi-pack-index-{root_checksum_hex}.{ext}"));
1476 if src.exists() {
1477 let dst = midx_d.join(format!("multi-pack-index-{root_checksum_hex}.{ext}"));
1478 hard_link_or_copy(&src, &dst)?;
1479 }
1480 }
1481 Ok(())
1482}
1483
1484fn clear_stale_split_layers(pack_dir: &Path, keep: &[String]) -> Result<()> {
1485 let midx_d = midx_d_dir(pack_dir);
1486 if !midx_d.exists() {
1487 return Ok(());
1488 }
1489 let keep: HashSet<&str> = keep.iter().map(|s| s.as_str()).collect();
1490 for ent in fs::read_dir(&midx_d).map_err(Error::Io)? {
1491 let ent = ent.map_err(Error::Io)?;
1492 let name = ent.file_name().to_string_lossy().to_string();
1493 let Some(rest) = name.strip_prefix("multi-pack-index-") else {
1494 continue;
1495 };
1496 let Some((hash_part, _ext)) = rest.split_once('.') else {
1497 continue;
1498 };
1499 if (hash_part.len() == 40 || hash_part.len() == 64) && !keep.contains(hash_part) {
1500 let _ = fs::remove_file(ent.path());
1501 }
1502 }
1503 Ok(())
1504}
1505
1506fn clear_incremental_midx_files(pack_dir: &Path) -> Result<()> {
1516 let midx_d = midx_d_dir(pack_dir);
1517 let _ = fs::remove_file(chain_file_path(pack_dir));
1519 if !midx_d.exists() {
1520 return Ok(());
1521 }
1522 for ent in fs::read_dir(&midx_d).map_err(Error::Io)? {
1523 let ent = ent.map_err(Error::Io)?;
1524 let name = ent.file_name().to_string_lossy().to_string();
1525 if name.starts_with("multi-pack-index-")
1526 && (name.ends_with(".midx") || name.ends_with(".bitmap") || name.ends_with(".rev"))
1527 {
1528 let _ = fs::remove_file(ent.path());
1529 }
1530 }
1531 Ok(())
1532}
1533
1534fn pack_mtime_for_midx(idx: &PackIndex) -> std::time::SystemTime {
1535 fs::metadata(&idx.pack_path)
1536 .and_then(|m| m.modified())
1537 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
1538}
1539
1540fn midx_pick_better_entry(
1541 cur: &MidxEntry,
1542 cand_pack: u32,
1543 cand_offset: u64,
1544 cand_mtime: std::time::SystemTime,
1545 preferred_pack: Option<u32>,
1546) -> bool {
1547 let cur_pref = preferred_pack == Some(cur.pack_id);
1548 let new_pref = preferred_pack == Some(cand_pack);
1549 if new_pref && !cur_pref {
1550 return true;
1551 }
1552 if cur_pref && !new_pref {
1553 return false;
1554 }
1555 match cand_mtime.cmp(&cur.pack_mtime) {
1556 std::cmp::Ordering::Greater => true,
1557 std::cmp::Ordering::Less => false,
1558 std::cmp::Ordering::Equal => {
1559 if cand_pack != cur.pack_id {
1560 cand_pack < cur.pack_id
1561 } else {
1562 cand_offset < cur.offset
1563 }
1564 }
1565 }
1566}
1567
1568#[allow(clippy::too_many_arguments)]
1573fn build_midx_bytes_filtered(
1574 idx_names: &[String],
1575 indexes: &[PackIndex],
1576 preferred_idx: Option<usize>,
1577 write_bitmap: bool,
1578 omit_embedded_ridx_chunk: bool,
1579 version: u8,
1580 hash_version: u8,
1581 exclude_oids: Option<&HashSet<ObjectId>>,
1582) -> Result<(Vec<u8>, Option<Vec<u32>>)> {
1583 let hash_len = if hash_version == 2 { 32 } else { 20 };
1585 let preferred_pack_idx = preferred_idx.map(|p| p as u32);
1586 let pack_mtimes: Vec<std::time::SystemTime> = indexes.iter().map(pack_mtime_for_midx).collect();
1587
1588 let mut best: HashMap<ObjectId, MidxEntry> = HashMap::new();
1589 for (pack_id, idx) in indexes.iter().enumerate() {
1590 let pack_id = u32::try_from(pack_id).map_err(|_| {
1591 Error::CorruptObject("too many pack files for multi-pack-index".to_owned())
1592 })?;
1593 let mtime = pack_mtimes[pack_id as usize];
1594 for e in idx.iter() {
1595 if e.oid().len() != hash_len {
1596 continue;
1597 }
1598 let Ok(oid) = ObjectId::from_bytes(e.oid()) else {
1599 continue;
1600 };
1601 if let Some(ex) = exclude_oids {
1602 if ex.contains(&oid) {
1603 continue;
1604 }
1605 }
1606 let offset = e.offset()?;
1607 let cand = MidxEntry {
1608 oid,
1609 pack_id,
1610 offset,
1611 pack_mtime: mtime,
1612 };
1613 match best.get(&oid) {
1614 None => {
1615 best.insert(oid, cand);
1616 }
1617 Some(cur) => {
1618 if midx_pick_better_entry(cur, pack_id, offset, mtime, preferred_pack_idx) {
1619 best.insert(oid, cand);
1620 }
1621 }
1622 }
1623 }
1624 }
1625
1626 let mut entries: Vec<MidxEntry> = best.into_values().collect();
1627 entries.sort_by_key(|a| a.oid);
1628
1629 let large_offsets_needed = entries.iter().any(|e| e.offset > u64::from(u32::MAX));
1637
1638 let num_packs = indexes.len() as u32;
1639
1640 let mut pack_names_blob = Vec::new();
1641 for name in idx_names {
1642 pack_names_blob.extend_from_slice(name.as_bytes());
1643 pack_names_blob.push(0);
1644 }
1645 let pad = (MIDX_CHUNK_ALIGNMENT - (pack_names_blob.len() % MIDX_CHUNK_ALIGNMENT))
1646 % MIDX_CHUNK_ALIGNMENT;
1647 pack_names_blob.extend(std::iter::repeat_n(0u8, pad));
1648 let chunk_pnam = pack_names_blob;
1649
1650 let mut chunk_oidf = vec![0u8; 256 * 4];
1651 let mut j = 0usize;
1652 for i in 0..256 {
1653 while j < entries.len() && entries[j].oid.as_bytes()[0] <= i as u8 {
1654 j += 1;
1655 }
1656 chunk_oidf[i * 4..(i + 1) * 4].copy_from_slice(&(j as u32).to_be_bytes());
1657 }
1658
1659 let mut chunk_oidl = Vec::with_capacity(entries.len() * 20);
1660 for e in &entries {
1661 chunk_oidl.extend_from_slice(e.oid.as_bytes());
1662 }
1663
1664 let mut large_offsets: Vec<u64> = Vec::new();
1665 let mut chunk_ooff = Vec::with_capacity(entries.len() * 8);
1666 for e in &entries {
1667 chunk_ooff.extend_from_slice(&e.pack_id.to_be_bytes());
1668 let encoded = if large_offsets_needed && e.offset >> 31 != 0 {
1669 let slot = u32::try_from(large_offsets.len()).map_err(|_| {
1670 Error::CorruptObject("too many large offsets in multi-pack-index".to_owned())
1671 })?;
1672 large_offsets.push(e.offset);
1673 MIDX_LARGE_OFFSET_NEEDED | slot
1674 } else {
1675 e.offset as u32
1679 };
1680 chunk_ooff.extend_from_slice(&encoded.to_be_bytes());
1681 }
1682
1683 let chunk_loff: Vec<u8> = if large_offsets.is_empty() {
1684 Vec::new()
1685 } else {
1686 let mut v = Vec::with_capacity(large_offsets.len() * 8);
1687 for off in &large_offsets {
1688 v.extend_from_slice(&off.to_be_bytes());
1689 }
1690 v
1691 };
1692
1693 let pref = preferred_pack_idx;
1694 let mut order: Vec<u32> = (0..entries.len() as u32).collect();
1695 order.sort_by(|&ai, &bi| {
1696 let a = &entries[ai as usize];
1697 let b = &entries[bi as usize];
1698 let a_pref = pref == Some(a.pack_id);
1699 let b_pref = pref == Some(b.pack_id);
1700 b_pref
1701 .cmp(&a_pref)
1702 .then_with(|| a.pack_id.cmp(&b.pack_id))
1703 .then_with(|| a.offset.cmp(&b.offset))
1704 .then_with(|| ai.cmp(&bi))
1705 });
1706
1707 let mut chunk_ridx = Vec::with_capacity(entries.len() * 4);
1708 for oid_idx in &order {
1709 chunk_ridx.extend_from_slice(&oid_idx.to_be_bytes());
1710 }
1711
1712 let bitmap_rid_order = if write_bitmap {
1715 Some(order.clone())
1716 } else {
1717 None
1718 };
1719 let chunk_btmp: Vec<u8> = if write_bitmap {
1720 let num_packs_usize = indexes.len();
1725 let mut bitmap_pos = vec![u32::MAX; num_packs_usize];
1726 let mut bitmap_nr = vec![0u32; num_packs_usize];
1727 for (rank, &oid_idx) in order.iter().enumerate() {
1728 let pack = entries[oid_idx as usize].pack_id as usize;
1729 if let Some(p) = bitmap_pos.get_mut(pack) {
1730 if *p == u32::MAX {
1731 *p = rank as u32;
1732 }
1733 }
1734 if let Some(n) = bitmap_nr.get_mut(pack) {
1735 *n += 1;
1736 }
1737 }
1738 let mut v = Vec::new();
1739 for pack in 0..num_packs_usize {
1740 let pos = if bitmap_pos[pack] == u32::MAX {
1741 0
1742 } else {
1743 bitmap_pos[pack]
1744 };
1745 v.extend_from_slice(&pos.to_be_bytes());
1746 v.extend_from_slice(&bitmap_nr[pack].to_be_bytes());
1747 }
1748 let pad = (MIDX_CHUNK_ALIGNMENT - (v.len() % MIDX_CHUNK_ALIGNMENT)) % MIDX_CHUNK_ALIGNMENT;
1749 v.extend(std::iter::repeat_n(0u8, pad));
1750 v
1751 } else {
1752 Vec::new()
1753 };
1754
1755 let mut chunks: Vec<(u32, Vec<u8>)> = vec![
1756 (MIDX_CHUNKID_PACKNAMES, chunk_pnam),
1757 (MIDX_CHUNKID_OIDFANOUT, chunk_oidf),
1758 (MIDX_CHUNKID_OIDLOOKUP, chunk_oidl),
1759 (MIDX_CHUNKID_OBJECTOFFSETS, chunk_ooff),
1760 ];
1761 if !chunk_loff.is_empty() {
1762 chunks.push((MIDX_CHUNKID_LARGEOFFSETS, chunk_loff));
1763 }
1764 if (pref.is_some() || write_bitmap) && !omit_embedded_ridx_chunk {
1765 chunks.push((MIDX_CHUNKID_REVINDEX, chunk_ridx));
1766 }
1767 if write_bitmap {
1768 chunks.push((MIDX_CHUNKID_BITMAPPED_PACKS, chunk_btmp));
1769 }
1770
1771 let num_chunks: u8 = chunks
1772 .len()
1773 .try_into()
1774 .map_err(|_| Error::CorruptObject("too many MIDX chunks".to_owned()))?;
1775
1776 let mut body = Vec::new();
1777 let mut cur_offset =
1778 MIDX_HEADER_SIZE as u64 + ((chunks.len() + 1) * CHUNK_TOC_ENTRY_SIZE) as u64;
1779
1780 for (id, data) in &chunks {
1781 body.extend_from_slice(&id.to_be_bytes());
1782 body.extend_from_slice(&cur_offset.to_be_bytes());
1783 cur_offset += data.len() as u64;
1784 }
1785 body.extend_from_slice(&0u32.to_be_bytes());
1786 body.extend_from_slice(&cur_offset.to_be_bytes());
1787
1788 for (_, data) in &chunks {
1789 body.extend_from_slice(data);
1790 }
1791
1792 let mut out = Vec::with_capacity(MIDX_HEADER_SIZE + body.len() + 20);
1793 out.extend_from_slice(&MIDX_SIGNATURE.to_be_bytes());
1794 out.push(if version == MIDX_VERSION_V1 {
1795 MIDX_VERSION_V1
1796 } else {
1797 MIDX_VERSION_V2
1798 });
1799 out.push(hash_version);
1800 out.push(num_chunks);
1801 out.push(0);
1802 out.extend_from_slice(&num_packs.to_be_bytes());
1803 out.extend_from_slice(&body);
1804
1805 let algo = if hash_version == 2 {
1807 HashAlgo::Sha256
1808 } else {
1809 HashAlgo::Sha1
1810 };
1811 out.extend_from_slice(&algo.checksum_digest(&out));
1812
1813 Ok((out, bitmap_rid_order))
1814}
1815
1816fn write_midx_rev_sidecar(
1821 path: &Path,
1822 pack_order: &[u32],
1823 midx_file_hash: &[u8],
1824 algo: HashAlgo,
1825) -> Result<()> {
1826 let hash_len = algo.len();
1827 let hash_id = u32::from(u8::from(algo));
1828 let body_len = RIDX_HEADER_SIZE + pack_order.len() * 4 + hash_len;
1829 let mut out = Vec::with_capacity(body_len + hash_len);
1830 out.extend_from_slice(&RIDX_SIGNATURE.to_be_bytes());
1831 out.extend_from_slice(&RIDX_VERSION.to_be_bytes());
1832 out.extend_from_slice(&hash_id.to_be_bytes());
1833 for idx in pack_order {
1834 out.extend_from_slice(&idx.to_be_bytes());
1835 }
1836 if midx_file_hash.len() >= hash_len {
1837 out.extend_from_slice(&midx_file_hash[..hash_len]);
1838 } else {
1839 return Err(Error::CorruptObject(format!(
1840 "MIDX checksum length {} does not match hash algorithm width {hash_len}",
1841 midx_file_hash.len()
1842 )));
1843 }
1844 append_hashfile_checksum(&mut out, hash_len);
1845 let mut file = fs::File::create(path).map_err(Error::Io)?;
1846 file.write_all(&out).map_err(Error::Io)?;
1847 file.sync_all().map_err(Error::Io)?;
1848 if !path.is_file() {
1849 return Err(Error::CorruptObject(format!(
1850 "MIDX rev sidecar not present after write ({})",
1851 path.display()
1852 )));
1853 }
1854 Ok(())
1855}
1856
1857fn repo_hash_algo_for_pack_dir(pack_dir: &Path) -> HashAlgo {
1858 pack_dir
1859 .parent()
1860 .map(|d| {
1861 crate::odb::hash_algo_for_objects_dir(&crate::environment::Environment::empty(), d)
1862 })
1863 .unwrap_or(HashAlgo::Sha1)
1864}
1865
1866fn find_chunk(data: &[u8], header_end: usize, chunk_id: u32) -> Result<(usize, usize)> {
1867 let (hdr, _, _) = parse_midx_header(data)?;
1868 let n = hdr.num_chunks as usize;
1869 let pos = header_end;
1870 let toc_end = pos + (n + 1) * CHUNK_TOC_ENTRY_SIZE;
1871 if data.len() < toc_end + 20 {
1872 return Err(Error::CorruptObject(
1873 "truncated MIDX chunk table".to_owned(),
1874 ));
1875 }
1876 for i in 0..n {
1877 let base = pos + i * CHUNK_TOC_ENTRY_SIZE;
1878 let id = read_be_u32(data, base)?;
1879 let off = read_be_u64(data, base + 4)? as usize;
1880 if id == chunk_id {
1881 let next_off = if i + 1 < n {
1882 let nb = pos + (i + 1) * CHUNK_TOC_ENTRY_SIZE;
1883 read_be_u64(data, nb + 4)? as usize
1884 } else {
1885 let term = pos + n * CHUNK_TOC_ENTRY_SIZE;
1886 read_be_u64(data, term + 4)? as usize
1887 };
1888 return Ok((off, next_off.saturating_sub(off)));
1889 }
1890 }
1891 Err(Error::CorruptObject(format!(
1892 "MIDX chunk {chunk_id:08x} not found"
1893 )))
1894}
1895
1896#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1898enum MidxChunkTableFatal {
1899 FileTooSmall,
1900 TableTruncated,
1901 RequiredPackNameChunkMissing,
1902}
1903
1904impl From<MidxChunkTableFatal> for MidxError {
1905 fn from(value: MidxChunkTableFatal) -> Self {
1906 match value {
1907 MidxChunkTableFatal::FileTooSmall => MidxError::InvalidChunkTable {
1908 detail: "multi-pack-index file too small".to_owned(),
1909 },
1910 MidxChunkTableFatal::TableTruncated => MidxError::InvalidChunkTable {
1911 detail: "multi-pack-index chunk table is truncated".to_owned(),
1912 },
1913 MidxChunkTableFatal::RequiredPackNameChunkMissing => {
1914 MidxError::RequiredPackNameChunkMissing
1915 }
1916 }
1917 }
1918}
1919
1920fn midx_write_warn(opts: &WriteMultiPackIndexOptions, warning: Warning) {
1921 if let Some(handle) = opts.diagnostics.as_ref() {
1922 handle.warn(warning);
1923 } else {
1924 NullDiagnostics.warn(warning);
1925 }
1926}
1927
1928struct TocEntry {
1930 id: u32,
1931 offset: usize,
1932}
1933
1934fn parse_midx_toc(
1938 data: &[u8],
1939 hash_len: usize,
1940 errors: &mut Vec<String>,
1941) -> std::result::Result<Vec<TocEntry>, MidxChunkTableFatal> {
1942 if data.len() < MIDX_HEADER_SIZE + hash_len {
1943 return Err(MidxChunkTableFatal::FileTooSmall);
1944 }
1945 let num_chunks = data[6] as usize;
1946 let toc_off = MIDX_HEADER_SIZE;
1947 let needed = toc_off + (num_chunks + 1) * CHUNK_TOC_ENTRY_SIZE;
1948 if data.len() < needed {
1949 return Err(MidxChunkTableFatal::TableTruncated);
1950 }
1951 let file_size = data.len();
1952 let mut chunks: Vec<TocEntry> = Vec::with_capacity(num_chunks);
1953
1954 let read_be64 = |off: usize| -> u64 {
1955 let mut b = [0u8; 8];
1956 b.copy_from_slice(&data[off..off + 8]);
1957 u64::from_be_bytes(b)
1958 };
1959 let read_be32 = |off: usize| -> u32 {
1960 let mut b = [0u8; 4];
1961 b.copy_from_slice(&data[off..off + 4]);
1962 u32::from_be_bytes(b)
1963 };
1964
1965 for i in 0..num_chunks {
1966 let entry = toc_off + i * CHUNK_TOC_ENTRY_SIZE;
1967 let chunk_id = read_be32(entry);
1968 let chunk_offset = read_be64(entry + 4);
1969
1970 if chunk_id == 0 {
1971 errors.push("terminating chunk id appears earlier than expected".to_owned());
1972 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1973 }
1974 if !(chunk_offset as usize).is_multiple_of(MIDX_CHUNK_ALIGNMENT) {
1975 errors.push(format!(
1976 "chunk id {chunk_id:x} not {MIDX_CHUNK_ALIGNMENT}-byte aligned"
1977 ));
1978 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1979 }
1980
1981 let next_entry = toc_off + (i + 1) * CHUNK_TOC_ENTRY_SIZE;
1982 let next_chunk_offset = read_be64(next_entry + 4);
1983
1984 if next_chunk_offset < chunk_offset
1985 || next_chunk_offset > (file_size as u64).saturating_sub(hash_len as u64)
1986 {
1987 errors.push(format!(
1988 "improper chunk offset(s) {chunk_offset:x} and {next_chunk_offset:x}"
1989 ));
1990 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1991 }
1992
1993 if chunks.iter().any(|c| c.id == chunk_id) {
1994 errors.push(format!("duplicate chunk ID {chunk_id:x} found"));
1995 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1996 }
1997
1998 chunks.push(TocEntry {
1999 id: chunk_id,
2000 offset: chunk_offset as usize,
2001 });
2002 }
2003
2004 let term_entry = toc_off + num_chunks * CHUNK_TOC_ENTRY_SIZE;
2006 let final_id = read_be32(term_entry);
2007 if final_id != 0 {
2008 errors.push(format!("final chunk has non-zero id {final_id:x}"));
2009 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
2010 }
2011
2012 let term_offset = read_be64(term_entry + 4) as usize;
2015 chunks.push(TocEntry {
2016 id: 0,
2017 offset: term_offset,
2018 });
2019
2020 Ok(chunks)
2021}
2022
2023fn toc_chunk_range(chunks: &[TocEntry], data_len: usize, id: u32) -> Option<(usize, usize)> {
2025 for (i, c) in chunks.iter().enumerate() {
2026 if c.id == id {
2027 let next = if i + 1 < chunks.len() {
2028 chunks[i + 1].offset
2029 } else {
2030 data_len.saturating_sub(20)
2031 };
2032 return Some((c.offset, next.saturating_sub(c.offset)));
2033 }
2034 }
2035 None
2036}
2037
2038pub fn verify_midx(objects_dir: &Path) -> std::result::Result<(), Vec<String>> {
2045 let pack_dir = objects_dir.join("pack");
2046 let path = match resolve_tip_midx_path(&pack_dir) {
2047 Some(p) => p,
2048 None => return Ok(()),
2049 };
2050 let data = match open_midx_layer_bytes(&path) {
2051 Ok(bytes) => bytes,
2052 Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2053 Err(_) => return Ok(()),
2054 };
2055 let data = data.as_ref();
2056
2057 let mut fatal: Vec<String> = Vec::new();
2058 let mut errors: Vec<String> = Vec::new();
2059
2060 if data.len() < MIDX_HEADER_SIZE + 20 {
2062 return Err(vec!["multi-pack-index file is too small".to_owned()]);
2063 }
2064 let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
2065 if sig != MIDX_SIGNATURE {
2066 return Err(vec![format!(
2067 "multi-pack-index signature 0x{sig:08x} does not match signature 0x{MIDX_SIGNATURE:08x}"
2068 )]);
2069 }
2070 let version = data[4];
2071 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
2072 return Err(vec![format!(
2073 "multi-pack-index version {version} not recognized"
2074 )]);
2075 }
2076 let hash_version = data[5];
2077 let expected_hash_version = repo_midx_hash_version_for_objects_dir(objects_dir);
2078 if hash_version != expected_hash_version {
2079 return Err(vec![format!(
2080 "multi-pack-index hash version {hash_version} does not match version {expected_hash_version}"
2081 )]);
2082 }
2083 let hash_len = if hash_version == 2 { 32usize } else { 20usize };
2084 let num_packs = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
2085
2086 let chunks = match parse_midx_toc(data, hash_len, &mut errors) {
2088 Ok(c) => c,
2089 Err(fatal) => {
2090 if !errors.is_empty() {
2091 return Err(errors);
2092 }
2093 errors.push(MidxError::from(fatal).to_string());
2094 return Err(errors);
2095 }
2096 };
2097
2098 let Some((pn_off, pn_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_PACKNAMES)
2100 else {
2101 errors.push("multi-pack-index required pack-name chunk missing or corrupted".to_owned());
2102 return Err(errors);
2103 };
2104
2105 let Some((fan_off, fan_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDFANOUT)
2107 else {
2108 errors.push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
2109 return Err(errors);
2110 };
2111 if fan_len != 256 * 4 {
2112 errors.push("multi-pack-index OID fanout is of the wrong size".to_owned());
2113 errors.push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
2114 return Err(errors);
2115 }
2116 let fanout = |i: usize| -> u32 {
2117 let b = fan_off + i * 4;
2118 u32::from_be_bytes([data[b], data[b + 1], data[b + 2], data[b + 3]])
2119 };
2120 for i in 0..255 {
2121 let f1 = fanout(i);
2122 let f2 = fanout(i + 1);
2123 if f1 > f2 {
2124 errors.push(format!(
2125 "oid fanout out of order: fanout[{i}] = {f1:x} > {f2:x} = fanout[{}]",
2126 i + 1
2127 ));
2128 errors
2129 .push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
2130 return Err(errors);
2131 }
2132 }
2133 let num_objects = fanout(255) as usize;
2134
2135 let Some((oidl_off, oidl_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDLOOKUP)
2137 else {
2138 errors.push("multi-pack-index required OID lookup chunk missing or corrupted".to_owned());
2139 return Err(errors);
2140 };
2141 if oidl_len != hash_len * num_objects {
2142 errors.push("multi-pack-index OID lookup chunk is the wrong size".to_owned());
2143 errors.push("multi-pack-index required OID lookup chunk missing or corrupted".to_owned());
2144 return Err(errors);
2145 }
2146
2147 let Some((ooff_off, ooff_len)) =
2149 toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OBJECTOFFSETS)
2150 else {
2151 errors
2152 .push("multi-pack-index required object offsets chunk missing or corrupted".to_owned());
2153 return Err(errors);
2154 };
2155 if ooff_len != num_objects * 8 {
2156 errors.push("multi-pack-index object offset chunk is the wrong size".to_owned());
2157 errors
2158 .push("multi-pack-index required object offsets chunk missing or corrupted".to_owned());
2159 return Err(errors);
2160 }
2161
2162 let large_off = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_LARGEOFFSETS);
2163
2164 let names = match parse_pack_names_blob(&data[pn_off..pn_off + pn_len]) {
2166 Ok(n) => n,
2167 Err(_) => {
2168 errors.push("multi-pack-index pack-name chunk is too short".to_owned());
2169 return Err(errors);
2170 }
2171 };
2172 if version == MIDX_VERSION_V1 {
2173 for i in 1..names.len() {
2174 if names[i] <= names[i - 1] {
2175 fatal.push(format!(
2176 "multi-pack-index pack names out of order: '{}' before '{}'",
2177 names[i - 1],
2178 names[i]
2179 ));
2180 errors.extend(fatal);
2182 return Err(errors);
2183 }
2184 }
2185 }
2186
2187 if !midx_checksum_is_valid(data) {
2189 errors.push("incorrect checksum".to_owned());
2190 }
2191
2192 let mut pack_indexes: Vec<Option<PackIndex>> = Vec::with_capacity(num_packs);
2194 for i in 0..num_packs {
2195 let loaded = match names.get(i) {
2201 Some(name) => read_pack_index_no_verify(&pack_dir.join(name)).ok(),
2202 None => None,
2203 };
2204 if loaded.is_none() {
2205 errors.push(format!("failed to load pack in position {i}"));
2206 }
2207 pack_indexes.push(loaded);
2208 }
2209
2210 if num_objects == 0 {
2211 errors.push("the midx contains no oid".to_owned());
2212 if errors.is_empty() {
2213 return Ok(());
2214 }
2215 return Err(errors);
2216 }
2217
2218 let oid_at =
2220 |i: usize| -> &[u8] { &data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len] };
2221 for i in 0..num_objects.saturating_sub(1) {
2222 let a = oid_at(i);
2223 let b = oid_at(i + 1);
2224 if a >= b {
2225 errors.push(format!(
2226 "oid lookup out of order: oid[{i}] = {} >= {} = oid[{}]",
2227 hex::encode(a),
2228 hex::encode(b),
2229 i + 1
2230 ));
2231 }
2232 }
2233
2234 for i in 0..num_objects {
2236 let ob = ooff_off + i * 8;
2237 let pack_int_id = u32::from_be_bytes([data[ob], data[ob + 1], data[ob + 2], data[ob + 3]]);
2238 let off_raw = u32::from_be_bytes([data[ob + 4], data[ob + 5], data[ob + 6], data[ob + 7]]);
2239 let oid_hex = hex::encode(oid_at(i));
2240
2241 if pack_int_id as usize >= num_packs {
2242 errors.push(format!(
2243 "bad pack-int-id: {pack_int_id} ({num_packs} total packs)"
2244 ));
2245 errors.push(format!(
2246 "failed to load pack entry for oid[{i}] = {oid_hex}"
2247 ));
2248 continue;
2249 }
2250
2251 let m_offset: u64 = if off_raw & MIDX_LARGE_OFFSET_NEEDED != 0 {
2253 let slot = (off_raw & !MIDX_LARGE_OFFSET_NEEDED) as usize;
2254 match large_off {
2255 Some((lo_off, lo_len)) if (slot + 1) * 8 <= lo_len => {
2256 let b = lo_off + slot * 8;
2257 let mut arr = [0u8; 8];
2258 arr.copy_from_slice(&data[b..b + 8]);
2259 u64::from_be_bytes(arr)
2260 }
2261 _ => {
2262 errors.push("multi-pack-index large offset out of bounds".to_owned());
2263 continue;
2264 }
2265 }
2266 } else {
2267 u64::from(off_raw)
2268 };
2269
2270 let Some(Some(idx)) = pack_indexes.get(pack_int_id as usize) else {
2271 errors.push(format!(
2272 "failed to load pack entry for oid[{i}] = {oid_hex}"
2273 ));
2274 continue;
2275 };
2276 let Ok(oid) = ObjectId::from_bytes(oid_at(i)) else {
2277 errors.push(format!(
2278 "failed to load pack entry for oid[{i}] = {oid_hex}"
2279 ));
2280 continue;
2281 };
2282 match idx.find_offset(&oid) {
2283 Ok(Some(p_offset)) => {
2284 if m_offset != p_offset {
2285 errors.push(format!(
2286 "incorrect object offset for oid[{i}] = {oid_hex}: {m_offset:x} != {p_offset:x}"
2287 ));
2288 }
2289 }
2290 Ok(None) => {
2291 errors.push(format!(
2292 "failed to load pack entry for oid[{i}] = {oid_hex}"
2293 ));
2294 }
2295 Err(err) => {
2296 errors.push(format!(
2297 "failed to load pack entry for oid[{i}] = {oid_hex}: {err}"
2298 ));
2299 }
2300 }
2301 }
2302
2303 if errors.is_empty() {
2304 Ok(())
2305 } else {
2306 Err(errors)
2307 }
2308}
2309
2310fn midx_checksum_is_valid(data: &[u8]) -> bool {
2313 let hash_len = midx_hash_len(data);
2314 if data.len() < hash_len {
2315 return false;
2316 }
2317 HashAlgo::from_len(hash_len).is_some_and(|algo| verify_trailer(algo, data).is_ok())
2318}
2319
2320pub fn read_midx_pack_idx_names(objects_dir: &Path) -> Result<Vec<String>> {
2331 let pack_dir = objects_dir.join("pack");
2332 let path = resolve_tip_midx_path(&pack_dir)
2333 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2334 let metadata = fs::metadata(&path).map_err(Error::Io)?;
2335 read_midx_pack_idx_names_from_path(&path, &metadata)
2336}
2337
2338pub(crate) fn read_midx_pack_idx_names_from_path(
2341 path: &Path,
2342 metadata: &fs::Metadata,
2343) -> Result<Vec<String>> {
2344 if let Some(names) = midx_cache::cached_pack_names(path, metadata) {
2345 return Ok(names);
2346 }
2347 let data = open_midx_layer_bytes(path)?;
2350 let (_, hdr_end, _) = parse_midx_header(&data)?;
2351 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2352 let blob = pn_off
2353 .checked_add(pn_len)
2354 .and_then(|end| data.get(pn_off..end))
2355 .ok_or_else(|| Error::CorruptObject("MIDX pack names are outside the file".to_owned()))?;
2356 parse_pack_names_blob(blob)
2357}
2358
2359pub struct MidxObjectRef {
2361 pub oid: ObjectId,
2363 pub pack_int_id: usize,
2365}
2366
2367pub fn read_midx_objects(objects_dir: &Path) -> Result<(Vec<String>, Vec<MidxObjectRef>)> {
2371 let pack_dir = objects_dir.join("pack");
2372 let path = resolve_tip_midx_path(&pack_dir)
2373 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2374 let data = fs::read(&path).map_err(Error::Io)?;
2375 let (_, hdr_end, _) = parse_midx_header(&data)?;
2376 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2377 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2378 let hash_len = midx_hash_len(&data);
2379 let (oidl_off, oidl_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2380 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2381 if oidl_len % hash_len != 0 || ooff_len % 8 != 0 {
2382 return Err(Error::CorruptObject(
2383 "bad MIDX oid-lookup / object-offsets size".to_owned(),
2384 ));
2385 }
2386 let num = oidl_len / hash_len;
2387 if num * 8 != ooff_len {
2388 return Err(Error::CorruptObject(
2389 "MIDX oid count does not match object-offsets".to_owned(),
2390 ));
2391 }
2392 let mut objects = Vec::with_capacity(num);
2393 for i in 0..num {
2394 let oid =
2395 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])
2396 .map_err(|e| Error::CorruptObject(e.to_string()))?;
2397 let base = ooff_off + i * 8;
2398 let pack_id = read_be_u32(&data, base)? as usize;
2399 objects.push(MidxObjectRef {
2400 oid,
2401 pack_int_id: pack_id,
2402 });
2403 }
2404 Ok((names, objects))
2405}
2406
2407pub fn midx_checksum_hex(objects_dir: &Path) -> Result<String> {
2409 let pack_dir = objects_dir.join("pack");
2410 let path = resolve_tip_midx_path(&pack_dir)
2411 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2412 midx_checksum_hex_from_path(&path)
2413}
2414
2415fn resolve_read_midx_path(pack_dir: &Path, checksum: Option<&str>) -> Result<std::path::PathBuf> {
2420 match checksum {
2421 Some(cs) => resolve_midx_layer_path(pack_dir, cs)
2422 .ok_or_else(|| Error::CorruptObject(format!("could not find MIDX with checksum {cs}"))),
2423 None => resolve_tip_midx_path(pack_dir)
2424 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned())),
2425 }
2426}
2427
2428pub fn format_midx_show_objects(objects_dir: &Path) -> Result<String> {
2431 format_midx_show_objects_layer(objects_dir, None)
2432}
2433
2434pub fn format_midx_show_objects_layer(
2436 objects_dir: &Path,
2437 checksum: Option<&str>,
2438) -> Result<String> {
2439 let mut out = format_midx_dump_layer(objects_dir, checksum)?;
2440 let pack_dir = objects_dir.join("pack");
2441 let path = resolve_read_midx_path(&pack_dir, checksum)?;
2442 let data = fs::read(&path).map_err(Error::Io)?;
2443 let (_, hdr_end, _) = parse_midx_header(&data)?;
2444 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2445 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2446 let hash_len = midx_hash_len(&data);
2447 let (oidl_off, oidl_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2448 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2449 if oidl_len % hash_len != 0 || ooff_len % 8 != 0 {
2450 return Err(Error::CorruptObject(
2451 "bad MIDX oid-lookup / object-offsets size".to_owned(),
2452 ));
2453 }
2454 let num = oidl_len / hash_len;
2455 if num * 8 != ooff_len {
2456 return Err(Error::CorruptObject(
2457 "MIDX oid count does not match object-offsets".to_owned(),
2458 ));
2459 }
2460 for i in 0..num {
2461 let oid =
2462 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])
2463 .map_err(|e| Error::CorruptObject(e.to_string()))?;
2464 let base = ooff_off + i * 8;
2465 let pack_id = read_be_u32(&data, base)? as usize;
2466 let offset = u64::from(read_be_u32(&data, base + 4)?);
2467 let idx_name = names
2468 .get(pack_id)
2469 .ok_or_else(|| Error::CorruptObject("pack id out of range in MIDX".to_owned()))?;
2470 let stem = idx_name.strip_suffix(".idx").unwrap_or(idx_name);
2474 let dir_disp = objects_dir.display().to_string();
2475 let dir_disp = if objects_dir.is_absolute() || dir_disp.starts_with("./") {
2476 dir_disp
2477 } else {
2478 format!("./{dir_disp}")
2479 };
2480 out.push_str(&format!(
2481 "{} {}\t{}/pack/{}.pack\n",
2482 oid.to_hex(),
2483 offset,
2484 dir_disp,
2485 stem
2486 ));
2487 }
2488 Ok(out)
2489}
2490
2491pub fn format_midx_dump(objects_dir: &Path) -> Result<String> {
2493 format_midx_dump_layer(objects_dir, None)
2494}
2495
2496pub fn format_midx_dump_layer(objects_dir: &Path, checksum: Option<&str>) -> Result<String> {
2499 let pack_dir = objects_dir.join("pack");
2500 let path = resolve_read_midx_path(&pack_dir, checksum)?;
2501 let data = fs::read(&path).map_err(Error::Io)?;
2502 let (hdr, hdr_end, _) = parse_midx_header(&data)?;
2503 let sig = read_be_u32(&data, 0)?;
2504 let version = data[4];
2505 let hash_len: u8 = match data[5] {
2508 1 => 20,
2509 2 => 32,
2510 other => other,
2511 };
2512 let num_chunks = hdr.num_chunks;
2513 let num_packs = read_be_u32(&data, 8)?;
2514
2515 let mut chunk_tags: Vec<&'static str> = Vec::new();
2516 let n = num_chunks as usize;
2517 let pos = hdr_end;
2518 let toc_end = pos + (n + 1) * CHUNK_TOC_ENTRY_SIZE;
2519 if data.len() < toc_end + 20 {
2520 return Err(Error::CorruptObject(
2521 "truncated MIDX chunk table".to_owned(),
2522 ));
2523 }
2524 for i in 0..n {
2525 let base = pos + i * CHUNK_TOC_ENTRY_SIZE;
2526 let id = read_be_u32(&data, base)?;
2527 let tag = match id {
2528 x if x == MIDX_CHUNKID_PACKNAMES => "pack-names",
2529 x if x == MIDX_CHUNKID_OIDFANOUT => "oid-fanout",
2530 x if x == MIDX_CHUNKID_OIDLOOKUP => "oid-lookup",
2531 x if x == MIDX_CHUNKID_OBJECTOFFSETS => "object-offsets",
2532 x if x == MIDX_CHUNKID_LARGEOFFSETS => "large-offsets",
2533 x if x == MIDX_CHUNKID_REVINDEX => "revindex",
2534 x if x == MIDX_CHUNKID_BITMAPPED_PACKS => "bitmapped-packs",
2535 _ => "unknown",
2536 };
2537 chunk_tags.push(tag);
2538 }
2539
2540 let (_ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2541 let num_objects = ooff_len / 8;
2542
2543 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2544 let pack_names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2545
2546 let mut out = String::new();
2547 out.push_str(&format!(
2548 "header: {:08x} {} {} {} {}\n",
2549 sig, version, hash_len, num_chunks, num_packs
2550 ));
2551 out.push_str("chunks:");
2552 for t in &chunk_tags {
2553 out.push(' ');
2554 out.push_str(t);
2555 }
2556 out.push('\n');
2557 out.push_str(&format!("num_objects: {num_objects}\n"));
2558 out.push_str("packs:\n");
2559 for n in &pack_names {
2560 out.push_str(n);
2561 out.push('\n');
2562 }
2563 out.push_str(&format!("object-dir: {}\n", objects_dir.display()));
2564 Ok(out)
2565}
2566
2567#[derive(Debug, Clone)]
2573pub struct MidxReuseTables {
2574 pub oids: Vec<ObjectId>,
2576 pub pack_and_offset: Vec<(u32, u64)>,
2578 pub rid_order: Vec<u32>,
2580 pub oid_idx_to_rank: Vec<u32>,
2582}
2583
2584pub fn load_midx_reuse_tables(objects_dir: &Path) -> Result<Option<MidxReuseTables>> {
2588 let pack_dir = objects_dir.join("pack");
2589 let Some(path) = resolve_tip_midx_path(&pack_dir) else {
2590 return Ok(None);
2591 };
2592 load_midx_reuse_tables_from_path(&path)
2593}
2594
2595pub fn load_midx_reuse_tables_from_path(midx_path: &Path) -> Result<Option<MidxReuseTables>> {
2597 let data = fs::read(midx_path).map_err(Error::Io)?;
2598 let hash_len = midx_hash_len(&data);
2599 let (_, hdr_end, _) = parse_midx_header(&data)?;
2600 let (oidl_off, oid_l_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2601 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2602 if oid_l_len % hash_len != 0 || ooff_len != oid_l_len / hash_len * 8 {
2603 return Err(Error::CorruptObject(
2604 "MIDX OID / offset chunk size mismatch".to_owned(),
2605 ));
2606 }
2607 let num_objects = oid_l_len / hash_len;
2608 if num_objects == 0 {
2609 return Ok(None);
2610 }
2611
2612 let rid_order =
2613 if let Ok((ridx_off, ridx_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_REVINDEX) {
2614 if ridx_len != num_objects.saturating_mul(4) {
2615 return Err(Error::CorruptObject(
2616 "MIDX reverse index length does not match object count".to_owned(),
2617 ));
2618 }
2619 let mut order = Vec::with_capacity(num_objects);
2620 for i in 0..num_objects {
2621 let base = ridx_off + i * 4;
2622 order.push(read_be_u32(&data, base)?);
2623 }
2624 order
2625 } else {
2626 let hash_hex = hex::encode(&data[data.len().saturating_sub(hash_len)..]);
2627 let rev_path = midx_path
2628 .parent()
2629 .map(|p| p.join(format!("multi-pack-index-{hash_hex}.rev")))
2630 .unwrap_or_else(|| PathBuf::from(format!("multi-pack-index-{hash_hex}.rev")));
2631 let rev_data = match fs::read(&rev_path) {
2632 Ok(d) => d,
2633 Err(_) => return Ok(None),
2634 };
2635 crate::pack_rev::try_rev_positions_in_pack_order(&rev_data, num_objects).ok_or_else(
2636 || {
2637 Error::CorruptObject(
2638 "MIDX standalone .rev sidecar is invalid or missing".to_owned(),
2639 )
2640 },
2641 )?
2642 };
2643
2644 let mut oids = Vec::with_capacity(num_objects);
2645 for i in 0..num_objects {
2646 let base = oidl_off + i * hash_len;
2647 oids.push(ObjectId::from_bytes(&data[base..base + hash_len])?);
2648 }
2649
2650 let mut pack_and_offset = Vec::with_capacity(num_objects);
2651 for i in 0..num_objects {
2652 let ob = ooff_off + i * 8;
2653 let pack_id = read_be_u32(&data, ob)?;
2654 let off32 = read_be_u32(&data, ob + 4)?;
2655 pack_and_offset.push((pack_id, u64::from(off32)));
2656 }
2657
2658 let mut oid_idx_to_rank = vec![0u32; num_objects];
2659 for (rank, &oid_idx) in rid_order.iter().enumerate() {
2660 let idx = usize::try_from(oid_idx)
2661 .map_err(|_| Error::CorruptObject("bad MIDX reverse index entry".to_owned()))?;
2662 if idx >= num_objects {
2663 return Err(Error::CorruptObject(
2664 "MIDX reverse index out of range".to_owned(),
2665 ));
2666 }
2667 oid_idx_to_rank[idx] = u32::try_from(rank)
2668 .map_err(|_| Error::CorruptObject("too many MIDX objects".to_owned()))?;
2669 }
2670
2671 Ok(Some(MidxReuseTables {
2672 oids,
2673 pack_and_offset,
2674 rid_order,
2675 oid_idx_to_rank,
2676 }))
2677}
2678
2679pub(crate) fn midx_reuse_tables_from_bytes(
2681 data: &[u8],
2682 rid_order: &[u32],
2683) -> Result<MidxReuseTables> {
2684 let hash_len = midx_hash_len(data);
2685 let (_, hdr_end, _) = parse_midx_header(data)?;
2686 let (oidl_off, oid_l_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2687 let (ooff_off, ooff_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2688 if oid_l_len % hash_len != 0 || ooff_len != oid_l_len / hash_len * 8 {
2689 return Err(Error::CorruptObject(
2690 "MIDX OID / offset chunk size mismatch".to_owned(),
2691 ));
2692 }
2693 let num_objects = oid_l_len / hash_len;
2694 if rid_order.len() != num_objects {
2695 return Err(Error::CorruptObject(
2696 "MIDX reverse index length does not match object count".to_owned(),
2697 ));
2698 }
2699
2700 let mut oids = Vec::with_capacity(num_objects);
2701 for i in 0..num_objects {
2702 let base = oidl_off + i * hash_len;
2703 oids.push(ObjectId::from_bytes(&data[base..base + hash_len])?);
2704 }
2705
2706 let mut pack_and_offset = Vec::with_capacity(num_objects);
2707 for i in 0..num_objects {
2708 let ob = ooff_off + i * 8;
2709 let pack_id = read_be_u32(data, ob)?;
2710 let off32 = read_be_u32(data, ob + 4)?;
2711 pack_and_offset.push((pack_id, u64::from(off32)));
2712 }
2713
2714 let mut oid_idx_to_rank = vec![0u32; num_objects];
2715 for (rank, &oid_idx) in rid_order.iter().enumerate() {
2716 let idx = usize::try_from(oid_idx)
2717 .map_err(|_| Error::CorruptObject("bad MIDX reverse index entry".to_owned()))?;
2718 if idx >= num_objects {
2719 return Err(Error::CorruptObject(
2720 "MIDX reverse index out of range".to_owned(),
2721 ));
2722 }
2723 oid_idx_to_rank[idx] = u32::try_from(rank)
2724 .map_err(|_| Error::CorruptObject("too many MIDX objects".to_owned()))?;
2725 }
2726
2727 Ok(MidxReuseTables {
2728 oids,
2729 pack_and_offset,
2730 rid_order: rid_order.to_vec(),
2731 oid_idx_to_rank,
2732 })
2733}
2734
2735impl MidxReuseTables {
2736 #[must_use]
2738 pub fn global_bitmap_bit(&self, oid: &ObjectId) -> Option<u32> {
2739 let oid_idx = self.oids.binary_search(oid).ok()?;
2740 Some(self.oid_idx_to_rank[oid_idx])
2741 }
2742
2743 #[must_use]
2748 pub fn canonical_pack(&self, oid: &ObjectId) -> Option<u32> {
2749 let oid_idx = self.oids.binary_search(oid).ok()?;
2750 Some(self.pack_and_offset[oid_idx].0)
2751 }
2752}
2753
2754#[derive(Debug, Clone, Copy)]
2756pub struct MidxBtmpPackRange {
2757 pub pack_id: u32,
2759 pub bitmap_pos: u32,
2761 pub bitmap_nr: u32,
2763}
2764
2765pub fn read_midx_btmp_ranges(objects_dir: &Path) -> Result<Vec<MidxBtmpPackRange>> {
2769 let pack_dir = objects_dir.join("pack");
2770 let Some(path) = resolve_tip_midx_path(&pack_dir) else {
2771 return Ok(Vec::new());
2772 };
2773 let data = fs::read(&path).map_err(Error::Io)?;
2774 let (_, hdr_end, _) = parse_midx_header(&data)?;
2775 let Ok((btmp_off, btmp_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_BITMAPPED_PACKS) else {
2776 return Ok(Vec::new());
2777 };
2778 if btmp_len == 0 || btmp_len % 8 != 0 {
2779 return Err(Error::CorruptObject(
2780 "invalid MIDX BTMP chunk length".to_owned(),
2781 ));
2782 }
2783 let num_packs = read_be_u32(&data, 8)?;
2784 let n_entries = btmp_len / 8;
2785 if u32::try_from(n_entries).ok() != Some(num_packs) {
2786 return Err(Error::CorruptObject(
2787 "MIDX BTMP entry count does not match num_packs".to_owned(),
2788 ));
2789 }
2790 let mut out = Vec::with_capacity(n_entries);
2791 for i in 0..n_entries {
2792 let base = btmp_off + i * 8;
2793 let bitmap_pos = read_be_u32(&data, base)?;
2794 let bitmap_nr = read_be_u32(&data, base + 4)?;
2795 out.push(MidxBtmpPackRange {
2796 pack_id: u32::try_from(i)
2797 .map_err(|_| Error::CorruptObject("too many packs in MIDX BTMP".to_owned()))?,
2798 bitmap_pos,
2799 bitmap_nr,
2800 });
2801 }
2802 Ok(out)
2803}
2804
2805pub fn format_midx_bitmapped_packs(objects_dir: &Path) -> Result<String> {
2810 let pack_dir = objects_dir.join("pack");
2811 let path = resolve_tip_midx_path(&pack_dir)
2812 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2813 let data = fs::read(&path).map_err(Error::Io)?;
2814 let (_, hdr_end, _) = parse_midx_header(&data)?;
2815 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2816 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2817 let Ok((btmp_off, btmp_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_BITMAPPED_PACKS) else {
2818 return Err(Error::CorruptObject(
2819 "MIDX does not contain the BTMP chunk".to_owned(),
2820 ));
2821 };
2822 let n_entries = btmp_len / 8;
2823 let mut out = String::new();
2824 for i in 0..n_entries {
2825 let base = btmp_off + i * 8;
2826 let bitmap_pos = read_be_u32(&data, base)?;
2827 let bitmap_nr = read_be_u32(&data, base + 4)?;
2828 let idx_name = names.get(i).ok_or_else(|| {
2829 Error::CorruptObject("BTMP entry has no corresponding pack name".to_owned())
2830 })?;
2831 let stem = idx_name.strip_suffix(".idx").unwrap_or(idx_name);
2832 out.push_str(&format!("{stem}.pack\n"));
2833 out.push_str(&format!(" bitmap_pos: {bitmap_pos}\n"));
2834 out.push_str(&format!(" bitmap_nr: {bitmap_nr}\n"));
2835 }
2836 Ok(out)
2837}
2838
2839pub fn midx_lookup_pack_and_offset_opt(
2843 objects_dir: &Path,
2844 oid: &ObjectId,
2845) -> Result<Option<(u32, u64)>> {
2846 let Some(chain) = prepared_midx_chain(objects_dir)? else {
2847 return Ok(None);
2848 };
2849 Ok(chain.lookup_pack_and_offset(oid))
2850}
2851
2852pub fn midx_lookup_pack_and_offset(objects_dir: &Path, oid: &ObjectId) -> Result<(u32, u64)> {
2854 let Some(chain) = prepared_midx_chain(objects_dir)? else {
2855 return Err(Error::CorruptObject("no multi-pack-index found".to_owned()));
2856 };
2857 chain.lookup_pack_and_offset(oid).ok_or_else(|| {
2858 Error::CorruptObject(format!("object {} not in multi-pack-index", oid.to_hex()))
2859 })
2860}
2861
2862pub fn midx_oid_listed_in_tip(objects_dir: &Path, oid: &ObjectId) -> Result<Option<bool>> {
2866 let Some(chain) = prepared_midx_chain(objects_dir)? else {
2867 return Ok(None);
2868 };
2869 Ok(Some(chain.oid_listed_in_tip(oid)))
2870}
2871
2872struct MidxReadView {
2874 oidf_off: usize,
2875 oidl_off: usize,
2876 ooff_off: usize,
2877 loff: Option<(usize, usize)>,
2878 num_objects: usize,
2879 pack_names: Vec<String>,
2880}
2881
2882enum MidxLoadResult {
2883 Ok(MidxReadView),
2884 Skip,
2886}
2887
2888fn midx_load_for_read(
2892 data: &PackData,
2893 expected_hash_version: u8,
2894 diagnostics: &dyn DiagnosticSink,
2895) -> Result<MidxLoadResult> {
2896 if data.len() < MIDX_HEADER_SIZE + 20 {
2897 diagnostics.warn(Warning::MidxChunkTableError {
2898 detail: "multi-pack-index file is too small".to_owned(),
2899 });
2900 return Ok(MidxLoadResult::Skip);
2901 }
2902 let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
2903 if sig != MIDX_SIGNATURE {
2904 return Err(MidxError::SignatureMismatch {
2905 found: sig,
2906 expected: MIDX_SIGNATURE,
2907 }
2908 .into());
2909 }
2910 let version = data[4];
2911 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
2912 return Err(MidxError::VersionNotRecognized { version }.into());
2913 }
2914 let hash_version = data[5];
2915 if hash_version != expected_hash_version {
2916 return Err(MidxError::HashVersionMismatch {
2917 found: hash_version,
2918 expected: expected_hash_version,
2919 }
2920 .into());
2921 }
2922 let hash_len = if hash_version == 2 { 32usize } else { 20usize };
2923 let num_packs = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
2924
2925 let mut toc_errors: Vec<String> = Vec::new();
2926 let chunks = match parse_midx_toc(data, hash_len, &mut toc_errors) {
2927 Ok(c) => c,
2928 Err(fatal) => {
2929 if !toc_errors.is_empty() {
2930 for e in toc_errors {
2931 diagnostics.warn(Warning::MidxChunkTableError { detail: e });
2932 }
2933 return Ok(MidxLoadResult::Skip);
2934 }
2935 return Err(MidxError::from(fatal).into());
2936 }
2937 };
2938
2939 let Some((pn_off, pn_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_PACKNAMES)
2940 else {
2941 return Err(MidxError::RequiredPackNameChunkMissing.into());
2942 };
2943
2944 let Some((oidf_off, oidf_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDFANOUT)
2945 else {
2946 return Err(MidxError::RequiredOidFanoutChunkMissing.into());
2947 };
2948 if oidf_len != 256 * 4 {
2949 return Err(MidxError::OidFanoutWrongSize.into());
2950 }
2951 let fanout = |i: usize| -> u32 {
2952 let b = oidf_off + i * 4;
2953 u32::from_be_bytes([data[b], data[b + 1], data[b + 2], data[b + 3]])
2954 };
2955 let num_objects = fanout(255) as usize;
2956
2957 let Some((oidl_off, oidl_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDLOOKUP)
2958 else {
2959 return Err(MidxError::RequiredOidLookupChunkMissing.into());
2960 };
2961 if oidl_len != hash_len * num_objects {
2962 return Err(MidxError::OidLookupWrongSize.into());
2963 }
2964
2965 let Some((ooff_off, ooff_len)) =
2966 toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OBJECTOFFSETS)
2967 else {
2968 return Err(MidxError::RequiredObjectOffsetsChunkMissing.into());
2969 };
2970 if ooff_len != num_objects * 8 {
2971 return Err(MidxError::ObjectOffsetsWrongSize.into());
2972 }
2973
2974 let loff = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_LARGEOFFSETS);
2975
2976 if let Some((_, rlen)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_REVINDEX) {
2977 if rlen != num_objects * 4 {
2978 diagnostics.warn(Warning::MidxRevIndexWrongSize);
2979 diagnostics.warn(Warning::MidxBitmapMissingReverseIndex);
2980 }
2981 }
2982
2983 let mut pack_names: Vec<String> = Vec::with_capacity(num_packs);
2984 let blob = &data[pn_off..pn_off + pn_len];
2985 let mut start = 0usize;
2986 for _ in 0..num_packs {
2987 let Some(rel) = blob[start..].iter().position(|&b| b == 0) else {
2988 return Err(MidxError::PackNameChunkTooShort.into());
2989 };
2990 let name = match std::str::from_utf8(&blob[start..start + rel]) {
2991 Ok(s) => s.to_string(),
2992 Err(_) => return Err(MidxError::PackNameChunkTooShort.into()),
2993 };
2994 pack_names.push(name);
2995 start += rel + 1;
2996 }
2997
2998 let payload_end = data.len() - hash_len;
2999 let algo = if hash_version == HASH_VERSION_SHA256 {
3000 HashAlgo::Sha256
3001 } else {
3002 HashAlgo::Sha1
3003 };
3004 let budget = crate::pack_map::ResidentBudget::new(data, 2 * 1024 * 1024);
3005 let valid = budget.checksum(algo, &data[..payload_end]) == data[payload_end..];
3006 data.release_resident();
3008 if !valid {
3009 diagnostics.warn(Warning::MidxChecksumMismatch);
3010 return Ok(MidxLoadResult::Skip);
3011 }
3012
3013 Ok(MidxLoadResult::Ok(MidxReadView {
3014 oidf_off,
3015 oidl_off,
3016 ooff_off,
3017 loff,
3018 num_objects,
3019 pack_names,
3020 }))
3021}
3022
3023pub fn validate_midx_referenced_packs(objects_dir: &Path) {
3031 validate_midx_referenced_packs_with_diagnostics(objects_dir, &NullDiagnostics);
3032}
3033
3034pub fn validate_midx_referenced_packs_with_diagnostics(
3036 objects_dir: &Path,
3037 diagnostics: &dyn DiagnosticSink,
3038) {
3039 if let Ok(Some(chain)) = midx_cache::prepared_chain(objects_dir, diagnostics) {
3040 chain.ensure_packs_validated(diagnostics);
3041 }
3042}
3043
3044pub fn try_read_object_via_midx(
3049 objects_dir: &Path,
3050 oid: &ObjectId,
3051) -> Result<Option<crate::objects::Object>> {
3052 try_read_object_via_midx_with_diagnostics(objects_dir, oid, &NullDiagnostics)
3053}
3054
3055pub fn try_read_object_via_midx_with_diagnostics(
3057 objects_dir: &Path,
3058 oid: &ObjectId,
3059 diagnostics: &dyn DiagnosticSink,
3060) -> Result<Option<crate::objects::Object>> {
3061 let Some(chain) = midx_cache::prepared_chain(objects_dir, diagnostics)? else {
3062 return Ok(None);
3063 };
3064 chain.try_read_object(oid, diagnostics)
3065}
3066
3067pub fn try_read_info_via_midx(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
3069 try_read_info_via_midx_with_diagnostics(objects_dir, oid, &NullDiagnostics)
3070}
3071
3072pub fn try_read_info_via_midx_with_diagnostics(
3074 objects_dir: &Path,
3075 oid: &ObjectId,
3076 diagnostics: &dyn DiagnosticSink,
3077) -> Result<Option<ObjectInfo>> {
3078 let Some(chain) = midx_cache::prepared_chain(objects_dir, diagnostics)? else {
3079 return Ok(None);
3080 };
3081 chain.try_read_info(oid, diagnostics)
3082}
3083
3084pub fn read_midx_preferred_idx_name(objects_dir: &Path) -> Result<String> {
3086 let pack_dir = objects_dir.join("pack");
3087 let path = resolve_tip_midx_path(&pack_dir)
3088 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
3089 let data = fs::read(&path).map_err(Error::Io)?;
3090 let (_, hdr_end, _) = parse_midx_header(&data)?;
3091 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
3092 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
3093 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
3094 let (ridx_off, ridx_len) = match find_chunk(&data, hdr_end, MIDX_CHUNKID_REVINDEX) {
3100 Ok(r) => r,
3101 Err(_) => {
3102 return Err(Error::CorruptObject(
3103 "could not determine MIDX preferred pack".to_owned(),
3104 ));
3105 }
3106 };
3107
3108 if ridx_len < 4 || ooff_len < 8 {
3109 return Err(Error::CorruptObject("truncated MIDX RIDX/OOFF".to_owned()));
3110 }
3111 let first_oid_idx = read_be_u32(&data, ridx_off)? as usize;
3112 let entry_base = ooff_off + first_oid_idx * 8;
3113 if entry_base + 8 > data.len() || entry_base + 8 > ooff_off + ooff_len {
3114 return Err(Error::CorruptObject(
3115 "bad MIDX object-offsets index".to_owned(),
3116 ));
3117 }
3118 let pack_id = read_be_u32(&data, entry_base)?;
3119 let idx = usize::try_from(pack_id)
3120 .map_err(|_| Error::CorruptObject("pack id overflow in multi-pack-index".to_owned()))?;
3121 names
3122 .get(idx)
3123 .cloned()
3124 .ok_or_else(|| Error::CorruptObject("preferred pack id out of range".to_owned()))
3125}
3126
3127pub fn clear_pack_midx_state(pack_dir: &Path) -> Result<()> {
3134 let _ = fs::remove_file(pack_dir.join("multi-pack-index"));
3135 scrub_root_midx_sidecars_except(pack_dir, None)?;
3136 let midx_d = midx_d_dir(pack_dir);
3137 if midx_d.exists() {
3138 let _ = fs::remove_dir_all(&midx_d);
3139 }
3140 midx_cache::evict_pack_dir(pack_dir);
3141 Ok(())
3142}
3143
3144pub fn write_multi_pack_index(pack_dir: &Path) -> Result<WriteMultiPackIndexResult> {
3146 write_multi_pack_index_with_options(pack_dir, &WriteMultiPackIndexOptions::default())
3147}
3148
3149fn repository_for_pack_dir(pack_dir: &Path) -> Result<Repository> {
3150 let objects = pack_dir.parent().ok_or_else(|| {
3151 Error::Io(std::io::Error::new(
3152 std::io::ErrorKind::NotFound,
3153 "pack directory has no parent",
3154 ))
3155 })?;
3156 let git_dir = objects.parent().ok_or_else(|| {
3157 Error::Io(std::io::Error::new(
3158 std::io::ErrorKind::NotFound,
3159 "objects directory has no parent",
3160 ))
3161 })?;
3162 Repository::open(git_dir, &RepositoryOptions::empty())
3163}
3164
3165fn midx_bitmap_sidecar_matches(path: &Path, midx_checksum: &[u8]) -> bool {
3166 let Ok(data) = fs::read(path) else {
3167 return false;
3168 };
3169 let header_end = 4 + 2 + 2 + 4;
3170 if data.len() < header_end + midx_checksum.len() {
3171 return false;
3172 }
3173 if &data[0..4] != b"BITM" {
3174 return false;
3175 }
3176 data[header_end..header_end + midx_checksum.len()] == *midx_checksum
3177}
3178
3179fn write_midx_reachability_bitmap(
3180 pack_dir: &Path,
3181 midx_bytes: &[u8],
3182 rid_order: &[u32],
3183 midx_checksum: &[u8],
3184 bitmap_path: &Path,
3185) -> Result<()> {
3186 write_midx_reachability_bitmap_with_options(
3187 pack_dir,
3188 midx_bytes,
3189 rid_order,
3190 midx_checksum,
3191 bitmap_path,
3192 &PackBitmapWriteOptions::default(),
3193 )
3194}
3195
3196fn write_midx_reachability_bitmap_with_options(
3197 pack_dir: &Path,
3198 midx_bytes: &[u8],
3199 rid_order: &[u32],
3200 midx_checksum: &[u8],
3201 bitmap_path: &Path,
3202 options: &PackBitmapWriteOptions,
3203) -> Result<()> {
3204 let repo = repository_for_pack_dir(pack_dir)?;
3205 MidxBitmapWriter::write_for_midx_layer(
3206 &repo,
3207 midx_bytes,
3208 rid_order,
3209 midx_checksum,
3210 bitmap_path,
3211 options,
3212 )
3213 .map_err(|e| MidxError::BitmapWriteFailed {
3214 detail: e.to_string(),
3215 })?;
3216 Ok(())
3217}
3218
3219pub fn write_multi_pack_index_with_options(
3221 pack_dir: &Path,
3222 opts: &WriteMultiPackIndexOptions,
3223) -> Result<WriteMultiPackIndexResult> {
3224 let version = midx_write_version(opts.version)?;
3225
3226 if opts.pack_names_subset_ordered.is_none() {
3229 if let Some(existing) = resolve_tip_midx_path(pack_dir) {
3230 if let Ok(bytes) = fs::read(&existing) {
3231 if midx_checksum_is_valid(&bytes) {
3232 if let Ok((_, existing_names)) = oids_and_packs_from_midx_data(&bytes) {
3236 for (i, name) in existing_names.iter().enumerate() {
3237 let stem = name.strip_suffix(".idx").unwrap_or(name);
3238 if !pack_dir.join(format!("{stem}.pack")).exists() {
3239 return Err(
3240 MidxError::ReferencedPackMissing { pack_index: i }.into()
3241 );
3242 }
3243 }
3244 }
3245 } else {
3246 midx_write_warn(opts, Warning::MidxIgnoringExistingChecksumMismatch);
3247 }
3248 }
3249 }
3250 }
3251
3252 let mut idx_names: Vec<String> = fs::read_dir(pack_dir)
3257 .map(|rd| {
3258 rd.filter_map(|e| e.ok())
3259 .filter_map(|e| {
3260 let name = e.file_name().to_string_lossy().to_string();
3261 let stem = name.strip_suffix(".idx")?;
3262 if pack_dir.join(format!("{stem}.pack")).exists() {
3263 Some(name)
3264 } else {
3265 None
3266 }
3267 })
3268 .collect()
3269 })
3270 .unwrap_or_default();
3271 idx_names.sort();
3272
3273 let idx_names: Vec<String> = if let Some(sub) = &opts.pack_names_subset_ordered {
3274 let mut out = Vec::new();
3275 for line in sub {
3276 let want = normalize_pack_idx_basename(line)?;
3277 if let Some(found) = idx_names.iter().find(|n| **n == want).cloned() {
3278 if !out.contains(&found) {
3279 out.push(found);
3280 }
3281 }
3282 }
3285 if version == MIDX_VERSION_V1 {
3287 out.sort();
3288 }
3289 out
3290 } else {
3291 idx_names
3292 };
3293
3294 let mut preferred_warned = false;
3297 if let Some(raw) = opts.preferred_pack_name.as_deref() {
3298 if opts.preferred_pack_idx.is_none()
3299 && !idx_names
3300 .iter()
3301 .any(|n| cmp_idx_or_pack_name(raw, n).is_eq())
3302 {
3303 midx_write_warn(
3304 opts,
3305 Warning::MidxUnknownPreferredPack {
3306 name: raw.to_owned(),
3307 },
3308 );
3309 preferred_warned = true;
3310 }
3311 }
3312
3313 if idx_names.is_empty() {
3314 return Err(MidxError::NoPacksToIndex.into());
3315 }
3316
3317 let (base_oids, base_pack_names) = if opts.incremental {
3318 collect_incremental_base(pack_dir)?
3319 } else {
3320 (HashSet::new(), HashSet::new())
3321 };
3322
3323 let layer_idx_names: Vec<String> = if opts.incremental {
3324 idx_names
3325 .iter()
3326 .filter(|n| {
3327 !base_pack_names
3328 .iter()
3329 .any(|bp| pack_names_match_layer(bp, n))
3330 })
3331 .cloned()
3332 .collect()
3333 } else {
3334 idx_names.clone()
3335 };
3336
3337 if opts.incremental && layer_idx_names.is_empty() {
3338 return Ok(WriteMultiPackIndexResult::default());
3339 }
3340
3341 let work_names = if opts.incremental {
3342 &layer_idx_names[..]
3343 } else {
3344 &idx_names[..]
3345 };
3346
3347 let mut preferred_idx = opts.preferred_pack_idx.map(|p| p as usize);
3348 if preferred_idx.is_none() && !preferred_warned {
3349 if let Some(raw) = opts.preferred_pack_name.as_deref() {
3350 preferred_idx = work_names
3352 .iter()
3353 .position(|n| cmp_idx_or_pack_name(raw, n).is_eq());
3354 }
3355 }
3356 if preferred_idx.is_none() && opts.write_bitmap && !work_names.is_empty() {
3357 preferred_idx = preferred_pack_index_by_mtime(pack_dir, work_names)?;
3358 }
3359 if let Some(p) = preferred_idx {
3360 if p >= work_names.len() {
3361 return Err(Error::CorruptObject(
3362 "preferred pack index out of range".to_owned(),
3363 ));
3364 }
3365 }
3366
3367 let mut indexes: Vec<PackIndex> = Vec::with_capacity(work_names.len());
3368 for name in work_names {
3369 let path = pack_dir.join(name);
3370 indexes.push(crate::pack::read_pack_index_no_verify(&path)?);
3373 }
3374
3375 if let Some(p) = preferred_idx {
3377 if indexes.get(p).map(PackIndex::len).unwrap_or(0) == 0 {
3378 let name = work_names.get(p).cloned().unwrap_or_default();
3379 let pack_name = name.strip_suffix(".idx").unwrap_or(&name);
3380 return Err(MidxError::PreferredPackEmpty {
3381 pack: format!("{pack_name}.pack"),
3382 }
3383 .into());
3384 }
3385 }
3386
3387 let pack_mtimes_layer: Vec<std::time::SystemTime> =
3388 indexes.iter().map(pack_mtime_for_midx).collect();
3389 let preferred_u32 = preferred_idx.map(|p| p as u32);
3390 let select_hash_len = if repo_midx_hash_version(pack_dir) == 2 {
3391 32
3392 } else {
3393 20
3394 };
3395
3396 let mut best: HashMap<ObjectId, MidxEntry> = HashMap::new();
3397 for (pack_id, idx) in indexes.iter().enumerate() {
3398 let pack_id = u32::try_from(pack_id).map_err(|_| {
3399 Error::CorruptObject("too many pack files for multi-pack-index".to_owned())
3400 })?;
3401 let mtime = pack_mtimes_layer[pack_id as usize];
3402 for e in idx.iter() {
3403 if e.oid().len() != select_hash_len {
3404 continue;
3405 }
3406 let Ok(oid) = ObjectId::from_bytes(e.oid()) else {
3407 continue;
3408 };
3409 if opts.incremental && base_oids.contains(&oid) {
3410 continue;
3411 }
3412 let offset = e.offset()?;
3413 let cand = MidxEntry {
3414 oid,
3415 pack_id,
3416 offset,
3417 pack_mtime: mtime,
3418 };
3419 match best.get(&oid) {
3420 None => {
3421 best.insert(oid, cand);
3422 }
3423 Some(cur) => {
3424 if midx_pick_better_entry(cur, pack_id, offset, mtime, preferred_u32) {
3425 best.insert(oid, cand);
3426 }
3427 }
3428 }
3429 }
3430 }
3431
3432 let write_bitmap_chunks = opts.write_bitmap && (!opts.incremental || !best.is_empty());
3433
3434 let omit_embedded_ridx = opts.write_rev_sidecar;
3435 let exclude = if opts.incremental && !base_oids.is_empty() {
3439 Some(&base_oids)
3440 } else {
3441 None
3442 };
3443 let (out, bitmap_rid_order) = build_midx_bytes_filtered(
3444 work_names,
3445 &indexes,
3446 preferred_idx,
3447 write_bitmap_chunks,
3448 omit_embedded_ridx,
3449 version,
3450 repo_midx_hash_version(pack_dir),
3451 exclude,
3452 )?;
3453
3454 let repo_algo = repo_hash_algo_for_pack_dir(pack_dir);
3455 let hash_len = repo_algo.len();
3456 let hash = &out[out.len() - hash_len..];
3457 let hash_hex = hex::encode(hash);
3458 let hash_arr: Vec<u8> = hash.to_vec();
3459
3460 let bitmap_outcome = if opts.write_bitmap {
3461 if opts.incremental {
3462 MidxBitmapWriteOutcome::SkippedIncrementalLayer
3463 } else {
3464 MidxBitmapWriteOutcome::Written
3465 }
3466 } else {
3467 MidxBitmapWriteOutcome::NotRequested
3468 };
3469
3470 if opts.incremental {
3471 let root_midx = pack_dir.join("multi-pack-index");
3472 let chain_path = chain_file_path(pack_dir);
3473 let chain_existed = chain_path.exists();
3474
3475 let mut chain = if root_midx.exists() && !chain_existed {
3476 let root_hex = midx_checksum_hex_from_path(&root_midx)?;
3477 link_root_midx_into_chain(pack_dir, &root_hex)?;
3478 vec![root_hex]
3479 } else {
3480 read_chain_layer_hashes(pack_dir).unwrap_or_default()
3481 };
3482
3483 chain.push(hash_hex.clone());
3484
3485 let midx_d = midx_d_dir(pack_dir);
3486 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
3487
3488 let layer_path = midx_d.join(format!("multi-pack-index-{hash_hex}.midx"));
3489 fs::write(&layer_path, &out).map_err(Error::Io)?;
3490
3491 let mut chain_data = String::new();
3492 for h in &chain {
3493 chain_data.push_str(h);
3494 chain_data.push('\n');
3495 }
3496 fs::write(chain_file_path(pack_dir), chain_data.as_bytes()).map_err(Error::Io)?;
3497
3498 clear_stale_split_layers(pack_dir, &chain)?;
3499
3500 let _ = fs::remove_file(pack_dir.join("multi-pack-index"));
3501 scrub_root_midx_sidecars(pack_dir)?;
3502 if write_bitmap_chunks && opts.write_rev_sidecar {
3503 let full = hex::encode(hash);
3504 let rev_path = midx_d.join(format!("multi-pack-index-{full}.rev"));
3505 if let Some(order) = bitmap_rid_order.as_ref() {
3506 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3507 }
3508 }
3509 } else {
3510 let dest = pack_dir.join("multi-pack-index");
3516
3517 let bitmap_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.bitmap"));
3521 let bitmap_ok = !opts.write_bitmap || midx_bitmap_sidecar_matches(&bitmap_path, hash);
3522 if bitmap_ok && !chain_file_path(pack_dir).exists() {
3526 if let Ok(existing) = fs::read(&dest) {
3527 if existing == out {
3528 if opts.write_rev_sidecar {
3529 if let Some(order) = bitmap_rid_order.as_ref() {
3530 let rev_path =
3531 pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
3532 if !rev_path.exists() {
3533 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3534 }
3535 }
3536 }
3537 return Ok(WriteMultiPackIndexResult {
3538 bitmap: bitmap_outcome,
3539 });
3540 }
3541 }
3542 }
3543
3544 clear_incremental_midx_files(pack_dir)?;
3545
3546 fs::write(&dest, &out).map_err(Error::Io)?;
3547
3548 scrub_root_midx_sidecars_except(pack_dir, Some(&hash_hex))?;
3549
3550 if opts.write_bitmap {
3551 let rid = bitmap_rid_order.as_ref().ok_or_else(|| {
3552 Error::CorruptObject("MIDX bitmap write missing reverse index order".to_owned())
3553 })?;
3554 write_midx_reachability_bitmap(pack_dir, &out, rid, hash, &bitmap_path)?;
3555 }
3556 if opts.write_rev_sidecar {
3557 let rev_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
3558 let order = bitmap_rid_order.as_ref().ok_or_else(|| {
3559 Error::CorruptObject(
3560 "MIDX rev sidecar write missing reverse index order".to_owned(),
3561 )
3562 })?;
3563 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3564 }
3565 }
3566
3567 if opts.write_rev_sidecar {
3568 let hash_hex = hex::encode(hash);
3569 let rev_root = pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
3570 let rev_layer = midx_d_dir(pack_dir).join(format!("multi-pack-index-{hash_hex}.rev"));
3571 if !rev_root.is_file() && !rev_layer.is_file() {
3572 return Err(Error::CorruptObject(format!(
3573 "MIDX rev sidecar missing after write ({hash_hex})"
3574 )));
3575 }
3576 }
3577
3578 midx_cache::evict_pack_dir(pack_dir);
3579 Ok(WriteMultiPackIndexResult {
3580 bitmap: bitmap_outcome,
3581 })
3582}
3583
3584fn pack_names_match_layer(base_name: &str, disk_idx: &str) -> bool {
3585 if base_name == disk_idx {
3586 return true;
3587 }
3588 cmp_idx_or_pack_name(disk_idx, base_name).is_eq()
3589}
3590
3591#[derive(Debug)]
3594pub enum CompactError {
3595 NoChain,
3597 MissingEndpoint(String),
3600 IdenticalEndpoints,
3602 NotAncestor(String, String),
3605 V1Format,
3607 Other(String),
3609}
3610
3611impl std::fmt::Display for CompactError {
3612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3613 match self {
3614 CompactError::NoChain => write!(f, "no multi-pack-index chain to compact"),
3615 CompactError::MissingEndpoint(s) => write!(f, "could not find MIDX: {s}"),
3616 CompactError::IdenticalEndpoints => {
3617 write!(f, "MIDX compaction endpoints must be unique")
3618 }
3619 CompactError::NotAncestor(from, to) => {
3620 write!(f, "MIDX {from} must be an ancestor of {to}")
3621 }
3622 CompactError::V1Format => write!(f, "cannot perform MIDX compaction with v1 format"),
3623 CompactError::Other(s) => write!(f, "{s}"),
3624 }
3625 }
3626}
3627
3628impl From<Error> for CompactError {
3629 fn from(e: Error) -> Self {
3630 CompactError::Other(e.to_string())
3631 }
3632}
3633
3634fn collect_layer_oids(pack_dir: &Path, hashes: &[String]) -> Result<HashSet<ObjectId>> {
3637 let mut oids = HashSet::new();
3638 for h in hashes {
3639 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{h}.midx"));
3640 let data = load_midx_file(&p)?;
3641 let (layer_oids, _) = oids_and_packs_from_midx_data(&data)?;
3642 oids.extend(layer_oids);
3643 }
3644 Ok(oids)
3645}
3646
3647fn layer_pack_names(pack_dir: &Path, hash: &str) -> Result<Vec<String>> {
3649 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{hash}.midx"));
3650 let data = load_midx_file(&p)?;
3651 let (_, hdr_end, _) = parse_midx_header(&data)?;
3652 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
3653 parse_pack_names_blob(&data[pn_off..pn_off + pn_len])
3654}
3655
3656pub fn compact_multi_pack_index(
3670 pack_dir: &Path,
3671 from_arg: &str,
3672 to_arg: &str,
3673 write_bitmaps: bool,
3674 write_rev: bool,
3675 version: Option<u8>,
3676) -> std::result::Result<(), CompactError> {
3677 let version = resolve_write_version(version, MIDX_VERSION_V2)?;
3678 if version == MIDX_VERSION_V1 {
3679 return Err(CompactError::V1Format);
3680 }
3681
3682 let chain = read_chain_layer_hashes(pack_dir).map_err(|_| CompactError::NoChain)?;
3683 if chain.is_empty() {
3684 return Err(CompactError::NoChain);
3685 }
3686
3687 let from_hex = from_arg.to_ascii_lowercase();
3688 let to_hex = to_arg.to_ascii_lowercase();
3689
3690 let from_pos = chain.iter().position(|h| *h == from_hex);
3691 let to_pos = chain.iter().position(|h| *h == to_hex);
3692
3693 let Some(from_pos) = from_pos else {
3695 return Err(CompactError::MissingEndpoint(from_arg.to_string()));
3696 };
3697 let Some(to_pos) = to_pos else {
3698 return Err(CompactError::MissingEndpoint(to_arg.to_string()));
3699 };
3700
3701 if from_pos == to_pos {
3702 return Err(CompactError::IdenticalEndpoints);
3703 }
3704 if from_pos > to_pos {
3708 return Err(CompactError::NotAncestor(
3709 from_arg.to_string(),
3710 to_arg.to_string(),
3711 ));
3712 }
3713
3714 let base_hashes = &chain[..from_pos];
3717 let merged_hashes = &chain[from_pos..=to_pos];
3718 let upper_hashes = &chain[to_pos + 1..];
3719
3720 let base_oids = collect_layer_oids(pack_dir, base_hashes)?;
3721
3722 let mut ordered_idx_names: Vec<String> = Vec::new();
3725 for h in merged_hashes {
3726 for name in layer_pack_names(pack_dir, h)? {
3727 if !ordered_idx_names.contains(&name) {
3728 ordered_idx_names.push(name);
3729 }
3730 }
3731 }
3732
3733 if ordered_idx_names.is_empty() {
3734 return Err(CompactError::Other(
3735 "no packs found in compaction range".to_owned(),
3736 ));
3737 }
3738
3739 let mut indexes: Vec<PackIndex> = Vec::with_capacity(ordered_idx_names.len());
3741 for name in &ordered_idx_names {
3742 let path = pack_dir.join(name);
3743 indexes.push(crate::pack::read_pack_index_no_verify(&path)?);
3744 }
3745
3746 let preferred_idx = if write_bitmaps { Some(0usize) } else { None };
3749
3750 let exclude = if base_oids.is_empty() {
3751 None
3752 } else {
3753 Some(&base_oids)
3754 };
3755
3756 let (out, bitmap_rid_order) = build_midx_bytes_filtered(
3757 &ordered_idx_names,
3758 &indexes,
3759 preferred_idx,
3760 write_bitmaps,
3761 write_rev,
3762 version,
3763 repo_midx_hash_version(pack_dir),
3764 exclude,
3765 )?;
3766
3767 let repo_algo = repo_hash_algo_for_pack_dir(pack_dir);
3768 let hash_len = repo_algo.len();
3769 let hash = &out[out.len() - hash_len..];
3770 let hash_hex = hex::encode(hash);
3771 let hash_arr: Vec<u8> = hash.to_vec();
3772
3773 let midx_d = midx_d_dir(pack_dir);
3774 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
3775
3776 let layer_path = midx_d.join(format!("multi-pack-index-{hash_hex}.midx"));
3777 fs::write(&layer_path, &out).map_err(Error::Io)?;
3778
3779 let mut new_chain: Vec<String> = Vec::new();
3781 new_chain.extend(base_hashes.iter().cloned());
3782 new_chain.push(hash_hex.clone());
3783 new_chain.extend(upper_hashes.iter().cloned());
3784
3785 let mut chain_data = String::new();
3786 for h in &new_chain {
3787 chain_data.push_str(h);
3788 chain_data.push('\n');
3789 }
3790 fs::write(chain_file_path(pack_dir), chain_data.as_bytes()).map_err(Error::Io)?;
3791
3792 if write_bitmaps {
3793 if write_rev {
3794 let rev_path = midx_d.join(format!("multi-pack-index-{hash_hex}.rev"));
3795 if let Some(order) = bitmap_rid_order.as_ref() {
3796 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3797 }
3798 }
3799 let bitmap_path = midx_d.join(format!("multi-pack-index-{hash_hex}.bitmap"));
3800 let rid = bitmap_rid_order.as_ref().ok_or_else(|| {
3801 CompactError::Other("MIDX bitmap write missing reverse index order".to_owned())
3802 })?;
3803 write_midx_reachability_bitmap_with_options(
3804 pack_dir,
3805 &out,
3806 rid,
3807 hash,
3808 &bitmap_path,
3809 &PackBitmapWriteOptions {
3810 verify_reachability_closure: false,
3811 ..PackBitmapWriteOptions::default()
3812 },
3813 )
3814 .map_err(|e| CompactError::Other(e.to_string()))?;
3815 }
3816
3817 clear_stale_split_layers(pack_dir, &new_chain)?;
3819
3820 midx_cache::evict_pack_dir(pack_dir);
3821 Ok(())
3822}
3823
3824fn scrub_root_midx_sidecars(pack_dir: &Path) -> Result<()> {
3825 scrub_root_midx_sidecars_except(pack_dir, None)
3826}
3827
3828fn scrub_root_midx_sidecars_except(pack_dir: &Path, keep_hex: Option<&str>) -> Result<()> {
3829 let Ok(rd) = fs::read_dir(pack_dir) else {
3830 return Ok(());
3831 };
3832 for ent in rd {
3833 let ent = ent.map_err(Error::Io)?;
3834 let name = ent.file_name().to_string_lossy().to_string();
3835 let Some(rest) = name.strip_prefix("multi-pack-index-") else {
3836 continue;
3837 };
3838 if !(rest.ends_with(".bitmap") || rest.ends_with(".rev")) {
3839 continue;
3840 }
3841 let hash_part = rest
3842 .strip_suffix(".bitmap")
3843 .or_else(|| rest.strip_suffix(".rev"))
3844 .unwrap_or(rest);
3845 if keep_hex.is_some_and(|k| k == hash_part) {
3849 continue;
3850 }
3851 let _ = fs::remove_file(ent.path());
3852 }
3853 Ok(())
3854}
3855
3856#[cfg(test)]
3857mod tests {
3858 use super::{
3859 clear_pack_midx_state, resolve_tip_midx_path, try_read_object_via_midx,
3860 write_multi_pack_index,
3861 };
3862
3863 use crate::error::{Error, MidxError};
3864 use crate::hot_path_test_metrics::HotPathMetricsScope;
3865 use crate::objects::ObjectId;
3866 use crate::odb::Odb;
3867 use crate::pack::{clear_pack_cache, read_object_from_packs};
3868 use std::fs;
3869 use std::path::Path;
3870 use std::process::Command;
3871 use std::sync::Arc;
3872
3873 fn git(dir: &Path, args: &[&str]) {
3874 let out = Command::new("git")
3875 .current_dir(dir)
3876 .args(args)
3877 .env("GIT_AUTHOR_NAME", "T")
3878 .env("GIT_AUTHOR_EMAIL", "t@example.com")
3879 .env("GIT_COMMITTER_NAME", "T")
3880 .env("GIT_COMMITTER_EMAIL", "t@example.com")
3881 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3882 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3883 .output()
3884 .expect("run git");
3885 assert!(
3886 out.status.success(),
3887 "git {:?} failed: {}",
3888 args,
3889 String::from_utf8_lossy(&out.stderr)
3890 );
3891 }
3892
3893 fn git_available() -> bool {
3894 Command::new("git")
3895 .arg("--version")
3896 .output()
3897 .map(|o| o.status.success())
3898 .unwrap_or(false)
3899 }
3900
3901 pub(super) fn midx_fixture(with_midx: bool) -> Option<(tempfile::TempDir, ObjectId)> {
3903 if !git_available() {
3904 return None;
3905 }
3906 let tmp = tempfile::tempdir().ok()?;
3907 let dir = tmp.path();
3908 git(dir, &["init", "-q", "-b", "main"]);
3909 std::fs::write(dir.join("f.txt"), b"hello midx cache").ok()?;
3910 git(dir, &["add", "f.txt"]);
3911 git(dir, &["commit", "-q", "-m", "c"]);
3912 git(dir, &["repack", "-adf"]);
3913 let pack_dir = dir.join(".git/objects/pack");
3914 if with_midx {
3915 write_multi_pack_index(&pack_dir).ok()?;
3916 }
3917 let hex = Command::new("git")
3918 .current_dir(dir)
3919 .args(["rev-parse", "HEAD"])
3920 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3921 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3922 .output()
3923 .ok()?;
3924 if !hex.status.success() {
3925 return None;
3926 }
3927 let oid = ObjectId::from_hex(std::str::from_utf8(&hex.stdout).ok()?.trim()).ok()?;
3928 Some((tmp, oid))
3929 }
3930
3931 #[cfg_attr(
3932 not(feature = "full-tests"),
3933 ignore = "starts system git or is heavy; run with --features full-tests"
3934 )]
3935 #[test]
3936 fn vanished_cached_tip_falls_back() {
3937 let Some((tmp, oid)) = midx_fixture(true) else {
3938 eprintln!("SKIP: git unavailable for MIDX fixture");
3939 return;
3940 };
3941 let git_dir = tmp.path().join(".git");
3942 let objects = git_dir.join("objects");
3943 let pack_dir = objects.join("pack");
3944 let midx_path = pack_dir.join("multi-pack-index");
3945 assert!(midx_path.is_file(), "expected root MIDX");
3946
3947 assert!(
3948 try_read_object_via_midx(&objects, &oid)
3949 .expect("prime midx read")
3950 .is_some(),
3951 "object should be reachable via MIDX"
3952 );
3953
3954 std::fs::remove_file(&midx_path).expect("simulate external MIDX removal");
3955
3956 assert!(
3959 try_read_object_via_midx(&objects, &oid)
3960 .expect("vanished tip must not fail reads")
3961 .is_some(),
3962 "cached prepared layer still serves until pack-dir evict"
3963 );
3964
3965 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3966 let via_pack = odb.read(&oid).expect("pack fallback after vanished MIDX");
3967 assert_eq!(via_pack.kind, crate::objects::ObjectKind::Commit);
3968 }
3969
3970 #[cfg_attr(
3971 not(feature = "full-tests"),
3972 ignore = "starts system git or is heavy; run with --features full-tests"
3973 )]
3974 #[test]
3975 fn writer_evicts_tip_cache() {
3976 let Some((tmp, oid)) = midx_fixture(true) else {
3977 eprintln!("SKIP: git unavailable for MIDX fixture");
3978 return;
3979 };
3980 let objects = tmp.path().join(".git/objects");
3981 let pack_dir = objects.join("pack");
3982
3983 assert!(try_read_object_via_midx(&objects, &oid)
3984 .expect("prime caches")
3985 .is_some());
3986 assert!(resolve_tip_midx_path(&pack_dir).is_some());
3987
3988 clear_pack_midx_state(&pack_dir).expect("clear midx");
3989 assert!(resolve_tip_midx_path(&pack_dir).is_none());
3990
3991 assert!(try_read_object_via_midx(&objects, &oid)
3993 .expect("read after clear")
3994 .is_none());
3995
3996 write_multi_pack_index(&pack_dir).expect("rewrite midx");
3997 assert!(
3998 try_read_object_via_midx(&objects, &oid)
3999 .expect("read after rewrite")
4000 .is_some(),
4001 "fresh MIDX must be visible after in-process rewrite"
4002 );
4003 }
4004
4005 #[cfg_attr(
4006 not(feature = "full-tests"),
4007 ignore = "starts system git or is heavy; run with --features full-tests"
4008 )]
4009 #[test]
4010 fn midx_hot_path_does_not_stat_after_cache_warm() {
4011 let Some((tmp, oid)) = midx_fixture(true) else {
4012 eprintln!("SKIP: git unavailable for MIDX fixture");
4013 return;
4014 };
4015 let git_dir = tmp.path().join(".git");
4016 let objects = git_dir.join("objects");
4017 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4018 let metrics = odb.hot_path_metrics_arc();
4019 let _scope = HotPathMetricsScope::install(Arc::clone(&metrics));
4020 metrics.set_stamp_counting(true);
4021 let _ = odb.read(&oid).expect("prime");
4022 assert!(
4023 metrics.midx_stamp_stat_calls() > 0,
4024 "cold read must stat the MIDX layer"
4025 );
4026 metrics.reset_midx_stamp_stats();
4027 for _ in 0..200 {
4028 let _ = odb.read(&oid).expect("repeat");
4029 }
4030 metrics.set_stamp_counting(false);
4031 assert_eq!(metrics.midx_stamp_stat_calls(), 0);
4032 }
4033
4034 #[test]
4035 fn decode_high_bit_offset_is_raw_without_loff_chunk() {
4036 use super::midx_cache::decode_midx_object_offset_for_test;
4037 assert_eq!(
4038 decode_midx_object_offset_for_test(&[], 0x8000_0010, None),
4039 Some(0x8000_0010)
4040 );
4041 }
4042
4043 #[cfg_attr(
4044 not(feature = "full-tests"),
4045 ignore = "starts system git or is heavy; run with --features full-tests"
4046 )]
4047 #[test]
4048 fn lookup_high_bit_raw_offset_written_without_loff() {
4049 use super::{
4050 clear_pack_midx_state, find_chunk, midx_lookup_pack_and_offset_opt, parse_midx_header,
4051 MIDX_CHUNKID_OBJECTOFFSETS, MIDX_CHUNKID_OIDLOOKUP,
4052 };
4053 let Some((tmp, oid)) = midx_fixture(true) else {
4054 eprintln!("SKIP: git unavailable for MIDX fixture");
4055 return;
4056 };
4057 let objects = tmp.path().join(".git/objects");
4058 let pack_dir = objects.join("pack");
4059 let midx_path = pack_dir.join("multi-pack-index");
4060 let mut data = std::fs::read(&midx_path).expect("midx bytes");
4061 let (_, hdr_end, _) = parse_midx_header(&data).expect("header");
4062 let (oidl_off, oidl_len) =
4063 find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP).expect("oidl");
4064 let (ooff_off, _) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS).expect("ooff");
4065 let hash_len = oid.as_bytes().len();
4066 let num_objects = oidl_len / hash_len;
4067 let needle = oid.as_bytes();
4068 let mut patched = false;
4069 for i in 0..num_objects {
4070 let start = oidl_off + i * hash_len;
4071 if data[start..start + hash_len] == *needle {
4072 let ob = ooff_off + i * 8 + 4;
4073 data[ob..ob + 4].copy_from_slice(&0x8000_0010u32.to_be_bytes());
4074 patched = true;
4075 break;
4076 }
4077 }
4078 assert!(patched, "oid row in MIDX");
4079 let hash_len = if data[5] == 2 { 32 } else { 20 };
4080 data.truncate(data.len() - hash_len);
4081 crate::pack_rev::append_hashfile_checksum(&mut data, hash_len);
4082 clear_pack_midx_state(&pack_dir).expect("evict midx cache");
4083 std::fs::write(&midx_path, &data).expect("write midx");
4084 crate::pack::clear_pack_cache();
4085 let (_pack_id, off) = midx_lookup_pack_and_offset_opt(&objects, &oid)
4086 .expect("lookup")
4087 .expect("listed");
4088 assert_eq!(off, 0x8000_0010);
4089 }
4090
4091 #[cfg_attr(
4092 not(feature = "full-tests"),
4093 ignore = "starts system git or is heavy; run with --features full-tests"
4094 )]
4095 #[test]
4096 fn tiny_midx_stub_skipped_without_aborting_reads() {
4097 let Some((tmp, oid)) = midx_fixture(true) else {
4098 eprintln!("SKIP: git unavailable for MIDX fixture");
4099 return;
4100 };
4101 let objects = tmp.path().join(".git/objects");
4102 let pack_dir = objects.join("pack");
4103 std::fs::write(pack_dir.join("multi-pack-index"), [0u8; 8]).expect("tiny stub midx");
4104 super::midx_cache::evict_pack_dir(&pack_dir);
4105 clear_pack_cache();
4106
4107 assert!(super::prepared_midx_chain(&objects)
4108 .expect("tiny MIDX must not fail chain build")
4109 .is_none());
4110 assert!(try_read_object_via_midx(&objects, &oid)
4111 .expect("tiny MIDX must not fail lookup")
4112 .is_none());
4113 read_object_from_packs(&objects, &oid).expect("pack fallback still works");
4114 }
4115
4116 #[cfg_attr(
4117 not(feature = "full-tests"),
4118 ignore = "starts system git or is heavy; run with --features full-tests"
4119 )]
4120 #[test]
4121 fn flipped_midx_checksum_warns_and_reads_from_pack() {
4122 let Some((tmp, oid)) = midx_fixture(true) else {
4123 eprintln!("SKIP: git unavailable for MIDX fixture");
4124 return;
4125 };
4126 let objects = tmp.path().join(".git/objects");
4127 let pack_dir = objects.join("pack");
4128 let midx_path = pack_dir.join("multi-pack-index");
4129 let mut bytes = fs::read(&midx_path).expect("read midx");
4130 let last = bytes.len() - 1;
4131 bytes[last] ^= 0xff;
4132 fs::write(&midx_path, &bytes).expect("corrupt midx checksum");
4133 super::midx_cache::evict_pack_dir(&pack_dir);
4134 clear_pack_cache();
4135
4136 let errs = super::verify_midx(&objects).expect_err("verify catches bad checksum");
4137 assert!(
4138 errs.iter().any(|e| e.contains("incorrect checksum")),
4139 "expected verify checksum error, got {errs:?}"
4140 );
4141 let via_midx =
4142 try_read_object_via_midx(&objects, &oid).expect("checksum mismatch must not fatal");
4143 assert!(
4144 via_midx.is_none(),
4145 "invalid MIDX checksum must be skipped for direct lookup"
4146 );
4147
4148 let git_dir = tmp.path().join(".git");
4149 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4150 let grit_obj = odb.read(&oid).expect("pack fallback read");
4151 let via_pack = read_object_from_packs(&objects, &oid).expect("pack read");
4152 assert_eq!(via_pack.data, grit_obj.data);
4153 }
4154
4155 #[cfg_attr(
4156 not(feature = "full-tests"),
4157 ignore = "starts system git or is heavy; run with --features full-tests"
4158 )]
4159 #[test]
4160 fn midx_shorter_than_min_len_skips_without_fatal_error() {
4161 let Some((tmp, oid)) = midx_fixture(true) else {
4162 eprintln!("SKIP: git unavailable for MIDX fixture");
4163 return;
4164 };
4165 let objects = tmp.path().join(".git/objects");
4166 let pack_dir = objects.join("pack");
4167 let midx_path = pack_dir.join("multi-pack-index");
4168 let mut bytes = fs::read(&midx_path).expect("read");
4169 bytes.truncate(super::MIDX_HEADER_SIZE + 10);
4170 fs::write(&midx_path, &bytes).expect("truncate");
4171 super::midx_cache::evict_pack_dir(&pack_dir);
4172 clear_pack_cache();
4173 assert!(
4174 try_read_object_via_midx(&objects, &oid)
4175 .expect("read")
4176 .is_none(),
4177 "truncated MIDX must be skipped for direct lookup"
4178 );
4179 read_object_from_packs(&objects, &oid).expect("pack fallback still works");
4180 }
4181
4182 #[cfg_attr(
4183 not(feature = "full-tests"),
4184 ignore = "starts system git or is heavy; run with --features full-tests"
4185 )]
4186 #[test]
4187 fn large_git_midx_layer_uses_mapped_backing() {
4188 let Some((tmp, _oid)) = midx_fixture(false) else {
4189 eprintln!("SKIP: git unavailable for MIDX fixture");
4190 return;
4191 };
4192 for i in 0..120 {
4193 std::fs::write(tmp.path().join(format!("bulk{i}.txt")), format!("{i}\n")).unwrap();
4194 git(tmp.path(), &["add", &format!("bulk{i}.txt")]);
4195 git(tmp.path(), &["commit", "-q", "-m", &format!("bulk{i}")]);
4196 }
4197 git(tmp.path(), &["repack", "-adf"]);
4198 git(tmp.path(), &["multi-pack-index", "write"]);
4199 let midx_path = tmp.path().join(".git/objects/pack/multi-pack-index");
4200 let len = std::fs::metadata(&midx_path).expect("meta").len();
4201 assert!(len > 8192, "midx size {len}");
4202 let storage =
4203 crate::pack_map::PackData::open_with_min_len(&midx_path, super::MIDX_MIN_FILE_LEN)
4204 .expect("open");
4205 assert!(storage.is_mapped_backing());
4206 }
4207
4208 #[cfg_attr(
4209 not(feature = "full-tests"),
4210 ignore = "starts system git or is heavy; run with --features full-tests"
4211 )]
4212 #[test]
4213 fn wrong_midx_hash_version_returns_typed_error() {
4214 let Some((tmp, oid)) = midx_fixture(true) else {
4215 eprintln!("SKIP: git unavailable for MIDX fixture");
4216 return;
4217 };
4218 let objects = tmp.path().join(".git/objects");
4219 let midx_path = objects.join("pack/multi-pack-index");
4220 let mut bytes = fs::read(&midx_path).expect("read midx");
4221 bytes[5] = 2;
4222 fs::write(&midx_path, &bytes).expect("write wrong hash version");
4223 super::midx_cache::evict_pack_dir(&objects.join("pack"));
4224
4225 let err = try_read_object_via_midx(&objects, &oid).expect_err("hash mismatch");
4226 assert!(
4227 matches!(
4228 err,
4229 Error::Midx(MidxError::HashVersionMismatch {
4230 found: 2,
4231 expected: 1
4232 })
4233 ),
4234 "unexpected error: {err:?}"
4235 );
4236 }
4237
4238 #[test]
4239 fn midx_load_error_display_roundtrip() {
4240 use crate::midx_error::MidxError;
4241 let err = MidxError::VersionNotRecognized { version: 99 };
4242 assert!(err.to_string().contains("not recognized"));
4243 }
4244
4245 #[test]
4246 fn compact_error_from_library_error() {
4247 use crate::error::Error;
4248 let err: super::CompactError = Error::CorruptObject("test".into()).into();
4249 assert!(err.to_string().contains("test"));
4250 }
4251
4252 #[cfg_attr(
4253 not(feature = "full-tests"),
4254 ignore = "starts system git or is heavy; run with --features full-tests"
4255 )]
4256 #[test]
4257 fn verify_midx_reports_missing_pack_name_chunk() {
4258 let Some((tmp, _oid)) = midx_fixture(true) else {
4259 eprintln!("SKIP: git unavailable for MIDX fixture");
4260 return;
4261 };
4262 let objects = tmp.path().join(".git/objects");
4263 let pack_dir = objects.join("pack");
4264 let path = pack_dir.join("multi-pack-index");
4265 let mut data = std::fs::read(&path).expect("midx");
4266 data[6] = 0;
4267 data.truncate(super::MIDX_HEADER_SIZE + 24);
4268 std::fs::write(&path, &data).expect("write broken midx");
4269 super::evict_midx_read_cache_for_pack_dir(&pack_dir);
4270 let errs = super::verify_midx(&objects).expect_err("verify");
4271 assert!(
4272 errs.iter().any(|e| {
4273 e.contains("pack-name") || e.contains("pack name") || e.contains("chunk")
4274 }),
4275 "{errs:?}"
4276 );
4277 }
4278
4279 #[cfg_attr(
4280 not(feature = "full-tests"),
4281 ignore = "starts system git or is heavy; run with --features full-tests"
4282 )]
4283 #[test]
4284 fn find_chunk_reports_missing_id() {
4285 let Some((tmp, _oid)) = midx_fixture(true) else {
4286 eprintln!("SKIP: git unavailable for MIDX fixture");
4287 return;
4288 };
4289 let midx_path = tmp.path().join(".git/objects/pack/multi-pack-index");
4290 let data = std::fs::read(midx_path).expect("midx");
4291 let (_, hdr_end, _) = super::parse_midx_header(&data).expect("header");
4292 let err = super::find_chunk(&data, hdr_end, 0xdead_beef).expect_err("missing chunk");
4293 assert!(err.to_string().contains("not found"));
4294 }
4295
4296 #[cfg_attr(
4297 not(feature = "full-tests"),
4298 ignore = "starts system git or is heavy; run with --features full-tests"
4299 )]
4300 #[test]
4301 fn read_after_in_process_pack_swap() {
4302 let Some((tmp, oid)) = midx_fixture(false) else {
4303 eprintln!("SKIP: git unavailable for MIDX fixture");
4304 return;
4305 };
4306 let objects = tmp.path().join(".git/objects");
4307 let before = read_object_from_packs(&objects, &oid).expect("read from pack");
4308
4309 std::fs::write(tmp.path().join("f2.txt"), b"replacement pack").expect("write");
4310 git(tmp.path(), &["add", "f2.txt"]);
4311 git(tmp.path(), &["commit", "-q", "-m", "c2"]);
4312 git(tmp.path(), &["repack", "-adf"]);
4313 clear_pack_cache();
4314
4315 let after = read_object_from_packs(&objects, &oid).expect("historical commit still packed");
4316 assert_eq!(before.kind, after.kind);
4317 assert_eq!(before.data, after.data);
4318 }
4319
4320 #[cfg_attr(
4321 not(feature = "full-tests"),
4322 ignore = "starts system git or is heavy; run with --features full-tests"
4323 )]
4324 #[test]
4325 fn git_midx_mapped_bytes_match_owned_prepare() {
4326 use super::midx_cache::PreparedMidxLayer;
4327 use crate::diagnostics::NullDiagnostics;
4328 use crate::pack_map::PackData;
4329
4330 let Some((tmp, oid)) = midx_fixture(false) else {
4331 eprintln!("SKIP: git unavailable for MIDX fixture");
4332 return;
4333 };
4334 for i in 0..120 {
4335 std::fs::write(tmp.path().join(format!("bulk{i}.txt")), format!("{i}\n")).unwrap();
4336 git(tmp.path(), &["add", &format!("bulk{i}.txt")]);
4337 git(tmp.path(), &["commit", "-q", "-m", &format!("bulk{i}")]);
4338 }
4339 git(tmp.path(), &["repack", "-adf"]);
4340 git(tmp.path(), &["multi-pack-index", "write"]);
4341
4342 let objects = tmp.path().join(".git/objects");
4343 let pack_dir = objects.join("pack");
4344 let midx_path = pack_dir.join("multi-pack-index");
4345 let midx_len = std::fs::metadata(&midx_path).expect("meta").len();
4346 assert!(midx_len > 8192, "fixture midx size {midx_len}");
4347 let hash_version = super::repo_midx_hash_version_for_objects_dir(&objects);
4348 let mapped =
4349 PackData::open_with_min_len(&midx_path, super::MIDX_MIN_FILE_LEN).expect("mmap");
4350 assert!(
4351 mapped.is_mapped_backing(),
4352 "expected mmap backing for {midx_len}-byte MIDX"
4353 );
4354 let owned = PackData::from_owned(std::fs::read(&midx_path).expect("read"));
4355 assert!(
4356 !owned.is_mapped_backing(),
4357 "explicit owned buffer must not use mmap"
4358 );
4359 let view_mapped =
4360 match super::midx_load_for_read(mapped.as_ref(), hash_version, &NullDiagnostics)
4361 .expect("load mapped")
4362 {
4363 super::MidxLoadResult::Ok(v) => v,
4364 super::MidxLoadResult::Skip => panic!("mapped midx skipped"),
4365 };
4366 let view_owned =
4367 match super::midx_load_for_read(owned.as_ref(), hash_version, &NullDiagnostics)
4368 .expect("load owned")
4369 {
4370 super::MidxLoadResult::Ok(v) => v,
4371 super::MidxLoadResult::Skip => panic!("owned midx skipped"),
4372 };
4373 let layer_mapped = PreparedMidxLayer::prepare(mapped, view_mapped, &pack_dir);
4374 let layer_owned = PreparedMidxLayer::prepare(owned, view_owned, &pack_dir);
4375 let loc = layer_mapped
4376 .lookup_pack_and_offset(&oid)
4377 .expect("mapped lookup");
4378 assert_eq!(
4379 loc,
4380 layer_owned
4381 .lookup_pack_and_offset(&oid)
4382 .expect("owned lookup")
4383 );
4384 let mapped_obj = layer_mapped
4385 .read_at(loc.0, loc.1)
4386 .expect("read mapped")
4387 .expect("object mapped");
4388 let owned_obj = layer_owned
4389 .read_at(loc.0, loc.1)
4390 .expect("read owned")
4391 .expect("object owned");
4392 assert_eq!(mapped_obj.data, owned_obj.data);
4393 }
4394
4395 fn config_from(text: &str) -> crate::config::ConfigSet {
4396 let file = crate::config::ConfigFile::parse(
4397 Path::new("test"),
4398 text,
4399 crate::config::ConfigScope::Local,
4400 )
4401 .expect("parse config");
4402 let mut set = crate::config::ConfigSet::new();
4403 set.merge(&file);
4404 set
4405 }
4406
4407 #[test]
4408 fn write_version_defaults_and_validation() {
4409 assert_eq!(super::resolve_write_version(None, 1).expect("default"), 1);
4410 assert_eq!(super::resolve_write_version(None, 2).expect("default"), 2);
4411 assert_eq!(super::resolve_write_version(Some(1), 2).expect("v1"), 1);
4412 assert_eq!(super::resolve_write_version(Some(2), 1).expect("v2"), 2);
4413 for bad in [0u8, 3, 255] {
4414 assert!(matches!(
4415 super::resolve_write_version(Some(bad), 1),
4416 Err(Error::Midx(MidxError::UnknownWriteVersion { version })) if version == i64::from(bad)
4417 ));
4418 }
4419 }
4420
4421 #[test]
4422 fn midx_version_config_parsing() {
4423 use crate::error::ConfigError;
4424 let read = |text: &str| super::midx_version_from_config(&config_from(text));
4425 assert_eq!(read("[core]\n\tbare = false\n").expect("unset"), None);
4426 assert_eq!(read("[midx]\n\tversion = 1\n").expect("v1"), Some(1));
4427 assert_eq!(read("[midx]\n\tversion = 2\n").expect("v2"), Some(2));
4428 assert_eq!(
4429 read("[midx]\n\tversion = 2\n[midx]\n\tversion = 1\n").expect("last wins"),
4430 Some(1)
4431 );
4432 for (text, version) in [
4433 ("[midx]\n\tversion = 3\n", 3),
4434 ("[midx]\n\tversion = 0\n", 0),
4435 ("[midx]\n\tversion = -1\n", -1),
4436 ("[midx]\n\tversion = 1k\n", 1024),
4437 ] {
4438 assert!(
4439 matches!(
4440 read(text),
4441 Err(Error::Midx(MidxError::UnknownWriteVersion { version: v })) if v == version
4442 ),
4443 "{text:?}"
4444 );
4445 }
4446 assert!(matches!(
4447 read("[midx]\n\tversion = two\n"),
4448 Err(Error::Config(ConfigError::BadNumericValue { ref key, .. })) if key == "midx.version"
4449 ));
4450 }
4451
4452 #[cfg_attr(
4453 not(feature = "full-tests"),
4454 ignore = "starts system git or is heavy; run with --features full-tests"
4455 )]
4456 #[test]
4457 fn default_write_is_v1_and_unknown_version_is_rejected() {
4458 let Some((tmp, _oid)) = midx_fixture(false) else {
4459 eprintln!("SKIP: git unavailable for MIDX fixture");
4460 return;
4461 };
4462 let pack_dir = tmp.path().join(".git/objects/pack");
4463 let midx_path = pack_dir.join("multi-pack-index");
4464 write_multi_pack_index(&pack_dir).expect("default write");
4465 assert_eq!(fs::read(&midx_path).expect("read midx")[4], 1);
4466
4467 fs::remove_file(&midx_path).expect("remove midx");
4468 let err = super::write_multi_pack_index_with_options(
4469 &pack_dir,
4470 &super::WriteMultiPackIndexOptions {
4471 version: Some(3),
4472 ..Default::default()
4473 },
4474 )
4475 .expect_err("version 3");
4476 assert!(matches!(
4477 err,
4478 Error::Midx(MidxError::UnknownWriteVersion { version: 3 })
4479 ));
4480 assert!(!midx_path.exists(), "rejected write must not leave a MIDX");
4481 }
4482}