#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(not(feature = "std"))]
use core as std;
use std::{borrow, error, fmt, hash, mem, ops, ptr, str};
#[derive(Copy, Clone, Eq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct FStr<const N: usize> {
inner: [u8; N],
}
impl<const N: usize> FStr<N> {
pub const LENGTH: usize = N;
pub const fn as_str(&self) -> &str {
debug_assert!(str::from_utf8(&self.inner).is_ok());
unsafe { str::from_utf8_unchecked(&self.inner) }
}
pub const fn as_mut_str(&mut self) -> &mut str {
debug_assert!(str::from_utf8(&self.inner).is_ok());
unsafe { str::from_utf8_unchecked_mut(&mut self.inner) }
}
pub const fn as_bytes(&self) -> &[u8; N] {
&self.inner
}
pub const fn into_inner(self) -> [u8; N] {
self.inner
}
pub const fn from_inner(utf8_bytes: [u8; N]) -> Result<Self, str::Utf8Error> {
match str::from_utf8(&utf8_bytes) {
Ok(_) => Ok(Self { inner: utf8_bytes }),
Err(e) => Err(e),
}
}
pub const unsafe fn from_inner_unchecked(utf8_bytes: [u8; N]) -> Self {
debug_assert!(str::from_utf8(&utf8_bytes).is_ok());
Self { inner: utf8_bytes }
}
pub const fn from_str_unwrap(s: &str) -> Self {
match Self::try_from_str(s) {
Ok(t) => t,
_ => panic!("invalid byte length"),
}
}
const fn try_from_str(s: &str) -> Result<Self, LengthError> {
match Self::copy_slice_to_array(s.as_bytes()) {
Ok(inner) => Ok(unsafe { Self::from_inner_unchecked(inner) }),
Err(e) => Err(e),
}
}
const fn try_from_slice(s: &[u8]) -> Result<Self, FromSliceError> {
match Self::copy_slice_to_array(s) {
Ok(inner) => match Self::from_inner(inner) {
Ok(t) => Ok(t),
Err(e) => Err(FromSliceError {
kind: FromSliceErrorKind::Utf8(e),
}),
},
Err(e) => Err(FromSliceError {
kind: FromSliceErrorKind::Length(e),
}),
}
}
pub const fn from_str_lossy(s: &str, filler: u8) -> Self {
if N == 0 {
return Self::from_ascii_filler(filler); }
assert!(filler.is_ascii(), "filler byte must represent ASCII char");
let len = if s.len() <= N {
s.len()
} else if is_utf8_char_boundary(s.as_bytes()[N]) {
N
} else if is_utf8_char_boundary(s.as_bytes()[N - 1]) {
N - 1
} else if is_utf8_char_boundary(s.as_bytes()[N - 2]) {
N - 2
} else if is_utf8_char_boundary(s.as_bytes()[N - 3]) {
N - 3
} else {
unreachable!() };
let mut inner = [filler; N];
debug_assert!(len <= s.len() && len <= inner.len());
unsafe { ptr::copy_nonoverlapping(s.as_ptr(), inner.as_mut_ptr(), len) };
unsafe { Self::from_inner_unchecked(inner) }
}
pub const fn from_ascii_filler(filler: u8) -> Self {
assert!(filler.is_ascii(), "filler byte must represent ASCII char");
unsafe { Self::from_inner_unchecked([filler; N]) }
}
#[doc(hidden)]
#[deprecated(since = "0.2.12", note = "renamed to `from_ascii_filler`")]
pub const fn repeat(filler: u8) -> Self {
Self::from_ascii_filler(filler)
}
pub fn slice_to_terminator(&self, terminator: char) -> &str {
match self.find(terminator) {
Some(i) => &self[..i],
_ => self,
}
}
#[doc(hidden)]
#[deprecated(since = "0.2.13", note = "use `writer_at(0)` instead")]
pub fn writer(&mut self) -> Cursor<&mut Self> {
self.writer_at(0)
}
pub fn writer_at(&mut self, index: usize) -> Cursor<&mut Self> {
Cursor::with_position(index, self).expect("index must point to char boundary")
}
pub fn from_fmt(args: fmt::Arguments<'_>, filler: u8) -> Result<Self, fmt::Error> {
assert!(filler.is_ascii(), "filler byte must represent ASCII char");
struct Writer<'s>(&'s mut [mem::MaybeUninit<u8>]);
impl fmt::Write for Writer<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
if s.len() <= self.0.len() {
let written;
(written, self.0) = mem::take(&mut self.0).split_at_mut(s.len());
written.copy_from_slice(unsafe {
mem::transmute::<&[u8], &[mem::MaybeUninit<u8>]>(s.as_bytes())
});
Ok(())
} else {
Err(fmt::Error)
}
}
}
let mut inner = [const { mem::MaybeUninit::uninit() }; N];
let mut w = Writer(inner.as_mut_slice());
if fmt::Write::write_fmt(&mut w, args).is_ok() {
w.0.fill(mem::MaybeUninit::new(filler));
Ok(unsafe {
Self::from_inner_unchecked(
mem::transmute_copy::<[mem::MaybeUninit<u8>; N], [u8; N]>(&inner),
)
})
} else {
const _STATIC_ASSERT: () = assert!(!mem::needs_drop::<u8>(), "u8 never needs drop");
Err(fmt::Error)
}
}
}
impl<const N: usize> FStr<N> {
const fn copy_slice_to_array(s: &[u8]) -> Result<[u8; N], LengthError> {
if s.len() == N {
Ok(unsafe { *s.as_ptr().cast::<[u8; N]>() })
} else {
Err(LengthError {
actual: s.len(),
expected: N,
})
}
}
}
impl<const N: usize> ops::Deref for FStr<N> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl<const N: usize> ops::DerefMut for FStr<N> {
fn deref_mut(&mut self) -> &mut str {
self.as_mut_str()
}
}
impl<const N: usize> Default for FStr<N> {
fn default() -> Self {
Self::from_ascii_filler(b' ')
}
}
impl<const N: usize> fmt::Debug for FStr<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(
match FStr::<32>::from_fmt(format_args!("FStr<{}>", N), b'\0') {
Ok(ref buffer) => buffer.slice_to_terminator('\0'),
Err(_) => "FStr", },
)
.field("inner", &self.as_str())
.finish()
}
}
impl<const N: usize> fmt::Display for FStr<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.as_str(), f)
}
}
impl<const N: usize> PartialEq for FStr<N> {
fn eq(&self, other: &FStr<N>) -> bool {
self.as_str().eq(other.as_str())
}
}
impl<const N: usize> PartialEq<str> for FStr<N> {
fn eq(&self, other: &str) -> bool {
self.as_str().eq(other)
}
}
impl<const N: usize> PartialEq<FStr<N>> for str {
fn eq(&self, other: &FStr<N>) -> bool {
self.eq(other.as_str())
}
}
impl<const N: usize> PartialEq<&str> for FStr<N> {
fn eq(&self, other: &&str) -> bool {
self.as_str().eq(*other)
}
}
impl<const N: usize> PartialEq<FStr<N>> for &str {
fn eq(&self, other: &FStr<N>) -> bool {
self.eq(&other.as_str())
}
}
impl<const N: usize> hash::Hash for FStr<N> {
fn hash<H: hash::Hasher>(&self, hasher: &mut H) {
self.as_str().hash(hasher)
}
}
impl<const N: usize> borrow::Borrow<str> for FStr<N> {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl<const N: usize> borrow::BorrowMut<str> for FStr<N> {
fn borrow_mut(&mut self) -> &mut str {
self.as_mut_str()
}
}
impl<const N: usize> AsRef<str> for FStr<N> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<const N: usize> AsMut<str> for FStr<N> {
fn as_mut(&mut self) -> &mut str {
self.as_mut_str()
}
}
impl<const N: usize> AsRef<[u8]> for FStr<N> {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl<const N: usize> From<FStr<N>> for [u8; N] {
fn from(value: FStr<N>) -> Self {
value.into_inner()
}
}
impl<const N: usize> str::FromStr for FStr<N> {
type Err = LengthError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from_str(s)
}
}
impl<const N: usize> TryFrom<[u8; N]> for FStr<N> {
type Error = str::Utf8Error;
fn try_from(value: [u8; N]) -> Result<Self, Self::Error> {
Self::from_inner(value)
}
}
impl<const N: usize> TryFrom<&[u8; N]> for FStr<N> {
type Error = str::Utf8Error;
fn try_from(value: &[u8; N]) -> Result<Self, Self::Error> {
Self::from_inner(*value)
}
}
impl<const N: usize> TryFrom<&[u8]> for FStr<N> {
type Error = FromSliceError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_slice(value)
}
}
#[derive(Debug)]
pub struct Cursor<T> {
inner: T,
pos: usize,
}
impl<T> Cursor<T> {
pub fn get_ref(&self) -> &T {
&self.inner
}
pub fn position(&self) -> usize {
self.pos
}
}
impl<T: AsRef<str>> Cursor<T> {
fn with_position(pos: usize, inner: T) -> Option<Self> {
match inner.as_ref().is_char_boundary(pos) {
true => Some(Self { inner, pos }),
false => None,
}
}
}
impl<T: AsMut<str>> fmt::Write for Cursor<T> {
fn write_str(&mut self, s: &str) -> fmt::Result {
match self.inner.as_mut().get_mut(self.pos..(self.pos + s.len())) {
Some(written) => {
unsafe { written.as_bytes_mut() }.copy_from_slice(s.as_bytes());
self.pos += written.len();
Ok(())
}
None => Err(fmt::Error),
}
}
}
#[derive(Copy, Eq, PartialEq, Clone, Debug)]
pub struct LengthError {
actual: usize,
expected: usize,
}
impl fmt::Display for LengthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid byte length of {} (expected: {})",
self.actual, self.expected
)
}
}
impl error::Error for LengthError {}
#[derive(Copy, Eq, PartialEq, Clone, Debug)]
pub struct FromSliceError {
kind: FromSliceErrorKind,
}
#[derive(Copy, Eq, PartialEq, Clone, Debug)]
enum FromSliceErrorKind {
Length(LengthError),
Utf8(str::Utf8Error),
}
impl fmt::Display for FromSliceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use FromSliceErrorKind::{Length, Utf8};
match self.kind {
Length(source) => write!(f, "could not convert slice to FStr: {}", source),
Utf8(source) => write!(f, "could not convert slice to FStr: {}", source),
}
}
}
impl error::Error for FromSliceError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.kind {
FromSliceErrorKind::Length(source) => Some(source),
FromSliceErrorKind::Utf8(source) => Some(source),
}
}
}
#[inline(always)]
const fn is_utf8_char_boundary(byte: u8) -> bool {
(byte as i8) >= -0x40 }
#[cfg(feature = "alloc")]
mod with_string {
use alloc::{borrow::ToOwned as _, string::String};
use super::{FStr, LengthError};
impl<const N: usize> From<FStr<N>> for String {
fn from(value: FStr<N>) -> Self {
value.as_str().to_owned()
}
}
impl<const N: usize> TryFrom<String> for FStr<N> {
type Error = LengthError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl<const N: usize> PartialEq<String> for FStr<N> {
fn eq(&self, other: &String) -> bool {
self.as_str().eq(other)
}
}
impl<const N: usize> PartialEq<FStr<N>> for String {
fn eq(&self, other: &FStr<N>) -> bool {
self.eq(other.as_str())
}
}
}
#[cfg(test)]
mod tests {
use super::FStr;
#[test]
fn eq() {
let x = FStr::from_inner(*b"hello").unwrap();
assert_eq!(x, x);
assert_eq!(&x, &x);
assert_eq!(x, FStr::from_inner(*b"hello").unwrap());
assert_eq!(FStr::from_inner(*b"hello").unwrap(), x);
assert_eq!(&x, &FStr::from_inner(*b"hello").unwrap());
assert_eq!(&FStr::from_inner(*b"hello").unwrap(), &x);
assert_eq!(x, "hello");
assert_eq!("hello", x);
assert_eq!(&x, "hello");
assert_eq!("hello", &x);
assert_eq!(&x[..], "hello");
assert_eq!("hello", &x[..]);
assert_eq!(&x as &str, "hello");
assert_eq!("hello", &x as &str);
assert_ne!(x, FStr::from_inner(*b"world").unwrap());
assert_ne!(FStr::from_inner(*b"world").unwrap(), x);
assert_ne!(&x, &FStr::from_inner(*b"world").unwrap());
assert_ne!(&FStr::from_inner(*b"world").unwrap(), &x);
assert_ne!(x, "world");
assert_ne!("world", x);
assert_ne!(&x, "world");
assert_ne!("world", &x);
assert_ne!(&x[..], "world");
assert_ne!("world", &x[..]);
assert_ne!(&x as &str, "world");
assert_ne!("world", &x as &str);
#[cfg(feature = "alloc")]
{
use alloc::{borrow::ToOwned as _, string::String, string::ToString as _};
assert_eq!(x, String::from("hello"));
assert_eq!(String::from("hello"), x);
assert_eq!(String::from(x), String::from("hello"));
assert_eq!(String::from("hello"), String::from(x));
assert_ne!(x, String::from("world"));
assert_ne!(String::from("world"), x);
assert_ne!(String::from(x), String::from("world"));
assert_ne!(String::from("world"), String::from(x));
assert_eq!(x.to_owned(), String::from("hello"));
assert_eq!(String::from("hello"), x.to_owned());
assert_eq!(x.to_string(), String::from("hello"));
assert_eq!(String::from("hello"), x.to_string());
}
}
#[test]
fn from_str_lossy_edge() {
assert!(FStr::<0>::from_str_lossy("", b' ').is_empty());
assert!(FStr::<0>::from_str_lossy("pizza", b' ').is_empty());
assert!(FStr::<0>::from_str_lossy("🥹🥹", b' ').is_empty());
assert_eq!(FStr::<1>::from_str_lossy("", b' '), " ");
assert_eq!(FStr::<1>::from_str_lossy("pizza", b' '), "p");
assert_eq!(FStr::<1>::from_str_lossy("🥹🥹", b' '), " ");
assert_eq!(FStr::<2>::from_str_lossy("🥹🥹", b' '), " ");
assert_eq!(FStr::<3>::from_str_lossy("🥹🥹", b' '), " ");
assert_eq!(FStr::<4>::from_str_lossy("🥹🥹", b' '), "🥹");
assert_eq!(FStr::<5>::from_str_lossy("🥹🥹", b' '), "🥹 ");
assert_eq!(FStr::<6>::from_str_lossy("🥹🥹", b' '), "🥹 ");
assert_eq!(FStr::<7>::from_str_lossy("🥹🥹", b' '), "🥹 ");
assert_eq!(FStr::<8>::from_str_lossy("🥹🥹", b' '), "🥹🥹");
assert_eq!(FStr::<9>::from_str_lossy("🥹🥹", b' '), "🥹🥹 ");
}
#[test]
fn from_str() {
assert!("ceremony".parse::<FStr<4>>().is_err());
assert!("strategy".parse::<FStr<12>>().is_err());
assert!("parallel".parse::<FStr<8>>().is_ok());
assert_eq!("parallel".parse::<FStr<8>>().unwrap(), "parallel");
assert!("😂".parse::<FStr<2>>().is_err());
assert!("😂".parse::<FStr<6>>().is_err());
assert!("😂".parse::<FStr<4>>().is_ok());
assert_eq!("😂".parse::<FStr<4>>().unwrap(), "😂");
}
#[test]
fn try_from_array() {
assert!(FStr::try_from(b"memory").is_ok());
assert!(FStr::try_from(*b"resort").is_ok());
assert!(FStr::try_from(&[0xff; 8]).is_err());
assert!(FStr::try_from([0xff; 8]).is_err());
}
#[test]
fn try_from_slice() {
assert!(FStr::<4>::try_from(b"memory".as_slice()).is_err());
assert!(FStr::<6>::try_from(b"memory".as_slice()).is_ok());
assert!(FStr::<8>::try_from(b"memory".as_slice()).is_err());
assert!(FStr::<7>::try_from([0xff; 8].as_slice()).is_err());
assert!(FStr::<8>::try_from([0xff; 8].as_slice()).is_err());
assert!(FStr::<9>::try_from([0xff; 8].as_slice()).is_err());
}
#[test]
fn write_str() {
use core::fmt::Write as _;
let mut a = FStr::<5>::from_ascii_filler(b' ');
assert!(write!(a.writer_at(0), "vanilla").is_err());
assert_eq!(a, " ");
let mut b = FStr::<7>::from_ascii_filler(b' ');
assert!(write!(b.writer_at(0), "vanilla").is_ok());
assert_eq!(b, "vanilla");
let mut c = FStr::<9>::from_ascii_filler(b' ');
assert!(write!(c.writer_at(0), "vanilla").is_ok());
assert_eq!(c, "vanilla ");
let mut d = FStr::<16>::from_ascii_filler(b'.');
assert!(write!(d.writer_at(0), "😂🤪😱👻").is_ok());
assert_eq!(d, "😂🤪😱👻");
assert!(write!(d.writer_at(0), "🔥").is_ok());
assert_eq!(d, "🔥🤪😱👻");
assert!(write!(d.writer_at(0), "🥺😭").is_ok());
assert_eq!(d, "🥺😭😱👻");
assert!(write!(d.writer_at(0), ".").is_err());
assert_eq!(d, "🥺😭😱👻");
let mut e = FStr::<12>::from_ascii_filler(b' ');
assert!(write!(e.writer_at(0), "{:04}/{:04}", 42, 334).is_ok());
assert_eq!(e, "0042/0334 ");
let mut w = e.writer_at(0);
assert!(write!(w, "{:02x}", 123).is_ok());
assert!(write!(w, "-{:04x}", 345).is_ok());
assert!(write!(w, "-{:04x}", 567).is_ok());
assert!(write!(w, "-{:04x}", 789).is_err());
assert_eq!(e, "7b-0159-0237");
assert!(write!(FStr::<0>::default().writer_at(0), "").is_ok());
assert!(write!(FStr::<0>::default().writer_at(0), " ").is_err());
assert!(write!(FStr::<5>::default().writer_at(5), "").is_ok());
assert!(write!(FStr::<5>::default().writer_at(5), " ").is_err());
}
#[test]
#[should_panic]
fn writer_at_index_middle_of_a_char() {
FStr::<8>::from_str_lossy("🙏", b' ').writer_at(1);
}
#[test]
#[should_panic]
fn writer_at_index_beyond_end() {
FStr::<5>::default().writer_at(7);
}
#[cfg(feature = "std")]
#[test]
fn hash_borrow() {
use std::collections::HashSet;
let mut s = HashSet::new();
s.insert(FStr::from_inner(*b"crisis").unwrap());
s.insert(FStr::from_inner(*b"eating").unwrap());
s.insert(FStr::from_inner(*b"lucent").unwrap());
assert!(s.contains("crisis"));
assert!(s.contains("eating"));
assert!(s.contains("lucent"));
assert!(!s.contains("system"));
assert!(!s.contains("unless"));
assert!(!s.contains("yellow"));
assert!(s.contains(&FStr::from_inner(*b"crisis").unwrap()));
assert!(s.contains(&FStr::from_inner(*b"eating").unwrap()));
assert!(s.contains(&FStr::from_inner(*b"lucent").unwrap()));
assert!(!s.contains(&FStr::from_inner(*b"system").unwrap()));
assert!(!s.contains(&FStr::from_inner(*b"unless").unwrap()));
assert!(!s.contains(&FStr::from_inner(*b"yellow").unwrap()));
}
#[cfg(feature = "alloc")]
#[test]
fn display_fmt() {
use alloc::format;
let a = FStr::from_inner(*b"you").unwrap();
assert_eq!(format!("{}", a), "you");
assert_eq!(format!("{:5}", a), "you ");
assert_eq!(format!("{:<6}", a), "you ");
assert_eq!(format!("{:-<7}", a), "you----");
assert_eq!(format!("{:>8}", a), " you");
assert_eq!(format!("{:^9}", a), " you ");
let b = FStr::from_inner(*b"junior").unwrap();
assert_eq!(format!("{}", b), "junior");
assert_eq!(format!("{:.3}", b), "jun");
assert_eq!(format!("{:5.3}", b), "jun ");
assert_eq!(format!("{:<6.3}", b), "jun ");
assert_eq!(format!("{:-<7.3}", b), "jun----");
assert_eq!(format!("{:>8.3}", b), " jun");
assert_eq!(format!("{:^9.3}", b), " jun ");
}
#[test]
fn from_fmt() {
let args = format_args!("vanilla");
assert!(FStr::<5>::from_fmt(args, b' ').is_err());
assert_eq!(FStr::<7>::from_fmt(args, b' ').unwrap(), "vanilla");
assert_eq!(FStr::<9>::from_fmt(args, b' ').unwrap(), "vanilla ");
assert_eq!(
FStr::<20>::from_fmt(format_args!("{:^6}", "😂🤪😱👻"), b'.').unwrap(),
" 😂🤪😱👻 .."
);
assert_eq!(
FStr::<12>::from_fmt(format_args!("{:04}/{:04}", 42, 334), b'\0').unwrap(),
"0042/0334\0\0\0"
);
assert_eq!(FStr::<0>::from_fmt(format_args!(""), b' ').unwrap(), "");
assert!(FStr::<0>::from_fmt(format_args!(" "), b' ').is_err());
}
}
#[cfg(feature = "serde")]
mod with_serde {
use super::{FStr, fmt};
use serde::{Deserializer, Serializer, de};
impl<const N: usize> serde::Serialize for FStr<N> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de, const N: usize> serde::Deserialize<'de> for FStr<N> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(VisitorImpl)
}
}
struct VisitorImpl<const N: usize>;
impl<const N: usize> de::Visitor<'_> for VisitorImpl<N> {
type Value = FStr<N>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a fixed-length string")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
value.parse().map_err(de::Error::custom)
}
fn visit_bytes<E: de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
if let Ok(inner) = value.try_into() {
if let Ok(t) = FStr::from_inner(inner) {
return Ok(t);
}
}
Err(de::Error::invalid_value(
de::Unexpected::Bytes(value),
&self,
))
}
}
#[test]
fn ser_de() {
use serde_test::Token;
let x = FStr::from_inner(*b"helloworld").unwrap();
serde_test::assert_tokens(&x, &[Token::Str("helloworld")]);
serde_test::assert_de_tokens(&x, &[Token::Bytes(b"helloworld")]);
let y = "😂🤪😱👻".parse::<FStr<16>>().unwrap();
serde_test::assert_tokens(&y, &[Token::Str("😂🤪😱👻")]);
serde_test::assert_de_tokens(
&y,
&[Token::Bytes(&[
240, 159, 152, 130, 240, 159, 164, 170, 240, 159, 152, 177, 240, 159, 145, 187,
])],
);
serde_test::assert_de_tokens_error::<FStr<5>>(
&[Token::Str("helloworld")],
"invalid byte length of 10 (expected: 5)",
);
serde_test::assert_de_tokens_error::<FStr<5>>(
&[Token::Bytes(b"helloworld")],
"invalid value: byte array, expected a fixed-length string",
);
serde_test::assert_de_tokens_error::<FStr<5>>(
&[Token::Bytes(&[b'h', b'e', b'l', b'l', 240])],
"invalid value: byte array, expected a fixed-length string",
);
}
}