Skip to main content

steel_utils/types/
identifier.rs

1use serde::{Deserialize, Serialize, de::Error as _};
2use simdnbt::owned::NbtTag;
3use std::cmp::Ordering;
4use std::{
5    borrow::Cow,
6    fmt::{self, Debug, Display, Formatter},
7    mem::MaybeUninit,
8    str::FromStr,
9};
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 PartialOrd for Identifier {
118    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
119        Some(self.cmp(other))
120    }
121}
122
123impl Ord for Identifier {
124    fn cmp(&self, other: &Self) -> Ordering {
125        self.path
126            .cmp(&other.path)
127            .then_with(|| self.namespace.cmp(&other.namespace))
128    }
129}
130
131impl FromStr for Identifier {
132    type Err = &'static str;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        let (namespace, path) = match s.split_once(':') {
136            Some(("", path)) => (Self::VANILLA_NAMESPACE, path),
137            Some((namespace, path)) => (namespace, path),
138            None => (Self::VANILLA_NAMESPACE, s),
139        };
140
141        if !Identifier::validate_namespace(namespace) {
142            return Err("Invalid namespace");
143        }
144
145        if !Identifier::validate_path(path) {
146            return Err("Invalid path");
147        }
148
149        Ok(Identifier {
150            namespace: Cow::Owned(namespace.to_owned()),
151            path: Cow::Owned(path.to_owned()),
152        })
153    }
154}
155impl Serialize for Identifier {
156    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: serde::Serializer,
159    {
160        serializer.serialize_str(&self.to_string())
161    }
162}
163
164impl<'de> Deserialize<'de> for Identifier {
165    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
166    where
167        D: serde::Deserializer<'de>,
168    {
169        let s = String::deserialize(deserializer)?;
170        Identifier::from_str(&s).map_err(D::Error::custom)
171    }
172}
173
174// SAFETY: This implementation delegates to the `str` and `String` implementations
175// which are already safe, and the Identifier type has the same serialized representation
176// as a String (length-prefixed UTF-8 bytes). The size_of method returns exactly the
177// number of bytes that write will produce.
178unsafe impl<C: Config> SchemaWrite<C> for Identifier {
179    type Src = Identifier;
180
181    fn size_of(src: &Self::Src) -> wincode::WriteResult<usize> {
182        <str as SchemaWrite<C>>::size_of(&src.to_string())
183    }
184
185    fn write(writer: impl Writer, src: &Self::Src) -> wincode::WriteResult<()> {
186        <str as SchemaWrite<C>>::write(writer, &src.to_string())
187    }
188}
189
190// SAFETY: This implementation delegates to the `String` implementation which is
191// already safe, and then validates the result as a valid Identifier. The read
192// method initializes `dst` if and only if it returns Ok(()).
193unsafe impl<'de, C: Config> SchemaRead<'de, C> for Identifier {
194    type Dst = Identifier;
195
196    fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> wincode::ReadResult<()> {
197        let mut s = MaybeUninit::<String>::uninit();
198        <String as SchemaRead<'de, C>>::read(reader, &mut s)?;
199
200        // SAFETY: String::read succeeded, so s is initialized
201        let s = unsafe { s.assume_init() };
202
203        dst.write(Identifier::from_str(&s).map_err(wincode::ReadError::Custom)?);
204        Ok(())
205    }
206}
207
208impl HashComponent for Identifier {
209    fn hash_component(&self, hasher: &mut ComponentHasher) {
210        // Identifiers are hashed as strings in "namespace:path" format
211        hasher.put_string(&self.to_string());
212    }
213}
214
215impl simdnbt::ToNbtTag for Identifier {
216    fn to_nbt_tag(self) -> NbtTag {
217        NbtTag::String(self.to_string().into())
218    }
219}
220
221impl simdnbt::FromNbtTag for Identifier {
222    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
223        let s = tag.string()?.to_str();
224        s.parse().ok()
225    }
226}