1268 lines
38 KiB
Rust
1268 lines
38 KiB
Rust
// This file is part of ICU4X. For terms of use, please see the file
|
|
// called LICENSE at the top level of the ICU4X source tree
|
|
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
|
|
|
|
use crate::asciibyte::AsciiByte;
|
|
use crate::int_ops::{Aligned4, Aligned8};
|
|
use crate::ParseError;
|
|
#[cfg(feature = "alloc")]
|
|
use alloc::string::String;
|
|
use core::borrow::Borrow;
|
|
use core::fmt;
|
|
use core::ops::Deref;
|
|
use core::str::{self, FromStr};
|
|
|
|
#[repr(transparent)]
|
|
#[derive(PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Hash)]
|
|
pub struct TinyAsciiStr<const N: usize> {
|
|
bytes: [AsciiByte; N],
|
|
}
|
|
|
|
impl<const N: usize> TinyAsciiStr<N> {
|
|
/// The empty string.
|
|
pub const EMPTY: Self = Self {
|
|
bytes: [AsciiByte::B0; N],
|
|
};
|
|
|
|
#[inline]
|
|
pub const fn try_from_str(s: &str) -> Result<Self, ParseError> {
|
|
Self::try_from_utf8(s.as_bytes())
|
|
}
|
|
|
|
/// Creates a `TinyAsciiStr<N>` from the given UTF-8 slice.
|
|
/// `code_units` may contain at most `N` non-null ASCII code points.
|
|
#[inline]
|
|
pub const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
|
|
Self::try_from_utf8_inner(code_units, false)
|
|
}
|
|
|
|
/// Creates a `TinyAsciiStr<N>` from the given UTF-16 slice.
|
|
/// `code_units` may contain at most `N` non-null ASCII code points.
|
|
#[inline]
|
|
pub const fn try_from_utf16(code_units: &[u16]) -> Result<Self, ParseError> {
|
|
Self::try_from_utf16_inner(code_units, 0, code_units.len(), false)
|
|
}
|
|
|
|
/// Creates a `TinyAsciiStr<N>` from a UTF-8 slice, replacing invalid code units.
|
|
///
|
|
/// Invalid code units, as well as null or non-ASCII code points
|
|
/// (i.e. those outside the range `U+0001..=U+007F`)
|
|
/// will be replaced with the replacement byte.
|
|
///
|
|
/// The input slice will be truncated if its length exceeds `N`.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`).
|
|
pub const fn from_utf8_lossy(code_units: &[u8], replacement: u8) -> Self {
|
|
assert!(
|
|
replacement > 0 && replacement < 0x80,
|
|
"replacement must be a non-null ASCII byte (1..=127)"
|
|
);
|
|
let mut out = [0; N];
|
|
let mut i = 0;
|
|
// Ord is not available in const, so no `.min(N)`
|
|
let len = if code_units.len() > N {
|
|
N
|
|
} else {
|
|
code_units.len()
|
|
};
|
|
|
|
// Indexing is protected by the len check above
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < len {
|
|
let b = code_units[i];
|
|
if b > 0 && b < 0x80 {
|
|
out[i] = b;
|
|
} else {
|
|
out[i] = replacement;
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
Self {
|
|
// SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
|
|
bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
|
|
}
|
|
}
|
|
|
|
/// Creates a `TinyAsciiStr<N>` from a UTF-16 slice, replacing invalid code units.
|
|
///
|
|
/// Invalid code units, as well as null or non-ASCII code points
|
|
/// (i.e. those outside the range `U+0001..=U+007F`)
|
|
/// will be replaced with the replacement byte.
|
|
///
|
|
/// The input slice will be truncated if its length exceeds `N`.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`).
|
|
pub const fn from_utf16_lossy(code_units: &[u16], replacement: u8) -> Self {
|
|
assert!(
|
|
replacement > 0 && replacement < 0x80,
|
|
"replacement must be a non-null ASCII byte (1..=127)"
|
|
);
|
|
let mut out = [0; N];
|
|
let mut i = 0;
|
|
// Ord is not available in const, so no `.min(N)`
|
|
let len = if code_units.len() > N {
|
|
N
|
|
} else {
|
|
code_units.len()
|
|
};
|
|
|
|
// Indexing is protected by the len check above
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < len {
|
|
let b = code_units[i];
|
|
if b > 0 && b < 0x80 {
|
|
out[i] = b as u8;
|
|
} else {
|
|
out[i] = replacement;
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
Self {
|
|
// SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
|
|
bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
|
|
}
|
|
}
|
|
|
|
/// Attempts to parse a fixed-length byte array to a `TinyAsciiStr`.
|
|
///
|
|
/// The byte array may contain trailing NUL bytes.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use tinystr::tinystr;
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// assert_eq!(
|
|
/// TinyAsciiStr::<3>::try_from_raw(*b"GB\0"),
|
|
/// Ok(tinystr!(3, "GB"))
|
|
/// );
|
|
/// assert_eq!(
|
|
/// TinyAsciiStr::<3>::try_from_raw(*b"USD"),
|
|
/// Ok(tinystr!(3, "USD"))
|
|
/// );
|
|
/// assert!(TinyAsciiStr::<3>::try_from_raw(*b"\0A\0").is_err());
|
|
/// ```
|
|
pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, ParseError> {
|
|
Self::try_from_utf8_inner(&raw, true)
|
|
}
|
|
|
|
pub(crate) const fn try_from_utf8_inner(
|
|
code_units: &[u8],
|
|
allow_trailing_null: bool,
|
|
) -> Result<Self, ParseError> {
|
|
if code_units.len() > N {
|
|
return Err(ParseError::TooLong {
|
|
max: N,
|
|
len: code_units.len(),
|
|
});
|
|
}
|
|
|
|
let mut out = [0; N];
|
|
let mut i = 0;
|
|
let mut found_null = false;
|
|
// Indexing is protected by TinyStrError::TooLarge
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < code_units.len() {
|
|
let b = code_units[i];
|
|
|
|
if b == 0 {
|
|
found_null = true;
|
|
} else if b >= 0x80 {
|
|
return Err(ParseError::NonAscii);
|
|
} else if found_null {
|
|
// Error if there are contentful bytes after null
|
|
return Err(ParseError::ContainsNull);
|
|
}
|
|
out[i] = b;
|
|
|
|
i += 1;
|
|
}
|
|
|
|
if !allow_trailing_null && found_null {
|
|
// We found some trailing nulls, error
|
|
return Err(ParseError::ContainsNull);
|
|
}
|
|
|
|
Ok(Self {
|
|
// SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
|
|
bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
|
|
})
|
|
}
|
|
|
|
pub(crate) const fn try_from_utf16_inner(
|
|
code_units: &[u16],
|
|
start: usize,
|
|
end: usize,
|
|
allow_trailing_null: bool,
|
|
) -> Result<Self, ParseError> {
|
|
let len = end - start;
|
|
if len > N {
|
|
return Err(ParseError::TooLong { max: N, len });
|
|
}
|
|
|
|
let mut out = [0; N];
|
|
let mut i = 0;
|
|
let mut found_null = false;
|
|
// Indexing is protected by TinyStrError::TooLarge
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < len {
|
|
let b = code_units[start + i];
|
|
|
|
if b == 0 {
|
|
found_null = true;
|
|
} else if b >= 0x80 {
|
|
return Err(ParseError::NonAscii);
|
|
} else if found_null {
|
|
// Error if there are contentful bytes after null
|
|
return Err(ParseError::ContainsNull);
|
|
}
|
|
out[i] = b as u8;
|
|
|
|
i += 1;
|
|
}
|
|
|
|
if !allow_trailing_null && found_null {
|
|
// We found some trailing nulls, error
|
|
return Err(ParseError::ContainsNull);
|
|
}
|
|
|
|
Ok(Self {
|
|
// SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
|
|
bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
|
|
})
|
|
}
|
|
|
|
/// Creates a `TinyAsciiStr<N>` containing the decimal representation of
|
|
/// the given unsigned integer.
|
|
///
|
|
/// If the number of decimal digits exceeds `N`, the highest-magnitude
|
|
/// digits are truncated, and the lowest-magnitude digits are returned
|
|
/// as the error.
|
|
///
|
|
/// Note: this function takes a u32. Larger integer types should probably
|
|
/// not be stored in a `TinyAsciiStr`.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::tinystr;
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s0_4 = TinyAsciiStr::<4>::new_unsigned_decimal(0).unwrap();
|
|
/// let s456_4 = TinyAsciiStr::<4>::new_unsigned_decimal(456).unwrap();
|
|
/// let s456_3 = TinyAsciiStr::<3>::new_unsigned_decimal(456).unwrap();
|
|
/// let s456_2 = TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_err();
|
|
///
|
|
/// assert_eq!(s0_4, tinystr!(4, "0"));
|
|
/// assert_eq!(s456_4, tinystr!(4, "456"));
|
|
/// assert_eq!(s456_3, tinystr!(3, "456"));
|
|
/// assert_eq!(s456_2, tinystr!(2, "56"));
|
|
/// ```
|
|
///
|
|
/// Example with saturating the value:
|
|
///
|
|
/// ```
|
|
/// use tinystr::tinystr;
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let str_truncated =
|
|
/// TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_or_else(|s| s);
|
|
/// let str_saturated = TinyAsciiStr::<2>::new_unsigned_decimal(456)
|
|
/// .unwrap_or(tinystr!(2, "99"));
|
|
///
|
|
/// assert_eq!(str_truncated, tinystr!(2, "56"));
|
|
/// assert_eq!(str_saturated, tinystr!(2, "99"));
|
|
/// ```
|
|
pub fn new_unsigned_decimal(number: u32) -> Result<Self, Self> {
|
|
let mut bytes = [AsciiByte::B0; N];
|
|
let mut x = number;
|
|
let mut i = 0usize;
|
|
#[expect(clippy::indexing_slicing)] // in-range: i < N
|
|
while i < N && (x != 0 || i == 0) {
|
|
bytes[N - i - 1] = AsciiByte::from_decimal_digit((x % 10) as u8);
|
|
x /= 10;
|
|
i += 1;
|
|
}
|
|
if i < N {
|
|
bytes.copy_within((N - i)..N, 0);
|
|
bytes[i..N].fill(AsciiByte::B0);
|
|
}
|
|
let s = Self { bytes };
|
|
if x != 0 {
|
|
Err(s)
|
|
} else {
|
|
Ok(s)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn as_str(&self) -> &str {
|
|
// as_utf8 is valid utf8
|
|
unsafe { str::from_utf8_unchecked(self.as_utf8()) }
|
|
}
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn len(&self) -> usize {
|
|
if N <= 4 {
|
|
Aligned4::from_ascii_bytes(&self.bytes).len()
|
|
} else if N <= 8 {
|
|
Aligned8::from_ascii_bytes(&self.bytes).len()
|
|
} else {
|
|
let mut i = 0;
|
|
#[expect(clippy::indexing_slicing)] // < N is safe
|
|
while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
|
|
i += 1
|
|
}
|
|
i
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_empty(&self) -> bool {
|
|
self.bytes[0] as u8 == AsciiByte::B0 as u8
|
|
}
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn as_utf8(&self) -> &[u8] {
|
|
// Safe because `self.bytes.as_slice()` pointer-casts to `&[u8]`,
|
|
// and changing the length of that slice to self.len() < N is safe.
|
|
unsafe {
|
|
core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len())
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn all_bytes(&self) -> &[u8; N] {
|
|
// SAFETY: `self.bytes` has same size as [u8; N]
|
|
unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) }
|
|
}
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
/// Resizes a `TinyAsciiStr<N>` to a `TinyAsciiStr<M>`.
|
|
///
|
|
/// If `M < len()` the string gets truncated, otherwise only the
|
|
/// memory representation changes.
|
|
pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> {
|
|
let mut bytes = [0; M];
|
|
let mut i = 0;
|
|
// Indexing is protected by the loop guard
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < M && i < N {
|
|
bytes[i] = self.bytes[i] as u8;
|
|
i += 1;
|
|
}
|
|
// `self.bytes` only contains ASCII bytes, with no null bytes between
|
|
// ASCII characters, so this also holds for `bytes`.
|
|
unsafe { TinyAsciiStr::from_utf8_unchecked(bytes) }
|
|
}
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
/// Returns a `TinyAsciiStr<Q>` with the concatenation of this string,
|
|
/// `TinyAsciiStr<N>`, and another string, `TinyAsciiStr<M>`.
|
|
///
|
|
/// If `Q < N + M`, the string gets truncated.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::tinystr;
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let abc = tinystr!(6, "abc");
|
|
/// let defg = tinystr!(6, "defg");
|
|
///
|
|
/// // The concatenation is successful if Q is large enough...
|
|
/// assert_eq!(abc.concat(defg), tinystr!(16, "abcdefg"));
|
|
/// assert_eq!(abc.concat(defg), tinystr!(12, "abcdefg"));
|
|
/// assert_eq!(abc.concat(defg), tinystr!(8, "abcdefg"));
|
|
/// assert_eq!(abc.concat(defg), tinystr!(7, "abcdefg"));
|
|
///
|
|
/// /// ...but it truncates of Q is too small.
|
|
/// assert_eq!(abc.concat(defg), tinystr!(6, "abcdef"));
|
|
/// assert_eq!(abc.concat(defg), tinystr!(2, "ab"));
|
|
/// ```
|
|
pub const fn concat<const M: usize, const Q: usize>(
|
|
self,
|
|
other: TinyAsciiStr<M>,
|
|
) -> TinyAsciiStr<Q> {
|
|
let mut result = self.resize::<Q>();
|
|
let mut i = self.len();
|
|
let mut j = 0;
|
|
// Indexing is protected by the loop guard
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < Q && j < M {
|
|
result.bytes[i] = other.bytes[j];
|
|
i += 1;
|
|
j += 1;
|
|
}
|
|
result
|
|
}
|
|
|
|
/// # Safety
|
|
/// Must be called with a bytes array made of valid ASCII bytes, with no null bytes
|
|
/// between ASCII characters
|
|
#[must_use]
|
|
pub const unsafe fn from_utf8_unchecked(code_units: [u8; N]) -> Self {
|
|
Self {
|
|
bytes: AsciiByte::to_ascii_byte_array(&code_units),
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! check_is {
|
|
($self:ident, $check_int:ident, $check_u8:ident) => {
|
|
if N <= 4 {
|
|
Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
|
|
} else if N <= 8 {
|
|
Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
|
|
} else {
|
|
let mut i = 0;
|
|
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
|
|
if !($self.bytes[i] as u8).$check_u8() {
|
|
return false;
|
|
}
|
|
i += 1;
|
|
}
|
|
true
|
|
}
|
|
};
|
|
($self:ident, $check_int:ident, !$check_u8_0_inv:ident, !$check_u8_1_inv:ident) => {
|
|
if N <= 4 {
|
|
Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
|
|
} else if N <= 8 {
|
|
Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
|
|
} else {
|
|
// Won't panic because N is > 8
|
|
if ($self.bytes[0] as u8).$check_u8_0_inv() {
|
|
return false;
|
|
}
|
|
let mut i = 1;
|
|
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
|
|
if ($self.bytes[i] as u8).$check_u8_1_inv() {
|
|
return false;
|
|
}
|
|
i += 1;
|
|
}
|
|
true
|
|
}
|
|
};
|
|
($self:ident, $check_int:ident, $check_u8_0_inv:ident, $check_u8_1_inv:ident) => {
|
|
if N <= 4 {
|
|
Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
|
|
} else if N <= 8 {
|
|
Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
|
|
} else {
|
|
// Won't panic because N is > 8
|
|
if !($self.bytes[0] as u8).$check_u8_0_inv() {
|
|
return false;
|
|
}
|
|
let mut i = 1;
|
|
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
|
|
if !($self.bytes[i] as u8).$check_u8_1_inv() {
|
|
return false;
|
|
}
|
|
i += 1;
|
|
}
|
|
true
|
|
}
|
|
};
|
|
}
|
|
|
|
impl<const N: usize> TinyAsciiStr<N> {
|
|
/// Checks if the value is composed of ASCII alphabetic characters:
|
|
///
|
|
/// * U+0041 'A' ..= U+005A 'Z', or
|
|
/// * U+0061 'a' ..= U+007A 'z'.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(s1.is_ascii_alphabetic());
|
|
/// assert!(!s2.is_ascii_alphabetic());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_alphabetic(&self) -> bool {
|
|
check_is!(self, is_ascii_alphabetic, is_ascii_alphabetic)
|
|
}
|
|
|
|
/// Checks if the value is composed of ASCII alphanumeric characters:
|
|
///
|
|
/// * U+0041 'A' ..= U+005A 'Z', or
|
|
/// * U+0061 'a' ..= U+007A 'z', or
|
|
/// * U+0030 '0' ..= U+0039 '9'.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "A15b".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "[3@w".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(s1.is_ascii_alphanumeric());
|
|
/// assert!(!s2.is_ascii_alphanumeric());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_alphanumeric(&self) -> bool {
|
|
check_is!(self, is_ascii_alphanumeric, is_ascii_alphanumeric)
|
|
}
|
|
|
|
/// Checks if the value is composed of ASCII decimal digits:
|
|
///
|
|
/// * U+0030 '0' ..= U+0039 '9'.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "312".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "3d".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(s1.is_ascii_numeric());
|
|
/// assert!(!s2.is_ascii_numeric());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_numeric(&self) -> bool {
|
|
check_is!(self, is_ascii_numeric, is_ascii_digit)
|
|
}
|
|
|
|
/// Checks if the value is in ASCII lower case.
|
|
///
|
|
/// All letter characters are checked for case. Non-letter characters are ignored.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
|
|
/// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(!s1.is_ascii_lowercase());
|
|
/// assert!(s2.is_ascii_lowercase());
|
|
/// assert!(s3.is_ascii_lowercase());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_lowercase(&self) -> bool {
|
|
check_is!(
|
|
self,
|
|
is_ascii_lowercase,
|
|
!is_ascii_uppercase,
|
|
!is_ascii_uppercase
|
|
)
|
|
}
|
|
|
|
/// Checks if the value is in ASCII title case.
|
|
///
|
|
/// This verifies that the first character is ASCII uppercase and all others ASCII lowercase.
|
|
/// Non-letter characters are ignored.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
|
|
/// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(!s1.is_ascii_titlecase());
|
|
/// assert!(s2.is_ascii_titlecase());
|
|
/// assert!(s3.is_ascii_titlecase());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_titlecase(&self) -> bool {
|
|
check_is!(
|
|
self,
|
|
is_ascii_titlecase,
|
|
!is_ascii_lowercase,
|
|
!is_ascii_uppercase
|
|
)
|
|
}
|
|
|
|
/// Checks if the value is in ASCII upper case.
|
|
///
|
|
/// All letter characters are checked for case. Non-letter characters are ignored.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
|
|
/// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(!s1.is_ascii_uppercase());
|
|
/// assert!(s2.is_ascii_uppercase());
|
|
/// assert!(!s3.is_ascii_uppercase());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_uppercase(&self) -> bool {
|
|
check_is!(
|
|
self,
|
|
is_ascii_uppercase,
|
|
!is_ascii_lowercase,
|
|
!is_ascii_lowercase
|
|
)
|
|
}
|
|
|
|
/// Checks if the value is composed of ASCII alphabetic lower case characters:
|
|
///
|
|
/// * U+0061 'a' ..= U+007A 'z',
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
|
|
/// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
/// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
|
|
/// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(!s1.is_ascii_alphabetic_lowercase());
|
|
/// assert!(!s2.is_ascii_alphabetic_lowercase());
|
|
/// assert!(!s3.is_ascii_alphabetic_lowercase());
|
|
/// assert!(s4.is_ascii_alphabetic_lowercase());
|
|
/// assert!(!s5.is_ascii_alphabetic_lowercase());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_alphabetic_lowercase(&self) -> bool {
|
|
check_is!(
|
|
self,
|
|
is_ascii_alphabetic_lowercase,
|
|
is_ascii_lowercase,
|
|
is_ascii_lowercase
|
|
)
|
|
}
|
|
|
|
/// Checks if the value is composed of ASCII alphabetic, with the first character being ASCII uppercase, and all others ASCII lowercase.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
|
|
/// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
/// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
|
|
/// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(s1.is_ascii_alphabetic_titlecase());
|
|
/// assert!(!s2.is_ascii_alphabetic_titlecase());
|
|
/// assert!(!s3.is_ascii_alphabetic_titlecase());
|
|
/// assert!(!s4.is_ascii_alphabetic_titlecase());
|
|
/// assert!(!s5.is_ascii_alphabetic_titlecase());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_alphabetic_titlecase(&self) -> bool {
|
|
check_is!(
|
|
self,
|
|
is_ascii_alphabetic_titlecase,
|
|
is_ascii_uppercase,
|
|
is_ascii_lowercase
|
|
)
|
|
}
|
|
|
|
/// Checks if the value is composed of ASCII alphabetic upper case characters:
|
|
///
|
|
/// * U+0041 'A' ..= U+005A 'Z',
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
|
|
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
|
|
/// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
/// let s4: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
|
|
/// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert!(!s1.is_ascii_alphabetic_uppercase());
|
|
/// assert!(!s2.is_ascii_alphabetic_uppercase());
|
|
/// assert!(!s3.is_ascii_alphabetic_uppercase());
|
|
/// assert!(s4.is_ascii_alphabetic_uppercase());
|
|
/// assert!(!s5.is_ascii_alphabetic_uppercase());
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_ascii_alphabetic_uppercase(&self) -> bool {
|
|
check_is!(
|
|
self,
|
|
is_ascii_alphabetic_uppercase,
|
|
is_ascii_uppercase,
|
|
is_ascii_uppercase
|
|
)
|
|
}
|
|
}
|
|
|
|
macro_rules! to {
|
|
($self:ident, $to:ident, $later_char_to:ident $(,$first_char_to:ident)?) => {{
|
|
let mut i = 0;
|
|
if N <= 4 {
|
|
let aligned = Aligned4::from_ascii_bytes(&$self.bytes).$to();
|
|
// Won't panic because self.bytes has length N and aligned has length >= N
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < N {
|
|
$self.bytes[i] = aligned[i];
|
|
i += 1;
|
|
}
|
|
} else if N <= 8 {
|
|
let aligned = Aligned8::from_ascii_bytes(&$self.bytes).$to();
|
|
// Won't panic because self.bytes has length N and aligned has length >= N
|
|
#[expect(clippy::indexing_slicing)]
|
|
while i < N {
|
|
$self.bytes[i] = aligned[i];
|
|
i += 1;
|
|
}
|
|
} else {
|
|
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
|
|
$self.bytes[i] = $self.bytes[i].$later_char_to();
|
|
i += 1;
|
|
}
|
|
$(
|
|
$self.bytes[0] = $self.bytes[0].$first_char_to();
|
|
)?
|
|
}
|
|
$self
|
|
}};
|
|
}
|
|
|
|
impl<const N: usize> TinyAsciiStr<N> {
|
|
/// Converts this type to its ASCII lower case equivalent in-place.
|
|
///
|
|
/// ASCII letters 'A' to 'Z' are mapped to 'a' to 'z', other characters are unchanged.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "TeS3".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert_eq!(&*s1.to_ascii_lowercase(), "tes3");
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn to_ascii_lowercase(mut self) -> Self {
|
|
to!(self, to_ascii_lowercase, to_ascii_lowercase)
|
|
}
|
|
|
|
/// Converts this type to its ASCII title case equivalent in-place.
|
|
///
|
|
/// The first character is converted to ASCII uppercase; the remaining characters
|
|
/// are converted to ASCII lowercase.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert_eq!(&*s1.to_ascii_titlecase(), "Test");
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn to_ascii_titlecase(mut self) -> Self {
|
|
to!(
|
|
self,
|
|
to_ascii_titlecase,
|
|
to_ascii_lowercase,
|
|
to_ascii_uppercase
|
|
)
|
|
}
|
|
|
|
/// Converts this type to its ASCII upper case equivalent in-place.
|
|
///
|
|
/// ASCII letters 'a' to 'z' are mapped to 'A' to 'Z', other characters are unchanged.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tinystr::TinyAsciiStr;
|
|
///
|
|
/// let s1: TinyAsciiStr<4> = "Tes3".parse().expect("Failed to parse.");
|
|
///
|
|
/// assert_eq!(&*s1.to_ascii_uppercase(), "TES3");
|
|
/// ```
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn to_ascii_uppercase(mut self) -> Self {
|
|
to!(self, to_ascii_uppercase, to_ascii_uppercase)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> fmt::Debug for TinyAsciiStr<N> {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt::Debug::fmt(self.as_str(), f)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> fmt::Display for TinyAsciiStr<N> {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt::Display::fmt(self.as_str(), f)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Deref for TinyAsciiStr<N> {
|
|
type Target = str;
|
|
#[inline]
|
|
fn deref(&self) -> &str {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Borrow<str> for TinyAsciiStr<N> {
|
|
#[inline]
|
|
fn borrow(&self) -> &str {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> FromStr for TinyAsciiStr<N> {
|
|
type Err = ParseError;
|
|
#[inline]
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::try_from_str(s)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> {
|
|
fn eq(&self, other: &str) -> bool {
|
|
self.deref() == other
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> {
|
|
fn eq(&self, other: &&str) -> bool {
|
|
self.deref() == *other
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
impl<const N: usize> PartialEq<String> for TinyAsciiStr<N> {
|
|
fn eq(&self, other: &String) -> bool {
|
|
self.deref() == other.deref()
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "alloc")]
|
|
impl<const N: usize> PartialEq<TinyAsciiStr<N>> for String {
|
|
fn eq(&self, other: &TinyAsciiStr<N>) -> bool {
|
|
self.deref() == other.deref()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use rand::distr::Distribution;
|
|
use rand::distr::StandardUniform;
|
|
use rand::rngs::SmallRng;
|
|
use rand::SeedableRng;
|
|
|
|
const STRINGS: [&str; 26] = [
|
|
"Latn",
|
|
"laTn",
|
|
"windows",
|
|
"AR",
|
|
"Hans",
|
|
"macos",
|
|
"AT",
|
|
"infiniband",
|
|
"FR",
|
|
"en",
|
|
"Cyrl",
|
|
"FromIntegral",
|
|
"NO",
|
|
"419",
|
|
"MacintoshOSX2019",
|
|
"a3z",
|
|
"A3z",
|
|
"A3Z",
|
|
"a3Z",
|
|
"3A",
|
|
"3Z",
|
|
"3a",
|
|
"3z",
|
|
"@@[`{",
|
|
"UK",
|
|
"E12",
|
|
];
|
|
|
|
fn gen_strings(num_strings: usize, allowed_lengths: &[usize]) -> Vec<String> {
|
|
use rand::seq::IndexedRandom;
|
|
let mut rng = SmallRng::seed_from_u64(2022);
|
|
// Need to do this in 2 steps since the RNG is needed twice
|
|
let string_lengths = core::iter::repeat_with(|| *allowed_lengths.choose(&mut rng).unwrap())
|
|
.take(num_strings)
|
|
.collect::<Vec<usize>>();
|
|
string_lengths
|
|
.iter()
|
|
.map(|len| {
|
|
StandardUniform
|
|
.sample_iter(&mut rng)
|
|
.filter(|b: &u8| *b > 0 && *b < 0x80)
|
|
.take(*len)
|
|
.collect::<Vec<u8>>()
|
|
})
|
|
.map(|byte_vec| String::from_utf8(byte_vec).expect("All ASCII"))
|
|
.collect()
|
|
}
|
|
|
|
fn check_operation<T, F1, F2, const N: usize>(reference_f: F1, tinystr_f: F2)
|
|
where
|
|
F1: Fn(&str) -> T,
|
|
F2: Fn(TinyAsciiStr<N>) -> T,
|
|
T: fmt::Debug + PartialEq,
|
|
{
|
|
for s in STRINGS
|
|
.into_iter()
|
|
.map(str::to_owned)
|
|
.chain(gen_strings(100, &[3, 4, 5, 8, 12]))
|
|
{
|
|
let t = match TinyAsciiStr::<N>::from_str(&s) {
|
|
Ok(t) => t,
|
|
Err(ParseError::TooLong { .. }) => continue,
|
|
Err(e) => panic!("{}", e),
|
|
};
|
|
let expected = reference_f(&s);
|
|
let actual = tinystr_f(t);
|
|
assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
|
|
|
|
let s_utf16: Vec<u16> = s.encode_utf16().collect();
|
|
let t = match TinyAsciiStr::<N>::try_from_utf16(&s_utf16) {
|
|
Ok(t) => t,
|
|
Err(ParseError::TooLong { .. }) => continue,
|
|
Err(e) => panic!("{}", e),
|
|
};
|
|
let expected = reference_f(&s);
|
|
let actual = tinystr_f(t);
|
|
assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_alphabetic() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| s.chars().all(|c| c.is_ascii_alphabetic()),
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_alphanumeric() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| s.chars().all(|c| c.is_ascii_alphanumeric()),
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphanumeric(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_numeric() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| s.chars().all(|c| c.is_ascii_digit()),
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_numeric(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_lowercase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
s == TinyAsciiStr::<16>::try_from_str(s)
|
|
.unwrap()
|
|
.to_ascii_lowercase()
|
|
.as_str()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_lowercase(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_titlecase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
s == TinyAsciiStr::<16>::try_from_str(s)
|
|
.unwrap()
|
|
.to_ascii_titlecase()
|
|
.as_str()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_titlecase(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_uppercase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
s == TinyAsciiStr::<16>::try_from_str(s)
|
|
.unwrap()
|
|
.to_ascii_uppercase()
|
|
.as_str()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_uppercase(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_alphabetic_lowercase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
// Check alphabetic
|
|
s.chars().all(|c| c.is_ascii_alphabetic()) &&
|
|
// Check lowercase
|
|
s == TinyAsciiStr::<16>::try_from_str(s)
|
|
.unwrap()
|
|
.to_ascii_lowercase()
|
|
.as_str()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_lowercase(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_alphabetic_titlecase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
// Check alphabetic
|
|
s.chars().all(|c| c.is_ascii_alphabetic()) &&
|
|
// Check titlecase
|
|
s == TinyAsciiStr::<16>::try_from_str(s)
|
|
.unwrap()
|
|
.to_ascii_titlecase()
|
|
.as_str()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_titlecase(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ascii_alphabetic_uppercase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
// Check alphabetic
|
|
s.chars().all(|c| c.is_ascii_alphabetic()) &&
|
|
// Check uppercase
|
|
s == TinyAsciiStr::<16>::try_from_str(s)
|
|
.unwrap()
|
|
.to_ascii_uppercase()
|
|
.as_str()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_uppercase(&t),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_ascii_lowercase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
s.chars()
|
|
.map(|c| c.to_ascii_lowercase())
|
|
.collect::<String>()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_lowercase(t).as_str().to_owned(),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_ascii_titlecase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
let mut r = s
|
|
.chars()
|
|
.map(|c| c.to_ascii_lowercase())
|
|
.collect::<String>();
|
|
// Safe because the string is nonempty and an ASCII string
|
|
unsafe { r.as_bytes_mut()[0].make_ascii_uppercase() };
|
|
r
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_titlecase(t).as_str().to_owned(),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_ascii_uppercase() {
|
|
fn check<const N: usize>() {
|
|
check_operation(
|
|
|s| {
|
|
s.chars()
|
|
.map(|c| c.to_ascii_uppercase())
|
|
.collect::<String>()
|
|
},
|
|
|t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_uppercase(t).as_str().to_owned(),
|
|
)
|
|
}
|
|
check::<2>();
|
|
check::<3>();
|
|
check::<4>();
|
|
check::<5>();
|
|
check::<8>();
|
|
check::<16>();
|
|
}
|
|
|
|
#[test]
|
|
fn lossy_constructor() {
|
|
assert_eq!(TinyAsciiStr::<4>::from_utf8_lossy(b"", b'?').as_str(), "");
|
|
assert_eq!(
|
|
TinyAsciiStr::<4>::from_utf8_lossy(b"oh\0o", b'?').as_str(),
|
|
"oh?o"
|
|
);
|
|
assert_eq!(
|
|
TinyAsciiStr::<4>::from_utf8_lossy(b"\0", b'?').as_str(),
|
|
"?"
|
|
);
|
|
assert_eq!(
|
|
TinyAsciiStr::<4>::from_utf8_lossy(b"toolong", b'?').as_str(),
|
|
"tool"
|
|
);
|
|
assert_eq!(
|
|
TinyAsciiStr::<4>::from_utf8_lossy(&[b'a', 0x80, 0xFF, b'1'], b'?').as_str(),
|
|
"a??1"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "replacement must be a non-null ASCII byte")]
|
|
fn from_utf8_lossy_rejects_non_ascii_replacement() {
|
|
// A non-ASCII replacement byte (>= 0x80) would create an invalid AsciiByte
|
|
// discriminant and then from_utf8_unchecked would be called on non-UTF-8
|
|
// data (UB). The fix validates the replacement byte on entry.
|
|
let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0xFF);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "replacement must be a non-null ASCII byte")]
|
|
fn from_utf16_lossy_rejects_non_ascii_replacement() {
|
|
let _ = TinyAsciiStr::<4>::from_utf16_lossy(&[0xFFFF], 0xFF);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "replacement must be a non-null ASCII byte")]
|
|
fn from_utf8_lossy_rejects_null_replacement() {
|
|
// Null (0x00) is also invalid — it's the TinyAsciiStr terminator.
|
|
let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0x00);
|
|
}
|
|
}
|