Skip to main content

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};
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/// Tree entry for directory listings
16#[derive(Debug, Clone)]
17pub struct TreeEntry {
18    pub name: String,
19    pub hash: Hash,
20    pub size: u64,
21    /// Type of content this entry points to (Blob, File, Dir, or Fanout)
22    pub link_type: LinkType,
23    /// Optional decryption key (for encrypted content)
24    pub key: Option<[u8; 32]>,
25    /// Optional metadata (createdAt, mimeType, thumbnail, etc.)
26    pub meta: Option<HashMap<String, serde_json::Value>>,
27}
28
29/// Walk entry for tree traversal
30#[derive(Debug, Clone)]
31pub struct WalkEntry {
32    pub path: String,
33    pub hash: Hash,
34    /// Type of content this entry points to (Blob, File, Dir, or Fanout)
35    pub link_type: LinkType,
36    pub size: u64,
37    /// Optional decryption key (for encrypted content)
38    pub key: Option<[u8; 32]>,
39}
40
41/// TreeReader - reads and traverses merkle trees
42pub 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    /// Get raw data by hash
115    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    /// Get and decode a tree node
123    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); // It's a blob, not a tree
136        }
137
138        let node = decode_tree_node(&data).map_err(ReaderError::Codec)?;
139        Ok(Some(node))
140    }
141
142    /// Check if hash points to a tree node or blob
143    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    /// Check if hash points to a directory (tree with named links)
157    /// vs a chunked file (tree with unnamed links) or raw blob
158    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    /// Read content by CID (handles both encrypted and public content)
172    ///
173    /// This is the unified read method that handles decryption automatically
174    /// when the CID contains an encryption key.
175    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    /// Read encrypted content by hash and key (internal)
184    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        // Decrypt the data
200        let decrypted = decrypt_chk(&encrypted_data, key)
201            .map_err(|e| ReaderError::Decryption(e.to_string()))?;
202
203        // Check if it's a tree node
204        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        // Single chunk data
211        Ok(Some(decrypted))
212    }
213
214    /// Assemble encrypted chunks from tree
215    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                // Intermediate tree node - recurse
233                let child_data = Box::pin(self.assemble_encrypted_chunks(&child_node)).await?;
234                parts.push(child_data);
235            } else {
236                // Leaf data chunk
237                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    /// Read a complete file (reassemble chunks if needed)
251    /// For unencrypted content only - use `get()` for unified access
252    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        // Check if it's a tree (chunked file) or raw blob
264        if !is_tree_node(&data) {
265            return Ok(Some(data)); // Direct blob
266        }
267
268        // It's a tree - reassemble chunks
269        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    /// Read a byte range from a file (fetches only necessary chunks)
275    ///
276    /// - `start`: Starting byte offset (inclusive)
277    /// - `end`: Ending byte offset (exclusive), or None to read to end
278    ///
279    /// For unencrypted content only - encrypted range reads not yet supported.
280    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        // Single blob - just slice it
297        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        // It's a chunked file - fetch only needed chunks
308        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    /// Assemble only the chunks needed for a byte range
314    async fn assemble_chunks_range(
315        &self,
316        node: &TreeNode,
317        start: u64,
318        end: Option<u64>,
319    ) -> Result<Vec<u8>, ReaderError> {
320        // First, flatten the tree to get all leaf chunks with their byte offsets
321        let chunks_info = self.collect_chunk_offsets(node).await?;
322
323        if chunks_info.is_empty() {
324            return Ok(vec![]);
325        }
326
327        // Calculate total size and actual end
328        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        // Find chunks that overlap with [start, actual_end)
336        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            // Check if this chunk overlaps with our range
344            if chunk_end > start && chunk_start < actual_end {
345                // Fetch this chunk
346                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                // Calculate slice bounds within this chunk
354                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            // Early exit if we've passed the requested range
371            if current_offset >= actual_end {
372                break;
373            }
374        }
375
376        Ok(result)
377    }
378
379    /// Collect all leaf chunk hashes with their byte offsets
380    /// Returns Vec<(hash, offset, size)>
381    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                // Intermediate node - recurse
408                Box::pin(self.collect_chunk_offsets_recursive(&child_node, chunks, offset)).await?;
409            } else {
410                // Leaf chunk
411                let size = child_data.len() as u64;
412                chunks.push((link.hash, *offset, size));
413                *offset += size;
414            }
415        }
416        Ok(())
417    }
418
419    /// Recursively assemble chunks from tree (unencrypted)
420    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                // Nested tree - recurse
433                parts.push(Box::pin(self.assemble_chunks(&child_node)).await?);
434            } else {
435                // Leaf blob
436                parts.push(child_data);
437            }
438        }
439
440        // Concatenate all parts
441        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    /// Read a file with streaming (returns chunks as vec)
451    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    /// Recursively collect chunks
471    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    /// List directory entries
493    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            // Skip internal chunk nodes (names starting with _chunk_)
503            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    /// Resolve a path within a tree
523    /// e.g., resolve_path("root/foo/bar.txt")
524    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(&current_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                // Check internal nodes
543                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    /// Find a link by name in a tree node
554    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    /// Search for name in internal subtrees
562    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            // Only search internal nodes
569            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            // Recurse deeper
583            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    /// Get total size of a tree
592    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        // Calculate from children
609        let mut total = 0u64;
610        for link in &node.links {
611            total += link.size;
612        }
613        Ok(total)
614    }
615
616    /// Walk entire tree depth-first
617    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, // TreeReader doesn't track keys
648                });
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, // directories are not encrypted
660        });
661
662        for link in &node.links {
663            let child_path = match &link.name {
664                Some(name) => {
665                    // Skip internal chunk nodes in path
666                    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
686/// Verify tree integrity
687/// Checks that all referenced hashes exist
688pub 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
743/// Verify tree integrity and content addresses.
744///
745/// Checks that:
746/// - all referenced hashes exist
747/// - every fetched blob/node satisfies `sha256(bytes) == referenced_hash`
748pub 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    // Strong integrity check: referenced hash must match fetched bytes.
792    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/// Result of tree verification
815#[derive(Debug, Clone)]
816pub struct VerifyResult {
817    pub valid: bool,
818    pub missing: Vec<Hash>,
819}
820
821/// Result of strong tree integrity verification.
822#[derive(Debug, Clone)]
823pub struct VerifyIntegrityResult {
824    pub valid: bool,
825    pub missing: Vec<Hash>,
826    pub corrupted: Vec<Hash>,
827}
828
829/// Reader error type
830#[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        // Use public() for tests that check raw data storage
940        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        // Delete one of the chunks
1113        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        // Delete one of the chunks
1152        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        // Pick a leaf chunk (non-root in this shape) and mutate bytes without changing key.
1175        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        // Legacy verifier checks only existence, so this still appears valid.
1188        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        // Read middle portion
1208        let result = reader.read_file_range(&hash, 7, Some(12)).await.unwrap();
1209        assert_eq!(result, Some(b"World".to_vec()));
1210
1211        // Read from start
1212        let result = reader.read_file_range(&hash, 0, Some(5)).await.unwrap();
1213        assert_eq!(result, Some(b"Hello".to_vec()));
1214
1215        // Read to end (no end specified)
1216        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        // Small chunk size to force chunking
1224        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        // Create 350 bytes of sequential data
1231        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        // Read bytes 50-150 (spans chunk boundary at 100)
1239        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        // Read bytes 200-300 (within third and fourth chunks)
1248        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        // Read last 50 bytes
1257        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        // Read entire file using range
1312        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        // Start past end of file
1330        let result = reader.read_file_range(&hash, 100, Some(200)).await.unwrap();
1331        assert_eq!(result, Some(vec![]));
1332
1333        // End past file length (should clamp)
1334        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        // Read single byte at chunk boundary
1355        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}