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