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}