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