1use std::collections::BTreeMap;
94use std::io::Read as _;
95
96use bytes::{Buf, BufMut};
97use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt, ReadRangeExt, Write};
98
99use crate::hash::{Hash, hash};
100use crate::merkle::{self, MerkleError, ObjectKind, Proof};
101use crate::object::{EntryMode, MAGIC, MkitError, Object, ObjectType, SCHEMA_VERSION, TreeEntry};
102use crate::sign::{verify_commit, verify_remix};
103use crate::store::MAX_TREE_DEPTH;
104
105pub const MAX_BUNDLE_BYTES: usize = 64 * 1024 * 1024;
110
111const BUNDLE_MAGIC: &[u8; 4] = b"MKDP";
113const BUNDLE_VERSION: u8 = 2;
117const MAX_COMMIT_BYTES: usize = 4 * 1024 * 1024;
120const MAX_LEN_PROOF_SLICE_BYTES: usize = 8 * 1024;
125
126#[derive(Debug, thiserror::Error)]
132pub enum VerifyError {
133 #[error("disclosure bundle exceeds the {MAX_BUNDLE_BYTES} byte cap")]
135 BundleTooLarge,
136 #[error("disclosure bundle magic is not \"MKDP\"")]
139 BadMagic,
140 #[error("disclosure bundle version {0} is not supported (v2 only)")]
143 UnsupportedBundleVersion(u8),
144 #[error("declared inner root does not wrap to the expected object id")]
148 InnerRootMismatch {
149 expected: Hash,
151 inner_root: Hash,
153 },
154 #[error("proof fold does not equal the declared inner root")]
158 InnerRootFoldMismatch,
159 #[error("disclosure bundle body is malformed (bad codec payload or trailing bytes)")]
162 Malformed,
163 #[error("disclosure bundle's embedded commit_id does not match the requested commit id")]
166 CommitIdMismatch,
167 #[error("commit_bytes do not hash to the commit id being verified")]
169 CommitBytesHashMismatch,
170 #[error("commit_bytes decode to a {0:?}, which is not a Commit or Remix")]
174 NotACommitOrRemix(ObjectType),
175 #[error("{0} steps exceeds MAX_TREE_DEPTH ({MAX_TREE_DEPTH})")]
177 TooManySteps(usize),
178 #[error("step {0}'s entry name fails SPEC-OBJECTS §4.1 validation")]
181 InvalidEntryName(usize),
182 #[error("step {0} is not the final step but its mode is not Tree")]
185 NonFinalStepNotTree(usize),
186 #[error("merkle proof verification failed: {0}")]
191 Merkle(#[from] MerkleError),
192 #[error("payload_kind byte {0:#04x} is not a recognized disclosure payload kind")]
195 InvalidPayloadKind(u8),
196 #[error("disclosed Object payload's id does not equal the authenticated leaf id")]
199 PayloadIdMismatch,
200 #[error("chunk index {index} is out of range for a {leaf_count}-leaf ChunkedBlob proof")]
203 ChunkIndexOutOfRange {
204 index: u32,
206 leaf_count: u32,
209 },
210 #[error("proof leaf_count implies more chunks than MAX_CHUNKS allows")]
213 TooManyChunks,
214 #[error("byte range length is zero")]
217 ZeroLengthRange,
218 #[error("chunk_len_proofs does not cover exactly indices 0..{0} with no gaps or duplicates")]
222 IncompleteLengthProofSet(u32),
223 #[error("chunk_len_proofs is only meaningful for a ChunkedBlob range at chunk index > 0")]
228 UnexpectedLengthProofs,
229 #[error("Bao slice verification failed: {0}")]
231 Bao(String),
232 #[error("Bao slice yielded {got} bytes, expected {expected}")]
234 ShortSlice {
235 expected: u64,
237 got: usize,
239 },
240 #[error("blob length-proof bytes are not a valid v1 Blob prologue")]
243 InvalidBlobPrologue,
244 #[error("offset/length computation overflowed u64")]
246 OffsetOverflow,
247 #[error(transparent)]
249 Decode(#[from] MkitError),
250 #[error("selector does not match the disclosed leaf's object type")]
254 SelectorLeafMismatch,
255 #[error("byte range crosses a chunk boundary, which this profile does not support")]
258 RangeCrossesChunkBoundary,
259 #[error("byte range is out of bounds for the leaf's content length")]
262 RangeOutOfBounds,
263 #[error("path component {0} was not found in its parent tree")]
266 PathNotFound(usize),
267 #[error("path continues past a non-Tree entry")]
271 PathThroughNonTree,
272 #[error(transparent)]
275 Store(#[from] crate::store::StoreError),
276 #[error("closure object set exceeds the pack MAX_ENTRIES cap")]
278 TooManyClosureObjects,
279 #[error("closure root is a {0:?}, which is not a Commit, Remix, or Tag")]
281 ClosureRootWrongType(ObjectType),
282 #[error(
284 "closure pack {pack_index} entry {entry_index} is not a raw (0x00) entry (closure profile is raw-only)"
285 )]
286 ClosureProfileViolation {
287 pack_index: usize,
289 entry_index: usize,
291 },
292 #[error("closure manifest magic is not \"MKCL\"")]
295 ClosureManifestBadMagic,
296 #[error("closure manifest version {0} is not supported (v1 only)")]
298 ClosureManifestUnsupportedVersion(u8),
299 #[error(
302 "closure manifest body is malformed (bad codec payload, unknown mode, or trailing bytes)"
303 )]
304 ClosureManifestMalformed,
305 #[error("closure manifest lists {expected} packs but {got} were supplied")]
307 ClosurePackCountMismatch {
308 expected: usize,
310 got: usize,
312 },
313 #[error("pack {index} hash does not match the closure manifest")]
315 ClosurePackKeyMismatch {
316 index: usize,
318 },
319 #[error("closure manifest root does not match the caller's trusted root")]
322 ClosureRootMismatch {
323 expected: Hash,
325 got: Hash,
327 },
328 #[error(transparent)]
330 Pack(#[from] crate::pack::PackError),
331}
332
333impl From<CodecError> for VerifyError {
334 fn from(_: CodecError) -> Self {
335 Self::Malformed
341 }
342}
343
344mod closure;
345pub use closure::{
346 ClosureExport, ClosureManifest, ClosureReport, MAX_CLOSURE_PACKS, ObjectSource, export_closure,
347 verify_closure, verify_closure_manifest, verify_closure_packs, verify_closure_store,
348 verify_closure_streaming,
349};
350mod push;
351pub use push::{PushReport, verify_push};
352pub mod proof_size;
354pub mod span;
355
356#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct Step {
369 pub name: Vec<u8>,
371 pub mode: EntryMode,
373 pub child_id: Hash,
375 pub inner_root: Hash,
380 pub position: u32,
382 pub proof: Proof,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct PathVerified {
391 pub tree_hash: Hash,
393 pub path: Vec<(Vec<u8>, EntryMode)>,
397 pub leaf_id: Hash,
400 pub signer: [u8; 32],
402 pub signature_valid: bool,
406}
407
408#[derive(Debug, Clone, PartialEq, Eq)]
412pub struct Disclosed {
413 pub commit_id: Hash,
415 pub tree_hash: Hash,
417 pub path: Vec<(Vec<u8>, EntryMode)>,
419 pub leaf_id: Hash,
421 pub payload: DisclosedPayload,
423 pub signer: [u8; 32],
425 pub signature_valid: bool,
428 pub step_inner_roots: Vec<Hash>,
433 pub chunk_inner_root: Option<Hash>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum DisclosedPayload {
442 Object {
445 bytes: Vec<u8>,
447 },
448 Chunk {
451 total_size: u64,
453 chunk_size: u32,
456 index: u32,
458 bytes: Vec<u8>,
460 },
461 Range {
463 blob_id: Hash,
467 chunk: Option<(u32, u64, u32)>,
471 offset_in_blob: u64,
474 absolute_offset: Option<u64>,
485 bytes: Vec<u8>,
487 },
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub enum Selector {
493 Object,
495 Chunk(u32),
497 Range {
499 offset: u64,
501 len: u64,
503 with_offsets: bool,
507 },
508}
509
510fn check_inner_root_wrap(
526 kind: ObjectKind,
527 expected_id: &Hash,
528 inner_root: &Hash,
529) -> Result<(), VerifyError> {
530 if merkle::wrap_id(kind, inner_root) == *expected_id {
531 Ok(())
532 } else {
533 Err(VerifyError::InnerRootMismatch {
534 expected: *expected_id,
535 inner_root: *inner_root,
536 })
537 }
538}
539
540fn check_inner_root_fold(
543 folded: Hash,
544 declared: &Hash,
545 expected_id: &Hash,
546 kind: ObjectKind,
547) -> Result<(), VerifyError> {
548 if folded != *declared {
549 return Err(VerifyError::InnerRootFoldMismatch);
550 }
551 if merkle::wrap_id(kind, &folded) == *expected_id {
552 Ok(())
553 } else {
554 Err(VerifyError::Merkle(MerkleError::VerificationFailed))
555 }
556}
557
558pub fn verify_object_id(bytes: &[u8], expected: &Hash) -> Result<Object, VerifyError> {
559 let obj = crate::serialize::deserialize(bytes)?;
560 let got = crate::object::id_from_object(&obj, bytes);
561 if &got == expected {
562 Ok(obj)
563 } else {
564 Err(VerifyError::PayloadIdMismatch)
565 }
566}
567
568pub fn verify_path(
582 commit_id: &Hash,
583 commit_bytes: &[u8],
584 steps: &[Step],
585) -> Result<PathVerified, VerifyError> {
586 if hash(commit_bytes) != *commit_id {
587 return Err(VerifyError::CommitBytesHashMismatch);
588 }
589 if steps.len() > MAX_TREE_DEPTH {
590 return Err(VerifyError::TooManySteps(steps.len()));
591 }
592 let obj = crate::serialize::deserialize(commit_bytes)?;
593 let (tree_hash, signer, signature_valid) = match &obj {
594 Object::Commit(c) => (c.tree_hash, c.signer, verify_commit(c).is_ok()),
595 Object::Remix(r) => (r.tree_hash, r.signer, verify_remix(r).is_ok()),
596 other => return Err(VerifyError::NotACommitOrRemix(other.object_type())),
597 };
598 verify_path_with_context(
599 steps,
600 &CommitContext {
601 tree_hash,
602 signer,
603 signature_valid,
604 },
605 )
606}
607
608#[derive(Clone, Copy)]
610pub(crate) struct CommitContext {
611 tree_hash: Hash,
612 signer: [u8; 32],
613 signature_valid: bool,
614}
615
616impl CommitContext {
617 pub(crate) fn from_disclosed(d: &Disclosed) -> Self {
618 Self {
619 tree_hash: d.tree_hash,
620 signer: d.signer,
621 signature_valid: d.signature_valid,
622 }
623 }
624}
625
626pub(crate) fn verify_path_reusing_context(
629 commit_id: &Hash,
630 commit_bytes: &[u8],
631 steps: &[Step],
632 context: &CommitContext,
633) -> Result<PathVerified, VerifyError> {
634 if hash(commit_bytes) != *commit_id {
635 return Err(VerifyError::CommitBytesHashMismatch);
636 }
637 verify_path_with_context(steps, context)
638}
639
640fn verify_path_with_context(
641 steps: &[Step],
642 context: &CommitContext,
643) -> Result<PathVerified, VerifyError> {
644 if steps.len() > MAX_TREE_DEPTH {
645 return Err(VerifyError::TooManySteps(steps.len()));
646 }
647
648 let mut expected_parent = context.tree_hash;
649 let mut leaf_id = context.tree_hash;
650 let mut path = Vec::with_capacity(steps.len());
651 let last = steps.len().wrapping_sub(1);
652 for (i, step) in steps.iter().enumerate() {
653 if !TreeEntry::validate_name(&step.name) {
654 return Err(VerifyError::InvalidEntryName(i));
655 }
656 if i != last && step.mode != EntryMode::Tree {
657 return Err(VerifyError::NonFinalStepNotTree(i));
658 }
659 let entry = TreeEntry {
660 name: step.name.clone(),
661 mode: step.mode,
662 object_hash: step.child_id,
663 };
664 check_inner_root_wrap(ObjectKind::Tree, &expected_parent, &step.inner_root)?;
665 let folded = step
666 .proof
667 .reconstruct_element_root(&merkle::tree_entry_leaf(&entry), step.position)?;
668 check_inner_root_fold(folded, &step.inner_root, &expected_parent, ObjectKind::Tree)?;
669 path.push((step.name.clone(), step.mode));
670 expected_parent = step.child_id;
671 leaf_id = step.child_id;
672 }
673
674 Ok(PathVerified {
675 tree_hash: context.tree_hash,
676 path,
677 leaf_id,
678 signer: context.signer,
679 signature_valid: context.signature_valid,
680 })
681}
682
683pub fn verify_chunk_with_meta(
698 chunked_id: &Hash,
699 total_size: u64,
700 chunk_size: u32,
701 chunk_hash: &Hash,
702 index: u32,
703 proof: &Proof,
704) -> Result<(), VerifyError> {
705 let chunk_count = proof
706 .leaf_count
707 .checked_sub(1)
708 .ok_or(VerifyError::ChunkIndexOutOfRange {
709 index,
710 leaf_count: proof.leaf_count,
711 })?;
712 if chunk_count > crate::serialize::MAX_CHUNKS {
713 return Err(VerifyError::TooManyChunks);
714 }
715 let position = index
716 .checked_add(1)
717 .filter(|&p| p < proof.leaf_count)
718 .ok_or(VerifyError::ChunkIndexOutOfRange {
719 index,
720 leaf_count: proof.leaf_count,
721 })?;
722 merkle::verify_chunk_with_meta_leaf(
723 chunked_id, total_size, chunk_size, chunk_hash, position, proof,
724 )
725 .map_err(VerifyError::from)
726}
727
728fn verify_chunk_with_declared_root(
732 chunked_id: &Hash,
733 inner_root: &Hash,
734 total_size: u64,
735 chunk_size: u32,
736 chunk_hash: &Hash,
737 index: u32,
738 proof: &Proof,
739) -> Result<(), VerifyError> {
740 let chunk_count = proof
741 .leaf_count
742 .checked_sub(1)
743 .ok_or(VerifyError::ChunkIndexOutOfRange {
744 index,
745 leaf_count: proof.leaf_count,
746 })?;
747 if chunk_count > crate::serialize::MAX_CHUNKS {
748 return Err(VerifyError::TooManyChunks);
749 }
750 let position = index
751 .checked_add(1)
752 .filter(|&p| p < proof.leaf_count)
753 .ok_or(VerifyError::ChunkIndexOutOfRange {
754 index,
755 leaf_count: proof.leaf_count,
756 })?;
757 check_inner_root_wrap(ObjectKind::ChunkedBlob, chunked_id, inner_root)?;
758 let meta_leaf = merkle::chunked_meta_leaf_raw(total_size, chunk_size);
759 let folded = proof.reconstruct_multi_root(&[(meta_leaf, 0u32), (*chunk_hash, position)])?;
760 check_inner_root_fold(folded, inner_root, chunked_id, ObjectKind::ChunkedBlob)
761}
762
763fn bao_verify_slice(
768 root: &Hash,
769 bao_offset: u64,
770 len: u64,
771 slice: &[u8],
772) -> Result<Vec<u8>, VerifyError> {
773 let h: bao::Hash = (*root).into();
774 let mut decoder =
775 bao::decode::SliceDecoder::new(std::io::Cursor::new(slice), &h, bao_offset, len);
776 let mut out = Vec::new();
777 decoder
778 .read_to_end(&mut out)
779 .map_err(|e| VerifyError::Bao(e.to_string()))?;
780 if out.len() as u64 != len {
781 return Err(VerifyError::ShortSlice {
782 expected: len,
783 got: out.len(),
784 });
785 }
786 Ok(out)
787}
788
789pub fn verify_blob_slice(
804 blob_id: &Hash,
805 content_offset: u64,
806 len: u64,
807 slice: &[u8],
808) -> Result<Vec<u8>, VerifyError> {
809 if len == 0 {
810 return Err(VerifyError::ZeroLengthRange);
811 }
812 let bao_offset = content_offset
813 .checked_add(10)
814 .ok_or(VerifyError::OffsetOverflow)?;
815 bao_verify_slice(blob_id, bao_offset, len, slice)
816}
817
818pub fn verify_blob_len_proof(blob_id: &Hash, slice: &[u8]) -> Result<u32, VerifyError> {
836 let bytes = bao_verify_slice(blob_id, 0, 10, slice)?;
837 if bytes[0] != ObjectType::Blob as u8 || bytes[1..5] != MAGIC || bytes[5] != SCHEMA_VERSION {
838 return Err(VerifyError::InvalidBlobPrologue);
839 }
840 let len_bytes: [u8; 4] = bytes[6..10]
841 .try_into()
842 .expect("bao_verify_slice guarantees exactly 10 bytes");
843 Ok(u32::from_le_bytes(len_bytes))
844}
845
846#[derive(Debug, Clone)]
854struct ChunkHdr {
855 total_size: u64,
856 chunk_size: u32,
857 index: u32,
858 inner_root: Hash,
859 chunk_id: Hash,
860 proof: Proof,
861}
862
863impl Write for ChunkHdr {
864 fn write(&self, writer: &mut impl BufMut) {
865 self.total_size.write(writer);
866 self.chunk_size.write(writer);
867 self.index.write(writer);
868 self.inner_root.write(writer);
869 self.chunk_id.write(writer);
870 self.proof.write(writer);
871 }
872}
873
874impl EncodeSize for ChunkHdr {
875 fn encode_size(&self) -> usize {
876 self.total_size.encode_size()
877 + self.chunk_size.encode_size()
878 + self.index.encode_size()
879 + self.inner_root.encode_size()
880 + self.chunk_id.encode_size()
881 + self.proof.encode_size()
882 }
883}
884
885impl Read for ChunkHdr {
886 type Cfg = ();
887
888 fn read_cfg(reader: &mut impl Buf, (): &Self::Cfg) -> Result<Self, CodecError> {
889 let total_size = u64::read(reader)?;
890 let chunk_size = u32::read(reader)?;
891 let index = u32::read(reader)?;
892 let inner_root = Hash::read(reader)?;
893 let chunk_id = Hash::read(reader)?;
894 let proof = Proof::read_cfg(reader, &2usize)?;
895 Ok(Self {
896 total_size,
897 chunk_size,
898 index,
899 inner_root,
900 chunk_id,
901 proof,
902 })
903 }
904}
905
906#[derive(Debug, Clone)]
909struct LenProof {
910 index: u32,
911 chunk_id: Hash,
912 proof: Proof,
913 slice: Vec<u8>,
914}
915
916impl Write for LenProof {
917 fn write(&self, writer: &mut impl BufMut) {
918 self.index.write(writer);
919 self.chunk_id.write(writer);
920 self.proof.write(writer);
921 self.slice.as_slice().write(writer);
922 }
923}
924
925impl EncodeSize for LenProof {
926 fn encode_size(&self) -> usize {
927 self.index.encode_size()
928 + self.chunk_id.encode_size()
929 + self.proof.encode_size()
930 + self.slice.as_slice().encode_size()
931 }
932}
933
934impl Read for LenProof {
935 type Cfg = ();
936
937 fn read_cfg(reader: &mut impl Buf, (): &Self::Cfg) -> Result<Self, CodecError> {
938 let index = u32::read(reader)?;
939 let chunk_id = Hash::read(reader)?;
940 let proof = Proof::read_cfg(reader, &1usize)?;
941 let slice = Vec::<u8>::read_range(reader, ..=MAX_LEN_PROOF_SLICE_BYTES)?;
942 Ok(Self {
943 index,
944 chunk_id,
945 proof,
946 slice,
947 })
948 }
949}
950
951impl Write for Step {
952 fn write(&self, writer: &mut impl BufMut) {
953 self.name.as_slice().write(writer);
954 (self.mode as u8).write(writer);
955 self.child_id.write(writer);
956 self.inner_root.write(writer);
957 self.position.write(writer);
958 self.proof.write(writer);
959 }
960}
961
962impl EncodeSize for Step {
963 fn encode_size(&self) -> usize {
964 self.name.as_slice().encode_size()
965 + 1
966 + self.child_id.encode_size()
967 + self.inner_root.encode_size()
968 + self.position.encode_size()
969 + self.proof.encode_size()
970 }
971}
972
973impl Read for Step {
974 type Cfg = ();
975
976 fn read_cfg(reader: &mut impl Buf, (): &Self::Cfg) -> Result<Self, CodecError> {
977 let name = Vec::<u8>::read_range(reader, 1..=255)?;
978 let mode_byte = u8::read(reader)?;
979 let mode = EntryMode::from_u8(mode_byte).map_err(|_| CodecError::InvalidEnum(mode_byte))?;
980 let child_id = Hash::read(reader)?;
981 let inner_root = Hash::read(reader)?;
982 let position = u32::read(reader)?;
983 let proof = Proof::read_cfg(reader, &1usize)?;
984 Ok(Self {
985 name,
986 mode,
987 child_id,
988 inner_root,
989 position,
990 proof,
991 })
992 }
993}
994
995enum PayloadWire {
999 Object {
1000 bytes: Vec<u8>,
1001 },
1002 Chunk {
1003 total_size: u64,
1004 chunk_size: u32,
1005 index: u32,
1006 inner_root: Hash,
1007 proof: Proof,
1008 bytes: Vec<u8>,
1009 },
1010 Range {
1011 chunk: Option<ChunkHdr>,
1012 offset_in_blob: u64,
1013 len: u64,
1014 slice: Vec<u8>,
1015 chunk_len_proofs: Vec<LenProof>,
1016 },
1017}
1018
1019fn decode_disclosure(bytes: &[u8]) -> Result<(Hash, Vec<u8>, Vec<Step>, PayloadWire), VerifyError> {
1024 if bytes.len() > MAX_BUNDLE_BYTES {
1025 return Err(VerifyError::BundleTooLarge);
1026 }
1027 if bytes.len() < 5 || bytes[..4] != *BUNDLE_MAGIC {
1028 return Err(VerifyError::BadMagic);
1029 }
1030 let version = bytes[4];
1031 if version != BUNDLE_VERSION {
1032 return Err(VerifyError::UnsupportedBundleVersion(version));
1033 }
1034 let mut r: &[u8] = &bytes[5..];
1035 let commit_id = Hash::read(&mut r)?;
1036 let commit_bytes = Vec::<u8>::read_range(&mut r, ..=MAX_COMMIT_BYTES)?;
1037 let steps = Vec::<Step>::read_range(&mut r, ..=MAX_TREE_DEPTH)?;
1038 let payload_kind = u8::read(&mut r)?;
1039 let payload = match payload_kind {
1040 0 => {
1041 let bytes = Vec::<u8>::read_range(&mut r, ..=crate::store::MAX_RAW_OBJECT_SIZE)?;
1042 PayloadWire::Object { bytes }
1043 }
1044 1 => {
1045 let total_size = u64::read(&mut r)?;
1046 let chunk_size = u32::read(&mut r)?;
1047 let index = u32::read(&mut r)?;
1048 let inner_root = Hash::read(&mut r)?;
1049 let proof = Proof::read_cfg(&mut r, &2usize)?;
1050 let bytes = Vec::<u8>::read_range(&mut r, ..=crate::store::MAX_RAW_OBJECT_SIZE)?;
1051 PayloadWire::Chunk {
1052 total_size,
1053 chunk_size,
1054 index,
1055 inner_root,
1056 proof,
1057 bytes,
1058 }
1059 }
1060 2 => {
1061 let chunk = Option::<ChunkHdr>::read(&mut r)?;
1062 let offset_in_blob = u64::read(&mut r)?;
1063 let len = u64::read(&mut r)?;
1064 let slice = Vec::<u8>::read_range(&mut r, ..=MAX_BUNDLE_BYTES)?;
1065 let chunk_len_proofs =
1066 Vec::<LenProof>::read_range(&mut r, ..=crate::serialize::MAX_CHUNKS as usize)?;
1067 PayloadWire::Range {
1068 chunk,
1069 offset_in_blob,
1070 len,
1071 slice,
1072 chunk_len_proofs,
1073 }
1074 }
1075 other => return Err(VerifyError::InvalidPayloadKind(other)),
1076 };
1077 if r.has_remaining() {
1078 return Err(VerifyError::Malformed);
1079 }
1080 Ok((commit_id, commit_bytes, steps, payload))
1081}
1082
1083fn encode_disclosure(
1085 commit_id: &Hash,
1086 commit_bytes: &[u8],
1087 steps: &[Step],
1088 payload: &PayloadWire,
1089) -> Vec<u8> {
1090 let mut out = Vec::new();
1091 out.extend_from_slice(BUNDLE_MAGIC);
1092 out.push(BUNDLE_VERSION);
1093 commit_id.write(&mut out);
1094 commit_bytes.write(&mut out);
1095 steps.write(&mut out);
1096 match payload {
1097 PayloadWire::Object { bytes } => {
1098 out.push(0);
1099 bytes.as_slice().write(&mut out);
1100 }
1101 PayloadWire::Chunk {
1102 total_size,
1103 chunk_size,
1104 index,
1105 inner_root,
1106 proof,
1107 bytes,
1108 } => {
1109 out.push(1);
1110 total_size.write(&mut out);
1111 chunk_size.write(&mut out);
1112 index.write(&mut out);
1113 inner_root.write(&mut out);
1114 proof.write(&mut out);
1115 bytes.as_slice().write(&mut out);
1116 }
1117 PayloadWire::Range {
1118 chunk,
1119 offset_in_blob,
1120 len,
1121 slice,
1122 chunk_len_proofs,
1123 } => {
1124 out.push(2);
1125 chunk.write(&mut out);
1126 offset_in_blob.write(&mut out);
1127 len.write(&mut out);
1128 slice.as_slice().write(&mut out);
1129 chunk_len_proofs.write(&mut out);
1130 }
1131 }
1132 out
1133}
1134
1135fn resolve_absolute_offset(
1145 chunked_id: &Hash,
1146 index: u32,
1147 chunk_len_proofs: &[LenProof],
1148 offset_in_blob: u64,
1149) -> Result<Option<u64>, VerifyError> {
1150 if index == 0 {
1151 if !chunk_len_proofs.is_empty() {
1157 return Err(VerifyError::UnexpectedLengthProofs);
1158 }
1159 return Ok(Some(offset_in_blob));
1160 }
1161 if chunk_len_proofs.is_empty() {
1162 return Ok(None);
1163 }
1164 let mut by_index: BTreeMap<u32, u32> = BTreeMap::new();
1165 for lp in chunk_len_proofs {
1166 if lp.index >= index || by_index.contains_key(&lp.index) {
1167 return Err(VerifyError::IncompleteLengthProofSet(index));
1168 }
1169 merkle::verify_chunk(chunked_id, &lp.chunk_id, lp.index + 1, &lp.proof)?;
1170 let len_j = verify_blob_len_proof(&lp.chunk_id, &lp.slice)?;
1171 by_index.insert(lp.index, len_j);
1172 }
1173 if u32::try_from(by_index.len()).unwrap_or(u32::MAX) != index {
1174 return Err(VerifyError::IncompleteLengthProofSet(index));
1176 }
1177 let sum: u64 = by_index.values().map(|&l| u64::from(l)).sum();
1178 let absolute = sum
1179 .checked_add(offset_in_blob)
1180 .ok_or(VerifyError::OffsetOverflow)?;
1181 Ok(Some(absolute))
1182}
1183
1184fn compose_payload(leaf_id: Hash, payload: PayloadWire) -> Result<DisclosedPayload, VerifyError> {
1185 match payload {
1186 PayloadWire::Object { bytes } => {
1187 verify_object_id(&bytes, &leaf_id)?;
1188 Ok(DisclosedPayload::Object { bytes })
1189 }
1190 PayloadWire::Chunk {
1191 total_size,
1192 chunk_size,
1193 index,
1194 inner_root,
1195 proof,
1196 bytes,
1197 } => {
1198 let chunk_hash = hash(&bytes);
1199 verify_chunk_with_declared_root(
1200 &leaf_id,
1201 &inner_root,
1202 total_size,
1203 chunk_size,
1204 &chunk_hash,
1205 index,
1206 &proof,
1207 )?;
1208 Ok(DisclosedPayload::Chunk {
1209 total_size,
1210 chunk_size,
1211 index,
1212 bytes,
1213 })
1214 }
1215 PayloadWire::Range {
1216 chunk: None,
1217 offset_in_blob,
1218 len,
1219 slice,
1220 chunk_len_proofs,
1221 } => {
1222 if !chunk_len_proofs.is_empty() {
1223 return Err(VerifyError::UnexpectedLengthProofs);
1224 }
1225 let bytes = verify_blob_slice(&leaf_id, offset_in_blob, len, &slice)?;
1226 Ok(DisclosedPayload::Range {
1227 blob_id: leaf_id,
1228 chunk: None,
1229 offset_in_blob,
1230 absolute_offset: Some(offset_in_blob),
1231 bytes,
1232 })
1233 }
1234 PayloadWire::Range {
1235 chunk: Some(hdr),
1236 offset_in_blob,
1237 len,
1238 slice,
1239 chunk_len_proofs,
1240 } => {
1241 if len == 0 {
1245 return Err(VerifyError::ZeroLengthRange);
1246 }
1247 verify_chunk_with_declared_root(
1248 &leaf_id,
1249 &hdr.inner_root,
1250 hdr.total_size,
1251 hdr.chunk_size,
1252 &hdr.chunk_id,
1253 hdr.index,
1254 &hdr.proof,
1255 )?;
1256 let bytes = verify_blob_slice(&hdr.chunk_id, offset_in_blob, len, &slice)?;
1257 let absolute_offset =
1258 resolve_absolute_offset(&leaf_id, hdr.index, &chunk_len_proofs, offset_in_blob)?;
1259 Ok(DisclosedPayload::Range {
1260 blob_id: hdr.chunk_id,
1261 chunk: Some((hdr.index, hdr.total_size, hdr.chunk_size)),
1262 offset_in_blob,
1263 absolute_offset,
1264 bytes,
1265 })
1266 }
1267 }
1268}
1269
1270pub fn verify_disclosure(commit_id: &Hash, bundle: &[u8]) -> Result<Disclosed, VerifyError> {
1279 let (wire_commit_id, commit_bytes, steps, payload) = decode_disclosure(bundle)?;
1280 if wire_commit_id != *commit_id {
1281 return Err(VerifyError::CommitIdMismatch);
1282 }
1283 let verified = verify_path(commit_id, &commit_bytes, &steps)?;
1284 let step_inner_roots: Vec<Hash> = steps.iter().map(|s| s.inner_root).collect();
1285 let chunk_inner_root = match &payload {
1286 PayloadWire::Chunk { inner_root, .. } => Some(*inner_root),
1287 PayloadWire::Range {
1288 chunk: Some(hdr), ..
1289 } => Some(hdr.inner_root),
1290 _ => None,
1291 };
1292 let payload = compose_payload(verified.leaf_id, payload)?;
1293 Ok(Disclosed {
1294 commit_id: *commit_id,
1295 tree_hash: verified.tree_hash,
1296 path: verified.path,
1297 leaf_id: verified.leaf_id,
1298 payload,
1299 signer: verified.signer,
1300 signature_valid: verified.signature_valid,
1301 step_inner_roots,
1302 chunk_inner_root,
1303 })
1304}
1305
1306pub(crate) fn verify_disclosure_reusing_context(
1309 commit_id: &Hash,
1310 bundle: &[u8],
1311 context: &CommitContext,
1312) -> Result<Disclosed, VerifyError> {
1313 let (wire_commit_id, commit_bytes, steps, payload) = decode_disclosure(bundle)?;
1314 if wire_commit_id != *commit_id {
1315 return Err(VerifyError::CommitIdMismatch);
1316 }
1317 let verified = verify_path_reusing_context(commit_id, &commit_bytes, &steps, context)?;
1318 let step_inner_roots = steps.iter().map(|s| s.inner_root).collect();
1319 let chunk_inner_root = match &payload {
1320 PayloadWire::Chunk { inner_root, .. } => Some(*inner_root),
1321 PayloadWire::Range {
1322 chunk: Some(hdr), ..
1323 } => Some(hdr.inner_root),
1324 _ => None,
1325 };
1326 let payload = compose_payload(verified.leaf_id, payload)?;
1327 Ok(Disclosed {
1328 commit_id: *commit_id,
1329 tree_hash: verified.tree_hash,
1330 path: verified.path,
1331 leaf_id: verified.leaf_id,
1332 payload,
1333 signer: verified.signer,
1334 signature_valid: verified.signature_valid,
1335 step_inner_roots,
1336 chunk_inner_root,
1337 })
1338}
1339
1340fn extract_bao_slice(bytes: &[u8], bao_offset: u64, len: u64) -> Result<Vec<u8>, VerifyError> {
1347 let (outboard, _root) = bao::encode::outboard(bytes);
1348 let mut extractor = bao::encode::SliceExtractor::new_outboard(
1349 std::io::Cursor::new(bytes),
1350 std::io::Cursor::new(outboard),
1351 bao_offset,
1352 len,
1353 );
1354 let mut out = Vec::new();
1355 extractor
1356 .read_to_end(&mut out)
1357 .map_err(|e| VerifyError::Bao(e.to_string()))?;
1358 Ok(out)
1359}
1360
1361pub fn build_disclosure(
1376 store: &crate::store::ObjectStore,
1377 commit_id: &Hash,
1378 path: &[&[u8]],
1379 selector: Selector,
1380) -> Result<Vec<u8>, VerifyError> {
1381 build_disclosure_from(store, commit_id, path, selector)
1382}
1383
1384pub fn build_disclosure_from<S: crate::store::ObjectSource + ?Sized>(
1414 source: &S,
1415 commit_id: &Hash,
1416 path: &[&[u8]],
1417 selector: Selector,
1418) -> Result<Vec<u8>, VerifyError> {
1419 let (commit_bytes, steps, leaf_id) = build_prefix(source, commit_id, path)?;
1420 let payload = build_payload(source, &leaf_id, selector)?;
1421 Ok(encode_disclosure(
1422 commit_id,
1423 &commit_bytes,
1424 &steps,
1425 &payload,
1426 ))
1427}
1428
1429fn build_prefix<S: crate::store::ObjectSource + ?Sized>(
1430 source: &S,
1431 commit_id: &Hash,
1432 path: &[&[u8]],
1433) -> Result<(Vec<u8>, Vec<Step>, Hash), VerifyError> {
1434 if path.len() > MAX_TREE_DEPTH {
1435 return Err(VerifyError::TooManySteps(path.len()));
1436 }
1437 let commit_bytes = source.read(commit_id)?;
1438 let commit_obj = crate::serialize::deserialize(&commit_bytes)?;
1439 let tree_hash = match &commit_obj {
1440 Object::Commit(c) => c.tree_hash,
1441 Object::Remix(r) => r.tree_hash,
1442 other => return Err(VerifyError::NotACommitOrRemix(other.object_type())),
1443 };
1444
1445 let mut steps = Vec::with_capacity(path.len());
1446 let mut current_tree_id = tree_hash;
1447 let mut leaf_id = tree_hash;
1448 for (i, &name) in path.iter().enumerate() {
1449 let Object::Tree(tree) = source.read_object(¤t_tree_id)? else {
1450 return Err(VerifyError::PathThroughNonTree);
1451 };
1452 let position =
1453 merkle::tree_entry_position(&tree, name).ok_or(VerifyError::PathNotFound(i))?;
1454 let entry = tree.entries[position as usize].clone();
1455 let proof = merkle::build_tree_entry_proof(&tree, position)?;
1456 steps.push(Step {
1457 name: name.to_vec(),
1458 mode: entry.mode,
1459 child_id: entry.object_hash,
1460 inner_root: merkle::tree_inner_root(&tree),
1461 position,
1462 proof,
1463 });
1464 leaf_id = entry.object_hash;
1465 let is_last = i + 1 == path.len();
1466 if entry.mode == EntryMode::Tree {
1467 current_tree_id = entry.object_hash;
1468 } else if !is_last {
1469 return Err(VerifyError::PathThroughNonTree);
1470 }
1471 }
1472
1473 Ok((commit_bytes, steps, leaf_id))
1474}
1475
1476fn build_payload<S: crate::store::ObjectSource + ?Sized>(
1477 source: &S,
1478 leaf_id: &Hash,
1479 selector: Selector,
1480) -> Result<PayloadWire, VerifyError> {
1481 match selector {
1482 Selector::Object => {
1483 let bytes = source.read(leaf_id)?;
1484 Ok(PayloadWire::Object { bytes })
1485 }
1486 Selector::Chunk(index) => {
1487 let Object::ChunkedBlob(cb) = source.read_object(leaf_id)? else {
1488 return Err(VerifyError::SelectorLeafMismatch);
1489 };
1490 let leaf_count = u32::try_from(cb.chunks.len())
1491 .ok()
1492 .and_then(|n| n.checked_add(1))
1493 .ok_or(VerifyError::TooManyChunks)?;
1494 let chunk_id = *cb
1495 .chunks
1496 .get(index as usize)
1497 .ok_or(VerifyError::ChunkIndexOutOfRange { index, leaf_count })?;
1498 let position = index
1499 .checked_add(1)
1500 .ok_or(VerifyError::ChunkIndexOutOfRange { index, leaf_count })?;
1501 let proof = merkle::build_chunks_multi_proof(&cb, [0, position])?;
1502 let bytes = source.read(&chunk_id)?;
1503 Ok(PayloadWire::Chunk {
1504 total_size: cb.total_size,
1505 chunk_size: cb.chunk_size,
1506 index,
1507 inner_root: merkle::chunked_inner_root(&cb),
1508 proof,
1509 bytes,
1510 })
1511 }
1512 Selector::Range {
1513 offset,
1514 len,
1515 with_offsets,
1516 } => {
1517 if len == 0 {
1518 return Err(VerifyError::ZeroLengthRange);
1519 }
1520 match source.read_object(leaf_id)? {
1521 Object::Blob(b) => {
1522 let end = offset.checked_add(len).ok_or(VerifyError::OffsetOverflow)?;
1523 if end > b.data.len() as u64 {
1524 return Err(VerifyError::RangeOutOfBounds);
1525 }
1526 let canonical = source.read(leaf_id)?;
1527 let bao_offset = offset.checked_add(10).ok_or(VerifyError::OffsetOverflow)?;
1528 let slice = extract_bao_slice(&canonical, bao_offset, len)?;
1529 Ok(PayloadWire::Range {
1530 chunk: None,
1531 offset_in_blob: offset,
1532 len,
1533 slice,
1534 chunk_len_proofs: Vec::new(),
1535 })
1536 }
1537 Object::ChunkedBlob(cb) => {
1538 build_chunked_range_payload(source, &cb, offset, len, with_offsets)
1539 }
1540 _ => Err(VerifyError::SelectorLeafMismatch),
1541 }
1542 }
1543 }
1544}
1545
1546fn build_chunked_range_payload<S: crate::store::ObjectSource + ?Sized>(
1547 source: &S,
1548 cb: &crate::object::ChunkedBlob,
1549 offset: u64,
1550 len: u64,
1551 with_offsets: bool,
1552) -> Result<PayloadWire, VerifyError> {
1553 let chunk_bytes: Vec<Vec<u8>> = cb
1558 .chunks
1559 .iter()
1560 .map(|id| source.read(id))
1561 .collect::<Result<_, _>>()?;
1562
1563 let mut cumulative: u64 = 0;
1567 let mut located = None;
1568 for (idx, bytes) in chunk_bytes.iter().enumerate() {
1569 let content_len = bytes
1572 .len()
1573 .checked_sub(10)
1574 .ok_or(VerifyError::Decode(MkitError::UnexpectedEof))? as u64;
1575 let chunk_end = cumulative
1576 .checked_add(content_len)
1577 .ok_or(VerifyError::OffsetOverflow)?;
1578 if offset < chunk_end {
1579 located = Some((idx, cumulative, content_len));
1580 break;
1581 }
1582 cumulative = chunk_end;
1583 }
1584 let (index, chunk_start, content_len) = located.ok_or(VerifyError::RangeOutOfBounds)?;
1585 let offset_in_chunk = offset
1586 .checked_sub(chunk_start)
1587 .ok_or(VerifyError::OffsetOverflow)?;
1588 let range_end = offset_in_chunk
1589 .checked_add(len)
1590 .ok_or(VerifyError::OffsetOverflow)?;
1591 if range_end > content_len {
1592 return Err(VerifyError::RangeCrossesChunkBoundary);
1593 }
1594
1595 let index_u32 = u32::try_from(index).map_err(|_| VerifyError::TooManyChunks)?;
1596 let position = index_u32.checked_add(1).ok_or(VerifyError::TooManyChunks)?;
1597 let proof = merkle::build_chunks_multi_proof(cb, [0, position])?;
1598 let bao_offset = offset_in_chunk
1599 .checked_add(10)
1600 .ok_or(VerifyError::OffsetOverflow)?;
1601 let slice = extract_bao_slice(&chunk_bytes[index], bao_offset, len)?;
1602
1603 let mut chunk_len_proofs = Vec::new();
1604 if with_offsets {
1605 for (j, preceding_bytes) in chunk_bytes.iter().enumerate().take(index) {
1606 let j_u32 = u32::try_from(j).map_err(|_| VerifyError::TooManyChunks)?;
1607 let position_j = j_u32.checked_add(1).ok_or(VerifyError::TooManyChunks)?;
1608 let proof_j = merkle::build_chunk_proof(cb, position_j)?;
1609 let slice_j = extract_bao_slice(preceding_bytes, 0, 10)?;
1610 chunk_len_proofs.push(LenProof {
1611 index: j_u32,
1612 chunk_id: cb.chunks[j],
1613 proof: proof_j,
1614 slice: slice_j,
1615 });
1616 }
1617 }
1618
1619 Ok(PayloadWire::Range {
1620 chunk: Some(ChunkHdr {
1621 total_size: cb.total_size,
1622 chunk_size: cb.chunk_size,
1623 index: index_u32,
1624 inner_root: merkle::chunked_inner_root(cb),
1625 chunk_id: cb.chunks[index],
1626 proof,
1627 }),
1628 offset_in_blob: offset_in_chunk,
1629 len,
1630 slice,
1631 chunk_len_proofs,
1632 })
1633}
1634
1635#[cfg(test)]
1640#[allow(clippy::unwrap_used)] mod tests {
1642 use super::*;
1643 use crate::hash::ZERO;
1644 use crate::layout::RepoLayout;
1645 use crate::object::{Commit, Identity, Tree};
1646 use crate::sign::{KeyPair, sign_commit};
1647 use crate::store::ObjectStore;
1648 use crate::worktree::store_file_object;
1649
1650 struct Fixture {
1656 _dir: tempfile::TempDir,
1657 store: ObjectStore,
1658 commit_id: Hash,
1659 chunked_bytes: Vec<u8>,
1660 }
1661
1662 fn build_fixture() -> Fixture {
1663 let dir = tempfile::TempDir::new().expect("tempdir");
1664 let store = ObjectStore::init(&RepoLayout::single(dir.path())).expect("store init");
1665
1666 let shallow = store_file_object(&store, b"shallow file content").unwrap();
1667 let deep_file = store_file_object(&store, b"three levels deep").unwrap();
1668 let exec = store_file_object(&store, b"#!/bin/sh\necho hi\n").unwrap();
1669
1670 let mut chunked_bytes = vec![0u8; 2 * 1024 * 1024];
1673 let mut x: u64 = 0x1234_5678_9abc_def0;
1674 for b in &mut chunked_bytes {
1675 x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1676 *b = (x >> 56) as u8;
1677 }
1678 let chunked_id = store_file_object(&store, &chunked_bytes).unwrap();
1679
1680 let deep_tree = Tree {
1681 entries: vec![TreeEntry {
1682 name: b"deep.txt".to_vec(),
1683 mode: EntryMode::Blob,
1684 object_hash: deep_file,
1685 }],
1686 };
1687 let deep_tree_id = store
1688 .write(&crate::serialize::serialize(&Object::Tree(deep_tree)).unwrap())
1689 .unwrap();
1690
1691 let mid_tree = Tree {
1692 entries: vec![TreeEntry {
1693 name: b"deep".to_vec(),
1694 mode: EntryMode::Tree,
1695 object_hash: deep_tree_id,
1696 }],
1697 };
1698 let mid_tree_id = store
1699 .write(&crate::serialize::serialize(&Object::Tree(mid_tree)).unwrap())
1700 .unwrap();
1701
1702 let root_tree = Tree {
1703 entries: vec![
1704 TreeEntry {
1705 name: b"chunked.bin".to_vec(),
1706 mode: EntryMode::Blob,
1707 object_hash: chunked_id,
1708 },
1709 TreeEntry {
1710 name: b"exec.sh".to_vec(),
1711 mode: EntryMode::Executable,
1712 object_hash: exec,
1713 },
1714 TreeEntry {
1715 name: b"shallow.txt".to_vec(),
1716 mode: EntryMode::Blob,
1717 object_hash: shallow,
1718 },
1719 TreeEntry {
1720 name: b"sub".to_vec(),
1721 mode: EntryMode::Tree,
1722 object_hash: mid_tree_id,
1723 },
1724 ],
1725 };
1726 let tree_hash = store
1727 .write(&crate::serialize::serialize(&Object::Tree(root_tree)).unwrap())
1728 .unwrap();
1729
1730 let kp = KeyPair::from_seed([0x07; 32]);
1731 let mut commit = Commit {
1732 tree_hash,
1733 parents: vec![],
1734 author: Identity::ed25519(kp.public.0),
1735 signer: kp.public.0,
1736 message: b"disclosure fixture".to_vec(),
1737 timestamp: 1_726_300_000,
1738 message_hash: ZERO,
1739 content_digest: ZERO,
1740 signature: [0u8; 64],
1741 };
1742 commit.signature = sign_commit(&commit, &kp).unwrap().0;
1743 let commit_bytes = crate::serialize::serialize(&Object::Commit(commit)).unwrap();
1744 let commit_id = store.write(&commit_bytes).unwrap();
1745
1746 Fixture {
1747 _dir: dir,
1748 store,
1749 commit_id,
1750 chunked_bytes,
1751 }
1752 }
1753
1754 #[test]
1755 #[allow(clippy::too_many_lines)] fn round_trips_every_selector() {
1757 let f = build_fixture();
1758
1759 let bundle = build_disclosure(&f.store, &f.commit_id, &[], Selector::Object).unwrap();
1761 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1762 assert!(d.path.is_empty());
1763 assert_eq!(d.leaf_id, d.tree_hash);
1764 assert!(d.signature_valid);
1765
1766 let bundle =
1768 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
1769 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1770 assert_eq!(d.path, vec![(b"shallow.txt".to_vec(), EntryMode::Blob)]);
1771 let DisclosedPayload::Object { bytes } = d.payload else {
1772 panic!("expected Object payload");
1773 };
1774 assert_eq!(
1775 bytes,
1776 crate::serialize::serialize(&Object::Blob(crate::object::Blob {
1777 data: b"shallow file content".to_vec()
1778 }))
1779 .unwrap()
1780 );
1781
1782 let bundle = build_disclosure(
1784 &f.store,
1785 &f.commit_id,
1786 &[b"sub", b"deep", b"deep.txt"],
1787 Selector::Object,
1788 )
1789 .unwrap();
1790 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1791 assert_eq!(
1792 d.path,
1793 vec![
1794 (b"sub".to_vec(), EntryMode::Tree),
1795 (b"deep".to_vec(), EntryMode::Tree),
1796 (b"deep.txt".to_vec(), EntryMode::Blob),
1797 ]
1798 );
1799
1800 let bundle =
1802 build_disclosure(&f.store, &f.commit_id, &[b"exec.sh"], Selector::Object).unwrap();
1803 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1804 assert_eq!(d.path, vec![(b"exec.sh".to_vec(), EntryMode::Executable)]);
1805
1806 let bundle = build_disclosure(
1808 &f.store,
1809 &f.commit_id,
1810 &[b"chunked.bin"],
1811 Selector::Chunk(1),
1812 )
1813 .unwrap();
1814 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1815 let DisclosedPayload::Chunk { index, .. } = d.payload else {
1816 panic!("expected Chunk payload");
1817 };
1818 assert_eq!(index, 1);
1819
1820 let bundle = build_disclosure(
1822 &f.store,
1823 &f.commit_id,
1824 &[b"chunked.bin"],
1825 Selector::Range {
1826 offset: 200_000,
1827 len: 64,
1828 with_offsets: false,
1829 },
1830 )
1831 .unwrap();
1832 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1833 let DisclosedPayload::Range {
1834 chunk,
1835 absolute_offset,
1836 bytes,
1837 ..
1838 } = d.payload
1839 else {
1840 panic!("expected Range payload");
1841 };
1842 assert!(chunk.is_some());
1843 assert_eq!(bytes.len(), 64);
1844
1845 let bundle_off = build_disclosure(
1848 &f.store,
1849 &f.commit_id,
1850 &[b"chunked.bin"],
1851 Selector::Range {
1852 offset: 200_000,
1853 len: 64,
1854 with_offsets: true,
1855 },
1856 )
1857 .unwrap();
1858 let d_off = verify_disclosure(&f.commit_id, &bundle_off).unwrap();
1859 let DisclosedPayload::Range {
1860 absolute_offset: abs_off,
1861 bytes: bytes_off,
1862 chunk: chunk_off,
1863 ..
1864 } = d_off.payload
1865 else {
1866 panic!("expected Range payload");
1867 };
1868 assert_ne!(chunk_off.map(|(idx, _, _)| idx), Some(0));
1869 assert_eq!(abs_off, Some(200_000));
1870 assert_eq!(bytes_off, f.chunked_bytes[200_000..200_064]);
1871 assert_eq!(bytes, bytes_off);
1874 assert_eq!(absolute_offset, None);
1875
1876 let bundle = build_disclosure(
1878 &f.store,
1879 &f.commit_id,
1880 &[b"shallow.txt"],
1881 Selector::Range {
1882 offset: 0,
1883 len: "shallow file content".len() as u64,
1884 with_offsets: false,
1885 },
1886 )
1887 .unwrap();
1888 let d = verify_disclosure(&f.commit_id, &bundle).unwrap();
1889 let DisclosedPayload::Range { bytes, chunk, .. } = d.payload else {
1890 panic!("expected Range payload");
1891 };
1892 assert!(chunk.is_none());
1893 assert_eq!(bytes, b"shallow file content");
1894 }
1895
1896 #[test]
1897 fn verify_object_id_rejects_mismatch() {
1898 let f = build_fixture();
1899 let bytes = f
1900 .store
1901 .read(&f.commit_id)
1902 .expect("commit_id was just written");
1903 assert!(matches!(
1904 verify_object_id(&bytes, &ZERO),
1905 Err(VerifyError::PayloadIdMismatch)
1906 ));
1907 verify_object_id(&bytes, &f.commit_id).expect("matches itself");
1908 }
1909
1910 #[test]
1911 fn verify_path_rejects_wrong_commit_bytes_hash() {
1912 let f = build_fixture();
1913 let commit_bytes = f.store.read(&f.commit_id).unwrap();
1914 assert!(matches!(
1915 verify_path(&ZERO, &commit_bytes, &[]),
1916 Err(VerifyError::CommitBytesHashMismatch)
1917 ));
1918 }
1919
1920 #[test]
1921 fn verify_path_rejects_non_final_non_tree_mode() {
1922 let f = build_fixture();
1923 let bundle =
1924 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
1925 let (_id, commit_bytes, mut steps, _payload) = decode_disclosure(&bundle).unwrap();
1929 assert_eq!(steps.len(), 1);
1930 let dup = steps[0].clone();
1931 steps.push(dup);
1932 assert!(matches!(
1933 verify_path(&f.commit_id, &commit_bytes, &steps),
1934 Err(VerifyError::NonFinalStepNotTree(0))
1935 ));
1936 }
1937
1938 #[test]
1939 fn verify_path_rejects_too_many_steps() {
1940 let f = build_fixture();
1941 let bundle =
1942 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
1943 let (_id, commit_bytes, steps, _payload) = decode_disclosure(&bundle).unwrap();
1944 let too_many: Vec<Step> =
1945 std::iter::repeat_n(steps[0].clone(), MAX_TREE_DEPTH + 1).collect();
1946 assert!(matches!(
1947 verify_path(&f.commit_id, &commit_bytes, &too_many),
1948 Err(VerifyError::TooManySteps(_))
1949 ));
1950 }
1951
1952 #[test]
1953 fn verify_path_rejects_invalid_entry_name() {
1954 let f = build_fixture();
1955 let bundle =
1956 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
1957 let (_id, commit_bytes, mut steps, _payload) = decode_disclosure(&bundle).unwrap();
1958 steps[0].name = b"trailing space ".to_vec();
1959 assert!(matches!(
1960 verify_path(&f.commit_id, &commit_bytes, &steps),
1961 Err(VerifyError::InvalidEntryName(0))
1962 ));
1963 }
1964
1965 #[test]
1966 fn verify_path_rejects_swapped_step_proof() {
1967 let f = build_fixture();
1968 let bundle = build_disclosure(
1969 &f.store,
1970 &f.commit_id,
1971 &[b"sub", b"deep", b"deep.txt"],
1972 Selector::Object,
1973 )
1974 .unwrap();
1975 let (_id, commit_bytes, mut steps, _payload) = decode_disclosure(&bundle).unwrap();
1976 assert_eq!(steps.len(), 3);
1977 steps.swap(0, 1);
1978 assert!(matches!(
1979 verify_path(&f.commit_id, &commit_bytes, &steps),
1980 Err(VerifyError::InnerRootMismatch { .. } | VerifyError::Merkle(_))
1981 ));
1982 }
1983
1984 #[test]
1985 fn verify_path_rejects_non_commit_non_remix() {
1986 let f = build_fixture();
1987 let tag = crate::object::Tag {
1989 target: f.commit_id,
1990 target_type: ObjectType::Commit,
1991 name: b"v1".to_vec(),
1992 tagger: Identity::ed25519([9u8; 32]),
1993 signer: [9u8; 32],
1994 message: Vec::new(),
1995 timestamp: 0,
1996 signature: [0u8; 64],
1997 };
1998 let tag_bytes = crate::serialize::serialize(&Object::Tag(tag)).unwrap();
1999 let tag_id = crate::hash::hash(&tag_bytes);
2000 assert!(matches!(
2001 verify_path(&tag_id, &tag_bytes, &[]),
2002 Err(VerifyError::NotACommitOrRemix(ObjectType::Tag))
2003 ));
2004 }
2005
2006 #[test]
2007 fn verify_chunk_with_meta_bounds() {
2008 let f = build_fixture();
2009 let bundle = build_disclosure(
2010 &f.store,
2011 &f.commit_id,
2012 &[b"chunked.bin"],
2013 Selector::Chunk(1),
2014 )
2015 .unwrap();
2016 let (_id, _commit_bytes, steps, payload) = decode_disclosure(&bundle).unwrap();
2017 let leaf_id = steps[0].child_id;
2018 let PayloadWire::Chunk {
2019 total_size,
2020 chunk_size,
2021 index,
2022 proof,
2023 bytes,
2024 ..
2025 } = payload
2026 else {
2027 panic!("expected Chunk payload");
2028 };
2029 let chunk_hash = hash(&bytes);
2030
2031 verify_chunk_with_meta(&leaf_id, total_size, chunk_size, &chunk_hash, index, &proof)
2032 .expect("freshly built chunk proof must verify");
2033 assert!(matches!(
2035 verify_chunk_with_meta(
2036 &leaf_id,
2037 total_size + 1,
2038 chunk_size,
2039 &chunk_hash,
2040 index,
2041 &proof
2042 ),
2043 Err(VerifyError::Merkle(_))
2044 ));
2045 assert!(matches!(
2047 verify_chunk_with_meta(
2048 &leaf_id,
2049 total_size,
2050 chunk_size,
2051 &chunk_hash,
2052 proof.leaf_count,
2053 &proof
2054 ),
2055 Err(VerifyError::ChunkIndexOutOfRange { .. })
2056 ));
2057 }
2058
2059 #[test]
2060 fn verify_blob_slice_and_len_proof_round_trip() {
2061 let content = b"the quick brown fox jumps over the lazy dog";
2062 let canonical = {
2063 let prologue = crate::serialize::blob_prologue(content.len()).unwrap();
2064 let mut v = prologue.to_vec();
2065 v.extend_from_slice(content);
2066 v
2067 };
2068 let blob_id = crate::hash::hash(&canonical);
2069 let (outboard, _root) = bao::encode::outboard(&canonical);
2070
2071 let mut extractor = bao::encode::SliceExtractor::new_outboard(
2072 std::io::Cursor::new(&canonical),
2073 std::io::Cursor::new(&outboard),
2074 10, content.len() as u64,
2076 );
2077 let mut slice = Vec::new();
2078 std::io::Read::read_to_end(&mut extractor, &mut slice).unwrap();
2079
2080 let got = verify_blob_slice(&blob_id, 0, content.len() as u64, &slice).unwrap();
2081 assert_eq!(got, content);
2082
2083 assert!(matches!(
2085 verify_blob_slice(&blob_id, 0, 0, &slice),
2086 Err(VerifyError::ZeroLengthRange)
2087 ));
2088
2089 let mut len_extractor = bao::encode::SliceExtractor::new_outboard(
2091 std::io::Cursor::new(&canonical),
2092 std::io::Cursor::new(&outboard),
2093 0,
2094 10,
2095 );
2096 let mut len_slice = Vec::new();
2097 std::io::Read::read_to_end(&mut len_extractor, &mut len_slice).unwrap();
2098 let len = verify_blob_len_proof(&blob_id, &len_slice).unwrap();
2099 assert_eq!(len as usize, content.len());
2100 }
2101
2102 #[test]
2103 fn zero_length_range_rejected_end_to_end() {
2104 let f = build_fixture();
2105 assert!(matches!(
2106 build_disclosure(
2107 &f.store,
2108 &f.commit_id,
2109 &[b"shallow.txt"],
2110 Selector::Range {
2111 offset: 0,
2112 len: 0,
2113 with_offsets: false
2114 }
2115 ),
2116 Err(VerifyError::ZeroLengthRange)
2117 ));
2118 }
2119
2120 #[test]
2121 fn range_crossing_chunk_boundary_rejected() {
2122 let f = build_fixture();
2123 assert!(matches!(
2126 build_disclosure(
2127 &f.store,
2128 &f.commit_id,
2129 &[b"chunked.bin"],
2130 Selector::Range {
2131 offset: 0,
2132 len: f.chunked_bytes.len() as u64,
2133 with_offsets: false,
2134 }
2135 ),
2136 Err(VerifyError::RangeCrossesChunkBoundary)
2137 ));
2138 }
2139
2140 #[test]
2141 fn incomplete_length_proof_set_is_rejected() {
2142 let f = build_fixture();
2143 let bundle = build_disclosure(
2144 &f.store,
2145 &f.commit_id,
2146 &[b"chunked.bin"],
2147 Selector::Range {
2148 offset: 700_000,
2149 len: 16,
2150 with_offsets: true,
2151 },
2152 )
2153 .unwrap();
2154 let (commit_id, commit_bytes, steps, payload) = decode_disclosure(&bundle).unwrap();
2155 let PayloadWire::Range {
2156 chunk,
2157 offset_in_blob,
2158 len,
2159 slice,
2160 mut chunk_len_proofs,
2161 } = payload
2162 else {
2163 panic!("expected Range payload");
2164 };
2165 let hdr = chunk.clone().expect("chunked leaf");
2166 assert!(hdr.index > 0, "test fixture assumption: chunk index > 0");
2167 chunk_len_proofs.remove(0);
2169 let tampered = encode_disclosure(
2170 &commit_id,
2171 &commit_bytes,
2172 &steps,
2173 &PayloadWire::Range {
2174 chunk,
2175 offset_in_blob,
2176 len,
2177 slice,
2178 chunk_len_proofs,
2179 },
2180 );
2181 assert!(matches!(
2182 verify_disclosure(&f.commit_id, &tampered),
2183 Err(VerifyError::IncompleteLengthProofSet(_))
2184 ));
2185 }
2186
2187 #[test]
2188 fn resolve_absolute_offset_rejects_overflow() {
2189 let f = build_fixture();
2193 let bundle = build_disclosure(
2194 &f.store,
2195 &f.commit_id,
2196 &[b"chunked.bin"],
2197 Selector::Range {
2198 offset: 700_000,
2199 len: 16,
2200 with_offsets: true,
2201 },
2202 )
2203 .unwrap();
2204 let (_id, _commit_bytes, steps, payload) = decode_disclosure(&bundle).unwrap();
2205 let leaf_id = steps[0].child_id;
2206 let PayloadWire::Range {
2207 chunk,
2208 offset_in_blob,
2209 chunk_len_proofs,
2210 ..
2211 } = payload
2212 else {
2213 panic!("expected Range payload");
2214 };
2215 let hdr = chunk.expect("chunked leaf");
2216 assert!(hdr.index > 0, "test fixture assumption: chunk index > 0");
2217
2218 assert!(matches!(
2219 resolve_absolute_offset(&leaf_id, hdr.index, &chunk_len_proofs, u64::MAX),
2220 Err(VerifyError::OffsetOverflow)
2221 ));
2222
2223 resolve_absolute_offset(&leaf_id, hdr.index, &chunk_len_proofs, offset_in_blob)
2226 .expect("freshly built length proofs must resolve");
2227 }
2228
2229 #[test]
2230 fn len_proofs_on_chunk0_are_rejected() {
2231 let f = build_fixture();
2237 let bundle = build_disclosure(
2238 &f.store,
2239 &f.commit_id,
2240 &[b"chunked.bin"],
2241 Selector::Range {
2242 offset: 0,
2243 len: 8,
2244 with_offsets: false,
2245 },
2246 )
2247 .unwrap();
2248 let (commit_id, commit_bytes, steps, payload) = decode_disclosure(&bundle).unwrap();
2249 let PayloadWire::Range {
2250 chunk,
2251 offset_in_blob,
2252 len,
2253 slice,
2254 chunk_len_proofs,
2255 } = payload
2256 else {
2257 panic!("expected Range payload");
2258 };
2259 let hdr = chunk.clone().expect("chunked leaf");
2260 assert_eq!(hdr.index, 0, "test fixture assumption: offset 0 is chunk 0");
2261 assert!(
2262 chunk_len_proofs.is_empty(),
2263 "builder never emits proofs for chunk 0"
2264 );
2265
2266 let forged = vec![LenProof {
2270 index: 0,
2271 chunk_id: [0u8; 32],
2272 proof: Proof::default(),
2273 slice: Vec::new(),
2274 }];
2275 let tampered = encode_disclosure(
2276 &commit_id,
2277 &commit_bytes,
2278 &steps,
2279 &PayloadWire::Range {
2280 chunk,
2281 offset_in_blob,
2282 len,
2283 slice,
2284 chunk_len_proofs: forged,
2285 },
2286 );
2287 assert!(matches!(
2288 verify_disclosure(&f.commit_id, &tampered),
2289 Err(VerifyError::UnexpectedLengthProofs)
2290 ));
2291 }
2292
2293 #[test]
2294 fn decode_rejects_bad_magic_version_and_trailing_bytes() {
2295 let f = build_fixture();
2296 let bundle =
2297 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
2298
2299 let mut bad_magic = bundle.clone();
2300 bad_magic[0] = b'X';
2301 assert!(matches!(
2302 verify_disclosure(&f.commit_id, &bad_magic),
2303 Err(VerifyError::BadMagic)
2304 ));
2305
2306 let mut bad_version = bundle.clone();
2307 bad_version[4] = 1;
2308 assert!(matches!(
2309 verify_disclosure(&f.commit_id, &bad_version),
2310 Err(VerifyError::UnsupportedBundleVersion(1))
2311 ));
2312
2313 let mut trailing = bundle.clone();
2314 trailing.push(0);
2315 assert!(matches!(
2316 verify_disclosure(&f.commit_id, &trailing),
2317 Err(VerifyError::Malformed)
2318 ));
2319
2320 let oversize = vec![0u8; MAX_BUNDLE_BYTES + 1];
2321 assert!(matches!(
2322 verify_disclosure(&f.commit_id, &oversize),
2323 Err(VerifyError::BundleTooLarge)
2324 ));
2325 }
2326
2327 #[test]
2328 fn decode_rejects_unknown_payload_kind() {
2329 let f = build_fixture();
2330 let bundle =
2331 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
2332 let (commit_id, commit_bytes, steps, _payload) = decode_disclosure(&bundle).unwrap();
2333
2334 let mut prefix = Vec::new();
2338 prefix.extend_from_slice(BUNDLE_MAGIC);
2339 prefix.push(BUNDLE_VERSION);
2340 commit_id.write(&mut prefix);
2341 commit_bytes.as_slice().write(&mut prefix);
2342 steps.as_slice().write(&mut prefix);
2343 let kind_offset = prefix.len();
2344 assert_eq!(bundle[kind_offset], 0, "Object payload_kind must be 0");
2345
2346 let mut tampered = bundle.clone();
2347 tampered[kind_offset] = 0xFF;
2348 assert!(matches!(
2349 verify_disclosure(&f.commit_id, &tampered),
2350 Err(VerifyError::InvalidPayloadKind(0xFF))
2351 ));
2352 }
2353
2354 #[test]
2355 fn payload_id_mismatch_is_rejected() {
2356 let f = build_fixture();
2357 let bundle =
2358 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
2359 let (commit_id, commit_bytes, steps, payload) = decode_disclosure(&bundle).unwrap();
2360 let PayloadWire::Object { mut bytes } = payload else {
2361 panic!("expected Object payload");
2362 };
2363 *bytes.last_mut().unwrap() ^= 0xFF;
2364 let tampered = encode_disclosure(
2365 &commit_id,
2366 &commit_bytes,
2367 &steps,
2368 &PayloadWire::Object { bytes },
2369 );
2370 assert!(matches!(
2371 verify_disclosure(&f.commit_id, &tampered),
2372 Err(VerifyError::PayloadIdMismatch)
2373 ));
2374 }
2375
2376 #[test]
2377 fn commit_id_mismatch_is_rejected() {
2378 let f = build_fixture();
2379 let bundle =
2380 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
2381 assert!(matches!(
2382 verify_disclosure(&ZERO, &bundle),
2383 Err(VerifyError::CommitIdMismatch)
2384 ));
2385 }
2386
2387 #[test]
2388 fn inner_root_forged_is_rejected() {
2389 let f = build_fixture();
2390 let bundle =
2391 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
2392 let (commit_id, commit_bytes, mut steps, payload) = decode_disclosure(&bundle).unwrap();
2393 steps[0].inner_root[0] ^= 0xFF;
2394 let tampered = encode_disclosure(&commit_id, &commit_bytes, &steps, &payload);
2395 assert!(matches!(
2396 verify_disclosure(&f.commit_id, &tampered),
2397 Err(VerifyError::InnerRootMismatch { .. })
2398 ));
2399 }
2400
2401 #[test]
2402 fn inner_root_fold_mismatch_is_rejected() {
2403 let f = build_fixture();
2404 let nested = build_disclosure(
2405 &f.store,
2406 &f.commit_id,
2407 &[b"sub", b"deep", b"deep.txt"],
2408 Selector::Object,
2409 )
2410 .unwrap();
2411 let shallow =
2412 build_disclosure(&f.store, &f.commit_id, &[b"shallow.txt"], Selector::Object).unwrap();
2413 let (commit_id, commit_bytes, mut steps, payload) = decode_disclosure(&shallow).unwrap();
2414 let (_, _, nested_steps, _) = decode_disclosure(&nested).unwrap();
2415 steps[0].proof = nested_steps[1].proof.clone();
2418 steps[0].position = nested_steps[1].position;
2419 let tampered = encode_disclosure(&commit_id, &commit_bytes, &steps, &payload);
2420 assert!(matches!(
2421 verify_disclosure(&f.commit_id, &tampered),
2422 Err(VerifyError::InnerRootFoldMismatch)
2423 ));
2424 }
2425
2426 use crate::store::StoreError;
2429
2430 struct MapSource(BTreeMap<Hash, Vec<u8>>);
2434
2435 impl MapSource {
2436 fn from_store(store: &ObjectStore) -> Self {
2438 let map = store
2439 .iter_object_hashes()
2440 .unwrap()
2441 .into_iter()
2442 .map(|h| (h, store.read(&h).unwrap()))
2443 .collect();
2444 Self(map)
2445 }
2446 }
2447
2448 impl crate::store::ObjectSource for MapSource {
2449 fn read(&self, h: &Hash) -> crate::store::StoreResult<Vec<u8>> {
2450 let bytes = self
2451 .0
2452 .get(h)
2453 .ok_or_else(|| StoreError::ObjectNotFound(crate::hash::to_hex(h)))?;
2454 verify_object_id(bytes, h).map_err(|_| StoreError::HashMismatch {
2455 expected: crate::hash::to_hex(h),
2456 actual: String::from("<mismatch>"),
2457 })?;
2458 Ok(bytes.clone())
2459 }
2460 }
2461
2462 struct LyingSource<'a> {
2465 inner: &'a MapSource,
2466 target: Hash,
2467 lie: Vec<u8>,
2468 }
2469
2470 impl crate::store::ObjectSource for LyingSource<'_> {
2471 fn read(&self, h: &Hash) -> crate::store::StoreResult<Vec<u8>> {
2472 if *h == self.target {
2473 return Ok(self.lie.clone());
2474 }
2475 crate::store::ObjectSource::read(self.inner, h)
2476 }
2477 }
2478
2479 struct SplitSource<'a> {
2483 inner: &'a MapSource,
2484 target: Hash,
2485 object_lie: Object,
2486 }
2487
2488 impl crate::store::ObjectSource for SplitSource<'_> {
2489 fn read(&self, h: &Hash) -> crate::store::StoreResult<Vec<u8>> {
2490 crate::store::ObjectSource::read(self.inner, h)
2491 }
2492
2493 fn read_object(&self, h: &Hash) -> crate::store::StoreResult<Object> {
2494 if *h == self.target {
2495 return Ok(self.object_lie.clone());
2496 }
2497 crate::store::ObjectSource::read_object(self.inner, h)
2498 }
2499 }
2500
2501 fn entry_id(store: &ObjectStore, tree_id: &Hash, name: &[u8]) -> Hash {
2503 let Object::Tree(tree) = store.read_object(tree_id).unwrap() else {
2504 panic!("expected a tree");
2505 };
2506 tree.entries
2507 .iter()
2508 .find(|e| e.name == name)
2509 .unwrap()
2510 .object_hash
2511 }
2512
2513 fn root_tree_id(store: &ObjectStore, commit_id: &Hash) -> Hash {
2514 let Object::Commit(c) = store.read_object(commit_id).unwrap() else {
2515 panic!("expected a commit");
2516 };
2517 c.tree_hash
2518 }
2519
2520 fn medium_blob_commit(f: &Fixture) -> Hash {
2523 let data: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
2524 let blob = store_file_object(&f.store, &data).unwrap();
2525 assert!(matches!(
2526 f.store.read_object(&blob).unwrap(),
2527 Object::Blob(_)
2528 ));
2529 let tree = Tree {
2530 entries: vec![TreeEntry {
2531 name: b"medium.bin".to_vec(),
2532 mode: EntryMode::Blob,
2533 object_hash: blob,
2534 }],
2535 };
2536 let tree_hash = f
2537 .store
2538 .write(&crate::serialize::serialize(&Object::Tree(tree)).unwrap())
2539 .unwrap();
2540 let kp = KeyPair::from_seed([0x07; 32]);
2541 let mut commit = Commit {
2542 tree_hash,
2543 parents: vec![],
2544 author: Identity::ed25519(kp.public.0),
2545 signer: kp.public.0,
2546 message: b"medium blob".to_vec(),
2547 timestamp: 1_726_300_001,
2548 message_hash: ZERO,
2549 content_digest: ZERO,
2550 signature: [0u8; 64],
2551 };
2552 commit.signature = sign_commit(&commit, &kp).unwrap().0;
2553 f.store
2554 .write(&crate::serialize::serialize(&Object::Commit(commit)).unwrap())
2555 .unwrap()
2556 }
2557
2558 type Case = (Hash, Vec<&'static [u8]>, Selector);
2559
2560 fn equivalence_cases(f: &Fixture, medium: Hash) -> Vec<Case> {
2563 let range = |offset, len, with_offsets| Selector::Range {
2564 offset,
2565 len,
2566 with_offsets,
2567 };
2568 let c = f.commit_id;
2569 let mut cases: Vec<Case> = vec![
2570 (c, vec![], Selector::Object),
2571 (c, vec![b"shallow.txt"], Selector::Object),
2572 (c, vec![b"sub", b"deep", b"deep.txt"], Selector::Object),
2573 (c, vec![b"sub"], Selector::Object),
2574 (c, vec![b"exec.sh"], Selector::Object),
2575 (c, vec![b"chunked.bin"], Selector::Object),
2576 (c, vec![b"shallow.txt"], range(0, 20, false)),
2577 (c, vec![b"shallow.txt"], range(8, 4, false)),
2578 (medium, vec![b"medium.bin"], range(0, 1024, false)),
2581 (medium, vec![b"medium.bin"], range(4096, 904, false)),
2582 (medium, vec![b"medium.bin"], range(1000, 100, false)),
2583 (medium, vec![b"medium.bin"], range(0, 5000, false)),
2584 (c, vec![b"chunked.bin"], range(10, 64, false)),
2587 (c, vec![b"chunked.bin"], range(10, 64, true)),
2588 (c, vec![b"chunked.bin"], range(200_000, 64, false)),
2589 (c, vec![b"chunked.bin"], range(200_000, 64, true)),
2590 ];
2591 let chunked_id = entry_id(&f.store, &root_tree_id(&f.store, &c), b"chunked.bin");
2592 let Object::ChunkedBlob(cb) = f.store.read_object(&chunked_id).unwrap() else {
2593 panic!("expected a chunked blob");
2594 };
2595 assert!(cb.chunks.len() > 1, "fixture must yield several chunks");
2596 for i in 0..cb.chunks.len() {
2597 let index = u32::try_from(i).unwrap();
2598 cases.push((c, vec![b"chunked.bin"], Selector::Chunk(index)));
2599 }
2600 cases
2601 }
2602
2603 fn assert_source_matches_store<S: crate::store::ObjectSource + ?Sized>(
2604 f: &Fixture,
2605 source: &S,
2606 medium: Hash,
2607 ) {
2608 for (commit, path, selector) in equivalence_cases(f, medium) {
2609 let expected = build_disclosure(&f.store, &commit, &path, selector).unwrap();
2610 let got = build_disclosure_from(source, &commit, &path, selector).unwrap();
2611 assert_eq!(
2612 got, expected,
2613 "bundle bytes differ for {path:?} / {selector:?}"
2614 );
2615 verify_disclosure(&commit, &got).unwrap();
2616 }
2617 }
2618
2619 #[test]
2620 fn disclosure_from_map_source_equals_store_builder() {
2621 let f = build_fixture();
2622 let medium = medium_blob_commit(&f);
2623 let map = MapSource::from_store(&f.store);
2624 assert_source_matches_store(&f, &map, medium);
2625 let dyn_source: &dyn crate::store::ObjectSource = ↦
2627 assert_source_matches_store(&f, dyn_source, medium);
2628 }
2629
2630 #[test]
2631 fn disclosure_from_ephemeral_sink_equals_store_builder() {
2632 let f = build_fixture();
2633 let medium = medium_blob_commit(&f);
2634 let sink = crate::store::EphemeralSink::new(&f.store);
2635 assert_source_matches_store(&f, &sink, medium);
2636 }
2637
2638 #[test]
2639 fn disclosure_from_missing_object_is_store_error() {
2640 let f = build_fixture();
2641 let sub_id = entry_id(&f.store, &root_tree_id(&f.store, &f.commit_id), b"sub");
2642 let path: [&[u8]; 3] = [b"sub", b"deep", b"deep.txt"];
2643
2644 let mut map = MapSource::from_store(&f.store);
2645 assert!(map.0.remove(&sub_id).is_some());
2646 let from_map =
2647 build_disclosure_from(&map, &f.commit_id, &path, Selector::Object).unwrap_err();
2648
2649 f.store.remove_object(&sub_id).unwrap();
2650 let from_store =
2651 build_disclosure(&f.store, &f.commit_id, &path, Selector::Object).unwrap_err();
2652
2653 let VerifyError::Store(StoreError::ObjectNotFound(map_hex)) = &from_map else {
2654 panic!("expected Store(ObjectNotFound), got {from_map:?}");
2655 };
2656 let VerifyError::Store(StoreError::ObjectNotFound(store_hex)) = &from_store else {
2657 panic!("expected Store(ObjectNotFound), got {from_store:?}");
2658 };
2659 assert_eq!(map_hex, store_hex);
2660 assert_eq!(map_hex, &crate::hash::to_hex(&sub_id));
2661 assert_eq!(from_map.to_string(), from_store.to_string());
2662 }
2663
2664 #[test]
2665 fn disclosure_from_lying_leaf_fails_verification() {
2666 let f = build_fixture();
2667 let root = root_tree_id(&f.store, &f.commit_id);
2668 let shallow_id = entry_id(&f.store, &root, b"shallow.txt");
2669 let exec_id = entry_id(&f.store, &root, b"exec.sh");
2670 let map = MapSource::from_store(&f.store);
2671 let liar = LyingSource {
2672 inner: &map,
2673 target: shallow_id,
2674 lie: f.store.read(&exec_id).unwrap(),
2676 };
2677 let bundle =
2678 build_disclosure_from(&liar, &f.commit_id, &[b"shallow.txt"], Selector::Object)
2679 .unwrap();
2680 assert!(matches!(
2681 verify_disclosure(&f.commit_id, &bundle),
2682 Err(VerifyError::PayloadIdMismatch)
2683 ));
2684 }
2685
2686 #[test]
2687 fn disclosure_from_lying_tree_fails_at_that_path_step() {
2688 let f = build_fixture();
2689 let root = root_tree_id(&f.store, &f.commit_id);
2690 let sub_id = entry_id(&f.store, &root, b"sub");
2691 let deep_tree_id = entry_id(&f.store, &sub_id, b"deep");
2692 let shallow_id = entry_id(&f.store, &root, b"shallow.txt");
2693 let forged = Tree {
2697 entries: vec![
2698 TreeEntry {
2699 name: b"aaa.txt".to_vec(),
2700 mode: EntryMode::Blob,
2701 object_hash: shallow_id,
2702 },
2703 TreeEntry {
2704 name: b"deep".to_vec(),
2705 mode: EntryMode::Tree,
2706 object_hash: deep_tree_id,
2707 },
2708 ],
2709 };
2710 let map = MapSource::from_store(&f.store);
2711 let liar = LyingSource {
2712 inner: &map,
2713 target: sub_id,
2714 lie: crate::serialize::serialize(&Object::Tree(forged)).unwrap(),
2715 };
2716 let path: [&[u8]; 3] = [b"sub", b"deep", b"deep.txt"];
2717 let bundle = build_disclosure_from(&liar, &f.commit_id, &path, Selector::Object).unwrap();
2718 let err = verify_disclosure(&f.commit_id, &bundle).unwrap_err();
2721 let VerifyError::InnerRootMismatch { expected, .. } = err else {
2722 panic!("expected InnerRootMismatch at the `sub` step, got {err:?}");
2723 };
2724 assert_eq!(expected, sub_id);
2725 }
2726
2727 #[test]
2728 fn disclosure_from_short_chunk_is_error_not_panic() {
2729 let f = build_fixture();
2730 let root = root_tree_id(&f.store, &f.commit_id);
2731 let chunked_id = entry_id(&f.store, &root, b"chunked.bin");
2732 let Object::ChunkedBlob(cb) = f.store.read_object(&chunked_id).unwrap() else {
2733 panic!("expected a chunked blob");
2734 };
2735 let map = MapSource::from_store(&f.store);
2736 for short_len in [0usize, 1, 9] {
2737 let liar = LyingSource {
2738 inner: &map,
2739 target: cb.chunks[0],
2740 lie: vec![0u8; short_len],
2741 };
2742 for with_offsets in [false, true] {
2743 let selector = Selector::Range {
2744 offset: 200_000,
2745 len: 64,
2746 with_offsets,
2747 };
2748 let err = build_disclosure_from(&liar, &f.commit_id, &[b"chunked.bin"], selector)
2749 .unwrap_err();
2750 assert!(
2751 matches!(err, VerifyError::Decode(MkitError::UnexpectedEof)),
2752 "{short_len}-byte chunk: got {err:?}"
2753 );
2754 }
2755 }
2756 }
2757
2758 #[test]
2759 fn disclosure_range_len_u64_max_is_error_not_panic() {
2760 let f = build_fixture();
2761 let map = MapSource::from_store(&f.store);
2762 let huge = |offset| Selector::Range {
2763 offset,
2764 len: u64::MAX,
2765 with_offsets: false,
2766 };
2767 for (path, offset) in [
2768 (b"chunked.bin".as_slice(), 10),
2769 (b"chunked.bin".as_slice(), 200_000),
2770 (b"shallow.txt".as_slice(), 1),
2771 ] {
2772 for result in [
2773 build_disclosure(&f.store, &f.commit_id, &[path], huge(offset)),
2774 build_disclosure_from(&map, &f.commit_id, &[path], huge(offset)),
2775 ] {
2776 let err = result.unwrap_err();
2777 assert!(
2778 matches!(err, VerifyError::OffsetOverflow),
2779 "{path:?} @ {offset}: got {err:?}"
2780 );
2781 }
2782 }
2783 let top = Selector::Range {
2785 offset: u64::MAX,
2786 len: 1,
2787 with_offsets: true,
2788 };
2789 let err = build_disclosure(&f.store, &f.commit_id, &[b"chunked.bin"], top).unwrap_err();
2790 assert!(matches!(err, VerifyError::RangeOutOfBounds), "got {err:?}");
2791 }
2792
2793 #[test]
2794 fn disclosure_from_split_blob_source_is_error_not_panic() {
2795 let f = build_fixture();
2796 let root = root_tree_id(&f.store, &f.commit_id);
2797 let shallow_id = entry_id(&f.store, &root, b"shallow.txt");
2798 let map = MapSource::from_store(&f.store);
2799 let liar = SplitSource {
2802 inner: &map,
2803 target: shallow_id,
2804 object_lie: Object::Blob(crate::object::Blob {
2805 data: vec![0u8; 4096],
2806 }),
2807 };
2808 let selector = Selector::Range {
2809 offset: 2048,
2810 len: 512,
2811 with_offsets: false,
2812 };
2813 if let Ok(bundle) = build_disclosure_from(&liar, &f.commit_id, &[b"shallow.txt"], selector)
2816 {
2817 assert!(verify_disclosure(&f.commit_id, &bundle).is_err());
2818 }
2819 }
2820}