use lua_types::{LuaError, LuaExit, LuaType, LuaValue};
use crate::state_stub::{LuaState, LuaStateStubExt as _};
use lua_vm::state::OsExecuteReason;
const STRFTIME_OPTIONS: &[u8] =
b"aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%||EcECExEXEyEYOdOeOHOIOmOMOSOuOUOVOwOWOy";
const SIZE_TIME_FMT: usize = 250;
#[derive(Debug, Default, Clone)]
pub struct TmFields {
pub tm_sec: i32,
pub tm_min: i32,
pub tm_hour: i32,
pub tm_mday: i32,
pub tm_mon: i32,
pub tm_year: i32,
pub tm_wday: i32,
pub tm_yday: i32,
pub tm_isdst: i32,
}
struct ByteDisplay<'a>(&'a [u8]);
impl std::fmt::Display for ByteDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for &b in self.0 {
write!(f, "{}", b as char)?;
}
Ok(())
}
}
fn set_field(state: &mut LuaState, key: &[u8], value: i32, delta: i32) -> Result<(), LuaError> {
state.push(LuaValue::Int((value as i64) + (delta as i64)));
state.set_field(-2, key)?;
Ok(())
}
fn set_bool_field(state: &mut LuaState, key: &[u8], value: i32) -> Result<(), LuaError> {
if value < 0 {
return Ok(());
}
state.push(LuaValue::Bool(value != 0));
state.set_field(-2, key)?;
Ok(())
}
fn set_all_fields(state: &mut LuaState, stm: &TmFields) -> Result<(), LuaError> {
set_field(state, b"year", stm.tm_year, 1900)?;
set_field(state, b"month", stm.tm_mon, 1)?;
set_field(state, b"day", stm.tm_mday, 0)?;
set_field(state, b"hour", stm.tm_hour, 0)?;
set_field(state, b"min", stm.tm_min, 0)?;
set_field(state, b"sec", stm.tm_sec, 0)?;
set_field(state, b"yday", stm.tm_yday, 1)?;
set_field(state, b"wday", stm.tm_wday, 1)?;
set_bool_field(state, b"isdst", stm.tm_isdst)?;
Ok(())
}
fn get_bool_field(state: &mut LuaState, key: &[u8]) -> Result<i32, LuaError> {
let ty = state.get_field(-1, key)?;
let res = if matches!(ty, LuaType::Nil) {
-1i32
} else {
state.to_boolean(-1) as i32
};
state.pop_n(1);
Ok(res)
}
fn get_field(
state: &mut LuaState,
key: &[u8],
d: i32,
delta: i32,
) -> Result<i32, LuaError> {
let ty = state.get_field(-1, key)?;
let maybe_int = state.to_integer_x(-1);
let res: i32 = match maybe_int {
Some(res) => {
let in_bounds = if res >= 0 {
res.saturating_sub(delta as i64) <= (i32::MAX as i64)
} else {
(i32::MIN as i64).saturating_add(delta as i64) <= res
};
if !in_bounds {
state.pop_n(1);
return Err(LuaError::runtime(format_args!(
"field '{}' is out-of-bound",
ByteDisplay(key),
)));
}
(res - delta as i64) as i32
}
None => {
if !matches!(ty, LuaType::Nil) {
state.pop_n(1);
return Err(LuaError::runtime(format_args!(
"field '{}' is not an integer",
ByteDisplay(key),
)));
} else if d < 0 {
state.pop_n(1);
return Err(LuaError::runtime(format_args!(
"field '{}' missing in date table",
ByteDisplay(key),
)));
}
d
}
};
state.pop_n(1);
Ok(res)
}
fn check_strftime_option<'a>(
_state: &mut LuaState,
conv: &'a [u8],
cc: &mut [u8; 4],
) -> Result<&'a [u8], LuaError> {
let options = STRFTIME_OPTIONS;
let mut oplen: usize = 1;
let mut i: usize = 0;
while i < options.len() && oplen <= conv.len() {
if options[i] == b'|' {
oplen += 1;
i += oplen;
} else if i + oplen <= options.len() && conv[..oplen] == options[i..i + oplen] {
debug_assert!(oplen <= 2, "STRFTIME_OPTIONS only has 1- and 2-char specifiers");
cc[1..=oplen].copy_from_slice(&conv[..oplen]);
cc[oplen + 1] = 0;
return Ok(&conv[oplen..]);
} else {
i += oplen;
}
}
Err(LuaError::arg_error(
1,
"invalid conversion specifier",
))
}
fn check_time(state: &mut LuaState, arg: i32) -> Result<i64, LuaError> {
let t = state.check_arg_integer(arg)?;
Ok(t)
}
fn unix_now() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn decompose_utc(t: i64) -> TmFields {
let days = t.div_euclid(86_400);
let sod = t.rem_euclid(86_400) as i32;
let tm_hour = sod / 3600;
let tm_min = (sod / 60) % 60;
let tm_sec = sod % 60;
let z = days + 719_468;
let era = (if z >= 0 { z } else { z - 146_096 }).div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy_mar = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy_mar + 2) / 153;
let day = (doy_mar - (153 * mp + 2) / 5 + 1) as i32;
let month: i32 = if mp < 10 { (mp + 3) as i32 } else { (mp - 9) as i32 };
let year = y + if month <= 2 { 1 } else { 0 };
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
const DAYS_BEFORE_MONTH: [i32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let tm_yday = DAYS_BEFORE_MONTH[(month - 1) as usize]
+ (day - 1)
+ if leap && month > 2 { 1 } else { 0 };
let tm_wday = (days + 4).rem_euclid(7) as i32;
TmFields {
tm_sec,
tm_min,
tm_hour,
tm_mday: day,
tm_mon: month - 1,
tm_year: (year - 1900) as i32,
tm_wday,
tm_yday,
tm_isdst: 0,
}
}
fn compose_utc(tm: &TmFields) -> i64 {
let mut y: i64 = (tm.tm_year as i64) + 1900;
let mut m: i64 = (tm.tm_mon as i64) + 1;
let dy = (m - 1).div_euclid(12);
y += dy;
m -= dy * 12;
let y_adj = if m <= 2 { y - 1 } else { y };
let era = (if y_adj >= 0 { y_adj } else { y_adj - 399 }).div_euclid(400);
let yoe = y_adj - era * 400;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + (tm.tm_mday as i64) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146_097 + doe - 719_468;
days * 86_400 + (tm.tm_hour as i64) * 3600 + (tm.tm_min as i64) * 60 + (tm.tm_sec as i64)
}
fn strftime_one(buf: &mut Vec<u8>, cc: &[u8; 4], oplen: usize, tm: &TmFields) {
use std::io::Write as _;
let spec = if oplen == 2 { cc[2] } else { cc[1] };
let year_full = (tm.tm_year as i64) + 1900;
let hour12 = {
let h = tm.tm_hour.rem_euclid(12);
if h == 0 { 12 } else { h }
};
const DAY_SHORT: [&[u8]; 7] = [b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
const DAY_LONG: [&[u8]; 7] = [
b"Sunday", b"Monday", b"Tuesday", b"Wednesday", b"Thursday", b"Friday", b"Saturday",
];
const MON_SHORT: [&[u8]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
b"Dec",
];
const MON_LONG: [&[u8]; 12] = [
b"January", b"February", b"March", b"April", b"May", b"June", b"July", b"August",
b"September", b"October", b"November", b"December",
];
let wday_idx = tm.tm_wday.rem_euclid(7) as usize;
let mon_idx = tm.tm_mon.rem_euclid(12) as usize;
match spec {
b'Y' => { let _ = write!(buf, "{}", year_full); }
b'y' => { let _ = write!(buf, "{:02}", year_full.rem_euclid(100)); }
b'C' => { let _ = write!(buf, "{:02}", year_full.div_euclid(100)); }
b'm' => { let _ = write!(buf, "{:02}", tm.tm_mon + 1); }
b'd' => { let _ = write!(buf, "{:02}", tm.tm_mday); }
b'e' => { let _ = write!(buf, "{:2}", tm.tm_mday); }
b'H' => { let _ = write!(buf, "{:02}", tm.tm_hour); }
b'I' => { let _ = write!(buf, "{:02}", hour12); }
b'k' => { let _ = write!(buf, "{:2}", tm.tm_hour); }
b'l' => { let _ = write!(buf, "{:2}", hour12); }
b'M' => { let _ = write!(buf, "{:02}", tm.tm_min); }
b'S' => { let _ = write!(buf, "{:02}", tm.tm_sec); }
b'w' => { let _ = write!(buf, "{}", tm.tm_wday); }
b'u' => {
let u = if tm.tm_wday == 0 { 7 } else { tm.tm_wday };
let _ = write!(buf, "{}", u);
}
b'j' => { let _ = write!(buf, "{:03}", tm.tm_yday + 1); }
b'a' => buf.extend_from_slice(DAY_SHORT[wday_idx]),
b'A' => buf.extend_from_slice(DAY_LONG[wday_idx]),
b'b' | b'h' => buf.extend_from_slice(MON_SHORT[mon_idx]),
b'B' => buf.extend_from_slice(MON_LONG[mon_idx]),
b'p' => buf.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }),
b'P' => buf.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }),
b'D' | b'x' => {
let _ = write!(buf, "{:02}/{:02}/{:02}", tm.tm_mon + 1, tm.tm_mday, year_full.rem_euclid(100));
}
b'F' => {
let _ = write!(buf, "{}-{:02}-{:02}", year_full, tm.tm_mon + 1, tm.tm_mday);
}
b'T' | b'X' => {
let _ = write!(buf, "{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec);
}
b'R' => { let _ = write!(buf, "{:02}:{:02}", tm.tm_hour, tm.tm_min); }
b'r' => {
let ampm: &[u8] = if tm.tm_hour < 12 { b"AM" } else { b"PM" };
let _ = write!(buf, "{:02}:{:02}:{:02} ", hour12, tm.tm_min, tm.tm_sec);
buf.extend_from_slice(ampm);
}
b'c' => {
let _ = write!(
buf,
"{} {} {:2} {:02}:{:02}:{:02} {}",
std::str::from_utf8(DAY_SHORT[wday_idx]).unwrap_or(""),
std::str::from_utf8(MON_SHORT[mon_idx]).unwrap_or(""),
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec,
year_full,
);
}
b'n' => buf.push(b'\n'),
b't' => buf.push(b'\t'),
b'%' => buf.push(b'%'),
b'z' => buf.extend_from_slice(b"+0000"),
b'Z' => buf.extend_from_slice(b"UTC"),
b's' => { let _ = write!(buf, "{}", compose_utc(tm)); }
b'U' => {
let week = (tm.tm_yday + 7 - tm.tm_wday) / 7;
let _ = write!(buf, "{:02}", week);
}
b'W' => {
let mwday = if tm.tm_wday == 0 { 6 } else { tm.tm_wday - 1 };
let week = (tm.tm_yday + 7 - mwday) / 7;
let _ = write!(buf, "{:02}", week);
}
b'V' | b'g' | b'G' => {
let _ = write!(buf, "{:02}", 1);
}
_ => {}
}
}
pub(crate) fn os_execute(state: &mut LuaState) -> Result<usize, LuaError> {
let cmd = state.opt_arg_lstring(1, None)?;
match cmd {
None => {
let has_shell = state.global().os_execute_hook.is_some();
state.push(LuaValue::Bool(has_shell));
Ok(1)
}
Some(cmd_bytes) => {
let hook = state.global().os_execute_hook;
match hook {
Some(execute_fn) => {
let cmd_owned: Vec<u8> = cmd_bytes.to_vec();
match execute_fn(&cmd_owned) {
Ok(result) => {
if result.success {
state.push(LuaValue::Bool(true));
} else {
state.push(LuaValue::Nil);
}
let reason_str: &[u8] = match result.reason {
OsExecuteReason::Exit => b"exit",
OsExecuteReason::Signal => b"signal",
};
state.push_string(reason_str)?;
state.push(LuaValue::Int(result.code as i64));
Ok(3)
}
Err(e) => {
state.push(LuaValue::Nil);
let msg = match &e {
LuaError::Runtime(LuaValue::Str(s)) => s.as_bytes().to_vec(),
other => format!("{:?}", other).into_bytes(),
};
let s = state.intern_str(&msg)?;
state.push(LuaValue::Str(s));
state.push(LuaValue::Int(-1));
Ok(3)
}
}
}
None => {
state.push(LuaValue::Nil);
state.push_string(b"os.execute: not implemented in lua-stdlib")?;
state.push(LuaValue::Int(-1));
Ok(3)
}
}
}
}
}
pub(crate) fn os_remove(state: &mut LuaState) -> Result<usize, LuaError> {
let filename: Vec<u8> = state.check_arg_string(1)?.to_vec();
let hook = state.global().file_remove_hook;
match hook {
Some(remove_fn) => match remove_fn(&filename) {
Ok(()) => {
state.push(LuaValue::Bool(true));
Ok(1)
}
Err(e) => {
state.push(LuaValue::Nil);
let msg = match &e {
LuaError::Runtime(LuaValue::Str(s)) => s.as_bytes().to_vec(),
other => format!("{:?}", other).into_bytes(),
};
let s = state.intern_str(&msg)?;
state.push(LuaValue::Str(s));
Ok(2)
}
},
None => {
state.push(LuaValue::Nil);
state.push_string(b"os.remove: no filesystem hook registered")?;
Ok(2)
}
}
}
pub(crate) fn os_rename(state: &mut LuaState) -> Result<usize, LuaError> {
let fromname: Vec<u8> = state.check_arg_string(1)?.to_vec();
let toname: Vec<u8> = state.check_arg_string(2)?.to_vec();
let hook = state.global().file_rename_hook;
match hook {
Some(rename_fn) => match rename_fn(&fromname, &toname) {
Ok(()) => {
state.push(LuaValue::Bool(true));
return Ok(1);
}
Err(e) => {
state.push(LuaValue::Nil);
let msg = match &e {
LuaError::Runtime(LuaValue::Str(s)) => s.as_bytes().to_vec(),
other => format!("{:?}", other).into_bytes(),
};
let s = state.intern_str(&msg)?;
state.push(LuaValue::Str(s));
return Ok(2);
}
},
None => {}
}
state.push(LuaValue::Nil);
state.push_string(b"os.rename: no filesystem hook registered")?;
Ok(2)
}
pub(crate) fn os_tmpname(state: &mut LuaState) -> Result<usize, LuaError> {
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut dir: Vec<u8> = {
let path = std::env::temp_dir();
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
{
path.to_string_lossy().as_bytes().to_vec()
}
};
if dir.last().copied() != Some(b'/') && dir.last().copied() != Some(b'\\') {
dir.push(b'/');
}
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let suffix = format!("lua_{:x}_{:x}_{:x}", std::process::id(), nanos, n);
dir.extend_from_slice(suffix.as_bytes());
state.push_string(&dir)?;
Ok(1)
}
pub(crate) fn os_getenv(state: &mut LuaState) -> Result<usize, LuaError> {
let name_bytes: Vec<u8> = state.check_arg_string(1)?.to_vec();
#[cfg(unix)]
let result: Option<Vec<u8>> = {
use std::ffi::OsStr;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
let os_name = OsStr::from_bytes(&name_bytes);
std::env::var_os(os_name).map(|v| v.into_vec())
};
#[cfg(not(unix))]
let result: Option<Vec<u8>> = {
match std::str::from_utf8(&name_bytes) {
Ok(name_str) => std::env::var(name_str).ok().map(|v| v.into_bytes()),
Err(_) => None,
}
};
match result {
Some(val) => {
state.push_string(&val)?;
}
None => {
state.push(LuaValue::Nil);
}
}
Ok(1)
}
pub(crate) fn os_clock(state: &mut LuaState) -> Result<usize, LuaError> {
state.push(LuaValue::Float(0.0));
Ok(1)
}
pub(crate) fn os_date(state: &mut LuaState) -> Result<usize, LuaError> {
let format: Vec<u8> = state.opt_arg_lstring(1, Some(b"%c"))?.unwrap_or_default();
let s: &[u8] = &format[..];
let t: i64 = if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
unix_now()
} else {
check_time(state, 2)?
};
let (_use_utc, s): (bool, &[u8]) = if s.first() == Some(&b'!') {
(true, &s[1..])
} else {
(false, s)
};
let stm = decompose_utc(t);
if s == b"*t" {
state.create_table(0, 9)?;
set_all_fields(state, &stm)?;
} else {
let mut result: Vec<u8> = Vec::new();
let mut pos: usize = 0;
while pos < s.len() {
if s[pos] != b'%' {
result.push(s[pos]);
pos += 1;
} else {
pos += 1;
let mut cc = [0u8; 4];
cc[0] = b'%';
let conv = &s[pos..];
let after = check_strftime_option(state, conv, &mut cc)?;
let oplen = conv.len() - after.len();
pos += oplen;
strftime_one(&mut result, &cc, oplen, &stm);
let _ = SIZE_TIME_FMT;
}
}
state.push_string(&result)?;
}
Ok(1)
}
pub(crate) fn os_time(state: &mut LuaState) -> Result<usize, LuaError> {
let t: i64;
if matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
t = unix_now();
} else {
state.check_arg_type(1, LuaType::Table)?;
lua_vm::api::set_top(state, 1)?;
let tm_year = get_field(state, b"year", -1, 1900)?;
let tm_mon = get_field(state, b"month", -1, 1)?;
let tm_mday = get_field(state, b"day", -1, 0)?;
let tm_hour = get_field(state, b"hour", 12, 0)?;
let tm_min = get_field(state, b"min", 0, 0)?;
let tm_sec = get_field(state, b"sec", 0, 0)?;
let tm_isdst = get_bool_field(state, b"isdst")?;
let raw = TmFields {
tm_year,
tm_mon,
tm_mday,
tm_hour,
tm_min,
tm_sec,
tm_isdst,
..TmFields::default()
};
t = compose_utc(&raw);
let stm = decompose_utc(t);
set_all_fields(state, &stm)?;
}
if t == -1 {
return Err(LuaError::runtime(format_args!(
"time result cannot be represented in this installation"
)));
}
state.push(LuaValue::Int(t));
Ok(1)
}
pub(crate) fn os_difftime(state: &mut LuaState) -> Result<usize, LuaError> {
let t1 = check_time(state, 1)?;
let t2 = check_time(state, 2)?;
state.push(LuaValue::Float((t1 - t2) as f64));
Ok(1)
}
pub(crate) fn os_setlocale(state: &mut LuaState) -> Result<usize, LuaError> {
const CAT_NAMES: &[&[u8]] = &[
b"all", b"collate", b"ctype", b"monetary", b"numeric", b"time",
];
let locale: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
let _op: usize = state.check_arg_option(2, Some(b"all"), CAT_NAMES)?;
let result_locale: Option<&[u8]> = match locale.as_deref() {
None => Some(b"C"), Some(b"C") | Some(b"POSIX") => Some(b"C"), Some(_) => None, };
match result_locale {
Some(s) => { state.push_string(s)?; }
None => state.push(LuaValue::Nil),
}
Ok(1)
}
pub(crate) fn os_exit(state: &mut LuaState) -> Result<usize, LuaError> {
let exit_code: i32 = if matches!(state.type_at(1), LuaType::Boolean) {
if state.to_boolean(1) { 0 } else { 1 } } else {
state.opt_arg_integer(1, 0)? as i32
};
if state.to_boolean(2) {
state.close();
}
std::panic::panic_any(LuaExit(exit_code));
}
pub type NativeFn = fn(&mut LuaState) -> Result<usize, LuaError>;
pub const OS_LIB: &[(&[u8], NativeFn)] = &[
(b"clock", os_clock),
(b"date", os_date),
(b"difftime", os_difftime),
(b"execute", os_execute),
(b"exit", os_exit),
(b"getenv", os_getenv),
(b"remove", os_remove),
(b"rename", os_rename),
(b"setlocale", os_setlocale),
(b"time", os_time),
(b"tmpname", os_tmpname),
];
pub fn open_os(state: &mut LuaState) -> Result<usize, LuaError> {
state.register_lib(b"os", OS_LIB)?;
Ok(1)
}