use crate::lib_registry::LibraryModule;
use crate::lua_value::LuaValue;
use crate::lua_vm::{LuaResult, LuaState};
use crate::platform_time;
use chrono::{DateTime, Datelike, Local, TimeZone, Timelike, Utc};
pub fn create_os_lib() -> LibraryModule {
crate::lib_module!("os", {
"clock" => os_clock,
"time" => os_time,
"date" => os_date,
"difftime" => os_difftime,
"execute" => os_execute,
"exit" => os_exit,
"getenv" => os_getenv,
"remove" => os_remove,
"rename" => os_rename,
"setlocale" => os_setlocale,
"tmpname" => os_tmpname,
})
}
fn os_clock(l: &mut LuaState) -> LuaResult<usize> {
let elapsed = l.vm_mut().start_time.elapsed_secs_f64();
l.push_value(LuaValue::float(elapsed))?;
Ok(1)
}
fn os_time(l: &mut LuaState) -> LuaResult<usize> {
let arg = l.get_arg(1);
if let Some(table_val) = arg {
if table_val.is_nil() {
let timestamp = platform_time::unix_secs();
l.push_value(LuaValue::integer(timestamp as i64))?;
return Ok(1);
}
if let Some(_tbl) = table_val.as_table() {
let get_field = |l: &mut LuaState, name: &str| -> Result<Option<i64>, String> {
let key = l.create_string(name).unwrap();
let val = table_val.as_table().unwrap().raw_get(&key);
match val {
Some(v) => {
if let Some(n) = v.as_integer() {
Ok(Some(n))
} else if let Some(n) = v.as_number() {
if n.fract() != 0.0 {
Err("not an integer".to_string())
} else {
Ok(Some(n as i64))
}
} else {
Err("not an integer".to_string())
}
}
None => Ok(None),
}
};
let year = get_field(l, "year")
.map_err(|e| l.error(format!("field 'year' is {}", e)))?
.ok_or_else(|| l.error("field 'year' missing in date table".to_string()))?;
let month = get_field(l, "month")
.map_err(|e| l.error(format!("field 'month' is {}", e)))?
.ok_or_else(|| l.error("field 'month' missing in date table".to_string()))?;
let day = get_field(l, "day")
.map_err(|e| l.error(format!("field 'day' is {}", e)))?
.ok_or_else(|| l.error("field 'day' missing in date table".to_string()))?;
let hour = match get_field(l, "hour") {
Ok(Some(v)) => v,
Ok(None) => 12,
Err(e) => return Err(l.error(format!("field 'hour' is {}", e))),
};
let min = match get_field(l, "min") {
Ok(Some(v)) => v,
Ok(None) => 0,
Err(e) => return Err(l.error(format!("field 'min' is {}", e))),
};
let sec = match get_field(l, "sec") {
Ok(Some(v)) => v,
Ok(None) => 0,
Err(e) => return Err(l.error(format!("field 'sec' is {}", e))),
};
let year_offset = year - 1900;
if year_offset < i32::MIN as i64 || year_offset > i32::MAX as i64 {
return Err(l.error("field 'year' is out-of-bound".to_string()));
}
if month < i32::MIN as i64 || month > i32::MAX as i64 {
return Err(l.error("field 'month' is out-of-bound".to_string()));
}
if day < i32::MIN as i64 || day > i32::MAX as i64 {
return Err(l.error("field 'day' is out-of-bound".to_string()));
}
if hour < i32::MIN as i64 || hour > i32::MAX as i64 {
return Err(l.error("field 'hour' is out-of-bound".to_string()));
}
if min < i32::MIN as i64 || min > i32::MAX as i64 {
return Err(l.error("field 'min' is out-of-bound".to_string()));
}
if sec < i32::MIN as i64 || sec > i32::MAX as i64 {
return Err(l.error("field 'sec' is out-of-bound".to_string()));
}
use chrono::NaiveDate;
let mut adj_year = year;
let mut adj_month = month;
if !(1..=12).contains(&adj_month) {
adj_year += (adj_month - 1).div_euclid(12);
adj_month = (adj_month - 1).rem_euclid(12) + 1;
}
if adj_year >= i32::MIN as i64 && adj_year <= i32::MAX as i64 {
use chrono::NaiveTime;
let base_date = NaiveDate::from_ymd_opt(adj_year as i32, adj_month as u32, 1);
let base_time = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
if let Some(base) = base_date {
let base_dt = base.and_time(base_time);
let dt = base_dt
+ chrono::Duration::days(day - 1)
+ chrono::Duration::hours(hour)
+ chrono::Duration::minutes(min)
+ chrono::Duration::seconds(sec);
let local_result = Local.from_local_datetime(&dt);
if let Some(local_dt) = local_result.single().or_else(|| local_result.latest())
{
let timestamp = local_dt.timestamp();
normalize_time_table(l, &table_val, &local_dt)?;
l.push_value(LuaValue::integer(timestamp))?;
return Ok(1);
}
}
} else {
let ts = mktime_approx(adj_year, adj_month, day, hour, min, sec);
if let Some(t) = ts {
let result_year = year_from_timestamp(t);
let result_year_offset = result_year - 1900;
if result_year_offset < i32::MIN as i64 || result_year_offset > i32::MAX as i64
{
return Err(l.error(
"time result cannot be represented in this installation".to_string(),
));
}
l.push_value(LuaValue::integer(t))?;
return Ok(1);
}
}
return Err(
l.error("time result cannot be represented in this installation".to_string())
);
} else {
return Err(l.error("table expected".to_string()));
}
}
let timestamp = platform_time::unix_secs();
l.push_value(LuaValue::integer(timestamp as i64))?;
Ok(1)
}
fn mktime_approx(year: i64, month: i64, day: i64, hour: i64, min: i64, sec: i64) -> Option<i64> {
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let m = m as u64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d as u64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe as i64 - 719468 }
let days = days_from_civil(year, month, day);
let ts = days
.checked_mul(86400)?
.checked_add(hour * 3600)?
.checked_add(min * 60)?
.checked_add(sec)?;
Some(ts)
}
fn year_from_timestamp(ts: i64) -> i64 {
let days = ts.div_euclid(86400);
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
if m <= 2 { y + 1 } else { y }
}
fn normalize_time_table(
l: &mut LuaState,
table_val: &LuaValue,
dt: &DateTime<Local>,
) -> LuaResult<()> {
let year_key = l.create_string("year")?;
l.raw_set(table_val, year_key, LuaValue::integer(dt.year() as i64));
let month_key = l.create_string("month")?;
l.raw_set(table_val, month_key, LuaValue::integer(dt.month() as i64));
let day_key = l.create_string("day")?;
l.raw_set(table_val, day_key, LuaValue::integer(dt.day() as i64));
let hour_key = l.create_string("hour")?;
l.raw_set(table_val, hour_key, LuaValue::integer(dt.hour() as i64));
let min_key = l.create_string("min")?;
l.raw_set(table_val, min_key, LuaValue::integer(dt.minute() as i64));
let sec_key = l.create_string("sec")?;
l.raw_set(table_val, sec_key, LuaValue::integer(dt.second() as i64));
let wday_key = l.create_string("wday")?;
l.raw_set(
table_val,
wday_key,
LuaValue::integer(dt.weekday().number_from_sunday() as i64),
);
let yday_key = l.create_string("yday")?;
l.raw_set(table_val, yday_key, LuaValue::integer(dt.ordinal() as i64));
Ok(())
}
fn os_date(l: &mut LuaState) -> LuaResult<usize> {
let format_arg = l.get_arg(1);
let time_arg = l.get_arg(2);
let timestamp = if let Some(t) = time_arg {
if let Some(n) = t.as_number() {
n as i64
} else {
return Err(l.error("bad argument #2 to 'date' (number expected)".to_string()));
}
} else {
platform_time::unix_secs() as i64
};
let format_str = if let Some(f) = format_arg {
if let Some(s) = f.as_str() {
s.to_string()
} else {
return Err(l.error("bad argument #1 to 'date' (string expected)".to_string()));
}
} else {
"%c".to_string() };
let (use_utc, actual_format) = if let Some(rest) = format_str.strip_prefix('!') {
(true, rest)
} else {
(false, format_str.as_str())
};
let dt: DateTime<Local> = if use_utc {
Utc.timestamp_opt(timestamp, 0)
.single()
.ok_or_else(|| {
l.error("time result cannot be represented in this installation".to_string())
})?
.with_timezone(&Local)
} else {
Local.timestamp_opt(timestamp, 0).single().ok_or_else(|| {
l.error("time result cannot be represented in this installation".to_string())
})?
};
match actual_format {
"*t" => {
let table = l.create_table(0, 9)?;
let year_key = l.create_string("year")?;
l.raw_set(&table, year_key, LuaValue::integer(dt.year() as i64));
let month_key = l.create_string("month")?;
l.raw_set(&table, month_key, LuaValue::integer(dt.month() as i64));
let day_key = l.create_string("day")?;
l.raw_set(&table, day_key, LuaValue::integer(dt.day() as i64));
let hour_key = l.create_string("hour")?;
l.raw_set(&table, hour_key, LuaValue::integer(dt.hour() as i64));
let min_key = l.create_string("min")?;
l.raw_set(&table, min_key, LuaValue::integer(dt.minute() as i64));
let sec_key = l.create_string("sec")?;
l.raw_set(&table, sec_key, LuaValue::integer(dt.second() as i64));
let wday_key = l.create_string("wday")?;
let wday = dt.weekday().number_from_sunday();
l.raw_set(&table, wday_key, LuaValue::integer(wday as i64));
let yday_key = l.create_string("yday")?;
let yday = dt.ordinal();
l.raw_set(&table, yday_key, LuaValue::integer(yday as i64));
let isdst_key = l.create_string("isdst")?;
l.raw_set(&table, isdst_key, LuaValue::boolean(false));
l.push_value(table)?;
Ok(1)
}
_ => {
let date_str = format_date_string(dt, actual_format).map_err(|e| l.error(e))?;
let result = l.create_string(&date_str)?;
l.push_value(result)?;
Ok(1)
}
}
}
fn format_date_string(dt: DateTime<Local>, format: &str) -> Result<String, String> {
let mut result = String::new();
let mut chars = format.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
if let Some(&next_ch) = chars.peek() {
chars.next(); let formatted = match next_ch {
'a' => dt.format("%a").to_string(),
'A' => dt.format("%A").to_string(),
'b' | 'h' => dt.format("%b").to_string(),
'B' => dt.format("%B").to_string(),
'c' => dt.format("%a %b %d %H:%M:%S %Y").to_string(),
'd' => dt.format("%d").to_string(),
'D' => dt.format("%m/%d/%y").to_string(),
'e' => dt.format("%e").to_string(),
'F' => dt.format("%Y-%m-%d").to_string(),
'g' => dt.format("%g").to_string(),
'G' => dt.format("%G").to_string(),
'H' => dt.format("%H").to_string(),
'I' => dt.format("%I").to_string(),
'j' => dt.format("%j").to_string(),
'm' => dt.format("%m").to_string(),
'M' => dt.format("%M").to_string(),
'n' => "\n".to_string(),
'p' => dt.format("%p").to_string(),
'r' => dt.format("%I:%M:%S %p").to_string(),
'R' => dt.format("%H:%M").to_string(),
'S' => dt.format("%S").to_string(),
't' => "\t".to_string(),
'T' => dt.format("%H:%M:%S").to_string(),
'u' => {
let d = dt.weekday().number_from_monday();
d.to_string()
}
'U' => dt.format("%U").to_string(),
'V' => dt.format("%V").to_string(),
'w' => dt.weekday().num_days_from_sunday().to_string(),
'W' => dt.format("%W").to_string(),
'x' => dt.format("%m/%d/%y").to_string(),
'X' => dt.format("%H:%M:%S").to_string(),
'y' => dt.format("%y").to_string(),
'Y' => dt.format("%Y").to_string(),
'z' => dt.format("%z").to_string(),
'Z' => dt.format("%Z").to_string(),
'%' => "%".to_string(),
'E' => {
if let Some(&mod_ch) = chars.peek() {
match mod_ch {
'c' | 'C' | 'x' | 'X' | 'y' | 'Y' => {
chars.next();
let formatted = match mod_ch {
'c' => dt.format("%a %b %d %H:%M:%S %Y").to_string(),
'C' => dt.format("%C").to_string(),
'x' => dt.format("%m/%d/%y").to_string(),
'X' => dt.format("%H:%M:%S").to_string(),
'y' => dt.format("%y").to_string(),
'Y' => dt.format("%Y").to_string(),
_ => unreachable!(),
};
result.push_str(&formatted);
continue;
}
_ => {
return Err(format!(
"invalid conversion specifier '%E{}'",
mod_ch
));
}
}
} else {
return Err("invalid conversion specifier '%E'".to_string());
}
}
'O' => {
if let Some(&mod_ch) = chars.peek() {
match mod_ch {
'd' | 'e' | 'H' | 'I' | 'm' | 'M' | 'S' | 'u' | 'U' | 'V' | 'w'
| 'W' | 'y' => {
chars.next();
let formatted = match mod_ch {
'd' => dt.format("%d").to_string(),
'e' => dt.format("%e").to_string(),
'H' => dt.format("%H").to_string(),
'I' => dt.format("%I").to_string(),
'm' => dt.format("%m").to_string(),
'M' => dt.format("%M").to_string(),
'S' => dt.format("%S").to_string(),
'u' => dt.weekday().number_from_monday().to_string(),
'U' => dt.format("%U").to_string(),
'V' => dt.format("%V").to_string(),
'w' => dt.weekday().num_days_from_sunday().to_string(),
'W' => dt.format("%W").to_string(),
'y' => dt.format("%y").to_string(),
_ => unreachable!(),
};
result.push_str(&formatted);
continue;
}
_ => {
return Err(format!(
"invalid conversion specifier '%O{}'",
mod_ch
));
}
}
} else {
return Err("invalid conversion specifier '%O'".to_string());
}
}
_ => {
return Err(format!("invalid conversion specifier '%{}'", next_ch));
}
};
result.push_str(&formatted);
} else {
return Err("invalid conversion specifier '%'".to_string());
}
} else {
result.push(ch);
}
}
Ok(result)
}
fn os_exit(_l: &mut LuaState) -> LuaResult<usize> {
std::process::exit(0);
}
fn os_difftime(l: &mut LuaState) -> LuaResult<usize> {
let t2 = l
.get_arg(1)
.and_then(|v| v.as_integer())
.ok_or_else(|| l.error("difftime: argument 1 must be a number".to_string()))?;
let t1 = l
.get_arg(2)
.and_then(|v| v.as_integer())
.ok_or_else(|| l.error("difftime: argument 2 must be a number".to_string()))?;
let diff = t2 - t1;
l.push_value(LuaValue::integer(diff))?;
Ok(1)
}
fn os_execute(l: &mut LuaState) -> LuaResult<usize> {
use std::process::Command;
let cmd_opt = l.get_arg(1).and_then(|v| {
if v.is_nil() {
None
} else {
v.as_str().map(|s| s.to_string())
}
});
let Some(cmd) = cmd_opt else {
l.push_value(LuaValue::boolean(true))?;
return Ok(1);
};
#[cfg(target_os = "windows")]
let output = Command::new("cmd").args(["/C", &cmd]).output();
#[cfg(not(target_os = "windows"))]
let output = Command::new("sh").arg("-c").arg(&cmd).output();
match output {
Ok(result) => {
let exit_code = result.status.code().unwrap_or(-1);
let exit_str = l.create_string("exit")?;
l.push_value(LuaValue::boolean(result.status.success()))?;
l.push_value(exit_str)?;
l.push_value(LuaValue::integer(exit_code as i64))?;
Ok(3)
}
Err(_) => {
let exit_str = l.create_string("exit")?;
l.push_value(LuaValue::nil())?;
l.push_value(exit_str)?;
l.push_value(LuaValue::integer(-1))?;
Ok(3)
}
}
}
fn os_getenv(l: &mut LuaState) -> LuaResult<usize> {
let varname = l
.get_arg(1)
.and_then(|v| v.as_str().map(|s| s.to_string()))
.ok_or_else(|| l.error("getenv: argument 1 must be a string".to_string()))?;
match std::env::var(&varname) {
Ok(value) => {
let result = l.create_string(&value)?;
l.push_value(result)?;
Ok(1)
}
Err(_) => {
l.push_value(LuaValue::nil())?;
Ok(1)
}
}
}
fn os_remove(l: &mut LuaState) -> LuaResult<usize> {
let filename = l
.get_arg(1)
.and_then(|v| v.as_str().map(|s| s.to_string()))
.ok_or_else(|| l.error("remove: argument 1 must be a string".to_string()))?;
match std::fs::remove_file(&filename) {
Ok(_) => {
l.push_value(LuaValue::boolean(true))?;
Ok(1)
}
Err(e) => {
let err_msg = l.create_string(&format!("{}", e))?;
l.push_value(LuaValue::nil())?;
l.push_value(err_msg)?;
Ok(2)
}
}
}
fn os_rename(l: &mut LuaState) -> LuaResult<usize> {
let oldname = l
.get_arg(1)
.and_then(|v| v.as_str().map(|s| s.to_string()))
.ok_or_else(|| l.error("rename: argument 1 must be a string".to_string()))?;
let newname = l
.get_arg(2)
.and_then(|v| v.as_str().map(|s| s.to_string()))
.ok_or_else(|| l.error("rename: argument 2 must be a string".to_string()))?;
match std::fs::rename(&oldname, &newname) {
Ok(_) => {
l.push_value(LuaValue::boolean(true))?;
Ok(1)
}
Err(e) => {
let err_msg = l.create_string(&format!("{}", e))?;
l.push_value(LuaValue::nil())?;
l.push_value(err_msg)?;
Ok(2)
}
}
}
fn os_setlocale(l: &mut LuaState) -> LuaResult<usize> {
let locale = l
.get_arg(1)
.and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "C".to_string());
let result = l.create_string(&locale)?;
l.push_value(result)?;
Ok(1)
}
fn os_tmpname(l: &mut LuaState) -> LuaResult<usize> {
let timestamp = platform_time::unix_nanos();
let tmpname = format!("/tmp/lua_tmp_{}", timestamp);
let result = l.create_string(&tmpname)?;
l.push_value(result)?;
Ok(1)
}