1#![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
21pub const DEFAULT_CHUNK_SIZE: usize = 2 * 1024 * 1024;
23
24pub const BEP52_CHUNK_SIZE: usize = 16 * 1024;
26
27pub const DEFAULT_MAX_LINKS: usize = 174;
29
30#[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 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 pub fn public(mut self) -> Self {
66 self.encrypted = false;
67 self
68 }
69
70 pub fn encrypted(mut self) -> Self {
72 self.encrypted = true;
73 self
74 }
75}
76
77#[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 pub fn is_encrypted(&self) -> bool {
101 self.encrypted
102 }
103
104 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 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 pub async fn put(&self, data: &[u8]) -> Result<(Cid, u64), BuilderError> {
142 let size = data.len() as u64;
143
144 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 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, meta: None,
166 });
167 offset = end;
168 }
169
170 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 async fn build_tree_internal(
185 &self,
186 links: Vec<Link>,
187 total_size: Option<u64>,
188 ) -> Result<(Hash, Option<ObjectKey>, LinkType), BuilderError> {
189 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 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 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 pub async fn put_directory(&self, entries: Vec<DirEntry>) -> Result<Hash, BuilderError> {
247 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 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 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 Box::pin(self.build_directory_fanout_level(sub_trees, LinkType::Fanout)).await
360 }
361
362 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#[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 buffer: Vec<u8>,
390
391 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 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 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 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, meta: None,
450 });
451
452 self.buffer = Vec::with_capacity(self.chunk_size);
453 Ok(())
454 }
455
456 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 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, 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 pub async fn finalize(mut self) -> Result<(Hash, u64), BuilderError> {
490 self.flush_chunk().await?;
492
493 if self.chunks.is_empty() {
494 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 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 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, meta: None,
554 });
555 }
556
557 Box::pin(self.build_tree_from_chunks(&sub_trees, total_size)).await
558 }
559
560 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#[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 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()); 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 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(); 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 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 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()); 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 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()); 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 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 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 assert_eq!(cid1.hash, cid2.hash);
870 assert_eq!(cid1.key, cid2.key);
871 assert_eq!(cid1.to_string(), cid2.to_string());
872 }
873}