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