helia_strings/lib.rs
1//! `helia-strings` makes working with strings in Helia simple & straightforward.
2//!
3//! This crate provides a simple and intuitive interface for adding and retrieving strings
4//! from your Helia node. Strings are stored using the raw codec (0x55) and content-addressed
5//! using SHA-256, making them verifiable and immutable.
6//!
7//! # Overview
8//!
9//! The Strings API is the simplest way to work with IPFS/Helia, providing an easy-to-use
10//! abstraction over content-addressed storage. It's perfect for:
11//!
12//! - Learning IPFS concepts (content addressing, CIDs, immutability)
13//! - Storing simple text data (notes, messages, configuration)
14//! - Building text-based applications
15//! - Prototyping IPFS integrations
16//!
17//! Under the hood, strings are:
18//! 1. Encoded as UTF-8 bytes
19//! 2. Hashed with SHA-256
20//! 3. Wrapped in a CID (v1, raw codec 0x55)
21//! 4. Stored in the blockstore
22//!
23//! # Quick Start
24//!
25//! ```no_run
26//! use rust_helia::create_helia_default;
27//! use helia_strings::{strings, StringsInterface};
28//!
29//! #[tokio::main]
30//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! // Create a Helia node
32//! let helia = std::sync::Arc::new(create_helia_default().await?);
33//!
34//! // Create strings interface
35//! let str_interface = strings(helia);
36//!
37//! // Add a string and get its CID
38//! let cid = str_interface.add("hello world", None).await?;
39//! println!("Stored with CID: {}", cid);
40//!
41//! // Retrieve the string using its CID
42//! let retrieved = str_interface.get(cid, None).await?;
43//! println!("Retrieved: {}", retrieved); // "hello world"
44//!
45//! Ok(())
46//! }
47//! ```
48//!
49//! # Core Concepts
50//!
51//! ## Content Addressing
52//!
53//! Strings are content-addressed, meaning the CID is derived from the content itself:
54//!
55//! ```no_run
56//! # use rust_helia::create_helia_default;
57//! # use helia_strings::{strings, StringsInterface};
58//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
59//! # let helia = std::sync::Arc::new(create_helia_default().await?);
60//! let str_interface = strings(helia);
61//!
62//! // Same content always produces same CID
63//! let cid1 = str_interface.add("hello", None).await?;
64//! let cid2 = str_interface.add("hello", None).await?;
65//! assert_eq!(cid1, cid2); // CIDs are identical
66//!
67//! // Different content produces different CID
68//! let cid3 = str_interface.add("world", None).await?;
69//! assert_ne!(cid1, cid3); // CIDs are different
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! ## Immutability
75//!
76//! Once stored, strings cannot be modified. To "update" a string, you add a new version:
77//!
78//! ```no_run
79//! # use rust_helia::create_helia_default;
80//! # use helia_strings::{strings, StringsInterface};
81//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
82//! # let helia = std::sync::Arc::new(create_helia_default().await?);
83//! let str_interface = strings(helia);
84//!
85//! // Store original version
86//! let v1 = str_interface.add("Version 1", None).await?;
87//!
88//! // "Update" by storing new version (original is unchanged)
89//! let v2 = str_interface.add("Version 2", None).await?;
90//!
91//! // Both versions remain accessible
92//! assert_eq!(str_interface.get(v1, None).await?, "Version 1");
93//! assert_eq!(str_interface.get(v2, None).await?, "Version 2");
94//! # Ok(())
95//! # }
96//! ```
97//!
98//! ## Codec Compatibility
99//!
100//! The Strings API accepts CIDs with these codecs:
101//!
102//! - **0x55 (raw)**: Primary codec for strings
103//! - **0x0129 (json)**: JSON-encoded strings
104//! - **0x0200 (dag-json)**: DAG-JSON strings
105//!
106//! This allows interoperability with other IPFS tools and libraries.
107//!
108//! # Usage Patterns
109//!
110//! ## Working with Unicode
111//!
112//! Full Unicode support including emojis and multi-byte characters:
113//!
114//! ```no_run
115//! # use rust_helia::create_helia_default;
116//! # use helia_strings::{strings, StringsInterface};
117//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
118//! # let helia = std::sync::Arc::new(create_helia_default().await?);
119//! let str_interface = strings(helia);
120//!
121//! // Unicode strings work seamlessly
122//! let text = "Hello, δΈη! π";
123//! let cid = str_interface.add(text, None).await?;
124//! let retrieved = str_interface.get(cid, None).await?;
125//! assert_eq!(retrieved, text);
126//! # Ok(())
127//! # }
128//! ```
129//!
130//! ## Multiline Content
131//!
132//! Strings can contain newlines and formatting:
133//!
134//! ```no_run
135//! # use rust_helia::create_helia_default;
136//! # use helia_strings::{strings, StringsInterface};
137//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
138//! # let helia = std::sync::Arc::new(create_helia_default().await?);
139//! let str_interface = strings(helia);
140//!
141//! let document = "\
142//! # Title
143//!
144//! Paragraph 1
145//!
146//! Paragraph 2
147//! ";
148//!
149//! let cid = str_interface.add(document, None).await?;
150//! let retrieved = str_interface.get(cid, None).await?;
151//! assert_eq!(retrieved, document);
152//! # Ok(())
153//! # }
154//! ```
155//!
156//! ## Error Handling
157//!
158//! Handle common error scenarios gracefully:
159//!
160//! ```no_run
161//! # use rust_helia::create_helia_default;
162//! # use helia_strings::{strings, StringsInterface, StringsError};
163//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
164//! # let helia = std::sync::Arc::new(create_helia_default().await?);
165//! let str_interface = strings(helia);
166//!
167//! // Parse CID from string
168//! let cid_str = "bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy";
169//! let cid: cid::Cid = cid_str.parse()?;
170//!
171//! // Handle retrieval errors
172//! match str_interface.get(cid, None).await {
173//! Ok(content) => {
174//! println!("Retrieved: {}", content);
175//! }
176//! Err(StringsError::InvalidCodec(msg)) => {
177//! eprintln!("Wrong codec: {}", msg);
178//! }
179//! Err(StringsError::Blockstore(msg)) => {
180//! eprintln!("Block not found: {}", msg);
181//! }
182//! Err(StringsError::Utf8(e)) => {
183//! eprintln!("Invalid UTF-8: {}", e);
184//! }
185//! }
186//! # Ok(())
187//! # }
188//! ```
189//!
190//! ## Building a Simple Key-Value Store
191//!
192//! Use strings to build a simple in-memory key-value store:
193//!
194//! ```no_run
195//! # use rust_helia::create_helia_default;
196//! # use helia_strings::{strings, StringsInterface};
197//! # use std::collections::HashMap;
198//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
199//! # let helia = std::sync::Arc::new(create_helia_default().await?);
200//! let str_interface = strings(helia);
201//! let mut store: HashMap<String, cid::Cid> = HashMap::new();
202//!
203//! // Store key-value pairs
204//! let cid = str_interface.add("value for key1", None).await?;
205//! store.insert("key1".to_string(), cid);
206//!
207//! // Retrieve by key
208//! if let Some(cid) = store.get("key1") {
209//! let value = str_interface.get(*cid, None).await?;
210//! println!("key1 = {}", value);
211//! }
212//! # Ok(())
213//! # }
214//! ```
215//!
216//! # Performance Considerations
217//!
218//! ## Memory Usage
219//!
220//! - **Storage**: Each string is stored once in the blockstore
221//! - **Deduplication**: Identical strings share the same CID and block
222//! - **Overhead**: CID overhead is ~36-38 bytes per string
223//!
224//! ## Speed
225//!
226//! - **Add operation**: O(n) where n is string length (hashing + storage)
227//! - **Get operation**: O(1) lookup + O(n) UTF-8 validation
228//! - **Typical latency**: Microseconds for small strings, milliseconds for large ones
229//!
230//! ## Best Practices
231//!
232//! - **Small to medium strings**: Ideal for <1MB strings
233//! - **Large strings**: Consider chunking or using UnixFS for >10MB
234//! - **Batch operations**: Add multiple strings in parallel with `tokio::join!`
235//! - **Caching**: Store frequently-accessed CIDs to avoid repeated lookups
236//!
237//! # Comparison with Other Formats
238//!
239//! | Feature | Strings | JSON | DAG-CBOR | UnixFS |
240//! |---------|---------|------|----------|--------|
241//! | **Simplicity** | β
Easiest | π‘ Simple | π‘ Moderate | π΄ Complex |
242//! | **Use Case** | Plain text | Structured data | Binary data | Files/dirs |
243//! | **Overhead** | Minimal | Low | Low | Higher |
244//! | **Best For** | Learning, text | Config, API | Efficient data | Large files |
245//!
246//! # Thread Safety
247//!
248//! The `StringsInterface` trait requires `Send + Sync`, making it safe to use across
249//! async tasks:
250//!
251//! ```no_run
252//! # use rust_helia::create_helia_default;
253//! # use helia_strings::{strings, StringsInterface};
254//! # use std::sync::Arc;
255//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
256//! # let helia = std::sync::Arc::new(create_helia_default().await?);
257//! let str_interface = Arc::new(strings(helia));
258//!
259//! // Use across multiple tasks
260//! let str1 = str_interface.clone();
261//! let task1 = tokio::spawn(async move {
262//! str1.add("task 1 data", None).await
263//! });
264//!
265//! let str2 = str_interface.clone();
266//! let task2 = tokio::spawn(async move {
267//! str2.add("task 2 data", None).await
268//! });
269//!
270//! let (cid1, cid2) = tokio::try_join!(task1, task2)?;
271//! # Ok(())
272//! # }
273//! ```
274//!
275//! # Examples
276//!
277//! See the `examples/` directory for complete working examples:
278//!
279//! - `basic_strings.rs` - Simple add/get operations
280//! - `unicode_strings.rs` - Working with Unicode text
281//! - `string_store.rs` - Building a key-value store
282//! - `batch_operations.rs` - Parallel string operations
283//!
284//! # Integration with IPFS Ecosystem
285//!
286//! Strings stored with helia-strings are compatible with:
287//!
288//! - **JavaScript Helia**: `@helia/strings` package
289//! - **IPFS CLI**: `ipfs block get <CID>` (raw bytes)
290//! - **HTTP Gateways**: `https://ipfs.io/ipfs/<CID>` (displays as text)
291//! - **Other IPFS implementations**: Kubo, Iroh, etc.
292//!
293//! # Limitations
294//!
295//! - **Size limit**: Depends on blockstore (typically <10MB recommended)
296//! - **No encryption**: Strings are stored as plaintext
297//! - **No compression**: Raw UTF-8 encoding (consider compressing large text)
298//! - **Immutable**: Cannot modify existing strings (must add new versions)
299//!
300//! # See Also
301//!
302//! - [`helia-json`](../helia_json) - For JSON data structures
303//! - [`helia-dag-json`](../helia_dag_json) - For linked JSON (DAG)
304//! - [`helia-unixfs`](../helia_unixfs) - For files and directories
305//! - [`helia-interface`](../helia_interface) - Core Helia traits
306
307use async_trait::async_trait;
308use bytes::Bytes;
309use cid::Cid;
310use helia_interface::Helia;
311use sha2::{Digest, Sha256};
312use std::sync::Arc;
313
314/// Error types for string operations
315#[derive(Debug, thiserror::Error)]
316pub enum StringsError {
317 #[error("Invalid codec error: {0}")]
318 InvalidCodec(String),
319 #[error("Blockstore error: {0}")]
320 Blockstore(String),
321 #[error("UTF-8 conversion error: {0}")]
322 Utf8(#[from] std::string::FromUtf8Error),
323}
324
325/// Options for adding strings
326#[derive(Default)]
327pub struct AddOptions {
328 // Future options can be added here
329}
330
331/// Options for getting strings
332#[derive(Default)]
333pub struct GetOptions {
334 // Future options can be added here
335}
336
337/// The Strings interface provides a simple and intuitive way to add/get strings
338/// with your Helia node and is a great place to start learning about IPFS.
339#[async_trait]
340pub trait StringsInterface: Send + Sync {
341 /// Add a string to your Helia node and get a CID that refers to the block the
342 /// string has been stored as.
343 async fn add(&self, string: &str, options: Option<AddOptions>) -> Result<Cid, StringsError>;
344
345 /// Get a string from your Helia node, either previously added to it or to
346 /// another node on the network.
347 async fn get(&self, cid: Cid, options: Option<GetOptions>) -> Result<String, StringsError>;
348}
349
350/// Default implementation of the Strings interface
351pub struct DefaultStrings {
352 helia: Arc<dyn Helia>,
353}
354
355impl DefaultStrings {
356 pub fn new(helia: Arc<dyn Helia>) -> Self {
357 Self { helia }
358 }
359}
360
361#[async_trait]
362impl StringsInterface for DefaultStrings {
363 async fn add(&self, string: &str, options: Option<AddOptions>) -> Result<Cid, StringsError> {
364 let _options = options.unwrap_or_default();
365 let data = string.as_bytes();
366
367 // Use SHA-256 hasher (matching JavaScript implementation)
368 let mut sha256 = Sha256::new();
369 sha256.update(data);
370 let hash_bytes = sha256.finalize();
371
372 // Create multihash with SHA-256 (0x12)
373 let mh: multihash::Multihash<64> = multihash::Multihash::wrap(0x12, &hash_bytes)
374 .map_err(|e| StringsError::Blockstore(format!("Multihash error: {}", e)))?;
375
376 // Create CID v1 with raw codec (0x55)
377 let cid = Cid::new_v1(0x55, mh);
378
379 // Store the raw bytes as Bytes
380 let bytes = Bytes::from(data.to_vec());
381 self.helia
382 .blockstore()
383 .put(&cid, bytes, None)
384 .await
385 .map_err(|e| StringsError::Blockstore(format!("Failed to store block: {}", e)))?;
386
387 Ok(cid)
388 }
389
390 async fn get(&self, cid: Cid, _options: Option<GetOptions>) -> Result<String, StringsError> {
391 // Check codec - allow raw (0x55), JSON (0x0129), and DAG-JSON (0x0200)
392 // This matches the JavaScript implementation behavior
393 match cid.codec() {
394 0x55 | 0x0129 | 0x0200 => {
395 // Valid codecs
396 }
397 _ => {
398 return Err(StringsError::InvalidCodec(
399 "The passed CID had an incorrect codec, it may correspond to a block data that cannot be interpreted as a string".to_string()
400 ));
401 }
402 }
403
404 let data = self
405 .helia
406 .blockstore()
407 .get(&cid, None)
408 .await
409 .map_err(|e| StringsError::Blockstore(format!("Failed to get block: {}", e)))?;
410
411 String::from_utf8(data.to_vec()).map_err(StringsError::Utf8)
412 }
413}
414
415/// Create a StringsInterface instance for use with Helia
416///
417/// # Example
418///
419/// ```no_run
420/// use rust_helia::create_helia_default;
421/// use helia_strings::{strings, StringsInterface};
422///
423/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
424/// let helia = std::sync::Arc::new(create_helia_default().await?);
425/// let str_interface = strings(helia);
426/// let cid = str_interface.add("hello world", None).await?;
427/// # Ok(())
428/// # }
429/// ```
430pub fn strings(helia: Arc<dyn Helia>) -> impl StringsInterface {
431 DefaultStrings::new(helia)
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use rust_helia::create_helia_default;
438 use std::sync::Arc;
439
440 async fn create_test_helia() -> Arc<dyn Helia> {
441 Arc::new(
442 create_helia_default()
443 .await
444 .expect("Failed to create Helia instance"),
445 )
446 }
447
448 #[tokio::test]
449 async fn test_add_string() {
450 let helia = create_test_helia().await;
451 let str_interface = strings(helia);
452
453 let cid = str_interface.add("hello world", None).await.unwrap();
454
455 // Verify CID format
456 assert_eq!(cid.codec(), 0x55); // raw codec
457 }
458
459 #[tokio::test]
460 async fn test_get_string() {
461 let helia = create_test_helia().await;
462 let str_interface = strings(helia);
463
464 let original = "hello world";
465 let cid = str_interface.add(original, None).await.unwrap();
466 let retrieved = str_interface.get(cid, None).await.unwrap();
467
468 assert_eq!(retrieved, original);
469 }
470
471 #[tokio::test]
472 async fn test_add_get_empty_string() {
473 let helia = create_test_helia().await;
474 let str_interface = strings(helia);
475
476 let original = "";
477 let cid = str_interface.add(original, None).await.unwrap();
478 let retrieved = str_interface.get(cid, None).await.unwrap();
479
480 assert_eq!(retrieved, original);
481 }
482
483 #[tokio::test]
484 async fn test_add_get_unicode_string() {
485 let helia = create_test_helia().await;
486 let str_interface = strings(helia);
487
488 let original = "Hello, δΈη! π";
489 let cid = str_interface.add(original, None).await.unwrap();
490 let retrieved = str_interface.get(cid, None).await.unwrap();
491
492 assert_eq!(retrieved, original);
493 }
494
495 #[tokio::test]
496 async fn test_add_get_multiline_string() {
497 let helia = create_test_helia().await;
498 let str_interface = strings(helia);
499
500 let original = "Line 1\nLine 2\nLine 3";
501 let cid = str_interface.add(original, None).await.unwrap();
502 let retrieved = str_interface.get(cid, None).await.unwrap();
503
504 assert_eq!(retrieved, original);
505 }
506
507 #[tokio::test]
508 async fn test_deterministic_cids() {
509 let helia1 = create_test_helia().await;
510 let helia2 = create_test_helia().await;
511 let str1 = strings(helia1);
512 let str2 = strings(helia2);
513
514 let content = "deterministic content";
515 let cid1 = str1.add(content, None).await.unwrap();
516 let cid2 = str2.add(content, None).await.unwrap();
517
518 assert_eq!(cid1, cid2, "Same content should produce same CID");
519 }
520
521 #[tokio::test]
522 async fn test_invalid_codec_error() {
523 let helia = create_test_helia().await;
524 let str_interface = strings(helia);
525
526 // Create a CID with an invalid codec for strings (using DAG-CBOR codec)
527 let mh: multihash::Multihash<64> = multihash::Multihash::wrap(0x12, &[0u8; 32]).unwrap();
528 let invalid_cid = Cid::new_v1(0x71, mh);
529
530 let result = str_interface.get(invalid_cid, None).await;
531 assert!(result.is_err());
532
533 if let Err(StringsError::InvalidCodec(msg)) = result {
534 assert!(msg.contains("incorrect codec"));
535 } else {
536 panic!("Expected InvalidCodec error");
537 }
538 }
539
540 #[tokio::test]
541 async fn test_get_nonexistent_cid() {
542 let helia = create_test_helia().await;
543 let str_interface = strings(helia);
544
545 // Create a valid raw CID that doesn't exist in the blockstore
546 let mh: multihash::Multihash<64> = multihash::Multihash::wrap(0x12, &[1u8; 32]).unwrap();
547 let nonexistent_cid = Cid::new_v1(0x55, mh);
548
549 let result = str_interface.get(nonexistent_cid, None).await;
550 assert!(result.is_err());
551
552 if let Err(StringsError::Blockstore(_)) = result {
553 // Expected error type
554 } else {
555 panic!("Expected Blockstore error");
556 }
557 }
558
559 #[tokio::test]
560 async fn test_empty_string() {
561 let helia = create_test_helia().await;
562 let str_interface = strings(helia);
563
564 let empty = "";
565 let cid = str_interface.add(empty, None).await.unwrap();
566 let retrieved = str_interface.get(cid, None).await.unwrap();
567
568 assert_eq!(retrieved, empty);
569 assert_eq!(retrieved.len(), 0);
570 }
571
572 #[tokio::test]
573 async fn test_very_long_string() {
574 let helia = create_test_helia().await;
575 let str_interface = strings(helia);
576
577 // Create a 10KB string
578 let long_string = "a".repeat(10_000);
579 let cid = str_interface.add(&long_string, None).await.unwrap();
580 let retrieved = str_interface.get(cid, None).await.unwrap();
581
582 assert_eq!(retrieved, long_string);
583 assert_eq!(retrieved.len(), 10_000);
584 }
585
586 #[tokio::test]
587 async fn test_special_characters() {
588 let helia = create_test_helia().await;
589 let str_interface = strings(helia);
590
591 let special = "!@#$%^&*()_+-=[]{}|;:',.<>?/\\`~\"";
592 let cid = str_interface.add(special, None).await.unwrap();
593 let retrieved = str_interface.get(cid, None).await.unwrap();
594
595 assert_eq!(retrieved, special);
596 }
597
598 #[tokio::test]
599 async fn test_whitespace_only() {
600 let helia = create_test_helia().await;
601 let str_interface = strings(helia);
602
603 let whitespace = " \t\n\r ";
604 let cid = str_interface.add(whitespace, None).await.unwrap();
605 let retrieved = str_interface.get(cid, None).await.unwrap();
606
607 assert_eq!(retrieved, whitespace);
608 }
609
610 #[tokio::test]
611 async fn test_json_string_roundtrip() {
612 let helia = create_test_helia().await;
613 let str_interface = strings(helia);
614
615 let json_str = r#"{"name":"Alice","age":30,"active":true}"#;
616 let cid = str_interface.add(json_str, None).await.unwrap();
617 let retrieved = str_interface.get(cid, None).await.unwrap();
618
619 assert_eq!(retrieved, json_str);
620 }
621
622 #[tokio::test]
623 async fn test_multiple_emojis() {
624 let helia = create_test_helia().await;
625 let str_interface = strings(helia);
626
627 let emojis = "ππππππ
π€£πππ";
628 let cid = str_interface.add(emojis, None).await.unwrap();
629 let retrieved = str_interface.get(cid, None).await.unwrap();
630
631 assert_eq!(retrieved, emojis);
632 // Verify emoji count (each emoji is multiple bytes)
633 assert_eq!(retrieved.chars().count(), 10);
634 }
635
636 #[tokio::test]
637 async fn test_cid_string_format() {
638 let helia = create_test_helia().await;
639 let str_interface = strings(helia);
640
641 let text = "test content";
642 let cid = str_interface.add(text, None).await.unwrap();
643
644 // Verify CID properties
645 assert_eq!(cid.version(), cid::Version::V1);
646 assert_eq!(cid.codec(), 0x55); // raw codec
647 assert_eq!(cid.hash().code(), 0x12); // SHA-256
648
649 // CID should be a valid string
650 let cid_string = cid.to_string();
651 assert!(cid_string.starts_with("bafkrei"));
652 }
653
654 #[tokio::test]
655 async fn test_concurrent_adds() {
656 use tokio::task::JoinSet;
657
658 let helia = create_test_helia().await;
659
660 // Add multiple strings concurrently
661 let mut set = JoinSet::new();
662 for i in 0..10 {
663 let helia_clone = Arc::clone(&helia);
664 set.spawn(async move {
665 let str_interface = strings(helia_clone);
666 str_interface
667 .add(&format!("concurrent test {}", i), None)
668 .await
669 });
670 }
671
672 // All should succeed
673 let mut count = 0;
674 while let Some(result) = set.join_next().await {
675 assert!(result.is_ok());
676 assert!(result.unwrap().is_ok());
677 count += 1;
678 }
679 assert_eq!(count, 10);
680 }
681}