safe-string

Provides a safe interface for interacting with multi-byte strings in Rust, namely IndexedStr, IndexedString, and IndexedSlice

12 releases

0.1.11 Jun 21, 2024
0.1.10 Apr 4, 2024
Download history 101/week @ 2024-07-14 103/week @ 2024-07-21 145/week @ 2024-07-28 226/week @ 2024-08-04 111/week @ 2024-08-11 336/week @ 2024-08-18 218/week @ 2024-08-25 35/week @ 2024-09-01 39/week @ 2024-09-08 142/week @ 2024-09-15 146/week @ 2024-09-22 136/week @ 2024-09-29 79/week @ 2024-10-06 136/week @ 2024-10-13 143/week @ 2024-10-20 127/week @ 2024-10-27

485 downloads per month
Used in 4 crates (via quoth)

MIT license

22KB
391 lines

safe-string

Crates.io docs.rs Build Status MIT License

This crate provides replacement types for String and &str that allow for safe indexing by character to avoid panics and the usual pitfalls of working with multi-byte UTF-8 characters, namely the scenario where the byte length of a string and the character length of that same string are not the same.

Specifically, IndexedString (replaces String) and IndexedSlice (replaces &str) allow for O(1) slicing and indexing by character, and they will never panic when indexing or slicing.

This is accomplished by storing the character offsets of each character in the string, along with the original String, and using this information to calculate the byte offsets of each character on the fly. Thus IndexedString uses ~2x the memory of a normal String, but IndexedSlice and other types implementing IndexedStr have only one usize extra in overhead over that of a regular &str slice / fat pointer. In theory this could be reduced down to the same size as a fat pointer using unsafe rust, but this way we get to have completely safe code and the difference is negligible.

Examples

use safe_string::{IndexedString, IndexedStr, IndexedSlice};

let message = IndexedString::from("Hello, 世界! 👋😊");
assert_eq!(message.as_str(), "Hello, 世界! 👋😊");
assert_eq!(message, "Hello, 世界! 👋😊"); // handy PartialEq impls

// Access characters by index
assert_eq!(message.char_at(7), Some(''));
assert_eq!(message.char_at(100), None); // Out of bounds access returns None

// Slice the IndexedString
let slice = message.slice(7..9);
assert_eq!(slice.as_str(), "世界");

// Convert slice back to IndexedString
let sliced_message = slice.to_indexed_string();
assert_eq!(sliced_message.as_str(), "世界");

// Nested slicing
let slice = message.slice(0..10);
let nested_slice = slice.slice(3..6);
assert_eq!(nested_slice.as_str(), "lo,");

// Display byte length and character length
assert_eq!(IndexedString::from_str("世界").byte_len(), 6); // "世界" is 6 bytes in UTF-8
assert_eq!(IndexedString::from_str("世界").len(), 2); // "世界" has 2 characters

// Demonstrate clamped slicing (no panic)
let clamped_slice = message.slice(20..30);
assert_eq!(clamped_slice.as_str(), "");

// Using `as_str` to interface with standard Rust string handling
let slice = message.slice(0..5);
let standard_str_slice = slice.as_str();
assert_eq!(standard_str_slice, "Hello");

No runtime deps