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 let metadata = match fs::metadata(&path) {
1003 Ok(meta) => meta,
1004 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1005 if require_every_layer {
1006 return Ok(None);
1007 }
1008 continue;
1009 }
1010 Err(err) => return Err(Error::Io(err)),
1011 };
1012 if metadata.len() < MIDX_MIN_FILE_LEN {
1013 if require_every_layer {
1014 return Ok(None);
1015 }
1016 continue;
1017 }
1018 let stamp = (metadata.modified().ok(), metadata.len());
1019 if let Some(cached) = state.layers.get(&path).filter(|cached| {
1020 stamp.0.is_some()
1021 && cached.stamp == stamp
1022 && cached.prepared.hash_len == if hash_version == 2 { 32 } else { 20 }
1023 }) {
1024 for warning in &cached.warnings {
1025 diagnostics.warn(warning.clone());
1026 }
1027 layers.push(Arc::clone(&cached.prepared));
1028 continue;
1029 }
1030 let bytes = match open_midx_layer_bytes(&path) {
1031 Ok(b) => b,
1032 Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
1033 if require_every_layer {
1034 return Ok(None);
1035 }
1036 continue;
1037 }
1038 Err(Error::CorruptObject(_)) => {
1039 if require_every_layer {
1040 return Ok(None);
1041 }
1042 continue;
1043 }
1044 Err(err) => return Err(err),
1045 };
1046 let collected = CollectingDiagnostics::new();
1049 let loaded = midx_load_for_read(bytes.as_ref(), hash_version, &collected);
1050 let warnings = collected.warnings();
1051 for warning in &warnings {
1052 diagnostics.warn(warning.clone());
1053 }
1054 let view = match loaded {
1055 Ok(MidxLoadResult::Ok(v)) => v,
1056 Ok(MidxLoadResult::Skip) => {
1057 if require_every_layer {
1058 return Ok(None);
1059 }
1060 continue;
1061 }
1062 Err(Error::Midx(_)) if require_every_layer => return Ok(None),
1063 Err(err) => return Err(err),
1064 };
1065 let prepared = Arc::new(PreparedMidxLayer::prepare(bytes, view, &pack_dir));
1066 state.layers.insert(
1067 path,
1068 CachedLayer {
1069 stamp,
1070 prepared: Arc::clone(&prepared),
1071 warnings,
1072 },
1073 );
1074 layers.push(prepared);
1075 }
1076 if layers.is_empty() {
1077 return Ok(None);
1078 }
1079 if require_every_layer && layers.len() != path_count {
1080 return Ok(None);
1081 }
1082 Ok(Some(Arc::new(PreparedMidxChain {
1083 layers,
1084 packs_validated: AtomicBool::new(false),
1085 })))
1086 }
1087
1088 pub fn prepared_chain(
1090 objects_dir: &Path,
1091 diagnostics: &dyn DiagnosticSink,
1092 ) -> Result<Option<Arc<PreparedMidxChain>>> {
1093 let (result, warnings) =
1094 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1095 if let Some((path, chain)) = &state.hot_chain {
1096 if path == objects_dir {
1097 return (Ok(Some(Arc::clone(chain))), Vec::new());
1098 }
1099 }
1100 let pack_dir = objects_dir.join("pack");
1101 if let Some(chain) = state.chains.get(&pack_dir) {
1102 let chain = Arc::clone(chain);
1103 state.hot_chain = Some((objects_dir.to_path_buf(), Arc::clone(&chain)));
1104 return (Ok(Some(chain)), Vec::new());
1105 }
1106 let collected = CollectingDiagnostics::new();
1107 let result = build_chain_impl(state, objects_dir, &collected, false);
1108 if let Ok(Some(chain)) = &result {
1109 state.chains.insert(pack_dir, Arc::clone(chain));
1110 state.hot_chain = Some((objects_dir.to_path_buf(), Arc::clone(chain)));
1111 }
1112 (result, collected.warnings())
1113 });
1114 for warning in warnings {
1116 diagnostics.warn(warning);
1117 }
1118 result
1119 }
1120
1121 pub fn prepared_chain_strict(
1124 objects_dir: &Path,
1125 diagnostics: &dyn DiagnosticSink,
1126 ) -> Result<Option<Arc<PreparedMidxChain>>> {
1127 let (result, warnings) =
1128 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1129 let pack_dir = objects_dir.join("pack");
1130 if let Some(chain) = state.strict_chains.get(&pack_dir) {
1131 return (Ok(Some(Arc::clone(chain))), Vec::new());
1132 }
1133 let collected = CollectingDiagnostics::new();
1134 let result = build_chain_impl(state, objects_dir, &collected, true);
1135 if let Ok(Some(chain)) = &result {
1136 state.strict_chains.insert(pack_dir, Arc::clone(chain));
1137 }
1138 (result, collected.warnings())
1139 });
1140 for warning in warnings {
1141 diagnostics.warn(warning);
1142 }
1143 result
1144 }
1145
1146 pub(super) fn cached_pack_names(path: &Path, metadata: &fs::Metadata) -> Option<Vec<String>> {
1147 let stamp = (metadata.modified().ok(), metadata.len());
1148 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1149 state
1150 .layers
1151 .get(path)
1152 .filter(|cached| stamp.0.is_some() && cached.stamp == stamp)
1153 .map(|cached| cached.prepared.pack_names.clone())
1154 })
1155 }
1156
1157 pub fn evict_pack_dir(pack_dir: &Path) {
1158 crate::pack_store::PackStore::with_current_or_implicit_midx(|state| {
1159 evict_pack_dir_in_state(state, pack_dir);
1160 });
1161 }
1162
1163 pub(crate) fn evict_pack_dir_on(state: &Mutex<State>, pack_dir: &Path) {
1164 let mut state = state.lock().unwrap_or_else(|poison| poison.into_inner());
1165 evict_pack_dir_in_state(&mut state, pack_dir);
1166 }
1167
1168 fn evict_pack_dir_in_state(state: &mut State, pack_dir: &Path) {
1169 if state.hot_chain.as_ref().is_some_and(|(objects_dir, _)| {
1170 objects_dir.join("pack") == pack_dir || pack_dir.starts_with(objects_dir)
1171 }) {
1172 state.hot_chain = None;
1173 }
1174 state.layers.retain(|path, _| !path.starts_with(pack_dir));
1175 state.chains.retain(|path, _| !path.starts_with(pack_dir));
1176 state
1177 .strict_chains
1178 .retain(|path, _| !path.starts_with(pack_dir));
1179 }
1180}
1181
1182pub(crate) use midx_cache::PreparedMidxChain;
1184
1185pub(crate) fn prepared_midx_chain(objects_dir: &Path) -> Result<Option<Arc<PreparedMidxChain>>> {
1186 midx_cache::prepared_chain(objects_dir, &NullDiagnostics)
1187}
1188
1189pub(crate) fn prepared_midx_chain_strict(
1191 objects_dir: &Path,
1192) -> Result<Option<Arc<PreparedMidxChain>>> {
1193 midx_cache::prepared_chain_strict(objects_dir, &NullDiagnostics)
1194}
1195
1196fn midx_chain_layer_paths_newest_first(pack_dir: &Path) -> Vec<PathBuf> {
1198 let root = pack_dir.join("multi-pack-index");
1199 if root.is_file() {
1200 return vec![root];
1201 }
1202 let Ok(hashes) = read_chain_layer_hashes(pack_dir) else {
1203 return Vec::new();
1204 };
1205 if hashes.is_empty() {
1206 return Vec::new();
1207 }
1208 let midx_d = midx_d_dir(pack_dir);
1209 hashes
1210 .iter()
1211 .rev()
1212 .map(|h| midx_d.join(format!("multi-pack-index-{h}.midx")))
1213 .collect()
1214}
1215
1216fn repo_midx_hash_version_for_objects_dir(objects_dir: &Path) -> u8 {
1218 let Some(gitdir) = objects_dir.parent() else {
1219 return HASH_VERSION_SHA1;
1220 };
1221 sniff_objectformat_hash_version(&gitdir.join("config"))
1222}
1223
1224fn sniff_objectformat_hash_version(config_path: &Path) -> u8 {
1226 sniff_objectformat_hash_version_uncached(config_path)
1227}
1228
1229fn sniff_objectformat_hash_version_uncached(config_path: &Path) -> u8 {
1231 let text = if let Some(caches) =
1232 crate::pack_store::PackStore::repo_caches_for_config_path(config_path)
1233 {
1234 if let Some(text) = caches.cached_local_config_text_for_path(config_path) {
1235 text
1236 } else {
1237 let Ok(text) = crate::repo_caches::read_local_config_file(config_path) else {
1238 return HASH_VERSION_SHA1;
1239 };
1240 text
1241 }
1242 } else {
1243 let Ok(text) = crate::repo_caches::read_local_config_file(config_path) else {
1244 return HASH_VERSION_SHA1;
1245 };
1246 text
1247 };
1248 let mut in_extensions = false;
1252 for raw in text.lines() {
1253 let line = raw.trim();
1254 if line.starts_with('[') {
1255 let section = line.trim_start_matches('[').trim_end_matches(']');
1256 let name = section.split_whitespace().next().unwrap_or("");
1257 in_extensions = name.eq_ignore_ascii_case("extensions");
1258 continue;
1259 }
1260 if !in_extensions {
1261 continue;
1262 }
1263 if let Some((key, value)) = line.split_once('=') {
1264 if key.trim().eq_ignore_ascii_case("objectformat")
1265 && value.trim().eq_ignore_ascii_case("sha256")
1266 {
1267 return HASH_VERSION_SHA256;
1268 }
1269 }
1270 }
1271 HASH_VERSION_SHA1
1272}
1273
1274#[must_use]
1276pub fn cached_tip_midx_path(pack_dir: &Path) -> Option<std::path::PathBuf> {
1277 resolve_tip_midx_path(pack_dir)
1278}
1279
1280pub fn evict_midx_read_cache_for_pack_dir(pack_dir: &Path) {
1282 midx_cache::evict_pack_dir(pack_dir);
1283}
1284
1285pub fn resolve_tip_midx_path(pack_dir: &Path) -> Option<std::path::PathBuf> {
1287 let root = pack_dir.join("multi-pack-index");
1288 if root.exists() {
1289 return Some(root);
1290 }
1291 let hashes = read_chain_layer_hashes(pack_dir).ok()?;
1292 let last = hashes.last()?;
1293 Some(midx_d_dir(pack_dir).join(format!("multi-pack-index-{last}.midx")))
1294}
1295
1296pub fn midx_sidecar_path_for_midx_file(midx_path: &Path, ext: &str) -> Result<PathBuf> {
1301 if let Some(name) = midx_path.file_name().and_then(|s| s.to_str()) {
1302 if let Some(hash_part) = name
1303 .strip_prefix("multi-pack-index-")
1304 .and_then(|r| r.strip_suffix(".midx"))
1305 {
1306 let parent = midx_path.parent().ok_or_else(|| {
1307 Error::CorruptObject("MIDX path has no parent directory".to_owned())
1308 })?;
1309 return Ok(parent.join(format!("multi-pack-index-{hash_part}.{ext}")));
1310 }
1311 }
1312 let hash_hex = midx_checksum_hex_from_path(midx_path)?;
1313 let parent = midx_path
1314 .parent()
1315 .ok_or_else(|| Error::CorruptObject("MIDX path has no parent directory".to_owned()))?;
1316 Ok(parent.join(format!("multi-pack-index-{hash_hex}.{ext}")))
1317}
1318
1319#[must_use]
1324pub fn resolve_midx_bitmap_layer(pack_dir: &Path) -> Option<(PathBuf, PathBuf)> {
1325 let is_real_bitmap =
1326 |path: &Path| path.is_file() && path.metadata().ok().is_some_and(|m| m.len() > 32);
1327
1328 if let Ok(chain) = read_chain_layer_hashes(pack_dir) {
1329 if !chain.is_empty() {
1330 let midx_d = midx_d_dir(pack_dir);
1331 for hash in chain.iter().rev() {
1332 let midx_path = midx_d.join(format!("multi-pack-index-{hash}.midx"));
1333 if !midx_path.is_file() {
1334 continue;
1335 }
1336 let bitmap_path = midx_d.join(format!("multi-pack-index-{hash}.bitmap"));
1337 if is_real_bitmap(&bitmap_path) {
1338 return Some((bitmap_path, midx_path));
1339 }
1340 }
1341 }
1342 }
1343
1344 let root = pack_dir.join("multi-pack-index");
1345 if root.is_file() {
1346 let hash_hex = midx_checksum_hex_from_path(&root).ok()?;
1347 let bitmap_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.bitmap"));
1348 if is_real_bitmap(&bitmap_path) {
1349 return Some((bitmap_path, root));
1350 }
1351 }
1352 None
1353}
1354
1355pub fn resolve_midx_layer_path(pack_dir: &Path, checksum: &str) -> Option<std::path::PathBuf> {
1357 let checksum = checksum.to_ascii_lowercase();
1358 if let Ok(hashes) = read_chain_layer_hashes(pack_dir) {
1359 if hashes.contains(&checksum) {
1360 return Some(midx_d_dir(pack_dir).join(format!("multi-pack-index-{checksum}.midx")));
1361 }
1362 }
1363 let root = pack_dir.join("multi-pack-index");
1364 if root.exists() {
1365 if let Ok(hex) = midx_checksum_hex_from_path(&root) {
1366 if hex == checksum {
1367 return Some(root);
1368 }
1369 }
1370 }
1371 None
1372}
1373
1374fn load_midx_file(path: &Path) -> Result<Vec<u8>> {
1375 let data = fs::read(path).map_err(Error::Io)?;
1376 let _ = parse_midx_header(&data)?;
1377 Ok(data)
1378}
1379
1380fn midx_hash_len(data: &[u8]) -> usize {
1383 if data.len() > 5 && data[5] == 2 {
1384 32
1385 } else {
1386 20
1387 }
1388}
1389
1390fn oids_and_packs_from_midx_data(data: &[u8]) -> Result<(HashSet<ObjectId>, Vec<String>)> {
1391 let hash_len = midx_hash_len(data);
1392 let (_, hdr_end, _) = parse_midx_header(data)?;
1393 let (pn_off, pn_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1394 let pack_names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
1395 let (_ooff_off, ooff_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1396 let (oidl_off, oidl_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
1397 let num_objects = ooff_len / 8;
1398 if oidl_len != num_objects * hash_len {
1399 return Err(Error::CorruptObject(
1400 "MIDX oid-lookup size mismatch".to_owned(),
1401 ));
1402 }
1403 let mut oids = HashSet::with_capacity(num_objects);
1404 for i in 0..num_objects {
1405 let start = oidl_off + i * hash_len;
1406 let oid = ObjectId::from_bytes(&data[start..start + hash_len])?;
1407 oids.insert(oid);
1408 }
1409 Ok((oids, pack_names))
1410}
1411
1412fn collect_incremental_base(pack_dir: &Path) -> Result<(HashSet<ObjectId>, HashSet<String>)> {
1413 let mut oids = HashSet::new();
1414 let mut packs = HashSet::new();
1415 let root = pack_dir.join("multi-pack-index");
1416 let chain_path = chain_file_path(pack_dir);
1417 if chain_path.exists() {
1418 for h in read_chain_layer_hashes(pack_dir)? {
1419 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{h}.midx"));
1420 let data = load_midx_file(&p)?;
1421 let (layer_oids, names) = oids_and_packs_from_midx_data(&data)?;
1422 oids.extend(layer_oids);
1423 for n in names {
1424 packs.insert(n);
1425 }
1426 }
1427 return Ok((oids, packs));
1428 }
1429 if root.exists() {
1430 let data = load_midx_file(&root)?;
1431 let (o, names) = oids_and_packs_from_midx_data(&data)?;
1432 oids = o;
1433 for n in names {
1434 packs.insert(n);
1435 }
1436 }
1437 Ok((oids, packs))
1438}
1439
1440fn midx_trailing_hash_len(data: &[u8]) -> Result<usize> {
1441 if data.len() < MIDX_HEADER_SIZE {
1442 return Err(Error::CorruptObject(
1443 "midx file too small for header".to_owned(),
1444 ));
1445 }
1446 HashAlgo::try_from(data[5]).map(|a| a.len()).map_err(|e| {
1447 Error::CorruptObject(format!(
1448 "multi-pack-index hash version {} not recognized",
1449 e.0
1450 ))
1451 })
1452}
1453
1454fn midx_checksum_hex_from_path(path: &Path) -> Result<String> {
1455 let data = fs::read(path).map_err(Error::Io)?;
1456 let hash_len = midx_trailing_hash_len(&data)?;
1457 if data.len() < hash_len {
1458 return Err(Error::CorruptObject(
1459 "midx too small for checksum".to_owned(),
1460 ));
1461 }
1462 let hash = &data[data.len() - hash_len..];
1463 Ok(hex::encode(hash))
1464}
1465
1466fn link_root_midx_into_chain(pack_dir: &Path, root_checksum_hex: &str) -> Result<()> {
1467 let midx_d = midx_d_dir(pack_dir);
1468 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
1469 let dst_midx = midx_d.join(format!("multi-pack-index-{root_checksum_hex}.midx"));
1470 hard_link_or_copy(&pack_dir.join("multi-pack-index"), &dst_midx)?;
1471 let exts = ["bitmap", "rev"];
1472 for ext in exts {
1473 let src = pack_dir.join(format!("multi-pack-index-{root_checksum_hex}.{ext}"));
1474 if src.exists() {
1475 let dst = midx_d.join(format!("multi-pack-index-{root_checksum_hex}.{ext}"));
1476 hard_link_or_copy(&src, &dst)?;
1477 }
1478 }
1479 Ok(())
1480}
1481
1482fn clear_stale_split_layers(pack_dir: &Path, keep: &[String]) -> Result<()> {
1483 let midx_d = midx_d_dir(pack_dir);
1484 if !midx_d.exists() {
1485 return Ok(());
1486 }
1487 let keep: HashSet<&str> = keep.iter().map(|s| s.as_str()).collect();
1488 for ent in fs::read_dir(&midx_d).map_err(Error::Io)? {
1489 let ent = ent.map_err(Error::Io)?;
1490 let name = ent.file_name().to_string_lossy().to_string();
1491 let Some(rest) = name.strip_prefix("multi-pack-index-") else {
1492 continue;
1493 };
1494 let Some((hash_part, _ext)) = rest.split_once('.') else {
1495 continue;
1496 };
1497 if (hash_part.len() == 40 || hash_part.len() == 64) && !keep.contains(hash_part) {
1498 let _ = fs::remove_file(ent.path());
1499 }
1500 }
1501 Ok(())
1502}
1503
1504fn clear_incremental_midx_files(pack_dir: &Path) -> Result<()> {
1514 let midx_d = midx_d_dir(pack_dir);
1515 let _ = fs::remove_file(chain_file_path(pack_dir));
1517 if !midx_d.exists() {
1518 return Ok(());
1519 }
1520 for ent in fs::read_dir(&midx_d).map_err(Error::Io)? {
1521 let ent = ent.map_err(Error::Io)?;
1522 let name = ent.file_name().to_string_lossy().to_string();
1523 if name.starts_with("multi-pack-index-")
1524 && (name.ends_with(".midx") || name.ends_with(".bitmap") || name.ends_with(".rev"))
1525 {
1526 let _ = fs::remove_file(ent.path());
1527 }
1528 }
1529 Ok(())
1530}
1531
1532fn pack_mtime_for_midx(idx: &PackIndex) -> std::time::SystemTime {
1533 fs::metadata(&idx.pack_path)
1534 .and_then(|m| m.modified())
1535 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
1536}
1537
1538fn midx_pick_better_entry(
1539 cur: &MidxEntry,
1540 cand_pack: u32,
1541 cand_offset: u64,
1542 cand_mtime: std::time::SystemTime,
1543 preferred_pack: Option<u32>,
1544) -> bool {
1545 let cur_pref = preferred_pack == Some(cur.pack_id);
1546 let new_pref = preferred_pack == Some(cand_pack);
1547 if new_pref && !cur_pref {
1548 return true;
1549 }
1550 if cur_pref && !new_pref {
1551 return false;
1552 }
1553 match cand_mtime.cmp(&cur.pack_mtime) {
1554 std::cmp::Ordering::Greater => true,
1555 std::cmp::Ordering::Less => false,
1556 std::cmp::Ordering::Equal => {
1557 if cand_pack != cur.pack_id {
1558 cand_pack < cur.pack_id
1559 } else {
1560 cand_offset < cur.offset
1561 }
1562 }
1563 }
1564}
1565
1566#[allow(clippy::too_many_arguments)]
1571fn build_midx_bytes_filtered(
1572 idx_names: &[String],
1573 indexes: &[PackIndex],
1574 preferred_idx: Option<usize>,
1575 write_bitmap: bool,
1576 omit_embedded_ridx_chunk: bool,
1577 version: u8,
1578 hash_version: u8,
1579 exclude_oids: Option<&HashSet<ObjectId>>,
1580) -> Result<(Vec<u8>, Option<Vec<u32>>)> {
1581 let hash_len = if hash_version == 2 { 32 } else { 20 };
1583 let preferred_pack_idx = preferred_idx.map(|p| p as u32);
1584 let pack_mtimes: Vec<std::time::SystemTime> = indexes.iter().map(pack_mtime_for_midx).collect();
1585
1586 let mut best: HashMap<ObjectId, MidxEntry> = HashMap::new();
1587 for (pack_id, idx) in indexes.iter().enumerate() {
1588 let pack_id = u32::try_from(pack_id).map_err(|_| {
1589 Error::CorruptObject("too many pack files for multi-pack-index".to_owned())
1590 })?;
1591 let mtime = pack_mtimes[pack_id as usize];
1592 for e in idx.iter() {
1593 if e.oid().len() != hash_len {
1594 continue;
1595 }
1596 let Ok(oid) = ObjectId::from_bytes(e.oid()) else {
1597 continue;
1598 };
1599 if let Some(ex) = exclude_oids {
1600 if ex.contains(&oid) {
1601 continue;
1602 }
1603 }
1604 let offset = e.offset()?;
1605 let cand = MidxEntry {
1606 oid,
1607 pack_id,
1608 offset,
1609 pack_mtime: mtime,
1610 };
1611 match best.get(&oid) {
1612 None => {
1613 best.insert(oid, cand);
1614 }
1615 Some(cur) => {
1616 if midx_pick_better_entry(cur, pack_id, offset, mtime, preferred_pack_idx) {
1617 best.insert(oid, cand);
1618 }
1619 }
1620 }
1621 }
1622 }
1623
1624 let mut entries: Vec<MidxEntry> = best.into_values().collect();
1625 entries.sort_by_key(|a| a.oid);
1626
1627 let large_offsets_needed = entries.iter().any(|e| e.offset > u64::from(u32::MAX));
1635
1636 let num_packs = indexes.len() as u32;
1637
1638 let mut pack_names_blob = Vec::new();
1639 for name in idx_names {
1640 pack_names_blob.extend_from_slice(name.as_bytes());
1641 pack_names_blob.push(0);
1642 }
1643 let pad = (MIDX_CHUNK_ALIGNMENT - (pack_names_blob.len() % MIDX_CHUNK_ALIGNMENT))
1644 % MIDX_CHUNK_ALIGNMENT;
1645 pack_names_blob.extend(std::iter::repeat_n(0u8, pad));
1646 let chunk_pnam = pack_names_blob;
1647
1648 let mut chunk_oidf = vec![0u8; 256 * 4];
1649 let mut j = 0usize;
1650 for i in 0..256 {
1651 while j < entries.len() && entries[j].oid.as_bytes()[0] <= i as u8 {
1652 j += 1;
1653 }
1654 chunk_oidf[i * 4..(i + 1) * 4].copy_from_slice(&(j as u32).to_be_bytes());
1655 }
1656
1657 let mut chunk_oidl = Vec::with_capacity(entries.len() * 20);
1658 for e in &entries {
1659 chunk_oidl.extend_from_slice(e.oid.as_bytes());
1660 }
1661
1662 let mut large_offsets: Vec<u64> = Vec::new();
1663 let mut chunk_ooff = Vec::with_capacity(entries.len() * 8);
1664 for e in &entries {
1665 chunk_ooff.extend_from_slice(&e.pack_id.to_be_bytes());
1666 let encoded = if large_offsets_needed && e.offset >> 31 != 0 {
1667 let slot = u32::try_from(large_offsets.len()).map_err(|_| {
1668 Error::CorruptObject("too many large offsets in multi-pack-index".to_owned())
1669 })?;
1670 large_offsets.push(e.offset);
1671 MIDX_LARGE_OFFSET_NEEDED | slot
1672 } else {
1673 e.offset as u32
1677 };
1678 chunk_ooff.extend_from_slice(&encoded.to_be_bytes());
1679 }
1680
1681 let chunk_loff: Vec<u8> = if large_offsets.is_empty() {
1682 Vec::new()
1683 } else {
1684 let mut v = Vec::with_capacity(large_offsets.len() * 8);
1685 for off in &large_offsets {
1686 v.extend_from_slice(&off.to_be_bytes());
1687 }
1688 v
1689 };
1690
1691 let pref = preferred_pack_idx;
1692 let mut order: Vec<u32> = (0..entries.len() as u32).collect();
1693 order.sort_by(|&ai, &bi| {
1694 let a = &entries[ai as usize];
1695 let b = &entries[bi as usize];
1696 let a_pref = pref == Some(a.pack_id);
1697 let b_pref = pref == Some(b.pack_id);
1698 b_pref
1699 .cmp(&a_pref)
1700 .then_with(|| a.pack_id.cmp(&b.pack_id))
1701 .then_with(|| a.offset.cmp(&b.offset))
1702 .then_with(|| ai.cmp(&bi))
1703 });
1704
1705 let mut chunk_ridx = Vec::with_capacity(entries.len() * 4);
1706 for oid_idx in &order {
1707 chunk_ridx.extend_from_slice(&oid_idx.to_be_bytes());
1708 }
1709
1710 let bitmap_rid_order = if write_bitmap {
1713 Some(order.clone())
1714 } else {
1715 None
1716 };
1717 let chunk_btmp: Vec<u8> = if write_bitmap {
1718 let num_packs_usize = indexes.len();
1723 let mut bitmap_pos = vec![u32::MAX; num_packs_usize];
1724 let mut bitmap_nr = vec![0u32; num_packs_usize];
1725 for (rank, &oid_idx) in order.iter().enumerate() {
1726 let pack = entries[oid_idx as usize].pack_id as usize;
1727 if let Some(p) = bitmap_pos.get_mut(pack) {
1728 if *p == u32::MAX {
1729 *p = rank as u32;
1730 }
1731 }
1732 if let Some(n) = bitmap_nr.get_mut(pack) {
1733 *n += 1;
1734 }
1735 }
1736 let mut v = Vec::new();
1737 for pack in 0..num_packs_usize {
1738 let pos = if bitmap_pos[pack] == u32::MAX {
1739 0
1740 } else {
1741 bitmap_pos[pack]
1742 };
1743 v.extend_from_slice(&pos.to_be_bytes());
1744 v.extend_from_slice(&bitmap_nr[pack].to_be_bytes());
1745 }
1746 let pad = (MIDX_CHUNK_ALIGNMENT - (v.len() % MIDX_CHUNK_ALIGNMENT)) % MIDX_CHUNK_ALIGNMENT;
1747 v.extend(std::iter::repeat_n(0u8, pad));
1748 v
1749 } else {
1750 Vec::new()
1751 };
1752
1753 let mut chunks: Vec<(u32, Vec<u8>)> = vec![
1754 (MIDX_CHUNKID_PACKNAMES, chunk_pnam),
1755 (MIDX_CHUNKID_OIDFANOUT, chunk_oidf),
1756 (MIDX_CHUNKID_OIDLOOKUP, chunk_oidl),
1757 (MIDX_CHUNKID_OBJECTOFFSETS, chunk_ooff),
1758 ];
1759 if !chunk_loff.is_empty() {
1760 chunks.push((MIDX_CHUNKID_LARGEOFFSETS, chunk_loff));
1761 }
1762 if (pref.is_some() || write_bitmap) && !omit_embedded_ridx_chunk {
1763 chunks.push((MIDX_CHUNKID_REVINDEX, chunk_ridx));
1764 }
1765 if write_bitmap {
1766 chunks.push((MIDX_CHUNKID_BITMAPPED_PACKS, chunk_btmp));
1767 }
1768
1769 let num_chunks: u8 = chunks
1770 .len()
1771 .try_into()
1772 .map_err(|_| Error::CorruptObject("too many MIDX chunks".to_owned()))?;
1773
1774 let mut body = Vec::new();
1775 let mut cur_offset =
1776 MIDX_HEADER_SIZE as u64 + ((chunks.len() + 1) * CHUNK_TOC_ENTRY_SIZE) as u64;
1777
1778 for (id, data) in &chunks {
1779 body.extend_from_slice(&id.to_be_bytes());
1780 body.extend_from_slice(&cur_offset.to_be_bytes());
1781 cur_offset += data.len() as u64;
1782 }
1783 body.extend_from_slice(&0u32.to_be_bytes());
1784 body.extend_from_slice(&cur_offset.to_be_bytes());
1785
1786 for (_, data) in &chunks {
1787 body.extend_from_slice(data);
1788 }
1789
1790 let mut out = Vec::with_capacity(MIDX_HEADER_SIZE + body.len() + 20);
1791 out.extend_from_slice(&MIDX_SIGNATURE.to_be_bytes());
1792 out.push(if version == MIDX_VERSION_V1 {
1793 MIDX_VERSION_V1
1794 } else {
1795 MIDX_VERSION_V2
1796 });
1797 out.push(hash_version);
1798 out.push(num_chunks);
1799 out.push(0);
1800 out.extend_from_slice(&num_packs.to_be_bytes());
1801 out.extend_from_slice(&body);
1802
1803 let algo = if hash_version == 2 {
1805 HashAlgo::Sha256
1806 } else {
1807 HashAlgo::Sha1
1808 };
1809 out.extend_from_slice(&algo.checksum_digest(&out));
1810
1811 Ok((out, bitmap_rid_order))
1812}
1813
1814fn write_midx_rev_sidecar(
1819 path: &Path,
1820 pack_order: &[u32],
1821 midx_file_hash: &[u8],
1822 algo: HashAlgo,
1823) -> Result<()> {
1824 let hash_len = algo.len();
1825 let hash_id = u32::from(u8::from(algo));
1826 let body_len = RIDX_HEADER_SIZE + pack_order.len() * 4 + hash_len;
1827 let mut out = Vec::with_capacity(body_len + hash_len);
1828 out.extend_from_slice(&RIDX_SIGNATURE.to_be_bytes());
1829 out.extend_from_slice(&RIDX_VERSION.to_be_bytes());
1830 out.extend_from_slice(&hash_id.to_be_bytes());
1831 for idx in pack_order {
1832 out.extend_from_slice(&idx.to_be_bytes());
1833 }
1834 if midx_file_hash.len() >= hash_len {
1835 out.extend_from_slice(&midx_file_hash[..hash_len]);
1836 } else {
1837 return Err(Error::CorruptObject(format!(
1838 "MIDX checksum length {} does not match hash algorithm width {hash_len}",
1839 midx_file_hash.len()
1840 )));
1841 }
1842 append_hashfile_checksum(&mut out, hash_len);
1843 let mut file = fs::File::create(path).map_err(Error::Io)?;
1844 file.write_all(&out).map_err(Error::Io)?;
1845 file.sync_all().map_err(Error::Io)?;
1846 if !path.is_file() {
1847 return Err(Error::CorruptObject(format!(
1848 "MIDX rev sidecar not present after write ({})",
1849 path.display()
1850 )));
1851 }
1852 Ok(())
1853}
1854
1855fn repo_hash_algo_for_pack_dir(pack_dir: &Path) -> HashAlgo {
1856 pack_dir
1857 .parent()
1858 .map(|d| {
1859 crate::odb::hash_algo_for_objects_dir(&crate::environment::Environment::empty(), d)
1860 })
1861 .unwrap_or(HashAlgo::Sha1)
1862}
1863
1864fn find_chunk(data: &[u8], header_end: usize, chunk_id: u32) -> Result<(usize, usize)> {
1865 let (hdr, _, _) = parse_midx_header(data)?;
1866 let n = hdr.num_chunks as usize;
1867 let pos = header_end;
1868 let toc_end = pos + (n + 1) * CHUNK_TOC_ENTRY_SIZE;
1869 if data.len() < toc_end + 20 {
1870 return Err(Error::CorruptObject(
1871 "truncated MIDX chunk table".to_owned(),
1872 ));
1873 }
1874 for i in 0..n {
1875 let base = pos + i * CHUNK_TOC_ENTRY_SIZE;
1876 let id = read_be_u32(data, base)?;
1877 let off = read_be_u64(data, base + 4)? as usize;
1878 if id == chunk_id {
1879 let next_off = if i + 1 < n {
1880 let nb = pos + (i + 1) * CHUNK_TOC_ENTRY_SIZE;
1881 read_be_u64(data, nb + 4)? as usize
1882 } else {
1883 let term = pos + n * CHUNK_TOC_ENTRY_SIZE;
1884 read_be_u64(data, term + 4)? as usize
1885 };
1886 return Ok((off, next_off.saturating_sub(off)));
1887 }
1888 }
1889 Err(Error::CorruptObject(format!(
1890 "MIDX chunk {chunk_id:08x} not found"
1891 )))
1892}
1893
1894#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1896enum MidxChunkTableFatal {
1897 FileTooSmall,
1898 TableTruncated,
1899 RequiredPackNameChunkMissing,
1900}
1901
1902impl From<MidxChunkTableFatal> for MidxError {
1903 fn from(value: MidxChunkTableFatal) -> Self {
1904 match value {
1905 MidxChunkTableFatal::FileTooSmall => MidxError::InvalidChunkTable {
1906 detail: "multi-pack-index file too small".to_owned(),
1907 },
1908 MidxChunkTableFatal::TableTruncated => MidxError::InvalidChunkTable {
1909 detail: "multi-pack-index chunk table is truncated".to_owned(),
1910 },
1911 MidxChunkTableFatal::RequiredPackNameChunkMissing => {
1912 MidxError::RequiredPackNameChunkMissing
1913 }
1914 }
1915 }
1916}
1917
1918fn midx_write_warn(opts: &WriteMultiPackIndexOptions, warning: Warning) {
1919 if let Some(handle) = opts.diagnostics.as_ref() {
1920 handle.warn(warning);
1921 } else {
1922 NullDiagnostics.warn(warning);
1923 }
1924}
1925
1926struct TocEntry {
1928 id: u32,
1929 offset: usize,
1930}
1931
1932fn parse_midx_toc(
1936 data: &[u8],
1937 hash_len: usize,
1938 errors: &mut Vec<String>,
1939) -> std::result::Result<Vec<TocEntry>, MidxChunkTableFatal> {
1940 if data.len() < MIDX_HEADER_SIZE + hash_len {
1941 return Err(MidxChunkTableFatal::FileTooSmall);
1942 }
1943 let num_chunks = data[6] as usize;
1944 let toc_off = MIDX_HEADER_SIZE;
1945 let needed = toc_off + (num_chunks + 1) * CHUNK_TOC_ENTRY_SIZE;
1946 if data.len() < needed {
1947 return Err(MidxChunkTableFatal::TableTruncated);
1948 }
1949 let file_size = data.len();
1950 let mut chunks: Vec<TocEntry> = Vec::with_capacity(num_chunks);
1951
1952 let read_be64 = |off: usize| -> u64 {
1953 let mut b = [0u8; 8];
1954 b.copy_from_slice(&data[off..off + 8]);
1955 u64::from_be_bytes(b)
1956 };
1957 let read_be32 = |off: usize| -> u32 {
1958 let mut b = [0u8; 4];
1959 b.copy_from_slice(&data[off..off + 4]);
1960 u32::from_be_bytes(b)
1961 };
1962
1963 for i in 0..num_chunks {
1964 let entry = toc_off + i * CHUNK_TOC_ENTRY_SIZE;
1965 let chunk_id = read_be32(entry);
1966 let chunk_offset = read_be64(entry + 4);
1967
1968 if chunk_id == 0 {
1969 errors.push("terminating chunk id appears earlier than expected".to_owned());
1970 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1971 }
1972 if !(chunk_offset as usize).is_multiple_of(MIDX_CHUNK_ALIGNMENT) {
1973 errors.push(format!(
1974 "chunk id {chunk_id:x} not {MIDX_CHUNK_ALIGNMENT}-byte aligned"
1975 ));
1976 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1977 }
1978
1979 let next_entry = toc_off + (i + 1) * CHUNK_TOC_ENTRY_SIZE;
1980 let next_chunk_offset = read_be64(next_entry + 4);
1981
1982 if next_chunk_offset < chunk_offset
1983 || next_chunk_offset > (file_size as u64).saturating_sub(hash_len as u64)
1984 {
1985 errors.push(format!(
1986 "improper chunk offset(s) {chunk_offset:x} and {next_chunk_offset:x}"
1987 ));
1988 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1989 }
1990
1991 if chunks.iter().any(|c| c.id == chunk_id) {
1992 errors.push(format!("duplicate chunk ID {chunk_id:x} found"));
1993 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
1994 }
1995
1996 chunks.push(TocEntry {
1997 id: chunk_id,
1998 offset: chunk_offset as usize,
1999 });
2000 }
2001
2002 let term_entry = toc_off + num_chunks * CHUNK_TOC_ENTRY_SIZE;
2004 let final_id = read_be32(term_entry);
2005 if final_id != 0 {
2006 errors.push(format!("final chunk has non-zero id {final_id:x}"));
2007 return Err(MidxChunkTableFatal::RequiredPackNameChunkMissing);
2008 }
2009
2010 let term_offset = read_be64(term_entry + 4) as usize;
2013 chunks.push(TocEntry {
2014 id: 0,
2015 offset: term_offset,
2016 });
2017
2018 Ok(chunks)
2019}
2020
2021fn toc_chunk_range(chunks: &[TocEntry], data_len: usize, id: u32) -> Option<(usize, usize)> {
2023 for (i, c) in chunks.iter().enumerate() {
2024 if c.id == id {
2025 let next = if i + 1 < chunks.len() {
2026 chunks[i + 1].offset
2027 } else {
2028 data_len.saturating_sub(20)
2029 };
2030 return Some((c.offset, next.saturating_sub(c.offset)));
2031 }
2032 }
2033 None
2034}
2035
2036pub fn verify_midx(objects_dir: &Path) -> std::result::Result<(), Vec<String>> {
2043 let pack_dir = objects_dir.join("pack");
2044 let path = match resolve_tip_midx_path(&pack_dir) {
2045 Some(p) => p,
2046 None => return Ok(()),
2047 };
2048 let data = match open_midx_layer_bytes(&path) {
2049 Ok(bytes) => bytes,
2050 Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2051 Err(_) => return Ok(()),
2052 };
2053 let data = data.as_ref();
2054
2055 let mut fatal: Vec<String> = Vec::new();
2056 let mut errors: Vec<String> = Vec::new();
2057
2058 if data.len() < MIDX_HEADER_SIZE + 20 {
2060 return Err(vec!["multi-pack-index file is too small".to_owned()]);
2061 }
2062 let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
2063 if sig != MIDX_SIGNATURE {
2064 return Err(vec![format!(
2065 "multi-pack-index signature 0x{sig:08x} does not match signature 0x{MIDX_SIGNATURE:08x}"
2066 )]);
2067 }
2068 let version = data[4];
2069 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
2070 return Err(vec![format!(
2071 "multi-pack-index version {version} not recognized"
2072 )]);
2073 }
2074 let hash_version = data[5];
2075 let expected_hash_version = repo_midx_hash_version_for_objects_dir(objects_dir);
2076 if hash_version != expected_hash_version {
2077 return Err(vec![format!(
2078 "multi-pack-index hash version {hash_version} does not match version {expected_hash_version}"
2079 )]);
2080 }
2081 let hash_len = if hash_version == 2 { 32usize } else { 20usize };
2082 let num_packs = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
2083
2084 let chunks = match parse_midx_toc(data, hash_len, &mut errors) {
2086 Ok(c) => c,
2087 Err(fatal) => {
2088 if !errors.is_empty() {
2089 return Err(errors);
2090 }
2091 errors.push(MidxError::from(fatal).to_string());
2092 return Err(errors);
2093 }
2094 };
2095
2096 let Some((pn_off, pn_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_PACKNAMES)
2098 else {
2099 errors.push("multi-pack-index required pack-name chunk missing or corrupted".to_owned());
2100 return Err(errors);
2101 };
2102
2103 let Some((fan_off, fan_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDFANOUT)
2105 else {
2106 errors.push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
2107 return Err(errors);
2108 };
2109 if fan_len != 256 * 4 {
2110 errors.push("multi-pack-index OID fanout is of the wrong size".to_owned());
2111 errors.push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
2112 return Err(errors);
2113 }
2114 let fanout = |i: usize| -> u32 {
2115 let b = fan_off + i * 4;
2116 u32::from_be_bytes([data[b], data[b + 1], data[b + 2], data[b + 3]])
2117 };
2118 for i in 0..255 {
2119 let f1 = fanout(i);
2120 let f2 = fanout(i + 1);
2121 if f1 > f2 {
2122 errors.push(format!(
2123 "oid fanout out of order: fanout[{i}] = {f1:x} > {f2:x} = fanout[{}]",
2124 i + 1
2125 ));
2126 errors
2127 .push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
2128 return Err(errors);
2129 }
2130 }
2131 let num_objects = fanout(255) as usize;
2132
2133 let Some((oidl_off, oidl_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDLOOKUP)
2135 else {
2136 errors.push("multi-pack-index required OID lookup chunk missing or corrupted".to_owned());
2137 return Err(errors);
2138 };
2139 if oidl_len != hash_len * num_objects {
2140 errors.push("multi-pack-index OID lookup chunk is the wrong size".to_owned());
2141 errors.push("multi-pack-index required OID lookup chunk missing or corrupted".to_owned());
2142 return Err(errors);
2143 }
2144
2145 let Some((ooff_off, ooff_len)) =
2147 toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OBJECTOFFSETS)
2148 else {
2149 errors
2150 .push("multi-pack-index required object offsets chunk missing or corrupted".to_owned());
2151 return Err(errors);
2152 };
2153 if ooff_len != num_objects * 8 {
2154 errors.push("multi-pack-index object offset chunk is the wrong size".to_owned());
2155 errors
2156 .push("multi-pack-index required object offsets chunk missing or corrupted".to_owned());
2157 return Err(errors);
2158 }
2159
2160 let large_off = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_LARGEOFFSETS);
2161
2162 let names = match parse_pack_names_blob(&data[pn_off..pn_off + pn_len]) {
2164 Ok(n) => n,
2165 Err(_) => {
2166 errors.push("multi-pack-index pack-name chunk is too short".to_owned());
2167 return Err(errors);
2168 }
2169 };
2170 if version == MIDX_VERSION_V1 {
2171 for i in 1..names.len() {
2172 if names[i] <= names[i - 1] {
2173 fatal.push(format!(
2174 "multi-pack-index pack names out of order: '{}' before '{}'",
2175 names[i - 1],
2176 names[i]
2177 ));
2178 errors.extend(fatal);
2180 return Err(errors);
2181 }
2182 }
2183 }
2184
2185 if !midx_checksum_is_valid(data) {
2187 errors.push("incorrect checksum".to_owned());
2188 }
2189
2190 let mut pack_indexes: Vec<Option<PackIndex>> = Vec::with_capacity(num_packs);
2192 for i in 0..num_packs {
2193 let loaded = match names.get(i) {
2199 Some(name) => read_pack_index_no_verify(&pack_dir.join(name)).ok(),
2200 None => None,
2201 };
2202 if loaded.is_none() {
2203 errors.push(format!("failed to load pack in position {i}"));
2204 }
2205 pack_indexes.push(loaded);
2206 }
2207
2208 if num_objects == 0 {
2209 errors.push("the midx contains no oid".to_owned());
2210 if errors.is_empty() {
2211 return Ok(());
2212 }
2213 return Err(errors);
2214 }
2215
2216 let oid_at =
2218 |i: usize| -> &[u8] { &data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len] };
2219 for i in 0..num_objects.saturating_sub(1) {
2220 let a = oid_at(i);
2221 let b = oid_at(i + 1);
2222 if a >= b {
2223 errors.push(format!(
2224 "oid lookup out of order: oid[{i}] = {} >= {} = oid[{}]",
2225 hex::encode(a),
2226 hex::encode(b),
2227 i + 1
2228 ));
2229 }
2230 }
2231
2232 for i in 0..num_objects {
2234 let ob = ooff_off + i * 8;
2235 let pack_int_id = u32::from_be_bytes([data[ob], data[ob + 1], data[ob + 2], data[ob + 3]]);
2236 let off_raw = u32::from_be_bytes([data[ob + 4], data[ob + 5], data[ob + 6], data[ob + 7]]);
2237 let oid_hex = hex::encode(oid_at(i));
2238
2239 if pack_int_id as usize >= num_packs {
2240 errors.push(format!(
2241 "bad pack-int-id: {pack_int_id} ({num_packs} total packs)"
2242 ));
2243 errors.push(format!(
2244 "failed to load pack entry for oid[{i}] = {oid_hex}"
2245 ));
2246 continue;
2247 }
2248
2249 let m_offset: u64 = if off_raw & MIDX_LARGE_OFFSET_NEEDED != 0 {
2251 let slot = (off_raw & !MIDX_LARGE_OFFSET_NEEDED) as usize;
2252 match large_off {
2253 Some((lo_off, lo_len)) if (slot + 1) * 8 <= lo_len => {
2254 let b = lo_off + slot * 8;
2255 let mut arr = [0u8; 8];
2256 arr.copy_from_slice(&data[b..b + 8]);
2257 u64::from_be_bytes(arr)
2258 }
2259 _ => {
2260 errors.push("multi-pack-index large offset out of bounds".to_owned());
2261 continue;
2262 }
2263 }
2264 } else {
2265 u64::from(off_raw)
2266 };
2267
2268 let Some(Some(idx)) = pack_indexes.get(pack_int_id as usize) else {
2269 errors.push(format!(
2270 "failed to load pack entry for oid[{i}] = {oid_hex}"
2271 ));
2272 continue;
2273 };
2274 let Ok(oid) = ObjectId::from_bytes(oid_at(i)) else {
2275 errors.push(format!(
2276 "failed to load pack entry for oid[{i}] = {oid_hex}"
2277 ));
2278 continue;
2279 };
2280 match idx.find_offset(&oid) {
2281 Ok(Some(p_offset)) => {
2282 if m_offset != p_offset {
2283 errors.push(format!(
2284 "incorrect object offset for oid[{i}] = {oid_hex}: {m_offset:x} != {p_offset:x}"
2285 ));
2286 }
2287 }
2288 Ok(None) => {
2289 errors.push(format!(
2290 "failed to load pack entry for oid[{i}] = {oid_hex}"
2291 ));
2292 }
2293 Err(err) => {
2294 errors.push(format!(
2295 "failed to load pack entry for oid[{i}] = {oid_hex}: {err}"
2296 ));
2297 }
2298 }
2299 }
2300
2301 if errors.is_empty() {
2302 Ok(())
2303 } else {
2304 Err(errors)
2305 }
2306}
2307
2308fn midx_checksum_is_valid(data: &[u8]) -> bool {
2311 let hash_len = midx_hash_len(data);
2312 if data.len() < hash_len {
2313 return false;
2314 }
2315 HashAlgo::from_len(hash_len).is_some_and(|algo| verify_trailer(algo, data).is_ok())
2316}
2317
2318pub fn read_midx_pack_idx_names(objects_dir: &Path) -> Result<Vec<String>> {
2329 let pack_dir = objects_dir.join("pack");
2330 let path = resolve_tip_midx_path(&pack_dir)
2331 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2332 let metadata = fs::metadata(&path).map_err(Error::Io)?;
2333 read_midx_pack_idx_names_from_path(&path, &metadata)
2334}
2335
2336pub(crate) fn read_midx_pack_idx_names_from_path(
2339 path: &Path,
2340 metadata: &fs::Metadata,
2341) -> Result<Vec<String>> {
2342 if let Some(names) = midx_cache::cached_pack_names(path, metadata) {
2343 return Ok(names);
2344 }
2345 let data = open_midx_layer_bytes(path)?;
2348 let (_, hdr_end, _) = parse_midx_header(&data)?;
2349 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2350 let blob = pn_off
2351 .checked_add(pn_len)
2352 .and_then(|end| data.get(pn_off..end))
2353 .ok_or_else(|| Error::CorruptObject("MIDX pack names are outside the file".to_owned()))?;
2354 parse_pack_names_blob(blob)
2355}
2356
2357pub struct MidxObjectRef {
2359 pub oid: ObjectId,
2361 pub pack_int_id: usize,
2363}
2364
2365pub fn read_midx_objects(objects_dir: &Path) -> Result<(Vec<String>, Vec<MidxObjectRef>)> {
2369 let pack_dir = objects_dir.join("pack");
2370 let path = resolve_tip_midx_path(&pack_dir)
2371 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2372 let data = fs::read(&path).map_err(Error::Io)?;
2373 let (_, hdr_end, _) = parse_midx_header(&data)?;
2374 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2375 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2376 let hash_len = midx_hash_len(&data);
2377 let (oidl_off, oidl_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2378 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2379 if oidl_len % hash_len != 0 || ooff_len % 8 != 0 {
2380 return Err(Error::CorruptObject(
2381 "bad MIDX oid-lookup / object-offsets size".to_owned(),
2382 ));
2383 }
2384 let num = oidl_len / hash_len;
2385 if num * 8 != ooff_len {
2386 return Err(Error::CorruptObject(
2387 "MIDX oid count does not match object-offsets".to_owned(),
2388 ));
2389 }
2390 let mut objects = Vec::with_capacity(num);
2391 for i in 0..num {
2392 let oid =
2393 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])
2394 .map_err(|e| Error::CorruptObject(e.to_string()))?;
2395 let base = ooff_off + i * 8;
2396 let pack_id = read_be_u32(&data, base)? as usize;
2397 objects.push(MidxObjectRef {
2398 oid,
2399 pack_int_id: pack_id,
2400 });
2401 }
2402 Ok((names, objects))
2403}
2404
2405pub fn midx_checksum_hex(objects_dir: &Path) -> Result<String> {
2407 let pack_dir = objects_dir.join("pack");
2408 let path = resolve_tip_midx_path(&pack_dir)
2409 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2410 midx_checksum_hex_from_path(&path)
2411}
2412
2413fn resolve_read_midx_path(pack_dir: &Path, checksum: Option<&str>) -> Result<std::path::PathBuf> {
2418 match checksum {
2419 Some(cs) => resolve_midx_layer_path(pack_dir, cs)
2420 .ok_or_else(|| Error::CorruptObject(format!("could not find MIDX with checksum {cs}"))),
2421 None => resolve_tip_midx_path(pack_dir)
2422 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned())),
2423 }
2424}
2425
2426pub fn format_midx_show_objects(objects_dir: &Path) -> Result<String> {
2429 format_midx_show_objects_layer(objects_dir, None)
2430}
2431
2432pub fn format_midx_show_objects_layer(
2434 objects_dir: &Path,
2435 checksum: Option<&str>,
2436) -> Result<String> {
2437 let mut out = format_midx_dump_layer(objects_dir, checksum)?;
2438 let pack_dir = objects_dir.join("pack");
2439 let path = resolve_read_midx_path(&pack_dir, checksum)?;
2440 let data = fs::read(&path).map_err(Error::Io)?;
2441 let (_, hdr_end, _) = parse_midx_header(&data)?;
2442 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2443 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2444 let hash_len = midx_hash_len(&data);
2445 let (oidl_off, oidl_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2446 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2447 if oidl_len % hash_len != 0 || ooff_len % 8 != 0 {
2448 return Err(Error::CorruptObject(
2449 "bad MIDX oid-lookup / object-offsets size".to_owned(),
2450 ));
2451 }
2452 let num = oidl_len / hash_len;
2453 if num * 8 != ooff_len {
2454 return Err(Error::CorruptObject(
2455 "MIDX oid count does not match object-offsets".to_owned(),
2456 ));
2457 }
2458 for i in 0..num {
2459 let oid =
2460 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])
2461 .map_err(|e| Error::CorruptObject(e.to_string()))?;
2462 let base = ooff_off + i * 8;
2463 let pack_id = read_be_u32(&data, base)? as usize;
2464 let offset = u64::from(read_be_u32(&data, base + 4)?);
2465 let idx_name = names
2466 .get(pack_id)
2467 .ok_or_else(|| Error::CorruptObject("pack id out of range in MIDX".to_owned()))?;
2468 let stem = idx_name.strip_suffix(".idx").unwrap_or(idx_name);
2472 let dir_disp = objects_dir.display().to_string();
2473 let dir_disp = if objects_dir.is_absolute() || dir_disp.starts_with("./") {
2474 dir_disp
2475 } else {
2476 format!("./{dir_disp}")
2477 };
2478 out.push_str(&format!(
2479 "{} {}\t{}/pack/{}.pack\n",
2480 oid.to_hex(),
2481 offset,
2482 dir_disp,
2483 stem
2484 ));
2485 }
2486 Ok(out)
2487}
2488
2489pub fn format_midx_dump(objects_dir: &Path) -> Result<String> {
2491 format_midx_dump_layer(objects_dir, None)
2492}
2493
2494pub fn format_midx_dump_layer(objects_dir: &Path, checksum: Option<&str>) -> Result<String> {
2497 let pack_dir = objects_dir.join("pack");
2498 let path = resolve_read_midx_path(&pack_dir, checksum)?;
2499 let data = fs::read(&path).map_err(Error::Io)?;
2500 let (hdr, hdr_end, _) = parse_midx_header(&data)?;
2501 let sig = read_be_u32(&data, 0)?;
2502 let version = data[4];
2503 let hash_len: u8 = match data[5] {
2506 1 => 20,
2507 2 => 32,
2508 other => other,
2509 };
2510 let num_chunks = hdr.num_chunks;
2511 let num_packs = read_be_u32(&data, 8)?;
2512
2513 let mut chunk_tags: Vec<&'static str> = Vec::new();
2514 let n = num_chunks as usize;
2515 let pos = hdr_end;
2516 let toc_end = pos + (n + 1) * CHUNK_TOC_ENTRY_SIZE;
2517 if data.len() < toc_end + 20 {
2518 return Err(Error::CorruptObject(
2519 "truncated MIDX chunk table".to_owned(),
2520 ));
2521 }
2522 for i in 0..n {
2523 let base = pos + i * CHUNK_TOC_ENTRY_SIZE;
2524 let id = read_be_u32(&data, base)?;
2525 let tag = match id {
2526 x if x == MIDX_CHUNKID_PACKNAMES => "pack-names",
2527 x if x == MIDX_CHUNKID_OIDFANOUT => "oid-fanout",
2528 x if x == MIDX_CHUNKID_OIDLOOKUP => "oid-lookup",
2529 x if x == MIDX_CHUNKID_OBJECTOFFSETS => "object-offsets",
2530 x if x == MIDX_CHUNKID_LARGEOFFSETS => "large-offsets",
2531 x if x == MIDX_CHUNKID_REVINDEX => "revindex",
2532 x if x == MIDX_CHUNKID_BITMAPPED_PACKS => "bitmapped-packs",
2533 _ => "unknown",
2534 };
2535 chunk_tags.push(tag);
2536 }
2537
2538 let (_ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2539 let num_objects = ooff_len / 8;
2540
2541 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2542 let pack_names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2543
2544 let mut out = String::new();
2545 out.push_str(&format!(
2546 "header: {:08x} {} {} {} {}\n",
2547 sig, version, hash_len, num_chunks, num_packs
2548 ));
2549 out.push_str("chunks:");
2550 for t in &chunk_tags {
2551 out.push(' ');
2552 out.push_str(t);
2553 }
2554 out.push('\n');
2555 out.push_str(&format!("num_objects: {num_objects}\n"));
2556 out.push_str("packs:\n");
2557 for n in &pack_names {
2558 out.push_str(n);
2559 out.push('\n');
2560 }
2561 out.push_str(&format!("object-dir: {}\n", objects_dir.display()));
2562 Ok(out)
2563}
2564
2565#[derive(Debug, Clone)]
2571pub struct MidxReuseTables {
2572 pub oids: Vec<ObjectId>,
2574 pub pack_and_offset: Vec<(u32, u64)>,
2576 pub rid_order: Vec<u32>,
2578 pub oid_idx_to_rank: Vec<u32>,
2580}
2581
2582pub fn load_midx_reuse_tables(objects_dir: &Path) -> Result<Option<MidxReuseTables>> {
2586 let pack_dir = objects_dir.join("pack");
2587 let Some(path) = resolve_tip_midx_path(&pack_dir) else {
2588 return Ok(None);
2589 };
2590 load_midx_reuse_tables_from_path(&path)
2591}
2592
2593pub fn load_midx_reuse_tables_from_path(midx_path: &Path) -> Result<Option<MidxReuseTables>> {
2595 let data = fs::read(midx_path).map_err(Error::Io)?;
2596 let hash_len = midx_hash_len(&data);
2597 let (_, hdr_end, _) = parse_midx_header(&data)?;
2598 let (oidl_off, oid_l_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2599 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2600 if oid_l_len % hash_len != 0 || ooff_len != oid_l_len / hash_len * 8 {
2601 return Err(Error::CorruptObject(
2602 "MIDX OID / offset chunk size mismatch".to_owned(),
2603 ));
2604 }
2605 let num_objects = oid_l_len / hash_len;
2606 if num_objects == 0 {
2607 return Ok(None);
2608 }
2609
2610 let rid_order =
2611 if let Ok((ridx_off, ridx_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_REVINDEX) {
2612 if ridx_len != num_objects.saturating_mul(4) {
2613 return Err(Error::CorruptObject(
2614 "MIDX reverse index length does not match object count".to_owned(),
2615 ));
2616 }
2617 let mut order = Vec::with_capacity(num_objects);
2618 for i in 0..num_objects {
2619 let base = ridx_off + i * 4;
2620 order.push(read_be_u32(&data, base)?);
2621 }
2622 order
2623 } else {
2624 let hash_hex = hex::encode(&data[data.len().saturating_sub(hash_len)..]);
2625 let rev_path = midx_path
2626 .parent()
2627 .map(|p| p.join(format!("multi-pack-index-{hash_hex}.rev")))
2628 .unwrap_or_else(|| PathBuf::from(format!("multi-pack-index-{hash_hex}.rev")));
2629 let rev_data = match fs::read(&rev_path) {
2630 Ok(d) => d,
2631 Err(_) => return Ok(None),
2632 };
2633 crate::pack_rev::try_rev_positions_in_pack_order(&rev_data, num_objects).ok_or_else(
2634 || {
2635 Error::CorruptObject(
2636 "MIDX standalone .rev sidecar is invalid or missing".to_owned(),
2637 )
2638 },
2639 )?
2640 };
2641
2642 let mut oids = Vec::with_capacity(num_objects);
2643 for i in 0..num_objects {
2644 let base = oidl_off + i * hash_len;
2645 oids.push(ObjectId::from_bytes(&data[base..base + hash_len])?);
2646 }
2647
2648 let mut pack_and_offset = Vec::with_capacity(num_objects);
2649 for i in 0..num_objects {
2650 let ob = ooff_off + i * 8;
2651 let pack_id = read_be_u32(&data, ob)?;
2652 let off32 = read_be_u32(&data, ob + 4)?;
2653 pack_and_offset.push((pack_id, u64::from(off32)));
2654 }
2655
2656 let mut oid_idx_to_rank = vec![0u32; num_objects];
2657 for (rank, &oid_idx) in rid_order.iter().enumerate() {
2658 let idx = usize::try_from(oid_idx)
2659 .map_err(|_| Error::CorruptObject("bad MIDX reverse index entry".to_owned()))?;
2660 if idx >= num_objects {
2661 return Err(Error::CorruptObject(
2662 "MIDX reverse index out of range".to_owned(),
2663 ));
2664 }
2665 oid_idx_to_rank[idx] = u32::try_from(rank)
2666 .map_err(|_| Error::CorruptObject("too many MIDX objects".to_owned()))?;
2667 }
2668
2669 Ok(Some(MidxReuseTables {
2670 oids,
2671 pack_and_offset,
2672 rid_order,
2673 oid_idx_to_rank,
2674 }))
2675}
2676
2677pub(crate) fn midx_reuse_tables_from_bytes(
2679 data: &[u8],
2680 rid_order: &[u32],
2681) -> Result<MidxReuseTables> {
2682 let hash_len = midx_hash_len(data);
2683 let (_, hdr_end, _) = parse_midx_header(data)?;
2684 let (oidl_off, oid_l_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
2685 let (ooff_off, ooff_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2686 if oid_l_len % hash_len != 0 || ooff_len != oid_l_len / hash_len * 8 {
2687 return Err(Error::CorruptObject(
2688 "MIDX OID / offset chunk size mismatch".to_owned(),
2689 ));
2690 }
2691 let num_objects = oid_l_len / hash_len;
2692 if rid_order.len() != num_objects {
2693 return Err(Error::CorruptObject(
2694 "MIDX reverse index length does not match object count".to_owned(),
2695 ));
2696 }
2697
2698 let mut oids = Vec::with_capacity(num_objects);
2699 for i in 0..num_objects {
2700 let base = oidl_off + i * hash_len;
2701 oids.push(ObjectId::from_bytes(&data[base..base + hash_len])?);
2702 }
2703
2704 let mut pack_and_offset = Vec::with_capacity(num_objects);
2705 for i in 0..num_objects {
2706 let ob = ooff_off + i * 8;
2707 let pack_id = read_be_u32(data, ob)?;
2708 let off32 = read_be_u32(data, ob + 4)?;
2709 pack_and_offset.push((pack_id, u64::from(off32)));
2710 }
2711
2712 let mut oid_idx_to_rank = vec![0u32; num_objects];
2713 for (rank, &oid_idx) in rid_order.iter().enumerate() {
2714 let idx = usize::try_from(oid_idx)
2715 .map_err(|_| Error::CorruptObject("bad MIDX reverse index entry".to_owned()))?;
2716 if idx >= num_objects {
2717 return Err(Error::CorruptObject(
2718 "MIDX reverse index out of range".to_owned(),
2719 ));
2720 }
2721 oid_idx_to_rank[idx] = u32::try_from(rank)
2722 .map_err(|_| Error::CorruptObject("too many MIDX objects".to_owned()))?;
2723 }
2724
2725 Ok(MidxReuseTables {
2726 oids,
2727 pack_and_offset,
2728 rid_order: rid_order.to_vec(),
2729 oid_idx_to_rank,
2730 })
2731}
2732
2733impl MidxReuseTables {
2734 #[must_use]
2736 pub fn global_bitmap_bit(&self, oid: &ObjectId) -> Option<u32> {
2737 let oid_idx = self.oids.binary_search(oid).ok()?;
2738 Some(self.oid_idx_to_rank[oid_idx])
2739 }
2740
2741 #[must_use]
2746 pub fn canonical_pack(&self, oid: &ObjectId) -> Option<u32> {
2747 let oid_idx = self.oids.binary_search(oid).ok()?;
2748 Some(self.pack_and_offset[oid_idx].0)
2749 }
2750}
2751
2752#[derive(Debug, Clone, Copy)]
2754pub struct MidxBtmpPackRange {
2755 pub pack_id: u32,
2757 pub bitmap_pos: u32,
2759 pub bitmap_nr: u32,
2761}
2762
2763pub fn read_midx_btmp_ranges(objects_dir: &Path) -> Result<Vec<MidxBtmpPackRange>> {
2767 let pack_dir = objects_dir.join("pack");
2768 let Some(path) = resolve_tip_midx_path(&pack_dir) else {
2769 return Ok(Vec::new());
2770 };
2771 let data = fs::read(&path).map_err(Error::Io)?;
2772 let (_, hdr_end, _) = parse_midx_header(&data)?;
2773 let Ok((btmp_off, btmp_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_BITMAPPED_PACKS) else {
2774 return Ok(Vec::new());
2775 };
2776 if btmp_len == 0 || btmp_len % 8 != 0 {
2777 return Err(Error::CorruptObject(
2778 "invalid MIDX BTMP chunk length".to_owned(),
2779 ));
2780 }
2781 let num_packs = read_be_u32(&data, 8)?;
2782 let n_entries = btmp_len / 8;
2783 if u32::try_from(n_entries).ok() != Some(num_packs) {
2784 return Err(Error::CorruptObject(
2785 "MIDX BTMP entry count does not match num_packs".to_owned(),
2786 ));
2787 }
2788 let mut out = Vec::with_capacity(n_entries);
2789 for i in 0..n_entries {
2790 let base = btmp_off + i * 8;
2791 let bitmap_pos = read_be_u32(&data, base)?;
2792 let bitmap_nr = read_be_u32(&data, base + 4)?;
2793 out.push(MidxBtmpPackRange {
2794 pack_id: u32::try_from(i)
2795 .map_err(|_| Error::CorruptObject("too many packs in MIDX BTMP".to_owned()))?,
2796 bitmap_pos,
2797 bitmap_nr,
2798 });
2799 }
2800 Ok(out)
2801}
2802
2803pub fn format_midx_bitmapped_packs(objects_dir: &Path) -> Result<String> {
2808 let pack_dir = objects_dir.join("pack");
2809 let path = resolve_tip_midx_path(&pack_dir)
2810 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2811 let data = fs::read(&path).map_err(Error::Io)?;
2812 let (_, hdr_end, _) = parse_midx_header(&data)?;
2813 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2814 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2815 let Ok((btmp_off, btmp_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_BITMAPPED_PACKS) else {
2816 return Err(Error::CorruptObject(
2817 "MIDX does not contain the BTMP chunk".to_owned(),
2818 ));
2819 };
2820 let n_entries = btmp_len / 8;
2821 let mut out = String::new();
2822 for i in 0..n_entries {
2823 let base = btmp_off + i * 8;
2824 let bitmap_pos = read_be_u32(&data, base)?;
2825 let bitmap_nr = read_be_u32(&data, base + 4)?;
2826 let idx_name = names.get(i).ok_or_else(|| {
2827 Error::CorruptObject("BTMP entry has no corresponding pack name".to_owned())
2828 })?;
2829 let stem = idx_name.strip_suffix(".idx").unwrap_or(idx_name);
2830 out.push_str(&format!("{stem}.pack\n"));
2831 out.push_str(&format!(" bitmap_pos: {bitmap_pos}\n"));
2832 out.push_str(&format!(" bitmap_nr: {bitmap_nr}\n"));
2833 }
2834 Ok(out)
2835}
2836
2837pub fn midx_lookup_pack_and_offset_opt(
2841 objects_dir: &Path,
2842 oid: &ObjectId,
2843) -> Result<Option<(u32, u64)>> {
2844 let Some(chain) = prepared_midx_chain(objects_dir)? else {
2845 return Ok(None);
2846 };
2847 Ok(chain.lookup_pack_and_offset(oid))
2848}
2849
2850pub fn midx_lookup_pack_and_offset(objects_dir: &Path, oid: &ObjectId) -> Result<(u32, u64)> {
2852 let Some(chain) = prepared_midx_chain(objects_dir)? else {
2853 return Err(Error::CorruptObject("no multi-pack-index found".to_owned()));
2854 };
2855 chain.lookup_pack_and_offset(oid).ok_or_else(|| {
2856 Error::CorruptObject(format!("object {} not in multi-pack-index", oid.to_hex()))
2857 })
2858}
2859
2860pub fn midx_oid_listed_in_tip(objects_dir: &Path, oid: &ObjectId) -> Result<Option<bool>> {
2864 let Some(chain) = prepared_midx_chain(objects_dir)? else {
2865 return Ok(None);
2866 };
2867 Ok(Some(chain.oid_listed_in_tip(oid)))
2868}
2869
2870struct MidxReadView {
2872 oidf_off: usize,
2873 oidl_off: usize,
2874 ooff_off: usize,
2875 loff: Option<(usize, usize)>,
2876 num_objects: usize,
2877 pack_names: Vec<String>,
2878}
2879
2880enum MidxLoadResult {
2881 Ok(MidxReadView),
2882 Skip,
2884}
2885
2886fn midx_load_for_read(
2890 data: &PackData,
2891 expected_hash_version: u8,
2892 diagnostics: &dyn DiagnosticSink,
2893) -> Result<MidxLoadResult> {
2894 if data.len() < MIDX_HEADER_SIZE + 20 {
2895 diagnostics.warn(Warning::MidxChunkTableError {
2896 detail: "multi-pack-index file is too small".to_owned(),
2897 });
2898 return Ok(MidxLoadResult::Skip);
2899 }
2900 let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
2901 if sig != MIDX_SIGNATURE {
2902 return Err(MidxError::SignatureMismatch {
2903 found: sig,
2904 expected: MIDX_SIGNATURE,
2905 }
2906 .into());
2907 }
2908 let version = data[4];
2909 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
2910 return Err(MidxError::VersionNotRecognized { version }.into());
2911 }
2912 let hash_version = data[5];
2913 if hash_version != expected_hash_version {
2914 return Err(MidxError::HashVersionMismatch {
2915 found: hash_version,
2916 expected: expected_hash_version,
2917 }
2918 .into());
2919 }
2920 let hash_len = if hash_version == 2 { 32usize } else { 20usize };
2921 let num_packs = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
2922
2923 let mut toc_errors: Vec<String> = Vec::new();
2924 let chunks = match parse_midx_toc(data, hash_len, &mut toc_errors) {
2925 Ok(c) => c,
2926 Err(fatal) => {
2927 if !toc_errors.is_empty() {
2928 for e in toc_errors {
2929 diagnostics.warn(Warning::MidxChunkTableError { detail: e });
2930 }
2931 return Ok(MidxLoadResult::Skip);
2932 }
2933 return Err(MidxError::from(fatal).into());
2934 }
2935 };
2936
2937 let Some((pn_off, pn_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_PACKNAMES)
2938 else {
2939 return Err(MidxError::RequiredPackNameChunkMissing.into());
2940 };
2941
2942 let Some((oidf_off, oidf_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDFANOUT)
2943 else {
2944 return Err(MidxError::RequiredOidFanoutChunkMissing.into());
2945 };
2946 if oidf_len != 256 * 4 {
2947 return Err(MidxError::OidFanoutWrongSize.into());
2948 }
2949 let fanout = |i: usize| -> u32 {
2950 let b = oidf_off + i * 4;
2951 u32::from_be_bytes([data[b], data[b + 1], data[b + 2], data[b + 3]])
2952 };
2953 let num_objects = fanout(255) as usize;
2954
2955 let Some((oidl_off, oidl_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDLOOKUP)
2956 else {
2957 return Err(MidxError::RequiredOidLookupChunkMissing.into());
2958 };
2959 if oidl_len != hash_len * num_objects {
2960 return Err(MidxError::OidLookupWrongSize.into());
2961 }
2962
2963 let Some((ooff_off, ooff_len)) =
2964 toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OBJECTOFFSETS)
2965 else {
2966 return Err(MidxError::RequiredObjectOffsetsChunkMissing.into());
2967 };
2968 if ooff_len != num_objects * 8 {
2969 return Err(MidxError::ObjectOffsetsWrongSize.into());
2970 }
2971
2972 let loff = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_LARGEOFFSETS);
2973
2974 if let Some((_, rlen)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_REVINDEX) {
2975 if rlen != num_objects * 4 {
2976 diagnostics.warn(Warning::MidxRevIndexWrongSize);
2977 diagnostics.warn(Warning::MidxBitmapMissingReverseIndex);
2978 }
2979 }
2980
2981 let mut pack_names: Vec<String> = Vec::with_capacity(num_packs);
2982 let blob = &data[pn_off..pn_off + pn_len];
2983 let mut start = 0usize;
2984 for _ in 0..num_packs {
2985 let Some(rel) = blob[start..].iter().position(|&b| b == 0) else {
2986 return Err(MidxError::PackNameChunkTooShort.into());
2987 };
2988 let name = match std::str::from_utf8(&blob[start..start + rel]) {
2989 Ok(s) => s.to_string(),
2990 Err(_) => return Err(MidxError::PackNameChunkTooShort.into()),
2991 };
2992 pack_names.push(name);
2993 start += rel + 1;
2994 }
2995
2996 let payload_end = data.len() - hash_len;
2997 let algo = if hash_version == HASH_VERSION_SHA256 {
2998 HashAlgo::Sha256
2999 } else {
3000 HashAlgo::Sha1
3001 };
3002 let budget = crate::pack_map::ResidentBudget::new(data, 2 * 1024 * 1024);
3003 let valid = budget.checksum(algo, &data[..payload_end]) == data[payload_end..];
3004 data.release_resident();
3006 if !valid {
3007 diagnostics.warn(Warning::MidxChecksumMismatch);
3008 return Ok(MidxLoadResult::Skip);
3009 }
3010
3011 Ok(MidxLoadResult::Ok(MidxReadView {
3012 oidf_off,
3013 oidl_off,
3014 ooff_off,
3015 loff,
3016 num_objects,
3017 pack_names,
3018 }))
3019}
3020
3021pub fn validate_midx_referenced_packs(objects_dir: &Path) {
3029 validate_midx_referenced_packs_with_diagnostics(objects_dir, &NullDiagnostics);
3030}
3031
3032pub fn validate_midx_referenced_packs_with_diagnostics(
3034 objects_dir: &Path,
3035 diagnostics: &dyn DiagnosticSink,
3036) {
3037 if let Ok(Some(chain)) = midx_cache::prepared_chain(objects_dir, diagnostics) {
3038 chain.ensure_packs_validated(diagnostics);
3039 }
3040}
3041
3042pub fn try_read_object_via_midx(
3047 objects_dir: &Path,
3048 oid: &ObjectId,
3049) -> Result<Option<crate::objects::Object>> {
3050 try_read_object_via_midx_with_diagnostics(objects_dir, oid, &NullDiagnostics)
3051}
3052
3053pub fn try_read_object_via_midx_with_diagnostics(
3055 objects_dir: &Path,
3056 oid: &ObjectId,
3057 diagnostics: &dyn DiagnosticSink,
3058) -> Result<Option<crate::objects::Object>> {
3059 let Some(chain) = midx_cache::prepared_chain(objects_dir, diagnostics)? else {
3060 return Ok(None);
3061 };
3062 chain.try_read_object(oid, diagnostics)
3063}
3064
3065pub fn try_read_info_via_midx(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
3067 try_read_info_via_midx_with_diagnostics(objects_dir, oid, &NullDiagnostics)
3068}
3069
3070pub fn try_read_info_via_midx_with_diagnostics(
3072 objects_dir: &Path,
3073 oid: &ObjectId,
3074 diagnostics: &dyn DiagnosticSink,
3075) -> Result<Option<ObjectInfo>> {
3076 let Some(chain) = midx_cache::prepared_chain(objects_dir, diagnostics)? else {
3077 return Ok(None);
3078 };
3079 chain.try_read_info(oid, diagnostics)
3080}
3081
3082pub fn read_midx_preferred_idx_name(objects_dir: &Path) -> Result<String> {
3084 let pack_dir = objects_dir.join("pack");
3085 let path = resolve_tip_midx_path(&pack_dir)
3086 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
3087 let data = fs::read(&path).map_err(Error::Io)?;
3088 let (_, hdr_end, _) = parse_midx_header(&data)?;
3089 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
3090 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
3091 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
3092 let (ridx_off, ridx_len) = match find_chunk(&data, hdr_end, MIDX_CHUNKID_REVINDEX) {
3098 Ok(r) => r,
3099 Err(_) => {
3100 return Err(Error::CorruptObject(
3101 "could not determine MIDX preferred pack".to_owned(),
3102 ));
3103 }
3104 };
3105
3106 if ridx_len < 4 || ooff_len < 8 {
3107 return Err(Error::CorruptObject("truncated MIDX RIDX/OOFF".to_owned()));
3108 }
3109 let first_oid_idx = read_be_u32(&data, ridx_off)? as usize;
3110 let entry_base = ooff_off + first_oid_idx * 8;
3111 if entry_base + 8 > data.len() || entry_base + 8 > ooff_off + ooff_len {
3112 return Err(Error::CorruptObject(
3113 "bad MIDX object-offsets index".to_owned(),
3114 ));
3115 }
3116 let pack_id = read_be_u32(&data, entry_base)?;
3117 let idx = usize::try_from(pack_id)
3118 .map_err(|_| Error::CorruptObject("pack id overflow in multi-pack-index".to_owned()))?;
3119 names
3120 .get(idx)
3121 .cloned()
3122 .ok_or_else(|| Error::CorruptObject("preferred pack id out of range".to_owned()))
3123}
3124
3125pub fn clear_pack_midx_state(pack_dir: &Path) -> Result<()> {
3132 let _ = fs::remove_file(pack_dir.join("multi-pack-index"));
3133 scrub_root_midx_sidecars_except(pack_dir, None)?;
3134 let midx_d = midx_d_dir(pack_dir);
3135 if midx_d.exists() {
3136 let _ = fs::remove_dir_all(&midx_d);
3137 }
3138 midx_cache::evict_pack_dir(pack_dir);
3139 Ok(())
3140}
3141
3142pub fn write_multi_pack_index(pack_dir: &Path) -> Result<WriteMultiPackIndexResult> {
3144 write_multi_pack_index_with_options(pack_dir, &WriteMultiPackIndexOptions::default())
3145}
3146
3147fn repository_for_pack_dir(pack_dir: &Path) -> Result<Repository> {
3148 let objects = pack_dir.parent().ok_or_else(|| {
3149 Error::Io(std::io::Error::new(
3150 std::io::ErrorKind::NotFound,
3151 "pack directory has no parent",
3152 ))
3153 })?;
3154 let git_dir = objects.parent().ok_or_else(|| {
3155 Error::Io(std::io::Error::new(
3156 std::io::ErrorKind::NotFound,
3157 "objects directory has no parent",
3158 ))
3159 })?;
3160 Repository::open(git_dir, &RepositoryOptions::empty())
3161}
3162
3163fn midx_bitmap_sidecar_matches(path: &Path, midx_checksum: &[u8]) -> bool {
3164 let Ok(data) = fs::read(path) else {
3165 return false;
3166 };
3167 let header_end = 4 + 2 + 2 + 4;
3168 if data.len() < header_end + midx_checksum.len() {
3169 return false;
3170 }
3171 if &data[0..4] != b"BITM" {
3172 return false;
3173 }
3174 data[header_end..header_end + midx_checksum.len()] == *midx_checksum
3175}
3176
3177fn write_midx_reachability_bitmap(
3178 pack_dir: &Path,
3179 midx_bytes: &[u8],
3180 rid_order: &[u32],
3181 midx_checksum: &[u8],
3182 bitmap_path: &Path,
3183) -> Result<()> {
3184 write_midx_reachability_bitmap_with_options(
3185 pack_dir,
3186 midx_bytes,
3187 rid_order,
3188 midx_checksum,
3189 bitmap_path,
3190 &PackBitmapWriteOptions::default(),
3191 )
3192}
3193
3194fn write_midx_reachability_bitmap_with_options(
3195 pack_dir: &Path,
3196 midx_bytes: &[u8],
3197 rid_order: &[u32],
3198 midx_checksum: &[u8],
3199 bitmap_path: &Path,
3200 options: &PackBitmapWriteOptions,
3201) -> Result<()> {
3202 let repo = repository_for_pack_dir(pack_dir)?;
3203 MidxBitmapWriter::write_for_midx_layer(
3204 &repo,
3205 midx_bytes,
3206 rid_order,
3207 midx_checksum,
3208 bitmap_path,
3209 options,
3210 )
3211 .map_err(|e| MidxError::BitmapWriteFailed {
3212 detail: e.to_string(),
3213 })?;
3214 Ok(())
3215}
3216
3217pub fn write_multi_pack_index_with_options(
3219 pack_dir: &Path,
3220 opts: &WriteMultiPackIndexOptions,
3221) -> Result<WriteMultiPackIndexResult> {
3222 let version = midx_write_version(opts.version)?;
3223
3224 if opts.pack_names_subset_ordered.is_none() {
3227 if let Some(existing) = resolve_tip_midx_path(pack_dir) {
3228 if let Ok(bytes) = fs::read(&existing) {
3229 if midx_checksum_is_valid(&bytes) {
3230 if let Ok((_, existing_names)) = oids_and_packs_from_midx_data(&bytes) {
3234 for (i, name) in existing_names.iter().enumerate() {
3235 let stem = name.strip_suffix(".idx").unwrap_or(name);
3236 if !pack_dir.join(format!("{stem}.pack")).exists() {
3237 return Err(
3238 MidxError::ReferencedPackMissing { pack_index: i }.into()
3239 );
3240 }
3241 }
3242 }
3243 } else {
3244 midx_write_warn(opts, Warning::MidxIgnoringExistingChecksumMismatch);
3245 }
3246 }
3247 }
3248 }
3249
3250 let mut idx_names: Vec<String> = fs::read_dir(pack_dir)
3255 .map(|rd| {
3256 rd.filter_map(|e| e.ok())
3257 .filter_map(|e| {
3258 let name = e.file_name().to_string_lossy().to_string();
3259 let stem = name.strip_suffix(".idx")?;
3260 if pack_dir.join(format!("{stem}.pack")).exists() {
3261 Some(name)
3262 } else {
3263 None
3264 }
3265 })
3266 .collect()
3267 })
3268 .unwrap_or_default();
3269 idx_names.sort();
3270
3271 let idx_names: Vec<String> = if let Some(sub) = &opts.pack_names_subset_ordered {
3272 let mut out = Vec::new();
3273 for line in sub {
3274 let want = normalize_pack_idx_basename(line)?;
3275 if let Some(found) = idx_names.iter().find(|n| **n == want).cloned() {
3276 if !out.contains(&found) {
3277 out.push(found);
3278 }
3279 }
3280 }
3283 if version == MIDX_VERSION_V1 {
3285 out.sort();
3286 }
3287 out
3288 } else {
3289 idx_names
3290 };
3291
3292 let mut preferred_warned = false;
3295 if let Some(raw) = opts.preferred_pack_name.as_deref() {
3296 if opts.preferred_pack_idx.is_none()
3297 && !idx_names
3298 .iter()
3299 .any(|n| cmp_idx_or_pack_name(raw, n).is_eq())
3300 {
3301 midx_write_warn(
3302 opts,
3303 Warning::MidxUnknownPreferredPack {
3304 name: raw.to_owned(),
3305 },
3306 );
3307 preferred_warned = true;
3308 }
3309 }
3310
3311 if idx_names.is_empty() {
3312 return Err(MidxError::NoPacksToIndex.into());
3313 }
3314
3315 let (base_oids, base_pack_names) = if opts.incremental {
3316 collect_incremental_base(pack_dir)?
3317 } else {
3318 (HashSet::new(), HashSet::new())
3319 };
3320
3321 let layer_idx_names: Vec<String> = if opts.incremental {
3322 idx_names
3323 .iter()
3324 .filter(|n| {
3325 !base_pack_names
3326 .iter()
3327 .any(|bp| pack_names_match_layer(bp, n))
3328 })
3329 .cloned()
3330 .collect()
3331 } else {
3332 idx_names.clone()
3333 };
3334
3335 if opts.incremental && layer_idx_names.is_empty() {
3336 return Ok(WriteMultiPackIndexResult::default());
3337 }
3338
3339 let work_names = if opts.incremental {
3340 &layer_idx_names[..]
3341 } else {
3342 &idx_names[..]
3343 };
3344
3345 let mut preferred_idx = opts.preferred_pack_idx.map(|p| p as usize);
3346 if preferred_idx.is_none() && !preferred_warned {
3347 if let Some(raw) = opts.preferred_pack_name.as_deref() {
3348 preferred_idx = work_names
3350 .iter()
3351 .position(|n| cmp_idx_or_pack_name(raw, n).is_eq());
3352 }
3353 }
3354 if preferred_idx.is_none() && opts.write_bitmap && !work_names.is_empty() {
3355 preferred_idx = preferred_pack_index_by_mtime(pack_dir, work_names)?;
3356 }
3357 if let Some(p) = preferred_idx {
3358 if p >= work_names.len() {
3359 return Err(Error::CorruptObject(
3360 "preferred pack index out of range".to_owned(),
3361 ));
3362 }
3363 }
3364
3365 let mut indexes: Vec<PackIndex> = Vec::with_capacity(work_names.len());
3366 for name in work_names {
3367 let path = pack_dir.join(name);
3368 indexes.push(crate::pack::read_pack_index_no_verify(&path)?);
3371 }
3372
3373 if let Some(p) = preferred_idx {
3375 if indexes.get(p).map(PackIndex::len).unwrap_or(0) == 0 {
3376 let name = work_names.get(p).cloned().unwrap_or_default();
3377 let pack_name = name.strip_suffix(".idx").unwrap_or(&name);
3378 return Err(MidxError::PreferredPackEmpty {
3379 pack: format!("{pack_name}.pack"),
3380 }
3381 .into());
3382 }
3383 }
3384
3385 let pack_mtimes_layer: Vec<std::time::SystemTime> =
3386 indexes.iter().map(pack_mtime_for_midx).collect();
3387 let preferred_u32 = preferred_idx.map(|p| p as u32);
3388 let select_hash_len = if repo_midx_hash_version(pack_dir) == 2 {
3389 32
3390 } else {
3391 20
3392 };
3393
3394 let mut best: HashMap<ObjectId, MidxEntry> = HashMap::new();
3395 for (pack_id, idx) in indexes.iter().enumerate() {
3396 let pack_id = u32::try_from(pack_id).map_err(|_| {
3397 Error::CorruptObject("too many pack files for multi-pack-index".to_owned())
3398 })?;
3399 let mtime = pack_mtimes_layer[pack_id as usize];
3400 for e in idx.iter() {
3401 if e.oid().len() != select_hash_len {
3402 continue;
3403 }
3404 let Ok(oid) = ObjectId::from_bytes(e.oid()) else {
3405 continue;
3406 };
3407 if opts.incremental && base_oids.contains(&oid) {
3408 continue;
3409 }
3410 let offset = e.offset()?;
3411 let cand = MidxEntry {
3412 oid,
3413 pack_id,
3414 offset,
3415 pack_mtime: mtime,
3416 };
3417 match best.get(&oid) {
3418 None => {
3419 best.insert(oid, cand);
3420 }
3421 Some(cur) => {
3422 if midx_pick_better_entry(cur, pack_id, offset, mtime, preferred_u32) {
3423 best.insert(oid, cand);
3424 }
3425 }
3426 }
3427 }
3428 }
3429
3430 let write_bitmap_chunks = opts.write_bitmap && (!opts.incremental || !best.is_empty());
3431
3432 let omit_embedded_ridx = opts.write_rev_sidecar;
3433 let exclude = if opts.incremental && !base_oids.is_empty() {
3437 Some(&base_oids)
3438 } else {
3439 None
3440 };
3441 let (out, bitmap_rid_order) = build_midx_bytes_filtered(
3442 work_names,
3443 &indexes,
3444 preferred_idx,
3445 write_bitmap_chunks,
3446 omit_embedded_ridx,
3447 version,
3448 repo_midx_hash_version(pack_dir),
3449 exclude,
3450 )?;
3451
3452 let repo_algo = repo_hash_algo_for_pack_dir(pack_dir);
3453 let hash_len = repo_algo.len();
3454 let hash = &out[out.len() - hash_len..];
3455 let hash_hex = hex::encode(hash);
3456 let hash_arr: Vec<u8> = hash.to_vec();
3457
3458 let bitmap_outcome = if opts.write_bitmap {
3459 if opts.incremental {
3460 MidxBitmapWriteOutcome::SkippedIncrementalLayer
3461 } else {
3462 MidxBitmapWriteOutcome::Written
3463 }
3464 } else {
3465 MidxBitmapWriteOutcome::NotRequested
3466 };
3467
3468 if opts.incremental {
3469 let root_midx = pack_dir.join("multi-pack-index");
3470 let chain_path = chain_file_path(pack_dir);
3471 let chain_existed = chain_path.exists();
3472
3473 let mut chain = if root_midx.exists() && !chain_existed {
3474 let root_hex = midx_checksum_hex_from_path(&root_midx)?;
3475 link_root_midx_into_chain(pack_dir, &root_hex)?;
3476 vec![root_hex]
3477 } else {
3478 read_chain_layer_hashes(pack_dir).unwrap_or_default()
3479 };
3480
3481 chain.push(hash_hex.clone());
3482
3483 let midx_d = midx_d_dir(pack_dir);
3484 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
3485
3486 let layer_path = midx_d.join(format!("multi-pack-index-{hash_hex}.midx"));
3487 fs::write(&layer_path, &out).map_err(Error::Io)?;
3488
3489 let mut chain_data = String::new();
3490 for h in &chain {
3491 chain_data.push_str(h);
3492 chain_data.push('\n');
3493 }
3494 fs::write(chain_file_path(pack_dir), chain_data.as_bytes()).map_err(Error::Io)?;
3495
3496 clear_stale_split_layers(pack_dir, &chain)?;
3497
3498 let _ = fs::remove_file(pack_dir.join("multi-pack-index"));
3499 scrub_root_midx_sidecars(pack_dir)?;
3500 if write_bitmap_chunks && opts.write_rev_sidecar {
3501 let full = hex::encode(hash);
3502 let rev_path = midx_d.join(format!("multi-pack-index-{full}.rev"));
3503 if let Some(order) = bitmap_rid_order.as_ref() {
3504 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3505 }
3506 }
3507 } else {
3508 let dest = pack_dir.join("multi-pack-index");
3514
3515 let bitmap_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.bitmap"));
3519 let bitmap_ok = !opts.write_bitmap || midx_bitmap_sidecar_matches(&bitmap_path, hash);
3520 if bitmap_ok && !chain_file_path(pack_dir).exists() {
3524 if let Ok(existing) = fs::read(&dest) {
3525 if existing == out {
3526 if opts.write_rev_sidecar {
3527 if let Some(order) = bitmap_rid_order.as_ref() {
3528 let rev_path =
3529 pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
3530 if !rev_path.exists() {
3531 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3532 }
3533 }
3534 }
3535 return Ok(WriteMultiPackIndexResult {
3536 bitmap: bitmap_outcome,
3537 });
3538 }
3539 }
3540 }
3541
3542 clear_incremental_midx_files(pack_dir)?;
3543
3544 fs::write(&dest, &out).map_err(Error::Io)?;
3545
3546 scrub_root_midx_sidecars_except(pack_dir, Some(&hash_hex))?;
3547
3548 if opts.write_bitmap {
3549 let rid = bitmap_rid_order.as_ref().ok_or_else(|| {
3550 Error::CorruptObject("MIDX bitmap write missing reverse index order".to_owned())
3551 })?;
3552 write_midx_reachability_bitmap(pack_dir, &out, rid, hash, &bitmap_path)?;
3553 }
3554 if opts.write_rev_sidecar {
3555 let rev_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
3556 let order = bitmap_rid_order.as_ref().ok_or_else(|| {
3557 Error::CorruptObject(
3558 "MIDX rev sidecar write missing reverse index order".to_owned(),
3559 )
3560 })?;
3561 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3562 }
3563 }
3564
3565 if opts.write_rev_sidecar {
3566 let hash_hex = hex::encode(hash);
3567 let rev_root = pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
3568 let rev_layer = midx_d_dir(pack_dir).join(format!("multi-pack-index-{hash_hex}.rev"));
3569 if !rev_root.is_file() && !rev_layer.is_file() {
3570 return Err(Error::CorruptObject(format!(
3571 "MIDX rev sidecar missing after write ({hash_hex})"
3572 )));
3573 }
3574 }
3575
3576 midx_cache::evict_pack_dir(pack_dir);
3577 Ok(WriteMultiPackIndexResult {
3578 bitmap: bitmap_outcome,
3579 })
3580}
3581
3582fn pack_names_match_layer(base_name: &str, disk_idx: &str) -> bool {
3583 if base_name == disk_idx {
3584 return true;
3585 }
3586 cmp_idx_or_pack_name(disk_idx, base_name).is_eq()
3587}
3588
3589#[derive(Debug)]
3592pub enum CompactError {
3593 NoChain,
3595 MissingEndpoint(String),
3598 IdenticalEndpoints,
3600 NotAncestor(String, String),
3603 V1Format,
3605 Other(String),
3607}
3608
3609impl std::fmt::Display for CompactError {
3610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3611 match self {
3612 CompactError::NoChain => write!(f, "no multi-pack-index chain to compact"),
3613 CompactError::MissingEndpoint(s) => write!(f, "could not find MIDX: {s}"),
3614 CompactError::IdenticalEndpoints => {
3615 write!(f, "MIDX compaction endpoints must be unique")
3616 }
3617 CompactError::NotAncestor(from, to) => {
3618 write!(f, "MIDX {from} must be an ancestor of {to}")
3619 }
3620 CompactError::V1Format => write!(f, "cannot perform MIDX compaction with v1 format"),
3621 CompactError::Other(s) => write!(f, "{s}"),
3622 }
3623 }
3624}
3625
3626impl From<Error> for CompactError {
3627 fn from(e: Error) -> Self {
3628 CompactError::Other(e.to_string())
3629 }
3630}
3631
3632fn collect_layer_oids(pack_dir: &Path, hashes: &[String]) -> Result<HashSet<ObjectId>> {
3635 let mut oids = HashSet::new();
3636 for h in hashes {
3637 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{h}.midx"));
3638 let data = load_midx_file(&p)?;
3639 let (layer_oids, _) = oids_and_packs_from_midx_data(&data)?;
3640 oids.extend(layer_oids);
3641 }
3642 Ok(oids)
3643}
3644
3645fn layer_pack_names(pack_dir: &Path, hash: &str) -> Result<Vec<String>> {
3647 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{hash}.midx"));
3648 let data = load_midx_file(&p)?;
3649 let (_, hdr_end, _) = parse_midx_header(&data)?;
3650 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
3651 parse_pack_names_blob(&data[pn_off..pn_off + pn_len])
3652}
3653
3654pub fn compact_multi_pack_index(
3668 pack_dir: &Path,
3669 from_arg: &str,
3670 to_arg: &str,
3671 write_bitmaps: bool,
3672 write_rev: bool,
3673 version: Option<u8>,
3674) -> std::result::Result<(), CompactError> {
3675 let version = resolve_write_version(version, MIDX_VERSION_V2)?;
3676 if version == MIDX_VERSION_V1 {
3677 return Err(CompactError::V1Format);
3678 }
3679
3680 let chain = read_chain_layer_hashes(pack_dir).map_err(|_| CompactError::NoChain)?;
3681 if chain.is_empty() {
3682 return Err(CompactError::NoChain);
3683 }
3684
3685 let from_hex = from_arg.to_ascii_lowercase();
3686 let to_hex = to_arg.to_ascii_lowercase();
3687
3688 let from_pos = chain.iter().position(|h| *h == from_hex);
3689 let to_pos = chain.iter().position(|h| *h == to_hex);
3690
3691 let Some(from_pos) = from_pos else {
3693 return Err(CompactError::MissingEndpoint(from_arg.to_string()));
3694 };
3695 let Some(to_pos) = to_pos else {
3696 return Err(CompactError::MissingEndpoint(to_arg.to_string()));
3697 };
3698
3699 if from_pos == to_pos {
3700 return Err(CompactError::IdenticalEndpoints);
3701 }
3702 if from_pos > to_pos {
3706 return Err(CompactError::NotAncestor(
3707 from_arg.to_string(),
3708 to_arg.to_string(),
3709 ));
3710 }
3711
3712 let base_hashes = &chain[..from_pos];
3715 let merged_hashes = &chain[from_pos..=to_pos];
3716 let upper_hashes = &chain[to_pos + 1..];
3717
3718 let base_oids = collect_layer_oids(pack_dir, base_hashes)?;
3719
3720 let mut ordered_idx_names: Vec<String> = Vec::new();
3723 for h in merged_hashes {
3724 for name in layer_pack_names(pack_dir, h)? {
3725 if !ordered_idx_names.contains(&name) {
3726 ordered_idx_names.push(name);
3727 }
3728 }
3729 }
3730
3731 if ordered_idx_names.is_empty() {
3732 return Err(CompactError::Other(
3733 "no packs found in compaction range".to_owned(),
3734 ));
3735 }
3736
3737 let mut indexes: Vec<PackIndex> = Vec::with_capacity(ordered_idx_names.len());
3739 for name in &ordered_idx_names {
3740 let path = pack_dir.join(name);
3741 indexes.push(crate::pack::read_pack_index_no_verify(&path)?);
3742 }
3743
3744 let preferred_idx = if write_bitmaps { Some(0usize) } else { None };
3747
3748 let exclude = if base_oids.is_empty() {
3749 None
3750 } else {
3751 Some(&base_oids)
3752 };
3753
3754 let (out, bitmap_rid_order) = build_midx_bytes_filtered(
3755 &ordered_idx_names,
3756 &indexes,
3757 preferred_idx,
3758 write_bitmaps,
3759 write_rev,
3760 version,
3761 repo_midx_hash_version(pack_dir),
3762 exclude,
3763 )?;
3764
3765 let repo_algo = repo_hash_algo_for_pack_dir(pack_dir);
3766 let hash_len = repo_algo.len();
3767 let hash = &out[out.len() - hash_len..];
3768 let hash_hex = hex::encode(hash);
3769 let hash_arr: Vec<u8> = hash.to_vec();
3770
3771 let midx_d = midx_d_dir(pack_dir);
3772 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
3773
3774 let layer_path = midx_d.join(format!("multi-pack-index-{hash_hex}.midx"));
3775 fs::write(&layer_path, &out).map_err(Error::Io)?;
3776
3777 let mut new_chain: Vec<String> = Vec::new();
3779 new_chain.extend(base_hashes.iter().cloned());
3780 new_chain.push(hash_hex.clone());
3781 new_chain.extend(upper_hashes.iter().cloned());
3782
3783 let mut chain_data = String::new();
3784 for h in &new_chain {
3785 chain_data.push_str(h);
3786 chain_data.push('\n');
3787 }
3788 fs::write(chain_file_path(pack_dir), chain_data.as_bytes()).map_err(Error::Io)?;
3789
3790 if write_bitmaps {
3791 if write_rev {
3792 let rev_path = midx_d.join(format!("multi-pack-index-{hash_hex}.rev"));
3793 if let Some(order) = bitmap_rid_order.as_ref() {
3794 write_midx_rev_sidecar(&rev_path, order, &hash_arr, repo_algo)?;
3795 }
3796 }
3797 let bitmap_path = midx_d.join(format!("multi-pack-index-{hash_hex}.bitmap"));
3798 let rid = bitmap_rid_order.as_ref().ok_or_else(|| {
3799 CompactError::Other("MIDX bitmap write missing reverse index order".to_owned())
3800 })?;
3801 write_midx_reachability_bitmap_with_options(
3802 pack_dir,
3803 &out,
3804 rid,
3805 hash,
3806 &bitmap_path,
3807 &PackBitmapWriteOptions {
3808 verify_reachability_closure: false,
3809 ..PackBitmapWriteOptions::default()
3810 },
3811 )
3812 .map_err(|e| CompactError::Other(e.to_string()))?;
3813 }
3814
3815 clear_stale_split_layers(pack_dir, &new_chain)?;
3817
3818 midx_cache::evict_pack_dir(pack_dir);
3819 Ok(())
3820}
3821
3822fn scrub_root_midx_sidecars(pack_dir: &Path) -> Result<()> {
3823 scrub_root_midx_sidecars_except(pack_dir, None)
3824}
3825
3826fn scrub_root_midx_sidecars_except(pack_dir: &Path, keep_hex: Option<&str>) -> Result<()> {
3827 let Ok(rd) = fs::read_dir(pack_dir) else {
3828 return Ok(());
3829 };
3830 for ent in rd {
3831 let ent = ent.map_err(Error::Io)?;
3832 let name = ent.file_name().to_string_lossy().to_string();
3833 let Some(rest) = name.strip_prefix("multi-pack-index-") else {
3834 continue;
3835 };
3836 if !(rest.ends_with(".bitmap") || rest.ends_with(".rev")) {
3837 continue;
3838 }
3839 let hash_part = rest
3840 .strip_suffix(".bitmap")
3841 .or_else(|| rest.strip_suffix(".rev"))
3842 .unwrap_or(rest);
3843 if keep_hex.is_some_and(|k| k == hash_part) {
3847 continue;
3848 }
3849 let _ = fs::remove_file(ent.path());
3850 }
3851 Ok(())
3852}
3853
3854#[cfg(test)]
3855mod tests {
3856 use super::{
3857 clear_pack_midx_state, resolve_tip_midx_path, try_read_object_via_midx,
3858 write_multi_pack_index,
3859 };
3860
3861 use crate::error::{Error, MidxError};
3862 use crate::hot_path_test_metrics::HotPathMetricsScope;
3863 use crate::objects::ObjectId;
3864 use crate::odb::Odb;
3865 use crate::pack::{clear_pack_cache, read_object_from_packs};
3866 use std::fs;
3867 use std::path::Path;
3868 use std::process::Command;
3869 use std::sync::Arc;
3870
3871 fn git(dir: &Path, args: &[&str]) {
3872 let out = Command::new("git")
3873 .current_dir(dir)
3874 .args(args)
3875 .env("GIT_AUTHOR_NAME", "T")
3876 .env("GIT_AUTHOR_EMAIL", "t@example.com")
3877 .env("GIT_COMMITTER_NAME", "T")
3878 .env("GIT_COMMITTER_EMAIL", "t@example.com")
3879 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3880 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3881 .output()
3882 .expect("run git");
3883 assert!(
3884 out.status.success(),
3885 "git {:?} failed: {}",
3886 args,
3887 String::from_utf8_lossy(&out.stderr)
3888 );
3889 }
3890
3891 fn git_available() -> bool {
3892 Command::new("git")
3893 .arg("--version")
3894 .output()
3895 .map(|o| o.status.success())
3896 .unwrap_or(false)
3897 }
3898
3899 pub(super) fn midx_fixture(with_midx: bool) -> Option<(tempfile::TempDir, ObjectId)> {
3901 if !git_available() {
3902 return None;
3903 }
3904 let tmp = tempfile::tempdir().ok()?;
3905 let dir = tmp.path();
3906 git(dir, &["init", "-q", "-b", "main"]);
3907 std::fs::write(dir.join("f.txt"), b"hello midx cache").ok()?;
3908 git(dir, &["add", "f.txt"]);
3909 git(dir, &["commit", "-q", "-m", "c"]);
3910 git(dir, &["repack", "-adf"]);
3911 let pack_dir = dir.join(".git/objects/pack");
3912 if with_midx {
3913 write_multi_pack_index(&pack_dir).ok()?;
3914 }
3915 let hex = Command::new("git")
3916 .current_dir(dir)
3917 .args(["rev-parse", "HEAD"])
3918 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3919 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3920 .output()
3921 .ok()?;
3922 if !hex.status.success() {
3923 return None;
3924 }
3925 let oid = ObjectId::from_hex(std::str::from_utf8(&hex.stdout).ok()?.trim()).ok()?;
3926 Some((tmp, oid))
3927 }
3928
3929 #[cfg_attr(
3930 not(feature = "full-tests"),
3931 ignore = "starts system git or is heavy; run with --features full-tests"
3932 )]
3933 #[test]
3934 fn vanished_cached_tip_falls_back() {
3935 let Some((tmp, oid)) = midx_fixture(true) else {
3936 eprintln!("SKIP: git unavailable for MIDX fixture");
3937 return;
3938 };
3939 let git_dir = tmp.path().join(".git");
3940 let objects = git_dir.join("objects");
3941 let pack_dir = objects.join("pack");
3942 let midx_path = pack_dir.join("multi-pack-index");
3943 assert!(midx_path.is_file(), "expected root MIDX");
3944
3945 assert!(
3946 try_read_object_via_midx(&objects, &oid)
3947 .expect("prime midx read")
3948 .is_some(),
3949 "object should be reachable via MIDX"
3950 );
3951
3952 std::fs::remove_file(&midx_path).expect("simulate external MIDX removal");
3953
3954 assert!(
3957 try_read_object_via_midx(&objects, &oid)
3958 .expect("vanished tip must not fail reads")
3959 .is_some(),
3960 "cached prepared layer still serves until pack-dir evict"
3961 );
3962
3963 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
3964 let via_pack = odb.read(&oid).expect("pack fallback after vanished MIDX");
3965 assert_eq!(via_pack.kind, crate::objects::ObjectKind::Commit);
3966 }
3967
3968 #[cfg_attr(
3969 not(feature = "full-tests"),
3970 ignore = "starts system git or is heavy; run with --features full-tests"
3971 )]
3972 #[test]
3973 fn writer_evicts_tip_cache() {
3974 let Some((tmp, oid)) = midx_fixture(true) else {
3975 eprintln!("SKIP: git unavailable for MIDX fixture");
3976 return;
3977 };
3978 let objects = tmp.path().join(".git/objects");
3979 let pack_dir = objects.join("pack");
3980
3981 assert!(try_read_object_via_midx(&objects, &oid)
3982 .expect("prime caches")
3983 .is_some());
3984 assert!(resolve_tip_midx_path(&pack_dir).is_some());
3985
3986 clear_pack_midx_state(&pack_dir).expect("clear midx");
3987 assert!(resolve_tip_midx_path(&pack_dir).is_none());
3988
3989 assert!(try_read_object_via_midx(&objects, &oid)
3991 .expect("read after clear")
3992 .is_none());
3993
3994 write_multi_pack_index(&pack_dir).expect("rewrite midx");
3995 assert!(
3996 try_read_object_via_midx(&objects, &oid)
3997 .expect("read after rewrite")
3998 .is_some(),
3999 "fresh MIDX must be visible after in-process rewrite"
4000 );
4001 }
4002
4003 #[cfg_attr(
4004 not(feature = "full-tests"),
4005 ignore = "starts system git or is heavy; run with --features full-tests"
4006 )]
4007 #[test]
4008 fn midx_hot_path_does_not_stat_after_cache_warm() {
4009 let Some((tmp, oid)) = midx_fixture(true) else {
4010 eprintln!("SKIP: git unavailable for MIDX fixture");
4011 return;
4012 };
4013 let git_dir = tmp.path().join(".git");
4014 let objects = git_dir.join("objects");
4015 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4016 let metrics = odb.hot_path_metrics_arc();
4017 let _scope = HotPathMetricsScope::install(Arc::clone(&metrics));
4018 metrics.set_stamp_counting(true);
4019 let _ = odb.read(&oid).expect("prime");
4020 metrics.reset_midx_stamp_stats();
4021 for _ in 0..200 {
4022 let _ = odb.read(&oid).expect("repeat");
4023 }
4024 metrics.set_stamp_counting(false);
4025 assert_eq!(metrics.midx_stamp_stat_calls(), 0);
4026 }
4027
4028 #[test]
4029 fn decode_high_bit_offset_is_raw_without_loff_chunk() {
4030 use super::midx_cache::decode_midx_object_offset_for_test;
4031 assert_eq!(
4032 decode_midx_object_offset_for_test(&[], 0x8000_0010, None),
4033 Some(0x8000_0010)
4034 );
4035 }
4036
4037 #[cfg_attr(
4038 not(feature = "full-tests"),
4039 ignore = "starts system git or is heavy; run with --features full-tests"
4040 )]
4041 #[test]
4042 fn lookup_high_bit_raw_offset_written_without_loff() {
4043 use super::{
4044 clear_pack_midx_state, find_chunk, midx_lookup_pack_and_offset_opt, parse_midx_header,
4045 MIDX_CHUNKID_OBJECTOFFSETS, MIDX_CHUNKID_OIDLOOKUP,
4046 };
4047 let Some((tmp, oid)) = midx_fixture(true) else {
4048 eprintln!("SKIP: git unavailable for MIDX fixture");
4049 return;
4050 };
4051 let objects = tmp.path().join(".git/objects");
4052 let pack_dir = objects.join("pack");
4053 let midx_path = pack_dir.join("multi-pack-index");
4054 let mut data = std::fs::read(&midx_path).expect("midx bytes");
4055 let (_, hdr_end, _) = parse_midx_header(&data).expect("header");
4056 let (oidl_off, oidl_len) =
4057 find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP).expect("oidl");
4058 let (ooff_off, _) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS).expect("ooff");
4059 let hash_len = oid.as_bytes().len();
4060 let num_objects = oidl_len / hash_len;
4061 let needle = oid.as_bytes();
4062 let mut patched = false;
4063 for i in 0..num_objects {
4064 let start = oidl_off + i * hash_len;
4065 if data[start..start + hash_len] == *needle {
4066 let ob = ooff_off + i * 8 + 4;
4067 data[ob..ob + 4].copy_from_slice(&0x8000_0010u32.to_be_bytes());
4068 patched = true;
4069 break;
4070 }
4071 }
4072 assert!(patched, "oid row in MIDX");
4073 let hash_len = if data[5] == 2 { 32 } else { 20 };
4074 data.truncate(data.len() - hash_len);
4075 crate::pack_rev::append_hashfile_checksum(&mut data, hash_len);
4076 clear_pack_midx_state(&pack_dir).expect("evict midx cache");
4077 std::fs::write(&midx_path, &data).expect("write midx");
4078 crate::pack::clear_pack_cache();
4079 let (_pack_id, off) = midx_lookup_pack_and_offset_opt(&objects, &oid)
4080 .expect("lookup")
4081 .expect("listed");
4082 assert_eq!(off, 0x8000_0010);
4083 }
4084
4085 #[cfg_attr(
4086 not(feature = "full-tests"),
4087 ignore = "starts system git or is heavy; run with --features full-tests"
4088 )]
4089 #[test]
4090 fn tiny_midx_stub_skipped_without_aborting_reads() {
4091 let Some((tmp, oid)) = midx_fixture(true) else {
4092 eprintln!("SKIP: git unavailable for MIDX fixture");
4093 return;
4094 };
4095 let objects = tmp.path().join(".git/objects");
4096 let pack_dir = objects.join("pack");
4097 std::fs::write(pack_dir.join("multi-pack-index"), [0u8; 8]).expect("tiny stub midx");
4098 super::midx_cache::evict_pack_dir(&pack_dir);
4099 clear_pack_cache();
4100
4101 assert!(super::prepared_midx_chain(&objects)
4102 .expect("tiny MIDX must not fail chain build")
4103 .is_none());
4104 assert!(try_read_object_via_midx(&objects, &oid)
4105 .expect("tiny MIDX must not fail lookup")
4106 .is_none());
4107 read_object_from_packs(&objects, &oid).expect("pack fallback still works");
4108 }
4109
4110 #[cfg_attr(
4111 not(feature = "full-tests"),
4112 ignore = "starts system git or is heavy; run with --features full-tests"
4113 )]
4114 #[test]
4115 fn flipped_midx_checksum_warns_and_reads_from_pack() {
4116 let Some((tmp, oid)) = midx_fixture(true) else {
4117 eprintln!("SKIP: git unavailable for MIDX fixture");
4118 return;
4119 };
4120 let objects = tmp.path().join(".git/objects");
4121 let pack_dir = objects.join("pack");
4122 let midx_path = pack_dir.join("multi-pack-index");
4123 let mut bytes = fs::read(&midx_path).expect("read midx");
4124 let last = bytes.len() - 1;
4125 bytes[last] ^= 0xff;
4126 fs::write(&midx_path, &bytes).expect("corrupt midx checksum");
4127 super::midx_cache::evict_pack_dir(&pack_dir);
4128 clear_pack_cache();
4129
4130 let errs = super::verify_midx(&objects).expect_err("verify catches bad checksum");
4131 assert!(
4132 errs.iter().any(|e| e.contains("incorrect checksum")),
4133 "expected verify checksum error, got {errs:?}"
4134 );
4135 let via_midx =
4136 try_read_object_via_midx(&objects, &oid).expect("checksum mismatch must not fatal");
4137 assert!(
4138 via_midx.is_none(),
4139 "invalid MIDX checksum must be skipped for direct lookup"
4140 );
4141
4142 let git_dir = tmp.path().join(".git");
4143 let odb = Odb::new(&objects).with_config_git_dir(git_dir);
4144 let grit_obj = odb.read(&oid).expect("pack fallback read");
4145 let via_pack = read_object_from_packs(&objects, &oid).expect("pack read");
4146 assert_eq!(via_pack.data, grit_obj.data);
4147 }
4148
4149 #[cfg_attr(
4150 not(feature = "full-tests"),
4151 ignore = "starts system git or is heavy; run with --features full-tests"
4152 )]
4153 #[test]
4154 fn midx_shorter_than_min_len_skips_without_fatal_error() {
4155 let Some((tmp, oid)) = midx_fixture(true) else {
4156 eprintln!("SKIP: git unavailable for MIDX fixture");
4157 return;
4158 };
4159 let objects = tmp.path().join(".git/objects");
4160 let pack_dir = objects.join("pack");
4161 let midx_path = pack_dir.join("multi-pack-index");
4162 let mut bytes = fs::read(&midx_path).expect("read");
4163 bytes.truncate(super::MIDX_HEADER_SIZE + 10);
4164 fs::write(&midx_path, &bytes).expect("truncate");
4165 super::midx_cache::evict_pack_dir(&pack_dir);
4166 clear_pack_cache();
4167 assert!(
4168 try_read_object_via_midx(&objects, &oid)
4169 .expect("read")
4170 .is_none(),
4171 "truncated MIDX must be skipped for direct lookup"
4172 );
4173 read_object_from_packs(&objects, &oid).expect("pack fallback still works");
4174 }
4175
4176 #[cfg_attr(
4177 not(feature = "full-tests"),
4178 ignore = "starts system git or is heavy; run with --features full-tests"
4179 )]
4180 #[test]
4181 fn large_git_midx_layer_uses_mapped_backing() {
4182 let Some((tmp, _oid)) = midx_fixture(false) else {
4183 eprintln!("SKIP: git unavailable for MIDX fixture");
4184 return;
4185 };
4186 for i in 0..120 {
4187 std::fs::write(tmp.path().join(format!("bulk{i}.txt")), format!("{i}\n")).unwrap();
4188 git(tmp.path(), &["add", &format!("bulk{i}.txt")]);
4189 git(tmp.path(), &["commit", "-q", "-m", &format!("bulk{i}")]);
4190 }
4191 git(tmp.path(), &["repack", "-adf"]);
4192 git(tmp.path(), &["multi-pack-index", "write"]);
4193 let midx_path = tmp.path().join(".git/objects/pack/multi-pack-index");
4194 let len = std::fs::metadata(&midx_path).expect("meta").len();
4195 assert!(len > 8192, "midx size {len}");
4196 let storage =
4197 crate::pack_map::PackData::open_with_min_len(&midx_path, super::MIDX_MIN_FILE_LEN)
4198 .expect("open");
4199 assert!(storage.is_mapped_backing());
4200 }
4201
4202 #[cfg_attr(
4203 not(feature = "full-tests"),
4204 ignore = "starts system git or is heavy; run with --features full-tests"
4205 )]
4206 #[test]
4207 fn wrong_midx_hash_version_returns_typed_error() {
4208 let Some((tmp, oid)) = midx_fixture(true) else {
4209 eprintln!("SKIP: git unavailable for MIDX fixture");
4210 return;
4211 };
4212 let objects = tmp.path().join(".git/objects");
4213 let midx_path = objects.join("pack/multi-pack-index");
4214 let mut bytes = fs::read(&midx_path).expect("read midx");
4215 bytes[5] = 2;
4216 fs::write(&midx_path, &bytes).expect("write wrong hash version");
4217 super::midx_cache::evict_pack_dir(&objects.join("pack"));
4218
4219 let err = try_read_object_via_midx(&objects, &oid).expect_err("hash mismatch");
4220 assert!(
4221 matches!(
4222 err,
4223 Error::Midx(MidxError::HashVersionMismatch {
4224 found: 2,
4225 expected: 1
4226 })
4227 ),
4228 "unexpected error: {err:?}"
4229 );
4230 }
4231
4232 #[test]
4233 fn midx_load_error_display_roundtrip() {
4234 use crate::midx_error::MidxError;
4235 let err = MidxError::VersionNotRecognized { version: 99 };
4236 assert!(err.to_string().contains("not recognized"));
4237 }
4238
4239 #[test]
4240 fn compact_error_from_library_error() {
4241 use crate::error::Error;
4242 let err: super::CompactError = Error::CorruptObject("test".into()).into();
4243 assert!(err.to_string().contains("test"));
4244 }
4245
4246 #[cfg_attr(
4247 not(feature = "full-tests"),
4248 ignore = "starts system git or is heavy; run with --features full-tests"
4249 )]
4250 #[test]
4251 fn verify_midx_reports_missing_pack_name_chunk() {
4252 let Some((tmp, _oid)) = midx_fixture(true) else {
4253 eprintln!("SKIP: git unavailable for MIDX fixture");
4254 return;
4255 };
4256 let objects = tmp.path().join(".git/objects");
4257 let pack_dir = objects.join("pack");
4258 let path = pack_dir.join("multi-pack-index");
4259 let mut data = std::fs::read(&path).expect("midx");
4260 data[6] = 0;
4261 data.truncate(super::MIDX_HEADER_SIZE + 24);
4262 std::fs::write(&path, &data).expect("write broken midx");
4263 super::evict_midx_read_cache_for_pack_dir(&pack_dir);
4264 let errs = super::verify_midx(&objects).expect_err("verify");
4265 assert!(
4266 errs.iter().any(|e| {
4267 e.contains("pack-name") || e.contains("pack name") || e.contains("chunk")
4268 }),
4269 "{errs:?}"
4270 );
4271 }
4272
4273 #[cfg_attr(
4274 not(feature = "full-tests"),
4275 ignore = "starts system git or is heavy; run with --features full-tests"
4276 )]
4277 #[test]
4278 fn find_chunk_reports_missing_id() {
4279 let Some((tmp, _oid)) = midx_fixture(true) else {
4280 eprintln!("SKIP: git unavailable for MIDX fixture");
4281 return;
4282 };
4283 let midx_path = tmp.path().join(".git/objects/pack/multi-pack-index");
4284 let data = std::fs::read(midx_path).expect("midx");
4285 let (_, hdr_end, _) = super::parse_midx_header(&data).expect("header");
4286 let err = super::find_chunk(&data, hdr_end, 0xdead_beef).expect_err("missing chunk");
4287 assert!(err.to_string().contains("not found"));
4288 }
4289
4290 #[cfg_attr(
4291 not(feature = "full-tests"),
4292 ignore = "starts system git or is heavy; run with --features full-tests"
4293 )]
4294 #[test]
4295 fn read_after_in_process_pack_swap() {
4296 let Some((tmp, oid)) = midx_fixture(false) else {
4297 eprintln!("SKIP: git unavailable for MIDX fixture");
4298 return;
4299 };
4300 let objects = tmp.path().join(".git/objects");
4301 let before = read_object_from_packs(&objects, &oid).expect("read from pack");
4302
4303 std::fs::write(tmp.path().join("f2.txt"), b"replacement pack").expect("write");
4304 git(tmp.path(), &["add", "f2.txt"]);
4305 git(tmp.path(), &["commit", "-q", "-m", "c2"]);
4306 git(tmp.path(), &["repack", "-adf"]);
4307 clear_pack_cache();
4308
4309 let after = read_object_from_packs(&objects, &oid).expect("historical commit still packed");
4310 assert_eq!(before.kind, after.kind);
4311 assert_eq!(before.data, after.data);
4312 }
4313
4314 #[cfg_attr(
4315 not(feature = "full-tests"),
4316 ignore = "starts system git or is heavy; run with --features full-tests"
4317 )]
4318 #[test]
4319 fn git_midx_mapped_bytes_match_owned_prepare() {
4320 use super::midx_cache::PreparedMidxLayer;
4321 use crate::diagnostics::NullDiagnostics;
4322 use crate::pack_map::PackData;
4323
4324 let Some((tmp, oid)) = midx_fixture(false) else {
4325 eprintln!("SKIP: git unavailable for MIDX fixture");
4326 return;
4327 };
4328 for i in 0..120 {
4329 std::fs::write(tmp.path().join(format!("bulk{i}.txt")), format!("{i}\n")).unwrap();
4330 git(tmp.path(), &["add", &format!("bulk{i}.txt")]);
4331 git(tmp.path(), &["commit", "-q", "-m", &format!("bulk{i}")]);
4332 }
4333 git(tmp.path(), &["repack", "-adf"]);
4334 git(tmp.path(), &["multi-pack-index", "write"]);
4335
4336 let objects = tmp.path().join(".git/objects");
4337 let pack_dir = objects.join("pack");
4338 let midx_path = pack_dir.join("multi-pack-index");
4339 let midx_len = std::fs::metadata(&midx_path).expect("meta").len();
4340 assert!(midx_len > 8192, "fixture midx size {midx_len}");
4341 let hash_version = super::repo_midx_hash_version_for_objects_dir(&objects);
4342 let mapped =
4343 PackData::open_with_min_len(&midx_path, super::MIDX_MIN_FILE_LEN).expect("mmap");
4344 assert!(
4345 mapped.is_mapped_backing(),
4346 "expected mmap backing for {midx_len}-byte MIDX"
4347 );
4348 let owned = PackData::from_owned(std::fs::read(&midx_path).expect("read"));
4349 assert!(
4350 !owned.is_mapped_backing(),
4351 "explicit owned buffer must not use mmap"
4352 );
4353 let view_mapped =
4354 match super::midx_load_for_read(mapped.as_ref(), hash_version, &NullDiagnostics)
4355 .expect("load mapped")
4356 {
4357 super::MidxLoadResult::Ok(v) => v,
4358 super::MidxLoadResult::Skip => panic!("mapped midx skipped"),
4359 };
4360 let view_owned =
4361 match super::midx_load_for_read(owned.as_ref(), hash_version, &NullDiagnostics)
4362 .expect("load owned")
4363 {
4364 super::MidxLoadResult::Ok(v) => v,
4365 super::MidxLoadResult::Skip => panic!("owned midx skipped"),
4366 };
4367 let layer_mapped = PreparedMidxLayer::prepare(mapped, view_mapped, &pack_dir);
4368 let layer_owned = PreparedMidxLayer::prepare(owned, view_owned, &pack_dir);
4369 let loc = layer_mapped
4370 .lookup_pack_and_offset(&oid)
4371 .expect("mapped lookup");
4372 assert_eq!(
4373 loc,
4374 layer_owned
4375 .lookup_pack_and_offset(&oid)
4376 .expect("owned lookup")
4377 );
4378 let mapped_obj = layer_mapped
4379 .read_at(loc.0, loc.1)
4380 .expect("read mapped")
4381 .expect("object mapped");
4382 let owned_obj = layer_owned
4383 .read_at(loc.0, loc.1)
4384 .expect("read owned")
4385 .expect("object owned");
4386 assert_eq!(mapped_obj.data, owned_obj.data);
4387 }
4388
4389 fn config_from(text: &str) -> crate::config::ConfigSet {
4390 let file = crate::config::ConfigFile::parse(
4391 Path::new("test"),
4392 text,
4393 crate::config::ConfigScope::Local,
4394 )
4395 .expect("parse config");
4396 let mut set = crate::config::ConfigSet::new();
4397 set.merge(&file);
4398 set
4399 }
4400
4401 #[test]
4402 fn write_version_defaults_and_validation() {
4403 assert_eq!(super::resolve_write_version(None, 1).expect("default"), 1);
4404 assert_eq!(super::resolve_write_version(None, 2).expect("default"), 2);
4405 assert_eq!(super::resolve_write_version(Some(1), 2).expect("v1"), 1);
4406 assert_eq!(super::resolve_write_version(Some(2), 1).expect("v2"), 2);
4407 for bad in [0u8, 3, 255] {
4408 assert!(matches!(
4409 super::resolve_write_version(Some(bad), 1),
4410 Err(Error::Midx(MidxError::UnknownWriteVersion { version })) if version == i64::from(bad)
4411 ));
4412 }
4413 }
4414
4415 #[test]
4416 fn midx_version_config_parsing() {
4417 use crate::error::ConfigError;
4418 let read = |text: &str| super::midx_version_from_config(&config_from(text));
4419 assert_eq!(read("[core]\n\tbare = false\n").expect("unset"), None);
4420 assert_eq!(read("[midx]\n\tversion = 1\n").expect("v1"), Some(1));
4421 assert_eq!(read("[midx]\n\tversion = 2\n").expect("v2"), Some(2));
4422 assert_eq!(
4423 read("[midx]\n\tversion = 2\n[midx]\n\tversion = 1\n").expect("last wins"),
4424 Some(1)
4425 );
4426 for (text, version) in [
4427 ("[midx]\n\tversion = 3\n", 3),
4428 ("[midx]\n\tversion = 0\n", 0),
4429 ("[midx]\n\tversion = -1\n", -1),
4430 ("[midx]\n\tversion = 1k\n", 1024),
4431 ] {
4432 assert!(
4433 matches!(
4434 read(text),
4435 Err(Error::Midx(MidxError::UnknownWriteVersion { version: v })) if v == version
4436 ),
4437 "{text:?}"
4438 );
4439 }
4440 assert!(matches!(
4441 read("[midx]\n\tversion = two\n"),
4442 Err(Error::Config(ConfigError::BadNumericValue { ref key, .. })) if key == "midx.version"
4443 ));
4444 }
4445
4446 #[cfg_attr(
4447 not(feature = "full-tests"),
4448 ignore = "starts system git or is heavy; run with --features full-tests"
4449 )]
4450 #[test]
4451 fn default_write_is_v1_and_unknown_version_is_rejected() {
4452 let Some((tmp, _oid)) = midx_fixture(false) else {
4453 eprintln!("SKIP: git unavailable for MIDX fixture");
4454 return;
4455 };
4456 let pack_dir = tmp.path().join(".git/objects/pack");
4457 let midx_path = pack_dir.join("multi-pack-index");
4458 write_multi_pack_index(&pack_dir).expect("default write");
4459 assert_eq!(fs::read(&midx_path).expect("read midx")[4], 1);
4460
4461 fs::remove_file(&midx_path).expect("remove midx");
4462 let err = super::write_multi_pack_index_with_options(
4463 &pack_dir,
4464 &super::WriteMultiPackIndexOptions {
4465 version: Some(3),
4466 ..Default::default()
4467 },
4468 )
4469 .expect_err("version 3");
4470 assert!(matches!(
4471 err,
4472 Error::Midx(MidxError::UnknownWriteVersion { version: 3 })
4473 ));
4474 assert!(!midx_path.exists(), "rejected write must not leave a MIDX");
4475 }
4476}