#![warn(missing_docs, clippy::all)]
mod raw;
use ahash::AHashSet;
use bstr::{BStr, BString};
use std::borrow::Borrow;
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::ptr::NonNull;
use std::sync::Mutex;
#[cfg(feature = "serde")]
use serde::de::{Deserializer, Visitor};
#[cfg(feature = "serde")]
use serde::ser::Serializer;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
static CACHE: std::sync::LazyLock<Mutex<AHashSet<Storage>>> =
std::sync::LazyLock::new(|| Mutex::new(AHashSet::new()));
#[repr(transparent)]
struct Storage(NonNull<raw::Payload>);
unsafe impl Send for Storage {}
unsafe impl Sync for Storage {}
impl PartialEq for Storage {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl Eq for Storage {}
impl Hash for Storage {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_bytes().hash(state)
}
}
impl Storage {
#[inline]
fn inc_ref(&self) -> usize {
unsafe { raw::Payload::inc_ref(self.0.as_ptr()) }
}
#[inline]
fn as_bytes(&self) -> &[u8] {
unsafe { &*raw::Payload::bytes(self.0.as_ptr()) }
}
}
impl Borrow<[u8]> for Storage {
#[inline]
fn borrow(&self) -> &[u8] {
self.as_bytes()
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct FlyStr(RawRepr);
#[cfg(feature = "json_schema")]
impl schemars::JsonSchema for FlyStr {
fn schema_name() -> String {
str::schema_name()
}
fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
str::json_schema(generator)
}
fn is_referenceable() -> bool {
false
}
}
static_assertions::assert_eq_size!(FlyStr, usize);
static_assertions::assert_not_impl_any!(FlyStr: Borrow<str>);
impl FlyStr {
#[inline]
pub fn new(s: impl AsRef<str> + Into<String>) -> Self {
Self(RawRepr::new_str(s))
}
#[inline]
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.0.as_bytes()) }
}
}
impl Default for FlyStr {
#[inline]
fn default() -> Self {
Self::new("")
}
}
impl From<&'_ str> for FlyStr {
#[inline]
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<&'_ String> for FlyStr {
#[inline]
fn from(s: &String) -> Self {
Self::new(&**s)
}
}
impl From<String> for FlyStr {
#[inline]
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<Box<str>> for FlyStr {
#[inline]
fn from(s: Box<str>) -> Self {
Self::new(s)
}
}
impl From<&Box<str>> for FlyStr {
#[inline]
fn from(s: &Box<str>) -> Self {
Self::new(&**s)
}
}
impl TryFrom<FlyByteStr> for FlyStr {
type Error = std::str::Utf8Error;
#[inline]
fn try_from(b: FlyByteStr) -> Result<FlyStr, Self::Error> {
std::str::from_utf8(b.as_bytes())?;
Ok(FlyStr(b.0))
}
}
impl From<FlyStr> for String {
#[inline]
fn from(s: FlyStr) -> String {
s.as_str().to_owned()
}
}
impl From<&'_ FlyStr> for String {
#[inline]
fn from(s: &FlyStr) -> String {
s.as_str().to_owned()
}
}
impl Deref for FlyStr {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl AsRef<str> for FlyStr {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl PartialOrd for FlyStr {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FlyStr {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_str().cmp(other.as_str())
}
}
impl PartialEq<str> for FlyStr {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&'_ str> for FlyStr {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<String> for FlyStr {
#[inline]
fn eq(&self, other: &String) -> bool {
self.as_str() == &**other
}
}
impl PartialEq<FlyByteStr> for FlyStr {
#[inline]
fn eq(&self, other: &FlyByteStr) -> bool {
self.0 == other.0
}
}
impl PartialEq<&'_ FlyByteStr> for FlyStr {
#[inline]
fn eq(&self, other: &&FlyByteStr) -> bool {
self.0 == other.0
}
}
impl PartialOrd<str> for FlyStr {
#[inline]
fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
self.as_str().partial_cmp(other)
}
}
impl PartialOrd<&str> for FlyStr {
#[inline]
fn partial_cmp(&self, other: &&str) -> Option<std::cmp::Ordering> {
self.as_str().partial_cmp(*other)
}
}
impl PartialOrd<FlyByteStr> for FlyStr {
#[inline]
fn partial_cmp(&self, other: &FlyByteStr) -> Option<std::cmp::Ordering> {
BStr::new(self.as_str()).partial_cmp(other.as_bstr())
}
}
impl PartialOrd<&'_ FlyByteStr> for FlyStr {
#[inline]
fn partial_cmp(&self, other: &&FlyByteStr) -> Option<std::cmp::Ordering> {
BStr::new(self.as_str()).partial_cmp(other.as_bstr())
}
}
impl Debug for FlyStr {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Debug::fmt(self.as_str(), f)
}
}
impl Display for FlyStr {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(self.as_str(), f)
}
}
#[cfg(feature = "serde")]
impl Serialize for FlyStr {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for FlyStr {
fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(FlyStrVisitor)
}
}
#[cfg(feature = "serde")]
struct FlyStrVisitor;
#[cfg(feature = "serde")]
impl Visitor<'_> for FlyStrVisitor {
type Value = FlyStr;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("a string")
}
fn visit_borrowed_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.into())
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.into())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.into())
}
}
macro_rules! new_raw_repr {
($borrowed_bytes:expr) => {
if $borrowed_bytes.len() <= MAX_INLINE_SIZE {
RawRepr::new_inline($borrowed_bytes)
} else {
let mut cache = CACHE.lock().unwrap();
if let Some(existing) = cache.get($borrowed_bytes) {
RawRepr::from_storage(existing)
} else {
RawRepr::new_for_storage(&mut cache, $borrowed_bytes)
}
}
};
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct FlyByteStr(RawRepr);
static_assertions::assert_eq_size!(FlyByteStr, usize);
static_assertions::assert_not_impl_any!(FlyByteStr: Borrow<str>);
impl FlyByteStr {
pub fn new(s: impl AsRef<[u8]> + Into<Vec<u8>>) -> Self {
Self(RawRepr::new(s))
}
#[inline]
pub fn as_bstr(&self) -> &BStr {
BStr::new(self.0.as_bytes())
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
impl Default for FlyByteStr {
#[inline]
fn default() -> Self {
Self::new(b"")
}
}
impl From<&'_ [u8]> for FlyByteStr {
#[inline]
fn from(s: &[u8]) -> Self {
Self::new(s)
}
}
impl<const N: usize> From<[u8; N]> for FlyByteStr {
#[inline]
fn from(s: [u8; N]) -> Self {
Self::new(s)
}
}
impl From<&'_ BStr> for FlyByteStr {
#[inline]
fn from(s: &BStr) -> Self {
let bytes: &[u8] = s.as_ref();
Self(new_raw_repr!(bytes))
}
}
impl From<&'_ str> for FlyByteStr {
#[inline]
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<&'_ Vec<u8>> for FlyByteStr {
#[inline]
fn from(s: &Vec<u8>) -> Self {
Self(new_raw_repr!(&s[..]))
}
}
impl From<&'_ String> for FlyByteStr {
#[inline]
fn from(s: &String) -> Self {
Self::new(&**s)
}
}
impl From<Vec<u8>> for FlyByteStr {
#[inline]
fn from(s: Vec<u8>) -> Self {
Self::new(s)
}
}
impl From<String> for FlyByteStr {
#[inline]
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<BString> for FlyByteStr {
#[inline]
fn from(s: BString) -> Self {
Self::new(s)
}
}
impl From<Box<[u8]>> for FlyByteStr {
#[inline]
fn from(s: Box<[u8]>) -> Self {
Self::new(s)
}
}
impl From<Box<str>> for FlyByteStr {
#[inline]
fn from(s: Box<str>) -> Self {
Self(new_raw_repr!(s.as_bytes()))
}
}
impl From<&'_ Box<[u8]>> for FlyByteStr {
#[inline]
fn from(s: &'_ Box<[u8]>) -> Self {
Self(new_raw_repr!(&**s))
}
}
impl From<&Box<str>> for FlyByteStr {
#[inline]
fn from(s: &Box<str>) -> Self {
Self::new(&**s)
}
}
impl From<FlyByteStr> for BString {
#[inline]
fn from(s: FlyByteStr) -> BString {
s.as_bstr().to_owned()
}
}
impl From<FlyByteStr> for Vec<u8> {
#[inline]
fn from(s: FlyByteStr) -> Vec<u8> {
s.as_bytes().to_owned()
}
}
impl From<FlyStr> for FlyByteStr {
#[inline]
fn from(s: FlyStr) -> FlyByteStr {
Self(s.0)
}
}
impl TryInto<String> for FlyByteStr {
type Error = std::string::FromUtf8Error;
#[inline]
fn try_into(self) -> Result<String, Self::Error> {
String::from_utf8(self.into())
}
}
impl Deref for FlyByteStr {
type Target = BStr;
#[inline]
fn deref(&self) -> &Self::Target {
self.as_bstr()
}
}
impl AsRef<BStr> for FlyByteStr {
#[inline]
fn as_ref(&self) -> &BStr {
self.as_bstr()
}
}
impl PartialOrd for FlyByteStr {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FlyByteStr {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_bstr().cmp(other.as_bstr())
}
}
impl PartialEq<[u8]> for FlyByteStr {
#[inline]
fn eq(&self, other: &[u8]) -> bool {
self.as_bytes() == other
}
}
impl PartialEq<BStr> for FlyByteStr {
#[inline]
fn eq(&self, other: &BStr) -> bool {
self.as_bytes() == other
}
}
impl PartialEq<str> for FlyByteStr {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl PartialEq<&'_ [u8]> for FlyByteStr {
#[inline]
fn eq(&self, other: &&[u8]) -> bool {
self.as_bytes() == *other
}
}
impl PartialEq<&'_ BStr> for FlyByteStr {
#[inline]
fn eq(&self, other: &&BStr) -> bool {
self.as_bstr() == *other
}
}
impl PartialEq<&'_ str> for FlyByteStr {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl PartialEq<String> for FlyByteStr {
#[inline]
fn eq(&self, other: &String) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl PartialEq<FlyStr> for FlyByteStr {
#[inline]
fn eq(&self, other: &FlyStr) -> bool {
self.0 == other.0
}
}
impl PartialEq<&'_ FlyStr> for FlyByteStr {
#[inline]
fn eq(&self, other: &&FlyStr) -> bool {
self.0 == other.0
}
}
impl PartialOrd<str> for FlyByteStr {
#[inline]
fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
self.as_bstr().partial_cmp(other)
}
}
impl PartialOrd<&str> for FlyByteStr {
#[inline]
fn partial_cmp(&self, other: &&str) -> Option<std::cmp::Ordering> {
self.as_bstr().partial_cmp(other)
}
}
impl PartialOrd<FlyStr> for FlyByteStr {
#[inline]
fn partial_cmp(&self, other: &FlyStr) -> Option<std::cmp::Ordering> {
self.as_bstr().partial_cmp(other.as_str())
}
}
impl Debug for FlyByteStr {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Debug::fmt(self.as_bstr(), f)
}
}
impl Display for FlyByteStr {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(self.as_bstr(), f)
}
}
#[cfg(feature = "serde")]
impl Serialize for FlyByteStr {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(self.as_bytes())
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for FlyByteStr {
fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_bytes(FlyByteStrVisitor)
}
}
#[cfg(feature = "serde")]
struct FlyByteStrVisitor;
#[cfg(feature = "serde")]
impl<'de> Visitor<'de> for FlyByteStrVisitor {
type Value = FlyByteStr;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("a string, a bytestring, or a sequence of bytes")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(FlyByteStr::from(v))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(FlyByteStr::from(v))
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(FlyByteStr::from(v))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut bytes = vec![];
while let Some(b) = seq.next_element::<u8>()? {
bytes.push(b);
}
Ok(FlyByteStr::from(bytes))
}
}
#[repr(C)] union RawRepr {
heap: NonNull<raw::Payload>,
inline: InlineRepr,
}
static_assertions::assert_eq_size!(NonNull<raw::Payload>, RawRepr);
static_assertions::const_assert!(std::mem::align_of::<raw::Payload>() > 1);
static_assertions::assert_type_eq_all!(byteorder::NativeEndian, byteorder::LittleEndian);
enum SafeRepr<'a> {
Heap(NonNull<raw::Payload>),
Inline(&'a InlineRepr),
}
unsafe impl Send for RawRepr {}
unsafe impl Sync for RawRepr {}
impl RawRepr {
fn new_str(s: impl AsRef<str>) -> Self {
let borrowed = s.as_ref();
new_raw_repr!(borrowed.as_bytes())
}
fn new(s: impl AsRef<[u8]>) -> Self {
let borrowed = s.as_ref();
new_raw_repr!(borrowed)
}
#[inline]
fn new_inline(s: &[u8]) -> Self {
assert!(s.len() <= MAX_INLINE_SIZE);
let new = Self {
inline: InlineRepr::new(s),
};
assert!(
new.is_inline(),
"least significant bit must be 1 for inline strings"
);
new
}
#[inline]
fn from_storage(storage: &Storage) -> Self {
if storage.inc_ref() == 0 {
storage.inc_ref();
}
Self { heap: storage.0 }
}
#[inline]
fn new_for_storage(cache: &mut AHashSet<Storage>, bytes: &[u8]) -> Self {
assert!(bytes.len() > MAX_INLINE_SIZE);
let new_storage = raw::Payload::alloc(bytes);
let for_cache = Storage(new_storage);
let new = Self { heap: new_storage };
assert!(
!new.is_inline(),
"least significant bit must be 0 for heap strings"
);
cache.insert(for_cache);
new
}
#[inline]
fn is_inline(&self) -> bool {
(unsafe { self.inline.masked_len } & 1) == 1
}
#[inline]
fn project(&self) -> SafeRepr<'_> {
if self.is_inline() {
SafeRepr::Inline(unsafe { &self.inline })
} else {
SafeRepr::Heap(unsafe { self.heap })
}
}
#[inline]
fn as_bytes(&self) -> &[u8] {
match self.project() {
SafeRepr::Heap(ptr) => unsafe { &*raw::Payload::bytes(ptr.as_ptr()) },
SafeRepr::Inline(i) => i.as_bytes(),
}
}
}
impl PartialEq for RawRepr {
#[inline]
fn eq(&self, other: &Self) -> bool {
let lhs = unsafe { &self.inline };
let rhs = unsafe { &other.inline };
lhs.eq(rhs)
}
}
impl Eq for RawRepr {}
impl Hash for RawRepr {
fn hash<H: Hasher>(&self, h: &mut H) {
let this = unsafe { &self.inline };
this.hash(h);
}
}
impl Clone for RawRepr {
fn clone(&self) -> Self {
match self.project() {
SafeRepr::Heap(ptr) => {
unsafe {
raw::Payload::inc_ref(ptr.as_ptr());
}
Self { heap: ptr }
}
SafeRepr::Inline(&inline) => Self { inline },
}
}
}
impl Drop for RawRepr {
fn drop(&mut self) {
if !self.is_inline() {
let heap = unsafe { self.heap };
let prev_refcount = unsafe { raw::Payload::dec_ref(heap.as_ptr()) };
if prev_refcount == 1 {
let mut cache = CACHE.lock().unwrap();
let current_refcount = unsafe { raw::Payload::dec_ref(heap.as_ptr()) };
if current_refcount <= 1 {
let bytes = unsafe { &*raw::Payload::bytes(heap.as_ptr()) };
assert!(
cache.remove(bytes),
"cache did not contain bytes, but this thread didn't remove them yet",
);
drop(cache);
unsafe { raw::Payload::dealloc(heap.as_ptr()) };
} else {
}
}
}
}
}
#[derive(Clone, Copy, Hash, PartialEq)]
#[repr(C)] struct InlineRepr {
masked_len: u8,
contents: [u8; MAX_INLINE_SIZE],
}
const MAX_INLINE_SIZE: usize = std::mem::size_of::<NonNull<raw::Payload>>() - 1;
static_assertions::const_assert!((u8::MAX >> 1) as usize >= MAX_INLINE_SIZE);
impl InlineRepr {
#[inline]
fn new(s: &[u8]) -> Self {
assert!(s.len() <= MAX_INLINE_SIZE);
let masked_len = ((s.len() as u8) << 1) | 1;
let mut contents = [0u8; MAX_INLINE_SIZE];
contents[..s.len()].copy_from_slice(s);
Self {
masked_len,
contents,
}
}
#[inline]
fn as_bytes(&self) -> &[u8] {
let len = self.masked_len >> 1;
&self.contents[..len as usize]
}
}
#[cfg(test)]
mod tests {
use super::*;
use static_assertions::{const_assert, const_assert_eq};
use std::collections::BTreeSet;
use test_case::test_case;
#[cfg(not(target_os = "fuchsia"))]
use serial_test::serial;
fn reset_global_cache() {
match CACHE.lock() {
Ok(mut c) => *c = AHashSet::new(),
Err(e) => *e.into_inner() = AHashSet::new(),
}
}
fn num_strings_in_global_cache() -> usize {
CACHE.lock().unwrap().len()
}
impl RawRepr {
fn refcount(&self) -> Option<usize> {
match self.project() {
SafeRepr::Heap(ptr) => {
let count = unsafe { raw::Payload::refcount(ptr.as_ptr()) };
Some(count)
}
SafeRepr::Inline(_) => None,
}
}
}
const SHORT_STRING: &str = "hello";
const_assert!(SHORT_STRING.len() < MAX_INLINE_SIZE);
const MAX_LEN_SHORT_STRING: &str = "hello!!";
const_assert_eq!(MAX_LEN_SHORT_STRING.len(), MAX_INLINE_SIZE);
const MIN_LEN_LONG_STRING: &str = "hello!!!";
const_assert_eq!(MIN_LEN_LONG_STRING.len(), MAX_INLINE_SIZE + 1);
const LONG_STRING: &str = "hello, world!!!!!!!!!!!!!!!!!!!!";
const_assert!(LONG_STRING.len() > MAX_INLINE_SIZE);
const SHORT_NON_UTF8: &[u8] = b"\xF0\x28\x8C\x28";
const_assert!(SHORT_NON_UTF8.len() < MAX_INLINE_SIZE);
const LONG_NON_UTF8: &[u8] = b"\xF0\x28\x8C\x28\xF0\x28\x8C\x28";
const_assert!(LONG_NON_UTF8.len() > MAX_INLINE_SIZE);
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[test_case(MIN_LEN_LONG_STRING ; "barely long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn string_formatting_is_equivalent_to_str(original: &str) {
reset_global_cache();
let cached = FlyStr::new(original);
assert_eq!(format!("{original}"), format!("{cached}"));
assert_eq!(format!("{original:?}"), format!("{cached:?}"));
let cached = FlyByteStr::new(original);
assert_eq!(format!("{original}"), format!("{cached}"));
assert_eq!(format!("{original:?}"), format!("{cached:?}"));
}
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[test_case(MIN_LEN_LONG_STRING ; "barely long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn string_equality_works(contents: &str) {
reset_global_cache();
let cached = FlyStr::new(contents);
let bytes_cached = FlyByteStr::new(contents);
assert_eq!(cached, cached.clone(), "must be equal to itself");
assert_eq!(cached, contents, "must be equal to the original");
assert_eq!(
cached,
contents.to_owned(),
"must be equal to an owned copy of the original"
);
assert_eq!(cached, bytes_cached);
assert_ne!(cached, "goodbye");
assert_ne!(bytes_cached, "goodbye");
}
#[test_case("", SHORT_STRING ; "empty and short string")]
#[test_case(SHORT_STRING, MAX_LEN_SHORT_STRING ; "two short strings")]
#[test_case(MAX_LEN_SHORT_STRING, MIN_LEN_LONG_STRING ; "short and long strings")]
#[test_case(MIN_LEN_LONG_STRING, LONG_STRING ; "barely long and long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn string_comparison_works(lesser_contents: &str, greater_contents: &str) {
reset_global_cache();
let lesser = FlyStr::new(lesser_contents);
let lesser_bytes = FlyByteStr::from(lesser_contents);
let greater = FlyStr::new(greater_contents);
let greater_bytes = FlyByteStr::from(greater_contents);
assert!(lesser < greater);
assert!(lesser < greater_bytes);
assert!(lesser_bytes < greater);
assert!(lesser_bytes < greater_bytes);
assert!(lesser <= greater);
assert!(lesser <= greater_bytes);
assert!(lesser_bytes <= greater);
assert!(lesser_bytes <= greater_bytes);
assert!(greater > lesser);
assert!(greater > lesser_bytes);
assert!(greater_bytes > lesser);
assert!(greater >= lesser);
assert!(greater >= lesser_bytes);
assert!(greater_bytes >= lesser);
assert!(greater_bytes >= lesser_bytes);
}
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn no_allocations_for_short_strings(contents: &str) {
reset_global_cache();
assert_eq!(num_strings_in_global_cache(), 0);
let original = FlyStr::new(contents);
assert_eq!(num_strings_in_global_cache(), 0);
assert_eq!(original.0.refcount(), None);
let cloned = original.clone();
assert_eq!(num_strings_in_global_cache(), 0);
assert_eq!(cloned.0.refcount(), None);
let deduped = FlyStr::new(contents);
assert_eq!(num_strings_in_global_cache(), 0);
assert_eq!(deduped.0.refcount(), None);
}
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn no_allocations_for_short_bytestrings(contents: &str) {
reset_global_cache();
assert_eq!(num_strings_in_global_cache(), 0);
let original = FlyByteStr::new(contents);
assert_eq!(num_strings_in_global_cache(), 0);
assert_eq!(original.0.refcount(), None);
let cloned = original.clone();
assert_eq!(num_strings_in_global_cache(), 0);
assert_eq!(cloned.0.refcount(), None);
let deduped = FlyByteStr::new(contents);
assert_eq!(num_strings_in_global_cache(), 0);
assert_eq!(deduped.0.refcount(), None);
}
#[test_case(MIN_LEN_LONG_STRING ; "barely long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn only_one_copy_allocated_for_long_strings(contents: &str) {
reset_global_cache();
assert_eq!(num_strings_in_global_cache(), 0);
let original = FlyStr::new(contents);
assert_eq!(
num_strings_in_global_cache(),
1,
"only one string allocated"
);
assert_eq!(original.0.refcount(), Some(1), "one copy on stack");
let cloned = original.clone();
assert_eq!(
num_strings_in_global_cache(),
1,
"cloning just incremented refcount"
);
assert_eq!(cloned.0.refcount(), Some(2), "two copies on stack");
let deduped = FlyStr::new(contents);
assert_eq!(num_strings_in_global_cache(), 1, "new string was deduped");
assert_eq!(deduped.0.refcount(), Some(3), "three copies on stack");
}
#[test_case(MIN_LEN_LONG_STRING ; "barely long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn only_one_copy_allocated_for_long_bytestrings(contents: &str) {
reset_global_cache();
assert_eq!(num_strings_in_global_cache(), 0);
let original = FlyByteStr::new(contents);
assert_eq!(
num_strings_in_global_cache(),
1,
"only one string allocated"
);
assert_eq!(original.0.refcount(), Some(1), "one copy on stack");
let cloned = original.clone();
assert_eq!(
num_strings_in_global_cache(),
1,
"cloning just incremented refcount"
);
assert_eq!(cloned.0.refcount(), Some(2), "two copies on stack");
let deduped = FlyByteStr::new(contents);
assert_eq!(num_strings_in_global_cache(), 1, "new string was deduped");
assert_eq!(deduped.0.refcount(), Some(3), "three copies on stack");
}
#[test]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn utf8_and_bytestrings_share_the_cache() {
reset_global_cache();
assert_eq!(num_strings_in_global_cache(), 0, "cache is empty");
let _utf8 = FlyStr::from(MIN_LEN_LONG_STRING);
assert_eq!(num_strings_in_global_cache(), 1, "string was allocated");
let _bytes = FlyByteStr::from(MIN_LEN_LONG_STRING);
assert_eq!(
num_strings_in_global_cache(),
1,
"bytestring was pulled from cache"
);
}
#[test_case(MIN_LEN_LONG_STRING ; "barely long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn cached_strings_dropped_when_refs_dropped(contents: &str) {
reset_global_cache();
let alloced = FlyStr::new(contents);
assert_eq!(
num_strings_in_global_cache(),
1,
"only one string allocated"
);
drop(alloced);
assert_eq!(num_strings_in_global_cache(), 0, "last reference dropped");
}
#[test_case(MIN_LEN_LONG_STRING ; "barely long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn cached_bytestrings_dropped_when_refs_dropped(contents: &str) {
reset_global_cache();
let alloced = FlyByteStr::new(contents);
assert_eq!(
num_strings_in_global_cache(),
1,
"only one string allocated"
);
drop(alloced);
assert_eq!(num_strings_in_global_cache(), 0, "last reference dropped");
}
#[test_case("", SHORT_STRING ; "empty and short string")]
#[test_case(SHORT_STRING, MAX_LEN_SHORT_STRING ; "two short strings")]
#[test_case(SHORT_STRING, LONG_STRING ; "short and long strings")]
#[test_case(LONG_STRING, MAX_LEN_SHORT_STRING ; "long and max-len-short strings")]
#[test_case(MIN_LEN_LONG_STRING, LONG_STRING ; "barely long and long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn equality_and_hashing_with_pointer_value_works_correctly(first: &str, second: &str) {
reset_global_cache();
let first = FlyStr::new(first);
let second = FlyStr::new(second);
let mut set = AHashSet::new();
set.insert(first.clone());
assert!(set.contains(&first));
assert!(!set.contains(&second));
set.insert(first);
assert_eq!(
set.len(),
1,
"set did not grow because the same string was inserted as before"
);
set.insert(second.clone());
assert_eq!(
set.len(),
2,
"inserting a different string must mutate the set"
);
assert!(set.contains(&second));
set.insert(second);
assert_eq!(set.len(), 2);
}
#[test_case("", SHORT_STRING ; "empty and short string")]
#[test_case(SHORT_STRING, MAX_LEN_SHORT_STRING ; "two short strings")]
#[test_case(SHORT_STRING, LONG_STRING ; "short and long strings")]
#[test_case(LONG_STRING, MAX_LEN_SHORT_STRING ; "long and max-len-short strings")]
#[test_case(MIN_LEN_LONG_STRING, LONG_STRING ; "barely long and long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn byte_equality_and_hashing_with_pointer_value_works_correctly(first: &str, second: &str) {
reset_global_cache();
let first = FlyByteStr::new(first);
let second = FlyByteStr::new(second);
let mut set = AHashSet::new();
set.insert(first.clone());
assert!(set.contains(&first));
assert!(!set.contains(&second));
set.insert(first);
assert_eq!(
set.len(),
1,
"set did not grow because the same string was inserted as before"
);
set.insert(second.clone());
assert_eq!(
set.len(),
2,
"inserting a different string must mutate the set"
);
assert!(set.contains(&second));
set.insert(second);
assert_eq!(set.len(), 2);
}
#[test_case("", SHORT_STRING ; "empty and short string")]
#[test_case(SHORT_STRING, MAX_LEN_SHORT_STRING ; "two short strings")]
#[test_case(SHORT_STRING, LONG_STRING ; "short and long strings")]
#[test_case(LONG_STRING, MAX_LEN_SHORT_STRING ; "long and max-len-short strings")]
#[test_case(MIN_LEN_LONG_STRING, LONG_STRING ; "barely long and long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn comparison_for_btree_storage_works(first: &str, second: &str) {
reset_global_cache();
let first = FlyStr::new(first);
let second = FlyStr::new(second);
let mut set = BTreeSet::new();
set.insert(first.clone());
assert!(set.contains(&first));
assert!(!set.contains(&second));
set.insert(first);
assert_eq!(
set.len(),
1,
"set did not grow because the same string was inserted as before"
);
set.insert(second.clone());
assert_eq!(
set.len(),
2,
"inserting a different string must mutate the set"
);
assert!(set.contains(&second));
set.insert(second);
assert_eq!(set.len(), 2);
}
#[test_case("", SHORT_STRING ; "empty and short string")]
#[test_case(SHORT_STRING, MAX_LEN_SHORT_STRING ; "two short strings")]
#[test_case(SHORT_STRING, LONG_STRING ; "short and long strings")]
#[test_case(LONG_STRING, MAX_LEN_SHORT_STRING ; "long and max-len-short strings")]
#[test_case(MIN_LEN_LONG_STRING, LONG_STRING ; "barely long and long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn byte_comparison_for_btree_storage_works(first: &str, second: &str) {
reset_global_cache();
let first = FlyByteStr::new(first);
let second = FlyByteStr::new(second);
let mut set = BTreeSet::new();
set.insert(first.clone());
assert!(set.contains(&first));
assert!(!set.contains(&second));
set.insert(first);
assert_eq!(
set.len(),
1,
"set did not grow because the same string was inserted as before"
);
set.insert(second.clone());
assert_eq!(
set.len(),
2,
"inserting a different string must mutate the set"
);
assert!(set.contains(&second));
set.insert(second);
assert_eq!(set.len(), 2);
}
#[cfg(feature = "serde")]
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[test_case(MIN_LEN_LONG_STRING ; "min len long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn serde_works(contents: &str) {
reset_global_cache();
let s = FlyStr::new(contents);
let as_json = serde_json::to_string(&s).unwrap();
assert_eq!(as_json, format!("\"{contents}\""));
assert_eq!(s, serde_json::from_str::<FlyStr>(&as_json).unwrap());
}
#[cfg(feature = "serde")]
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[test_case(MIN_LEN_LONG_STRING ; "min len long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn serde_works_bytestring(contents: &str) {
reset_global_cache();
let s = FlyByteStr::new(contents);
let as_json = serde_json::to_string(&s).unwrap();
assert_eq!(s, serde_json::from_str::<FlyByteStr>(&as_json).unwrap());
}
#[test_case(SHORT_NON_UTF8 ; "short non-utf8 bytestring")]
#[test_case(LONG_NON_UTF8 ; "long non-utf8 bytestring")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn non_utf8_works(contents: &[u8]) {
reset_global_cache();
let res: Result<FlyStr, _> = FlyByteStr::from(contents).try_into();
res.unwrap_err();
}
#[test_case("" ; "empty string")]
#[test_case(SHORT_STRING ; "short strings")]
#[test_case(MAX_LEN_SHORT_STRING ; "max len short strings")]
#[test_case(MIN_LEN_LONG_STRING ; "min len long strings")]
#[test_case(LONG_STRING ; "long strings")]
#[cfg_attr(not(target_os = "fuchsia"), serial)]
fn flystr_to_flybytestr_and_back(contents: &str) {
reset_global_cache();
let bytestr = FlyByteStr::from(contents);
let flystr = FlyStr::try_from(bytestr.clone()).unwrap();
assert_eq!(bytestr, flystr);
let bytestr2 = FlyByteStr::from(flystr.clone());
assert_eq!(bytestr, bytestr2);
}
}