1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3use thiserror::Error;
4
5pub const MAX_RESREF_LEN: usize = 16;
7
8#[derive(Debug, Clone, PartialEq, Eq, Error)]
10pub enum ResRefError {
11 #[error("resref length {len} exceeds maximum {max}")]
13 TooLong {
14 len: usize,
16 max: usize,
18 },
19 #[error("invalid resref character '{ch}'")]
21 InvalidChar {
22 ch: char,
24 },
25}
26
27#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
39pub struct ResRef {
40 bytes: [u8; MAX_RESREF_LEN],
41 len: u8,
42}
43
44impl ResRef {
45 pub fn new(value: impl AsRef<str>) -> Result<Self, ResRefError> {
51 let raw = value.as_ref();
52 if raw.len() > MAX_RESREF_LEN {
53 return Err(ResRefError::TooLong {
54 len: raw.len(),
55 max: MAX_RESREF_LEN,
56 });
57 }
58 let mut bytes = [0u8; MAX_RESREF_LEN];
59 for (i, ch) in raw.chars().enumerate() {
60 if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
61 return Err(ResRefError::InvalidChar { ch });
62 }
63 bytes[i] = u8::try_from(ch.to_ascii_lowercase())
66 .expect("ASCII char is guaranteed to fit in u8");
67 }
68 let len = u8::try_from(raw.len()).expect("len already bounded by MAX_RESREF_LEN");
69 Ok(Self { bytes, len })
70 }
71
72 pub const fn blank() -> Self {
74 Self {
75 bytes: [0u8; MAX_RESREF_LEN],
76 len: 0,
77 }
78 }
79
80 pub fn as_str(&self) -> &str {
82 std::str::from_utf8(&self.bytes[..usize::from(self.len)])
85 .expect("all bytes are validated ASCII during construction")
86 }
87
88 pub const fn is_blank(&self) -> bool {
90 self.len == 0
91 }
92
93 pub const fn is_empty(&self) -> bool {
97 self.len == 0
98 }
99
100 #[allow(clippy::as_conversions)]
102 pub const fn len(&self) -> usize {
103 self.len as usize
106 }
107}
108
109impl std::fmt::Debug for ResRef {
110 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
111 write!(f, "ResRef({:?})", self.as_str())
112 }
113}
114
115impl Default for ResRef {
116 fn default() -> Self {
117 Self::blank()
118 }
119}
120
121impl Display for ResRef {
122 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
123 f.write_str(self.as_str())
124 }
125}
126
127impl FromStr for ResRef {
128 type Err = ResRefError;
129
130 fn from_str(s: &str) -> Result<Self, Self::Err> {
131 Self::new(s)
132 }
133}
134
135impl TryFrom<&str> for ResRef {
136 type Error = ResRefError;
137
138 fn try_from(value: &str) -> Result<Self, Self::Error> {
139 Self::new(value)
140 }
141}
142
143impl TryFrom<String> for ResRef {
144 type Error = ResRefError;
145
146 fn try_from(value: String) -> Result<Self, Self::Error> {
147 Self::new(value)
148 }
149}
150
151impl From<ResRef> for String {
152 fn from(val: ResRef) -> Self {
153 val.as_str().to_owned()
154 }
155}
156
157impl AsRef<str> for ResRef {
158 fn as_ref(&self) -> &str {
159 self.as_str()
160 }
161}
162
163impl PartialEq<str> for ResRef {
164 fn eq(&self, other: &str) -> bool {
165 self.as_str() == other
166 }
167}
168
169impl PartialEq<&str> for ResRef {
170 fn eq(&self, other: &&str) -> bool {
171 self.as_str() == *other
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn accepts_valid_resref() {
181 let parsed = ResRef::new("P_Bastila").expect("valid resref");
182 assert_eq!(parsed.as_str(), "p_bastila");
183 }
184
185 #[test]
186 fn rejects_too_long_resref() {
187 let err = ResRef::new("this_name_is_longer_than_sixteen").expect_err("must fail");
188 assert!(matches!(err, ResRefError::TooLong { .. }));
189 }
190
191 #[test]
192 fn rejects_invalid_characters() {
193 let err = ResRef::new("bad!name").expect_err("must fail");
194 assert_eq!(err, ResRefError::InvalidChar { ch: '!' });
195 }
196
197 #[test]
198 fn accepts_hyphen_resrefs() {
199 let parsed = ResRef::new("t3-m4").expect("hyphen is valid");
200 assert_eq!(parsed.as_str(), "t3-m4");
201 }
202
203 #[test]
204 fn blank_is_empty() {
205 let r = ResRef::blank();
206 assert!(r.is_blank());
207 assert_eq!(r.as_str(), "");
208 assert_eq!(r.len(), 0);
209 }
210
211 #[test]
212 fn max_length_accepted() {
213 let parsed = ResRef::new("a23456789_123456").expect("16 chars is valid");
214 assert_eq!(parsed.as_str(), "a23456789_123456");
215 assert_eq!(parsed.len(), 16);
216 }
217
218 #[test]
219 fn is_copy() {
220 fn takes_copy<T: Copy>(_: T) {}
221 takes_copy(ResRef::blank());
222 }
223
224 #[test]
225 fn roundtrip_through_string() {
226 let original = ResRef::new("test_resref").expect("valid");
227 let s: String = original.into();
228 assert_eq!(s, "test_resref");
229 let back: ResRef = s.try_into().expect("valid");
230 assert_eq!(back, original);
231 }
232}