use std::{ fs, io::Read, path::Path, sync::{Arc, atomic::AtomicBool}, }; use anyhow::{Result, anyhow}; use gix::error::{ErrorExt, NotFoundError, ResultExt, message}; use gix::{ NestedProgress, hash::ObjectId, object, odb, odb::{loose, pack}, prelude::Write, }; #[derive(Default, Clone, Eq, PartialEq, Debug)] pub enum SafetyCheck { SkipFileChecksumVerification, SkipFileAndObjectChecksumVerification, SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError, #[default] All, } impl SafetyCheck { pub fn variants() -> &'static [&'static str] { &[ "all", "skip-file-checksum", "skip-file-and-object-checksum", "skip-file-and-object-checksum-and-no-abort-on-decode", ] } } impl std::str::FromStr for SafetyCheck { type Err = String; fn from_str(s: &str) -> Result { Ok(match s { "skip-file-checksum" => SafetyCheck::SkipFileChecksumVerification, "skip-file-and-object-checksum" => SafetyCheck::SkipFileAndObjectChecksumVerification, "skip-file-and-object-checksum-and-no-abort-on-decode" => { SafetyCheck::SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError } "all" => SafetyCheck::All, _ => return Err(format!("Unknown value for safety check: '{s}'")), }) } } impl From for pack::index::traverse::SafetyCheck { fn from(v: SafetyCheck) -> Self { use pack::index::traverse::SafetyCheck::*; match v { SafetyCheck::All => All, SafetyCheck::SkipFileChecksumVerification => SkipFileChecksumVerification, SafetyCheck::SkipFileAndObjectChecksumVerification => SkipFileAndObjectChecksumVerification, SafetyCheck::SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError => { SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError } } } } #[expect( clippy::large_enum_variant, reason = "will be removed once `gix-error` is used consistently" )] #[derive(Clone)] enum OutputWriter { Loose(loose::Store), Sink(odb::Sink), } impl gix::objs::Write for OutputWriter { fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result { match self { OutputWriter::Loose(db) => db.write_buf(kind, from), OutputWriter::Sink(db) => db.write_buf(kind, from), } } fn write_buf_with_known_id(&self, kind: object::Kind, from: &[u8], id: ObjectId) -> Result { match self { OutputWriter::Loose(db) => db.write_buf_with_known_id(kind, from, id), OutputWriter::Sink(db) => db.write_buf_with_known_id(kind, from, id), } } fn write_stream(&self, kind: object::Kind, size: u64, from: &mut dyn Read) -> Result { match self { OutputWriter::Loose(db) => db.write_stream(kind, size, from), OutputWriter::Sink(db) => db.write_stream(kind, size, from), } } fn write_stream_with_known_id( &self, kind: object::Kind, size: u64, from: &mut dyn Read, id: ObjectId, ) -> Result { match self { OutputWriter::Loose(db) => db.write_stream_with_known_id(kind, size, from, id), OutputWriter::Sink(db) => db.write_stream_with_known_id(kind, size, from, id), } } } impl OutputWriter { fn new(path: Option>, compress: bool, object_hash: gix::hash::Kind) -> Self { match path { Some(path) => OutputWriter::Loose(loose::Store::at(path.as_ref(), object_hash)), None => OutputWriter::Sink( odb::sink(object_hash).compress(compress.then_some(gix::zlib::Compression::BEST_SPEED)), ), } } } #[derive(Default)] pub struct Context { pub thread_limit: Option, pub delete_pack: bool, pub sink_compress: bool, pub verify: bool, pub should_interrupt: Arc, pub object_hash: gix::hash::Kind, } pub fn pack_or_pack_index( pack_path: impl AsRef, object_path: Option>, check: SafetyCheck, mut progress: impl NestedProgress + 'static, Context { thread_limit, delete_pack, sink_compress, verify, should_interrupt, object_hash, }: Context, ) -> Result<()> { use anyhow::Context; let path = pack_path.as_ref(); let bundle = pack::Bundle::at(path, object_hash) .map_err(gix::Exn::into_error) .with_context(|| { format!( "Could not find .idx or .pack file from given file at '{}'", path.display() ) })?; if !object_path.as_ref().is_none_or(|p| p.as_ref().is_dir()) { return Err(anyhow!( "The object directory at '{}' is inaccessible", object_path .expect("path present if no directory on disk") .as_ref() .display() )); } let algorithm = object_path.as_ref().map_or_else( || { if sink_compress { pack::index::traverse::Algorithm::Lookup } else { pack::index::traverse::Algorithm::DeltaTreeLookup } }, |_| pack::index::traverse::Algorithm::Lookup, ); let pack::index::traverse::Outcome { .. } = bundle .index .traverse( &bundle.pack, &mut progress, &should_interrupt, { let object_path = object_path.map(|p| p.as_ref().to_owned()); let out = OutputWriter::new(object_path.clone(), sink_compress, object_hash); let loose_odb = verify .then(|| { object_path.as_ref().map(|path| loose::Store::at(path, object_hash)) }) .flatten(); let mut read_buf = Vec::new(); move |object_kind, buf, index_entry, progress| { let written_id = out .write_buf(object_kind, buf) .map_err(|err| { err.raise(message!( "Failed to write {object_kind} object {}", index_entry.oid )) .erased() })?; if let Err(err) = written_id.verify(&index_entry.oid) { if let object::Kind::Tree = object_kind { progress.info(format!( "The tree in pack named {} was written as {} due to modes 100664 and 100640 rewritten as 100644.", index_entry.oid, written_id )); } else { return Err(err .and_raise(message!("{object_kind} object wasn't re-encoded without change")) .erased()); } } if let Some(verifier) = loose_odb.as_ref() { let obj = verifier .try_find(&written_id, &mut read_buf) .map_err(|err| { err.and_raise(message!( "The recently written file for loose object {written_id} could not be read" )) .erased() })? .ok_or_else(|| { NotFoundError::new(format!( "The recently written file for loose object {written_id} could not be found" )) .raise_erased() })?; obj.verify_checksum(&written_id).or_erased()?; } Ok(()) } }, pack::index::traverse::Options { traversal: algorithm, thread_limit, check: check.into(), alloc_limit_bytes: bundle.pack.alloc_limit_bytes, make_pack_lookup_cache: pack::cache::lru::StaticLinkedList::<64>::default, }, ) .map_err(gix::Exn::into_error) .with_context(|| "Failed to explode the entire pack - some loose objects may have been created nonetheless")?; let (index_path, data_path) = (bundle.index.path().to_owned(), bundle.pack.path().to_owned()); drop(bundle); if delete_pack { fs::remove_file(&index_path) .and_then(|_| fs::remove_file(&data_path)) .with_context(|| { format!( "Failed to delete pack index file at '{} or data file at '{}'", index_path.display(), data_path.display() ) })?; progress.info(format!( "Removed '{}' and '{}'", index_path.display(), data_path.display() )); } Ok(()) }