mxp 1.2.1

Rust implementation of the MXP (Mud eXtension Protocol) standard
Documentation
use crate::{Error, ErrorKind};

pub(crate) fn split_name(s: &str) -> (&str, &str) {
    let s = s.trim_ascii_start();
    let Some(split_at) = s.as_bytes().iter().position(|&c| c == b' ') else {
        return (s, "");
    };
    // SAFETY: `split_at` and `split_at + 1` are valid indices.
    unsafe { (s.get_unchecked(..split_at), s.get_unchecked(split_at + 1..)) }
}

pub(crate) fn strip_terminating_slash(s: &str) -> (&str, bool) {
    match s.as_bytes() {
        // SAFETY: Valid UTF-8.
        [s @ .., b' ', b'/'] | [s @ .., b'/'] => (unsafe { str::from_utf8_unchecked(s) }, true),
        _ => (s, false),
    }
}

/// If the specified target is valid to use as an MXP identifier or value, returns `Ok(())`.
/// Otherwise, returns an [`mxp::Error`](Error) for the target with the specified error kind.
///
/// # Examples
///
/// ```
/// let err = mxp::ErrorKind::InvalidEntityName;
/// assert!(mxp::validate("abc", err).is_ok());
/// assert!(mxp::validate("aBc_-.", err).is_ok());
/// assert!(mxp::validate("", err).is_err());
/// assert!(mxp::validate("_test", err).is_err());
/// assert!(mxp::validate("abc!", err).is_err());
/// ```
pub fn validate(target: &str, error: ErrorKind) -> crate::Result<()> {
    if is_valid(target) {
        Ok(())
    } else {
        Err(Error::new(target, error))
    }
}

/// Equivalent to [`str::from_utf8`], but returns an [`mxp::Error`](Error) instead of a
/// [`Utf8Error`](std::str::Utf8Error).
pub fn validate_utf8(bytes: &[u8]) -> crate::Result<&str> {
    if let Ok(utf8) = str::from_utf8(bytes) {
        return Ok(utf8);
    }
    Err(Error::new(
        String::from_utf8_lossy(bytes),
        ErrorKind::InvalidUtf8,
    ))
}

/// If the specified target is valid to use as an MXP identifier or value, returns `true`.
/// Otherwise, returns `false`.
///
/// # Examples
///
/// ```
/// let err = mxp::ErrorKind::InvalidEntityName;
/// assert!(mxp::is_valid("abc"));
/// assert!(mxp::is_valid("aBc_-."));
/// assert!(!mxp::is_valid(""));
/// assert!(!mxp::is_valid("_test"));
/// assert!(!mxp::is_valid("abc!"));
/// ```
pub const fn is_valid(target: &str) -> bool {
    let [b'A'..=b'Z' | b'a'..=b'z', rest @ ..] = target.as_bytes() else {
        return false;
    };
    let mut target = rest;
    while let [first, rest @ ..] = target {
        if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-'| b'.') {
            return false;
        }
        target = rest;
    }
    true
}