#[derive(Debug)]
pub(crate) enum NameValidationError {
TooShort,
TooLong,
UnexpectedColon,
UnexpectedUnderscore,
InvalidCharacter,
}
pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
pub(crate) const MAX_LENGTH: usize = u8::MAX as usize;
pub(crate) const fn validate(name: &str) -> Result<(), NameValidationError> {
let bytes = name.as_bytes();
let mut idx = 0;
let mut num_components = 0;
if bytes.is_empty() {
return Err(NameValidationError::TooShort);
}
if bytes.len() > MAX_LENGTH {
return Err(NameValidationError::TooLong);
}
if bytes[0] == b':' {
return Err(NameValidationError::UnexpectedColon);
} else if bytes[0] == b'_' {
return Err(NameValidationError::UnexpectedUnderscore);
}
while idx < bytes.len() {
let byte = bytes[idx];
let is_colon = byte == b':';
if is_colon {
if (idx + 1) < bytes.len() {
if bytes[idx + 1] != b':' {
return Err(NameValidationError::UnexpectedColon);
}
} else {
return Err(NameValidationError::UnexpectedColon);
}
if (idx + 2) < bytes.len() {
if bytes[idx + 2] == b':' {
return Err(NameValidationError::UnexpectedColon);
} else if bytes[idx + 2] == b'_' {
return Err(NameValidationError::UnexpectedUnderscore);
}
} else {
return Err(NameValidationError::UnexpectedColon);
}
idx += 2;
num_components += 1;
} else if is_valid_char(byte) {
idx += 1;
} else {
return Err(NameValidationError::InvalidCharacter);
}
}
num_components += 1;
if num_components < MIN_NUM_COMPONENTS {
return Err(NameValidationError::TooShort);
}
Ok(())
}
const fn is_valid_char(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
#[cfg(test)]
pub(super) mod tests {
use alloc::string::String;
use std::borrow::ToOwned;
use std::string::ToString;
use assert_matches::assert_matches;
use rstest::rstest;
use super::*;
use crate::account::{AccountComponentName, StorageSlotName};
const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
#[test]
fn slot_name_fails_on_invalid_colon_placement() {
assert_matches!(validate(":").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("0::1:").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate(":0::1").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("0::1:2").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("::").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("1::2::").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("::1::2").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate(":::").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("1::2:::").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate(":::1::2").unwrap_err(), NameValidationError::UnexpectedColon);
assert_matches!(validate("1::2:::3").unwrap_err(), NameValidationError::UnexpectedColon);
}
#[test]
fn slot_name_fails_on_invalid_underscore_placement() {
assert_matches!(
validate("_one::two").unwrap_err(),
NameValidationError::UnexpectedUnderscore
);
assert_matches!(
validate("one::_two").unwrap_err(),
NameValidationError::UnexpectedUnderscore
);
}
#[test]
fn slot_name_fails_on_empty_string() {
assert_matches!(validate("").unwrap_err(), NameValidationError::TooShort);
}
#[test]
fn slot_name_fails_on_single_component() {
assert_matches!(validate("single_component").unwrap_err(), NameValidationError::TooShort);
}
#[test]
fn slot_name_fails_on_string_whose_length_exceeds_max_length() {
let mut string = get_max_length_name();
string.push('a');
assert_matches!(validate(&string).unwrap_err(), NameValidationError::TooLong);
}
#[test]
fn slot_name_allows_ascii_alphanumeric_and_underscore() {
let name = format!("{FULL_ALPHABET}::second");
validate(&name).unwrap();
}
#[test]
fn slot_name_fails_on_invalid_character() {
assert_matches!(
validate("na#me::second").unwrap_err(),
NameValidationError::InvalidCharacter
);
assert_matches!(
validate("first_entry::secönd").unwrap_err(),
NameValidationError::InvalidCharacter
);
assert_matches!(
validate("first::sec::th!rd").unwrap_err(),
NameValidationError::InvalidCharacter
);
}
#[test]
fn slot_name_with_min_components_is_valid() {
validate("miden::component").unwrap()
}
#[test]
fn slot_name_with_many_components_is_valid() {
validate("miden::faucet0::fungible_1::b4sic::metadata").unwrap();
}
#[test]
fn slot_name_with_max_length_is_valid() {
validate(&get_max_length_name()).unwrap();
}
#[rstest]
#[case("")]
#[case("single")]
#[case(":leading_colon::ok")]
#[case("_leading_underscore::ok")]
#[case("ok::_leading_underscore")]
#[case("ok::bad#char")]
#[case("triple:::colon::ok")]
fn shared_validation_parity(#[case] name: &str) {
assert!(StorageSlotName::new(name.to_string()).is_err());
assert!(AccountComponentName::new(name.to_string()).is_err());
}
pub(crate) fn get_max_length_name() -> String {
const MIDEN_STR: &str = "miden::";
let remainder = ['a'; MAX_LENGTH - MIDEN_STR.len()];
let mut string = MIDEN_STR.to_owned();
string.extend(remainder);
assert_eq!(string.len(), MAX_LENGTH);
string
}
}