Skip to main content

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 TryFrom<&str> for StorageSlotName {
214    type Error = StorageSlotNameError;
215
216    fn try_from(value: &str) -> Result<Self, Self::Error> {
217        value.parse()
218    }
219}
220
221impl TryFrom<String> for StorageSlotName {
222    type Error = StorageSlotNameError;
223
224    fn try_from(value: String) -> Result<Self, Self::Error> {
225        value.parse()
226    }
227}
228
229impl From<StorageSlotName> for String {
230    fn from(slot_name: StorageSlotName) -> Self {
231        slot_name.name.to_string()
232    }
233}
234
235impl Serializable for StorageSlotName {
236    fn write_into<W: ByteWriter>(&self, target: &mut W) {
237        target.write_u8(self.len());
238        target.write_many(self.as_str().as_bytes())
239    }
240
241    fn get_size_hint(&self) -> usize {
242        // Slot name length + slot name bytes
243        1 + self.as_str().len()
244    }
245}
246
247impl Deserializable for StorageSlotName {
248    fn read_from<R: miden_core::utils::ByteReader>(
249        source: &mut R,
250    ) -> Result<Self, DeserializationError> {
251        let len = source.read_u8()?;
252        let name = source.read_many(len as usize)?;
253        String::from_utf8(name)
254            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
255            .and_then(|name| {
256                Self::new(name).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
257            })
258    }
259}
260
261// TESTS
262// ================================================================================================
263
264#[cfg(test)]
265mod tests {
266    use std::borrow::ToOwned;
267
268    use assert_matches::assert_matches;
269
270    use super::*;
271
272    // A string containing all allowed characters of a slot name.
273    const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
274
275    // Invalid colon or underscore tests
276    // --------------------------------------------------------------------------------------------
277
278    #[test]
279    fn slot_name_fails_on_invalid_colon_placement() {
280        // Single colon.
281        assert_matches!(
282            StorageSlotName::new(":").unwrap_err(),
283            StorageSlotNameError::UnexpectedColon
284        );
285        assert_matches!(
286            StorageSlotName::new("0::1:").unwrap_err(),
287            StorageSlotNameError::UnexpectedColon
288        );
289        assert_matches!(
290            StorageSlotName::new(":0::1").unwrap_err(),
291            StorageSlotNameError::UnexpectedColon
292        );
293        assert_matches!(
294            StorageSlotName::new("0::1:2").unwrap_err(),
295            StorageSlotNameError::UnexpectedColon
296        );
297
298        // Double colon (placed invalidly).
299        assert_matches!(
300            StorageSlotName::new("::").unwrap_err(),
301            StorageSlotNameError::UnexpectedColon
302        );
303        assert_matches!(
304            StorageSlotName::new("1::2::").unwrap_err(),
305            StorageSlotNameError::UnexpectedColon
306        );
307        assert_matches!(
308            StorageSlotName::new("::1::2").unwrap_err(),
309            StorageSlotNameError::UnexpectedColon
310        );
311
312        // Triple colon.
313        assert_matches!(
314            StorageSlotName::new(":::").unwrap_err(),
315            StorageSlotNameError::UnexpectedColon
316        );
317        assert_matches!(
318            StorageSlotName::new("1::2:::").unwrap_err(),
319            StorageSlotNameError::UnexpectedColon
320        );
321        assert_matches!(
322            StorageSlotName::new(":::1::2").unwrap_err(),
323            StorageSlotNameError::UnexpectedColon
324        );
325        assert_matches!(
326            StorageSlotName::new("1::2:::3").unwrap_err(),
327            StorageSlotNameError::UnexpectedColon
328        );
329    }
330
331    #[test]
332    fn slot_name_fails_on_invalid_underscore_placement() {
333        assert_matches!(
334            StorageSlotName::new("_one::two").unwrap_err(),
335            StorageSlotNameError::UnexpectedUnderscore
336        );
337        assert_matches!(
338            StorageSlotName::new("one::_two").unwrap_err(),
339            StorageSlotNameError::UnexpectedUnderscore
340        );
341    }
342
343    // Length validation tests
344    // --------------------------------------------------------------------------------------------
345
346    #[test]
347    fn slot_name_fails_on_empty_string() {
348        assert_matches!(StorageSlotName::new("").unwrap_err(), StorageSlotNameError::TooShort);
349    }
350
351    #[test]
352    fn slot_name_fails_on_single_component() {
353        assert_matches!(
354            StorageSlotName::new("single_component").unwrap_err(),
355            StorageSlotNameError::TooShort
356        );
357    }
358
359    #[test]
360    fn slot_name_fails_on_string_whose_length_exceeds_max_length() {
361        let mut string = get_max_length_slot_name();
362        string.push('a');
363        assert_matches!(StorageSlotName::new(string).unwrap_err(), StorageSlotNameError::TooLong);
364    }
365
366    // Alphabet validation tests
367    // --------------------------------------------------------------------------------------------
368
369    #[test]
370    fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> {
371        let name = format!("{FULL_ALPHABET}::second");
372        let slot_name = StorageSlotName::new(name.clone())?;
373        assert_eq!(slot_name.as_str(), name);
374
375        Ok(())
376    }
377
378    #[test]
379    fn slot_name_fails_on_invalid_character() {
380        assert_matches!(
381            StorageSlotName::new("na#me::second").unwrap_err(),
382            StorageSlotNameError::InvalidCharacter
383        );
384        assert_matches!(
385            StorageSlotName::new("first_entry::secönd").unwrap_err(),
386            StorageSlotNameError::InvalidCharacter
387        );
388        assert_matches!(
389            StorageSlotName::new("first::sec::th!rd").unwrap_err(),
390            StorageSlotNameError::InvalidCharacter
391        );
392    }
393
394    // Valid slot name tests
395    // --------------------------------------------------------------------------------------------
396
397    #[test]
398    fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> {
399        StorageSlotName::new("miden::component")?;
400        Ok(())
401    }
402
403    #[test]
404    fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> {
405        StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
406        Ok(())
407    }
408
409    #[test]
410    fn slot_name_with_max_length_is_valid() -> anyhow::Result<()> {
411        StorageSlotName::new(get_max_length_slot_name())?;
412        Ok(())
413    }
414
415    // Serialization tests
416    // --------------------------------------------------------------------------------------------
417
418    #[test]
419    fn serde_slot_name() -> anyhow::Result<()> {
420        let slot_name = StorageSlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
421        assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?);
422        Ok(())
423    }
424
425    #[test]
426    fn serde_max_length_slot_name() -> anyhow::Result<()> {
427        let slot_name = StorageSlotName::new(get_max_length_slot_name())?;
428        assert_eq!(slot_name, StorageSlotName::read_from_bytes(&slot_name.to_bytes())?);
429        Ok(())
430    }
431
432    // Test helpers
433    // --------------------------------------------------------------------------------------------
434
435    fn get_max_length_slot_name() -> String {
436        const MIDEN_STR: &str = "miden::";
437        let remainder = ['a'; StorageSlotName::MAX_LENGTH - MIDEN_STR.len()];
438        let mut string = MIDEN_STR.to_owned();
439        string.extend(remainder);
440        assert_eq!(string.len(), StorageSlotName::MAX_LENGTH);
441        string
442    }
443}