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