1use mlua::prelude::*;
75use std::path::{Path, PathBuf};
76
77pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
78 let t = lua.create_table()?;
79
80 t.set(
81 "join",
82 lua.create_function(|_, parts: LuaMultiValue| {
83 let mut path = PathBuf::new();
84 for (i, part) in parts.iter().enumerate() {
85 match part {
86 LuaValue::String(s) => path.push(&*s.to_str()?),
87 other => {
88 return Err(LuaError::external(format!(
89 "path.join: argument {} must be a string, got {}",
90 i + 1,
91 other.type_name()
92 )));
93 }
94 }
95 }
96 Ok(path.to_string_lossy().to_string())
97 })?,
98 )?;
99
100 t.set(
101 "parent",
102 lua.create_function(|_, p: String| {
103 Ok(Path::new(&p)
104 .parent()
105 .map(|p| p.to_string_lossy().to_string()))
106 })?,
107 )?;
108
109 t.set(
110 "filename",
111 lua.create_function(|_, p: String| {
112 Ok(Path::new(&p)
113 .file_name()
114 .map(|f| f.to_string_lossy().to_string()))
115 })?,
116 )?;
117
118 t.set(
119 "stem",
120 lua.create_function(|_, p: String| {
121 Ok(Path::new(&p)
122 .file_stem()
123 .map(|f| f.to_string_lossy().to_string()))
124 })?,
125 )?;
126
127 t.set(
128 "ext",
129 lua.create_function(|_, p: String| {
130 Ok(Path::new(&p)
131 .extension()
132 .map(|e| e.to_string_lossy().to_string()))
133 })?,
134 )?;
135
136 t.set(
149 "absolute",
150 lua.create_function(|lua, p: String| {
151 let access = crate::util::check_path(lua, &p, crate::policy::PathOp::Read)?;
152 access
153 .canonicalize()
154 .map(|p| p.to_string_lossy().to_string())
155 .map_err(|e| {
156 if e.kind() == std::io::ErrorKind::Unsupported {
157 LuaError::external(
158 "path.absolute is not supported in sandboxed mode: \
159 cap_std canonicalize returns a relative path but \
160 path.absolute must return an absolute path. \
161 Use path.is_absolute() + path.join() instead.",
162 )
163 } else {
164 LuaError::external(e)
165 }
166 })
167 })?,
168 )?;
169
170 t.set(
171 "is_absolute",
172 lua.create_function(|_, p: String| Ok(Path::new(&p).is_absolute()))?,
173 )?;
174
175 Ok(t)
176}
177
178#[cfg(test)]
179mod tests {
180 use mlua::Lua;
181
182 use crate::util::test_eval as eval;
183
184 #[test]
185 fn join_parts() {
186 let s: String = eval(r#"return std.path.join("/usr", "local", "bin")"#);
187 assert_eq!(s, "/usr/local/bin");
188 }
189
190 #[test]
191 fn parent_of_file() {
192 let s: String = eval(r#"return std.path.parent("/usr/local/bin/foo")"#);
193 assert_eq!(s, "/usr/local/bin");
194 }
195
196 #[test]
197 fn filename_extraction() {
198 let s: String = eval(r#"return std.path.filename("/usr/local/config.toml")"#);
199 assert_eq!(s, "config.toml");
200 }
201
202 #[test]
203 fn stem_without_extension() {
204 let s: String = eval(r#"return std.path.stem("/usr/local/config.toml")"#);
205 assert_eq!(s, "config");
206 }
207
208 #[test]
209 fn ext_extraction() {
210 let s: String = eval(r#"return std.path.ext("/usr/local/config.toml")"#);
211 assert_eq!(s, "toml");
212 }
213
214 #[test]
215 fn is_absolute_true() {
216 let b: bool = eval(r#"return std.path.is_absolute("/usr/local")"#);
217 assert!(b);
218 }
219
220 #[test]
221 fn is_absolute_false() {
222 let b: bool = eval(r#"return std.path.is_absolute("relative/path")"#);
223 assert!(!b);
224 }
225
226 #[test]
227 fn parent_of_root_is_nil() {
228 let b: bool = eval(
229 r#"
230 return std.path.parent("/") == nil
231 "#,
232 );
233 assert!(b);
234 }
235
236 #[test]
237 fn absolute_resolves_existing_path() {
238 let s: String = eval(r#"return std.path.absolute("/tmp")"#);
239 assert!(std::path::Path::new(&s).is_absolute());
240 }
241
242 #[test]
243 fn absolute_nonexistent_returns_error() {
244 let lua = Lua::new();
245 crate::register_all(&lua, "std").unwrap();
246 let result: mlua::Result<mlua::Value> = lua
247 .load(r#"return std.path.absolute("/nonexistent_mlua_bat_xyz")"#)
248 .eval();
249 assert!(result.is_err());
250 }
251
252 #[cfg(feature = "sandbox")]
253 #[test]
254 fn absolute_sandboxed_returns_clear_error() {
255 let sandbox = std::env::temp_dir().join("mlua_bat_test_path_sandbox");
256 std::fs::create_dir_all(&sandbox).unwrap();
257 std::fs::write(sandbox.join("file.txt"), "").unwrap();
258
259 let lua = Lua::new();
260 let config = crate::config::Config::builder()
261 .path_policy(crate::policy::Sandboxed::new([&sandbox]).unwrap())
262 .build()
263 .unwrap();
264 crate::register_all_with(&lua, "std", config).unwrap();
265
266 let path_str = sandbox.join("file.txt").to_string_lossy().to_string();
267 let code = format!(r#"return std.path.absolute("{path_str}")"#);
268 let result: mlua::Result<mlua::Value> = lua.load(&code).eval();
269 assert!(result.is_err());
270 let err_msg = result.unwrap_err().to_string();
271 assert!(
272 err_msg.contains("path.absolute is not supported in sandboxed mode"),
273 "error message should mention path.absolute and sandboxed mode, got: {err_msg}"
274 );
275
276 let _ = std::fs::remove_dir_all(&sandbox);
277 }
278
279 #[test]
280 fn join_rejects_non_string_argument() {
281 let lua = Lua::new();
282 crate::register_all(&lua, "std").unwrap();
283 let result: mlua::Result<mlua::Value> = lua
284 .load(r#"return std.path.join("/usr", 42, "bin")"#)
285 .eval();
286 assert!(result.is_err());
287 let err_msg = result.unwrap_err().to_string();
288 assert!(err_msg.contains("must be a string"));
289 }
290
291 #[test]
292 fn join_with_empty_string() {
293 let s: String = eval(r#"return std.path.join("/usr", "", "bin")"#);
294 assert_eq!(s, "/usr/bin");
295 }
296
297 #[test]
298 fn join_single_argument() {
299 let s: String = eval(r#"return std.path.join("foo")"#);
300 assert_eq!(s, "foo");
301 }
302
303 #[test]
304 fn parent_of_single_component() {
305 let b: bool = eval(
307 r#"
308 local p = std.path.parent("foo")
309 return p == ""
310 "#,
311 );
312 assert!(b);
313 }
314
315 #[test]
316 fn ext_no_extension_returns_nil() {
317 let b: bool = eval(r#"return std.path.ext("Makefile") == nil"#);
318 assert!(b);
319 }
320
321 #[test]
322 fn filename_of_root_is_nil() {
323 let b: bool = eval(r#"return std.path.filename("/") == nil"#);
324 assert!(b);
325 }
326}