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::{
9 ByteReader,
10 ByteWriter,
11 Deserializable,
12 DeserializationError,
13 Serializable,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct StorageSlotName {
45 name: Arc<str>,
46 id: StorageSlotId,
47}
48
49impl StorageSlotName {
50 pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
55
56 pub(crate) const MAX_LENGTH: usize = u8::MAX as usize;
58
59 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 pub fn as_str(&self) -> &str {
80 &self.name
81 }
82
83 #[allow(clippy::len_without_is_empty)]
87 pub fn len(&self) -> u8 {
88 debug_assert!(self.name.len() <= Self::MAX_LENGTH);
90 self.name.len() as u8
91 }
92
93 pub fn id(&self) -> StorageSlotId {
95 self.id
96 }
97
98 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 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 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 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 idx += 2;
167
168 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 num_components += 1;
179
180 if num_components < Self::MIN_NUM_COMPONENTS {
181 return Err(StorageSlotNameError::TooShort);
182 }
183
184 Ok(())
185 }
186
187 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 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#[cfg(test)]
269mod tests {
270 use std::borrow::ToOwned;
271
272 use assert_matches::assert_matches;
273
274 use super::*;
275
276 const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
278
279 #[test]
283 fn slot_name_fails_on_invalid_colon_placement() {
284 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 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 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 #[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 #[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 #[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 #[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 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}