use num_bigint::BigInt;
use num_traits::ToPrimitive;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::format::{Item, StrftimeItems};
use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveTime, TimeDelta, TimeZone, Timelike, Utc};
use crate::vm::machine::VM;
use crate::vm::value::{Value, VmTable};
mod storage;
fn os_clock(_vm: &mut VM, _args: &[Value]) -> Result<Vec<Value>, String> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
Ok(vec![Value::Float(now.as_secs_f64())])
}
fn os_time(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let Some(Value::Table(fields)) = args.first() else {
if let Some(other) = args.first().filter(|v| !matches!(v, Value::Nil)) {
return Err(format!("t must be a table, got {}", other.type_name()));
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
return Ok(vec![Value::from_bigint(BigInt::from(now.as_secs()))]);
};
let fields = fields.borrow();
let field = |name: &str, default: Option<i64>| -> Result<i64, String> {
match fields.get_str(name) {
Value::Nil => default.ok_or_else(|| format!("field '{}' is missing", name)),
Value::Int(i) => Ok(i),
Value::Float(f) if f.fract() == 0.0 && f.abs() < 1e15 => Ok(f as i64),
other => Err(format!(
"field '{}' must be an integer, got {}",
name,
other.type_name()
)),
}
};
let (year, month, day) = (
field("year", None)?,
field("month", None)?,
field("day", None)?,
);
let (hour, min, sec) = (
field("hour", Some(12))?,
field("min", Some(0))?,
field("sec", Some(0))?,
);
let months = year
.checked_mul(12)
.and_then(|m| m.checked_add(month - 1))
.ok_or("date is out of range")?;
let first = i32::try_from(months.div_euclid(12))
.ok()
.and_then(|y| NaiveDate::from_ymd_opt(y, months.rem_euclid(12) as u32 + 1, 1))
.ok_or("date is out of range")?;
let offset = TimeDelta::try_days(day - 1)
.zip(TimeDelta::try_hours(hour))
.zip(TimeDelta::try_minutes(min))
.zip(TimeDelta::try_seconds(sec))
.and_then(|(((d, h), m), s)| d.checked_add(&h)?.checked_add(&m)?.checked_add(&s));
let naive = offset
.and_then(|offset| first.and_time(NaiveTime::MIN).checked_add_signed(offset))
.ok_or("date is out of range")?;
let local = Local
.from_local_datetime(&naive)
.earliest()
.ok_or("that local time does not exist")?;
Ok(vec![Value::Int(local.timestamp())])
}
fn date_table<Tz: TimeZone>(time: &DateTime<Tz>) -> Value {
let mut table = VmTable::new();
table.set_str("year", Value::Int(time.year() as i64));
table.set_str("month", Value::Int(time.month() as i64));
table.set_str("day", Value::Int(time.day() as i64));
table.set_str("hour", Value::Int(time.hour() as i64));
table.set_str("min", Value::Int(time.minute() as i64));
table.set_str("sec", Value::Int(time.second() as i64));
table.set_str(
"wday",
Value::Int(time.weekday().num_days_from_sunday() as i64 + 1),
);
table.set_str("yday", Value::Int(time.ordinal() as i64));
Value::Table(Rc::new(RefCell::new(table)))
}
fn format_time<Tz: TimeZone>(time: &DateTime<Tz>, format: &str) -> Result<String, String>
where
Tz::Offset: std::fmt::Display,
{
let items: Vec<Item> = StrftimeItems::new(format).collect();
if items.iter().any(|item| matches!(item, Item::Error)) {
return Err(format!("invalid date format `{}`", format));
}
let mut out = String::new();
use std::fmt::Write;
write!(out, "{}", time.format_with_items(items.into_iter()))
.map_err(|_| format!("invalid date format `{}`", format))?;
Ok(out)
}
fn os_date(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let format = match args.first() {
None | Some(Value::Nil) => "%c",
Some(Value::String(s)) => &**s,
Some(other) => {
return Err(format!(
"format must be a string, got {}",
other.type_name()
));
}
};
let seconds = match args.get(1) {
None | Some(Value::Nil) => None,
Some(value) => Some(
crate::vm::libs::args::number(value)
.map_err(|_| format!("time must be a number, got {}", value.type_name()))?,
),
};
let utc_time = match seconds {
None => Utc::now(),
Some(seconds) => {
let whole = seconds.floor();
DateTime::from_timestamp(whole as i64, ((seconds - whole) * 1e9) as u32)
.filter(|_| seconds.is_finite())
.ok_or("time is out of range")?
}
};
let (utc, format) = match format.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, format),
};
let value = match (utc, format) {
(true, "*t") => date_table(&utc_time),
(false, "*t") => date_table(&utc_time.with_timezone(&Local)),
(true, _) => Value::string(format_time(&utc_time, format)?),
(false, _) => Value::string(format_time(&utc_time.with_timezone(&Local), format)?),
};
Ok(vec![value])
}
fn os_difftime(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let t2 = match args
.first()
.ok_or_else(|| "os.difftime expects 2 arguments".to_string())?
{
Value::Int(i) => i.to_f64().unwrap_or(0.0),
Value::Float(f) => *f,
_ => return Err("os.difftime expects numbers".to_string()),
};
let t1 = match args
.get(1)
.ok_or_else(|| "os.difftime expects 2 arguments".to_string())?
{
Value::Int(i) => i.to_f64().unwrap_or(0.0),
Value::Float(f) => *f,
_ => return Err("os.difftime expects numbers".to_string()),
};
Ok(vec![Value::Float(t2 - t1)])
}
static SCRIPT_ARGS: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
pub fn set_script_args(args: Vec<String>) {
let _ = SCRIPT_ARGS.set(args);
}
fn os_args(_vm: &mut VM, _args: &[Value]) -> Result<Vec<Value>, String> {
let args = SCRIPT_ARGS.get().map(Vec::as_slice).unwrap_or_default();
Ok(vec![crate::vm::libs::args::array(
args.iter().map(|a| Value::string(a.clone())).collect(),
)])
}
fn string_field(table: &VmTable, name: &str) -> Result<Option<String>, String> {
match table.get_str(name) {
Value::Nil => Ok(None),
Value::String(s) => Ok(Some(s.to_string())),
other => Err(format!(
"{} must be a string, got {}",
name,
other.type_name()
)),
}
}
fn os_run(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut command = match args.first() {
Some(Value::String(line)) => {
let mut command = if cfg!(windows) {
Command::new("cmd")
} else {
Command::new("sh")
};
command
.arg(if cfg!(windows) { "/C" } else { "-c" })
.arg(&**line);
command
}
Some(Value::Table(list)) => {
let list = list.borrow();
let mut parts = Vec::with_capacity(list.array.len());
for (index, part) in list.array.iter().enumerate() {
match part {
Value::String(s) => parts.push(s.to_string()),
other => {
return Err(format!(
"command[{}] must be a string, got {}",
index + 1,
other.type_name()
));
}
}
}
let (program, rest) = parts.split_first().ok_or("command must not be empty")?;
let mut command = Command::new(program);
command.args(rest);
command
}
None | Some(Value::Nil) => return Err("command must be provided".to_string()),
Some(other) => {
return Err(format!(
"command must be a string or a list of strings, got {}",
other.type_name()
));
}
};
let mut capture = true;
let mut input = None;
match args.get(1) {
None | Some(Value::Nil) => {}
Some(Value::Table(options)) => {
let options = options.borrow();
if let Some(cwd) = string_field(&options, "cwd")? {
command.current_dir(cwd);
}
input = string_field(&options, "stdin")?;
match options.get_str("capture") {
Value::Nil => {}
Value::Bool(b) => capture = b,
other => {
return Err(format!(
"capture must be a boolean, got {}",
other.type_name()
));
}
}
match options.get_str("env") {
Value::Nil => {}
Value::Table(env) => {
for (key, value) in env.borrow().fields.iter() {
match value {
Value::String(v) => {
command.env(&**key, &**v);
}
other => {
return Err(format!(
"env.{} must be a string, got {}",
&**key,
other.type_name()
));
}
}
}
}
other => {
return Err(format!("env must be a table, got {}", other.type_name()));
}
}
}
Some(other) => {
return Err(format!(
"options must be a table, got {}",
other.type_name()
));
}
}
command.stdin(if input.is_some() {
Stdio::piped()
} else {
Stdio::inherit()
});
if capture {
command.stdout(Stdio::piped()).stderr(Stdio::piped());
}
crate::vm::libs::background::run(
vm,
move || {
let mut child = command
.spawn()
.map_err(|err| format!("cannot run command: {}", err))?;
if let Some(text) = input
&& let Some(mut stdin) = child.stdin.take()
{
let bytes = text.into_bytes();
std::thread::spawn(move || {
let _ = stdin.write_all(&bytes);
});
}
child
.wait_with_output()
.map_err(|err| format!("cannot run command: {}", err))
},
|_vm, output| Ok(vec![run_result(output)]),
)
}
fn run_result(output: std::process::Output) -> Value {
let mut result = VmTable::new();
result.set_str("ok", Value::Bool(output.status.success()));
result.set_str(
"code",
output
.status
.code()
.map_or(Value::Nil, |c| Value::Int(c as i64)),
);
result.set_str(
"stdout",
Value::string(String::from_utf8_lossy(&output.stdout).into_owned()),
);
result.set_str(
"stderr",
Value::string(String::from_utf8_lossy(&output.stderr).into_owned()),
);
Value::Table(Rc::new(RefCell::new(result)))
}
fn os_sleep(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let seconds = crate::vm::libs::args::number_arg(args, 0, "seconds")?;
if !seconds.is_finite() || seconds < 0.0 {
return Err("seconds must be a non-negative number".to_string());
}
std::thread::sleep(std::time::Duration::from_secs_f64(seconds));
Ok(vec![])
}
fn os_exit(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let code = match args.first() {
None | Some(Value::Nil) | Some(Value::Bool(true)) => 0,
Some(Value::Bool(false)) => 1,
Some(Value::Int(i)) => i32::try_from(*i).map_err(|_| "exit code is out of range")?,
Some(other) => {
return Err(format!(
"code must be a boolean or an integer, got {}",
other.type_name()
));
}
};
use std::io::Write;
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
std::process::exit(code)
}
fn os_getenv(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let varname = match args
.first()
.ok_or_else(|| "os.getenv expects variable name".to_string())?
{
Value::String(s) => s,
_ => return Err("os.getenv expects string".to_string()),
};
match std::env::var(&**varname) {
Ok(v) => Ok(vec![Value::string(v)]),
Err(_) => Ok(vec![Value::Nil]),
}
}
fn os_app(_vm: &mut VM, _args: &[Value]) -> Result<Vec<Value>, String> {
let mut table = VmTable::new();
let bundle = crate::app::installed();
table.set_str("bundled", Value::Bool(bundle.is_some()));
if let Some(bundle) = bundle {
for key in ["name", "version", "identifier"] {
if let Some(value) = bundle.meta(key) {
table.set_str(key, Value::string(value.to_string()));
}
}
}
Ok(vec![crate::vm::libs::args::frozen(table)])
}
pub fn create_os_lib() -> Value {
let mut table = VmTable::new();
table.set_str("clock", crate::native!("os.clock", os_clock));
table.set_str("time", crate::native!("os.time", os_time));
table.set_str("date", crate::native!("os.date", os_date));
table.set_str("difftime", crate::native!("os.difftime", os_difftime));
table.set_str("getenv", crate::native!("os.getenv", os_getenv));
table.set_str("sleep", crate::native!("os.sleep", os_sleep));
table.set_str("exit", crate::native!("os.exit", os_exit));
table.set_str("args", crate::native!("os.args", os_args));
table.set_str("run", crate::native!("os.run", os_run));
table.set_str("app", crate::native!("os.app", os_app));
table.set_str(
"spawn",
crate::native!("os.spawn", crate::vm::libs::net::os_spawn),
);
table.set_str(
"dataDir",
crate::native!("os.dataDir", storage::os_data_dir),
);
table.set_str(
"configDir",
crate::native!("os.configDir", storage::os_config_dir),
);
table.set_str(
"cacheDir",
crate::native!("os.cacheDir", storage::os_cache_dir),
);
table.set_str("protect", crate::native!("os.protect", storage::os_protect));
table.set_str(
"unprotect",
crate::native!("os.unprotect", storage::os_unprotect),
);
table.frozen = true;
Value::Table(Rc::new(RefCell::new(table)))
}