Skip to main content

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}