hashtree_core/
reader.rs

1//! Tree reader and traversal utilities
2//!
3//! Read files and directories from content-addressed storage
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::codec::{decode_tree_node, is_directory_node, is_tree_node, try_decode_tree_node};
9use crate::store::Store;
10use crate::types::{to_hex, Cid, Hash, Link, LinkType, TreeNode};
11
12use crate::crypto::{decrypt_chk, EncryptionKey};
13
14/// Tree entry for directory listings
15#[derive(Debug, Clone)]
16pub struct TreeEntry {
17    pub name: String,
18    pub hash: Hash,
19    pub size: u64,
20    /// Type of content this entry points to (Blob, File, or Dir)
21    pub link_type: LinkType,
22    /// Optional decryption key (for encrypted content)
23    pub key: Option<[u8; 32]>,
24    /// Optional metadata (createdAt, mimeType, thumbnail, etc.)
25    pub meta: Option<HashMap<String, serde_json::Value>>,
26}
27
28/// Walk entry for tree traversal
29#[derive(Debug, Clone)]
30pub struct WalkEntry {
31    pub path: String,
32    pub hash: Hash,
33    /// Type of content this entry points to (Blob, File, or Dir)
34    pub link_type: LinkType,
35    pub size: u64,
36    /// Optional decryption key (for encrypted content)
37    pub key: Option<[u8; 32]>,
38}
39
40/// TreeReader - reads and traverses merkle trees
41pub struct TreeReader<S: Store> {
42    store: Arc<S>,
43}
44
45impl<S: Store> TreeReader<S> {
46    pub fn new(store: Arc<S>) -> Self {
47        Self { store }
48    }
49
50    /// Get raw data by hash
51    pub async fn get_blob(&self, hash: &Hash) -> Result<Option<Vec<u8>>, ReaderError> {
52        self.store
53            .get(hash)
54            .await
55            .map_err(|e| ReaderError::Store(e.to_string()))
56    }
57
58    /// Get and decode a tree node
59    pub async fn get_tree_node(&self, hash: &Hash) -> Result<Option<TreeNode>, ReaderError> {
60        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
61            Some(d) => d,
62            None => return Ok(None),
63        };
64
65        if !is_tree_node(&data) {
66            return Ok(None); // It's a blob, not a tree
67        }
68
69        let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
70        Ok(Some(node))
71    }
72
73    /// Check if hash points to a tree node or blob
74    pub async fn is_tree(&self, hash: &Hash) -> Result<bool, ReaderError> {
75        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
76            Some(d) => d,
77            None => return Ok(false),
78        };
79        Ok(is_tree_node(&data))
80    }
81
82    /// Check if hash points to a directory (tree with named links)
83    /// vs a chunked file (tree with unnamed links) or raw blob
84    pub async fn is_directory(&self, hash: &Hash) -> Result<bool, ReaderError> {
85        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
86            Some(d) => d,
87            None => return Ok(false),
88        };
89        Ok(is_directory_node(&data))
90    }
91
92    /// Read content by CID (handles both encrypted and public content)
93    ///
94    /// This is the unified read method that handles decryption automatically
95    /// when the CID contains an encryption key.
96    pub async fn get(&self, cid: &Cid) -> Result<Option<Vec<u8>>, ReaderError> {
97        if let Some(key) = cid.key {
98            self.get_encrypted(&cid.hash, &key).await
99        } else {
100            self.read_file(&cid.hash).await
101        }
102    }
103
104    /// Read encrypted content by hash and key (internal)
105    async fn get_encrypted(
106        &self,
107        hash: &Hash,
108        key: &EncryptionKey,
109    ) -> Result<Option<Vec<u8>>, ReaderError> {
110        let encrypted_data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
111            Some(d) => d,
112            None => return Ok(None),
113        };
114
115        // Decrypt the data
116        let decrypted = decrypt_chk(&encrypted_data, key)
117            .map_err(|e| ReaderError::Decryption(e.to_string()))?;
118
119        // Check if it's a tree node
120        if is_tree_node(&decrypted) {
121            let node = decode_tree_node(&decrypted)?;
122            let assembled = self.assemble_encrypted_chunks(&node).await?;
123            return Ok(Some(assembled));
124        }
125
126        // Single chunk data
127        Ok(Some(decrypted))
128    }
129
130    /// Assemble encrypted chunks from tree
131    async fn assemble_encrypted_chunks(&self, node: &TreeNode) -> Result<Vec<u8>, ReaderError> {
132        let mut parts: Vec<Vec<u8>> = Vec::new();
133
134        for link in &node.links {
135            let chunk_key = link.key.ok_or(ReaderError::MissingKey)?;
136
137            let encrypted_child = self
138                .store
139                .get(&link.hash)
140                .await
141                .map_err(|e| ReaderError::Store(e.to_string()))?
142                .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
143
144            let decrypted = decrypt_chk(&encrypted_child, &chunk_key)
145                .map_err(|e| ReaderError::Decryption(e.to_string()))?;
146
147            if is_tree_node(&decrypted) {
148                // Intermediate tree node - recurse
149                let child_node = decode_tree_node(&decrypted)?;
150                let child_data = Box::pin(self.assemble_encrypted_chunks(&child_node)).await?;
151                parts.push(child_data);
152            } else {
153                // Leaf data chunk
154                parts.push(decrypted);
155            }
156        }
157
158        let total_len: usize = parts.iter().map(|p| p.len()).sum();
159        let mut result = Vec::with_capacity(total_len);
160        for part in parts {
161            result.extend_from_slice(&part);
162        }
163
164        Ok(result)
165    }
166
167    /// Read a complete file (reassemble chunks if needed)
168    /// For unencrypted content only - use `get()` for unified access
169    pub async fn read_file(&self, hash: &Hash) -> Result<Option<Vec<u8>>, ReaderError> {
170        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
171            Some(d) => d,
172            None => return Ok(None),
173        };
174
175        // Check if it's a tree (chunked file) or raw blob
176        if !is_tree_node(&data) {
177            return Ok(Some(data)); // Direct blob
178        }
179
180        // It's a tree - reassemble chunks
181        let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
182        let assembled = self.assemble_chunks(&node).await?;
183        Ok(Some(assembled))
184    }
185
186    /// Recursively assemble chunks from tree (unencrypted)
187    async fn assemble_chunks(&self, node: &TreeNode) -> Result<Vec<u8>, ReaderError> {
188        let mut parts: Vec<Vec<u8>> = Vec::new();
189
190        for link in &node.links {
191            let child_data = self
192                .store
193                .get(&link.hash)
194                .await
195                .map_err(|e| ReaderError::Store(e.to_string()))?
196                .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
197
198            if is_tree_node(&child_data) {
199                // Nested tree - recurse
200                let child_node = decode_tree_node(&child_data).map_err(ReaderError::Codec)?;
201                parts.push(Box::pin(self.assemble_chunks(&child_node)).await?);
202            } else {
203                // Leaf blob
204                parts.push(child_data);
205            }
206        }
207
208        // Concatenate all parts
209        let total_length: usize = parts.iter().map(|p| p.len()).sum();
210        let mut result = Vec::with_capacity(total_length);
211        for part in parts {
212            result.extend_from_slice(&part);
213        }
214
215        Ok(result)
216    }
217
218    /// Read a file with streaming (returns chunks as vec)
219    pub async fn read_file_chunks(&self, hash: &Hash) -> Result<Vec<Vec<u8>>, ReaderError> {
220        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
221            Some(d) => d,
222            None => return Ok(vec![]),
223        };
224
225        if !is_tree_node(&data) {
226            return Ok(vec![data]);
227        }
228
229        let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
230        self.collect_chunks(&node).await
231    }
232
233    /// Recursively collect chunks
234    async fn collect_chunks(&self, node: &TreeNode) -> Result<Vec<Vec<u8>>, ReaderError> {
235        let mut chunks = Vec::new();
236
237        for link in &node.links {
238            let child_data = self
239                .store
240                .get(&link.hash)
241                .await
242                .map_err(|e| ReaderError::Store(e.to_string()))?
243                .ok_or_else(|| ReaderError::MissingChunk(to_hex(&link.hash)))?;
244
245            if is_tree_node(&child_data) {
246                let child_node = decode_tree_node(&child_data).map_err(ReaderError::Codec)?;
247                chunks.extend(Box::pin(self.collect_chunks(&child_node)).await?);
248            } else {
249                chunks.push(child_data);
250            }
251        }
252
253        Ok(chunks)
254    }
255
256    /// List directory entries
257    pub async fn list_directory(&self, hash: &Hash) -> Result<Vec<TreeEntry>, ReaderError> {
258        let node = match self.get_tree_node(hash).await? {
259            Some(n) => n,
260            None => return Ok(vec![]),
261        };
262
263        let mut entries = Vec::new();
264
265        for link in &node.links {
266            // Skip internal chunk nodes (names starting with _chunk_)
267            if let Some(ref name) = link.name {
268                if name.starts_with("_chunk_") {
269                    // This is an internal split - recurse into it
270                    let sub_entries = Box::pin(self.list_directory(&link.hash)).await?;
271                    entries.extend(sub_entries);
272                    continue;
273                }
274
275                // Skip internal group nodes (names starting with _ but not _chunk_)
276                if name.starts_with('_') {
277                    let sub_entries = Box::pin(self.list_directory(&link.hash)).await?;
278                    entries.extend(sub_entries);
279                    continue;
280                }
281            }
282
283            entries.push(TreeEntry {
284                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
285                hash: link.hash,
286                size: link.size,
287                link_type: link.link_type,
288                key: link.key,
289                meta: link.meta.clone(),
290            });
291        }
292
293        Ok(entries)
294    }
295
296    /// Resolve a path within a tree
297    /// e.g., resolve_path("root/foo/bar.txt")
298    pub async fn resolve_path(&self, root_hash: &Hash, path: &str) -> Result<Option<Hash>, ReaderError> {
299        let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
300
301        let mut current_hash = *root_hash;
302
303        for part in parts {
304            let node = match self.get_tree_node(&current_hash).await? {
305                Some(n) => n,
306                None => return Ok(None),
307            };
308
309            if let Some(link) = self.find_link(&node, part) {
310                current_hash = link.hash;
311            } else {
312                // Check internal nodes
313                match self.find_in_subtrees(&node, part).await? {
314                    Some(hash) => current_hash = hash,
315                    None => return Ok(None),
316                }
317            }
318        }
319
320        Ok(Some(current_hash))
321    }
322
323    /// Find a link by name in a tree node
324    fn find_link(&self, node: &TreeNode, name: &str) -> Option<Link> {
325        node.links
326            .iter()
327            .find(|l| l.name.as_deref() == Some(name))
328            .cloned()
329    }
330
331    /// Search for name in internal subtrees
332    async fn find_in_subtrees(&self, node: &TreeNode, name: &str) -> Result<Option<Hash>, ReaderError> {
333        for link in &node.links {
334            // Only search internal nodes
335            if !link.name.as_ref().map(|n| n.starts_with('_')).unwrap_or(false) {
336                continue;
337            }
338
339            let sub_node = match self.get_tree_node(&link.hash).await? {
340                Some(n) => n,
341                None => continue,
342            };
343
344            if let Some(found) = self.find_link(&sub_node, name) {
345                return Ok(Some(found.hash));
346            }
347
348            // Recurse deeper
349            if let Some(deep_found) = Box::pin(self.find_in_subtrees(&sub_node, name)).await? {
350                return Ok(Some(deep_found));
351            }
352        }
353
354        Ok(None)
355    }
356
357    /// Get total size of a tree
358    pub async fn get_size(&self, hash: &Hash) -> Result<u64, ReaderError> {
359        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
360            Some(d) => d,
361            None => return Ok(0),
362        };
363
364        if !is_tree_node(&data) {
365            return Ok(data.len() as u64);
366        }
367
368        let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
369        // Calculate from children
370        let mut total = 0u64;
371        for link in &node.links {
372            total += link.size;
373        }
374        Ok(total)
375    }
376
377    /// Walk entire tree depth-first
378    pub async fn walk(&self, hash: &Hash, path: &str) -> Result<Vec<WalkEntry>, ReaderError> {
379        let mut entries = Vec::new();
380        self.walk_recursive(hash, path, &mut entries).await?;
381        Ok(entries)
382    }
383
384    async fn walk_recursive(
385        &self,
386        hash: &Hash,
387        path: &str,
388        entries: &mut Vec<WalkEntry>,
389    ) -> Result<(), ReaderError> {
390        let data = match self.store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
391            Some(d) => d,
392            None => return Ok(()),
393        };
394
395        let node = match try_decode_tree_node(&data) {
396            Some(n) => n,
397            None => {
398                entries.push(WalkEntry {
399                    path: path.to_string(),
400                    hash: *hash,
401                    link_type: LinkType::Blob,
402                    size: data.len() as u64,
403                    key: None, // TreeReader doesn't track keys
404                });
405                return Ok(());
406            }
407        };
408
409        let node_size: u64 = node.links.iter().map(|l| l.size).sum();
410        entries.push(WalkEntry {
411            path: path.to_string(),
412            hash: *hash,
413            link_type: node.node_type,
414            size: node_size,
415            key: None, // directories are not encrypted
416        });
417
418        for link in &node.links {
419            let child_path = match &link.name {
420                Some(name) => {
421                    // Skip internal chunk nodes in path
422                    if name.starts_with("_chunk_") || name.starts_with('_') {
423                        Box::pin(self.walk_recursive(&link.hash, path, entries)).await?;
424                        continue;
425                    }
426                    if path.is_empty() {
427                        name.clone()
428                    } else {
429                        format!("{}/{}", path, name)
430                    }
431                }
432                None => path.to_string(),
433            };
434
435            Box::pin(self.walk_recursive(&link.hash, &child_path, entries)).await?;
436        }
437
438        Ok(())
439    }
440}
441
442/// Verify tree integrity
443/// Checks that all referenced hashes exist
444pub async fn verify_tree<S: Store>(store: Arc<S>, root_hash: &Hash) -> Result<VerifyResult, ReaderError> {
445    let mut missing = Vec::new();
446    let mut visited = std::collections::HashSet::new();
447
448    verify_recursive(store, root_hash, &mut missing, &mut visited).await?;
449
450    Ok(VerifyResult {
451        valid: missing.is_empty(),
452        missing,
453    })
454}
455
456async fn verify_recursive<S: Store>(
457    store: Arc<S>,
458    hash: &Hash,
459    missing: &mut Vec<Hash>,
460    visited: &mut std::collections::HashSet<String>,
461) -> Result<(), ReaderError> {
462    let hex = to_hex(hash);
463    if visited.contains(&hex) {
464        return Ok(());
465    }
466    visited.insert(hex);
467
468    let data = match store.get(hash).await.map_err(|e| ReaderError::Store(e.to_string()))? {
469        Some(d) => d,
470        None => {
471            missing.push(*hash);
472            return Ok(());
473        }
474    };
475
476    if is_tree_node(&data) {
477        let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
478        for link in &node.links {
479            Box::pin(verify_recursive(store.clone(), &link.hash, missing, visited)).await?;
480        }
481    }
482
483    Ok(())
484}
485
486/// Result of tree verification
487#[derive(Debug, Clone)]
488pub struct VerifyResult {
489    pub valid: bool,
490    pub missing: Vec<Hash>,
491}
492
493/// Reader error type
494#[derive(Debug, thiserror::Error)]
495pub enum ReaderError {
496    #[error("Store error: {0}")]
497    Store(String),
498    #[error("Codec error: {0}")]
499    Codec(#[from] crate::codec::CodecError),
500    #[error("Missing chunk: {0}")]
501    MissingChunk(String),
502    #[error("Decryption error: {0}")]
503    Decryption(String),
504    #[error("Missing decryption key")]
505    MissingKey,
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use crate::builder::{BuilderConfig, TreeBuilder};
512    use crate::store::MemoryStore;
513    use crate::types::DirEntry;
514
515    fn make_store() -> Arc<MemoryStore> {
516        Arc::new(MemoryStore::new())
517    }
518
519    #[tokio::test]
520    async fn test_get_blob() {
521        let store = make_store();
522        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
523        let reader = TreeReader::new(store);
524
525        let data = vec![1u8, 2, 3, 4, 5];
526        let hash = builder.put_blob(&data).await.unwrap();
527
528        let result = reader.get_blob(&hash).await.unwrap();
529        assert_eq!(result, Some(data));
530    }
531
532    #[tokio::test]
533    async fn test_get_blob_missing() {
534        let store = make_store();
535        let reader = TreeReader::new(store);
536
537        let hash = [0u8; 32];
538        let result = reader.get_blob(&hash).await.unwrap();
539        assert!(result.is_none());
540    }
541
542    #[tokio::test]
543    async fn test_get_tree_node() {
544        let store = make_store();
545        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
546        let reader = TreeReader::new(store);
547
548        let file_hash = builder.put_blob(&[1u8]).await.unwrap();
549        let dir_hash = builder
550            .put_directory(vec![DirEntry::new("test.txt", file_hash).with_size(1)])
551            .await
552            .unwrap();
553
554        let node = reader.get_tree_node(&dir_hash).await.unwrap();
555        assert!(node.is_some());
556        assert_eq!(node.unwrap().links.len(), 1);
557    }
558
559    #[tokio::test]
560    async fn test_get_tree_node_returns_none_for_blob() {
561        let store = make_store();
562        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
563        let reader = TreeReader::new(store);
564
565        let hash = builder.put_blob(&[1u8, 2, 3]).await.unwrap();
566        let node = reader.get_tree_node(&hash).await.unwrap();
567        assert!(node.is_none());
568    }
569
570    #[tokio::test]
571    async fn test_is_tree() {
572        let store = make_store();
573        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
574        let reader = TreeReader::new(store);
575
576        let file_hash = builder.put_blob(&[1u8]).await.unwrap();
577        let dir_hash = builder
578            .put_directory(vec![DirEntry::new("test.txt", file_hash)])
579            .await
580            .unwrap();
581
582        assert!(reader.is_tree(&dir_hash).await.unwrap());
583        assert!(!reader.is_tree(&file_hash).await.unwrap());
584    }
585
586    #[tokio::test]
587    async fn test_read_file_small() {
588        let store = make_store();
589        // Use public() for tests that check raw data storage
590        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()).public());
591        let reader = TreeReader::new(store);
592
593        let data = vec![1u8, 2, 3, 4, 5];
594        let (cid, _size) = builder.put(&data).await.unwrap();
595
596        let result = reader.read_file(&cid.hash).await.unwrap();
597        assert_eq!(result, Some(data));
598    }
599
600    #[tokio::test]
601    async fn test_read_file_chunked() {
602        let store = make_store();
603        let config = BuilderConfig::new(store.clone()).with_chunk_size(100).public();
604        let builder = TreeBuilder::new(config);
605        let reader = TreeReader::new(store);
606
607        let mut data = vec![0u8; 350];
608        for i in 0..data.len() {
609            data[i] = (i % 256) as u8;
610        }
611
612        let (cid, _size) = builder.put(&data).await.unwrap();
613        let result = reader.read_file(&cid.hash).await.unwrap();
614
615        assert_eq!(result, Some(data));
616    }
617
618    #[tokio::test]
619    async fn test_list_directory() {
620        let store = make_store();
621        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
622        let reader = TreeReader::new(store);
623
624        let h1 = builder.put_blob(&[1u8]).await.unwrap();
625        let h2 = builder.put_blob(&[2u8]).await.unwrap();
626
627        let dir_hash = builder
628            .put_directory(
629                vec![
630                    DirEntry::new("first.txt", h1).with_size(1),
631                    DirEntry::new("second.txt", h2).with_size(1),
632                ],
633            )
634            .await
635            .unwrap();
636
637        let entries = reader.list_directory(&dir_hash).await.unwrap();
638
639        assert_eq!(entries.len(), 2);
640        assert!(entries.iter().any(|e| e.name == "first.txt"));
641        assert!(entries.iter().any(|e| e.name == "second.txt"));
642    }
643
644    #[tokio::test]
645    async fn test_resolve_path() {
646        let store = make_store();
647        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
648        let reader = TreeReader::new(store);
649
650        let file_data = vec![1u8, 2, 3];
651        let file_hash = builder.put_blob(&file_data).await.unwrap();
652
653        let dir_hash = builder
654            .put_directory(vec![DirEntry::new("test.txt", file_hash)])
655            .await
656            .unwrap();
657
658        let resolved = reader.resolve_path(&dir_hash, "test.txt").await.unwrap();
659        assert_eq!(resolved, Some(file_hash));
660    }
661
662    #[tokio::test]
663    async fn test_resolve_path_nested() {
664        let store = make_store();
665        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
666        let reader = TreeReader::new(store);
667
668        let file_hash = builder.put_blob(&[1u8]).await.unwrap();
669
670        let sub_sub_dir = builder
671            .put_directory(vec![DirEntry::new("deep.txt", file_hash)])
672            .await
673            .unwrap();
674
675        let sub_dir = builder
676            .put_directory(vec![DirEntry::new("level2", sub_sub_dir)])
677            .await
678            .unwrap();
679
680        let root_dir = builder
681            .put_directory(vec![DirEntry::new("level1", sub_dir)])
682            .await
683            .unwrap();
684
685        let resolved = reader
686            .resolve_path(&root_dir, "level1/level2/deep.txt")
687            .await
688            .unwrap();
689        assert_eq!(resolved, Some(file_hash));
690    }
691
692    #[tokio::test]
693    async fn test_get_size() {
694        let store = make_store();
695        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
696        let reader = TreeReader::new(store);
697
698        let data = vec![0u8; 123];
699        let hash = builder.put_blob(&data).await.unwrap();
700
701        assert_eq!(reader.get_size(&hash).await.unwrap(), 123);
702    }
703
704    #[tokio::test]
705    async fn test_walk() {
706        let store = make_store();
707        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
708        let reader = TreeReader::new(store);
709
710        let f1 = builder.put_blob(&[1u8]).await.unwrap();
711        let f2 = builder.put_blob(&[2u8, 3]).await.unwrap();
712
713        let sub_dir = builder
714            .put_directory(vec![DirEntry::new("nested.txt", f2).with_size(2)])
715            .await
716            .unwrap();
717
718        let root_dir = builder
719            .put_directory(
720                vec![
721                    DirEntry::new("root.txt", f1).with_size(1),
722                    DirEntry::new("sub", sub_dir),
723                ],
724            )
725            .await
726            .unwrap();
727
728        let entries = reader.walk(&root_dir, "").await.unwrap();
729        let paths: Vec<_> = entries.iter().map(|e| e.path.as_str()).collect();
730
731        assert!(paths.contains(&""));
732        assert!(paths.contains(&"root.txt"));
733        assert!(paths.contains(&"sub"));
734        assert!(paths.contains(&"sub/nested.txt"));
735    }
736
737    #[tokio::test]
738    async fn test_verify_tree_valid() {
739        let store = make_store();
740        let config = BuilderConfig::new(store.clone()).with_chunk_size(100).public();
741        let builder = TreeBuilder::new(config);
742
743        let data = vec![0u8; 350];
744        let (cid, _size) = builder.put(&data).await.unwrap();
745
746        let result = verify_tree(store, &cid.hash).await.unwrap();
747        assert!(result.valid);
748        assert!(result.missing.is_empty());
749    }
750
751    #[tokio::test]
752    async fn test_verify_tree_missing() {
753        let store = make_store();
754        let config = BuilderConfig::new(store.clone()).with_chunk_size(100).public();
755        let builder = TreeBuilder::new(config);
756
757        let data = vec![0u8; 350];
758        let (cid, _size) = builder.put(&data).await.unwrap();
759
760        // Delete one of the chunks
761        let keys = store.keys();
762        if let Some(chunk_to_delete) = keys.iter().find(|k| **k != cid.hash) {
763            store.delete(chunk_to_delete).await.unwrap();
764        }
765
766        let result = verify_tree(store, &cid.hash).await.unwrap();
767        assert!(!result.valid);
768        assert!(!result.missing.is_empty());
769    }
770}