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        let size = data.len() as u64;
581
582        // Small file - store as single chunk
583        if data.len() <= self.chunk_size {
584            let (hash, key) = self.put_chunk_internal(data).await?;
585            return Ok((Cid { hash, key }, size));
586        }
587
588        // Large file - chunk it
589        let mut links: Vec<Link> = Vec::new();
590        let mut offset = 0;
591
592        while offset < data.len() {
593            let end = (offset + self.chunk_size).min(data.len());
594            let chunk = &data[offset..end];
595            let chunk_size = (end - offset) as u64;
596
597            let (hash, key) = self.put_chunk_internal(chunk).await?;
598            links.push(Link {
599                hash,
600                name: None,
601                size: chunk_size,
602                key,
603                link_type: LinkType::Blob, // Leaf chunk
604                meta: None,
605            });
606            offset = end;
607        }
608
609        // Build tree from chunks (uses encryption if enabled)
610        let (root_hash, root_key, _) = self.build_tree_internal(links, Some(size)).await?;
611        Ok((
612            Cid {
613                hash: root_hash,
614                key: root_key,
615            },
616            size,
617        ))
618    }
619
620    /// Build a directory from entries
621    /// Returns Cid with key if encrypted
622    ///
623    /// Large directories are split into BUD-17 fanout nodes.
624    pub async fn put_directory(&self, entries: Vec<DirEntry>) -> Result<Cid, HashTreeError> {
625        // Sort entries by name for deterministic hashing
626        let mut sorted = entries;
627        sorted.sort_by(|a, b| a.name.cmp(&b.name));
628
629        let links: Vec<Link> = sorted
630            .into_iter()
631            .map(|e| Link {
632                hash: e.hash,
633                name: Some(e.name),
634                size: e.size,
635                key: e.key,
636                link_type: e.link_type,
637                meta: e.meta,
638            })
639            .collect();
640
641        if links.len() <= self.max_links {
642            return self.put_directory_node(links).await;
643        }
644
645        self.build_directory_by_chunks(links).await
646    }
647
648    async fn put_directory_node(&self, links: Vec<Link>) -> Result<Cid, HashTreeError> {
649        self.put_tree_node_with_type(LinkType::Dir, links).await
650    }
651
652    async fn put_tree_node_with_type(
653        &self,
654        node_type: LinkType,
655        links: Vec<Link>,
656    ) -> Result<Cid, HashTreeError> {
657        let node = TreeNode { node_type, links };
658        let (data, plain_hash) = encode_and_hash(&node)?;
659
660        if self.encrypted {
661            let (encrypted, key) =
662                encrypt_chk(&data).map_err(|e| HashTreeError::Encryption(e.to_string()))?;
663            let hash = sha256(&encrypted);
664            self.store
665                .put(hash, encrypted)
666                .await
667                .map_err(|e| HashTreeError::Store(e.to_string()))?;
668            return Ok(Cid {
669                hash,
670                key: Some(key),
671            });
672        }
673
674        self.store
675            .put(plain_hash, data)
676            .await
677            .map_err(|e| HashTreeError::Store(e.to_string()))?;
678        Ok(Cid {
679            hash: plain_hash,
680            key: None,
681        })
682    }
683
684    async fn build_directory_by_chunks(&self, links: Vec<Link>) -> Result<Cid, HashTreeError> {
685        let spans = links
686            .into_iter()
687            .map(|link| {
688                let name = link.name.clone().unwrap_or_else(|| to_hex(&link.hash));
689                DirectoryFanoutSpan {
690                    link,
691                    count: 1,
692                    first: name.clone(),
693                    last: name,
694                }
695            })
696            .collect();
697        self.build_directory_fanout_level(spans, LinkType::Dir)
698            .await
699    }
700
701    async fn build_directory_fanout_level(
702        &self,
703        spans: Vec<DirectoryFanoutSpan>,
704        child_node_type: LinkType,
705    ) -> Result<Cid, HashTreeError> {
706        let mut sub_trees: Vec<DirectoryFanoutSpan> = Vec::new();
707
708        for batch in spans.chunks(self.max_links) {
709            let Some(first_span) = batch.first() else {
710                continue;
711            };
712            let last_span = batch.last().expect("non-empty fanout batch");
713            let count: usize = batch.iter().map(|span| span.count).sum();
714            let batch_size: u64 = batch.iter().map(|span| span.link.size).sum();
715            let child_links = batch.iter().map(|span| span.link.clone()).collect();
716            let child_cid = self
717                .put_tree_node_with_type(child_node_type, child_links)
718                .await?;
719
720            sub_trees.push(DirectoryFanoutSpan {
721                link: Link {
722                    hash: child_cid.hash,
723                    name: None,
724                    size: batch_size,
725                    key: child_cid.key,
726                    link_type: child_node_type,
727                    meta: Some(directory_fanout_meta(
728                        count,
729                        &first_span.first,
730                        &last_span.last,
731                    )),
732                },
733                count,
734                first: first_span.first.clone(),
735                last: last_span.last.clone(),
736            });
737        }
738
739        if sub_trees.len() <= self.max_links {
740            return self
741                .put_tree_node_with_type(
742                    LinkType::Fanout,
743                    sub_trees.into_iter().map(|span| span.link).collect(),
744                )
745                .await;
746        }
747
748        Box::pin(self.build_directory_fanout_level(sub_trees, LinkType::Fanout)).await
749    }
750
751    /// Create a tree node with custom links
752    pub async fn put_tree_node(&self, links: Vec<Link>) -> Result<Hash, HashTreeError> {
753        let node = TreeNode {
754            node_type: LinkType::Dir,
755            links,
756        };
757
758        let (data, hash) = encode_and_hash(&node)?;
759        self.store
760            .put(hash, data)
761            .await
762            .map_err(|e| HashTreeError::Store(e.to_string()))?;
763        Ok(hash)
764    }
765
766    // ============ READ ============
767
768    /// Get raw data by hash
769    pub async fn get_blob(&self, hash: &Hash) -> Result<Option<Vec<u8>>, HashTreeError> {
770        self.store
771            .get(hash)
772            .await
773            .map_err(|e| HashTreeError::Store(e.to_string()))
774    }
775
776    async fn has_stored_chunk(&self, hash: &Hash) -> Result<bool, HashTreeError> {
777        self.store
778            .get(hash)
779            .await
780            .map(|data| data.is_some())
781            .map_err(|e| HashTreeError::Store(e.to_string()))
782    }
783
784    /// Get and decode a tree node (unencrypted)
785    pub async fn get_tree_node(&self, hash: &Hash) -> Result<Option<TreeNode>, HashTreeError> {
786        let data = match self
787            .store
788            .get(hash)
789            .await
790            .map_err(|e| HashTreeError::Store(e.to_string()))?
791        {
792            Some(d) => d,
793            None => return Ok(None),
794        };
795
796        if !is_tree_node(&data) {
797            return Ok(None);
798        }
799
800        let node = decode_tree_node(&data)?;
801        Ok(Some(node))
802    }
803
804    async fn get_cid_root_bytes(&self, cid: &Cid) -> Result<Option<Vec<u8>>, HashTreeError> {
805        let data = match self
806            .store
807            .get(&cid.hash)
808            .await
809            .map_err(|e| HashTreeError::Store(e.to_string()))?
810        {
811            Some(d) => d,
812            None => return Ok(None),
813        };
814
815        let Some(key) = &cid.key else {
816            return Ok(Some(data));
817        };
818
819        let raw_is_tree = is_tree_node(&data);
820        match decrypt_chk(&data, key) {
821            Ok(decrypted) => {
822                if is_tree_node(&decrypted) || !raw_is_tree {
823                    Ok(Some(decrypted))
824                } else {
825                    Ok(Some(data))
826                }
827            }
828            Err(err) => {
829                if raw_is_tree {
830                    Ok(Some(data))
831                } else {
832                    Err(HashTreeError::Decryption(err.to_string()))
833                }
834            }
835        }
836    }
837
838    /// Get and decode a tree node using Cid (with decryption if key present)
839    pub async fn get_node(&self, cid: &Cid) -> Result<Option<TreeNode>, HashTreeError> {
840        let decrypted = match self.get_cid_root_bytes(cid).await? {
841            Some(d) => d,
842            None => return Ok(None),
843        };
844
845        if !is_tree_node(&decrypted) {
846            return Ok(None);
847        }
848
849        let node = decode_tree_node(&decrypted)?;
850        Ok(Some(node))
851    }
852
853    /// Get directory node, handling historical byte-chunked directory data.
854    /// Use this when you know the target is a directory (from parent link_type)
855    pub async fn get_directory_node(&self, cid: &Cid) -> Result<Option<TreeNode>, HashTreeError> {
856        let decrypted = match self.get_cid_root_bytes(cid).await? {
857            Some(d) => d,
858            None => return Ok(None),
859        };
860
861        if !is_tree_node(&decrypted) {
862            return Ok(None);
863        }
864
865        let node = decode_tree_node(&decrypted)?;
866
867        // If this is a file tree (chunked data), reassemble to get actual directory
868        if node.node_type == LinkType::File {
869            let mut bytes_read = 0u64;
870            let assembled = self
871                .assemble_chunks_limited(&node, None, &mut bytes_read)
872                .await?;
873            if is_tree_node(&assembled) {
874                let inner_node = decode_tree_node(&assembled)?;
875                return Ok(Some(inner_node));
876            }
877        }
878
879        Ok(Some(node))
880    }
881
882    /// Check if hash points to a tree node (no decryption)
883    pub async fn is_tree(&self, hash: &Hash) -> Result<bool, HashTreeError> {
884        let data = match self
885            .store
886            .get(hash)
887            .await
888            .map_err(|e| HashTreeError::Store(e.to_string()))?
889        {
890            Some(d) => d,
891            None => return Ok(false),
892        };
893        Ok(is_tree_node(&data))
894    }
895
896    /// Check if Cid points to a directory (with decryption)
897    pub async fn is_dir(&self, cid: &Cid) -> Result<bool, HashTreeError> {
898        Ok(matches!(
899            self.get_directory_node(cid).await?,
900            Some(node) if node.node_type.is_directory_like()
901        ))
902    }
903
904    /// Check if hash points to a directory (tree with named links, no decryption)
905    pub async fn is_directory(&self, hash: &Hash) -> Result<bool, HashTreeError> {
906        let data = match self
907            .store
908            .get(hash)
909            .await
910            .map_err(|e| HashTreeError::Store(e.to_string()))?
911        {
912            Some(d) => d,
913            None => return Ok(false),
914        };
915        Ok(is_directory_node(&data))
916    }
917
918    /// Read a complete file (reassemble chunks if needed)
919    pub async fn read_file(&self, hash: &Hash) -> Result<Option<Vec<u8>>, HashTreeError> {
920        self.read_file_with_limit(hash, None).await
921    }
922
923    /// Read a complete file with optional size limit.
924    async fn read_file_with_limit(
925        &self,
926        hash: &Hash,
927        max_size: Option<u64>,
928    ) -> Result<Option<Vec<u8>>, HashTreeError> {
929        let data = match self
930            .store
931            .get(hash)
932            .await
933            .map_err(|e| HashTreeError::Store(e.to_string()))?
934        {
935            Some(d) => d,
936            None => return Ok(None),
937        };
938
939        // Check if it's a tree (chunked file) or raw blob
940        if !is_tree_node(&data) {
941            Self::ensure_size_limit(max_size, data.len() as u64)?;
942            return Ok(Some(data));
943        }
944
945        // It's a tree - reassemble chunks
946        let node = decode_tree_node(&data)?;
947        let declared_size: u64 = node.links.iter().map(|l| l.size).sum();
948        Self::ensure_size_limit(max_size, declared_size)?;
949
950        let mut bytes_read = 0u64;
951        let assembled = self
952            .assemble_chunks_limited(&node, max_size, &mut bytes_read)
953            .await?;
954        Ok(Some(assembled))
955    }
956
957    /// Read a byte range from a file (fetches only necessary chunks)
958    ///
959    /// - `start`: Starting byte offset (inclusive)
960    /// - `end`: Ending byte offset (exclusive), or None to read to end
961    ///
962    /// This is more efficient than read_file() for partial reads of large files.
963    pub async fn read_file_range(
964        &self,
965        hash: &Hash,
966        start: u64,
967        end: Option<u64>,
968    ) -> Result<Option<Vec<u8>>, HashTreeError> {
969        let data = match self
970            .store
971            .get(hash)
972            .await
973            .map_err(|e| HashTreeError::Store(e.to_string()))?
974        {
975            Some(d) => d,
976            None => return Ok(None),
977        };
978
979        // Single blob - just slice it
980        if !is_tree_node(&data) {
981            let start_idx = start as usize;
982            let end_idx = end.map(|e| e as usize).unwrap_or(data.len());
983            if start_idx >= data.len() {
984                return Ok(Some(vec![]));
985            }
986            let end_idx = end_idx.min(data.len());
987            return Ok(Some(data[start_idx..end_idx].to_vec()));
988        }
989
990        // It's a chunked file - fetch only needed chunks
991        let node = decode_tree_node(&data)?;
992        let range_data = self.assemble_chunks_range(&node, start, end).await?;
993        Ok(Some(range_data))
994    }
995
996    /// Read a byte range from a file using a Cid (handles decryption if key present)
997    pub async fn read_file_range_cid(
998        &self,
999        cid: &Cid,
1000        start: u64,
1001        end: Option<u64>,
1002    ) -> Result<Option<Vec<u8>>, HashTreeError> {
1003        if let Some(key) = cid.key {
1004            let data = match self.get_encrypted_root(&cid.hash, &key).await? {
1005                Some(d) => d,
1006                None => return Ok(None),
1007            };
1008
1009            if is_tree_node(&data) {
1010                let node = decode_tree_node(&data)?;
1011                let total_size: u64 = node.links.iter().map(|link| link.size).sum();
1012                let actual_end = end.unwrap_or(total_size).min(total_size);
1013                if start >= actual_end {
1014                    return Ok(Some(vec![]));
1015                }
1016
1017                let mut result = Vec::with_capacity((actual_end - start) as usize);
1018                self.append_encrypted_range(&node, start, actual_end, 0, &mut result)
1019                    .await?;
1020                return Ok(Some(result));
1021            }
1022
1023            let start_idx = start as usize;
1024            let end_idx = end.map(|e| e as usize).unwrap_or(data.len());
1025            if start_idx >= data.len() {
1026                return Ok(Some(vec![]));
1027            }
1028            let end_idx = end_idx.min(data.len());
1029            return Ok(Some(data[start_idx..end_idx].to_vec()));
1030        }
1031
1032        self.read_file_range(&cid.hash, start, end).await
1033    }
1034
1035    async fn append_encrypted_range(
1036        &self,
1037        node: &TreeNode,
1038        start: u64,
1039        end: u64,
1040        base_offset: u64,
1041        result: &mut Vec<u8>,
1042    ) -> Result<(), HashTreeError> {
1043        let mut current_offset = base_offset;
1044
1045        for link in &node.links {
1046            let child_start = current_offset;
1047            let child_end = child_start.saturating_add(link.size);
1048            current_offset = child_end;
1049
1050            if child_end <= start {
1051                continue;
1052            }
1053            if child_start >= end {
1054                break;
1055            }
1056
1057            let chunk_key = link
1058                .key
1059                .ok_or_else(|| HashTreeError::Encryption("missing chunk key".to_string()))?;
1060
1061            let encrypted_child = self
1062                .store
1063                .get(&link.hash)
1064                .await
1065                .map_err(|e| HashTreeError::Store(e.to_string()))?
1066                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1067            let decrypted_child = decrypt_chk(&encrypted_child, &chunk_key)
1068                .map_err(|e| HashTreeError::Encryption(e.to_string()))?;
1069
1070            if let Some(child_node) = Self::decode_linked_file_node(link, &decrypted_child)? {
1071                Box::pin(self.append_encrypted_range(&child_node, start, end, child_start, result))
1072                    .await?;
1073                continue;
1074            }
1075
1076            let slice_start = if start > child_start {
1077                (start - child_start) as usize
1078            } else {
1079                0
1080            };
1081            let slice_end = if end < child_end {
1082                (end - child_start) as usize
1083            } else {
1084                decrypted_child.len()
1085            };
1086            result.extend_from_slice(&decrypted_child[slice_start..slice_end]);
1087        }
1088
1089        Ok(())
1090    }
1091
1092    /// Assemble only the chunks needed for a byte range
1093    async fn assemble_chunks_range(
1094        &self,
1095        node: &TreeNode,
1096        start: u64,
1097        end: Option<u64>,
1098    ) -> Result<Vec<u8>, HashTreeError> {
1099        // First, flatten the tree to get all leaf chunks with their byte offsets
1100        let chunks_info = self.collect_chunk_offsets(node).await?;
1101
1102        if chunks_info.is_empty() {
1103            return Ok(vec![]);
1104        }
1105
1106        // Calculate total size and actual end
1107        let total_size: u64 = chunks_info.iter().map(|(_, _, size)| size).sum();
1108        let actual_end = end.unwrap_or(total_size).min(total_size);
1109
1110        if start >= actual_end {
1111            return Ok(vec![]);
1112        }
1113
1114        // Find chunks that overlap with [start, actual_end)
1115        let mut result = Vec::with_capacity((actual_end - start) as usize);
1116
1117        for (chunk_hash, chunk_start, chunk_size) in &chunks_info {
1118            let chunk_start = *chunk_start;
1119            let chunk_size = *chunk_size;
1120            let chunk_end = chunk_start + chunk_size;
1121
1122            // Check if this chunk overlaps with our range
1123            if chunk_end > start && chunk_start < actual_end {
1124                let read_start = start.saturating_sub(chunk_start);
1125                let read_end_exclusive = actual_end.min(chunk_end) - chunk_start;
1126                if read_start >= read_end_exclusive {
1127                    continue;
1128                }
1129
1130                let chunk_data = self
1131                    .store
1132                    .get_range(chunk_hash, read_start, read_end_exclusive - 1)
1133                    .await
1134                    .map_err(|e| HashTreeError::Store(e.to_string()))?
1135                    .ok_or_else(|| HashTreeError::MissingChunk(to_hex(chunk_hash)))?;
1136
1137                let expected_len = (read_end_exclusive - read_start) as usize;
1138                if chunk_data.len() != expected_len {
1139                    return Err(HashTreeError::Store(format!(
1140                        "range read for {} returned {} bytes, expected {}",
1141                        to_hex(chunk_hash),
1142                        chunk_data.len(),
1143                        expected_len
1144                    )));
1145                }
1146
1147                result.extend_from_slice(&chunk_data);
1148            }
1149
1150            // Early exit if we've passed the requested range
1151            if chunk_end >= actual_end {
1152                break;
1153            }
1154        }
1155
1156        Ok(result)
1157    }
1158
1159    /// Collect all leaf chunk hashes with their byte offsets
1160    /// Returns Vec<(hash, offset, size)>
1161    async fn collect_chunk_offsets(
1162        &self,
1163        node: &TreeNode,
1164    ) -> Result<Vec<(Hash, u64, u64)>, HashTreeError> {
1165        let mut chunks = Vec::new();
1166        let mut offset = 0u64;
1167        self.collect_chunk_offsets_recursive(node, &mut chunks, &mut offset)
1168            .await?;
1169        Ok(chunks)
1170    }
1171
1172    async fn collect_chunk_offsets_recursive(
1173        &self,
1174        node: &TreeNode,
1175        chunks: &mut Vec<(Hash, u64, u64)>,
1176        offset: &mut u64,
1177    ) -> Result<(), HashTreeError> {
1178        for link in &node.links {
1179            if link.link_type == LinkType::Blob {
1180                chunks.push((link.hash, *offset, link.size));
1181                *offset += link.size;
1182                continue;
1183            }
1184
1185            let child_data = self
1186                .store
1187                .get(&link.hash)
1188                .await
1189                .map_err(|e| HashTreeError::Store(e.to_string()))?
1190                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1191
1192            if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
1193                // Intermediate node - recurse
1194                Box::pin(self.collect_chunk_offsets_recursive(&child_node, chunks, offset)).await?;
1195            } else {
1196                // Leaf chunk
1197                let size = child_data.len() as u64;
1198                chunks.push((link.hash, *offset, size));
1199                *offset += size;
1200            }
1201        }
1202        Ok(())
1203    }
1204
1205    /// Recursively assemble chunks from tree
1206    async fn assemble_chunks_limited(
1207        &self,
1208        node: &TreeNode,
1209        max_size: Option<u64>,
1210        bytes_read: &mut u64,
1211    ) -> Result<Vec<u8>, HashTreeError> {
1212        let mut parts: Vec<Vec<u8>> = Vec::new();
1213
1214        for link in &node.links {
1215            let projected = (*bytes_read).saturating_add(link.size);
1216            Self::ensure_size_limit(max_size, projected)?;
1217
1218            let child_data = self
1219                .store
1220                .get(&link.hash)
1221                .await
1222                .map_err(|e| HashTreeError::Store(e.to_string()))?
1223                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1224
1225            if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
1226                parts.push(
1227                    Box::pin(self.assemble_chunks_limited(&child_node, max_size, bytes_read))
1228                        .await?,
1229                );
1230            } else {
1231                let projected = (*bytes_read).saturating_add(child_data.len() as u64);
1232                Self::ensure_size_limit(max_size, projected)?;
1233                *bytes_read = projected;
1234                parts.push(child_data);
1235            }
1236        }
1237
1238        // Concatenate all parts
1239        let total_length: usize = parts.iter().map(|p| p.len()).sum();
1240        let mut result = Vec::with_capacity(total_length);
1241        for part in parts {
1242            result.extend_from_slice(&part);
1243        }
1244
1245        Ok(result)
1246    }
1247
1248    /// Read file chunks as Vec (non-streaming version)
1249    pub async fn read_file_chunks(&self, hash: &Hash) -> Result<Vec<Vec<u8>>, HashTreeError> {
1250        let data = match self
1251            .store
1252            .get(hash)
1253            .await
1254            .map_err(|e| HashTreeError::Store(e.to_string()))?
1255        {
1256            Some(d) => d,
1257            None => return Ok(vec![]),
1258        };
1259
1260        if !is_tree_node(&data) {
1261            return Ok(vec![data]);
1262        }
1263
1264        let node = decode_tree_node(&data)?;
1265        self.collect_chunks(&node).await
1266    }
1267
1268    async fn collect_chunks(&self, node: &TreeNode) -> Result<Vec<Vec<u8>>, HashTreeError> {
1269        let mut chunks = Vec::new();
1270
1271        for link in &node.links {
1272            let child_data = self
1273                .store
1274                .get(&link.hash)
1275                .await
1276                .map_err(|e| HashTreeError::Store(e.to_string()))?
1277                .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&link.hash)))?;
1278
1279            if let Some(child_node) = Self::decode_linked_file_node(link, &child_data)? {
1280                chunks.extend(Box::pin(self.collect_chunks(&child_node)).await?);
1281            } else {
1282                chunks.push(child_data);
1283            }
1284        }
1285
1286        Ok(chunks)
1287    }
1288
1289    /// List directory entries (Cid-based, supports encrypted directories)
1290    pub async fn list(&self, cid: &Cid) -> Result<Vec<TreeEntry>, HashTreeError> {
1291        let node = match self.get_node(cid).await? {
1292            Some(n) => n,
1293            None => return Ok(vec![]),
1294        };
1295
1296        let mut entries = Vec::new();
1297
1298        for link in &node.links {
1299            // Skip internal chunk nodes - recurse into them
1300            if is_internal_directory_link(&node, link) {
1301                let chunk_cid = Cid {
1302                    hash: link.hash,
1303                    key: link.key,
1304                };
1305                let sub_entries = Box::pin(self.list(&chunk_cid)).await?;
1306                entries.extend(sub_entries);
1307                continue;
1308            }
1309
1310            entries.push(TreeEntry {
1311                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
1312                hash: link.hash,
1313                size: link.size,
1314                link_type: link.link_type,
1315                key: link.key,
1316                meta: link.meta.clone(),
1317            });
1318        }
1319
1320        Ok(entries)
1321    }
1322
1323    /// List directory entries using Cid (with decryption if key present).
1324    /// Handles both regular and fanout directory data.
1325    pub async fn list_directory(&self, cid: &Cid) -> Result<Vec<TreeEntry>, HashTreeError> {
1326        // Use get_directory_node which handles chunked directory data
1327        let node = match self.get_directory_node(cid).await? {
1328            Some(n) => n,
1329            None => return Ok(vec![]),
1330        };
1331
1332        let mut entries = Vec::new();
1333
1334        for link in &node.links {
1335            // Skip internal fanout nodes.
1336            if is_internal_directory_link(&node, link) {
1337                let sub_cid = Cid {
1338                    hash: link.hash,
1339                    key: link.key,
1340                };
1341                let sub_entries = Box::pin(self.list_directory(&sub_cid)).await?;
1342                entries.extend(sub_entries);
1343                continue;
1344            }
1345
1346            entries.push(TreeEntry {
1347                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
1348                hash: link.hash,
1349                size: link.size,
1350                link_type: link.link_type,
1351                key: link.key,
1352                meta: link.meta.clone(),
1353            });
1354        }
1355
1356        Ok(entries)
1357    }
1358
1359    /// List a directory reached through an existing parent link.
1360    /// Unlike `list_directory`, an unavailable node is not an empty directory.
1361    pub async fn list_directory_required(
1362        &self,
1363        cid: &Cid,
1364    ) -> Result<Vec<TreeEntry>, HashTreeError> {
1365        let node = self
1366            .get_directory_node(cid)
1367            .await?
1368            .ok_or_else(|| HashTreeError::MissingChunk(to_hex(&cid.hash)))?;
1369
1370        let mut entries = Vec::new();
1371        for link in &node.links {
1372            if is_internal_directory_link(&node, link) {
1373                let sub_cid = Cid {
1374                    hash: link.hash,
1375                    key: link.key,
1376                };
1377                let sub_entries = Box::pin(self.list_directory_required(&sub_cid)).await?;
1378                entries.extend(sub_entries);
1379                continue;
1380            }
1381
1382            entries.push(TreeEntry {
1383                name: link.name.clone().unwrap_or_else(|| to_hex(&link.hash)),
1384                hash: link.hash,
1385                size: link.size,
1386                link_type: link.link_type,
1387                key: link.key,
1388                meta: link.meta.clone(),
1389            });
1390        }
1391
1392        Ok(entries)
1393    }
1394
1395    /// Resolve a path within a tree (returns Cid with key if encrypted)
1396    pub async fn resolve(&self, cid: &Cid, path: &str) -> Result<Option<Cid>, HashTreeError> {
1397        let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
1398        if parts.is_empty() {
1399            return Ok(Some(cid.clone()));
1400        }
1401
1402        let mut current_cid = cid.clone();
1403
1404        for part in parts {
1405            // Use get_directory_node which handles chunked directory data
1406            let node = match self.get_directory_node(&current_cid).await? {
1407                Some(n) => n,
1408                None => {
1409                    if !self.has_stored_chunk(&current_cid.hash).await? {
1410                        return Err(HashTreeError::MissingChunk(to_hex(&current_cid.hash)));
1411                    }
1412                    return Ok(None);
1413                }
1414            };
1415
1416            if let Some(link) = self.find_link(&node, part) {
1417                current_cid = Cid {
1418                    hash: link.hash,
1419                    key: link.key,
1420                };
1421            } else {
1422                // Check internal nodes
1423                match self
1424                    .find_link_in_subtrees_cid(&node, part, &current_cid)
1425                    .await?
1426                {
1427                    Some(link) => {
1428                        current_cid = Cid {
1429                            hash: link.hash,
1430                            key: link.key,
1431                        };
1432                    }
1433                    None => return Ok(None),
1434                }
1435            }
1436        }
1437
1438        Ok(Some(current_cid))
1439    }
1440
1441    /// Resolve a path within a tree using Cid (with decryption if key present)
1442    pub async fn resolve_path(&self, cid: &Cid, path: &str) -> Result<Option<Cid>, HashTreeError> {
1443        self.resolve(cid, path).await
1444    }
1445
1446    fn find_link(&self, node: &TreeNode, name: &str) -> Option<Link> {
1447        node.links
1448            .iter()
1449            .find(|l| !is_internal_directory_link(node, l) && l.name.as_deref() == Some(name))
1450            .cloned()
1451    }
1452
1453    /// Find a link in subtrees using Cid (with decryption support)
1454    async fn find_link_in_subtrees_cid(
1455        &self,
1456        node: &TreeNode,
1457        name: &str,
1458        _parent_cid: &Cid,
1459    ) -> Result<Option<Link>, HashTreeError> {
1460        for link in &node.links {
1461            if !is_internal_directory_link(node, link) {
1462                continue;
1463            }
1464
1465            // Internal nodes inherit encryption from parent context
1466            let sub_cid = Cid {
1467                hash: link.hash,
1468                key: link.key,
1469            };
1470
1471            let sub_node = match self.get_node(&sub_cid).await? {
1472                Some(n) => n,
1473                None => {
1474                    if !self.has_stored_chunk(&sub_cid.hash).await? {
1475                        return Err(HashTreeError::MissingChunk(to_hex(&sub_cid.hash)));
1476                    }
1477                    continue;
1478                }
1479            };
1480
1481            if let Some(found) = self.find_link(&sub_node, name) {
1482                return Ok(Some(found));
1483            }
1484
1485            if let Some(deep_found) =
1486                Box::pin(self.find_link_in_subtrees_cid(&sub_node, name, &sub_cid)).await?
1487            {
1488                return Ok(Some(deep_found));
1489            }
1490        }
1491
1492        Ok(None)
1493    }
1494
1495    /// Get total size of a tree
1496    pub async fn get_size(&self, hash: &Hash) -> Result<u64, HashTreeError> {
1497        let data = match self
1498            .store
1499            .get(hash)
1500            .await
1501            .map_err(|e| HashTreeError::Store(e.to_string()))?
1502        {
1503            Some(d) => d,
1504            None => return Ok(0),
1505        };
1506
1507        if !is_tree_node(&data) {
1508            return Ok(data.len() as u64);
1509        }
1510
1511        let node = decode_tree_node(&data)?;
1512        // Calculate from children
1513        let mut total = 0u64;
1514        for link in &node.links {
1515            total += link.size;
1516        }
1517        Ok(total)
1518    }
1519
1520    /// Get total size using a Cid (handles decryption if key present)
1521    pub async fn get_size_cid(&self, cid: &Cid) -> Result<u64, HashTreeError> {
1522        if let Some(key) = cid.key {
1523            let data = match self.get_encrypted_root(&cid.hash, &key).await? {
1524                Some(d) => d,
1525                None => return Ok(0),
1526            };
1527            if is_tree_node(&data) {
1528                let node = decode_tree_node(&data)?;
1529                return Ok(node.links.iter().map(|link| link.size).sum());
1530            }
1531            return Ok(data.len() as u64);
1532        }
1533
1534        self.get_size(&cid.hash).await
1535    }
1536
1537    // ============ EDIT ============
1538
1539    /// Add or update an entry in a directory
1540    /// Returns new root Cid (immutable operation)
1541    pub async fn set_entry(
1542        &self,
1543        root: &Cid,
1544        path: &[&str],
1545        name: &str,
1546        entry_cid: &Cid,
1547        size: u64,
1548        link_type: LinkType,
1549    ) -> Result<Cid, HashTreeError> {
1550        self.set_entry_with_meta(root, path, name, entry_cid, size, link_type, None)
1551            .await
1552    }
1553
1554    /// Add or update an entry in a directory with optional link metadata.
1555    /// Returns new root Cid (immutable operation)
1556    pub async fn set_entry_with_meta(
1557        &self,
1558        root: &Cid,
1559        path: &[&str],
1560        name: &str,
1561        entry_cid: &Cid,
1562        size: u64,
1563        link_type: LinkType,
1564        meta: Option<std::collections::HashMap<String, serde_json::Value>>,
1565    ) -> Result<Cid, HashTreeError> {
1566        let dir_cid = self.resolve_path_array(root, path).await?;
1567        let dir_cid = dir_cid.ok_or_else(|| HashTreeError::PathNotFound(path.join("/")))?;
1568
1569        let entries = self.list_directory(&dir_cid).await?;
1570        let mut new_entries: Vec<DirEntry> = entries
1571            .into_iter()
1572            .filter(|e| e.name != name)
1573            .map(|e| DirEntry {
1574                name: e.name,
1575                hash: e.hash,
1576                size: e.size,
1577                key: e.key,
1578                link_type: e.link_type,
1579                meta: e.meta,
1580            })
1581            .collect();
1582
1583        new_entries.push(DirEntry {
1584            name: name.to_string(),
1585            hash: entry_cid.hash,
1586            size,
1587            key: entry_cid.key,
1588            link_type,
1589            meta,
1590        });
1591
1592        let new_dir_cid = self.put_directory(new_entries).await?;
1593        self.rebuild_path(root, path, new_dir_cid).await
1594    }
1595
1596    /// Remove an entry from a directory
1597    /// Returns new root Cid
1598    pub async fn remove_entry(
1599        &self,
1600        root: &Cid,
1601        path: &[&str],
1602        name: &str,
1603    ) -> Result<Cid, HashTreeError> {
1604        let dir_cid = self.resolve_path_array(root, path).await?;
1605        let dir_cid = dir_cid.ok_or_else(|| HashTreeError::PathNotFound(path.join("/")))?;
1606
1607        let entries = self.list_directory(&dir_cid).await?;
1608        let new_entries: Vec<DirEntry> = entries
1609            .into_iter()
1610            .filter(|e| e.name != name)
1611            .map(|e| DirEntry {
1612                name: e.name,
1613                hash: e.hash,
1614                size: e.size,
1615                key: e.key,
1616                link_type: e.link_type,
1617                meta: e.meta,
1618            })
1619            .collect();
1620
1621        let new_dir_cid = self.put_directory(new_entries).await?;
1622        self.rebuild_path(root, path, new_dir_cid).await
1623    }
1624
1625    /// Rename an entry in a directory
1626    /// Returns new root Cid
1627    pub async fn rename_entry(
1628        &self,
1629        root: &Cid,
1630        path: &[&str],
1631        old_name: &str,
1632        new_name: &str,
1633    ) -> Result<Cid, HashTreeError> {
1634        if old_name == new_name {
1635            return Ok(root.clone());
1636        }
1637
1638        let dir_cid = self.resolve_path_array(root, path).await?;
1639        let dir_cid = dir_cid.ok_or_else(|| HashTreeError::PathNotFound(path.join("/")))?;
1640
1641        let entries = self.list_directory(&dir_cid).await?;
1642        let entry = entries
1643            .iter()
1644            .find(|e| e.name == old_name)
1645            .ok_or_else(|| HashTreeError::EntryNotFound(old_name.to_string()))?;
1646
1647        let entry_hash = entry.hash;
1648        let entry_size = entry.size;
1649        let entry_key = entry.key;
1650        let entry_link_type = entry.link_type;
1651        let entry_meta = entry.meta.clone();
1652
1653        let new_entries: Vec<DirEntry> = entries
1654            .into_iter()
1655            .filter(|e| e.name != old_name)
1656            .map(|e| DirEntry {
1657                name: e.name,
1658                hash: e.hash,
1659                size: e.size,
1660                key: e.key,
1661                link_type: e.link_type,
1662                meta: e.meta,
1663            })
1664            .chain(std::iter::once(DirEntry {
1665                name: new_name.to_string(),
1666                hash: entry_hash,
1667                size: entry_size,
1668                key: entry_key,
1669                link_type: entry_link_type,
1670                meta: entry_meta,
1671            }))
1672            .collect();
1673
1674        let new_dir_cid = self.put_directory(new_entries).await?;
1675        self.rebuild_path(root, path, new_dir_cid).await
1676    }
1677
1678    /// Move an entry to a different directory
1679    /// Returns new root Cid
1680    pub async fn move_entry(
1681        &self,
1682        root: &Cid,
1683        source_path: &[&str],
1684        name: &str,
1685        target_path: &[&str],
1686    ) -> Result<Cid, HashTreeError> {
1687        let source_dir_cid = self.resolve_path_array(root, source_path).await?;
1688        let source_dir_cid =
1689            source_dir_cid.ok_or_else(|| HashTreeError::PathNotFound(source_path.join("/")))?;
1690
1691        let source_entries = self.list_directory(&source_dir_cid).await?;
1692        let entry = source_entries
1693            .iter()
1694            .find(|e| e.name == name)
1695            .ok_or_else(|| HashTreeError::EntryNotFound(name.to_string()))?;
1696
1697        let entry_cid = Cid {
1698            hash: entry.hash,
1699            key: entry.key,
1700        };
1701        let entry_size = entry.size;
1702        let entry_link_type = entry.link_type;
1703
1704        // Remove from source
1705        let new_root = self.remove_entry(root, source_path, name).await?;
1706
1707        // Add to target
1708        self.set_entry(
1709            &new_root,
1710            target_path,
1711            name,
1712            &entry_cid,
1713            entry_size,
1714            entry_link_type,
1715        )
1716        .await
1717    }
1718
1719    async fn resolve_path_array(
1720        &self,
1721        root: &Cid,
1722        path: &[&str],
1723    ) -> Result<Option<Cid>, HashTreeError> {
1724        if path.is_empty() {
1725            return Ok(Some(root.clone()));
1726        }
1727        self.resolve_path(root, &path.join("/")).await
1728    }
1729
1730    async fn rebuild_path(
1731        &self,
1732        root: &Cid,
1733        path: &[&str],
1734        new_child: Cid,
1735    ) -> Result<Cid, HashTreeError> {
1736        if path.is_empty() {
1737            return Ok(new_child);
1738        }
1739
1740        let mut child_cid = new_child;
1741        let parts: Vec<&str> = path.to_vec();
1742
1743        for i in (0..parts.len()).rev() {
1744            let child_name = parts[i];
1745            let parent_path = &parts[..i];
1746
1747            let parent_cid = if parent_path.is_empty() {
1748                root.clone()
1749            } else {
1750                self.resolve_path_array(root, parent_path)
1751                    .await?
1752                    .ok_or_else(|| HashTreeError::PathNotFound(parent_path.join("/")))?
1753            };
1754
1755            let parent_entries = self.list_directory(&parent_cid).await?;
1756            let new_parent_entries: Vec<DirEntry> = parent_entries
1757                .into_iter()
1758                .map(|e| {
1759                    if e.name == child_name {
1760                        DirEntry {
1761                            name: e.name,
1762                            hash: child_cid.hash,
1763                            size: 0, // Directories don't have a meaningful size in the link
1764                            key: child_cid.key,
1765                            link_type: e.link_type,
1766                            meta: e.meta,
1767                        }
1768                    } else {
1769                        DirEntry {
1770                            name: e.name,
1771                            hash: e.hash,
1772                            size: e.size,
1773                            key: e.key,
1774                            link_type: e.link_type,
1775                            meta: e.meta,
1776                        }
1777                    }
1778                })
1779                .collect();
1780
1781            child_cid = self.put_directory(new_parent_entries).await?;
1782        }
1783
1784        Ok(child_cid)
1785    }
1786
1787    // ============ UTILITY ============
1788
1789    /// Get the underlying store
1790    pub fn get_store(&self) -> Arc<S> {
1791        self.store.clone()
1792    }
1793
1794    /// Get chunk size configuration
1795    pub fn chunk_size(&self) -> usize {
1796        self.chunk_size
1797    }
1798
1799    /// Get max links configuration
1800    pub fn max_links(&self) -> usize {
1801        self.max_links
1802    }
1803}
1804
1805fn stream_put_batch_target_bytes() -> usize {
1806    std::env::var(STREAM_PUT_BATCH_TARGET_BYTES_ENV)
1807        .ok()
1808        .and_then(|value| value.parse::<usize>().ok())
1809        .filter(|value| *value > 0)
1810        .unwrap_or(DEFAULT_STREAM_PUT_BATCH_TARGET_BYTES)
1811}
1812
1813/// Verify tree integrity - checks that all referenced hashes exist
1814pub async fn verify_tree<S: Store>(
1815    store: Arc<S>,
1816    root_hash: &Hash,
1817) -> Result<crate::reader::VerifyResult, HashTreeError> {
1818    let mut missing = Vec::new();
1819    let mut visited = std::collections::HashSet::new();
1820
1821    verify_recursive(store, root_hash, &mut missing, &mut visited).await?;
1822
1823    Ok(crate::reader::VerifyResult {
1824        valid: missing.is_empty(),
1825        missing,
1826    })
1827}
1828
1829async fn verify_recursive<S: Store>(
1830    store: Arc<S>,
1831    hash: &Hash,
1832    missing: &mut Vec<Hash>,
1833    visited: &mut std::collections::HashSet<String>,
1834) -> Result<(), HashTreeError> {
1835    let hex = to_hex(hash);
1836    if visited.contains(&hex) {
1837        return Ok(());
1838    }
1839    visited.insert(hex);
1840
1841    let data = match store
1842        .get(hash)
1843        .await
1844        .map_err(|e| HashTreeError::Store(e.to_string()))?
1845    {
1846        Some(d) => d,
1847        None => {
1848            missing.push(*hash);
1849            return Ok(());
1850        }
1851    };
1852
1853    if is_tree_node(&data) {
1854        let node = decode_tree_node(&data)?;
1855        for link in &node.links {
1856            Box::pin(verify_recursive(
1857                store.clone(),
1858                &link.hash,
1859                missing,
1860                visited,
1861            ))
1862            .await?;
1863        }
1864    }
1865
1866    Ok(())
1867}
1868
1869#[cfg(test)]
1870mod tests;