radix_common/data/scrypto/model/
non_fungible_local_id.rs

1use crate::data::manifest::ManifestCustomValueKind;
2use crate::data::scrypto::model::*;
3use crate::data::scrypto::*;
4use crate::*;
5use radix_rust::copy_u8_array;
6use sbor::rust::prelude::*;
7use sbor::*;
8
9pub const NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH: usize = 64;
10
11/// Marks the rust type that represents a non-fungible id, of any kind (i.e. String, Integer, Bytes and RUID).
12pub trait IsNonFungibleLocalId: Into<NonFungibleLocalId> {
13    fn id_type() -> NonFungibleIdType;
14}
15
16impl IsNonFungibleLocalId for StringNonFungibleLocalId {
17    fn id_type() -> NonFungibleIdType {
18        NonFungibleIdType::String
19    }
20}
21impl IsNonFungibleLocalId for IntegerNonFungibleLocalId {
22    fn id_type() -> NonFungibleIdType {
23        NonFungibleIdType::Integer
24    }
25}
26impl IsNonFungibleLocalId for BytesNonFungibleLocalId {
27    fn id_type() -> NonFungibleIdType {
28        NonFungibleIdType::Bytes
29    }
30}
31impl IsNonFungibleLocalId for RUIDNonFungibleLocalId {
32    fn id_type() -> NonFungibleIdType {
33        NonFungibleIdType::RUID
34    }
35}
36
37/// Marks the rust type that represents a non-fungible id, of non-auto-generated kind (i.e. String, Integer and Bytes).
38pub trait IsNonAutoGeneratedNonFungibleLocalId: IsNonFungibleLocalId {}
39
40impl IsNonAutoGeneratedNonFungibleLocalId for StringNonFungibleLocalId {}
41impl IsNonAutoGeneratedNonFungibleLocalId for IntegerNonFungibleLocalId {}
42impl IsNonAutoGeneratedNonFungibleLocalId for BytesNonFungibleLocalId {}
43
44impl TryFrom<String> for NonFungibleLocalId {
45    type Error = ContentValidationError;
46
47    fn try_from(value: String) -> Result<Self, Self::Error> {
48        Ok(StringNonFungibleLocalId::new(value)?.into())
49    }
50}
51
52impl From<u64> for NonFungibleLocalId {
53    fn from(value: u64) -> Self {
54        IntegerNonFungibleLocalId::new(value).into()
55    }
56}
57
58impl TryFrom<Vec<u8>> for NonFungibleLocalId {
59    type Error = ContentValidationError;
60
61    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
62        Ok(BytesNonFungibleLocalId::new(value)?.into())
63    }
64}
65
66impl From<[u8; 32]> for NonFungibleLocalId {
67    fn from(value: [u8; 32]) -> Self {
68        Self::RUID(value.into())
69    }
70}
71
72/// Represents the local id of a non-fungible.
73#[cfg_attr(
74    feature = "fuzzing",
75    derive(::arbitrary::Arbitrary, ::serde::Serialize, ::serde::Deserialize)
76)]
77#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub enum NonFungibleLocalId {
79    /// String matching `[_0-9a-zA-Z]{1,64}`.
80    ///
81    /// Create using `NonFungibleLocalId::string(...).unwrap()`.
82    String(StringNonFungibleLocalId),
83    /// Unsigned integers, up to u64.
84    ///
85    /// Create using `NonFungibleLocalId::integer(...)`.
86    Integer(IntegerNonFungibleLocalId),
87    /// Bytes, of length between 1 and 64.
88    ///
89    /// Create using `NonFungibleLocalId::bytes(...).unwrap()`.
90    Bytes(BytesNonFungibleLocalId),
91    /// RUID, v4, variant 1, big endian. See https://www.rfc-editor.org/rfc/rfc4122
92    ///
93    /// Create using `NonFungibleLocalId::ruid(...).unwrap()`.
94    RUID(RUIDNonFungibleLocalId),
95}
96
97impl NonFungibleLocalId {
98    pub fn string<T: AsRef<str>>(value: T) -> Result<Self, ContentValidationError> {
99        StringNonFungibleLocalId::new(value).map(Self::String)
100    }
101
102    pub const fn integer(value: u64) -> Self {
103        Self::Integer(IntegerNonFungibleLocalId(value))
104    }
105
106    pub fn bytes<T: Into<Vec<u8>>>(value: T) -> Result<Self, ContentValidationError> {
107        value.into().try_into()
108    }
109
110    pub const fn ruid(value: [u8; 32]) -> Self {
111        Self::RUID(RUIDNonFungibleLocalId(value))
112    }
113
114    pub fn to_key(&self) -> Vec<u8> {
115        scrypto_encode(self).expect("Failed to encode non-fungible local id")
116    }
117}
118
119/// The implementation of const constructors for the non-fungible local id.
120///
121/// The const constructors are different from the non-const constructors for two main reasons:
122/// 1. They have a more restricted interface that works with the nature of what is and is not
123///    allowed in const contexts. As an example, [`&'static str`] for the string non-fungible local
124///    id instead of an [`AsRef<[u8]>`].
125/// 2. We wish to maintain backward compatibility of the existing interface and make very little
126///    changes there.
127impl NonFungibleLocalId {
128    pub const fn const_string(value: &'static str) -> Result<Self, ContentValidationError> {
129        match StringNonFungibleLocalId::validate_slice(value.as_bytes()) {
130            Ok(()) => Ok(Self::String(StringNonFungibleLocalId(Cow::Borrowed(value)))),
131            Err(error) => Err(error),
132        }
133    }
134
135    pub const fn const_integer(value: u64) -> Self {
136        Self::integer(value)
137    }
138
139    pub const fn const_bytes(value: &'static [u8]) -> Result<Self, ContentValidationError> {
140        match BytesNonFungibleLocalId::validate(value) {
141            Ok(()) => Ok(Self::Bytes(BytesNonFungibleLocalId(Cow::Borrowed(value)))),
142            Err(error) => Err(error),
143        }
144    }
145
146    pub const fn const_ruid(value: [u8; 32]) -> Self {
147        Self::ruid(value)
148    }
149}
150
151impl From<StringNonFungibleLocalId> for NonFungibleLocalId {
152    fn from(value: StringNonFungibleLocalId) -> Self {
153        Self::String(value)
154    }
155}
156
157impl From<IntegerNonFungibleLocalId> for NonFungibleLocalId {
158    fn from(value: IntegerNonFungibleLocalId) -> Self {
159        Self::Integer(value)
160    }
161}
162
163impl From<BytesNonFungibleLocalId> for NonFungibleLocalId {
164    fn from(value: BytesNonFungibleLocalId) -> Self {
165        Self::Bytes(value)
166    }
167}
168
169impl From<RUIDNonFungibleLocalId> for NonFungibleLocalId {
170    fn from(value: RUIDNonFungibleLocalId) -> Self {
171        Self::RUID(value)
172    }
173}
174
175/// A string matching `[_0-9a-zA-Z]{1,64}`.
176#[cfg_attr(feature = "fuzzing", derive(::serde::Serialize, ::serde::Deserialize))]
177#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
178pub struct StringNonFungibleLocalId(Cow<'static, str>);
179
180impl StringNonFungibleLocalId {
181    pub fn new<S: AsRef<str>>(id: S) -> Result<Self, ContentValidationError> {
182        Self::validate_slice(id.as_ref().as_bytes())
183            .map(|_| Self(Cow::Owned(id.as_ref().to_owned())))
184    }
185
186    pub const fn validate_slice(slice: &[u8]) -> Result<(), ContentValidationError> {
187        if slice.is_empty() {
188            return Err(ContentValidationError::Empty);
189        }
190        if slice.len() > NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH {
191            return Err(ContentValidationError::TooLong);
192        }
193
194        // For loops are not permitted in const contexts. Thus, the only thing that can be used here
195        // is a `while` to check all of the characters. Alternatively, a recursive check is also an
196        // option.
197        // Tracking issue: https://github.com/rust-lang/rust/issues/87575
198        let mut index = 0usize;
199        let slice_len = slice.len();
200        while index < slice_len {
201            let byte = slice[index];
202
203            if !(byte >= b'a' && byte <= b'z'
204                || byte >= b'A' && byte <= b'Z'
205                || byte >= b'0' && byte <= b'9'
206                || byte == b'_')
207            {
208                return Err(ContentValidationError::ContainsBadCharacter);
209            }
210
211            index += 1;
212        }
213
214        Ok(())
215    }
216
217    pub fn value(&self) -> &str {
218        self.0.as_ref()
219    }
220
221    pub fn as_bytes(&self) -> &[u8] {
222        self.value().as_bytes()
223    }
224}
225
226#[cfg(feature = "fuzzing")]
227impl<'a> ::arbitrary::Arbitrary<'a> for StringNonFungibleLocalId {
228    fn arbitrary(u: &mut ::arbitrary::Unstructured<'a>) -> ::arbitrary::Result<Self> {
229        let charset: Vec<char> =
230            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWZYZ012345678989_"
231                .chars()
232                .collect();
233        let len: u8 = u
234            .int_in_range(1..=NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH as u8)
235            .unwrap();
236        let s: String = (0..len).map(|_| *u.choose(&charset[..]).unwrap()).collect();
237
238        Ok(Self(Cow::Owned(s)))
239    }
240}
241
242impl TryFrom<String> for StringNonFungibleLocalId {
243    type Error = ContentValidationError;
244
245    fn try_from(value: String) -> Result<Self, Self::Error> {
246        Self::new(value)
247    }
248}
249
250impl TryFrom<&str> for StringNonFungibleLocalId {
251    type Error = ContentValidationError;
252
253    fn try_from(value: &str) -> Result<Self, Self::Error> {
254        Self::new(value)
255    }
256}
257
258/// Unsigned integers, up to u64.
259#[cfg_attr(
260    feature = "fuzzing",
261    derive(::arbitrary::Arbitrary, ::serde::Serialize, ::serde::Deserialize)
262)]
263#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
264pub struct IntegerNonFungibleLocalId(u64);
265
266impl IntegerNonFungibleLocalId {
267    pub fn new(id: u64) -> Self {
268        Self(id)
269    }
270
271    pub fn value(&self) -> u64 {
272        self.0
273    }
274}
275
276impl From<u64> for IntegerNonFungibleLocalId {
277    fn from(value: u64) -> Self {
278        IntegerNonFungibleLocalId::new(value)
279    }
280}
281
282/// Bytes, of length between 1 and 64.
283#[cfg_attr(feature = "fuzzing", derive(::serde::Serialize, ::serde::Deserialize))]
284#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
285pub struct BytesNonFungibleLocalId(Cow<'static, [u8]>);
286
287impl BytesNonFungibleLocalId {
288    pub fn new(id: Vec<u8>) -> Result<Self, ContentValidationError> {
289        Self::validate(&id).map(|_| Self(Cow::Owned(id)))
290    }
291
292    pub const fn validate(slice: &[u8]) -> Result<(), ContentValidationError> {
293        if slice.is_empty() {
294            return Err(ContentValidationError::Empty);
295        }
296        if slice.len() > NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH {
297            return Err(ContentValidationError::TooLong);
298        }
299        Ok(())
300    }
301
302    pub fn value(&self) -> &[u8] {
303        self.0.as_ref()
304    }
305}
306
307#[cfg(feature = "fuzzing")]
308impl<'a> ::arbitrary::Arbitrary<'a> for BytesNonFungibleLocalId {
309    fn arbitrary(u: &mut ::arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
310        let len: u8 = u
311            .int_in_range(1..=NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH as u8)
312            .unwrap();
313        let s = (0..len).map(|_| u8::arbitrary(u).unwrap()).collect();
314
315        Ok(Self(s))
316    }
317}
318
319impl TryFrom<Vec<u8>> for BytesNonFungibleLocalId {
320    type Error = ContentValidationError;
321
322    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
323        Self::new(value)
324    }
325}
326
327#[cfg_attr(
328    feature = "fuzzing",
329    derive(::arbitrary::Arbitrary, ::serde::Serialize, ::serde::Deserialize)
330)]
331/// RUID, v4, variant 1, big endian. See https://www.rfc-editor.org/rfc/rfc4122
332#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
333pub struct RUIDNonFungibleLocalId([u8; 32]);
334
335impl RUIDNonFungibleLocalId {
336    pub fn new(id: [u8; 32]) -> Self {
337        Self(id)
338    }
339
340    pub fn value(&self) -> &[u8; 32] {
341        &self.0
342    }
343}
344
345impl From<[u8; 32]> for RUIDNonFungibleLocalId {
346    fn from(value: [u8; 32]) -> Self {
347        Self::new(value)
348    }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub enum ContentValidationError {
353    TooLong,
354    Empty,
355    ContainsBadCharacter,
356}
357
358impl NonFungibleLocalId {
359    pub fn id_type(&self) -> NonFungibleIdType {
360        match self {
361            NonFungibleLocalId::String(..) => NonFungibleIdType::String,
362            NonFungibleLocalId::Integer(..) => NonFungibleIdType::Integer,
363            NonFungibleLocalId::Bytes(..) => NonFungibleIdType::Bytes,
364            NonFungibleLocalId::RUID(..) => NonFungibleIdType::RUID,
365        }
366    }
367
368    pub fn encode_body_common<X: CustomValueKind, E: Encoder<X>>(
369        &self,
370        encoder: &mut E,
371    ) -> Result<(), EncodeError> {
372        match self {
373            NonFungibleLocalId::String(v) => {
374                encoder.write_discriminator(0)?;
375                encoder.write_size(v.0.len())?;
376                encoder.write_slice(v.as_bytes())?;
377            }
378            NonFungibleLocalId::Integer(v) => {
379                encoder.write_discriminator(1)?;
380                encoder.write_slice(&v.0.to_be_bytes())?; // TODO: variable length encoding?
381            }
382            NonFungibleLocalId::Bytes(v) => {
383                encoder.write_discriminator(2)?;
384                encoder.write_size(v.0.len())?;
385                encoder.write_slice(v.0.as_ref())?;
386            }
387            NonFungibleLocalId::RUID(v) => {
388                encoder.write_discriminator(3)?;
389                encoder.write_slice(v.value().as_slice())?;
390            }
391        }
392        Ok(())
393    }
394
395    pub fn to_vec(&self) -> Vec<u8> {
396        let mut buffer = Vec::new();
397        let mut encoder = ScryptoEncoder::new(&mut buffer, 1);
398        self.encode_body_common(&mut encoder).unwrap();
399        buffer
400    }
401
402    pub fn decode_body_common<X: CustomValueKind, D: Decoder<X>>(
403        decoder: &mut D,
404    ) -> Result<Self, DecodeError> {
405        match decoder.read_discriminator()? {
406            0 => {
407                let size = decoder.read_size()?;
408                let slice = decoder.read_slice(size)?;
409                let str =
410                    core::str::from_utf8(slice).map_err(|_| DecodeError::InvalidCustomValue)?;
411                Self::string(str).map_err(|_| DecodeError::InvalidCustomValue)
412            }
413            1 => Ok(Self::integer(u64::from_be_bytes(copy_u8_array(
414                decoder.read_slice(8)?,
415            )))),
416            2 => {
417                let size = decoder.read_size()?;
418                Self::bytes(decoder.read_slice(size)?.to_vec())
419                    .map_err(|_| DecodeError::InvalidCustomValue)
420            }
421            3 => Ok(Self::ruid(copy_u8_array(decoder.read_slice(32)?))),
422            _ => Err(DecodeError::InvalidCustomValue),
423        }
424    }
425}
426
427//========
428// error
429//========
430
431/// Represents an error when decoding non-fungible id.
432#[derive(Debug, Clone, PartialEq, Eq)]
433pub enum ParseNonFungibleLocalIdError {
434    UnknownType,
435    InvalidInteger,
436    InvalidBytes,
437    InvalidRUID,
438    ContentValidationError(ContentValidationError),
439}
440
441#[cfg(not(feature = "alloc"))]
442impl std::error::Error for ParseNonFungibleLocalIdError {}
443
444#[cfg(not(feature = "alloc"))]
445impl fmt::Display for ParseNonFungibleLocalIdError {
446    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
447        write!(f, "{:?}", self)
448    }
449}
450
451//========
452// binary
453//========
454
455impl Categorize<ScryptoCustomValueKind> for NonFungibleLocalId {
456    #[inline]
457    fn value_kind() -> ValueKind<ScryptoCustomValueKind> {
458        ValueKind::Custom(ScryptoCustomValueKind::NonFungibleLocalId)
459    }
460}
461
462impl<E: Encoder<ScryptoCustomValueKind>> Encode<ScryptoCustomValueKind, E> for NonFungibleLocalId {
463    #[inline]
464    fn encode_value_kind(&self, encoder: &mut E) -> Result<(), EncodeError> {
465        encoder.write_value_kind(Self::value_kind())
466    }
467
468    #[inline]
469    fn encode_body(&self, encoder: &mut E) -> Result<(), EncodeError> {
470        self.encode_body_common(encoder)
471    }
472}
473
474impl<D: Decoder<ScryptoCustomValueKind>> Decode<ScryptoCustomValueKind, D> for NonFungibleLocalId {
475    fn decode_body_with_value_kind(
476        decoder: &mut D,
477        value_kind: ValueKind<ScryptoCustomValueKind>,
478    ) -> Result<Self, DecodeError> {
479        decoder.check_preloaded_value_kind(value_kind, Self::value_kind())?;
480        Self::decode_body_common(decoder)
481    }
482}
483
484//====================
485// binary (manifest)
486//====================
487
488impl Categorize<ManifestCustomValueKind> for NonFungibleLocalId {
489    #[inline]
490    fn value_kind() -> ValueKind<ManifestCustomValueKind> {
491        ValueKind::Custom(ManifestCustomValueKind::NonFungibleLocalId)
492    }
493}
494
495impl<E: Encoder<ManifestCustomValueKind>> Encode<ManifestCustomValueKind, E>
496    for NonFungibleLocalId
497{
498    #[inline]
499    fn encode_value_kind(&self, encoder: &mut E) -> Result<(), EncodeError> {
500        encoder.write_value_kind(Self::value_kind())
501    }
502
503    #[inline]
504    fn encode_body(&self, encoder: &mut E) -> Result<(), EncodeError> {
505        self.encode_body_common(encoder)
506    }
507}
508
509impl<D: Decoder<ManifestCustomValueKind>> Decode<ManifestCustomValueKind, D>
510    for NonFungibleLocalId
511{
512    fn decode_body_with_value_kind(
513        decoder: &mut D,
514        value_kind: ValueKind<ManifestCustomValueKind>,
515    ) -> Result<Self, DecodeError> {
516        decoder.check_preloaded_value_kind(value_kind, Self::value_kind())?;
517        Self::decode_body_common(decoder)
518    }
519}
520
521impl Describe<ScryptoCustomTypeKind> for NonFungibleLocalId {
522    const TYPE_ID: RustTypeId =
523        RustTypeId::WellKnown(well_known_scrypto_custom_types::NON_FUNGIBLE_LOCAL_ID_TYPE);
524
525    fn type_data() -> TypeData<ScryptoCustomTypeKind, RustTypeId> {
526        well_known_scrypto_custom_types::non_fungible_local_id_type_data()
527    }
528}
529
530//======
531// text
532//======
533
534/// We wish to be stricter than `from_str_radix` in order to ensure a canonical format, and in particular:
535/// * Not allow + at the start
536/// * Not allow leading 0s
537/// * Not allow an empty string
538fn is_canonically_formatted_integer(digits: &str) -> bool {
539    if digits == "0" {
540        return true;
541    }
542    let mut chars = digits.chars();
543    // A non-zero integer must start with a digit between 1 and 9
544    let first_char = chars.next();
545    match first_char {
546        None => {
547            return false;
548        }
549        Some('1'..='9') => {}
550        _ => {
551            return false;
552        }
553    }
554    // The remaining chars must be digits
555    for char in chars {
556        if !char.is_ascii_digit() {
557            return false;
558        }
559    }
560    true
561}
562
563impl FromStr for NonFungibleLocalId {
564    type Err = ParseNonFungibleLocalIdError;
565
566    fn from_str(s: &str) -> Result<Self, Self::Err> {
567        let local_id = if s.starts_with('<') && s.ends_with('>') {
568            Self::string(&s[1..s.len() - 1])
569                .map_err(ParseNonFungibleLocalIdError::ContentValidationError)?
570        } else if s.len() > 1 && s.starts_with('#') && s.ends_with('#') {
571            let digits = &s[1..s.len() - 1];
572            if !is_canonically_formatted_integer(digits) {
573                return Err(ParseNonFungibleLocalIdError::InvalidInteger);
574            }
575            NonFungibleLocalId::integer(
576                s[1..s.len() - 1]
577                    .parse::<u64>()
578                    .map_err(|_| ParseNonFungibleLocalIdError::InvalidInteger)?,
579            )
580        } else if s.starts_with('[') && s.ends_with(']') {
581            NonFungibleLocalId::bytes(
582                hex::decode(&s[1..s.len() - 1])
583                    .map_err(|_| ParseNonFungibleLocalIdError::InvalidBytes)?,
584            )
585            .map_err(ParseNonFungibleLocalIdError::ContentValidationError)?
586        } else if s.starts_with('{') && s.ends_with('}') {
587            let chars: Vec<char> = s[1..s.len() - 1].chars().collect();
588            if chars.len() == 32 * 2 + 3 && chars[16] == '-' && chars[33] == '-' && chars[50] == '-'
589            {
590                let hyphen_stripped: String = chars.into_iter().filter(|c| *c != '-').collect();
591                if hyphen_stripped.len() == 64 {
592                    NonFungibleLocalId::RUID(RUIDNonFungibleLocalId(
593                        hex::decode(&hyphen_stripped)
594                            .map_err(|_| ParseNonFungibleLocalIdError::InvalidRUID)?
595                            .try_into()
596                            .unwrap(),
597                    ))
598                } else {
599                    return Err(ParseNonFungibleLocalIdError::InvalidRUID);
600                }
601            } else {
602                return Err(ParseNonFungibleLocalIdError::InvalidRUID);
603            }
604        } else {
605            return Err(ParseNonFungibleLocalIdError::UnknownType);
606        };
607
608        Ok(local_id)
609    }
610}
611
612impl fmt::Display for NonFungibleLocalId {
613    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
614        match self {
615            NonFungibleLocalId::String(v) => write!(f, "<{}>", v.value()),
616            NonFungibleLocalId::Integer(IntegerNonFungibleLocalId(v)) => write!(f, "#{}#", v),
617            NonFungibleLocalId::Bytes(BytesNonFungibleLocalId(v)) => {
618                write!(f, "[{}]", hex::encode(v))
619            }
620            NonFungibleLocalId::RUID(RUIDNonFungibleLocalId(v)) => {
621                let hex = hex::encode(v.as_slice());
622                write!(
623                    f,
624                    "{{{}-{}-{}-{}}}",
625                    &hex[0..16],
626                    &hex[16..32],
627                    &hex[32..48],
628                    &hex[48..64]
629                )
630            }
631        }
632    }
633}
634
635impl fmt::Debug for NonFungibleLocalId {
636    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
637        write!(f, "{}", self)
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    use crate::internal_prelude::*;
645
646    #[test]
647    fn test_non_fungible_length_validation() {
648        // Bytes length
649        let validation_result = NonFungibleLocalId::bytes([0; NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH]);
650        assert_matches!(validation_result, Ok(_));
651        let validation_result =
652            NonFungibleLocalId::bytes([0; 1 + NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH]);
653        assert_eq!(validation_result, Err(ContentValidationError::TooLong));
654        let validation_result = NonFungibleLocalId::bytes(vec![]);
655        assert_eq!(validation_result, Err(ContentValidationError::Empty));
656
657        // String length
658        let validation_result =
659            NonFungibleLocalId::string(string_of_length(NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH));
660        assert_matches!(validation_result, Ok(_));
661        let validation_result =
662            NonFungibleLocalId::string(string_of_length(1 + NON_FUNGIBLE_LOCAL_ID_MAX_LENGTH));
663        assert_eq!(validation_result, Err(ContentValidationError::TooLong));
664        let validation_result = NonFungibleLocalId::string("");
665        assert_eq!(validation_result, Err(ContentValidationError::Empty));
666
667        let validation_result =
668            NonFungibleLocalId::from_str("{--------------4----8---------------1}");
669        assert_eq!(
670            validation_result,
671            Err(ParseNonFungibleLocalIdError::InvalidRUID)
672        );
673    }
674
675    fn string_of_length(size: usize) -> String {
676        let mut str_buf = String::new();
677        for _ in 0..size {
678            str_buf.push('a');
679        }
680        str_buf
681    }
682
683    #[test]
684    fn test_non_fungible_string_validation() {
685        let valid_id_string = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWZYZ_0123456789";
686        let validation_result = NonFungibleLocalId::string(valid_id_string);
687        assert!(validation_result.is_ok());
688
689        test_invalid_char('.');
690        test_invalid_char('`');
691        test_invalid_char('\\');
692        test_invalid_char('"');
693        test_invalid_char(' ');
694        test_invalid_char('\r');
695        test_invalid_char('\n');
696        test_invalid_char('\t');
697        test_invalid_char('\u{0000}'); // Null
698        test_invalid_char('\u{0301}'); // Combining acute accent
699        test_invalid_char('\u{2764}'); // ❤
700        test_invalid_char('\u{000C}'); // Form feed
701        test_invalid_char('\u{202D}'); // LTR override
702        test_invalid_char('\u{202E}'); // RTL override
703        test_invalid_char('\u{1F600}'); // :-) emoji
704
705        test_invalid_string("`HelloWorld"); // Invalid char at the start
706        test_invalid_string("Hello`World"); // Invalid char in the middle
707        test_invalid_string("HelloWorld`"); // Invalid char at the end
708    }
709
710    fn test_invalid_char(char: char) {
711        let validation_result = NonFungibleLocalId::string(format!("valid_{}", char));
712        assert_eq!(
713            validation_result,
714            Err(ContentValidationError::ContainsBadCharacter)
715        );
716    }
717
718    fn test_invalid_string(string: &str) {
719        let validation_result = NonFungibleLocalId::string(string);
720        assert_eq!(
721            validation_result,
722            Err(ContentValidationError::ContainsBadCharacter)
723        );
724    }
725
726    #[test]
727    fn test_from_str() {
728        // Unknown type
729        assert_eq!(
730            NonFungibleLocalId::from_str("#"),
731            Err(ParseNonFungibleLocalIdError::UnknownType)
732        );
733        assert_eq!(
734            NonFungibleLocalId::from_str("{"),
735            Err(ParseNonFungibleLocalIdError::UnknownType)
736        );
737        assert_eq!(
738            NonFungibleLocalId::from_str("<"),
739            Err(ParseNonFungibleLocalIdError::UnknownType)
740        );
741        assert_eq!(
742            NonFungibleLocalId::from_str("["),
743            Err(ParseNonFungibleLocalIdError::UnknownType)
744        );
745        // Integers and invalid integers:
746        assert_eq!(
747            NonFungibleLocalId::from_str("#1#").unwrap(),
748            NonFungibleLocalId::integer(1)
749        );
750        assert_eq!(
751            NonFungibleLocalId::from_str("#10#").unwrap(),
752            NonFungibleLocalId::integer(10)
753        );
754        assert_eq!(
755            NonFungibleLocalId::from_str("#0#").unwrap(),
756            NonFungibleLocalId::integer(0)
757        );
758        // Non-canonical, invalid integers
759        assert_eq!(
760            NonFungibleLocalId::from_str("##"),
761            Err(ParseNonFungibleLocalIdError::InvalidInteger)
762        );
763        assert_eq!(
764            NonFungibleLocalId::from_str("#+10#"),
765            Err(ParseNonFungibleLocalIdError::InvalidInteger)
766        );
767        assert_eq!(
768            NonFungibleLocalId::from_str("#010#"),
769            Err(ParseNonFungibleLocalIdError::InvalidInteger)
770        );
771        assert_eq!(
772            NonFungibleLocalId::from_str("# 10#"),
773            Err(ParseNonFungibleLocalIdError::InvalidInteger)
774        );
775        assert_eq!(
776            NonFungibleLocalId::from_str("#000#"),
777            Err(ParseNonFungibleLocalIdError::InvalidInteger)
778        );
779        assert_eq!(
780            NonFungibleLocalId::from_str("#-10#"),
781            Err(ParseNonFungibleLocalIdError::InvalidInteger)
782        );
783        assert_eq!(
784            NonFungibleLocalId::from_str(
785                "{1111111111111111-1111111111111111-1111111111111111-1111111111111111}"
786            )
787            .unwrap(),
788            NonFungibleLocalId::ruid([0x11; 32])
789        );
790        assert_eq!(
791            NonFungibleLocalId::from_str("<test>").unwrap(),
792            NonFungibleLocalId::string("test").unwrap()
793        );
794        assert_eq!(
795            NonFungibleLocalId::from_str("[010a]").unwrap(),
796            NonFungibleLocalId::bytes(vec![1, 10]).unwrap()
797        );
798    }
799
800    #[test]
801    fn test_to_string() {
802        assert_eq!(NonFungibleLocalId::integer(0).to_string(), "#0#",);
803        assert_eq!(NonFungibleLocalId::integer(1).to_string(), "#1#",);
804        assert_eq!(NonFungibleLocalId::integer(10).to_string(), "#10#",);
805        assert_eq!(
806            NonFungibleLocalId::ruid([
807                0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
808                0x22, 0x22, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44,
809                0x44, 0x44, 0x44, 0x44,
810            ])
811            .to_string(),
812            "{1111111111111111-2222222222222222-3333333333333333-4444444444444444}",
813        );
814        assert_eq!(
815            NonFungibleLocalId::string("test").unwrap().to_string(),
816            "<test>"
817        );
818        assert_eq!(
819            NonFungibleLocalId::bytes(vec![1, 10]).unwrap().to_string(),
820            "[010a]"
821        );
822    }
823
824    #[test]
825    fn const_non_fungible_local_ids_can_be_created() {
826        const _INTEGER: NonFungibleLocalId = NonFungibleLocalId::const_integer(1);
827        const _RUID: NonFungibleLocalId = NonFungibleLocalId::const_ruid([
828            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
829            0x22, 0x22, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44,
830            0x44, 0x44, 0x44, 0x44,
831        ]);
832        const STRING: Result<NonFungibleLocalId, ContentValidationError> =
833            NonFungibleLocalId::const_string("HelloWorld");
834        const BYTES: Result<NonFungibleLocalId, ContentValidationError> =
835            NonFungibleLocalId::const_bytes(&[
836                110, 101, 118, 101, 114, 32, 103, 111, 110, 110, 97, 32, 103, 105, 118, 101, 32,
837                121, 111, 117, 32, 117, 112,
838            ]);
839
840        assert!(STRING.is_ok());
841        assert!(BYTES.is_ok());
842    }
843}