Skip to main content

gitoxide_core/pack/
explode.rs

1use std::{
2    fs,
3    io::Read,
4    path::Path,
5    sync::{Arc, atomic::AtomicBool},
6};
7
8use anyhow::{Result, anyhow};
9use gix::{
10    NestedProgress,
11    hash::ObjectId,
12    object, objs, odb,
13    odb::{loose, pack},
14    prelude::Write,
15};
16
17#[derive(Default, Clone, Eq, PartialEq, Debug)]
18pub enum SafetyCheck {
19    SkipFileChecksumVerification,
20    SkipFileAndObjectChecksumVerification,
21    SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError,
22    #[default]
23    All,
24}
25
26impl SafetyCheck {
27    pub fn variants() -> &'static [&'static str] {
28        &[
29            "all",
30            "skip-file-checksum",
31            "skip-file-and-object-checksum",
32            "skip-file-and-object-checksum-and-no-abort-on-decode",
33        ]
34    }
35}
36
37impl std::str::FromStr for SafetyCheck {
38    type Err = String;
39
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        Ok(match s {
42            "skip-file-checksum" => SafetyCheck::SkipFileChecksumVerification,
43            "skip-file-and-object-checksum" => SafetyCheck::SkipFileAndObjectChecksumVerification,
44            "skip-file-and-object-checksum-and-no-abort-on-decode" => {
45                SafetyCheck::SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError
46            }
47            "all" => SafetyCheck::All,
48            _ => return Err(format!("Unknown value for safety check: '{s}'")),
49        })
50    }
51}
52
53impl From<SafetyCheck> for pack::index::traverse::SafetyCheck {
54    fn from(v: SafetyCheck) -> Self {
55        use pack::index::traverse::SafetyCheck::*;
56        match v {
57            SafetyCheck::All => All,
58            SafetyCheck::SkipFileChecksumVerification => SkipFileChecksumVerification,
59            SafetyCheck::SkipFileAndObjectChecksumVerification => SkipFileAndObjectChecksumVerification,
60            SafetyCheck::SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError => {
61                SkipFileAndObjectChecksumVerificationAndNoAbortOnDecodeError
62            }
63        }
64    }
65}
66
67#[derive(Debug, thiserror::Error)]
68enum Error {
69    #[error("An IO error occurred while writing an object")]
70    Io(#[from] std::io::Error),
71    #[error("An object could not be written to the database")]
72    OdbWrite(#[from] loose::write::Error),
73    #[error("Failed to write {kind} object {id}")]
74    Write {
75        source: Box<dyn std::error::Error + Send + Sync>,
76        kind: object::Kind,
77        id: ObjectId,
78    },
79    #[error("Object didn't verify after right after writing it")]
80    Verify(#[from] objs::data::verify::Error),
81    #[error("{kind} object wasn't re-encoded without change")]
82    ObjectEncodeMismatch {
83        #[source]
84        source: gix::hash::verify::Error,
85        kind: object::Kind,
86    },
87    #[error("The recently written file for loose object {id} could not be found")]
88    WrittenFileMissing { id: ObjectId },
89    #[error("The recently written file for loose object {id} cold not be read")]
90    WrittenFileCorrupt { source: loose::find::Error, id: ObjectId },
91}
92
93#[expect(
94    clippy::large_enum_variant,
95    reason = "will be removed once `gix-error` is used consistently"
96)]
97#[derive(Clone)]
98enum OutputWriter {
99    Loose(loose::Store),
100    Sink(odb::Sink),
101}
102
103impl gix::objs::Write for OutputWriter {
104    fn write_buf(&self, kind: object::Kind, from: &[u8]) -> Result<ObjectId, gix::objs::write::Error> {
105        match self {
106            OutputWriter::Loose(db) => db.write_buf(kind, from),
107            OutputWriter::Sink(db) => db.write_buf(kind, from),
108        }
109    }
110
111    fn write_buf_with_known_id(
112        &self,
113        kind: object::Kind,
114        from: &[u8],
115        id: ObjectId,
116    ) -> Result<ObjectId, gix::objs::write::Error> {
117        match self {
118            OutputWriter::Loose(db) => db.write_buf_with_known_id(kind, from, id),
119            OutputWriter::Sink(db) => db.write_buf_with_known_id(kind, from, id),
120        }
121    }
122
123    fn write_stream(
124        &self,
125        kind: object::Kind,
126        size: u64,
127        from: &mut dyn Read,
128    ) -> Result<ObjectId, gix::objs::write::Error> {
129        match self {
130            OutputWriter::Loose(db) => db.write_stream(kind, size, from),
131            OutputWriter::Sink(db) => db.write_stream(kind, size, from),
132        }
133    }
134
135    fn write_stream_with_known_id(
136        &self,
137        kind: object::Kind,
138        size: u64,
139        from: &mut dyn Read,
140        id: ObjectId,
141    ) -> Result<ObjectId, gix::objs::write::Error> {
142        match self {
143            OutputWriter::Loose(db) => db.write_stream_with_known_id(kind, size, from, id),
144            OutputWriter::Sink(db) => db.write_stream_with_known_id(kind, size, from, id),
145        }
146    }
147}
148
149impl OutputWriter {
150    fn new(path: Option<impl AsRef<Path>>, compress: bool, object_hash: gix::hash::Kind) -> Self {
151        match path {
152            Some(path) => OutputWriter::Loose(loose::Store::at(
153                path.as_ref(),
154                loose::Options {
155                    object_hash,
156                    ..Default::default()
157                },
158            )),
159            None => OutputWriter::Sink(
160                odb::sink(object_hash).compress(compress.then_some(gix::zlib::Compression::BEST_SPEED)),
161            ),
162        }
163    }
164}
165
166#[derive(Default)]
167pub struct Context {
168    pub thread_limit: Option<usize>,
169    pub delete_pack: bool,
170    pub sink_compress: bool,
171    pub verify: bool,
172    pub should_interrupt: Arc<AtomicBool>,
173    pub object_hash: gix::hash::Kind,
174}
175
176pub fn pack_or_pack_index(
177    pack_path: impl AsRef<Path>,
178    object_path: Option<impl AsRef<Path>>,
179    check: SafetyCheck,
180    mut progress: impl NestedProgress + 'static,
181    Context {
182        thread_limit,
183        delete_pack,
184        sink_compress,
185        verify,
186        should_interrupt,
187        object_hash,
188    }: Context,
189) -> Result<()> {
190    use anyhow::Context;
191
192    let path = pack_path.as_ref();
193    let bundle = pack::Bundle::at(path, object_hash).with_context(|| {
194        format!(
195            "Could not find .idx or .pack file from given file at '{}'",
196            path.display()
197        )
198    })?;
199
200    if !object_path.as_ref().is_none_or(|p| p.as_ref().is_dir()) {
201        return Err(anyhow!(
202            "The object directory at '{}' is inaccessible",
203            object_path
204                .expect("path present if no directory on disk")
205                .as_ref()
206                .display()
207        ));
208    }
209
210    let algorithm = object_path.as_ref().map_or_else(
211        || {
212            if sink_compress {
213                pack::index::traverse::Algorithm::Lookup
214            } else {
215                pack::index::traverse::Algorithm::DeltaTreeLookup
216            }
217        },
218        |_| pack::index::traverse::Algorithm::Lookup,
219    );
220
221    let pack::index::traverse::Outcome { .. } = bundle
222        .index
223        .traverse(
224            &bundle.pack,
225            &mut progress,
226            &should_interrupt,
227            {
228                let object_path = object_path.map(|p| p.as_ref().to_owned());
229                let out = OutputWriter::new(object_path.clone(), sink_compress, object_hash);
230                let loose_odb = verify
231                    .then(|| {
232                        object_path.as_ref().map(|path| {
233                            loose::Store::at(
234                                path,
235                                loose::Options {
236                                    object_hash,
237                                    ..Default::default()
238                                },
239                            )
240                        })
241                    })
242                    .flatten();
243                let mut read_buf = Vec::new();
244                move |object_kind, buf, index_entry, progress| {
245                    let written_id = out.write_buf(object_kind, buf).map_err(|err| Error::Write {
246                        source: err,
247                        kind: object_kind,
248                        id: index_entry.oid,
249                    })?;
250                    if let Err(err) = written_id.verify(&index_entry.oid) {
251                        if let object::Kind::Tree = object_kind {
252                            progress.info(format!(
253                                "The tree in pack named {} was written as {} due to modes 100664 and 100640 rewritten as 100644.",
254                                index_entry.oid, written_id
255                            ));
256                        } else {
257                            return Err(Error::ObjectEncodeMismatch {
258                                source: err,
259                                kind: object_kind,
260                            });
261                        }
262                    }
263                    if let Some(verifier) = loose_odb.as_ref() {
264                        let obj = verifier
265                            .try_find(&written_id, &mut read_buf)
266                            .map_err(|err| Error::WrittenFileCorrupt {
267                                source: err,
268                                id: written_id,
269                            })?
270                            .ok_or(Error::WrittenFileMissing { id: written_id })?;
271                        obj.verify_checksum(&written_id)?;
272                    }
273                    Ok(())
274                }
275            },
276            pack::index::traverse::Options {
277                traversal: algorithm,
278                thread_limit,
279                check: check.into(),
280                alloc_limit_bytes: bundle.pack.alloc_limit_bytes,
281                make_pack_lookup_cache: pack::cache::lru::StaticLinkedList::<64>::default,
282            },
283        )
284        .with_context(|| "Failed to explode the entire pack - some loose objects may have been created nonetheless")?;
285
286    let (index_path, data_path) = (bundle.index.path().to_owned(), bundle.pack.path().to_owned());
287    drop(bundle);
288
289    if delete_pack {
290        fs::remove_file(&index_path)
291            .and_then(|_| fs::remove_file(&data_path))
292            .with_context(|| {
293                format!(
294                    "Failed to delete pack index file at '{} or data file at '{}'",
295                    index_path.display(),
296                    data_path.display()
297                )
298            })?;
299        progress.info(format!(
300            "Removed '{}' and '{}'",
301            index_path.display(),
302            data_path.display()
303        ));
304    }
305    Ok(())
306}