miden_protocol/account/storage/slot/
slot_name.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct StorageSlotName {
39 name: Arc<str>,
40 id: StorageSlotId,
41}
42
43impl StorageSlotName {
44 pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
49
50 pub(crate) const MAX_LENGTH: usize = u8::MAX as usize;
52
53 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 pub fn as_str(&self) -> &str {
74 &self.name
75 }
76
77 #[allow(clippy::len_without_is_empty)]
81 pub fn len(&self) -> u8 {
82 debug_assert!(self.name.len() <= Self::MAX_LENGTH);
84 self.name.len() as u8
85 }
86
87 pub fn id(&self) -> StorageSlotId {
89 self.id
90 }
91
92 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 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 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 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 idx += 2;
161
162 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 num_components += 1;
173
174 if num_components < Self::MIN_NUM_COMPONENTS {
175 return Err(StorageSlotNameError::TooShort);
176 }
177
178 Ok(())
179 }
180
181 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 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#[cfg(test)]
265mod tests {
266 use std::borrow::ToOwned;
267
268 use assert_matches::assert_matches;
269
270 use super::*;
271
272 const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
274
275 #[test]
279 fn slot_name_fails_on_invalid_colon_placement() {
280 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 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 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 #[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 #[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 #[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 #[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 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}