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
31pub(crate) const PACK_STREAM_COMPRESSION_WINDOW_OBJECTS: usize = 256;
35
36#[derive(Debug, Clone)]
42pub struct PackWriteOptions {
43 pub window: usize,
46 pub depth: usize,
48 pub prefer_ofs_delta: bool,
52 pub thin_bases: HashMap<ObjectId, EncodedObject>,
57 pub preferred_thin_bases: HashMap<ObjectId, ObjectId>,
62 pub reorder: bool,
68 pub compression_level: u32,
70}
71
72impl Default for PackWriteOptions {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl PackWriteOptions {
79 pub fn new() -> Self {
83 Self {
84 window: DEFAULT_PACK_WINDOW,
85 depth: DEFAULT_PACK_DEPTH,
86 prefer_ofs_delta: true,
87 thin_bases: HashMap::new(),
88 preferred_thin_bases: HashMap::new(),
89 reorder: true,
90 compression_level: 6,
91 }
92 }
93
94 pub fn with_window(mut self, window: usize) -> Self {
96 self.window = window;
97 self
98 }
99
100 pub fn with_depth(mut self, depth: usize) -> Self {
102 self.depth = depth;
103 self
104 }
105
106 pub fn with_prefer_ofs_delta(mut self, prefer_ofs_delta: bool) -> Self {
109 self.prefer_ofs_delta = prefer_ofs_delta;
110 self
111 }
112
113 pub fn with_thin_bases(mut self, thin_bases: HashMap<ObjectId, EncodedObject>) -> Self {
115 self.thin_bases = thin_bases;
116 self
117 }
118
119 pub fn with_preferred_thin_bases(
121 mut self,
122 preferred_thin_bases: HashMap<ObjectId, ObjectId>,
123 ) -> Self {
124 self.preferred_thin_bases = preferred_thin_bases;
125 self
126 }
127
128 pub fn with_reorder(mut self, reorder: bool) -> Self {
131 self.reorder = reorder;
132 self
133 }
134
135 pub fn with_compression_level(mut self, level: u32) -> Self {
137 self.compression_level = level.min(9);
138 self
139 }
140}
141
142impl PackFile {
143 pub fn write_undeltified_sha1<T>(objects: &[T]) -> Result<PackWrite>
144 where
145 T: Borrow<EncodedObject>,
146 {
147 Self::write_undeltified(objects, ObjectFormat::Sha1)
148 }
149
150 pub fn write_undeltified<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
156 where
157 T: Borrow<EncodedObject>,
158 {
159 let options = PackWriteOptions::new().with_depth(0).with_reorder(false);
160 Self::write_packed_impl(objects, format, &options)
161 }
162
163 pub fn write_packed<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
172 where
173 T: Borrow<EncodedObject>,
174 {
175 Self::write_packed_with_options(objects, format, &PackWriteOptions::new())
176 }
177
178 pub fn write_packed_with_options<T>(
182 objects: &[T],
183 format: ObjectFormat,
184 options: &PackWriteOptions,
185 ) -> Result<PackWrite>
186 where
187 T: Borrow<EncodedObject>,
188 {
189 Self::write_packed_impl(objects, format, options)
190 }
191
192 pub fn write_packed_with_known_ids(
201 inputs: &[PackInput<'_>],
202 format: ObjectFormat,
203 ) -> Result<PackWrite> {
204 Self::write_packed_with_known_ids_and_options(inputs, format, &PackWriteOptions::new())
205 }
206
207 pub fn write_packed_with_known_ids_and_options(
210 inputs: &[PackInput<'_>],
211 format: ObjectFormat,
212 options: &PackWriteOptions,
213 ) -> Result<PackWrite> {
214 if inputs.len() > u32::MAX as usize {
215 return Err(GitError::InvalidFormat("too many pack objects".into()));
216 }
217 let mut objects = Vec::with_capacity(inputs.len());
218 let mut object_ids = Vec::with_capacity(inputs.len());
219 for input in inputs {
220 if input.oid.format() != format {
221 return Err(GitError::InvalidObjectId(format!(
222 "pack object id {} uses {}, pack uses {}",
223 input.oid,
224 input.oid.format().name(),
225 format.name()
226 )));
227 }
228 objects.push(input.object);
229 object_ids.push(*input.oid);
230 }
231 Self::write_packed_from_parts(objects, object_ids, format, options)
232 }
233
234 pub fn write_packed_with_known_ids_to_writer<W>(
235 inputs: &[PackInput<'_>],
236 format: ObjectFormat,
237 options: &PackWriteOptions,
238 writer: &mut W,
239 ) -> Result<PackWriteSummary>
240 where
241 W: Write,
242 {
243 if inputs.len() > u32::MAX as usize {
244 return Err(GitError::InvalidFormat("too many pack objects".into()));
245 }
246 let mut objects = Vec::with_capacity(inputs.len());
247 let mut object_ids = Vec::with_capacity(inputs.len());
248 for input in inputs {
249 if input.oid.format() != format {
250 return Err(GitError::InvalidObjectId(format!(
251 "pack object id {} uses {}, pack uses {}",
252 input.oid,
253 input.oid.format().name(),
254 format.name()
255 )));
256 }
257 objects.push(input.object);
258 object_ids.push(*input.oid);
259 }
260 Self::write_packed_from_parts_to_writer(objects, object_ids, format, options, writer)
261 }
262
263 pub fn write_thin<T>(
272 objects: &[T],
273 format: ObjectFormat,
274 external_bases: HashMap<ObjectId, EncodedObject>,
275 ) -> Result<PackWrite>
276 where
277 T: Borrow<EncodedObject>,
278 {
279 let options = PackWriteOptions::new().with_thin_bases(external_bases);
280 Self::write_packed_impl(objects, format, &options)
281 }
282
283 pub(crate) fn write_packed_impl<T>(
284 objects: &[T],
285 format: ObjectFormat,
286 options: &PackWriteOptions,
287 ) -> Result<PackWrite>
288 where
289 T: Borrow<EncodedObject>,
290 {
291 if objects.len() > u32::MAX as usize {
292 return Err(GitError::InvalidFormat("too many pack objects".into()));
293 }
294 let objects: Vec<&EncodedObject> = objects.iter().map(Borrow::borrow).collect();
295
296 let mut object_ids: Vec<ObjectId> = Vec::with_capacity(objects.len());
299 for object in &objects {
300 object_ids.push(object.object_id(format)?);
301 }
302 Self::write_packed_from_parts(objects, object_ids, format, options)
303 }
304
305 pub(crate) fn write_packed_from_parts(
306 objects: Vec<&EncodedObject>,
307 object_ids: Vec<ObjectId>,
308 format: ObjectFormat,
309 options: &PackWriteOptions,
310 ) -> Result<PackWrite> {
311 let mut seen = HashSet::with_capacity(object_ids.len());
312 for oid in &object_ids {
313 if !seen.insert(oid) {
314 return Err(GitError::InvalidFormat(format!(
315 "pack contains duplicate object id {oid}"
316 )));
317 }
318 }
319
320 for oid in options.thin_bases.keys() {
322 if oid.format() != format {
323 return Err(GitError::InvalidObjectId(
324 "thin pack base object id format does not match pack format".into(),
325 ));
326 }
327 }
328
329 let (plan, order) = plan_pack_deltas(&objects, &object_ids, options)?;
335
336 let mut pack = Vec::new();
337 pack.extend_from_slice(b"PACK");
338 pack.extend_from_slice(&2u32.to_be_bytes());
339 pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
340
341 let mut index_entries = Vec::with_capacity(objects.len());
342 let mut delta_count = 0u32;
343 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
346
347 let compressed_payloads =
348 compress_planned_payloads(&objects, &plan, &order, options.compression_level)?;
349
350 for (order_pos, &idx) in order.iter().enumerate() {
351 let offset = pack.len() as u64;
352 let mut entry_bytes = Vec::new();
353 match &plan[idx].base {
354 PlannedBase::None => {
355 write_entry_header(
356 &mut entry_bytes,
357 objects[idx].object_type,
358 objects[idx].body.len() as u64,
359 );
360 }
361 PlannedBase::InPack { base_idx, delta } => {
362 delta_count += 1;
363 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
364 GitError::InvalidFormat(
365 "in-pack delta base emitted after dependent object".into(),
366 )
367 })?;
368 if options.prefer_ofs_delta {
369 write_pack_entry_header_kind(&mut entry_bytes, 6, delta.len() as u64);
370 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
371 GitError::InvalidFormat("ofs-delta base offset is after delta".into())
372 })?;
373 write_ofs_delta_offset(&mut entry_bytes, relative)?;
374 } else {
375 write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
376 entry_bytes.extend_from_slice(object_ids[*base_idx].as_bytes());
377 }
378 }
379 PlannedBase::External { base_oid, delta } => {
380 delta_count += 1;
381 write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
382 entry_bytes.extend_from_slice(base_oid.as_bytes());
383 }
384 }
385 entry_bytes.extend_from_slice(&compressed_payloads[order_pos]);
386 let crc32 = crc32fast::hash(&entry_bytes);
387 pack.extend_from_slice(&entry_bytes);
388 written_offsets[idx] = Some(offset);
389 index_entries.push(PackIndexEntry {
390 oid: object_ids[idx].clone(),
391 crc32,
392 offset,
393 });
394 }
395
396 let checksum = sley_core::digest_bytes(format, &pack)?;
397 pack.extend_from_slice(checksum.as_bytes());
398 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
399 Ok(PackWrite {
400 pack,
401 index,
402 checksum,
403 entries: index_entries,
404 delta_count,
405 })
406 }
407
408 pub(crate) fn write_packed_from_parts_to_writer<W>(
409 objects: Vec<&EncodedObject>,
410 object_ids: Vec<ObjectId>,
411 format: ObjectFormat,
412 options: &PackWriteOptions,
413 writer: &mut W,
414 ) -> Result<PackWriteSummary>
415 where
416 W: Write,
417 {
418 let mut seen = HashSet::with_capacity(object_ids.len());
419 for oid in &object_ids {
420 if !seen.insert(oid) {
421 return Err(GitError::InvalidFormat(format!(
422 "pack contains duplicate object id {oid}"
423 )));
424 }
425 }
426
427 for oid in options.thin_bases.keys() {
428 if oid.format() != format {
429 return Err(GitError::InvalidObjectId(
430 "thin pack base object id format does not match pack format".into(),
431 ));
432 }
433 }
434
435 let (plan, order) = plan_pack_deltas(&objects, &object_ids, options)?;
436 let mut output = PackDigestWriter::new(writer, format);
437 output.write_pack_bytes(b"PACK")?;
438 output.write_pack_bytes(&2u32.to_be_bytes())?;
439 output.write_pack_bytes(&(objects.len() as u32).to_be_bytes())?;
440
441 let mut index_entries = Vec::with_capacity(objects.len());
442 let mut delta_count = 0u32;
443 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
444
445 for order_window in order.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
446 let compressed_payloads = compress_planned_payloads(
447 &objects,
448 &plan,
449 order_window,
450 options.compression_level,
451 )?;
452 for (&idx, compressed_payload) in order_window.iter().zip(&compressed_payloads) {
453 let offset = output.position();
454 let mut entry_header = Vec::new();
455 match &plan[idx].base {
456 PlannedBase::None => {
457 write_entry_header(
458 &mut entry_header,
459 objects[idx].object_type,
460 objects[idx].body.len() as u64,
461 );
462 }
463 PlannedBase::InPack { base_idx, delta } => {
464 delta_count += 1;
465 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
466 GitError::InvalidFormat(
467 "in-pack delta base emitted after dependent object".into(),
468 )
469 })?;
470 if options.prefer_ofs_delta {
471 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
472 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
473 GitError::InvalidFormat(
474 "ofs-delta base offset is after delta".into(),
475 )
476 })?;
477 write_ofs_delta_offset(&mut entry_header, relative)?;
478 } else {
479 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
480 entry_header.extend_from_slice(object_ids[*base_idx].as_bytes());
481 }
482 }
483 PlannedBase::External { base_oid, delta } => {
484 delta_count += 1;
485 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
486 entry_header.extend_from_slice(base_oid.as_bytes());
487 }
488 }
489 let mut crc32 = crc32fast::Hasher::new();
490 crc32.update(&entry_header);
491 crc32.update(compressed_payload);
492 output.write_pack_bytes(&entry_header)?;
493 output.write_pack_bytes(compressed_payload)?;
494 written_offsets[idx] = Some(offset);
495 index_entries.push(PackIndexEntry {
496 oid: object_ids[idx],
497 crc32: crc32.finalize(),
498 offset,
499 });
500 }
501 }
502
503 let (checksum, pack_size) = output.finish()?;
504 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
505 Ok(PackWriteSummary {
506 index,
507 checksum,
508 entries: index_entries,
509 delta_count,
510 pack_size,
511 })
512 }
513
514 pub fn write_undeltified_from_source_to_writer<W, F>(
515 object_ids: &[ObjectId],
516 format: ObjectFormat,
517 options: &PackWriteOptions,
518 read_object: F,
519 writer: &mut W,
520 ) -> Result<PackWriteSummary>
521 where
522 W: Write,
523 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
524 {
525 Self::write_undeltified_from_source_to_writer_with_cancel(
526 object_ids,
527 format,
528 options,
529 read_object,
530 writer,
531 CancelFlag::never(),
532 )
533 }
534
535 pub fn write_undeltified_from_source_to_writer_with_cancel<W, F>(
537 object_ids: &[ObjectId],
538 format: ObjectFormat,
539 options: &PackWriteOptions,
540 mut read_object: F,
541 writer: &mut W,
542 cancel: CancelFlag<'_>,
543 ) -> Result<PackWriteSummary>
544 where
545 W: Write,
546 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
547 {
548 let mut seen = HashSet::with_capacity(object_ids.len());
549 for oid in object_ids {
550 if oid.format() != format {
551 return Err(GitError::InvalidObjectId(
552 "pack object id format does not match pack format".into(),
553 ));
554 }
555 if !seen.insert(oid) {
556 return Err(GitError::InvalidFormat(format!(
557 "pack contains duplicate object id {oid}"
558 )));
559 }
560 }
561
562 let mut output = PackDigestWriter::new(writer, format);
563 output.write_pack_bytes(b"PACK")?;
564 output.write_pack_bytes(&2u32.to_be_bytes())?;
565 output.write_pack_bytes(&(object_ids.len() as u32).to_be_bytes())?;
566
567 let mut index_entries = Vec::with_capacity(object_ids.len());
568 for oid_window in object_ids.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
569 cancel.check()?;
570 let mut objects = Vec::with_capacity(oid_window.len());
571 for oid in oid_window {
572 objects.push(read_object(oid)?);
573 }
574 let compressed_payloads =
575 compress_undeltified_payloads(&objects, options.compression_level)?;
576 for ((oid, object), compressed_payload) in
577 oid_window.iter().zip(&objects).zip(&compressed_payloads)
578 {
579 let offset = output.position();
580 let mut entry_header = Vec::new();
581 write_entry_header(
582 &mut entry_header,
583 object.object_type,
584 object.body.len() as u64,
585 );
586 let mut crc32 = crc32fast::Hasher::new();
587 crc32.update(&entry_header);
588 crc32.update(compressed_payload);
589 output.write_pack_bytes(&entry_header)?;
590 output.write_pack_bytes(compressed_payload)?;
591 index_entries.push(PackIndexEntry {
592 oid: *oid,
593 crc32: crc32.finalize(),
594 offset,
595 });
596 }
597 }
598
599 let (checksum, pack_size) = output.finish()?;
600 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
601 Ok(PackWriteSummary {
602 index,
603 checksum,
604 entries: index_entries,
605 delta_count: 0,
606 pack_size,
607 })
608 }
609
610 pub fn write_packed_from_source_to_writer<W, F>(
611 object_ids: &[ObjectId],
612 format: ObjectFormat,
613 options: &PackWriteOptions,
614 read_object: F,
615 writer: &mut W,
616 ) -> Result<PackWriteSummary>
617 where
618 W: Write,
619 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
620 {
621 Self::write_packed_from_source_to_writer_with_cancel(
622 object_ids,
623 format,
624 options,
625 read_object,
626 writer,
627 CancelFlag::never(),
628 )
629 }
630
631 pub fn write_packed_from_source_to_writer_with_cancel<W, F>(
634 object_ids: &[ObjectId],
635 format: ObjectFormat,
636 options: &PackWriteOptions,
637 mut read_object: F,
638 writer: &mut W,
639 cancel: CancelFlag<'_>,
640 ) -> Result<PackWriteSummary>
641 where
642 W: Write,
643 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
644 {
645 if object_ids.len() > u32::MAX as usize {
646 return Err(GitError::InvalidFormat("too many pack objects".into()));
647 }
648
649 let mut seen = HashSet::with_capacity(object_ids.len());
650 for oid in object_ids {
651 if oid.format() != format {
652 return Err(GitError::InvalidObjectId(
653 "pack object id format does not match pack format".into(),
654 ));
655 }
656 if !seen.insert(*oid) {
657 return Err(GitError::InvalidFormat(format!(
658 "pack contains duplicate object id {oid}"
659 )));
660 }
661 }
662
663 for oid in options.thin_bases.keys() {
664 if oid.format() != format {
665 return Err(GitError::InvalidObjectId(
666 "thin pack base object id format does not match pack format".into(),
667 ));
668 }
669 }
670
671 let mut output = PackDigestWriter::new(writer, format);
672 output.write_pack_bytes(b"PACK")?;
673 output.write_pack_bytes(&2u32.to_be_bytes())?;
674 output.write_pack_bytes(&(object_ids.len() as u32).to_be_bytes())?;
675
676 let mut index_entries = Vec::with_capacity(object_ids.len());
677 let mut delta_count = 0u32;
678 let mut base_horizon: VecDeque<StreamingDeltaBase> = VecDeque::new();
679
680 for oid_window in object_ids.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
681 cancel.check()?;
682 let mut objects = Vec::with_capacity(oid_window.len());
683 for oid in oid_window {
684 objects.push(read_object(oid)?);
685 }
686
687 let (plan, order) =
688 plan_streaming_window_deltas(&objects, oid_window, &base_horizon, options);
689 let compressed_payloads = compress_streaming_planned_payloads(
690 &objects,
691 &plan,
692 &order,
693 options.compression_level,
694 )?;
695 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
696
697 for (&idx, compressed_payload) in order.iter().zip(&compressed_payloads) {
698 let offset = output.position();
699 let mut entry_header = Vec::new();
700 match &plan[idx].base {
701 StreamingPlannedBase::None => {
702 write_entry_header(
703 &mut entry_header,
704 objects[idx].object_type,
705 objects[idx].body.len() as u64,
706 );
707 }
708 StreamingPlannedBase::Current { base_idx, delta } => {
709 delta_count += 1;
710 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
711 GitError::InvalidFormat(
712 "in-pack delta base emitted after dependent object".into(),
713 )
714 })?;
715 if options.prefer_ofs_delta {
716 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
717 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
718 GitError::InvalidFormat(
719 "ofs-delta base offset is after delta".into(),
720 )
721 })?;
722 write_ofs_delta_offset(&mut entry_header, relative)?;
723 } else {
724 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
725 entry_header.extend_from_slice(oid_window[*base_idx].as_bytes());
726 }
727 }
728 StreamingPlannedBase::Previous {
729 base_oid,
730 base_offset,
731 delta,
732 } => {
733 delta_count += 1;
734 if options.prefer_ofs_delta {
735 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
736 let relative = offset.checked_sub(*base_offset).ok_or_else(|| {
737 GitError::InvalidFormat(
738 "ofs-delta base offset is after delta".into(),
739 )
740 })?;
741 write_ofs_delta_offset(&mut entry_header, relative)?;
742 } else {
743 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
744 entry_header.extend_from_slice(base_oid.as_bytes());
745 }
746 }
747 StreamingPlannedBase::External { base_oid, delta } => {
748 delta_count += 1;
749 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
750 entry_header.extend_from_slice(base_oid.as_bytes());
751 }
752 }
753
754 let mut crc32 = crc32fast::Hasher::new();
755 crc32.update(&entry_header);
756 crc32.update(compressed_payload);
757 output.write_pack_bytes(&entry_header)?;
758 output.write_pack_bytes(compressed_payload)?;
759 written_offsets[idx] = Some(offset);
760 index_entries.push(PackIndexEntry {
761 oid: oid_window[idx],
762 crc32: crc32.finalize(),
763 offset,
764 });
765
766 if options.depth > 0 && options.window > 0 {
767 base_horizon.push_back(StreamingDeltaBase {
768 oid: oid_window[idx],
769 object: Arc::clone(&objects[idx]),
770 offset,
771 depth: plan[idx].depth,
772 });
773 while base_horizon.len() > options.window {
774 base_horizon.pop_front();
775 }
776 }
777 }
778 }
779
780 let (checksum, pack_size) = output.finish()?;
781 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
782 Ok(PackWriteSummary {
783 index,
784 checksum,
785 entries: index_entries,
786 delta_count,
787 pack_size,
788 })
789 }
790}
791
792pub(crate) struct PackDigestWriter<'a, W> {
793 writer: &'a mut W,
794 digest: StreamingDigest,
795 position: u64,
796}
797
798impl<'a, W> PackDigestWriter<'a, W>
799where
800 W: Write,
801{
802 pub(crate) fn new(writer: &'a mut W, format: ObjectFormat) -> Self {
803 Self {
804 writer,
805 digest: StreamingDigest::new(format),
806 position: 0,
807 }
808 }
809
810 pub(crate) fn position(&self) -> u64 {
811 self.position
812 }
813
814 pub(crate) fn write_pack_bytes(&mut self, bytes: &[u8]) -> Result<()> {
815 self.writer.write_all(bytes)?;
816 self.digest.update(bytes);
817 self.position = self
818 .position
819 .checked_add(bytes.len() as u64)
820 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
821 Ok(())
822 }
823
824 pub(crate) fn finish(mut self) -> Result<(ObjectId, u64)> {
825 let checksum = self.digest.finalize()?;
826 self.writer.write_all(checksum.as_bytes())?;
827 self.position = self
828 .position
829 .checked_add(checksum.as_bytes().len() as u64)
830 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
831 Ok((checksum, self.position))
832 }
833}
834pub(crate) fn compress_planned_payloads(
835 objects: &[&EncodedObject],
836 plan: &[PlannedEntry],
837 order: &[usize],
838 compression_level: u32,
839) -> Result<Vec<Vec<u8>>> {
840 if order.is_empty() {
841 return Ok(Vec::new());
842 }
843
844 let worker_count = std::thread::available_parallelism()
845 .map(|threads| threads.get())
846 .unwrap_or(1)
847 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
848 .min(order.len());
849 if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
850 let mut payloads = Vec::with_capacity(order.len());
851 for &idx in order {
852 payloads.push(compressed_payload(
853 planned_payload(objects, plan, idx),
854 compression_level,
855 )?);
856 }
857 return Ok(payloads);
858 }
859
860 let chunk_len = order.len().div_ceil(worker_count);
861 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
862 std::thread::scope(|scope| {
863 let mut handles = Vec::new();
864 for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
865 let chunk_start = chunk_idx * chunk_len;
866 handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
867 let mut chunk_payloads = Vec::with_capacity(chunk.len());
868 for (offset, &idx) in chunk.iter().enumerate() {
869 chunk_payloads.push((
870 chunk_start + offset,
871 compressed_payload(planned_payload(objects, plan, idx), compression_level)?,
872 ));
873 }
874 Ok(chunk_payloads)
875 }));
876 }
877
878 let mut first_error = None;
879 for handle in handles {
880 match handle.join() {
881 Ok(Ok(chunk_payloads)) => {
882 if first_error.is_none() {
883 for (pos, payload) in chunk_payloads {
884 payloads[pos] = payload;
885 }
886 }
887 }
888 Ok(Err(err)) => {
889 first_error.get_or_insert(err);
890 }
891 Err(_) => {
892 first_error.get_or_insert_with(|| {
893 GitError::InvalidObject("pack compression worker panicked".into())
894 });
895 }
896 }
897 }
898
899 match first_error {
900 Some(err) => Err(err),
901 None => Ok(()),
902 }
903 })?;
904 Ok(payloads)
905}
906
907pub(crate) fn compress_streaming_planned_payloads(
908 objects: &[Arc<EncodedObject>],
909 plan: &[StreamingPlannedEntry],
910 order: &[usize],
911 compression_level: u32,
912) -> Result<Vec<Vec<u8>>> {
913 if order.is_empty() {
914 return Ok(Vec::new());
915 }
916
917 let worker_count = std::thread::available_parallelism()
918 .map(|threads| threads.get())
919 .unwrap_or(1)
920 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
921 .min(order.len());
922 if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
923 let mut payloads = Vec::with_capacity(order.len());
924 for &idx in order {
925 payloads.push(compressed_payload(
926 streaming_planned_payload(objects, plan, idx),
927 compression_level,
928 )?);
929 }
930 return Ok(payloads);
931 }
932
933 let chunk_len = order.len().div_ceil(worker_count);
934 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
935 std::thread::scope(|scope| {
936 let mut handles = Vec::new();
937 for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
938 let chunk_start = chunk_idx * chunk_len;
939 handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
940 let mut chunk_payloads = Vec::with_capacity(chunk.len());
941 for (offset, &idx) in chunk.iter().enumerate() {
942 chunk_payloads.push((
943 chunk_start + offset,
944 compressed_payload(
945 streaming_planned_payload(objects, plan, idx),
946 compression_level,
947 )?,
948 ));
949 }
950 Ok(chunk_payloads)
951 }));
952 }
953
954 let mut first_error = None;
955 for handle in handles {
956 match handle.join() {
957 Ok(Ok(chunk_payloads)) => {
958 if first_error.is_none() {
959 for (pos, payload) in chunk_payloads {
960 payloads[pos] = payload;
961 }
962 }
963 }
964 Ok(Err(err)) => {
965 first_error.get_or_insert(err);
966 }
967 Err(_) => {
968 first_error.get_or_insert_with(|| {
969 GitError::InvalidObject("pack compression worker panicked".into())
970 });
971 }
972 }
973 }
974
975 match first_error {
976 Some(err) => Err(err),
977 None => Ok(()),
978 }
979 })?;
980 Ok(payloads)
981}
982
983pub(crate) fn compress_undeltified_payloads(
984 objects: &[Arc<EncodedObject>],
985 compression_level: u32,
986) -> Result<Vec<Vec<u8>>> {
987 if objects.is_empty() {
988 return Ok(Vec::new());
989 }
990
991 let worker_count = std::thread::available_parallelism()
992 .map(|threads| threads.get())
993 .unwrap_or(1)
994 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
995 .min(objects.len());
996 if worker_count <= 1 || objects.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
997 let mut payloads = Vec::with_capacity(objects.len());
998 for object in objects {
999 payloads.push(compressed_payload(&object.body, compression_level)?);
1000 }
1001 return Ok(payloads);
1002 }
1003
1004 let chunk_len = objects.len().div_ceil(worker_count);
1005 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new)
1006 .take(objects.len())
1007 .collect();
1008 std::thread::scope(|scope| {
1009 let mut handles = Vec::new();
1010 for (chunk_idx, chunk) in objects.chunks(chunk_len).enumerate() {
1011 let chunk_start = chunk_idx * chunk_len;
1012 handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
1013 let mut chunk_payloads = Vec::with_capacity(chunk.len());
1014 for (offset, object) in chunk.iter().enumerate() {
1015 chunk_payloads.push((
1016 chunk_start + offset,
1017 compressed_payload(&object.body, compression_level)?,
1018 ));
1019 }
1020 Ok(chunk_payloads)
1021 }));
1022 }
1023
1024 let mut first_error = None;
1025 for handle in handles {
1026 match handle.join() {
1027 Ok(Ok(chunk_payloads)) => {
1028 if first_error.is_none() {
1029 for (pos, payload) in chunk_payloads {
1030 payloads[pos] = payload;
1031 }
1032 }
1033 }
1034 Ok(Err(err)) => {
1035 first_error.get_or_insert(err);
1036 }
1037 Err(_) => {
1038 first_error.get_or_insert_with(|| {
1039 GitError::InvalidObject("pack compression worker panicked".into())
1040 });
1041 }
1042 }
1043 }
1044
1045 match first_error {
1046 Some(err) => Err(err),
1047 None => Ok(()),
1048 }
1049 })?;
1050 Ok(payloads)
1051}
1052
1053pub(crate) fn streaming_planned_payload<'a>(
1054 objects: &'a [Arc<EncodedObject>],
1055 plan: &'a [StreamingPlannedEntry],
1056 idx: usize,
1057) -> &'a [u8] {
1058 match &plan[idx].base {
1059 StreamingPlannedBase::None => &objects[idx].body,
1060 StreamingPlannedBase::Current { delta, .. }
1061 | StreamingPlannedBase::Previous { delta, .. }
1062 | StreamingPlannedBase::External { delta, .. } => delta,
1063 }
1064}
1065
1066pub(crate) fn planned_payload<'a>(
1067 objects: &'a [&'a EncodedObject],
1068 plan: &'a [PlannedEntry],
1069 idx: usize,
1070) -> &'a [u8] {
1071 match &plan[idx].base {
1072 PlannedBase::None => &objects[idx].body,
1073 PlannedBase::InPack { delta, .. } | PlannedBase::External { delta, .. } => delta,
1074 }
1075}
1076
1077pub(crate) fn compressed_payload(body: &[u8], compression_level: u32) -> Result<Vec<u8>> {
1078 let mut out = Vec::new();
1079 write_compressed_payload(&mut out, body, compression_level)?;
1080 Ok(out)
1081}
1082pub(crate) fn write_compressed_payload(
1083 out: &mut Vec<u8>,
1084 body: &[u8],
1085 compression_level: u32,
1086) -> Result<()> {
1087 let mut compressor = Compress::new(Compression::new(compression_level.min(9)), true);
1088 out.reserve(zlib_compress_bound(body.len()));
1089 let status = compressor
1090 .compress_vec(body, out, FlushCompress::Finish)
1091 .map_err(|err| GitError::InvalidObject(format!("zlib compression failed: {err}")))?;
1092 if status != Status::StreamEnd || compressor.total_in() != body.len() as u64 {
1093 return Err(GitError::InvalidObject(
1094 "zlib compression did not finish pack entry".into(),
1095 ));
1096 }
1097 Ok(())
1098}
1099
1100pub(crate) fn zlib_compress_bound(len: usize) -> usize {
1101 len.saturating_add(len >> 12)
1102 .saturating_add(len >> 14)
1103 .saturating_add(len >> 25)
1104 .saturating_add(13)
1105}
1106
1107pub(crate) fn write_entry_header(out: &mut Vec<u8>, object_type: ObjectType, size: u64) {
1108 let type_code = match object_type {
1109 ObjectType::Commit => 1,
1110 ObjectType::Tree => 2,
1111 ObjectType::Blob => 3,
1112 ObjectType::Tag => 4,
1113 };
1114 write_pack_entry_header_kind(out, type_code, size);
1115}
1116
1117pub(crate) fn write_pack_entry_header_kind(out: &mut Vec<u8>, type_code: u8, mut size: u64) {
1118 let mut byte = (type_code << 4) | ((size as u8) & 0x0f);
1119 size >>= 4;
1120 if size != 0 {
1121 byte |= 0x80;
1122 }
1123 out.push(byte);
1124 while size != 0 {
1125 let mut byte = (size as u8) & 0x7f;
1126 size >>= 7;
1127 if size != 0 {
1128 byte |= 0x80;
1129 }
1130 out.push(byte);
1131 }
1132}
1133
1134pub(crate) fn write_ofs_delta_offset(out: &mut Vec<u8>, relative: u64) -> Result<()> {
1135 if relative == 0 {
1136 return Err(GitError::InvalidFormat(
1137 "ofs-delta relative offset cannot be zero".into(),
1138 ));
1139 }
1140 let mut value = relative;
1141 let mut bytes = vec![(value & 0x7f) as u8];
1142 value >>= 7;
1143 while value != 0 {
1144 value -= 1;
1145 bytes.push(((value & 0x7f) as u8) | 0x80);
1146 value >>= 7;
1147 }
1148 bytes.reverse();
1149 out.extend_from_slice(&bytes);
1150 Ok(())
1151}
1152#[derive(Debug, Clone)]
1165pub struct PackBitmapWriter {
1166 format: ObjectFormat,
1167 pack_checksum: ObjectId,
1168 object_count: u32,
1169 commit_positions: Vec<u32>,
1170 tree_positions: Vec<u32>,
1171 blob_positions: Vec<u32>,
1172 tag_positions: Vec<u32>,
1173 name_hash_cache: Option<Vec<u32>>,
1174 write_lookup_table: bool,
1175 selected: Vec<SelectedCommit>,
1176 pseudo_merges: Vec<PackBitmapPseudoMerge>,
1177}
1178
1179#[derive(Debug, Clone)]
1180pub(crate) struct SelectedCommit {
1181 commit_index_position: u32,
1185 flags: u8,
1186 reachable: Vec<u32>,
1187}
1188
1189impl PackBitmapWriter {
1190 pub const FLAG_NONE: u8 = 0;
1194
1195 pub fn new(
1202 format: ObjectFormat,
1203 pack_checksum: ObjectId,
1204 object_types: &[ObjectType],
1205 ) -> Result<Self> {
1206 if object_types.len() > u32::MAX as usize {
1207 return Err(GitError::InvalidFormat(
1208 "too many objects for a pack bitmap".into(),
1209 ));
1210 }
1211 if pack_checksum.format() != format {
1212 return Err(GitError::InvalidObjectId(
1213 "pack checksum format does not match bitmap format".into(),
1214 ));
1215 }
1216 let object_count = object_types.len() as u32;
1217 let mut commit_positions = Vec::new();
1218 let mut tree_positions = Vec::new();
1219 let mut blob_positions = Vec::new();
1220 let mut tag_positions = Vec::new();
1221 for (index, object_type) in object_types.iter().enumerate() {
1222 let position = index as u32;
1223 match object_type {
1224 ObjectType::Commit => commit_positions.push(position),
1225 ObjectType::Tree => tree_positions.push(position),
1226 ObjectType::Blob => blob_positions.push(position),
1227 ObjectType::Tag => tag_positions.push(position),
1228 }
1229 }
1230 Ok(Self {
1231 format,
1232 pack_checksum,
1233 object_count,
1234 commit_positions,
1235 tree_positions,
1236 blob_positions,
1237 tag_positions,
1238 name_hash_cache: None,
1239 write_lookup_table: false,
1240 selected: Vec::new(),
1241 pseudo_merges: Vec::new(),
1242 })
1243 }
1244
1245 pub fn with_name_hash_cache(mut self, cache: Vec<u32>) -> Result<Self> {
1251 if cache.len() != self.object_count as usize {
1252 return Err(GitError::InvalidFormat(format!(
1253 "name hash cache has {} entries but pack has {} objects",
1254 cache.len(),
1255 self.object_count
1256 )));
1257 }
1258 self.name_hash_cache = Some(cache);
1259 Ok(self)
1260 }
1261
1262 pub fn with_lookup_table(mut self, enabled: bool) -> Self {
1265 self.write_lookup_table = enabled;
1266 self
1267 }
1268
1269 pub fn add_commit(
1281 &mut self,
1282 commit_position: u32,
1283 commit_index_position: u32,
1284 reachable: &[u32],
1285 ) -> Result<()> {
1286 if commit_position >= self.object_count {
1287 return Err(GitError::InvalidFormat(format!(
1288 "commit position {commit_position} out of range for {} objects",
1289 self.object_count
1290 )));
1291 }
1292 if commit_index_position >= self.object_count {
1293 return Err(GitError::InvalidFormat(format!(
1294 "commit index position {commit_index_position} out of range for {} objects",
1295 self.object_count
1296 )));
1297 }
1298 if !self.commit_positions.contains(&commit_position) {
1299 return Err(GitError::InvalidFormat(format!(
1300 "bitmap commit position {commit_position} is not a commit object"
1301 )));
1302 }
1303 for &position in reachable {
1304 if position >= self.object_count {
1305 return Err(GitError::InvalidFormat(format!(
1306 "reachable position {position} out of range for {} objects",
1307 self.object_count
1308 )));
1309 }
1310 }
1311 let mut reachable = reachable.to_vec();
1312 reachable.push(commit_position);
1313 self.selected.push(SelectedCommit {
1314 commit_index_position,
1315 flags: Self::FLAG_NONE,
1316 reachable,
1317 });
1318 Ok(())
1319 }
1320
1321 pub fn add_pseudo_merge(&mut self, commits: &[u32], reachable: &[u32]) -> Result<()> {
1326 if commits.is_empty() {
1327 return Err(GitError::InvalidFormat(
1328 "pseudo-merge must contain at least one commit".into(),
1329 ));
1330 }
1331 for &position in commits {
1332 if position >= self.object_count {
1333 return Err(GitError::InvalidFormat(format!(
1334 "pseudo-merge commit position {position} out of range for {} objects",
1335 self.object_count
1336 )));
1337 }
1338 if !self.commit_positions.contains(&position) {
1339 return Err(GitError::InvalidFormat(format!(
1340 "pseudo-merge commit position {position} is not a commit object"
1341 )));
1342 }
1343 }
1344 for &position in reachable {
1345 if position >= self.object_count {
1346 return Err(GitError::InvalidFormat(format!(
1347 "pseudo-merge reachable position {position} out of range for {} objects",
1348 self.object_count
1349 )));
1350 }
1351 }
1352 self.pseudo_merges.push(PackBitmapPseudoMerge {
1353 commits: EwahBitmap::from_positions(self.object_count, commits)?,
1354 bitmap: EwahBitmap::from_positions(self.object_count, reachable)?,
1355 });
1356 Ok(())
1357 }
1358
1359 pub fn build(&self) -> Result<PackBitmapIndex> {
1366 let commits = EwahBitmap::from_positions(self.object_count, &self.commit_positions)?;
1367 let trees = EwahBitmap::from_positions(self.object_count, &self.tree_positions)?;
1368 let blobs = EwahBitmap::from_positions(self.object_count, &self.blob_positions)?;
1369 let tags = EwahBitmap::from_positions(self.object_count, &self.tag_positions)?;
1370
1371 let mut entries = Vec::with_capacity(self.selected.len());
1372 for selected in &self.selected {
1373 let bitmap = EwahBitmap::from_positions(self.object_count, &selected.reachable)?;
1374 entries.push(PackBitmapEntry {
1375 object_position: selected.commit_index_position,
1376 xor_offset: 0,
1377 flags: selected.flags,
1378 bitmap,
1379 });
1380 }
1381
1382 let mut options = PackBitmapIndex::OPTION_FULL_DAG;
1383 if self.name_hash_cache.is_some() {
1384 options |= PackBitmapIndex::OPTION_HASH_CACHE;
1385 }
1386 if !self.pseudo_merges.is_empty() {
1387 options |= PackBitmapIndex::OPTION_PSEUDO_MERGES;
1388 }
1389 if self.write_lookup_table {
1390 options |= PackBitmapIndex::OPTION_LOOKUP_TABLE;
1391 }
1392
1393 let placeholder_checksum = ObjectId::null(self.format);
1398 Ok(PackBitmapIndex {
1399 version: 1,
1400 format: self.format,
1401 options,
1402 pack_checksum: self.pack_checksum.clone(),
1403 index_checksum: placeholder_checksum,
1404 type_bitmaps: PackBitmapTypeBitmaps {
1405 commits,
1406 trees,
1407 blobs,
1408 tags,
1409 },
1410 entries,
1411 pseudo_merges: self.pseudo_merges.clone(),
1412 lookup_table: self.write_lookup_table,
1413 name_hash_cache: self.name_hash_cache.clone(),
1414 })
1415 }
1416
1417 pub fn write(&self) -> Result<Vec<u8>> {
1420 self.build()?.write()
1421 }
1422}
1423
1424impl PackBitmapIndex {
1425 pub fn write(&self) -> Result<Vec<u8>> {
1439 if self.version != 1 {
1440 return Err(GitError::Unsupported(format!(
1441 "bitmap index version {}",
1442 self.version
1443 )));
1444 }
1445 let mut options = self.options;
1446 if !self.pseudo_merges.is_empty() {
1447 options |= Self::OPTION_PSEUDO_MERGES;
1448 }
1449 if self.lookup_table {
1450 options |= Self::OPTION_LOOKUP_TABLE;
1451 }
1452 let known_options = Self::OPTION_FULL_DAG
1453 | Self::OPTION_HASH_CACHE
1454 | Self::OPTION_LOOKUP_TABLE
1455 | Self::OPTION_PSEUDO_MERGES;
1456 if options & !known_options != 0 {
1457 return Err(GitError::Unsupported(format!(
1458 "bitmap index options {:#06x}",
1459 options & !known_options
1460 )));
1461 }
1462 if self.pack_checksum.format() != self.format {
1463 return Err(GitError::InvalidObjectId(
1464 "bitmap pack checksum format does not match index format".into(),
1465 ));
1466 }
1467 if self.entries.len() > u32::MAX as usize {
1468 return Err(GitError::InvalidFormat(
1469 "too many bitmap index entries".into(),
1470 ));
1471 }
1472 if options & Self::OPTION_PSEUDO_MERGES != 0 && self.pseudo_merges.is_empty() {
1473 return Err(GitError::InvalidFormat(
1474 "OPTION_PSEUDO_MERGES set without pseudo-merge records".into(),
1475 ));
1476 }
1477 let want_cache = options & Self::OPTION_HASH_CACHE != 0;
1478 match (&self.name_hash_cache, want_cache) {
1479 (Some(_), false) => {
1480 return Err(GitError::InvalidFormat(
1481 "name hash cache present without OPTION_HASH_CACHE".into(),
1482 ));
1483 }
1484 (None, true) => {
1485 return Err(GitError::InvalidFormat(
1486 "OPTION_HASH_CACHE set without a name hash cache".into(),
1487 ));
1488 }
1489 _ => {}
1490 }
1491
1492 let mut out = Vec::new();
1493 out.extend_from_slice(b"BITM");
1494 out.extend_from_slice(&self.version.to_be_bytes());
1495 out.extend_from_slice(&options.to_be_bytes());
1496 out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1497 out.extend_from_slice(self.pack_checksum.as_bytes());
1498
1499 self.type_bitmaps.commits.append_bytes(&mut out);
1500 self.type_bitmaps.trees.append_bytes(&mut out);
1501 self.type_bitmaps.blobs.append_bytes(&mut out);
1502 self.type_bitmaps.tags.append_bytes(&mut out);
1503
1504 let mut entry_offsets = Vec::with_capacity(self.entries.len());
1505 for (idx, entry) in self.entries.iter().enumerate() {
1506 if entry.xor_offset as usize > idx {
1507 return Err(GitError::InvalidFormat(
1508 "bitmap index entry has invalid XOR offset".into(),
1509 ));
1510 }
1511 entry_offsets.push(out.len() as u64);
1512 out.extend_from_slice(&entry.object_position.to_be_bytes());
1513 out.push(entry.xor_offset);
1514 out.push(entry.flags);
1515 entry.bitmap.append_bytes(&mut out);
1516 }
1517
1518 if !self.pseudo_merges.is_empty() {
1519 append_bitmap_pseudo_merges(&mut out, &self.pseudo_merges)?;
1520 }
1521
1522 if self.lookup_table {
1523 append_bitmap_lookup_table(&mut out, &self.entries, &entry_offsets)?;
1524 }
1525
1526 if let Some(cache) = &self.name_hash_cache {
1527 for value in cache {
1528 out.extend_from_slice(&value.to_be_bytes());
1529 }
1530 }
1531
1532 let checksum = sley_core::digest_bytes(self.format, &out)?;
1533 out.extend_from_slice(checksum.as_bytes());
1534 Ok(out)
1535 }
1536}
1537
1538fn append_bitmap_lookup_table(
1539 out: &mut Vec<u8>,
1540 entries: &[PackBitmapEntry],
1541 entry_offsets: &[u64],
1542) -> Result<()> {
1543 if entries.len() != entry_offsets.len() {
1544 return Err(GitError::InvalidFormat(
1545 "bitmap lookup table offset count mismatch".into(),
1546 ));
1547 }
1548 let mut table: Vec<usize> = (0..entries.len()).collect();
1549 table.sort_by_key(|&index| entries[index].object_position);
1550 let mut inverse = vec![0u32; entries.len()];
1551 for (row, &entry_index) in table.iter().enumerate() {
1552 inverse[entry_index] = row as u32;
1553 }
1554 for &entry_index in &table {
1555 let entry = &entries[entry_index];
1556 let xor_row = if entry.xor_offset == 0 {
1557 u32::MAX
1558 } else {
1559 let base = entry_index
1560 .checked_sub(entry.xor_offset as usize)
1561 .ok_or_else(|| {
1562 GitError::InvalidFormat("bitmap lookup table XOR base underflow".into())
1563 })?;
1564 inverse[base]
1565 };
1566 out.extend_from_slice(&entry.object_position.to_be_bytes());
1567 out.extend_from_slice(&entry_offsets[entry_index].to_be_bytes());
1568 out.extend_from_slice(&xor_row.to_be_bytes());
1569 }
1570 Ok(())
1571}
1572
1573pub(crate) fn append_bitmap_pseudo_merges(
1574 out: &mut Vec<u8>,
1575 pseudo_merges: &[PackBitmapPseudoMerge],
1576) -> Result<()> {
1577 if pseudo_merges.len() > u32::MAX as usize {
1578 return Err(GitError::InvalidFormat(
1579 "too many pseudo-merge bitmap records".into(),
1580 ));
1581 }
1582 let start = out.len();
1583 let mut pseudo_offsets = Vec::with_capacity(pseudo_merges.len());
1584 let mut commit_to_offsets: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
1585 for merge in pseudo_merges {
1586 let offset = u64::try_from(out.len())
1587 .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
1588 pseudo_offsets.push(offset);
1589 for commit_pos in merge.commits.to_positions()? {
1590 commit_to_offsets
1591 .entry(commit_pos)
1592 .or_default()
1593 .push(offset);
1594 }
1595 merge.commits.append_bytes(out);
1596 merge.bitmap.append_bytes(out);
1597 }
1598 if commit_to_offsets.len() > u32::MAX as usize {
1599 return Err(GitError::InvalidFormat(
1600 "too many pseudo-merge commits".into(),
1601 ));
1602 }
1603
1604 let lookup_start = out.len();
1605 let lookup_len = commit_to_offsets
1606 .len()
1607 .checked_mul(12)
1608 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?;
1609 let mut next_extended = u64::try_from(
1610 lookup_start
1611 .checked_add(lookup_len)
1612 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?,
1613 )
1614 .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
1615 let mut rows = Vec::with_capacity(commit_to_offsets.len());
1616 for (commit_pos, offsets) in commit_to_offsets {
1617 let extended_offset = if offsets.len() > 1 {
1618 if next_extended & (1u64 << 63) != 0 {
1619 return Err(GitError::InvalidFormat(
1620 "pseudo-merge extended offset overflow".into(),
1621 ));
1622 }
1623 let offset = next_extended;
1624 let ext_len = offsets
1625 .len()
1626 .checked_mul(8)
1627 .and_then(|len| len.checked_add(4))
1628 .ok_or_else(|| {
1629 GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
1630 })?;
1631 next_extended = next_extended.checked_add(ext_len as u64).ok_or_else(|| {
1632 GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
1633 })?;
1634 Some(offset)
1635 } else {
1636 None
1637 };
1638 rows.push((commit_pos, offsets, extended_offset));
1639 }
1640
1641 for (commit_pos, offsets, extended_offset) in &rows {
1642 out.extend_from_slice(&commit_pos.to_be_bytes());
1643 match extended_offset {
1644 Some(offset) => out.extend_from_slice(&(offset | (1u64 << 63)).to_be_bytes()),
1645 None => out.extend_from_slice(&offsets[0].to_be_bytes()),
1646 }
1647 }
1648
1649 for (_commit_pos, offsets, extended_offset) in &rows {
1650 if extended_offset.is_none() {
1651 continue;
1652 }
1653 let count = u32::try_from(offsets.len())
1654 .map_err(|_| GitError::InvalidFormat("pseudo-merge extended lookup overflow".into()))?;
1655 out.extend_from_slice(&count.to_be_bytes());
1656 for offset in offsets {
1657 out.extend_from_slice(&offset.to_be_bytes());
1658 }
1659 }
1660
1661 for offset in &pseudo_offsets {
1662 out.extend_from_slice(&offset.to_be_bytes());
1663 }
1664 out.extend_from_slice(&(pseudo_merges.len() as u32).to_be_bytes());
1665 out.extend_from_slice(&(rows.len() as u32).to_be_bytes());
1666 let lookup_relative = lookup_start
1667 .checked_sub(start)
1668 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup underflow".into()))?;
1669 out.extend_from_slice(&(lookup_relative as u64).to_be_bytes());
1670 let extension_size = out
1671 .len()
1672 .checked_sub(start)
1673 .and_then(|len| len.checked_add(8))
1674 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge extension overflow".into()))?;
1675 out.extend_from_slice(&(extension_size as u64).to_be_bytes());
1676 Ok(())
1677}
1678
1679pub fn write_bitmap(
1688 format: ObjectFormat,
1689 pack_checksum: ObjectId,
1690 object_types: &[ObjectType],
1691 commits: &[(u32, u32, Vec<u32>)],
1692 name_hash_cache: Option<Vec<u32>>,
1693) -> Result<Vec<u8>> {
1694 let mut writer = PackBitmapWriter::new(format, pack_checksum, object_types)?;
1695 if let Some(cache) = name_hash_cache {
1696 writer = writer.with_name_hash_cache(cache)?;
1697 }
1698 for (commit_position, commit_index_position, reachable) in commits {
1699 writer.add_commit(*commit_position, *commit_index_position, reachable)?;
1700 }
1701 writer.write()
1702}