use std::io::{Read, Seek, SeekFrom, Write};
use crate::numeric::{self, FloatFmt, Num};
use crate::runtime::value::f2i_exact;
use crate::runtime::{FileHandle, Gc, Table, Userdata, UserdataPayload, Value};
use crate::version::LuaVersion;
use crate::vm::argcheck::{self, Args};
use crate::vm::builtins::{arg_error, raise_str};
use crate::vm::error::LuaError;
use crate::vm::exec::Vm;
const EINVAL: i32 = 22;
const ENOMEM: i32 = 12;
const EBADF: i32 = 9;
#[cfg(not(unix))]
const ESPIPE: i32 = 29;
const LUAL_BUFFERSIZE: i64 = 1024;
const READ_CHUNK: usize = 4096;
pub(crate) fn open_io(vm: &mut Vm) {
let v = vm.version();
let io = vm.heap.new_table();
for (name, f) in [
("close", io_close as crate::runtime::value::NativeFn),
("flush", io_flush),
("input", io_input),
("lines", io_lines),
("open", io_open),
("output", io_output),
("popen", io_popen),
("read", io_read),
("tmpfile", io_tmpfile),
("type", io_type),
("write", io_write),
] {
put_native(vm, io, name, f);
}
let close: crate::runtime::value::NativeFn = match v {
LuaVersion::Lua51 => f_close_51,
LuaVersion::Lua52 => io_close,
_ => f_close,
};
let methods: [(&str, crate::runtime::value::NativeFn); 7] = [
("close", close),
("flush", f_flush),
("lines", f_lines),
("read", f_read),
("seek", f_seek),
("setvbuf", f_setvbuf),
("write", f_write),
];
let mt = vm.heap.new_table();
let index = if v >= LuaVersion::Lua54 {
vm.heap.new_table()
} else {
mt
};
for (name, f) in methods {
put_native(vm, index, name, f);
}
put(vm, mt, "__index", Value::Table(index));
put_native(vm, mt, "__gc", f_gc);
put_native(vm, mt, "__tostring", f_tostring);
if v >= LuaVersion::Lua53 {
let n = Value::Str(vm.heap.intern(b"FILE*"));
put(vm, mt, "__name", n);
}
if v >= LuaVersion::Lua54 {
put_native(vm, mt, "__close", f_gc);
}
vm.barrier_back_table(index);
vm.barrier_back_table(mt);
vm.file_mt = Some(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);
put(vm, io, name, Value::Userdata(h));
match name {
"stdin" => {
vm.io_input = Some(h);
vm.io_stdin = Some(h);
}
"stdout" => vm.io_output = Some(h),
_ => {}
}
}
vm.set_global("io", Value::Table(io))
.expect("stdlib registration");
vm.barrier_back_table(io);
}
impl Vm {
pub fn read_stdin_line(&mut self, size: usize) -> std::io::Result<Option<Vec<u8>>> {
let mut line = Vec::new();
if let Some(u) = self.io_stdin {
while line.len() + 1 < size {
let Some(b) = getc(u)? else { break };
line.push(b);
if b == b'\n' {
break;
}
}
} else {
use std::io::BufRead;
let mut input = std::io::stdin().lock();
while line.len() + 1 < size && line.last() != Some(&b'\n') {
let buf = input.fill_buf()?;
if buf.is_empty() {
break;
}
let room = &buf[..buf.len().min(size - 1 - line.len())];
let n = room
.iter()
.position(|&b| b == b'\n')
.map_or(room.len(), |i| i + 1);
line.extend_from_slice(&room[..n]);
input.consume(n);
}
}
Ok((!line.is_empty()).then_some(line))
}
}
fn put(vm: &mut Vm, t: Gc<Table>, k: &str, v: Value) {
let k = Value::Str(vm.heap.intern(k.as_bytes()));
unsafe { t.as_mut() }
.set(&mut vm.heap, k, v)
.expect("valid key");
}
fn put_native(vm: &mut Vm, t: Gc<Table>, k: &str, f: crate::runtime::value::NativeFn) {
let fv = vm.native(f);
put(vm, t, k, fv);
}
fn posix_error(code: i32) -> std::io::Error {
#[cfg(windows)]
{
std::io::Error::other(PosixErrno(code))
}
#[cfg(not(windows))]
{
std::io::Error::from_raw_os_error(code)
}
}
#[cfg(windows)]
#[derive(Debug)]
struct PosixErrno(i32);
#[cfg(windows)]
impl std::fmt::Display for PosixErrno {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self.0 {
EBADF => "Bad file descriptor",
ENOMEM => "Not enough space",
EINVAL => "Invalid argument",
ESPIPE => "Invalid seek",
_ => "Unknown error",
})
}
}
#[cfg(windows)]
impl std::error::Error for PosixErrno {}
fn errno(e: &std::io::Error) -> Option<i32> {
#[cfg(windows)]
if let Some(p) = e.get_ref().and_then(|r| r.downcast_ref::<PosixErrno>()) {
return Some(p.0);
}
e.raw_os_error()
}
pub(crate) fn strerror(e: &std::io::Error) -> String {
let s = e.to_string();
match e.raw_os_error() {
Some(code) => s
.strip_suffix(&format!(" (os error {code})"))
.expect("std renders OS errors with an '(os error N)' suffix")
.to_string(),
None => s,
}
}
pub(crate) fn file_fail(vm: &mut Vm, fs: u32, fname: Option<&[u8]>, e: &std::io::Error) -> u32 {
let vals = file_fail_values(vm, fname, e);
vm.nat_return(fs, &vals)
}
fn file_fail_values(vm: &mut Vm, fname: Option<&[u8]>, e: &std::io::Error) -> [Value; 3] {
let mut msg = Vec::new();
if let Some(n) = fname {
msg.extend_from_slice(c_str(n));
msg.extend_from_slice(b": ");
}
msg.extend_from_slice(strerror(e).as_bytes());
let code = errno(e).map_or(0, i64::from);
let m = Value::Str(vm.heap.intern(&msg));
[Value::Nil, m, Value::Int(code)]
}
fn file_ok(vm: &mut Vm, fs: u32) -> u32 {
vm.nat_return(fs, &[Value::Bool(true)])
}
#[cfg(any(unix, windows))]
pub(crate) fn exec_result(
vm: &mut Vm,
fs: u32,
status: std::io::Result<std::process::ExitStatus>,
) -> u32 {
let status = match status {
Ok(s) => s,
Err(e) => return file_fail(vm, fs, None, &e),
};
let (what, code) = exit_status_breakdown(&status);
let ok = if what == "exit" && code == 0 {
Value::Bool(true)
} else {
Value::Nil
};
let w = Value::Str(vm.heap.intern(what.as_bytes()));
vm.nat_return(fs, &[ok, w, Value::Int(code as i64)])
}
#[cfg(any(unix, windows))]
pub(crate) 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);
}
}
(
"exit",
status
.code()
.expect("a status that is not a signal carries a code"),
)
}
pub(crate) fn c_str(b: &[u8]) -> &[u8] {
b.split(|&c| c == 0)
.next()
.expect("split yields a first piece")
}
pub(crate) fn os_path(b: &[u8]) -> std::path::PathBuf {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
std::ffi::OsStr::from_bytes(c_str(b)).into()
}
#[cfg(not(unix))]
{
String::from_utf8_lossy(c_str(b)).into_owned().into()
}
}
fn new_file(vm: &mut Vm, fh: FileHandle, writable: bool) -> Gc<Userdata> {
let u = vm.heap.new_userdata(UserdataPayload::File(fh), writable);
unsafe { u.as_mut() }.set_metatable(vm.file_mt);
vm.heap.register_finalizable_userdata(u);
u
}
fn test_file(vm: &Vm, v: Value) -> Option<Gc<Userdata>> {
match v {
Value::Userdata(u)
if matches!(u.payload, UserdataPayload::File(_))
&& u.metatable()
.zip(vm.file_mt)
.is_some_and(|(a, b)| a.ptr_eq(b)) =>
{
Some(u)
}
_ => None,
}
}
fn check_stream(vm: &mut Vm, a: Args, i: u32) -> Result<Gc<Userdata>, LuaError> {
match test_file(vm, a.get(vm, i)) {
Some(u) if !a.is_none(i) => Ok(u),
_ => Err(argcheck::type_error(vm, a, i, "FILE*")),
}
}
fn check_open(vm: &mut Vm, a: Args, i: u32) -> Result<Gc<Userdata>, LuaError> {
let u = check_stream(vm, a, i)?;
if u.file().is_closed() {
return Err(raise_str(vm, "attempt to use a closed file"));
}
Ok(u)
}
pub(crate) fn flush_all(vm: &mut Vm) {
for u in vm.heap.finalizable_userdata() {
if matches!(u.payload, UserdataPayload::File(ref fh) if !fh.is_closed()) {
let _ = drain_write_buf(u);
}
}
let _ = std::io::stdout().flush(); }
enum Closed {
Std,
File(std::io::Result<()>),
#[cfg(any(unix, windows))]
Pipe(std::io::Result<std::process::ExitStatus>),
}
fn close_stream(u: Gc<Userdata>) -> Closed {
if u.file().is_std() {
return Closed::Std;
}
let flushed = drain_write_buf(u);
let m = unsafe { u.as_mut() };
*m.file_mut() = FileHandle::Closed;
m.read_buf = Vec::new();
m.read_pos = 0;
#[cfg(any(unix, windows))]
if let Some(mut child) = m.popen_child.take() {
return Closed::Pipe(child.wait());
}
Closed::File(flushed)
}
fn push_closed(vm: &mut Vm, fs: u32, c: Closed) -> u32 {
match c {
Closed::Std => {
let m = Value::Str(vm.heap.intern(b"cannot close standard file"));
vm.nat_return(fs, &[Value::Nil, m])
}
Closed::File(Ok(())) => file_ok(vm, fs),
Closed::File(Err(e)) => file_fail(vm, fs, None, &e),
#[cfg(any(unix, windows))]
Closed::Pipe(r) if vm.version() == LuaVersion::Lua51 => match r {
Ok(_) => file_ok(vm, fs),
Err(e) => file_fail(vm, fs, None, &e),
},
#[cfg(any(unix, windows))]
Closed::Pipe(r) => exec_result(vm, fs, r),
}
}
fn io_close(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = if nargs == 0 {
let d = default_file(vm, Io::Output);
if d.file().is_closed() {
return Err(raise_str(vm, "attempt to use a closed file"));
}
d
} else {
check_open(vm, Args::new(fs, nargs), 0)?
};
let c = close_stream(u);
Ok(push_closed(vm, fs, c))
}
fn f_close_51(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
if nargs == 0 {
return Err(arg_error(vm, 1, "FILE* expected, got nil"));
}
f_close(vm, fs, nargs)
}
fn f_close(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, Args::new(fs, nargs), 0)?;
let c = close_stream(u);
Ok(push_closed(vm, fs, c))
}
fn f_gc(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_stream(vm, Args::new(fs, nargs), 0)?;
if !u.file().is_closed() {
let _ = close_stream(u); }
Ok(vm.nat_return(fs, &[]))
}
fn f_tostring(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_stream(vm, Args::new(fs, nargs), 0)?;
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 io_type(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let v = argcheck::check_any(vm, Args::new(fs, nargs), 0)?;
let r = match test_file(vm, v) {
None => Value::Nil,
Some(u) if u.file().is_closed() => Value::Str(vm.heap.intern(b"closed file")),
Some(_) => Value::Str(vm.heap.intern(b"file")),
};
Ok(vm.nat_return(fs, &[r]))
}
fn fopen_options(mode: &[u8]) -> Option<(std::fs::OpenOptions, bool)> {
let mut o = std::fs::OpenOptions::new();
let first = *mode.first()?;
let rest = &mode[1..];
let plus = rest.contains(&b'+');
let excl = rest.contains(&b'x');
let writable = match first {
b'r' => {
o.read(true).write(plus);
plus
}
b'w' => {
o.write(true).read(plus).truncate(true);
if excl {
o.create_new(true);
} else {
o.create(true);
}
true
}
b'a' => {
o.append(true).read(plus);
if excl {
o.create_new(true);
} else {
o.create(true);
}
true
}
_ => return None,
};
Some((o, writable))
}
fn mode_ok(v: LuaVersion, mode: &[u8]) -> bool {
let Some((&first, rest)) = mode.split_first() else {
return false;
};
if !b"rwa".contains(&first) {
return false;
}
let rest = rest.strip_prefix(b"+").unwrap_or(rest);
match v {
LuaVersion::Lua52 => rest.is_empty() || rest == b"b",
_ => rest.iter().all(|&c| c == b'b'),
}
}
fn open_file(name: &[u8], mode: &[u8]) -> std::io::Result<(std::fs::File, bool)> {
let (o, writable) = fopen_options(c_str(mode)).ok_or_else(|| posix_error(EINVAL))?;
Ok((o.open(os_path(name))?, writable))
}
fn io_open(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
let name = argcheck::check_string(vm, a, 0)?.as_bytes().to_vec();
let mode = match argcheck::opt_string(vm, a, 1)? {
Some(m) => m.as_bytes().to_vec(),
None => b"r".to_vec(),
};
if vm.version() >= LuaVersion::Lua52 && !mode_ok(vm.version(), &mode) {
return Err(arg_error(vm, 2, "invalid mode"));
}
match open_file(&name, &mode) {
Ok((f, writable)) => {
let u = new_file(vm, FileHandle::File(f), writable);
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
Err(e) => Ok(file_fail(vm, fs, Some(&name), &e)),
}
}
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_fail(vm, fs, None, &e)),
};
if let Err(e) = std::fs::remove_file(&path) {
return Ok(file_fail(vm, fs, None, &e));
}
let u = new_file(vm, FileHandle::File(file), true);
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
#[cfg(any(unix, windows))]
fn io_popen(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
let prog = argcheck::check_string(vm, a, 0)?.as_bytes().to_vec();
let mode = match argcheck::opt_string(vm, a, 1)? {
Some(m) => m.as_bytes().to_vec(),
None => b"r".to_vec(),
};
let read = match c_str(&mode) {
b"r" => true,
b"w" => false,
_ if vm.version() >= LuaVersion::Lua53 => return Err(arg_error(vm, 2, "invalid mode")),
_ => {
let e = posix_error(EINVAL);
return Ok(file_fail(vm, fs, Some(&prog), &e));
}
};
flush_all(vm);
let mut cmd = shell_command(&prog);
if read {
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_fail(vm, fs, Some(&prog), &e)),
};
let file = if read {
pipe_file(child.stdout.take().expect("stdout was piped"))
} else {
pipe_file(child.stdin.take().expect("stdin was piped"))
};
let u = new_file(vm, FileHandle::File(file), !read);
unsafe { u.as_mut() }.popen_child = Some(child);
Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
}
#[cfg(unix)]
fn pipe_file(p: impl Into<std::os::fd::OwnedFd>) -> std::fs::File {
std::fs::File::from(p.into())
}
#[cfg(windows)]
fn pipe_file(p: impl Into<std::os::windows::io::OwnedHandle>) -> std::fs::File {
std::fs::File::from(p.into())
}
#[cfg(not(any(unix, windows)))]
fn io_popen(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
argcheck::check_string(vm, a, 0)?;
argcheck::opt_string(vm, a, 1)?;
Err(raise_str(vm, "'popen' not supported"))
}
#[cfg(any(unix, windows))]
pub(crate) fn shell_command(cmd: &[u8]) -> std::process::Command {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let mut c = std::process::Command::new("/bin/sh");
c.arg("-c").arg(std::ffi::OsStr::from_bytes(c_str(cmd)));
c
}
#[cfg(windows)]
{
let mut c = std::process::Command::new("cmd");
c.arg("/C")
.arg(String::from_utf8_lossy(c_str(cmd)).into_owned());
c
}
}
#[derive(Clone, Copy)]
enum Io {
Input,
Output,
}
fn default_file(vm: &Vm, which: Io) -> Gc<Userdata> {
match which {
Io::Input => vm.io_input,
Io::Output => vm.io_output,
}
.expect("default streams are set when io opens")
}
fn get_io_file(vm: &mut Vm, which: Io) -> Result<Gc<Userdata>, LuaError> {
let u = default_file(vm, which);
if u.file().is_closed() {
let what = match which {
Io::Input => "input",
Io::Output => "output",
};
let adj = if vm.version() >= LuaVersion::Lua54 {
"default"
} else {
"standard"
};
return Err(raise_str(vm, &format!("{adj} {what} file is closed")));
}
Ok(u)
}
fn g_iofile(vm: &mut Vm, fs: u32, nargs: u32, which: Io) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
if !a.is_none_or_nil(vm, 0) {
let v = a.get(vm, 0);
let u = match argcheck::to_str_bytes(vm, v) {
Some(name) => {
let mode: &[u8] = match which {
Io::Input => b"r",
Io::Output => b"w",
};
open_checked(vm, &name, mode)?
}
None => check_open(vm, a, 0)?,
};
match which {
Io::Input => vm.io_input = Some(u),
Io::Output => vm.io_output = Some(u),
}
}
let cur = default_file(vm, which);
Ok(vm.nat_return(fs, &[Value::Userdata(cur)]))
}
fn open_checked(vm: &mut Vm, name: &[u8], mode: &[u8]) -> Result<Gc<Userdata>, LuaError> {
match open_file(name, mode) {
Ok((f, writable)) => Ok(new_file(vm, FileHandle::File(f), writable)),
Err(e) => {
let n = String::from_utf8_lossy(c_str(name)).into_owned();
let err = strerror(&e);
Err(if vm.version() == LuaVersion::Lua51 {
arg_error(vm, 1, &format!("{n}: {err}"))
} else {
raise_str(vm, &format!("cannot open file '{n}' ({err})"))
})
}
}
}
fn io_input(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
g_iofile(vm, fs, nargs, Io::Input)
}
fn io_output(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
g_iofile(vm, fs, nargs, Io::Output)
}
fn fill(u: Gc<Userdata>) -> std::io::Result<bool> {
let m = unsafe { u.as_mut() };
let mut chunk = vec![0u8; READ_CHUNK];
let n = match m.file_mut() {
FileHandle::File(f) => f.read(&mut chunk)?,
FileHandle::Stdin => std::io::stdin().read(&mut chunk)?,
FileHandle::Stdout | FileHandle::Stderr => {
return Err(posix_error(EBADF));
}
FileHandle::Closed => unreachable!("reads check the stream is open"),
};
chunk.truncate(n);
m.read_buf = chunk;
m.read_pos = 0;
Ok(n > 0)
}
fn getc(u: Gc<Userdata>) -> std::io::Result<Option<u8>> {
if u.read_pos >= u.read_buf.len() && !fill(u)? {
return Ok(None);
}
let m = unsafe { u.as_mut() };
let b = m.read_buf[m.read_pos];
m.read_pos += 1;
Ok(Some(b))
}
fn unget(u: Gc<Userdata>, bytes: &[u8]) {
let m = unsafe { u.as_mut() };
if m.read_pos >= bytes.len() && m.read_buf[m.read_pos - bytes.len()..m.read_pos] == *bytes {
m.read_pos -= bytes.len();
return;
}
let mut buf = bytes.to_vec();
buf.extend_from_slice(&m.read_buf[m.read_pos..]);
m.read_buf = buf;
m.read_pos = 0;
}
fn read_ahead(u: Gc<Userdata>) -> i64 {
(u.read_buf.len() - u.read_pos) as i64
}
fn unread_ahead(u: Gc<Userdata>) -> std::io::Result<()> {
let ahead = read_ahead(u);
let m = unsafe { u.as_mut() };
if ahead > 0
&& let FileHandle::File(f) = m.file_mut()
{
f.seek(SeekFrom::Current(-ahead))?;
}
m.read_buf = Vec::new();
m.read_pos = 0;
Ok(())
}
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(posix_error(EBADF)),
FileHandle::Closed => unreachable!("writes check the stream is open"),
}
}
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 put_bytes(u: Gc<Userdata>, bytes: &[u8]) -> std::io::Result<()> {
if !matches!(u.file(), FileHandle::File(_)) || !u.writable {
return write_to(u, bytes);
}
unread_ahead(u)?;
let m = unsafe { u.as_mut() };
match m.buf_mode {
BUF_NO => write_to(u, bytes),
BUF_LINE => {
m.write_buf.extend_from_slice(bytes);
match m.write_buf.iter().rposition(|&b| b == b'\n') {
Some(nl) => {
let out: Vec<u8> = m.write_buf.drain(..=nl).collect();
write_to(u, &out)
}
None => Ok(()),
}
}
_ => {
m.write_buf.extend_from_slice(bytes);
Ok(())
}
}
}
const BUF_FULL: u8 = 0;
const BUF_LINE: u8 = 1;
const BUF_NO: u8 = 2;
fn flush_stream(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(),
FileHandle::Stdin | FileHandle::Closed => Ok(()),
}
}
fn number_text(vm: &Vm, n: Num) -> Vec<u8> {
let fmt = match vm.version() {
LuaVersion::Lua55 => vm.float_fmt(),
_ => FloatFmt::Legacy14,
};
numeric::num_to_string_for(n, fmt).into_bytes()
}
fn g_write(vm: &mut Vm, fs: u32, u: Gc<Userdata>, args: Args, first: u32) -> Result<u32, LuaError> {
let v = vm.version();
let mut total: i64 = 0;
let mut failure: Option<std::io::Error> = None;
for i in first..args.n {
let bytes = match args.get(vm, i) {
Value::Int(x) => number_text(vm, Num::Int(x)),
Value::Float(f) => number_text(vm, Num::Float(f)),
_ => argcheck::check_string(vm, args, i)?.as_bytes().to_vec(),
};
if failure.is_some() {
continue;
}
match put_bytes(u, &bytes) {
Ok(()) => total += bytes.len() as i64,
Err(e) if v >= LuaVersion::Lua55 => {
let mut vals = file_fail_values(vm, None, &e).to_vec();
vals.push(Value::Int(total));
return Ok(vm.nat_return(fs, &vals));
}
Err(e) => failure = Some(e),
}
}
Ok(match failure {
Some(e) => file_fail(vm, fs, None, &e),
None if v == LuaVersion::Lua51 => file_ok(vm, fs),
None => vm.nat_return(fs, &[Value::Userdata(u)]),
})
}
fn io_write(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = get_io_file(vm, Io::Output)?;
g_write(vm, fs, u, Args::new(fs, nargs), 0)
}
fn f_write(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
let u = check_open(vm, a, 0)?;
g_write(vm, fs, u, a, 1)
}
fn io_flush(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
let u = get_io_file(vm, Io::Output)?;
Ok(match flush_stream(u) {
Ok(()) => file_ok(vm, fs),
Err(e) => file_fail(vm, fs, None, &e),
})
}
fn f_flush(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, Args::new(fs, nargs), 0)?;
Ok(match flush_stream(u) {
Ok(()) => file_ok(vm, fs),
Err(e) => file_fail(vm, fs, None, &e),
})
}
fn f_seek(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
let u = check_open(vm, a, 0)?;
let op = argcheck::check_option(vm, a, 1, Some("cur"), &["set", "cur", "end"])?;
let offset = if vm.version() == LuaVersion::Lua52 {
let p3 = argcheck::opt_number(vm, a, 2, 0.0)?;
let off = p3 as i64;
if off as f64 != p3 {
return Err(arg_error(vm, 3, "not an integer in proper range"));
}
off
} else {
argcheck::opt_integer(vm, a, 2, 0)?
};
match seek_stream(u, op, offset) {
Ok(pos) if vm.version() <= LuaVersion::Lua52 => {
Ok(vm.nat_return(fs, &[Value::Float(pos as f64)]))
}
Ok(pos) => Ok(vm.nat_return(fs, &[Value::Int(pos as i64)])),
Err(e) => Ok(file_fail(vm, fs, None, &e)),
}
}
fn seek_stream(u: Gc<Userdata>, op: usize, offset: i64) -> std::io::Result<u64> {
drain_write_buf(u)?;
let ahead = read_ahead(u);
let m = unsafe { u.as_mut() };
let from = match op {
0 if offset < 0 => return Err(posix_error(EINVAL)),
0 => SeekFrom::Start(offset as u64),
1 => match offset.checked_sub(ahead) {
Some(off) => SeekFrom::Current(off),
None => return Err(posix_error(EINVAL)),
},
_ => SeekFrom::End(offset),
};
let pos = match m.file_mut() {
FileHandle::File(f) => f.seek(from)?,
std_stream => seek_std(std_stream, from)?,
};
m.read_buf = Vec::new();
m.read_pos = 0;
Ok(pos)
}
#[cfg(unix)]
fn seek_std(fh: &FileHandle, from: SeekFrom) -> std::io::Result<u64> {
use std::os::fd::AsFd;
let fd = match fh {
FileHandle::Stdin => std::io::stdin().as_fd().try_clone_to_owned()?,
FileHandle::Stdout => std::io::stdout().as_fd().try_clone_to_owned()?,
FileHandle::Stderr => std::io::stderr().as_fd().try_clone_to_owned()?,
FileHandle::File(_) | FileHandle::Closed => unreachable!("only standard streams"),
};
std::fs::File::from(fd).seek(from)
}
#[cfg(not(unix))]
fn seek_std(_fh: &FileHandle, _from: SeekFrom) -> std::io::Result<u64> {
Err(posix_error(ESPIPE))
}
fn f_setvbuf(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
let u = check_open(vm, a, 0)?;
let op = argcheck::check_option(vm, a, 1, None, &["no", "full", "line"])?;
argcheck::opt_integer(vm, a, 2, LUAL_BUFFERSIZE)?;
let mode = [BUF_NO, BUF_FULL, BUF_LINE][op];
unsafe { u.as_mut() }.buf_mode = mode;
if mode == BUF_NO
&& let Err(e) = drain_write_buf(u)
{
return Ok(file_fail(vm, fs, None, &e));
}
Ok(file_ok(vm, fs))
}
enum ReadOut {
Values(Vec<Value>),
Error(std::io::Error),
}
enum Fmt {
Count(i64),
Number,
Line { keep_nl: bool },
All,
}
fn parse_format(vm: &mut Vm, fmt: Value, argno: u32) -> Result<Fmt, LuaError> {
let v = vm.version();
match fmt {
Value::Int(n) => return Ok(Fmt::Count(n)),
Value::Float(f) if v <= LuaVersion::Lua52 => return Ok(Fmt::Count(f as i64)),
Value::Float(f) => {
return f2i_exact(f)
.map(Fmt::Count)
.ok_or_else(|| arg_error(vm, argno, "number has no integer representation"));
}
_ => {}
}
let spec = match fmt {
Value::Str(s) => s.as_bytes().to_vec(),
_ if v <= LuaVersion::Lua52 => return Err(arg_error(vm, argno, "invalid option")),
_ => {
let tn = argcheck::typename_of(vm, fmt);
return Err(arg_error(vm, argno, &format!("string expected, got {tn}")));
}
};
let body = match spec.strip_prefix(b"*") {
Some(b) => b,
None if v <= LuaVersion::Lua52 => return Err(arg_error(vm, argno, "invalid option")),
None => &spec,
};
Ok(match body.first() {
Some(b'n') => Fmt::Number,
Some(b'l') => Fmt::Line { keep_nl: false },
Some(b'L') if v >= LuaVersion::Lua52 => Fmt::Line { keep_nl: true },
Some(b'a') => Fmt::All,
_ => return Err(arg_error(vm, argno, "invalid format")),
})
}
fn g_read(vm: &mut Vm, u: Gc<Userdata>, fmts: &[Value], argno0: u32) -> Result<ReadOut, LuaError> {
if let Err(e) = drain_write_buf(u) {
return Ok(ReadOut::Error(e));
}
if fmts.is_empty() {
return Ok(match read_line(vm, u, false) {
Ok(v) => ReadOut::Values(vec![v]),
Err(e) => ReadOut::Error(e),
});
}
let mut out = Vec::with_capacity(fmts.len());
for (i, &f) in fmts.iter().enumerate() {
let fmt = parse_format(vm, f, argno0 + i as u32)?;
let r = match fmt {
Fmt::Count(n) => read_count(vm, u, n)?,
Fmt::Number => read_number(vm, u),
Fmt::Line { keep_nl } => read_line(vm, u, keep_nl),
Fmt::All => read_all(vm, u),
};
match r {
Ok(v) => {
let stop = v.is_nil();
out.push(v);
if stop {
break;
}
}
Err(e) => return Ok(ReadOut::Error(e)),
}
}
Ok(ReadOut::Values(out))
}
fn push_read(vm: &mut Vm, fs: u32, r: ReadOut) -> u32 {
match r {
ReadOut::Values(vals) => vm.nat_return(fs, &vals),
ReadOut::Error(e) => file_fail(vm, fs, None, &e),
}
}
fn io_read(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = get_io_file(vm, Io::Input)?;
let fmts: Vec<Value> = (0..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
let r = g_read(vm, u, &fmts, 1)?;
Ok(push_read(vm, fs, r))
}
fn f_read(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, Args::new(fs, nargs), 0)?;
let fmts: Vec<Value> = (1..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
let r = g_read(vm, u, &fmts, 2)?;
Ok(push_read(vm, fs, r))
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
const BUFSIZ: usize = 1024;
#[cfg(not(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
)))]
const BUFSIZ: usize = 8192;
fn read_line(vm: &mut Vm, u: Gc<Userdata>, keep_nl: bool) -> std::io::Result<Value> {
if vm.version() <= LuaVersion::Lua52 {
return read_line_fgets(vm, u, keep_nl);
}
let mut buf = Vec::new();
let mut got_nl = false;
while let Some(c) = getc(u)? {
if c == b'\n' {
got_nl = true;
if keep_nl {
buf.push(c);
}
break;
}
buf.push(c);
}
if got_nl || !buf.is_empty() {
read_str(vm, &buf)
} else {
Ok(Value::Nil)
}
}
fn read_line_fgets(vm: &mut Vm, u: Gc<Userdata>, keep_nl: bool) -> std::io::Result<Value> {
let mut out = Vec::new();
loop {
let mut chunk = Vec::new();
while chunk.len() < BUFSIZ - 1 {
match getc(u)? {
Some(c) => {
chunk.push(c);
if c == b'\n' {
break;
}
}
None => break,
}
}
if chunk.is_empty() {
return if out.is_empty() {
Ok(Value::Nil)
} else {
read_str(vm, &out)
};
}
let len = chunk.iter().position(|&b| b == 0).unwrap_or(chunk.len());
if len == 0 || chunk[len - 1] != b'\n' {
out.extend_from_slice(&chunk[..len]);
} else {
let end = if keep_nl { len } else { len - 1 };
out.extend_from_slice(&chunk[..end]);
return read_str(vm, &out);
}
}
}
fn read_str(vm: &mut Vm, bytes: &[u8]) -> std::io::Result<Value> {
if bytes.len() > crate::runtime::string::MAX_LEN {
return Err(posix_error(ENOMEM));
}
Ok(Value::Str(vm.heap.intern(bytes)))
}
fn read_all(vm: &mut Vm, u: Gc<Userdata>) -> std::io::Result<Value> {
let mut buf = Vec::new();
loop {
buf.extend_from_slice(&u.read_buf[u.read_pos..]);
unsafe { u.as_mut() }.read_pos = u.read_buf.len();
if !fill(u)? {
break;
}
}
read_str(vm, &buf)
}
const UNALLOCATABLE: u64 = 1 << 47;
fn read_count(vm: &mut Vm, u: Gc<Userdata>, n: i64) -> Result<std::io::Result<Value>, LuaError> {
let size = n as u64;
if size == 0 {
return Ok(test_eof(vm, u));
}
if size >= UNALLOCATABLE && vm.version() >= LuaVersion::Lua52 {
return Err(match vm.version() {
LuaVersion::Lua52 if size > u64::MAX - 64 => {
vm.plain_err("memory allocation error: block too big")
}
LuaVersion::Lua53 => raise_str(vm, "not enough memory for buffer allocation"),
LuaVersion::Lua55 if size >= i64::MAX as u64 => {
raise_str(vm, "resulting string too large")
}
_ => vm.plain_err("not enough memory"),
});
}
let mut buf = Vec::new();
while (buf.len() as u64) < size {
let want = (size - buf.len() as u64) as usize;
if u.read_pos >= u.read_buf.len() {
match fill(u) {
Ok(true) => {}
Ok(false) => break,
Err(e) => return Ok(Err(e)),
}
}
let take = want.min(u.read_buf.len() - u.read_pos);
buf.extend_from_slice(&u.read_buf[u.read_pos..u.read_pos + take]);
unsafe { u.as_mut() }.read_pos += take;
}
Ok(if buf.is_empty() {
Ok(Value::Nil)
} else {
read_str(vm, &buf)
})
}
fn test_eof(vm: &mut Vm, u: Gc<Userdata>) -> std::io::Result<Value> {
Ok(match getc(u)? {
Some(c) => {
unget(u, &[c]);
Value::Str(vm.heap.intern(b""))
}
None => Value::Nil,
})
}
fn read_number(vm: &mut Vm, u: Gc<Userdata>) -> std::io::Result<Value> {
if vm.version() <= LuaVersion::Lua52 {
return scan_double(u);
}
let buf = read_numeral(u)?;
Ok(match numeric::str2num(&buf, true, true) {
Some(Num::Int(i)) => Value::Int(i),
Some(Num::Float(f)) => Value::Float(f),
None => Value::Nil,
})
}
fn scan_double(u: Gc<Userdata>) -> std::io::Result<Value> {
let mut c = getc(u)?;
while matches!(c, Some(b) if is_c_space(b)) {
c = getc(u)?;
}
let mut buf: Vec<u8> = Vec::new();
let mut commit = 0; let mut state = Scan::Start;
while let Some(b) = c {
let next = scan_step(state, b, &buf);
let Some((st, complete)) = next else { break };
buf.push(b);
if complete {
commit = buf.len();
}
state = st;
c = getc(u)?;
}
if let Some(b) = c {
buf.push(b);
}
unget(u, &buf[commit..]);
if commit == 0 {
return Ok(Value::Nil);
}
Ok(Value::Float(parse_c_double(&buf[..commit])))
}
fn is_c_space(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r')
}
#[derive(Clone, Copy, PartialEq)]
enum Scan {
Start,
Sign,
Zero,
Int,
Dot,
Frac,
ExpMark,
ExpSign,
ExpDigits,
HexX,
HexInt,
HexDot,
HexFrac,
Word,
NanOpen,
Done,
}
fn scan_step(s: Scan, b: u8, buf: &[u8]) -> Option<(Scan, bool)> {
let digits = |st| Some((st, true));
match s {
Scan::Start | Scan::Sign => match b {
b'+' | b'-' if s == Scan::Start => Some((Scan::Sign, false)),
b'0' => digits(Scan::Zero),
b'1'..=b'9' => digits(Scan::Int),
b'.' => Some((Scan::Dot, false)),
b'i' | b'I' | b'n' | b'N' => Some((Scan::Word, false)),
_ => None,
},
Scan::Zero if matches!(b, b'x' | b'X') => Some((Scan::HexX, false)),
Scan::Zero | Scan::Int => match b {
b'0'..=b'9' => digits(Scan::Int),
b'.' => digits(Scan::Frac),
b'e' | b'E' => Some((Scan::ExpMark, false)),
_ => None,
},
Scan::Dot => match b {
b'0'..=b'9' => digits(Scan::Frac),
_ => None,
},
Scan::Frac => match b {
b'0'..=b'9' => digits(Scan::Frac),
b'e' | b'E' => Some((Scan::ExpMark, false)),
_ => None,
},
Scan::ExpMark => match b {
b'+' | b'-' => Some((Scan::ExpSign, false)),
b'0'..=b'9' => digits(Scan::ExpDigits),
_ => None,
},
Scan::ExpSign | Scan::ExpDigits => match b {
b'0'..=b'9' => digits(Scan::ExpDigits),
_ => None,
},
Scan::HexX => match b {
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => digits(Scan::HexInt),
b'.' => Some((Scan::HexDot, false)),
_ => None,
},
Scan::HexInt => match b {
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => digits(Scan::HexInt),
b'.' => digits(Scan::HexFrac),
b'p' | b'P' => Some((Scan::ExpMark, false)),
_ => None,
},
Scan::HexDot | Scan::HexFrac => match b {
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => digits(Scan::HexFrac),
b'p' | b'P' if s == Scan::HexFrac => Some((Scan::ExpMark, false)),
_ => None,
},
Scan::Word => {
let word: Vec<u8> = buf
.iter()
.skip_while(|&&c| c == b'+' || c == b'-')
.map(u8::to_ascii_lowercase)
.chain(std::iter::once(b.to_ascii_lowercase()))
.collect();
if b"infinity".starts_with(&word) {
Some((Scan::Word, word == b"inf" || word == b"infinity"))
} else if b"nan".starts_with(&word) {
Some((Scan::Word, word == b"nan"))
} else if word == b"nan(" {
Some((Scan::NanOpen, false))
} else {
None
}
}
Scan::NanOpen => match b {
b')' => Some((Scan::Done, true)),
b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' => Some((Scan::NanOpen, false)),
_ => None,
},
Scan::Done => None,
}
}
fn parse_c_double(s: &[u8]) -> f64 {
let (neg, body) = match s.split_first() {
Some((b'-', rest)) => (true, rest),
Some((b'+', rest)) => (false, rest),
_ => (false, s),
};
let lower = body.to_ascii_lowercase();
let mag = if lower.starts_with(b"inf") {
f64::INFINITY
} else if lower.starts_with(b"nan") {
f64::NAN
} else if lower.starts_with(b"0x") {
match numeric::str2num(body, false, true) {
Some(n) => n.as_f64(),
None => unreachable!("the scanner only commits valid hex numerals"),
}
} else {
std::str::from_utf8(body)
.expect("decimal numerals are ASCII")
.parse::<f64>()
.expect("the scanner only commits valid decimal numerals")
};
if neg { -mag } else { mag }
}
const L_MAXLENNUM: usize = 200;
struct Rn {
buf: Vec<u8>,
c: Option<u8>,
}
fn rn_next(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 = getc(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_next(rn, u);
}
Ok(false)
}
fn rn_digits(rn: &mut Rn, u: Gc<Userdata>, hex: bool) -> std::io::Result<u32> {
let mut count = 0;
while matches!(rn.c, Some(c) if if hex { c.is_ascii_hexdigit() } else { c.is_ascii_digit() })
&& rn_next(rn, u)?
{
count += 1;
}
Ok(count)
}
fn read_numeral(u: Gc<Userdata>) -> std::io::Result<Vec<u8>> {
let mut c = getc(u)?;
while matches!(c, Some(b) if is_c_space(b)) {
c = getc(u)?;
}
let mut rn = Rn { buf: Vec::new(), c };
let mut count = 0;
let mut hex = false;
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 && rn_test(&mut rn, u, if hex { b"pP" } else { b"eE" })? {
rn_test(&mut rn, u, b"-+")?;
rn_digits(&mut rn, u, false)?;
}
if let Some(b) = rn.c {
unget(u, &[b]);
}
Ok(rn.buf)
}
fn check_line_formats(vm: &mut Vm, n: u32) -> Result<(), LuaError> {
let (max, argno, msg) = match vm.version() {
LuaVersion::Lua51 => return Ok(()),
LuaVersion::Lua52 => (17, 17, "too many options"),
_ => (250, 252, "too many arguments"),
};
if n > max {
return Err(arg_error(vm, argno, msg));
}
Ok(())
}
fn make_lines(vm: &mut Vm, u: Gc<Userdata>, toclose: bool, fmts: &[Value]) -> Value {
let mut up = vec![Value::Userdata(u), Value::Bool(toclose)];
if vm.version() >= LuaVersion::Lua52 {
up.extend_from_slice(fmts);
}
vm.native_with(io_readline, up.into_boxed_slice())
}
fn f_lines(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let u = check_open(vm, Args::new(fs, nargs), 0)?;
check_line_formats(vm, nargs.saturating_sub(1))?;
let fmts: Vec<Value> = (1..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
let it = make_lines(vm, u, false, &fmts);
Ok(vm.nat_return(fs, &[it]))
}
fn io_lines(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
let a = Args::new(fs, nargs);
let (u, toclose) = if a.is_none_or_nil(vm, 0) {
let d = default_file(vm, Io::Input);
if d.file().is_closed() {
return Err(raise_str(vm, "attempt to use a closed file"));
}
(d, false)
} else {
let name = argcheck::check_string(vm, a, 0)?.as_bytes().to_vec();
(open_checked(vm, &name, b"r")?, true)
};
let nfmt = nargs.saturating_sub(1);
check_line_formats(vm, nfmt)?;
let fmts: Vec<Value> = (1..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
let it = make_lines(vm, u, toclose, &fmts);
if toclose && vm.version() >= LuaVersion::Lua54 {
return Ok(vm.nat_return(fs, &[it, Value::Nil, Value::Nil, Value::Userdata(u)]));
}
Ok(vm.nat_return(fs, &[it]))
}
fn io_readline(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
let Value::Userdata(u) = vm.nat_upval(fs, 0) else {
unreachable!("line iterator upvalue 0 is its file");
};
if u.file().is_closed() {
return Err(raise_str(vm, "file is already closed"));
}
let fmts: Vec<Value> = (2..vm.nat_upcount(fs))
.map(|i| vm.nat_upval(fs, i))
.collect();
let vals = match g_read(vm, u, &fmts, 2)? {
ReadOut::Values(v) => v,
ReadOut::Error(e) => return Err(raise_str(vm, &strerror(&e))),
};
if !vals[0].is_nil() {
return Ok(vm.nat_return(fs, &vals));
}
if let Value::Bool(true) = vm.nat_upval(fs, 1) {
let _ = close_stream(u); }
Ok(vm.nat_return(fs, &[]))
}