rakata_core/
resource_identifier.rs1use std::fmt::{Display, Formatter};
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4
5use crate::ResRef;
6
7use crate::resource_type::ResourceType;
8
9#[cfg(feature = "tracing")]
10macro_rules! trace_debug {
11 ($($arg:tt)*) => {
12 tracing::debug!($($arg)*);
13 };
14}
15
16#[cfg(not(feature = "tracing"))]
17macro_rules! trace_debug {
18 ($($arg:tt)*) => {};
19}
20
21#[derive(Debug, Clone)]
26pub struct ResourceIdentifier {
27 resname: String,
28 restype: ResourceType,
29 canonical_lower: String,
30}
31
32impl ResourceIdentifier {
33 pub fn new(resname: impl Into<String>, restype: ResourceType) -> Self {
35 let resname = resname.into();
36 let canonical_lower = Self::build_canonical_lower(&resname, restype);
37 Self {
38 resname,
39 restype,
40 canonical_lower,
41 }
42 }
43
44 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(path)))]
49 pub fn from_path(path: impl AsRef<Path>) -> Self {
50 let path = path.as_ref();
51 let filename = path
52 .file_name()
53 .and_then(|s| s.to_str())
54 .unwrap_or_default();
55
56 if filename.is_empty() {
57 trace_debug!("resource identifier path had empty filename");
58 return Self::new("", ResourceType::Invalid);
59 }
60
61 if filename.ends_with('.') {
62 trace_debug!(
63 filename,
64 "resource identifier filename ended with trailing dot"
65 );
66 return Self::new(filename, ResourceType::Invalid);
67 }
68
69 if let Some((stem, ext)) = filename.rsplit_once('.') {
70 if stem.is_empty() {
71 trace_debug!(
73 filename,
74 "resource identifier filename had empty stem before extension"
75 );
76 return Self::new(filename, ResourceType::Invalid);
77 }
78 let restype = ResourceType::from_extension(ext);
79 trace_debug!(filename, stem, ext, restype = ?restype, "resource identifier parsed");
80 return Self::new(stem, restype);
81 }
82
83 trace_debug!(
84 filename,
85 "resource identifier filename had no extension; defaulting to invalid type"
86 );
87 Self::new(filename, ResourceType::Invalid)
88 }
89
90 pub fn resname(&self) -> &str {
92 &self.resname
93 }
94
95 pub fn restype(&self) -> ResourceType {
97 self.restype
98 }
99
100 pub fn unpack(&self) -> (&str, ResourceType) {
102 (&self.resname, self.restype)
103 }
104
105 pub fn resref(&self) -> Result<ResRef, crate::ResRefError> {
107 ResRef::new(&self.resname)
108 }
109
110 fn build_canonical_lower(resname: &str, restype: ResourceType) -> String {
111 if restype == ResourceType::Invalid {
112 resname.to_ascii_lowercase()
113 } else {
114 format!("{}.{}", resname.to_ascii_lowercase(), restype.extension())
115 }
116 }
117}
118
119impl PartialEq for ResourceIdentifier {
120 fn eq(&self, other: &Self) -> bool {
121 self.canonical_lower == other.canonical_lower
122 }
123}
124
125impl Eq for ResourceIdentifier {}
126
127impl Hash for ResourceIdentifier {
128 fn hash<H: Hasher>(&self, state: &mut H) {
129 self.canonical_lower.hash(state);
130 }
131}
132
133impl Display for ResourceIdentifier {
134 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
135 f.write_str(&self.canonical_lower)
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn parses_basic_resource_path() {
145 let ident = ResourceIdentifier::from_path("foo/bar/P_Bastila.utc");
146 assert_eq!(ident.resname(), "P_Bastila");
147 assert_eq!(ident.restype(), ResourceType::Utc);
148 }
149
150 #[test]
151 fn equality_is_case_insensitive() {
152 let a = ResourceIdentifier::new("p_bastila", ResourceType::Utc);
153 let b = ResourceIdentifier::new("P_BASTILA", ResourceType::Utc);
154 assert_eq!(a, b);
155 }
156
157 #[test]
158 fn display_is_cached_and_canonical() {
159 let ident = ResourceIdentifier::new("P_Bastila", ResourceType::Utc);
160 assert_eq!(ident.to_string(), "p_bastila.utc");
161 }
162}