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