use crate::error::{LuaError, LuaResult, RuntimeError};
use crate::vm::execute::coerce_to_number;
use crate::vm::gc::arena::GcRef;
use crate::vm::state::LuaState;
use crate::vm::table::Table;
use crate::vm::value::Val;
use crate::platform::{
self, CLOCKS_PER_SEC, TimeT, Tm, c_gmtime, c_localtime, c_tmpname, clock, current_time, mktime,
setlocale, strftime,
};
#[inline]
fn nargs(state: &LuaState) -> usize {
state.top.saturating_sub(state.base)
}
#[inline]
fn arg(state: &LuaState, n: usize) -> Val {
let idx = state.base + n;
if idx < state.top {
state.stack_get(idx)
} else {
Val::Nil
}
}
fn bad_argument(name: &str, n: usize, msg: &str) -> LuaError {
LuaError::Runtime(RuntimeError {
message: format!("bad argument #{n} to '{name}' ({msg})"),
level: 0,
traceback: vec![],
})
}
fn runtime_error(msg: String) -> LuaError {
LuaError::Runtime(RuntimeError {
message: msg,
level: 0,
traceback: vec![],
})
}
fn check_number(state: &LuaState, name: &str, n: usize) -> LuaResult<f64> {
let val = arg(state, n);
match val {
Val::Num(v) => Ok(v),
Val::Str(_) => coerce_to_number(val, &state.gc)
.ok_or_else(|| bad_argument(name, n + 1, "number expected")),
_ => Err(bad_argument(name, n + 1, "number expected")),
}
}
fn check_string(state: &LuaState, name: &str, n: usize) -> LuaResult<Vec<u8>> {
let val = arg(state, n);
match val {
Val::Str(r) => state
.gc
.string_arena
.get(r)
.map(|s| s.data().to_vec())
.ok_or_else(|| bad_argument(name, n + 1, "string expected")),
Val::Num(_) => Ok(format!("{val}").into_bytes()),
_ => Err(bad_argument(name, n + 1, "string expected")),
}
}
fn opt_string(state: &LuaState, name: &str, n: usize) -> LuaResult<Option<Vec<u8>>> {
if nargs(state) <= n || matches!(arg(state, n), Val::Nil) {
Ok(None)
} else {
check_string(state, name, n).map(Some)
}
}
#[inline]
#[allow(clippy::unnecessary_wraps)]
fn push_true(state: &mut LuaState) -> LuaResult<u32> {
state.push(Val::Bool(true));
Ok(1)
}
fn push_error(state: &mut LuaState, filename: &str, err: &std::io::Error) -> u32 {
let msg = format!("{filename}: {err}");
let msg_val = Val::Str(state.gc.intern_string(msg.as_bytes()));
#[allow(clippy::cast_precision_loss)]
let code = f64::from(err.raw_os_error().unwrap_or(0));
state.push(Val::Nil);
state.push(msg_val);
state.push(Val::Num(code));
3
}
#[allow(unsafe_code)]
pub fn os_clock(state: &mut LuaState) -> LuaResult<u32> {
let ticks = unsafe { clock() };
#[allow(clippy::cast_precision_loss, clippy::cast_lossless)]
let secs = ticks as f64 / CLOCKS_PER_SEC;
state.push(Val::Num(secs));
Ok(1)
}
fn get_date_field(
state: &mut LuaState,
table_ref: GcRef<Table>,
key: &str,
default: Option<i32>,
) -> LuaResult<i32> {
let key_val = Val::Str(state.gc.intern_string(key.as_bytes()));
let table = state
.gc
.tables
.get(table_ref)
.ok_or_else(|| runtime_error("table not found".into()))?;
let val = table.get(key_val, &state.gc.string_arena);
match val {
Val::Num(n) =>
{
#[allow(clippy::cast_possible_truncation)]
Ok(n as i32)
}
_ => match default {
Some(d) => Ok(d),
None => Err(runtime_error(format!(
"field '{key}' missing in date table"
))),
},
}
}
fn get_date_bool_field(state: &mut LuaState, table_ref: GcRef<Table>, key: &str) -> LuaResult<i32> {
let key_val = Val::Str(state.gc.intern_string(key.as_bytes()));
let table = state
.gc
.tables
.get(table_ref)
.ok_or_else(|| runtime_error("table not found".into()))?;
let val = table.get(key_val, &state.gc.string_arena);
match val {
Val::Bool(b) => Ok(i32::from(b)),
_ => Ok(-1),
}
}
#[allow(unsafe_code, clippy::field_reassign_with_default)]
pub fn os_time(state: &mut LuaState) -> LuaResult<u32> {
if nargs(state) == 0 || matches!(arg(state, 0), Val::Nil) {
let t = current_time();
#[allow(clippy::cast_precision_loss)]
let v = t as f64;
state.push(Val::Num(v));
return Ok(1);
}
let Val::Table(table_ref) = arg(state, 0) else {
return Err(bad_argument("time", 1, "table expected"));
};
let sec = get_date_field(state, table_ref, "sec", Some(0))?;
let min = get_date_field(state, table_ref, "min", Some(0))?;
let hour = get_date_field(state, table_ref, "hour", Some(12))?;
let day = get_date_field(state, table_ref, "day", None)?;
let month = get_date_field(state, table_ref, "month", None)?;
let year = get_date_field(state, table_ref, "year", None)?;
let isdst = get_date_bool_field(state, table_ref, "isdst")?;
let mut tm = Tm::default();
tm.tm_sec = sec;
tm.tm_min = min;
tm.tm_hour = hour;
tm.tm_mday = day;
tm.tm_mon = month - 1; tm.tm_year = year - 1900; tm.tm_isdst = isdst;
let t = unsafe { mktime(&raw mut tm) };
if t == -1 {
state.push(Val::Nil);
} else {
#[allow(clippy::cast_precision_loss)]
let v = t as f64;
state.push(Val::Num(v));
}
Ok(1)
}
#[allow(unsafe_code)]
pub fn os_date(state: &mut LuaState) -> LuaResult<u32> {
let format_bytes = if nargs(state) > 0 && !matches!(arg(state, 0), Val::Nil) {
check_string(state, "date", 0)?
} else {
b"%c".to_vec()
};
let format_str = &format_bytes;
let t: TimeT = if nargs(state) > 1 && !matches!(arg(state, 1), Val::Nil) {
#[allow(clippy::cast_possible_truncation)]
let v = check_number(state, "date", 1)? as TimeT;
v
} else {
current_time()
};
let (use_utc, fmt) = if format_str.first() == Some(&b'!') {
(true, &format_str[1..])
} else {
(false, format_str.as_slice())
};
let mut tm = Tm::default();
let ok = if use_utc {
c_gmtime(&raw const t, &raw mut tm)
} else {
c_localtime(&raw const t, &raw mut tm)
};
if !ok {
state.push(Val::Nil);
return Ok(1);
}
if fmt == b"*t" {
return os_date_table(state, &tm);
}
let mut buf = [0u8; 256];
let mut fmt_c = Vec::with_capacity(fmt.len() + 1);
fmt_c.extend_from_slice(fmt);
fmt_c.push(0);
let n = unsafe { strftime(buf.as_mut_ptr(), buf.len(), fmt_c.as_ptr(), &raw const tm) };
if n == 0 && !fmt.is_empty() {
return Err(runtime_error("'date' format too long".into()));
}
let result_str = &buf[..n];
let val = Val::Str(state.gc.intern_string(result_str));
state.push(val);
Ok(1)
}
fn os_date_table(state: &mut LuaState, tm: &Tm) -> LuaResult<u32> {
let table = state.gc.alloc_table(Table::new());
let mut set_int = |key: &str, val: i32| -> LuaResult<()> {
let k = Val::Str(state.gc.intern_string(key.as_bytes()));
#[allow(clippy::cast_precision_loss)]
let v = Val::Num(f64::from(val));
let t = state
.gc
.tables
.get_mut(table)
.ok_or_else(|| runtime_error("date table not found".into()))?;
t.raw_set(k, v, &state.gc.string_arena)?;
Ok(())
};
set_int("sec", tm.tm_sec)?;
set_int("min", tm.tm_min)?;
set_int("hour", tm.tm_hour)?;
set_int("day", tm.tm_mday)?;
set_int("month", tm.tm_mon + 1)?; set_int("year", tm.tm_year + 1900)?; set_int("wday", tm.tm_wday + 1)?; set_int("yday", tm.tm_yday + 1)?;
if tm.tm_isdst >= 0 {
let k = Val::Str(state.gc.intern_string(b"isdst"));
let v = Val::Bool(tm.tm_isdst != 0);
let t = state
.gc
.tables
.get_mut(table)
.ok_or_else(|| runtime_error("date table not found".into()))?;
t.raw_set(k, v, &state.gc.string_arena)?;
}
state.push(Val::Table(table));
Ok(1)
}
pub fn os_difftime(state: &mut LuaState) -> LuaResult<u32> {
let t1 = check_number(state, "difftime", 0)?;
let t2 = if nargs(state) > 1 && !matches!(arg(state, 1), Val::Nil) {
check_number(state, "difftime", 1)?
} else {
0.0
};
state.push(Val::Num(t1 - t2));
Ok(1)
}
pub fn os_execute(state: &mut LuaState) -> LuaResult<u32> {
if nargs(state) == 0 || matches!(arg(state, 0), Val::Nil) {
state.push(Val::Num(1.0));
return Ok(1);
}
let cmd = check_string(state, "execute", 0)?;
let cmd_str = String::from_utf8_lossy(&cmd);
#[cfg(not(target_os = "windows"))]
let status = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(cmd_str.as_ref())
.status();
#[cfg(target_os = "windows")]
let status = std::process::Command::new("cmd")
.arg("/C")
.arg(cmd_str.as_ref())
.status();
match status {
Ok(exit) => {
#[allow(clippy::cast_precision_loss)]
let code = f64::from(exit.code().unwrap_or(-1));
state.push(Val::Num(code));
}
Err(_) => {
state.push(Val::Num(-1.0));
}
}
Ok(1)
}
pub fn os_exit(state: &mut LuaState) -> LuaResult<u32> {
#[allow(clippy::cast_possible_truncation)]
let code = if nargs(state) > 0 && !matches!(arg(state, 0), Val::Nil) {
check_number(state, "exit", 0)? as i32
} else {
0
};
std::process::exit(code);
}
pub fn os_getenv(state: &mut LuaState) -> LuaResult<u32> {
let name = check_string(state, "getenv", 0)?;
let name_str = String::from_utf8_lossy(&name);
match std::env::var(name_str.as_ref()) {
Ok(val) => {
let s = state.gc.intern_string(val.as_bytes());
state.push(Val::Str(s));
}
Err(_) => {
state.push(Val::Nil);
}
}
Ok(1)
}
pub fn os_remove(state: &mut LuaState) -> LuaResult<u32> {
let name = check_string(state, "remove", 0)?;
let name_str = String::from_utf8_lossy(&name).into_owned();
match std::fs::remove_file(&name_str) {
Ok(()) => push_true(state),
Err(e) => Ok(push_error(state, &name_str, &e)),
}
}
pub fn os_rename(state: &mut LuaState) -> LuaResult<u32> {
let oldname = check_string(state, "rename", 0)?;
let newname = check_string(state, "rename", 1)?;
let old_str = String::from_utf8_lossy(&oldname).into_owned();
let new_str = String::from_utf8_lossy(&newname).into_owned();
match std::fs::rename(&old_str, &new_str) {
Ok(()) => push_true(state),
Err(e) => Ok(push_error(state, &old_str, &e)),
}
}
pub fn os_tmpname(state: &mut LuaState) -> LuaResult<u32> {
let name =
c_tmpname().ok_or_else(|| runtime_error("unable to generate a unique filename".into()))?;
let s = state.gc.intern_string(&name);
state.push(Val::Str(s));
Ok(1)
}
#[allow(unsafe_code)]
pub fn os_setlocale(state: &mut LuaState) -> LuaResult<u32> {
let locale_arg = opt_string(state, "setlocale", 0)?;
let cat_name = if nargs(state) > 1 && !matches!(arg(state, 1), Val::Nil) {
check_string(state, "setlocale", 1)?
} else {
b"all".to_vec()
};
let cat = match cat_name.as_slice() {
b"all" => platform::locale::LC_ALL,
b"collate" => platform::locale::LC_COLLATE,
b"ctype" => platform::locale::LC_CTYPE,
b"monetary" => platform::locale::LC_MONETARY,
b"numeric" => platform::locale::LC_NUMERIC,
b"time" => platform::locale::LC_TIME,
_ => {
return Err(bad_argument("setlocale", 2, "invalid option"));
}
};
let locale_buf: Option<Vec<u8>> = locale_arg.map(|mut s| {
s.push(0);
s
});
let locale_ptr = match &locale_buf {
Some(buf) => buf.as_ptr(),
None => std::ptr::null(),
};
let result = unsafe { setlocale(cat, locale_ptr) };
if result.is_null() {
state.push(Val::Nil);
} else {
let cstr = unsafe { std::ffi::CStr::from_ptr(result.cast()) };
let s = state.gc.intern_string(cstr.to_bytes());
state.push(Val::Str(s));
}
Ok(1)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn tm_default_zeroed() {
let tm = Tm::default();
assert_eq!(tm.tm_sec, 0);
assert_eq!(tm.tm_min, 0);
assert_eq!(tm.tm_hour, 0);
assert_eq!(tm.tm_mday, 0);
assert_eq!(tm.tm_mon, 0);
assert_eq!(tm.tm_year, 0);
assert_eq!(tm.tm_wday, 0);
assert_eq!(tm.tm_yday, 0);
assert_eq!(tm.tm_isdst, 0);
}
#[test]
#[allow(unsafe_code)]
fn libc_clock_returns_nonnegative() {
let ticks = unsafe { clock() };
assert!(ticks >= 0);
}
#[test]
fn current_time_returns_reasonable_value() {
let t = current_time();
assert!(t > 1_704_067_200);
}
#[test]
#[allow(unsafe_code)]
fn c_localtime_roundtrip() {
let t: TimeT = 1_000_000_000; let mut tm = Tm::default();
assert!(c_localtime(&raw const t, &raw mut tm));
let t2 = unsafe { mktime(&raw mut tm) };
assert_eq!(t, t2);
}
#[test]
fn c_gmtime_epoch() {
let t: TimeT = 0; let mut tm = Tm::default();
assert!(c_gmtime(&raw const t, &raw mut tm));
assert_eq!(tm.tm_year, 70); assert_eq!(tm.tm_mon, 0); assert_eq!(tm.tm_mday, 1);
assert_eq!(tm.tm_hour, 0);
assert_eq!(tm.tm_min, 0);
assert_eq!(tm.tm_sec, 0);
}
#[test]
#[allow(unsafe_code)]
fn libc_strftime_basic() {
let t: TimeT = 0;
let mut tm = Tm::default();
c_gmtime(&raw const t, &raw mut tm);
let mut buf = [0u8; 64];
let fmt = b"%Y-%m-%d\0";
let n = unsafe { strftime(buf.as_mut_ptr(), buf.len(), fmt.as_ptr(), &raw const tm) };
assert!(n > 0);
let result = std::str::from_utf8(&buf[..n]).expect("valid utf8");
assert_eq!(result, "1970-01-01");
}
#[test]
fn c_tmpname_creates_file() {
let name = c_tmpname().expect("c_tmpname should succeed");
assert!(!name.is_empty());
let name_str = std::str::from_utf8(&name).expect("valid utf8");
let _ = std::fs::remove_file(name_str);
}
}