1use super::*;
6
7pub const DEFAULT_PACK_WINDOW: usize = 10;
13
14pub const DEFAULT_PACK_DEPTH: usize = 50;
20
21pub(crate) const PACK_PARALLEL_COMPRESSION_MIN_OBJECTS: usize = 64;
25
26pub(crate) const PACK_PARALLEL_COMPRESSION_MAX_THREADS: usize = 4;
30
31const PACK_OBJECT_WINDOW_OVERHEAD: u64 = 64;
34
35#[derive(Debug, Clone)]
41pub struct PackWriteOptions {
42 pub window: usize,
45 pub depth: usize,
47 pub prefer_ofs_delta: bool,
51 pub thin_bases: HashMap<ObjectId, EncodedObject>,
56 pub preferred_thin_bases: HashMap<ObjectId, ObjectId>,
61 pub reorder: bool,
67 pub compression_level: u32,
69}
70
71impl Default for PackWriteOptions {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl PackWriteOptions {
78 pub fn new() -> Self {
82 Self {
83 window: DEFAULT_PACK_WINDOW,
84 depth: DEFAULT_PACK_DEPTH,
85 prefer_ofs_delta: true,
86 thin_bases: HashMap::new(),
87 preferred_thin_bases: HashMap::new(),
88 reorder: true,
89 compression_level: 6,
90 }
91 }
92
93 pub fn with_window(mut self, window: usize) -> Self {
95 self.window = window;
96 self
97 }
98
99 pub fn with_depth(mut self, depth: usize) -> Self {
101 self.depth = depth;
102 self
103 }
104
105 pub fn with_prefer_ofs_delta(mut self, prefer_ofs_delta: bool) -> Self {
108 self.prefer_ofs_delta = prefer_ofs_delta;
109 self
110 }
111
112 pub fn with_thin_bases(mut self, thin_bases: HashMap<ObjectId, EncodedObject>) -> Self {
114 self.thin_bases = thin_bases;
115 self
116 }
117
118 pub fn with_preferred_thin_bases(
120 mut self,
121 preferred_thin_bases: HashMap<ObjectId, ObjectId>,
122 ) -> Self {
123 self.preferred_thin_bases = preferred_thin_bases;
124 self
125 }
126
127 pub fn with_reorder(mut self, reorder: bool) -> Self {
130 self.reorder = reorder;
131 self
132 }
133
134 pub fn with_compression_level(mut self, level: u32) -> Self {
136 self.compression_level = level.min(9);
137 self
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct PackWriteLimits {
158 pub compression_working_set: ByteBudget,
161 pub decoded_object: ByteBudget,
164 pub delta_base: ByteBudget,
167}
168
169impl Default for PackWriteLimits {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl PackWriteLimits {
176 pub fn new() -> Self {
179 Self {
180 compression_working_set: ByteBudget::new(32 * 1024 * 1024),
181 decoded_object: ByteBudget::new(512 * 1024 * 1024),
182 delta_base: ByteBudget::new(32 * 1024 * 1024),
183 }
184 }
185
186 pub fn with_compression_working_set(mut self, budget: ByteBudget) -> Self {
187 self.compression_working_set = budget;
188 self
189 }
190
191 pub fn with_decoded_object(mut self, budget: ByteBudget) -> Self {
192 self.decoded_object = budget;
193 self
194 }
195
196 pub fn with_delta_base(mut self, budget: ByteBudget) -> Self {
197 self.delta_base = budget;
198 self
199 }
200}
201
202fn pack_object_window_cost(object: &EncodedObject) -> u64 {
203 (object.body.len() as u64).saturating_add(PACK_OBJECT_WINDOW_OVERHEAD)
204}
205
206fn pack_delta_base_cost(object: &EncodedObject) -> u64 {
207 pack_object_window_cost(object)
208}
209
210fn duplicate_pack_object_id(oid: ObjectId) -> GitError {
211 GitError::InvalidFormat(format!("pack contains duplicate object id {oid}"))
212}
213
214fn next_compression_window_end(
215 objects: &[&EncodedObject],
216 order: &[usize],
217 start: usize,
218 budget: ByteBudget,
219) -> usize {
220 if start >= order.len() {
221 return start;
222 }
223 let mut end = start + 1;
224 let mut used = pack_object_window_cost(objects[order[start]]);
225 if used > budget.as_u64() {
226 return end;
227 }
228 while end < order.len() {
229 let additional = pack_object_window_cost(objects[order[end]]);
230 if !budget.allows(used, additional) {
231 break;
232 }
233 used = used.saturating_add(additional);
234 end += 1;
235 }
236 end
237}
238
239struct PendingSourceObject {
240 oid: ObjectId,
241 object: Arc<EncodedObject>,
242}
243
244fn leftover_window_cost(leftover: Option<&PendingSourceObject>) -> u64 {
245 leftover
246 .map(|pending| pack_object_window_cost(&pending.object))
247 .unwrap_or(0)
248}
249
250struct SourcePackStream<I, F> {
251 selected: I,
252 leftover: Option<PendingSourceObject>,
253 seen: HashSet<ObjectId>,
254 yielded: u64,
255 object_count: u32,
256 format: ObjectFormat,
257 limits: PackWriteLimits,
258 read_object: F,
259 index_entries: Vec<PackIndexEntry>,
260 delta_count: u32,
261 peak_working_set_bytes: u64,
262}
263
264impl<I, F> SourcePackStream<I, F>
265where
266 I: Iterator<Item = ObjectId>,
267 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
268{
269 fn new(
270 selected: I,
271 object_count: u32,
272 format: ObjectFormat,
273 limits: PackWriteLimits,
274 read_object: F,
275 ) -> Self {
276 Self {
277 selected,
278 leftover: None,
279 seen: HashSet::new(),
280 yielded: 0,
281 object_count,
282 format,
283 limits,
284 read_object,
285 index_entries: Vec::new(),
286 delta_count: 0,
287 peak_working_set_bytes: 0,
288 }
289 }
290
291 fn pull(&mut self) -> Result<Option<PendingSourceObject>> {
292 if let Some(pending) = self.leftover.take() {
293 return Ok(Some(pending));
294 }
295 let Some(oid) = self.selected.next() else {
296 return Ok(None);
297 };
298 if self.yielded >= u64::from(self.object_count) {
299 return Err(GitError::count_mismatch(
300 u64::from(self.object_count),
301 self.yielded.saturating_add(1),
302 ));
303 }
304 self.yielded = self.yielded.saturating_add(1);
305 if oid.format() != self.format {
306 return Err(GitError::InvalidObjectId(
307 "pack object id format does not match pack format".into(),
308 ));
309 }
310 if !self.seen.insert(oid) {
311 return Err(duplicate_pack_object_id(oid));
312 }
313 let object = (self.read_object)(&oid)?;
314 let attempted = object.body.len() as u64;
315 let limit = self.limits.decoded_object.as_u64();
316 if attempted > limit {
317 return Err(GitError::resource_limit(
318 ResourceLimitKind::DecodedObject,
319 limit,
320 attempted,
321 ));
322 }
323 Ok(Some(PendingSourceObject { oid, object }))
324 }
325
326 fn fill_window(&mut self, cancel: CancelFlag<'_>) -> Result<(Vec<PendingSourceObject>, u64)> {
327 let mut window = Vec::new();
328 let mut used = 0u64;
329 loop {
330 cancel.check()?;
331 let Some(pending) = self.pull()? else {
332 break;
333 };
334 let cost = pack_object_window_cost(&pending.object);
335 if window.is_empty() {
336 window.push(pending);
337 used = cost;
338 if cost > self.limits.compression_working_set.as_u64() {
339 break;
340 }
341 continue;
342 }
343 if !self.limits.compression_working_set.allows(used, cost) {
344 self.leftover = Some(pending);
345 break;
346 }
347 used = used.saturating_add(cost);
348 window.push(pending);
349 }
350 Ok((window, used))
351 }
352
353 fn note_peak(&mut self, window_bytes: u64, horizon_bytes: u64) {
354 let now = window_bytes
355 .saturating_add(horizon_bytes)
356 .saturating_add(leftover_window_cost(self.leftover.as_ref()));
357 if now > self.peak_working_set_bytes {
358 self.peak_working_set_bytes = now;
359 }
360 }
361
362 fn finish(self, output: PackDigestWriter<'_, impl Write>) -> Result<PackWriteSummary> {
363 if self.leftover.is_some() || self.yielded != u64::from(self.object_count) {
364 return Err(GitError::count_mismatch(
365 u64::from(self.object_count),
366 self.yielded,
367 ));
368 }
369 let (checksum, pack_size) = output.finish()?;
370 let index = PackIndex::write_v2(self.format, &self.index_entries, &checksum)?;
371 Ok(PackWriteSummary {
372 index,
373 checksum,
374 entries: self.index_entries,
375 delta_count: self.delta_count,
376 pack_size,
377 peak_working_set_bytes: self.peak_working_set_bytes,
378 })
379 }
380}
381
382fn retain_streaming_delta_base(
383 horizon: &mut VecDeque<StreamingDeltaBase>,
384 horizon_bytes: &mut u64,
385 base: StreamingDeltaBase,
386 options: &PackWriteOptions,
387 limits: PackWriteLimits,
388) {
389 if options.depth == 0 || options.window == 0 {
390 return;
391 }
392 let cost = pack_delta_base_cost(&base.object);
393 horizon.push_back(base);
394 *horizon_bytes = horizon_bytes.saturating_add(cost);
395 while horizon.len() > options.window {
396 if let Some(evicted) = horizon.pop_front() {
397 *horizon_bytes = horizon_bytes.saturating_sub(pack_delta_base_cost(&evicted.object));
398 }
399 }
400 while horizon.len() > 1 && *horizon_bytes > limits.delta_base.as_u64() {
401 if let Some(evicted) = horizon.pop_front() {
402 *horizon_bytes = horizon_bytes.saturating_sub(pack_delta_base_cost(&evicted.object));
403 }
404 }
405}
406
407fn validate_thin_base_formats(options: &PackWriteOptions, format: ObjectFormat) -> Result<()> {
408 for oid in options.thin_bases.keys() {
409 if oid.format() != format {
410 return Err(GitError::InvalidObjectId(
411 "thin pack base object id format does not match pack format".into(),
412 ));
413 }
414 }
415 Ok(())
416}
417
418impl PackFile {
419 pub fn write_undeltified_sha1<T>(objects: &[T]) -> Result<PackWrite>
420 where
421 T: Borrow<EncodedObject>,
422 {
423 Self::write_undeltified(objects, ObjectFormat::Sha1)
424 }
425
426 pub fn write_undeltified<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
432 where
433 T: Borrow<EncodedObject>,
434 {
435 let options = PackWriteOptions::new().with_depth(0).with_reorder(false);
436 Self::write_packed_impl(objects, format, &options)
437 }
438
439 pub fn write_packed<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
448 where
449 T: Borrow<EncodedObject>,
450 {
451 Self::write_packed_with_options(objects, format, &PackWriteOptions::new())
452 }
453
454 pub fn write_packed_with_options<T>(
458 objects: &[T],
459 format: ObjectFormat,
460 options: &PackWriteOptions,
461 ) -> Result<PackWrite>
462 where
463 T: Borrow<EncodedObject>,
464 {
465 Self::write_packed_impl(objects, format, options)
466 }
467
468 pub fn write_packed_with_known_ids(
477 inputs: &[PackInput<'_>],
478 format: ObjectFormat,
479 ) -> Result<PackWrite> {
480 Self::write_packed_with_known_ids_and_options(inputs, format, &PackWriteOptions::new())
481 }
482
483 pub fn write_packed_with_known_ids_and_options(
486 inputs: &[PackInput<'_>],
487 format: ObjectFormat,
488 options: &PackWriteOptions,
489 ) -> Result<PackWrite> {
490 Self::write_packed_with_known_ids_and_options_and_hints(
491 inputs,
492 format,
493 options,
494 PackPlanningHints::new(),
495 )
496 }
497
498 pub fn write_packed_with_known_ids_and_options_and_name_hashes(
500 inputs: &[PackInput<'_>],
501 format: ObjectFormat,
502 options: &PackWriteOptions,
503 name_hashes: Option<&HashMap<ObjectId, u32>>,
504 ) -> Result<PackWrite> {
505 let hints = name_hashes.map_or_else(PackPlanningHints::new, |hashes| {
506 PackPlanningHints::new().with_name_hashes(hashes)
507 });
508 Self::write_packed_with_known_ids_and_options_and_hints(inputs, format, options, hints)
509 }
510
511 pub fn write_packed_with_known_ids_and_options_and_hints(
515 inputs: &[PackInput<'_>],
516 format: ObjectFormat,
517 options: &PackWriteOptions,
518 hints: PackPlanningHints<'_>,
519 ) -> Result<PackWrite> {
520 if inputs.len() > u32::MAX as usize {
521 return Err(GitError::InvalidFormat("too many pack objects".into()));
522 }
523 let mut objects = Vec::with_capacity(inputs.len());
524 let mut object_ids = Vec::with_capacity(inputs.len());
525 for input in inputs {
526 if input.oid.format() != format {
527 return Err(GitError::InvalidObjectId(format!(
528 "pack object id {} uses {}, pack uses {}",
529 input.oid,
530 input.oid.format().name(),
531 format.name()
532 )));
533 }
534 objects.push(input.object);
535 object_ids.push(*input.oid);
536 }
537 Self::write_packed_from_parts(objects, object_ids, format, options, hints)
538 }
539
540 pub fn write_packed_with_known_ids_to_writer<W>(
541 inputs: &[PackInput<'_>],
542 format: ObjectFormat,
543 options: &PackWriteOptions,
544 writer: &mut W,
545 ) -> Result<PackWriteSummary>
546 where
547 W: Write,
548 {
549 Self::write_packed_with_known_ids_and_options_and_hints_to_writer(
550 inputs,
551 format,
552 options,
553 PackPlanningHints::new(),
554 writer,
555 )
556 }
557
558 pub fn write_packed_with_known_ids_and_options_and_hints_to_writer<W>(
560 inputs: &[PackInput<'_>],
561 format: ObjectFormat,
562 options: &PackWriteOptions,
563 hints: PackPlanningHints<'_>,
564 writer: &mut W,
565 ) -> Result<PackWriteSummary>
566 where
567 W: Write,
568 {
569 if inputs.len() > u32::MAX as usize {
570 return Err(GitError::InvalidFormat("too many pack objects".into()));
571 }
572 let mut objects = Vec::with_capacity(inputs.len());
573 let mut object_ids = Vec::with_capacity(inputs.len());
574 for input in inputs {
575 if input.oid.format() != format {
576 return Err(GitError::InvalidObjectId(format!(
577 "pack object id {} uses {}, pack uses {}",
578 input.oid,
579 input.oid.format().name(),
580 format.name()
581 )));
582 }
583 objects.push(input.object);
584 object_ids.push(*input.oid);
585 }
586 Self::write_packed_from_parts_to_writer(objects, object_ids, format, options, hints, writer)
587 }
588
589 pub fn write_thin<T>(
598 objects: &[T],
599 format: ObjectFormat,
600 external_bases: HashMap<ObjectId, EncodedObject>,
601 ) -> Result<PackWrite>
602 where
603 T: Borrow<EncodedObject>,
604 {
605 let options = PackWriteOptions::new().with_thin_bases(external_bases);
606 Self::write_packed_impl(objects, format, &options)
607 }
608
609 pub(crate) fn write_packed_impl<T>(
610 objects: &[T],
611 format: ObjectFormat,
612 options: &PackWriteOptions,
613 ) -> Result<PackWrite>
614 where
615 T: Borrow<EncodedObject>,
616 {
617 if objects.len() > u32::MAX as usize {
618 return Err(GitError::InvalidFormat("too many pack objects".into()));
619 }
620 let objects: Vec<&EncodedObject> = objects.iter().map(Borrow::borrow).collect();
621
622 let mut object_ids: Vec<ObjectId> = Vec::with_capacity(objects.len());
625 for object in &objects {
626 object_ids.push(object.object_id(format)?);
627 }
628 Self::write_packed_from_parts(
629 objects,
630 object_ids,
631 format,
632 options,
633 PackPlanningHints::new(),
634 )
635 }
636
637 pub(crate) fn write_packed_from_parts(
638 objects: Vec<&EncodedObject>,
639 object_ids: Vec<ObjectId>,
640 format: ObjectFormat,
641 options: &PackWriteOptions,
642 hints: PackPlanningHints<'_>,
643 ) -> Result<PackWrite> {
644 let mut seen = HashSet::with_capacity(object_ids.len());
645 for oid in &object_ids {
646 if !seen.insert(oid) {
647 return Err(duplicate_pack_object_id(*oid));
648 }
649 }
650
651 for oid in options.thin_bases.keys() {
653 if oid.format() != format {
654 return Err(GitError::InvalidObjectId(
655 "thin pack base object id format does not match pack format".into(),
656 ));
657 }
658 }
659
660 let (plan, order) = plan_pack_deltas(&objects, &object_ids, options, hints)?;
666
667 let mut pack = Vec::new();
668 pack.extend_from_slice(b"PACK");
669 pack.extend_from_slice(&2u32.to_be_bytes());
670 pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
671
672 let mut index_entries = Vec::with_capacity(objects.len());
673 let mut delta_count = 0u32;
674 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
677
678 let compressed_payloads =
679 compress_planned_payloads(&objects, &plan, &order, options.compression_level)?;
680
681 for (order_pos, &idx) in order.iter().enumerate() {
682 let offset = pack.len() as u64;
683 let mut entry_bytes = Vec::new();
684 match &plan[idx].base {
685 PlannedBase::None => {
686 write_entry_header(
687 &mut entry_bytes,
688 objects[idx].object_type,
689 objects[idx].body.len() as u64,
690 );
691 }
692 PlannedBase::InPack { base_idx, delta } => {
693 delta_count += 1;
694 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
695 GitError::InvalidFormat(
696 "in-pack delta base emitted after dependent object".into(),
697 )
698 })?;
699 if options.prefer_ofs_delta {
700 write_pack_entry_header_kind(&mut entry_bytes, 6, delta.len() as u64);
701 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
702 GitError::InvalidFormat("ofs-delta base offset is after delta".into())
703 })?;
704 write_ofs_delta_offset(&mut entry_bytes, relative)?;
705 } else {
706 write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
707 entry_bytes.extend_from_slice(object_ids[*base_idx].as_bytes());
708 }
709 }
710 PlannedBase::External { base_oid, delta } => {
711 delta_count += 1;
712 write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
713 entry_bytes.extend_from_slice(base_oid.as_bytes());
714 }
715 }
716 entry_bytes.extend_from_slice(&compressed_payloads[order_pos]);
717 let crc32 = crc32fast::hash(&entry_bytes);
718 pack.extend_from_slice(&entry_bytes);
719 written_offsets[idx] = Some(offset);
720 index_entries.push(PackIndexEntry {
721 oid: object_ids[idx].clone(),
722 crc32,
723 offset,
724 });
725 }
726
727 let checksum = sley_core::digest_bytes(format, &pack)?;
728 pack.extend_from_slice(checksum.as_bytes());
729 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
730 Ok(PackWrite {
731 pack,
732 index,
733 checksum,
734 entries: index_entries,
735 delta_count,
736 })
737 }
738
739 pub(crate) fn write_packed_from_parts_to_writer<W>(
740 objects: Vec<&EncodedObject>,
741 object_ids: Vec<ObjectId>,
742 format: ObjectFormat,
743 options: &PackWriteOptions,
744 hints: PackPlanningHints<'_>,
745 writer: &mut W,
746 ) -> Result<PackWriteSummary>
747 where
748 W: Write,
749 {
750 let mut seen = HashSet::with_capacity(object_ids.len());
751 for oid in &object_ids {
752 if !seen.insert(oid) {
753 return Err(duplicate_pack_object_id(*oid));
754 }
755 }
756
757 for oid in options.thin_bases.keys() {
758 if oid.format() != format {
759 return Err(GitError::InvalidObjectId(
760 "thin pack base object id format does not match pack format".into(),
761 ));
762 }
763 }
764
765 let (plan, order) = plan_pack_deltas(&objects, &object_ids, options, hints)?;
766 let mut output = PackDigestWriter::new(writer, format);
767 output.write_pack_bytes(b"PACK")?;
768 output.write_pack_bytes(&2u32.to_be_bytes())?;
769 output.write_pack_bytes(&(objects.len() as u32).to_be_bytes())?;
770
771 let mut index_entries = Vec::with_capacity(objects.len());
772 let mut delta_count = 0u32;
773 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
774
775 let compression_budget = PackWriteLimits::new().compression_working_set;
776 let mut window_start = 0;
777 while window_start < order.len() {
778 let window_end =
779 next_compression_window_end(&objects, &order, window_start, compression_budget);
780 let order_window = &order[window_start..window_end];
781 let compressed_payloads = compress_planned_payloads(
782 &objects,
783 &plan,
784 order_window,
785 options.compression_level,
786 )?;
787 for (&idx, compressed_payload) in order_window.iter().zip(&compressed_payloads) {
788 let offset = output.position();
789 let mut entry_header = Vec::new();
790 match &plan[idx].base {
791 PlannedBase::None => {
792 write_entry_header(
793 &mut entry_header,
794 objects[idx].object_type,
795 objects[idx].body.len() as u64,
796 );
797 }
798 PlannedBase::InPack { base_idx, delta } => {
799 delta_count += 1;
800 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
801 GitError::InvalidFormat(
802 "in-pack delta base emitted after dependent object".into(),
803 )
804 })?;
805 if options.prefer_ofs_delta {
806 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
807 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
808 GitError::InvalidFormat(
809 "ofs-delta base offset is after delta".into(),
810 )
811 })?;
812 write_ofs_delta_offset(&mut entry_header, relative)?;
813 } else {
814 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
815 entry_header.extend_from_slice(object_ids[*base_idx].as_bytes());
816 }
817 }
818 PlannedBase::External { base_oid, delta } => {
819 delta_count += 1;
820 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
821 entry_header.extend_from_slice(base_oid.as_bytes());
822 }
823 }
824 let mut crc32 = crc32fast::Hasher::new();
825 crc32.update(&entry_header);
826 crc32.update(compressed_payload);
827 output.write_pack_bytes(&entry_header)?;
828 output.write_pack_bytes(compressed_payload)?;
829 written_offsets[idx] = Some(offset);
830 index_entries.push(PackIndexEntry {
831 oid: object_ids[idx],
832 crc32: crc32.finalize(),
833 offset,
834 });
835 }
836 window_start = window_end;
837 }
838
839 let (checksum, pack_size) = output.finish()?;
840 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
841 Ok(PackWriteSummary {
842 index,
843 checksum,
844 entries: index_entries,
845 delta_count,
846 pack_size,
847 peak_working_set_bytes: 0,
848 })
849 }
850
851 pub fn write_undeltified_from_source_to_writer<W, I, F>(
852 selected_objects: I,
853 object_count: u32,
854 format: ObjectFormat,
855 options: &PackWriteOptions,
856 limits: PackWriteLimits,
857 read_object: F,
858 writer: &mut W,
859 ) -> Result<PackWriteSummary>
860 where
861 W: Write,
862 I: IntoIterator<Item = ObjectId>,
863 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
864 {
865 Self::write_undeltified_from_source_to_writer_with_cancel(
866 selected_objects,
867 object_count,
868 format,
869 options,
870 limits,
871 read_object,
872 writer,
873 CancelFlag::never(),
874 )
875 }
876
877 #[allow(clippy::too_many_arguments)]
885 pub fn write_undeltified_from_source_to_writer_with_cancel<W, I, F>(
886 selected_objects: I,
887 object_count: u32,
888 format: ObjectFormat,
889 options: &PackWriteOptions,
890 limits: PackWriteLimits,
891 read_object: F,
892 writer: &mut W,
893 cancel: CancelFlag<'_>,
894 ) -> Result<PackWriteSummary>
895 where
896 W: Write,
897 I: IntoIterator<Item = ObjectId>,
898 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
899 {
900 cancel.check()?;
901 let mut stream = SourcePackStream::new(
902 selected_objects.into_iter(),
903 object_count,
904 format,
905 limits,
906 read_object,
907 );
908 let mut output = PackDigestWriter::new(writer, format);
909 output.write_pack_bytes(b"PACK")?;
910 output.write_pack_bytes(&2u32.to_be_bytes())?;
911 output.write_pack_bytes(&object_count.to_be_bytes())?;
912
913 loop {
914 cancel.check()?;
915 let (window, window_bytes) = stream.fill_window(cancel)?;
916 if window.is_empty() {
917 break;
918 }
919 stream.note_peak(window_bytes, 0);
920 let objects = window
921 .iter()
922 .map(|entry| Arc::clone(&entry.object))
923 .collect::<Vec<_>>();
924 let compressed_payloads =
925 compress_undeltified_payloads(&objects, options.compression_level)?;
926 for (entry, compressed_payload) in window.iter().zip(&compressed_payloads) {
927 let offset = output.position();
928 let mut entry_header = Vec::new();
929 write_entry_header(
930 &mut entry_header,
931 entry.object.object_type,
932 entry.object.body.len() as u64,
933 );
934 let mut crc32 = crc32fast::Hasher::new();
935 crc32.update(&entry_header);
936 crc32.update(compressed_payload);
937 output.write_pack_bytes(&entry_header)?;
938 output.write_pack_bytes(compressed_payload)?;
939 stream.index_entries.push(PackIndexEntry {
940 oid: entry.oid,
941 crc32: crc32.finalize(),
942 offset,
943 });
944 }
945 }
946
947 stream.finish(output)
948 }
949
950 pub fn write_packed_from_source_to_writer<W, I, F>(
951 selected_objects: I,
952 object_count: u32,
953 format: ObjectFormat,
954 options: &PackWriteOptions,
955 limits: PackWriteLimits,
956 read_object: F,
957 writer: &mut W,
958 ) -> Result<PackWriteSummary>
959 where
960 W: Write,
961 I: IntoIterator<Item = ObjectId>,
962 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
963 {
964 Self::write_packed_from_source_to_writer_with_hints_and_cancel(
965 selected_objects,
966 object_count,
967 format,
968 options,
969 PackPlanningHints::new(),
970 limits,
971 read_object,
972 writer,
973 CancelFlag::never(),
974 )
975 }
976
977 #[allow(clippy::too_many_arguments)]
979 pub fn write_packed_from_source_to_writer_with_hints<W, I, F>(
980 selected_objects: I,
981 object_count: u32,
982 format: ObjectFormat,
983 options: &PackWriteOptions,
984 hints: PackPlanningHints<'_>,
985 limits: PackWriteLimits,
986 read_object: F,
987 writer: &mut W,
988 ) -> Result<PackWriteSummary>
989 where
990 W: Write,
991 I: IntoIterator<Item = ObjectId>,
992 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
993 {
994 Self::write_packed_from_source_to_writer_with_hints_and_cancel(
995 selected_objects,
996 object_count,
997 format,
998 options,
999 hints,
1000 limits,
1001 read_object,
1002 writer,
1003 CancelFlag::never(),
1004 )
1005 }
1006
1007 #[allow(clippy::too_many_arguments)]
1026 pub fn write_packed_from_source_to_writer_with_cancel<W, I, F>(
1027 selected_objects: I,
1028 object_count: u32,
1029 format: ObjectFormat,
1030 options: &PackWriteOptions,
1031 limits: PackWriteLimits,
1032 read_object: F,
1033 writer: &mut W,
1034 cancel: CancelFlag<'_>,
1035 ) -> Result<PackWriteSummary>
1036 where
1037 W: Write,
1038 I: IntoIterator<Item = ObjectId>,
1039 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
1040 {
1041 Self::write_packed_from_source_to_writer_with_hints_and_cancel(
1042 selected_objects,
1043 object_count,
1044 format,
1045 options,
1046 PackPlanningHints::new(),
1047 limits,
1048 read_object,
1049 writer,
1050 cancel,
1051 )
1052 }
1053
1054 #[allow(clippy::too_many_arguments)]
1057 pub fn write_packed_from_source_to_writer_with_hints_and_cancel<W, I, F>(
1058 selected_objects: I,
1059 object_count: u32,
1060 format: ObjectFormat,
1061 options: &PackWriteOptions,
1062 hints: PackPlanningHints<'_>,
1063 limits: PackWriteLimits,
1064 read_object: F,
1065 writer: &mut W,
1066 cancel: CancelFlag<'_>,
1067 ) -> Result<PackWriteSummary>
1068 where
1069 W: Write,
1070 I: IntoIterator<Item = ObjectId>,
1071 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
1072 {
1073 validate_thin_base_formats(options, format)?;
1074 cancel.check()?;
1075 let mut stream = SourcePackStream::new(
1076 selected_objects.into_iter(),
1077 object_count,
1078 format,
1079 limits,
1080 read_object,
1081 );
1082 let mut output = PackDigestWriter::new(writer, format);
1083 output.write_pack_bytes(b"PACK")?;
1084 output.write_pack_bytes(&2u32.to_be_bytes())?;
1085 output.write_pack_bytes(&object_count.to_be_bytes())?;
1086
1087 let mut base_horizon: VecDeque<StreamingDeltaBase> = VecDeque::new();
1088 let mut horizon_bytes = 0u64;
1089
1090 loop {
1091 cancel.check()?;
1092 let (window, window_bytes) = stream.fill_window(cancel)?;
1093 if window.is_empty() {
1094 break;
1095 }
1096 stream.note_peak(window_bytes, horizon_bytes);
1097
1098 let objects = window
1099 .iter()
1100 .map(|entry| Arc::clone(&entry.object))
1101 .collect::<Vec<_>>();
1102 let object_ids = window.iter().map(|entry| entry.oid).collect::<Vec<_>>();
1103 let (plan, order) =
1104 plan_streaming_window_deltas(&objects, &object_ids, &base_horizon, options, hints);
1105 let compressed_payloads = compress_streaming_planned_payloads(
1106 &objects,
1107 &plan,
1108 &order,
1109 options.compression_level,
1110 )?;
1111 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
1112
1113 for (&idx, compressed_payload) in order.iter().zip(&compressed_payloads) {
1114 let offset = output.position();
1115 let mut entry_header = Vec::new();
1116 match &plan[idx].base {
1117 StreamingPlannedBase::None => {
1118 write_entry_header(
1119 &mut entry_header,
1120 objects[idx].object_type,
1121 objects[idx].body.len() as u64,
1122 );
1123 }
1124 StreamingPlannedBase::Current { base_idx, delta } => {
1125 stream.delta_count += 1;
1126 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
1127 GitError::InvalidFormat(
1128 "in-pack delta base emitted after dependent object".into(),
1129 )
1130 })?;
1131 if options.prefer_ofs_delta {
1132 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
1133 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
1134 GitError::InvalidFormat(
1135 "ofs-delta base offset is after delta".into(),
1136 )
1137 })?;
1138 write_ofs_delta_offset(&mut entry_header, relative)?;
1139 } else {
1140 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
1141 entry_header.extend_from_slice(object_ids[*base_idx].as_bytes());
1142 }
1143 }
1144 StreamingPlannedBase::Previous {
1145 base_oid,
1146 base_offset,
1147 delta,
1148 } => {
1149 stream.delta_count += 1;
1150 if options.prefer_ofs_delta {
1151 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
1152 let relative = offset.checked_sub(*base_offset).ok_or_else(|| {
1153 GitError::InvalidFormat(
1154 "ofs-delta base offset is after delta".into(),
1155 )
1156 })?;
1157 write_ofs_delta_offset(&mut entry_header, relative)?;
1158 } else {
1159 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
1160 entry_header.extend_from_slice(base_oid.as_bytes());
1161 }
1162 }
1163 StreamingPlannedBase::External { base_oid, delta } => {
1164 stream.delta_count += 1;
1165 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
1166 entry_header.extend_from_slice(base_oid.as_bytes());
1167 }
1168 }
1169
1170 let mut crc32 = crc32fast::Hasher::new();
1171 crc32.update(&entry_header);
1172 crc32.update(compressed_payload);
1173 output.write_pack_bytes(&entry_header)?;
1174 output.write_pack_bytes(compressed_payload)?;
1175 written_offsets[idx] = Some(offset);
1176 stream.index_entries.push(PackIndexEntry {
1177 oid: object_ids[idx],
1178 crc32: crc32.finalize(),
1179 offset,
1180 });
1181
1182 retain_streaming_delta_base(
1183 &mut base_horizon,
1184 &mut horizon_bytes,
1185 StreamingDeltaBase {
1186 oid: object_ids[idx],
1187 object: Arc::clone(&objects[idx]),
1188 offset,
1189 depth: plan[idx].depth,
1190 },
1191 options,
1192 limits,
1193 );
1194 stream.note_peak(window_bytes, horizon_bytes);
1195 }
1196 }
1197
1198 stream.finish(output)
1199 }
1200}
1201
1202pub(crate) struct PackDigestWriter<'a, W> {
1203 writer: &'a mut W,
1204 digest: StreamingDigest,
1205 position: u64,
1206}
1207
1208impl<'a, W> PackDigestWriter<'a, W>
1209where
1210 W: Write,
1211{
1212 pub(crate) fn new(writer: &'a mut W, format: ObjectFormat) -> Self {
1213 Self {
1214 writer,
1215 digest: StreamingDigest::new(format),
1216 position: 0,
1217 }
1218 }
1219
1220 pub(crate) fn position(&self) -> u64 {
1221 self.position
1222 }
1223
1224 pub(crate) fn write_pack_bytes(&mut self, bytes: &[u8]) -> Result<()> {
1225 self.writer.write_all(bytes)?;
1226 self.digest.update(bytes);
1227 self.position = self
1228 .position
1229 .checked_add(bytes.len() as u64)
1230 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
1231 Ok(())
1232 }
1233
1234 pub(crate) fn finish(mut self) -> Result<(ObjectId, u64)> {
1235 let checksum = self.digest.finalize()?;
1236 self.writer.write_all(checksum.as_bytes())?;
1237 self.position = self
1238 .position
1239 .checked_add(checksum.as_bytes().len() as u64)
1240 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
1241 Ok((checksum, self.position))
1242 }
1243}
1244pub(crate) fn compress_planned_payloads(
1245 objects: &[&EncodedObject],
1246 plan: &[PlannedEntry],
1247 order: &[usize],
1248 compression_level: u32,
1249) -> Result<Vec<Vec<u8>>> {
1250 if order.is_empty() {
1251 return Ok(Vec::new());
1252 }
1253
1254 let worker_count = std::thread::available_parallelism()
1255 .map(|threads| threads.get())
1256 .unwrap_or(1)
1257 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
1258 .min(order.len());
1259 if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
1260 let mut payloads = Vec::with_capacity(order.len());
1261 for &idx in order {
1262 payloads.push(compressed_payload(
1263 planned_payload(objects, plan, idx),
1264 compression_level,
1265 )?);
1266 }
1267 return Ok(payloads);
1268 }
1269
1270 let chunk_len = order.len().div_ceil(worker_count);
1271 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
1272 std::thread::scope(|scope| {
1273 let mut handles = Vec::new();
1274 for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
1275 let chunk_start = chunk_idx * chunk_len;
1276 handles.push(scope.spawn(sley_core::diagnostics::inherit(
1277 move || -> Result<Vec<(usize, Vec<u8>)>> {
1278 let mut chunk_payloads = Vec::with_capacity(chunk.len());
1279 for (offset, &idx) in chunk.iter().enumerate() {
1280 chunk_payloads.push((
1281 chunk_start + offset,
1282 compressed_payload(
1283 planned_payload(objects, plan, idx),
1284 compression_level,
1285 )?,
1286 ));
1287 }
1288 Ok(chunk_payloads)
1289 },
1290 )));
1291 }
1292
1293 let mut first_error = None;
1294 for handle in handles {
1295 match handle.join() {
1296 Ok(Ok(chunk_payloads)) => {
1297 if first_error.is_none() {
1298 for (pos, payload) in chunk_payloads {
1299 payloads[pos] = payload;
1300 }
1301 }
1302 }
1303 Ok(Err(err)) => {
1304 first_error.get_or_insert(err);
1305 }
1306 Err(_) => {
1307 first_error.get_or_insert_with(|| {
1308 GitError::InvalidObject("pack compression worker panicked".into())
1309 });
1310 }
1311 }
1312 }
1313
1314 match first_error {
1315 Some(err) => Err(err),
1316 None => Ok(()),
1317 }
1318 })?;
1319 Ok(payloads)
1320}
1321
1322pub(crate) fn compress_streaming_planned_payloads(
1323 objects: &[Arc<EncodedObject>],
1324 plan: &[StreamingPlannedEntry],
1325 order: &[usize],
1326 compression_level: u32,
1327) -> Result<Vec<Vec<u8>>> {
1328 if order.is_empty() {
1329 return Ok(Vec::new());
1330 }
1331
1332 let worker_count = std::thread::available_parallelism()
1333 .map(|threads| threads.get())
1334 .unwrap_or(1)
1335 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
1336 .min(order.len());
1337 if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
1338 let mut payloads = Vec::with_capacity(order.len());
1339 for &idx in order {
1340 payloads.push(compressed_payload(
1341 streaming_planned_payload(objects, plan, idx),
1342 compression_level,
1343 )?);
1344 }
1345 return Ok(payloads);
1346 }
1347
1348 let chunk_len = order.len().div_ceil(worker_count);
1349 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
1350 std::thread::scope(|scope| {
1351 let mut handles = Vec::new();
1352 for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
1353 let chunk_start = chunk_idx * chunk_len;
1354 handles.push(scope.spawn(sley_core::diagnostics::inherit(
1355 move || -> Result<Vec<(usize, Vec<u8>)>> {
1356 let mut chunk_payloads = Vec::with_capacity(chunk.len());
1357 for (offset, &idx) in chunk.iter().enumerate() {
1358 chunk_payloads.push((
1359 chunk_start + offset,
1360 compressed_payload(
1361 streaming_planned_payload(objects, plan, idx),
1362 compression_level,
1363 )?,
1364 ));
1365 }
1366 Ok(chunk_payloads)
1367 },
1368 )));
1369 }
1370
1371 let mut first_error = None;
1372 for handle in handles {
1373 match handle.join() {
1374 Ok(Ok(chunk_payloads)) => {
1375 if first_error.is_none() {
1376 for (pos, payload) in chunk_payloads {
1377 payloads[pos] = payload;
1378 }
1379 }
1380 }
1381 Ok(Err(err)) => {
1382 first_error.get_or_insert(err);
1383 }
1384 Err(_) => {
1385 first_error.get_or_insert_with(|| {
1386 GitError::InvalidObject("pack compression worker panicked".into())
1387 });
1388 }
1389 }
1390 }
1391
1392 match first_error {
1393 Some(err) => Err(err),
1394 None => Ok(()),
1395 }
1396 })?;
1397 Ok(payloads)
1398}
1399
1400pub(crate) fn compress_undeltified_payloads(
1401 objects: &[Arc<EncodedObject>],
1402 compression_level: u32,
1403) -> Result<Vec<Vec<u8>>> {
1404 if objects.is_empty() {
1405 return Ok(Vec::new());
1406 }
1407
1408 let worker_count = std::thread::available_parallelism()
1409 .map(|threads| threads.get())
1410 .unwrap_or(1)
1411 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
1412 .min(objects.len());
1413 if worker_count <= 1 || objects.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
1414 let mut payloads = Vec::with_capacity(objects.len());
1415 for object in objects {
1416 payloads.push(compressed_payload(&object.body, compression_level)?);
1417 }
1418 return Ok(payloads);
1419 }
1420
1421 let chunk_len = objects.len().div_ceil(worker_count);
1422 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new)
1423 .take(objects.len())
1424 .collect();
1425 std::thread::scope(|scope| {
1426 let mut handles = Vec::new();
1427 for (chunk_idx, chunk) in objects.chunks(chunk_len).enumerate() {
1428 let chunk_start = chunk_idx * chunk_len;
1429 handles.push(scope.spawn(sley_core::diagnostics::inherit(
1430 move || -> Result<Vec<(usize, Vec<u8>)>> {
1431 let mut chunk_payloads = Vec::with_capacity(chunk.len());
1432 for (offset, object) in chunk.iter().enumerate() {
1433 chunk_payloads.push((
1434 chunk_start + offset,
1435 compressed_payload(&object.body, compression_level)?,
1436 ));
1437 }
1438 Ok(chunk_payloads)
1439 },
1440 )));
1441 }
1442
1443 let mut first_error = None;
1444 for handle in handles {
1445 match handle.join() {
1446 Ok(Ok(chunk_payloads)) => {
1447 if first_error.is_none() {
1448 for (pos, payload) in chunk_payloads {
1449 payloads[pos] = payload;
1450 }
1451 }
1452 }
1453 Ok(Err(err)) => {
1454 first_error.get_or_insert(err);
1455 }
1456 Err(_) => {
1457 first_error.get_or_insert_with(|| {
1458 GitError::InvalidObject("pack compression worker panicked".into())
1459 });
1460 }
1461 }
1462 }
1463
1464 match first_error {
1465 Some(err) => Err(err),
1466 None => Ok(()),
1467 }
1468 })?;
1469 Ok(payloads)
1470}
1471
1472pub(crate) fn streaming_planned_payload<'a>(
1473 objects: &'a [Arc<EncodedObject>],
1474 plan: &'a [StreamingPlannedEntry],
1475 idx: usize,
1476) -> &'a [u8] {
1477 match &plan[idx].base {
1478 StreamingPlannedBase::None => &objects[idx].body,
1479 StreamingPlannedBase::Current { delta, .. }
1480 | StreamingPlannedBase::Previous { delta, .. }
1481 | StreamingPlannedBase::External { delta, .. } => delta,
1482 }
1483}
1484
1485pub(crate) fn planned_payload<'a>(
1486 objects: &'a [&'a EncodedObject],
1487 plan: &'a [PlannedEntry],
1488 idx: usize,
1489) -> &'a [u8] {
1490 match &plan[idx].base {
1491 PlannedBase::None => &objects[idx].body,
1492 PlannedBase::InPack { delta, .. } | PlannedBase::External { delta, .. } => delta,
1493 }
1494}
1495
1496pub(crate) fn compressed_payload(body: &[u8], compression_level: u32) -> Result<Vec<u8>> {
1497 let mut out = Vec::new();
1498 write_compressed_payload(&mut out, body, compression_level)?;
1499 Ok(out)
1500}
1501pub(crate) fn write_compressed_payload(
1502 out: &mut Vec<u8>,
1503 body: &[u8],
1504 compression_level: u32,
1505) -> Result<()> {
1506 let mut compressor = Compress::new(Compression::new(compression_level.min(9)), true);
1507 out.reserve(zlib_compress_bound(body.len()));
1508 let status = compressor
1509 .compress_vec(body, out, FlushCompress::Finish)
1510 .map_err(|err| GitError::InvalidObject(format!("zlib compression failed: {err}")))?;
1511 if status != Status::StreamEnd || compressor.total_in() != body.len() as u64 {
1512 return Err(GitError::InvalidObject(
1513 "zlib compression did not finish pack entry".into(),
1514 ));
1515 }
1516 Ok(())
1517}
1518
1519pub(crate) fn zlib_compress_bound(len: usize) -> usize {
1520 len.saturating_add(len >> 12)
1521 .saturating_add(len >> 14)
1522 .saturating_add(len >> 25)
1523 .saturating_add(13)
1524}
1525
1526pub(crate) fn write_entry_header(out: &mut Vec<u8>, object_type: ObjectType, size: u64) {
1527 let type_code = match object_type {
1528 ObjectType::Commit => 1,
1529 ObjectType::Tree => 2,
1530 ObjectType::Blob => 3,
1531 ObjectType::Tag => 4,
1532 };
1533 write_pack_entry_header_kind(out, type_code, size);
1534}
1535
1536pub(crate) fn write_pack_entry_header_kind(out: &mut Vec<u8>, type_code: u8, mut size: u64) {
1537 let mut byte = (type_code << 4) | ((size as u8) & 0x0f);
1538 size >>= 4;
1539 if size != 0 {
1540 byte |= 0x80;
1541 }
1542 out.push(byte);
1543 while size != 0 {
1544 let mut byte = (size as u8) & 0x7f;
1545 size >>= 7;
1546 if size != 0 {
1547 byte |= 0x80;
1548 }
1549 out.push(byte);
1550 }
1551}
1552
1553pub(crate) fn write_ofs_delta_offset(out: &mut Vec<u8>, relative: u64) -> Result<()> {
1554 if relative == 0 {
1555 return Err(GitError::InvalidFormat(
1556 "ofs-delta relative offset cannot be zero".into(),
1557 ));
1558 }
1559 let mut value = relative;
1560 let mut bytes = vec![(value & 0x7f) as u8];
1561 value >>= 7;
1562 while value != 0 {
1563 value -= 1;
1564 bytes.push(((value & 0x7f) as u8) | 0x80);
1565 value >>= 7;
1566 }
1567 bytes.reverse();
1568 out.extend_from_slice(&bytes);
1569 Ok(())
1570}
1571#[derive(Debug, Clone)]
1584pub struct PackBitmapWriter {
1585 format: ObjectFormat,
1586 pack_checksum: ObjectId,
1587 object_count: u32,
1588 commit_positions: Vec<u32>,
1589 tree_positions: Vec<u32>,
1590 blob_positions: Vec<u32>,
1591 tag_positions: Vec<u32>,
1592 name_hash_cache: Option<Vec<u32>>,
1593 write_lookup_table: bool,
1594 selected: Vec<SelectedCommit>,
1595 pseudo_merges: Vec<PackBitmapPseudoMerge>,
1596}
1597
1598#[derive(Debug, Clone)]
1599pub(crate) struct SelectedCommit {
1600 commit_index_position: u32,
1604 flags: u8,
1605 reachable: Vec<u32>,
1606}
1607
1608impl PackBitmapWriter {
1609 pub const FLAG_NONE: u8 = 0;
1613
1614 pub fn new(
1621 format: ObjectFormat,
1622 pack_checksum: ObjectId,
1623 object_types: &[ObjectType],
1624 ) -> Result<Self> {
1625 if object_types.len() > u32::MAX as usize {
1626 return Err(GitError::InvalidFormat(
1627 "too many objects for a pack bitmap".into(),
1628 ));
1629 }
1630 if pack_checksum.format() != format {
1631 return Err(GitError::InvalidObjectId(
1632 "pack checksum format does not match bitmap format".into(),
1633 ));
1634 }
1635 let object_count = object_types.len() as u32;
1636 let mut commit_positions = Vec::new();
1637 let mut tree_positions = Vec::new();
1638 let mut blob_positions = Vec::new();
1639 let mut tag_positions = Vec::new();
1640 for (index, object_type) in object_types.iter().enumerate() {
1641 let position = index as u32;
1642 match object_type {
1643 ObjectType::Commit => commit_positions.push(position),
1644 ObjectType::Tree => tree_positions.push(position),
1645 ObjectType::Blob => blob_positions.push(position),
1646 ObjectType::Tag => tag_positions.push(position),
1647 }
1648 }
1649 Ok(Self {
1650 format,
1651 pack_checksum,
1652 object_count,
1653 commit_positions,
1654 tree_positions,
1655 blob_positions,
1656 tag_positions,
1657 name_hash_cache: None,
1658 write_lookup_table: false,
1659 selected: Vec::new(),
1660 pseudo_merges: Vec::new(),
1661 })
1662 }
1663
1664 pub fn with_name_hash_cache(mut self, cache: Vec<u32>) -> Result<Self> {
1670 if cache.len() != self.object_count as usize {
1671 return Err(GitError::InvalidFormat(format!(
1672 "name hash cache has {} entries but pack has {} objects",
1673 cache.len(),
1674 self.object_count
1675 )));
1676 }
1677 self.name_hash_cache = Some(cache);
1678 Ok(self)
1679 }
1680
1681 pub fn with_lookup_table(mut self, enabled: bool) -> Self {
1684 self.write_lookup_table = enabled;
1685 self
1686 }
1687
1688 pub fn add_commit(
1700 &mut self,
1701 commit_position: u32,
1702 commit_index_position: u32,
1703 reachable: &[u32],
1704 ) -> Result<()> {
1705 if commit_position >= self.object_count {
1706 return Err(GitError::InvalidFormat(format!(
1707 "commit position {commit_position} out of range for {} objects",
1708 self.object_count
1709 )));
1710 }
1711 if commit_index_position >= self.object_count {
1712 return Err(GitError::InvalidFormat(format!(
1713 "commit index position {commit_index_position} out of range for {} objects",
1714 self.object_count
1715 )));
1716 }
1717 if !self.commit_positions.contains(&commit_position) {
1718 return Err(GitError::InvalidFormat(format!(
1719 "bitmap commit position {commit_position} is not a commit object"
1720 )));
1721 }
1722 for &position in reachable {
1723 if position >= self.object_count {
1724 return Err(GitError::InvalidFormat(format!(
1725 "reachable position {position} out of range for {} objects",
1726 self.object_count
1727 )));
1728 }
1729 }
1730 let mut reachable = reachable.to_vec();
1731 reachable.push(commit_position);
1732 self.selected.push(SelectedCommit {
1733 commit_index_position,
1734 flags: Self::FLAG_NONE,
1735 reachable,
1736 });
1737 Ok(())
1738 }
1739
1740 pub fn add_pseudo_merge(&mut self, commits: &[u32], reachable: &[u32]) -> Result<()> {
1745 if commits.is_empty() {
1746 return Err(GitError::InvalidFormat(
1747 "pseudo-merge must contain at least one commit".into(),
1748 ));
1749 }
1750 for &position in commits {
1751 if position >= self.object_count {
1752 return Err(GitError::InvalidFormat(format!(
1753 "pseudo-merge commit position {position} out of range for {} objects",
1754 self.object_count
1755 )));
1756 }
1757 if !self.commit_positions.contains(&position) {
1758 return Err(GitError::InvalidFormat(format!(
1759 "pseudo-merge commit position {position} is not a commit object"
1760 )));
1761 }
1762 }
1763 for &position in reachable {
1764 if position >= self.object_count {
1765 return Err(GitError::InvalidFormat(format!(
1766 "pseudo-merge reachable position {position} out of range for {} objects",
1767 self.object_count
1768 )));
1769 }
1770 }
1771 self.pseudo_merges.push(PackBitmapPseudoMerge {
1772 commits: EwahBitmap::from_positions(self.object_count, commits)?,
1773 bitmap: EwahBitmap::from_positions(self.object_count, reachable)?,
1774 });
1775 Ok(())
1776 }
1777
1778 pub fn build(&self) -> Result<PackBitmapIndex> {
1785 let commits = EwahBitmap::from_positions(self.object_count, &self.commit_positions)?;
1786 let trees = EwahBitmap::from_positions(self.object_count, &self.tree_positions)?;
1787 let blobs = EwahBitmap::from_positions(self.object_count, &self.blob_positions)?;
1788 let tags = EwahBitmap::from_positions(self.object_count, &self.tag_positions)?;
1789
1790 let mut entries = Vec::with_capacity(self.selected.len());
1791 for selected in &self.selected {
1792 let bitmap = EwahBitmap::from_positions(self.object_count, &selected.reachable)?;
1793 entries.push(PackBitmapEntry {
1794 object_position: selected.commit_index_position,
1795 xor_offset: 0,
1796 flags: selected.flags,
1797 bitmap,
1798 });
1799 }
1800
1801 let mut options = PackBitmapIndex::OPTION_FULL_DAG;
1802 if self.name_hash_cache.is_some() {
1803 options |= PackBitmapIndex::OPTION_HASH_CACHE;
1804 }
1805 if !self.pseudo_merges.is_empty() {
1806 options |= PackBitmapIndex::OPTION_PSEUDO_MERGES;
1807 }
1808 if self.write_lookup_table {
1809 options |= PackBitmapIndex::OPTION_LOOKUP_TABLE;
1810 }
1811
1812 let placeholder_checksum = ObjectId::null(self.format);
1817 Ok(PackBitmapIndex {
1818 version: 1,
1819 format: self.format,
1820 options,
1821 pack_checksum: self.pack_checksum.clone(),
1822 index_checksum: placeholder_checksum,
1823 type_bitmaps: PackBitmapTypeBitmaps {
1824 commits,
1825 trees,
1826 blobs,
1827 tags,
1828 },
1829 entries,
1830 pseudo_merges: self.pseudo_merges.clone(),
1831 lookup_table: self.write_lookup_table,
1832 name_hash_cache: self.name_hash_cache.clone(),
1833 })
1834 }
1835
1836 pub fn write(&self) -> Result<Vec<u8>> {
1839 self.build()?.write()
1840 }
1841}
1842
1843impl PackBitmapIndex {
1844 pub fn write(&self) -> Result<Vec<u8>> {
1858 if self.version != 1 {
1859 return Err(GitError::Unsupported(format!(
1860 "bitmap index version {}",
1861 self.version
1862 )));
1863 }
1864 let mut options = self.options;
1865 if !self.pseudo_merges.is_empty() {
1866 options |= Self::OPTION_PSEUDO_MERGES;
1867 }
1868 if self.lookup_table {
1869 options |= Self::OPTION_LOOKUP_TABLE;
1870 }
1871 let known_options = Self::OPTION_FULL_DAG
1872 | Self::OPTION_HASH_CACHE
1873 | Self::OPTION_LOOKUP_TABLE
1874 | Self::OPTION_PSEUDO_MERGES;
1875 if options & !known_options != 0 {
1876 return Err(GitError::Unsupported(format!(
1877 "bitmap index options {:#06x}",
1878 options & !known_options
1879 )));
1880 }
1881 if self.pack_checksum.format() != self.format {
1882 return Err(GitError::InvalidObjectId(
1883 "bitmap pack checksum format does not match index format".into(),
1884 ));
1885 }
1886 if self.entries.len() > u32::MAX as usize {
1887 return Err(GitError::InvalidFormat(
1888 "too many bitmap index entries".into(),
1889 ));
1890 }
1891 if options & Self::OPTION_PSEUDO_MERGES != 0 && self.pseudo_merges.is_empty() {
1892 return Err(GitError::InvalidFormat(
1893 "OPTION_PSEUDO_MERGES set without pseudo-merge records".into(),
1894 ));
1895 }
1896 let want_cache = options & Self::OPTION_HASH_CACHE != 0;
1897 match (&self.name_hash_cache, want_cache) {
1898 (Some(_), false) => {
1899 return Err(GitError::InvalidFormat(
1900 "name hash cache present without OPTION_HASH_CACHE".into(),
1901 ));
1902 }
1903 (None, true) => {
1904 return Err(GitError::InvalidFormat(
1905 "OPTION_HASH_CACHE set without a name hash cache".into(),
1906 ));
1907 }
1908 _ => {}
1909 }
1910
1911 let mut out = Vec::new();
1912 out.extend_from_slice(b"BITM");
1913 out.extend_from_slice(&self.version.to_be_bytes());
1914 out.extend_from_slice(&options.to_be_bytes());
1915 out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1916 out.extend_from_slice(self.pack_checksum.as_bytes());
1917
1918 self.type_bitmaps.commits.append_bytes(&mut out);
1919 self.type_bitmaps.trees.append_bytes(&mut out);
1920 self.type_bitmaps.blobs.append_bytes(&mut out);
1921 self.type_bitmaps.tags.append_bytes(&mut out);
1922
1923 let mut entry_offsets = Vec::with_capacity(self.entries.len());
1924 for (idx, entry) in self.entries.iter().enumerate() {
1925 if entry.xor_offset as usize > idx {
1926 return Err(GitError::InvalidFormat(
1927 "bitmap index entry has invalid XOR offset".into(),
1928 ));
1929 }
1930 entry_offsets.push(out.len() as u64);
1931 out.extend_from_slice(&entry.object_position.to_be_bytes());
1932 out.push(entry.xor_offset);
1933 out.push(entry.flags);
1934 entry.bitmap.append_bytes(&mut out);
1935 }
1936
1937 if !self.pseudo_merges.is_empty() {
1938 append_bitmap_pseudo_merges(&mut out, &self.pseudo_merges)?;
1939 }
1940
1941 if self.lookup_table {
1942 append_bitmap_lookup_table(&mut out, &self.entries, &entry_offsets)?;
1943 }
1944
1945 if let Some(cache) = &self.name_hash_cache {
1946 for value in cache {
1947 out.extend_from_slice(&value.to_be_bytes());
1948 }
1949 }
1950
1951 let checksum = sley_core::digest_bytes(self.format, &out)?;
1952 out.extend_from_slice(checksum.as_bytes());
1953 Ok(out)
1954 }
1955}
1956
1957fn append_bitmap_lookup_table(
1958 out: &mut Vec<u8>,
1959 entries: &[PackBitmapEntry],
1960 entry_offsets: &[u64],
1961) -> Result<()> {
1962 if entries.len() != entry_offsets.len() {
1963 return Err(GitError::InvalidFormat(
1964 "bitmap lookup table offset count mismatch".into(),
1965 ));
1966 }
1967 let mut table: Vec<usize> = (0..entries.len()).collect();
1968 table.sort_by_key(|&index| entries[index].object_position);
1969 let mut inverse = vec![0u32; entries.len()];
1970 for (row, &entry_index) in table.iter().enumerate() {
1971 inverse[entry_index] = row as u32;
1972 }
1973 for &entry_index in &table {
1974 let entry = &entries[entry_index];
1975 let xor_row = if entry.xor_offset == 0 {
1976 u32::MAX
1977 } else {
1978 let base = entry_index
1979 .checked_sub(entry.xor_offset as usize)
1980 .ok_or_else(|| {
1981 GitError::InvalidFormat("bitmap lookup table XOR base underflow".into())
1982 })?;
1983 inverse[base]
1984 };
1985 out.extend_from_slice(&entry.object_position.to_be_bytes());
1986 out.extend_from_slice(&entry_offsets[entry_index].to_be_bytes());
1987 out.extend_from_slice(&xor_row.to_be_bytes());
1988 }
1989 Ok(())
1990}
1991
1992pub(crate) fn append_bitmap_pseudo_merges(
1993 out: &mut Vec<u8>,
1994 pseudo_merges: &[PackBitmapPseudoMerge],
1995) -> Result<()> {
1996 if pseudo_merges.len() > u32::MAX as usize {
1997 return Err(GitError::InvalidFormat(
1998 "too many pseudo-merge bitmap records".into(),
1999 ));
2000 }
2001 let start = out.len();
2002 let mut pseudo_offsets = Vec::with_capacity(pseudo_merges.len());
2003 let mut commit_to_offsets: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
2004 for merge in pseudo_merges {
2005 let offset = u64::try_from(out.len())
2006 .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
2007 pseudo_offsets.push(offset);
2008 for commit_pos in merge.commits.to_positions()? {
2009 commit_to_offsets
2010 .entry(commit_pos)
2011 .or_default()
2012 .push(offset);
2013 }
2014 merge.commits.append_bytes(out);
2015 merge.bitmap.append_bytes(out);
2016 }
2017 if commit_to_offsets.len() > u32::MAX as usize {
2018 return Err(GitError::InvalidFormat(
2019 "too many pseudo-merge commits".into(),
2020 ));
2021 }
2022
2023 let lookup_start = out.len();
2024 let lookup_len = commit_to_offsets
2025 .len()
2026 .checked_mul(12)
2027 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?;
2028 let mut next_extended = u64::try_from(
2029 lookup_start
2030 .checked_add(lookup_len)
2031 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?,
2032 )
2033 .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
2034 let mut rows = Vec::with_capacity(commit_to_offsets.len());
2035 for (commit_pos, offsets) in commit_to_offsets {
2036 let extended_offset = if offsets.len() > 1 {
2037 if next_extended & (1u64 << 63) != 0 {
2038 return Err(GitError::InvalidFormat(
2039 "pseudo-merge extended offset overflow".into(),
2040 ));
2041 }
2042 let offset = next_extended;
2043 let ext_len = offsets
2044 .len()
2045 .checked_mul(8)
2046 .and_then(|len| len.checked_add(4))
2047 .ok_or_else(|| {
2048 GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
2049 })?;
2050 next_extended = next_extended.checked_add(ext_len as u64).ok_or_else(|| {
2051 GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
2052 })?;
2053 Some(offset)
2054 } else {
2055 None
2056 };
2057 rows.push((commit_pos, offsets, extended_offset));
2058 }
2059
2060 for (commit_pos, offsets, extended_offset) in &rows {
2061 out.extend_from_slice(&commit_pos.to_be_bytes());
2062 match extended_offset {
2063 Some(offset) => out.extend_from_slice(&(offset | (1u64 << 63)).to_be_bytes()),
2064 None => out.extend_from_slice(&offsets[0].to_be_bytes()),
2065 }
2066 }
2067
2068 for (_commit_pos, offsets, extended_offset) in &rows {
2069 if extended_offset.is_none() {
2070 continue;
2071 }
2072 let count = u32::try_from(offsets.len())
2073 .map_err(|_| GitError::InvalidFormat("pseudo-merge extended lookup overflow".into()))?;
2074 out.extend_from_slice(&count.to_be_bytes());
2075 for offset in offsets {
2076 out.extend_from_slice(&offset.to_be_bytes());
2077 }
2078 }
2079
2080 for offset in &pseudo_offsets {
2081 out.extend_from_slice(&offset.to_be_bytes());
2082 }
2083 out.extend_from_slice(&(pseudo_merges.len() as u32).to_be_bytes());
2084 out.extend_from_slice(&(rows.len() as u32).to_be_bytes());
2085 let lookup_relative = lookup_start
2086 .checked_sub(start)
2087 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup underflow".into()))?;
2088 out.extend_from_slice(&(lookup_relative as u64).to_be_bytes());
2089 let extension_size = out
2090 .len()
2091 .checked_sub(start)
2092 .and_then(|len| len.checked_add(8))
2093 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge extension overflow".into()))?;
2094 out.extend_from_slice(&(extension_size as u64).to_be_bytes());
2095 Ok(())
2096}
2097
2098pub fn write_bitmap(
2107 format: ObjectFormat,
2108 pack_checksum: ObjectId,
2109 object_types: &[ObjectType],
2110 commits: &[(u32, u32, Vec<u32>)],
2111 name_hash_cache: Option<Vec<u32>>,
2112) -> Result<Vec<u8>> {
2113 let mut writer = PackBitmapWriter::new(format, pack_checksum, object_types)?;
2114 if let Some(cache) = name_hash_cache {
2115 writer = writer.with_name_hash_cache(cache)?;
2116 }
2117 for (commit_position, commit_index_position, reachable) in commits {
2118 writer.add_commit(*commit_position, *commit_index_position, reachable)?;
2119 }
2120 writer.write()
2121}