use std::{
env::{self, temp_dir},
ffi::OsStr,
fs::{self, File},
os::unix::{
prelude::{OpenOptionsExt, OsStrExt, OsStringExt},
process::ExitStatusExt,
},
path::Path,
process::{Command, Stdio},
time::Instant,
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use time::{
ext::NumericalDuration,
format_description::{self, well_known::Rfc2822},
Date, OffsetDateTime, Time, UtcOffset,
};
use super::StdLib;
use crate::{
error::Result,
vm::{Local, Table, Value, ValueType, VM},
Error, LuaError,
};
pub(super) fn module(stdlib: &mut StdLib) -> Result<()> {
stdlib
.module("os")
.func("clock", clock)?
.func("date", date)?
.func("difftime", difftime)?
.func("execute", execute)?
.func("exit", exit)?
.func("getenv", getenv)?
.func("setlocale", setlocale)?
.func("remove", remove)?
.func("rename", rename)?
.func("time", time)?
.func("tmpname", tmpname)?;
Ok(())
}
fn clock(vm: &mut VM) -> Result<Value> {
Ok(Value::float(
Instant::now().duration_since(vm.started).as_secs_f64(),
))
}
fn date(vm: &mut VM) -> Result<Value> {
let mut format = match vm.arg_opt(0) {
Some(format) => format.to_string_coerce()?,
None => vec![b'%', b'c'],
};
let datetime = if let Some(timestamp) = vm.arg_opt(1) {
let timestamp = timestamp.to_int_coerce()?;
if timestamp < i32::MIN as i64 || timestamp > i32::MAX as i64 + 1 {
return err!(LuaError::DateTimeFieldBounds("timestamp"));
}
let offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
OffsetDateTime::from_unix_timestamp(timestamp)
.map_err(|_| Error::from_lua(LuaError::DateRepresentation))?
.to_offset(offset)
} else if format.get(0) == Some(&b'!') {
format.drain(0..1);
OffsetDateTime::now_utc()
} else {
OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
};
if &format == &vec![b'*', b't'] {
let table = vm.alloc_table();
_datetime_fields(&mut table.borrow_mut(), datetime)?;
Ok(Value::Table(table))
} else {
let mut out = Vec::new();
let mut format = format.into_iter();
macro_rules! add {
( $format:literal ) => {{
out.extend(
datetime
.format(&format_description::parse($format).unwrap())?
.as_bytes(),
);
}};
}
while let Some(char) = format.next() {
match char {
b'%' => match format.next() {
Some(b'a') => add!("[weekday repr:short]"),
Some(b'A') => add!("[weekday]"),
Some(b'b' | b'h') => add!("[month repr:short]"),
Some(b'B') => add!("[month repr:long]"),
Some(b'c') => out.extend(datetime.format(&Rfc2822)?.as_bytes()),
Some(b'C') => out.extend(format!("{:02}", datetime.year() / 100).as_bytes()),
Some(b'd') => add!("[day]"),
Some(b'D' | b'x') => add!("[month]/[day]/[year repr:last_two]"),
Some(b'e') => add!("[day padding:space]"),
Some(b'F') => add!("[year]-[month]-[day]"),
Some(b'G') => add!("[year base:iso_week]"),
Some(b'g') => add!("[year repr:last_two base:iso_week]"),
Some(b'H') => add!("[hour]"),
Some(b'I') => add!("[hour repr:12]"),
Some(b'j') => add!("[ordinal]"),
Some(b'm') => add!("[month]"),
Some(b'M') => add!("[minute]"),
Some(b'n') => out.push(b'\n'),
Some(b'p') => add!("[period case:upper]"),
Some(b'P') => add!("[period case:lower]"),
Some(b'r') => add!("[hour repr:12]:[minute]:[second] [period case:upper]"),
Some(b'R') => add!("[hour]:[minute]"),
Some(b's') => add!("[unix_timestamp]"),
Some(b'S') => add!("[second]"),
Some(b't') => out.push(b'\t'),
Some(b'T' | b'X') => add!("[hour]:[minute]:[second]"),
Some(b'u') => add!("[weekday repr:monday one_indexed:true]"),
Some(b'U') => add!("[week_number repr:sunday]"),
Some(b'V') => add!("[week_number repr:iso]"),
Some(b'w') => add!("[weekday repr:sunday one_indexed:false]"),
Some(b'W') => add!("[week_number repr:monday]"),
Some(b'y') => add!("[year repr:last_two]"),
Some(b'Y') => add!("[year]"),
Some(b'z') => add!("[offset_hour sign:mandatory][offset_minute]"),
Some(b'%') => out.push(b'%'),
Some(b'E') => match format.next() {
Some(b'c') => out.extend(datetime.format(&Rfc2822)?.as_bytes()),
Some(b'C') => {
out.extend(format!("{:02}", datetime.year() / 100).as_bytes())
}
Some(b'x') => add!("[month]/[day]/[year repr:last_two]"),
Some(b'X') => add!("[hour]:[minute]:[second]"),
Some(b'y') => add!("[year repr:last_two]"),
Some(b'Y') => add!("[year]"),
None => return err!(LuaError::DateFormat("%E".to_string())),
Some(c) => return err!(LuaError::DateFormat(format!("%E{}", c as char))),
},
Some(b'O') => match format.next() {
Some(b'd') => add!("[day]"),
Some(b'e') => add!("[day padding:space]"),
Some(b'H') => add!("[hour]"),
Some(b'I') => add!("[hour repr:12]"),
Some(b'm') => add!("[month]"),
Some(b'M') => add!("[minute]"),
Some(b'S') => add!("[second]"),
Some(b'u') => add!("[weekday repr:monday one_indexed:true]"),
Some(b'U') => add!("[week_number repr:sunday]"),
Some(b'V') => add!("[week_number repr:iso]"),
Some(b'w') => add!("[weekday repr:sunday one_indexed:false]"),
Some(b'W') => add!("[week_number repr:monday]"),
Some(b'y') => add!("[year repr:last_two]"),
None => return err!(LuaError::DateFormat("%O".to_string())),
Some(c) => return err!(LuaError::DateFormat(format!("%O{}", c as char))),
},
None => return err!(LuaError::DateFormat("%".to_string())),
Some(c) => return err!(LuaError::DateFormat(format!("%{}", c as char))),
},
_ => out.push(char),
}
}
Ok(Value::str_bytes(out))
}
}
fn _datetime_fields(table: &mut Table, datetime: OffsetDateTime) -> Result<()> {
table.set(Value::str("year"), Value::int(datetime.year() as i64))?;
table.set(Value::str("month"), Value::int(datetime.month() as i64))?;
table.set(Value::str("day"), Value::int(datetime.day() as i64))?;
table.set(Value::str("hour"), Value::int(datetime.hour() as i64))?;
table.set(Value::str("min"), Value::int(datetime.minute() as i64))?;
table.set(Value::str("sec"), Value::int(datetime.second() as i64))?;
table.set(
Value::str("wday"),
Value::int(datetime.weekday().number_from_sunday() as i64),
)?;
table.set(Value::str("yday"), Value::int(datetime.ordinal() as i64))?;
Ok(())
}
fn difftime(vm: &mut VM) -> Result<Value> {
let t2 = vm.arg_int_coerce(0)?;
let t1 = vm.arg_int_coerce(1)?;
let t2 = OffsetDateTime::from_unix_timestamp(t2)
.map_err(|_| Error::from_lua(LuaError::DateRepresentation))?;
let t1 = OffsetDateTime::from_unix_timestamp(t1)
.map_err(|_| Error::from_lua(LuaError::DateRepresentation))?;
Ok(Value::float((t2 - t1).as_seconds_f64()))
}
fn execute(vm: &mut VM) -> Result<Value> {
let command = match vm.arg_opt(0) {
Some(command) => {
let command = command.to_string_coerce()?;
String::from_utf8_lossy(&command).into_owned()
}
None => "true".to_string(),
};
let mut child = Command::new("/bin/sh")
.arg("-c")
.arg(command)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
let exit = child.wait()?;
if vm.arg_len() == 0 {
Ok(Value::Bool(exit.success()))
} else {
let success = if exit.success() {
Value::Bool(true)
} else {
Value::Nil
};
let (why, code) = match exit.code() {
Some(code) => ("exit", code as i64),
None => ("signal", exit.signal().unwrap_or_default() as i64),
};
Ok(Value::Mult(vec![
success,
Value::str(why),
Value::int(code),
]))
}
}
fn exit(vm: &mut VM) -> Result<Value> {
let code = match vm.arg_opt(0) {
Some(code) => match code {
Value::Nil => 0,
Value::Number(num) => num.coerce_int()? as i32,
Value::String(..) => code.to_int_coerce()? as i32,
Value::Bool(res) => {
if res {
0
} else {
1
}
}
_ => return err!(LuaError::ExpectedType(ValueType::Number, code.value_type())),
},
None => 0,
};
if vm.arg_opt(1).map(|v| v.is_truthy()).unwrap_or(false) {
let mut err = vm
.thread
.borrow_mut()
.error
.take()
.map(|(e, _, _)| Error::from_lua(e));
while let Some(frame) = vm.frames.pop() {
for loc in frame.locals.into_iter().rev() {
let val = match loc {
Local::Stack { val, to_close, .. } if to_close => val,
Local::Heap { var, to_close, .. } if to_close => var.take(),
Local::Temp | Local::Stack { .. } | Local::Heap { .. } => continue,
};
if val.is_truthy() {
if let Some(meta) = vm.get_metatable(&val) {
let func = meta.borrow().get(&Value::str("__close")).to_func()?;
let args = vec![
val.clone(),
match err.as_ref() {
Some(e) => e.to_value(),
None => Value::Nil,
},
];
if let Err(e) = vm.call_recursive(func, args) {
err = Some(e);
}
}
}
}
}
*vm = VM::default();
}
std::process::exit(code);
}
fn getenv(vm: &mut VM) -> Result<Value> {
let varname = vm.arg_string_coerce(0)?;
let value = env::var_os(OsStr::from_bytes(&varname));
Ok(value
.map(OsStringExt::into_vec)
.map(Value::str_bytes)
.unwrap_or(Value::Nil))
}
fn setlocale(vm: &mut VM) -> Result<Value> {
Ok(match vm.arg_or_nil(0) {
Value::Nil => Value::str("C"),
v => {
let locale = v.to_string()?;
if locale == &vec![b'C'] {
Value::str("C")
} else {
Value::Nil
}
}
})
}
fn remove(vm: &mut VM) -> Result<Value> {
let filename = String::from_utf8_lossy(&vm.arg_string(0)?).into_owned();
let res = if Path::new(&filename).is_dir() {
fs::remove_dir(filename)
} else {
fs::remove_file(filename)
};
Ok(match res {
Ok(_) => Value::Bool(true),
Err(e) => e.into(),
})
}
fn rename(vm: &mut VM) -> Result<Value> {
let oldname = String::from_utf8_lossy(&vm.arg_string(0)?).into_owned();
let newname = String::from_utf8_lossy(&vm.arg_string(1)?).into_owned();
Ok(match fs::rename(oldname, newname) {
Ok(_) => Value::Bool(true),
Err(e) => e.into(),
})
}
fn time(vm: &mut VM) -> Result<Value> {
let time = match vm.arg_opt(0) {
None => OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()),
Some(val) => {
let table = val.to_table()?;
let mut table = table.borrow_mut();
let year = _time_field(&table, "year", None)?;
let month = _time_field(&table, "month", None)?;
let day = _time_field(&table, "day", None)? - 1;
let hour = _time_field(&table, "hour", Some(12))?;
let min = _time_field(&table, "min", Some(0))?;
let sec = _time_field(&table, "sec", Some(0))?;
let year = year + ((month - 1) / 12);
let month = ((((month - 1) % 12) + 1) as u8).try_into().unwrap();
if year < i32::MIN as i64 + 1900 || year > i32::MAX as i64 + 1900 {
return err!(LuaError::DateTimeFieldBounds("year"));
}
let offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
let datetime = Date::from_calendar_date(year as i32, month, 1)
.ok()
.and_then(|d| d.checked_add(day.days()))
.map(|d| OffsetDateTime::new_in_offset(d, Time::MIDNIGHT, offset))
.and_then(|d| d.checked_add(hour.hours()))
.and_then(|d| d.checked_add(min.minutes()))
.and_then(|d| d.checked_add(sec.seconds()))
.ok_or_else(|| Error::from_lua(LuaError::DateInvalid))?;
_datetime_fields(&mut table, datetime)?;
datetime
}
};
if time.unix_timestamp() < i32::MIN as i64 || time.unix_timestamp() > i32::MAX as i64 + 1 {
err!(LuaError::DateRepresentation)
} else {
Ok(Value::int(time.unix_timestamp()))
}
}
fn _time_field(table: &Table, name: &'static str, default: Option<i64>) -> Result<i64> {
match table.get(&Value::str(name)) {
Value::Nil => match default {
Some(default) => Ok(default),
None => err!(LuaError::DateTimeFieldMissing(name)),
},
val => {
let val = val
.to_number_coerce()
.and_then(|v| v.coerce_int())
.map_err(|_| Error::from_lua(LuaError::DateTimeFieldInteger(name)))?;
if val < i32::MIN as i64 || val > i32::MAX as i64 {
err!(LuaError::DateTimeFieldBounds(name))
} else {
Ok(val)
}
}
}
}
pub(super) fn tmpname(_: &mut VM) -> Result<Value> {
let dir = temp_dir();
let mut rng = thread_rng();
for _ in 0..10 {
let mut filename = "luallaby_".to_string();
filename.extend((0..6).map(|_| rng.sample(Alphanumeric) as char));
let path = dir.join(filename);
if File::options()
.write(true)
.create_new(true)
.mode(0o600)
.open(&path)
.is_ok()
{
return Ok(Value::str(&path.to_string_lossy()));
}
}
Ok(Value::Mult(vec![
Value::Nil,
Value::str("too many temporary files"),
]))
}