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