nvec 0.10.0

N-vectors and N-strings.
Documentation
// Copyright 2025-2026 Gabriel Bjørnager Jensen.
//
// SPDX: MIT OR Apache-2.0

//! The [`NString`] type.

mod convert;
mod cmp;
mod fmt;
mod iter;
mod oct;
mod ops;
mod test;

use core::borrow::{Borrow, BorrowMut};
use core::hash::{Hash, Hasher};
use core::str::{self, FromStr};
use nvec::cold_path;
use nvec::n_vec::{NVec, TryReserveError};

/// An N-string.
#[derive(Clone)]
pub struct NString<const N: usize>(NVec<u8, N>);

impl<const N: usize> NString<N> {
	/// Constructs a new, empty N-string.
	#[inline]
	#[must_use]
	pub const fn new() -> Self {
		let bytes = NVec::new();

		// SAFETY: This vector is guaranteed to be zero-
		// length, in which case all buffer values are
		// valid.
		unsafe { Self::from_utf8_unchecked(bytes) }
	}

	/// Attempts to construct a new N-string with a
	/// minimum capacity.
	///
	/// # Errors
	///
	/// As all N-strings are capacity-constrained by
	/// `N`, this constructer merely tests if the
	/// provided capacity is within the bounds of the
	/// specific `NVec` instance.
	#[inline]
	pub const fn try_with_capacity(capacity: usize) -> Result<Self, TryReserveError> {
		// NOTE: We avoid relying on the logic of `NVec` so
		// as to also avoid `unsafe`.

		if capacity <= N {
			Ok(Self::new())
		} else {
			Err(TryReserveError)
		}
	}

	/// Attempts to reserve capacity for additional
	/// octets.
	///
	/// # Errors
	///
	/// As the total capacity of the string cannot be
	/// grown, this method will simply test if the
	/// remaining capacity is sufficient to store the
	/// specific octet count: If the space is
	/// insufficient, this method will return an [`Err`]
	/// instance.
	pub const fn try_reserve(&self, extra: usize) -> Result<(), TryReserveError> {
		self.0.try_reserve(extra)
	}

	/// Attempts to push a character into the string.
	///
	/// # Errors
	///
	/// This method will return an [`Err`] instance if
	/// the provided character could not be fitted.
	pub const fn try_push(&mut self, c: char) -> Result<(), TryReserveError> {
		let mut buf = [0; 4];
		let s = c.encode_utf8(&mut buf);

		self.try_push_str(s)
	}

	/// Attempts to push a substring into the string.
	///
	/// # Errors
	///
	/// This method will return an [`Err`] instance if
	/// the provided string could not be fitted.
	pub const fn try_push_str(&mut self, s: &str) -> Result<(), TryReserveError> {
		if let Err(e) = self.try_reserve(s.len()) {
			return Err(e);
		}

		let slot = {
			let base = self.as_mut_ptr();

			// SAFETY: `len` is guaranteed to, at most, be one
			// past the end of the buffer.
			unsafe { base.add(self.len()) }
		};

		// SAFETY:
		//
		// * The source pointer, deriving from a reference,
		//   is guaranteed to be valid for reads. The length
		//   comes from the same reference and guarantees
		//   the object size.
		//
		// * The destination pointer, initially deriving
		//   from a mutable reference, is guaranteed to be
		//   valid for writes. We have made sure that the
		//   spare capacity is sufficient for this write to
		//   be made.
		//
		// * Both pointers, deriving from references and/or
		//   being offset only with `add`, are guaranteed
		//   to be aligned.
		//
		// * The two pointers cannot overlap as both a pro-
		//   vided by the caller with one being mutable (and
		//   thus exclusive.)
		unsafe { slot.copy_from_nonoverlapping(s.as_ptr(), s.len()); }

		// SAFETY: We have reserved this extra capacity.
		unsafe { self.set_len(self.len() + s.len()) };

		Ok(())
	}

	/// Truncates the string.
	///
	/// If the provided length is greater than or equal
	/// to the string's current length, the string
	/// remains unchanged.
	#[inline]
	pub const fn truncate(&mut self, len: usize) {
		if len >= self.len() {
			cold_path();
			return;
		}

		// NOTE: Do not call `Drop::drop` on the elements.

		// SAFETY: We have above tested that the new length
		// is less than the current length, so we cannot
		// expose any uninitialised octets.
		unsafe { self.set_len(len) };
	}

	/// Clears the string.
	#[inline]
	pub const fn clear(&mut self) {
		self.truncate(0)
	}

	/// Retrieves the total capacity of the string.
	#[inline]
	#[must_use]
	pub const fn capacity(&self) -> usize {
		self.0.capacity()
	}

	/// Computes the amount of remaining capacity in the
	/// string.
	#[inline]
	#[must_use]
	pub const fn remaining_capacity(&self) -> usize {
		self.0.remaining_capacity()
	}

	/// Overwrites the length of the string.
	///
	/// # Safety
	///
	/// The following prerequisites must be met when
	/// calling this method:
	///
	/// * `len` may not be greater than the capacity of
	///   the string (i.e. `N`).
	/// * The first `len` elements of the string buffer
	///   must already be denote a valid UTF-8 sequence.
	///
	/// Behaviour is undefined if any of these
	/// conditions are
	#[inline(always)]
	#[track_caller]
	pub const unsafe fn set_len(&mut self, len: usize) {
		// SAFETY: Caller guarantees that the length
		// doesn't exceed the capacity.
		unsafe { self.0.set_len(len) };
	}

	/// Retrieves the length of the string.
	#[inline]
	#[must_use]
	pub const fn len(&self) -> usize {
		self.0.len()
	}

	/// Tests if the string is empty.
	#[inline]
	#[must_use]
	pub const fn is_empty(&self) -> bool {
		self.len() == 0
	}

	/// Copies the N-string.
	///
	/// As [`NString`] is a wrapper for [`NVec`], it
	/// can't implement [`Copy`] (due to the latter also
	/// not implementing it.) This method does the oper-
	/// ation expected from a copy.
	#[inline(always)]
	#[must_use]
	pub const fn copied(&self) -> Self {
		let bytes = self.to_bytes();

		// SAFETY: The bytes themselves come from an exist-
		// ing N-string.
		unsafe { Self::from_utf8_unchecked(bytes) }
	}
}

impl<const N: usize> Borrow<str> for NString<N> {
	#[inline]
	fn borrow(&self) -> &str {
		self
	}
}

impl<const N: usize> BorrowMut<str> for NString<N> {
	#[inline]
	fn borrow_mut(&mut self) -> &mut str {
		self
	}
}

impl<const N: usize> Default for NString<N> {
	#[inline]
	fn default() -> Self {
		Self::new()
	}
}

impl<const N: usize> FromStr for NString<N> {
	type Err = TryReserveError;

	/// See [`NString::try_from_str`].
	#[inline]
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		Self::try_from_str(s)
	}
}

impl<const N: usize> Hash for NString<N> {
	#[inline]
	fn hash<H: Hasher>(&self, state: &mut H) {
		self.as_str().hash(state);
	}
}