Repository: ParkMyCar/compact_str
Pull request #57 (2022-02-27 21:44:35)

feature: Add from_utf8 API
This PR adds a `CompactStr::from_utf8(...)` API that is similar to `String::from_utf8(...)`. It also adds property based testing and fuzz testing for the new API


Patch:
diff --git a/compact_str/src/lib.rs b/compact_str/src/lib.rs
index e74eda5e..8b2251f6 100644
--- a/compact_str/src/lib.rs
+++ b/compact_str/src/lib.rs
@@ -22,7 +22,10 @@ use core::hash::{
 };
 use core::iter::FromIterator;
 use core::ops::Deref;
-use core::str::FromStr;
+use core::str::{
+    FromStr,
+    Utf8Error,
+};
 
 mod asserts;
 mod features;
@@ -199,6 +202,39 @@ impl CompactStr {
         }
     }
 
+    /// Convert a slice of bytes into a [`CompactStr`].
+    ///
+    /// A [`CompactStr`] is a contiguous collection of bytes (`u8`s) that is valid [`UTF-8`](https://en.wikipedia.org/wiki/UTF-8).
+    /// This method converts from an arbitrary contiguous collection of bytes into a [`CompactStr`],
+    /// failing if the provided bytes are not `UTF-8`.
+    ///
+    /// Note: If you want to create a [`CompactStr`] from a non-contiguous collection of bytes,
+    /// enable the `bytes` feature of this crate, and checkout [`CompactStr::from_utf8_buf`]
+    ///
+    /// # Examples
+    /// ### Valid UTF-8
+    /// ```
+    /// # use compact_str::CompactStr;
+    /// let bytes = vec![240, 159, 166, 128, 240, 159, 146, 175];
+    /// let compact = CompactStr::from_utf8(bytes).expect("valid UTF-8");
+    ///
+    /// assert_eq!(compact, "🦀💯");
+    /// ```
+    ///
+    /// ### Invalid UTF-8
+    /// ```
+    /// # use compact_str::CompactStr;
+    /// let bytes = vec![255, 255, 255];
+    /// let result = CompactStr::from_utf8(bytes);
+    ///
+    /// assert!(result.is_err());
+    /// ```
+    #[inline]
+    pub fn from_utf8<B: AsRef<[u8]>>(buf: B) -> Result<Self, Utf8Error> {
+        let repr = Repr::from_utf8(buf)?;
+        Ok(CompactStr { repr })
+    }
+
     /// Returns the length of the `CompactStr` in `bytes`, not `chars` or graphemes.
     ///
     /// When using UTF-8 encoding (which all strings in Rust do) a single character will be 1 - 4
diff --git a/compact_str/src/repr/mod.rs b/compact_str/src/repr/mod.rs
index b0284cf1..55df14b2 100644
--- a/compact_str/src/repr/mod.rs
+++ b/compact_str/src/repr/mod.rs
@@ -1,5 +1,6 @@
 use std::iter::Extend;
 use std::mem::ManuallyDrop;
+use std::str::Utf8Error;
 
 #[cfg(feature = "bytes")]
 mod bytes;
@@ -78,6 +79,14 @@ impl Repr {
         }
     }
 
+    #[inline]
+    pub fn from_utf8<B: AsRef<[u8]>>(buf: B) -> Result<Self, Utf8Error> {
+        // Get a &str from the Vec, failing if it's not valid UTF-8
+        let s = core::str::from_utf8(buf.as_ref())?;
+        // Construct a Repr from the &str
+        Ok(Self::new(s))
+    }
+
     #[inline]
     pub fn len(&self) -> usize {
         self.cast().len()
diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs
index 246b0f6d..700b448b 100644
--- a/fuzz/src/lib.rs
+++ b/fuzz/src/lib.rs
@@ -19,10 +19,11 @@ pub struct Scenario<'a> {
 #[derive(Arbitrary, Debug)]
 pub enum Creation<'a> {
     Bytes(&'a [u8]),
+    Buf(&'a [u8]),
     IterChar(Vec<char>),
     IterString(Vec<String>),
     Word(String),
-    NonContiguousBytes(&'a [u8]),
+    NonContiguousBuf(&'a [u8]),
 }
 
 impl Creation<'_> {
@@ -59,8 +60,29 @@ impl Creation<'_> {
 
                 Some((compact, std_str))
             }
-            // Create a `CompactStr` from a buffer of bytes
+            // Create a `CompactStr` from a slice of bytes
             Bytes(data) => {
+                let compact = CompactStr::from_utf8(data);
+                let std_str = std::str::from_utf8(data);
+
+                match (compact, std_str) {
+                    // valid UTF-8
+                    (Ok(c), Ok(s)) => {
+                        assert_eq!(c, s);
+                        assert_properly_allocated(&c, s);
+
+                        Some((c, s.to_string()))
+                    }
+                    // non-valid UTF-8
+                    (Err(c_err), Err(s_err)) => {
+                        assert_eq!(c_err, s_err);
+                        None
+                    }
+                    _ => panic!("CompactStr and core::str read UTF-8 differently?"),
+                }
+            }
+            // Create a `CompactStr` from a buffer of bytes
+            Buf(data) => {
                 let mut buffer = Cursor::new(data);
 
                 let compact = CompactStr::from_utf8_buf(&mut buffer);
@@ -83,7 +105,7 @@ impl Creation<'_> {
                 }
             }
             // Create a `CompactStr` from a non-contiguous buffer of bytes
-            NonContiguousBytes(data) => {
+            NonContiguousBuf(data) => {
                 let mut queue = if data.len() > 3 {
                     // if our data is long, make it non-contiguous
                     let (front, back) = data.split_at(data.len() / 2 + 1);


Test patch:
diff --git a/compact_str/src/tests.rs b/compact_str/src/tests.rs
index 5b353f20..d55d43ca 100644
--- a/compact_str/src/tests.rs
+++ b/compact_str/src/tests.rs
@@ -74,6 +74,29 @@ proptest! {
         prop_assert_eq!(&word, &compact);
     }
 
+    #[test]
+    #[cfg_attr(miri, ignore)]
+    fn test_from_bytes_roundtrips(word in rand_unicode()) {
+        let bytes = word.into_bytes();
+        let compact = CompactStr::from_utf8(&bytes).unwrap();
+        let word = String::from_utf8(bytes).unwrap();
+
+        prop_assert_eq!(compact, word);
+    }
+
+    #[test]
+    #[cfg_attr(miri, ignore)]
+    fn test_from_bytes_only_valid_utf8(bytes in proptest::collection::vec(any::<u8>(), 0..80)) {
+        let compact_result = CompactStr::from_utf8(&bytes);
+        let word_result = String::from_utf8(bytes);
+
+        match (compact_result, word_result) {
+            (Ok(c), Ok(s)) => prop_assert_eq!(c, s),
+            (Err(c_err), Err(s_err)) => prop_assert_eq!(c_err, s_err.utf8_error()),
+            _ => panic!("CompactStr and core::str read UTF-8 differently?"),
+        }
+    }
+
     #[test]
     #[cfg_attr(miri, ignore)]
     fn test_reserve_and_write_bytes(word in rand_unicode()) {