Skip to main content

ntex_bytes/
string.rs

1//! Immutable UTF-8 strings backed by [`crate::Bytes`].
2use std::{borrow, fmt, hash, ops, slice, str, sync::Arc};
3
4use crate::{Bytes, BytesMut};
5
6/// An immutable UTF-8 string backed by [`Bytes`].
7#[derive(Clone, Default, Eq, PartialOrd, Ord)]
8pub struct ByteString(Bytes);
9
10impl ByteString {
11    /// Creates a new empty `ByteString`.
12    #[inline]
13    #[must_use]
14    pub const fn new() -> Self {
15        ByteString(Bytes::new())
16    }
17
18    /// Returns this value as a string slice.
19    #[inline]
20    pub fn as_str(&self) -> &str {
21        self
22    }
23
24    /// Returns the underlying byte slice.
25    #[inline]
26    pub fn as_slice(&self) -> &[u8] {
27        self.0.as_ref()
28    }
29
30    /// Returns the underlying [`Bytes`] value.
31    #[inline]
32    pub fn as_bytes(&self) -> &Bytes {
33        &self.0
34    }
35
36    /// Converts this value into its underlying [`Bytes`].
37    #[inline]
38    #[must_use]
39    pub fn into_bytes(self) -> Bytes {
40        self.0
41    }
42
43    /// Creates a new `ByteString` from a `&'static str`.
44    #[inline]
45    #[must_use]
46    pub const fn from_static(src: &'static str) -> ByteString {
47        Self(Bytes::from_static(src.as_bytes()))
48    }
49
50    /// Returns a slice of self for the provided range.
51    ///
52    /// This will increment the reference count for the underlying memory and
53    /// return a new `ByteString` handle set to the slice.
54    ///
55    /// This operation is `O(1)`.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use ntex_bytes::ByteString;
61    ///
62    /// let a = ByteString::from("hello world");
63    /// let b = a.slice(2..5);
64    ///
65    /// assert_eq!(b, "llo");
66    /// ```
67    ///
68    /// # Panics
69    ///
70    /// Requires that `begin <= end`, `end <= self.len()` and that both bounds
71    /// lie on UTF-8 char boundaries, otherwise slicing will panic.
72    #[must_use]
73    pub fn slice(
74        &self,
75        range: impl ops::RangeBounds<usize> + slice::SliceIndex<str> + Clone,
76    ) -> ByteString {
77        ops::Index::index(self.as_ref(), range.clone());
78        ByteString(self.0.slice(range))
79    }
80
81    /// Splits the string into two at the given byte index.
82    ///
83    /// Afterwards `self` contains elements `[0, at)`, and the returned `ByteString`
84    /// contains elements `[at, len)`.
85    ///
86    /// This is an `O(1)` operation that just increases the reference count and
87    /// sets a few indices.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use ntex_bytes::ByteString;
93    ///
94    /// let mut a = ByteString::from("hello world");
95    /// let b = a.split_off(5);
96    ///
97    /// assert_eq!(a, "hello");
98    /// assert_eq!(b, " world");
99    /// ```
100    ///
101    /// # Panics
102    ///
103    /// Panics if `at > len` or if `at` does not lie on a UTF-8 char boundary.
104    #[must_use]
105    pub fn split_off(&mut self, at: usize) -> ByteString {
106        // panics unless `at` is on a char boundary
107        let _ = self.split_at(at);
108
109        ByteString(self.0.split_off(at))
110    }
111
112    /// Splits the string into two at the given byte index.
113    ///
114    /// Afterwards `self` contains elements `[at, len)`, and the returned
115    /// `ByteString` contains elements `[0, at)`.
116    ///
117    /// This is an `O(1)` operation that just increases the reference count and
118    /// sets a few indices.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use ntex_bytes::ByteString;
124    ///
125    /// let mut a = ByteString::from("hello world");
126    /// let b = a.split_to(5);
127    ///
128    /// assert_eq!(a, " world");
129    /// assert_eq!(b, "hello");
130    /// ```
131    ///
132    /// # Panics
133    ///
134    /// Panics if `at > len` or if `at` does not lie on a UTF-8 char boundary.
135    #[must_use]
136    pub fn split_to(&mut self, at: usize) -> ByteString {
137        // panics unless `at` is on a char boundary
138        let _ = self.split_at(at);
139
140        ByteString(self.0.split_to(at))
141    }
142
143    /// Compacts the underlying storage to this string's current byte range.
144    ///
145    /// The string contents are unchanged.
146    #[inline]
147    pub fn trimdown(&mut self) {
148        self.0.trimdown();
149    }
150
151    /// Clears the buffer, removing all data.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use ntex_bytes::ByteString;
157    ///
158    /// let mut a = ByteString::from("hello world");
159    /// a.clear();
160    ///
161    /// assert!(a.is_empty());
162    /// ```
163    #[inline]
164    pub fn clear(&mut self) {
165        self.0.clear();
166    }
167
168    /// Creates a `ByteString` from bytes without validating UTF-8.
169    ///
170    /// # Safety
171    ///
172    /// `src` must contain valid UTF-8. Violating this invariant can cause
173    /// undefined behavior because safe methods assume that every `ByteString`
174    /// contains valid UTF-8.
175    #[inline]
176    pub const unsafe fn from_bytes_unchecked(src: Bytes) -> ByteString {
177        Self(src)
178    }
179}
180
181impl PartialEq<str> for ByteString {
182    fn eq(&self, other: &str) -> bool {
183        &self[..] == other
184    }
185}
186
187impl<T: AsRef<str>> PartialEq<T> for ByteString {
188    fn eq(&self, other: &T) -> bool {
189        &self[..] == other.as_ref()
190    }
191}
192
193impl PartialEq<ByteString> for &ByteString {
194    fn eq(&self, other: &ByteString) -> bool {
195        self.as_str() == other.as_str()
196    }
197}
198
199impl PartialEq<ByteString> for str {
200    fn eq(&self, other: &ByteString) -> bool {
201        self == other.as_str()
202    }
203}
204
205impl PartialEq<ByteString> for &str {
206    fn eq(&self, other: &ByteString) -> bool {
207        *self == other.as_str()
208    }
209}
210
211impl AsRef<str> for ByteString {
212    #[inline]
213    fn as_ref(&self) -> &str {
214        self
215    }
216}
217
218impl hash::Hash for ByteString {
219    fn hash<H: hash::Hasher>(&self, state: &mut H) {
220        (**self).hash(state);
221    }
222}
223
224impl ops::Deref for ByteString {
225    type Target = str;
226
227    #[inline]
228    fn deref(&self) -> &str {
229        let bytes = self.0.as_ref();
230        // SAFETY:
231        // UTF-8 validity is guaranteed during construction.
232        unsafe { str::from_utf8_unchecked(bytes) }
233    }
234}
235
236impl borrow::Borrow<str> for ByteString {
237    #[inline]
238    fn borrow(&self) -> &str {
239        self
240    }
241}
242
243impl From<String> for ByteString {
244    #[inline]
245    fn from(value: String) -> Self {
246        Self(Bytes::from(value))
247    }
248}
249
250impl From<&str> for ByteString {
251    #[inline]
252    fn from(value: &str) -> Self {
253        Self(Bytes::copy_from_slice(value.as_ref()))
254    }
255}
256
257impl From<&ByteString> for ByteString {
258    #[inline]
259    fn from(value: &ByteString) -> Self {
260        value.clone()
261    }
262}
263
264impl<'a> From<borrow::Cow<'a, str>> for ByteString {
265    #[inline]
266    fn from(value: borrow::Cow<'a, str>) -> Self {
267        match value {
268            borrow::Cow::Owned(s) => Self::from(s),
269            borrow::Cow::Borrowed(s) => Self::from(s),
270        }
271    }
272}
273
274impl From<Arc<str>> for ByteString {
275    #[inline]
276    fn from(value: Arc<str>) -> Self {
277        ByteString::from_ext(value)
278    }
279}
280
281impl TryFrom<&[u8]> for ByteString {
282    type Error = ();
283
284    #[inline]
285    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
286        if utf8::is_valid(value) {
287            Ok(ByteString(Bytes::copy_from_slice(value)))
288        } else {
289            Err(())
290        }
291    }
292}
293
294impl TryFrom<Vec<u8>> for ByteString {
295    type Error = ();
296
297    #[inline]
298    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
299        if utf8::is_valid(&value) {
300            Ok(ByteString(Bytes::from(value)))
301        } else {
302            Err(())
303        }
304    }
305}
306
307impl TryFrom<Bytes> for ByteString {
308    type Error = ();
309
310    #[inline]
311    fn try_from(value: Bytes) -> Result<Self, Self::Error> {
312        if utf8::is_valid(&value) {
313            Ok(ByteString(value))
314        } else {
315            Err(())
316        }
317    }
318}
319
320impl TryFrom<&Bytes> for ByteString {
321    type Error = ();
322
323    #[inline]
324    fn try_from(value: &Bytes) -> Result<Self, Self::Error> {
325        if utf8::is_valid(value) {
326            Ok(ByteString(value.clone()))
327        } else {
328            Err(())
329        }
330    }
331}
332
333impl TryFrom<BytesMut> for ByteString {
334    type Error = ();
335
336    #[inline]
337    fn try_from(value: BytesMut) -> Result<Self, Self::Error> {
338        if utf8::is_valid(&value) {
339            Ok(ByteString(value.freeze()))
340        } else {
341            Err(())
342        }
343    }
344}
345
346impl fmt::Debug for ByteString {
347    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
348        (**self).fmt(fmt)
349    }
350}
351
352impl fmt::Display for ByteString {
353    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
354        (**self).fmt(fmt)
355    }
356}
357
358mod serde {
359    use serde::de::{Deserialize, Deserializer};
360    use serde::ser::{Serialize, Serializer};
361
362    use super::ByteString;
363
364    impl Serialize for ByteString {
365        #[inline]
366        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
367        where
368            S: Serializer,
369        {
370            serializer.serialize_str(self.as_ref())
371        }
372    }
373
374    impl<'de> Deserialize<'de> for ByteString {
375        #[inline]
376        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
377        where
378            D: Deserializer<'de>,
379        {
380            String::deserialize(deserializer).map(ByteString::from)
381        }
382    }
383}
384
385#[cfg(feature = "simd")]
386mod utf8 {
387    pub(super) fn is_valid(input: &[u8]) -> bool {
388        simdutf8::basic::from_utf8(input).is_ok()
389    }
390}
391
392#[cfg(not(feature = "simd"))]
393mod utf8 {
394    pub(super) fn is_valid(input: &[u8]) -> bool {
395        std::str::from_utf8(input).is_ok()
396    }
397}
398
399#[cfg(test)]
400mod test {
401    use std::borrow::{Borrow, Cow};
402    use std::collections::hash_map::DefaultHasher;
403    use std::hash::{Hash, Hasher};
404
405    use super::*;
406
407    /// STATEMENT: `ByteString` exposes string-compatible views, formatting, and ownership conversions.
408    ///
409    /// HOW:
410    /// - Compare static `test` through string, byte, borrowed, display, and debug views and round-trip through `Bytes`.
411    /// - Slice to `te`, convert borrowed and owned `Cow` strings, and clear the result to an empty string.
412    #[test]
413    fn test_basics() {
414        let mut s = ByteString::from_static("test");
415        s.trimdown();
416        assert_eq!(s, "test");
417        assert_eq!(s, *"test");
418        assert_eq!(s, "test".to_owned());
419        assert_eq!(s.as_str(), "test");
420        assert_eq!(s.as_slice(), b"test");
421        assert_eq!(s.as_bytes(), &Bytes::copy_from_slice(b"test"));
422        assert_eq!(Borrow::<str>::borrow(&s), "test");
423        assert_eq!("test", s);
424        assert_eq!("test", &s);
425
426        assert_eq!(format!("{s}"), "test");
427        assert_eq!(format!("{s:?}"), "\"test\"");
428
429        let b = s.into_bytes();
430        assert_eq!(b, Bytes::copy_from_slice(b"test"));
431
432        // SAFETY: The buffer contains the valid UTF-8 literal `test`.
433        let s = unsafe { ByteString::from_bytes_unchecked(b) };
434        assert_eq!(s, "test");
435        assert_eq!(s.slice(0..2), "te");
436
437        let s = ByteString::from(Cow::Borrowed("test"));
438        assert_eq!(s, "test");
439        let mut s = ByteString::from(Cow::Owned("test".to_string()));
440        assert_eq!(s, "test");
441
442        s.clear();
443        assert_eq!(s, "");
444    }
445
446    /// STATEMENT: `ByteString` splitting returns the requested half and retains the complementary half.
447    ///
448    /// HOW:
449    /// - Split static `helloworld` at five with `split_off` and `split_to` and check `hello` and `world` in both roles.
450    #[test]
451    fn test_split() {
452        let mut s = ByteString::from_static("helloworld");
453        let s1 = s.split_off(5);
454        assert_eq!(s, "hello");
455        assert_eq!(s1, "world");
456
457        let mut s = ByteString::from_static("helloworld");
458        let s1 = s.split_to(5);
459        assert_eq!(s, "world");
460        assert_eq!(s1, "hello");
461    }
462
463    /// STATEMENT: `ByteString::new` constructs a value of the string wrapper type.
464    ///
465    /// HOW:
466    /// - Assign `ByteString::new()` to an explicitly typed `ByteString` binding without further assertions.
467    #[test]
468    fn test_new() {
469        let _: ByteString = ByteString::new();
470    }
471
472    /// STATEMENT: `ByteString` hashes identically to its corresponding string slice.
473    ///
474    /// HOW:
475    /// - Hash `str` as a slice and static `ByteString` with separate `DefaultHasher` instances and compare their results.
476    #[test]
477    fn test_hash() {
478        let mut hasher1 = DefaultHasher::default();
479        "str".hash(&mut hasher1);
480
481        let mut hasher2 = DefaultHasher::default();
482        let s = ByteString::from_static("str");
483        s.hash(&mut hasher2);
484        assert_eq!(hasher1.finish(), hasher2.finish());
485    }
486
487    /// STATEMENT: Owned, borrowed, and static string inputs support `ByteString` construction.
488    ///
489    /// HOW:
490    /// - Convert an owned `String`, a string slice, and a borrowed `ByteString`, checking the `hello` contents and `AsRef<str>` view.
491    /// - Construct a static `ByteString` initializer and another static-string value.
492    #[test]
493    #[allow(clippy::items_after_statements)]
494    fn test_from() {
495        // String
496        let s: ByteString = "hello".to_owned().into();
497        assert_eq!(&s, "hello");
498        let t: &str = s.as_ref();
499        assert_eq!(t, "hello");
500
501        // str
502        let _: ByteString = "str".into();
503
504        // static str
505        static _S: ByteString = ByteString::from_static("hello");
506        let _ = ByteString::from_static("str");
507
508        let s = ByteString::from_static("hello");
509        let s1 = ByteString::from(&s);
510        assert_eq!(s1, "hello");
511    }
512
513    /// STATEMENT: Fallible `ByteString` conversions accept UTF-8 bytes and reject invalid encodings.
514    ///
515    /// HOW:
516    /// - Convert valid `nice bytes` and invalid byte sequences from slices, vectors, owned and borrowed `Bytes`, and `BytesMut`.
517    /// - Unwrap valid conversions and verify each invalid conversion returns an error.
518    #[test]
519    fn test_try_from() {
520        let _ = ByteString::try_from(&b"nice bytes"[..]).unwrap();
521        assert!(ByteString::try_from(b"\xc3\x28".as_ref()).is_err());
522
523        let _ = ByteString::try_from(b"nice bytes".to_vec()).unwrap();
524        assert!(ByteString::try_from(vec![b'\xc3']).is_err());
525
526        let _ = ByteString::try_from(Bytes::from_static(b"nice bytes")).unwrap();
527        assert!(ByteString::try_from(Bytes::from_static(b"\xc3\x28")).is_err());
528
529        let _ = ByteString::try_from(&Bytes::from_static(b"nice bytes")).unwrap();
530        assert!(ByteString::try_from(&Bytes::from_static(b"\xc3\x28")).is_err());
531
532        let _ = ByteString::try_from(BytesMut::copy_from_slice(&b"nice bytes"[..])).unwrap();
533        assert!(ByteString::try_from(BytesMut::copy_from_slice(b"\xc3\x28")).is_err());
534    }
535
536    /// STATEMENT: A JSON string deserializes to the same `ByteString` text.
537    ///
538    /// HOW:
539    /// - Parse the JSON string `"nice bytes"` with `serde_json::from_str` and compare the result with `nice bytes`.
540    #[test]
541    fn test_serialize() {
542        let s: ByteString = serde_json::from_str(r#""nice bytes""#).unwrap();
543        assert_eq!(s, "nice bytes");
544    }
545
546    /// STATEMENT: `ByteString` serializes as a JSON string rather than a byte array.
547    ///
548    /// HOW:
549    /// - Serialize static `nice bytes` with `serde_json::to_string` and compare with the quoted JSON string.
550    #[test]
551    fn test_deserialize() {
552        let s = serde_json::to_string(&ByteString::from_static("nice bytes")).unwrap();
553        assert_eq!(s, r#""nice bytes""#);
554    }
555
556    /// STATEMENT: Borrowed `ByteString` equality compares text and long `Arc<str>` conversion shares ownership.
557    ///
558    /// HOW:
559    /// - Compare borrowed `hello` with equal and different strings.
560    /// - Convert a cloned long `Arc<str>`, check text and a strong count of two, then drop the wrapper and check one remains.
561    #[test]
562    fn test_ref_eq_and_arc() {
563        let s = ByteString::from_static("hello");
564        assert_eq!(&s, ByteString::from_static("hello"));
565        assert_ne!(&s, ByteString::from_static("world"));
566
567        let arc: std::sync::Arc<str> = "a long string that is not stored inline".into();
568        let s = ByteString::from(arc.clone());
569        assert_eq!(s, &*arc);
570        assert_eq!(std::sync::Arc::strong_count(&arc), 2);
571        drop(s);
572        assert_eq!(std::sync::Arc::strong_count(&arc), 1);
573    }
574}