diff --git a/src/containers/fixed_capacity/mod.rs b/src/containers/fixed_capacity/mod.rs index 8d9dd3e0..957ca7a2 100644 --- a/src/containers/fixed_capacity/mod.rs +++ b/src/containers/fixed_capacity/mod.rs @@ -12,7 +12,9 @@ // ******************************************************************************* mod queue; +mod string; mod vec; pub use self::queue::FixedCapacityQueue; +pub use self::string::FixedCapacityString; pub use self::vec::FixedCapacityVec; diff --git a/src/containers/fixed_capacity/string.rs b/src/containers/fixed_capacity/string.rs new file mode 100644 index 00000000..ccce602e --- /dev/null +++ b/src/containers/fixed_capacity/string.rs @@ -0,0 +1,191 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use core::fmt; +use core::ops; + +use crate::generic::string::GenericString; +use crate::storage::Heap; + +/// A fixed-capacity Unicode string. +/// +/// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. +/// +/// The string can hold between 0 and `CAPACITY` **bytes**, and behaves similarly to Rust's `String`, +/// except that it allocates memory immediately on construction, and can't shrink or grow. +pub struct FixedCapacityString { + inner: GenericString>, +} + +impl FixedCapacityString { + /// Creates an empty string and allocates memory for up to `capacity` bytes, where `capacity <= u32::MAX`. + /// + /// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. + /// + /// # Panics + /// + /// - Panics if `capacity > u32::MAX`. + /// - Panics if the memory allocation fails. + #[must_use] + pub fn new(capacity: usize) -> Self { + assert!(capacity <= u32::MAX as usize, "FixedCapacityString can hold at most u32::MAX bytes"); + Self { + inner: GenericString::new(capacity as u32), + } + } + + /// Tries to create an empty string for up to `capacity` bytes, where `capacity <= u32::MAX`. + /// + /// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. + /// + /// Returns `None` if `capacity > u32::MAX`, or if the memory allocation fails. + #[must_use] + pub fn try_new(capacity: usize) -> Option { + if capacity <= u32::MAX as usize { + Some(Self { + inner: GenericString::try_new(capacity as u32)?, + }) + } else { + None + } + } +} + +impl Drop for FixedCapacityString { + fn drop(&mut self) { + self.inner.clear(); + } +} + +impl ops::Deref for FixedCapacityString { + type Target = GenericString>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl ops::DerefMut for FixedCapacityString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl fmt::Display for FixedCapacityString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl fmt::Debug for FixedCapacityString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn push_and_pop() { + fn run_test(n: usize) { + let mut string = FixedCapacityString::new(n); + let mut control = String::new(); + + let result = string.pop(); + assert_eq!(result, None); + + let sample = "abcdefghi"; + for ch in sample.chars().take(n) { + let result = string.push(ch); + assert!(result.is_ok()); + control.push(ch); + assert_eq!(string.as_str(), control.as_str()); + } + + let result = string.push('x'); + assert!(result.is_err()); + + for _ in 0..n { + let expected = control.pop().unwrap(); + let actual = string.pop(); + assert_eq!(actual, Some(expected)); + } + + let result = string.pop(); + assert_eq!(result, None); + } + + for i in 0..6 { + run_test(i); + } + } + + #[test] + fn push_str() { + fn run_test(n: usize) { + let mut string = FixedCapacityString::new(n); + let mut control = String::new(); + + let samples = ["abc", "\0", "πŸ˜‰", "πŸ‘πŸΌπŸš€", "Ξ±Ξ²Ξ³"]; + for sample in samples { + let should_fit = string.capacity() - string.len() >= sample.len(); + let result = string.push_str(sample); + if should_fit { + assert!(result.is_ok()); + control.push_str(sample); + } else { + assert!(result.is_err()); + } + assert_eq!(string.as_str(), control.as_str()); + assert_eq!(string.len(), control.len()); + assert_eq!(string.is_empty(), string.is_empty()); + assert_eq!(string.is_full(), string.capacity() - string.len() == 0); + } + } + + for i in [0, 1, 5, 20, 30] { + run_test(i); + } + } + + #[test] + fn is_full_and_is_empty() { + fn run_test(n: usize) { + let mut string = FixedCapacityString::new(n); + assert!(string.is_empty()); + + let sample = "abcdefghi"; + for ch in sample.chars().take(n) { + assert!(!string.is_full()); + string.push(ch).unwrap(); + assert!(!string.is_empty()); + } + + assert!(string.is_full()); + + for _ in 0..n { + assert!(!string.is_empty()); + string.pop(); + assert!(!string.is_full()); + } + + assert!(string.is_empty()); + } + + for i in 0..6 { + run_test(i); + } + } +} diff --git a/src/containers/fixed_capacity/vec.rs b/src/containers/fixed_capacity/vec.rs index f1567050..d77a9d00 100644 --- a/src/containers/fixed_capacity/vec.rs +++ b/src/containers/fixed_capacity/vec.rs @@ -42,6 +42,20 @@ impl FixedCapacityVec { inner: GenericVec::new(capacity as u32), } } + + /// Tries to create an empty vector for up to `capacity` elements, where `capacity <= u32::MAX`. + /// + /// Returns `None` if `capacity > u32::MAX`, or if the memory allocation fails. + #[must_use] + pub fn try_new(capacity: usize) -> Option { + if capacity <= u32::MAX as usize { + Some(Self { + inner: GenericVec::try_new(capacity as u32)?, + }) + } else { + None + } + } } impl Drop for FixedCapacityVec { diff --git a/src/containers/generic/mod.rs b/src/containers/generic/mod.rs index db30d83c..04392bd3 100644 --- a/src/containers/generic/mod.rs +++ b/src/containers/generic/mod.rs @@ -12,4 +12,5 @@ // ******************************************************************************* pub(crate) mod queue; +pub(crate) mod string; pub(crate) mod vec; diff --git a/src/containers/generic/queue.rs b/src/containers/generic/queue.rs index 7502f385..cdde8f5e 100644 --- a/src/containers/generic/queue.rs +++ b/src/containers/generic/queue.rs @@ -11,12 +11,12 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use core::fmt; use core::marker::PhantomData; use core::mem::needs_drop; use core::ops::Range; use core::ptr; +use crate::InsufficientCapacity; use crate::storage::Storage; #[repr(C)] @@ -104,8 +104,8 @@ impl> GenericQueue { /// Tries to push an element to the back of the queue. /// /// If the queue has spare capacity, the push succeeds and a reference to that element - /// is returned; otherwise, `Err(QueueFull)` is returned. - pub fn push_back(&mut self, value: T) -> Result<&mut T, QueueFull> { + /// is returned; otherwise, `Err(InsufficientCapacity)` is returned. + pub fn push_back(&mut self, value: T) -> Result<&mut T, InsufficientCapacity> { let capacity = self.storage.capacity(); if self.len < capacity { let write_pos = self.front_index as u64 + self.len as u64; @@ -117,15 +117,15 @@ impl> GenericQueue { self.len += 1; Ok(unsafe { self.storage.element_mut(write_pos).write(value) }) } else { - Err(QueueFull) + Err(InsufficientCapacity) } } /// Tries to push an element to the front of the queue. /// /// If the queue has spare capacity, the push succeeds and a reference to that element - /// is returned; otherwise, `Err(QueueFull)` is returned. - pub fn push_front(&mut self, value: T) -> Result<&mut T, QueueFull> { + /// is returned; otherwise, `Err(InsufficientCapacity)` is returned. + pub fn push_front(&mut self, value: T) -> Result<&mut T, InsufficientCapacity> { let capacity = self.storage.capacity(); if self.len < capacity { let write_pos = if self.front_index > 0 { @@ -138,7 +138,7 @@ impl> GenericQueue { self.front_index = write_pos; Ok(element) } else { - Err(QueueFull) + Err(InsufficientCapacity) } } @@ -207,18 +207,6 @@ impl> GenericQueue { } } -/// Indicates that an operation failed because the queue would exceed its maximum capacity. -#[derive(Clone, Copy, Default, Debug)] -pub struct QueueFull; - -impl fmt::Display for QueueFull { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "queue is full") - } -} - -impl core::error::Error for QueueFull {} - #[cfg(test)] mod tests { use std::{collections::VecDeque, mem::MaybeUninit}; diff --git a/src/containers/generic/string.rs b/src/containers/generic/string.rs new file mode 100644 index 00000000..f32656e7 --- /dev/null +++ b/src/containers/generic/string.rs @@ -0,0 +1,252 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use core::fmt; +use core::ops; +use core::str; + +use super::vec::GenericVec; +use crate::InsufficientCapacity; +use crate::storage::Storage; + +/// A UTF-8 encoded string which is generic over its storage method. +#[repr(transparent)] +pub struct GenericString> { + /// The UTF-8 encoded characters of the string. + vec: GenericVec, +} + +impl> GenericString { + /// Creates an empty string with the given capacity in bytes. + /// + /// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. + /// + /// # Panics + /// + /// Panics if not enough memory could be allocated. + pub fn new(capacity: u32) -> Self { + Self { + vec: GenericVec::new(capacity), + } + } + + /// Tries to create an empty string with the given capacity in bytes. + /// + /// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. + /// + /// Returns `None` if not enough memory could be allocated. + pub fn try_new(capacity: u32) -> Option { + Some(Self { + vec: GenericVec::try_new(capacity)?, + }) + } + + pub fn as_bytes(&self) -> &[u8] { + self.vec.as_slice() + } + + /// Extracts a string slice containing the entire string. + /// + /// Equivalent to `&v[..]`. + pub fn as_str(&self) -> &str { + unsafe { str::from_utf8_unchecked(self.vec.as_slice()) } + } + + /// Extracts a mutable string slice of the entire string. + /// + /// Equivalent to `&mut v[..]`. + pub fn as_mut_str(&mut self) -> &mut str { + unsafe { str::from_utf8_unchecked_mut(self.vec.as_mut_slice()) } + } + + /// Returns the maximum length of the string in bytes. + /// + /// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. + pub fn capacity(&self) -> usize { + self.vec.capacity() + } + + /// Returns the length of the string in bytes. + /// + /// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. + pub fn len(&self) -> usize { + self.vec.len() + } + + /// Returns `true` if and only if the string doesn't contain any characters. + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + /// Returns `true` if and only if the string has reached its capacity. + pub fn is_full(&self) -> bool { + self.vec.is_full() + } + + /// Tries to append the given character to the end of the string. + /// + /// If the string has sufficient spare capacity, the operation succeeds; otherwise, `Err(InsufficientCapacity)` is returned. + pub fn push(&mut self, ch: char) -> Result<(), InsufficientCapacity> { + let mut buffer = [0_u8; 4]; + self.push_str(ch.encode_utf8(&mut buffer)) + } + + /// Tries to append the given string slice to the end of the string. + /// + /// If the string has sufficient spare capacity, the operation succeeds; otherwise, `Err(InsufficientCapacity)` is returned. + pub fn push_str(&mut self, other: &str) -> Result<(), InsufficientCapacity> { + match self.vec.extend_from_slice(other.as_bytes()) { + Ok(_) => Ok(()), + Err(_) => Err(InsufficientCapacity), + } + } + + /// Removes the last character from the string and returns it. + /// + /// Returns `None` if the string is empty. + pub fn pop(&mut self) -> Option { + let ch = self.chars().next_back()?; + let new_len = self.len() - ch.len_utf8(); + // SAFETY: + // - This decreases the length of the internal vector, so it doesn't expose any uninitialized bytes. + // - The string was valid UTF-8 before; we've removed exactly one codepoint, so the rest is still valid UTF-8. + unsafe { + self.vec.set_len(new_len); + } + Some(ch) + } + + /// Clears the string, removing all characters. + pub fn clear(&mut self) { + self.vec.clear(); + } +} + +impl> ops::Deref for GenericString { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl> ops::DerefMut for GenericString { + fn deref_mut(&mut self) -> &mut Self::Target { + self.as_mut_str() + } +} + +impl> fmt::Debug for GenericString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +#[cfg(test)] +mod tests { + use std::mem::MaybeUninit; + + use super::*; + + #[test] + fn push_and_pop() { + fn run_test(n: usize) { + let mut string = GenericString::>>::new(n as u32); + let mut control = String::new(); + + let result = string.pop(); + assert_eq!(result, None); + + let sample = "abcdefghi"; + for ch in sample.chars().take(n) { + let result = string.push(ch); + assert!(result.is_ok()); + control.push(ch); + assert_eq!(string.as_str(), control.as_str()); + } + + let result = string.push('x'); + assert!(result.is_err()); + + for _ in 0..n { + let expected = control.pop().unwrap(); + let actual = string.pop(); + assert_eq!(actual, Some(expected)); + } + + let result = string.pop(); + assert_eq!(result, None); + } + + for i in 0..6 { + run_test(i); + } + } + + #[test] + fn push_str() { + fn run_test(n: usize) { + let mut string = GenericString::>>::new(n as u32); + let mut control = String::new(); + + let samples = ["abc", "\0", "πŸ˜‰", "πŸ‘πŸΌπŸš€", "Ξ±Ξ²Ξ³"]; + for sample in samples { + let should_fit = string.capacity() - string.len() >= sample.len(); + let result = string.push_str(sample); + if should_fit { + assert!(result.is_ok()); + control.push_str(sample); + } else { + assert!(result.is_err()); + } + assert_eq!(string.as_str(), control.as_str()); + assert_eq!(string.len(), control.len()); + assert_eq!(string.is_empty(), string.is_empty()); + assert_eq!(string.is_full(), string.capacity() - string.len() == 0); + } + } + + for i in [0, 1, 5, 20, 30] { + run_test(i); + } + } + + #[test] + fn is_full_and_is_empty() { + fn run_test(n: usize) { + let mut string = GenericString::>>::new(n as u32); + assert!(string.is_empty()); + + let sample = "abcdefghi"; + for ch in sample.chars().take(n) { + assert!(!string.is_full()); + string.push(ch).unwrap(); + assert!(!string.is_empty()); + } + + assert!(string.is_full()); + + for _ in 0..n { + assert!(!string.is_empty()); + string.pop(); + assert!(!string.is_full()); + } + + assert!(string.is_empty()); + } + + for i in 0..6 { + run_test(i); + } + } +} diff --git a/src/containers/generic/vec.rs b/src/containers/generic/vec.rs index dc4eb5a1..ec9f1300 100644 --- a/src/containers/generic/vec.rs +++ b/src/containers/generic/vec.rs @@ -17,6 +17,7 @@ use core::mem::needs_drop; use core::ops; use core::ptr; +use crate::InsufficientCapacity; use crate::storage::Storage; #[repr(C)] @@ -27,7 +28,11 @@ pub struct GenericVec> { } impl> GenericVec { - /// Creates an empty vector. + /// Creates an empty vector with the given capacity. + /// + /// # Panics + /// + /// Panics if not enough memory could be allocated. pub fn new(capacity: u32) -> Self { Self { len: 0, @@ -36,6 +41,17 @@ impl> GenericVec { } } + /// Tries to create an empty vector with the given capacity. + /// + /// Returns `None` if not enough memory could be allocated. + pub fn try_new(capacity: u32) -> Option { + Some(Self { + len: 0, + storage: S::try_new(capacity)?, + _marker: PhantomData, + }) + } + /// Extracts a slice containing the entire vector. /// /// Equivalent to `&v[..]`. @@ -73,14 +89,14 @@ impl> GenericVec { /// Tries to push an element to the back of the vector. /// /// If the vector has spare capacity, the push succeeds and a reference to that element - /// is returned; otherwise, `Err(VectorFull)` is returned. - pub fn push(&mut self, value: T) -> Result<&mut T, VectorFull> { + /// is returned; otherwise, `Err(InsufficientCapacity)` is returned. + pub fn push(&mut self, value: T) -> Result<&mut T, InsufficientCapacity> { if self.len < self.storage.capacity() { let element = unsafe { self.storage.element_mut(self.len) }.write(value); self.len += 1; Ok(element) } else { - Err(VectorFull) + Err(InsufficientCapacity) } } @@ -108,6 +124,47 @@ impl> GenericVec { } } } + + /// Manually sets the length of the vector. + /// + /// # Safety + /// + /// - `new_len <= self.capacity()` must hold + /// - if `new_len` is greater than the current length, the elements in the new range must have been initialized + pub(super) unsafe fn set_len(&mut self, new_len: usize) { + debug_assert!(new_len <= self.capacity()); + self.len = new_len as u32; + } +} + +impl> GenericVec { + /// Tries to append a copy of the given slice to the end of the vector. + /// + /// If the vector has sufficient spare capacity, the operation succeeds and a reference to those elements is returned; + /// otherwise, `Err(InsufficientCapacity)` is returned. + pub fn extend_from_slice(&mut self, other: &[T]) -> Result<&mut [T], InsufficientCapacity> { + let new_len = (self.len as usize).checked_add(other.len()).ok_or(InsufficientCapacity)?; + if new_len <= self.capacity() { + let new_len = new_len as u32; // No overflow, because new_len <= capacity <= u32::MAX + // SAFETY: + // - `self.len <= new_len``, because the addition didn't overflow + // - `new_len <= self.capacity()` as per check above + let target = unsafe { self.storage.subslice_mut(self.len, new_len) }; + // SAFETY: + // - `other.as_ptr()` is valid for reads of `other.len()` elements, because it's a valid slice reference + // - `target` is valid for writes of `other.len()` elements, because we got it from `subslice_mut()`, + // and `new_len - self.len == other.len()` + // - the memory regions don't overlap, because `&mut self` precludes `other: &[T]` from overlapping + unsafe { + (target as *mut T).copy_from_nonoverlapping(other.as_ptr(), other.len()); + } + self.len = new_len; + // SAFETY: the memory in the `target` slice has now been initialized + Ok(unsafe { &mut *target }) + } else { + Err(InsufficientCapacity) + } + } } impl> ops::Deref for GenericVec { @@ -130,18 +187,6 @@ impl> fmt::Debug for GenericVec { } } -/// Indicates that an operation failed because the vector would exceed its maximum capacity. -#[derive(Clone, Copy, Default, Debug)] -pub struct VectorFull; - -impl fmt::Display for VectorFull { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "vector is full") - } -} - -impl core::error::Error for VectorFull {} - #[cfg(test)] mod tests { use std::mem::MaybeUninit; diff --git a/src/containers/inline/mod.rs b/src/containers/inline/mod.rs index c3cabdb5..a70e8b6f 100644 --- a/src/containers/inline/mod.rs +++ b/src/containers/inline/mod.rs @@ -12,7 +12,9 @@ // ******************************************************************************* mod queue; +mod string; mod vec; pub use self::queue::InlineQueue; +pub use self::string::InlineString; pub use self::vec::InlineVec; diff --git a/src/containers/inline/string.rs b/src/containers/inline/string.rs new file mode 100644 index 00000000..9d066f0b --- /dev/null +++ b/src/containers/inline/string.rs @@ -0,0 +1,183 @@ +// ******************************************************************************* +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use core::fmt; +use core::ops; + +use crate::generic::string::GenericString; +use crate::storage::Inline; + +/// A fixed-capacity Unicode string with inline storage. +/// +/// Note that the string is encoded as UTF-8, so each character (Unicode codepoint) requires between 1 and 4 bytes of storage. +/// +/// The string can hold between 0 and `CAPACITY` **bytes**, and behaves similarly to Rust's `String`, +/// except that it stores the characters inline and doesn't allocate. +/// `CAPACITY` must be `>= 1` and `<= u32::MAX`. +/// +/// This data structure has a stable, well-defined memory layout and satisfies the requirements for +/// [ABI-compatible types](https://eclipse-score.github.io/score/main/features/communication/abi_compatible_data_types/index.html). +/// Its layout is structurally equivalent to: +/// +/// ```ignore +/// #[repr(C)] +/// struct String { +/// len: u32, +/// bytes: [u8; N], +/// } +/// ``` +#[repr(transparent)] +pub struct InlineString { + inner: GenericString>, +} + +impl InlineString { + const CHECK_CAPACITY: () = assert!(0 < CAPACITY && CAPACITY <= u32::MAX as usize); + + /// Creates an empty string. + pub fn new() -> Self { + let () = Self::CHECK_CAPACITY; + + Self { + inner: GenericString::new(CAPACITY as u32), + } + } +} + +impl Default for InlineString { + fn default() -> Self { + Self::new() + } +} + +impl ops::Deref for InlineString { + type Target = GenericString>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl ops::DerefMut for InlineString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl fmt::Debug for InlineString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn push_and_pop() { + fn run_test() { + let mut string = InlineString::::new(); + let mut control = String::new(); + + let result = string.pop(); + assert_eq!(result, None); + + let sample = "abcdefghi"; + for ch in sample.chars().take(N) { + let result = string.push(ch); + assert!(result.is_ok()); + control.push(ch); + assert_eq!(string.as_str(), control.as_str()); + } + + let result = string.push('x'); + assert!(result.is_err()); + + for _ in 0..N { + let expected = control.pop().unwrap(); + let actual = string.pop(); + assert_eq!(actual, Some(expected)); + } + + let result = string.pop(); + assert_eq!(result, None); + } + + run_test::<1>(); + run_test::<2>(); + run_test::<3>(); + run_test::<4>(); + run_test::<5>(); + } + + #[test] + fn push_str() { + fn run_test() { + let mut string = InlineString::::new(); + let mut control = String::new(); + + let samples = ["abc", "\0", "πŸ˜‰", "πŸ‘πŸΌπŸš€", "Ξ±Ξ²Ξ³"]; + for sample in samples { + let should_fit = string.capacity() - string.len() >= sample.len(); + let result = string.push_str(sample); + if should_fit { + assert!(result.is_ok()); + control.push_str(sample); + } else { + assert!(result.is_err()); + } + assert_eq!(string.as_str(), control.as_str()); + assert_eq!(string.len(), control.len()); + assert_eq!(string.is_empty(), string.is_empty()); + assert_eq!(string.is_full(), string.capacity() - string.len() == 0); + } + } + + run_test::<1>(); + run_test::<5>(); + run_test::<20>(); + run_test::<30>(); + } + + #[test] + fn is_full_and_is_empty() { + fn run_test() { + let mut string = InlineString::::new(); + assert!(string.is_empty()); + + let sample = "abcdefghi"; + for ch in sample.chars().take(N) { + assert!(!string.is_full()); + string.push(ch).unwrap(); + assert!(!string.is_empty()); + } + + assert!(string.is_full()); + + for _ in 0..N { + assert!(!string.is_empty()); + string.pop(); + assert!(!string.is_full()); + } + + assert!(string.is_empty()); + } + + run_test::<1>(); + run_test::<2>(); + run_test::<3>(); + run_test::<4>(); + run_test::<5>(); + } +} diff --git a/src/containers/lib.rs b/src/containers/lib.rs index 4e106ab0..46265859 100644 --- a/src/containers/lib.rs +++ b/src/containers/lib.rs @@ -19,3 +19,19 @@ pub mod fixed_capacity; pub(crate) mod generic; pub mod inline; pub(crate) mod storage; + +use core::fmt; + +/// Indicates that an operation failed because the container doesn't have enough remaining capacity. +/// +/// Note that this doesn't necessarily mean that the container is full. +#[derive(Clone, Copy, Default, Debug)] +pub struct InsufficientCapacity; + +impl fmt::Display for InsufficientCapacity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "insufficient capacity for this operation") + } +} + +impl core::error::Error for InsufficientCapacity {} diff --git a/src/containers/storage/heap.rs b/src/containers/storage/heap.rs index fe50c2d1..332c1129 100644 --- a/src/containers/storage/heap.rs +++ b/src/containers/storage/heap.rs @@ -11,9 +11,9 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use alloc::alloc::Layout; use alloc::alloc::alloc; use alloc::alloc::dealloc; -use alloc::alloc::Layout; use core::marker::PhantomData; use core::mem::MaybeUninit; use core::ptr; @@ -41,29 +41,31 @@ impl Heap { } impl Storage for Heap { + /// Creates a new instance with capacity for exactly the given number of elements. + /// + /// # Panics + /// + /// Panics if the memory allocation failed. fn new(capacity: u32) -> Self { + Self::try_new(capacity).unwrap_or_else(|| panic!("failed to allocate {capacity} elements of {typ}", typ = core::any::type_name::())) + } + + /// Tries to create a new instance with capacity for exactly the given number of elements. + /// + /// Returns `None` if the memory allocation failed. + fn try_new(capacity: u32) -> Option { let storage = if capacity > 0 { - let layout = Self::layout(capacity).unwrap_or_else(|| { - panic!( - "{capacity} elements of {typ} would overflow isize::MAX", - typ = core::any::type_name::(), - ) - }); + let layout = Self::layout(capacity)?; // SAFETY: `layout` has a non-zero size (because `capacity` is > 0) - NonNull::new(unsafe { alloc(layout) }).unwrap_or_else(|| { - panic!( - "failed to allocate {capacity} elements of {typ}", - typ = core::any::type_name::() - ) - }) + NonNull::new(unsafe { alloc(layout) })? } else { NonNull::dangling() }; - Self { + Some(Self { capacity, elements: storage.cast::(), _marker: PhantomData, - } + }) } fn capacity(&self) -> u32 { @@ -145,20 +147,11 @@ mod tests { if capacity > 2 { let partial_slice = unsafe { instance.subslice(1, 2) }; assert_eq!(partial_slice.len(), 1); - assert_eq!( - partial_slice as *const T, - instance.elements.as_ptr().wrapping_add(1) - ); + assert_eq!(partial_slice as *const T, instance.elements.as_ptr().wrapping_add(1)); let end_slice = unsafe { instance.subslice(capacity - 1, capacity) }; assert_eq!(end_slice.len(), 1); - assert_eq!( - end_slice as *const T, - instance - .elements - .as_ptr() - .wrapping_add(capacity as usize - 1) - ); + assert_eq!(end_slice as *const T, instance.elements.as_ptr().wrapping_add(capacity as usize - 1)); } } @@ -185,20 +178,11 @@ mod tests { if capacity >= 2 { let partial_slice = unsafe { instance.subslice_mut(1, 2) }; assert_eq!(partial_slice.len(), 1); - assert_eq!( - partial_slice as *mut T, - instance.elements.as_ptr().wrapping_add(1) - ); + assert_eq!(partial_slice as *mut T, instance.elements.as_ptr().wrapping_add(1)); let end_slice = unsafe { instance.subslice_mut(capacity - 1, capacity) }; assert_eq!(end_slice.len(), 1); - assert_eq!( - end_slice as *mut T, - instance - .elements - .as_ptr() - .wrapping_add(capacity as usize - 1) - ); + assert_eq!(end_slice as *mut T, instance.elements.as_ptr().wrapping_add(capacity as usize - 1)); } } @@ -219,30 +203,15 @@ mod tests { assert_eq!(first_element.as_ptr(), instance.elements.as_ptr()); let last_element = unsafe { instance.element(capacity - 1) }; - assert_eq!( - last_element.as_ptr(), - instance - .elements - .as_ptr() - .wrapping_add(capacity as usize - 1) - ); + assert_eq!(last_element.as_ptr(), instance.elements.as_ptr().wrapping_add(capacity as usize - 1)); } if capacity >= 2 { let second_element = unsafe { instance.element(1) }; - assert_eq!( - second_element.as_ptr(), - instance.elements.as_ptr().wrapping_add(1) - ); + assert_eq!(second_element.as_ptr(), instance.elements.as_ptr().wrapping_add(1)); let last_element = unsafe { instance.element(capacity - 2) }; - assert_eq!( - last_element.as_ptr(), - instance - .elements - .as_ptr() - .wrapping_add(capacity as usize - 2) - ); + assert_eq!(last_element.as_ptr(), instance.elements.as_ptr().wrapping_add(capacity as usize - 2)); } } @@ -263,30 +232,15 @@ mod tests { assert_eq!(first_element.as_ptr(), instance.elements.as_ptr()); let last_element = unsafe { instance.element_mut(capacity - 1) }; - assert_eq!( - last_element.as_ptr(), - instance - .elements - .as_ptr() - .wrapping_add(capacity as usize - 1) - ); + assert_eq!(last_element.as_ptr(), instance.elements.as_ptr().wrapping_add(capacity as usize - 1)); } if capacity >= 2 { let second_element = unsafe { instance.element_mut(1) }; - assert_eq!( - second_element.as_ptr(), - instance.elements.as_ptr().wrapping_add(1) - ); + assert_eq!(second_element.as_ptr(), instance.elements.as_ptr().wrapping_add(1)); let last_element = unsafe { instance.element_mut(capacity - 2) }; - assert_eq!( - last_element.as_ptr(), - instance - .elements - .as_ptr() - .wrapping_add(capacity as usize - 2) - ); + assert_eq!(last_element.as_ptr(), instance.elements.as_ptr().wrapping_add(capacity as usize - 2)); } } diff --git a/src/containers/storage/inline.rs b/src/containers/storage/inline.rs index 7c09ded1..b6555426 100644 --- a/src/containers/storage/inline.rs +++ b/src/containers/storage/inline.rs @@ -31,6 +31,11 @@ impl Inline { } impl Storage for Inline { + /// Creates a new instance. + /// + /// # Panics + /// + /// Panics if and only if `capacity != CAPACITY`. fn new(capacity: u32) -> Self { let () = Self::CHECK_CAPACITY; @@ -40,6 +45,21 @@ impl Storage for Inline { } } + /// Tries to create a new instance. + /// + /// Returns `None` if and only if `capacity != CAPACITY`. + fn try_new(capacity: u32) -> Option { + let () = Self::CHECK_CAPACITY; + + if capacity as usize == CAPACITY { + Some(Self { + elements: [const { MaybeUninit::uninit() }; CAPACITY], + }) + } else { + None + } + } + fn capacity(&self) -> u32 { let () = Self::CHECK_CAPACITY; diff --git a/src/containers/storage/mod.rs b/src/containers/storage/mod.rs index aa4e0525..79808611 100644 --- a/src/containers/storage/mod.rs +++ b/src/containers/storage/mod.rs @@ -20,10 +20,26 @@ pub use self::inline::Inline; use core::mem::MaybeUninit; /// Interface to abstract over element storage kinds. +/// +/// # Panics +/// +/// With the exception of [`new`](Storage::new), the methods in this trait are *not* allowed to panic when `cfg(debug_assertions)` is not enabled. +/// Implementors should use `debug_assert!` to check that preconditions are fulfilled. pub trait Storage { /// Creates a new instance with enough capacity for the given number of elements. + /// + /// # Panics + /// + /// This method is allowed to panic when `capacity` is invalid, or when not enough memory could be allocated. fn new(capacity: u32) -> Self; + /// Tries to create a new instance with enough capacity for the given number of elements. + /// + /// Returns `None` if the allocation failed for any reason. + fn try_new(capacity: u32) -> Option + where + Self: Sized; + /// Returns the allocated capacity. fn capacity(&self) -> u32; @@ -64,7 +80,17 @@ mod test_utils { impl Storage for Vec> { fn new(capacity: u32) -> Self { - (0..capacity).map(|_| MaybeUninit::zeroed()).collect() + Self::try_new(capacity).unwrap_or_else(|| panic!("failed to allocate for {capacity} elements")) + } + + fn try_new(capacity: u32) -> Option + where + Self: Sized, + { + let mut instance = vec![]; + instance.try_reserve_exact(capacity as usize).ok()?; + instance.extend((0..capacity).map(|_| MaybeUninit::zeroed())); + Some(instance) } fn capacity(&self) -> u32 {