1use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::codec::{decode_tree_node, is_directory_node, is_tree_node};
9use crate::hash::sha256;
10use crate::store::Store;
11use crate::types::{to_hex, Cid, Hash, Link, LinkType, TreeNode};
12
13use crate::crypto::{decrypt_chk, EncryptionKey};
14
15#[derive(Debug, Clone)]
17pub struct TreeEntry {
18 pub name: String,
19 pub hash: Hash,
20 pub size: u64,
21 pub link_type: LinkType,
23 pub key: Option<[u8; 32]>,
25 pub meta: Option<HashMap<String, serde_json::Value>>,
27}
28
29#[derive(Debug, Clone)]
31pub struct WalkEntry {
32 pub path: String,
33 pub hash: Hash,
34 pub link_type: LinkType,
36 pub size: u64,
37 pub key: Option<[u8; 32]>,
39}
40
41pub struct TreeReader<S: Store> {
43 store: Arc<S>,
44}
45
46impl<S: Store> TreeReader<S> {
47 fn internal_chunk_start(name: &str) -> Option<usize> {
48 let suffix = name.strip_prefix("_chunk_")?;
49 if suffix.is_empty() || !suffix.bytes().all(|byte| byte.is_ascii_digit()) {
50 return None;
51 }
52 suffix.parse().ok()
53 }
54
55 fn node_uses_legacy_directory_fanout(node: &TreeNode) -> bool {
56 node.node_type == LinkType::Dir
57 && !node.links.is_empty()
58 && node.links.iter().all(|link| {
59 let Some(name) = link.name.as_deref() else {
60 return false;
61 };
62 Self::internal_chunk_start(name).is_some() && link.link_type == LinkType::Dir
63 })
64 }
65
66 fn is_internal_directory_link(node: &TreeNode, link: &Link) -> bool {
67 if node.node_type == LinkType::Fanout {
68 return matches!(link.link_type, LinkType::Dir | LinkType::Fanout);
69 }
70
71 if !Self::node_uses_legacy_directory_fanout(node) || link.link_type != LinkType::Dir {
72 return false;
73 }
74
75 let Some(name) = link.name.as_deref() else {
76 return false;
77 };
78 Self::internal_chunk_start(name).is_some()
79 }
80
81 fn decode_node_or_blob(data: &[u8]) -> Result<Option<TreeNode>, ReaderError> {
82 match decode_tree_node(data) {
83 Ok(node) => Ok(Some(node)),
84 Err(err) if is_tree_node(data) => Err(ReaderError::Codec(err)),
85 Err(_) => Ok(None),
86 }
87 }
88
89 fn decode_linked_file_node(link: &Link, data: &[u8]) -> Result<Option<TreeNode>, ReaderError> {
90 match link.link_type {
91 LinkType::File => match decode_tree_node(data) {
92 Ok(node) => Ok(Some(node)),
93 Err(_) if link.size == data.len() as u64 => Ok(None),
94 Err(err) => Err(ReaderError::Codec(err)),
95 },
96 LinkType::Blob => {
97 if link.size == data.len() as u64 {
98 return Ok(None);
99 }
100
101 match decode_tree_node(data) {
102 Ok(node) if node.node_type == LinkType::File => Ok(Some(node)),
103 _ => Ok(None),
104 }
105 }
106 LinkType::Dir | LinkType::Fanout => Ok(None),
107 }
108 }
109
110 pub fn new(store: Arc<S>) -> Self {
111 Self { store }
112 }
113
114 pub async fn get_blob(&self, hash: &Hash) -> Result<Option<Vec<u8>>, ReaderError> {
116 self.store
117 .get(hash)
118 .await
119 .map_err(|e| ReaderError::Store(e.to_string()))
120 }
121
122 pub async fn get_tree_node(&self, hash: &Hash) -> Result<Option<TreeNode>, ReaderError> {
124 let data = match self
125 .store
126 .get(hash)
127 .await
128 .map_err(|e| ReaderError::Store(e.to_string()))?
129 {
130 Some(d) => d,
131 None => return Ok(None),
132 };
133
134 if !is_tree_node(&data) {
135 return Ok(None); }
137
138 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
139 Ok(Some(node))
140 }
141
142 pub async fn is_tree(&self, hash: &Hash) -> Result<bool, ReaderError> {
144 let data = match self
145 .store
146 .get(hash)
147 .await
148 .map_err(|e| ReaderError::Store(e.to_string()))?
149 {
150 Some(d) => d,
151 None => return Ok(false),
152 };
153 Ok(is_tree_node(&data))
154 }
155
156 pub async fn is_directory(&self, hash: &Hash) -> Result<bool, ReaderError> {
159 let data = match self
160 .store
161 .get(hash)
162 .await
163 .map_err(|e| ReaderError::Store(e.to_string()))?
164 {
165 Some(d) => d,
166 None => return Ok(false),
167 };
168 Ok(is_directory_node(&data))
169 }
170
171 pub async fn get(&self, cid: &Cid) -> Result<Option<Vec<u8>>, ReaderError> {
176 if let Some(key) = cid.key {
177 self.get_encrypted(&cid.hash, &key).await
178 } else {
179 self.read_file(&cid.hash).await
180 }
181 }
182
183 async fn get_encrypted(
185 &self,
186 hash: &Hash,
187 key: &EncryptionKey,
188 ) -> Result<Option<Vec<u8>>, ReaderError> {
189 let encrypted_data = match self
190 .store
191 .get(hash)
192 .await
193 .map_err(|e| ReaderError::Store(e.to_string()))?
194 {
195 Some(d) => d,
196 None => return Ok(None),
197 };
198
199 let decrypted = decrypt_chk(&encrypted_data, key)
201 .map_err(|e| ReaderError::Decryption(e.to_string()))?;
202
203 if is_tree_node(&decrypted) {
205 let node = decode_tree_node(&decrypted)?;
206 let assembled = self.assemble_encrypted_chunks(&node).await?;
207 return Ok(Some(assembled));
208 }
209
210 Ok(Some(decrypted))
212 }
213
214 async fn assemble_encrypted_chunks(&self, node: &TreeNode) -> Result<Vec<u8>, ReaderError> {
216 let mut parts: Vec<Vec<u8>> = Vec::new();
217
218 for link in &node.links {
219 let chunk_key = link.key.ok_or(ReaderError::MissingKey)?;
220
221 let encrypted_child = self
222 .store
223 .get(&link.hash)
224 .await
225 .map_err(|e| ReaderError::Store(e.to_string()))?
226 .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
227
228 let decrypted = decrypt_chk(&encrypted_child, &chunk_key)
229 .map_err(|e| ReaderError::Decryption(e.to_string()))?;
230
231 if let Some(child_node) = Self::decode_linked_file_node(link, &decrypted)? {
232 let child_data = Box::pin(self.assemble_encrypted_chunks(&child_node)).await?;
234 parts.push(child_data);
235 } else {
236 parts.push(decrypted);
238 }
239 }
240
241 let total_len: usize = parts.iter().map(|p| p.len()).sum();
242 let mut result = Vec::with_capacity(total_len);
243 for part in parts {
244 result.extend_from_slice(&part);
245 }
246
247 Ok(result)
248 }
249
250 pub async fn read_file(&self, hash: &Hash) -> Result<Option<Vec<u8>>, ReaderError> {
253 let data = match self
254 .store
255 .get(hash)
256 .await
257 .map_err(|e| ReaderError::Store(e.to_string()))?
258 {
259 Some(d) => d,
260 None => return Ok(None),
261 };
262
263 if !is_tree_node(&data) {
265 return Ok(Some(data)); }
267
268 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
270 let assembled = self.assemble_chunks(&node).await?;
271 Ok(Some(assembled))
272 }
273
274 pub async fn read_file_range(
281 &self,
282 hash: &Hash,
283 start: u64,
284 end: Option<u64>,
285 ) -> Result<Option<Vec<u8>>, ReaderError> {
286 let data = match self
287 .store
288 .get(hash)
289 .await
290 .map_err(|e| ReaderError::Store(e.to_string()))?
291 {
292 Some(d) => d,
293 None => return Ok(None),
294 };
295
296 if !is_tree_node(&data) {
298 let start_idx = start as usize;
299 let end_idx = end.map(|e| e as usize).unwrap_or(data.len());
300 if start_idx >= data.len() {
301 return Ok(Some(vec![]));
302 }
303 let end_idx = end_idx.min(data.len());
304 return Ok(Some(data[start_idx..end_idx].to_vec()));
305 }
306
307 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
309 let range_data = self.assemble_chunks_range(&node, start, end).await?;
310 Ok(Some(range_data))
311 }
312
313 async fn assemble_chunks_range(
315 &self,
316 node: &TreeNode,
317 start: u64,
318 end: Option<u64>,
319 ) -> Result<Vec<u8>, ReaderError> {
320 let chunks_info = self.collect_chunk_offsets(node).await?;
322
323 if chunks_info.is_empty() {
324 return Ok(vec![]);
325 }
326
327 let total_size: u64 = chunks_info.iter().map(|(_, _, size)| size).sum();
329 let actual_end = end.unwrap_or(total_size).min(total_size);
330
331 if start >= actual_end {
332 return Ok(vec![]);
333 }
334
335 let mut result = Vec::with_capacity((actual_end - start) as usize);
337 let mut current_offset = 0u64;
338
339 for (chunk_hash, _chunk_offset, chunk_size) in &chunks_info {
340 let chunk_start = current_offset;
341 let chunk_end = current_offset + chunk_size;
342
343 if chunk_end > start && chunk_start < actual_end {
345 let chunk_data = self
347 .store
348 .get(chunk_hash)
349 .await
350 .map_err(|e| ReaderError::Store(e.to_string()))?
351 .ok_or_else(|| ReaderError::MissingChunk(to_hex(chunk_hash)))?;
352
353 let slice_start = if start > chunk_start {
355 (start - chunk_start) as usize
356 } else {
357 0
358 };
359 let slice_end = if actual_end < chunk_end {
360 (actual_end - chunk_start) as usize
361 } else {
362 chunk_data.len()
363 };
364
365 result.extend_from_slice(&chunk_data[slice_start..slice_end]);
366 }
367
368 current_offset = chunk_end;
369
370 if current_offset >= actual_end {
372 break;
373 }
374 }
375
376 Ok(result)
377 }
378
379 async fn collect_chunk_offsets(
382 &self,
383 node: &TreeNode,
384 ) -> Result<Vec<(Hash, u64, u64)>, ReaderError> {
385 let mut chunks = Vec::new();
386 let mut offset = 0u64;
387 self.collect_chunk_offsets_recursive(node, &mut chunks, &mut offset)
388 .await?;
389 Ok(chunks)
390 }
391
392 async fn collect_chunk_offsets_recursive(
393 &self,
394 node: &TreeNode,
395 chunks: &mut Vec<(Hash, u64, u64)>,
396 offset: &mut u64,
397 ) -> Result<(), ReaderError> {
398 for link in &node.links {
399 let child_data = self
400 .store
401 .get(&link.hash)
402 .await
403 .map_err(|e| ReaderError::Store(e.to_string()))?
404 .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
405
406 if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
407 Box::pin(self.collect_chunk_offsets_recursive(&child_node, chunks, offset)).await?;
409 } else {
410 let size = child_data.len() as u64;
412 chunks.push((link.hash, *offset, size));
413 *offset += size;
414 }
415 }
416 Ok(())
417 }
418
419 async fn assemble_chunks(&self, node: &TreeNode) -> Result<Vec<u8>, ReaderError> {
421 let mut parts: Vec<Vec<u8>> = Vec::new();
422
423 for link in &node.links {
424 let child_data = self
425 .store
426 .get(&link.hash)
427 .await
428 .map_err(|e| ReaderError::Store(e.to_string()))?
429 .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
430
431 if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
432 parts.push(Box::pin(self.assemble_chunks(&child_node)).await?);
434 } else {
435 parts.push(child_data);
437 }
438 }
439
440 let total_length: usize = parts.iter().map(|p| p.len()).sum();
442 let mut result = Vec::with_capacity(total_length);
443 for part in parts {
444 result.extend_from_slice(&part);
445 }
446
447 Ok(result)
448 }
449
450 pub async fn read_file_chunks(&self, hash: &Hash) -> Result<Vec<Vec<u8>>, ReaderError> {
452 let data = match self
453 .store
454 .get(hash)
455 .await
456 .map_err(|e| ReaderError::Store(e.to_string()))?
457 {
458 Some(d) => d,
459 None => return Ok(vec![]),
460 };
461
462 if !is_tree_node(&data) {
463 return Ok(vec![data]);
464 }
465
466 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
467 self.collect_chunks(&node).await
468 }
469
470 async fn collect_chunks(&self, node: &TreeNode) -> Result<Vec<Vec<u8>>, ReaderError> {
472 let mut chunks = Vec::new();
473
474 for link in &node.links {
475 let child_data = self
476 .store
477 .get(&link.hash)
478 .await
479 .map_err(|e| ReaderError::Store(e.to_string()))?
480 .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
481
482 if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
483 chunks.extend(Box::pin(self.collect_chunks(&child_node)).await?);
484 } else {
485 chunks.push(child_data);
486 }
487 }
488
489 Ok(chunks)
490 }
491
492 pub async fn list_directory(&self, hash: &Hash) -> Result<Vec<TreeEntry>, ReaderError> {
494 let node = match self.get_tree_node(hash).await? {
495 Some(n) => n,
496 None => return Ok(vec![]),
497 };
498
499 let mut entries = Vec::new();
500
501 for link in &node.links {
502 if Self::is_internal_directory_link(&node, link) {
504 let sub_entries = Box::pin(self.list_directory(&link.hash)).await?;
505 entries.extend(sub_entries);
506 continue;
507 }
508
509 entries.push(TreeEntry {
510 name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
511 hash: link.hash,
512 size: link.size,
513 link_type: link.link_type,
514 key: link.key,
515 meta: link.meta.clone(),
516 });
517 }
518
519 Ok(entries)
520 }
521
522 pub async fn resolve_path(
525 &self,
526 root_hash: &Hash,
527 path: &str,
528 ) -> Result<Option<Hash>, ReaderError> {
529 let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
530
531 let mut current_hash = *root_hash;
532
533 for part in parts {
534 let node = match self.get_tree_node(¤t_hash).await? {
535 Some(n) => n,
536 None => return Ok(None),
537 };
538
539 if let Some(link) = self.find_link(&node, part) {
540 current_hash = link.hash;
541 } else {
542 match self.find_in_subtrees(&node, part).await? {
544 Some(hash) => current_hash = hash,
545 None => return Ok(None),
546 }
547 }
548 }
549
550 Ok(Some(current_hash))
551 }
552
553 fn find_link(&self, node: &TreeNode, name: &str) -> Option<Link> {
555 node.links
556 .iter()
557 .find(|l| !Self::is_internal_directory_link(node, l) && l.name.as_deref() == Some(name))
558 .cloned()
559 }
560
561 async fn find_in_subtrees(
563 &self,
564 node: &TreeNode,
565 name: &str,
566 ) -> Result<Option<Hash>, ReaderError> {
567 for link in &node.links {
568 if !Self::is_internal_directory_link(node, link) {
570 continue;
571 }
572
573 let sub_node = match self.get_tree_node(&link.hash).await? {
574 Some(n) => n,
575 None => continue,
576 };
577
578 if let Some(found) = self.find_link(&sub_node, name) {
579 return Ok(Some(found.hash));
580 }
581
582 if let Some(deep_found) = Box::pin(self.find_in_subtrees(&sub_node, name)).await? {
584 return Ok(Some(deep_found));
585 }
586 }
587
588 Ok(None)
589 }
590
591 pub async fn get_size(&self, hash: &Hash) -> Result<u64, ReaderError> {
593 let data = match self
594 .store
595 .get(hash)
596 .await
597 .map_err(|e| ReaderError::Store(e.to_string()))?
598 {
599 Some(d) => d,
600 None => return Ok(0),
601 };
602
603 if !is_tree_node(&data) {
604 return Ok(data.len() as u64);
605 }
606
607 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
608 let mut total = 0u64;
610 for link in &node.links {
611 total += link.size;
612 }
613 Ok(total)
614 }
615
616 pub async fn walk(&self, hash: &Hash, path: &str) -> Result<Vec<WalkEntry>, ReaderError> {
618 let mut entries = Vec::new();
619 self.walk_recursive(hash, path, &mut entries).await?;
620 Ok(entries)
621 }
622
623 async fn walk_recursive(
624 &self,
625 hash: &Hash,
626 path: &str,
627 entries: &mut Vec<WalkEntry>,
628 ) -> Result<(), ReaderError> {
629 let data = match self
630 .store
631 .get(hash)
632 .await
633 .map_err(|e| ReaderError::Store(e.to_string()))?
634 {
635 Some(d) => d,
636 None => return Ok(()),
637 };
638
639 let node = match Self::decode_node_or_blob(&data)? {
640 Some(node) => node,
641 None => {
642 entries.push(WalkEntry {
643 path: path.to_string(),
644 hash: *hash,
645 link_type: LinkType::Blob,
646 size: data.len() as u64,
647 key: None, });
649 return Ok(());
650 }
651 };
652
653 let node_size: u64 = node.links.iter().map(|l| l.size).sum();
654 entries.push(WalkEntry {
655 path: path.to_string(),
656 hash: *hash,
657 link_type: node.node_type,
658 size: node_size,
659 key: None, });
661
662 for link in &node.links {
663 let child_path = match &link.name {
664 Some(name) => {
665 if Self::is_internal_directory_link(&node, link) {
667 Box::pin(self.walk_recursive(&link.hash, path, entries)).await?;
668 continue;
669 }
670 if path.is_empty() {
671 name.clone()
672 } else {
673 format!("{}/{}", path, name)
674 }
675 }
676 None => path.to_string(),
677 };
678
679 Box::pin(self.walk_recursive(&link.hash, &child_path, entries)).await?;
680 }
681
682 Ok(())
683 }
684}
685
686pub async fn verify_tree<S: Store>(
689 store: Arc<S>,
690 root_hash: &Hash,
691) -> Result<VerifyResult, ReaderError> {
692 let mut missing = Vec::new();
693 let mut visited = std::collections::HashSet::new();
694
695 verify_recursive(store, root_hash, &mut missing, &mut visited).await?;
696
697 Ok(VerifyResult {
698 valid: missing.is_empty(),
699 missing,
700 })
701}
702
703async fn verify_recursive<S: Store>(
704 store: Arc<S>,
705 hash: &Hash,
706 missing: &mut Vec<Hash>,
707 visited: &mut std::collections::HashSet<String>,
708) -> Result<(), ReaderError> {
709 let hex = to_hex(hash);
710 if visited.contains(&hex) {
711 return Ok(());
712 }
713 visited.insert(hex);
714
715 let data = match store
716 .get(hash)
717 .await
718 .map_err(|e| ReaderError::Store(e.to_string()))?
719 {
720 Some(d) => d,
721 None => {
722 missing.push(*hash);
723 return Ok(());
724 }
725 };
726
727 if is_tree_node(&data) {
728 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
729 for link in &node.links {
730 Box::pin(verify_recursive(
731 store.clone(),
732 &link.hash,
733 missing,
734 visited,
735 ))
736 .await?;
737 }
738 }
739
740 Ok(())
741}
742
743pub async fn verify_tree_integrity<S: Store>(
749 store: Arc<S>,
750 root_hash: &Hash,
751) -> Result<VerifyIntegrityResult, ReaderError> {
752 let mut missing = Vec::new();
753 let mut corrupted = Vec::new();
754 let mut visited = std::collections::HashSet::new();
755
756 verify_integrity_recursive(store, root_hash, &mut missing, &mut corrupted, &mut visited)
757 .await?;
758
759 Ok(VerifyIntegrityResult {
760 valid: missing.is_empty() && corrupted.is_empty(),
761 missing,
762 corrupted,
763 })
764}
765
766async fn verify_integrity_recursive<S: Store>(
767 store: Arc<S>,
768 hash: &Hash,
769 missing: &mut Vec<Hash>,
770 corrupted: &mut Vec<Hash>,
771 visited: &mut std::collections::HashSet<String>,
772) -> Result<(), ReaderError> {
773 let hex = to_hex(hash);
774 if visited.contains(&hex) {
775 return Ok(());
776 }
777 visited.insert(hex);
778
779 let data = match store
780 .get(hash)
781 .await
782 .map_err(|e| ReaderError::Store(e.to_string()))?
783 {
784 Some(d) => d,
785 None => {
786 missing.push(*hash);
787 return Ok(());
788 }
789 };
790
791 if sha256(&data) != *hash {
793 corrupted.push(*hash);
794 return Ok(());
795 }
796
797 if is_tree_node(&data) {
798 let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
799 for link in &node.links {
800 Box::pin(verify_integrity_recursive(
801 store.clone(),
802 &link.hash,
803 missing,
804 corrupted,
805 visited,
806 ))
807 .await?;
808 }
809 }
810
811 Ok(())
812}
813
814#[derive(Debug, Clone)]
816pub struct VerifyResult {
817 pub valid: bool,
818 pub missing: Vec<Hash>,
819}
820
821#[derive(Debug, Clone)]
823pub struct VerifyIntegrityResult {
824 pub valid: bool,
825 pub missing: Vec<Hash>,
826 pub corrupted: Vec<Hash>,
827}
828
829#[derive(Debug, thiserror::Error)]
831pub enum ReaderError {
832 #[error("Store error: {0}")]
833 Store(String),
834 #[error("Codec error: {0}")]
835 Codec(#[from] crate::codec::CodecError),
836 #[error("Missing chunk: {0}")]
837 MissingChunk(String),
838 #[error("Decryption error: {0}")]
839 Decryption(String),
840 #[error("Missing decryption key")]
841 MissingKey,
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use crate::builder::{BuilderConfig, TreeBuilder};
848 use crate::store::MemoryStore;
849 use crate::types::DirEntry;
850
851 fn make_store() -> Arc<MemoryStore> {
852 Arc::new(MemoryStore::new())
853 }
854
855 fn invalid_tree_shape_blob() -> Vec<u8> {
856 #[derive(serde::Serialize)]
857 struct Shape {
858 l: Vec<()>,
859 t: u8,
860 }
861
862 rmp_serde::to_vec_named(&Shape {
863 l: Vec::new(),
864 t: 98,
865 })
866 .unwrap()
867 }
868
869 #[tokio::test]
870 async fn test_get_blob() {
871 let store = make_store();
872 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
873 let reader = TreeReader::new(store);
874
875 let data = vec![1u8, 2, 3, 4, 5];
876 let hash = builder.put_blob(&data).await.unwrap();
877
878 let result = reader.get_blob(&hash).await.unwrap();
879 assert_eq!(result, Some(data));
880 }
881
882 #[tokio::test]
883 async fn test_get_blob_missing() {
884 let store = make_store();
885 let reader = TreeReader::new(store);
886
887 let hash = [0u8; 32];
888 let result = reader.get_blob(&hash).await.unwrap();
889 assert!(result.is_none());
890 }
891
892 #[tokio::test]
893 async fn test_get_tree_node() {
894 let store = make_store();
895 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
896 let reader = TreeReader::new(store);
897
898 let file_hash = builder.put_blob(&[1u8]).await.unwrap();
899 let dir_hash = builder
900 .put_directory(vec![DirEntry::new("test.txt", file_hash).with_size(1)])
901 .await
902 .unwrap();
903
904 let node = reader.get_tree_node(&dir_hash).await.unwrap();
905 assert!(node.is_some());
906 assert_eq!(node.unwrap().links.len(), 1);
907 }
908
909 #[tokio::test]
910 async fn test_get_tree_node_returns_none_for_blob() {
911 let store = make_store();
912 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
913 let reader = TreeReader::new(store);
914
915 let hash = builder.put_blob(&[1u8, 2, 3]).await.unwrap();
916 let node = reader.get_tree_node(&hash).await.unwrap();
917 assert!(node.is_none());
918 }
919
920 #[tokio::test]
921 async fn test_is_tree() {
922 let store = make_store();
923 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
924 let reader = TreeReader::new(store);
925
926 let file_hash = builder.put_blob(&[1u8]).await.unwrap();
927 let dir_hash = builder
928 .put_directory(vec![DirEntry::new("test.txt", file_hash)])
929 .await
930 .unwrap();
931
932 assert!(reader.is_tree(&dir_hash).await.unwrap());
933 assert!(!reader.is_tree(&file_hash).await.unwrap());
934 }
935
936 #[tokio::test]
937 async fn test_read_file_small() {
938 let store = make_store();
939 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()).public());
941 let reader = TreeReader::new(store);
942
943 let data = vec![1u8, 2, 3, 4, 5];
944 let (cid, _size) = builder.put(&data).await.unwrap();
945
946 let result = reader.read_file(&cid.hash).await.unwrap();
947 assert_eq!(result, Some(data));
948 }
949
950 #[tokio::test]
951 async fn test_read_file_chunked() {
952 let store = make_store();
953 let config = BuilderConfig::new(store.clone())
954 .with_chunk_size(100)
955 .public();
956 let builder = TreeBuilder::new(config);
957 let reader = TreeReader::new(store);
958
959 let mut data = vec![0u8; 350];
960 for i in 0..data.len() {
961 data[i] = (i % 256) as u8;
962 }
963
964 let (cid, _size) = builder.put(&data).await.unwrap();
965 let result = reader.read_file(&cid.hash).await.unwrap();
966
967 assert_eq!(result, Some(data));
968 }
969
970 #[tokio::test]
971 async fn test_list_directory() {
972 let store = make_store();
973 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
974 let reader = TreeReader::new(store);
975
976 let h1 = builder.put_blob(&[1u8]).await.unwrap();
977 let h2 = builder.put_blob(&[2u8]).await.unwrap();
978
979 let dir_hash = builder
980 .put_directory(vec![
981 DirEntry::new("first.txt", h1).with_size(1),
982 DirEntry::new("second.txt", h2).with_size(1),
983 ])
984 .await
985 .unwrap();
986
987 let entries = reader.list_directory(&dir_hash).await.unwrap();
988
989 assert_eq!(entries.len(), 2);
990 assert!(entries.iter().any(|e| e.name == "first.txt"));
991 assert!(entries.iter().any(|e| e.name == "second.txt"));
992 }
993
994 #[tokio::test]
995 async fn test_resolve_path() {
996 let store = make_store();
997 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
998 let reader = TreeReader::new(store);
999
1000 let file_data = vec![1u8, 2, 3];
1001 let file_hash = builder.put_blob(&file_data).await.unwrap();
1002
1003 let dir_hash = builder
1004 .put_directory(vec![DirEntry::new("test.txt", file_hash)])
1005 .await
1006 .unwrap();
1007
1008 let resolved = reader.resolve_path(&dir_hash, "test.txt").await.unwrap();
1009 assert_eq!(resolved, Some(file_hash));
1010 }
1011
1012 #[tokio::test]
1013 async fn test_resolve_path_nested() {
1014 let store = make_store();
1015 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
1016 let reader = TreeReader::new(store);
1017
1018 let file_hash = builder.put_blob(&[1u8]).await.unwrap();
1019
1020 let sub_sub_dir = builder
1021 .put_directory(vec![DirEntry::new("deep.txt", file_hash)])
1022 .await
1023 .unwrap();
1024
1025 let sub_dir = builder
1026 .put_directory(vec![DirEntry::new("level2", sub_sub_dir)])
1027 .await
1028 .unwrap();
1029
1030 let root_dir = builder
1031 .put_directory(vec![DirEntry::new("level1", sub_dir)])
1032 .await
1033 .unwrap();
1034
1035 let resolved = reader
1036 .resolve_path(&root_dir, "level1/level2/deep.txt")
1037 .await
1038 .unwrap();
1039 assert_eq!(resolved, Some(file_hash));
1040 }
1041
1042 #[tokio::test]
1043 async fn test_get_size() {
1044 let store = make_store();
1045 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
1046 let reader = TreeReader::new(store);
1047
1048 let data = vec![0u8; 123];
1049 let hash = builder.put_blob(&data).await.unwrap();
1050
1051 assert_eq!(reader.get_size(&hash).await.unwrap(), 123);
1052 }
1053
1054 #[tokio::test]
1055 async fn test_walk() {
1056 let store = make_store();
1057 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
1058 let reader = TreeReader::new(store);
1059
1060 let f1 = builder.put_blob(&[1u8]).await.unwrap();
1061 let f2 = builder.put_blob(&[2u8, 3]).await.unwrap();
1062
1063 let sub_dir = builder
1064 .put_directory(vec![DirEntry::new("nested.txt", f2).with_size(2)])
1065 .await
1066 .unwrap();
1067
1068 let root_dir = builder
1069 .put_directory(vec![
1070 DirEntry::new("root.txt", f1).with_size(1),
1071 DirEntry::new("sub", sub_dir),
1072 ])
1073 .await
1074 .unwrap();
1075
1076 let entries = reader.walk(&root_dir, "").await.unwrap();
1077 let paths: Vec<_> = entries.iter().map(|e| e.path.as_str()).collect();
1078
1079 assert!(paths.contains(&""));
1080 assert!(paths.contains(&"root.txt"));
1081 assert!(paths.contains(&"sub"));
1082 assert!(paths.contains(&"sub/nested.txt"));
1083 }
1084
1085 #[tokio::test]
1086 async fn test_verify_tree_valid() {
1087 let store = make_store();
1088 let config = BuilderConfig::new(store.clone())
1089 .with_chunk_size(100)
1090 .public();
1091 let builder = TreeBuilder::new(config);
1092
1093 let data = vec![0u8; 350];
1094 let (cid, _size) = builder.put(&data).await.unwrap();
1095
1096 let result = verify_tree(store, &cid.hash).await.unwrap();
1097 assert!(result.valid);
1098 assert!(result.missing.is_empty());
1099 }
1100
1101 #[tokio::test]
1102 async fn test_verify_tree_missing() {
1103 let store = make_store();
1104 let config = BuilderConfig::new(store.clone())
1105 .with_chunk_size(100)
1106 .public();
1107 let builder = TreeBuilder::new(config);
1108
1109 let data = vec![0u8; 350];
1110 let (cid, _size) = builder.put(&data).await.unwrap();
1111
1112 let keys = store.keys();
1114 if let Some(chunk_to_delete) = keys.iter().find(|k| **k != cid.hash) {
1115 store.delete(chunk_to_delete).await.unwrap();
1116 }
1117
1118 let result = verify_tree(store, &cid.hash).await.unwrap();
1119 assert!(!result.valid);
1120 assert!(!result.missing.is_empty());
1121 }
1122
1123 #[tokio::test]
1124 async fn test_verify_tree_integrity_valid() {
1125 let store = make_store();
1126 let config = BuilderConfig::new(store.clone())
1127 .with_chunk_size(100)
1128 .public();
1129 let builder = TreeBuilder::new(config);
1130
1131 let data = vec![0u8; 350];
1132 let (cid, _size) = builder.put(&data).await.unwrap();
1133
1134 let result = verify_tree_integrity(store, &cid.hash).await.unwrap();
1135 assert!(result.valid);
1136 assert!(result.missing.is_empty());
1137 assert!(result.corrupted.is_empty());
1138 }
1139
1140 #[tokio::test]
1141 async fn test_verify_tree_integrity_missing() {
1142 let store = make_store();
1143 let config = BuilderConfig::new(store.clone())
1144 .with_chunk_size(100)
1145 .public();
1146 let builder = TreeBuilder::new(config);
1147
1148 let data = vec![0u8; 350];
1149 let (cid, _size) = builder.put(&data).await.unwrap();
1150
1151 let keys = store.keys();
1153 if let Some(chunk_to_delete) = keys.iter().find(|k| **k != cid.hash) {
1154 store.delete(chunk_to_delete).await.unwrap();
1155 }
1156
1157 let result = verify_tree_integrity(store, &cid.hash).await.unwrap();
1158 assert!(!result.valid);
1159 assert!(!result.missing.is_empty());
1160 assert!(result.corrupted.is_empty());
1161 }
1162
1163 #[tokio::test]
1164 async fn test_verify_tree_integrity_corrupted_hash_mismatch() {
1165 let store = make_store();
1166 let config = BuilderConfig::new(store.clone())
1167 .with_chunk_size(100)
1168 .public();
1169 let builder = TreeBuilder::new(config);
1170
1171 let data = vec![0u8; 350];
1172 let (cid, _size) = builder.put(&data).await.unwrap();
1173
1174 let keys = store.keys();
1176 let target = keys
1177 .iter()
1178 .find(|k| **k != cid.hash)
1179 .copied()
1180 .expect("expected at least one child chunk");
1181
1182 let mut corrupted = store.get(&target).await.unwrap().unwrap();
1183 corrupted[0] ^= 0xff;
1184 store.delete(&target).await.unwrap();
1185 store.put(target, corrupted).await.unwrap();
1186
1187 let legacy = verify_tree(store.clone(), &cid.hash).await.unwrap();
1189 assert!(legacy.valid);
1190
1191 let strict = verify_tree_integrity(store, &cid.hash).await.unwrap();
1192 assert!(!strict.valid);
1193 assert!(strict.missing.is_empty());
1194 assert!(!strict.corrupted.is_empty());
1195 assert!(strict.corrupted.contains(&target));
1196 }
1197
1198 #[tokio::test]
1199 async fn test_read_file_range_small_blob() {
1200 let store = make_store();
1201 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()).public());
1202 let reader = TreeReader::new(store);
1203
1204 let data = b"Hello, World!";
1205 let hash = builder.put_blob(data).await.unwrap();
1206
1207 let result = reader.read_file_range(&hash, 7, Some(12)).await.unwrap();
1209 assert_eq!(result, Some(b"World".to_vec()));
1210
1211 let result = reader.read_file_range(&hash, 0, Some(5)).await.unwrap();
1213 assert_eq!(result, Some(b"Hello".to_vec()));
1214
1215 let result = reader.read_file_range(&hash, 7, None).await.unwrap();
1217 assert_eq!(result, Some(b"World!".to_vec()));
1218 }
1219
1220 #[tokio::test]
1221 async fn test_read_file_range_chunked() {
1222 let store = make_store();
1223 let config = BuilderConfig::new(store.clone())
1225 .with_chunk_size(100)
1226 .public();
1227 let builder = TreeBuilder::new(config);
1228 let reader = TreeReader::new(store);
1229
1230 let mut data = vec![0u8; 350];
1232 for i in 0..data.len() {
1233 data[i] = (i % 256) as u8;
1234 }
1235
1236 let (cid, _size) = builder.put(&data).await.unwrap();
1237
1238 let result = reader
1240 .read_file_range(&cid.hash, 50, Some(150))
1241 .await
1242 .unwrap()
1243 .unwrap();
1244 assert_eq!(result.len(), 100);
1245 assert_eq!(result, data[50..150].to_vec());
1246
1247 let result = reader
1249 .read_file_range(&cid.hash, 200, Some(300))
1250 .await
1251 .unwrap()
1252 .unwrap();
1253 assert_eq!(result.len(), 100);
1254 assert_eq!(result, data[200..300].to_vec());
1255
1256 let result = reader
1258 .read_file_range(&cid.hash, 300, None)
1259 .await
1260 .unwrap()
1261 .unwrap();
1262 assert_eq!(result.len(), 50);
1263 assert_eq!(result, data[300..].to_vec());
1264 }
1265
1266 #[tokio::test]
1267 async fn test_chunked_raw_leaf_can_look_like_invalid_tree_node() {
1268 let store = make_store();
1269 let mut data = invalid_tree_shape_blob();
1270 let chunk_size = data.len();
1271 data.extend_from_slice(b"tail");
1272
1273 let config = BuilderConfig::new(store.clone())
1274 .with_chunk_size(chunk_size)
1275 .public();
1276 let builder = TreeBuilder::new(config);
1277 let reader = TreeReader::new(store);
1278
1279 let (cid, _size) = builder.put(&data).await.unwrap();
1280
1281 let full = reader.read_file(&cid.hash).await.unwrap().unwrap();
1282 assert_eq!(full, data);
1283
1284 let range = reader
1285 .read_file_range(&cid.hash, 1, Some((data.len() - 1) as u64))
1286 .await
1287 .unwrap()
1288 .unwrap();
1289 assert_eq!(range, data[1..data.len() - 1].to_vec());
1290
1291 let chunks = reader.read_file_chunks(&cid.hash).await.unwrap();
1292 assert_eq!(chunks.concat(), data);
1293 }
1294
1295 #[tokio::test]
1296 async fn test_read_file_range_entire_file() {
1297 let store = make_store();
1298 let config = BuilderConfig::new(store.clone())
1299 .with_chunk_size(100)
1300 .public();
1301 let builder = TreeBuilder::new(config);
1302 let reader = TreeReader::new(store);
1303
1304 let mut data = vec![0u8; 350];
1305 for i in 0..data.len() {
1306 data[i] = (i % 256) as u8;
1307 }
1308
1309 let (cid, _size) = builder.put(&data).await.unwrap();
1310
1311 let result = reader
1313 .read_file_range(&cid.hash, 0, None)
1314 .await
1315 .unwrap()
1316 .unwrap();
1317 assert_eq!(result, data);
1318 }
1319
1320 #[tokio::test]
1321 async fn test_read_file_range_out_of_bounds() {
1322 let store = make_store();
1323 let builder = TreeBuilder::new(BuilderConfig::new(store.clone()).public());
1324 let reader = TreeReader::new(store);
1325
1326 let data = b"Short";
1327 let hash = builder.put_blob(data).await.unwrap();
1328
1329 let result = reader.read_file_range(&hash, 100, Some(200)).await.unwrap();
1331 assert_eq!(result, Some(vec![]));
1332
1333 let result = reader.read_file_range(&hash, 0, Some(100)).await.unwrap();
1335 assert_eq!(result, Some(b"Short".to_vec()));
1336 }
1337
1338 #[tokio::test]
1339 async fn test_read_file_range_single_byte() {
1340 let store = make_store();
1341 let config = BuilderConfig::new(store.clone())
1342 .with_chunk_size(100)
1343 .public();
1344 let builder = TreeBuilder::new(config);
1345 let reader = TreeReader::new(store);
1346
1347 let mut data = vec![0u8; 350];
1348 for i in 0..data.len() {
1349 data[i] = (i % 256) as u8;
1350 }
1351
1352 let (cid, _size) = builder.put(&data).await.unwrap();
1353
1354 let result = reader
1356 .read_file_range(&cid.hash, 100, Some(101))
1357 .await
1358 .unwrap()
1359 .unwrap();
1360 assert_eq!(result.len(), 1);
1361 assert_eq!(result[0], 100);
1362 }
1363}