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