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