alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Stable plugin identity proof.

use crate::plugin::{MAX_PLUGIN_IDENTITY_BYTES, PluginIdentityError};

/// Validated stable plugin identity.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct PluginIdentity(String);

impl PluginIdentity {
    /// Creates a stable plugin identity from untrusted input.
    ///
    /// # Errors
    ///
    /// Returns [`PluginIdentityError`] when `identity` is empty, too long, or outside the
    /// conservative ASCII grammar `[a-z0-9][a-z0-9._-]{0,127}`.
    pub fn try_new(identity: impl Into<String>) -> Result<Self, PluginIdentityError> {
        let identity = identity.into();
        validate_plugin_identity(&identity)?;
        Ok(Self(identity))
    }

    /// Returns the validated identity string.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns the validated identity as an owned string.
    #[must_use]
    pub fn into_string(self) -> String {
        self.0
    }
}

/// Validates a stable plugin identity.
fn validate_plugin_identity(identity: &str) -> Result<(), PluginIdentityError> {
    if identity.is_empty() {
        return Err(PluginIdentityError::Empty);
    }
    if identity.len() > MAX_PLUGIN_IDENTITY_BYTES {
        return Err(PluginIdentityError::TooLong);
    }

    let mut bytes = identity.bytes();
    let first = bytes.next().expect("empty identity returned above");
    if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
        return Err(PluginIdentityError::InvalidStart);
    }

    if bytes.any(|byte| {
        !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && !matches!(byte, b'.' | b'_' | b'-')
    }) {
        return Err(PluginIdentityError::InvalidCharacter);
    }

    Ok(())
}