Skip to main content

hashtree_core/
builder.rs

1//! Tree builder with chunking and fanout support
2//!
3//! - Large files are split into chunks
4//! - Large directories are split into sub-trees
5//! - Supports streaming appends
6//! - Encryption enabled by default (CHK - Content Hash Key)
7
8// The deprecated types below still implement and test themselves.
9#![allow(deprecated)]
10
11use std::sync::Arc;
12
13use crate::codec::encode_and_hash;
14use crate::directory::{directory_fanout_meta, DirectoryFanoutSpan};
15use crate::hash::sha256;
16use crate::store::Store;
17use crate::types::{Cid, DirEntry, Hash, Link, LinkType, TreeNode};
18
19use crate::encryption::{encrypt_object, EncryptionMode, ObjectKey};
20
21/// Default chunk size: 2MB (optimized for blossom uploads, matches hashtree-ts)
22pub const DEFAULT_CHUNK_SIZE: usize = 2 * 1024 * 1024;
23
24/// BEP52 chunk size: 16KB
25pub const BEP52_CHUNK_SIZE: usize = 16 * 1024;
26
27/// Default max links per tree node (fanout)
28pub const DEFAULT_MAX_LINKS: usize = 174;
29
30/// Builder configuration
31#[deprecated(
32    since = "0.3.1",
33    note = "use `HashTree`, which covers the same operations"
34)]
35#[derive(Clone)]
36pub struct BuilderConfig<S: Store> {
37    pub store: Arc<S>,
38    pub chunk_size: usize,
39    pub max_links: usize,
40    /// Whether to encrypt content (default: true when encryption feature enabled)
41    pub encrypted: bool,
42}
43
44impl<S: Store> BuilderConfig<S> {
45    pub fn new(store: Arc<S>) -> Self {
46        Self {
47            store,
48            chunk_size: DEFAULT_CHUNK_SIZE,
49            max_links: DEFAULT_MAX_LINKS,
50            encrypted: true,
51        }
52    }
53
54    pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
55        self.chunk_size = chunk_size;
56        self
57    }
58
59    pub fn with_max_links(mut self, max_links: usize) -> Self {
60        self.max_links = max_links;
61        self
62    }
63
64    /// Disable encryption (store content publicly)
65    pub fn public(mut self) -> Self {
66        self.encrypted = false;
67        self
68    }
69
70    /// Enable encryption (CHK - Content Hash Key)
71    pub fn encrypted(mut self) -> Self {
72        self.encrypted = true;
73        self
74    }
75}
76
77/// TreeBuilder - builds content-addressed merkle trees
78#[deprecated(
79    since = "0.3.1",
80    note = "use `HashTree`, which covers the same operations"
81)]
82pub struct TreeBuilder<S: Store> {
83    store: Arc<S>,
84    chunk_size: usize,
85    max_links: usize,
86    encrypted: bool,
87}
88
89impl<S: Store> TreeBuilder<S> {
90    pub fn new(config: BuilderConfig<S>) -> Self {
91        Self {
92            store: config.store,
93            chunk_size: config.chunk_size,
94            max_links: config.max_links,
95            encrypted: config.encrypted,
96        }
97    }
98
99    /// Check if encryption is enabled
100    pub fn is_encrypted(&self) -> bool {
101        self.encrypted
102    }
103
104    /// Store a blob directly (small data, no encryption)
105    /// Returns the content hash
106    pub async fn put_blob(&self, data: &[u8]) -> Result<Hash, BuilderError> {
107        let hash = sha256(data);
108        self.store
109            .put(hash, data.to_vec())
110            .await
111            .map_err(|e| BuilderError::Store(e.to_string()))?;
112        Ok(hash)
113    }
114
115    /// Store a chunk with optional encryption
116    /// Returns (hash, optional_key) where hash is of stored data
117    async fn put_chunk_internal(
118        &self,
119        data: &[u8],
120    ) -> Result<(Hash, Option<ObjectKey>), BuilderError> {
121        if self.encrypted {
122            let (encrypted, key) = encrypt_object(data, EncryptionMode::default())
123                .map_err(|e| BuilderError::Encryption(e.to_string()))?;
124            let hash = sha256(&encrypted);
125            self.store
126                .put(hash, encrypted)
127                .await
128                .map_err(|e| BuilderError::Store(e.to_string()))?;
129            Ok((hash, Some(key)))
130        } else {
131            let hash = self.put_blob(data).await?;
132            Ok((hash, None))
133        }
134    }
135
136    /// Store a file, chunking if necessary
137    /// Returns (Cid, size) where Cid contains hash and optional encryption key
138    ///
139    /// When encryption is enabled (default), each chunk is CHK encrypted
140    /// and the result contains the decryption key.
141    pub async fn put(&self, data: &[u8]) -> Result<(Cid, u64), BuilderError> {
142        let size = data.len() as u64;
143
144        // Small file - store as single chunk
145        if data.len() <= self.chunk_size {
146            let (hash, key) = self.put_chunk_internal(data).await?;
147            return Ok((Cid { hash, key }, size));
148        }
149
150        // Large file - chunk it
151        let mut links: Vec<Link> = Vec::new();
152        let mut offset = 0;
153
154        while offset < data.len() {
155            let end = (offset + self.chunk_size).min(data.len());
156            let chunk = &data[offset..end];
157            let chunk_size = chunk.len() as u64;
158            let (hash, key) = self.put_chunk_internal(chunk).await?;
159            links.push(Link {
160                hash,
161                name: None,
162                size: chunk_size,
163                key,
164                link_type: LinkType::Blob, // leaf chunk
165                meta: None,
166            });
167            offset = end;
168        }
169
170        // Build tree from chunks
171        let (root_hash, root_key, _) = self.build_tree_internal(links, Some(size)).await?;
172
173        Ok((
174            Cid {
175                hash: root_hash,
176                key: root_key,
177            },
178            size,
179        ))
180    }
181
182    /// Build tree and return (hash, optional_key)
183    /// When encrypted, tree nodes are also CHK encrypted
184    async fn build_tree_internal(
185        &self,
186        links: Vec<Link>,
187        total_size: Option<u64>,
188    ) -> Result<(Hash, Option<ObjectKey>, LinkType), BuilderError> {
189        // Single link with matching size - return directly
190        if links.len() == 1 {
191            if let Some(ts) = total_size {
192                if links[0].size == ts {
193                    return Ok((links[0].hash, links[0].key, links[0].link_type));
194                }
195            }
196        }
197
198        if links.len() <= self.max_links {
199            let node = TreeNode {
200                node_type: LinkType::File,
201                links,
202            };
203            let (data, _) = encode_and_hash(&node)?;
204
205            if self.encrypted {
206                let (encrypted, key) = encrypt_object(&data, EncryptionMode::default())
207                    .map_err(|e| BuilderError::Encryption(e.to_string()))?;
208                let hash = sha256(&encrypted);
209                self.store
210                    .put(hash, encrypted)
211                    .await
212                    .map_err(|e| BuilderError::Store(e.to_string()))?;
213                return Ok((hash, Some(key), LinkType::File));
214            }
215
216            // Unencrypted path
217            let hash = sha256(&data);
218            self.store
219                .put(hash, data)
220                .await
221                .map_err(|e| BuilderError::Store(e.to_string()))?;
222            return Ok((hash, None, LinkType::File));
223        }
224
225        // Too many links - create subtrees
226        let mut sub_links = Vec::new();
227        for batch in links.chunks(self.max_links) {
228            let batch_size: u64 = batch.iter().map(|l| l.size).sum();
229            let (hash, key, link_type) =
230                Box::pin(self.build_tree_internal(batch.to_vec(), Some(batch_size))).await?;
231            sub_links.push(Link {
232                hash,
233                name: None,
234                size: batch_size,
235                key,
236                link_type,
237                meta: None,
238            });
239        }
240
241        Box::pin(self.build_tree_internal(sub_links, total_size)).await
242    }
243
244    /// Build a directory from entries
245    /// Entries can be files or subdirectories
246    pub async fn put_directory(&self, entries: Vec<DirEntry>) -> Result<Hash, BuilderError> {
247        // Sort entries by name for deterministic hashing
248        let mut sorted = entries;
249        sorted.sort_by(|a, b| a.name.cmp(&b.name));
250
251        let links: Vec<Link> = sorted
252            .into_iter()
253            .map(|e| Link {
254                hash: e.hash,
255                name: Some(e.name),
256                size: e.size,
257                key: e.key,
258                link_type: e.link_type,
259                meta: e.meta,
260            })
261            .collect();
262
263        // Fits in one node
264        if links.len() <= self.max_links {
265            let node = TreeNode {
266                node_type: LinkType::Dir,
267                links,
268            };
269            let (data, hash) = encode_and_hash(&node)?;
270            self.store
271                .put(hash, data)
272                .await
273                .map_err(|e| BuilderError::Store(e.to_string()))?;
274            return Ok(hash);
275        }
276
277        self.build_directory_by_chunks(links).await
278    }
279
280    /// Split directories into canonical BUD-17 fanout nodes.
281    async fn build_directory_by_chunks(&self, links: Vec<Link>) -> Result<Hash, BuilderError> {
282        let spans = links
283            .into_iter()
284            .map(|link| {
285                let name = link
286                    .name
287                    .clone()
288                    .unwrap_or_else(|| crate::types::to_hex(&link.hash));
289                DirectoryFanoutSpan {
290                    link,
291                    count: 1,
292                    first: name.clone(),
293                    last: name,
294                }
295            })
296            .collect();
297        self.build_directory_fanout_level(spans, LinkType::Dir)
298            .await
299    }
300
301    async fn build_directory_fanout_level(
302        &self,
303        spans: Vec<DirectoryFanoutSpan>,
304        child_node_type: LinkType,
305    ) -> Result<Hash, BuilderError> {
306        let mut sub_trees: Vec<DirectoryFanoutSpan> = Vec::new();
307
308        for batch in spans.chunks(self.max_links) {
309            let Some(first_span) = batch.first() else {
310                continue;
311            };
312            let last_span = batch.last().expect("non-empty fanout batch");
313            let count: usize = batch.iter().map(|span| span.count).sum();
314            let batch_size: u64 = batch.iter().map(|span| span.link.size).sum();
315
316            let node = TreeNode {
317                node_type: child_node_type,
318                links: batch.iter().map(|span| span.link.clone()).collect(),
319            };
320            let (data, hash) = encode_and_hash(&node)?;
321            self.store
322                .put(hash, data)
323                .await
324                .map_err(|e| BuilderError::Store(e.to_string()))?;
325
326            sub_trees.push(DirectoryFanoutSpan {
327                link: Link {
328                    hash,
329                    name: None,
330                    size: batch_size,
331                    key: None,
332                    link_type: child_node_type,
333                    meta: Some(directory_fanout_meta(
334                        count,
335                        &first_span.first,
336                        &last_span.last,
337                    )),
338                },
339                count,
340                first: first_span.first.clone(),
341                last: last_span.last.clone(),
342            });
343        }
344
345        if sub_trees.len() <= self.max_links {
346            let node = TreeNode {
347                node_type: LinkType::Fanout,
348                links: sub_trees.into_iter().map(|span| span.link).collect(),
349            };
350            let (data, hash) = encode_and_hash(&node)?;
351            self.store
352                .put(hash, data)
353                .await
354                .map_err(|e| BuilderError::Store(e.to_string()))?;
355            return Ok(hash);
356        }
357
358        // Recursively build more levels
359        Box::pin(self.build_directory_fanout_level(sub_trees, LinkType::Fanout)).await
360    }
361
362    /// Create a tree node
363    pub async fn put_tree_node(&self, links: Vec<Link>) -> Result<Hash, BuilderError> {
364        let node = TreeNode {
365            node_type: LinkType::Dir,
366            links,
367        };
368
369        let (data, hash) = encode_and_hash(&node)?;
370        self.store
371            .put(hash, data)
372            .await
373            .map_err(|e| BuilderError::Store(e.to_string()))?;
374        Ok(hash)
375    }
376}
377
378/// StreamBuilder - supports incremental appends
379#[deprecated(
380    since = "0.3.1",
381    note = "use `HashTree`, which covers the same operations"
382)]
383pub struct StreamBuilder<S: Store> {
384    store: Arc<S>,
385    chunk_size: usize,
386    max_links: usize,
387
388    // Current partial chunk being built
389    buffer: Vec<u8>,
390
391    // Completed chunks
392    chunks: Vec<Link>,
393    total_size: u64,
394}
395
396impl<S: Store> StreamBuilder<S> {
397    pub fn new(config: BuilderConfig<S>) -> Self {
398        Self {
399            store: config.store,
400            chunk_size: config.chunk_size,
401            max_links: config.max_links,
402            buffer: Vec::with_capacity(config.chunk_size),
403            chunks: Vec::new(),
404            total_size: 0,
405        }
406    }
407
408    /// Append data to the stream
409    pub async fn append(&mut self, data: &[u8]) -> Result<(), BuilderError> {
410        let mut offset = 0;
411
412        while offset < data.len() {
413            let space = self.chunk_size - self.buffer.len();
414            let to_write = space.min(data.len() - offset);
415
416            self.buffer
417                .extend_from_slice(&data[offset..offset + to_write]);
418            offset += to_write;
419
420            // Flush full chunk
421            if self.buffer.len() == self.chunk_size {
422                self.flush_chunk().await?;
423            }
424        }
425
426        self.total_size += data.len() as u64;
427        Ok(())
428    }
429
430    /// Flush current buffer as a chunk
431    async fn flush_chunk(&mut self) -> Result<(), BuilderError> {
432        if self.buffer.is_empty() {
433            return Ok(());
434        }
435
436        let chunk = std::mem::take(&mut self.buffer);
437        let hash = sha256(&chunk);
438        self.store
439            .put(hash, chunk.clone())
440            .await
441            .map_err(|e| BuilderError::Store(e.to_string()))?;
442
443        self.chunks.push(Link {
444            hash,
445            name: None,
446            size: chunk.len() as u64,
447            key: None,
448            link_type: LinkType::Blob, // Leaf chunk (raw blob)
449            meta: None,
450        });
451
452        self.buffer = Vec::with_capacity(self.chunk_size);
453        Ok(())
454    }
455
456    /// Get current root hash without finalizing
457    /// Useful for checkpoints
458    pub async fn current_root(&mut self) -> Result<Option<Hash>, BuilderError> {
459        if self.chunks.is_empty() && self.buffer.is_empty() {
460            return Ok(None);
461        }
462
463        // Temporarily include buffer
464        let mut temp_chunks = self.chunks.clone();
465        if !self.buffer.is_empty() {
466            let chunk = self.buffer.clone();
467            let hash = sha256(&chunk);
468            self.store
469                .put(hash, chunk.clone())
470                .await
471                .map_err(|e| BuilderError::Store(e.to_string()))?;
472            temp_chunks.push(Link {
473                hash,
474                name: None,
475                size: chunk.len() as u64,
476                key: None,
477                link_type: LinkType::Blob, // Leaf chunk (raw blob)
478                meta: None,
479            });
480        }
481
482        let hash = self
483            .build_tree_from_chunks(&temp_chunks, self.total_size)
484            .await?;
485        Ok(Some(hash))
486    }
487
488    /// Finalize the stream and return root hash
489    pub async fn finalize(mut self) -> Result<(Hash, u64), BuilderError> {
490        // Flush remaining buffer
491        self.flush_chunk().await?;
492
493        if self.chunks.is_empty() {
494            // Empty stream - return hash of empty data
495            let empty_hash = sha256(&[]);
496            self.store
497                .put(empty_hash, vec![])
498                .await
499                .map_err(|e| BuilderError::Store(e.to_string()))?;
500            return Ok((empty_hash, 0));
501        }
502
503        let hash = self
504            .build_tree_from_chunks(&self.chunks, self.total_size)
505            .await?;
506        Ok((hash, self.total_size))
507    }
508
509    /// Build balanced tree from chunks
510    async fn build_tree_from_chunks(
511        &self,
512        chunks: &[Link],
513        total_size: u64,
514    ) -> Result<Hash, BuilderError> {
515        if chunks.len() == 1 {
516            return Ok(chunks[0].hash);
517        }
518
519        if chunks.len() <= self.max_links {
520            let node = TreeNode {
521                node_type: LinkType::File,
522                links: chunks.to_vec(),
523            };
524            let (data, hash) = encode_and_hash(&node)?;
525            self.store
526                .put(hash, data)
527                .await
528                .map_err(|e| BuilderError::Store(e.to_string()))?;
529            return Ok(hash);
530        }
531
532        // Build intermediate level
533        let mut sub_trees: Vec<Link> = Vec::new();
534        for batch in chunks.chunks(self.max_links) {
535            let batch_size: u64 = batch.iter().map(|l| l.size).sum();
536
537            let node = TreeNode {
538                node_type: LinkType::File,
539                links: batch.to_vec(),
540            };
541            let (data, hash) = encode_and_hash(&node)?;
542            self.store
543                .put(hash, data)
544                .await
545                .map_err(|e| BuilderError::Store(e.to_string()))?;
546
547            sub_trees.push(Link {
548                hash,
549                name: None,
550                size: batch_size,
551                key: None,
552                link_type: LinkType::File, // Internal tree node
553                meta: None,
554            });
555        }
556
557        Box::pin(self.build_tree_from_chunks(&sub_trees, total_size)).await
558    }
559
560    /// Get stats
561    pub fn stats(&self) -> StreamStats {
562        StreamStats {
563            chunks: self.chunks.len(),
564            buffered: self.buffer.len(),
565            total_size: self.total_size,
566        }
567    }
568}
569
570#[derive(Debug, Clone, PartialEq)]
571pub struct StreamStats {
572    pub chunks: usize,
573    pub buffered: usize,
574    pub total_size: u64,
575}
576
577/// Builder error type
578#[derive(Debug, thiserror::Error)]
579pub enum BuilderError {
580    #[error("Store error: {0}")]
581    Store(String),
582    #[error("Codec error: {0}")]
583    Codec(#[from] crate::codec::CodecError),
584    #[error("Encryption error: {0}")]
585    Encryption(String),
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use crate::store::MemoryStore;
592    use crate::types::to_hex;
593    use std::collections::HashMap;
594
595    fn make_store() -> Arc<MemoryStore> {
596        Arc::new(MemoryStore::new())
597    }
598
599    #[tokio::test]
600    async fn test_put_blob() {
601        let store = make_store();
602        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
603
604        let data = vec![1u8, 2, 3, 4, 5];
605        let hash = builder.put_blob(&data).await.unwrap();
606
607        assert_eq!(hash.len(), 32);
608        assert!(store.has(&hash).await.unwrap());
609
610        let retrieved = store.get(&hash).await.unwrap();
611        assert_eq!(retrieved, Some(data));
612    }
613
614    #[tokio::test]
615    async fn test_put_blob_correct_hash() {
616        let store = make_store();
617        let builder = TreeBuilder::new(BuilderConfig::new(store));
618
619        let data = vec![1u8, 2, 3];
620        let hash = builder.put_blob(&data).await.unwrap();
621        let expected_hash = sha256(&data);
622
623        assert_eq!(to_hex(&hash), to_hex(&expected_hash));
624    }
625
626    #[tokio::test]
627    async fn test_put_small() {
628        let store = make_store();
629        // Use public() to disable encryption for this test
630        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()).public());
631
632        let data = vec![1u8, 2, 3, 4, 5];
633        let (cid, size) = builder.put(&data).await.unwrap();
634
635        assert_eq!(size, 5);
636        assert!(cid.key.is_none()); // public content
637        let retrieved = store.get(&cid.hash).await.unwrap();
638        assert_eq!(retrieved, Some(data));
639    }
640
641    #[tokio::test]
642    async fn test_put_chunked() {
643        let store = make_store();
644        let config = BuilderConfig::new(store.clone())
645            .with_chunk_size(1024)
646            .public();
647        let builder = TreeBuilder::new(config);
648
649        let mut data = vec![0u8; 1024 * 2 + 100];
650        for (i, byte) in data.iter_mut().enumerate() {
651            *byte = (i % 256) as u8;
652        }
653
654        let (_cid, size) = builder.put(&data).await.unwrap();
655        assert_eq!(size, data.len() as u64);
656
657        // Verify store has multiple items (chunks + tree node)
658        assert!(store.size() > 1);
659    }
660
661    #[tokio::test]
662    async fn test_put_directory() {
663        let store = make_store();
664        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
665
666        let file1 = vec![1u8, 2, 3];
667        let file2 = vec![4u8, 5, 6, 7];
668
669        let hash1 = builder.put_blob(&file1).await.unwrap();
670        let hash2 = builder.put_blob(&file2).await.unwrap();
671
672        let dir_hash = builder
673            .put_directory(vec![
674                DirEntry::new("a.txt", hash1).with_size(file1.len() as u64),
675                DirEntry::new("b.txt", hash2).with_size(file2.len() as u64),
676            ])
677            .await
678            .unwrap();
679
680        assert!(store.has(&dir_hash).await.unwrap());
681    }
682
683    #[tokio::test]
684    async fn test_put_directory_sorted() {
685        let store = make_store();
686        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
687
688        let hash = builder.put_blob(&[1u8]).await.unwrap();
689
690        let dir_hash = builder
691            .put_directory(vec![
692                DirEntry::new("zebra", hash),
693                DirEntry::new("apple", hash),
694                DirEntry::new("mango", hash),
695            ])
696            .await
697            .unwrap();
698
699        let data = store.get(&dir_hash).await.unwrap().unwrap();
700        let node = crate::codec::decode_tree_node(&data).unwrap();
701
702        let names: Vec<_> = node.links.iter().filter_map(|l| l.name.clone()).collect();
703        assert_eq!(names, vec!["apple", "mango", "zebra"]);
704    }
705
706    #[tokio::test]
707    async fn test_put_tree_node_with_link_meta() {
708        let store = make_store();
709        let builder = TreeBuilder::new(BuilderConfig::new(store.clone()));
710
711        let hash = builder.put_blob(&[1u8]).await.unwrap();
712
713        let mut meta = HashMap::new();
714        meta.insert("version".to_string(), serde_json::json!(2));
715        meta.insert("created".to_string(), serde_json::json!("2024-01-01"));
716
717        let node_hash = builder
718            .put_tree_node(vec![Link {
719                hash,
720                name: Some("test".to_string()),
721                size: 1,
722                key: None,
723                link_type: LinkType::Blob,
724                meta: Some(meta.clone()),
725            }])
726            .await
727            .unwrap();
728
729        let data = store.get(&node_hash).await.unwrap().unwrap();
730        let node = crate::codec::decode_tree_node(&data).unwrap();
731
732        assert!(node.links[0].meta.is_some());
733        let m = node.links[0].meta.as_ref().unwrap();
734        assert_eq!(m.get("version"), Some(&serde_json::json!(2)));
735    }
736
737    #[tokio::test]
738    async fn test_stream_builder() {
739        let store = make_store();
740        let config = BuilderConfig::new(store.clone()).with_chunk_size(100);
741        let mut stream = StreamBuilder::new(config);
742
743        stream.append(&[1u8, 2, 3]).await.unwrap();
744        stream.append(&[4u8, 5]).await.unwrap();
745        stream.append(&[6u8, 7, 8, 9]).await.unwrap();
746
747        let (hash, size) = stream.finalize().await.unwrap();
748
749        assert_eq!(size, 9);
750        assert!(store.has(&hash).await.unwrap());
751    }
752
753    #[tokio::test]
754    async fn test_stream_stats() {
755        let store = make_store();
756        let config = BuilderConfig::new(store).with_chunk_size(100);
757        let mut stream = StreamBuilder::new(config);
758
759        assert_eq!(stream.stats().chunks, 0);
760        assert_eq!(stream.stats().buffered, 0);
761        assert_eq!(stream.stats().total_size, 0);
762
763        stream.append(&[0u8; 50]).await.unwrap();
764        assert_eq!(stream.stats().buffered, 50);
765        assert_eq!(stream.stats().total_size, 50);
766
767        stream.append(&[0u8; 60]).await.unwrap(); // Crosses boundary
768        assert_eq!(stream.stats().chunks, 1);
769        assert_eq!(stream.stats().buffered, 10);
770        assert_eq!(stream.stats().total_size, 110);
771    }
772
773    #[tokio::test]
774    async fn test_stream_current_root() {
775        let store = make_store();
776        let config = BuilderConfig::new(store).with_chunk_size(100);
777        let mut stream = StreamBuilder::new(config);
778
779        stream.append(&[1u8, 2, 3]).await.unwrap();
780        let root1 = stream.current_root().await.unwrap();
781
782        stream.append(&[4u8, 5, 6]).await.unwrap();
783        let root2 = stream.current_root().await.unwrap();
784
785        // Roots should be different
786        assert_ne!(to_hex(&root1.unwrap()), to_hex(&root2.unwrap()));
787    }
788
789    #[tokio::test]
790    async fn test_stream_empty() {
791        let store = make_store();
792        let config = BuilderConfig::new(store.clone());
793        let stream = StreamBuilder::new(config);
794
795        let (hash, size) = stream.finalize().await.unwrap();
796        assert_eq!(size, 0);
797        assert!(store.has(&hash).await.unwrap());
798    }
799
800    #[tokio::test]
801    async fn test_unified_put_public() {
802        let store = make_store();
803        // Use .public() to disable encryption
804        let config = BuilderConfig::new(store.clone()).public();
805        let builder = TreeBuilder::new(config);
806
807        let data = b"Hello, World!";
808        let (cid, size) = builder.put(data).await.unwrap();
809
810        assert_eq!(size, data.len() as u64);
811        assert!(cid.key.is_none()); // No encryption key for public content
812        assert!(store.has(&cid.hash).await.unwrap());
813    }
814
815    #[tokio::test]
816    async fn test_unified_put_encrypted() {
817        use crate::reader::TreeReader;
818
819        let store = make_store();
820        // Default config has encryption enabled
821        let config = BuilderConfig::new(store.clone());
822        let builder = TreeBuilder::new(config);
823
824        let data = b"Hello, encrypted world!";
825        let (cid, size) = builder.put(data).await.unwrap();
826
827        assert_eq!(size, data.len() as u64);
828        assert!(cid.key.is_some()); // Has encryption key
829
830        // Verify we can read it back
831        let reader = TreeReader::new(store);
832        let retrieved = reader.get(&cid).await.unwrap().unwrap();
833        assert_eq!(retrieved, data);
834    }
835
836    #[tokio::test]
837    async fn test_unified_put_encrypted_chunked() {
838        use crate::reader::TreeReader;
839
840        let store = make_store();
841        let config = BuilderConfig::new(store.clone()).with_chunk_size(100);
842        let builder = TreeBuilder::new(config);
843
844        // Data larger than chunk size
845        let data: Vec<u8> = (0..500).map(|i| (i % 256) as u8).collect();
846        let (cid, size) = builder.put(&data).await.unwrap();
847
848        assert_eq!(size, data.len() as u64);
849        assert!(cid.key.is_some());
850
851        // Verify roundtrip
852        let reader = TreeReader::new(store);
853        let retrieved = reader.get(&cid).await.unwrap().unwrap();
854        assert_eq!(retrieved, data);
855    }
856
857    #[tokio::test]
858    async fn test_cid_deterministic() {
859        let store = make_store();
860        let config = BuilderConfig::new(store.clone());
861        let builder = TreeBuilder::new(config);
862
863        let data = b"Same content produces same CID";
864
865        let (cid1, _) = builder.put(data).await.unwrap();
866        let (cid2, _) = builder.put(data).await.unwrap();
867
868        // CHK: same content = same hash AND same key
869        assert_eq!(cid1.hash, cid2.hash);
870        assert_eq!(cid1.key, cid2.key);
871        assert_eq!(cid1.to_string(), cid2.to_string());
872    }
873}