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