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