Skip to main content

steel_utils/types/
identifier.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Debug, Display, Formatter},
4    mem::MaybeUninit,
5    str::FromStr,
6};
7
8use serde::{Deserialize, Serialize, de::Error as _};
9use simdnbt::owned::NbtTag;
10use wincode::{SchemaRead, SchemaWrite, config::Config, io::Reader, io::Writer};
11
12use crate::hash::{ComponentHasher, HashComponent};
13
14/// An identifier used by Minecraft.
15#[derive(Clone, PartialEq, Eq, Hash, Default)]
16pub struct Identifier {
17    /// The namespace of the identifier.
18    pub namespace: Cow<'static, str>,
19    /// The path of the identifier.
20    pub path: Cow<'static, str>,
21}
22
23impl Debug for Identifier {
24    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25        f.write_str(&format!("{}:{}", self.namespace, self.path))
26    }
27}
28
29impl Identifier {
30    /// The vanilla namespace.
31    pub const VANILLA_NAMESPACE: &'static str = "minecraft";
32    /// The Steel namespace.
33    pub const STEEL_NAMESPACE: &'static str = "steel";
34
35    /// Creates a new `Identifier` with the given namespace and path.
36    #[must_use]
37    pub fn new(
38        namespace: impl Into<Cow<'static, str>>,
39        path: impl Into<Cow<'static, str>>,
40    ) -> Self {
41        Identifier {
42            namespace: namespace.into(),
43            path: path.into(),
44        }
45    }
46    #[must_use]
47    pub const fn new_static(namespace: &'static str, path: &'static str) -> Self {
48        Identifier {
49            namespace: Cow::Borrowed(namespace),
50            path: Cow::Borrowed(path),
51        }
52    }
53
54    /// Creates a new `Identifier` with the Steel namespace.
55    #[must_use]
56    pub fn from_steel(path: impl Into<Cow<'static, str>>) -> Self {
57        Self::new(Self::STEEL_NAMESPACE, path)
58    }
59
60    /// Creates a new `Identifier` with the vanilla namespace.
61    #[must_use]
62    pub const fn vanilla(path: String) -> Self {
63        Identifier {
64            namespace: Cow::Borrowed(Self::VANILLA_NAMESPACE),
65            path: Cow::Owned(path),
66        }
67    }
68
69    /// Creates a new `Identifier` with the vanilla namespace and a static path.
70    #[must_use]
71    pub const fn vanilla_static(path: &'static str) -> Self {
72        Identifier {
73            namespace: Cow::Borrowed(Self::VANILLA_NAMESPACE),
74            path: Cow::Borrowed(path),
75        }
76    }
77
78    /// Returns whether the character is a valid namespace character.
79    #[must_use]
80    pub const fn valid_namespace_char(char: char) -> bool {
81        char == '_'
82            || char == '-'
83            || char.is_ascii_lowercase()
84            || char.is_ascii_digit()
85            || char == '.'
86    }
87
88    /// Returns whether the character is a valid path character.
89    #[must_use]
90    pub const fn valid_char(char: char) -> bool {
91        Self::valid_namespace_char(char) || char == '/'
92    }
93
94    /// Returns whether the namespace is valid.
95    pub fn validate_namespace(namespace: &str) -> bool {
96        namespace != ".." && namespace.chars().all(Self::valid_namespace_char)
97    }
98
99    /// Returns whether the path is valid.
100    pub fn validate_path(path: &str) -> bool {
101        path.chars().all(Self::valid_char)
102    }
103
104    /// Returns whether the namespace and path are valid.
105    #[must_use]
106    pub fn validate(namespace: &str, path: &str) -> bool {
107        Self::validate_namespace(namespace) && Self::validate_path(path)
108    }
109}
110
111impl Display for Identifier {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "{}:{}", self.namespace, self.path)
114    }
115}
116
117impl FromStr for Identifier {
118    type Err = &'static str;
119
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        let (namespace, path) = match s.split_once(':') {
122            Some(("", path)) => (Self::VANILLA_NAMESPACE, path),
123            Some((namespace, path)) => (namespace, path),
124            None => (Self::VANILLA_NAMESPACE, s),
125        };
126
127        if !Identifier::validate_namespace(namespace) {
128            return Err("Invalid namespace");
129        }
130
131        if !Identifier::validate_path(path) {
132            return Err("Invalid path");
133        }
134
135        Ok(Identifier {
136            namespace: Cow::Owned(namespace.to_owned()),
137            path: Cow::Owned(path.to_owned()),
138        })
139    }
140}
141impl Serialize for Identifier {
142    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
143    where
144        S: serde::Serializer,
145    {
146        serializer.serialize_str(&self.to_string())
147    }
148}
149
150impl<'de> Deserialize<'de> for Identifier {
151    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152    where
153        D: serde::Deserializer<'de>,
154    {
155        let s = String::deserialize(deserializer)?;
156        Identifier::from_str(&s).map_err(D::Error::custom)
157    }
158}
159
160// SAFETY: This implementation delegates to the `str` and `String` implementations
161// which are already safe, and the Identifier type has the same serialized representation
162// as a String (length-prefixed UTF-8 bytes). The size_of method returns exactly the
163// number of bytes that write will produce.
164unsafe impl<C: Config> SchemaWrite<C> for Identifier {
165    type Src = Identifier;
166
167    fn size_of(src: &Self::Src) -> wincode::WriteResult<usize> {
168        <str as SchemaWrite<C>>::size_of(&src.to_string())
169    }
170
171    fn write(writer: impl Writer, src: &Self::Src) -> wincode::WriteResult<()> {
172        <str as SchemaWrite<C>>::write(writer, &src.to_string())
173    }
174}
175
176// SAFETY: This implementation delegates to the `String` implementation which is
177// already safe, and then validates the result as a valid Identifier. The read
178// method initializes `dst` if and only if it returns Ok(()).
179unsafe impl<'de, C: Config> SchemaRead<'de, C> for Identifier {
180    type Dst = Identifier;
181
182    fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> wincode::ReadResult<()> {
183        let mut s = MaybeUninit::<String>::uninit();
184        <String as SchemaRead<'de, C>>::read(reader, &mut s)?;
185
186        // SAFETY: String::read succeeded, so s is initialized
187        let s = unsafe { s.assume_init() };
188
189        dst.write(Identifier::from_str(&s).map_err(wincode::ReadError::Custom)?);
190        Ok(())
191    }
192}
193
194impl HashComponent for Identifier {
195    fn hash_component(&self, hasher: &mut ComponentHasher) {
196        // Identifiers are hashed as strings in "namespace:path" format
197        hasher.put_string(&self.to_string());
198    }
199}
200
201impl simdnbt::ToNbtTag for Identifier {
202    fn to_nbt_tag(self) -> NbtTag {
203        NbtTag::String(self.to_string().into())
204    }
205}
206
207impl simdnbt::FromNbtTag for Identifier {
208    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
209        let s = tag.string()?.to_str();
210        s.parse().ok()
211    }
212}