Skip to main content

hashtree_core/
hashtree.rs

1//! HashTree - Unified merkle tree operations
2//!
3//! Single struct for creating, reading, and editing content-addressed merkle trees.
4//! Mirrors the hashtree-ts HashTree class API.
5
6use std::pin::Pin;
7use std::sync::Arc;
8
9use futures::io::AsyncRead;
10use futures::stream::{self, Stream};
11use futures::AsyncReadExt;
12
13use crate::builder::{BuilderError, DEFAULT_CHUNK_SIZE, DEFAULT_MAX_LINKS};
14use crate::codec::{decode_tree_node, encode_and_hash, is_directory_node, is_tree_node};
15use crate::directory::{directory_fanout_meta, is_internal_directory_link, DirectoryFanoutSpan};
16use crate::hash::sha256;
17use crate::reader::{ReaderError, TreeEntry, WalkEntry};
18use crate::store::Store;
19use crate::types::{to_hex, Cid, DirEntry, Hash, Link, LinkType, TreeNode};
20
21use crate::crypto::{decrypt_chk, encrypt_chk, EncryptionKey};
22
23const DEFAULT_STREAM_PUT_BATCH_TARGET_BYTES: usize = 64 * 1024 * 1024;
24const STREAM_PUT_BATCH_TARGET_BYTES_ENV: &str = "HTREE_STREAM_PUT_BATCH_TARGET_BYTES";
25const STREAM_PUT_BATCH_MAX_ITEMS: usize = 128;
26
27#[path = "hashtree/stream.rs"]
28mod read_stream;
29mod walk;
30
31/// HashTree configuration
32#[derive(Clone)]
33pub struct HashTreeConfig<S: Store> {
34    pub store: Arc<S>,
35    pub chunk_size: usize,
36    pub max_links: usize,
37    /// Whether to encrypt content (default: true when encryption feature enabled)
38    pub encrypted: bool,
39}
40
41impl<S: Store> HashTreeConfig<S> {
42    pub fn new(store: Arc<S>) -> Self {
43        Self {
44            store,
45            chunk_size: DEFAULT_CHUNK_SIZE,
46            max_links: DEFAULT_MAX_LINKS,
47            encrypted: true,
48        }
49    }
50
51    pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
52        self.chunk_size = chunk_size;
53        self
54    }
55
56    pub fn with_max_links(mut self, max_links: usize) -> Self {
57        self.max_links = max_links;
58        self
59    }
60
61    /// Disable encryption (store content publicly)
62    pub fn public(mut self) -> Self {
63        self.encrypted = false;
64        self
65    }
66}
67
68/// HashTree error type
69#[derive(Debug, thiserror::Error)]
70pub enum HashTreeError {
71    #[error("Store error: {0}")]
72    Store(String),
73    #[error("Codec error: {0}")]
74    Codec(#[from] crate::codec::CodecError),
75    #[error("Missing chunk: {0}")]
76    MissingChunk(String),
77    #[error("Path not found: {0}")]
78    PathNotFound(String),
79    #[error("Entry not found: {0}")]
80    EntryNotFound(String),
81    #[error("Encryption error: {0}")]
82    Encryption(String),
83    #[error("Decryption error: {0}")]
84    Decryption(String),
85    #[error("Content size {actual_size} exceeds max_size {max_size}")]
86    SizeLimitExceeded { max_size: u64, actual_size: u64 },
87}
88
89impl From<BuilderError> for HashTreeError {
90    fn from(e: BuilderError) -> Self {
91        match e {
92            BuilderError::Store(s) => HashTreeError::Store(s),
93            BuilderError::Codec(c) => HashTreeError::Codec(c),
94            BuilderError::Encryption(s) => HashTreeError::Encryption(s),
95        }
96    }
97}
98
99impl From<ReaderError> for HashTreeError {
100    fn from(e: ReaderError) -> Self {
101        match e {
102            ReaderError::Store(s) => HashTreeError::Store(s),
103            ReaderError::Codec(c) => HashTreeError::Codec(c),
104            ReaderError::MissingChunk(s) => HashTreeError::MissingChunk(s),
105            ReaderError::Decryption(s) => HashTreeError::Encryption(s),
106            ReaderError::MissingKey => {
107                HashTreeError::Encryption("missing decryption key".to_string())
108            }
109        }
110    }
111}
112
113/// HashTree - unified create, read, and edit merkle tree operations
114pub struct HashTree<S: Store> {
115    store: Arc<S>,
116    chunk_size: usize,
117    max_links: usize,
118    encrypted: bool,
119}
120
121impl<S: Store> HashTree<S> {
122    pub fn new(config: HashTreeConfig<S>) -> Self {
123        Self {
124            store: config.store,
125            chunk_size: config.chunk_size,
126            max_links: config.max_links,
127            encrypted: config.encrypted,
128        }
129    }
130
131    /// Check if encryption is enabled
132    pub fn is_encrypted(&self) -> bool {
133        self.encrypted
134    }
135
136    // ============ UNIFIED API ============
137
138    /// Store content, returns (Cid, size) where Cid is hash + optional key
139    /// Encrypts by default when encryption feature is enabled
140    pub async fn put(&self, data: &[u8]) -> Result<(Cid, u64), HashTreeError> {
141        let size = data.len() as u64;
142
143        // Small data - store as single chunk
144        if data.len() <= self.chunk_size {
145            let (hash, key) = self.put_chunk_internal(data).await?;
146            return Ok((Cid { hash, key }, size));
147        }
148
149        // Large data - chunk it
150        let mut links: Vec<Link> = Vec::new();
151        let mut offset = 0;
152
153        while offset < data.len() {
154            let end = (offset + self.chunk_size).min(data.len());
155            let chunk = &data[offset..end];
156            let chunk_size = chunk.len() as u64;
157            let (hash, key) = self.put_chunk_internal(chunk).await?;
158            links.push(Link {
159                hash,
160                name: None,
161                size: chunk_size,
162                key,
163                link_type: LinkType::Blob, // Leaf chunk (raw blob)
164                meta: None,
165            });
166            offset = end;
167        }
168
169        // Build tree from chunks
170        let (root_hash, root_key, _) = self.build_tree_internal(links, Some(size)).await?;
171        Ok((
172            Cid {
173                hash: root_hash,
174                key: root_key,
175            },
176            size,
177        ))
178    }
179
180    /// Get content by Cid (handles decryption automatically)
181    ///
182    /// - `max_size`: Optional max plaintext size in bytes. If exceeded, returns
183    ///   `HashTreeError::SizeLimitExceeded`.
184    pub async fn get(
185        &self,
186        cid: &Cid,
187        max_size: Option<u64>,
188    ) -> Result<Option<Vec<u8>>, HashTreeError> {
189        if let Some(key) = cid.key {
190            self.get_encrypted(&cid.hash, &key, max_size).await
191        } else {
192            self.read_file_with_limit(&cid.hash, max_size).await
193        }
194    }
195
196    /// Store content from an async reader (streaming put)
197    ///
198    /// Reads data in chunks and builds a merkle tree incrementally.
199    /// Useful for large files or streaming data sources.
200    /// Returns (Cid, size) where Cid is hash + optional key
201    pub async fn put_stream<R: AsyncRead + Unpin>(
202        &self,
203        reader: R,
204    ) -> Result<(Cid, u64), HashTreeError> {
205        self.put_stream_with_progress(reader, |_| {}).await
206    }
207
208    /// Store content from an async reader and report each stored chunk size.
209    pub async fn put_stream_with_progress<R, F>(
210        &self,
211        mut reader: R,
212        mut progress: F,
213    ) -> Result<(Cid, u64), HashTreeError>
214    where
215        R: AsyncRead + Unpin,
216        F: FnMut(u64),
217    {
218        let mut buffer = vec![0u8; self.chunk_size];
219        let mut links = Vec::new();
220        let mut total_size: u64 = 0;
221        let mut consistent_key: Option<[u8; 32]> = None;
222        let mut pending_store_items = Vec::new();
223        let mut pending_store_bytes = 0usize;
224        let batch_target_bytes = stream_put_batch_target_bytes();
225
226        loop {
227            let mut chunk = Vec::new();
228            let mut bytes_read = 0;
229
230            // Read until we have a full chunk or EOF
231            while bytes_read < self.chunk_size {
232                let n = reader
233                    .read(&mut buffer[..self.chunk_size - bytes_read])
234                    .await
235                    .map_err(|e| HashTreeError::Store(format!("read error: {}", e)))?;
236                if n == 0 {
237                    break; // EOF
238                }
239                chunk.extend_from_slice(&buffer[..n]);
240                bytes_read += n;
241            }
242
243            if chunk.is_empty() {
244                break; // No more data
245            }
246
247            let chunk_len = chunk.len() as u64;
248            total_size += chunk_len;
249
250            let (hash, data, key) = self.prepare_chunk_for_store(&chunk)?;
251
252            // Track consistent key for single-key result
253            if links.is_empty() {
254                consistent_key = key;
255            } else if consistent_key != key {
256                consistent_key = None;
257            }
258
259            links.push(Link {
260                hash,
261                name: None,
262                size: chunk_len,
263                key,
264                link_type: LinkType::Blob, // Leaf chunk (raw blob)
265                meta: None,
266            });
267
268            pending_store_bytes = pending_store_bytes.saturating_add(data.len());
269            pending_store_items.push((hash, data, chunk_len));
270            if pending_store_bytes >= batch_target_bytes
271                || pending_store_items.len() >= STREAM_PUT_BATCH_MAX_ITEMS
272            {
273                Self::flush_stream_store_batch(
274                    self.store.as_ref(),
275                    &mut pending_store_items,
276                    &mut pending_store_bytes,
277                    &mut progress,
278                )
279                .await?;
280            }
281        }
282
283        if links.is_empty() {
284            // Empty input
285            let (hash, key) = self.put_chunk_internal(&[]).await?;
286            return Ok((Cid { hash, key }, 0));
287        }
288
289        Self::flush_stream_store_batch(
290            self.store.as_ref(),
291            &mut pending_store_items,
292            &mut pending_store_bytes,
293            &mut progress,
294        )
295        .await?;
296
297        // Build tree from chunks
298        let (root_hash, root_key, _) = self.build_tree_internal(links, Some(total_size)).await?;
299        Ok((
300            Cid {
301                hash: root_hash,
302                key: root_key,
303            },
304            total_size,
305        ))
306    }
307
308    /// Store a chunk with optional encryption
309    async fn put_chunk_internal(
310        &self,
311        data: &[u8],
312    ) -> Result<(Hash, Option<EncryptionKey>), HashTreeError> {
313        let (hash, stored_data, key) = self.prepare_chunk_for_store(data)?;
314        self.store
315            .put(hash, stored_data)
316            .await
317            .map_err(|e| HashTreeError::Store(e.to_string()))?;
318        Ok((hash, key))
319    }
320
321    fn prepare_chunk_for_store(
322        &self,
323        data: &[u8],
324    ) -> Result<(Hash, Vec<u8>, Option<EncryptionKey>), HashTreeError> {
325        if self.encrypted {
326            let (encrypted, key) =
327                encrypt_chk(data).map_err(|e| HashTreeError::Encryption(e.to_string()))?;
328            let hash = sha256(&encrypted);
329            Ok((hash, encrypted, Some(key)))
330        } else {
331            let hash = sha256(data);
332            Ok((hash, data.to_vec(), None))
333        }
334    }
335
336    async fn flush_stream_store_batch<F>(
337        store: &S,
338        pending: &mut Vec<(Hash, Vec<u8>, u64)>,
339        pending_bytes: &mut usize,
340        progress: &mut F,
341    ) -> Result<(), HashTreeError>
342    where
343        F: FnMut(u64),
344    {
345        if pending.is_empty() {
346            return Ok(());
347        }
348
349        let mut items = Vec::with_capacity(pending.len());
350        let mut sizes = Vec::with_capacity(pending.len());
351        for (hash, data, size) in pending.drain(..) {
352            items.push((hash, data));
353            sizes.push(size);
354        }
355        *pending_bytes = 0;
356
357        store
358            .put_many(items)
359            .await
360            .map_err(|e| HashTreeError::Store(e.to_string()))?;
361        for size in sizes {
362            progress(size);
363        }
364        Ok(())
365    }
366
367    /// Build tree and return (hash, optional_key)
368    async fn build_tree_internal(
369        &self,
370        links: Vec<Link>,
371        total_size: Option<u64>,
372    ) -> Result<(Hash, Option<[u8; 32]>, LinkType), HashTreeError> {
373        // Single link with matching size - return directly
374        if links.len() == 1 {
375            if let Some(ts) = total_size {
376                if links[0].size == ts {
377                    return Ok((links[0].hash, links[0].key, links[0].link_type));
378                }
379            }
380        }
381
382        if links.len() <= self.max_links {
383            let node = TreeNode {
384                node_type: LinkType::File,
385                links,
386            };
387            let (data, _) = encode_and_hash(&node)?;
388
389            if self.encrypted {
390                let (encrypted, key) =
391                    encrypt_chk(&data).map_err(|e| HashTreeError::Encryption(e.to_string()))?;
392                let hash = sha256(&encrypted);
393                self.store
394                    .put(hash, encrypted)
395                    .await
396                    .map_err(|e| HashTreeError::Store(e.to_string()))?;
397                return Ok((hash, Some(key), LinkType::File));
398            }
399
400            // Unencrypted path
401            let hash = sha256(&data);
402            self.store
403                .put(hash, data)
404                .await
405                .map_err(|e| HashTreeError::Store(e.to_string()))?;
406            return Ok((hash, None, LinkType::File));
407        }
408
409        // Too many links - create subtrees
410        let mut sub_links = Vec::new();
411        for batch in links.chunks(self.max_links) {
412            let batch_size: u64 = batch.iter().map(|l| l.size).sum();
413            let (hash, key, link_type) =
414                Box::pin(self.build_tree_internal(batch.to_vec(), Some(batch_size))).await?;
415            sub_links.push(Link {
416                hash,
417                name: None,
418                size: batch_size,
419                key,
420                link_type,
421                meta: None,
422            });
423        }
424
425        Box::pin(self.build_tree_internal(sub_links, total_size)).await
426    }
427
428    /// Get encrypted content by hash and key
429    async fn get_encrypted(
430        &self,
431        hash: &Hash,
432        key: &EncryptionKey,
433        max_size: Option<u64>,
434    ) -> Result<Option<Vec<u8>>, HashTreeError> {
435        let decrypted = match self.get_encrypted_root(hash, key).await? {
436            Some(data) => data,
437            None => return Ok(None),
438        };
439
440        // Check if it's a tree node
441        if is_tree_node(&decrypted) {
442            let node = decode_tree_node(&decrypted)?;
443            let declared_size: u64 = node.links.iter().map(|l| l.size).sum();
444            Self::ensure_size_limit(max_size, declared_size)?;
445
446            let mut bytes_read = 0u64;
447            let assembled = self
448                .assemble_encrypted_chunks_limited(&node, max_size, &mut bytes_read)
449                .await?;
450            return Ok(Some(assembled));
451        }
452
453        // Single chunk data
454        Self::ensure_size_limit(max_size, decrypted.len() as u64)?;
455        Ok(Some(decrypted))
456    }
457
458    async fn get_encrypted_root(
459        &self,
460        hash: &Hash,
461        key: &EncryptionKey,
462    ) -> Result<Option<Vec<u8>>, HashTreeError> {
463        self.get_cid_root_bytes(&Cid {
464            hash: *hash,
465            key: Some(*key),
466        })
467        .await
468        .map_err(|err| match err {
469            HashTreeError::Decryption(message) => HashTreeError::Encryption(message),
470            other => other,
471        })
472    }
473
474    fn ensure_size_limit(max_size: Option<u64>, actual_size: u64) -> Result<(), HashTreeError> {
475        if let Some(max_size) = max_size {
476            if actual_size > max_size {
477                return Err(HashTreeError::SizeLimitExceeded {
478                    max_size,
479                    actual_size,
480                });
481            }
482        }
483        Ok(())
484    }
485
486    fn decode_linked_file_node(
487        link: &Link,
488        data: &[u8],
489    ) -> Result<Option<TreeNode>, HashTreeError> {
490        match link.link_type {
491            LinkType::File => match decode_tree_node(data) {
492                Ok(node) => Ok(Some(node)),
493                Err(_) if link.size == data.len() as u64 => Ok(None),
494                Err(err) => Err(HashTreeError::Codec(err)),
495            },
496            LinkType::Blob => {
497                if link.size == data.len() as u64 {
498                    return Ok(None);
499                }
500
501                match decode_tree_node(data) {
502                    Ok(node) if node.node_type == LinkType::File => Ok(Some(node)),
503                    _ => Ok(None),
504                }
505            }
506            LinkType::Dir | LinkType::Fanout => Ok(None),
507        }
508    }
509
510    /// Assemble encrypted chunks from tree
511    async fn assemble_encrypted_chunks_limited(
512        &self,
513        node: &TreeNode,
514        max_size: Option<u64>,
515        bytes_read: &mut u64,
516    ) -> Result<Vec<u8>, HashTreeError> {
517        let mut parts: Vec<Vec<u8>> = Vec::new();
518
519        for link in &node.links {
520            let projected = (*bytes_read).saturating_add(link.size);
521            Self::ensure_size_limit(max_size, projected)?;
522
523            let chunk_key = link
524                .key
525                .ok_or_else(|| HashTreeError::Encryption("missing chunk key".to_string()))?;
526
527            let encrypted_child = self
528                .store
529                .get(&link.hash)
530                .await
531                .map_err(|e| HashTreeError::Store(e.to_string()))?
532                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
533
534            let decrypted = decrypt_chk(&encrypted_child, &chunk_key)
535                .map_err(|e| HashTreeError::Encryption(e.to_string()))?;
536
537            if let Some(child_node) = Self::decode_linked_file_node(link, &decrypted)? {
538                // Intermediate tree node - recurse
539                let child_data = Box::pin(self.assemble_encrypted_chunks_limited(
540                    &child_node,
541                    max_size,
542                    bytes_read,
543                ))
544                .await?;
545                parts.push(child_data);
546            } else {
547                // Leaf data chunk
548                let projected = (*bytes_read).saturating_add(decrypted.len() as u64);
549                Self::ensure_size_limit(max_size, projected)?;
550                *bytes_read = projected;
551                parts.push(decrypted);
552            }
553        }
554
555        let total_len: usize = parts.iter().map(|p| p.len()).sum();
556        let mut result = Vec::with_capacity(total_len);
557        for part in parts {
558            result.extend_from_slice(&part);
559        }
560
561        Ok(result)
562    }
563
564    // ============ LOW-LEVEL CREATE ============
565
566    /// Store a blob directly (small data, no encryption)
567    /// Returns the content hash
568    pub async fn put_blob(&self, data: &[u8]) -> Result<Hash, HashTreeError> {
569        let hash = sha256(data);
570        self.store
571            .put(hash, data.to_vec())
572            .await
573            .map_err(|e| HashTreeError::Store(e.to_string()))?;
574        Ok(hash)
575    }
576
577    /// Store a file, chunking if necessary
578    /// Returns (Cid, size) where Cid is hash + optional key
579    pub async fn put_file(&self, data: &[u8]) -> Result<(Cid, u64), HashTreeError> {
580        self.put(data).await
581    }
582
583    /// Build a directory from entries
584    /// Returns Cid with key if encrypted
585    ///
586    /// Large directories are split into BUD-17 fanout nodes.
587    pub async fn put_directory(&self, entries: Vec<DirEntry>) -> Result<Cid, HashTreeError> {
588        // Sort entries by name for deterministic hashing
589        let mut sorted = entries;
590        sorted.sort_by(|a, b| a.name.cmp(&b.name));
591
592        let links: Vec<Link> = sorted
593            .into_iter()
594            .map(|e| Link {
595                hash: e.hash,
596                name: Some(e.name),
597                size: e.size,
598                key: e.key,
599                link_type: e.link_type,
600                meta: e.meta,
601            })
602            .collect();
603
604        if links.len() <= self.max_links {
605            return self.put_directory_node(links).await;
606        }
607
608        self.build_directory_by_chunks(links).await
609    }
610
611    async fn put_directory_node(&self, links: Vec<Link>) -> Result<Cid, HashTreeError> {
612        self.put_tree_node_with_type(LinkType::Dir, links).await
613    }
614
615    async fn put_tree_node_with_type(
616        &self,
617        node_type: LinkType,
618        links: Vec<Link>,
619    ) -> Result<Cid, HashTreeError> {
620        let node = TreeNode { node_type, links };
621        let (data, plain_hash) = encode_and_hash(&node)?;
622
623        if self.encrypted {
624            let (encrypted, key) =
625                encrypt_chk(&data).map_err(|e| HashTreeError::Encryption(e.to_string()))?;
626            let hash = sha256(&encrypted);
627            self.store
628                .put(hash, encrypted)
629                .await
630                .map_err(|e| HashTreeError::Store(e.to_string()))?;
631            return Ok(Cid {
632                hash,
633                key: Some(key),
634            });
635        }
636
637        self.store
638            .put(plain_hash, data)
639            .await
640            .map_err(|e| HashTreeError::Store(e.to_string()))?;
641        Ok(Cid {
642            hash: plain_hash,
643            key: None,
644        })
645    }
646
647    async fn build_directory_by_chunks(&self, links: Vec<Link>) -> Result<Cid, HashTreeError> {
648        let spans = links
649            .into_iter()
650            .map(|link| {
651                let name = link.name.clone().unwrap_or_else(|| to_hex(&link.hash));
652                DirectoryFanoutSpan {
653                    link,
654                    count: 1,
655                    first: name.clone(),
656                    last: name,
657                }
658            })
659            .collect();
660        self.build_directory_fanout_level(spans, LinkType::Dir)
661            .await
662    }
663
664    async fn build_directory_fanout_level(
665        &self,
666        spans: Vec<DirectoryFanoutSpan>,
667        child_node_type: LinkType,
668    ) -> Result<Cid, HashTreeError> {
669        let mut sub_trees: Vec<DirectoryFanoutSpan> = Vec::new();
670
671        for batch in spans.chunks(self.max_links) {
672            let Some(first_span) = batch.first() else {
673                continue;
674            };
675            let last_span = batch.last().expect("non-empty fanout batch");
676            let count: usize = batch.iter().map(|span| span.count).sum();
677            let batch_size: u64 = batch.iter().map(|span| span.link.size).sum();
678            let child_links = batch.iter().map(|span| span.link.clone()).collect();
679            let child_cid = self
680                .put_tree_node_with_type(child_node_type, child_links)
681                .await?;
682
683            sub_trees.push(DirectoryFanoutSpan {
684                link: Link {
685                    hash: child_cid.hash,
686                    name: None,
687                    size: batch_size,
688                    key: child_cid.key,
689                    link_type: child_node_type,
690                    meta: Some(directory_fanout_meta(
691                        count,
692                        &first_span.first,
693                        &last_span.last,
694                    )),
695                },
696                count,
697                first: first_span.first.clone(),
698                last: last_span.last.clone(),
699            });
700        }
701
702        if sub_trees.len() <= self.max_links {
703            return self
704                .put_tree_node_with_type(
705                    LinkType::Fanout,
706                    sub_trees.into_iter().map(|span| span.link).collect(),
707                )
708                .await;
709        }
710
711        Box::pin(self.build_directory_fanout_level(sub_trees, LinkType::Fanout)).await
712    }
713
714    /// Create a tree node with custom links
715    pub async fn put_tree_node(&self, links: Vec<Link>) -> Result<Hash, HashTreeError> {
716        let node = TreeNode {
717            node_type: LinkType::Dir,
718            links,
719        };
720
721        let (data, hash) = encode_and_hash(&node)?;
722        self.store
723            .put(hash, data)
724            .await
725            .map_err(|e| HashTreeError::Store(e.to_string()))?;
726        Ok(hash)
727    }
728
729    // ============ READ ============
730
731    /// Get raw data by hash
732    pub async fn get_blob(&self, hash: &Hash) -> Result<Option<Vec<u8>>, HashTreeError> {
733        self.store
734            .get(hash)
735            .await
736            .map_err(|e| HashTreeError::Store(e.to_string()))
737    }
738
739    async fn has_stored_chunk(&self, hash: &Hash) -> Result<bool, HashTreeError> {
740        self.store
741            .get(hash)
742            .await
743            .map(|data| data.is_some())
744            .map_err(|e| HashTreeError::Store(e.to_string()))
745    }
746
747    /// Get and decode a tree node (unencrypted)
748    pub async fn get_tree_node(&self, hash: &Hash) -> Result<Option<TreeNode>, HashTreeError> {
749        let data = match self
750            .store
751            .get(hash)
752            .await
753            .map_err(|e| HashTreeError::Store(e.to_string()))?
754        {
755            Some(d) => d,
756            None => return Ok(None),
757        };
758
759        if !is_tree_node(&data) {
760            return Ok(None);
761        }
762
763        let node = decode_tree_node(&data)?;
764        Ok(Some(node))
765    }
766
767    /// Get and decode a tree node through its CID, decrypting it when needed.
768    pub async fn get_tree_node_by_cid(&self, cid: &Cid) -> Result<Option<TreeNode>, HashTreeError> {
769        let Some(data) = self.get_cid_root_bytes(cid).await? else {
770            return Ok(None);
771        };
772        if !is_tree_node(&data) {
773            return Ok(None);
774        }
775        Ok(Some(decode_tree_node(&data)?))
776    }
777
778    async fn get_cid_root_bytes(&self, cid: &Cid) -> Result<Option<Vec<u8>>, HashTreeError> {
779        let data = match self
780            .store
781            .get(&cid.hash)
782            .await
783            .map_err(|e| HashTreeError::Store(e.to_string()))?
784        {
785            Some(d) => d,
786            None => return Ok(None),
787        };
788
789        let Some(key) = &cid.key else {
790            return Ok(Some(data));
791        };
792
793        let raw_is_tree = is_tree_node(&data);
794        match decrypt_chk(&data, key) {
795            // AES-GCM authentication makes a successful decryption
796            // authoritative. Ciphertext can coincidentally deserialize into
797            // the loose tree shape; preferring that shape here makes valid
798            // encrypted blobs fail with a bogus node type.
799            Ok(decrypted) => Ok(Some(decrypted)),
800            Err(err) => {
801                if raw_is_tree {
802                    Ok(Some(data))
803                } else {
804                    Err(HashTreeError::Decryption(err.to_string()))
805                }
806            }
807        }
808    }
809
810    /// Get and decode a tree node using Cid (with decryption if key present)
811    pub async fn get_node(&self, cid: &Cid) -> Result<Option<TreeNode>, HashTreeError> {
812        let decrypted = match self.get_cid_root_bytes(cid).await? {
813            Some(d) => d,
814            None => return Ok(None),
815        };
816
817        if !is_tree_node(&decrypted) {
818            return Ok(None);
819        }
820
821        let node = decode_tree_node(&decrypted)?;
822        Ok(Some(node))
823    }
824
825    /// Get directory node, handling historical byte-chunked directory data.
826    /// Use this when you know the target is a directory (from parent link_type)
827    pub async fn get_directory_node(&self, cid: &Cid) -> Result<Option<TreeNode>, HashTreeError> {
828        let decrypted = match self.get_cid_root_bytes(cid).await? {
829            Some(d) => d,
830            None => return Ok(None),
831        };
832
833        if !is_tree_node(&decrypted) {
834            return Ok(None);
835        }
836
837        let node = decode_tree_node(&decrypted)?;
838
839        // If this is a file tree (chunked data), reassemble to get actual directory
840        if node.node_type == LinkType::File {
841            let mut bytes_read = 0u64;
842            let assembled = self
843                .assemble_chunks_limited(&node, None, &mut bytes_read)
844                .await?;
845            if is_tree_node(&assembled) {
846                let inner_node = decode_tree_node(&assembled)?;
847                return Ok(Some(inner_node));
848            }
849        }
850
851        Ok(Some(node))
852    }
853
854    /// Check if hash points to a tree node (no decryption)
855    pub async fn is_tree(&self, hash: &Hash) -> Result<bool, HashTreeError> {
856        let data = match self
857            .store
858            .get(hash)
859            .await
860            .map_err(|e| HashTreeError::Store(e.to_string()))?
861        {
862            Some(d) => d,
863            None => return Ok(false),
864        };
865        Ok(is_tree_node(&data))
866    }
867
868    /// Check if Cid points to a directory (with decryption)
869    pub async fn is_dir(&self, cid: &Cid) -> Result<bool, HashTreeError> {
870        Ok(matches!(
871            self.get_directory_node(cid).await?,
872            Some(node) if node.node_type.is_directory_like()
873        ))
874    }
875
876    /// Check if hash points to a directory (tree with named links, no decryption)
877    pub async fn is_directory(&self, hash: &Hash) -> Result<bool, HashTreeError> {
878        let data = match self
879            .store
880            .get(hash)
881            .await
882            .map_err(|e| HashTreeError::Store(e.to_string()))?
883        {
884            Some(d) => d,
885            None => return Ok(false),
886        };
887        Ok(is_directory_node(&data))
888    }
889
890    /// Read a complete file (reassemble chunks if needed)
891    pub async fn read_file(&self, hash: &Hash) -> Result<Option<Vec<u8>>, HashTreeError> {
892        self.read_file_with_limit(hash, None).await
893    }
894
895    /// Read a complete file with optional size limit.
896    async fn read_file_with_limit(
897        &self,
898        hash: &Hash,
899        max_size: Option<u64>,
900    ) -> Result<Option<Vec<u8>>, HashTreeError> {
901        let data = match self
902            .store
903            .get(hash)
904            .await
905            .map_err(|e| HashTreeError::Store(e.to_string()))?
906        {
907            Some(d) => d,
908            None => return Ok(None),
909        };
910
911        // Check if it's a tree (chunked file) or raw blob
912        if !is_tree_node(&data) {
913            Self::ensure_size_limit(max_size, data.len() as u64)?;
914            return Ok(Some(data));
915        }
916
917        // It's a tree - reassemble chunks
918        let node = decode_tree_node(&data)?;
919        let declared_size: u64 = node.links.iter().map(|l| l.size).sum();
920        Self::ensure_size_limit(max_size, declared_size)?;
921
922        let mut bytes_read = 0u64;
923        let assembled = self
924            .assemble_chunks_limited(&node, max_size, &mut bytes_read)
925            .await?;
926        Ok(Some(assembled))
927    }
928
929    /// Read a byte range from a file (fetches only necessary chunks)
930    ///
931    /// - `start`: Starting byte offset (inclusive)
932    /// - `end`: Ending byte offset (exclusive), or None to read to end
933    ///
934    /// This is more efficient than read_file() for partial reads of large files.
935    pub async fn read_file_range(
936        &self,
937        hash: &Hash,
938        start: u64,
939        end: Option<u64>,
940    ) -> Result<Option<Vec<u8>>, HashTreeError> {
941        let data = match self
942            .store
943            .get(hash)
944            .await
945            .map_err(|e| HashTreeError::Store(e.to_string()))?
946        {
947            Some(d) => d,
948            None => return Ok(None),
949        };
950
951        // Single blob - just slice it
952        if !is_tree_node(&data) {
953            let start_idx = start as usize;
954            let end_idx = end.map(|e| e as usize).unwrap_or(data.len());
955            if start_idx >= data.len() {
956                return Ok(Some(vec![]));
957            }
958            let end_idx = end_idx.min(data.len());
959            return Ok(Some(data[start_idx..end_idx].to_vec()));
960        }
961
962        // It's a chunked file - fetch only needed chunks
963        let node = decode_tree_node(&data)?;
964        let range_data = self.assemble_chunks_range(&node, start, end).await?;
965        Ok(Some(range_data))
966    }
967
968    /// Read a byte range from a file using a Cid (handles decryption if key present)
969    pub async fn read_file_range_cid(
970        &self,
971        cid: &Cid,
972        start: u64,
973        end: Option<u64>,
974    ) -> Result<Option<Vec<u8>>, HashTreeError> {
975        if let Some(key) = cid.key {
976            let data = match self.get_encrypted_root(&cid.hash, &key).await? {
977                Some(d) => d,
978                None => return Ok(None),
979            };
980
981            if is_tree_node(&data) {
982                let node = decode_tree_node(&data)?;
983                let total_size: u64 = node.links.iter().map(|link| link.size).sum();
984                let actual_end = end.unwrap_or(total_size).min(total_size);
985                if start >= actual_end {
986                    return Ok(Some(vec![]));
987                }
988
989                let mut result = Vec::with_capacity((actual_end - start) as usize);
990                self.append_encrypted_range(&node, start, actual_end, 0, &mut result)
991                    .await?;
992                return Ok(Some(result));
993            }
994
995            let start_idx = start as usize;
996            let end_idx = end.map(|e| e as usize).unwrap_or(data.len());
997            if start_idx >= data.len() {
998                return Ok(Some(vec![]));
999            }
1000            let end_idx = end_idx.min(data.len());
1001            return Ok(Some(data[start_idx..end_idx].to_vec()));
1002        }
1003
1004        self.read_file_range(&cid.hash, start, end).await
1005    }
1006
1007    async fn append_encrypted_range(
1008        &self,
1009        node: &TreeNode,
1010        start: u64,
1011        end: u64,
1012        base_offset: u64,
1013        result: &mut Vec<u8>,
1014    ) -> Result<(), HashTreeError> {
1015        let mut current_offset = base_offset;
1016
1017        for link in &node.links {
1018            let child_start = current_offset;
1019            let child_end = child_start.saturating_add(link.size);
1020            current_offset = child_end;
1021
1022            if child_end <= start {
1023                continue;
1024            }
1025            if child_start >= end {
1026                break;
1027            }
1028
1029            let chunk_key = link
1030                .key
1031                .ok_or_else(|| HashTreeError::Encryption("missing chunk key".to_string()))?;
1032
1033            let encrypted_child = self
1034                .store
1035                .get(&link.hash)
1036                .await
1037                .map_err(|e| HashTreeError::Store(e.to_string()))?
1038                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1039            let decrypted_child = decrypt_chk(&encrypted_child, &chunk_key)
1040                .map_err(|e| HashTreeError::Encryption(e.to_string()))?;
1041
1042            if let Some(child_node) = Self::decode_linked_file_node(link, &decrypted_child)? {
1043                Box::pin(self.append_encrypted_range(&child_node, start, end, child_start, result))
1044                    .await?;
1045                continue;
1046            }
1047
1048            let slice_start = if start > child_start {
1049                (start - child_start) as usize
1050            } else {
1051                0
1052            };
1053            let slice_end = if end < child_end {
1054                (end - child_start) as usize
1055            } else {
1056                decrypted_child.len()
1057            };
1058            result.extend_from_slice(&decrypted_child[slice_start..slice_end]);
1059        }
1060
1061        Ok(())
1062    }
1063
1064    /// Assemble only the chunks needed for a byte range
1065    async fn assemble_chunks_range(
1066        &self,
1067        node: &TreeNode,
1068        start: u64,
1069        end: Option<u64>,
1070    ) -> Result<Vec<u8>, HashTreeError> {
1071        // First, flatten the tree to get all leaf chunks with their byte offsets
1072        let chunks_info = self.collect_chunk_offsets(node).await?;
1073
1074        if chunks_info.is_empty() {
1075            return Ok(vec![]);
1076        }
1077
1078        // Calculate total size and actual end
1079        let total_size: u64 = chunks_info.iter().map(|(_, _, size)| size).sum();
1080        let actual_end = end.unwrap_or(total_size).min(total_size);
1081
1082        if start >= actual_end {
1083            return Ok(vec![]);
1084        }
1085
1086        // Find chunks that overlap with [start, actual_end)
1087        let mut result = Vec::with_capacity((actual_end - start) as usize);
1088
1089        for (chunk_hash, chunk_start, chunk_size) in &chunks_info {
1090            let chunk_start = *chunk_start;
1091            let chunk_size = *chunk_size;
1092            let chunk_end = chunk_start + chunk_size;
1093
1094            // Check if this chunk overlaps with our range
1095            if chunk_end > start && chunk_start < actual_end {
1096                let read_start = start.saturating_sub(chunk_start);
1097                let read_end_exclusive = actual_end.min(chunk_end) - chunk_start;
1098                if read_start >= read_end_exclusive {
1099                    continue;
1100                }
1101
1102                let chunk_data = self
1103                    .store
1104                    .get_range(chunk_hash, read_start, read_end_exclusive - 1)
1105                    .await
1106                    .map_err(|e| HashTreeError::Store(e.to_string()))?
1107                    .ok_or_else(|| HashTreeError::MissingChunk(to_hex(chunk_hash)))?;
1108
1109                let expected_len = (read_end_exclusive - read_start) as usize;
1110                if chunk_data.len() != expected_len {
1111                    return Err(HashTreeError::Store(format!(
1112                        "range read for {} returned {} bytes, expected {}",
1113                        to_hex(chunk_hash),
1114                        chunk_data.len(),
1115                        expected_len
1116                    )));
1117                }
1118
1119                result.extend_from_slice(&chunk_data);
1120            }
1121
1122            // Early exit if we've passed the requested range
1123            if chunk_end >= actual_end {
1124                break;
1125            }
1126        }
1127
1128        Ok(result)
1129    }
1130
1131    /// Collect all leaf chunk hashes with their byte offsets
1132    /// Returns Vec<(hash, offset, size)>
1133    async fn collect_chunk_offsets(
1134        &self,
1135        node: &TreeNode,
1136    ) -> Result<Vec<(Hash, u64, u64)>, HashTreeError> {
1137        let mut chunks = Vec::new();
1138        let mut offset = 0u64;
1139        self.collect_chunk_offsets_recursive(node, &mut chunks, &mut offset)
1140            .await?;
1141        Ok(chunks)
1142    }
1143
1144    async fn collect_chunk_offsets_recursive(
1145        &self,
1146        node: &TreeNode,
1147        chunks: &mut Vec<(Hash, u64, u64)>,
1148        offset: &mut u64,
1149    ) -> Result<(), HashTreeError> {
1150        for link in &node.links {
1151            if link.link_type == LinkType::Blob {
1152                chunks.push((link.hash, *offset, link.size));
1153                *offset += link.size;
1154                continue;
1155            }
1156
1157            let child_data = self
1158                .store
1159                .get(&link.hash)
1160                .await
1161                .map_err(|e| HashTreeError::Store(e.to_string()))?
1162                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1163
1164            if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
1165                // Intermediate node - recurse
1166                Box::pin(self.collect_chunk_offsets_recursive(&child_node, chunks, offset)).await?;
1167            } else {
1168                // Leaf chunk
1169                let size = child_data.len() as u64;
1170                chunks.push((link.hash, *offset, size));
1171                *offset += size;
1172            }
1173        }
1174        Ok(())
1175    }
1176
1177    /// Recursively assemble chunks from tree
1178    async fn assemble_chunks_limited(
1179        &self,
1180        node: &TreeNode,
1181        max_size: Option<u64>,
1182        bytes_read: &mut u64,
1183    ) -> Result<Vec<u8>, HashTreeError> {
1184        let mut parts: Vec<Vec<u8>> = Vec::new();
1185
1186        for link in &node.links {
1187            let projected = (*bytes_read).saturating_add(link.size);
1188            Self::ensure_size_limit(max_size, projected)?;
1189
1190            let child_data = self
1191                .store
1192                .get(&link.hash)
1193                .await
1194                .map_err(|e| HashTreeError::Store(e.to_string()))?
1195                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1196
1197            if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
1198                parts.push(
1199                    Box::pin(self.assemble_chunks_limited(&child_node, max_size, bytes_read))
1200                        .await?,
1201                );
1202            } else {
1203                let projected = (*bytes_read).saturating_add(child_data.len() as u64);
1204                Self::ensure_size_limit(max_size, projected)?;
1205                *bytes_read = projected;
1206                parts.push(child_data);
1207            }
1208        }
1209
1210        // Concatenate all parts
1211        let total_length: usize = parts.iter().map(|p| p.len()).sum();
1212        let mut result = Vec::with_capacity(total_length);
1213        for part in parts {
1214            result.extend_from_slice(&part);
1215        }
1216
1217        Ok(result)
1218    }
1219
1220    /// Read file chunks as Vec (non-streaming version)
1221    pub async fn read_file_chunks(&self, hash: &Hash) -> Result<Vec<Vec<u8>>, HashTreeError> {
1222        let data = match self
1223            .store
1224            .get(hash)
1225            .await
1226            .map_err(|e| HashTreeError::Store(e.to_string()))?
1227        {
1228            Some(d) => d,
1229            None => return Ok(vec![]),
1230        };
1231
1232        if !is_tree_node(&data) {
1233            return Ok(vec![data]);
1234        }
1235
1236        let node = decode_tree_node(&data)?;
1237        self.collect_chunks(&node).await
1238    }
1239
1240    async fn collect_chunks(&self, node: &TreeNode) -> Result<Vec<Vec<u8>>, HashTreeError> {
1241        let mut chunks = Vec::new();
1242
1243        for link in &node.links {
1244            let child_data = self
1245                .store
1246                .get(&link.hash)
1247                .await
1248                .map_err(|e| HashTreeError::Store(e.to_string()))?
1249                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1250
1251            if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
1252                chunks.extend(Box::pin(self.collect_chunks(&child_node)).await?);
1253            } else {
1254                chunks.push(child_data);
1255            }
1256        }
1257
1258        Ok(chunks)
1259    }
1260
1261    /// List directory entries (Cid-based, supports encrypted directories)
1262    pub async fn list(&self, cid: &Cid) -> Result<Vec<TreeEntry>, HashTreeError> {
1263        let node = match self.get_node(cid).await? {
1264            Some(n) => n,
1265            None => return Ok(vec![]),
1266        };
1267
1268        let mut entries = Vec::new();
1269
1270        for link in &node.links {
1271            // Skip internal chunk nodes - recurse into them
1272            if is_internal_directory_link(&node, link) {
1273                let chunk_cid = Cid {
1274                    hash: link.hash,
1275                    key: link.key,
1276                };
1277                let sub_entries = Box::pin(self.list(&chunk_cid)).await?;
1278                entries.extend(sub_entries);
1279                continue;
1280            }
1281
1282            entries.push(TreeEntry {
1283                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
1284                hash: link.hash,
1285                size: link.size,
1286                link_type: link.link_type,
1287                key: link.key,
1288                meta: link.meta.clone(),
1289            });
1290        }
1291
1292        Ok(entries)
1293    }
1294
1295    /// List directory entries using Cid (with decryption if key present).
1296    /// Handles both regular and fanout directory data.
1297    pub async fn list_directory(&self, cid: &Cid) -> Result<Vec<TreeEntry>, HashTreeError> {
1298        // Use get_directory_node which handles chunked directory data
1299        let node = match self.get_directory_node(cid).await? {
1300            Some(n) => n,
1301            None => return Ok(vec![]),
1302        };
1303
1304        let mut entries = Vec::new();
1305
1306        for link in &node.links {
1307            // Skip internal fanout nodes.
1308            if is_internal_directory_link(&node, link) {
1309                let sub_cid = Cid {
1310                    hash: link.hash,
1311                    key: link.key,
1312                };
1313                let sub_entries = Box::pin(self.list_directory(&sub_cid)).await?;
1314                entries.extend(sub_entries);
1315                continue;
1316            }
1317
1318            entries.push(TreeEntry {
1319                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
1320                hash: link.hash,
1321                size: link.size,
1322                link_type: link.link_type,
1323                key: link.key,
1324                meta: link.meta.clone(),
1325            });
1326        }
1327
1328        Ok(entries)
1329    }
1330
1331    /// List a directory reached through an existing parent link.
1332    /// Unlike `list_directory`, an unavailable node is not an empty directory.
1333    pub async fn list_directory_required(
1334        &self,
1335        cid: &Cid,
1336    ) -> Result<Vec<TreeEntry>, HashTreeError> {
1337        let node = self
1338            .get_directory_node(cid)
1339            .await?
1340            .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&cid.hash)))?;
1341
1342        let mut entries = Vec::new();
1343        for link in &node.links {
1344            if is_internal_directory_link(&node, link) {
1345                let sub_cid = Cid {
1346                    hash: link.hash,
1347                    key: link.key,
1348                };
1349                let sub_entries = Box::pin(self.list_directory_required(&sub_cid)).await?;
1350                entries.extend(sub_entries);
1351                continue;
1352            }
1353
1354            entries.push(TreeEntry {
1355                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
1356                hash: link.hash,
1357                size: link.size,
1358                link_type: link.link_type,
1359                key: link.key,
1360                meta: link.meta.clone(),
1361            });
1362        }
1363
1364        Ok(entries)
1365    }
1366
1367    /// Resolve a path within a tree (returns Cid with key if encrypted)
1368    pub async fn resolve(&self, cid: &Cid, path: &str) -> Result<Option<Cid>, HashTreeError> {
1369        let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
1370        if parts.is_empty() {
1371            return Ok(Some(cid.clone()));
1372        }
1373
1374        let mut current_cid = cid.clone();
1375
1376        for part in parts {
1377            // Use get_directory_node which handles chunked directory data
1378            let node = match self.get_directory_node(&current_cid).await? {
1379                Some(n) => n,
1380                None => {
1381                    if !self.has_stored_chunk(&current_cid.hash).await? {
1382                        return Err(HashTreeError::MissingChunk(to_hex(&current_cid.hash)));
1383                    }
1384                    return Ok(None);
1385                }
1386            };
1387
1388            if let Some(link) = self.find_link(&node, part) {
1389                current_cid = Cid {
1390                    hash: link.hash,
1391                    key: link.key,
1392                };
1393            } else {
1394                // Check internal nodes
1395                match self
1396                    .find_link_in_subtrees_cid(&node, part, &current_cid)
1397                    .await?
1398                {
1399                    Some(link) => {
1400                        current_cid = Cid {
1401                            hash: link.hash,
1402                            key: link.key,
1403                        };
1404                    }
1405                    None => return Ok(None),
1406                }
1407            }
1408        }
1409
1410        Ok(Some(current_cid))
1411    }
1412
1413    /// Resolve a path within a tree using Cid (with decryption if key present)
1414    pub async fn resolve_path(&self, cid: &Cid, path: &str) -> Result<Option<Cid>, HashTreeError> {
1415        self.resolve(cid, path).await
1416    }
1417
1418    fn find_link(&self, node: &TreeNode, name: &str) -> Option<Link> {
1419        node.links
1420            .iter()
1421            .find(|l| !is_internal_directory_link(node, l) && l.name.as_deref() == Some(name))
1422            .cloned()
1423    }
1424
1425    /// Find a link in subtrees using Cid (with decryption support)
1426    async fn find_link_in_subtrees_cid(
1427        &self,
1428        node: &TreeNode,
1429        name: &str,
1430        _parent_cid: &Cid,
1431    ) -> Result<Option<Link>, HashTreeError> {
1432        for link in &node.links {
1433            if !is_internal_directory_link(node, link) {
1434                continue;
1435            }
1436
1437            // Internal nodes inherit encryption from parent context
1438            let sub_cid = Cid {
1439                hash: link.hash,
1440                key: link.key,
1441            };
1442
1443            let sub_node = match self.get_node(&sub_cid).await? {
1444                Some(n) => n,
1445                None => {
1446                    if !self.has_stored_chunk(&sub_cid.hash).await? {
1447                        return Err(HashTreeError::MissingChunk(to_hex(&sub_cid.hash)));
1448                    }
1449                    continue;
1450                }
1451            };
1452
1453            if let Some(found) = self.find_link(&sub_node, name) {
1454                return Ok(Some(found));
1455            }
1456
1457            if let Some(deep_found) =
1458                Box::pin(self.find_link_in_subtrees_cid(&sub_node, name, &sub_cid)).await?
1459            {
1460                return Ok(Some(deep_found));
1461            }
1462        }
1463
1464        Ok(None)
1465    }
1466
1467    /// Get total size of a tree
1468    pub async fn get_size(&self, hash: &Hash) -> Result<u64, HashTreeError> {
1469        let data = match self
1470            .store
1471            .get(hash)
1472            .await
1473            .map_err(|e| HashTreeError::Store(e.to_string()))?
1474        {
1475            Some(d) => d,
1476            None => return Ok(0),
1477        };
1478
1479        if !is_tree_node(&data) {
1480            return Ok(data.len() as u64);
1481        }
1482
1483        let node = decode_tree_node(&data)?;
1484        // Calculate from children
1485        let mut total = 0u64;
1486        for link in &node.links {
1487            total += link.size;
1488        }
1489        Ok(total)
1490    }
1491
1492    /// Get total size using a Cid (handles decryption if key present)
1493    pub async fn get_size_cid(&self, cid: &Cid) -> Result<u64, HashTreeError> {
1494        if let Some(key) = cid.key {
1495            let data = match self.get_encrypted_root(&cid.hash, &key).await? {
1496                Some(d) => d,
1497                None => return Ok(0),
1498            };
1499            if is_tree_node(&data) {
1500                let node = decode_tree_node(&data)?;
1501                return Ok(node.links.iter().map(|link| link.size).sum());
1502            }
1503            return Ok(data.len() as u64);
1504        }
1505
1506        self.get_size(&cid.hash).await
1507    }
1508
1509    // ============ EDIT ============
1510
1511    /// Add or update an entry in a directory
1512    /// Returns new root Cid (immutable operation)
1513    pub async fn set_entry(
1514        &self,
1515        root: &Cid,
1516        path: &[&str],
1517        name: &str,
1518        entry_cid: &Cid,
1519        size: u64,
1520        link_type: LinkType,
1521    ) -> Result<Cid, HashTreeError> {
1522        self.set_entry_with_meta(root, path, name, entry_cid, size, link_type, None)
1523            .await
1524    }
1525
1526    /// Add or update an entry in a directory with optional link metadata.
1527    /// Returns new root Cid (immutable operation)
1528    #[allow(clippy::too_many_arguments)]
1529    pub async fn set_entry_with_meta(
1530        &self,
1531        root: &Cid,
1532        path: &[&str],
1533        name: &str,
1534        entry_cid: &Cid,
1535        size: u64,
1536        link_type: LinkType,
1537        meta: Option<std::collections::HashMap<String, serde_json::Value>>,
1538    ) -> Result<Cid, HashTreeError> {
1539        let dir_cid = self.resolve_path_array(root, path).await?;
1540        let dir_cid = dir_cid.ok_or_else(|| HashTreeError::PathNotFound(path.join("/")))?;
1541
1542        let entries = self.list_directory(&dir_cid).await?;
1543        let mut new_entries: Vec<DirEntry> = entries
1544            .into_iter()
1545            .filter(|e| e.name != name)
1546            .map(|e| DirEntry {
1547                name: e.name,
1548                hash: e.hash,
1549                size: e.size,
1550                key: e.key,
1551                link_type: e.link_type,
1552                meta: e.meta,
1553            })
1554            .collect();
1555
1556        new_entries.push(DirEntry {
1557            name: name.to_string(),
1558            hash: entry_cid.hash,
1559            size,
1560            key: entry_cid.key,
1561            link_type,
1562            meta,
1563        });
1564
1565        let new_dir_cid = self.put_directory(new_entries).await?;
1566        self.rebuild_path(root, path, new_dir_cid).await
1567    }
1568
1569    /// Remove an entry from a directory
1570    /// Returns new root Cid
1571    pub async fn remove_entry(
1572        &self,
1573        root: &Cid,
1574        path: &[&str],
1575        name: &str,
1576    ) -> Result<Cid, HashTreeError> {
1577        let dir_cid = self.resolve_path_array(root, path).await?;
1578        let dir_cid = dir_cid.ok_or_else(|| HashTreeError::PathNotFound(path.join("/")))?;
1579
1580        let entries = self.list_directory(&dir_cid).await?;
1581        let new_entries: Vec<DirEntry> = entries
1582            .into_iter()
1583            .filter(|e| e.name != name)
1584            .map(|e| DirEntry {
1585                name: e.name,
1586                hash: e.hash,
1587                size: e.size,
1588                key: e.key,
1589                link_type: e.link_type,
1590                meta: e.meta,
1591            })
1592            .collect();
1593
1594        let new_dir_cid = self.put_directory(new_entries).await?;
1595        self.rebuild_path(root, path, new_dir_cid).await
1596    }
1597
1598    /// Rename an entry in a directory
1599    /// Returns new root Cid
1600    pub async fn rename_entry(
1601        &self,
1602        root: &Cid,
1603        path: &[&str],
1604        old_name: &str,
1605        new_name: &str,
1606    ) -> Result<Cid, HashTreeError> {
1607        if old_name == new_name {
1608            return Ok(root.clone());
1609        }
1610
1611        let dir_cid = self.resolve_path_array(root, path).await?;
1612        let dir_cid = dir_cid.ok_or_else(|| HashTreeError::PathNotFound(path.join("/")))?;
1613
1614        let entries = self.list_directory(&dir_cid).await?;
1615        let entry = entries
1616            .iter()
1617            .find(|e| e.name == old_name)
1618            .ok_or_else(|| HashTreeError::EntryNotFound(old_name.to_string()))?;
1619
1620        let entry_hash = entry.hash;
1621        let entry_size = entry.size;
1622        let entry_key = entry.key;
1623        let entry_link_type = entry.link_type;
1624        let entry_meta = entry.meta.clone();
1625
1626        let new_entries: Vec<DirEntry> = entries
1627            .into_iter()
1628            .filter(|e| e.name != old_name)
1629            .map(|e| DirEntry {
1630                name: e.name,
1631                hash: e.hash,
1632                size: e.size,
1633                key: e.key,
1634                link_type: e.link_type,
1635                meta: e.meta,
1636            })
1637            .chain(std::iter::once(DirEntry {
1638                name: new_name.to_string(),
1639                hash: entry_hash,
1640                size: entry_size,
1641                key: entry_key,
1642                link_type: entry_link_type,
1643                meta: entry_meta,
1644            }))
1645            .collect();
1646
1647        let new_dir_cid = self.put_directory(new_entries).await?;
1648        self.rebuild_path(root, path, new_dir_cid).await
1649    }
1650
1651    /// Move an entry to a different directory
1652    /// Returns new root Cid
1653    pub async fn move_entry(
1654        &self,
1655        root: &Cid,
1656        source_path: &[&str],
1657        name: &str,
1658        target_path: &[&str],
1659    ) -> Result<Cid, HashTreeError> {
1660        let source_dir_cid = self.resolve_path_array(root, source_path).await?;
1661        let source_dir_cid =
1662            source_dir_cid.ok_or_else(|| HashTreeError::PathNotFound(source_path.join("/")))?;
1663
1664        let source_entries = self.list_directory(&source_dir_cid).await?;
1665        let entry = source_entries
1666            .iter()
1667            .find(|e| e.name == name)
1668            .ok_or_else(|| HashTreeError::EntryNotFound(name.to_string()))?;
1669
1670        let entry_cid = Cid {
1671            hash: entry.hash,
1672            key: entry.key,
1673        };
1674        let entry_size = entry.size;
1675        let entry_link_type = entry.link_type;
1676
1677        // Remove from source
1678        let new_root = self.remove_entry(root, source_path, name).await?;
1679
1680        // Add to target
1681        self.set_entry(
1682            &new_root,
1683            target_path,
1684            name,
1685            &entry_cid,
1686            entry_size,
1687            entry_link_type,
1688        )
1689        .await
1690    }
1691
1692    async fn resolve_path_array(
1693        &self,
1694        root: &Cid,
1695        path: &[&str],
1696    ) -> Result<Option<Cid>, HashTreeError> {
1697        if path.is_empty() {
1698            return Ok(Some(root.clone()));
1699        }
1700        self.resolve_path(root, &path.join("/")).await
1701    }
1702
1703    async fn rebuild_path(
1704        &self,
1705        root: &Cid,
1706        path: &[&str],
1707        new_child: Cid,
1708    ) -> Result<Cid, HashTreeError> {
1709        if path.is_empty() {
1710            return Ok(new_child);
1711        }
1712
1713        let mut child_cid = new_child;
1714        let parts: Vec<&str> = path.to_vec();
1715
1716        for i in (0..parts.len()).rev() {
1717            let child_name = parts[i];
1718            let parent_path = &parts[..i];
1719
1720            let parent_cid = if parent_path.is_empty() {
1721                root.clone()
1722            } else {
1723                self.resolve_path_array(root, parent_path)
1724                    .await?
1725                    .ok_or_else(|| HashTreeError::PathNotFound(parent_path.join("/")))?
1726            };
1727
1728            let parent_entries = self.list_directory(&parent_cid).await?;
1729            let new_parent_entries: Vec<DirEntry> = parent_entries
1730                .into_iter()
1731                .map(|e| {
1732                    if e.name == child_name {
1733                        DirEntry {
1734                            name: e.name,
1735                            hash: child_cid.hash,
1736                            size: 0, // Directories don't have a meaningful size in the link
1737                            key: child_cid.key,
1738                            link_type: e.link_type,
1739                            meta: e.meta,
1740                        }
1741                    } else {
1742                        DirEntry {
1743                            name: e.name,
1744                            hash: e.hash,
1745                            size: e.size,
1746                            key: e.key,
1747                            link_type: e.link_type,
1748                            meta: e.meta,
1749                        }
1750                    }
1751                })
1752                .collect();
1753
1754            child_cid = self.put_directory(new_parent_entries).await?;
1755        }
1756
1757        Ok(child_cid)
1758    }
1759
1760    // ============ UTILITY ============
1761
1762    /// Get the underlying store
1763    pub fn get_store(&self) -> Arc<S> {
1764        self.store.clone()
1765    }
1766
1767    /// Get chunk size configuration
1768    pub fn chunk_size(&self) -> usize {
1769        self.chunk_size
1770    }
1771
1772    /// Get max links configuration
1773    pub fn max_links(&self) -> usize {
1774        self.max_links
1775    }
1776}
1777
1778fn stream_put_batch_target_bytes() -> usize {
1779    std::env::var(STREAM_PUT_BATCH_TARGET_BYTES_ENV)
1780        .ok()
1781        .and_then(|value| value.parse::<usize>().ok())
1782        .filter(|value| *value > 0)
1783        .unwrap_or(DEFAULT_STREAM_PUT_BATCH_TARGET_BYTES)
1784}
1785
1786/// Verify tree integrity - checks that all referenced hashes exist
1787pub async fn verify_tree<S: Store>(
1788    store: Arc<S>,
1789    root_hash: &Hash,
1790) -> Result<crate::reader::VerifyResult, HashTreeError> {
1791    let mut missing = Vec::new();
1792    let mut visited = std::collections::HashSet::new();
1793
1794    verify_recursive(store, root_hash, &mut missing, &mut visited).await?;
1795
1796    Ok(crate::reader::VerifyResult {
1797        valid: missing.is_empty(),
1798        missing,
1799    })
1800}
1801
1802async fn verify_recursive<S: Store>(
1803    store: Arc<S>,
1804    hash: &Hash,
1805    missing: &mut Vec<Hash>,
1806    visited: &mut std::collections::HashSet<String>,
1807) -> Result<(), HashTreeError> {
1808    let hex = to_hex(hash);
1809    if visited.contains(&hex) {
1810        return Ok(());
1811    }
1812    visited.insert(hex);
1813
1814    let data = match store
1815        .get(hash)
1816        .await
1817        .map_err(|e| HashTreeError::Store(e.to_string()))?
1818    {
1819        Some(d) => d,
1820        None => {
1821            missing.push(*hash);
1822            return Ok(());
1823        }
1824    };
1825
1826    if is_tree_node(&data) {
1827        let node = decode_tree_node(&data)?;
1828        for link in &node.links {
1829            Box::pin(verify_recursive(
1830                store.clone(),
1831                &link.hash,
1832                missing,
1833                visited,
1834            ))
1835            .await?;
1836        }
1837    }
1838
1839    Ok(())
1840}
1841
1842#[cfg(test)]
1843mod tests;