use core::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StringKind {
Ci,
Utf8,
}
impl StringKind {
const fn name(self) -> &'static str {
match self {
Self::Ci => "CiString",
Self::Utf8 => "string",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvalidString {
kind: StringKind,
reason: Reason,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Reason {
TooLong { len: usize, max: usize },
WrongLength { len: usize, expected: usize },
NonPrintable { at: usize, ch: char },
NonAscii { at: usize, ch: char },
}
impl InvalidString {
pub(crate) const fn too_long(len: usize, max: usize, kind: StringKind) -> Self {
Self { kind, reason: Reason::TooLong { len, max } }
}
pub(crate) const fn wrong_length(len: usize, expected: usize, kind: StringKind) -> Self {
Self { kind, reason: Reason::WrongLength { len, expected } }
}
#[must_use]
pub const fn is_too_long(&self) -> bool {
matches!(self.reason, Reason::TooLong { .. })
}
#[must_use]
pub const fn kind(&self) -> StringKind {
self.kind
}
}
impl fmt::Display for InvalidString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.kind.name();
match self.reason {
Reason::TooLong { len, max } => {
write!(f, "{name}({max}) cannot hold {len} characters")
}
Reason::WrongLength { len, expected } => {
write!(f, "{name} must be exactly {expected} characters, not {len}")
}
Reason::NonPrintable { at, ch } => write!(
f,
"{name} must contain only printable characters, found U+{:04X} at index {at}",
ch as u32
),
Reason::NonAscii { at, ch } => {
write!(f, "{name} must contain only ASCII, found U+{:04X} at index {at}", ch as u32)
}
}
}
}
impl std::error::Error for InvalidString {}
pub(crate) fn check_printable_ascii(value: &str, kind: StringKind) -> Result<(), InvalidString> {
for (at, ch) in value.char_indices() {
if !ch.is_ascii() {
return Err(InvalidString { kind, reason: Reason::NonAscii { at, ch } });
}
if !is_printable_ascii(ch) {
return Err(InvalidString { kind, reason: Reason::NonPrintable { at, ch } });
}
}
Ok(())
}
pub(crate) fn check_printable_utf8(value: &str, kind: StringKind) -> Result<(), InvalidString> {
for (at, ch) in value.char_indices() {
if ch.is_control() {
return Err(InvalidString { kind, reason: Reason::NonPrintable { at, ch } });
}
}
Ok(())
}
const fn is_printable_ascii(ch: char) -> bool {
matches!(ch, ' '..='~')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_rules() {
assert!(check_printable_ascii("Hello, World! ~", StringKind::Ci).is_ok());
assert!(check_printable_ascii("tab\there", StringKind::Ci).is_err());
assert!(check_printable_ascii("\u{7f}", StringKind::Ci).is_err(), "DEL is not printable");
assert!(check_printable_ascii("é", StringKind::Ci).is_err());
}
#[test]
fn utf8_rules() {
assert!(check_printable_utf8("Straße — 日本語 🚗", StringKind::Utf8).is_ok());
assert!(check_printable_utf8("line\nbreak", StringKind::Utf8).is_err());
assert!(check_printable_utf8("\u{85}", StringKind::Utf8).is_err(), "C1 NEL is a control");
}
#[test]
fn error_messages_name_the_offending_index() {
let e = check_printable_ascii("ab\tcd", StringKind::Ci).unwrap_err();
assert!(e.to_string().contains("index 2"), "{e}");
assert!(!e.is_too_long());
}
}