1#[cfg(test)]
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::{
7 collections::{BTreeSet, HashMap, HashSet},
8 fs::File,
9 io::Read,
10 path::Path,
11};
12
13use bytes::Bytes;
14use heddle_format::delta::{DeltaDecoder, MAX_DELTA_OUTPUT_SIZE};
15
16use super::{
17 ObjectType, PackLogicalId, PackObjectId, PackObjectRecord, PackRepresentationHash,
18 append_container_checksum, decode_tagged_entry_header, decompress_pack_payload, has_zstd_magic,
19 pack_container_spec, pack_identity::LogicalIdBuilder, pack_index::PackIndex, varint,
20 verify_supported_container, verify_supported_container_layout, write_container_header,
21};
22use crate::{
23 object::ContentHash,
24 store::{Result, StoreError},
25};
26
27const MAX_PACK_DELTA_OUTPUT_SIZE: usize = MAX_DELTA_OUTPUT_SIZE;
28const MAX_DELTA_CHAIN_DEPTH: usize = 50;
29const MMAP_THRESHOLD_BYTES: u64 = 256 * 1024;
30
31type DecodedCompactObject = (PackObjectId, ObjectType, Vec<u8>);
32type DecodedCompactObjects = Vec<DecodedCompactObject>;
33
34#[derive(Debug, Clone, Copy, Eq, PartialEq)]
40pub enum PackReadTier {
41 Hot,
43 SolidFrame,
45}
46
47fn read_file_bytes_for_pack(path: &Path) -> Result<Bytes> {
48 let file = File::open(path)?;
49 let len = file.metadata()?.len();
50 if len == 0 {
51 return Ok(Bytes::new());
52 }
53 if len >= MMAP_THRESHOLD_BYTES {
54 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
55 if mmap.len() != checked_file_len_to_usize(len)? {
56 return Err(StoreError::InvalidObject(
57 "pack file size changed during memory mapping".to_string(),
58 ));
59 }
60 return Ok(Bytes::from_owner(mmap));
61 }
62 let mut data = Vec::with_capacity(checked_file_len_to_usize(len)?);
63 let mut reader = file;
64 reader.read_to_end(&mut data)?;
65 Ok(Bytes::from(data))
66}
67
68fn checked_file_len_to_usize(len: u64) -> Result<usize> {
69 usize::try_from(len).map_err(|_| {
70 StoreError::InvalidObject(format!("file length {len} exceeds platform limits"))
71 })
72}
73
74enum PackData<'a> {
83 Borrowed(&'a [u8]),
84 Owned(Bytes),
85}
86
87impl<'a> PackData<'a> {
88 fn as_slice(&self) -> &[u8] {
89 match self {
90 Self::Borrowed(data) => data,
91 Self::Owned(data) => data,
92 }
93 }
94
95 fn slice(&self, range: std::ops::Range<usize>) -> Bytes {
96 match self {
97 Self::Borrowed(data) => Bytes::copy_from_slice(&data[range]),
98 Self::Owned(data) => data.slice(range),
99 }
100 }
101}
102
103pub struct PackReader<'a> {
104 data: PackData<'a>,
105 index: PackIndex,
106 aliased_offsets: HashSet<u64>,
107 content_end: usize,
108 #[cfg(test)]
109 compact_frame_reads: AtomicUsize,
110}
111
112#[derive(Debug, Clone)]
113pub struct EncodedPackSubset {
114 pub pack_data: Vec<u8>,
115 pub index_data: Vec<u8>,
116 pub encoded_bytes_copied: u64,
117}
118
119impl PackReader<'static> {
120 pub fn open(pack_path: &Path, index_path: &Path) -> Result<Self> {
124 Self::open_with_verification(pack_path, index_path, true)
125 }
126
127 pub(super) fn open_lazy(pack_path: &Path, index_path: &Path) -> Result<Self> {
128 Self::open_with_verification(pack_path, index_path, false)
129 }
130
131 fn open_with_verification(
132 pack_path: &Path,
133 index_path: &Path,
134 verify_checksum: bool,
135 ) -> Result<Self> {
136 let pack_bytes = read_file_bytes_for_pack(pack_path)?;
137 let index_data = read_file_bytes_for_pack(index_path)?;
138 let (_, _, content_end) = if verify_checksum {
139 verify_supported_container(&pack_bytes)?
140 } else {
141 verify_supported_container_layout(&pack_bytes)?
142 };
143 let index = PackIndex::from_owned_bytes(index_data)?;
144 let aliased_offsets = index.aliased_offsets()?;
145 Ok(Self {
146 data: PackData::Owned(pack_bytes),
147 index,
148 aliased_offsets,
149 content_end,
150 #[cfg(test)]
151 compact_frame_reads: AtomicUsize::new(0),
152 })
153 }
154
155 pub fn from_bytes(pack_data: impl Into<Bytes>, index_data: impl AsRef<[u8]>) -> Result<Self> {
156 let pack_data = pack_data.into();
157 let (_, _, content_end) = verify_supported_container(&pack_data)?;
158 let index = PackIndex::from_bytes(index_data.as_ref())?;
159 let aliased_offsets = index.aliased_offsets()?;
160 Ok(Self {
161 data: PackData::Owned(pack_data),
162 index,
163 aliased_offsets,
164 content_end,
165 #[cfg(test)]
166 compact_frame_reads: AtomicUsize::new(0),
167 })
168 }
169}
170
171impl<'a> PackReader<'a> {
172 pub fn from_slice(pack_data: &'a [u8], index_data: impl AsRef<[u8]>) -> Result<Self> {
173 let (_, _, content_end) = verify_supported_container(pack_data)?;
174 let index = PackIndex::from_bytes(index_data.as_ref())?;
175 let aliased_offsets = index.aliased_offsets()?;
176 Ok(Self {
177 data: PackData::Borrowed(pack_data),
178 index,
179 aliased_offsets,
180 content_end,
181 #[cfg(test)]
182 compact_frame_reads: AtomicUsize::new(0),
183 })
184 }
185
186 pub fn list_ids(&self) -> Result<Vec<PackObjectId>> {
188 self.index.ids()
189 }
190
191 pub fn logical_id(&self) -> Result<PackLogicalId> {
197 let mut identity = LogicalIdBuilder::new();
198 self.visit_objects(|id, object_type, data| {
199 identity.push(id, object_type, data);
200 Ok(())
201 })?;
202 Ok(identity.finish())
203 }
204
205 pub fn representation_hash(&self) -> PackRepresentationHash {
207 PackRepresentationHash::compute(self.data.as_slice())
208 }
209
210 pub(super) fn indexed_read_tiers(&self) -> Result<Vec<(PackObjectId, PackReadTier)>> {
217 let entries = self.index.entries()?;
218 let mut aliases = HashMap::<u64, usize>::with_capacity(entries.len());
219 for entry in &entries {
220 *aliases.entry(entry.offset).or_default() += 1;
221 }
222 Ok(entries
223 .into_iter()
224 .map(|entry| {
225 let tier = if aliases[&entry.offset] > 1 {
226 PackReadTier::SolidFrame
227 } else {
228 PackReadTier::Hot
229 };
230 (entry.id, tier)
231 })
232 .collect())
233 }
234
235 pub(super) fn contains_object(&self, id: &PackObjectId) -> Result<bool> {
238 Ok(self.index.find(id)?.is_some())
239 }
240
241 #[cfg(test)]
242 pub(super) fn compact_frame_read_count(&self) -> usize {
243 self.compact_frame_reads.load(Ordering::Relaxed)
244 }
245
246 #[cfg(test)]
247 fn record_compact_frame_read(&self) {
248 self.compact_frame_reads.fetch_add(1, Ordering::Relaxed);
249 }
250
251 pub fn list_hashes(&self) -> Result<Vec<ContentHash>> {
252 Ok(self
253 .list_ids()?
254 .into_iter()
255 .filter_map(|id| match id {
256 PackObjectId::Hash(hash) => Some(hash),
257 PackObjectId::StateId(_) | PackObjectId::AnnotatedTag(_) => None,
258 })
259 .collect())
260 }
261
262 pub fn has_object(&self, id: &PackObjectId) -> Result<bool> {
263 Ok(self.index.find(id)?.is_some())
264 }
265
266 pub fn encoded_payload_bytes(&self, obj_type: ObjectType) -> Result<u64> {
271 let mut offsets = BTreeSet::new();
272 for id in self.index.ids()? {
273 if let Some(offset) = self.index.find(&id)? {
274 offsets.insert(checked_index_offset(offset)?);
275 }
276 }
277 let mut bytes = 0u64;
278 for offset in offsets {
279 let header = decode_tagged_entry_header(self.content_from(offset)?)?;
280 if header.obj_type == obj_type {
281 bytes = bytes.saturating_add(header.compressed_size as u64);
282 }
283 }
284 Ok(bytes)
285 }
286
287 pub fn visit_objects(
292 &self,
293 mut visitor: impl FnMut(PackObjectId, ObjectType, &[u8]) -> Result<()>,
294 ) -> Result<()> {
295 let mut locations = std::collections::BTreeMap::<usize, Vec<PackObjectId>>::new();
296 for id in self.index.ids()? {
297 let offset = self
298 .index
299 .find(&id)?
300 .ok_or_else(|| StoreError::InvalidObject("indexed object disappeared".into()))?;
301 locations
302 .entry(checked_index_offset(offset)?)
303 .or_default()
304 .push(id);
305 }
306 for (offset, indexed_ids) in locations {
307 if let Some(objects) = self.read_compact_objects_at(offset)? {
308 let actual = objects.iter().map(|(id, _, _)| *id).collect::<HashSet<_>>();
309 let indexed = indexed_ids.iter().copied().collect::<HashSet<_>>();
310 if actual != indexed
311 || actual.len() != objects.len()
312 || indexed.len() != indexed_ids.len()
313 {
314 return Err(StoreError::InvalidObject(
315 "compact frame object set differs from its index".into(),
316 ));
317 }
318 for (id, object_type, data) in objects {
319 visitor(id, object_type, &data)?;
320 }
321 continue;
322 }
323 if indexed_ids.len() != 1 {
324 return Err(StoreError::InvalidObject(
325 "ordinary pack record is indexed by multiple object ids".into(),
326 ));
327 }
328 let id = indexed_ids[0];
329 let (object_type, data) = self
330 .get_object(&id)?
331 .ok_or_else(|| StoreError::InvalidObject("indexed object is missing".into()))?;
332 visitor(id, object_type, &data)?;
333 }
334 Ok(())
335 }
336
337 pub fn copy_hosted_encoded_subset(
344 &self,
345 expected: &[(PackObjectId, ObjectType, u64)],
346 ) -> Result<Option<EncodedPackSubset>> {
347 if expected.is_empty() {
348 return Ok(None);
349 }
350 let mut unique = HashSet::with_capacity(expected.len());
351 if expected.iter().any(|(id, obj_type, _)| {
352 !unique.insert(*id)
353 || matches!(
354 obj_type,
355 ObjectType::Delta | ObjectType::StateAttachment | ObjectType::SnapshotCommit
356 )
357 }) {
358 return Ok(None);
359 }
360
361 let mut pack_data = Vec::new();
362 write_container_header(&mut pack_data, pack_container_spec(), expected.len() as u64);
363 let mut index = PackIndex::new();
364 let mut encoded_bytes_copied = 0u64;
365 for (expected_id, expected_type, expected_size) in expected {
366 let Some(offset) = self.index.find(expected_id)? else {
367 return Ok(None);
368 };
369 let offset = checked_index_offset(offset)?;
370 if offset >= self.content_end {
371 return Err(StoreError::InvalidObject(
372 "Entry offset out of bounds".to_string(),
373 ));
374 }
375 let header = decode_tagged_entry_header(self.content_from(offset)?)?;
376 if self.read_compact_objects_at(offset)?.is_some() {
377 return Ok(None);
378 }
379 let expected_size = usize::try_from(*expected_size).ok();
380 if header.id != *expected_id
381 || header.obj_type != *expected_type
382 || Some(header.uncompressed_size) != expected_size
383 || matches!(
384 header.obj_type,
385 ObjectType::Delta | ObjectType::StateAttachment | ObjectType::SnapshotCommit
386 )
387 {
388 return Ok(None);
389 }
390 let encoded_len = header
391 .header_len
392 .checked_add(header.compressed_size)
393 .ok_or_else(|| {
394 StoreError::InvalidObject("pack entry length overflow".to_string())
395 })?;
396 let encoded_end = offset
397 .checked_add(encoded_len)
398 .ok_or_else(|| StoreError::InvalidObject("pack entry end overflow".to_string()))?;
399 if encoded_end > self.content_end {
400 return Err(StoreError::InvalidObject(
401 "pack entry extends beyond content boundary".to_string(),
402 ));
403 }
404 let output_offset = u64::try_from(pack_data.len()).map_err(|_| {
405 StoreError::InvalidObject("reused pack offset exceeds u64".to_string())
406 })?;
407 index.add(*expected_id, output_offset);
408 pack_data.extend_from_slice(&self.data.as_slice()[offset..encoded_end]);
409 encoded_bytes_copied = encoded_bytes_copied
410 .checked_add(u64::try_from(encoded_len).map_err(|_| {
411 StoreError::InvalidObject("encoded pack entry length exceeds u64".to_string())
412 })?)
413 .ok_or_else(|| {
414 StoreError::InvalidObject("encoded reused byte count overflow".to_string())
415 })?;
416 }
417 index.sort();
418 append_container_checksum(&mut pack_data);
419 Ok(Some(EncodedPackSubset {
420 pack_data,
421 index_data: index.to_bytes(),
422 encoded_bytes_copied,
423 }))
424 }
425
426 pub fn get_object(&self, id: &PackObjectId) -> Result<Option<(ObjectType, Vec<u8>)>> {
439 let offset = match self.index.find(id)? {
440 Some(offset) => checked_index_offset(offset)?,
441 None => return Ok(None),
442 };
443
444 let record = self.read_record_at_depth(id, offset, 0)?;
445 Ok(Some((record.obj_type, record.data)))
446 }
447
448 pub fn get_hashed_object(&self, hash: &ContentHash) -> Result<Option<(ObjectType, Vec<u8>)>> {
449 self.get_object(&PackObjectId::Hash(*hash))
450 }
451
452 pub fn get_hashed_object_type(&self, hash: &ContentHash) -> Result<Option<ObjectType>> {
460 let id = PackObjectId::Hash(*hash);
461 let Some(offset) = self.index.find(&id)? else {
462 return Ok(None);
463 };
464 self.read_object_type_at_depth(&id, checked_index_offset(offset)?, 0)
465 .map(Some)
466 }
467
468 pub fn get_object_bytes(&self, id: &PackObjectId) -> Result<Option<(ObjectType, Bytes)>> {
478 let Some(offset) = self.index.find(id)? else {
479 return Ok(None);
480 };
481 let offset = checked_index_offset(offset)?;
482 if offset >= self.content_end {
483 return Err(StoreError::InvalidObject(
484 "Entry offset out of bounds".to_string(),
485 ));
486 }
487
488 let (record_id, id_len) = PackObjectId::decode_tagged(self.content_from(offset)?)?;
493 let header_start = checked_index_add(offset, id_len, "record header start")?;
494 let (encoded_type, uncompressed_size, type_len) =
495 varint::decode_type_and_size(self.content_from(header_start)?).ok_or_else(|| {
496 StoreError::InvalidObject("Truncated type+size varint".to_string())
497 })?;
498 let obj_type = decoded_entry_type(record_id, encoded_type)?;
499 let uncompressed_size = checked_decoded_size("uncompressed_size", uncompressed_size)?;
500 let varint_start = checked_index_add(header_start, type_len, "compressed_size start")?;
501 let (compressed_size, comp_len) = varint::decode_varint(self.content_from(varint_start)?)
502 .ok_or_else(truncated_compressed_size_varint)?;
503 let compressed_size = checked_decoded_size("compressed_size", compressed_size)?;
504
505 if record_id == *id && obj_type != ObjectType::Delta && compressed_size == uncompressed_size
509 {
510 let data_start = checked_index_add(varint_start, comp_len, "entry data start")?;
511 let data_end = checked_data_end(data_start, compressed_size, self.content_end)?;
512 let data = &self.data.as_slice()[data_start..data_end];
513 if !is_compact_frame(data) {
514 return Ok(Some((obj_type, self.data.slice(data_start..data_end))));
515 }
516 }
517
518 let record = self.read_record_at_depth(id, offset, 0)?;
522 Ok(Some((record.obj_type, Bytes::from(record.data))))
523 }
524
525 pub fn get_hashed_object_bytes(
526 &self,
527 hash: &ContentHash,
528 ) -> Result<Option<(ObjectType, Bytes)>> {
529 self.get_object_bytes(&PackObjectId::Hash(*hash))
530 }
531
532 pub fn get_hashed_object_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
542 let id = PackObjectId::Hash(*hash);
543 let Some(offset) = self.index.find(&id)? else {
544 return Ok(None);
545 };
546 let offset = checked_index_offset(offset)?;
547 if offset >= self.content_end {
548 return Err(StoreError::InvalidObject(
549 "Entry offset out of bounds".to_string(),
550 ));
551 }
552 let (record_id, id_len) = PackObjectId::decode_tagged(self.content_from(offset)?)?;
553 let header_start = checked_index_add(offset, id_len, "record header start")?;
554 let (obj_type, uncompressed_size, _type_len) = super::varint::decode_type_and_size(
555 self.content_from(header_start)?,
556 )
557 .ok_or_else(|| StoreError::InvalidObject("Truncated type+size varint".to_string()))?;
558 if (obj_type == ObjectType::Blob && self.aliased_offsets.contains(&(offset as u64)))
559 || matches!(obj_type, ObjectType::Tree | ObjectType::State)
560 {
561 let Some((_, data)) = self.get_object(&id)? else {
562 return Ok(None);
563 };
564 return Ok(Some(data.len() as u64));
565 }
566 verify_record_id_matches(&id, &record_id)?;
567 if obj_type == ObjectType::Delta {
568 return Ok(Some(uncompressed_size));
573 }
574 Ok(Some(uncompressed_size))
575 }
576
577 fn read_object_type_at_depth(
578 &self,
579 requested_id: &PackObjectId,
580 offset: usize,
581 depth: usize,
582 ) -> Result<ObjectType> {
583 if depth > MAX_DELTA_CHAIN_DEPTH {
584 return Err(StoreError::InvalidObject(format!(
585 "Delta chain depth {depth} exceeds max {MAX_DELTA_CHAIN_DEPTH}"
586 )));
587 }
588 if offset >= self.content_end {
589 return Err(StoreError::InvalidObject(
590 "Entry offset out of bounds".to_string(),
591 ));
592 }
593
594 let header = decode_tagged_entry_header(self.content_from(offset)?)?;
595 if header.id != *requested_id {
596 return self
597 .read_record_at_depth(requested_id, offset, depth)
598 .map(|record| record.obj_type);
599 }
600 if header.obj_type != ObjectType::Delta {
601 return Ok(header.obj_type);
602 }
603
604 let base_hash = Self::require_delta_base_hash(header.delta_base)?;
605 let base_id = PackObjectId::Hash(base_hash);
606 let base_offset = self
607 .index
608 .find(&base_id)?
609 .ok_or_else(|| StoreError::NotFound(base_hash.to_string()))?;
610 self.read_object_type_at_depth(&base_id, checked_index_offset(base_offset)?, depth + 1)
611 }
612
613 fn read_record_at_depth(
614 &self,
615 requested_id: &PackObjectId,
616 offset: usize,
617 depth: usize,
618 ) -> Result<PackObjectRecord> {
619 if offset >= self.content_end {
620 return Err(StoreError::InvalidObject(
621 "Entry offset out of bounds".to_string(),
622 ));
623 }
624
625 let (id, id_len) = PackObjectId::decode_tagged(self.content_from(offset)?)?;
626 let header_start = checked_index_add(offset, id_len, "record header start")?;
627
628 let (encoded_type, uncompressed_size, type_len) =
629 varint::decode_type_and_size(self.content_from(header_start)?).ok_or_else(|| {
630 StoreError::InvalidObject("Truncated type+size varint".to_string())
631 })?;
632 let obj_type = decoded_entry_type(id, encoded_type)?;
633 let uncompressed_size = checked_decoded_size("uncompressed_size", uncompressed_size)?;
634
635 let varint_start = checked_index_add(header_start, type_len, "compressed_size start")?;
636 let (compressed_size, comp_len) = varint::decode_varint(self.content_from(varint_start)?)
637 .ok_or_else(truncated_compressed_size_varint)?;
638 let compressed_size = checked_decoded_size("compressed_size", compressed_size)?;
639
640 let mut data_start = checked_index_add(varint_start, comp_len, "entry data start")?;
641
642 let base_id = if obj_type == ObjectType::Delta {
644 let (base_id, base_len) = PackObjectId::decode_tagged(self.content_from(data_start)?)?;
645 data_start = checked_index_add(data_start, base_len, "delta data start")?;
646 Some(base_id)
647 } else {
648 None
649 };
650
651 let data_end = checked_data_end(data_start, compressed_size, self.content_end)?;
652
653 let stored_data = &self.data.as_slice()[data_start..data_end];
654
655 let decompressed = if obj_type == ObjectType::Delta {
659 if has_zstd_magic(stored_data) {
660 decompress_pack_payload(stored_data, 0)?
661 } else {
662 stored_data.to_vec()
663 }
664 } else if compressed_size != uncompressed_size {
665 decompress_pack_payload(stored_data, uncompressed_size)?
666 } else {
667 stored_data.to_vec()
668 };
669
670 let shared_blob =
671 obj_type == ObjectType::Blob && self.aliased_offsets.contains(&(offset as u64));
672 if obj_type != ObjectType::Delta && (shared_blob || is_compact_frame(&decompressed)) {
673 #[cfg(test)]
674 self.record_compact_frame_read();
675 if let Some(data) =
676 decode_compact_object(requested_id, obj_type, &decompressed, shared_blob)?
677 {
678 return Ok(PackObjectRecord {
679 id: *requested_id,
680 obj_type,
681 data,
682 delta_base: None,
683 path_hint: None,
684 });
685 }
686 }
687 verify_record_id_matches(requested_id, &id)?;
688 let (resolved_type, final_data) = if obj_type == ObjectType::Delta {
689 self.read_delta_record(base_id, &decompressed, uncompressed_size, depth)?
690 } else {
691 (obj_type, decompressed)
692 };
693
694 if final_data.len() != uncompressed_size {
695 return Err(StoreError::InvalidObject(format!(
696 "Size mismatch: expected {}, got {}",
697 uncompressed_size,
698 final_data.len()
699 )));
700 }
701
702 Ok(PackObjectRecord {
703 id,
704 obj_type: resolved_type,
705 data: final_data,
706 delta_base: None,
707 path_hint: None,
708 })
709 }
710
711 fn read_compact_objects_at(&self, offset: usize) -> Result<Option<DecodedCompactObjects>> {
712 if offset >= self.content_end {
713 return Err(StoreError::InvalidObject(
714 "Entry offset out of bounds".to_string(),
715 ));
716 }
717 let header = decode_tagged_entry_header(self.content_from(offset)?)?;
718 if !matches!(
719 header.obj_type,
720 ObjectType::Blob | ObjectType::Tree | ObjectType::State
721 ) {
722 return Ok(None);
723 }
724 let shared_blob =
725 header.obj_type == ObjectType::Blob && self.aliased_offsets.contains(&(offset as u64));
726 if header.obj_type == ObjectType::Blob && !shared_blob {
727 return Ok(None);
728 }
729 let data_start = checked_index_add(offset, header.header_len, "entry data start")?;
730 let data_end = checked_data_end(data_start, header.compressed_size, self.content_end)?;
731 let stored = &self.data.as_slice()[data_start..data_end];
732 let data = if header.compressed_size != header.uncompressed_size {
733 decompress_pack_payload(stored, header.uncompressed_size)?
734 } else {
735 stored.to_vec()
736 };
737 if data.len() != header.uncompressed_size {
738 return Err(StoreError::InvalidObject(format!(
739 "Size mismatch: expected {}, got {}",
740 header.uncompressed_size,
741 data.len()
742 )));
743 }
744 #[cfg(test)]
745 if shared_blob || is_compact_frame(&data) {
746 self.record_compact_frame_read();
747 }
748 decode_compact_objects(header.obj_type, &data, shared_blob)
749 }
750
751 fn read_delta_record(
752 &self,
753 base_id: Option<PackObjectId>,
754 delta: &[u8],
755 uncompressed_size: usize,
756 depth: usize,
757 ) -> Result<(ObjectType, Vec<u8>)> {
758 if depth > MAX_DELTA_CHAIN_DEPTH {
759 return Err(StoreError::InvalidObject(format!(
760 "Delta chain depth {} exceeds max {}",
761 depth, MAX_DELTA_CHAIN_DEPTH
762 )));
763 }
764
765 if uncompressed_size > MAX_PACK_DELTA_OUTPUT_SIZE {
766 return Err(StoreError::InvalidObject(format!(
767 "Delta output size {} exceeds max {}",
768 uncompressed_size, MAX_PACK_DELTA_OUTPUT_SIZE
769 )));
770 }
771
772 let base_hash = Self::require_delta_base_hash(base_id)?;
773 let base_offset = self
774 .index
775 .find(&PackObjectId::Hash(base_hash))?
776 .ok_or_else(|| StoreError::NotFound(base_hash.to_string()))?;
777 let base_offset = checked_index_offset(base_offset)?;
778 let base_id = PackObjectId::Hash(base_hash);
779 let base_record = self.read_record_at_depth(&base_id, base_offset, depth + 1)?;
780 let base_type = base_record.obj_type;
781 let base_data = base_record.data;
782
783 let decoded = DeltaDecoder::decode(&base_data, delta, uncompressed_size)
784 .map_err(|error| StoreError::InvalidObject(format!("Delta decode failed: {error}")))?;
785
786 Ok((base_type, decoded))
787 }
788
789 fn require_delta_base_hash(base_id: Option<PackObjectId>) -> Result<ContentHash> {
790 match base_id {
791 Some(PackObjectId::Hash(hash)) => Ok(hash),
792 Some(PackObjectId::StateId(_) | PackObjectId::AnnotatedTag(_)) => Err(
793 StoreError::InvalidObject("pack delta base must be hash-backed content".into()),
794 ),
795 None => Err(StoreError::InvalidObject(
796 "pack object type is Delta but base hash is missing".into(),
797 )),
798 }
799 }
800
801 fn content_from(&self, offset: usize) -> Result<&[u8]> {
802 if offset > self.content_end {
803 return Err(StoreError::InvalidObject(
804 "Entry header out of bounds".to_string(),
805 ));
806 }
807 Ok(&self.data.as_slice()[offset..self.content_end])
808 }
809}
810
811fn checked_index_offset(offset: u64) -> Result<usize> {
812 usize::try_from(offset)
813 .map_err(|_| StoreError::InvalidObject("Entry offset exceeds platform limits".to_string()))
814}
815
816fn checked_decoded_size(field: &str, size: u64) -> Result<usize> {
817 let size = usize::try_from(size).map_err(|_| {
818 StoreError::InvalidObject(format!("Decoded {field} exceeds platform limits"))
819 })?;
820 if field == "uncompressed_size" && size > super::shared::MAX_PACK_OBJECT_OUTPUT_SIZE {
821 return Err(StoreError::InvalidObject(format!(
822 "Pack object output size {size} exceeds max {}",
823 super::shared::MAX_PACK_OBJECT_OUTPUT_SIZE
824 )));
825 }
826 Ok(size)
827}
828
829fn checked_index_add(start: usize, len: usize, field: &str) -> Result<usize> {
830 start.checked_add(len).ok_or_else(|| {
831 StoreError::InvalidObject(format!("{field} offset overflows platform limits"))
832 })
833}
834
835fn checked_data_end(
836 data_start: usize,
837 compressed_size: usize,
838 content_end: usize,
839) -> Result<usize> {
840 let data_end = data_start.checked_add(compressed_size).ok_or_else(|| {
841 StoreError::InvalidObject("Entry data range overflows platform limits".to_string())
842 })?;
843 if data_end > content_end {
844 return Err(StoreError::InvalidObject(
845 "Entry data out of bounds".to_string(),
846 ));
847 }
848 Ok(data_end)
849}
850
851fn truncated_compressed_size_varint() -> StoreError {
852 StoreError::InvalidObject("Truncated compressed_size varint".to_string())
853}
854
855fn decoded_entry_type(id: PackObjectId, encoded: ObjectType) -> Result<ObjectType> {
856 if matches!(id, PackObjectId::AnnotatedTag(_)) {
857 if encoded != ObjectType::Blob {
858 return Err(StoreError::InvalidObject(
859 "annotated-tag pack entry has invalid encoded type".to_string(),
860 ));
861 }
862 Ok(ObjectType::AnnotatedTag)
863 } else {
864 Ok(encoded)
865 }
866}
867
868fn verify_record_id_matches(requested: &PackObjectId, found: &PackObjectId) -> Result<()> {
877 if requested == found {
878 return Ok(());
879 }
880 Err(StoreError::InvalidObject(format!(
881 "pack index routed lookup for {requested:?} to record tagged {found:?} \
882 — index is stale or corrupt; the loose-store path will re-promote on \
883 the next read"
884 )))
885}
886
887fn is_compact_frame(data: &[u8]) -> bool {
888 heddle_object_model::compact::is_blob_frame(data)
889 || heddle_object_model::compact::is_tree_frame(data)
890 || heddle_object_model::compact::is_state_frame(data)
891}
892
893fn decode_compact_object(
894 requested_id: &PackObjectId,
895 obj_type: ObjectType,
896 data: &[u8],
897 require_blob_frame: bool,
898) -> Result<Option<Vec<u8>>> {
899 match (obj_type, requested_id) {
900 (ObjectType::Tree, PackObjectId::Hash(hash))
901 if heddle_object_model::compact::is_tree_frame(data) =>
902 {
903 let tree = heddle_object_model::compact::extract_tree(data, *hash)
904 .map_err(|error| compact_extract_error(requested_id, error))?;
905 rmp_serde::to_vec_named(&tree)
906 .map(Some)
907 .map_err(|error| StoreError::InvalidObject(error.to_string()))
908 }
909 (ObjectType::State, PackObjectId::StateId(id))
910 if heddle_object_model::compact::is_state_frame(data) =>
911 {
912 let state = heddle_object_model::compact::extract_state(data, *id)
913 .map_err(|error| compact_extract_error(requested_id, error))?;
914 rmp_serde::to_vec_named(&state)
915 .map(Some)
916 .map_err(|error| StoreError::InvalidObject(error.to_string()))
917 }
918 _ => {
919 let Some(objects) = decode_compact_objects(obj_type, data, require_blob_frame)? else {
920 return Ok(None);
921 };
922 objects
923 .into_iter()
924 .find_map(|(id, _, bytes)| (id == *requested_id).then_some(bytes))
925 .map(Some)
926 .ok_or_else(|| compact_index_miss(requested_id))
927 }
928 }
929}
930
931fn compact_extract_error(
932 id: &PackObjectId,
933 error: heddle_object_model::compact::CompactError,
934) -> StoreError {
935 if matches!(error, heddle_object_model::compact::CompactError::Missing) {
936 compact_index_miss(id)
937 } else {
938 StoreError::InvalidObject(error.to_string())
939 }
940}
941
942fn decode_compact_objects(
943 obj_type: ObjectType,
944 data: &[u8],
945 require_blob_frame: bool,
946) -> Result<Option<DecodedCompactObjects>> {
947 match obj_type {
948 ObjectType::Blob if require_blob_frame => {
949 heddle_object_model::compact::decode_blob_frame(data)
950 .map_err(|error| StoreError::InvalidObject(error.to_string()))?
951 .into_iter()
952 .map(|(hash, body)| Ok((PackObjectId::Hash(hash), ObjectType::Blob, body.to_vec())))
953 .collect::<Result<Vec<_>>>()
954 .map(Some)
955 }
956 ObjectType::Blob => Ok(None),
957 ObjectType::Tree if heddle_object_model::compact::is_tree_frame(data) => {
958 heddle_object_model::compact::decode_tree_frame(data)
959 .map_err(|error| StoreError::InvalidObject(error.to_string()))?
960 .into_iter()
961 .map(|tree| {
962 let id = PackObjectId::Hash(tree.hash());
963 let bytes = rmp_serde::to_vec_named(&tree)
964 .map_err(|error| StoreError::InvalidObject(error.to_string()))?;
965 Ok((id, ObjectType::Tree, bytes))
966 })
967 .collect::<Result<Vec<_>>>()
968 .map(Some)
969 }
970 ObjectType::State if heddle_object_model::compact::is_state_frame(data) => {
971 heddle_object_model::compact::decode_state_frame(data)
972 .map_err(|error| StoreError::InvalidObject(error.to_string()))?
973 .into_iter()
974 .map(|state| {
975 let id = PackObjectId::StateId(state.state_id);
976 let bytes = rmp_serde::to_vec_named(&state)
977 .map_err(|error| StoreError::InvalidObject(error.to_string()))?;
978 Ok((id, ObjectType::State, bytes))
979 })
980 .collect::<Result<Vec<_>>>()
981 .map(Some)
982 }
983 _ if is_compact_frame(data) => Err(StoreError::InvalidObject(
984 "compact frame magic does not match its pack object type".into(),
985 )),
986 _ => Ok(None),
987 }
988}
989
990fn compact_index_miss(id: &PackObjectId) -> StoreError {
991 StoreError::InvalidObject(format!(
992 "compact frame does not contain indexed object {id:?}"
993 ))
994}
995
996#[cfg(test)]
997mod tests {
998 use super::{PackObjectId, PackReader, verify_record_id_matches};
999 use crate::{object::ContentHash, store::StoreError};
1000
1001 #[test]
1002 fn test_require_delta_base_hash_rejects_missing_hash() {
1003 let error =
1004 PackReader::require_delta_base_hash(None).expect_err("missing hash should fail");
1005
1006 assert!(
1007 matches!(error, StoreError::InvalidObject(message) if message == "pack object type is Delta but base hash is missing")
1008 );
1009 }
1010
1011 #[test]
1012 fn verify_record_id_matches_accepts_identical_ids() {
1013 let id = PackObjectId::Hash(ContentHash::from_bytes([7u8; 32]));
1014 verify_record_id_matches(&id, &id).expect("matching ids must verify");
1015 }
1016
1017 #[test]
1018 fn verify_record_id_matches_rejects_mismatched_ids() {
1019 let asked = PackObjectId::Hash(ContentHash::from_bytes([7u8; 32]));
1020 let found = PackObjectId::Hash(ContentHash::from_bytes([8u8; 32]));
1021 let error = verify_record_id_matches(&asked, &found)
1022 .expect_err("mismatched record id must error rather than silently route");
1023 assert!(
1024 matches!(&error, StoreError::InvalidObject(message) if message.contains("stale or corrupt")),
1025 "stale-index mismatch must surface as InvalidObject with the diagnostic phrase, got: {error:?}",
1026 );
1027 }
1028}