From e89363eabe48a98aa81ce05f2479c4caea344e90 Mon Sep 17 00:00:00 2001 From: Gregory Petrosyan Date: Fri, 28 Aug 2026 18:38:28 +0300 Subject: [PATCH 1/2] Rename DynamicVec to EcoBytes --- src/{dynamic.rs => bytes.rs} | 10 +++++----- src/lib.rs | 2 +- src/string.rs | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) rename src/{dynamic.rs => bytes.rs} (99%) diff --git a/src/dynamic.rs b/src/bytes.rs similarity index 99% rename from src/dynamic.rs rename to src/bytes.rs index a6a0303..9f557dd 100644 --- a/src/dynamic.rs +++ b/src/bytes.rs @@ -6,7 +6,7 @@ use super::EcoVec; /// A byte vector that can hold up to 15 bytes inline and then spills to an /// `EcoVec`. -pub(crate) struct DynamicVec(Repr); +pub(crate) struct EcoBytes(Repr); /// The internal representation. /// @@ -63,7 +63,7 @@ const LEN_TAG: u8 = 0b1000_0000; /// This is used to mask off the tag to get the inline variant's length. const LEN_MASK: u8 = 0b0111_1111; -impl DynamicVec { +impl EcoBytes { #[inline] pub const fn new() -> Self { Self::from_inline(InlineVec::new()) @@ -213,7 +213,7 @@ impl DynamicVec { } } -impl DynamicVec { +impl EcoBytes { // If this returns true, guarantees that `self.0.inline` is initialized. // Otherwise, guarantees that `self.0.spilled` is initialized. #[inline] @@ -256,7 +256,7 @@ impl DynamicVec { } } -impl Clone for DynamicVec { +impl Clone for EcoBytes { #[inline] fn clone(&self) -> Self { match self.variant() { @@ -266,7 +266,7 @@ impl Clone for DynamicVec { } } -impl Drop for DynamicVec { +impl Drop for EcoBytes { #[inline] fn drop(&mut self) { if let VariantMut::Spilled(spilled) = self.variant_mut() { diff --git a/src/lib.rs b/src/lib.rs index e108956..dfb9be6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ extern crate alloc; pub mod string; pub mod vec; -mod dynamic; +mod bytes; mod vendor; pub use self::string::EcoString; diff --git a/src/string.rs b/src/string.rs index bbf7f3e..cd60745 100644 --- a/src/string.rs +++ b/src/string.rs @@ -15,7 +15,7 @@ use std::path::Path; #[cfg(not(feature = "std"))] use alloc::string::String; -use crate::dynamic::{DynamicVec, InlineVec}; +use crate::bytes::{EcoBytes, InlineVec}; /// Create a new [`EcoString`] from a format string. /// ``` @@ -66,7 +66,7 @@ macro_rules! eco_format { /// 64-bit big-endian systems, the type's size increases to 24 bytes and the /// amount of inline storage to 23 bytes. #[derive(Clone)] -pub struct EcoString(DynamicVec); +pub struct EcoString(EcoBytes); impl EcoString { /// Maximum number of bytes for an inline `EcoString` before spilling on @@ -76,12 +76,12 @@ impl EcoString { /// /// # Note /// This value is semver exempt and can be changed with any update. - pub const INLINE_LIMIT: usize = crate::dynamic::LIMIT; + pub const INLINE_LIMIT: usize = crate::bytes::LIMIT; /// Create a new, empty string. #[inline] pub const fn new() -> Self { - Self(DynamicVec::new()) + Self(EcoBytes::new()) } /// Create a new, inline string. @@ -93,19 +93,19 @@ impl EcoString { let Ok(inline) = InlineVec::from_slice(string.as_bytes()) else { exceeded_inline_capacity(); }; - Self(DynamicVec::from_inline(inline)) + Self(EcoBytes::from_inline(inline)) } /// Create a new, empty string with the given `capacity`. #[inline] pub fn with_capacity(capacity: usize) -> Self { - Self(DynamicVec::with_capacity(capacity)) + Self(EcoBytes::with_capacity(capacity)) } /// Create an instance from a string slice. #[inline] fn from_str(string: &str) -> Self { - Self(DynamicVec::from_slice(string.as_bytes())) + Self(EcoBytes::from_slice(string.as_bytes())) } /// Whether the string is empty. @@ -284,7 +284,7 @@ impl EcoString { pub fn repeat(&self, n: usize) -> Self { let slice = self.as_bytes(); let capacity = slice.len().saturating_mul(n); - let mut vec = DynamicVec::with_capacity(capacity); + let mut vec = EcoBytes::with_capacity(capacity); for _ in 0..n { vec.extend_from_slice(slice); } From 79cf7db38629ea9cd46cf3804a5b4ff6bd6bb8ea Mon Sep 17 00:00:00 2001 From: Gregory Petrosyan Date: Fri, 28 Aug 2026 18:39:08 +0300 Subject: [PATCH 2/2] Expose `EcoBytes` --- Cargo.toml | 4 +- README.md | 21 +- src/bytes.rs | 652 +++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 13 +- src/string.rs | 79 ++++-- tests/tests.rs | 107 +++++++- 6 files changed, 816 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a3ff6f6..9f2a61e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,12 @@ version = "0.3.0" rust-version = "1.73" # also change in ci.yml authors = ["Laurenz "] edition = "2021" -description = "Compact, clone-on-write vector and string." +description = "Compact, clone-on-write vector, string, and byte buffer." repository = "https://github.com/typst/ecow" readme = "README.md" license = "MIT OR Apache-2.0" categories = ["data-structures", "no-std"] -keywords = ["string", "vector", "sso", "cow"] +keywords = ["string", "vector", "bytes", "sso", "cow"] [features] default = ["std"] diff --git a/README.md b/README.md index 609cca2..85fc0eb 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,20 @@ [![Crates.io](https://img.shields.io/crates/v/ecow.svg)](https://crates.io/crates/ecow) [![Documentation](https://docs.rs/ecow/badge.svg)](https://docs.rs/ecow) -Compact, clone-on-write vector and string. +Compact, clone-on-write vector, string, and byte buffer. ## Types -- An `EcoVec` is a reference-counted clone-on-write vector. It takes up two - words of space (= 2 usize) and has the same memory layout as a `&[T]` slice. - Within its allocation, it stores a reference count, its capacity and its - elements. - -- An `EcoString` is a reference-counted clone-on-write string with inline - storage. It takes up 16 bytes of space. It has 15 bytes of inline storage and - starting from 16 bytes it becomes an `EcoVec`. +- `EcoVec` is a reference-counted clone-on-write vector. It takes up two words + of space (= 2 usize) and has the same memory layout as a `&[T]` slice. Within + its allocation, it stores a reference count, its capacity and its elements. + +- `EcoString` is a reference-counted clone-on-write string with inline storage. + It takes up 16 bytes of space. It has 15 bytes of inline storage and starting + from 16 bytes it becomes an `EcoVec`. + +- `EcoBytes` is a reference-counted clone-on-write byte buffer with inline + storage. Like an `EcoString`, it takes up 16 bytes and stores up to 15 bytes + inline before spilling to an `EcoVec`. ## Example ```rust diff --git a/src/bytes.rs b/src/bytes.rs index 9f557dd..2d7ad5c 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -1,12 +1,43 @@ +//! A clone-on-write byte buffer with inline storage. + +use alloc::borrow::Cow; +use alloc::vec::Vec; +use core::borrow::Borrow; +use core::cmp::Ordering; +use core::fmt::{self, Debug, Formatter}; +use core::hash::{Hash, Hasher}; use core::mem::{self, ManuallyDrop}; -use core::ops::RangeBounds; +use core::ops::{Deref, RangeBounds}; use core::ptr; use super::EcoVec; -/// A byte vector that can hold up to 15 bytes inline and then spills to an -/// `EcoVec`. -pub(crate) struct EcoBytes(Repr); +/// An economical byte buffer with inline storage and clone-on-write semantics. +/// +/// This type has a size of 16 bytes. It has 15 bytes of inline storage and, +/// starting at 16 bytes, spills into an [`EcoVec`]. The internal reference +/// counter of the heap variant is atomic, making this type [`Sync`] and +/// [`Send`]. +/// +/// # Example +/// ``` +/// use ecow::EcoBytes; +/// +/// // This is stored inline. +/// let small = EcoBytes::from(b"Welcome"); +/// +/// // This spills to the heap. The clone shares its allocation until mutation. +/// let mut big = small.repeat(3); +/// let clone = big.clone(); +/// big.push(b'!'); +/// assert_ne!(big, clone); +/// ``` +/// +/// # Note +/// The above holds true for normal 32-bit or 64-bit little-endian systems. On +/// 64-bit big-endian systems, the type's size increases to 24 bytes and the +/// amount of inline storage to 23 bytes. +pub struct EcoBytes(Repr); /// The internal representation. /// @@ -64,18 +95,53 @@ const LEN_TAG: u8 = 0b1000_0000; const LEN_MASK: u8 = 0b0111_1111; impl EcoBytes { + /// Maximum number of bytes for an inline `EcoBytes` before spilling to the + /// heap. + /// + /// The exact value for this is architecture dependent. + /// + /// # Note + /// This value is semver exempt and can be changed with any update. + pub const INLINE_LIMIT: usize = LIMIT; + + /// Create a new, empty byte buffer. + /// + /// This does not allocate. #[inline] pub const fn new() -> Self { Self::from_inline(InlineVec::new()) } + /// Create a new, inline byte buffer. + /// + /// Panics if the slice's length exceeds the capacity of the inline storage. + #[inline] + pub const fn inline(bytes: &[u8]) -> Self { + let Ok(inline) = InlineVec::from_slice(bytes) else { + exceeded_inline_capacity(); + }; + Self::from_inline(inline) + } + + /// Try to create a new, inline byte buffer. + /// + /// Returns `None` if the slice's length exceeds the capacity of the inline + /// storage. #[inline] - pub const fn from_inline(inline: InlineVec) -> Self { + pub const fn try_inline(bytes: &[u8]) -> Option { + match InlineVec::from_slice(bytes) { + Ok(inline) => Some(Self::from_inline(inline)), + Err(()) => None, + } + } + + #[inline] + pub(crate) const fn from_inline(inline: InlineVec) -> Self { Self(Repr { inline }) } #[inline] - pub const fn from_eco(vec: EcoVec) -> Self { + const fn from_eco(vec: EcoVec) -> Self { // Safety: // Explicitly set `tagged_len` to 0 to mark this as a spilled variant. // Just initializing with `Repr { spilled: ... }` would leave @@ -87,14 +153,7 @@ impl EcoBytes { Self(repr) } - #[inline] - pub fn from_slice(bytes: &[u8]) -> Self { - match InlineVec::from_slice(bytes) { - Ok(inline) => Self::from_inline(inline), - _ => Self::from_eco(EcoVec::from(bytes)), - } - } - + /// Create a new, empty byte buffer with at least the specified capacity. #[inline] pub fn with_capacity(capacity: usize) -> Self { if capacity <= LIMIT { @@ -104,6 +163,13 @@ impl EcoBytes { } } + /// Returns `true` if the buffer contains no bytes. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The number of bytes in the buffer. #[inline] pub fn len(&self) -> usize { match self.variant() { @@ -112,6 +178,19 @@ impl EcoBytes { } } + /// How many bytes the buffer can hold without allocating. + /// + /// If the buffer's heap allocation is shared, mutation can still allocate + /// even when the requested length fits within this capacity. + #[inline] + pub fn capacity(&self) -> usize { + match self.variant() { + Variant::Inline(_) => LIMIT, + Variant::Spilled(spilled) => spilled.capacity(), + } + } + + /// Extracts a slice containing the entire buffer. #[inline] pub fn as_slice(&self) -> &[u8] { match self.variant() { @@ -120,6 +199,9 @@ impl EcoBytes { } } + /// Produce a mutable slice containing the entire buffer. + /// + /// Clones the buffer if its reference count is larger than 1. #[inline] pub fn make_mut(&mut self) -> &mut [u8] { match self.variant_mut() { @@ -128,13 +210,17 @@ impl EcoBytes { } } + /// Add a byte at the end of the buffer. + /// + /// Clones the buffer if its reference count is larger than 1. #[inline] pub fn push(&mut self, byte: u8) { match self.variant_mut() { VariantMut::Inline(inline) => { if inline.push(byte).is_err() { - let mut eco = EcoVec::with_capacity(LIMIT * 2); - eco.extend_from_byte_slice(self.as_slice()); + let capacity = EcoVec::::amortized_cap(inline.len(), 1, LIMIT); + let mut eco = EcoVec::with_capacity(capacity); + eco.extend_from_byte_slice(inline.as_slice()); eco.push(byte); *self = Self::from_eco(eco); } @@ -145,14 +231,65 @@ impl EcoBytes { } } + /// Removes and returns the last byte, or returns `None` if the buffer is + /// empty. + /// + /// Clones the buffer if its reference count is larger than 1. + #[inline] + pub fn pop(&mut self) -> Option { + match self.variant_mut() { + VariantMut::Inline(inline) => inline.pop(), + VariantMut::Spilled(spilled) => spilled.pop(), + } + } + + /// Inserts a byte at an index within the buffer, shifting all bytes after it + /// to the right. + /// + /// Clones the buffer if its reference count is larger than 1. + /// + /// Panics if `index > len`. + pub fn insert(&mut self, index: usize, byte: u8) { + match self.variant_mut() { + VariantMut::Inline(inline) => { + if inline.insert(index, byte).is_err() { + let capacity = EcoVec::::amortized_cap(inline.len(), 1, LIMIT); + let mut eco = EcoVec::with_capacity(capacity); + eco.extend_from_byte_slice(inline.as_slice()); + eco.insert(index, byte); + *self = Self::from_eco(eco); + } + } + VariantMut::Spilled(spilled) => spilled.insert(index, byte), + } + } + + /// Removes and returns the byte at position index within the buffer, + /// shifting all bytes after it to the left. + /// + /// Clones the buffer if its reference count is larger than 1. + /// + /// Panics if `index >= len`. + pub fn remove(&mut self, index: usize) -> u8 { + match self.variant_mut() { + VariantMut::Inline(inline) => inline.remove(index), + VariantMut::Spilled(spilled) => spilled.remove(index), + } + } + + /// Copies and pushes all bytes in a slice to the buffer. #[inline] pub fn extend_from_slice(&mut self, bytes: &[u8]) { + if bytes.is_empty() { + return; + } + match self.variant_mut() { VariantMut::Inline(inline) => { if inline.extend_from_slice(bytes).is_err() { let needed = inline.len() + bytes.len(); let mut eco = EcoVec::with_capacity(needed.next_power_of_two()); - eco.extend_from_byte_slice(self.as_slice()); + eco.extend_from_byte_slice(inline.as_slice()); eco.extend_from_byte_slice(bytes); *self = Self::from_eco(eco); } @@ -163,6 +300,9 @@ impl EcoBytes { } } + /// Inserts the given byte slice at the `index`. + /// + /// Clones the buffer if its reference count is larger than 1. #[inline] pub fn insert_slice(&mut self, index: usize, bytes: &[u8]) { match self.variant_mut() { @@ -183,6 +323,7 @@ impl EcoBytes { } } + /// Removes all bytes from the buffer. #[inline] pub fn clear(&mut self) { match self.variant_mut() { @@ -191,6 +332,11 @@ impl EcoBytes { } } + /// Shortens the buffer, keeping the first `target` bytes and dropping the + /// rest. + /// + /// Clones the buffer if its reference count is larger than 1 and + /// `target < len`. #[inline] pub fn truncate(&mut self, target: usize) { match self.variant_mut() { @@ -199,8 +345,37 @@ impl EcoBytes { } } + /// Reserve space for at least `additional` more bytes. + /// + /// Guarantees that the resulting buffer has space for `additional` more + /// bytes and, if spilled, uniquely owns its backing allocation. + pub fn reserve(&mut self, additional: usize) { + match self.variant_mut() { + VariantMut::Inline(inline) => { + if additional > LIMIT - inline.len() { + let capacity = + EcoVec::::amortized_cap(inline.len(), additional, LIMIT); + let mut eco = EcoVec::with_capacity(capacity); + eco.extend_from_byte_slice(inline.as_slice()); + *self = Self::from_eco(eco); + } + } + VariantMut::Spilled(spilled) => spilled.reserve(additional), + } + } + + /// Repeat this byte buffer `n` times. + pub fn repeat(&self, n: usize) -> Self { + let capacity = self.len().saturating_mul(n); + let mut bytes = Self::with_capacity(capacity); + for _ in 0..n { + bytes.extend_from_slice(self); + } + bytes + } + #[inline] - pub fn remove_range(&mut self, range: R) + pub(crate) fn remove_range(&mut self, range: R) where R: RangeBounds, { @@ -211,13 +386,12 @@ impl EcoBytes { } } } -} -impl EcoBytes { + /// Whether this byte buffer is stored inline. // If this returns true, guarantees that `self.0.inline` is initialized. // Otherwise, guarantees that `self.0.spilled` is initialized. #[inline] - fn is_inline(&self) -> bool { + pub fn is_inline(&self) -> bool { // Safety: // We always initialize tagged_len, even for the `EcoVec` variant. For // the inline variant the highest-order bit is always `1`. For the @@ -278,6 +452,285 @@ impl Drop for EcoBytes { } } +impl Default for EcoBytes { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Debug for EcoBytes { + #[inline] + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self.as_slice(), f) + } +} + +impl Hash for EcoBytes { + #[inline] + fn hash(&self, state: &mut H) { + self.as_slice().hash(state); + } +} + +impl Eq for EcoBytes {} + +impl PartialEq for EcoBytes { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl PartialEq<[u8]> for EcoBytes { + #[inline] + fn eq(&self, other: &[u8]) -> bool { + self.as_slice() == other + } +} + +impl PartialEq<&[u8]> for EcoBytes { + #[inline] + fn eq(&self, other: &&[u8]) -> bool { + self.as_slice() == *other + } +} + +impl PartialEq<[u8; N]> for EcoBytes { + #[inline] + fn eq(&self, other: &[u8; N]) -> bool { + self.as_slice() == other + } +} + +impl PartialEq<&[u8; N]> for EcoBytes { + #[inline] + fn eq(&self, other: &&[u8; N]) -> bool { + self.as_slice() == *other + } +} + +impl PartialEq> for EcoBytes { + #[inline] + fn eq(&self, other: &Vec) -> bool { + self.as_slice() == other + } +} + +impl PartialEq for [u8] { + #[inline] + fn eq(&self, other: &EcoBytes) -> bool { + self == other.as_slice() + } +} + +impl PartialEq for [u8; N] { + #[inline] + fn eq(&self, other: &EcoBytes) -> bool { + self == other.as_slice() + } +} + +impl PartialEq for Vec { + #[inline] + fn eq(&self, other: &EcoBytes) -> bool { + self == other.as_slice() + } +} + +impl PartialEq> for EcoBytes { + #[inline] + fn eq(&self, other: &EcoVec) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl PartialEq for EcoVec { + #[inline] + fn eq(&self, other: &EcoBytes) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Ord for EcoBytes { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.as_slice().cmp(other.as_slice()) + } +} + +impl PartialOrd for EcoBytes { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Deref for EcoBytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl Borrow<[u8]> for EcoBytes { + #[inline] + fn borrow(&self) -> &[u8] { + self.as_slice() + } +} + +impl AsRef<[u8]> for EcoBytes { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +impl From<&[u8]> for EcoBytes { + #[inline] + fn from(bytes: &[u8]) -> Self { + match InlineVec::from_slice(bytes) { + Ok(inline) => Self::from_inline(inline), + Err(()) => Self::from_eco(EcoVec::from(bytes)), + } + } +} + +impl From<&[u8; N]> for EcoBytes { + #[inline] + fn from(bytes: &[u8; N]) -> Self { + Self::from(bytes.as_slice()) + } +} + +impl From<[u8; N]> for EcoBytes { + #[inline] + fn from(bytes: [u8; N]) -> Self { + Self::from(bytes.as_slice()) + } +} + +impl From> for EcoBytes { + /// When the bytes do not fit inline, this needs to allocate to change the + /// layout. + #[inline] + fn from(bytes: Vec) -> Self { + if bytes.len() <= LIMIT { + Self::inline(&bytes) + } else { + Self::from_eco(EcoVec::from(bytes)) + } + } +} + +impl From<&Vec> for EcoBytes { + #[inline] + fn from(bytes: &Vec) -> Self { + Self::from(bytes.as_slice()) + } +} + +impl From> for EcoBytes { + /// This does not allocate. The resulting byte buffer remains spilled even + /// if its contents would fit inline. + #[inline] + fn from(bytes: EcoVec) -> Self { + Self::from_eco(bytes) + } +} + +impl From<&EcoBytes> for EcoBytes { + #[inline] + fn from(bytes: &EcoBytes) -> Self { + bytes.clone() + } +} + +impl From> for EcoBytes { + #[inline] + fn from(bytes: Cow<[u8]>) -> Self { + Self::from(&*bytes) + } +} + +impl From for Vec { + /// This needs to allocate to change the layout. + #[inline] + fn from(bytes: EcoBytes) -> Self { + bytes.as_slice().into() + } +} + +impl From for EcoVec { + /// When the byte buffer is stored inline, this needs to allocate to change + /// the layout. Otherwise, it reuses the existing allocation. + #[inline] + fn from(mut bytes: EcoBytes) -> Self { + match bytes.variant_mut() { + VariantMut::Inline(inline) => EcoVec::from(inline.as_slice()), + VariantMut::Spilled(spilled) => mem::take(spilled), + } + } +} + +impl From<&EcoBytes> for Vec { + #[inline] + fn from(bytes: &EcoBytes) -> Self { + bytes.as_slice().into() + } +} + +impl From<&EcoBytes> for EcoVec { + #[inline] + fn from(bytes: &EcoBytes) -> Self { + match bytes.variant() { + Variant::Inline(inline) => inline.as_slice().into(), + Variant::Spilled(spilled) => spilled.clone(), + } + } +} + +impl FromIterator for EcoBytes { + fn from_iter>(iter: I) -> Self { + let iter = iter.into_iter(); + let mut bytes = Self::with_capacity(iter.size_hint().0); + bytes.extend(iter); + bytes + } +} + +impl Extend for EcoBytes { + fn extend>(&mut self, iter: I) { + let iter = iter.into_iter(); + let hint = iter.size_hint().0; + if hint > 0 { + self.reserve(hint); + } + for byte in iter { + self.push(byte); + } + } +} + +impl<'a> Extend<&'a u8> for EcoBytes { + fn extend>(&mut self, iter: I) { + self.extend(iter.into_iter().copied()); + } +} + +impl<'a> IntoIterator for &'a EcoBytes { + type IntoIter = core::slice::Iter<'a, u8>; + type Item = &'a u8; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() + } +} + #[repr(C)] #[derive(Debug, Copy, Clone)] pub(crate) struct InlineVec { @@ -367,10 +820,73 @@ impl InlineVec { } } + #[inline] + pub fn pop(&mut self) -> Option { + let len = self.len(); + let byte = self.as_slice().last().copied()?; + unsafe { + // Safety: Finding a last element guarantees that `len > 0`, so the + // new length is in bounds. + self.set_len(len - 1); + } + Some(byte) + } + + #[inline] + pub fn insert(&mut self, index: usize, byte: u8) -> Result<(), ()> { + let len = self.len(); + if index > len { + out_of_bounds(index, len); + } + if len >= LIMIT { + return Err(()); + } + + let ptr = self.buf.as_mut_ptr(); + unsafe { + // Safety: + // - `index <= len < LIMIT`, as checked above. + // - The source is valid for `len - index` reads. + // - The destination is valid for `len - index` writes because the + // inline buffer has at least one free byte. + let at = ptr.add(index); + ptr::copy(at, at.add(1), len - index); + ptr::write(at, byte); + + // Safety: `len < LIMIT`, as checked above. + self.set_len(len + 1); + } + Ok(()) + } + + #[inline] + pub fn remove(&mut self, index: usize) -> u8 { + let len = self.len(); + if index >= len { + out_of_bounds(index, len); + } + + let ptr = self.buf.as_mut_ptr(); + unsafe { + // Safety: `index < len`, so this byte is initialized. + let at = ptr.add(index); + let byte = ptr::read(at); + + // Safety: + // - The source is valid for `len - index - 1` reads. + // - The destination is valid for the same number of writes. + ptr::copy(at.add(1), at, len - index - 1); + + // Safety: `index < len` guarantees `len > 0`. + self.set_len(len - 1); + byte + } + } + #[inline] pub fn extend_from_slice(&mut self, bytes: &[u8]) -> Result<(), ()> { let len = self.len(); - let grown = len + bytes.len(); + let Some(grown) = len.checked_add(bytes.len()) else { return Err(()) }; if let Some(segment) = self.buf.get_mut(len..grown) { segment.copy_from_slice(bytes); unsafe { @@ -388,20 +904,20 @@ impl InlineVec { let len = self.len(); assert!(index <= len, "index {index} out of range for slice of length {len}"); - let grown = len + bytes.len(); + let Some(grown) = len.checked_add(bytes.len()) else { return Err(()) }; let tail_len = len - index; if grown <= LIMIT { let ptr = self.buf.as_mut_ptr(); unsafe { // Safety: Checked that `index <= len` and - // `len + bytes.len() == grown < LIMIT`. + // `len + bytes.len() == grown <= LIMIT`. ptr::copy(ptr.add(index), ptr.add(index + bytes.len()), tail_len); // Safety: Checked that `index <= len` and - // `len + bytes.len() == grown < LIMIT`. + // `len + bytes.len() == grown <= LIMIT`. ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(index), bytes.len()); - // Safety: Checked that `grown < LIMIT` + // Safety: Checked that `grown <= LIMIT`. self.set_len(grown); } Ok(()) @@ -442,3 +958,87 @@ impl InlineVec { } } } + +#[cold] +const fn exceeded_inline_capacity() -> ! { + panic!("exceeded inline capacity"); +} + +#[cold] +fn out_of_bounds(index: usize, len: usize) -> ! { + panic!("index is out bounds (index: {index}, len: {len})"); +} + +#[cfg(feature = "std")] +impl std::io::Write for EcoBytes { + #[inline] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.extend_from_slice(buf); + Ok(buf.len()) + } + + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[cfg(feature = "serde")] +mod serde { + use super::EcoBytes; + use core::fmt; + use serde::de::{Deserializer, SeqAccess, Visitor}; + + impl serde::Serialize for EcoBytes { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.as_slice()) + } + } + + struct EcoBytesVisitor; + + impl<'de> Visitor<'de> for EcoBytesVisitor { + type Value = EcoBytes; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte buffer") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut bytes = EcoBytes::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(byte) = seq.next_element()? { + bytes.push(byte); + } + Ok(bytes) + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where + E: serde::de::Error, + { + Ok(EcoBytes::from(bytes)) + } + + fn visit_str(self, string: &str) -> Result + where + E: serde::de::Error, + { + self.visit_bytes(string.as_bytes()) + } + } + + impl<'de> serde::Deserialize<'de> for EcoBytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_bytes(EcoBytesVisitor) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index dfb9be6..f15b292 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,20 @@ /*! -Compact, clone-on-write vector and string. +Compact, clone-on-write vector, string, and byte buffer. ## Types -- An [`EcoVec`] is a reference-counted clone-on-write vector. It takes up two +- [`EcoVec`] is a reference-counted clone-on-write vector. It takes up two words of space (= 2 usize) and has the same memory layout as a `&[T]` slice. Within its allocation, it stores a reference count, its capacity and its elements. -- An [`EcoString`] is a reference-counted clone-on-write string with inline +- [`EcoString`] is a reference-counted clone-on-write string with inline storage. It takes up 16 bytes of space. It has 15 bytes of inline storage and starting from 16 bytes it becomes an [`EcoVec`]. +- [`EcoBytes`] is a reference-counted clone-on-write byte buffer with inline + storage. Like an [`EcoString`], it takes up 16 bytes and stores up to 15 bytes + inline before spilling to an [`EcoVec`]. + ## Example ``` // This is stored inline. @@ -45,12 +49,13 @@ assert_eq!(third, "Welcome to earth! "); extern crate alloc; +pub mod bytes; pub mod string; pub mod vec; -mod bytes; mod vendor; +pub use self::bytes::EcoBytes; pub use self::string::EcoString; pub use self::vec::EcoVec; diff --git a/src/string.rs b/src/string.rs index cd60745..a371199 100644 --- a/src/string.rs +++ b/src/string.rs @@ -16,6 +16,7 @@ use std::path::Path; use alloc::string::String; use crate::bytes::{EcoBytes, InlineVec}; +use crate::EcoVec; /// Create a new [`EcoString`] from a format string. /// ``` @@ -76,7 +77,7 @@ impl EcoString { /// /// # Note /// This value is semver exempt and can be changed with any update. - pub const INLINE_LIMIT: usize = crate::bytes::LIMIT; + pub const INLINE_LIMIT: usize = EcoBytes::INLINE_LIMIT; /// Create a new, empty string. #[inline] @@ -90,10 +91,19 @@ impl EcoString { /// storage. #[inline] pub const fn inline(string: &str) -> Self { - let Ok(inline) = InlineVec::from_slice(string.as_bytes()) else { - exceeded_inline_capacity(); - }; - Self(EcoBytes::from_inline(inline)) + Self(EcoBytes::inline(string.as_bytes())) + } + + /// Try to create a new, inline string. + /// + /// Returns `None` if the string's length exceeds the capacity of the inline + /// storage. + #[inline] + pub const fn try_inline(string: &str) -> Option { + match InlineVec::from_slice(string.as_bytes()) { + Ok(inline) => Some(Self(EcoBytes::from_inline(inline))), + Err(()) => None, + } } /// Create a new, empty string with the given `capacity`. @@ -105,7 +115,7 @@ impl EcoString { /// Create an instance from a string slice. #[inline] fn from_str(string: &str) -> Self { - Self(EcoBytes::from_slice(string.as_bytes())) + Self(EcoBytes::from(string.as_bytes())) } /// Whether the string is empty. @@ -114,6 +124,12 @@ impl EcoString { self.len() == 0 } + /// Whether this string is stored inline. + #[inline] + pub fn is_inline(&self) -> bool { + self.0.is_inline() + } + /// The length of the string in bytes. #[inline] pub fn len(&self) -> usize { @@ -282,13 +298,7 @@ impl EcoString { /// Repeat this string `n` times. pub fn repeat(&self, n: usize) -> Self { - let slice = self.as_bytes(); - let capacity = slice.len().saturating_mul(n); - let mut vec = EcoBytes::with_capacity(capacity); - for _ in 0..n { - vec.extend_from_slice(slice); - } - Self(vec) + Self(self.0.repeat(n)) } } @@ -585,6 +595,44 @@ impl From<&EcoString> for String { } } +impl From for EcoBytes { + /// This does not allocate. + #[inline] + fn from(string: EcoString) -> Self { + string.0 + } +} + +impl TryFrom for EcoString { + type Error = core::str::Utf8Error; + + /// This validates UTF-8 without allocating. + #[inline] + fn try_from(bytes: EcoBytes) -> Result { + core::str::from_utf8(&bytes)?; + Ok(Self(bytes)) + } +} + +impl From for EcoVec { + /// When the string is stored inline, this needs to allocate to change the + /// layout. Otherwise, it reuses the existing allocation. + #[inline] + fn from(string: EcoString) -> Self { + string.0.into() + } +} + +impl TryFrom> for EcoString { + type Error = core::str::Utf8Error; + + /// This validates UTF-8 without allocating. + #[inline] + fn try_from(bytes: EcoVec) -> Result { + Self::try_from(EcoBytes::from(bytes)) + } +} + impl FromStr for EcoString { type Err = core::convert::Infallible; @@ -609,11 +657,6 @@ impl ToEcoString for T { } } -#[cold] -const fn exceeded_inline_capacity() -> ! { - panic!("exceeded inline capacity"); -} - #[cfg(feature = "serde")] mod serde { use crate::EcoString; diff --git a/tests/tests.rs b/tests/tests.rs index 8ec6d01..1c195ee 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -10,7 +10,7 @@ use std::mem; use std::sync::atomic::{AtomicUsize, Ordering::*}; use ecow::string::ToEcoString; -use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use ecow::{eco_format, eco_vec, EcoBytes, EcoString, EcoVec}; const ALPH: &str = "abcdefghijklmnopqrstuvwxyz"; const LIMIT: usize = EcoString::INLINE_LIMIT; @@ -24,6 +24,8 @@ fn test_mem_size() { let word = mem::size_of::(); assert_eq!(mem::size_of::>(), 2 * word); assert_eq!(mem::size_of::>>(), 2 * word); + assert_eq!(mem::size_of::(), mem::size_of::()); + assert_eq!(mem::size_of::>(), mem::size_of::>()); if cfg!(target_endian = "little") { if cfg!(target_pointer_width = "32") { @@ -371,6 +373,105 @@ fn test_array_from_vec() { assert_eq!(<[String; 0]>::try_from(EcoVec::new()).unwrap(), <[String; 0]>::default()); } +#[test] +fn test_bytes_new() { + assert_eq!(EcoBytes::new(), &[]); + assert_eq!(EcoBytes::from([1, 2, 3]), [1, 2, 3]); + + let inline = EcoBytes::from(&ALPH.as_bytes()[..LIMIT]); + let spilled = EcoBytes::from(&ALPH.as_bytes()[..LIMIT + 1]); + assert!(inline.is_inline()); + assert!(!spilled.is_inline()); +} + +#[test] +fn test_bytes_inline_okay() { + const BYTES: EcoBytes = EcoBytes::inline(b"hello"); + assert_eq!(BYTES, b"hello"); + assert_eq!(EcoBytes::try_inline(b"hello").unwrap(), b"hello"); + assert!(EcoBytes::try_inline(ALPH.as_bytes()).is_none()); +} + +#[test] +#[should_panic(expected = "exceeded inline capacity")] +fn test_bytes_inline_capacity_exceeded() { + EcoBytes::inline(ALPH.as_bytes()); +} + +#[test] +fn test_bytes_push() { + let mut bytes = EcoBytes::from(&ALPH.as_bytes()[..LIMIT]); + let original = bytes.clone(); + + bytes.push(b'!'); + assert_eq!(bytes.pop(), Some(b'!')); + assert_eq!(bytes, original); + assert!(!bytes.is_inline()); +} + +#[test] +fn test_bytes_insert() { + let mut bytes = EcoBytes::from([1, 2, 3]); + bytes.insert(1, 4); + assert_eq!(bytes, [1, 4, 2, 3]); + + let mut bytes = EcoBytes::from(&ALPH.as_bytes()[..LIMIT]); + bytes.insert(LIMIT / 2, b'_'); + assert_eq!(bytes[LIMIT / 2], b'_'); + assert!(!bytes.is_inline()); +} + +#[test] +#[should_panic(expected = "index is out bounds (index: 4, len: 3)")] +fn test_bytes_insert_fail() { + EcoBytes::from([1, 2, 3]).insert(4, 0); +} + +#[test] +fn test_bytes_remove() { + let mut bytes = EcoBytes::from([1, 2, 3]); + assert_eq!(bytes.remove(1), 2); + assert_eq!(bytes, [1, 3]); + + let mut bytes = EcoBytes::from(ALPH.as_bytes()); + let clone = bytes.clone(); + assert_eq!(bytes.remove(1), b'b'); + assert_eq!(clone, ALPH.as_bytes()); +} + +#[test] +#[should_panic(expected = "index is out bounds (index: 4, len: 3)")] +fn test_bytes_remove_fail() { + EcoBytes::from([1, 2, 3]).remove(4); +} + +#[test] +fn test_bytes_make_mut() { + let mut first = EcoBytes::from(ALPH.as_bytes()); + let second = first.clone(); + first.make_mut()[0] = b'A'; + assert_eq!(first[0], b'A'); + assert_eq!(second[0], b'a'); +} + +#[test] +fn test_bytes_conversions() { + let string = EcoString::from(ALPH); + let ptr = string.as_ptr(); + let bytes = EcoBytes::from(string); + assert_eq!(bytes.as_ptr(), ptr); + + let string = EcoString::try_from(bytes).unwrap(); + assert_eq!(string.as_ptr(), ptr); + assert_eq!(string, ALPH); + assert!(EcoString::try_from(EcoBytes::from([0xFF])).is_err()); + + let vec = EcoVec::from(*b"abcdefghijklmnopqrstuvwxyz"); + let ptr = vec.as_ptr(); + let bytes = EcoBytes::from(vec); + assert_eq!(EcoVec::from(bytes).as_ptr(), ptr); +} + #[test] fn test_str_macro() { assert_eq!( @@ -541,6 +642,10 @@ fn test_str_repeat() { #[test] fn test_str_inline_okay() { assert_eq!(EcoString::inline("hello"), "hello"); + assert_eq!(EcoString::try_inline("hello").unwrap(), "hello"); + assert!(EcoString::try_inline(ALPH).is_none()); + assert!(EcoString::from("hello").is_inline()); + assert!(!EcoString::from(ALPH).is_inline()); } #[test]