miden_objects/account/storage/slot/
slot_name.rs

1use alloc::borrow::Cow;
2use alloc::string::String;
3
4use crate::errors::SlotNameError;
5
6/// The name of an account storage slot.
7///
8/// A typical slot name looks like this:
9///
10/// ```text
11/// miden::basic_fungible_faucet::metadata
12/// ```
13///
14/// The double-colon (`::`) serves as a separator and the strings in between the separators are
15/// called components.
16///
17/// It is generally recommended that slot names have at least three components and follow this
18/// structure:
19///
20/// ```text
21/// organization::component::slot_name
22/// ```
23///
24/// ## Requirements
25///
26/// For a string to be a valid slot name it needs to satisfy the following criteria:
27/// - It needs to have at least 2 components.
28/// - Each component must consist of at least one character.
29/// - Each component must only consist of the characters `a` to `z`, `A` to `Z`, `0` to `9` or `_`
30///   (underscore).
31/// - Each component must not start with an underscore.
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct SlotName {
34    name: Cow<'static, str>,
35}
36
37impl SlotName {
38    // CONSTANTS
39    // --------------------------------------------------------------------------------------------
40
41    // The minimum number of components that a slot name must contain.
42    pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
43
44    // CONSTRUCTORS
45    // --------------------------------------------------------------------------------------------
46
47    /// Constructs a new [`SlotName`] from a static string.
48    ///
49    /// This function is `const` and can be used to define slot names as constants, e.g.:
50    ///
51    /// ```rust
52    /// # use miden_objects::account::SlotName;
53    /// const SLOT_NAME: SlotName = SlotName::from_static_str("miden::basic_fungible_faucet::metadata");
54    /// ```
55    ///
56    /// This is convenient because using a string that is not a valid slot name fails to compile.
57    ///
58    /// # Panics
59    ///
60    /// Panics if:
61    /// - the slot name is invalid (see the type-level docs for the requirements).
62    pub const fn from_static_str(name: &'static str) -> Self {
63        match Self::validate(name) {
64            Ok(()) => Self { name: Cow::Borrowed(name) },
65            // We cannot format the error in a const context.
66            Err(_) => panic!("invalid slot name"),
67        }
68    }
69
70    /// Constructs a new [`SlotName`] from a string.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if:
75    /// - the slot name is invalid (see the type-level docs for the requirements).
76    pub fn new(name: impl Into<String>) -> Result<Self, SlotNameError> {
77        let name = name.into();
78        Self::validate(&name)?;
79        Ok(Self { name: Cow::Owned(name) })
80    }
81
82    // ACCESSORS
83    // --------------------------------------------------------------------------------------------
84
85    /// Returns the slot name as a string slice.
86    pub fn as_str(&self) -> &str {
87        &self.name
88    }
89
90    // HELPERS
91    // --------------------------------------------------------------------------------------------
92
93    /// Validates a slot name.
94    ///
95    /// This checks that components are separated by double colons, that each component contains
96    /// only valid characters and that the name is not empty or starts or ends with a colon.
97    ///
98    /// We must check the validity of a slot name against the raw bytes of the UTF-8 string because
99    /// typical character APIs are not available in a const version. We can do this because any byte
100    /// in a UTF-8 string that is an ASCII character never represents anything other than such a
101    /// character, even though UTF-8 can contain multibyte sequences:
102    ///
103    /// > UTF-8, the object of this memo, has a one-octet encoding unit. It uses all bits of an
104    /// > octet, but has the quality of preserving the full US-ASCII range: US-ASCII characters
105    /// > are encoded in one octet having the normal US-ASCII value, and any octet with such a value
106    /// > can only stand for a US-ASCII character, and nothing else.
107    /// > https://www.rfc-editor.org/rfc/rfc3629
108    const fn validate(name: &str) -> Result<(), SlotNameError> {
109        let bytes = name.as_bytes();
110        let mut idx = 0;
111        let mut num_components = 0;
112
113        if bytes.is_empty() {
114            return Err(SlotNameError::TooShort);
115        }
116
117        // Slot names must not start with a colon or underscore.
118        // SAFETY: We just checked that we're not dealing with an empty slice.
119        if bytes[0] == b':' {
120            return Err(SlotNameError::UnexpectedColon);
121        } else if bytes[0] == b'_' {
122            return Err(SlotNameError::UnexpectedUnderscore);
123        }
124
125        while idx < bytes.len() {
126            let byte = bytes[idx];
127
128            let is_colon = byte == b':';
129
130            if is_colon {
131                // A colon must always be followed by another colon. In other words, we
132                // expect a double colon.
133                if (idx + 1) < bytes.len() {
134                    if bytes[idx + 1] != b':' {
135                        return Err(SlotNameError::UnexpectedColon);
136                    }
137                } else {
138                    return Err(SlotNameError::UnexpectedColon);
139                }
140
141                // A component cannot end with a colon, so this allows us to validate the start of a
142                // component: It must not start with a colon or an underscore.
143                if (idx + 2) < bytes.len() {
144                    if bytes[idx + 2] == b':' {
145                        return Err(SlotNameError::UnexpectedColon);
146                    } else if bytes[idx + 2] == b'_' {
147                        return Err(SlotNameError::UnexpectedUnderscore);
148                    }
149                } else {
150                    return Err(SlotNameError::UnexpectedColon);
151                }
152
153                // Advance past the double colon.
154                idx += 2;
155
156                // A double colon completes a slot name component.
157                num_components += 1;
158            } else if Self::is_valid_char(byte) {
159                idx += 1;
160            } else {
161                return Err(SlotNameError::InvalidCharacter);
162            }
163        }
164
165        // The last component is not counted as part of the loop because no double colon follows.
166        num_components += 1;
167
168        if num_components < Self::MIN_NUM_COMPONENTS {
169            return Err(SlotNameError::TooShort);
170        }
171
172        Ok(())
173    }
174
175    /// Returns `true` if the given byte is a valid slot name character, `false` otherwise.
176    const fn is_valid_char(byte: u8) -> bool {
177        byte.is_ascii_alphanumeric() || byte == b'_'
178    }
179}
180
181// TESTS
182// ================================================================================================
183
184#[cfg(test)]
185mod tests {
186    use assert_matches::assert_matches;
187
188    use super::*;
189
190    // A string containing all allowed characters of a slot name.
191    const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
192
193    // Const function tests
194    // --------------------------------------------------------------------------------------------
195
196    const _NAME0: SlotName = SlotName::from_static_str("name::component");
197    const _NAME1: SlotName = SlotName::from_static_str("one::two::three::four::five");
198    const _NAME2: SlotName = SlotName::from_static_str("one::two_three::four");
199
200    #[test]
201    #[should_panic(expected = "invalid slot name")]
202    fn slot_name_panics_on_invalid_character() {
203        SlotName::from_static_str("miden!::component");
204    }
205
206    #[test]
207    #[should_panic(expected = "invalid slot name")]
208    fn slot_name_panics_on_invalid_character2() {
209        SlotName::from_static_str("miden_ö::component");
210    }
211
212    #[test]
213    #[should_panic(expected = "invalid slot name")]
214    fn slot_name_panics_when_too_short() {
215        SlotName::from_static_str("one");
216    }
217
218    #[test]
219    #[should_panic(expected = "invalid slot name")]
220    fn slot_name_panics_on_component_starting_with_underscores() {
221        SlotName::from_static_str("one::_two");
222    }
223
224    // Invalid colon or underscore tests
225    // --------------------------------------------------------------------------------------------
226
227    #[test]
228    fn slot_name_fails_on_invalid_colon_placement() {
229        // Single colon.
230        assert_matches!(SlotName::new(":").unwrap_err(), SlotNameError::UnexpectedColon);
231        assert_matches!(SlotName::new("0::1:").unwrap_err(), SlotNameError::UnexpectedColon);
232        assert_matches!(SlotName::new(":0::1").unwrap_err(), SlotNameError::UnexpectedColon);
233        assert_matches!(SlotName::new("0::1:2").unwrap_err(), SlotNameError::UnexpectedColon);
234
235        // Double colon (placed invalidly).
236        assert_matches!(SlotName::new("::").unwrap_err(), SlotNameError::UnexpectedColon);
237        assert_matches!(SlotName::new("1::2::").unwrap_err(), SlotNameError::UnexpectedColon);
238        assert_matches!(SlotName::new("::1::2").unwrap_err(), SlotNameError::UnexpectedColon);
239
240        // Triple colon.
241        assert_matches!(SlotName::new(":::").unwrap_err(), SlotNameError::UnexpectedColon);
242        assert_matches!(SlotName::new("1::2:::").unwrap_err(), SlotNameError::UnexpectedColon);
243        assert_matches!(SlotName::new(":::1::2").unwrap_err(), SlotNameError::UnexpectedColon);
244        assert_matches!(SlotName::new("1::2:::3").unwrap_err(), SlotNameError::UnexpectedColon);
245    }
246
247    #[test]
248    fn slot_name_fails_on_invalid_underscore_placement() {
249        assert_matches!(
250            SlotName::new("_one::two").unwrap_err(),
251            SlotNameError::UnexpectedUnderscore
252        );
253        assert_matches!(
254            SlotName::new("one::_two").unwrap_err(),
255            SlotNameError::UnexpectedUnderscore
256        );
257    }
258
259    // Num components tests
260    // --------------------------------------------------------------------------------------------
261
262    #[test]
263    fn slot_name_fails_on_empty_string() {
264        assert_matches!(SlotName::new("").unwrap_err(), SlotNameError::TooShort);
265    }
266
267    #[test]
268    fn slot_name_fails_on_single_component() {
269        assert_matches!(SlotName::new("single_component").unwrap_err(), SlotNameError::TooShort);
270    }
271
272    // Alphabet validation tests
273    // --------------------------------------------------------------------------------------------
274
275    #[test]
276    fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> {
277        let name = format!("{FULL_ALPHABET}::second");
278        let slot_name = SlotName::new(&name)?;
279        assert_eq!(slot_name.as_str(), name);
280
281        Ok(())
282    }
283
284    #[test]
285    fn slot_name_fails_on_invalid_character() {
286        assert_matches!(
287            SlotName::new("na#me::second").unwrap_err(),
288            SlotNameError::InvalidCharacter
289        );
290        assert_matches!(
291            SlotName::new("first_entry::secönd").unwrap_err(),
292            SlotNameError::InvalidCharacter
293        );
294        assert_matches!(
295            SlotName::new("first::sec::th!rd").unwrap_err(),
296            SlotNameError::InvalidCharacter
297        );
298    }
299
300    // Valid slot name tests
301    // --------------------------------------------------------------------------------------------
302
303    #[test]
304    fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> {
305        SlotName::new("miden::component")?;
306        Ok(())
307    }
308
309    #[test]
310    fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> {
311        SlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
312        Ok(())
313    }
314}