use core::{fmt, ops::Range};
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct FixedFilename<const N: usize> {
pub data: [u8; N],
pub len: usize,
}
impl<const N: usize> FixedFilename<N> {
pub const fn empty() -> Self {
Self {
data: [0; N],
len: 0,
}
}
pub const fn with_size(size: usize) -> Self {
assert!(size <= N);
Self {
data: [0; N],
len: size,
}
}
pub fn as_str(&self) -> &str {
core::str::from_utf8(self.as_bytes()).expect("FixedFilename contains invalid UTF-8")
}
pub fn try_as_str(&self) -> Result<&str, core::str::Utf8Error> {
core::str::from_utf8(self.as_bytes())
}
pub fn allocate(&mut self, bytes: usize) {
let len = self.len;
assert!(bytes + len <= N);
self.len += bytes;
}
pub fn as_bytes(&self) -> &[u8] {
&self.data[0..self.len]
}
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
&mut self.data[0..self.len]
}
pub fn truncate(&mut self, new_size: usize) {
assert!(new_size <= N);
self.len = new_size;
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn push_slice(&mut self, slice: &[u8]) -> Range<usize> {
assert!(self.len + slice.len() <= self.data.len());
let start = self.len;
self.len += slice.len();
self.data[start..self.len].copy_from_slice(slice);
start..self.len
}
pub fn push_byte(&mut self, b: u8) -> usize {
assert!(self.len < N);
self.data[self.len] = b;
self.len += 1;
self.len - 1
}
pub fn try_push_slice(&mut self, slice: &[u8]) -> Option<Range<usize>> {
if self.len + slice.len() > N {
return None;
}
Some(self.push_slice(slice))
}
pub fn try_push_byte(&mut self, b: u8) -> Option<usize> {
if self.len >= N {
return None;
}
Some(self.push_byte(b))
}
pub fn remaining_capacity(&self) -> usize {
N - self.len
}
}
impl<const N: usize> fmt::Debug for FixedFilename<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.try_as_str() {
Ok(s) => f.debug_tuple("FixedFilename").field(&s).finish(),
Err(_) => f
.debug_tuple("FixedFilename")
.field(&self.as_bytes())
.finish(),
}
}
}
impl<const N: usize> fmt::Display for FixedFilename<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.try_as_str() {
Ok(s) => f.write_str(s),
Err(_) => write!(f, "{:?}", self.as_bytes()),
}
}
}
impl<const N: usize> From<&[u8]> for FixedFilename<N> {
fn from(value: &[u8]) -> Self {
assert!(value.len() <= N);
let mut str = FixedFilename::with_size(value.len());
str.data[0..value.len()].copy_from_slice(value);
str
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_as_str_rejects_invalid_utf8() {
let mut filename = FixedFilename::<10>::with_size(10);
filename.as_bytes_mut()[0] = 0xff;
assert!(filename.try_as_str().is_err());
}
#[test]
fn debug_does_not_panic_on_invalid_utf8() {
use core::fmt::Write as _;
struct Sink;
impl core::fmt::Write for Sink {
fn write_str(&mut self, _: &str) -> core::fmt::Result {
Ok(())
}
}
let mut filename = FixedFilename::<10>::with_size(10);
filename.as_bytes_mut()[0] = 0xff;
write!(Sink, "{:?}", filename).unwrap();
write!(Sink, "{}", filename).unwrap();
}
#[test]
fn try_as_str_round_trips_valid_utf8() {
let name = FixedFilename::<64>::from(b"readme.txt".as_slice());
assert_eq!(name.try_as_str().unwrap(), "readme.txt");
assert_eq!(name.as_str(), "readme.txt");
}
#[test]
#[should_panic(expected = "invalid UTF-8")]
fn as_str_panics_on_invalid_utf8() {
let mut filename = FixedFilename::<10>::with_size(10);
filename.as_bytes_mut()[0] = 0xff;
let _ = filename.as_str();
}
}