1use bstr::ByteSlice as _;
15use mlua::{Error, Lua, Result, String as LuaString, Table, Value};
16
17use crate::{NormalizedPath, is_normalized_path, normalize_path};
18
19pub const MODULE_NAME: &str = "dream_path";
21
22pub fn create_module(lua: &Lua) -> Result<Table> {
33 let module = lua.create_table()?;
34 module.set(
35 "normalize",
36 lua.create_function(|lua, path: Value| {
37 let path = expect_string(path)?;
38 lua.create_string(normalize_path(path.as_bytes()).as_slice())
39 })?,
40 )?;
41 module.set(
42 "is_normalized",
43 lua.create_function(|_, path: Value| {
44 let path = expect_string(path)?;
45 Ok(is_normalized_path(path.as_bytes().as_ref()))
46 })?,
47 )?;
48 module.set(
49 "file_name",
50 lua.create_function(|lua, path: Value| {
51 let path = expect_string(path)?;
52 component(lua, &path, NormalizedPath::file_name)
53 })?,
54 )?;
55 module.set(
56 "parent",
57 lua.create_function(|lua, path: Value| {
58 let path = expect_string(path)?;
59 component(lua, &path, NormalizedPath::parent)
60 })?,
61 )?;
62 module.set(
63 "extension",
64 lua.create_function(|lua, path: Value| {
65 let path = expect_string(path)?;
66 component(lua, &path, NormalizedPath::extension)
67 })?,
68 )?;
69 module.set(
70 "is_utf8",
71 lua.create_function(|_, path: Value| {
72 let path = expect_string(path)?;
73 Ok(path.as_bytes().as_ref().is_utf8())
74 })?,
75 )?;
76 Ok(module)
77}
78
79pub fn register_module(lua: &Lua) -> Result<()> {
85 register_module_as(lua, MODULE_NAME)
86}
87
88pub fn register_module_as(lua: &Lua, name: &str) -> Result<()> {
99 if name.is_empty() {
100 return Err(Error::RuntimeError(
101 "Lua module global name must not be empty".to_owned(),
102 ));
103 }
104 let module = create_module(lua)?;
105 lua.globals().set(name, module)
106}
107
108fn component(
109 lua: &Lua,
110 path: &LuaString,
111 select: impl FnOnce(&NormalizedPath) -> Option<&bstr::BStr>,
112) -> Result<Option<LuaString>> {
113 let path = NormalizedPath::new(path.as_bytes());
114 select(&path)
115 .map(|value| lua.create_string(value.as_bytes()))
116 .transpose()
117}
118
119fn expect_string(value: Value) -> Result<LuaString> {
120 match value {
121 Value::String(value) => Ok(value),
122 value => Err(Error::FromLuaConversionError {
123 from: value.type_name(),
124 to: "string".to_owned(),
125 message: Some("path arguments must be Lua strings".to_owned()),
126 }),
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use mlua::{Lua, String as LuaString};
133
134 use super::{MODULE_NAME, register_module, register_module_as};
135
136 #[test]
137 fn module_normalizes_lua_strings_as_bytes() {
138 let lua = Lua::new();
139 register_module(&lua).expect("module registration should succeed");
140
141 let normalized: LuaString = lua
142 .load(r#"return dream_path.normalize("Textures\\Foo.DDS")"#)
143 .eval()
144 .expect("normalization should succeed");
145
146 assert_eq!(normalized.as_bytes().as_ref(), b"textures/foo.dds");
147 }
148
149 #[test]
150 fn module_preserves_invalid_utf8_bytes() {
151 let lua = Lua::new();
152 register_module(&lua).expect("module registration should succeed");
153
154 let normalized: LuaString = lua
155 .load(r#"return dream_path.normalize("DIR/\255/FILE")"#)
156 .eval()
157 .expect("normalization should succeed");
158 let is_utf8: bool = lua
159 .load(r#"return dream_path.is_utf8("DIR/\255/FILE")"#)
160 .eval()
161 .expect("UTF-8 check should succeed");
162
163 assert_eq!(normalized.as_bytes().as_ref(), b"dir/\xff/file");
164 assert!(!is_utf8);
165 }
166
167 #[test]
168 fn module_preserves_embedded_nul_bytes() {
169 let lua = Lua::new();
170 register_module(&lua).expect("module registration should succeed");
171
172 let normalized: LuaString = lua
173 .load(r#"return dream_path.normalize("A\0B")"#)
174 .eval()
175 .expect("normalization should succeed");
176
177 assert_eq!(normalized.as_bytes().as_ref(), b"a\0b");
178 }
179
180 #[test]
181 fn module_helpers_normalize_before_splitting() {
182 let lua = Lua::new();
183 register_module(&lua).expect("module registration should succeed");
184
185 let values: (LuaString, LuaString, LuaString, bool) = lua
186 .load(
187 r#"
188 return
189 dream_path.parent("/Textures\\Architecture/Wall.DDS"),
190 dream_path.file_name("/Textures\\Architecture/Wall.DDS"),
191 dream_path.extension("/Textures\\Architecture/Wall.DDS"),
192 dream_path.is_normalized("textures/architecture/wall.dds")
193 "#,
194 )
195 .eval()
196 .expect("helper calls should succeed");
197
198 assert_eq!(values.0.as_bytes().as_ref(), b"textures/architecture");
199 assert_eq!(values.1.as_bytes().as_ref(), b"wall.dds");
200 assert_eq!(values.2.as_bytes().as_ref(), b"dds");
201 assert!(values.3);
202 }
203
204 #[test]
205 fn module_helpers_return_nil_for_missing_components() {
206 let lua = Lua::new();
207 register_module(&lua).expect("module registration should succeed");
208
209 let values: (
210 Option<LuaString>,
211 Option<LuaString>,
212 Option<LuaString>,
213 Option<LuaString>,
214 ) = lua
215 .load(
216 r#"
217 return
218 dream_path.file_name("/"),
219 dream_path.parent("foo"),
220 dream_path.extension(".hidden"),
221 dream_path.extension("foo.")
222 "#,
223 )
224 .eval()
225 .expect("helper calls should succeed");
226
227 assert!(values.0.is_none());
228 assert!(values.1.is_none());
229 assert!(values.2.is_none());
230 assert!(values.3.is_none());
231 }
232
233 #[test]
234 fn module_rejects_missing_or_non_string_path_arguments() {
235 let lua = Lua::new();
236 register_module(&lua).expect("module registration should succeed");
237
238 assert!(
239 lua.load("return dream_path.normalize()")
240 .eval::<LuaString>()
241 .is_err()
242 );
243 assert!(
244 lua.load("return dream_path.normalize(nil)")
245 .eval::<LuaString>()
246 .is_err()
247 );
248 assert!(
249 lua.load("return dream_path.normalize(42)")
250 .eval::<LuaString>()
251 .is_err()
252 );
253 assert!(
254 lua.load("return dream_path.normalize({})")
255 .eval::<LuaString>()
256 .is_err()
257 );
258 }
259
260 #[test]
261 fn module_helpers_preserve_invalid_byte_extensions() {
262 let lua = Lua::new();
263 register_module(&lua).expect("module registration should succeed");
264
265 let extension: LuaString = lua
266 .load(r#"return dream_path.extension("Foo.\255")"#)
267 .eval()
268 .expect("extension should succeed");
269
270 assert_eq!(extension.as_bytes().as_ref(), b"\xff");
271 }
272
273 #[test]
274 fn module_can_be_registered_under_custom_name() {
275 let lua = Lua::new();
276 register_module_as(&lua, "paths").expect("module registration should succeed");
277
278 let normalized: LuaString = lua
279 .load(r#"return paths.normalize("Meshes\\Door.NIF")"#)
280 .eval()
281 .expect("normalization should succeed");
282 let default_global_exists: bool = lua
283 .load(format!(r"return {MODULE_NAME} ~= nil"))
284 .eval()
285 .expect("global check should succeed");
286
287 assert_eq!(normalized.as_bytes().as_ref(), b"meshes/door.nif");
288 assert!(!default_global_exists);
289 }
290
291 #[test]
292 fn module_rejects_empty_registration_name() {
293 let lua = Lua::new();
294
295 assert!(register_module_as(&lua, "").is_err());
296 }
297}