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}