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