use core::{fmt, str};
#[derive(Clone, PartialEq, Eq)]
pub struct BufStr<const N: usize> {
pub bytes: [u8; N],
pub len: u16,
}
impl<const N: usize> Default for BufStr<N> {
#[inline(always)]
fn default() -> Self {
Self {
bytes: [0; N],
len: 0,
}
}
}
impl<const N: usize> BufStr<N> {
pub const SIZE: usize = N;
#[inline]
pub fn new(s: &str) -> Self {
let mut bytes = [0u8; N];
let len = copy_str_prefix(&mut bytes, s, N) as u16;
Self { bytes, len }
}
#[inline]
pub fn from_bytes(bytes: &[u8]) -> Self {
let mut arr = [0u8; N];
let copy_len = bytes.len().min(N);
arr[..copy_len].copy_from_slice(&bytes[..copy_len]);
let len = arr[..copy_len]
.iter()
.position(|&b| b == 0)
.unwrap_or(copy_len);
Self {
bytes: arr,
len: len as u16,
}
}
#[inline(always)]
pub fn len(&self) -> usize {
(self.len as usize).min(N)
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn as_str(&self) -> &str {
match str::from_utf8(self.as_bytes()) {
Ok(s) => s,
Err(e) => handle_invalid_utf8(self.as_bytes(), e),
}
}
#[inline(always)]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len()]
}
}
impl<const N: usize> fmt::Write for BufStr<N> {
#[inline(never)]
fn write_str(&mut self, s: &str) -> fmt::Result {
let current = self.len();
let remaining = N.saturating_sub(current);
if remaining == 0 {
return Ok(());
}
let written = copy_str_prefix(&mut self.bytes[current..], s, remaining);
self.len = (current + written) as u16;
Ok(())
}
}
impl<const N: usize> fmt::Display for BufStr<N> {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<const N: usize> fmt::Debug for BufStr<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.as_str())
}
}
#[cfg(feature = "serde")]
impl<const N: usize> serde::Serialize for BufStr<N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.as_str().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, const N: usize> serde::Deserialize<'de> for BufStr<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: &str = serde::Deserialize::deserialize(deserializer)?;
Ok(BufStr::new(s))
}
}
#[cfg(feature = "defmt")]
impl<const N: usize> defmt::Format for BufStr<N> {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "{}", self.as_str());
}
}
#[inline(never)]
fn copy_str_prefix(dst: &mut [u8], src: &str, max_len: usize) -> usize {
let bytes = src.as_bytes();
let mut len = bytes.len().min(max_len).min(dst.len());
while !src.is_char_boundary(len) {
len -= 1;
}
if len != 0 {
dst[..len].copy_from_slice(&bytes[..len]);
}
len
}
#[cold]
#[inline(never)]
fn handle_invalid_utf8(slice: &[u8], e: core::str::Utf8Error) -> &str {
let valid = e.valid_up_to();
if valid == 0 {
"\u{FFFD}"
} else {
str::from_utf8(&slice[..valid]).unwrap_or("\u{FFFD}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn as_str_valid() {
let s = BufStr::<16>::new("hello");
assert_eq!(s.as_str(), "hello");
assert_eq!(s.len(), 5);
assert_eq!(BufStr::<8>::default().as_str(), "");
assert!(BufStr::<8>::default().is_empty());
}
#[test]
fn as_str_invalid_leading_byte() {
let s = BufStr::<8>::from_bytes(&[0xFF, b'a']);
assert_eq!(s.as_str(), "\u{FFFD}");
assert_eq!(s.len(), 2);
}
#[test]
fn as_str_valid_prefix_then_garbage() {
let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xFF, b'!']);
assert_eq!(s.as_str(), "hi");
assert_eq!(s.len(), 4);
}
#[test]
fn as_str_truncated_multibyte_at_start() {
let s = BufStr::<8>::from_bytes(&[0xE2, 0x82]);
assert_eq!(s.as_str(), "\u{FFFD}");
}
#[test]
fn as_str_truncated_multibyte_after_valid_prefix() {
let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xE2, 0x82]);
assert_eq!(s.as_str(), "hi");
}
#[test]
fn as_str_stops_at_nul() {
let s = BufStr::<8>::from_bytes(b"ab\0cd");
assert_eq!(s.as_str(), "ab");
assert_eq!(s.as_bytes(), b"ab");
assert_eq!(s.len(), 2);
}
#[test]
fn from_bytes_zero_padded() {
let mut raw = [0u8; 8];
raw[..5].copy_from_slice(b"hello");
let s = BufStr::<8>::from_bytes(&raw);
assert_eq!(s.as_str(), "hello");
assert_eq!(s.len(), 5);
}
#[test]
fn new_truncates_on_char_boundary() {
assert_eq!(BufStr::<2>::new("€").as_str(), "");
assert_eq!(BufStr::<2>::new("€").len(), 0);
assert_eq!(BufStr::<3>::new("€").as_str(), "€");
assert_eq!(BufStr::<3>::new("€").len(), 3);
assert_eq!(BufStr::<4>::new("a€b").as_str(), "a€");
assert_eq!(BufStr::<4>::new("a€b").len(), 4);
assert_eq!(BufStr::<5>::new("a€b").as_str(), "a€b");
assert_eq!(BufStr::<5>::new("a€b").len(), 5);
}
#[test]
fn write_str_appends_and_truncates() {
use core::fmt::Write;
let mut s = BufStr::<5>::default();
write!(&mut s, "hi").unwrap();
assert_eq!(s.len(), 2);
write!(&mut s, "€").unwrap(); assert_eq!(s.as_str(), "hi€");
assert_eq!(s.len(), 5);
write!(&mut s, "x").unwrap(); assert_eq!(s.as_str(), "hi€");
assert_eq!(s.len(), 5);
}
}