1use sley_core::{
2 CancelFlag, CancellableRead, GitError, MissingObjectContext, ObjectFormat, ObjectId, Result,
3};
4use sley_formats::{Bundle, BundleReference};
5use sley_object::{EncodedObject, ObjectType};
6use sley_pack::{
7 PackFile, PackIndex, PackIndexBuild, PackIndexProgress, PackInput, PackWrite, PackWriteOptions,
8 fix_thin_pack,
9};
10use std::collections::HashSet;
11use std::fs;
12use std::io::{Read, Write};
13use std::path::{Path, PathBuf};
14use std::sync::mpsc;
15
16use crate::{ObjectReader, ObjectWriter, unique_temp_path};
17
18use crate::pack::{FileObjectDatabase, ObjectDatabase};
19use crate::repack::pack_index_entries_match_writer;
20
21pub struct BundleUnbundleResult {
22 pub written_objects: Vec<ObjectId>,
23 pub references: Vec<BundleReference>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct PackUnpackResult {
28 pub written_objects: Vec<ObjectId>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PackInstallResult {
33 pub pack_name: String,
34 pub pack_path: PathBuf,
35 pub index_path: PathBuf,
36 pub promisor_path: Option<PathBuf>,
37 pub object_ids: Vec<ObjectId>,
38}
39
40#[derive(Debug)]
48pub struct IncomingPackQuarantine {
49 git_dir: PathBuf,
50 object_dir: PathBuf,
51 destination_objects_dir: PathBuf,
52 format: ObjectFormat,
53 promisor_remote_present: bool,
54 promoted: bool,
55}
56
57impl IncomingPackQuarantine {
58 pub fn new(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<Self> {
59 let source_git_dir = git_dir.as_ref().to_path_buf();
60 let destination_objects_dir = crate::repository_objects_dir(&source_git_dir);
61 fs::create_dir_all(&destination_objects_dir)?;
62 let quarantine_git_dir = create_incoming_object_dir(&destination_objects_dir)?;
63 let object_dir = quarantine_git_dir.join("objects");
64 let result = (|| -> Result<()> {
65 fs::create_dir_all(object_dir.join("pack"))?;
66 fs::create_dir_all(object_dir.join("info"))?;
67 let mut alternates = vec![
68 fs::canonicalize(&destination_objects_dir)
69 .unwrap_or_else(|_| destination_objects_dir.clone()),
70 ];
71 alternates.extend(crate::registry::alternate_object_dirs(
77 &destination_objects_dir,
78 ));
79 let mut alternate_file = String::new();
80 for alternate in alternates {
81 let alternate = fs::canonicalize(&alternate).unwrap_or(alternate);
82 alternate_file.push_str(&alternate.to_string_lossy());
83 alternate_file.push('\n');
84 }
85 fs::write(object_dir.join("info/alternates"), alternate_file)?;
86 let shallow = quarantine_git_dir.join("shallow");
87 let source_shallow = source_git_dir.join("shallow");
88 if source_shallow.exists() {
89 fs::copy(source_shallow, shallow)?;
90 }
91 Ok(())
92 })();
93 if let Err(err) = result {
94 let _ = fs::remove_dir_all(&quarantine_git_dir);
95 return Err(err);
96 }
97 Ok(Self {
98 git_dir: quarantine_git_dir,
99 object_dir,
100 destination_objects_dir,
101 format,
102 promisor_remote_present: false,
103 promoted: false,
104 })
105 }
106
107 pub fn object_dir(&self) -> &Path {
108 &self.object_dir
109 }
110
111 pub fn git_dir(&self) -> &Path {
114 &self.git_dir
115 }
116
117 pub fn with_promisor_remote_present(mut self, present: bool) -> Self {
120 self.promisor_remote_present = present;
121 self
122 }
123
124 pub fn database(&self) -> FileObjectDatabase {
125 FileObjectDatabase::new(self.object_dir.clone(), self.format)
126 .with_promisor_remote_present(self.promisor_remote_present)
127 }
128
129 pub fn promote(mut self) -> Result<()> {
135 let mut files = incoming_object_files(&self.object_dir)?;
136 files.sort_by_key(|path| {
137 let is_pack = path.extension().is_some_and(|ext| ext == "pack");
138 (is_pack, path.clone())
139 });
140 let mut moved = Vec::new();
141 for source in files {
142 let relative = source
143 .strip_prefix(&self.object_dir)
144 .map_err(|_| GitError::InvalidPath("incoming object escaped quarantine".into()))?;
145 let destination = self.destination_objects_dir.join(relative);
146 let parent = destination.parent().ok_or_else(|| {
147 GitError::InvalidPath("incoming object has no destination parent".into())
148 })?;
149 fs::create_dir_all(parent)?;
150 if destination.exists() {
151 fs::remove_file(&source)?;
152 continue;
153 }
154 if let Err(err) = fs::rename(&source, &destination) {
155 for (promoted, staged) in moved.iter().rev() {
156 let _ = fs::rename(promoted, staged);
157 }
158 return Err(GitError::Io(err.to_string()));
159 }
160 moved.push((destination, source));
161 }
162 self.promoted = true;
163 fs::remove_dir_all(&self.git_dir)?;
164 Ok(())
165 }
166}
167
168impl Drop for IncomingPackQuarantine {
169 fn drop(&mut self) {
170 if !self.promoted {
171 let _ = fs::remove_dir_all(&self.git_dir);
172 }
173 }
174}
175
176fn create_incoming_object_dir(objects_dir: &Path) -> Result<PathBuf> {
177 for _ in 0..100 {
178 let object_dir = unique_temp_path(objects_dir).with_extension("incoming");
179 match fs::create_dir(&object_dir) {
180 Ok(()) => return Ok(object_dir),
181 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
182 Err(err) => return Err(GitError::Io(err.to_string())),
183 }
184 }
185 Err(GitError::Io(
186 "could not create incoming object quarantine".into(),
187 ))
188}
189
190fn incoming_object_files(object_dir: &Path) -> Result<Vec<PathBuf>> {
191 let mut files = Vec::new();
192 let pack_dir = object_dir.join("pack");
193 if pack_dir.exists() {
194 for entry in fs::read_dir(pack_dir)? {
195 let path = entry?.path();
196 if path.is_file() {
197 files.push(path);
198 }
199 }
200 }
201 for entry in fs::read_dir(object_dir)? {
202 let entry = entry?;
203 let name = entry.file_name();
204 let name = name.to_string_lossy();
205 if name.len() != 2 || !name.bytes().all(|byte| byte.is_ascii_hexdigit()) {
206 continue;
207 }
208 for loose in fs::read_dir(entry.path())? {
209 let path = loose?.path();
210 if path.is_file() {
211 files.push(path);
212 }
213 }
214 }
215 Ok(files)
216}
217
218#[derive(Debug)]
219pub struct RawPackStreamingInstall {
220 format: ObjectFormat,
221 expected_pack_id: ObjectId,
222 expected_pack_size: u64,
223 options: RawPackInstallOptions,
224 pack_dir: PathBuf,
225 pack_name: String,
226 pack_path: PathBuf,
227 index_path: PathBuf,
228 temp_pack_path: PathBuf,
229 file: Option<fs::File>,
230 written: u64,
231 finished: bool,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct RawPackInstallResult {
236 pub object_ids: Vec<ObjectId>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct RawPackIndexResult {
241 pub pack_id: ObjectId,
242 pub index: Vec<u8>,
243 pub objects: Vec<RawPackIndexedObject>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct RawPackIndexedObject {
248 pub oid: ObjectId,
249 pub object_type: ObjectType,
250 pub size: u64,
251 pub offset: u64,
252}
253
254#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
256pub struct PackInstallProgress {
257 pub received_bytes: u64,
259 pub indexed_objects: u64,
261 pub total_objects: u64,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct ReachablePackFile {
267 pub pack_path: PathBuf,
268 pub pack_size: u64,
269 pub checksum: ObjectId,
270 pub object_count: usize,
271 pub delta_count: u32,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct ReachablePackWriteSummary {
276 pub index: Vec<u8>,
277 pub checksum: ObjectId,
278 pub object_count: usize,
279 pub delta_count: u32,
280 pub pack_size: u64,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
284pub struct RawPackInstallOptions {
285 pub promisor: bool,
286 pub max_input_size: Option<u64>,
289}
290
291pub trait RawPackInstaller {
292 fn install_raw_pack_from_reader_with_options<R>(
293 &self,
294 reader: &mut R,
295 options: RawPackInstallOptions,
296 ) -> Result<RawPackInstallResult>
297 where
298 R: Read;
299
300 fn install_raw_pack_from_reader<R>(&self, reader: &mut R) -> Result<RawPackInstallResult>
301 where
302 R: Read,
303 {
304 self.install_raw_pack_from_reader_with_options(reader, RawPackInstallOptions::default())
305 }
306
307 fn install_raw_pack_from_reader_with_progress<R, F>(
315 &self,
316 reader: &mut R,
317 options: RawPackInstallOptions,
318 progress: F,
319 ) -> Result<RawPackInstallResult>
320 where
321 R: Read,
322 F: FnMut(PackInstallProgress),
323 {
324 self.install_raw_pack_from_reader_with_progress_and_cancel(
325 reader,
326 options,
327 CancelFlag::never(),
328 progress,
329 )
330 }
331
332 fn install_raw_pack_from_reader_with_progress_and_cancel<R, F>(
339 &self,
340 reader: &mut R,
341 options: RawPackInstallOptions,
342 cancel: CancelFlag<'_>,
343 _progress: F,
344 ) -> Result<RawPackInstallResult>
345 where
346 R: Read,
347 F: FnMut(PackInstallProgress),
348 {
349 cancel.check()?;
350 let mut cancellable = CancellableRead::new(reader, cancel.as_ref());
351 self.install_raw_pack_from_reader_with_options(&mut cancellable, options)
352 .map_err(map_install_cancel_error)
353 }
354}
355
356fn map_install_cancel_error(err: GitError) -> GitError {
363 if err.is_cancelled() {
364 GitError::Cancelled
365 } else {
366 err
367 }
368}
369
370const PACK_RECEIVE_BUFFER_BYTES: usize = 1024 * 1024;
371const PACK_RECEIVE_QUEUE_DEPTH: usize = 4;
372const PACK_RECEIVE_PROGRESS_BYTES: u64 = 256 * 1024;
373
374#[derive(Debug, Clone, Copy)]
375struct PackReceiveSummary {
376 bytes: u64,
377}
378
379fn receive_pack_to_file<R, F>(
380 reader: &mut R,
381 mut file: fs::File,
382 max_input_size: Option<u64>,
383 cancel: CancelFlag<'_>,
384 progress: &mut F,
385) -> Result<PackReceiveSummary>
386where
387 R: Read,
388 F: FnMut(PackInstallProgress),
389{
390 let (filled_sender, filled_receiver) = mpsc::sync_channel::<Vec<u8>>(PACK_RECEIVE_QUEUE_DEPTH);
391 let (empty_sender, empty_receiver) = mpsc::sync_channel::<Vec<u8>>(PACK_RECEIVE_QUEUE_DEPTH);
392 for _ in 0..PACK_RECEIVE_QUEUE_DEPTH {
393 empty_sender
394 .send(vec![0u8; PACK_RECEIVE_BUFFER_BYTES])
395 .map_err(|_| GitError::Io("could not initialize pack receive buffers".into()))?;
396 }
397
398 std::thread::scope(|scope| {
399 let writer = scope.spawn(move || -> Result<()> {
400 for mut chunk in filled_receiver {
401 #[cfg(feature = "fetch-profile")]
402 let _profile_span = sley_core::fetch_profile::Span::enter(
403 sley_core::fetch_profile::Stage::ObjectStoreWrite,
404 );
405 file.write_all(&chunk)?;
406 #[cfg(feature = "fetch-profile")]
407 {
408 sley_core::fetch_profile::add_count(
409 sley_core::fetch_profile::Stage::ObjectStoreWrite,
410 1,
411 );
412 sley_core::fetch_profile::add_bytes(
413 sley_core::fetch_profile::Stage::ObjectStoreWrite,
414 chunk.len() as u64,
415 );
416 }
417 chunk.resize(PACK_RECEIVE_BUFFER_BYTES, 0);
418 if empty_sender.send(chunk).is_err() {
419 break;
420 }
421 }
422 file.flush()?;
423 file.sync_all()?;
424 #[cfg(feature = "fetch-profile")]
425 sley_core::fetch_profile::add_fsync();
426 Ok(())
427 });
428
429 let receive_result = (|| -> Result<PackReceiveSummary> {
430 let mut bytes = 0u64;
431 let mut last_progress = 0u64;
432 let mut header = Vec::with_capacity(12);
433 let mut total_objects = 0u64;
434 loop {
435 cancel.check()?;
436 let mut chunk = empty_receiver.recv().map_err(|_| {
437 GitError::Io("pack staging writer stopped before receive completed".into())
438 })?;
439 let read = reader.read(&mut chunk)?;
440 if read == 0 {
441 break;
442 }
443 chunk.truncate(read);
444 bytes = bytes
445 .checked_add(read as u64)
446 .ok_or_else(|| GitError::InvalidFormat("pack size overflow".into()))?;
447 if let Some(limit) = max_input_size
448 && bytes > limit
449 {
450 return Err(GitError::InvalidFormat(format!(
451 "pack exceeds maximum allowed size ({limit})"
452 )));
453 }
454 if header.len() < 12 {
455 let needed = 12 - header.len();
456 header.extend_from_slice(&chunk[..needed.min(chunk.len())]);
457 if header.len() == 12 && &header[..4] == b"PACK" {
458 total_objects = u64::from(u32::from_be_bytes([
459 header[8], header[9], header[10], header[11],
460 ]));
461 progress(PackInstallProgress {
462 received_bytes: 12,
463 indexed_objects: 0,
464 total_objects,
465 });
466 last_progress = 12;
467 cancel.check()?;
468 }
469 }
470 filled_sender.send(chunk).map_err(|_| {
471 GitError::Io("pack staging writer stopped before receive completed".into())
472 })?;
473 if bytes.saturating_sub(last_progress) >= PACK_RECEIVE_PROGRESS_BYTES {
474 last_progress = bytes;
475 progress(PackInstallProgress {
476 received_bytes: bytes,
477 indexed_objects: 0,
478 total_objects,
479 });
480 cancel.check()?;
481 }
482 }
483 progress(PackInstallProgress {
484 received_bytes: bytes,
485 indexed_objects: 0,
486 total_objects,
487 });
488 cancel.check()?;
489 Ok(PackReceiveSummary { bytes })
490 })();
491 drop(filled_sender);
492 let write_result = match writer.join() {
493 Ok(result) => result,
494 Err(_) => Err(GitError::Io("pack staging writer panicked".into())),
495 };
496 let summary = receive_result?;
497 write_result?;
498 Ok(summary)
499 })
500}
501
502#[cfg(test)]
503pub(crate) const REACHABLE_PACK_STREAMING_MIN_OBJECTS: usize = 32;
504#[cfg(not(test))]
505pub(crate) const REACHABLE_PACK_STREAMING_MIN_OBJECTS: usize = 4096;
506
507#[derive(Debug, Clone, PartialEq, Eq)]
508pub enum ObjectPrefixResolution {
509 Missing,
510 Unique(ObjectId),
511 Ambiguous(Vec<ObjectId>),
512}
513
514#[derive(Debug, Clone, PartialEq, Eq)]
515pub struct ObjectStorageInfo {
516 pub disk_size: u64,
517 pub deltabase: ObjectId,
518}
519
520impl RawPackInstaller for FileObjectDatabase {
521 fn install_raw_pack_from_reader_with_options<R>(
522 &self,
523 reader: &mut R,
524 options: RawPackInstallOptions,
525 ) -> Result<RawPackInstallResult>
526 where
527 R: Read,
528 {
529 let result =
530 FileObjectDatabase::install_raw_pack_from_reader_with_options(self, reader, options)?;
531 Ok(RawPackInstallResult {
532 object_ids: result.object_ids,
533 })
534 }
535
536 fn install_raw_pack_from_reader_with_progress_and_cancel<R, F>(
537 &self,
538 reader: &mut R,
539 options: RawPackInstallOptions,
540 cancel: CancelFlag<'_>,
541 progress: F,
542 ) -> Result<RawPackInstallResult>
543 where
544 R: Read,
545 F: FnMut(PackInstallProgress),
546 {
547 let result = FileObjectDatabase::install_raw_pack_from_reader_with_progress_and_cancel(
548 self, reader, options, cancel, progress,
549 )?;
550 Ok(RawPackInstallResult {
551 object_ids: result.object_ids,
552 })
553 }
554}
555
556impl RawPackInstaller for ObjectDatabase {
557 fn install_raw_pack_from_reader_with_options<R>(
558 &self,
559 reader: &mut R,
560 options: RawPackInstallOptions,
561 ) -> Result<RawPackInstallResult>
562 where
563 R: Read,
564 {
565 let mut pack_bytes = Vec::new();
566 match options.max_input_size {
567 Some(limit) => {
568 reader
569 .take(limit.saturating_add(1))
570 .read_to_end(&mut pack_bytes)?;
571 if pack_bytes.len() as u64 > limit {
572 return Err(GitError::InvalidFormat(format!(
573 "pack exceeds maximum allowed size ({limit})"
574 )));
575 }
576 }
577 None => {
578 reader.read_to_end(&mut pack_bytes)?;
579 }
580 }
581 let result = unpack_packfile_objects(&pack_bytes, self.format, self)?;
582 Ok(RawPackInstallResult {
583 object_ids: result.written_objects,
584 })
585 }
586}
587
588impl RawPackStreamingInstall {
589 pub fn bytes_written(&self) -> u64 {
590 self.written
591 }
592
593 pub fn pack_path(&self) -> &Path {
594 &self.pack_path
595 }
596
597 pub fn index_path(&self) -> &Path {
598 &self.index_path
599 }
600
601 pub fn finish(mut self) -> Result<PackInstallResult> {
602 let result = (|| -> Result<PackInstallResult> {
603 let mut file = self.file.take().ok_or_else(|| {
604 GitError::InvalidFormat("raw pack stream already finished".into())
605 })?;
606 #[cfg(feature = "fetch-profile")]
607 let _profile_span = sley_core::fetch_profile::Span::enter(
608 sley_core::fetch_profile::Stage::ObjectStoreWrite,
609 );
610 file.flush()?;
611 file.sync_all()?;
612 #[cfg(feature = "fetch-profile")]
613 sley_core::fetch_profile::add_fsync();
614 drop(file);
615
616 if self.written != self.expected_pack_size {
617 return Err(GitError::InvalidFormat(format!(
618 "raw pack stream length mismatch: expected {}, got {}",
619 self.expected_pack_size, self.written
620 )));
621 }
622
623 let built = {
624 let mapped = sley_mmap::MappedFile::open_pack(&self.temp_pack_path)?;
625 PackIndex::write_v2_for_pack(mapped.as_bytes(), self.format)?
626 };
627 if built.pack_checksum != self.expected_pack_id {
628 return Err(GitError::InvalidFormat(format!(
629 "raw pack stream checksum mismatch: expected {}, got {}",
630 self.expected_pack_id, built.pack_checksum
631 )));
632 }
633
634 match fs::rename(&self.temp_pack_path, &self.pack_path) {
635 Ok(()) => {}
636 Err(_) if self.pack_path.exists() => {
637 let _ = fs::remove_file(&self.temp_pack_path);
638 }
639 Err(err) => return Err(GitError::Io(err.to_string())),
640 }
641 write_pack_component(&self.index_path, &built.index)?;
642 let promisor_path = write_promisor_pack_sidecar(
643 &self.pack_dir,
644 &self.pack_name,
645 self.options.promisor,
646 )?;
647 Ok(PackInstallResult {
648 pack_name: self.pack_name.clone(),
649 pack_path: self.pack_path.clone(),
650 index_path: self.index_path.clone(),
651 promisor_path,
652 object_ids: built.entries.iter().map(|entry| entry.oid).collect(),
653 })
654 })();
655
656 if result.is_ok() {
657 self.finished = true;
658 } else {
659 let _ = fs::remove_file(&self.temp_pack_path);
660 }
661 result
662 }
663}
664
665impl Write for RawPackStreamingInstall {
666 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
667 let next_written = self.written.checked_add(buf.len() as u64).ok_or_else(|| {
668 std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
669 })?;
670 if next_written > self.expected_pack_size {
671 return Err(std::io::Error::new(
672 std::io::ErrorKind::InvalidData,
673 format!(
674 "raw pack stream exceeds expected size {}; got at least {}",
675 self.expected_pack_size, next_written
676 ),
677 ));
678 }
679 let file = self.file.as_mut().ok_or_else(|| {
680 std::io::Error::new(
681 std::io::ErrorKind::BrokenPipe,
682 "raw pack stream already finished",
683 )
684 })?;
685 let written = file.write(buf)?;
686 self.written = self.written.checked_add(written as u64).ok_or_else(|| {
687 std::io::Error::new(std::io::ErrorKind::InvalidData, "pack size overflow")
688 })?;
689 Ok(written)
690 }
691
692 fn flush(&mut self) -> std::io::Result<()> {
693 match self.file.as_mut() {
694 Some(file) => file.flush(),
695 None => Ok(()),
696 }
697 }
698}
699
700impl Drop for RawPackStreamingInstall {
701 fn drop(&mut self) {
702 if !self.finished {
703 let _ = self.file.take();
704 let _ = fs::remove_file(&self.temp_pack_path);
705 }
706 }
707}
708
709pub fn verify_bundle_prerequisites<R: ObjectReader>(bundle: &Bundle, reader: &R) -> Result<()> {
710 let mut missing = Vec::new();
711 for prerequisite in &bundle.prerequisites {
712 match reader.read_object(&prerequisite.oid) {
713 Ok(object) => {
714 let actual = object.object_id(bundle.format)?;
715 if actual != prerequisite.oid {
716 return Err(GitError::InvalidObject(format!(
717 "bundle prerequisite {} hashes to {actual}",
718 prerequisite.oid
719 )));
720 }
721 }
722 Err(GitError::NotFound(_)) => missing.push(prerequisite.oid),
723 Err(err) => return Err(err),
724 }
725 }
726 if missing.is_empty() {
727 return Ok(());
728 }
729 Err(GitError::object_not_found_in(
730 missing[0],
731 MissingObjectContext::PackInstall,
732 ))
733}
734
735pub fn unbundle_objects<R, W>(
736 bundle: &Bundle,
737 prerequisite_reader: &R,
738 writer: &mut W,
739) -> Result<BundleUnbundleResult>
740where
741 R: ObjectReader,
742 W: ObjectWriter,
743{
744 verify_bundle_prerequisites(bundle, prerequisite_reader)?;
745 let pack = PackFile::parse_bundle(bundle)?;
746 let written_objects = write_pack_objects(pack, writer, "bundle")?.written_objects;
747 Ok(BundleUnbundleResult {
748 written_objects,
749 references: bundle.references.clone(),
750 })
751}
752
753pub fn install_bundle_pack<R>(
754 bundle: &Bundle,
755 prerequisite_reader: &R,
756 destination: &impl RawPackInstaller,
757) -> Result<BundleUnbundleResult>
758where
759 R: ObjectReader,
760{
761 verify_bundle_prerequisites(bundle, prerequisite_reader)?;
762 let mut reader = bundle.pack.as_slice();
763 let install = destination.install_raw_pack_from_reader(&mut reader)?;
764 Ok(BundleUnbundleResult {
765 written_objects: install.object_ids,
766 references: bundle.references.clone(),
767 })
768}
769
770pub fn unpack_packfile_objects<W>(
771 pack_bytes: &[u8],
772 format: ObjectFormat,
773 writer: &W,
774) -> Result<PackUnpackResult>
775where
776 W: ObjectWriter,
777{
778 let pack = PackFile::parse(pack_bytes, format)?;
779 write_pack_objects(pack, writer, "pack")
780}
781
782pub fn index_raw_pack(pack_bytes: &[u8], format: ObjectFormat) -> Result<RawPackIndexResult> {
783 let built = PackIndex::write_v2_for_pack(pack_bytes, format)?;
784 Ok(index_build_to_raw_result(built))
785}
786
787pub fn index_raw_pack_file(
788 path: impl AsRef<Path>,
789 format: ObjectFormat,
790) -> Result<RawPackIndexResult> {
791 let mapped = sley_mmap::MappedFile::open_pack(path.as_ref())?;
792 Ok(index_build_to_raw_result(PackIndex::write_v2_for_pack(
793 mapped.as_bytes(),
794 format,
795 )?))
796}
797
798fn index_build_to_raw_result(built: PackIndexBuild) -> RawPackIndexResult {
799 let objects = built
800 .objects
801 .into_iter()
802 .map(|object| RawPackIndexedObject {
803 oid: object.oid,
804 object_type: object.object_type,
805 size: object.size,
806 offset: object.offset,
807 })
808 .collect::<Vec<_>>();
809 RawPackIndexResult {
810 pack_id: built.pack_checksum,
811 index: built.index,
812 objects,
813 }
814}
815
816fn write_pack_objects<W>(pack: PackFile, writer: &W, source: &str) -> Result<PackUnpackResult>
817where
818 W: ObjectWriter,
819{
820 let mut written_objects = Vec::with_capacity(pack.entries.len());
821 for entry in pack.entries {
822 let expected = entry.entry.oid;
823 let actual = writer.write_object(entry.object)?;
824 if actual != expected {
825 return Err(GitError::InvalidObject(format!(
826 "{source} object id mismatch: expected {expected}, wrote {actual}"
827 )));
828 }
829 written_objects.push(actual);
830 }
831 Ok(PackUnpackResult { written_objects })
832}
833pub(crate) fn validate_pack_checksum(
834 pack: &[u8],
835 format: ObjectFormat,
836 expected: &ObjectId,
837 context: &str,
838) -> Result<()> {
839 if expected.format() != format {
840 return Err(GitError::InvalidObjectId(format!(
841 "{context} checksum format does not match object format"
842 )));
843 }
844 let hash_len = format.raw_len();
845 if pack.len() < 12 + hash_len {
846 return Err(GitError::InvalidFormat(format!(
847 "{context} pack file too short"
848 )));
849 }
850 if &pack[..4] != b"PACK" {
851 return Err(GitError::InvalidFormat(format!(
852 "{context} pack file missing PACK signature"
853 )));
854 }
855 let trailer_offset = pack.len() - hash_len;
856 let actual = sley_core::digest_bytes(format, &pack[..trailer_offset])?;
857 let trailer = ObjectId::from_raw(format, &pack[trailer_offset..])?;
858 if &actual != expected || trailer != *expected {
859 return Err(GitError::InvalidFormat(format!(
860 "{context} pack checksum does not match generated pack"
861 )));
862 }
863 Ok(())
864}
865
866impl FileObjectDatabase {
867 pub fn install_pack(&self, pack: &PackWrite) -> Result<PackInstallResult> {
868 self.install_pack_with_options(pack, RawPackInstallOptions::default())
869 }
870
871 pub fn write_blob_as_pack(
872 &self,
873 oid: ObjectId,
874 object: &EncodedObject,
875 compression_level: u32,
876 ) -> Result<ObjectId> {
877 if object.object_type != ObjectType::Blob {
878 return Err(GitError::InvalidObject(
879 "write_blob_as_pack requires a blob object".into(),
880 ));
881 }
882 if oid.format() != self.format {
883 return Err(GitError::InvalidObjectId(format!(
884 "object {oid} uses {}, store uses {}",
885 oid.format().name(),
886 self.format.name()
887 )));
888 }
889 if self.contains(&oid)? {
890 return Ok(oid);
891 }
892 let input = [PackInput { oid: &oid, object }];
893 let options = PackWriteOptions::new()
894 .with_window(0)
895 .with_depth(0)
896 .with_reorder(false)
897 .with_compression_level(compression_level);
898 let pack =
899 PackFile::write_packed_with_known_ids_and_options(&input, self.format, &options)?;
900 self.install_pack(&pack)?;
901 Ok(oid)
902 }
903
904 pub fn write_blobs_as_pack(
905 &self,
906 objects: &[(ObjectId, EncodedObject)],
907 compression_level: u32,
908 ) -> Result<()> {
909 let mut seen = HashSet::with_capacity(objects.len());
910 let mut inputs = Vec::new();
911 for (oid, object) in objects {
912 if object.object_type != ObjectType::Blob {
913 return Err(GitError::InvalidObject(
914 "write_blobs_as_pack requires blob objects".into(),
915 ));
916 }
917 if oid.format() != self.format {
918 return Err(GitError::InvalidObjectId(format!(
919 "object {oid} uses {}, store uses {}",
920 oid.format().name(),
921 self.format.name()
922 )));
923 }
924 if seen.insert(*oid) && !self.contains(oid)? {
925 inputs.push(PackInput { oid, object });
926 }
927 }
928 if inputs.is_empty() {
929 return Ok(());
930 }
931 let options = PackWriteOptions::new()
932 .with_window(0)
933 .with_depth(0)
934 .with_reorder(false)
935 .with_compression_level(compression_level);
936 let pack =
937 PackFile::write_packed_with_known_ids_and_options(&inputs, self.format, &options)?;
938 self.install_pack(&pack)?;
939 Ok(())
940 }
941
942 pub fn install_pack_with_options(
943 &self,
944 pack: &PackWrite,
945 options: RawPackInstallOptions,
946 ) -> Result<PackInstallResult> {
947 if pack.checksum.format() != self.format {
948 return Err(GitError::InvalidObjectId(format!(
949 "pack checksum uses {}, store uses {}",
950 pack.checksum.format().name(),
951 self.format.name()
952 )));
953 }
954 for entry in &pack.entries {
955 if entry.oid.format() != self.format {
956 return Err(GitError::InvalidObjectId(format!(
957 "pack entry {} uses {}, store uses {}",
958 entry.oid,
959 entry.oid.format().name(),
960 self.format.name()
961 )));
962 }
963 }
964 let canonical_index = PackIndex::write_v2_for_pack(&pack.pack, self.format)?;
965 let parsed_index = PackIndex::parse(&pack.index, self.format)?;
966 if canonical_index.pack_checksum != pack.checksum
967 || parsed_index.pack_checksum != pack.checksum
968 {
969 return Err(GitError::InvalidFormat(
970 "pack and index checksums do not match pack write".into(),
971 ));
972 }
973 if pack.index != canonical_index.index {
974 return Err(GitError::InvalidFormat(
975 "pack index does not match pack contents".into(),
976 ));
977 }
978
979 let pack_dir = self.objects_dir.join("pack");
980 fs::create_dir_all(&pack_dir)?;
981 let pack_name = format!("pack-{}", pack.checksum.to_hex());
982 let pack_path = pack_dir.join(format!("{pack_name}.pack"));
983 let index_path = pack_dir.join(format!("{pack_name}.idx"));
984 if !pack_path.exists() || !index_path.exists() {
985 write_pack_component(&pack_path, &pack.pack)?;
986 write_pack_component(&index_path, &pack.index)?;
987 }
988 let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
989 Ok(PackInstallResult {
990 pack_name,
991 pack_path,
992 index_path,
993 promisor_path,
994 object_ids: canonical_index
995 .entries
996 .iter()
997 .map(|entry| entry.oid)
998 .collect(),
999 })
1000 }
1001
1002 pub fn install_written_pack(&self, pack: &PackWrite) -> Result<PackInstallResult> {
1010 self.install_written_pack_with_options(pack, RawPackInstallOptions::default())
1011 }
1012
1013 pub fn install_written_pack_with_options(
1014 &self,
1015 pack: &PackWrite,
1016 options: RawPackInstallOptions,
1017 ) -> Result<PackInstallResult> {
1018 validate_pack_checksum(&pack.pack, self.format, &pack.checksum, "pack write")?;
1019 let parsed_index = PackIndex::parse(&pack.index, self.format)?;
1020 if parsed_index.pack_checksum != pack.checksum {
1021 return Err(GitError::InvalidFormat(
1022 "pack write index checksum does not match pack".into(),
1023 ));
1024 }
1025 if !pack_index_entries_match_writer(&parsed_index.entries, &pack.entries) {
1026 return Err(GitError::InvalidFormat(
1027 "pack write index does not match generated entries".into(),
1028 ));
1029 }
1030 self.install_generated_pack_unchecked(pack, options)
1031 }
1032
1033 fn install_generated_pack_unchecked(
1034 &self,
1035 pack: &PackWrite,
1036 options: RawPackInstallOptions,
1037 ) -> Result<PackInstallResult> {
1038 let pack_dir = self.objects_dir.join("pack");
1039 fs::create_dir_all(&pack_dir)?;
1040 let pack_name = format!("pack-{}", pack.checksum.to_hex());
1041 let pack_path = pack_dir.join(format!("{pack_name}.pack"));
1042 let index_path = pack_dir.join(format!("{pack_name}.idx"));
1043 if !pack_path.exists() || !index_path.exists() {
1044 write_pack_component(&pack_path, &pack.pack)?;
1045 write_pack_component(&index_path, &pack.index)?;
1046 }
1047 let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
1048 Ok(PackInstallResult {
1049 pack_name,
1050 pack_path,
1051 index_path,
1052 promisor_path,
1053 object_ids: pack.entries.iter().map(|entry| entry.oid).collect(),
1054 })
1055 }
1056
1057 pub(crate) fn install_pack_file_from_temp(
1058 &self,
1059 temp_pack_path: &Path,
1060 pack_checksum: ObjectId,
1061 index: &[u8],
1062 object_ids: Vec<ObjectId>,
1063 options: RawPackInstallOptions,
1064 ) -> Result<PackInstallResult> {
1065 let pack_dir = self.objects_dir.join("pack");
1066 fs::create_dir_all(&pack_dir)?;
1067 let pack_name = format!("pack-{}", pack_checksum.to_hex());
1068 let pack_path = pack_dir.join(format!("{pack_name}.pack"));
1069 let index_path = pack_dir.join(format!("{pack_name}.idx"));
1070 match fs::rename(temp_pack_path, &pack_path) {
1071 Ok(()) => {}
1072 Err(_) if pack_path.exists() => {
1073 let _ = fs::remove_file(temp_pack_path);
1074 }
1075 Err(err) => return Err(GitError::Io(err.to_string())),
1076 }
1077 write_pack_component(&index_path, index)?;
1078 let promisor_path = write_promisor_pack_sidecar(&pack_dir, &pack_name, options.promisor)?;
1079 Ok(PackInstallResult {
1080 pack_name,
1081 pack_path,
1082 index_path,
1083 promisor_path,
1084 object_ids,
1085 })
1086 }
1087
1088 pub fn install_raw_pack_from_reader<R>(&self, reader: &mut R) -> Result<PackInstallResult>
1089 where
1090 R: Read,
1091 {
1092 self.install_raw_pack_from_reader_with_options(reader, RawPackInstallOptions::default())
1093 }
1094
1095 pub fn install_raw_pack_from_reader_with_external_bases<R>(
1100 &self,
1101 reader: &mut R,
1102 ) -> Result<PackInstallResult>
1103 where
1104 R: Read,
1105 {
1106 let mut pack = Vec::new();
1107 reader.read_to_end(&mut pack)?;
1108 let fixed = fix_thin_pack(&pack, self.format, |oid| match self.read_object(oid) {
1109 Ok(object) => Ok(Some((*object).clone())),
1110 Err(GitError::NotFound(_)) => Ok(None),
1111 Err(err) => Err(err),
1112 })?;
1113 let pack = fixed.pack;
1114 let built = fixed.index;
1115 let pack_dir = self.objects_dir.join("pack");
1116 fs::create_dir_all(&pack_dir)?;
1117 let temp_pack_path = unique_temp_path(&pack_dir).with_extension("pack");
1118 fs::write(&temp_pack_path, &pack)?;
1119 let result = self.install_pack_file_from_temp(
1120 &temp_pack_path,
1121 built.pack_checksum,
1122 &built.index,
1123 built.entries.iter().map(|entry| entry.oid).collect(),
1124 RawPackInstallOptions::default(),
1125 );
1126 if result.is_err() {
1127 let _ = fs::remove_file(&temp_pack_path);
1128 }
1129 result
1130 }
1131
1132 pub fn begin_raw_pack_install(
1133 &self,
1134 expected_pack_id: ObjectId,
1135 expected_pack_size: u64,
1136 ) -> Result<RawPackStreamingInstall> {
1137 self.begin_raw_pack_install_with_options(
1138 expected_pack_id,
1139 expected_pack_size,
1140 RawPackInstallOptions::default(),
1141 )
1142 }
1143
1144 pub fn begin_raw_pack_install_with_options(
1145 &self,
1146 expected_pack_id: ObjectId,
1147 expected_pack_size: u64,
1148 options: RawPackInstallOptions,
1149 ) -> Result<RawPackStreamingInstall> {
1150 if expected_pack_id.format() != self.format {
1151 return Err(GitError::InvalidObjectId(format!(
1152 "pack checksum uses {}, store uses {}",
1153 expected_pack_id.format().name(),
1154 self.format.name()
1155 )));
1156 }
1157 let pack_dir = self.objects_dir.join("pack");
1158 fs::create_dir_all(&pack_dir)?;
1159 let pack_name = format!("pack-{}", expected_pack_id.to_hex());
1160 let pack_path = pack_dir.join(format!("{pack_name}.pack"));
1161 let index_path = pack_dir.join(format!("{pack_name}.idx"));
1162 let temp_pack_path = unique_temp_path(&pack_dir).with_extension("pack");
1163 let file = fs::OpenOptions::new()
1164 .write(true)
1165 .create_new(true)
1166 .open(&temp_pack_path)?;
1167 Ok(RawPackStreamingInstall {
1168 format: self.format,
1169 expected_pack_id,
1170 expected_pack_size,
1171 options,
1172 pack_dir,
1173 pack_name,
1174 pack_path,
1175 index_path,
1176 temp_pack_path,
1177 file: Some(file),
1178 written: 0,
1179 finished: false,
1180 })
1181 }
1182
1183 pub fn install_raw_pack_from_reader_with_options<R>(
1184 &self,
1185 reader: &mut R,
1186 options: RawPackInstallOptions,
1187 ) -> Result<PackInstallResult>
1188 where
1189 R: Read,
1190 {
1191 self.install_raw_pack_from_reader_with_progress(reader, options, |_| {})
1192 }
1193
1194 pub fn install_raw_pack_from_reader_with_progress<R, F>(
1203 &self,
1204 reader: &mut R,
1205 options: RawPackInstallOptions,
1206 progress: F,
1207 ) -> Result<PackInstallResult>
1208 where
1209 R: Read,
1210 F: FnMut(PackInstallProgress),
1211 {
1212 self.install_raw_pack_from_reader_with_progress_and_cancel(
1213 reader,
1214 options,
1215 CancelFlag::never(),
1216 progress,
1217 )
1218 }
1219
1220 pub fn install_raw_pack_from_reader_with_progress_and_cancel<R, F>(
1227 &self,
1228 reader: &mut R,
1229 options: RawPackInstallOptions,
1230 cancel: CancelFlag<'_>,
1231 progress: F,
1232 ) -> Result<PackInstallResult>
1233 where
1234 R: Read,
1235 F: FnMut(PackInstallProgress),
1236 {
1237 cancel.check()?;
1239 let pack_dir = self.objects_dir.join("pack");
1240 fs::create_dir_all(&pack_dir)?;
1241 let temp_pack_path = unique_temp_path(&pack_dir).with_extension("pack");
1242 let result = (|| -> Result<PackInstallResult> {
1243 let file = fs::OpenOptions::new()
1246 .write(true)
1247 .create_new(true)
1248 .open(&temp_pack_path)?;
1249 let mut progress = progress;
1250 let receive =
1251 receive_pack_to_file(reader, file, options.max_input_size, cancel, &mut progress)
1252 .map_err(map_install_cancel_error)?;
1253 let built = {
1254 let mapped = sley_mmap::MappedFile::open_pack(&temp_pack_path)?;
1255 PackIndex::write_v2_for_pack_with_options(
1256 mapped.as_bytes(),
1257 self.format,
1258 |_| Ok(None),
1259 sley_pack::PackIndexOptions::default(),
1260 cancel,
1261 |indexed: PackIndexProgress| {
1262 progress(PackInstallProgress {
1263 received_bytes: receive.bytes,
1264 indexed_objects: indexed.completed_objects,
1265 total_objects: indexed.total_objects,
1266 });
1267 },
1268 )?
1269 };
1270
1271 self.install_pack_file_from_temp(
1272 &temp_pack_path,
1273 built.pack_checksum,
1274 &built.index,
1275 built.entries.iter().map(|entry| entry.oid).collect(),
1276 options,
1277 )
1278 })();
1279 if result.is_err() {
1280 let _ = fs::remove_file(&temp_pack_path);
1281 }
1282 result
1283 }
1284}
1285
1286pub(crate) fn write_pack_component(path: &Path, bytes: &[u8]) -> Result<()> {
1287 if path.exists() {
1288 return Ok(());
1289 }
1290 let parent = path
1291 .parent()
1292 .ok_or_else(|| GitError::InvalidPath("pack component path has no parent".into()))?;
1293 fs::create_dir_all(parent)?;
1294 let temp_path = unique_temp_path(parent);
1295 let write_result = (|| -> Result<()> {
1296 {
1297 let mut file = fs::OpenOptions::new()
1298 .write(true)
1299 .create_new(true)
1300 .open(&temp_path)?;
1301 #[cfg(feature = "fetch-profile")]
1302 let _profile_span = sley_core::fetch_profile::Span::enter(
1303 sley_core::fetch_profile::Stage::ObjectStoreWrite,
1304 );
1305 file.write_all(bytes)?;
1306 file.sync_all()?;
1307 #[cfg(feature = "fetch-profile")]
1308 {
1309 sley_core::fetch_profile::add_count(
1310 sley_core::fetch_profile::Stage::ObjectStoreWrite,
1311 1,
1312 );
1313 sley_core::fetch_profile::add_bytes(
1314 sley_core::fetch_profile::Stage::ObjectStoreWrite,
1315 bytes.len() as u64,
1316 );
1317 sley_core::fetch_profile::add_fsync();
1318 }
1319 }
1320 match fs::rename(&temp_path, path) {
1321 Ok(()) => Ok(()),
1322 Err(_) if path.exists() => {
1323 let _ = fs::remove_file(&temp_path);
1324 Ok(())
1325 }
1326 Err(err) => Err(GitError::Io(err.to_string())),
1327 }
1328 })();
1329 if write_result.is_err() {
1330 let _ = fs::remove_file(&temp_path);
1331 }
1332 write_result
1333}
1334
1335pub(crate) fn replace_pack_component(path: &Path, bytes: &[u8]) -> Result<()> {
1340 let parent = path
1341 .parent()
1342 .ok_or_else(|| GitError::InvalidPath("pack component path has no parent".into()))?;
1343 fs::create_dir_all(parent)?;
1344 let temp_path = unique_temp_path(parent);
1345 let write_result = (|| -> Result<()> {
1346 {
1347 let mut file = fs::OpenOptions::new()
1348 .write(true)
1349 .create_new(true)
1350 .open(&temp_path)?;
1351 file.write_all(bytes)?;
1352 file.sync_all()?;
1353 }
1354 match fs::rename(&temp_path, path) {
1355 Ok(()) => Ok(()),
1356 Err(_) if path.exists() => {
1357 fs::remove_file(path)?;
1358 fs::rename(&temp_path, path)?;
1359 Ok(())
1360 }
1361 Err(err) => Err(GitError::Io(err.to_string())),
1362 }
1363 })();
1364 if write_result.is_err() {
1365 let _ = fs::remove_file(&temp_path);
1366 }
1367 write_result
1368}
1369
1370pub(crate) fn write_promisor_pack_sidecar(
1371 pack_dir: &Path,
1372 pack_name: &str,
1373 promisor: bool,
1374) -> Result<Option<PathBuf>> {
1375 if !promisor {
1376 return Ok(None);
1377 }
1378 let path = pack_dir.join(format!("{pack_name}.promisor"));
1379 write_pack_component(&path, b"")?;
1380 Ok(Some(path))
1381}