use std::io::{Read, Seek, SeekFrom, Write};
use crate::runtime::{FileHandle, Gc, Userdata, UserdataPayload, Value};
use crate::vm::builtins::{arg_error, raise_str};
use crate::vm::error::LuaError;
use crate::vm::exec::Vm;
pub(crate) fn open_os_io(vm: &mut Vm) {
let os = vm.heap.new_table();
let set = |vm: &mut Vm, t: crate::runtime::Gc<crate::runtime::Table>, name: &str, f| {
let fv = vm.native(f);
let k = Value::Str(vm.heap.intern(name.as_bytes()));
unsafe { t.as_mut() }
.set(&mut vm.heap, k, fv)
.expect("valid key");
};
set(vm, os, "time", os_time);
set(vm, os, "clock", os_clock);
set(vm, os, "date", os_date);
set(vm, os, "difftime", os_difftime);
set(vm, os, "getenv", os_getenv);
set(vm, os, "setlocale", os_setlocale);
set(vm, os, "tmpname", os_tmpname);
set(vm, os, "remove", os_remove);
set(vm, os, "rename", os_rename);
set(vm, os, "execute", os_execute);
set(vm, os, "exit", os_exit);
vm.set_global("os", Value::Table(os))
.expect("stdlib registration");
vm.barrier_back_table(os);
let io = vm.heap.new_table();
set(vm, io, "write", io_write2);
set(vm, io, "read", io_read2);
set(vm, io, "type", io_type);
set(vm, io, "input", io_input);
set(vm, io, "output", io_output);
set(vm, io, "close", io_close);
set(vm, io, "open", io_open);
set(vm, io, "tmpfile", io_tmpfile);
set(vm, io, "lines", io_lines);
set(vm, io, "flush", io_flush);
set(vm, io, "popen", io_popen);
let methods = vm.heap.new_table();
set(vm, methods, "close", f_close);
set(vm, methods, "read", f_read);
set(vm, methods, "write", f_write);
set(vm, methods, "seek", f_seek);
set(vm, methods, "flush", f_flush);
set(vm, methods, "setvbuf", f_setvbuf);
set(vm, methods, "lines", f_lines);
let file_mt = vm.heap.new_table();
{
let put = |vm: &mut Vm, k: &[u8], v: Value| {
let kk = Value::Str(vm.heap.intern(k));
unsafe { file_mt.as_mut() }
.set(&mut vm.heap, kk, v)
.expect("valid key");
};
let mname = Value::Str(vm.heap.intern(b"FILE*"));
put(vm, b"__name", mname);
put(vm, b"__index", Value::Table(methods));
let f = vm.native(f_tostring);
put(vm, b"__tostring", f);
let f = vm.native(f_gc);
put(vm, b"__gc", f);
let f = vm.native(f_gc);
put(vm, b"__close", f);
}
vm.barrier_back_table(methods);
vm.barrier_back_table(file_mt);
vm.file_mt = Some(file_mt);
for (name, fh) in [
("stdin", FileHandle::Stdin),
("stdout", FileHandle::Stdout),
("stderr", FileHandle::Stderr),
] {
let writable = !matches!(fh, FileHandle::Stdin);
let h = new_file(vm, fh, writable);
let k = Value::Str(vm.heap.intern(name.as_bytes()));
unsafe { io.as_mut() }
.set(&mut vm.heap, k, Value::Userdata(h))
.expect("valid key");
match name {
"stdin" => vm.io_input = Some(h),
"stdout" => vm.io_output = Some(h),
_ => {}
}
}
vm.set_global("io", Value::Table(io))
.expect("stdlib registration");
vm.barrier_back_table(io);
let f = vm.native(nat_loadfile);
vm.set_global("loadfile", f).expect("stdlib registration");
let f = vm.native(nat_dofile);
vm.set_global("dofile", f).expect("stdlib registration");
}
fn days_from_civil(y: i64, m: u32, d: u32) -> Option<i64> {
Some(days_from_civil_impl(y, m, d))
}
fn days_from_civil_impl(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y / 400 } else { (y - 399) / 400 };
let yoe = y - era * 400; let mm = m as i64;
let doy = (153 * (mm + if mm > 2 { -3 } else { 9 }) + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719468;
let era = if z >= 0 {
z / 146097
} 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 d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn day_of_year(y: i64, m: u32, d: u32) -> u32 {
static CUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let mut yd = CUM[(m - 1) as usize] + d;
if m > 2 && is_leap(y) {
yd += 1;
}
yd
}
struct BrokenDown {
year: i64,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
wday: u32, yday: u32,
}
fn broken_down(secs: i64) -> BrokenDown {
let day_secs = 86_400i64;
let days = secs.div_euclid(day_secs);
let time = secs.rem_euclid(day_secs) as u32;
let (y, m, d) = civil_from_days(days);
let wday = ((days + 4).rem_euclid(7) + 1) as u32;
BrokenDown {
year: y,
month: m,
day: d,
hour: time / 3600,
min: (time % 3600) / 60,
sec: time % 60,
wday,
yday: day_of_year(y, m, d),
}
}
fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn os_time(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
if nargs == 0 || vm.nat_arg(fs, nargs, 0).is_nil() {
return Ok(vm.nat_return(fs, &[Value::Int(now_secs())]));
}
let Value::Table(t) = vm.nat_arg(fs, nargs, 0) else {
return Err(arg_error(vm, 1, "time", "table expected"));
};
let year = check_int_field(vm, t, "year")?;
let month = check_int_field(vm, t, "month")?;
let day = check_int_field(vm, t, "day")?;
let hour = opt_int_field(vm, t, "hour", 12)?;
let min = opt_int_field(vm, t, "min", 0)?;
let sec = opt_int_field(vm, t, "sec", 0)?;
if year
.checked_sub(1900)
.is_none_or(|v| v < i32::MIN as i64 || v > i32::MAX as i64)
{
return Err(raise_str(vm, "field 'year' is out-of-bound"));
}
let year_b = year as i32; let month_b = bound_i32(vm, "month", month)?;
let day_b = bound_i32(vm, "day", day)?;
let hour_b = bound_i32(vm, "hour", hour)?;
let min_b = bound_i32(vm, "min", min)?;
let sec_b = bound_i32(vm, "sec", sec)?;
let year_full = year_b as i64;
let secs = compose_time(year_full, month_b, day_b, hour_b, min_b, sec_b)
.ok_or_else(|| raise_str(vm, "time result cannot be represented in this installation"))?;
let bd = broken_down(secs);
set_int_field(vm, t, "year", bd.year);
set_int_field(vm, t, "month", bd.month as i64);
set_int_field(vm, t, "day", bd.day as i64);
set_int_field(vm, t, "hour", bd.hour as i64);
set_int_field(vm, t, "min", bd.min as i64);
set_int_field(vm, t, "sec", bd.sec as i64);
set_int_field(vm, t, "wday", bd.wday as i64);
set_int_field(vm, t, "yday", bd.yday as i64);
Ok(vm.nat_return(fs, &[Value::Int(secs)]))
}
fn compose_time(y: i64, m: i32, d: i32, h: i32, mi: i32, s: i32) -> Option<i64> {
let m_total: i64 = (m as i64) - 1; let year_carry = m_total.div_euclid(12);
let month0 = m_total.rem_euclid(12) as u32; let year = y.checked_add(year_carry)?;
let days = days_from_civil(year, month0 + 1, 1)?.checked_add(d as i64 - 1)?;
days.checked_mul(86_400)?
.checked_add(h as i64 * 3600)?
.checked_add(mi as i64 * 60)?
.checked_add(s as i64)
}
fn check_int_field(
vm: &mut Vm,
t: crate::runtime::Gc<crate::runtime::Table>,
name: &str,
) -> Result<i64, LuaError> {
let k = Value::Str(vm.heap.intern(name.as_bytes()));
let v = t.get(k);
match v {
Value::Nil => Err(raise_str(
vm,
&format!("field '{name}' missing in date table"),
)),
Value::Int(i) => Ok(i),
Value::Float(f) if f.fract() == 0.0 && f.is_finite() => Ok(f as i64),
_ => Err(raise_str(vm, &format!("field '{name}' is not an integer"))),
}
}
fn opt_int_field(
vm: &mut Vm,
t: crate::runtime::Gc<crate::runtime::Table>,
name: &str,
default: i64,
) -> Result<i64, LuaError> {
let k = Value::Str(vm.heap.intern(name.as_bytes()));
let v = t.get(k);
match v {
Value::Nil => Ok(default),
Value::Int(i) => Ok(i),
Value::Float(f) if f.fract() == 0.0 && f.is_finite() => Ok(f as i64),
_ => Err(raise_str(vm, &format!("field '{name}' is not an integer"))),
}
}
fn bound_i32(vm: &mut Vm, name: &str, v: i64) -> Result<i32, LuaError> {
if v < i32::MIN as i64 || v > i32::MAX as i64 {
return Err(raise_str(vm, &format!("field '{name}' is out-of-bound")));
}
Ok(v as i32)
}
fn set_int_field(vm: &mut Vm, t: crate::runtime::Gc<crate::runtime::Table>, name: &str, v: i64) {
let k = Value::Str(vm.heap.intern(name.as_bytes()));
unsafe { t.as_mut() }
.set(&mut vm.heap, k, Value::Int(v))
.expect("valid key");
}
fn os_clock(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
let secs = vm.uptime().as_secs_f64();
Ok(vm.nat_return(fs, &[Value::Float(secs)]))
}
fn os_difftime(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let t2 = num_arg(vm, fs, nargs, 0, "difftime")?;
let t1 = if nargs >= 2 {
num_arg(vm, fs, nargs, 1, "difftime")?
} else {
0.0
};
Ok(vm.nat_return(fs, &[Value::Float(t2 - t1)]))
}
fn num_arg(vm: &mut Vm, fs: u32, nargs: u32, i: u32, who: &str) -> Result<f64, LuaError> {
match vm.nat_arg(fs, nargs, i) {
Value::Int(v) => Ok(v as f64),
Value::Float(v) => Ok(v),
v => Err(arg_error(
vm,
i + 1,
who,
&format!("number expected, got {}", v.type_name()),
)),
}
}
fn os_date(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let fmt_v = if nargs == 0 {
Value::Str(vm.heap.intern(b"%c"))
} else {
vm.nat_arg(fs, nargs, 0)
};
let fmt_owned: Vec<u8> = match fmt_v {
Value::Nil => b"%c".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
v => {
return Err(arg_error(
vm,
1,
"date",
&format!("string expected, got {}", v.type_name()),
));
}
};
let t = if nargs >= 2 && !vm.nat_arg(fs, nargs, 1).is_nil() {
match vm.nat_arg(fs, nargs, 1) {
Value::Int(i) => i,
Value::Float(f) => f as i64,
_ => {
return Err(arg_error(vm, 2, "date", "integer expected"));
}
}
} else {
now_secs()
};
let mut fmt: &[u8] = &fmt_owned;
if fmt.first() == Some(&b'!') {
fmt = &fmt[1..];
}
let bd = broken_down(t);
if fmt == b"*t" {
let table = vm.heap.new_table();
set_int_field(vm, table, "year", bd.year);
set_int_field(vm, table, "month", bd.month as i64);
set_int_field(vm, table, "day", bd.day as i64);
set_int_field(vm, table, "hour", bd.hour as i64);
set_int_field(vm, table, "min", bd.min as i64);
set_int_field(vm, table, "sec", bd.sec as i64);
set_int_field(vm, table, "wday", bd.wday as i64);
set_int_field(vm, table, "yday", bd.yday as i64);
let k = Value::Str(vm.heap.intern(b"isdst"));
unsafe { table.as_mut() }
.set(&mut vm.heap, k, Value::Bool(false))
.expect("valid key");
vm.barrier_back_table(table);
return Ok(vm.nat_return(fs, &[Value::Table(table)]));
}
let mut out = Vec::new();
let mut i = 0;
while i < fmt.len() {
if fmt[i] != b'%' {
out.push(fmt[i]);
i += 1;
continue;
}
if i + 1 >= fmt.len() {
return Err(raise_str(vm, "invalid conversion specifier '%'"));
}
let c = fmt[i + 1];
match c {
b'%' => out.push(b'%'),
b'Y' => out.extend_from_slice(format!("{}", bd.year).as_bytes()),
b'y' => out.extend_from_slice(format!("{:02}", bd.year.rem_euclid(100)).as_bytes()),
b'm' => out.extend_from_slice(format!("{:02}", bd.month).as_bytes()),
b'd' => out.extend_from_slice(format!("{:02}", bd.day).as_bytes()),
b'H' => out.extend_from_slice(format!("{:02}", bd.hour).as_bytes()),
b'M' => out.extend_from_slice(format!("{:02}", bd.min).as_bytes()),
b'S' => out.extend_from_slice(format!("{:02}", bd.sec).as_bytes()),
b'j' => out.extend_from_slice(format!("{:03}", bd.yday).as_bytes()),
b'w' => out.extend_from_slice(format!("{}", bd.wday - 1).as_bytes()),
b'p' => out.extend_from_slice(if bd.hour < 12 { b"AM" } else { b"PM" }),
b'a' | b'A' => {
let names: [&[u8]; 7] = if c == b'a' {
[b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"]
} else {
[
b"Sunday",
b"Monday",
b"Tuesday",
b"Wednesday",
b"Thursday",
b"Friday",
b"Saturday",
]
};
out.extend_from_slice(names[(bd.wday - 1) as usize]);
}
b'b' | b'B' => {
let names: [&[u8]; 12] = if c == b'b' {
[
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep",
b"Oct", b"Nov", b"Dec",
]
} else {
[
b"January",
b"February",
b"March",
b"April",
b"May",
b"June",
b"July",
b"August",
b"September",
b"October",
b"November",
b"December",
]
};
out.extend_from_slice(names[(bd.month - 1) as usize]);
}
b'c' => {
let wd = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][(bd.wday - 1) as usize];
let mn = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
"Dec",
][(bd.month - 1) as usize];
out.extend_from_slice(
format!(
"{wd} {mn} {:2} {:02}:{:02}:{:02} {}",
bd.day, bd.hour, bd.min, bd.sec, bd.year
)
.as_bytes(),
);
}
b'x' => out.extend_from_slice(
format!(
"{:02}/{:02}/{:02}",
bd.month,
bd.day,
bd.year.rem_euclid(100)
)
.as_bytes(),
),
b'X' => out.extend_from_slice(
format!("{:02}:{:02}:{:02}", bd.hour, bd.min, bd.sec).as_bytes(),
),
b'Z' => {} _ => {
return Err(raise_str(
vm,
&format!("invalid conversion specifier '%{}'", c as char),
));
}
}
i += 2;
}
let v = Value::Str(vm.heap.intern(&out));
Ok(vm.nat_return(fs, &[v]))
}
fn os_getenv(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let Value::Str(name) = vm.nat_arg(fs, nargs, 0) else {
return Err(arg_error(vm, 1, "getenv", "string expected"));
};
let name = String::from_utf8_lossy(name.as_bytes()).into_owned();
let v = match std::env::var_os(&name) {
Some(val) => Value::Str(vm.heap.intern(val.to_string_lossy().as_bytes())),
None => Value::Nil,
};
Ok(vm.nat_return(fs, &[v]))
}
fn os_setlocale(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let v = match vm.nat_arg(fs, nargs, 0) {
Value::Nil => Value::Str(vm.heap.intern(b"C")),
Value::Str(s) if s.as_bytes() == b"C" || s.as_bytes().is_empty() => {
Value::Str(vm.heap.intern(b"C"))
}
_ => Value::Nil,
};
Ok(vm.nat_return(fs, &[v]))
}
fn new_file(vm: &mut Vm, fh: FileHandle, writable: bool) -> Gc<Userdata> {
let u = vm.heap.new_userdata(UserdataPayload::File(fh), writable);
if let Some(mt) = vm.file_mt {
unsafe { u.as_mut() }.set_metatable(Some(mt));
}
u
}
fn check_file(vm: &mut Vm, fs: u32, nargs: u32, who: &str) -> Result<Gc<Userdata>, LuaError> {
match vm.nat_arg(fs, nargs, 0) {
Value::Userdata(u) => Ok(u),
_ if nargs == 0 => Err(raise_str(
vm,
&format!("bad argument #1 to '{who}' (got no value)"),
)),
v => {
let tn = vm.obj_typename(v);
Err(arg_error(vm, 1, who, &format!("FILE* expected, got {tn}")))
}
}
}
fn io_type(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let r = match vm.nat_arg(fs, nargs, 0) {
Value::Userdata(u) => {
if u.file().is_closed() {
"closed file"
} else {
"file"
}
}
_ => return Ok(vm.nat_return(fs, &[Value::Nil])),
};
let s = Value::Str(vm.heap.intern(r.as_bytes()));
Ok(vm.nat_return(fs, &[s]))
}
fn io_input(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
if nargs >= 1 && !vm.nat_arg(fs, nargs, 0).is_nil() {
let u = io_default_arg(vm, fs, nargs, "r", "input")?;
vm.io_input = Some(u);
}
let cur = vm.io_input.expect("default input set at startup");
Ok(vm.nat_return(fs, &[Value::Userdata(cur)]))
}
fn io_default_arg(
vm: &mut Vm,
fs: u32,
nargs: u32,
mode: &str,
who: &str,
) -> Result<Gc<Userdata>, LuaError> {
match vm.nat_arg(fs, nargs, 0) {
Value::Userdata(u) => Ok(u),
Value::Str(s) => {
let path = String::from_utf8_lossy(s.as_bytes()).into_owned();
let (opts, writable) = parse_mode(mode.as_bytes()).expect("static mode is valid");
match opts.open(&path) {
Ok(file) => Ok(new_file(vm, FileHandle::File(file), writable)),
Err(e) => Err(raise_str(vm, &format!("{path}: {e}"))),
}
}
v => {
let tn = vm.obj_typename(v);
Err(arg_error(vm, 1, who, &format!("FILE* expected, got {tn}")))
}
}
}
fn io_output(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
if nargs >= 1 && !vm.nat_arg(fs, nargs, 0).is_nil() {
let u = io_default_arg(vm, fs, nargs, "w", "output")?;
vm.io_output = Some(u);
}
let cur = vm.io_output.expect("default output set at startup");
Ok(vm.nat_return(fs, &[Value::Userdata(cur)]))
}
fn io_close(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = if nargs >= 1 && !vm.nat_arg(fs, nargs, 0).is_nil() {
match vm.nat_arg(fs, nargs, 0) {
Value::Userdata(u) => u,
v => {
let tn = vm.obj_typename(v);
return Err(arg_error(
vm,
1,
"close",
&format!("FILE* expected, got {tn}"),
));
}
}
} else {
vm.io_output.expect("default output set at startup")
};
close_file(vm, fs, u)
}
fn f_close(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_file(vm, fs, nargs, "close")?;
close_file(vm, fs, u)
}
fn close_file(vm: &mut Vm, fs: u32, u: Gc<Userdata>) -> Result<u32, LuaError> {
if u.file().is_closed() {
return Err(raise_str(vm, "attempt to use a closed file"));
}
if u.file().is_std() {
let msg = Value::Str(vm.heap.intern(b"cannot close standard file"));
return Ok(vm.nat_return(fs, &[Value::Bool(false), msg]));
}
let drain = drain_write_buf(u);
*unsafe { u.as_mut() }.file_mut() = FileHandle::Closed;
#[cfg(any(unix, windows))]
{
use crate::version::LuaVersion;
let popen_child = unsafe { u.as_mut() }.popen_child.take();
if let Some(mut child) = popen_child {
let _ = drain; let status = match child.wait() {
Ok(s) => s,
Err(e) => return Ok(file_result_err(vm, fs, "popen", &e)),
};
let (kind, code) = exit_status_breakdown(&status);
if vm.version() <= LuaVersion::Lua51 {
return Ok(vm.nat_return(fs, &[Value::Int(code as i64)]));
}
let kind_s = Value::Str(vm.heap.intern(kind.as_bytes()));
let ok = matches!(kind, "exit") && code == 0;
return Ok(vm.nat_return(fs, &[Value::Bool(ok), kind_s, Value::Int(code as i64)]));
}
}
match drain {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Bool(true)])),
Err(e) => Ok(file_result_err(vm, fs, "file", &e)),
}
}
fn f_tostring(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_file(vm, fs, nargs, "tostring")?;
let s = if u.file().is_closed() {
"file (closed)".to_string()
} else {
format!("file ({:p})", u.as_ptr())
};
let v = Value::Str(vm.heap.intern(s.as_bytes()));
Ok(vm.nat_return(fs, &[v]))
}
fn f_gc(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_file(vm, fs, nargs, "__gc")?;
if matches!(u.file(), FileHandle::File(_)) {
*unsafe { u.as_mut() }.file_mut() = FileHandle::Closed;
}
Ok(vm.nat_return(fs, &[]))
}
fn check_open(vm: &mut Vm, fs: u32, nargs: u32, who: &str) -> Result<Gc<Userdata>, LuaError> {
let u = check_file(vm, fs, nargs, who)?;
if u.file().is_closed() {
return Err(raise_str(vm, "attempt to use a closed file"));
}
Ok(u)
}
fn parse_mode(m: &[u8]) -> Option<(std::fs::OpenOptions, bool)> {
let mut it = m.iter().copied().peekable();
let mut opts = std::fs::OpenOptions::new();
let writable = match it.next()? {
b'r' => {
opts.read(true);
if it.peek() == Some(&b'+') {
it.next();
opts.write(true); true
} else {
false
}
}
b'w' => {
opts.write(true).create(true).truncate(true);
if it.peek() == Some(&b'+') {
it.next();
opts.read(true);
}
true
}
b'a' => {
opts.append(true).create(true);
if it.peek() == Some(&b'+') {
it.next();
opts.read(true);
}
true
}
_ => return None,
};
if it.peek() == Some(&b'b') {
it.next(); }
if it.next().is_some() {
return None; }
Some((opts, writable))
}
fn file_result_err(vm: &mut Vm, fs: u32, name: &str, e: &std::io::Error) -> u32 {
let msg = Value::Str(vm.heap.intern(format!("{name}: {e}").as_bytes()));
let code = Value::Int(e.raw_os_error().unwrap_or(-1) as i64);
vm.nat_return(fs, &[Value::Nil, msg, code])
}
fn str_arg(vm: &mut Vm, fs: u32, nargs: u32, i: u32, who: &str) -> Result<String, LuaError> {
match vm.nat_arg(fs, nargs, i) {
Value::Str(s) => Ok(String::from_utf8_lossy(s.as_bytes()).into_owned()),
v => Err(arg_error(
vm,
i + 1,
who,
&format!("string expected, got {}", v.type_name()),
)),
}
}
fn io_open(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let path = str_arg(vm, fs, nargs, 0, "open")?;
let mode: Vec<u8> = match vm.nat_arg(fs, nargs, 1) {
Value::Nil => b"r".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
v => {
return Err(arg_error(
vm,
2,
"open",
&format!("string expected, got {}", v.type_name()),
));
}
};
let Some((opts, writable)) = parse_mode(&mode) else {
let m = String::from_utf8_lossy(&mode);
return Err(arg_error(vm, 2, "open", &format!("invalid mode '{m}'")));
};
match opts.open(&path) {
Ok(file) => {
let u = new_file(vm, FileHandle::File(file), writable);
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
Err(e) => Ok(file_result_err(vm, fs, &path, &e)),
}
}
#[cfg(any(unix, windows))]
fn io_popen(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let prog = str_arg(vm, fs, nargs, 0, "popen")?;
let mode: Vec<u8> = match vm.nat_arg(fs, nargs, 1) {
Value::Nil => b"r".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
v => {
return Err(arg_error(
vm,
2,
"popen",
&format!("string expected, got {}", v.type_name()),
));
}
};
let read_mode = match mode.first().copied() {
Some(b'r') => true,
Some(b'w') => false,
_ => {
let m = String::from_utf8_lossy(&mode);
return Err(arg_error(vm, 2, "popen", &format!("invalid mode '{m}'")));
}
};
let mut cmd = if cfg!(windows) {
let mut c = std::process::Command::new("cmd");
c.arg("/C").arg(&prog);
c
} else {
let mut c = std::process::Command::new("sh");
c.arg("-c").arg(&prog);
c
};
if read_mode {
cmd.stdout(std::process::Stdio::piped());
} else {
cmd.stdin(std::process::Stdio::piped());
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => return Ok(file_result_err(vm, fs, &prog, &e)),
};
let file = if read_mode {
let pipe = child
.stdout
.take()
.expect("Stdio::piped() always populates stdout");
pipe_to_file(pipe)
} else {
let pipe = child
.stdin
.take()
.expect("Stdio::piped() always populates stdin");
pipe_to_file(pipe)
};
let u = new_file(vm, FileHandle::File(file), !read_mode);
unsafe { u.as_mut() }.popen_child = Some(child);
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
#[cfg(not(any(unix, windows)))]
fn io_popen(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let _prog = str_arg(vm, fs, nargs, 0, "popen")?;
match vm.nat_arg(fs, nargs, 1) {
Value::Nil | Value::Str(_) => {}
v => {
return Err(arg_error(
vm,
2,
"popen",
&format!("string expected, got {}", v.type_name()),
));
}
};
let msg = Value::Str(vm.heap.intern(b"popen not supported on this platform"));
Ok(vm.nat_return(fs, &[Value::Nil, msg, Value::Int(-1)]))
}
#[cfg(any(unix, windows))]
fn pipe_to_file<T>(pipe: T) -> std::fs::File
where
T: PipeAsFile,
{
pipe.into_file()
}
#[cfg(any(unix, windows))]
trait PipeAsFile {
fn into_file(self) -> std::fs::File;
}
#[cfg(unix)]
impl PipeAsFile for std::process::ChildStdout {
fn into_file(self) -> std::fs::File {
use std::os::unix::io::{FromRawFd, IntoRawFd};
unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
}
}
#[cfg(unix)]
impl PipeAsFile for std::process::ChildStdin {
fn into_file(self) -> std::fs::File {
use std::os::unix::io::{FromRawFd, IntoRawFd};
unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
}
}
#[cfg(windows)]
impl PipeAsFile for std::process::ChildStdout {
fn into_file(self) -> std::fs::File {
use std::os::windows::io::{FromRawHandle, IntoRawHandle};
unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
}
}
#[cfg(windows)]
impl PipeAsFile for std::process::ChildStdin {
fn into_file(self) -> std::fs::File {
use std::os::windows::io::{FromRawHandle, IntoRawHandle};
unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
}
}
fn gather_write(vm: &mut Vm, fs: u32, nargs: u32, start: u32) -> Result<Vec<u8>, LuaError> {
let mut out = Vec::new();
for i in start..nargs {
match vm.nat_arg(fs, nargs, i) {
Value::Str(s) => out.extend_from_slice(s.as_bytes()),
v @ (Value::Int(_) | Value::Float(_)) => out.extend(vm.tostring_basic(v)),
v => {
return Err(arg_error(
vm,
i + 1,
"write",
&format!("string expected, got {}", v.type_name()),
));
}
}
}
Ok(out)
}
fn write_to(u: Gc<Userdata>, bytes: &[u8]) -> std::io::Result<()> {
match unsafe { u.as_mut() }.file_mut() {
FileHandle::File(f) => f.write_all(bytes),
FileHandle::Stdout => std::io::stdout().write_all(bytes),
FileHandle::Stderr => std::io::stderr().write_all(bytes),
FileHandle::Stdin => Err(std::io::Error::other("cannot write to input file")),
FileHandle::Closed => Err(std::io::Error::other("closed file")),
}
}
fn drain_write_buf(u: Gc<Userdata>) -> std::io::Result<()> {
let buf = std::mem::take(&mut unsafe { u.as_mut() }.write_buf);
if buf.is_empty() {
return Ok(());
}
write_to(u, &buf)
}
fn f_write(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, fs, nargs, "write")?;
let bytes = gather_write(vm, fs, nargs, 1)?;
if matches!(u.file(), FileHandle::File(_)) && u.writable {
match u.buf_mode {
2 => match write_to(u, &bytes) {
Ok(()) => return Ok(vm.nat_return(fs, &[Value::Userdata(u)])),
Err(e) => return Ok(file_result_err(vm, fs, "file", &e)),
},
1 => {
unsafe { u.as_mut() }.write_buf.extend_from_slice(&bytes);
let last_nl = unsafe { u.as_mut() }
.write_buf
.iter()
.rposition(|&b| b == b'\n');
if let Some(last_nl) = last_nl {
let split = last_nl + 1;
let to_flush: Vec<u8> = unsafe {
let buf = &mut u.as_mut().write_buf;
buf.drain(..split).collect()
};
if let Err(e) = write_to(u, &to_flush) {
return Ok(file_result_err(vm, fs, "file", &e));
}
}
return Ok(vm.nat_return(fs, &[Value::Userdata(u)]));
}
_ => {
unsafe { u.as_mut() }.write_buf.extend_from_slice(&bytes);
return Ok(vm.nat_return(fs, &[Value::Userdata(u)]));
}
}
}
match write_to(u, &bytes) {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Userdata(u)])),
Err(e) => Ok(file_result_err(vm, fs, "file", &e)),
}
}
fn seek_handle(u: Gc<Userdata>, from: SeekFrom) -> std::io::Result<u64> {
drain_write_buf(u)?;
match unsafe { u.as_mut() }.file_mut() {
FileHandle::File(f) => f.seek(from),
_ => Err(std::io::Error::from_raw_os_error(29)), }
}
fn f_seek(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, fs, nargs, "seek")?;
let whence: Vec<u8> = match vm.nat_arg(fs, nargs, 1) {
Value::Nil => b"cur".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
v => {
return Err(arg_error(
vm,
2,
"seek",
&format!("string expected, got {}", v.type_name()),
));
}
};
let off = match vm.nat_arg(fs, nargs, 2) {
Value::Nil => 0,
Value::Int(i) => i,
Value::Float(f) => f as i64,
v => {
return Err(arg_error(
vm,
3,
"seek",
&format!("number expected, got {}", v.type_name()),
));
}
};
let from = match whence.as_slice() {
b"set" => SeekFrom::Start(off.max(0) as u64),
b"cur" => SeekFrom::Current(off),
b"end" => SeekFrom::End(off),
_ => return Err(arg_error(vm, 2, "seek", "invalid option")),
};
match seek_handle(u, from) {
Ok(pos) => Ok(vm.nat_return(fs, &[Value::Int(pos as i64)])),
Err(e) => Ok(file_result_err(vm, fs, "file", &e)),
}
}
fn f_setvbuf(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, fs, nargs, "setvbuf")?;
let mode = match vm.nat_arg(fs, nargs, 1) {
Value::Str(s) => s.as_bytes().to_vec(),
v => {
return Err(arg_error(
vm,
2,
"setvbuf",
&format!("string expected, got {}", v.type_name()),
));
}
};
let buf_mode: u8 = match mode.as_slice() {
b"full" => 0,
b"line" => 1,
b"no" => 2,
_ => return Err(arg_error(vm, 2, "setvbuf", "invalid option")),
};
unsafe { u.as_mut() }.buf_mode = buf_mode;
if buf_mode == 2 {
let _ = drain_write_buf(u);
}
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
fn f_flush(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, fs, nargs, "flush")?;
match flush_file(u) {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Userdata(u)])),
Err(e) => Ok(file_result_err(vm, fs, "file", &e)),
}
}
fn io_flush(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
let u = vm.io_output.expect("default output set at startup");
if u.file().is_closed() {
return Err(raise_str(vm, "default output file is closed"));
}
match flush_file(u) {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Userdata(u)])),
Err(e) => Ok(file_result_err(vm, fs, "file", &e)),
}
}
fn flush_file(u: Gc<Userdata>) -> std::io::Result<()> {
drain_write_buf(u)?;
match unsafe { u.as_mut() }.file_mut() {
FileHandle::File(f) => f.flush(),
FileHandle::Stdout => std::io::stdout().flush(),
FileHandle::Stderr => std::io::stderr().flush(),
_ => Ok(()),
}
}
fn read_byte(u: Gc<Userdata>) -> std::io::Result<Option<u8>> {
if let Some(b) = unsafe { u.as_mut() }.peeked.take() {
return Ok(Some(b));
}
let mut b = [0u8; 1];
let n = match unsafe { u.as_mut() }.file_mut() {
FileHandle::File(f) => f.read(&mut b)?,
FileHandle::Stdin => std::io::stdin().read(&mut b)?,
_ => return Ok(None),
};
Ok(if n == 0 { None } else { Some(b[0]) })
}
fn unread_byte(u: Gc<Userdata>, b: u8) {
unsafe { u.as_mut() }.peeked = Some(b);
}
const L_MAXLENNUM: usize = 200;
struct Rn {
buf: Vec<u8>,
c: Option<u8>,
}
fn rn_save(rn: &mut Rn, u: Gc<Userdata>) -> std::io::Result<bool> {
if rn.buf.len() >= L_MAXLENNUM {
rn.buf.clear();
return Ok(false);
}
if let Some(b) = rn.c {
rn.buf.push(b);
}
rn.c = read_byte(u)?;
Ok(true)
}
fn rn_test(rn: &mut Rn, u: Gc<Userdata>, set: &[u8]) -> std::io::Result<bool> {
if matches!(rn.c, Some(c) if set.contains(&c)) {
return rn_save(rn, u);
}
Ok(false)
}
fn rn_digits(rn: &mut Rn, u: Gc<Userdata>, hex: bool) -> std::io::Result<u32> {
let mut count = 0;
loop {
let is_digit = match rn.c {
Some(c) if hex => c.is_ascii_hexdigit(),
Some(c) => c.is_ascii_digit(),
None => false,
};
if !is_digit || !rn_save(rn, u)? {
break;
}
count += 1;
}
Ok(count)
}
fn read_numeral(u: Gc<Userdata>) -> std::io::Result<Vec<u8>> {
let mut c = read_byte(u)?;
while matches!(c, Some(b) if b.is_ascii_whitespace()) {
c = read_byte(u)?;
}
let mut rn = Rn { buf: Vec::new(), c };
let mut hex = false;
let mut count = 0u32;
rn_test(&mut rn, u, b"-+")?; if rn_test(&mut rn, u, b"0")? {
if rn_test(&mut rn, u, b"xX")? {
hex = true; } else {
count = 1; }
}
count += rn_digits(&mut rn, u, hex)?; if rn_test(&mut rn, u, b".")? {
count += rn_digits(&mut rn, u, hex)?; }
if count > 0 {
let exp: &[u8] = if hex { b"pP" } else { b"eE" };
if rn_test(&mut rn, u, exp)? {
rn_test(&mut rn, u, b"-+")?; rn_digits(&mut rn, u, false)?; }
}
if let Some(c) = rn.c {
unread_byte(u, c); }
Ok(rn.buf)
}
enum ReadFail {
Lua(LuaError),
Io(std::io::Error),
}
impl From<LuaError> for ReadFail {
fn from(e: LuaError) -> Self {
ReadFail::Lua(e)
}
}
impl From<std::io::Error> for ReadFail {
fn from(e: std::io::Error) -> Self {
ReadFail::Io(e)
}
}
fn read_format(vm: &mut Vm, u: Gc<Userdata>, fmt: Value) -> Result<Value, ReadFail> {
let n_opt: Option<i64> = match fmt {
Value::Int(n) => Some(n),
Value::Float(f)
if f.is_finite()
&& f.fract() == 0.0
&& f >= i64::MIN as f64
&& f <= i64::MAX as f64 =>
{
Some(f as i64)
}
_ => None,
};
if let Some(n) = n_opt {
let n = n.max(0) as usize;
if n == 0 {
return Ok(match read_byte(u)? {
Some(b) => {
unread_byte(u, b);
Value::Str(vm.heap.intern(b""))
}
None => Value::Nil,
});
}
let mut buf = Vec::with_capacity(n);
for _ in 0..n {
match read_byte(u)? {
Some(b) => buf.push(b),
None => break,
}
}
if buf.is_empty() && n > 0 {
return Ok(Value::Nil); }
return Ok(Value::Str(vm.heap.intern(&buf)));
}
let f = match fmt {
Value::Str(s) => {
let b = s.as_bytes();
if b.first() == Some(&b'*') {
b.get(1).copied()
} else {
b.first().copied()
}
}
Value::Nil => Some(b'l'),
_ => return Err(ReadFail::Lua(arg_error(vm, 1, "read", "invalid format"))),
};
match f {
Some(b'l') | Some(b'L') => {
let keep = f == Some(b'L');
let mut buf = Vec::new();
let mut got = false;
loop {
match read_byte(u)? {
Some(b'\n') => {
got = true;
if keep {
buf.push(b'\n');
}
break;
}
Some(c) => {
got = true;
buf.push(c);
}
None => break,
}
}
if !got && buf.is_empty() {
Ok(Value::Nil) } else {
Ok(Value::Str(vm.heap.intern(&buf)))
}
}
Some(b'a') => {
let mut buf = Vec::new();
while let Some(b) = read_byte(u)? {
buf.push(b);
}
Ok(Value::Str(vm.heap.intern(&buf))) }
Some(b'n') => {
let buf = read_numeral(u)?;
match crate::numeric::str2num(&buf, true, true) {
Some(crate::numeric::Num::Int(i)) => Ok(Value::Int(i)),
Some(crate::numeric::Num::Float(f)) => Ok(Value::Float(f)),
None => Ok(Value::Nil),
}
}
_ => Err(ReadFail::Lua(arg_error(vm, 1, "read", "invalid format"))),
}
}
fn f_read(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, fs, nargs, "read")?;
if let Err(e) = drain_write_buf(u) {
return Ok(file_result_err(vm, fs, "file", &e));
}
read_dispatch(vm, fs, nargs, u, 1)
}
fn read_dispatch(
vm: &mut Vm,
fs: u32,
nargs: u32,
u: Gc<Userdata>,
start: u32,
) -> Result<u32, LuaError> {
if nargs <= start {
let v = match read_format(vm, u, Value::Nil) {
Ok(v) => v,
Err(ReadFail::Lua(e)) => return Err(e),
Err(ReadFail::Io(e)) => return Ok(file_result_err(vm, fs, "file", &e)),
};
return Ok(vm.nat_return(fs, &[v]));
}
let mut out = Vec::new();
for i in start..nargs {
let fmt = vm.nat_arg(fs, nargs, i);
let v = match read_format(vm, u, fmt) {
Ok(v) => v,
Err(ReadFail::Lua(e)) => return Err(e),
Err(ReadFail::Io(e)) => return Ok(file_result_err(vm, fs, "file", &e)),
};
let stop = v.is_nil();
out.push(v);
if stop {
break; }
}
Ok(vm.nat_return(fs, &out))
}
fn io_write2(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = vm.io_output.expect("default output set at startup");
if u.file().is_closed() {
return Err(raise_str(vm, "default output file is closed"));
}
let bytes = gather_write(vm, fs, nargs, 0)?;
match write_to(u, &bytes) {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Userdata(u)])),
Err(e) => Ok(file_result_err(vm, fs, "file", &e)),
}
}
fn io_read2(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = vm.io_input.expect("default input set at startup");
if u.file().is_closed() {
return Err(raise_str(vm, "default input file is closed"));
}
read_dispatch(vm, fs, nargs, u, 0)
}
fn lines_iter(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
let Value::Userdata(u) = vm.nat_upval(fs, 0) else {
return Ok(vm.nat_return(fs, &[Value::Nil]));
};
if u.file().is_closed() {
return Err(raise_str(vm, "file is already closed"));
}
let nfmt = vm.nat_upcount(fs).saturating_sub(2);
let mut out = Vec::new();
let mut eof = false;
if nfmt == 0 {
let v = read_line_for_iter(vm, u, Value::Nil)?;
eof = v.is_nil();
out.push(v);
} else {
for i in 0..nfmt {
let fmt = vm.nat_upval(fs, 2 + i);
let v = read_line_for_iter(vm, u, fmt)?;
if i == 0 {
eof = v.is_nil();
}
out.push(v);
if v.is_nil() {
break; }
}
}
if eof {
if let Value::Bool(true) = vm.nat_upval(fs, 1)
&& !u.file().is_std()
{
*unsafe { u.as_mut() }.file_mut() = FileHandle::Closed;
}
}
Ok(vm.nat_return(fs, &out))
}
fn read_line_for_iter(vm: &mut Vm, u: Gc<Userdata>, fmt: Value) -> Result<Value, LuaError> {
match read_format(vm, u, fmt) {
Ok(v) => Ok(v),
Err(ReadFail::Lua(e)) => Err(e),
Err(ReadFail::Io(e)) => Err(raise_str(vm, &e.to_string())),
}
}
const MAXARGLINE: u32 = 250;
fn make_lines(vm: &mut Vm, u: Gc<Userdata>, owned: bool, formats: &[Value]) -> Value {
let mut upvals = Vec::with_capacity(2 + formats.len());
upvals.push(Value::Userdata(u));
upvals.push(Value::Bool(owned));
upvals.extend_from_slice(formats);
vm.native_with(lines_iter, upvals.into_boxed_slice())
}
fn gather_line_formats(
vm: &mut Vm,
fs: u32,
nargs: u32,
start: u32,
who: &str,
) -> Result<Vec<Value>, LuaError> {
if nargs.saturating_sub(start) > MAXARGLINE {
return Err(arg_error(vm, MAXARGLINE + 2, who, "too many arguments"));
}
Ok((start..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect())
}
fn f_lines(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, fs, nargs, "lines")?;
let formats = gather_line_formats(vm, fs, nargs, 1, "lines")?;
let it = make_lines(vm, u, false, &formats);
Ok(vm.nat_return(fs, &[it]))
}
fn io_lines(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let (u, owned) = if nargs >= 1 && !vm.nat_arg(fs, nargs, 0).is_nil() {
let path = str_arg(vm, fs, nargs, 0, "lines")?;
match std::fs::OpenOptions::new().read(true).open(&path) {
Ok(file) => (new_file(vm, FileHandle::File(file), false), true),
Err(e) => return Err(raise_str(vm, &format!("{path}: {e}"))),
}
} else {
(vm.io_input.expect("default input set at startup"), false)
};
let formats = gather_line_formats(vm, fs, nargs, 1, "lines")?;
let it = make_lines(vm, u, owned, &formats);
if owned && vm.version() >= crate::version::LuaVersion::Lua54 {
Ok(vm.nat_return(fs, &[it, Value::Nil, Value::Nil, Value::Userdata(u)]))
} else {
Ok(vm.nat_return(fs, &[it]))
}
}
fn io_tmpfile(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
use std::sync::atomic::{AtomicU64, Ordering};
static CTR: AtomicU64 = AtomicU64::new(0);
let n = CTR.fetch_add(1, Ordering::Relaxed);
let mut path = std::env::temp_dir();
path.push(format!("lua_tmp_{}_{n}", std::process::id()));
let file = match std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&path)
{
Ok(f) => f,
Err(e) => return Ok(file_result_err(vm, fs, "tmpfile", &e)),
};
let _ = std::fs::remove_file(&path);
let u = new_file(vm, FileHandle::File(file), true);
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
fn os_tmpname(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
use std::sync::atomic::{AtomicU64, Ordering};
static CTR: AtomicU64 = AtomicU64::new(0);
let n = CTR.fetch_add(1, Ordering::Relaxed);
let mut p = std::env::temp_dir();
p.push(format!("lua_{}_{n}", std::process::id()));
let s = Value::Str(vm.heap.intern(p.to_string_lossy().as_bytes()));
Ok(vm.nat_return(fs, &[s]))
}
fn os_remove(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let path = str_arg(vm, fs, nargs, 0, "remove")?;
match std::fs::remove_file(&path).or_else(|_| std::fs::remove_dir(&path)) {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Bool(true)])),
Err(e) => Ok(file_result_err(vm, fs, &path, &e)),
}
}
fn os_rename(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let from = str_arg(vm, fs, nargs, 0, "rename")?;
let to = str_arg(vm, fs, nargs, 1, "rename")?;
match std::fs::rename(&from, &to) {
Ok(()) => Ok(vm.nat_return(fs, &[Value::Bool(true)])),
Err(e) => Ok(file_result_err(vm, fs, &from, &e)),
}
}
#[cfg(any(unix, windows))]
fn os_execute(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
use crate::version::LuaVersion;
if nargs == 0 || matches!(vm.nat_arg(fs, nargs, 0), Value::Nil) {
return Ok(if vm.version() <= LuaVersion::Lua51 {
vm.nat_return(fs, &[Value::Int(1)])
} else {
vm.nat_return(fs, &[Value::Bool(true)])
});
}
let cmd = str_arg(vm, fs, nargs, 0, "execute")?;
let mut c = if cfg!(windows) {
let mut cmd0 = std::process::Command::new("cmd");
cmd0.arg("/C").arg(&cmd);
cmd0
} else {
let mut cmd0 = std::process::Command::new("sh");
cmd0.arg("-c").arg(&cmd);
cmd0
};
let status = match c.status() {
Ok(s) => s,
Err(e) => {
return Ok(file_result_err(vm, fs, &cmd, &e));
}
};
let (kind, code) = exit_status_breakdown(&status);
if vm.version() <= LuaVersion::Lua51 {
return Ok(vm.nat_return(fs, &[Value::Int(code as i64)]));
}
let kind_s = Value::Str(vm.heap.intern(kind.as_bytes()));
let ok = matches!(kind, "exit") && code == 0;
Ok(vm.nat_return(fs, &[Value::Bool(ok), kind_s, Value::Int(code as i64)]))
}
#[cfg(not(any(unix, windows)))]
fn os_execute(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
use crate::version::LuaVersion;
if nargs == 0 || matches!(vm.nat_arg(fs, nargs, 0), Value::Nil) {
return Ok(if vm.version() <= LuaVersion::Lua51 {
vm.nat_return(fs, &[Value::Int(0)])
} else {
vm.nat_return(fs, &[Value::Bool(false)])
});
}
let _cmd = str_arg(vm, fs, nargs, 0, "execute")?;
if vm.version() <= LuaVersion::Lua51 {
return Ok(vm.nat_return(fs, &[Value::Int(-1)]));
}
let kind = Value::Str(vm.heap.intern(b"exit"));
Ok(vm.nat_return(fs, &[Value::Bool(false), kind, Value::Int(-1)]))
}
#[cfg(any(unix, windows))]
fn exit_status_breakdown(status: &std::process::ExitStatus) -> (&'static str, i32) {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = status.signal() {
return ("signal", sig);
}
}
match status.code() {
Some(c) => ("exit", c),
None => ("signal", -1),
}
}
fn os_exit(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let code = match vm.nat_arg(fs, nargs, 0) {
Value::Nil => 0,
Value::Bool(true) => 0,
Value::Bool(false) => 1,
Value::Int(i) => i as i32,
Value::Float(f) => f as i32,
v => {
return Err(arg_error(
vm,
1,
"exit",
&format!("number expected, got {}", v.type_name()),
));
}
};
std::process::exit(code);
}
fn load_path(vm: &mut Vm, fs: u32, nargs: u32) -> Result<Result<Value, Value>, LuaError> {
let Value::Str(path) = vm.nat_arg(fs, nargs, 0) else {
return Err(arg_error(vm, 1, "loadfile", "string expected"));
};
let path_s = String::from_utf8_lossy(path.as_bytes()).into_owned();
let mode: Vec<u8> = match vm.nat_arg(fs, nargs, 1) {
Value::Nil => b"bt".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
_ => b"bt".to_vec(),
};
if mode.iter().any(|c| !matches!(c, b'b' | b't')) {
return Err(raise_str(
vm,
&format!("invalid mode '{}'", String::from_utf8_lossy(&mode)),
));
}
match std::fs::read(&path_s) {
Ok(src) => {
let mut chunkname = vec![b'@'];
chunkname.extend_from_slice(path.as_bytes());
let src = crate::frontend::lexer::Lexer::strip_shebang_bom(&src);
let src: &[u8] = match src {
[b'\n', rest @ ..] | [b'\r', b'\n', rest @ ..] | [b'\r', rest @ ..]
if rest.first() == Some(&0x1b) =>
{
rest
}
_ => src,
};
let binary = crate::vm::dump::is_binary_chunk(src);
if binary && !mode.contains(&b'b') || !binary && !mode.contains(&b't') {
let kind = if binary { "binary" } else { "text" };
let msg = format!(
"attempt to load a {kind} chunk (mode is '{}')",
String::from_utf8_lossy(&mode)
);
return Ok(Err(Value::Str(vm.heap.intern(msg.as_bytes()))));
}
match vm.load(src, &chunkname) {
Ok(cl) => Ok(Ok(Value::Closure(cl))),
Err(e) => {
let msg = format!("{path_s}:{e}");
Ok(Err(Value::Str(vm.heap.intern(msg.as_bytes()))))
}
}
}
Err(e) => {
let msg = format!("cannot open {path_s} ({e})");
Ok(Err(Value::Str(vm.heap.intern(msg.as_bytes()))))
}
}
}
fn nat_loadfile(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
match load_path(vm, fs, nargs)? {
Ok(Value::Closure(cl)) => {
if nargs >= 3 {
let env = vm.nat_arg(fs, nargs, 2);
let uv = vm.heap.new_upvalue(crate::runtime::UpvalState::Closed(env));
unsafe { cl.as_mut() }.upvals_mut()[0] = uv;
}
Ok(vm.nat_return(fs, &[Value::Closure(cl)]))
}
Ok(other) => Ok(vm.nat_return(fs, &[other])),
Err(msg) => Ok(vm.nat_return(fs, &[Value::Nil, msg])),
}
}
fn nat_dofile(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
match load_path(vm, fs, nargs)? {
Ok(f) => {
let results = vm.call_value(f, &[])?;
Ok(vm.nat_return(fs, &results))
}
Err(msg) => Err(raise_str(vm, &vm_text(vm, msg))),
}
}
fn vm_text(_vm: &Vm, v: Value) -> String {
match v {
Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
_ => format!("(error object is a {} value)", v.type_name()),
}
}
pub(crate) fn open_package(vm: &mut Vm) {
let pkg = vm.heap.new_table();
let loaded = vm.heap.new_table();
for name in [
"string",
"math",
"table",
"os",
"io",
"utf8",
"debug",
"coroutine",
"_G",
"package",
] {
let k = Value::Str(vm.heap.intern(name.as_bytes()));
let v = if name == "package" {
Value::Table(pkg)
} else if name == "_G" {
Value::Table(vm.globals())
} else {
let gk = Value::Str(vm.heap.intern(name.as_bytes()));
vm.globals().get(gk)
};
unsafe { loaded.as_mut() }
.set(&mut vm.heap, k, v)
.expect("valid key");
}
let lk = Value::Str(vm.heap.intern(b"loaded"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, lk, Value::Table(loaded))
.expect("valid key");
let preload = vm.heap.new_table();
let plk = Value::Str(vm.heap.intern(b"preload"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, plk, Value::Table(preload))
.expect("valid key");
let pk = Value::Str(vm.heap.intern(b"path"));
let pv = Value::Str(vm.heap.intern(b"./?.lua;./?/init.lua"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, pk, pv)
.expect("valid key");
let ck = Value::Str(vm.heap.intern(b"cpath"));
let cv = Value::Str(vm.heap.intern(b""));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, ck, cv)
.expect("valid key");
let cfk = Value::Str(vm.heap.intern(b"config"));
let cfv = Value::Str(vm.heap.intern(b"/\n;\n?\n!\n-\n"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, cfk, cfv)
.expect("valid key");
let searchers = vm.heap.new_table();
let sk = Value::Str(vm.heap.intern(b"searchers"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, sk, Value::Table(searchers))
.expect("valid key");
let sp = vm.native(nat_searchpath);
let spk = Value::Str(vm.heap.intern(b"searchpath"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, spk, sp)
.expect("valid key");
vm.set_global("package", Value::Table(pkg))
.expect("stdlib registration");
let req = vm.native_with(
nat_require,
Box::new([
Value::Table(pkg),
Value::Table(loaded),
Value::Table(preload),
]),
);
vm.set_global("require", req).expect("stdlib registration");
if vm.version() == crate::version::LuaVersion::Lua51 {
let m = vm.native_with(nat_module, Box::new([Value::Table(loaded)]));
vm.set_global("module", m).expect("stdlib registration");
let s = vm.native(nat_package_seeall);
let sk = Value::Str(vm.heap.intern(b"seeall"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, sk, s)
.expect("valid key");
}
let ll = vm.native(nat_loadlib_stub);
let llk = Value::Str(vm.heap.intern(b"loadlib"));
unsafe { pkg.as_mut() }
.set(&mut vm.heap, llk, ll)
.expect("valid key");
vm.barrier_back_table(pkg);
vm.barrier_back_table(loaded);
vm.barrier_back_table(preload);
vm.barrier_back_table(searchers);
}
fn nat_loadlib_stub(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
let msg = Value::Str(
vm.heap
.intern(b"dynamic libraries not enabled; check your Lua installation"),
);
let when = Value::Str(vm.heap.intern(b"absent"));
Ok(vm.nat_return(fs, &[Value::Nil, msg, when]))
}
fn nat_module(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let name_v = vm.nat_arg(fs, nargs, 0);
let name_bytes = match name_v {
Value::Str(s) => s.as_bytes().to_vec(),
v => {
return Err(arg_error(
vm,
1,
"module",
&format!("string expected, got {}", v.type_name()),
));
}
};
let name_str = String::from_utf8_lossy(&name_bytes).into_owned();
let loaded = if vm.nat_upcount(fs) >= 1 {
match vm.nat_upval(fs, 0) {
Value::Table(t) => t,
_ => return Err(raise_str(vm, "'package.loaded' upvalue missing")),
}
} else {
let pkg_k = Value::Str(vm.heap.intern(b"package"));
let Value::Table(pkg) = vm.globals().get(pkg_k) else {
return Err(raise_str(vm, "'package' table missing"));
};
let loaded_k = Value::Str(vm.heap.intern(b"loaded"));
let Value::Table(t) = pkg.get(loaded_k) else {
return Err(raise_str(vm, "'package.loaded' must be a table"));
};
t
};
let name_key = Value::Str(vm.heap.intern(&name_bytes));
let module_tab = match loaded.get(name_key) {
Value::Table(t) => t,
_ => {
let t = resolve_or_create_dotted(vm, &name_bytes)?;
unsafe { loaded.as_mut() }
.set(&mut vm.heap, name_key, Value::Table(t))
.expect("valid key");
t
}
};
let pre_dot = match name_bytes.iter().rposition(|&b| b == b'.') {
Some(i) => name_bytes[..=i].to_vec(),
None => Vec::new(),
};
let name_val = Value::Str(vm.heap.intern(&name_bytes));
let pkg_val = Value::Str(vm.heap.intern(&pre_dot));
let name_k = Value::Str(vm.heap.intern(b"_NAME"));
let m_k = Value::Str(vm.heap.intern(b"_M"));
let p_k = Value::Str(vm.heap.intern(b"_PACKAGE"));
unsafe { module_tab.as_mut() }
.set(&mut vm.heap, name_k, name_val)
.expect("valid key");
unsafe { module_tab.as_mut() }
.set(&mut vm.heap, m_k, Value::Table(module_tab))
.expect("valid key");
unsafe { module_tab.as_mut() }
.set(&mut vm.heap, p_k, pkg_val)
.expect("valid key");
for i in 1..nargs {
let f = vm.nat_arg(fs, nargs, i);
if !f.is_nil() {
vm.call_value(f, &[Value::Table(module_tab)])?;
}
}
if let Some(cl) = vm.lua_closure_at_level(1) {
let mut env_idx = None;
for (i, d) in cl.proto.upvals.iter().enumerate() {
if &*d.name == "_ENV" {
env_idx = Some(i);
break;
}
}
let Some(env_idx) = env_idx else {
return Err(raise_str(
vm,
&format!("module '{name_str}' caller has no '_ENV' upvalue"),
));
};
let uv = cl.upvals()[env_idx];
unsafe { uv.as_mut() }.set_closed(Value::Table(module_tab));
vm.barrier_forward_upvalue(uv, Value::Table(module_tab));
} else {
return Err(raise_str(
vm,
&format!("module '{name_str}' needs a Lua caller frame"),
));
}
Ok(vm.nat_return(fs, &[Value::Table(module_tab)]))
}
fn resolve_or_create_dotted(
vm: &mut Vm,
name: &[u8],
) -> Result<crate::runtime::Gc<crate::runtime::Table>, LuaError> {
let mut tab = vm.globals();
let mut start = 0;
let mut parts: Vec<&[u8]> = Vec::new();
for (i, &b) in name.iter().enumerate() {
if b == b'.' {
parts.push(&name[start..i]);
start = i + 1;
}
}
parts.push(&name[start..]);
for p in parts.iter() {
let k = Value::Str(vm.heap.intern(p));
let next = tab.get(k);
tab = match next {
Value::Table(t) => t,
Value::Nil => {
let t = vm.heap.new_table();
unsafe { tab.as_mut() }
.set(&mut vm.heap, k, Value::Table(t))
.expect("valid key");
t
}
_ => {
let s = String::from_utf8_lossy(name);
return Err(raise_str(vm, &format!("name conflict for module '{s}'")));
}
};
}
Ok(tab)
}
fn nat_package_seeall(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let m = vm.nat_arg(fs, nargs, 0);
let Value::Table(t) = m else {
return Err(arg_error(vm, 1, "seeall", "table expected"));
};
let mt = vm.heap.new_table();
let k = Value::Str(vm.heap.intern(b"__index"));
let g = Value::Table(vm.globals());
unsafe { mt.as_mut() }
.set(&mut vm.heap, k, g)
.expect("valid key");
unsafe { t.as_mut() }.set_metatable(Some(mt));
Ok(vm.nat_return(fs, &[]))
}
fn template_expand(tpl: &[u8], subst: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(tpl.len() + subst.len());
for &b in tpl {
if b == b'?' {
out.extend_from_slice(subst);
} else {
out.push(b);
}
}
out
}
fn replace_bytes(src: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
if from.is_empty() {
return src.to_vec();
}
let mut out = Vec::with_capacity(src.len());
let mut i = 0;
while i < src.len() {
if i + from.len() <= src.len() && &src[i..i + from.len()] == from {
out.extend_from_slice(to);
i += from.len();
} else {
out.push(src[i]);
i += 1;
}
}
out
}
fn nat_searchpath(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let Value::Str(name) = vm.nat_arg(fs, nargs, 0) else {
return Err(arg_error(vm, 1, "searchpath", "string expected"));
};
let Value::Str(path) = vm.nat_arg(fs, nargs, 1) else {
return Err(arg_error(vm, 2, "searchpath", "string expected"));
};
let sep: Vec<u8> = match vm.nat_arg(fs, nargs, 2) {
Value::Nil => b".".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
_ => return Err(arg_error(vm, 3, "searchpath", "string expected")),
};
let rep: Vec<u8> = match vm.nat_arg(fs, nargs, 3) {
Value::Nil => b"/".to_vec(),
Value::Str(s) => s.as_bytes().to_vec(),
_ => return Err(arg_error(vm, 4, "searchpath", "string expected")),
};
let name_bytes = name.as_bytes().to_vec();
let path_bytes = path.as_bytes().to_vec();
let translated = replace_bytes(&name_bytes, &sep, &rep);
let mut err = Vec::new();
for tpl in path_bytes.split(|&b| b == b';') {
if tpl.is_empty() {
continue;
}
let expanded = template_expand(tpl, &translated);
if std::fs::File::open(std::path::Path::new(
std::str::from_utf8(&expanded).unwrap_or(""),
))
.is_ok()
{
let v = Value::Str(vm.heap.intern(&expanded));
return Ok(vm.nat_return(fs, &[v]));
}
err.extend_from_slice(b"\n\tno file '");
err.extend_from_slice(&expanded);
err.push(b'\'');
}
let err_v = Value::Str(vm.heap.intern(&err));
Ok(vm.nat_return(fs, &[Value::Nil, err_v]))
}
fn nat_require(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let Value::Str(name) = vm.nat_arg(fs, nargs, 0) else {
return Err(arg_error(vm, 1, "require", "string expected"));
};
let key = Value::Str(name);
let name_s = String::from_utf8_lossy(name.as_bytes()).into_owned();
let (pkg, loaded) = if vm.nat_upcount(fs) >= 2 {
let p = match vm.nat_upval(fs, 0) {
Value::Table(t) => t,
_ => {
return Err(raise_str(vm, "'package' upvalue missing"));
}
};
let l = match vm.nat_upval(fs, 1) {
Value::Table(t) => t,
_ => {
return Err(raise_str(vm, "'package.loaded' upvalue missing"));
}
};
(p, l)
} else {
let pkg_k = Value::Str(vm.heap.intern(b"package"));
let Value::Table(p) = vm.globals().get(pkg_k) else {
return Err(raise_str(vm, "'package' table missing"));
};
let loaded_k = Value::Str(vm.heap.intern(b"loaded"));
let Value::Table(l) = p.get(loaded_k) else {
return Err(raise_str(vm, "'package.loaded' must be a table"));
};
(p, l)
};
let cached = loaded.get(key);
let already_loaded = if vm.version() <= crate::version::LuaVersion::Lua51 {
cached.truthy()
} else {
!cached.is_nil()
};
if already_loaded {
return Ok(vm.nat_return(fs, &[cached]));
}
let mut err = String::new();
let preload = if vm.nat_upcount(fs) >= 3 {
match vm.nat_upval(fs, 2) {
Value::Table(t) => t,
_ => return Err(raise_str(vm, "'package.preload' upvalue missing")),
}
} else {
let preload_k = Value::Str(vm.heap.intern(b"preload"));
let Value::Table(t) = pkg.get(preload_k) else {
return Err(raise_str(vm, "'package.preload' must be a table"));
};
t
};
let loader = preload.get(key);
if !loader.is_nil() {
let pv = Value::Str(vm.heap.intern(b":preload:"));
let args: &[Value] = if vm.version() <= crate::version::LuaVersion::Lua51 {
&[key]
} else {
&[key, pv]
};
let results = vm.call_value(loader, args)?;
let returned = results.first().copied().unwrap_or(Value::Nil);
let value = if !returned.is_nil() {
returned
} else {
let post = loaded.get(key);
if !post.is_nil() {
post
} else {
Value::Bool(true)
}
};
unsafe { loaded.as_mut() }
.set(&mut vm.heap, key, value)
.expect("valid key");
vm.barrier_back_table(loaded);
return Ok(vm.nat_return(fs, &[value, pv]));
}
err.push_str(&format!("\n\tno field package.preload['{name_s}']"));
let path_k = Value::Str(vm.heap.intern(b"path"));
let path_v = pkg.get(path_k);
let path_bytes = match path_v {
Value::Str(s) => s.as_bytes().to_vec(),
_ => return Err(raise_str(vm, "'package.path' must be a string")),
};
let translated_name = replace_bytes(name.as_bytes(), b".", b"/");
let mut found: Option<(Vec<u8>, Vec<u8>)> = None;
for tpl in path_bytes.split(|&b| b == b';') {
if tpl.is_empty() {
continue;
}
let expanded = template_expand(tpl, &translated_name);
if found.is_none()
&& let Ok(src) = std::fs::read(std::str::from_utf8(&expanded).unwrap_or(""))
{
found = Some((expanded.clone(), src));
}
err.push_str("\n\tno file '");
err.push_str(&String::from_utf8_lossy(&expanded));
err.push('\'');
}
let cpath_k = Value::Str(vm.heap.intern(b"cpath"));
let cpath_v = pkg.get(cpath_k);
let cpath_bytes = match cpath_v {
Value::Str(s) => s.as_bytes().to_vec(),
Value::Nil => Vec::new(),
_ => return Err(raise_str(vm, "'package.cpath' must be a string")),
};
for tpl in cpath_bytes.split(|&b| b == b';') {
if tpl.is_empty() {
continue;
}
let expanded = template_expand(tpl, &translated_name);
err.push_str("\n\tno file '");
err.push_str(&String::from_utf8_lossy(&expanded));
err.push('\'');
}
if let Some((path_b, src)) = found {
let path_s = String::from_utf8_lossy(&path_b).into_owned();
let chunkname = format!("@{path_s}");
let src = crate::frontend::lexer::Lexer::strip_shebang_bom(&src);
let cl = match vm.load(src, chunkname.as_bytes()) {
Ok(cl) => cl,
Err(e) => {
return Err(raise_str(
vm,
&format!("error loading module '{name_s}' from file '{path_s}':\n\t{e}"),
));
}
};
let pv = Value::Str(vm.heap.intern(path_s.as_bytes()));
let results = vm.call_value(Value::Closure(cl), &[key, pv])?;
let value = results.first().copied().unwrap_or(Value::Nil);
let value = if value.is_nil() {
Value::Bool(true)
} else {
value
};
let post = loaded.get(key);
let final_v = if !post.is_nil() { post } else { value };
unsafe { loaded.as_mut() }
.set(&mut vm.heap, key, final_v)
.expect("valid key");
vm.barrier_back_table(loaded);
return Ok(vm.nat_return(fs, &[final_v, pv]));
}
Err(raise_str(vm, &format!("module '{name_s}' not found:{err}")))
}