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