Skip to main content

sley_odb/
install.rs

1use sley_core::{GitError, MissingObjectContext, ObjectFormat, ObjectId, Result};
2use sley_formats::{Bundle, BundleReference};
3use sley_object::{EncodedObject, ObjectType};
4use sley_pack::{
5    PackFile, PackIndex, PackInput, PackStreamIndexBuild, PackStreamProgress, PackWrite,
6    PackWriteOptions,
7};
8use std::collections::{HashMap, HashSet};
9use std::fs;
10use std::io::{Read, Write};
11use std::path::{Path, PathBuf};
12
13use crate::{ObjectReader, ObjectWriter, unique_temp_path};
14
15use crate::pack::{FileObjectDatabase, ObjectDatabase};
16use crate::repack::pack_index_entries_match_writer;
17
18pub struct BundleUnbundleResult {
19    pub written_objects: Vec<ObjectId>,
20    pub references: Vec<BundleReference>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct PackUnpackResult {
25    pub written_objects: Vec<ObjectId>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct PackInstallResult {
30    pub pack_name: String,
31    pub pack_path: PathBuf,
32    pub index_path: PathBuf,
33    pub promisor_path: Option<PathBuf>,
34    pub object_ids: Vec<ObjectId>,
35}
36
37/// Disposable object database for an untrusted incoming pack.
38///
39/// Objects are written below the destination object directory, while an
40/// `info/alternates` entry makes the destination's existing objects available
41/// as delta bases and during connectivity validation. Dropping an unpromoted
42/// quarantine removes every incoming object. [`Self::promote`] moves accepted
43/// object files into the destination with per-file atomic renames.
44#[derive(Debug)]
45pub struct IncomingPackQuarantine {
46    git_dir: PathBuf,
47    object_dir: PathBuf,
48    destination_objects_dir: PathBuf,
49    format: ObjectFormat,
50    promisor_remote_present: bool,
51    promoted: bool,
52}
53
54impl IncomingPackQuarantine {
55    pub fn new(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<Self> {
56        let source_git_dir = git_dir.as_ref().to_path_buf();
57        let destination_objects_dir = crate::repository_objects_dir(&source_git_dir);
58        fs::create_dir_all(&destination_objects_dir)?;
59        let quarantine_git_dir = create_incoming_object_dir(&destination_objects_dir)?;
60        let object_dir = quarantine_git_dir.join("objects");
61        let result = (|| -> Result<()> {
62            fs::create_dir_all(object_dir.join("pack"))?;
63            fs::create_dir_all(object_dir.join("info"))?;
64            let mut alternates = vec![
65                fs::canonicalize(&destination_objects_dir)
66                    .unwrap_or_else(|_| destination_objects_dir.clone()),
67            ];
68            // FileObjectDatabase deliberately treats alternate entries as a
69            // flat search list. Preserve the destination's existing alternate
70            // visibility explicitly so a quarantined fetch into a shared or
71            // reference clone can validate objects that were already borrowed
72            // from its source repository.
73            alternates.extend(crate::registry::alternate_object_dirs(
74                &destination_objects_dir,
75            ));
76            let mut alternate_file = String::new();
77            for alternate in alternates {
78                let alternate = fs::canonicalize(&alternate).unwrap_or(alternate);
79                alternate_file.push_str(&alternate.to_string_lossy());
80                alternate_file.push('\n');
81            }
82            fs::write(object_dir.join("info/alternates"), alternate_file)?;
83            let shallow = quarantine_git_dir.join("shallow");
84            let source_shallow = source_git_dir.join("shallow");
85            if source_shallow.exists() {
86                fs::copy(source_shallow, shallow)?;
87            }
88            Ok(())
89        })();
90        if let Err(err) = result {
91            let _ = fs::remove_dir_all(&quarantine_git_dir);
92            return Err(err);
93        }
94        Ok(Self {
95            git_dir: quarantine_git_dir,
96            object_dir,
97            destination_objects_dir,
98            format,
99            promisor_remote_present: false,
100            promoted: false,
101        })
102    }
103
104    pub fn object_dir(&self) -> &Path {
105        &self.object_dir
106    }
107
108    /// A minimal bare repository path whose object database is quarantined.
109    /// Existing destination objects remain readable through its alternate.
110    pub fn git_dir(&self) -> &Path {
111        &self.git_dir
112    }
113
114    /// Mark promised objects as valid missing links while validating a fetch
115    /// into a partial-clone repository.
116    pub fn with_promisor_remote_present(mut self, present: bool) -> Self {
117        self.promisor_remote_present = present;
118        self
119    }
120
121    pub fn database(&self) -> FileObjectDatabase {
122        FileObjectDatabase::new(self.object_dir.clone(), self.format)
123            .with_promisor_remote_present(self.promisor_remote_present)
124    }
125
126    /// Promote all accepted loose and packed objects into the destination.
127    ///
128    /// Pack indexes and sidecars are made visible before their `.pack`; a
129    /// reader therefore never observes a pack without its index. If any rename
130    /// fails, files newly moved by this call are rolled back into quarantine.
131    pub fn promote(mut self) -> Result<()> {
132        let mut files = incoming_object_files(&self.object_dir)?;
133        files.sort_by_key(|path| {
134            let is_pack = path.extension().is_some_and(|ext| ext == "pack");
135            (is_pack, path.clone())
136        });
137        let mut moved = Vec::new();
138        for source in files {
139            let relative = source
140                .strip_prefix(&self.object_dir)
141                .map_err(|_| GitError::InvalidPath("incoming object escaped quarantine".into()))?;
142            let destination = self.destination_objects_dir.join(relative);
143            let parent = destination.parent().ok_or_else(|| {
144                GitError::InvalidPath("incoming object has no destination parent".into())
145            })?;
146            fs::create_dir_all(parent)?;
147            if destination.exists() {
148                fs::remove_file(&source)?;
149                continue;
150            }
151            if let Err(err) = fs::rename(&source, &destination) {
152                for (promoted, staged) in moved.iter().rev() {
153                    let _ = fs::rename(promoted, staged);
154                }
155                return Err(GitError::Io(err.to_string()));
156            }
157            moved.push((destination, source));
158        }
159        self.promoted = true;
160        fs::remove_dir_all(&self.git_dir)?;
161        Ok(())
162    }
163}
164
165impl Drop for IncomingPackQuarantine {
166    fn drop(&mut self) {
167        if !self.promoted {
168            let _ = fs::remove_dir_all(&self.git_dir);
169        }
170    }
171}
172
173fn create_incoming_object_dir(objects_dir: &Path) -> Result<PathBuf> {
174    for _ in 0..100 {
175        let object_dir = unique_temp_path(objects_dir).with_extension("incoming");
176        match fs::create_dir(&object_dir) {
177            Ok(()) => return Ok(object_dir),
178            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
179            Err(err) => return Err(GitError::Io(err.to_string())),
180        }
181    }
182    Err(GitError::Io(
183        "could not create incoming object quarantine".into(),
184    ))
185}
186
187fn incoming_object_files(object_dir: &Path) -> Result<Vec<PathBuf>> {
188    let mut files = Vec::new();
189    let pack_dir = object_dir.join("pack");
190    if pack_dir.exists() {
191        for entry in fs::read_dir(pack_dir)? {
192            let path = entry?.path();
193            if path.is_file() {
194                files.push(path);
195            }
196        }
197    }
198    for entry in fs::read_dir(object_dir)? {
199        let entry = entry?;
200        let name = entry.file_name();
201        let name = name.to_string_lossy();
202        if name.len() != 2 || !name.bytes().all(|byte| byte.is_ascii_hexdigit()) {
203            continue;
204        }
205        for loose in fs::read_dir(entry.path())? {
206            let path = loose?.path();
207            if path.is_file() {
208                files.push(path);
209            }
210        }
211    }
212    Ok(files)
213}
214
215#[derive(Debug)]
216pub struct RawPackStreamingInstall {
217    format: ObjectFormat,
218    expected_pack_id: ObjectId,
219    expected_pack_size: u64,
220    options: RawPackInstallOptions,
221    pack_dir: PathBuf,
222    pack_name: String,
223    pack_path: PathBuf,
224    index_path: PathBuf,
225    temp_pack_path: PathBuf,
226    file: Option<fs::File>,
227    written: u64,
228    finished: bool,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct RawPackInstallResult {
233    pub object_ids: Vec<ObjectId>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct RawPackIndexResult {
238    pub pack_id: ObjectId,
239    pub index: Vec<u8>,
240    pub objects: Vec<RawPackIndexedObject>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct RawPackIndexedObject {
245    pub oid: ObjectId,
246    pub object_type: ObjectType,
247    pub size: u64,
248    pub offset: u64,
249}
250
251struct PackInstallTeeReader<'a, R, W> {
252    reader: &'a mut R,
253    writer: &'a mut W,
254    max_input_size: Option<u64>,
255    written: u64,
256}
257
258impl<R, W> Read for PackInstallTeeReader<'_, R, W>
259where
260    R: Read,
261    W: Write,
262{
263    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
264        let len = self.reader.read(buf)?;
265        if len > 0 {
266            let next_written = self.written.checked_add(len as u64).ok_or_else(|| {
267                std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
268            })?;
269            if let Some(limit) = self.max_input_size
270                && next_written > limit
271            {
272                return Err(std::io::Error::new(
273                    std::io::ErrorKind::InvalidData,
274                    format!("pack exceeds maximum allowed size ({limit})"),
275                ));
276            }
277            self.writer.write_all(&buf[..len])?;
278            self.written = next_written;
279        }
280        Ok(len)
281    }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct ReachablePackFile {
286    pub pack_path: PathBuf,
287    pub pack_size: u64,
288    pub checksum: ObjectId,
289    pub object_count: usize,
290    pub delta_count: u32,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct ReachablePackWriteSummary {
295    pub index: Vec<u8>,
296    pub checksum: ObjectId,
297    pub object_count: usize,
298    pub delta_count: u32,
299    pub pack_size: u64,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
303pub struct RawPackInstallOptions {
304    pub promisor: bool,
305    /// Maximum raw pack bytes to accept from the reader. `None` means unlimited,
306    /// mirroring unset `fetch.maxInputSize` / `transfer.maxSize`.
307    pub max_input_size: Option<u64>,
308}
309
310pub trait RawPackInstaller {
311    fn install_raw_pack_from_reader_with_options<R>(
312        &self,
313        reader: &mut R,
314        options: RawPackInstallOptions,
315    ) -> Result<RawPackInstallResult>
316    where
317        R: Read;
318
319    fn install_raw_pack_from_reader<R>(&self, reader: &mut R) -> Result<RawPackInstallResult>
320    where
321        R: Read,
322    {
323        self.install_raw_pack_from_reader_with_options(reader, RawPackInstallOptions::default())
324    }
325
326    /// Install a raw pack while reporting streaming pack progress via `progress`.
327    ///
328    /// The default implementation ignores `progress` and delegates to
329    /// [`install_raw_pack_from_reader_with_options`], so installers that do not
330    /// stream through the pack indexer (e.g. the in-memory store) keep working
331    /// unchanged. [`FileObjectDatabase`] overrides this to thread real counters.
332    ///
333    /// [`install_raw_pack_from_reader_with_options`]: RawPackInstaller::install_raw_pack_from_reader_with_options
334    fn install_raw_pack_from_reader_with_progress<R, F>(
335        &self,
336        reader: &mut R,
337        options: RawPackInstallOptions,
338        _progress: F,
339    ) -> Result<RawPackInstallResult>
340    where
341        R: Read,
342        F: FnMut(PackStreamProgress),
343    {
344        self.install_raw_pack_from_reader_with_options(reader, options)
345    }
346}
347
348#[cfg(test)]
349pub(crate) const REACHABLE_PACK_STREAMING_MIN_OBJECTS: usize = 32;
350#[cfg(not(test))]
351pub(crate) const REACHABLE_PACK_STREAMING_MIN_OBJECTS: usize = 4096;
352
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub enum ObjectPrefixResolution {
355    Missing,
356    Unique(ObjectId),
357    Ambiguous(Vec<ObjectId>),
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct ObjectStorageInfo {
362    pub disk_size: u64,
363    pub deltabase: ObjectId,
364}
365
366impl RawPackInstaller for FileObjectDatabase {
367    fn install_raw_pack_from_reader_with_options<R>(
368        &self,
369        reader: &mut R,
370        options: RawPackInstallOptions,
371    ) -> Result<RawPackInstallResult>
372    where
373        R: Read,
374    {
375        let result =
376            FileObjectDatabase::install_raw_pack_from_reader_with_options(self, reader, options)?;
377        Ok(RawPackInstallResult {
378            object_ids: result.object_ids,
379        })
380    }
381
382    fn install_raw_pack_from_reader_with_progress<R, F>(
383        &self,
384        reader: &mut R,
385        options: RawPackInstallOptions,
386        progress: F,
387    ) -> Result<RawPackInstallResult>
388    where
389        R: Read,
390        F: FnMut(PackStreamProgress),
391    {
392        let result = FileObjectDatabase::install_raw_pack_from_reader_with_progress(
393            self, reader, options, progress,
394        )?;
395        Ok(RawPackInstallResult {
396            object_ids: result.object_ids,
397        })
398    }
399}
400
401impl RawPackInstaller for ObjectDatabase {
402    fn install_raw_pack_from_reader_with_options<R>(
403        &self,
404        reader: &mut R,
405        options: RawPackInstallOptions,
406    ) -> Result<RawPackInstallResult>
407    where
408        R: Read,
409    {
410        let mut pack_bytes = Vec::new();
411        match options.max_input_size {
412            Some(limit) => {
413                reader
414                    .take(limit.saturating_add(1))
415                    .read_to_end(&mut pack_bytes)?;
416                if pack_bytes.len() as u64 > limit {
417                    return Err(GitError::InvalidFormat(format!(
418                        "pack exceeds maximum allowed size ({limit})"
419                    )));
420                }
421            }
422            None => {
423                reader.read_to_end(&mut pack_bytes)?;
424            }
425        }
426        let result = unpack_packfile_objects(&pack_bytes, self.format, self)?;
427        Ok(RawPackInstallResult {
428            object_ids: result.written_objects,
429        })
430    }
431}
432
433impl RawPackStreamingInstall {
434    pub fn bytes_written(&self) -> u64 {
435        self.written
436    }
437
438    pub fn pack_path(&self) -> &Path {
439        &self.pack_path
440    }
441
442    pub fn index_path(&self) -> &Path {
443        &self.index_path
444    }
445
446    pub fn finish(mut self) -> Result<PackInstallResult> {
447        let result = (|| -> Result<PackInstallResult> {
448            let mut file = self.file.take().ok_or_else(|| {
449                GitError::InvalidFormat("raw pack stream already finished".into())
450            })?;
451            file.flush()?;
452            file.sync_all()?;
453            drop(file);
454
455            if self.written != self.expected_pack_size {
456                return Err(GitError::InvalidFormat(format!(
457                    "raw pack stream length mismatch: expected {}, got {}",
458                    self.expected_pack_size, self.written
459                )));
460            }
461
462            let built = PackIndex::write_v2_for_pack_path(&self.temp_pack_path, self.format)?;
463            if built.pack_checksum != self.expected_pack_id {
464                return Err(GitError::InvalidFormat(format!(
465                    "raw pack stream checksum mismatch: expected {}, got {}",
466                    self.expected_pack_id, built.pack_checksum
467                )));
468            }
469
470            match fs::rename(&self.temp_pack_path, &self.pack_path) {
471                Ok(()) => {}
472                Err(_) if self.pack_path.exists() => {
473                    let _ = fs::remove_file(&self.temp_pack_path);
474                }
475                Err(err) => return Err(GitError::Io(err.to_string())),
476            }
477            write_pack_component(&self.index_path, &built.index)?;
478            let promisor_path = write_promisor_pack_sidecar(
479                &self.pack_dir,
480                &self.pack_name,
481                self.options.promisor,
482            )?;
483            Ok(PackInstallResult {
484                pack_name: self.pack_name.clone(),
485                pack_path: self.pack_path.clone(),
486                index_path: self.index_path.clone(),
487                promisor_path,
488                object_ids: built.entries.iter().map(|entry| entry.oid).collect(),
489            })
490        })();
491
492        if result.is_ok() {
493            self.finished = true;
494        } else {
495            let _ = fs::remove_file(&self.temp_pack_path);
496        }
497        result
498    }
499}
500
501impl Write for RawPackStreamingInstall {
502    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
503        let next_written = self.written.checked_add(buf.len() as u64).ok_or_else(|| {
504            std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
505        })?;
506        if next_written > self.expected_pack_size {
507            return Err(std::io::Error::new(
508                std::io::ErrorKind::InvalidData,
509                format!(
510                    "raw pack stream exceeds expected size {}; got at least {}",
511                    self.expected_pack_size, next_written
512                ),
513            ));
514        }
515        let file = self.file.as_mut().ok_or_else(|| {
516            std::io::Error::new(
517                std::io::ErrorKind::BrokenPipe,
518                "raw pack stream already finished",
519            )
520        })?;
521        let written = file.write(buf)?;
522        self.written = self.written.checked_add(written as u64).ok_or_else(|| {
523            std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
524        })?;
525        Ok(written)
526    }
527
528    fn flush(&mut self) -> std::io::Result<()> {
529        match self.file.as_mut() {
530            Some(file) => file.flush(),
531            None => Ok(()),
532        }
533    }
534}
535
536impl Drop for RawPackStreamingInstall {
537    fn drop(&mut self) {
538        if !self.finished {
539            let _ = self.file.take();
540            let _ = fs::remove_file(&self.temp_pack_path);
541        }
542    }
543}
544
545pub fn verify_bundle_prerequisites<R: ObjectReader>(bundle: &Bundle, reader: &R) -> Result<()> {
546    let mut missing = Vec::new();
547    for prerequisite in &bundle.prerequisites {
548        match reader.read_object(&prerequisite.oid) {
549            Ok(object) => {
550                let actual = object.object_id(bundle.format)?;
551                if actual != prerequisite.oid {
552                    return Err(GitError::InvalidObject(format!(
553                        "bundle prerequisite {} hashes to {actual}",
554                        prerequisite.oid
555                    )));
556                }
557            }
558            Err(GitError::NotFound(_)) => missing.push(prerequisite.oid),
559            Err(err) => return Err(err),
560        }
561    }
562    if missing.is_empty() {
563        return Ok(());
564    }
565    Err(GitError::object_not_found_in(
566        missing[0],
567        MissingObjectContext::PackInstall,
568    ))
569}
570
571pub fn unbundle_objects<R, W>(
572    bundle: &Bundle,
573    prerequisite_reader: &R,
574    writer: &mut W,
575) -> Result<BundleUnbundleResult>
576where
577    R: ObjectReader,
578    W: ObjectWriter,
579{
580    verify_bundle_prerequisites(bundle, prerequisite_reader)?;
581    let pack = PackFile::parse_bundle(bundle)?;
582    let written_objects = write_pack_objects(pack, writer, "bundle")?.written_objects;
583    Ok(BundleUnbundleResult {
584        written_objects,
585        references: bundle.references.clone(),
586    })
587}
588
589pub fn install_bundle_pack<R>(
590    bundle: &Bundle,
591    prerequisite_reader: &R,
592    destination: &impl RawPackInstaller,
593) -> Result<BundleUnbundleResult>
594where
595    R: ObjectReader,
596{
597    verify_bundle_prerequisites(bundle, prerequisite_reader)?;
598    let mut reader = bundle.pack.as_slice();
599    let install = destination.install_raw_pack_from_reader(&mut reader)?;
600    Ok(BundleUnbundleResult {
601        written_objects: install.object_ids,
602        references: bundle.references.clone(),
603    })
604}
605
606pub fn unpack_packfile_objects<W>(
607    pack_bytes: &[u8],
608    format: ObjectFormat,
609    writer: &W,
610) -> Result<PackUnpackResult>
611where
612    W: ObjectWriter,
613{
614    let pack = PackFile::parse(pack_bytes, format)?;
615    write_pack_objects(pack, writer, "pack")
616}
617
618pub fn index_raw_pack(pack_bytes: &[u8], format: ObjectFormat) -> Result<RawPackIndexResult> {
619    let pack = PackFile::parse(pack_bytes, format)?;
620    let built = PackIndex::write_v2_for_pack(pack_bytes, format)?;
621    if built.pack_checksum != pack.checksum {
622        return Err(GitError::InvalidFormat(
623            "pack index checksum does not match parsed pack checksum".to_string(),
624        ));
625    }
626
627    let offsets = built
628        .entries
629        .iter()
630        .map(|entry| (entry.oid, entry.offset))
631        .collect::<HashMap<_, _>>();
632    let mut objects = Vec::with_capacity(pack.entries.len());
633    for object in pack.entries {
634        let offset = offsets.get(&object.entry.oid).copied().ok_or_else(|| {
635            GitError::InvalidFormat(format!(
636                "pack index is missing object {}",
637                object.entry.oid.to_hex()
638            ))
639        })?;
640        objects.push(RawPackIndexedObject {
641            oid: object.entry.oid,
642            object_type: object.object.object_type,
643            size: object.object.body.len() as u64,
644            offset,
645        });
646    }
647
648    Ok(RawPackIndexResult {
649        pack_id: built.pack_checksum,
650        index: built.index,
651        objects,
652    })
653}
654
655pub fn index_raw_pack_from_reader<R>(
656    reader: &mut R,
657    format: ObjectFormat,
658) -> Result<RawPackIndexResult>
659where
660    R: Read,
661{
662    Ok(stream_index_build_to_raw_result(
663        PackIndex::write_v2_for_pack_reader_to_trailer(reader, format)?,
664    ))
665}
666
667pub fn index_raw_pack_from_reader_with_len<R>(
668    reader: &mut R,
669    format: ObjectFormat,
670    pack_len: u64,
671) -> Result<RawPackIndexResult>
672where
673    R: Read,
674{
675    Ok(stream_index_build_to_raw_result(
676        PackIndex::write_v2_for_pack_reader_with_len(reader, format, pack_len)?,
677    ))
678}
679
680pub fn index_raw_pack_file(
681    path: impl AsRef<Path>,
682    format: ObjectFormat,
683) -> Result<RawPackIndexResult> {
684    Ok(stream_index_build_to_raw_result(
685        PackIndex::write_v2_for_pack_path(path, format)?,
686    ))
687}
688
689fn stream_index_build_to_raw_result(built: PackStreamIndexBuild) -> RawPackIndexResult {
690    let objects = built
691        .objects
692        .into_iter()
693        .map(|object| RawPackIndexedObject {
694            oid: object.oid,
695            object_type: object.object_type,
696            size: object.size,
697            offset: object.offset,
698        })
699        .collect::<Vec<_>>();
700    RawPackIndexResult {
701        pack_id: built.pack_checksum,
702        index: built.index,
703        objects,
704    }
705}
706
707fn write_pack_objects<W>(pack: PackFile, writer: &W, source: &str) -> Result<PackUnpackResult>
708where
709    W: ObjectWriter,
710{
711    let mut written_objects = Vec::with_capacity(pack.entries.len());
712    for entry in pack.entries {
713        let expected = entry.entry.oid;
714        let actual = writer.write_object(entry.object)?;
715        if actual != expected {
716            return Err(GitError::InvalidObject(format!(
717                "{source} object id mismatch: expected {expected}, wrote {actual}"
718            )));
719        }
720        written_objects.push(actual);
721    }
722    Ok(PackUnpackResult { written_objects })
723}
724pub(crate) fn validate_pack_checksum(
725    pack: &[u8],
726    format: ObjectFormat,
727    expected: &ObjectId,
728    context: &str,
729) -> Result<()> {
730    if expected.format() != format {
731        return Err(GitError::InvalidObjectId(format!(
732            "{context} checksum format does not match object format"
733        )));
734    }
735    let hash_len = format.raw_len();
736    if pack.len() < 12 + hash_len {
737        return Err(GitError::InvalidFormat(format!(
738            "{context} pack file too short"
739        )));
740    }
741    if &pack[..4] != b"PACK" {
742        return Err(GitError::InvalidFormat(format!(
743            "{context} pack file missing PACK signature"
744        )));
745    }
746    let trailer_offset = pack.len() - hash_len;
747    let actual = sley_core::digest_bytes(format, &pack[..trailer_offset])?;
748    let trailer = ObjectId::from_raw(format, &pack[trailer_offset..])?;
749    if &actual != expected || trailer != *expected {
750        return Err(GitError::InvalidFormat(format!(
751            "{context} pack checksum does not match generated pack"
752        )));
753    }
754    Ok(())
755}
756
757impl FileObjectDatabase {
758    pub fn install_pack(&self, pack: &PackWrite) -> Result<PackInstallResult> {
759        self.install_pack_with_options(pack, RawPackInstallOptions::default())
760    }
761
762    pub fn write_blob_as_pack(
763        &self,
764        oid: ObjectId,
765        object: &EncodedObject,
766        compression_level: u32,
767    ) -> Result<ObjectId> {
768        if object.object_type != ObjectType::Blob {
769            return Err(GitError::InvalidObject(
770                "write_blob_as_pack requires a blob object".into(),
771            ));
772        }
773        if oid.format() != self.format {
774            return Err(GitError::InvalidObjectId(format!(
775                "object {oid} uses {}, store uses {}",
776                oid.format().name(),
777                self.format.name()
778            )));
779        }
780        if self.contains(&oid)? {
781            return Ok(oid);
782        }
783        let input = [PackInput { oid: &oid, object }];
784        let options = PackWriteOptions::new()
785            .with_window(0)
786            .with_depth(0)
787            .with_reorder(false)
788            .with_compression_level(compression_level);
789        let pack =
790            PackFile::write_packed_with_known_ids_and_options(&input, self.format, &options)?;
791        self.install_pack(&pack)?;
792        Ok(oid)
793    }
794
795    pub fn write_blobs_as_pack(
796        &self,
797        objects: &[(ObjectId, EncodedObject)],
798        compression_level: u32,
799    ) -> Result<()> {
800        let mut seen = HashSet::with_capacity(objects.len());
801        let mut inputs = Vec::new();
802        for (oid, object) in objects {
803            if object.object_type != ObjectType::Blob {
804                return Err(GitError::InvalidObject(
805                    "write_blobs_as_pack requires blob objects".into(),
806                ));
807            }
808            if oid.format() != self.format {
809                return Err(GitError::InvalidObjectId(format!(
810                    "object {oid} uses {}, store uses {}",
811                    oid.format().name(),
812                    self.format.name()
813                )));
814            }
815            if seen.insert(*oid) && !self.contains(oid)? {
816                inputs.push(PackInput { oid, object });
817            }
818        }
819        if inputs.is_empty() {
820            return Ok(());
821        }
822        let options = PackWriteOptions::new()
823            .with_window(0)
824            .with_depth(0)
825            .with_reorder(false)
826            .with_compression_level(compression_level);
827        let pack =
828            PackFile::write_packed_with_known_ids_and_options(&inputs, self.format, &options)?;
829        self.install_pack(&pack)?;
830        Ok(())
831    }
832
833    pub fn install_pack_with_options(
834        &self,
835        pack: &PackWrite,
836        options: RawPackInstallOptions,
837    ) -> Result<PackInstallResult> {
838        if pack.checksum.format() != self.format {
839            return Err(GitError::InvalidObjectId(format!(
840                "pack checksum uses {}, store uses {}",
841                pack.checksum.format().name(),
842                self.format.name()
843            )));
844        }
845        for entry in &pack.entries {
846            if entry.oid.format() != self.format {
847                return Err(GitError::InvalidObjectId(format!(
848                    "pack entry {} uses {}, store uses {}",
849                    entry.oid,
850                    entry.oid.format().name(),
851                    self.format.name()
852                )));
853            }
854        }
855        let canonical_index = PackIndex::write_v2_for_pack(&pack.pack, self.format)?;
856        let parsed_index = PackIndex::parse(&pack.index, self.format)?;
857        if canonical_index.pack_checksum != pack.checksum
858            || parsed_index.pack_checksum != pack.checksum
859        {
860            return Err(GitError::InvalidFormat(
861                "pack and index checksums do not match pack write".into(),
862            ));
863        }
864        if pack.index != canonical_index.index {
865            return Err(GitError::InvalidFormat(
866                "pack index does not match pack contents".into(),
867            ));
868        }
869
870        let pack_dir = self.objects_dir.join("pack");
871        fs::create_dir_all(&pack_dir)?;
872        let pack_name = format!("pack-{}", pack.checksum.to_hex());
873        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
874        let index_path = pack_dir.join(format!("{pack_name}.idx"));
875        if !pack_path.exists() || !index_path.exists() {
876            write_pack_component(&pack_path, &pack.pack)?;
877            write_pack_component(&index_path, &pack.index)?;
878        }
879        let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
880        Ok(PackInstallResult {
881            pack_name,
882            pack_path,
883            index_path,
884            promisor_path,
885            object_ids: canonical_index
886                .entries
887                .iter()
888                .map(|entry| entry.oid)
889                .collect(),
890        })
891    }
892
893    /// Install a pack that was produced in this process by [`PackFile::write_packed`].
894    ///
895    /// Unlike [`Self::install_raw_pack_from_reader_with_options`], this does not re-inflate
896    /// every pack entry to rebuild the index. It validates the generated pack
897    /// trailer and generated index against the writer's object ids, CRCs, and
898    /// offsets, then writes those bytes directly. Use the raw installer for
899    /// arbitrary pack bytes received from an untrusted transport.
900    pub fn install_written_pack(&self, pack: &PackWrite) -> Result<PackInstallResult> {
901        self.install_written_pack_with_options(pack, RawPackInstallOptions::default())
902    }
903
904    pub fn install_written_pack_with_options(
905        &self,
906        pack: &PackWrite,
907        options: RawPackInstallOptions,
908    ) -> Result<PackInstallResult> {
909        validate_pack_checksum(&pack.pack, self.format, &pack.checksum, "pack write")?;
910        let parsed_index = PackIndex::parse(&pack.index, self.format)?;
911        if parsed_index.pack_checksum != pack.checksum {
912            return Err(GitError::InvalidFormat(
913                "pack write index checksum does not match pack".into(),
914            ));
915        }
916        if !pack_index_entries_match_writer(&parsed_index.entries, &pack.entries) {
917            return Err(GitError::InvalidFormat(
918                "pack write index does not match generated entries".into(),
919            ));
920        }
921        self.install_generated_pack_unchecked(pack, options)
922    }
923
924    fn install_generated_pack_unchecked(
925        &self,
926        pack: &PackWrite,
927        options: RawPackInstallOptions,
928    ) -> Result<PackInstallResult> {
929        let pack_dir = self.objects_dir.join("pack");
930        fs::create_dir_all(&pack_dir)?;
931        let pack_name = format!("pack-{}", pack.checksum.to_hex());
932        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
933        let index_path = pack_dir.join(format!("{pack_name}.idx"));
934        if !pack_path.exists() || !index_path.exists() {
935            write_pack_component(&pack_path, &pack.pack)?;
936            write_pack_component(&index_path, &pack.index)?;
937        }
938        let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
939        Ok(PackInstallResult {
940            pack_name,
941            pack_path,
942            index_path,
943            promisor_path,
944            object_ids: pack.entries.iter().map(|entry| entry.oid).collect(),
945        })
946    }
947
948    pub(crate) fn install_pack_file_from_temp(
949        &self,
950        temp_pack_path: &Path,
951        pack_checksum: ObjectId,
952        index: &[u8],
953        object_ids: Vec<ObjectId>,
954        options: RawPackInstallOptions,
955    ) -> Result<PackInstallResult> {
956        let pack_dir = self.objects_dir.join("pack");
957        fs::create_dir_all(&pack_dir)?;
958        let pack_name = format!("pack-{}", pack_checksum.to_hex());
959        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
960        let index_path = pack_dir.join(format!("{pack_name}.idx"));
961        match fs::rename(temp_pack_path, &pack_path) {
962            Ok(()) => {}
963            Err(_) if pack_path.exists() => {
964                let _ = fs::remove_file(temp_pack_path);
965            }
966            Err(err) => return Err(GitError::Io(err.to_string())),
967        }
968        write_pack_component(&index_path, index)?;
969        let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
970        Ok(PackInstallResult {
971            pack_name,
972            pack_path,
973            index_path,
974            promisor_path,
975            object_ids,
976        })
977    }
978
979    pub fn install_raw_pack_from_reader<R>(&self, reader: &mut R) -> Result<PackInstallResult>
980    where
981        R: Read,
982    {
983        self.install_raw_pack_from_reader_with_options(reader, RawPackInstallOptions::default())
984    }
985
986    /// Install a pack whose ref-deltas may use objects already available from
987    /// this database (including alternates) as bases.
988    pub fn install_raw_pack_from_reader_with_external_bases<R>(
989        &self,
990        reader: &mut R,
991    ) -> Result<PackInstallResult>
992    where
993        R: Read,
994    {
995        let mut pack = Vec::new();
996        reader.read_to_end(&mut pack)?;
997        let built = PackIndex::write_v2_for_pack_with_base(&pack, self.format, |oid| {
998            match self.read_object(oid) {
999                Ok(object) => Ok(Some((*object).clone())),
1000                Err(GitError::NotFound(_)) => Ok(None),
1001                Err(err) => Err(err),
1002            }
1003        })?;
1004        let pack_dir = self.objects_dir.join("pack");
1005        fs::create_dir_all(&pack_dir)?;
1006        let temp_pack_path = unique_temp_path(&pack_dir);
1007        fs::write(&temp_pack_path, &pack)?;
1008        let result = self.install_pack_file_from_temp(
1009            &temp_pack_path,
1010            built.pack_checksum,
1011            &built.index,
1012            built.entries.iter().map(|entry| entry.oid).collect(),
1013            RawPackInstallOptions::default(),
1014        );
1015        if result.is_err() {
1016            let _ = fs::remove_file(&temp_pack_path);
1017        }
1018        result
1019    }
1020
1021    pub fn begin_raw_pack_install(
1022        &self,
1023        expected_pack_id: ObjectId,
1024        expected_pack_size: u64,
1025    ) -> Result<RawPackStreamingInstall> {
1026        self.begin_raw_pack_install_with_options(
1027            expected_pack_id,
1028            expected_pack_size,
1029            RawPackInstallOptions::default(),
1030        )
1031    }
1032
1033    pub fn begin_raw_pack_install_with_options(
1034        &self,
1035        expected_pack_id: ObjectId,
1036        expected_pack_size: u64,
1037        options: RawPackInstallOptions,
1038    ) -> Result<RawPackStreamingInstall> {
1039        if expected_pack_id.format() != self.format {
1040            return Err(GitError::InvalidObjectId(format!(
1041                "pack checksum uses {}, store uses {}",
1042                expected_pack_id.format().name(),
1043                self.format.name()
1044            )));
1045        }
1046        let pack_dir = self.objects_dir.join("pack");
1047        fs::create_dir_all(&pack_dir)?;
1048        let pack_name = format!("pack-{}", expected_pack_id.to_hex());
1049        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
1050        let index_path = pack_dir.join(format!("{pack_name}.idx"));
1051        let temp_pack_path = unique_temp_path(&pack_dir);
1052        let file = fs::OpenOptions::new()
1053            .write(true)
1054            .create_new(true)
1055            .open(&temp_pack_path)?;
1056        Ok(RawPackStreamingInstall {
1057            format: self.format,
1058            expected_pack_id,
1059            expected_pack_size,
1060            options,
1061            pack_dir,
1062            pack_name,
1063            pack_path,
1064            index_path,
1065            temp_pack_path,
1066            file: Some(file),
1067            written: 0,
1068            finished: false,
1069        })
1070    }
1071
1072    pub fn install_raw_pack_from_reader_with_options<R>(
1073        &self,
1074        reader: &mut R,
1075        options: RawPackInstallOptions,
1076    ) -> Result<PackInstallResult>
1077    where
1078        R: Read,
1079    {
1080        self.install_raw_pack_from_reader_with_progress(reader, options, |_| {})
1081    }
1082
1083    /// [`install_raw_pack_from_reader_with_options`] that reports streaming pack
1084    /// progress. `progress` is threaded to the pack indexer, so it advances as
1085    /// the pack is received off `reader` (during download for streaming
1086    /// transports; during the index walk for already-buffered ones).
1087    ///
1088    /// [`install_raw_pack_from_reader_with_options`]: FileObjectDatabase::install_raw_pack_from_reader_with_options
1089    pub fn install_raw_pack_from_reader_with_progress<R, F>(
1090        &self,
1091        reader: &mut R,
1092        options: RawPackInstallOptions,
1093        progress: F,
1094    ) -> Result<PackInstallResult>
1095    where
1096        R: Read,
1097        F: FnMut(PackStreamProgress),
1098    {
1099        let pack_dir = self.objects_dir.join("pack");
1100        fs::create_dir_all(&pack_dir)?;
1101        let temp_pack_path = unique_temp_path(&pack_dir);
1102        let result = (|| -> Result<PackInstallResult> {
1103            // Stage directly in objects/pack so validation, indexing, and the
1104            // eventual checksum-named rename use one streamed write.
1105            let mut file = fs::OpenOptions::new()
1106                .write(true)
1107                .create_new(true)
1108                .open(&temp_pack_path)?;
1109            let built = {
1110                let mut tee = PackInstallTeeReader {
1111                    reader,
1112                    writer: &mut file,
1113                    max_input_size: options.max_input_size,
1114                    written: 0,
1115                };
1116                PackIndex::write_v2_for_pack_reader_to_trailer_with_progress(
1117                    &mut tee,
1118                    self.format,
1119                    progress,
1120                )?
1121            };
1122            file.flush()?;
1123            file.sync_all()?;
1124            drop(file);
1125
1126            self.install_pack_file_from_temp(
1127                &temp_pack_path,
1128                built.pack_checksum,
1129                &built.index,
1130                built.entries.iter().map(|entry| entry.oid).collect(),
1131                options,
1132            )
1133        })();
1134        if result.is_err() {
1135            let _ = fs::remove_file(&temp_pack_path);
1136        }
1137        result
1138    }
1139}
1140
1141pub(crate) fn write_pack_component(path: &Path, bytes: &[u8]) -> Result<()> {
1142    if path.exists() {
1143        return Ok(());
1144    }
1145    let parent = path
1146        .parent()
1147        .ok_or_else(|| GitError::InvalidPath("pack component path has no parent".into()))?;
1148    fs::create_dir_all(parent)?;
1149    let temp_path = unique_temp_path(parent);
1150    let write_result = (|| -> Result<()> {
1151        {
1152            let mut file = fs::OpenOptions::new()
1153                .write(true)
1154                .create_new(true)
1155                .open(&temp_path)?;
1156            file.write_all(bytes)?;
1157            file.sync_all()?;
1158        }
1159        match fs::rename(&temp_path, path) {
1160            Ok(()) => Ok(()),
1161            Err(_) if path.exists() => {
1162                let _ = fs::remove_file(&temp_path);
1163                Ok(())
1164            }
1165            Err(err) => Err(GitError::Io(err.to_string())),
1166        }
1167    })();
1168    if write_result.is_err() {
1169        let _ = fs::remove_file(&temp_path);
1170    }
1171    write_result
1172}
1173
1174/// Write a mutable pack sidecar through a completed temporary file, replacing
1175/// an existing destination. Unix can rename over the destination atomically;
1176/// platforms which reject that operation fall back to removing the old file
1177/// only after the replacement has been fully written and synced.
1178pub(crate) fn replace_pack_component(path: &Path, bytes: &[u8]) -> Result<()> {
1179    let parent = path
1180        .parent()
1181        .ok_or_else(|| GitError::InvalidPath("pack component path has no parent".into()))?;
1182    fs::create_dir_all(parent)?;
1183    let temp_path = unique_temp_path(parent);
1184    let write_result = (|| -> Result<()> {
1185        {
1186            let mut file = fs::OpenOptions::new()
1187                .write(true)
1188                .create_new(true)
1189                .open(&temp_path)?;
1190            file.write_all(bytes)?;
1191            file.sync_all()?;
1192        }
1193        match fs::rename(&temp_path, path) {
1194            Ok(()) => Ok(()),
1195            Err(_) if path.exists() => {
1196                fs::remove_file(path)?;
1197                fs::rename(&temp_path, path)?;
1198                Ok(())
1199            }
1200            Err(err) => Err(GitError::Io(err.to_string())),
1201        }
1202    })();
1203    if write_result.is_err() {
1204        let _ = fs::remove_file(&temp_path);
1205    }
1206    write_result
1207}
1208
1209pub(crate) fn write_promisor_pack_sidecar(
1210    pack_dir: &Path,
1211    pack_name: &str,
1212    promisor: bool,
1213) -> Result<Option<PathBuf>> {
1214    if !promisor {
1215        return Ok(None);
1216    }
1217    let path = pack_dir.join(format!("{pack_name}.promisor"));
1218    write_pack_component(&path, b"")?;
1219    Ok(Some(path))
1220}