Skip to main content

sley_odb/
install.rs

1use flate2::Compression;
2use flate2::read::ZlibDecoder;
3use flate2::write::ZlibEncoder;
4use flate2::{Decompress, FlushDecompress};
5use parking_lot::RwLock;
6use std::sync::Mutex;
7use sley_core::{GitError, MissingObjectContext, ObjectFormat, ObjectId, Result};
8use sley_formats::{Bundle, BundleReference};
9use sley_object::{
10    Commit, EncodedObject, ObjectType, Tag, TreeEntries, parse_framed_object,
11    tree_entry_object_type,
12};
13use sley_pack::{
14    MultiPackIndex, MultiPackIndexOidLookup, PackBitmapIndex, PackBitmapWriter, PackFile,
15    PackIndex, PackIndexByteSource, PackIndexEntry, PackIndexViewData, PackInput,
16    PackReverseIndex, PackStreamIndexBuild, PackWrite, PackWriteOptions, PackWriteSummary,
17};
18use std::collections::{HashMap, HashSet};
19use std::io::{Read, Write};
20use std::path::{Path, PathBuf};
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::sync::{Arc, OnceLock};
23use std::{env, fs};
24
25use crate::{
26    grafted_parents, implied_empty_tree_object, unique_temp_path, with_missing_object_context,
27    ObjectReader, ObjectWriter,
28};
29
30use crate::pack::{FileObjectDatabase, ObjectDatabase};
31use crate::loose::LooseObjectStore;
32use crate::repack::pack_index_entries_match_writer;
33
34pub struct BundleUnbundleResult {
35    pub written_objects: Vec<ObjectId>,
36    pub references: Vec<BundleReference>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PackUnpackResult {
41    pub written_objects: Vec<ObjectId>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct PackInstallResult {
46    pub pack_name: String,
47    pub pack_path: PathBuf,
48    pub index_path: PathBuf,
49    pub promisor_path: Option<PathBuf>,
50    pub object_ids: Vec<ObjectId>,
51}
52
53#[derive(Debug)]
54pub struct RawPackStreamingInstall {
55    format: ObjectFormat,
56    expected_pack_id: ObjectId,
57    expected_pack_size: u64,
58    options: RawPackInstallOptions,
59    pack_dir: PathBuf,
60    pack_name: String,
61    pack_path: PathBuf,
62    index_path: PathBuf,
63    temp_pack_path: PathBuf,
64    file: Option<fs::File>,
65    written: u64,
66    finished: bool,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct RawPackInstallResult {
71    pub object_ids: Vec<ObjectId>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct RawPackIndexResult {
76    pub pack_id: ObjectId,
77    pub index: Vec<u8>,
78    pub objects: Vec<RawPackIndexedObject>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct RawPackIndexedObject {
83    pub oid: ObjectId,
84    pub object_type: ObjectType,
85    pub size: u64,
86    pub offset: u64,
87}
88
89struct PackInstallTeeReader<'a, R, W> {
90    reader: &'a mut R,
91    writer: &'a mut W,
92    max_input_size: Option<u64>,
93    written: u64,
94}
95
96impl<R, W> Read for PackInstallTeeReader<'_, R, W>
97where
98    R: Read,
99    W: Write,
100{
101    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
102        let len = self.reader.read(buf)?;
103        if len > 0 {
104            let next_written = self.written.checked_add(len as u64).ok_or_else(|| {
105                std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
106            })?;
107            if let Some(limit) = self.max_input_size
108                && next_written > limit
109            {
110                return Err(std::io::Error::new(
111                    std::io::ErrorKind::InvalidData,
112                    format!("pack exceeds maximum allowed size ({limit})"),
113                ));
114            }
115            self.writer.write_all(&buf[..len])?;
116            self.written = next_written;
117        }
118        Ok(len)
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ReachablePackFile {
124    pub pack_path: PathBuf,
125    pub pack_size: u64,
126    pub checksum: ObjectId,
127    pub object_count: usize,
128    pub delta_count: u32,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct ReachablePackWriteSummary {
133    pub index: Vec<u8>,
134    pub checksum: ObjectId,
135    pub object_count: usize,
136    pub delta_count: u32,
137    pub pack_size: u64,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141pub struct RawPackInstallOptions {
142    pub promisor: bool,
143    /// Maximum raw pack bytes to accept from the reader. `None` means unlimited,
144    /// mirroring unset `fetch.maxInputSize` / `transfer.maxSize`.
145    pub max_input_size: Option<u64>,
146}
147
148pub trait RawPackInstaller {
149    fn install_raw_pack_from_reader_with_options<R>(
150        &self,
151        reader: &mut R,
152        options: RawPackInstallOptions,
153    ) -> Result<RawPackInstallResult>
154    where
155        R: Read;
156
157    fn install_raw_pack_from_reader<R>(&self, reader: &mut R) -> Result<RawPackInstallResult>
158    where
159        R: Read,
160    {
161        self.install_raw_pack_from_reader_with_options(reader, RawPackInstallOptions::default())
162    }
163}
164
165#[cfg(test)]
166pub(crate) const REACHABLE_PACK_STREAMING_MIN_OBJECTS: usize = 32;
167#[cfg(not(test))]
168pub(crate) const REACHABLE_PACK_STREAMING_MIN_OBJECTS: usize = 4096;
169
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum ObjectPrefixResolution {
172    Missing,
173    Unique(ObjectId),
174    Ambiguous(Vec<ObjectId>),
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct ObjectStorageInfo {
179    pub disk_size: u64,
180    pub deltabase: ObjectId,
181}
182
183impl RawPackInstaller for FileObjectDatabase {
184    fn install_raw_pack_from_reader_with_options<R>(
185        &self,
186        reader: &mut R,
187        options: RawPackInstallOptions,
188    ) -> Result<RawPackInstallResult>
189    where
190        R: Read,
191    {
192        let result =
193            FileObjectDatabase::install_raw_pack_from_reader_with_options(self, reader, options)?;
194        Ok(RawPackInstallResult {
195            object_ids: result.object_ids,
196        })
197    }
198}
199
200impl RawPackInstaller for ObjectDatabase {
201    fn install_raw_pack_from_reader_with_options<R>(
202        &self,
203        reader: &mut R,
204        options: RawPackInstallOptions,
205    ) -> Result<RawPackInstallResult>
206    where
207        R: Read,
208    {
209        let mut pack_bytes = Vec::new();
210        match options.max_input_size {
211            Some(limit) => {
212                reader
213                    .take(limit.saturating_add(1))
214                    .read_to_end(&mut pack_bytes)?;
215                if pack_bytes.len() as u64 > limit {
216                    return Err(GitError::InvalidFormat(format!(
217                        "pack exceeds maximum allowed size ({limit})"
218                    )));
219                }
220            }
221            None => {
222                reader.read_to_end(&mut pack_bytes)?;
223            }
224        }
225        let result = unpack_packfile_objects(&pack_bytes, self.format, self)?;
226        Ok(RawPackInstallResult {
227            object_ids: result.written_objects,
228        })
229    }
230}
231
232impl RawPackStreamingInstall {
233    pub fn bytes_written(&self) -> u64 {
234        self.written
235    }
236
237    pub fn pack_path(&self) -> &Path {
238        &self.pack_path
239    }
240
241    pub fn index_path(&self) -> &Path {
242        &self.index_path
243    }
244
245    pub fn finish(mut self) -> Result<PackInstallResult> {
246        let result = (|| -> Result<PackInstallResult> {
247            let mut file = self.file.take().ok_or_else(|| {
248                GitError::InvalidFormat("raw pack stream already finished".into())
249            })?;
250            file.flush()?;
251            file.sync_all()?;
252            drop(file);
253
254            if self.written != self.expected_pack_size {
255                return Err(GitError::InvalidFormat(format!(
256                    "raw pack stream length mismatch: expected {}, got {}",
257                    self.expected_pack_size, self.written
258                )));
259            }
260
261            let built = PackIndex::write_v2_for_pack_path(&self.temp_pack_path, self.format)?;
262            if built.pack_checksum != self.expected_pack_id {
263                return Err(GitError::InvalidFormat(format!(
264                    "raw pack stream checksum mismatch: expected {}, got {}",
265                    self.expected_pack_id, built.pack_checksum
266                )));
267            }
268
269            match fs::rename(&self.temp_pack_path, &self.pack_path) {
270                Ok(()) => {}
271                Err(_) if self.pack_path.exists() => {
272                    let _ = fs::remove_file(&self.temp_pack_path);
273                }
274                Err(err) => return Err(GitError::Io(err.to_string())),
275            }
276            write_pack_component(&self.index_path, &built.index)?;
277            let promisor_path = write_promisor_pack_sidecar(
278                &self.pack_dir,
279                &self.pack_name,
280                self.options.promisor,
281            )?;
282            Ok(PackInstallResult {
283                pack_name: self.pack_name.clone(),
284                pack_path: self.pack_path.clone(),
285                index_path: self.index_path.clone(),
286                promisor_path,
287                object_ids: built.entries.iter().map(|entry| entry.oid).collect(),
288            })
289        })();
290
291        if result.is_ok() {
292            self.finished = true;
293        } else {
294            let _ = fs::remove_file(&self.temp_pack_path);
295        }
296        result
297    }
298}
299
300impl Write for RawPackStreamingInstall {
301    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
302        let next_written = self.written.checked_add(buf.len() as u64).ok_or_else(|| {
303            std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
304        })?;
305        if next_written > self.expected_pack_size {
306            return Err(std::io::Error::new(
307                std::io::ErrorKind::InvalidData,
308                format!(
309                    "raw pack stream exceeds expected size {}; got at least {}",
310                    self.expected_pack_size, next_written
311                ),
312            ));
313        }
314        let file = self.file.as_mut().ok_or_else(|| {
315            std::io::Error::new(
316                std::io::ErrorKind::BrokenPipe,
317                "raw pack stream already finished",
318            )
319        })?;
320        let written = file.write(buf)?;
321        self.written = self.written.checked_add(written as u64).ok_or_else(|| {
322            std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
323        })?;
324        Ok(written)
325    }
326
327    fn flush(&mut self) -> std::io::Result<()> {
328        match self.file.as_mut() {
329            Some(file) => file.flush(),
330            None => Ok(()),
331        }
332    }
333}
334
335impl Drop for RawPackStreamingInstall {
336    fn drop(&mut self) {
337        if !self.finished {
338            let _ = self.file.take();
339            let _ = fs::remove_file(&self.temp_pack_path);
340        }
341    }
342}
343
344pub fn verify_bundle_prerequisites<R: ObjectReader>(bundle: &Bundle, reader: &R) -> Result<()> {
345    let mut missing = Vec::new();
346    for prerequisite in &bundle.prerequisites {
347        match reader.read_object(&prerequisite.oid) {
348            Ok(object) => {
349                let actual = object.object_id(bundle.format)?;
350                if actual != prerequisite.oid {
351                    return Err(GitError::InvalidObject(format!(
352                        "bundle prerequisite {} hashes to {actual}",
353                        prerequisite.oid
354                    )));
355                }
356            }
357            Err(GitError::NotFound(_)) => missing.push(prerequisite.oid),
358            Err(err) => return Err(err),
359        }
360    }
361    if missing.is_empty() {
362        return Ok(());
363    }
364    Err(GitError::object_not_found_in(
365        missing[0],
366        MissingObjectContext::PackInstall,
367    ))
368}
369
370pub fn unbundle_objects<R, W>(
371    bundle: &Bundle,
372    prerequisite_reader: &R,
373    writer: &mut W,
374) -> Result<BundleUnbundleResult>
375where
376    R: ObjectReader,
377    W: ObjectWriter,
378{
379    verify_bundle_prerequisites(bundle, prerequisite_reader)?;
380    let pack = PackFile::parse_bundle(bundle)?;
381    let written_objects = write_pack_objects(pack, writer, "bundle")?.written_objects;
382    Ok(BundleUnbundleResult {
383        written_objects,
384        references: bundle.references.clone(),
385    })
386}
387
388pub fn install_bundle_pack<R>(
389    bundle: &Bundle,
390    prerequisite_reader: &R,
391    destination: &impl RawPackInstaller,
392) -> Result<BundleUnbundleResult>
393where
394    R: ObjectReader,
395{
396    verify_bundle_prerequisites(bundle, prerequisite_reader)?;
397    let mut reader = bundle.pack.as_slice();
398    let install = destination.install_raw_pack_from_reader(&mut reader)?;
399    Ok(BundleUnbundleResult {
400        written_objects: install.object_ids,
401        references: bundle.references.clone(),
402    })
403}
404
405pub fn unpack_packfile_objects<W>(
406    pack_bytes: &[u8],
407    format: ObjectFormat,
408    writer: &W,
409) -> Result<PackUnpackResult>
410where
411    W: ObjectWriter,
412{
413    let pack = PackFile::parse(pack_bytes, format)?;
414    write_pack_objects(pack, writer, "pack")
415}
416
417pub fn index_raw_pack(pack_bytes: &[u8], format: ObjectFormat) -> Result<RawPackIndexResult> {
418    let pack = PackFile::parse(pack_bytes, format)?;
419    let built = PackIndex::write_v2_for_pack(pack_bytes, format)?;
420    if built.pack_checksum != pack.checksum {
421        return Err(GitError::InvalidFormat(
422            "pack index checksum does not match parsed pack checksum".to_string(),
423        ));
424    }
425
426    let offsets = built
427        .entries
428        .iter()
429        .map(|entry| (entry.oid, entry.offset))
430        .collect::<HashMap<_, _>>();
431    let mut objects = Vec::with_capacity(pack.entries.len());
432    for object in pack.entries {
433        let offset = offsets.get(&object.entry.oid).copied().ok_or_else(|| {
434            GitError::InvalidFormat(format!(
435                "pack index is missing object {}",
436                object.entry.oid.to_hex()
437            ))
438        })?;
439        objects.push(RawPackIndexedObject {
440            oid: object.entry.oid,
441            object_type: object.object.object_type,
442            size: object.object.body.len() as u64,
443            offset,
444        });
445    }
446
447    Ok(RawPackIndexResult {
448        pack_id: built.pack_checksum,
449        index: built.index,
450        objects,
451    })
452}
453
454pub fn index_raw_pack_from_reader<R>(
455    reader: &mut R,
456    format: ObjectFormat,
457) -> Result<RawPackIndexResult>
458where
459    R: Read,
460{
461    Ok(stream_index_build_to_raw_result(
462        PackIndex::write_v2_for_pack_reader_to_trailer(reader, format)?,
463    ))
464}
465
466pub fn index_raw_pack_from_reader_with_len<R>(
467    reader: &mut R,
468    format: ObjectFormat,
469    pack_len: u64,
470) -> Result<RawPackIndexResult>
471where
472    R: Read,
473{
474    Ok(stream_index_build_to_raw_result(
475        PackIndex::write_v2_for_pack_reader_with_len(reader, format, pack_len)?,
476    ))
477}
478
479pub fn index_raw_pack_file(
480    path: impl AsRef<Path>,
481    format: ObjectFormat,
482) -> Result<RawPackIndexResult> {
483    Ok(stream_index_build_to_raw_result(
484        PackIndex::write_v2_for_pack_path(path, format)?,
485    ))
486}
487
488fn stream_index_build_to_raw_result(built: PackStreamIndexBuild) -> RawPackIndexResult {
489    let objects = built
490        .objects
491        .into_iter()
492        .map(|object| RawPackIndexedObject {
493            oid: object.oid,
494            object_type: object.object_type,
495            size: object.size,
496            offset: object.offset,
497        })
498        .collect::<Vec<_>>();
499    RawPackIndexResult {
500        pack_id: built.pack_checksum,
501        index: built.index,
502        objects,
503    }
504}
505
506fn write_pack_objects<W>(pack: PackFile, writer: &W, source: &str) -> Result<PackUnpackResult>
507where
508    W: ObjectWriter,
509{
510    let mut written_objects = Vec::with_capacity(pack.entries.len());
511    for entry in pack.entries {
512        let expected = entry.entry.oid;
513        let actual = writer.write_object(entry.object)?;
514        if actual != expected {
515            return Err(GitError::InvalidObject(format!(
516                "{source} object id mismatch: expected {expected}, wrote {actual}"
517            )));
518        }
519        written_objects.push(actual);
520    }
521    Ok(PackUnpackResult { written_objects })
522}
523pub(crate) fn validate_pack_checksum(
524    pack: &[u8],
525    format: ObjectFormat,
526    expected: &ObjectId,
527    context: &str,
528) -> Result<()> {
529    if expected.format() != format {
530        return Err(GitError::InvalidObjectId(format!(
531            "{context} checksum format does not match object format"
532        )));
533    }
534    let hash_len = format.raw_len();
535    if pack.len() < 12 + hash_len {
536        return Err(GitError::InvalidFormat(format!(
537            "{context} pack file too short"
538        )));
539    }
540    if &pack[..4] != b"PACK" {
541        return Err(GitError::InvalidFormat(format!(
542            "{context} pack file missing PACK signature"
543        )));
544    }
545    let trailer_offset = pack.len() - hash_len;
546    let actual = sley_core::digest_bytes(format, &pack[..trailer_offset])?;
547    let trailer = ObjectId::from_raw(format, &pack[trailer_offset..])?;
548    if &actual != expected || trailer != *expected {
549        return Err(GitError::InvalidFormat(format!(
550            "{context} pack checksum does not match generated pack"
551        )));
552    }
553    Ok(())
554}
555
556impl FileObjectDatabase {
557    pub fn install_pack(&self, pack: &PackWrite) -> Result<PackInstallResult> {
558        self.install_pack_with_options(pack, RawPackInstallOptions::default())
559    }
560
561    pub fn write_blob_as_pack(
562        &self,
563        oid: ObjectId,
564        object: &EncodedObject,
565        compression_level: u32,
566    ) -> Result<ObjectId> {
567        if object.object_type != ObjectType::Blob {
568            return Err(GitError::InvalidObject(
569                "write_blob_as_pack requires a blob object".into(),
570            ));
571        }
572        if oid.format() != self.format {
573            return Err(GitError::InvalidObjectId(format!(
574                "object {oid} uses {}, store uses {}",
575                oid.format().name(),
576                self.format.name()
577            )));
578        }
579        if self.contains(&oid)? {
580            return Ok(oid);
581        }
582        let input = [PackInput { oid: &oid, object }];
583        let options = PackWriteOptions::new()
584            .with_window(0)
585            .with_depth(0)
586            .with_reorder(false)
587            .with_compression_level(compression_level);
588        let pack =
589            PackFile::write_packed_with_known_ids_and_options(&input, self.format, &options)?;
590        self.install_pack(&pack)?;
591        Ok(oid)
592    }
593
594    pub fn write_blobs_as_pack(
595        &self,
596        objects: &[(ObjectId, EncodedObject)],
597        compression_level: u32,
598    ) -> Result<()> {
599        let mut seen = HashSet::with_capacity(objects.len());
600        let mut inputs = Vec::new();
601        for (oid, object) in objects {
602            if object.object_type != ObjectType::Blob {
603                return Err(GitError::InvalidObject(
604                    "write_blobs_as_pack requires blob objects".into(),
605                ));
606            }
607            if oid.format() != self.format {
608                return Err(GitError::InvalidObjectId(format!(
609                    "object {oid} uses {}, store uses {}",
610                    oid.format().name(),
611                    self.format.name()
612                )));
613            }
614            if seen.insert(*oid) && !self.contains(oid)? {
615                inputs.push(PackInput { oid, object });
616            }
617        }
618        if inputs.is_empty() {
619            return Ok(());
620        }
621        let options = PackWriteOptions::new()
622            .with_window(0)
623            .with_depth(0)
624            .with_reorder(false)
625            .with_compression_level(compression_level);
626        let pack =
627            PackFile::write_packed_with_known_ids_and_options(&inputs, self.format, &options)?;
628        self.install_pack(&pack)?;
629        Ok(())
630    }
631
632    pub fn install_pack_with_options(
633        &self,
634        pack: &PackWrite,
635        options: RawPackInstallOptions,
636    ) -> Result<PackInstallResult> {
637        if pack.checksum.format() != self.format {
638            return Err(GitError::InvalidObjectId(format!(
639                "pack checksum uses {}, store uses {}",
640                pack.checksum.format().name(),
641                self.format.name()
642            )));
643        }
644        for entry in &pack.entries {
645            if entry.oid.format() != self.format {
646                return Err(GitError::InvalidObjectId(format!(
647                    "pack entry {} uses {}, store uses {}",
648                    entry.oid,
649                    entry.oid.format().name(),
650                    self.format.name()
651                )));
652            }
653        }
654        let canonical_index = PackIndex::write_v2_for_pack(&pack.pack, self.format)?;
655        let parsed_index = PackIndex::parse(&pack.index, self.format)?;
656        if canonical_index.pack_checksum != pack.checksum
657            || parsed_index.pack_checksum != pack.checksum
658        {
659            return Err(GitError::InvalidFormat(
660                "pack and index checksums do not match pack write".into(),
661            ));
662        }
663        if pack.index != canonical_index.index {
664            return Err(GitError::InvalidFormat(
665                "pack index does not match pack contents".into(),
666            ));
667        }
668
669        let pack_dir = self.objects_dir.join("pack");
670        fs::create_dir_all(&pack_dir)?;
671        let pack_name = format!("pack-{}", pack.checksum.to_hex());
672        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
673        let index_path = pack_dir.join(format!("{pack_name}.idx"));
674        if !pack_path.exists() || !index_path.exists() {
675            write_pack_component(&pack_path, &pack.pack)?;
676            write_pack_component(&index_path, &pack.index)?;
677        }
678        let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
679        Ok(PackInstallResult {
680            pack_name,
681            pack_path,
682            index_path,
683            promisor_path,
684            object_ids: canonical_index
685                .entries
686                .iter()
687                .map(|entry| entry.oid)
688                .collect(),
689        })
690    }
691
692    /// Install a pack that was produced in this process by [`PackFile::write_packed`].
693    ///
694    /// Unlike [`Self::install_raw_pack_with_options`], this does not re-inflate
695    /// every pack entry to rebuild the index. It validates the generated pack
696    /// trailer and generated index against the writer's object ids, CRCs, and
697    /// offsets, then writes those bytes directly. Use the raw installer for
698    /// arbitrary pack bytes received from an untrusted transport.
699    pub fn install_written_pack(&self, pack: &PackWrite) -> Result<PackInstallResult> {
700        self.install_written_pack_with_options(pack, RawPackInstallOptions::default())
701    }
702
703    pub fn install_written_pack_with_options(
704        &self,
705        pack: &PackWrite,
706        options: RawPackInstallOptions,
707    ) -> Result<PackInstallResult> {
708        validate_pack_checksum(&pack.pack, self.format, &pack.checksum, "pack write")?;
709        let parsed_index = PackIndex::parse(&pack.index, self.format)?;
710        if parsed_index.pack_checksum != pack.checksum {
711            return Err(GitError::InvalidFormat(
712                "pack write index checksum does not match pack".into(),
713            ));
714        }
715        if !pack_index_entries_match_writer(&parsed_index.entries, &pack.entries) {
716            return Err(GitError::InvalidFormat(
717                "pack write index does not match generated entries".into(),
718            ));
719        }
720        self.install_generated_pack_unchecked(pack, options)
721    }
722
723    fn install_generated_pack_unchecked(
724        &self,
725        pack: &PackWrite,
726        options: RawPackInstallOptions,
727    ) -> Result<PackInstallResult> {
728        let pack_dir = self.objects_dir.join("pack");
729        fs::create_dir_all(&pack_dir)?;
730        let pack_name = format!("pack-{}", pack.checksum.to_hex());
731        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
732        let index_path = pack_dir.join(format!("{pack_name}.idx"));
733        if !pack_path.exists() || !index_path.exists() {
734            write_pack_component(&pack_path, &pack.pack)?;
735            write_pack_component(&index_path, &pack.index)?;
736        }
737        let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
738        Ok(PackInstallResult {
739            pack_name,
740            pack_path,
741            index_path,
742            promisor_path,
743            object_ids: pack.entries.iter().map(|entry| entry.oid).collect(),
744        })
745    }
746
747    pub(crate) fn install_pack_file_from_temp(
748        &self,
749        temp_pack_path: &Path,
750        pack_checksum: ObjectId,
751        index: &[u8],
752        object_ids: Vec<ObjectId>,
753        options: RawPackInstallOptions,
754    ) -> Result<PackInstallResult> {
755        let pack_dir = self.objects_dir.join("pack");
756        fs::create_dir_all(&pack_dir)?;
757        let pack_name = format!("pack-{}", pack_checksum.to_hex());
758        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
759        let index_path = pack_dir.join(format!("{pack_name}.idx"));
760        match fs::rename(temp_pack_path, &pack_path) {
761            Ok(()) => {}
762            Err(_) if pack_path.exists() => {
763                let _ = fs::remove_file(temp_pack_path);
764            }
765            Err(err) => return Err(GitError::Io(err.to_string())),
766        }
767        write_pack_component(&index_path, index)?;
768        let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
769        Ok(PackInstallResult {
770            pack_name,
771            pack_path,
772            index_path,
773            promisor_path,
774            object_ids,
775        })
776    }
777
778    pub fn install_raw_pack_from_reader<R>(&self, reader: &mut R) -> Result<PackInstallResult>
779    where
780        R: Read,
781    {
782        self.install_raw_pack_from_reader_with_options(reader, RawPackInstallOptions::default())
783    }
784
785    pub fn begin_raw_pack_install(
786        &self,
787        expected_pack_id: ObjectId,
788        expected_pack_size: u64,
789    ) -> Result<RawPackStreamingInstall> {
790        self.begin_raw_pack_install_with_options(
791            expected_pack_id,
792            expected_pack_size,
793            RawPackInstallOptions::default(),
794        )
795    }
796
797    pub fn begin_raw_pack_install_with_options(
798        &self,
799        expected_pack_id: ObjectId,
800        expected_pack_size: u64,
801        options: RawPackInstallOptions,
802    ) -> Result<RawPackStreamingInstall> {
803        if expected_pack_id.format() != self.format {
804            return Err(GitError::InvalidObjectId(format!(
805                "pack checksum uses {}, store uses {}",
806                expected_pack_id.format().name(),
807                self.format.name()
808            )));
809        }
810        let pack_dir = self.objects_dir.join("pack");
811        fs::create_dir_all(&pack_dir)?;
812        let pack_name = format!("pack-{}", expected_pack_id.to_hex());
813        let pack_path = pack_dir.join(format!("{pack_name}.pack"));
814        let index_path = pack_dir.join(format!("{pack_name}.idx"));
815        let temp_pack_path = unique_temp_path(&pack_dir);
816        let file = fs::OpenOptions::new()
817            .write(true)
818            .create_new(true)
819            .open(&temp_pack_path)?;
820        Ok(RawPackStreamingInstall {
821            format: self.format,
822            expected_pack_id,
823            expected_pack_size,
824            options,
825            pack_dir,
826            pack_name,
827            pack_path,
828            index_path,
829            temp_pack_path,
830            file: Some(file),
831            written: 0,
832            finished: false,
833        })
834    }
835
836    pub fn install_raw_pack_from_reader_with_options<R>(
837        &self,
838        reader: &mut R,
839        options: RawPackInstallOptions,
840    ) -> Result<PackInstallResult>
841    where
842        R: Read,
843    {
844        let pack_dir = self.objects_dir.join("pack");
845        fs::create_dir_all(&pack_dir)?;
846        let temp_pack_path = unique_temp_path(&pack_dir);
847        let result = (|| -> Result<PackInstallResult> {
848            // Stage directly in objects/pack so validation, indexing, and the
849            // eventual checksum-named rename use one streamed write.
850            let mut file = fs::OpenOptions::new()
851                .write(true)
852                .create_new(true)
853                .open(&temp_pack_path)?;
854            let built = {
855                let mut tee = PackInstallTeeReader {
856                    reader,
857                    writer: &mut file,
858                    max_input_size: options.max_input_size,
859                    written: 0,
860                };
861                PackIndex::write_v2_for_pack_reader_to_trailer(&mut tee, self.format)?
862            };
863            file.flush()?;
864            file.sync_all()?;
865            drop(file);
866
867            self.install_pack_file_from_temp(
868                &temp_pack_path,
869                built.pack_checksum,
870                &built.index,
871                built.entries.iter().map(|entry| entry.oid).collect(),
872                options,
873            )
874        })();
875        if result.is_err() {
876            let _ = fs::remove_file(&temp_pack_path);
877        }
878        result
879    }
880
881}
882
883pub(crate) fn write_pack_component(path: &Path, bytes: &[u8]) -> Result<()> {
884    if path.exists() {
885        return Ok(());
886    }
887    let parent = path
888        .parent()
889        .ok_or_else(|| GitError::InvalidPath("pack component path has no parent".into()))?;
890    fs::create_dir_all(parent)?;
891    let temp_path = unique_temp_path(parent);
892    let write_result = (|| -> Result<()> {
893        {
894            let mut file = fs::OpenOptions::new()
895                .write(true)
896                .create_new(true)
897                .open(&temp_path)?;
898            file.write_all(bytes)?;
899            file.sync_all()?;
900        }
901        match fs::rename(&temp_path, path) {
902            Ok(()) => Ok(()),
903            Err(_) if path.exists() => {
904                let _ = fs::remove_file(&temp_path);
905                Ok(())
906            }
907            Err(err) => Err(GitError::Io(err.to_string())),
908        }
909    })();
910    if write_result.is_err() {
911        let _ = fs::remove_file(&temp_path);
912    }
913    write_result
914}
915
916pub(crate) fn write_promisor_pack_sidecar(
917    pack_dir: &Path,
918    pack_name: &str,
919    promisor: bool,
920) -> Result<Option<PathBuf>> {
921    if !promisor {
922        return Ok(None);
923    }
924    let path = pack_dir.join(format!("{pack_name}.promisor"));
925    write_pack_component(&path, b"")?;
926    Ok(Some(path))
927}