1use std::collections::{HashMap, HashSet};
13use std::fs;
14use std::io::{BufRead, BufReader};
15use std::path::Path;
16
17use sha1::{Digest, Sha1};
18use sha2::{Digest as Sha256Digest, Sha256};
19
20use crate::error::{Error, Result};
21use crate::objects::ObjectId;
22use crate::pack::{read_pack_index_no_verify, PackIndex};
23
24const MIDX_SIGNATURE: u32 = 0x4d49_4458;
25const MIDX_VERSION_V1: u8 = 1;
26const MIDX_VERSION_V2: u8 = 2;
27const HASH_VERSION_SHA1: u8 = 1;
28const HASH_VERSION_SHA256: u8 = 2;
29const MIDX_HEADER_SIZE: usize = 12;
30const CHUNK_TOC_ENTRY_SIZE: usize = 12;
31const MIDX_CHUNKID_PACKNAMES: u32 = 0x504e_414d;
32const MIDX_CHUNKID_OIDFANOUT: u32 = 0x4f49_4446;
33const MIDX_CHUNKID_OIDLOOKUP: u32 = 0x4f49_444c;
34const MIDX_CHUNKID_OBJECTOFFSETS: u32 = 0x4f4f_4646;
35const MIDX_CHUNKID_LARGEOFFSETS: u32 = 0x4c4f_4646;
36const MIDX_CHUNKID_REVINDEX: u32 = 0x5249_4458;
37const MIDX_CHUNKID_BITMAPPED_PACKS: u32 = 0x4254_4d50;
38
39const RIDX_SIGNATURE: u32 = 0x5249_4458;
41const RIDX_VERSION: u32 = 1;
42const RIDX_HEADER_SIZE: usize = 12;
43const MIDX_CHUNK_ALIGNMENT: usize = 4;
44
45const MIDX_LARGE_OFFSET_NEEDED: u32 = 0x8000_0000;
47
48struct MidxEntry {
49 oid: ObjectId,
50 pack_id: u32,
51 offset: u64,
52 pack_mtime: std::time::SystemTime,
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct WriteMultiPackIndexOptions {
58 pub preferred_pack_idx: Option<u32>,
61 pub preferred_pack_name: Option<String>,
64 pub pack_names_subset_ordered: Option<Vec<String>>,
66 pub write_bitmap_placeholders: bool,
68 pub incremental: bool,
71 pub write_rev_placeholder: bool,
74 pub version: Option<u8>,
77}
78
79fn normalize_pack_idx_basename(raw: &str) -> Result<String> {
80 let t = raw.trim();
81 let t = std::path::Path::new(t)
82 .file_name()
83 .and_then(|s| s.to_str())
84 .unwrap_or(t);
85 let t = t.strip_prefix("./").unwrap_or(t);
86 if t.ends_with(".idx") {
87 Ok(t.to_string())
88 } else if t.ends_with(".pack") {
89 Ok(format!("{}.idx", t.strip_suffix(".pack").unwrap_or(t)))
90 } else {
91 Ok(format!("{t}.idx"))
92 }
93}
94
95fn read_be_u32(data: &[u8], off: usize) -> Result<u32> {
101 let end = off.checked_add(4).filter(|&e| e <= data.len());
102 let Some(end) = end else {
103 return Err(Error::CorruptObject(
104 "truncated MIDX data reading u32".to_owned(),
105 ));
106 };
107 let bytes: [u8; 4] = data[off..end]
108 .try_into()
109 .map_err(|_| Error::CorruptObject("truncated MIDX data reading u32".to_owned()))?;
110 Ok(u32::from_be_bytes(bytes))
111}
112
113fn read_be_u64(data: &[u8], off: usize) -> Result<u64> {
119 let end = off.checked_add(8).filter(|&e| e <= data.len());
120 let Some(end) = end else {
121 return Err(Error::CorruptObject(
122 "truncated MIDX data reading u64".to_owned(),
123 ));
124 };
125 let bytes: [u8; 8] = data[off..end]
126 .try_into()
127 .map_err(|_| Error::CorruptObject("truncated MIDX data reading u64".to_owned()))?;
128 Ok(u64::from_be_bytes(bytes))
129}
130
131struct MidxFileHeader {
132 num_chunks: u8,
133}
134
135fn parse_midx_header(data: &[u8]) -> Result<(MidxFileHeader, usize, u8)> {
136 if data.len() < MIDX_HEADER_SIZE + 20 {
137 return Err(Error::CorruptObject("midx file too small".to_owned()));
138 }
139 let sig = read_be_u32(data, 0)?;
140 if sig != MIDX_SIGNATURE {
141 return Err(Error::CorruptObject("bad MIDX signature".to_owned()));
142 }
143 let version = data[4];
144 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
145 return Err(Error::CorruptObject(format!(
146 "multi-pack-index version {version} not recognized"
147 )));
148 }
149 let object_hash_bytes = data[5];
150 let num_chunks = data[6];
151 let _num_packs = read_be_u32(data, 8)?;
152 Ok((
153 MidxFileHeader { num_chunks },
154 MIDX_HEADER_SIZE,
155 object_hash_bytes,
156 ))
157}
158
159fn parse_pack_names_blob(pn: &[u8]) -> Result<Vec<String>> {
160 let mut names = Vec::new();
161 let mut start = 0usize;
162 for (i, &b) in pn.iter().enumerate() {
163 if b == 0 && i >= start {
164 if i > start {
165 let s = std::str::from_utf8(&pn[start..i])
166 .map_err(|_| Error::CorruptObject("non-utf8 pack name in MIDX".to_owned()))?;
167 names.push(s.to_string());
168 }
169 start = i + 1;
170 }
171 }
172 Ok(names)
173}
174
175fn cmp_idx_or_pack_name(idx_or_pack_name: &str, idx_name: &str) -> std::cmp::Ordering {
177 let a = idx_or_pack_name.as_bytes();
178 let b = idx_name.as_bytes();
179 let mut i = 0usize;
180 let min = a.len().min(b.len());
181 while i < min && a[i] == b[i] {
182 i += 1;
183 }
184 let suf_a = &a[i..];
185 let suf_b = &b[i..];
186 if suf_b == b"idx" && suf_a == b"pack" {
187 return std::cmp::Ordering::Equal;
188 }
189 suf_a.cmp(suf_b)
190}
191
192fn preferred_pack_index_by_mtime(pack_dir: &Path, names: &[String]) -> Result<Option<usize>> {
193 let mut best: Option<(usize, std::time::SystemTime)> = None;
194 for (i, n) in names.iter().enumerate() {
195 let meta = fs::metadata(pack_dir.join(n)).map_err(Error::Io)?;
196 let mtime = meta.modified().map_err(Error::Io)?;
197 match best {
198 None => best = Some((i, mtime)),
199 Some((_, t)) if mtime < t => best = Some((i, mtime)),
200 _ => {}
201 }
202 }
203 Ok(best.map(|(i, _)| i))
204}
205
206fn midx_d_dir(pack_dir: &Path) -> std::path::PathBuf {
207 pack_dir.join("multi-pack-index.d")
208}
209
210fn chain_file_path(pack_dir: &Path) -> std::path::PathBuf {
211 midx_d_dir(pack_dir).join("multi-pack-index-chain")
212}
213
214fn read_chain_layer_hashes(pack_dir: &Path) -> Result<Vec<String>> {
215 let path = chain_file_path(pack_dir);
216 let f = fs::File::open(&path).map_err(Error::Io)?;
217 let mut out = Vec::new();
218 for line in BufReader::new(f).lines() {
219 let line = line.map_err(Error::Io)?;
220 let t = line.trim();
221 if t.is_empty() {
222 continue;
223 }
224 if t.len() != 40 || !t.chars().all(|c| c.is_ascii_hexdigit()) {
225 return Err(Error::CorruptObject(format!(
226 "invalid multi-pack-index chain line: {t}"
227 )));
228 }
229 out.push(t.to_ascii_lowercase());
230 }
231 Ok(out)
232}
233
234fn repo_midx_hash_version(pack_dir: &Path) -> u8 {
242 let Some(objects_dir) = pack_dir.parent() else {
244 return HASH_VERSION_SHA1;
245 };
246 repo_midx_hash_version_for_objects_dir(objects_dir)
247}
248
249mod midx_cache {
262 use crate::error::{Error, Result};
263 use std::collections::HashMap;
264 use std::fs;
265 use std::path::{Path, PathBuf};
266 use std::sync::{Arc, Mutex, OnceLock};
267 use std::time::SystemTime;
268
269 type Stamp = (SystemTime, u64);
270
271 #[derive(Default)]
272 struct State {
273 bytes: HashMap<PathBuf, (Stamp, Arc<Vec<u8>>)>,
274 hash_version: HashMap<PathBuf, (Option<Stamp>, u8)>,
275 }
276
277 static CACHE: OnceLock<Mutex<State>> = OnceLock::new();
278
279 fn lock() -> std::sync::MutexGuard<'static, State> {
280 CACHE
281 .get_or_init(|| Mutex::new(State::default()))
282 .lock()
283 .unwrap_or_else(std::sync::PoisonError::into_inner)
284 }
285
286 fn stamp(path: &Path) -> Option<Stamp> {
287 let m = fs::metadata(path).ok()?;
288 Some((m.modified().unwrap_or(SystemTime::UNIX_EPOCH), m.len()))
289 }
290
291 pub fn get_bytes(path: &Path) -> Result<Arc<Vec<u8>>> {
293 let sig = stamp(path);
294 if let Some(sig) = sig {
295 let g = lock();
296 if let Some((s, b)) = g.bytes.get(path) {
297 if *s == sig {
298 return Ok(Arc::clone(b));
299 }
300 }
301 }
302 let data = Arc::new(fs::read(path).map_err(Error::Io)?);
303 if let Some(sig) = sig {
304 lock()
305 .bytes
306 .insert(path.to_path_buf(), (sig, Arc::clone(&data)));
307 }
308 Ok(data)
309 }
310
311 pub fn hash_version(config_path: &Path, compute: impl FnOnce() -> u8) -> u8 {
315 let sig = stamp(config_path);
316 {
317 let g = lock();
318 if let Some((s, v)) = g.hash_version.get(config_path) {
319 if *s == sig {
320 return *v;
321 }
322 }
323 }
324 let v = compute();
325 lock()
326 .hash_version
327 .insert(config_path.to_path_buf(), (sig, v));
328 v
329 }
330
331 pub fn evict_pack_dir(pack_dir: &Path) {
333 lock().bytes.retain(|p, _| !p.starts_with(pack_dir));
334 }
335}
336
337fn repo_midx_hash_version_for_objects_dir(objects_dir: &Path) -> u8 {
341 let Some(gitdir) = objects_dir.parent() else {
342 return HASH_VERSION_SHA1;
343 };
344 let config_path = gitdir.join("config");
345 midx_cache::hash_version(&config_path, || {
346 sniff_objectformat_hash_version(&config_path)
347 })
348}
349
350fn sniff_objectformat_hash_version(config_path: &Path) -> u8 {
352 let Ok(text) = fs::read_to_string(config_path) else {
353 return HASH_VERSION_SHA1;
354 };
355 let mut in_extensions = false;
359 for raw in text.lines() {
360 let line = raw.trim();
361 if line.starts_with('[') {
362 let section = line.trim_start_matches('[').trim_end_matches(']');
363 let name = section.split_whitespace().next().unwrap_or("");
364 in_extensions = name.eq_ignore_ascii_case("extensions");
365 continue;
366 }
367 if !in_extensions {
368 continue;
369 }
370 if let Some((key, value)) = line.split_once('=') {
371 if key.trim().eq_ignore_ascii_case("objectformat")
372 && value.trim().eq_ignore_ascii_case("sha256")
373 {
374 return HASH_VERSION_SHA256;
375 }
376 }
377 }
378 HASH_VERSION_SHA1
379}
380
381pub fn resolve_tip_midx_path(pack_dir: &Path) -> Option<std::path::PathBuf> {
382 let root = pack_dir.join("multi-pack-index");
383 if root.exists() {
384 return Some(root);
385 }
386 let hashes = read_chain_layer_hashes(pack_dir).ok()?;
387 let last = hashes.last()?;
388 Some(midx_d_dir(pack_dir).join(format!("multi-pack-index-{last}.midx")))
389}
390
391pub fn resolve_midx_layer_path(pack_dir: &Path, checksum: &str) -> Option<std::path::PathBuf> {
395 let checksum = checksum.to_ascii_lowercase();
396 if let Ok(hashes) = read_chain_layer_hashes(pack_dir) {
397 if hashes.contains(&checksum) {
398 return Some(midx_d_dir(pack_dir).join(format!("multi-pack-index-{checksum}.midx")));
399 }
400 }
401 let root = pack_dir.join("multi-pack-index");
402 if root.exists() {
403 if let Ok(hex) = midx_checksum_hex_from_path(&root) {
404 if hex == checksum {
405 return Some(root);
406 }
407 }
408 }
409 None
410}
411
412fn load_midx_file(path: &Path) -> Result<Vec<u8>> {
413 let data = fs::read(path).map_err(Error::Io)?;
414 let _ = parse_midx_header(&data)?;
415 Ok(data)
416}
417
418fn midx_hash_len(data: &[u8]) -> usize {
421 if data.len() > 5 && data[5] == 2 {
422 32
423 } else {
424 20
425 }
426}
427
428fn oids_and_packs_from_midx_data(data: &[u8]) -> Result<(HashSet<ObjectId>, Vec<String>)> {
429 let hash_len = midx_hash_len(data);
430 let (_, hdr_end, _) = parse_midx_header(data)?;
431 let (pn_off, pn_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
432 let pack_names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
433 let (_ooff_off, ooff_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
434 let (oidl_off, oidl_len) = find_chunk(data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
435 let num_objects = ooff_len / 8;
436 if oidl_len != num_objects * hash_len {
437 return Err(Error::CorruptObject(
438 "MIDX oid-lookup size mismatch".to_owned(),
439 ));
440 }
441 let mut oids = HashSet::with_capacity(num_objects);
442 for i in 0..num_objects {
443 let start = oidl_off + i * hash_len;
444 let oid = ObjectId::from_bytes(&data[start..start + hash_len])?;
445 oids.insert(oid);
446 }
447 Ok((oids, pack_names))
448}
449
450fn collect_incremental_base(pack_dir: &Path) -> Result<(HashSet<ObjectId>, HashSet<String>)> {
451 let mut oids = HashSet::new();
452 let mut packs = HashSet::new();
453 let root = pack_dir.join("multi-pack-index");
454 let chain_path = chain_file_path(pack_dir);
455 if chain_path.exists() {
456 for h in read_chain_layer_hashes(pack_dir)? {
457 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{h}.midx"));
458 let data = load_midx_file(&p)?;
459 let (layer_oids, names) = oids_and_packs_from_midx_data(&data)?;
460 oids.extend(layer_oids);
461 for n in names {
462 packs.insert(n);
463 }
464 }
465 return Ok((oids, packs));
466 }
467 if root.exists() {
468 let data = load_midx_file(&root)?;
469 let (o, names) = oids_and_packs_from_midx_data(&data)?;
470 oids = o;
471 for n in names {
472 packs.insert(n);
473 }
474 }
475 Ok((oids, packs))
476}
477
478fn midx_checksum_hex_from_path(path: &Path) -> Result<String> {
479 let data = fs::read(path).map_err(Error::Io)?;
480 if data.len() < 20 {
481 return Err(Error::CorruptObject(
482 "midx too small for checksum".to_owned(),
483 ));
484 }
485 let hash = &data[data.len() - 20..];
486 Ok(hex::encode(hash))
487}
488
489fn hard_link_or_copy(src: &Path, dst: &Path) -> Result<()> {
490 let _ = fs::remove_file(dst);
491 if fs::hard_link(src, dst).is_ok() {
492 return Ok(());
493 }
494 fs::copy(src, dst).map_err(Error::Io)?;
495 Ok(())
496}
497
498fn link_root_midx_into_chain(pack_dir: &Path, root_checksum_hex: &str) -> Result<()> {
499 let midx_d = midx_d_dir(pack_dir);
500 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
501 let dst_midx = midx_d.join(format!("multi-pack-index-{root_checksum_hex}.midx"));
502 hard_link_or_copy(&pack_dir.join("multi-pack-index"), &dst_midx)?;
503 let exts = ["bitmap", "rev"];
504 for ext in exts {
505 let src = pack_dir.join(format!("multi-pack-index-{root_checksum_hex}.{ext}"));
506 if src.exists() {
507 let dst = midx_d.join(format!("multi-pack-index-{root_checksum_hex}.{ext}"));
508 hard_link_or_copy(&src, &dst)?;
509 }
510 }
511 Ok(())
512}
513
514fn clear_stale_split_layers(pack_dir: &Path, keep: &[String]) -> Result<()> {
515 let midx_d = midx_d_dir(pack_dir);
516 if !midx_d.exists() {
517 return Ok(());
518 }
519 let keep: HashSet<&str> = keep.iter().map(|s| s.as_str()).collect();
520 for ent in fs::read_dir(&midx_d).map_err(Error::Io)? {
521 let ent = ent.map_err(Error::Io)?;
522 let name = ent.file_name().to_string_lossy().to_string();
523 let Some(rest) = name.strip_prefix("multi-pack-index-") else {
524 continue;
525 };
526 let Some((hash_part, _ext)) = rest.split_once('.') else {
527 continue;
528 };
529 if hash_part.len() == 40 && !keep.contains(hash_part) {
530 let _ = fs::remove_file(ent.path());
531 }
532 }
533 Ok(())
534}
535
536fn clear_incremental_midx_files(pack_dir: &Path) -> Result<()> {
546 let midx_d = midx_d_dir(pack_dir);
547 let _ = fs::remove_file(chain_file_path(pack_dir));
549 if !midx_d.exists() {
550 return Ok(());
551 }
552 for ent in fs::read_dir(&midx_d).map_err(Error::Io)? {
553 let ent = ent.map_err(Error::Io)?;
554 let name = ent.file_name().to_string_lossy().to_string();
555 if name.starts_with("multi-pack-index-")
556 && (name.ends_with(".midx") || name.ends_with(".bitmap") || name.ends_with(".rev"))
557 {
558 let _ = fs::remove_file(ent.path());
559 }
560 }
561 Ok(())
562}
563
564fn pack_mtime_for_midx(idx: &PackIndex) -> std::time::SystemTime {
565 fs::metadata(&idx.pack_path)
566 .and_then(|m| m.modified())
567 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
568}
569
570fn midx_pick_better_entry(
571 cur: &MidxEntry,
572 cand_pack: u32,
573 cand_offset: u64,
574 cand_mtime: std::time::SystemTime,
575 preferred_pack: Option<u32>,
576) -> bool {
577 let cur_pref = preferred_pack == Some(cur.pack_id);
578 let new_pref = preferred_pack == Some(cand_pack);
579 if new_pref && !cur_pref {
580 return true;
581 }
582 if cur_pref && !new_pref {
583 return false;
584 }
585 match cand_mtime.cmp(&cur.pack_mtime) {
586 std::cmp::Ordering::Greater => true,
587 std::cmp::Ordering::Less => false,
588 std::cmp::Ordering::Equal => {
589 if cand_pack != cur.pack_id {
590 cand_pack < cur.pack_id
591 } else {
592 cand_offset < cur.offset
593 }
594 }
595 }
596}
597
598#[allow(clippy::too_many_arguments)]
603fn build_midx_bytes_filtered(
604 idx_names: &[String],
605 indexes: &[PackIndex],
606 preferred_idx: Option<usize>,
607 write_bitmap_placeholders: bool,
608 omit_embedded_ridx_chunk: bool,
609 version: u8,
610 hash_version: u8,
611 exclude_oids: Option<&HashSet<ObjectId>>,
612) -> Result<(Vec<u8>, Option<Vec<u32>>)> {
613 let hash_len = if hash_version == 2 { 32 } else { 20 };
615 let preferred_pack_idx = preferred_idx.map(|p| p as u32);
616 let pack_mtimes: Vec<std::time::SystemTime> = indexes.iter().map(pack_mtime_for_midx).collect();
617
618 let mut best: HashMap<ObjectId, MidxEntry> = HashMap::new();
619 for (pack_id, idx) in indexes.iter().enumerate() {
620 let pack_id = u32::try_from(pack_id).map_err(|_| {
621 Error::CorruptObject("too many pack files for multi-pack-index".to_owned())
622 })?;
623 let mtime = pack_mtimes[pack_id as usize];
624 for e in &idx.entries {
625 if e.oid.len() != hash_len {
626 continue;
627 }
628 let Ok(oid) = ObjectId::from_bytes(&e.oid) else {
629 continue;
630 };
631 if let Some(ex) = exclude_oids {
632 if ex.contains(&oid) {
633 continue;
634 }
635 }
636 let cand = MidxEntry {
637 oid,
638 pack_id,
639 offset: e.offset,
640 pack_mtime: mtime,
641 };
642 match best.get(&oid) {
643 None => {
644 best.insert(oid, cand);
645 }
646 Some(cur) => {
647 if midx_pick_better_entry(cur, pack_id, e.offset, mtime, preferred_pack_idx) {
648 best.insert(oid, cand);
649 }
650 }
651 }
652 }
653 }
654
655 let mut entries: Vec<MidxEntry> = best.into_values().collect();
656 entries.sort_by_key(|a| a.oid);
657
658 let large_offsets_needed = entries.iter().any(|e| e.offset > u64::from(u32::MAX));
666
667 let num_packs = indexes.len() as u32;
668
669 let mut pack_names_blob = Vec::new();
670 for name in idx_names {
671 pack_names_blob.extend_from_slice(name.as_bytes());
672 pack_names_blob.push(0);
673 }
674 let pad = (MIDX_CHUNK_ALIGNMENT - (pack_names_blob.len() % MIDX_CHUNK_ALIGNMENT))
675 % MIDX_CHUNK_ALIGNMENT;
676 pack_names_blob.extend(std::iter::repeat_n(0u8, pad));
677 let chunk_pnam = pack_names_blob;
678
679 let mut chunk_oidf = vec![0u8; 256 * 4];
680 let mut j = 0usize;
681 for i in 0..256 {
682 while j < entries.len() && entries[j].oid.as_bytes()[0] <= i as u8 {
683 j += 1;
684 }
685 chunk_oidf[i * 4..(i + 1) * 4].copy_from_slice(&(j as u32).to_be_bytes());
686 }
687
688 let mut chunk_oidl = Vec::with_capacity(entries.len() * 20);
689 for e in &entries {
690 chunk_oidl.extend_from_slice(e.oid.as_bytes());
691 }
692
693 let mut large_offsets: Vec<u64> = Vec::new();
694 let mut chunk_ooff = Vec::with_capacity(entries.len() * 8);
695 for e in &entries {
696 chunk_ooff.extend_from_slice(&e.pack_id.to_be_bytes());
697 let encoded = if large_offsets_needed && e.offset >> 31 != 0 {
698 let slot = u32::try_from(large_offsets.len()).map_err(|_| {
699 Error::CorruptObject("too many large offsets in multi-pack-index".to_owned())
700 })?;
701 large_offsets.push(e.offset);
702 MIDX_LARGE_OFFSET_NEEDED | slot
703 } else {
704 e.offset as u32
708 };
709 chunk_ooff.extend_from_slice(&encoded.to_be_bytes());
710 }
711
712 let chunk_loff: Vec<u8> = if large_offsets.is_empty() {
713 Vec::new()
714 } else {
715 let mut v = Vec::with_capacity(large_offsets.len() * 8);
716 for off in &large_offsets {
717 v.extend_from_slice(&off.to_be_bytes());
718 }
719 v
720 };
721
722 let pref = preferred_pack_idx;
723 let mut order: Vec<u32> = (0..entries.len() as u32).collect();
724 order.sort_by(|&ai, &bi| {
725 let a = &entries[ai as usize];
726 let b = &entries[bi as usize];
727 let a_pref = pref == Some(a.pack_id);
728 let b_pref = pref == Some(b.pack_id);
729 b_pref
730 .cmp(&a_pref)
731 .then_with(|| a.pack_id.cmp(&b.pack_id))
732 .then_with(|| a.offset.cmp(&b.offset))
733 .then_with(|| ai.cmp(&bi))
734 });
735
736 let mut chunk_ridx = Vec::with_capacity(entries.len() * 4);
737 for oid_idx in &order {
738 chunk_ridx.extend_from_slice(&oid_idx.to_be_bytes());
739 }
740
741 let rev_sidecar_order = if omit_embedded_ridx_chunk && write_bitmap_placeholders {
744 Some(order.clone())
745 } else {
746 None
747 };
748 let chunk_btmp: Vec<u8> = if write_bitmap_placeholders {
749 let num_packs_usize = indexes.len();
754 let mut bitmap_pos = vec![u32::MAX; num_packs_usize];
755 let mut bitmap_nr = vec![0u32; num_packs_usize];
756 for (rank, &oid_idx) in order.iter().enumerate() {
757 let pack = entries[oid_idx as usize].pack_id as usize;
758 if let Some(p) = bitmap_pos.get_mut(pack) {
759 if *p == u32::MAX {
760 *p = rank as u32;
761 }
762 }
763 if let Some(n) = bitmap_nr.get_mut(pack) {
764 *n += 1;
765 }
766 }
767 let mut v = Vec::new();
768 for pack in 0..num_packs_usize {
769 let pos = if bitmap_pos[pack] == u32::MAX {
770 0
771 } else {
772 bitmap_pos[pack]
773 };
774 v.extend_from_slice(&pos.to_be_bytes());
775 v.extend_from_slice(&bitmap_nr[pack].to_be_bytes());
776 }
777 let pad = (MIDX_CHUNK_ALIGNMENT - (v.len() % MIDX_CHUNK_ALIGNMENT)) % MIDX_CHUNK_ALIGNMENT;
778 v.extend(std::iter::repeat_n(0u8, pad));
779 v
780 } else {
781 Vec::new()
782 };
783
784 let mut chunks: Vec<(u32, Vec<u8>)> = vec![
785 (MIDX_CHUNKID_PACKNAMES, chunk_pnam),
786 (MIDX_CHUNKID_OIDFANOUT, chunk_oidf),
787 (MIDX_CHUNKID_OIDLOOKUP, chunk_oidl),
788 (MIDX_CHUNKID_OBJECTOFFSETS, chunk_ooff),
789 ];
790 if !chunk_loff.is_empty() {
791 chunks.push((MIDX_CHUNKID_LARGEOFFSETS, chunk_loff));
792 }
793 if (pref.is_some() || write_bitmap_placeholders) && !omit_embedded_ridx_chunk {
794 chunks.push((MIDX_CHUNKID_REVINDEX, chunk_ridx));
795 }
796 if write_bitmap_placeholders {
797 chunks.push((MIDX_CHUNKID_BITMAPPED_PACKS, chunk_btmp));
798 }
799
800 let num_chunks: u8 = chunks
801 .len()
802 .try_into()
803 .map_err(|_| Error::CorruptObject("too many MIDX chunks".to_owned()))?;
804
805 let mut body = Vec::new();
806 let mut cur_offset =
807 MIDX_HEADER_SIZE as u64 + ((chunks.len() + 1) * CHUNK_TOC_ENTRY_SIZE) as u64;
808
809 for (id, data) in &chunks {
810 body.extend_from_slice(&id.to_be_bytes());
811 body.extend_from_slice(&cur_offset.to_be_bytes());
812 cur_offset += data.len() as u64;
813 }
814 body.extend_from_slice(&0u32.to_be_bytes());
815 body.extend_from_slice(&cur_offset.to_be_bytes());
816
817 for (_, data) in &chunks {
818 body.extend_from_slice(data);
819 }
820
821 let mut out = Vec::with_capacity(MIDX_HEADER_SIZE + body.len() + 20);
822 out.extend_from_slice(&MIDX_SIGNATURE.to_be_bytes());
823 out.push(if version == MIDX_VERSION_V1 {
824 MIDX_VERSION_V1
825 } else {
826 MIDX_VERSION_V2
827 });
828 out.push(hash_version);
829 out.push(num_chunks);
830 out.push(0);
831 out.extend_from_slice(&num_packs.to_be_bytes());
832 out.extend_from_slice(&body);
833
834 if hash_version == 2 {
836 let mut hasher = Sha256::new();
837 Sha256Digest::update(&mut hasher, &out);
838 out.extend_from_slice(&hasher.finalize());
839 } else {
840 let mut hasher = Sha1::new();
841 hasher.update(&out);
842 out.extend_from_slice(&hasher.finalize());
843 }
844
845 Ok((out, rev_sidecar_order))
846}
847
848fn write_midx_rev_sidecar(path: &Path, pack_order: &[u32], midx_file_hash: &[u8]) -> Result<()> {
853 let hash_id: u32 = if midx_file_hash.len() == 32 { 2 } else { 1 };
854 let mut body =
855 Vec::with_capacity(RIDX_HEADER_SIZE + pack_order.len() * 4 + midx_file_hash.len());
856 body.extend_from_slice(&RIDX_SIGNATURE.to_be_bytes());
857 body.extend_from_slice(&RIDX_VERSION.to_be_bytes());
858 body.extend_from_slice(&hash_id.to_be_bytes());
859 for idx in pack_order {
860 body.extend_from_slice(&idx.to_be_bytes());
861 }
862 body.extend_from_slice(midx_file_hash);
863 fs::write(path, body).map_err(Error::Io)
864}
865
866fn find_chunk(data: &[u8], header_end: usize, chunk_id: u32) -> Result<(usize, usize)> {
867 let (hdr, _, _) = parse_midx_header(data)?;
868 let n = hdr.num_chunks as usize;
869 let pos = header_end;
870 let toc_end = pos + (n + 1) * CHUNK_TOC_ENTRY_SIZE;
871 if data.len() < toc_end + 20 {
872 return Err(Error::CorruptObject(
873 "truncated MIDX chunk table".to_owned(),
874 ));
875 }
876 for i in 0..n {
877 let base = pos + i * CHUNK_TOC_ENTRY_SIZE;
878 let id = read_be_u32(data, base)?;
879 let off = read_be_u64(data, base + 4)? as usize;
880 if id == chunk_id {
881 let next_off = if i + 1 < n {
882 let nb = pos + (i + 1) * CHUNK_TOC_ENTRY_SIZE;
883 read_be_u64(data, nb + 4)? as usize
884 } else {
885 let term = pos + n * CHUNK_TOC_ENTRY_SIZE;
886 read_be_u64(data, term + 4)? as usize
887 };
888 return Ok((off, next_off.saturating_sub(off)));
889 }
890 }
891 Err(Error::CorruptObject(format!(
892 "MIDX chunk {chunk_id:08x} not found"
893 )))
894}
895
896#[derive(Debug, Clone)]
900pub struct MidxLoadError(pub String);
901
902impl std::fmt::Display for MidxLoadError {
903 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
904 write!(f, "{}", self.0)
905 }
906}
907
908struct TocEntry {
910 id: u32,
911 offset: usize,
912}
913
914fn parse_midx_toc(
918 data: &[u8],
919 hash_len: usize,
920 errors: &mut Vec<String>,
921) -> std::result::Result<Vec<TocEntry>, MidxLoadError> {
922 if data.len() < MIDX_HEADER_SIZE + hash_len {
923 return Err(MidxLoadError("multi-pack-index file too small".to_owned()));
924 }
925 let num_chunks = data[6] as usize;
926 let toc_off = MIDX_HEADER_SIZE;
927 let needed = toc_off + (num_chunks + 1) * CHUNK_TOC_ENTRY_SIZE;
928 if data.len() < needed {
929 return Err(MidxLoadError(
930 "multi-pack-index chunk table is truncated".to_owned(),
931 ));
932 }
933 let file_size = data.len();
934 let mut chunks: Vec<TocEntry> = Vec::with_capacity(num_chunks);
935
936 let read_be64 = |off: usize| -> u64 {
937 let mut b = [0u8; 8];
938 b.copy_from_slice(&data[off..off + 8]);
939 u64::from_be_bytes(b)
940 };
941 let read_be32 = |off: usize| -> u32 {
942 let mut b = [0u8; 4];
943 b.copy_from_slice(&data[off..off + 4]);
944 u32::from_be_bytes(b)
945 };
946
947 for i in 0..num_chunks {
948 let entry = toc_off + i * CHUNK_TOC_ENTRY_SIZE;
949 let chunk_id = read_be32(entry);
950 let chunk_offset = read_be64(entry + 4);
951
952 if chunk_id == 0 {
953 errors.push("terminating chunk id appears earlier than expected".to_owned());
954 return Err(MidxLoadError(
955 "multi-pack-index required pack-name chunk missing or corrupted".to_owned(),
956 ));
957 }
958 if !(chunk_offset as usize).is_multiple_of(MIDX_CHUNK_ALIGNMENT) {
959 errors.push(format!(
960 "chunk id {chunk_id:x} not {MIDX_CHUNK_ALIGNMENT}-byte aligned"
961 ));
962 return Err(MidxLoadError(
963 "multi-pack-index required pack-name chunk missing or corrupted".to_owned(),
964 ));
965 }
966
967 let next_entry = toc_off + (i + 1) * CHUNK_TOC_ENTRY_SIZE;
968 let next_chunk_offset = read_be64(next_entry + 4);
969
970 if next_chunk_offset < chunk_offset
971 || next_chunk_offset > (file_size as u64).saturating_sub(hash_len as u64)
972 {
973 errors.push(format!(
974 "improper chunk offset(s) {chunk_offset:x} and {next_chunk_offset:x}"
975 ));
976 return Err(MidxLoadError(
977 "multi-pack-index required pack-name chunk missing or corrupted".to_owned(),
978 ));
979 }
980
981 if chunks.iter().any(|c| c.id == chunk_id) {
982 errors.push(format!("duplicate chunk ID {chunk_id:x} found"));
983 return Err(MidxLoadError(
984 "multi-pack-index required pack-name chunk missing or corrupted".to_owned(),
985 ));
986 }
987
988 chunks.push(TocEntry {
989 id: chunk_id,
990 offset: chunk_offset as usize,
991 });
992 }
993
994 let term_entry = toc_off + num_chunks * CHUNK_TOC_ENTRY_SIZE;
996 let final_id = read_be32(term_entry);
997 if final_id != 0 {
998 errors.push(format!("final chunk has non-zero id {final_id:x}"));
999 return Err(MidxLoadError(
1000 "multi-pack-index required pack-name chunk missing or corrupted".to_owned(),
1001 ));
1002 }
1003
1004 let term_offset = read_be64(term_entry + 4) as usize;
1007 chunks.push(TocEntry {
1008 id: 0,
1009 offset: term_offset,
1010 });
1011
1012 Ok(chunks)
1013}
1014
1015fn toc_chunk_range(chunks: &[TocEntry], data_len: usize, id: u32) -> Option<(usize, usize)> {
1017 for (i, c) in chunks.iter().enumerate() {
1018 if c.id == id {
1019 let next = if i + 1 < chunks.len() {
1020 chunks[i + 1].offset
1021 } else {
1022 data_len.saturating_sub(20)
1023 };
1024 return Some((c.offset, next.saturating_sub(c.offset)));
1025 }
1026 }
1027 None
1028}
1029
1030pub fn verify_midx(objects_dir: &Path) -> std::result::Result<(), Vec<String>> {
1037 let pack_dir = objects_dir.join("pack");
1038 let path = match resolve_tip_midx_path(&pack_dir) {
1039 Some(p) => p,
1040 None => return Ok(()),
1041 };
1042 let data = match fs::read(&path) {
1043 Ok(d) => d,
1044 Err(_) => return Ok(()),
1045 };
1046
1047 let mut fatal: Vec<String> = Vec::new();
1048 let mut errors: Vec<String> = Vec::new();
1049
1050 if data.len() < MIDX_HEADER_SIZE + 20 {
1052 return Err(vec!["multi-pack-index file is too small".to_owned()]);
1053 }
1054 let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
1055 if sig != MIDX_SIGNATURE {
1056 return Err(vec![format!(
1057 "multi-pack-index signature 0x{sig:08x} does not match signature 0x{MIDX_SIGNATURE:08x}"
1058 )]);
1059 }
1060 let version = data[4];
1061 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
1062 return Err(vec![format!(
1063 "multi-pack-index version {version} not recognized"
1064 )]);
1065 }
1066 let hash_version = data[5];
1067 let expected_hash_version = repo_midx_hash_version_for_objects_dir(objects_dir);
1068 if hash_version != expected_hash_version {
1069 return Err(vec![format!(
1070 "multi-pack-index hash version {hash_version} does not match version {expected_hash_version}"
1071 )]);
1072 }
1073 let hash_len = if hash_version == 2 { 32usize } else { 20usize };
1074 let num_packs = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
1075
1076 let chunks = match parse_midx_toc(&data, hash_len, &mut errors) {
1078 Ok(c) => c,
1079 Err(e) => {
1080 errors.push(e.0);
1081 return Err(errors);
1082 }
1083 };
1084
1085 let Some((pn_off, pn_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_PACKNAMES)
1087 else {
1088 errors.push("multi-pack-index required pack-name chunk missing or corrupted".to_owned());
1089 return Err(errors);
1090 };
1091
1092 let Some((fan_off, fan_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDFANOUT)
1094 else {
1095 errors.push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
1096 return Err(errors);
1097 };
1098 if fan_len != 256 * 4 {
1099 errors.push("multi-pack-index OID fanout is of the wrong size".to_owned());
1100 errors.push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
1101 return Err(errors);
1102 }
1103 let fanout = |i: usize| -> u32 {
1104 let b = fan_off + i * 4;
1105 u32::from_be_bytes([data[b], data[b + 1], data[b + 2], data[b + 3]])
1106 };
1107 for i in 0..255 {
1108 let f1 = fanout(i);
1109 let f2 = fanout(i + 1);
1110 if f1 > f2 {
1111 errors.push(format!(
1112 "oid fanout out of order: fanout[{i}] = {f1:x} > {f2:x} = fanout[{}]",
1113 i + 1
1114 ));
1115 errors
1116 .push("multi-pack-index required OID fanout chunk missing or corrupted".to_owned());
1117 return Err(errors);
1118 }
1119 }
1120 let num_objects = fanout(255) as usize;
1121
1122 let Some((oidl_off, oidl_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDLOOKUP)
1124 else {
1125 errors.push("multi-pack-index required OID lookup chunk missing or corrupted".to_owned());
1126 return Err(errors);
1127 };
1128 if oidl_len != hash_len * num_objects {
1129 errors.push("multi-pack-index OID lookup chunk is the wrong size".to_owned());
1130 errors.push("multi-pack-index required OID lookup chunk missing or corrupted".to_owned());
1131 return Err(errors);
1132 }
1133
1134 let Some((ooff_off, ooff_len)) =
1136 toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OBJECTOFFSETS)
1137 else {
1138 errors
1139 .push("multi-pack-index required object offsets chunk missing or corrupted".to_owned());
1140 return Err(errors);
1141 };
1142 if ooff_len != num_objects * 8 {
1143 errors.push("multi-pack-index object offset chunk is the wrong size".to_owned());
1144 errors
1145 .push("multi-pack-index required object offsets chunk missing or corrupted".to_owned());
1146 return Err(errors);
1147 }
1148
1149 let large_off = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_LARGEOFFSETS);
1150
1151 let names = match parse_pack_names_blob(&data[pn_off..pn_off + pn_len]) {
1153 Ok(n) => n,
1154 Err(_) => {
1155 errors.push("multi-pack-index pack-name chunk is too short".to_owned());
1156 return Err(errors);
1157 }
1158 };
1159 if version == MIDX_VERSION_V1 {
1160 for i in 1..names.len() {
1161 if names[i] <= names[i - 1] {
1162 fatal.push(format!(
1163 "multi-pack-index pack names out of order: '{}' before '{}'",
1164 names[i - 1],
1165 names[i]
1166 ));
1167 errors.extend(fatal);
1169 return Err(errors);
1170 }
1171 }
1172 }
1173
1174 if !midx_checksum_is_valid(&data) {
1176 errors.push("incorrect checksum".to_owned());
1177 }
1178
1179 let mut pack_indexes: Vec<Option<PackIndex>> = Vec::with_capacity(num_packs);
1181 for i in 0..num_packs {
1182 let loaded = match names.get(i) {
1188 Some(name) => read_pack_index_no_verify(&pack_dir.join(name)).ok(),
1189 None => None,
1190 };
1191 if loaded.is_none() {
1192 errors.push(format!("failed to load pack in position {i}"));
1193 }
1194 pack_indexes.push(loaded);
1195 }
1196
1197 if num_objects == 0 {
1198 errors.push("the midx contains no oid".to_owned());
1199 if errors.is_empty() {
1200 return Ok(());
1201 }
1202 return Err(errors);
1203 }
1204
1205 let oid_at =
1207 |i: usize| -> &[u8] { &data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len] };
1208 for i in 0..num_objects.saturating_sub(1) {
1209 let a = oid_at(i);
1210 let b = oid_at(i + 1);
1211 if a >= b {
1212 errors.push(format!(
1213 "oid lookup out of order: oid[{i}] = {} >= {} = oid[{}]",
1214 hex::encode(a),
1215 hex::encode(b),
1216 i + 1
1217 ));
1218 }
1219 }
1220
1221 for i in 0..num_objects {
1223 let ob = ooff_off + i * 8;
1224 let pack_int_id = u32::from_be_bytes([data[ob], data[ob + 1], data[ob + 2], data[ob + 3]]);
1225 let off_raw = u32::from_be_bytes([data[ob + 4], data[ob + 5], data[ob + 6], data[ob + 7]]);
1226 let oid_hex = hex::encode(oid_at(i));
1227
1228 if pack_int_id as usize >= num_packs {
1229 errors.push(format!(
1230 "bad pack-int-id: {pack_int_id} ({num_packs} total packs)"
1231 ));
1232 errors.push(format!(
1233 "failed to load pack entry for oid[{i}] = {oid_hex}"
1234 ));
1235 continue;
1236 }
1237
1238 let m_offset: u64 = if off_raw & MIDX_LARGE_OFFSET_NEEDED != 0 {
1240 let slot = (off_raw & !MIDX_LARGE_OFFSET_NEEDED) as usize;
1241 match large_off {
1242 Some((lo_off, lo_len)) if (slot + 1) * 8 <= lo_len => {
1243 let b = lo_off + slot * 8;
1244 let mut arr = [0u8; 8];
1245 arr.copy_from_slice(&data[b..b + 8]);
1246 u64::from_be_bytes(arr)
1247 }
1248 _ => {
1249 errors.push("multi-pack-index large offset out of bounds".to_owned());
1250 continue;
1251 }
1252 }
1253 } else {
1254 u64::from(off_raw)
1255 };
1256
1257 let Some(Some(idx)) = pack_indexes.get(pack_int_id as usize) else {
1258 errors.push(format!(
1259 "failed to load pack entry for oid[{i}] = {oid_hex}"
1260 ));
1261 continue;
1262 };
1263 let Ok(oid) = ObjectId::from_bytes(oid_at(i)) else {
1264 errors.push(format!(
1265 "failed to load pack entry for oid[{i}] = {oid_hex}"
1266 ));
1267 continue;
1268 };
1269 match idx.find_offset(&oid) {
1270 Some(p_offset) => {
1271 if m_offset != p_offset {
1272 errors.push(format!(
1273 "incorrect object offset for oid[{i}] = {oid_hex}: {m_offset:x} != {p_offset:x}"
1274 ));
1275 }
1276 }
1277 None => {
1278 errors.push(format!(
1279 "failed to load pack entry for oid[{i}] = {oid_hex}"
1280 ));
1281 }
1282 }
1283 }
1284
1285 if errors.is_empty() {
1286 Ok(())
1287 } else {
1288 Err(errors)
1289 }
1290}
1291
1292fn midx_checksum_is_valid(data: &[u8]) -> bool {
1295 let hash_len = midx_hash_len(data);
1296 if data.len() < hash_len {
1297 return false;
1298 }
1299 let body = &data[..data.len() - hash_len];
1300 let stored = &data[data.len() - hash_len..];
1301 if hash_len == 32 {
1302 let mut hasher = Sha256::new();
1303 Sha256Digest::update(&mut hasher, body);
1304 hasher.finalize().as_slice() == stored
1305 } else {
1306 let mut hasher = Sha1::new();
1307 hasher.update(body);
1308 hasher.finalize().as_slice() == stored
1309 }
1310}
1311
1312pub fn read_midx_pack_idx_names(objects_dir: &Path) -> Result<Vec<String>> {
1319 let pack_dir = objects_dir.join("pack");
1320 let path = resolve_tip_midx_path(&pack_dir)
1321 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
1322 let data = fs::read(&path).map_err(Error::Io)?;
1323 let (_, hdr_end, _) = parse_midx_header(&data)?;
1324 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1325 parse_pack_names_blob(&data[pn_off..pn_off + pn_len])
1326}
1327
1328pub struct MidxObjectRef {
1330 pub oid: ObjectId,
1331 pub pack_int_id: usize,
1333}
1334
1335pub fn read_midx_objects(objects_dir: &Path) -> Result<(Vec<String>, Vec<MidxObjectRef>)> {
1339 let pack_dir = objects_dir.join("pack");
1340 let path = resolve_tip_midx_path(&pack_dir)
1341 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
1342 let data = fs::read(&path).map_err(Error::Io)?;
1343 let (_, hdr_end, _) = parse_midx_header(&data)?;
1344 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1345 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
1346 let hash_len = midx_hash_len(&data);
1347 let (oidl_off, oidl_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
1348 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1349 if oidl_len % hash_len != 0 || ooff_len % 8 != 0 {
1350 return Err(Error::CorruptObject(
1351 "bad MIDX oid-lookup / object-offsets size".to_owned(),
1352 ));
1353 }
1354 let num = oidl_len / hash_len;
1355 if num * 8 != ooff_len {
1356 return Err(Error::CorruptObject(
1357 "MIDX oid count does not match object-offsets".to_owned(),
1358 ));
1359 }
1360 let mut objects = Vec::with_capacity(num);
1361 for i in 0..num {
1362 let oid =
1363 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])
1364 .map_err(|e| Error::CorruptObject(e.to_string()))?;
1365 let base = ooff_off + i * 8;
1366 let pack_id = read_be_u32(&data, base)? as usize;
1367 objects.push(MidxObjectRef {
1368 oid,
1369 pack_int_id: pack_id,
1370 });
1371 }
1372 Ok((names, objects))
1373}
1374
1375pub fn midx_checksum_hex(objects_dir: &Path) -> Result<String> {
1377 let pack_dir = objects_dir.join("pack");
1378 let path = resolve_tip_midx_path(&pack_dir)
1379 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
1380 midx_checksum_hex_from_path(&path)
1381}
1382
1383fn resolve_read_midx_path(pack_dir: &Path, checksum: Option<&str>) -> Result<std::path::PathBuf> {
1388 match checksum {
1389 Some(cs) => resolve_midx_layer_path(pack_dir, cs)
1390 .ok_or_else(|| Error::CorruptObject(format!("could not find MIDX with checksum {cs}"))),
1391 None => resolve_tip_midx_path(pack_dir)
1392 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned())),
1393 }
1394}
1395
1396pub fn format_midx_show_objects(objects_dir: &Path) -> Result<String> {
1399 format_midx_show_objects_layer(objects_dir, None)
1400}
1401
1402pub fn format_midx_show_objects_layer(
1404 objects_dir: &Path,
1405 checksum: Option<&str>,
1406) -> Result<String> {
1407 let mut out = format_midx_dump_layer(objects_dir, checksum)?;
1408 let pack_dir = objects_dir.join("pack");
1409 let path = resolve_read_midx_path(&pack_dir, checksum)?;
1410 let data = fs::read(&path).map_err(Error::Io)?;
1411 let (_, hdr_end, _) = parse_midx_header(&data)?;
1412 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1413 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
1414 let hash_len = midx_hash_len(&data);
1415 let (oidl_off, oidl_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
1416 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1417 if oidl_len % hash_len != 0 || ooff_len % 8 != 0 {
1418 return Err(Error::CorruptObject(
1419 "bad MIDX oid-lookup / object-offsets size".to_owned(),
1420 ));
1421 }
1422 let num = oidl_len / hash_len;
1423 if num * 8 != ooff_len {
1424 return Err(Error::CorruptObject(
1425 "MIDX oid count does not match object-offsets".to_owned(),
1426 ));
1427 }
1428 for i in 0..num {
1429 let oid =
1430 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])
1431 .map_err(|e| Error::CorruptObject(e.to_string()))?;
1432 let base = ooff_off + i * 8;
1433 let pack_id = read_be_u32(&data, base)? as usize;
1434 let offset = u64::from(read_be_u32(&data, base + 4)?);
1435 let idx_name = names
1436 .get(pack_id)
1437 .ok_or_else(|| Error::CorruptObject("pack id out of range in MIDX".to_owned()))?;
1438 let stem = idx_name.strip_suffix(".idx").unwrap_or(idx_name);
1442 let dir_disp = objects_dir.display().to_string();
1443 let dir_disp = if objects_dir.is_absolute() || dir_disp.starts_with("./") {
1444 dir_disp
1445 } else {
1446 format!("./{dir_disp}")
1447 };
1448 out.push_str(&format!(
1449 "{} {}\t{}/pack/{}.pack\n",
1450 oid.to_hex(),
1451 offset,
1452 dir_disp,
1453 stem
1454 ));
1455 }
1456 Ok(out)
1457}
1458
1459pub fn format_midx_dump(objects_dir: &Path) -> Result<String> {
1460 format_midx_dump_layer(objects_dir, None)
1461}
1462
1463pub fn format_midx_dump_layer(objects_dir: &Path, checksum: Option<&str>) -> Result<String> {
1466 let pack_dir = objects_dir.join("pack");
1467 let path = resolve_read_midx_path(&pack_dir, checksum)?;
1468 let data = fs::read(&path).map_err(Error::Io)?;
1469 let (hdr, hdr_end, _) = parse_midx_header(&data)?;
1470 let sig = read_be_u32(&data, 0)?;
1471 let version = data[4];
1472 let hash_len: u8 = match data[5] {
1475 1 => 20,
1476 2 => 32,
1477 other => other,
1478 };
1479 let num_chunks = hdr.num_chunks;
1480 let num_packs = read_be_u32(&data, 8)?;
1481
1482 let mut chunk_tags: Vec<&'static str> = Vec::new();
1483 let n = num_chunks as usize;
1484 let pos = hdr_end;
1485 let toc_end = pos + (n + 1) * CHUNK_TOC_ENTRY_SIZE;
1486 if data.len() < toc_end + 20 {
1487 return Err(Error::CorruptObject(
1488 "truncated MIDX chunk table".to_owned(),
1489 ));
1490 }
1491 for i in 0..n {
1492 let base = pos + i * CHUNK_TOC_ENTRY_SIZE;
1493 let id = read_be_u32(&data, base)?;
1494 let tag = match id {
1495 x if x == MIDX_CHUNKID_PACKNAMES => "pack-names",
1496 x if x == MIDX_CHUNKID_OIDFANOUT => "oid-fanout",
1497 x if x == MIDX_CHUNKID_OIDLOOKUP => "oid-lookup",
1498 x if x == MIDX_CHUNKID_OBJECTOFFSETS => "object-offsets",
1499 x if x == MIDX_CHUNKID_LARGEOFFSETS => "large-offsets",
1500 x if x == MIDX_CHUNKID_REVINDEX => "revindex",
1501 x if x == 0x4254_4d50 => "bitmapped-packs",
1502 _ => "unknown",
1503 };
1504 chunk_tags.push(tag);
1505 }
1506
1507 let (_ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1508 let num_objects = ooff_len / 8;
1509
1510 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1511 let pack_names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
1512
1513 let mut out = String::new();
1514 out.push_str(&format!(
1515 "header: {:08x} {} {} {} {}\n",
1516 sig, version, hash_len, num_chunks, num_packs
1517 ));
1518 out.push_str("chunks:");
1519 for t in &chunk_tags {
1520 out.push(' ');
1521 out.push_str(t);
1522 }
1523 out.push('\n');
1524 out.push_str(&format!("num_objects: {num_objects}\n"));
1525 out.push_str("packs:\n");
1526 for n in &pack_names {
1527 out.push_str(n);
1528 out.push('\n');
1529 }
1530 out.push_str(&format!("object-dir: {}\n", objects_dir.display()));
1531 Ok(out)
1532}
1533
1534#[derive(Debug, Clone)]
1540pub struct MidxReuseTables {
1541 pub oids: Vec<ObjectId>,
1543 pub pack_and_offset: Vec<(u32, u64)>,
1545 pub rid_order: Vec<u32>,
1547 pub oid_idx_to_rank: Vec<u32>,
1549}
1550
1551pub fn load_midx_reuse_tables(objects_dir: &Path) -> Result<Option<MidxReuseTables>> {
1555 let pack_dir = objects_dir.join("pack");
1556 let Some(path) = resolve_tip_midx_path(&pack_dir) else {
1557 return Ok(None);
1558 };
1559 let data = fs::read(&path).map_err(Error::Io)?;
1560 let hash_len = midx_hash_len(&data);
1561 let (_, hdr_end, _) = parse_midx_header(&data)?;
1562 let (oidl_off, oid_l_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
1563 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1564 let Ok((ridx_off, ridx_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_REVINDEX) else {
1565 return Ok(None);
1566 };
1567 if oid_l_len % hash_len != 0 || ooff_len != oid_l_len / hash_len * 8 {
1568 return Err(Error::CorruptObject(
1569 "MIDX OID / offset chunk size mismatch".to_owned(),
1570 ));
1571 }
1572 let num_objects = oid_l_len / hash_len;
1573 if ridx_len != num_objects.saturating_mul(4) {
1574 return Err(Error::CorruptObject(
1575 "MIDX reverse index length does not match object count".to_owned(),
1576 ));
1577 }
1578 if num_objects == 0 {
1579 return Ok(None);
1580 }
1581
1582 let mut oids = Vec::with_capacity(num_objects);
1583 for i in 0..num_objects {
1584 let base = oidl_off + i * hash_len;
1585 oids.push(ObjectId::from_bytes(&data[base..base + hash_len])?);
1586 }
1587
1588 let mut pack_and_offset = Vec::with_capacity(num_objects);
1589 for i in 0..num_objects {
1590 let ob = ooff_off + i * 8;
1591 let pack_id = read_be_u32(&data, ob)?;
1592 let off32 = read_be_u32(&data, ob + 4)?;
1593 pack_and_offset.push((pack_id, u64::from(off32)));
1594 }
1595
1596 let mut rid_order = Vec::with_capacity(num_objects);
1597 for i in 0..num_objects {
1598 let base = ridx_off + i * 4;
1599 rid_order.push(read_be_u32(&data, base)?);
1600 }
1601
1602 let mut oid_idx_to_rank = vec![0u32; num_objects];
1603 for (rank, &oid_idx) in rid_order.iter().enumerate() {
1604 let idx = usize::try_from(oid_idx)
1605 .map_err(|_| Error::CorruptObject("bad MIDX reverse index entry".to_owned()))?;
1606 if idx >= num_objects {
1607 return Err(Error::CorruptObject(
1608 "MIDX reverse index out of range".to_owned(),
1609 ));
1610 }
1611 oid_idx_to_rank[idx] = u32::try_from(rank)
1612 .map_err(|_| Error::CorruptObject("too many MIDX objects".to_owned()))?;
1613 }
1614
1615 Ok(Some(MidxReuseTables {
1616 oids,
1617 pack_and_offset,
1618 rid_order,
1619 oid_idx_to_rank,
1620 }))
1621}
1622
1623impl MidxReuseTables {
1624 #[must_use]
1626 pub fn global_bitmap_bit(&self, oid: &ObjectId) -> Option<u32> {
1627 let oid_idx = self.oids.binary_search(oid).ok()?;
1628 Some(self.oid_idx_to_rank[oid_idx])
1629 }
1630
1631 #[must_use]
1636 pub fn canonical_pack(&self, oid: &ObjectId) -> Option<u32> {
1637 let oid_idx = self.oids.binary_search(oid).ok()?;
1638 Some(self.pack_and_offset[oid_idx].0)
1639 }
1640}
1641
1642#[derive(Debug, Clone, Copy)]
1644pub struct MidxBtmpPackRange {
1645 pub pack_id: u32,
1647 pub bitmap_pos: u32,
1649 pub bitmap_nr: u32,
1651}
1652
1653pub fn read_midx_btmp_ranges(objects_dir: &Path) -> Result<Vec<MidxBtmpPackRange>> {
1657 let pack_dir = objects_dir.join("pack");
1658 let Some(path) = resolve_tip_midx_path(&pack_dir) else {
1659 return Ok(Vec::new());
1660 };
1661 let data = fs::read(&path).map_err(Error::Io)?;
1662 let (_, hdr_end, _) = parse_midx_header(&data)?;
1663 let Ok((btmp_off, btmp_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_BITMAPPED_PACKS) else {
1664 return Ok(Vec::new());
1665 };
1666 if btmp_len == 0 || btmp_len % 8 != 0 {
1667 return Err(Error::CorruptObject(
1668 "invalid MIDX BTMP chunk length".to_owned(),
1669 ));
1670 }
1671 let num_packs = read_be_u32(&data, 8)?;
1672 let n_entries = btmp_len / 8;
1673 if u32::try_from(n_entries).ok() != Some(num_packs) {
1674 return Err(Error::CorruptObject(
1675 "MIDX BTMP entry count does not match num_packs".to_owned(),
1676 ));
1677 }
1678 let mut out = Vec::with_capacity(n_entries);
1679 for i in 0..n_entries {
1680 let base = btmp_off + i * 8;
1681 let bitmap_pos = read_be_u32(&data, base)?;
1682 let bitmap_nr = read_be_u32(&data, base + 4)?;
1683 out.push(MidxBtmpPackRange {
1684 pack_id: u32::try_from(i)
1685 .map_err(|_| Error::CorruptObject("too many packs in MIDX BTMP".to_owned()))?,
1686 bitmap_pos,
1687 bitmap_nr,
1688 });
1689 }
1690 Ok(out)
1691}
1692
1693pub fn format_midx_bitmapped_packs(objects_dir: &Path) -> Result<String> {
1698 let pack_dir = objects_dir.join("pack");
1699 let path = resolve_tip_midx_path(&pack_dir)
1700 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
1701 let data = fs::read(&path).map_err(Error::Io)?;
1702 let (_, hdr_end, _) = parse_midx_header(&data)?;
1703 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
1704 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
1705 let Ok((btmp_off, btmp_len)) = find_chunk(&data, hdr_end, MIDX_CHUNKID_BITMAPPED_PACKS) else {
1706 return Err(Error::CorruptObject(
1707 "MIDX does not contain the BTMP chunk".to_owned(),
1708 ));
1709 };
1710 let n_entries = btmp_len / 8;
1711 let mut out = String::new();
1712 for i in 0..n_entries {
1713 let base = btmp_off + i * 8;
1714 let bitmap_pos = read_be_u32(&data, base)?;
1715 let bitmap_nr = read_be_u32(&data, base + 4)?;
1716 let idx_name = names.get(i).ok_or_else(|| {
1717 Error::CorruptObject("BTMP entry has no corresponding pack name".to_owned())
1718 })?;
1719 let stem = idx_name.strip_suffix(".idx").unwrap_or(idx_name);
1720 out.push_str(&format!("{stem}.pack\n"));
1721 out.push_str(&format!(" bitmap_pos: {bitmap_pos}\n"));
1722 out.push_str(&format!(" bitmap_nr: {bitmap_nr}\n"));
1723 }
1724 Ok(out)
1725}
1726
1727pub fn midx_lookup_pack_and_offset(objects_dir: &Path, oid: &ObjectId) -> Result<(u32, u64)> {
1729 let pack_dir = objects_dir.join("pack");
1730 let path = resolve_tip_midx_path(&pack_dir)
1731 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
1732 let data = fs::read(&path).map_err(Error::Io)?;
1733 let hash_len = midx_hash_len(&data);
1734 let (_, hdr_end, _) = parse_midx_header(&data)?;
1735 let (fanout_off, fanout_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDFANOUT)?;
1736 let (oidl_off, oid_l_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OIDLOOKUP)?;
1737 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
1738 if fanout_len != 256 * 4 || oid_l_len % hash_len != 0 || ooff_len != oid_l_len / hash_len * 8 {
1739 return Err(Error::CorruptObject("truncated MIDX OID chunks".to_owned()));
1740 }
1741 let num_objects = oid_l_len / hash_len;
1742 let first = oid.as_bytes()[0] as usize;
1743 let j0 = if first == 0 {
1744 0usize
1745 } else {
1746 read_be_u32(&data, fanout_off + (first - 1) * 4)? as usize
1747 };
1748 let j1 = read_be_u32(&data, fanout_off + first * 4)? as usize;
1749 let mut lo = j0;
1750 let mut hi = j1;
1751 while lo < hi {
1752 let mid = (lo + hi) / 2;
1753 let base = oidl_off + mid * hash_len;
1754 let cmp = data[base..base + hash_len].cmp(oid.as_bytes());
1755 if cmp == std::cmp::Ordering::Less {
1756 lo = mid + 1;
1757 } else {
1758 hi = mid;
1759 }
1760 }
1761 if lo >= num_objects {
1762 return Err(Error::CorruptObject(format!(
1763 "object {} not in multi-pack-index",
1764 oid.to_hex()
1765 )));
1766 }
1767 let base = oidl_off + lo * hash_len;
1768 if data[base..base + hash_len] != *oid.as_bytes() {
1769 return Err(Error::CorruptObject(format!(
1770 "object {} not in multi-pack-index",
1771 oid.to_hex()
1772 )));
1773 }
1774 let ob = ooff_off + lo * 8;
1775 let pack_id = read_be_u32(&data, ob)?;
1776 let off32 = read_be_u32(&data, ob + 4)?;
1777 Ok((pack_id, u64::from(off32)))
1778}
1779
1780pub fn midx_oid_listed_in_tip(objects_dir: &Path, oid: &ObjectId) -> Result<Option<bool>> {
1784 let pack_dir = objects_dir.join("pack");
1785 let Some(midx_path) = resolve_tip_midx_path(&pack_dir) else {
1786 return Ok(None);
1787 };
1788 let data = midx_cache::get_bytes(&midx_path)?;
1789 let hash_len = midx_hash_len(&data);
1790 let MidxReadView {
1791 oidf_off,
1792 oidl_off,
1793 num_objects,
1794 ..
1795 } = match midx_load_for_read(&data, repo_midx_hash_version_for_objects_dir(objects_dir)) {
1796 MidxLoadResult::Ok(v) => v,
1797 MidxLoadResult::Skip => return Ok(None),
1798 };
1799
1800 let first = oid.as_bytes()[0] as usize;
1801 let lo = if first == 0 {
1802 0u32
1803 } else {
1804 read_be_u32(&data, oidf_off + (first - 1) * 4)?
1805 };
1806 let hi = read_be_u32(&data, oidf_off + first * 4)?;
1807
1808 let mut i = lo as usize;
1809 while i < hi as usize && i < num_objects {
1810 let o =
1811 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])?;
1812 match o.cmp(oid) {
1813 std::cmp::Ordering::Equal => return Ok(Some(true)),
1814 std::cmp::Ordering::Greater => return Ok(Some(false)),
1815 std::cmp::Ordering::Less => i += 1,
1816 }
1817 }
1818 Ok(Some(false))
1819}
1820
1821struct MidxReadView {
1823 oidf_off: usize,
1824 oidl_off: usize,
1825 ooff_off: usize,
1826 loff: Option<(usize, usize)>,
1827 num_objects: usize,
1828 pack_names: Vec<String>,
1829}
1830
1831enum MidxLoadResult {
1832 Ok(MidxReadView),
1833 Skip,
1836}
1837
1838fn midx_warn_once(line: &str) {
1844 use std::sync::Mutex;
1845 use std::sync::OnceLock;
1846 static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
1847 let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new()));
1848 if let Ok(mut set) = seen.lock() {
1849 if set.insert(line.to_string()) {
1850 eprintln!("{line}");
1851 }
1852 } else {
1853 eprintln!("{line}");
1854 }
1855}
1856
1857fn midx_die(lines: &[&str]) -> ! {
1860 use std::io::Write;
1861 let mut err = std::io::stderr().lock();
1862 let n = lines.len();
1863 for (i, l) in lines.iter().enumerate() {
1864 if i + 1 == n {
1865 let _ = writeln!(err, "fatal: {l}");
1866 } else {
1867 let _ = writeln!(err, "error: {l}");
1868 }
1869 }
1870 let _ = err.flush();
1871 std::process::exit(128);
1872}
1873
1874fn midx_load_for_read(data: &[u8], expected_hash_version: u8) -> MidxLoadResult {
1878 if data.len() < MIDX_HEADER_SIZE + 20 {
1879 return MidxLoadResult::Skip;
1880 }
1881 let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
1882 if sig != MIDX_SIGNATURE {
1883 midx_die(&[&format!(
1884 "multi-pack-index signature 0x{sig:08x} does not match signature 0x{MIDX_SIGNATURE:08x}"
1885 )]);
1886 }
1887 let version = data[4];
1888 if version != MIDX_VERSION_V1 && version != MIDX_VERSION_V2 {
1889 midx_die(&[&format!(
1890 "multi-pack-index version {version} not recognized"
1891 )]);
1892 }
1893 let hash_version = data[5];
1894 if hash_version != expected_hash_version {
1895 midx_warn_once(&format!(
1899 "error: multi-pack-index hash version {hash_version} does not match version {expected_hash_version}"
1900 ));
1901 return MidxLoadResult::Skip;
1902 }
1903 let hash_len = if hash_version == 2 { 32usize } else { 20usize };
1904 let num_packs = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
1905
1906 let mut toc_errors: Vec<String> = Vec::new();
1910 let chunks = match parse_midx_toc(data, hash_len, &mut toc_errors) {
1911 Ok(c) => c,
1912 Err(_) => {
1913 for e in &toc_errors {
1914 midx_warn_once(&format!("error: {e}"));
1915 }
1916 return MidxLoadResult::Skip;
1917 }
1918 };
1919
1920 let Some((pn_off, pn_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_PACKNAMES)
1922 else {
1923 midx_die(&["multi-pack-index required pack-name chunk missing or corrupted"]);
1924 };
1925
1926 let Some((oidf_off, oidf_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDFANOUT)
1928 else {
1929 midx_die(&["multi-pack-index required OID fanout chunk missing or corrupted"]);
1930 };
1931 if oidf_len != 256 * 4 {
1932 midx_die(&[
1933 "multi-pack-index OID fanout is of the wrong size",
1934 "multi-pack-index required OID fanout chunk missing or corrupted",
1935 ]);
1936 }
1937 let fanout = |i: usize| -> u32 {
1938 let b = oidf_off + i * 4;
1939 u32::from_be_bytes([data[b], data[b + 1], data[b + 2], data[b + 3]])
1940 };
1941 for i in 0..255 {
1942 let f1 = fanout(i);
1943 let f2 = fanout(i + 1);
1944 if f1 > f2 {
1945 midx_die(&[
1946 &format!(
1947 "oid fanout out of order: fanout[{i}] = {f1:x} > {f2:x} = fanout[{}]",
1948 i + 1
1949 ),
1950 "multi-pack-index required OID fanout chunk missing or corrupted",
1951 ]);
1952 }
1953 }
1954 let num_objects = fanout(255) as usize;
1955
1956 let Some((oidl_off, oidl_len)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OIDLOOKUP)
1958 else {
1959 midx_die(&["multi-pack-index required OID lookup chunk missing or corrupted"]);
1960 };
1961 if oidl_len != hash_len * num_objects {
1962 midx_die(&[
1963 "multi-pack-index OID lookup chunk is the wrong size",
1964 "multi-pack-index required OID lookup chunk missing or corrupted",
1965 ]);
1966 }
1967
1968 let Some((ooff_off, ooff_len)) =
1970 toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_OBJECTOFFSETS)
1971 else {
1972 midx_die(&["multi-pack-index required object offsets chunk missing or corrupted"]);
1973 };
1974 if ooff_len != num_objects * 8 {
1975 midx_die(&[
1976 "multi-pack-index object offset chunk is the wrong size",
1977 "multi-pack-index required object offsets chunk missing or corrupted",
1978 ]);
1979 }
1980
1981 let loff = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_LARGEOFFSETS);
1982
1983 if let Some((_, rlen)) = toc_chunk_range(&chunks, data.len(), MIDX_CHUNKID_REVINDEX) {
1985 if rlen != num_objects * 4 {
1986 midx_warn_once("error: multi-pack-index reverse-index chunk is the wrong size");
1987 midx_warn_once("warning: multi-pack bitmap is missing required reverse index");
1988 }
1989 }
1990
1991 let mut pack_names: Vec<String> = Vec::with_capacity(num_packs);
1993 let blob = &data[pn_off..pn_off + pn_len];
1994 let mut start = 0usize;
1995 for _ in 0..num_packs {
1996 let Some(rel) = blob[start..].iter().position(|&b| b == 0) else {
1997 midx_die(&["multi-pack-index pack-name chunk is too short"]);
1998 };
1999 let name = match std::str::from_utf8(&blob[start..start + rel]) {
2000 Ok(s) => s.to_string(),
2001 Err(_) => midx_die(&["multi-pack-index pack-name chunk is too short"]),
2002 };
2003 if version == MIDX_VERSION_V1
2004 && !pack_names.is_empty()
2005 && name.as_str() <= pack_names.last().map(|s| s.as_str()).unwrap_or("")
2006 {
2007 midx_die(&[&format!(
2008 "multi-pack-index pack names out of order: '{}' before '{name}'",
2009 pack_names.last().cloned().unwrap_or_default()
2010 )]);
2011 }
2012 pack_names.push(name);
2013 start += rel + 1;
2014 }
2015
2016 MidxLoadResult::Ok(MidxReadView {
2017 oidf_off,
2018 oidl_off,
2019 ooff_off,
2020 loff,
2021 num_objects,
2022 pack_names,
2023 })
2024}
2025
2026pub fn validate_midx_referenced_packs(objects_dir: &Path) {
2035 use std::sync::Mutex;
2036 use std::sync::OnceLock;
2037 static DONE: OnceLock<Mutex<HashSet<std::path::PathBuf>>> = OnceLock::new();
2038 let done = DONE.get_or_init(|| Mutex::new(HashSet::new()));
2039 if let Ok(mut set) = done.lock() {
2040 if !set.insert(objects_dir.to_path_buf()) {
2041 return;
2042 }
2043 }
2044
2045 let pack_dir = objects_dir.join("pack");
2046 let Some(midx_path) = resolve_tip_midx_path(&pack_dir) else {
2047 return;
2048 };
2049 let Ok(data) = fs::read(&midx_path) else {
2050 return;
2051 };
2052 let MidxReadView { pack_names, .. } =
2053 match midx_load_for_read(&data, repo_midx_hash_version_for_objects_dir(objects_dir)) {
2054 MidxLoadResult::Ok(v) => v,
2055 MidxLoadResult::Skip => return,
2056 };
2057 for idx_name in &pack_names {
2058 let idx_path = pack_dir.join(idx_name);
2059 if !idx_path.exists() {
2063 continue;
2064 }
2065 if crate::pack::read_pack_index_no_verify(&idx_path).is_err() {
2071 let mut pack_path = idx_path.clone();
2072 pack_path.set_extension("pack");
2073 midx_warn_once(&format!(
2074 "error: packfile {} index unavailable",
2075 pack_path.display()
2076 ));
2077 }
2078 }
2079}
2080
2081pub fn try_read_object_via_midx(
2086 objects_dir: &Path,
2087 oid: &ObjectId,
2088) -> Result<Option<crate::objects::Object>> {
2089 let pack_dir = objects_dir.join("pack");
2090 let Some(midx_path) = resolve_tip_midx_path(&pack_dir) else {
2091 return Ok(None);
2092 };
2093 let data = midx_cache::get_bytes(&midx_path)?;
2094
2095 let MidxReadView {
2099 oidf_off,
2100 oidl_off,
2101 ooff_off,
2102 loff,
2103 num_objects,
2104 pack_names,
2105 } = match midx_load_for_read(&data, repo_midx_hash_version_for_objects_dir(objects_dir)) {
2106 MidxLoadResult::Ok(v) => v,
2107 MidxLoadResult::Skip => return Ok(None),
2108 };
2109
2110 let first = oid.as_bytes()[0] as usize;
2111 let lo = if first == 0 {
2112 0u32
2113 } else {
2114 read_be_u32(&data, oidf_off + (first - 1) * 4)?
2115 };
2116 let hi = read_be_u32(&data, oidf_off + first * 4)?;
2117
2118 let hash_len = midx_hash_len(&data);
2119 let mut pos = None;
2120 let mut i = lo as usize;
2121 while i < hi as usize && i < num_objects {
2122 let o =
2123 ObjectId::from_bytes(&data[oidl_off + i * hash_len..oidl_off + (i + 1) * hash_len])?;
2124 let c = o.cmp(oid);
2125 if c == std::cmp::Ordering::Equal {
2126 pos = Some(i);
2127 break;
2128 }
2129 if c == std::cmp::Ordering::Greater {
2130 break;
2131 }
2132 i += 1;
2133 }
2134 let Some(pos) = pos else {
2135 return Ok(None);
2136 };
2137
2138 let obase = ooff_off + pos * 8;
2139 let pack_id = read_be_u32(&data, obase)?;
2140 let raw_off = read_be_u32(&data, obase + 4)?;
2141 let _offset = if (raw_off & MIDX_LARGE_OFFSET_NEEDED) != 0 {
2142 let idx = (raw_off & !MIDX_LARGE_OFFSET_NEEDED) as usize;
2143 let need = (idx + 1) * 8;
2144 match loff {
2145 Some((loff_off, loff_len)) if loff_len >= need => {
2146 read_be_u64(&data, loff_off + idx * 8)?
2147 }
2148 _ => {
2149 midx_die(&["multi-pack-index large offset out of bounds"]);
2151 }
2152 }
2153 } else {
2154 u64::from(raw_off)
2155 };
2156
2157 let idx_name = pack_names
2158 .get(pack_id as usize)
2159 .ok_or_else(|| Error::CorruptObject("bad pack-int-id".to_owned()))?;
2160 let idx_path = pack_dir.join(idx_name);
2161 if !idx_path.exists() {
2166 return Ok(None);
2167 }
2168 let idx = match crate::pack::read_pack_index_cached(&idx_path) {
2176 Ok(idx) => idx,
2177 Err(_) => {
2178 let mut pack_path = idx_path.clone();
2179 pack_path.set_extension("pack");
2180 midx_warn_once(&format!(
2181 "error: packfile {} index unavailable",
2182 pack_path.display()
2183 ));
2184 return Ok(None);
2185 }
2186 };
2187 crate::pack::read_object_from_pack(&idx, oid).map(Some)
2188}
2189
2190pub fn read_midx_preferred_idx_name(objects_dir: &Path) -> Result<String> {
2191 let pack_dir = objects_dir.join("pack");
2192 let path = resolve_tip_midx_path(&pack_dir)
2193 .ok_or_else(|| Error::CorruptObject("no multi-pack-index found".to_owned()))?;
2194 let data = fs::read(&path).map_err(Error::Io)?;
2195 let (_, hdr_end, _) = parse_midx_header(&data)?;
2196 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2197 let names = parse_pack_names_blob(&data[pn_off..pn_off + pn_len])?;
2198 let (ooff_off, ooff_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_OBJECTOFFSETS)?;
2199 let (ridx_off, ridx_len) = match find_chunk(&data, hdr_end, MIDX_CHUNKID_REVINDEX) {
2205 Ok(r) => r,
2206 Err(_) => {
2207 return Err(Error::CorruptObject(
2208 "could not determine MIDX preferred pack".to_owned(),
2209 ));
2210 }
2211 };
2212
2213 if ridx_len < 4 || ooff_len < 8 {
2214 return Err(Error::CorruptObject("truncated MIDX RIDX/OOFF".to_owned()));
2215 }
2216 let first_oid_idx = read_be_u32(&data, ridx_off)? as usize;
2217 let entry_base = ooff_off + first_oid_idx * 8;
2218 if entry_base + 8 > data.len() || entry_base + 8 > ooff_off + ooff_len {
2219 return Err(Error::CorruptObject(
2220 "bad MIDX object-offsets index".to_owned(),
2221 ));
2222 }
2223 let pack_id = read_be_u32(&data, entry_base)?;
2224 let idx = usize::try_from(pack_id)
2225 .map_err(|_| Error::CorruptObject("pack id overflow in multi-pack-index".to_owned()))?;
2226 names
2227 .get(idx)
2228 .cloned()
2229 .ok_or_else(|| Error::CorruptObject("preferred pack id out of range".to_owned()))
2230}
2231
2232pub fn clear_pack_midx_state(pack_dir: &Path) -> Result<()> {
2239 let _ = fs::remove_file(pack_dir.join("multi-pack-index"));
2240 scrub_root_midx_sidecars_except(pack_dir, None)?;
2241 let midx_d = midx_d_dir(pack_dir);
2242 if midx_d.exists() {
2243 let _ = fs::remove_dir_all(&midx_d);
2244 }
2245 Ok(())
2246}
2247
2248pub fn write_multi_pack_index(pack_dir: &Path) -> Result<()> {
2249 write_multi_pack_index_with_options(pack_dir, &WriteMultiPackIndexOptions::default())
2250}
2251
2252pub fn write_multi_pack_index_with_options(
2254 pack_dir: &Path,
2255 opts: &WriteMultiPackIndexOptions,
2256) -> Result<()> {
2257 if opts.pack_names_subset_ordered.is_none() {
2260 if let Some(existing) = resolve_tip_midx_path(pack_dir) {
2261 if let Ok(bytes) = fs::read(&existing) {
2262 if midx_checksum_is_valid(&bytes) {
2263 if let Ok((_, existing_names)) = oids_and_packs_from_midx_data(&bytes) {
2267 for (i, name) in existing_names.iter().enumerate() {
2268 let stem = name.strip_suffix(".idx").unwrap_or(name);
2269 if !pack_dir.join(format!("{stem}.pack")).exists() {
2270 eprintln!("error: could not load pack {i}");
2271 return Err(Error::CorruptObject(format!(
2272 "could not load pack {i}"
2273 )));
2274 }
2275 }
2276 }
2277 } else {
2278 eprintln!("warning: ignoring existing multi-pack-index; checksum mismatch");
2279 }
2280 }
2281 }
2282 }
2283
2284 let mut idx_names: Vec<String> = fs::read_dir(pack_dir)
2289 .map(|rd| {
2290 rd.filter_map(|e| e.ok())
2291 .filter_map(|e| {
2292 let name = e.file_name().to_string_lossy().to_string();
2293 let stem = name.strip_suffix(".idx")?;
2294 if pack_dir.join(format!("{stem}.pack")).exists() {
2295 Some(name)
2296 } else {
2297 None
2298 }
2299 })
2300 .collect()
2301 })
2302 .unwrap_or_default();
2303 idx_names.sort();
2304
2305 let idx_names: Vec<String> = if let Some(sub) = &opts.pack_names_subset_ordered {
2306 let mut out = Vec::new();
2307 for line in sub {
2308 let want = normalize_pack_idx_basename(line)?;
2309 if let Some(found) = idx_names.iter().find(|n| **n == want).cloned() {
2310 if !out.contains(&found) {
2311 out.push(found);
2312 }
2313 }
2314 }
2317 out
2318 } else {
2319 idx_names
2320 };
2321
2322 let mut preferred_warned = false;
2325 if let Some(raw) = opts.preferred_pack_name.as_deref() {
2326 if opts.preferred_pack_idx.is_none()
2327 && !idx_names
2328 .iter()
2329 .any(|n| cmp_idx_or_pack_name(raw, n).is_eq())
2330 {
2331 eprintln!("warning: unknown preferred pack: '{raw}'");
2332 preferred_warned = true;
2333 }
2334 }
2335
2336 if idx_names.is_empty() {
2337 eprintln!("error: no pack files to index.");
2339 return Err(Error::CorruptObject("no pack files to index.".to_owned()));
2340 }
2341
2342 let (base_oids, base_pack_names) = if opts.incremental {
2343 collect_incremental_base(pack_dir)?
2344 } else {
2345 (HashSet::new(), HashSet::new())
2346 };
2347
2348 let layer_idx_names: Vec<String> = if opts.incremental {
2349 idx_names
2350 .iter()
2351 .filter(|n| {
2352 !base_pack_names
2353 .iter()
2354 .any(|bp| pack_names_match_layer(bp, n))
2355 })
2356 .cloned()
2357 .collect()
2358 } else {
2359 idx_names.clone()
2360 };
2361
2362 if opts.incremental && layer_idx_names.is_empty() {
2363 return Ok(());
2364 }
2365
2366 let work_names = if opts.incremental {
2367 &layer_idx_names[..]
2368 } else {
2369 &idx_names[..]
2370 };
2371
2372 let mut preferred_idx = opts.preferred_pack_idx.map(|p| p as usize);
2373 if preferred_idx.is_none() && !preferred_warned {
2374 if let Some(raw) = opts.preferred_pack_name.as_deref() {
2375 preferred_idx = work_names
2377 .iter()
2378 .position(|n| cmp_idx_or_pack_name(raw, n).is_eq());
2379 }
2380 }
2381 if preferred_idx.is_none() && opts.write_bitmap_placeholders && !work_names.is_empty() {
2382 preferred_idx = preferred_pack_index_by_mtime(pack_dir, work_names)?;
2383 }
2384 if let Some(p) = preferred_idx {
2385 if p >= work_names.len() {
2386 return Err(Error::CorruptObject(
2387 "preferred pack index out of range".to_owned(),
2388 ));
2389 }
2390 }
2391
2392 let mut indexes: Vec<PackIndex> = Vec::with_capacity(work_names.len());
2393 for name in work_names {
2394 let path = pack_dir.join(name);
2395 indexes.push(crate::pack::read_pack_index_no_verify(&path)?);
2398 }
2399
2400 if let Some(p) = preferred_idx {
2402 if indexes.get(p).map(|i| i.entries.len()).unwrap_or(0) == 0 {
2403 let name = work_names.get(p).cloned().unwrap_or_default();
2404 let pack_name = name.strip_suffix(".idx").unwrap_or(&name);
2405 eprintln!("error: cannot select preferred pack {pack_name}.pack with no objects");
2406 return Err(Error::CorruptObject(
2407 "cannot select preferred pack with no objects".to_owned(),
2408 ));
2409 }
2410 }
2411
2412 let pack_mtimes_layer: Vec<std::time::SystemTime> =
2413 indexes.iter().map(pack_mtime_for_midx).collect();
2414 let preferred_u32 = preferred_idx.map(|p| p as u32);
2415 let select_hash_len = if repo_midx_hash_version(pack_dir) == 2 {
2416 32
2417 } else {
2418 20
2419 };
2420
2421 let mut best: HashMap<ObjectId, MidxEntry> = HashMap::new();
2422 for (pack_id, idx) in indexes.iter().enumerate() {
2423 let pack_id = u32::try_from(pack_id).map_err(|_| {
2424 Error::CorruptObject("too many pack files for multi-pack-index".to_owned())
2425 })?;
2426 let mtime = pack_mtimes_layer[pack_id as usize];
2427 for e in &idx.entries {
2428 if e.oid.len() != select_hash_len {
2429 continue;
2430 }
2431 let Ok(oid) = ObjectId::from_bytes(&e.oid) else {
2432 continue;
2433 };
2434 if opts.incremental && base_oids.contains(&oid) {
2435 continue;
2436 }
2437 let cand = MidxEntry {
2438 oid,
2439 pack_id,
2440 offset: e.offset,
2441 pack_mtime: mtime,
2442 };
2443 match best.get(&oid) {
2444 None => {
2445 best.insert(oid, cand);
2446 }
2447 Some(cur) => {
2448 if midx_pick_better_entry(cur, pack_id, e.offset, mtime, preferred_u32) {
2449 best.insert(oid, cand);
2450 }
2451 }
2452 }
2453 }
2454 }
2455
2456 let bitmap_placeholders =
2457 opts.write_bitmap_placeholders && (!opts.incremental || !best.is_empty());
2458
2459 let omit_embedded_ridx = opts.write_rev_placeholder;
2460 let exclude = if opts.incremental && !base_oids.is_empty() {
2464 Some(&base_oids)
2465 } else {
2466 None
2467 };
2468 let (out, rev_sidecar_order) = build_midx_bytes_filtered(
2469 work_names,
2470 &indexes,
2471 preferred_idx,
2472 bitmap_placeholders,
2473 omit_embedded_ridx,
2474 opts.version.unwrap_or(MIDX_VERSION_V2),
2475 repo_midx_hash_version(pack_dir),
2476 exclude,
2477 )?;
2478
2479 let hash_len = if repo_midx_hash_version(pack_dir) == 2 {
2480 32
2481 } else {
2482 20
2483 };
2484 let hash = &out[out.len() - hash_len..];
2485 let hash_hex = hex::encode(hash);
2486 let hash_arr: Vec<u8> = hash.to_vec();
2487
2488 if opts.incremental {
2489 let root_midx = pack_dir.join("multi-pack-index");
2490 let chain_path = chain_file_path(pack_dir);
2491 let chain_existed = chain_path.exists();
2492
2493 let mut chain = if root_midx.exists() && !chain_existed {
2494 let root_hex = midx_checksum_hex_from_path(&root_midx)?;
2495 link_root_midx_into_chain(pack_dir, &root_hex)?;
2496 vec![root_hex]
2497 } else {
2498 read_chain_layer_hashes(pack_dir).unwrap_or_default()
2499 };
2500
2501 chain.push(hash_hex.clone());
2502
2503 let midx_d = midx_d_dir(pack_dir);
2504 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
2505
2506 let layer_path = midx_d.join(format!("multi-pack-index-{hash_hex}.midx"));
2507 fs::write(&layer_path, &out).map_err(Error::Io)?;
2508
2509 let mut chain_data = String::new();
2510 for h in &chain {
2511 chain_data.push_str(h);
2512 chain_data.push('\n');
2513 }
2514 fs::write(chain_file_path(pack_dir), chain_data.as_bytes()).map_err(Error::Io)?;
2515
2516 clear_stale_split_layers(pack_dir, &chain)?;
2517
2518 let _ = fs::remove_file(pack_dir.join("multi-pack-index"));
2519 scrub_root_midx_sidecars(pack_dir)?;
2520 if bitmap_placeholders {
2521 let full = hex::encode(hash);
2522 fs::write(midx_d.join(format!("multi-pack-index-{full}.bitmap")), [])
2523 .map_err(Error::Io)?;
2524 if opts.write_rev_placeholder {
2525 let rev_path = midx_d.join(format!("multi-pack-index-{full}.rev"));
2526 if let Some(order) = rev_sidecar_order.as_ref() {
2527 write_midx_rev_sidecar(&rev_path, order, &hash_arr)?;
2528 } else {
2529 fs::write(rev_path, []).map_err(Error::Io)?;
2530 }
2531 }
2532 }
2533 } else {
2534 let dest = pack_dir.join("multi-pack-index");
2540
2541 let bitmap_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.bitmap"));
2545 let bitmap_ok = !opts.write_bitmap_placeholders || bitmap_path.exists();
2546 if bitmap_ok && !chain_file_path(pack_dir).exists() {
2550 if let Ok(existing) = fs::read(&dest) {
2551 if existing == out {
2552 return Ok(());
2553 }
2554 }
2555 }
2556
2557 clear_incremental_midx_files(pack_dir)?;
2558
2559 fs::write(&dest, &out).map_err(Error::Io)?;
2560
2561 scrub_root_midx_sidecars_except(pack_dir, Some(&hash_hex))?;
2562
2563 if opts.write_bitmap_placeholders {
2564 fs::write(
2565 pack_dir.join(format!("multi-pack-index-{hash_hex}.bitmap")),
2566 [],
2567 )
2568 .map_err(Error::Io)?;
2569 if opts.write_rev_placeholder {
2570 let rev_path = pack_dir.join(format!("multi-pack-index-{hash_hex}.rev"));
2571 if let Some(order) = rev_sidecar_order.as_ref() {
2572 write_midx_rev_sidecar(&rev_path, order, &hash_arr)?;
2573 } else {
2574 fs::write(rev_path, []).map_err(Error::Io)?;
2575 }
2576 }
2577 }
2578 }
2579
2580 midx_cache::evict_pack_dir(pack_dir);
2581 Ok(())
2582}
2583
2584fn pack_names_match_layer(base_name: &str, disk_idx: &str) -> bool {
2585 if base_name == disk_idx {
2586 return true;
2587 }
2588 cmp_idx_or_pack_name(disk_idx, base_name).is_eq()
2589}
2590
2591#[derive(Debug)]
2594pub enum CompactError {
2595 NoChain,
2597 MissingEndpoint(String),
2600 IdenticalEndpoints,
2602 NotAncestor(String, String),
2605 V1Format,
2607 Other(String),
2609}
2610
2611impl std::fmt::Display for CompactError {
2612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2613 match self {
2614 CompactError::NoChain => write!(f, "no multi-pack-index chain to compact"),
2615 CompactError::MissingEndpoint(s) => write!(f, "could not find MIDX: {s}"),
2616 CompactError::IdenticalEndpoints => {
2617 write!(f, "MIDX compaction endpoints must be unique")
2618 }
2619 CompactError::NotAncestor(from, to) => {
2620 write!(f, "MIDX {from} must be an ancestor of {to}")
2621 }
2622 CompactError::V1Format => write!(f, "cannot perform MIDX compaction with v1 format"),
2623 CompactError::Other(s) => write!(f, "{s}"),
2624 }
2625 }
2626}
2627
2628impl From<Error> for CompactError {
2629 fn from(e: Error) -> Self {
2630 CompactError::Other(e.to_string())
2631 }
2632}
2633
2634fn collect_layer_oids(pack_dir: &Path, hashes: &[String]) -> Result<HashSet<ObjectId>> {
2637 let mut oids = HashSet::new();
2638 for h in hashes {
2639 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{h}.midx"));
2640 let data = load_midx_file(&p)?;
2641 let (layer_oids, _) = oids_and_packs_from_midx_data(&data)?;
2642 oids.extend(layer_oids);
2643 }
2644 Ok(oids)
2645}
2646
2647fn layer_pack_names(pack_dir: &Path, hash: &str) -> Result<Vec<String>> {
2649 let p = midx_d_dir(pack_dir).join(format!("multi-pack-index-{hash}.midx"));
2650 let data = load_midx_file(&p)?;
2651 let (_, hdr_end, _) = parse_midx_header(&data)?;
2652 let (pn_off, pn_len) = find_chunk(&data, hdr_end, MIDX_CHUNKID_PACKNAMES)?;
2653 parse_pack_names_blob(&data[pn_off..pn_off + pn_len])
2654}
2655
2656pub fn compact_multi_pack_index(
2665 pack_dir: &Path,
2666 from_arg: &str,
2667 to_arg: &str,
2668 write_bitmaps: bool,
2669 write_rev: bool,
2670 version: Option<u8>,
2671) -> std::result::Result<(), CompactError> {
2672 if version == Some(MIDX_VERSION_V1) {
2673 return Err(CompactError::V1Format);
2674 }
2675
2676 let chain = read_chain_layer_hashes(pack_dir).map_err(|_| CompactError::NoChain)?;
2677 if chain.is_empty() {
2678 return Err(CompactError::NoChain);
2679 }
2680
2681 let from_hex = from_arg.to_ascii_lowercase();
2682 let to_hex = to_arg.to_ascii_lowercase();
2683
2684 let from_pos = chain.iter().position(|h| *h == from_hex);
2685 let to_pos = chain.iter().position(|h| *h == to_hex);
2686
2687 let Some(from_pos) = from_pos else {
2689 return Err(CompactError::MissingEndpoint(from_arg.to_string()));
2690 };
2691 let Some(to_pos) = to_pos else {
2692 return Err(CompactError::MissingEndpoint(to_arg.to_string()));
2693 };
2694
2695 if from_pos == to_pos {
2696 return Err(CompactError::IdenticalEndpoints);
2697 }
2698 if from_pos > to_pos {
2702 return Err(CompactError::NotAncestor(
2703 from_arg.to_string(),
2704 to_arg.to_string(),
2705 ));
2706 }
2707
2708 let base_hashes = &chain[..from_pos];
2711 let merged_hashes = &chain[from_pos..=to_pos];
2712 let upper_hashes = &chain[to_pos + 1..];
2713
2714 let base_oids = collect_layer_oids(pack_dir, base_hashes)?;
2715
2716 let mut ordered_idx_names: Vec<String> = Vec::new();
2719 for h in merged_hashes {
2720 for name in layer_pack_names(pack_dir, h)? {
2721 if !ordered_idx_names.contains(&name) {
2722 ordered_idx_names.push(name);
2723 }
2724 }
2725 }
2726
2727 if ordered_idx_names.is_empty() {
2728 return Err(CompactError::Other(
2729 "no packs found in compaction range".to_owned(),
2730 ));
2731 }
2732
2733 let mut indexes: Vec<PackIndex> = Vec::with_capacity(ordered_idx_names.len());
2735 for name in &ordered_idx_names {
2736 let path = pack_dir.join(name);
2737 indexes.push(crate::pack::read_pack_index_no_verify(&path)?);
2738 }
2739
2740 let preferred_idx = if write_bitmaps { Some(0usize) } else { None };
2743
2744 let exclude = if base_oids.is_empty() {
2745 None
2746 } else {
2747 Some(&base_oids)
2748 };
2749
2750 let (out, rev_sidecar_order) = build_midx_bytes_filtered(
2751 &ordered_idx_names,
2752 &indexes,
2753 preferred_idx,
2754 write_bitmaps,
2755 write_rev,
2756 version.unwrap_or(MIDX_VERSION_V2),
2757 repo_midx_hash_version(pack_dir),
2758 exclude,
2759 )?;
2760
2761 let hash_len = if repo_midx_hash_version(pack_dir) == 2 {
2762 32
2763 } else {
2764 20
2765 };
2766 let hash = &out[out.len() - hash_len..];
2767 let hash_hex = hex::encode(hash);
2768 let hash_arr: Vec<u8> = hash.to_vec();
2769
2770 let midx_d = midx_d_dir(pack_dir);
2771 fs::create_dir_all(&midx_d).map_err(Error::Io)?;
2772
2773 let layer_path = midx_d.join(format!("multi-pack-index-{hash_hex}.midx"));
2774 fs::write(&layer_path, &out).map_err(Error::Io)?;
2775
2776 let mut new_chain: Vec<String> = Vec::new();
2778 new_chain.extend(base_hashes.iter().cloned());
2779 new_chain.push(hash_hex.clone());
2780 new_chain.extend(upper_hashes.iter().cloned());
2781
2782 let mut chain_data = String::new();
2783 for h in &new_chain {
2784 chain_data.push_str(h);
2785 chain_data.push('\n');
2786 }
2787 fs::write(chain_file_path(pack_dir), chain_data.as_bytes()).map_err(Error::Io)?;
2788
2789 if write_bitmaps {
2790 fs::write(
2791 midx_d.join(format!("multi-pack-index-{hash_hex}.bitmap")),
2792 [],
2793 )
2794 .map_err(Error::Io)?;
2795 let rev_path = midx_d.join(format!("multi-pack-index-{hash_hex}.rev"));
2796 if write_rev {
2797 if let Some(order) = rev_sidecar_order.as_ref() {
2798 write_midx_rev_sidecar(&rev_path, order, &hash_arr)?;
2799 } else {
2800 fs::write(rev_path, []).map_err(Error::Io)?;
2801 }
2802 }
2803 }
2804
2805 clear_stale_split_layers(pack_dir, &new_chain)?;
2807
2808 midx_cache::evict_pack_dir(pack_dir);
2809 Ok(())
2810}
2811
2812fn scrub_root_midx_sidecars(pack_dir: &Path) -> Result<()> {
2813 scrub_root_midx_sidecars_except(pack_dir, None)
2814}
2815
2816fn scrub_root_midx_sidecars_except(pack_dir: &Path, keep_hex: Option<&str>) -> Result<()> {
2817 let Ok(rd) = fs::read_dir(pack_dir) else {
2818 return Ok(());
2819 };
2820 for ent in rd {
2821 let ent = ent.map_err(Error::Io)?;
2822 let name = ent.file_name().to_string_lossy().to_string();
2823 let Some(rest) = name.strip_prefix("multi-pack-index-") else {
2824 continue;
2825 };
2826 if !(rest.ends_with(".bitmap") || rest.ends_with(".rev")) {
2827 continue;
2828 }
2829 let hash_part = rest
2830 .strip_suffix(".bitmap")
2831 .or_else(|| rest.strip_suffix(".rev"))
2832 .unwrap_or(rest);
2833 if keep_hex.is_some_and(|k| k == hash_part) {
2837 continue;
2838 }
2839 let _ = fs::remove_file(ent.path());
2840 }
2841 Ok(())
2842}