1use serde::{Deserialize, Serialize};
17use std::collections::{BTreeMap, BTreeSet};
18
19const FNV_OFFSET_BASIS_64: u64 = 14_695_981_039_346_656_037;
24const FNV_PRIME_64: u64 = 1_099_511_628_211;
25
26fn fnv1a_64(data: &[u8]) -> u64 {
28 let mut hash = FNV_OFFSET_BASIS_64;
29 for &byte in data {
30 hash ^= u64::from(byte);
31 hash = hash.wrapping_mul(FNV_PRIME_64);
32 }
33 hash
34}
35
36fn fnv1a_hex(data: &[u8]) -> String {
38 format!("{:016x}", fnv1a_64(data))
39}
40
41fn combine_hashes(left: &str, right: &str) -> String {
46 let mut combined = Vec::with_capacity(left.len() + right.len());
47 combined.extend_from_slice(left.as_bytes());
48 combined.extend_from_slice(right.as_bytes());
49 fnv1a_hex(&combined)
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct ManifestEntry {
62 pub path: String,
64
65 pub cid: String,
67
68 pub size_bytes: u64,
70
71 pub chunk_index: u32,
74
75 pub is_final_chunk: bool,
77}
78
79impl ManifestEntry {
80 pub fn new(
82 path: impl Into<String>,
83 cid: impl Into<String>,
84 size_bytes: u64,
85 chunk_index: u32,
86 is_final_chunk: bool,
87 ) -> Self {
88 Self {
89 path: path.into(),
90 cid: cid.into(),
91 size_bytes,
92 chunk_index,
93 is_final_chunk,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct MerkleTree {
114 pub leaves: Vec<String>,
116
117 pub nodes: Vec<String>,
121}
122
123impl MerkleTree {
124 pub fn build(cids: &[String]) -> Self {
128 if cids.is_empty() {
129 return Self {
130 leaves: Vec::new(),
131 nodes: Vec::new(),
132 };
133 }
134
135 let leaf_hashes: Vec<String> = cids.iter().map(|c| fnv1a_hex(c.as_bytes())).collect();
137
138 let mut all_nodes: Vec<String> = leaf_hashes.clone();
140 let mut current_level = leaf_hashes;
141
142 while current_level.len() > 1 {
143 let mut next_level: Vec<String> = Vec::new();
144
145 let mut i = 0;
146 while i < current_level.len() {
147 let left = ¤t_level[i];
148 let right = if i + 1 < current_level.len() {
150 ¤t_level[i + 1]
151 } else {
152 ¤t_level[i]
153 };
154 next_level.push(combine_hashes(left, right));
155 i += 2;
156 }
157
158 all_nodes.extend(next_level.iter().cloned());
159 current_level = next_level;
160 }
161
162 Self {
163 leaves: cids.to_vec(),
164 nodes: all_nodes,
165 }
166 }
167
168 pub fn root(&self) -> Option<&str> {
170 self.nodes.last().map(|s| s.as_str())
171 }
172
173 pub fn proof_for(&self, index: usize) -> Option<Vec<String>> {
179 let n = self.leaves.len();
180 if n == 0 || index >= n {
181 return None;
182 }
183
184 let mut proof = Vec::new();
185 let mut current_index = index;
186 let mut level_size = n;
187 let mut level_start = 0usize;
188
189 while level_size > 1 {
190 let sibling_index = if current_index.is_multiple_of(2) {
192 (current_index + 1).min(level_size - 1)
194 } else {
195 current_index - 1
197 };
198
199 proof.push(self.nodes[level_start + sibling_index].clone());
200
201 level_start += level_size;
202 level_size = level_size.div_ceil(2);
203 current_index /= 2;
204 }
205
206 Some(proof)
207 }
208
209 pub fn verify_proof(leaf: &str, index: usize, proof: &[String], root: &str) -> bool {
218 let mut current_hash = fnv1a_hex(leaf.as_bytes());
219 let mut current_index = index;
220
221 for sibling in proof {
222 current_hash = if current_index.is_multiple_of(2) {
223 combine_hashes(¤t_hash, sibling)
224 } else {
225 combine_hashes(sibling, ¤t_hash)
226 };
227 current_index /= 2;
228 }
229
230 current_hash == root
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct ContentManifest {
246 pub manifest_id: String,
249
250 pub entries: Vec<ManifestEntry>,
252
253 pub root_cid: String,
256
257 pub total_size_bytes: u64,
259
260 pub created_at_ms: u64,
262}
263
264impl ContentManifest {
265 pub fn new(mut entries: Vec<ManifestEntry>) -> Self {
271 entries.sort_by(|a, b| {
273 a.path
274 .cmp(&b.path)
275 .then_with(|| a.chunk_index.cmp(&b.chunk_index))
276 });
277
278 let total_size_bytes: u64 = entries.iter().map(|e| e.size_bytes).sum();
279
280 let mut sorted_cids: Vec<&str> = entries.iter().map(|e| e.cid.as_str()).collect();
282 sorted_cids.sort_unstable();
283 let id_input: String = sorted_cids.join("");
284 let manifest_id = fnv1a_hex(id_input.as_bytes());
285
286 let ordered_cids: Vec<String> = entries.iter().map(|e| e.cid.clone()).collect();
288 let tree = MerkleTree::build(&ordered_cids);
289 let root_cid = tree.root().unwrap_or("").to_string();
290
291 let created_at_ms = created_at_ms_now();
293
294 Self {
295 manifest_id,
296 entries,
297 root_cid,
298 total_size_bytes,
299 created_at_ms,
300 }
301 }
302
303 pub fn get_entries_for_path<'a>(&'a self, path: &str) -> Vec<&'a ManifestEntry> {
305 self.entries.iter().filter(|e| e.path == path).collect()
306 }
307
308 pub fn total_chunks_for_path(&self, path: &str) -> usize {
310 self.entries.iter().filter(|e| e.path == path).count()
311 }
312
313 pub fn is_complete_for_path(&self, path: &str) -> bool {
320 let path_entries: Vec<&ManifestEntry> =
321 self.entries.iter().filter(|e| e.path == path).collect();
322
323 if path_entries.is_empty() {
324 return false;
325 }
326
327 let final_entries: Vec<&&ManifestEntry> =
329 path_entries.iter().filter(|e| e.is_final_chunk).collect();
330
331 if final_entries.len() != 1 {
332 return false;
333 }
334
335 let max_index = final_entries[0].chunk_index;
336 let expected_count = (max_index as usize) + 1;
337
338 if path_entries.len() != expected_count {
339 return false;
340 }
341
342 let indices: BTreeSet<u32> = path_entries.iter().map(|e| e.chunk_index).collect();
344 for idx in 0..=max_index {
345 if !indices.contains(&idx) {
346 return false;
347 }
348 }
349
350 true
351 }
352
353 pub fn entry_count(&self) -> usize {
355 self.entries.len()
356 }
357
358 pub fn paths(&self) -> Vec<String> {
360 let mut seen: BTreeSet<&str> = BTreeSet::new();
361 for entry in &self.entries {
362 seen.insert(entry.path.as_str());
363 }
364 seen.into_iter().map(|s| s.to_string()).collect()
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
378pub struct ManifestDiff {
379 pub added: Vec<ManifestEntry>,
381
382 pub removed: Vec<ManifestEntry>,
384}
385
386impl ManifestDiff {
387 pub fn diff(old: &ContentManifest, new: &ContentManifest) -> Self {
389 let old_cids: BTreeMap<&str, &ManifestEntry> =
390 old.entries.iter().map(|e| (e.cid.as_str(), e)).collect();
391 let new_cids: BTreeMap<&str, &ManifestEntry> =
392 new.entries.iter().map(|e| (e.cid.as_str(), e)).collect();
393
394 let added: Vec<ManifestEntry> = new_cids
395 .iter()
396 .filter(|(cid, _)| !old_cids.contains_key(*cid))
397 .map(|(_, e)| (*e).clone())
398 .collect();
399
400 let removed: Vec<ManifestEntry> = old_cids
401 .iter()
402 .filter(|(cid, _)| !new_cids.contains_key(*cid))
403 .map(|(_, e)| (*e).clone())
404 .collect();
405
406 Self { added, removed }
407 }
408
409 pub fn is_empty(&self) -> bool {
411 self.added.is_empty() && self.removed.is_empty()
412 }
413}
414
415fn created_at_ms_now() -> u64 {
425 #[cfg(not(target_arch = "wasm32"))]
426 {
427 use std::time::{SystemTime, UNIX_EPOCH};
428 SystemTime::now()
429 .duration_since(UNIX_EPOCH)
430 .map(|d| d.as_millis() as u64)
431 .unwrap_or(0)
432 }
433 #[cfg(target_arch = "wasm32")]
434 {
435 0
436 }
437}
438
439#[cfg(test)]
444mod tests {
445 use super::*;
446
447 fn entry(path: &str, cid: &str, size: u64, idx: u32, final_chunk: bool) -> ManifestEntry {
452 ManifestEntry::new(path, cid, size, idx, final_chunk)
453 }
454
455 fn sample_entries() -> Vec<ManifestEntry> {
456 vec![
457 entry("a/b.txt", "cid-ab-0", 100, 0, false),
458 entry("a/b.txt", "cid-ab-1", 200, 1, true),
459 entry("c/d.bin", "cid-cd-0", 50, 0, true),
460 ]
461 }
462
463 #[test]
468 fn test_manifest_new_total_size() {
469 let m = ContentManifest::new(sample_entries());
470 assert_eq!(m.total_size_bytes, 350);
471 }
472
473 #[test]
474 fn test_manifest_new_entry_count() {
475 let m = ContentManifest::new(sample_entries());
476 assert_eq!(m.entry_count(), 3);
477 }
478
479 #[test]
480 fn test_manifest_new_sorted_entries() {
481 let entries = vec![
483 entry("z/last.txt", "cid-z", 10, 0, true),
484 entry("a/first.txt", "cid-a", 20, 0, true),
485 ];
486 let m = ContentManifest::new(entries);
487 assert_eq!(m.entries[0].path, "a/first.txt");
488 assert_eq!(m.entries[1].path, "z/last.txt");
489 }
490
491 #[test]
496 fn test_get_entries_for_path_found() {
497 let m = ContentManifest::new(sample_entries());
498 let entries = m.get_entries_for_path("a/b.txt");
499 assert_eq!(entries.len(), 2);
500 assert_eq!(entries[0].chunk_index, 0);
501 assert_eq!(entries[1].chunk_index, 1);
502 }
503
504 #[test]
505 fn test_get_entries_for_path_not_found() {
506 let m = ContentManifest::new(sample_entries());
507 let entries = m.get_entries_for_path("nonexistent.txt");
508 assert!(entries.is_empty());
509 }
510
511 #[test]
516 fn test_paths_unique_sorted() {
517 let m = ContentManifest::new(sample_entries());
518 let paths = m.paths();
519 assert_eq!(paths, vec!["a/b.txt", "c/d.bin"]);
520 }
521
522 #[test]
523 fn test_paths_empty_manifest() {
524 let m = ContentManifest::new(vec![]);
525 assert!(m.paths().is_empty());
526 }
527
528 #[test]
533 fn test_is_complete_for_path_single_chunk() {
534 let m = ContentManifest::new(sample_entries());
535 assert!(m.is_complete_for_path("c/d.bin"));
536 }
537
538 #[test]
539 fn test_is_complete_for_path_multi_chunk_complete() {
540 let m = ContentManifest::new(sample_entries());
541 assert!(m.is_complete_for_path("a/b.txt"));
542 }
543
544 #[test]
545 fn test_is_complete_for_path_missing_final() {
546 let entries = vec![
548 entry("f.txt", "cid-f-0", 10, 0, false),
549 entry("f.txt", "cid-f-1", 10, 1, false),
550 ];
551 let m = ContentManifest::new(entries);
552 assert!(!m.is_complete_for_path("f.txt"));
553 }
554
555 #[test]
556 fn test_is_complete_for_path_gap() {
557 let entries = vec![
559 entry("g.bin", "cid-g-0", 10, 0, false),
560 entry("g.bin", "cid-g-2", 10, 2, true),
561 ];
562 let m = ContentManifest::new(entries);
563 assert!(!m.is_complete_for_path("g.bin"));
564 }
565
566 #[test]
567 fn test_is_complete_for_path_nonexistent() {
568 let m = ContentManifest::new(sample_entries());
569 assert!(!m.is_complete_for_path("does_not_exist.txt"));
570 }
571
572 #[test]
577 fn test_manifest_id_deterministic() {
578 let m1 = ContentManifest::new(sample_entries());
579 let m2 = ContentManifest::new(sample_entries());
580 assert_eq!(m1.manifest_id, m2.manifest_id);
581 }
582
583 #[test]
584 fn test_manifest_id_order_independent() {
585 let e1 = vec![
587 entry("a.txt", "cid-X", 10, 0, true),
588 entry("b.txt", "cid-Y", 20, 0, true),
589 ];
590 let e2 = vec![
591 entry("b.txt", "cid-Y", 20, 0, true),
592 entry("a.txt", "cid-X", 10, 0, true),
593 ];
594 let m1 = ContentManifest::new(e1);
595 let m2 = ContentManifest::new(e2);
596 assert_eq!(m1.manifest_id, m2.manifest_id);
597 }
598
599 #[test]
604 fn test_merkle_tree_single_leaf() {
605 let cids = vec!["cid-1".to_string()];
606 let tree = MerkleTree::build(&cids);
607 assert_eq!(tree.leaves.len(), 1);
608 let leaf_hash = fnv1a_hex(b"cid-1");
610 assert_eq!(tree.root(), Some(leaf_hash.as_str()));
611 }
612
613 #[test]
614 fn test_merkle_tree_two_leaves() {
615 let cids = vec!["cid-A".to_string(), "cid-B".to_string()];
616 let tree = MerkleTree::build(&cids);
617 assert_eq!(tree.leaves.len(), 2);
618 assert_eq!(tree.nodes.len(), 3);
620 let root = tree.root().expect("root must exist");
621 assert_eq!(root.len(), 16); }
623
624 #[test]
625 fn test_merkle_tree_three_leaves() {
626 let cids: Vec<String> = ["c1", "c2", "c3"].iter().map(|s| s.to_string()).collect();
627 let tree = MerkleTree::build(&cids);
628 assert_eq!(tree.nodes.len(), 6);
631 assert!(tree.root().is_some());
632 }
633
634 #[test]
635 fn test_merkle_tree_four_leaves() {
636 let cids: Vec<String> = ["c1", "c2", "c3", "c4"]
637 .iter()
638 .map(|s| s.to_string())
639 .collect();
640 let tree = MerkleTree::build(&cids);
641 assert_eq!(tree.nodes.len(), 7);
644 }
645
646 #[test]
647 fn test_merkle_tree_empty() {
648 let tree = MerkleTree::build(&[]);
649 assert!(tree.root().is_none());
650 assert!(tree.nodes.is_empty());
651 }
652
653 #[test]
658 fn test_merkle_root_deterministic() {
659 let cids: Vec<String> = ["x", "y", "z"].iter().map(|s| s.to_string()).collect();
660 let t1 = MerkleTree::build(&cids);
661 let t2 = MerkleTree::build(&cids);
662 assert_eq!(t1.root(), t2.root());
663 }
664
665 #[test]
670 fn test_proof_for_correct_length_two_leaves() {
671 let cids = vec!["cid-L".to_string(), "cid-R".to_string()];
672 let tree = MerkleTree::build(&cids);
673 let proof = tree.proof_for(0).expect("proof must exist");
674 assert_eq!(proof.len(), 1);
676 }
677
678 #[test]
679 fn test_proof_for_correct_length_four_leaves() {
680 let cids: Vec<String> = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
681 let tree = MerkleTree::build(&cids);
682 for idx in 0..4usize {
684 let proof = tree.proof_for(idx).expect("proof must exist");
685 assert_eq!(proof.len(), 2, "leaf {idx} proof length mismatch");
686 }
687 }
688
689 #[test]
690 fn test_verify_proof_valid() {
691 let cids: Vec<String> = ["alpha", "beta", "gamma", "delta"]
692 .iter()
693 .map(|s| s.to_string())
694 .collect();
695 let tree = MerkleTree::build(&cids);
696 let root = tree.root().expect("root").to_string();
697
698 for (idx, cid) in cids.iter().enumerate() {
699 let proof = tree.proof_for(idx).expect("proof");
700 assert!(
701 MerkleTree::verify_proof(cid, idx, &proof, &root),
702 "valid proof failed for index {idx}"
703 );
704 }
705 }
706
707 #[test]
708 fn test_verify_proof_tampered_leaf() {
709 let cids: Vec<String> = ["one", "two", "three", "four"]
710 .iter()
711 .map(|s| s.to_string())
712 .collect();
713 let tree = MerkleTree::build(&cids);
714 let root = tree.root().expect("root").to_string();
715 let proof = tree.proof_for(0).expect("proof");
716
717 assert!(
719 !MerkleTree::verify_proof("tampered", 0, &proof, &root),
720 "tampered proof must fail"
721 );
722 }
723
724 #[test]
725 fn test_proof_for_out_of_range() {
726 let cids = vec!["only-one".to_string()];
727 let tree = MerkleTree::build(&cids);
728 assert!(tree.proof_for(5).is_none());
729 }
730
731 #[test]
736 fn test_manifest_diff_added_removed() {
737 let old_entries = vec![
738 entry("shared.txt", "cid-shared", 100, 0, true),
739 entry("old-only.txt", "cid-old", 50, 0, true),
740 ];
741 let new_entries = vec![
742 entry("shared.txt", "cid-shared", 100, 0, true),
743 entry("new-only.txt", "cid-new", 75, 0, true),
744 ];
745 let old = ContentManifest::new(old_entries);
746 let new = ContentManifest::new(new_entries);
747 let diff = ManifestDiff::diff(&old, &new);
748
749 assert_eq!(diff.added.len(), 1);
750 assert_eq!(diff.added[0].cid, "cid-new");
751 assert_eq!(diff.removed.len(), 1);
752 assert_eq!(diff.removed[0].cid, "cid-old");
753 }
754
755 #[test]
756 fn test_manifest_diff_empty_when_identical() {
757 let m1 = ContentManifest::new(sample_entries());
758 let m2 = ContentManifest::new(sample_entries());
759 let diff = ManifestDiff::diff(&m1, &m2);
760 assert!(diff.is_empty());
761 }
762
763 #[test]
764 fn test_manifest_diff_all_added() {
765 let empty = ContentManifest::new(vec![]);
766 let full = ContentManifest::new(sample_entries());
767 let diff = ManifestDiff::diff(&empty, &full);
768 assert_eq!(diff.added.len(), 3);
769 assert!(diff.removed.is_empty());
770 }
771
772 #[test]
773 fn test_manifest_diff_all_removed() {
774 let full = ContentManifest::new(sample_entries());
775 let empty = ContentManifest::new(vec![]);
776 let diff = ManifestDiff::diff(&full, &empty);
777 assert!(diff.added.is_empty());
778 assert_eq!(diff.removed.len(), 3);
779 }
780}