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#[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 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 pub fn git_dir(&self) -> &Path {
111 &self.git_dir
112 }
113
114 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 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 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 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 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 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 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 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
1174pub(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}