1use std::cell::RefCell;
26use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28use std::time::{SystemTime, UNIX_EPOCH};
29
30use lua_types::error::LuaError;
31use lua_types::value::LuaValue;
32use lua_vm::state::LuaState;
33
34struct DirIterState {
43 iter: Option<std::fs::ReadDir>,
44}
45
46thread_local! {
47 static DIR_ITER_REGISTRY: RefCell<HashMap<usize, DirIterState>> =
48 RefCell::new(HashMap::new());
49}
50
51fn path_from_bytes(bytes: &[u8]) -> Result<PathBuf, LuaError> {
56 #[cfg(unix)]
57 {
58 use std::os::unix::ffi::OsStrExt;
59 Ok(PathBuf::from(std::ffi::OsStr::from_bytes(bytes)))
60 }
61 #[cfg(not(unix))]
62 {
63 let s = std::str::from_utf8(bytes).map_err(|_| {
64 LuaError::runtime(format_args!("path is not valid UTF-8"))
65 })?;
66 Ok(PathBuf::from(s))
67 }
68}
69
70fn path_to_bytes(p: &Path) -> Vec<u8> {
74 #[cfg(unix)]
75 {
76 use std::os::unix::ffi::OsStrExt;
77 p.as_os_str().as_bytes().to_vec()
78 }
79 #[cfg(not(unix))]
80 {
81 p.to_string_lossy().into_owned().into_bytes()
82 }
83}
84
85fn push_fail(state: &mut LuaState, msg: &str) -> Result<usize, LuaError> {
88 state.push(LuaValue::Nil);
89 state.push_string(msg.as_bytes())?;
90 Ok(2)
91}
92
93fn lfs_currentdir(state: &mut LuaState) -> Result<usize, LuaError> {
98 match std::env::current_dir() {
99 Ok(p) => {
100 let bytes = path_to_bytes(&p);
101 state.push_string(&bytes)?;
102 Ok(1)
103 }
104 Err(e) => push_fail(state, &e.to_string()),
105 }
106}
107
108fn lfs_chdir(state: &mut LuaState) -> Result<usize, LuaError> {
113 let path = path_from_bytes(&state.check_arg_string(1)?)?;
114 match std::env::set_current_dir(&path) {
115 Ok(()) => {
116 state.push(LuaValue::Bool(true));
117 Ok(1)
118 }
119 Err(e) => push_fail(state, &format!("Unable to change working directory to '{}': {}", path.display(), e)),
120 }
121}
122
123fn lfs_mkdir(state: &mut LuaState) -> Result<usize, LuaError> {
128 let path = path_from_bytes(&state.check_arg_string(1)?)?;
129 match std::fs::create_dir(&path) {
130 Ok(()) => {
131 state.push(LuaValue::Bool(true));
132 Ok(1)
133 }
134 Err(e) => push_fail(state, &e.to_string()),
135 }
136}
137
138fn lfs_rmdir(state: &mut LuaState) -> Result<usize, LuaError> {
143 let path = path_from_bytes(&state.check_arg_string(1)?)?;
144 match std::fs::remove_dir(&path) {
145 Ok(()) => {
146 state.push(LuaValue::Bool(true));
147 Ok(1)
148 }
149 Err(e) => push_fail(state, &e.to_string()),
150 }
151}
152
153fn lfs_link(state: &mut LuaState) -> Result<usize, LuaError> {
162 let old_path = path_from_bytes(&state.check_arg_string(1)?)?;
163 let new_path = path_from_bytes(&state.check_arg_string(2)?)?;
164 let symlink = lua_vm::api::to_boolean(state, 3);
165
166 let result = if symlink {
167 #[cfg(unix)]
168 {
169 std::os::unix::fs::symlink(&old_path, &new_path)
170 }
171 #[cfg(windows)]
172 {
173 if old_path.is_dir() {
174 std::os::windows::fs::symlink_dir(&old_path, &new_path)
175 } else {
176 std::os::windows::fs::symlink_file(&old_path, &new_path)
177 }
178 }
179 #[cfg(not(any(unix, windows)))]
180 {
181 Err(std::io::Error::new(std::io::ErrorKind::Other, "symlinks not supported on this platform"))
182 }
183 } else {
184 std::fs::hard_link(&old_path, &new_path)
185 };
186
187 match result {
188 Ok(()) => {
189 state.push(LuaValue::Bool(true));
190 Ok(1)
191 }
192 Err(e) => push_fail(state, &e.to_string()),
193 }
194}
195
196fn lfs_touch(state: &mut LuaState) -> Result<usize, LuaError> {
203 let path = path_from_bytes(&state.check_arg_string(1)?)?;
204
205 let now_secs: f64 = SystemTime::now()
206 .duration_since(UNIX_EPOCH)
207 .map(|d| d.as_secs_f64())
208 .unwrap_or(0.0);
209
210 let arg2_type = lua_vm::api::lua_type_at(state, 2);
211 let atime: f64 = match arg2_type {
212 lua_types::LuaType::None | lua_types::LuaType::Nil => now_secs,
213 _ => state.check_number(2)?,
214 };
215 let arg3_type = lua_vm::api::lua_type_at(state, 3);
216 let mtime: f64 = match arg3_type {
217 lua_types::LuaType::None | lua_types::LuaType::Nil => atime,
218 _ => state.check_number(3)?,
219 };
220
221 let atime_ft = filetime::FileTime::from_unix_time(
222 atime.trunc() as i64,
223 ((atime.fract().abs()) * 1_000_000_000.0) as u32,
224 );
225 let mtime_ft = filetime::FileTime::from_unix_time(
226 mtime.trunc() as i64,
227 ((mtime.fract().abs()) * 1_000_000_000.0) as u32,
228 );
229
230 match filetime::set_file_times(&path, atime_ft, mtime_ft) {
231 Ok(()) => {
232 state.push(LuaValue::Bool(true));
233 Ok(1)
234 }
235 Err(e) => push_fail(state, &e.to_string()),
236 }
237}
238
239fn mode_string(ft: std::fs::FileType) -> &'static [u8] {
245 if ft.is_file() {
246 b"file"
247 } else if ft.is_dir() {
248 b"directory"
249 } else if ft.is_symlink() {
250 b"link"
251 } else {
252 #[cfg(unix)]
253 {
254 use std::os::unix::fs::FileTypeExt;
255 if ft.is_socket() {
256 return b"socket";
257 }
258 if ft.is_fifo() {
259 return b"named pipe";
260 }
261 if ft.is_char_device() {
262 return b"char device";
263 }
264 if ft.is_block_device() {
265 return b"block device";
266 }
267 }
268 b"other"
269 }
270}
271
272fn system_time_to_secs(t: std::io::Result<SystemTime>) -> i64 {
276 match t {
277 Ok(st) => match st.duration_since(UNIX_EPOCH) {
278 Ok(d) => d.as_secs() as i64,
279 Err(e) => -(e.duration().as_secs() as i64),
280 },
281 Err(_) => 0,
282 }
283}
284
285fn push_attr_field(
289 state: &mut LuaState,
290 field: &[u8],
291 md: &std::fs::Metadata,
292) -> Result<bool, LuaError> {
293 match field {
294 b"mode" => {
295 state.push_string(mode_string(md.file_type()))?;
296 }
297 b"size" => {
298 state.push(LuaValue::Int(md.len() as i64));
299 }
300 b"modification" => {
301 state.push(LuaValue::Int(system_time_to_secs(md.modified())));
302 }
303 b"access" => {
304 state.push(LuaValue::Int(system_time_to_secs(md.accessed())));
305 }
306 b"change" => {
307 #[cfg(unix)]
308 {
309 use std::os::unix::fs::MetadataExt;
310 state.push(LuaValue::Int(md.ctime()));
311 }
312 #[cfg(not(unix))]
313 {
314 state.push(LuaValue::Int(system_time_to_secs(md.modified())));
315 }
316 }
317 b"permissions" => {
318 #[cfg(unix)]
319 {
320 use std::os::unix::fs::PermissionsExt;
321 let m = md.permissions().mode() & 0o777;
322 let mut buf = [b'-'; 9];
323 let bits = [
324 (0o400, 0, b'r'), (0o200, 1, b'w'), (0o100, 2, b'x'),
325 (0o040, 3, b'r'), (0o020, 4, b'w'), (0o010, 5, b'x'),
326 (0o004, 6, b'r'), (0o002, 7, b'w'), (0o001, 8, b'x'),
327 ];
328 for (mask, idx, ch) in bits {
329 if m & mask != 0 {
330 buf[idx] = ch;
331 }
332 }
333 state.push_string(&buf)?;
334 }
335 #[cfg(not(unix))]
336 {
337 let s = if md.permissions().readonly() { b"r--r--r--" } else { b"rw-rw-rw-" };
338 state.push_string(s)?;
339 }
340 }
341 b"nlink" => {
342 #[cfg(unix)]
343 {
344 use std::os::unix::fs::MetadataExt;
345 state.push(LuaValue::Int(md.nlink() as i64));
346 }
347 #[cfg(not(unix))]
348 {
349 state.push(LuaValue::Int(1));
350 }
351 }
352 b"uid" => {
353 #[cfg(unix)]
354 {
355 use std::os::unix::fs::MetadataExt;
356 state.push(LuaValue::Int(md.uid() as i64));
357 }
358 #[cfg(not(unix))]
359 {
360 state.push(LuaValue::Int(0));
361 }
362 }
363 b"gid" => {
364 #[cfg(unix)]
365 {
366 use std::os::unix::fs::MetadataExt;
367 state.push(LuaValue::Int(md.gid() as i64));
368 }
369 #[cfg(not(unix))]
370 {
371 state.push(LuaValue::Int(0));
372 }
373 }
374 b"dev" => {
375 #[cfg(unix)]
376 {
377 use std::os::unix::fs::MetadataExt;
378 state.push(LuaValue::Int(md.dev() as i64));
379 }
380 #[cfg(not(unix))]
381 {
382 state.push(LuaValue::Int(0));
383 }
384 }
385 b"rdev" => {
386 #[cfg(unix)]
387 {
388 use std::os::unix::fs::MetadataExt;
389 state.push(LuaValue::Int(md.rdev() as i64));
390 }
391 #[cfg(not(unix))]
392 {
393 state.push(LuaValue::Int(0));
394 }
395 }
396 b"ino" => {
397 #[cfg(unix)]
398 {
399 use std::os::unix::fs::MetadataExt;
400 state.push(LuaValue::Int(md.ino() as i64));
401 }
402 #[cfg(not(unix))]
403 {
404 state.push(LuaValue::Int(0));
405 }
406 }
407 b"blocks" => {
408 #[cfg(unix)]
409 {
410 use std::os::unix::fs::MetadataExt;
411 state.push(LuaValue::Int(md.blocks() as i64));
412 }
413 #[cfg(not(unix))]
414 {
415 state.push(LuaValue::Int(0));
416 }
417 }
418 b"blksize" => {
419 #[cfg(unix)]
420 {
421 use std::os::unix::fs::MetadataExt;
422 state.push(LuaValue::Int(md.blksize() as i64));
423 }
424 #[cfg(not(unix))]
425 {
426 state.push(LuaValue::Int(0));
427 }
428 }
429 _ => return Ok(false),
430 }
431 Ok(true)
432}
433
434fn lfs_attributes(state: &mut LuaState) -> Result<usize, LuaError> {
441 let path = path_from_bytes(&state.check_arg_string(1)?)?;
442 let md = match std::fs::metadata(&path) {
443 Ok(m) => m,
444 Err(e) => {
445 return push_fail(state, &format!("cannot obtain information from file '{}': {}", path.display(), e));
446 }
447 };
448
449 let arg2_type = lua_vm::api::lua_type_at(state, 2);
450 match arg2_type {
451 lua_types::LuaType::String => {
452 let req = state.check_arg_string(2)?;
453 if !push_attr_field(state, &req, &md)? {
454 return Err(LuaError::runtime(format_args!(
455 "invalid attribute name '{}'",
456 String::from_utf8_lossy(&req)
457 )));
458 }
459 Ok(1)
460 }
461 lua_types::LuaType::Table => {
462 lua_vm::api::push_value(state, 2);
463 populate_attr_table(state, &md)?;
464 Ok(1)
465 }
466 _ => {
467 state.create_table(0, 14)?;
468 populate_attr_table(state, &md)?;
469 Ok(1)
470 }
471 }
472}
473
474fn populate_attr_table(
477 state: &mut LuaState,
478 md: &std::fs::Metadata,
479) -> Result<(), LuaError> {
480 const FIELDS: &[&[u8]] = &[
481 b"mode", b"size", b"modification", b"access", b"change",
482 b"permissions", b"nlink", b"uid", b"gid", b"dev", b"rdev",
483 b"ino", b"blocks", b"blksize",
484 ];
485 for field in FIELDS {
486 if !push_attr_field(state, field, md)? {
487 continue;
488 }
489 lua_vm::api::set_field(state, -2, field)?;
490 }
491 Ok(())
492}
493
494fn lfs_dir_next(state: &mut LuaState) -> Result<usize, LuaError> {
500 let ud_idx = upvalue_index(1);
501 lua_vm::api::push_value(state, ud_idx);
502 let v = state.pop();
503 let id = match v {
504 LuaValue::UserData(u) => u.identity(),
505 _ => {
506 return Err(LuaError::runtime(format_args!(
507 "lfs.dir iterator: missing handle upvalue"
508 )));
509 }
510 };
511
512 let next = DIR_ITER_REGISTRY.with(|reg| {
513 let mut map = reg.borrow_mut();
514 let entry = match map.get_mut(&id) {
515 Some(e) => e,
516 None => return None,
517 };
518 let iter = match entry.iter.as_mut() {
519 Some(i) => i,
520 None => return None,
521 };
522 loop {
523 match iter.next() {
524 Some(Ok(de)) => {
525 let name = de.file_name();
526 let bytes = {
527 #[cfg(unix)]
528 {
529 use std::os::unix::ffi::OsStrExt;
530 name.as_bytes().to_vec()
531 }
532 #[cfg(not(unix))]
533 {
534 name.to_string_lossy().into_owned().into_bytes()
535 }
536 };
537 return Some(Some(bytes));
538 }
539 Some(Err(_)) => continue,
540 None => {
541 entry.iter = None;
542 return Some(None);
543 }
544 }
545 }
546 });
547
548 match next {
549 Some(Some(bytes)) => {
550 state.push_string(&bytes)?;
551 Ok(1)
552 }
553 _ => {
554 state.push(LuaValue::Nil);
555 Ok(1)
556 }
557 }
558}
559
560fn lfs_dir(state: &mut LuaState) -> Result<usize, LuaError> {
571 let path = path_from_bytes(&state.check_arg_string(1)?)?;
572 let iter = std::fs::read_dir(&path).map_err(|e| {
573 LuaError::runtime(format_args!(
574 "cannot open directory '{}': {}",
575 path.display(),
576 e
577 ))
578 })?;
579
580 let ud = state.new_userdata_typed(b"lfs.dir.handle", 0, 0)?;
581 let id = ud.identity();
582 DIR_ITER_REGISTRY.with(|reg| {
583 reg.borrow_mut().insert(id, DirIterState { iter: Some(iter) });
584 });
585
586 lua_vm::api::push_cclosure(state, lfs_dir_next, 1)?;
587 Ok(1)
588}
589
590fn upvalue_index(i: i32) -> i32 {
596 -1_001_000 - i
597}
598
599const LFS_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
600 (b"attributes", lfs_attributes),
601 (b"chdir", lfs_chdir),
602 (b"currentdir", lfs_currentdir),
603 (b"dir", lfs_dir),
604 (b"link", lfs_link),
605 (b"mkdir", lfs_mkdir),
606 (b"rmdir", lfs_rmdir),
607 (b"touch", lfs_touch),
608];
609
610pub fn luaopen_lfs(state: &mut LuaState) -> Result<usize, LuaError> {
618 state.create_table(0, LFS_FUNCS.len() as i32)?;
619 for (name, func) in LFS_FUNCS {
620 lua_vm::api::push_cclosure(state, *func, 0)?;
621 lua_vm::api::set_field(state, -2, name)?;
622 }
623 Ok(1)
624}
625
626