use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::fmt::Display;
use core::str::FromStr;
use crate::account::storage::slot::StorageSlotId;
use crate::errors::StorageSlotNameError;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StorageSlotName {
name: Arc<str>,
id: StorageSlotId,
}
impl StorageSlotName {
pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
pub(crate) const MAX_LENGTH: usize = u8::MAX as usize;
pub fn new(name: impl Into<Arc<str>>) -> Result<Self, StorageSlotNameError> {
let name: Arc<str> = name.into();
Self::validate(&name)?;
let id = StorageSlotId::from_str(&name);
Ok(Self { name, id })
}
pub fn as_str(&self) -> &str {
&self.name
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u8 {
debug_assert!(self.name.len() <= Self::MAX_LENGTH);
self.name.len() as u8
}
pub fn id(&self) -> StorageSlotId {
self.id
}
const fn validate(name: &str) -> Result<(), StorageSlotNameError> {
let bytes = name.as_bytes();
let mut idx = 0;
let mut num_components = 0;
if bytes.is_empty() {
return Err(StorageSlotNameError::TooShort);
}
if bytes.len() > Self::MAX_LENGTH {
return Err(StorageSlotNameError::TooLong);
}
if bytes[0] == b':' {
return Err(StorageSlotNameError::UnexpectedColon);
} else if bytes[0] == b'_' {
return Err(StorageSlotNameError::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(StorageSlotNameError::UnexpectedColon);
}
} else {
return Err(StorageSlotNameError::UnexpectedColon);
}
if (idx + 2) < bytes.len() {
if bytes[idx + 2] == b':' {
return Err(StorageSlotNameError::UnexpectedColon);
} else if bytes[idx + 2] == b'_' {
return Err(StorageSlotNameError::UnexpectedUnderscore);
}
} else {
return Err(StorageSlotNameError::UnexpectedColon);
}
idx += 2;
num_components += 1;
} else if Self::is_valid_char(byte) {
idx += 1;
} else {
return Err(StorageSlotNameError::InvalidCharacter);
}
}
num_components += 1;
if num_components < Self::MIN_NUM_COMPONENTS {
return Err(StorageSlotNameError::TooShort);
}
Ok(())
}
const fn is_valid_char(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
}
impl Ord for StorageSlotName {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.id().cmp(&other.id())
}
}
impl PartialOrd for StorageSlotName {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Display for StorageSlotName {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for StorageSlotName {
type Err = StorageSlotNameError;
fn from_str(string: &str) -> Result<Self, Self::Err> {
StorageSlotName::new(string)
}
}
impl TryFrom<&str> for StorageSlotName {
type Error = StorageSlotNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<String> for StorageSlotName {
type Error = StorageSlotNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<StorageSlotName> for String {
fn from(slot_name: StorageSlotName) -> Self {
slot_name.name.to_string()
}
}
impl Serializable for StorageSlotName {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_u8(self.len());
target.write_many(self.as_str().as_bytes())
}
fn get_size_hint(&self) -> usize {
1 + self.as_str().len()
}
}
impl Deserializable for StorageSlotName {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let len = source.read_u8()?;
let name = source.read_many_iter(len as usize)?.collect::<Result<_, _>>()?;
String::from_utf8(name)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
.and_then(|name| {
Self::new(name).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
})
}
}
#[cfg(test)]
mod tests {
use std::borrow::ToOwned;
use assert_matches::assert_matches;
use super::*;
const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
#[test]
fn slot_name_fails_on_invalid_colon_placement() {
assert_matches!(
StorageSlotName::new(":").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("0::1:").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new(":0::1").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("0::1:2").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("::").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("1::2::").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("::1::2").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new(":::").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("1::2:::").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new(":::1::2").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
assert_matches!(
StorageSlotName::new("1::2:::3").unwrap_err(),
StorageSlotNameError::UnexpectedColon
);
}
#[test]
fn slot_name_fails_on_invalid_underscore_placement() {
assert_matches!(
StorageSlotName::new("_one::two").unwrap_err(),
StorageSlotNameError::UnexpectedUnderscore
);
assert_matches!(
StorageSlotName::new("one::_two").unwrap_err(),
StorageSlotNameError::UnexpectedUnderscore
);
}
#[test]
fn slot_name_fails_on_empty_string() {
assert_matches!(StorageSlotName::new("").unwrap_err(), StorageSlotNameError::TooShort);
}
#[test]
fn slot_name_fails_on_single_component() {
assert_matches!(
StorageSlotName::new("single_component").unwrap_err(),
StorageSlotNameError::TooShort
);
}
#[test]
fn slot_name_fails_on_string_whose_length_exceeds_max_length() {
let mut string = get_max_length_slot_name();
string.push('a');
assert_matches!(StorageSlotName::new(string).unwrap_err(), StorageSlotNameError::TooLong);
}
#[test]
fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> {
let name = format!("{FULL_ALPHABET}::second");
let slot_name = StorageSlotName::new(name.clone())?;
assert_eq!(slot_name.as_str(), name);
Ok(())
}
#[test]
fn slot_name_fails_on_invalid_character() {
assert_matches!(
StorageSlotName::new("na#me::second").unwrap_err(),
StorageSlotNameError::InvalidCharacter
);
assert_matches!(
StorageSlotName::new("first_entry::secönd").unwrap_err(),
StorageSlotNameError::InvalidCharacter
);
assert_matches!(
StorageSlotName::new("first::sec::th!rd").unwrap_err(),
StorageSlotNameError::InvalidCharacter
);
}
#[test]
fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> {
StorageSlotName::new("miden::component")?;
Ok(())
}
#[test]
fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> {
StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
Ok(())
}
#[test]
fn slot_name_with_max_length_is_valid() -> anyhow::Result<()> {
StorageSlotName::new(get_max_length_slot_name())?;
Ok(())
}
#[test]
fn serde_slot_name() -> anyhow::Result<()> {
let slot_name = StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?);
Ok(())
}
#[test]
fn serde_max_length_slot_name() -> anyhow::Result<()> {
let slot_name = StorageSlotName::new(get_max_length_slot_name())?;
assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?);
Ok(())
}
fn get_max_length_slot_name() -> String {
const MIDEN_STR: &str = "miden::";
let remainder = ['a'; StorageSlotName::MAX_LENGTH - MIDEN_STR.len()];
let mut string = MIDEN_STR.to_owned();
string.extend(remainder);
assert_eq!(string.len(), StorageSlotName::MAX_LENGTH);
string
}
}