miden_protocol/account/storage/slot/
slot_name.rs

1use alloc::string::{String, ToString};
2use alloc::sync::Arc;
3use core::fmt::Display;
4use core::str::FromStr;
5
6use crate::account::storage::slot::StorageSlotId;
7use crate::errors::StorageSlotNameError;
8use crate::utils::serde::{ByteWriter, Deserializable, DeserializationError, Serializable};
9
10/// The name of an account storage slot.
11///
12/// A typical slot name looks like this:
13///
14/// ```text
15/// miden::standards::fungible_faucets::metadata
16/// ```
17///
18/// The double-colon (`::`) serves as a separator and the strings in between the separators are
19/// called components.
20///
21/// It is generally recommended that slot names have at least three components and follow this
22/// structure:
23///
24/// ```text
25/// project_name::component_name::slot_name
26/// ```
27///
28/// ## Requirements
29///
30/// For a string to be a valid slot name it needs to satisfy the following criteria:
31/// - Its length must be less than 255.
32/// - It needs to have at least 2 components.
33/// - Each component must consist of at least one character.
34/// - Each component must only consist of the characters `a` to `z`, `A` to `Z`, `0` to `9` or `_`
35///   (underscore).
36/// - Each component must not start with an underscore.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct StorageSlotName {
39    name: Arc<str>,
40    id: StorageSlotId,
41}
42
43impl StorageSlotName {
44    // CONSTANTS
45    // --------------------------------------------------------------------------------------------
46
47    /// The minimum number of components that a slot name must contain.
48    pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
49
50    /// The maximum number of characters in a slot name.
51    pub(crate) const MAX_LENGTH: usize = u8::MAX as usize;
52
53    // CONSTRUCTORS
54    // --------------------------------------------------------------------------------------------
55
56    /// Constructs a new [`StorageSlotName`] from a string.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if:
61    /// - the slot name is invalid (see the type-level docs for the requirements).
62    pub fn new(name: impl Into<Arc<str>>) -> Result<Self, StorageSlotNameError> {
63        let name: Arc<str> = name.into();
64        Self::validate(&name)?;
65        let id = StorageSlotId::from_str(&name);
66        Ok(Self { name, id })
67    }
68
69    // ACCESSORS
70    // --------------------------------------------------------------------------------------------
71
72    /// Returns the slot name as a string slice.
73    pub fn as_str(&self) -> &str {
74        &self.name
75    }
76
77    /// Returns the slot name as a string slice.
78    // allow is_empty to be missing because it would always return false since slot names are
79    // enforced to have a length greater than zero, so it does not have much use.
80    #[allow(clippy::len_without_is_empty)]
81    pub fn len(&self) -> u8 {
82        // SAFETY: Slot name validation should enforce length fits into a u8.
83        debug_assert!(self.name.len() <= Self::MAX_LENGTH);
84        self.name.len() as u8
85    }
86
87    /// Returns the [`StorageSlotId`] derived from the slot name.
88    pub fn id(&self) -> StorageSlotId {
89        self.id
90    }
91
92    // HELPERS
93    // --------------------------------------------------------------------------------------------
94
95    /// Validates a slot name.
96    ///
97    /// This checks that components are separated by double colons, that each component contains
98    /// only valid characters and that the name is not empty or starts or ends with a colon.
99    ///
100    /// We must check the validity of a slot name against the raw bytes of the UTF-8 string because
101    /// typical character APIs are not available in a const version. We can do this because any byte
102    /// in a UTF-8 string that is an ASCII character never represents anything other than such a
103    /// character, even though UTF-8 can contain multibyte sequences:
104    ///
105    /// > UTF-8, the object of this memo, has a one-octet encoding unit. It uses all bits of an
106    /// > octet, but has the quality of preserving the full US-ASCII range: US-ASCII characters
107    /// > are encoded in one octet having the normal US-ASCII value, and any octet with such a value
108    /// > can only stand for a US-ASCII character, and nothing else.
109    /// > https://www.rfc-editor.org/rfc/rfc3629
110    const fn validate(name: &str) -> Result<(), StorageSlotNameError> {
111        let bytes = name.as_bytes();
112        let mut idx = 0;
113        let mut num_components = 0;
114
115        if bytes.is_empty() {
116            return Err(StorageSlotNameError::TooShort);
117        }
118
119        if bytes.len() > Self::MAX_LENGTH {
120            return Err(StorageSlotNameError::TooLong);
121        }
122
123        // Slot names must not start with a colon or underscore.
124        // SAFETY: We just checked that we're not dealing with an empty slice.
125        if bytes[0] == b':' {
126            return Err(StorageSlotNameError::UnexpectedColon);
127        } else if bytes[0] == b'_' {
128            return Err(StorageSlotNameError::UnexpectedUnderscore);
129        }
130
131        while idx < bytes.len() {
132            let byte = bytes[idx];
133
134            let is_colon = byte == b':';
135
136            if is_colon {
137                // A colon must always be followed by another colon. In other words, we
138                // expect a double colon.
139                if (idx + 1) < bytes.len() {
140                    if bytes[idx + 1] != b':' {
141                        return Err(StorageSlotNameError::UnexpectedColon);
142                    }
143                } else {
144                    return Err(StorageSlotNameError::UnexpectedColon);
145                }
146
147                // A component cannot end with a colon, so this allows us to validate the start of a
148                // component: It must not start with a colon or an underscore.
149                if (idx + 2) < bytes.len() {
150                    if bytes[idx + 2] == b':' {
151                        return Err(StorageSlotNameError::UnexpectedColon);
152                    } else if bytes[idx + 2] == b'_' {
153                        return Err(StorageSlotNameError::UnexpectedUnderscore);
154                    }
155                } else {
156                    return Err(StorageSlotNameError::UnexpectedColon);
157                }
158
159                // Advance past the double colon.
160                idx += 2;
161
162                // A double colon completes a slot name component.
163                num_components += 1;
164            } else if Self::is_valid_char(byte) {
165                idx += 1;
166            } else {
167                return Err(StorageSlotNameError::InvalidCharacter);
168            }
169        }
170
171        // The last component is not counted as part of the loop because no double colon follows.
172        num_components += 1;
173
174        if num_components < Self::MIN_NUM_COMPONENTS {
175            return Err(StorageSlotNameError::TooShort);
176        }
177
178        Ok(())
179    }
180
181    /// Returns `true` if the given byte is a valid slot name character, `false` otherwise.
182    const fn is_valid_char(byte: u8) -> bool {
183        byte.is_ascii_alphanumeric() || byte == b'_'
184    }
185}
186
187impl Ord for StorageSlotName {
188    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
189        self.id().cmp(&other.id())
190    }
191}
192
193impl PartialOrd for StorageSlotName {
194    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
195        Some(self.cmp(other))
196    }
197}
198
199impl Display for StorageSlotName {
200    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
201        f.write_str(self.as_str())
202    }
203}
204
205impl FromStr for StorageSlotName {
206    type Err = StorageSlotNameError;
207
208    fn from_str(string: &str) -> Result<Self, Self::Err> {
209        StorageSlotName::new(string)
210    }
211}
212
213impl From<StorageSlotName> for String {
214    fn from(slot_name: StorageSlotName) -> Self {
215        slot_name.name.to_string()
216    }
217}
218
219impl Serializable for StorageSlotName {
220    fn write_into<W: ByteWriter>(&self, target: &mut W) {
221        target.write_u8(self.len());
222        target.write_many(self.as_str().as_bytes())
223    }
224
225    fn get_size_hint(&self) -> usize {
226        // Slot name length + slot name bytes
227        1 + self.as_str().len()
228    }
229}
230
231impl Deserializable for StorageSlotName {
232    fn read_from<R: miden_core::utils::ByteReader>(
233        source: &mut R,
234    ) -> Result<Self, DeserializationError> {
235        let len = source.read_u8()?;
236        let name = source.read_many(len as usize)?;
237        String::from_utf8(name)
238            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
239            .and_then(|name| {
240                Self::new(name).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
241            })
242    }
243}
244
245// TESTS
246// ================================================================================================
247
248#[cfg(test)]
249mod tests {
250    use std::borrow::ToOwned;
251
252    use assert_matches::assert_matches;
253
254    use super::*;
255
256    // A string containing all allowed characters of a slot name.
257    const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
258
259    // Invalid colon or underscore tests
260    // --------------------------------------------------------------------------------------------
261
262    #[test]
263    fn slot_name_fails_on_invalid_colon_placement() {
264        // Single colon.
265        assert_matches!(
266            StorageSlotName::new(":").unwrap_err(),
267            StorageSlotNameError::UnexpectedColon
268        );
269        assert_matches!(
270            StorageSlotName::new("0::1:").unwrap_err(),
271            StorageSlotNameError::UnexpectedColon
272        );
273        assert_matches!(
274            StorageSlotName::new(":0::1").unwrap_err(),
275            StorageSlotNameError::UnexpectedColon
276        );
277        assert_matches!(
278            StorageSlotName::new("0::1:2").unwrap_err(),
279            StorageSlotNameError::UnexpectedColon
280        );
281
282        // Double colon (placed invalidly).
283        assert_matches!(
284            StorageSlotName::new("::").unwrap_err(),
285            StorageSlotNameError::UnexpectedColon
286        );
287        assert_matches!(
288            StorageSlotName::new("1::2::").unwrap_err(),
289            StorageSlotNameError::UnexpectedColon
290        );
291        assert_matches!(
292            StorageSlotName::new("::1::2").unwrap_err(),
293            StorageSlotNameError::UnexpectedColon
294        );
295
296        // Triple colon.
297        assert_matches!(
298            StorageSlotName::new(":::").unwrap_err(),
299            StorageSlotNameError::UnexpectedColon
300        );
301        assert_matches!(
302            StorageSlotName::new("1::2:::").unwrap_err(),
303            StorageSlotNameError::UnexpectedColon
304        );
305        assert_matches!(
306            StorageSlotName::new(":::1::2").unwrap_err(),
307            StorageSlotNameError::UnexpectedColon
308        );
309        assert_matches!(
310            StorageSlotName::new("1::2:::3").unwrap_err(),
311            StorageSlotNameError::UnexpectedColon
312        );
313    }
314
315    #[test]
316    fn slot_name_fails_on_invalid_underscore_placement() {
317        assert_matches!(
318            StorageSlotName::new("_one::two").unwrap_err(),
319            StorageSlotNameError::UnexpectedUnderscore
320        );
321        assert_matches!(
322            StorageSlotName::new("one::_two").unwrap_err(),
323            StorageSlotNameError::UnexpectedUnderscore
324        );
325    }
326
327    // Length validation tests
328    // --------------------------------------------------------------------------------------------
329
330    #[test]
331    fn slot_name_fails_on_empty_string() {
332        assert_matches!(StorageSlotName::new("").unwrap_err(), StorageSlotNameError::TooShort);
333    }
334
335    #[test]
336    fn slot_name_fails_on_single_component() {
337        assert_matches!(
338            StorageSlotName::new("single_component").unwrap_err(),
339            StorageSlotNameError::TooShort
340        );
341    }
342
343    #[test]
344    fn slot_name_fails_on_string_whose_length_exceeds_max_length() {
345        let mut string = get_max_length_slot_name();
346        string.push('a');
347        assert_matches!(StorageSlotName::new(string).unwrap_err(), StorageSlotNameError::TooLong);
348    }
349
350    // Alphabet validation tests
351    // --------------------------------------------------------------------------------------------
352
353    #[test]
354    fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> {
355        let name = format!("{FULL_ALPHABET}::second");
356        let slot_name = StorageSlotName::new(name.clone())?;
357        assert_eq!(slot_name.as_str(), name);
358
359        Ok(())
360    }
361
362    #[test]
363    fn slot_name_fails_on_invalid_character() {
364        assert_matches!(
365            StorageSlotName::new("na#me::second").unwrap_err(),
366            StorageSlotNameError::InvalidCharacter
367        );
368        assert_matches!(
369            StorageSlotName::new("first_entry::secönd").unwrap_err(),
370            StorageSlotNameError::InvalidCharacter
371        );
372        assert_matches!(
373            StorageSlotName::new("first::sec::th!rd").unwrap_err(),
374            StorageSlotNameError::InvalidCharacter
375        );
376    }
377
378    // Valid slot name tests
379    // --------------------------------------------------------------------------------------------
380
381    #[test]
382    fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> {
383        StorageSlotName::new("miden::component")?;
384        Ok(())
385    }
386
387    #[test]
388    fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> {
389        StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
390        Ok(())
391    }
392
393    #[test]
394    fn slot_name_with_max_length_is_valid() -> anyhow::Result<()> {
395        StorageSlotName::new(get_max_length_slot_name())?;
396        Ok(())
397    }
398
399    // Serialization tests
400    // --------------------------------------------------------------------------------------------
401
402    #[test]
403    fn serde_slot_name() -> anyhow::Result<()> {
404        let slot_name = StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
405        assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?);
406        Ok(())
407    }
408
409    #[test]
410    fn serde_max_length_slot_name() -> anyhow::Result<()> {
411        let slot_name = StorageSlotName::new(get_max_length_slot_name())?;
412        assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?);
413        Ok(())
414    }
415
416    // Test helpers
417    // --------------------------------------------------------------------------------------------
418
419    fn get_max_length_slot_name() -> String {
420        const MIDEN_STR: &str = "miden::";
421        let remainder = ['a'; StorageSlotName::MAX_LENGTH - MIDEN_STR.len()];
422        let mut string = MIDEN_STR.to_owned();
423        string.extend(remainder);
424        assert_eq!(string.len(), StorageSlotName::MAX_LENGTH);
425        string
426    }
427}