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 mut read_object: F,
519 writer: &mut W,
520 ) -> Result<PackWriteSummary>
521 where
522 W: Write,
523 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
524 {
525 let mut seen = HashSet::with_capacity(object_ids.len());
526 for oid in object_ids {
527 if oid.format() != format {
528 return Err(GitError::InvalidObjectId(
529 "pack object id format does not match pack format".into(),
530 ));
531 }
532 if !seen.insert(oid) {
533 return Err(GitError::InvalidFormat(format!(
534 "pack contains duplicate object id {oid}"
535 )));
536 }
537 }
538
539 let mut output = PackDigestWriter::new(writer, format);
540 output.write_pack_bytes(b"PACK")?;
541 output.write_pack_bytes(&2u32.to_be_bytes())?;
542 output.write_pack_bytes(&(object_ids.len() as u32).to_be_bytes())?;
543
544 let mut index_entries = Vec::with_capacity(object_ids.len());
545 for oid_window in object_ids.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
546 let mut objects = Vec::with_capacity(oid_window.len());
547 for oid in oid_window {
548 objects.push(read_object(oid)?);
549 }
550 let compressed_payloads =
551 compress_undeltified_payloads(&objects, options.compression_level)?;
552 for ((oid, object), compressed_payload) in
553 oid_window.iter().zip(&objects).zip(&compressed_payloads)
554 {
555 let offset = output.position();
556 let mut entry_header = Vec::new();
557 write_entry_header(
558 &mut entry_header,
559 object.object_type,
560 object.body.len() as u64,
561 );
562 let mut crc32 = crc32fast::Hasher::new();
563 crc32.update(&entry_header);
564 crc32.update(compressed_payload);
565 output.write_pack_bytes(&entry_header)?;
566 output.write_pack_bytes(compressed_payload)?;
567 index_entries.push(PackIndexEntry {
568 oid: *oid,
569 crc32: crc32.finalize(),
570 offset,
571 });
572 }
573 }
574
575 let (checksum, pack_size) = output.finish()?;
576 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
577 Ok(PackWriteSummary {
578 index,
579 checksum,
580 entries: index_entries,
581 delta_count: 0,
582 pack_size,
583 })
584 }
585
586 pub fn write_packed_from_source_to_writer<W, F>(
587 object_ids: &[ObjectId],
588 format: ObjectFormat,
589 options: &PackWriteOptions,
590 mut read_object: F,
591 writer: &mut W,
592 ) -> Result<PackWriteSummary>
593 where
594 W: Write,
595 F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
596 {
597 if object_ids.len() > u32::MAX as usize {
598 return Err(GitError::InvalidFormat("too many pack objects".into()));
599 }
600
601 let mut seen = HashSet::with_capacity(object_ids.len());
602 for oid in object_ids {
603 if oid.format() != format {
604 return Err(GitError::InvalidObjectId(
605 "pack object id format does not match pack format".into(),
606 ));
607 }
608 if !seen.insert(*oid) {
609 return Err(GitError::InvalidFormat(format!(
610 "pack contains duplicate object id {oid}"
611 )));
612 }
613 }
614
615 for oid in options.thin_bases.keys() {
616 if oid.format() != format {
617 return Err(GitError::InvalidObjectId(
618 "thin pack base object id format does not match pack format".into(),
619 ));
620 }
621 }
622
623 let mut output = PackDigestWriter::new(writer, format);
624 output.write_pack_bytes(b"PACK")?;
625 output.write_pack_bytes(&2u32.to_be_bytes())?;
626 output.write_pack_bytes(&(object_ids.len() as u32).to_be_bytes())?;
627
628 let mut index_entries = Vec::with_capacity(object_ids.len());
629 let mut delta_count = 0u32;
630 let mut base_horizon: VecDeque<StreamingDeltaBase> = VecDeque::new();
631
632 for oid_window in object_ids.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
633 let mut objects = Vec::with_capacity(oid_window.len());
634 for oid in oid_window {
635 objects.push(read_object(oid)?);
636 }
637
638 let (plan, order) =
639 plan_streaming_window_deltas(&objects, oid_window, &base_horizon, options);
640 let compressed_payloads = compress_streaming_planned_payloads(
641 &objects,
642 &plan,
643 &order,
644 options.compression_level,
645 )?;
646 let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
647
648 for (&idx, compressed_payload) in order.iter().zip(&compressed_payloads) {
649 let offset = output.position();
650 let mut entry_header = Vec::new();
651 match &plan[idx].base {
652 StreamingPlannedBase::None => {
653 write_entry_header(
654 &mut entry_header,
655 objects[idx].object_type,
656 objects[idx].body.len() as u64,
657 );
658 }
659 StreamingPlannedBase::Current { base_idx, delta } => {
660 delta_count += 1;
661 let base_offset = written_offsets[*base_idx].ok_or_else(|| {
662 GitError::InvalidFormat(
663 "in-pack delta base emitted after dependent object".into(),
664 )
665 })?;
666 if options.prefer_ofs_delta {
667 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
668 let relative = offset.checked_sub(base_offset).ok_or_else(|| {
669 GitError::InvalidFormat(
670 "ofs-delta base offset is after delta".into(),
671 )
672 })?;
673 write_ofs_delta_offset(&mut entry_header, relative)?;
674 } else {
675 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
676 entry_header.extend_from_slice(oid_window[*base_idx].as_bytes());
677 }
678 }
679 StreamingPlannedBase::Previous {
680 base_oid,
681 base_offset,
682 delta,
683 } => {
684 delta_count += 1;
685 if options.prefer_ofs_delta {
686 write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
687 let relative = offset.checked_sub(*base_offset).ok_or_else(|| {
688 GitError::InvalidFormat(
689 "ofs-delta base offset is after delta".into(),
690 )
691 })?;
692 write_ofs_delta_offset(&mut entry_header, relative)?;
693 } else {
694 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
695 entry_header.extend_from_slice(base_oid.as_bytes());
696 }
697 }
698 StreamingPlannedBase::External { base_oid, delta } => {
699 delta_count += 1;
700 write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
701 entry_header.extend_from_slice(base_oid.as_bytes());
702 }
703 }
704
705 let mut crc32 = crc32fast::Hasher::new();
706 crc32.update(&entry_header);
707 crc32.update(compressed_payload);
708 output.write_pack_bytes(&entry_header)?;
709 output.write_pack_bytes(compressed_payload)?;
710 written_offsets[idx] = Some(offset);
711 index_entries.push(PackIndexEntry {
712 oid: oid_window[idx],
713 crc32: crc32.finalize(),
714 offset,
715 });
716
717 if options.depth > 0 && options.window > 0 {
718 base_horizon.push_back(StreamingDeltaBase {
719 oid: oid_window[idx],
720 object: Arc::clone(&objects[idx]),
721 offset,
722 depth: plan[idx].depth,
723 });
724 while base_horizon.len() > options.window {
725 base_horizon.pop_front();
726 }
727 }
728 }
729 }
730
731 let (checksum, pack_size) = output.finish()?;
732 let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
733 Ok(PackWriteSummary {
734 index,
735 checksum,
736 entries: index_entries,
737 delta_count,
738 pack_size,
739 })
740 }
741}
742
743pub(crate) struct PackDigestWriter<'a, W> {
744 writer: &'a mut W,
745 digest: StreamingDigest,
746 position: u64,
747}
748
749impl<'a, W> PackDigestWriter<'a, W>
750where
751 W: Write,
752{
753 pub(crate) fn new(writer: &'a mut W, format: ObjectFormat) -> Self {
754 Self {
755 writer,
756 digest: StreamingDigest::new(format),
757 position: 0,
758 }
759 }
760
761 pub(crate) fn position(&self) -> u64 {
762 self.position
763 }
764
765 pub(crate) fn write_pack_bytes(&mut self, bytes: &[u8]) -> Result<()> {
766 self.writer.write_all(bytes)?;
767 self.digest.update(bytes);
768 self.position = self
769 .position
770 .checked_add(bytes.len() as u64)
771 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
772 Ok(())
773 }
774
775 pub(crate) fn finish(mut self) -> Result<(ObjectId, u64)> {
776 let checksum = self.digest.finalize()?;
777 self.writer.write_all(checksum.as_bytes())?;
778 self.position = self
779 .position
780 .checked_add(checksum.as_bytes().len() as u64)
781 .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
782 Ok((checksum, self.position))
783 }
784}
785pub(crate) fn compress_planned_payloads(
786 objects: &[&EncodedObject],
787 plan: &[PlannedEntry],
788 order: &[usize],
789 compression_level: u32,
790) -> Result<Vec<Vec<u8>>> {
791 if order.is_empty() {
792 return Ok(Vec::new());
793 }
794
795 let worker_count = std::thread::available_parallelism()
796 .map(|threads| threads.get())
797 .unwrap_or(1)
798 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
799 .min(order.len());
800 if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
801 let mut payloads = Vec::with_capacity(order.len());
802 for &idx in order {
803 payloads.push(compressed_payload(
804 planned_payload(objects, plan, idx),
805 compression_level,
806 )?);
807 }
808 return Ok(payloads);
809 }
810
811 let chunk_len = order.len().div_ceil(worker_count);
812 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
813 std::thread::scope(|scope| {
814 let mut handles = Vec::new();
815 for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
816 let chunk_start = chunk_idx * chunk_len;
817 handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
818 let mut chunk_payloads = Vec::with_capacity(chunk.len());
819 for (offset, &idx) in chunk.iter().enumerate() {
820 chunk_payloads.push((
821 chunk_start + offset,
822 compressed_payload(planned_payload(objects, plan, idx), compression_level)?,
823 ));
824 }
825 Ok(chunk_payloads)
826 }));
827 }
828
829 let mut first_error = None;
830 for handle in handles {
831 match handle.join() {
832 Ok(Ok(chunk_payloads)) => {
833 if first_error.is_none() {
834 for (pos, payload) in chunk_payloads {
835 payloads[pos] = payload;
836 }
837 }
838 }
839 Ok(Err(err)) => {
840 first_error.get_or_insert(err);
841 }
842 Err(_) => {
843 first_error.get_or_insert_with(|| {
844 GitError::InvalidObject("pack compression worker panicked".into())
845 });
846 }
847 }
848 }
849
850 match first_error {
851 Some(err) => Err(err),
852 None => Ok(()),
853 }
854 })?;
855 Ok(payloads)
856}
857
858pub(crate) fn compress_streaming_planned_payloads(
859 objects: &[Arc<EncodedObject>],
860 plan: &[StreamingPlannedEntry],
861 order: &[usize],
862 compression_level: u32,
863) -> Result<Vec<Vec<u8>>> {
864 if order.is_empty() {
865 return Ok(Vec::new());
866 }
867
868 let worker_count = std::thread::available_parallelism()
869 .map(|threads| threads.get())
870 .unwrap_or(1)
871 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
872 .min(order.len());
873 if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
874 let mut payloads = Vec::with_capacity(order.len());
875 for &idx in order {
876 payloads.push(compressed_payload(
877 streaming_planned_payload(objects, plan, idx),
878 compression_level,
879 )?);
880 }
881 return Ok(payloads);
882 }
883
884 let chunk_len = order.len().div_ceil(worker_count);
885 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
886 std::thread::scope(|scope| {
887 let mut handles = Vec::new();
888 for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
889 let chunk_start = chunk_idx * chunk_len;
890 handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
891 let mut chunk_payloads = Vec::with_capacity(chunk.len());
892 for (offset, &idx) in chunk.iter().enumerate() {
893 chunk_payloads.push((
894 chunk_start + offset,
895 compressed_payload(
896 streaming_planned_payload(objects, plan, idx),
897 compression_level,
898 )?,
899 ));
900 }
901 Ok(chunk_payloads)
902 }));
903 }
904
905 let mut first_error = None;
906 for handle in handles {
907 match handle.join() {
908 Ok(Ok(chunk_payloads)) => {
909 if first_error.is_none() {
910 for (pos, payload) in chunk_payloads {
911 payloads[pos] = payload;
912 }
913 }
914 }
915 Ok(Err(err)) => {
916 first_error.get_or_insert(err);
917 }
918 Err(_) => {
919 first_error.get_or_insert_with(|| {
920 GitError::InvalidObject("pack compression worker panicked".into())
921 });
922 }
923 }
924 }
925
926 match first_error {
927 Some(err) => Err(err),
928 None => Ok(()),
929 }
930 })?;
931 Ok(payloads)
932}
933
934pub(crate) fn compress_undeltified_payloads(
935 objects: &[Arc<EncodedObject>],
936 compression_level: u32,
937) -> Result<Vec<Vec<u8>>> {
938 if objects.is_empty() {
939 return Ok(Vec::new());
940 }
941
942 let worker_count = std::thread::available_parallelism()
943 .map(|threads| threads.get())
944 .unwrap_or(1)
945 .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
946 .min(objects.len());
947 if worker_count <= 1 || objects.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
948 let mut payloads = Vec::with_capacity(objects.len());
949 for object in objects {
950 payloads.push(compressed_payload(&object.body, compression_level)?);
951 }
952 return Ok(payloads);
953 }
954
955 let chunk_len = objects.len().div_ceil(worker_count);
956 let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new)
957 .take(objects.len())
958 .collect();
959 std::thread::scope(|scope| {
960 let mut handles = Vec::new();
961 for (chunk_idx, chunk) in objects.chunks(chunk_len).enumerate() {
962 let chunk_start = chunk_idx * chunk_len;
963 handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
964 let mut chunk_payloads = Vec::with_capacity(chunk.len());
965 for (offset, object) in chunk.iter().enumerate() {
966 chunk_payloads.push((
967 chunk_start + offset,
968 compressed_payload(&object.body, compression_level)?,
969 ));
970 }
971 Ok(chunk_payloads)
972 }));
973 }
974
975 let mut first_error = None;
976 for handle in handles {
977 match handle.join() {
978 Ok(Ok(chunk_payloads)) => {
979 if first_error.is_none() {
980 for (pos, payload) in chunk_payloads {
981 payloads[pos] = payload;
982 }
983 }
984 }
985 Ok(Err(err)) => {
986 first_error.get_or_insert(err);
987 }
988 Err(_) => {
989 first_error.get_or_insert_with(|| {
990 GitError::InvalidObject("pack compression worker panicked".into())
991 });
992 }
993 }
994 }
995
996 match first_error {
997 Some(err) => Err(err),
998 None => Ok(()),
999 }
1000 })?;
1001 Ok(payloads)
1002}
1003
1004pub(crate) fn streaming_planned_payload<'a>(
1005 objects: &'a [Arc<EncodedObject>],
1006 plan: &'a [StreamingPlannedEntry],
1007 idx: usize,
1008) -> &'a [u8] {
1009 match &plan[idx].base {
1010 StreamingPlannedBase::None => &objects[idx].body,
1011 StreamingPlannedBase::Current { delta, .. }
1012 | StreamingPlannedBase::Previous { delta, .. }
1013 | StreamingPlannedBase::External { delta, .. } => delta,
1014 }
1015}
1016
1017pub(crate) fn planned_payload<'a>(
1018 objects: &'a [&'a EncodedObject],
1019 plan: &'a [PlannedEntry],
1020 idx: usize,
1021) -> &'a [u8] {
1022 match &plan[idx].base {
1023 PlannedBase::None => &objects[idx].body,
1024 PlannedBase::InPack { delta, .. } | PlannedBase::External { delta, .. } => delta,
1025 }
1026}
1027
1028pub(crate) fn compressed_payload(body: &[u8], compression_level: u32) -> Result<Vec<u8>> {
1029 let mut out = Vec::new();
1030 write_compressed_payload(&mut out, body, compression_level)?;
1031 Ok(out)
1032}
1033pub(crate) fn write_compressed_payload(
1034 out: &mut Vec<u8>,
1035 body: &[u8],
1036 compression_level: u32,
1037) -> Result<()> {
1038 let mut compressor = Compress::new(Compression::new(compression_level.min(9)), true);
1039 out.reserve(zlib_compress_bound(body.len()));
1040 let status = compressor
1041 .compress_vec(body, out, FlushCompress::Finish)
1042 .map_err(|err| GitError::InvalidObject(format!("zlib compression failed: {err}")))?;
1043 if status != Status::StreamEnd || compressor.total_in() != body.len() as u64 {
1044 return Err(GitError::InvalidObject(
1045 "zlib compression did not finish pack entry".into(),
1046 ));
1047 }
1048 Ok(())
1049}
1050
1051pub(crate) fn zlib_compress_bound(len: usize) -> usize {
1052 len.saturating_add(len >> 12)
1053 .saturating_add(len >> 14)
1054 .saturating_add(len >> 25)
1055 .saturating_add(13)
1056}
1057
1058pub(crate) fn write_entry_header(out: &mut Vec<u8>, object_type: ObjectType, size: u64) {
1059 let type_code = match object_type {
1060 ObjectType::Commit => 1,
1061 ObjectType::Tree => 2,
1062 ObjectType::Blob => 3,
1063 ObjectType::Tag => 4,
1064 };
1065 write_pack_entry_header_kind(out, type_code, size);
1066}
1067
1068pub(crate) fn write_pack_entry_header_kind(out: &mut Vec<u8>, type_code: u8, mut size: u64) {
1069 let mut byte = (type_code << 4) | ((size as u8) & 0x0f);
1070 size >>= 4;
1071 if size != 0 {
1072 byte |= 0x80;
1073 }
1074 out.push(byte);
1075 while size != 0 {
1076 let mut byte = (size as u8) & 0x7f;
1077 size >>= 7;
1078 if size != 0 {
1079 byte |= 0x80;
1080 }
1081 out.push(byte);
1082 }
1083}
1084
1085pub(crate) fn write_ofs_delta_offset(out: &mut Vec<u8>, relative: u64) -> Result<()> {
1086 if relative == 0 {
1087 return Err(GitError::InvalidFormat(
1088 "ofs-delta relative offset cannot be zero".into(),
1089 ));
1090 }
1091 let mut value = relative;
1092 let mut bytes = vec![(value & 0x7f) as u8];
1093 value >>= 7;
1094 while value != 0 {
1095 value -= 1;
1096 bytes.push(((value & 0x7f) as u8) | 0x80);
1097 value >>= 7;
1098 }
1099 bytes.reverse();
1100 out.extend_from_slice(&bytes);
1101 Ok(())
1102}
1103#[derive(Debug, Clone)]
1116pub struct PackBitmapWriter {
1117 format: ObjectFormat,
1118 pack_checksum: ObjectId,
1119 object_count: u32,
1120 commit_positions: Vec<u32>,
1121 tree_positions: Vec<u32>,
1122 blob_positions: Vec<u32>,
1123 tag_positions: Vec<u32>,
1124 name_hash_cache: Option<Vec<u32>>,
1125 write_lookup_table: bool,
1126 selected: Vec<SelectedCommit>,
1127 pseudo_merges: Vec<PackBitmapPseudoMerge>,
1128}
1129
1130#[derive(Debug, Clone)]
1131pub(crate) struct SelectedCommit {
1132 commit_index_position: u32,
1136 flags: u8,
1137 reachable: Vec<u32>,
1138}
1139
1140impl PackBitmapWriter {
1141 pub const FLAG_NONE: u8 = 0;
1145
1146 pub fn new(
1153 format: ObjectFormat,
1154 pack_checksum: ObjectId,
1155 object_types: &[ObjectType],
1156 ) -> Result<Self> {
1157 if object_types.len() > u32::MAX as usize {
1158 return Err(GitError::InvalidFormat(
1159 "too many objects for a pack bitmap".into(),
1160 ));
1161 }
1162 if pack_checksum.format() != format {
1163 return Err(GitError::InvalidObjectId(
1164 "pack checksum format does not match bitmap format".into(),
1165 ));
1166 }
1167 let object_count = object_types.len() as u32;
1168 let mut commit_positions = Vec::new();
1169 let mut tree_positions = Vec::new();
1170 let mut blob_positions = Vec::new();
1171 let mut tag_positions = Vec::new();
1172 for (index, object_type) in object_types.iter().enumerate() {
1173 let position = index as u32;
1174 match object_type {
1175 ObjectType::Commit => commit_positions.push(position),
1176 ObjectType::Tree => tree_positions.push(position),
1177 ObjectType::Blob => blob_positions.push(position),
1178 ObjectType::Tag => tag_positions.push(position),
1179 }
1180 }
1181 Ok(Self {
1182 format,
1183 pack_checksum,
1184 object_count,
1185 commit_positions,
1186 tree_positions,
1187 blob_positions,
1188 tag_positions,
1189 name_hash_cache: None,
1190 write_lookup_table: false,
1191 selected: Vec::new(),
1192 pseudo_merges: Vec::new(),
1193 })
1194 }
1195
1196 pub fn with_name_hash_cache(mut self, cache: Vec<u32>) -> Result<Self> {
1202 if cache.len() != self.object_count as usize {
1203 return Err(GitError::InvalidFormat(format!(
1204 "name hash cache has {} entries but pack has {} objects",
1205 cache.len(),
1206 self.object_count
1207 )));
1208 }
1209 self.name_hash_cache = Some(cache);
1210 Ok(self)
1211 }
1212
1213 pub fn with_lookup_table(mut self, enabled: bool) -> Self {
1216 self.write_lookup_table = enabled;
1217 self
1218 }
1219
1220 pub fn add_commit(
1232 &mut self,
1233 commit_position: u32,
1234 commit_index_position: u32,
1235 reachable: &[u32],
1236 ) -> Result<()> {
1237 if commit_position >= self.object_count {
1238 return Err(GitError::InvalidFormat(format!(
1239 "commit position {commit_position} out of range for {} objects",
1240 self.object_count
1241 )));
1242 }
1243 if commit_index_position >= self.object_count {
1244 return Err(GitError::InvalidFormat(format!(
1245 "commit index position {commit_index_position} out of range for {} objects",
1246 self.object_count
1247 )));
1248 }
1249 if !self.commit_positions.contains(&commit_position) {
1250 return Err(GitError::InvalidFormat(format!(
1251 "bitmap commit position {commit_position} is not a commit object"
1252 )));
1253 }
1254 for &position in reachable {
1255 if position >= self.object_count {
1256 return Err(GitError::InvalidFormat(format!(
1257 "reachable position {position} out of range for {} objects",
1258 self.object_count
1259 )));
1260 }
1261 }
1262 let mut reachable = reachable.to_vec();
1263 reachable.push(commit_position);
1264 self.selected.push(SelectedCommit {
1265 commit_index_position,
1266 flags: Self::FLAG_NONE,
1267 reachable,
1268 });
1269 Ok(())
1270 }
1271
1272 pub fn add_pseudo_merge(&mut self, commits: &[u32], reachable: &[u32]) -> Result<()> {
1277 if commits.is_empty() {
1278 return Err(GitError::InvalidFormat(
1279 "pseudo-merge must contain at least one commit".into(),
1280 ));
1281 }
1282 for &position in commits {
1283 if position >= self.object_count {
1284 return Err(GitError::InvalidFormat(format!(
1285 "pseudo-merge commit position {position} out of range for {} objects",
1286 self.object_count
1287 )));
1288 }
1289 if !self.commit_positions.contains(&position) {
1290 return Err(GitError::InvalidFormat(format!(
1291 "pseudo-merge commit position {position} is not a commit object"
1292 )));
1293 }
1294 }
1295 for &position in reachable {
1296 if position >= self.object_count {
1297 return Err(GitError::InvalidFormat(format!(
1298 "pseudo-merge reachable position {position} out of range for {} objects",
1299 self.object_count
1300 )));
1301 }
1302 }
1303 self.pseudo_merges.push(PackBitmapPseudoMerge {
1304 commits: EwahBitmap::from_positions(self.object_count, commits)?,
1305 bitmap: EwahBitmap::from_positions(self.object_count, reachable)?,
1306 });
1307 Ok(())
1308 }
1309
1310 pub fn build(&self) -> Result<PackBitmapIndex> {
1317 let commits = EwahBitmap::from_positions(self.object_count, &self.commit_positions)?;
1318 let trees = EwahBitmap::from_positions(self.object_count, &self.tree_positions)?;
1319 let blobs = EwahBitmap::from_positions(self.object_count, &self.blob_positions)?;
1320 let tags = EwahBitmap::from_positions(self.object_count, &self.tag_positions)?;
1321
1322 let mut entries = Vec::with_capacity(self.selected.len());
1323 for selected in &self.selected {
1324 let bitmap = EwahBitmap::from_positions(self.object_count, &selected.reachable)?;
1325 entries.push(PackBitmapEntry {
1326 object_position: selected.commit_index_position,
1327 xor_offset: 0,
1328 flags: selected.flags,
1329 bitmap,
1330 });
1331 }
1332
1333 let mut options = PackBitmapIndex::OPTION_FULL_DAG;
1334 if self.name_hash_cache.is_some() {
1335 options |= PackBitmapIndex::OPTION_HASH_CACHE;
1336 }
1337 if !self.pseudo_merges.is_empty() {
1338 options |= PackBitmapIndex::OPTION_PSEUDO_MERGES;
1339 }
1340 if self.write_lookup_table {
1341 options |= PackBitmapIndex::OPTION_LOOKUP_TABLE;
1342 }
1343
1344 let placeholder_checksum = ObjectId::null(self.format);
1349 Ok(PackBitmapIndex {
1350 version: 1,
1351 format: self.format,
1352 options,
1353 pack_checksum: self.pack_checksum.clone(),
1354 index_checksum: placeholder_checksum,
1355 type_bitmaps: PackBitmapTypeBitmaps {
1356 commits,
1357 trees,
1358 blobs,
1359 tags,
1360 },
1361 entries,
1362 pseudo_merges: self.pseudo_merges.clone(),
1363 lookup_table: self.write_lookup_table,
1364 name_hash_cache: self.name_hash_cache.clone(),
1365 })
1366 }
1367
1368 pub fn write(&self) -> Result<Vec<u8>> {
1371 self.build()?.write()
1372 }
1373}
1374
1375impl PackBitmapIndex {
1376 pub fn write(&self) -> Result<Vec<u8>> {
1390 if self.version != 1 {
1391 return Err(GitError::Unsupported(format!(
1392 "bitmap index version {}",
1393 self.version
1394 )));
1395 }
1396 let mut options = self.options;
1397 if !self.pseudo_merges.is_empty() {
1398 options |= Self::OPTION_PSEUDO_MERGES;
1399 }
1400 if self.lookup_table {
1401 options |= Self::OPTION_LOOKUP_TABLE;
1402 }
1403 let known_options = Self::OPTION_FULL_DAG
1404 | Self::OPTION_HASH_CACHE
1405 | Self::OPTION_LOOKUP_TABLE
1406 | Self::OPTION_PSEUDO_MERGES;
1407 if options & !known_options != 0 {
1408 return Err(GitError::Unsupported(format!(
1409 "bitmap index options {:#06x}",
1410 options & !known_options
1411 )));
1412 }
1413 if self.pack_checksum.format() != self.format {
1414 return Err(GitError::InvalidObjectId(
1415 "bitmap pack checksum format does not match index format".into(),
1416 ));
1417 }
1418 if self.entries.len() > u32::MAX as usize {
1419 return Err(GitError::InvalidFormat(
1420 "too many bitmap index entries".into(),
1421 ));
1422 }
1423 if options & Self::OPTION_PSEUDO_MERGES != 0 && self.pseudo_merges.is_empty() {
1424 return Err(GitError::InvalidFormat(
1425 "OPTION_PSEUDO_MERGES set without pseudo-merge records".into(),
1426 ));
1427 }
1428 let want_cache = options & Self::OPTION_HASH_CACHE != 0;
1429 match (&self.name_hash_cache, want_cache) {
1430 (Some(_), false) => {
1431 return Err(GitError::InvalidFormat(
1432 "name hash cache present without OPTION_HASH_CACHE".into(),
1433 ));
1434 }
1435 (None, true) => {
1436 return Err(GitError::InvalidFormat(
1437 "OPTION_HASH_CACHE set without a name hash cache".into(),
1438 ));
1439 }
1440 _ => {}
1441 }
1442
1443 let mut out = Vec::new();
1444 out.extend_from_slice(b"BITM");
1445 out.extend_from_slice(&self.version.to_be_bytes());
1446 out.extend_from_slice(&options.to_be_bytes());
1447 out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1448 out.extend_from_slice(self.pack_checksum.as_bytes());
1449
1450 self.type_bitmaps.commits.append_bytes(&mut out);
1451 self.type_bitmaps.trees.append_bytes(&mut out);
1452 self.type_bitmaps.blobs.append_bytes(&mut out);
1453 self.type_bitmaps.tags.append_bytes(&mut out);
1454
1455 let mut entry_offsets = Vec::with_capacity(self.entries.len());
1456 for (idx, entry) in self.entries.iter().enumerate() {
1457 if entry.xor_offset as usize > idx {
1458 return Err(GitError::InvalidFormat(
1459 "bitmap index entry has invalid XOR offset".into(),
1460 ));
1461 }
1462 entry_offsets.push(out.len() as u64);
1463 out.extend_from_slice(&entry.object_position.to_be_bytes());
1464 out.push(entry.xor_offset);
1465 out.push(entry.flags);
1466 entry.bitmap.append_bytes(&mut out);
1467 }
1468
1469 if !self.pseudo_merges.is_empty() {
1470 append_bitmap_pseudo_merges(&mut out, &self.pseudo_merges)?;
1471 }
1472
1473 if self.lookup_table {
1474 append_bitmap_lookup_table(&mut out, &self.entries, &entry_offsets)?;
1475 }
1476
1477 if let Some(cache) = &self.name_hash_cache {
1478 for value in cache {
1479 out.extend_from_slice(&value.to_be_bytes());
1480 }
1481 }
1482
1483 let checksum = sley_core::digest_bytes(self.format, &out)?;
1484 out.extend_from_slice(checksum.as_bytes());
1485 Ok(out)
1486 }
1487}
1488
1489fn append_bitmap_lookup_table(
1490 out: &mut Vec<u8>,
1491 entries: &[PackBitmapEntry],
1492 entry_offsets: &[u64],
1493) -> Result<()> {
1494 if entries.len() != entry_offsets.len() {
1495 return Err(GitError::InvalidFormat(
1496 "bitmap lookup table offset count mismatch".into(),
1497 ));
1498 }
1499 let mut table: Vec<usize> = (0..entries.len()).collect();
1500 table.sort_by_key(|&index| entries[index].object_position);
1501 let mut inverse = vec![0u32; entries.len()];
1502 for (row, &entry_index) in table.iter().enumerate() {
1503 inverse[entry_index] = row as u32;
1504 }
1505 for &entry_index in &table {
1506 let entry = &entries[entry_index];
1507 let xor_row = if entry.xor_offset == 0 {
1508 u32::MAX
1509 } else {
1510 let base = entry_index
1511 .checked_sub(entry.xor_offset as usize)
1512 .ok_or_else(|| {
1513 GitError::InvalidFormat("bitmap lookup table XOR base underflow".into())
1514 })?;
1515 inverse[base]
1516 };
1517 out.extend_from_slice(&entry.object_position.to_be_bytes());
1518 out.extend_from_slice(&entry_offsets[entry_index].to_be_bytes());
1519 out.extend_from_slice(&xor_row.to_be_bytes());
1520 }
1521 Ok(())
1522}
1523
1524pub(crate) fn append_bitmap_pseudo_merges(
1525 out: &mut Vec<u8>,
1526 pseudo_merges: &[PackBitmapPseudoMerge],
1527) -> Result<()> {
1528 if pseudo_merges.len() > u32::MAX as usize {
1529 return Err(GitError::InvalidFormat(
1530 "too many pseudo-merge bitmap records".into(),
1531 ));
1532 }
1533 let start = out.len();
1534 let mut pseudo_offsets = Vec::with_capacity(pseudo_merges.len());
1535 let mut commit_to_offsets: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
1536 for merge in pseudo_merges {
1537 let offset = u64::try_from(out.len())
1538 .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
1539 pseudo_offsets.push(offset);
1540 for commit_pos in merge.commits.to_positions()? {
1541 commit_to_offsets
1542 .entry(commit_pos)
1543 .or_default()
1544 .push(offset);
1545 }
1546 merge.commits.append_bytes(out);
1547 merge.bitmap.append_bytes(out);
1548 }
1549 if commit_to_offsets.len() > u32::MAX as usize {
1550 return Err(GitError::InvalidFormat(
1551 "too many pseudo-merge commits".into(),
1552 ));
1553 }
1554
1555 let lookup_start = out.len();
1556 let lookup_len = commit_to_offsets
1557 .len()
1558 .checked_mul(12)
1559 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?;
1560 let mut next_extended = u64::try_from(
1561 lookup_start
1562 .checked_add(lookup_len)
1563 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?,
1564 )
1565 .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
1566 let mut rows = Vec::with_capacity(commit_to_offsets.len());
1567 for (commit_pos, offsets) in commit_to_offsets {
1568 let extended_offset = if offsets.len() > 1 {
1569 if next_extended & (1u64 << 63) != 0 {
1570 return Err(GitError::InvalidFormat(
1571 "pseudo-merge extended offset overflow".into(),
1572 ));
1573 }
1574 let offset = next_extended;
1575 let ext_len = offsets
1576 .len()
1577 .checked_mul(8)
1578 .and_then(|len| len.checked_add(4))
1579 .ok_or_else(|| {
1580 GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
1581 })?;
1582 next_extended = next_extended.checked_add(ext_len as u64).ok_or_else(|| {
1583 GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
1584 })?;
1585 Some(offset)
1586 } else {
1587 None
1588 };
1589 rows.push((commit_pos, offsets, extended_offset));
1590 }
1591
1592 for (commit_pos, offsets, extended_offset) in &rows {
1593 out.extend_from_slice(&commit_pos.to_be_bytes());
1594 match extended_offset {
1595 Some(offset) => out.extend_from_slice(&(offset | (1u64 << 63)).to_be_bytes()),
1596 None => out.extend_from_slice(&offsets[0].to_be_bytes()),
1597 }
1598 }
1599
1600 for (_commit_pos, offsets, extended_offset) in &rows {
1601 if extended_offset.is_none() {
1602 continue;
1603 }
1604 let count = u32::try_from(offsets.len())
1605 .map_err(|_| GitError::InvalidFormat("pseudo-merge extended lookup overflow".into()))?;
1606 out.extend_from_slice(&count.to_be_bytes());
1607 for offset in offsets {
1608 out.extend_from_slice(&offset.to_be_bytes());
1609 }
1610 }
1611
1612 for offset in &pseudo_offsets {
1613 out.extend_from_slice(&offset.to_be_bytes());
1614 }
1615 out.extend_from_slice(&(pseudo_merges.len() as u32).to_be_bytes());
1616 out.extend_from_slice(&(rows.len() as u32).to_be_bytes());
1617 let lookup_relative = lookup_start
1618 .checked_sub(start)
1619 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup underflow".into()))?;
1620 out.extend_from_slice(&(lookup_relative as u64).to_be_bytes());
1621 let extension_size = out
1622 .len()
1623 .checked_sub(start)
1624 .and_then(|len| len.checked_add(8))
1625 .ok_or_else(|| GitError::InvalidFormat("pseudo-merge extension overflow".into()))?;
1626 out.extend_from_slice(&(extension_size as u64).to_be_bytes());
1627 Ok(())
1628}
1629
1630pub fn write_bitmap(
1639 format: ObjectFormat,
1640 pack_checksum: ObjectId,
1641 object_types: &[ObjectType],
1642 commits: &[(u32, u32, Vec<u32>)],
1643 name_hash_cache: Option<Vec<u32>>,
1644) -> Result<Vec<u8>> {
1645 let mut writer = PackBitmapWriter::new(format, pack_checksum, object_types)?;
1646 if let Some(cache) = name_hash_cache {
1647 writer = writer.with_name_hash_cache(cache)?;
1648 }
1649 for (commit_position, commit_index_position, reachable) in commits {
1650 writer.add_commit(*commit_position, *commit_index_position, reachable)?;
1651 }
1652 writer.write()
1653}