use std::cell::RefCell;
use std::collections::HashMap;
use std::io::{self, SeekFrom};
use std::rc::Rc;
use lua_types::{LuaError, LuaFileHandle, LuaType, LuaValue};
use lua_vm::state::{InputHook, OutputHook};
use crate::state_stub::{LuaState, LuaStateStubExt as _};
thread_local! {
static LSTREAM_REGISTRY: RefCell<HashMap<usize, Rc<RefCell<LStream>>>>
= RefCell::new(HashMap::new());
}
fn register_lstream(ud_id: usize, lstream: LStream) -> Rc<RefCell<LStream>> {
let cell = Rc::new(RefCell::new(lstream));
LSTREAM_REGISTRY.with(|reg| {
reg.borrow_mut().insert(ud_id, cell.clone());
});
cell
}
fn lookup_lstream(ud_id: usize) -> Option<Rc<RefCell<LStream>>> {
LSTREAM_REGISTRY.with(|reg| reg.borrow().get(&ud_id).cloned())
}
pub const LUA_FILE_HANDLE: &[u8] = b"FILE*";
const IO_INPUT_KEY: &[u8] = b"_IO_input";
const IO_OUTPUT_KEY: &[u8] = b"_IO_output";
const IO_PREFIX_LEN: usize = 4;
const MAX_ARG_LINE: usize = 250;
const L_MAX_LEN_NUM: usize = 200;
const EOF_SENTINEL: i32 = -1;
const LUAL_BUFFER_SIZE: usize = 8192;
pub trait LuaFileOps: LuaFileHandle {
fn set_buf_mode(&mut self, mode: BufMode, size: usize) -> io::Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeekWhence {
Set,
Cur,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufMode {
No,
Full,
Line,
}
pub enum StdFileKind {
Stdin,
Stdout,
Stderr,
}
pub struct LStream {
pub file: Option<Box<dyn LuaFileHandle>>,
pub close_fn: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
}
impl LStream {
pub fn is_closed(&self) -> bool {
self.close_fn.is_none()
}
}
struct StdStreamHandle {
kind: StdFileKind,
input_hook: Option<InputHook>,
output_hook: Option<OutputHook>,
unread: Option<u8>,
}
impl LuaFileHandle for StdStreamHandle {
fn read_byte(&mut self) -> i32 {
if let Some(byte) = self.unread.take() {
return byte as i32;
}
match self.kind {
StdFileKind::Stdin => {
if let Some(read_fn) = self.input_hook {
let mut buf = [0u8; 1];
return match read_fn(&mut buf) {
Ok(1) => buf[0] as i32,
_ => EOF_SENTINEL,
};
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
EOF_SENTINEL
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
use std::io::Read;
let mut buf = [0u8; 1];
match std::io::stdin().read(&mut buf) {
Ok(1) => buf[0] as i32,
_ => EOF_SENTINEL,
}
}
}
_ => EOF_SENTINEL,
}
}
fn unread_byte(&mut self, byte: i32) {
if (0..=u8::MAX as i32).contains(&byte) {
self.unread = Some(byte as u8);
}
}
fn write_bytes(&mut self, data: &[u8]) -> io::Result<usize> {
if let Some(write_fn) = self.output_hook {
write_fn(data)?;
return Ok(data.len());
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
let _ = data;
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"standard output not available in this host",
));
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
use std::io::Write;
match self.kind {
StdFileKind::Stderr => {
std::io::stderr().write_all(data)?;
Ok(data.len())
}
_ => {
std::io::stdout().write_all(data)?;
Ok(data.len())
}
}
}
}
fn flush(&mut self) -> io::Result<()> {
if self.output_hook.is_some() {
return Ok(());
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"standard output not available in this host",
));
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
use std::io::Write;
match self.kind {
StdFileKind::Stderr => std::io::stderr().flush(),
_ => std::io::stdout().flush(),
}
}
}
fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
Err(io::Error::new(io::ErrorKind::Unsupported, "stdio seek"))
}
fn tell(&mut self) -> io::Result<u64> {
Err(io::Error::new(io::ErrorKind::Unsupported, "stdio tell"))
}
fn clear_error(&mut self) {}
fn has_error(&self) -> bool { false }
}
impl LuaFileOps for StdStreamHandle {
fn set_buf_mode(&mut self, _mode: BufMode, _size: usize) -> io::Result<()> { Ok(()) }
}
impl StdStreamHandle {
fn new(
kind: StdFileKind,
input_hook: Option<InputHook>,
output_hook: Option<OutputHook>,
) -> Self {
StdStreamHandle {
kind,
input_hook,
output_hook,
unread: None,
}
}
}
struct ReadNumState {
current: i32,
count: usize,
buf: [u8; L_MAX_LEN_NUM + 1],
}
impl ReadNumState {
fn new(first_byte: i32) -> Self {
ReadNumState {
current: first_byte,
count: 0,
buf: [0u8; L_MAX_LEN_NUM + 1],
}
}
fn advance(&mut self, file: &mut dyn LuaFileHandle) -> bool {
if self.count >= L_MAX_LEN_NUM {
self.buf[0] = 0;
return false;
}
self.buf[self.count] = self.current as u8;
self.count += 1;
self.current = file.read_byte();
true
}
fn try2(&mut self, file: &mut dyn LuaFileHandle, set: [u8; 2]) -> bool {
if self.current == set[0] as i32 || self.current == set[1] as i32 {
self.advance(file)
} else {
false
}
}
fn read_digits(&mut self, file: &mut dyn LuaFileHandle, hex: bool) -> usize {
let mut count = 0usize;
loop {
let is_digit = if hex {
(self.current as u8).is_ascii_hexdigit()
} else {
(self.current as u8).is_ascii_digit()
};
if !is_digit || self.current == EOF_SENTINEL {
break;
}
if !self.advance(file) {
break;
}
count += 1;
}
count
}
fn as_bytes(&self) -> &[u8] {
&self.buf[..self.count]
}
}
pub const IO_LIB: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
(b"close", io_close),
(b"flush", io_flush),
(b"input", io_input),
(b"lines", io_lines),
(b"open", io_open),
(b"output", io_output),
(b"popen", io_popen),
(b"read", io_read),
(b"tmpfile", io_tmpfile),
(b"type", io_type),
(b"write", io_write),
];
pub const FILE_METHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
(b"read", f_read),
(b"write", f_write),
(b"lines", f_lines),
(b"flush", f_flush),
(b"seek", f_seek),
(b"close", f_close),
(b"setvbuf", f_setvbuf),
];
pub const FILE_METAMETHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
(b"__gc", f_gc),
(b"__close", f_gc),
(b"__tostring", f_tostring),
];
fn check_mode(mode: &[u8]) -> bool {
if mode.is_empty() {
return false;
}
let mut idx = 0usize;
if !matches!(mode[idx], b'r' | b'w' | b'a') {
return false;
}
idx += 1;
if idx < mode.len() && mode[idx] == b'+' {
idx += 1;
}
mode[idx..].iter().all(|&b| b == b'b')
}
fn check_mode_popen(mode: &[u8]) -> bool {
matches!(mode, b"r" | b"w")
}
fn file_result(
state: &mut LuaState,
success: bool,
fname: Option<&[u8]>,
os_err: io::Error,
) -> Result<usize, LuaError> {
if success {
state.push(LuaValue::Bool(true));
return Ok(1);
}
state.push(LuaValue::Bool(false));
let msg = os_err.to_string();
match fname {
Some(name) => {
let mut s = Vec::with_capacity(name.len() + 2 + msg.len());
s.extend_from_slice(name);
s.extend_from_slice(b": ");
s.extend_from_slice(msg.as_bytes());
state.push_string(&s)?;
}
None => {
state.push_string(msg.as_bytes())?;
}
}
let errno_code = os_err.raw_os_error().unwrap_or(0) as i64;
state.push(LuaValue::Int(errno_code));
Ok(3)
}
fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
if stat == 0 {
state.push(LuaValue::Bool(true));
Ok(1)
} else {
state.push(LuaValue::Bool(false));
state.push_string(b"exit")?;
state.push(LuaValue::Int(stat as i64));
Ok(3)
}
}
fn get_lstream(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
let ud = state.check_arg_userdata(1, LUA_FILE_HANDLE)?;
lookup_lstream(ud.identity()).ok_or_else(|| {
LuaError::runtime(format_args!("invalid file handle"))
})
}
fn lstream_from_upvalue(
state: &mut LuaState,
idx: i32,
) -> Result<Rc<RefCell<LStream>>, LuaError> {
let v = state.value_at(crate::state_stub::upvalue_index(idx));
let ud_id = match v {
LuaValue::UserData(ud) => ud.identity(),
_ => {
return Err(LuaError::runtime(format_args!(
"invalid file handle in upvalue {}",
idx
)));
}
};
lookup_lstream(ud_id).ok_or_else(|| {
LuaError::runtime(format_args!("invalid file handle in upvalue {}", idx))
})
}
fn tofile(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
let p_rc = get_lstream(state)?;
{
let p = p_rc.borrow();
if p.is_closed() {
return Err(LuaError::runtime(format_args!(
"attempt to use a closed file"
)));
}
debug_assert!(p.file.is_some());
}
Ok(p_rc)
}
fn new_pre_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
let ud = state.new_userdata_typed(LUA_FILE_HANDLE, std::mem::size_of::<LStream>(), 0)?;
state.set_metatable_by_name(LUA_FILE_HANDLE)?;
let cell = register_lstream(ud.identity(), LStream { file: None, close_fn: None });
Ok(cell)
}
fn new_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
let cell = new_pre_file(state)?;
cell.borrow_mut().close_fn = Some(io_fclose);
Ok(cell)
}
fn opencheck(state: &mut LuaState, fname: &[u8], mode: &[u8]) -> Result<(), LuaError> {
let hook = state.global().file_open_hook;
let fh = match hook {
Some(open_fn) => open_fn(fname, mode).map_err(|e| {
LuaError::runtime(format_args!(
"cannot open file '{}' ({})",
fname.escape_ascii(),
match &e {
LuaError::Runtime(LuaValue::Str(s)) => {
String::from_utf8_lossy(s.as_bytes()).into_owned()
}
other => format!("{:?}", other),
}
))
})?,
None => {
return Err(LuaError::runtime(format_args!(
"cannot open file '{}' (no filesystem hook registered)",
fname.escape_ascii()
)));
}
};
let cell = new_file(state)?;
cell.borrow_mut().file = Some(fh);
Ok(())
}
fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_lstream(state)?;
let _closed = p_rc.borrow_mut().file.take();
state.push(LuaValue::Bool(true));
Ok(1)
}
fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_lstream(state)?;
let _closed = p_rc.borrow_mut().file.take();
exec_result(state, 0)
}
fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_lstream(state)?;
p_rc.borrow_mut().close_fn = Some(io_noclose); state.push(LuaValue::Bool(false));
state.push_string(b"cannot close standard file")?;
Ok(2)
}
fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_lstream(state)?;
let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
LuaError::runtime(format_args!("attempt to close an already-closed file"))
})?;
cf(state)
}
pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
match maybe_userdata {
None => {
state.push(LuaValue::Bool(false));
}
Some(ud) => {
let is_closed = match lookup_lstream(ud.identity()) {
Some(rc) => rc.borrow().is_closed(),
None => true, };
if is_closed {
state.push_string(b"closed file")?;
} else {
state.push_string(b"file")?;
}
}
}
Ok(1)
}
fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_lstream(state)?;
let closed = p_rc.borrow().is_closed();
if closed {
state.push_string(b"file (closed)")?;
} else {
state.push_string(b"file (0x?)")?;
}
Ok(1)
}
fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
let _ = tofile(state)?; aux_close(state)
}
pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
if state.type_at(1) == LuaType::None {
state.registry_get(IO_OUTPUT_KEY)?;
}
f_close(state)
}
fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_lstream(state)?;
let needs_close = {
let p = p_rc.borrow();
!p.is_closed() && p.file.is_some()
};
if needs_close {
let _ = aux_close(state);
}
Ok(0)
}
pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
let filename: Vec<u8> = state.check_arg_string(1)?;
let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
if !check_mode(&mode) {
return Err(LuaError::arg_error(2, "invalid mode"));
}
let hook = state.global().file_open_hook;
match hook {
Some(open_fn) => match open_fn(&filename, &mode) {
Ok(fh) => {
let cell = new_file(state)?;
cell.borrow_mut().file = Some(fh);
Ok(1)
}
Err(e) => {
let os_err = io::Error::new(
io::ErrorKind::Other,
match &e {
LuaError::Runtime(LuaValue::Str(s)) => {
String::from_utf8_lossy(s.as_bytes()).into_owned()
}
other => format!("{:?}", other),
},
);
file_result(state, false, Some(&filename), os_err)
}
},
None => {
let os_err = io::Error::new(
io::ErrorKind::Unsupported,
"no filesystem hook registered",
);
file_result(state, false, Some(&filename), os_err)
}
}
}
pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
let filename: Vec<u8> = state.check_arg_string(1)?;
let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
if !check_mode_popen(&mode) {
return Err(LuaError::arg_error(2, "invalid mode"));
}
let hook = state.global().popen_hook;
match hook {
Some(spawn_fn) => match spawn_fn(&filename, &mode) {
Ok(fh) => {
let cell = new_pre_file(state)?;
let mut p = cell.borrow_mut();
p.file = Some(fh);
p.close_fn = Some(io_pclose);
drop(p);
Ok(1)
}
Err(e) => {
let os_err = io::Error::new(
io::ErrorKind::Other,
match &e {
LuaError::Runtime(LuaValue::Str(s)) => {
String::from_utf8_lossy(s.as_bytes()).into_owned()
}
other => format!("{:?}", other),
},
);
file_result(state, false, Some(&filename), os_err)
}
},
None => {
let os_err = io::Error::new(
io::ErrorKind::Unsupported,
"popen not enabled in this build",
);
file_result(state, false, Some(&filename), os_err)
}
}
}
fn native_temp_name() -> io::Result<Vec<u8>> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"temporary files not available in this host",
));
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
path.push(b'/');
}
let unique = format!(
"lua_tmpfile_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
path.extend_from_slice(unique.as_bytes());
Ok(path)
}
}
pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
let hook = state.global().file_open_hook;
let Some(open_fn) = hook else {
let os_err = io::Error::new(
io::ErrorKind::Unsupported,
"no filesystem hook registered",
);
return file_result(state, false, None, os_err);
};
let temp_name_hook = state.global().temp_name_hook;
let path = match temp_name_hook {
Some(temp_fn) => match temp_fn() {
Ok(path) => path,
Err(e) => {
let msg = match &e {
LuaError::Runtime(LuaValue::Str(s)) => {
String::from_utf8_lossy(s.as_bytes()).into_owned()
}
other => format!("{:?}", other),
};
return file_result(
state,
false,
None,
io::Error::new(io::ErrorKind::Unsupported, msg),
);
}
},
None => match native_temp_name() {
Ok(path) => path,
Err(e) => return file_result(state, false, None, e),
},
};
match open_fn(&path, b"w+b") {
Ok(fh) => {
let cell = new_file(state)?;
cell.borrow_mut().file = Some(fh);
Ok(1)
}
Err(e) => {
let os_err = io::Error::new(
io::ErrorKind::Other,
match &e {
LuaError::Runtime(LuaValue::Str(s)) => {
String::from_utf8_lossy(s.as_bytes()).into_owned()
}
other => format!("{:?}", other),
},
);
file_result(state, false, None, os_err)
}
}
}
#[expect(dead_code, unreachable_code, unused_variables, reason = "io default-file helper: not yet wired; pending LStream-from-registry port")]
fn get_io_file<'a>(
state: &'a mut LuaState,
key: &[u8],
) -> Result<&'a mut dyn LuaFileHandle, LuaError> {
state.registry_get(key)?;
let label = &key[IO_PREFIX_LEN..]; let p: &mut LStream = todo!("TODO(port): extract LStream from registry userdata");
if p.is_closed() {
return Err(LuaError::runtime(format_args!(
"default {} file is closed",
label.escape_ascii()
)));
}
Ok(p.file.as_mut().expect("open stream has no file handle").as_mut())
}
fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
if state.type_at(1) == LuaType::String {
let filename = state.check_arg_string(1)?;
opencheck(state, &filename, mode)?;
} else {
let _ = tofile(state)?;
state.push_value_at(1)?;
}
state.registry_set(key)?;
}
state.registry_get(key)?;
Ok(1)
}
pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
g_iofile(state, IO_INPUT_KEY, b"r")
}
pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
g_iofile(state, IO_OUTPUT_KEY, b"w")
}
fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
let first = loop {
let b = file.read_byte();
if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
break b;
}
};
let mut rn = ReadNumState::new(first);
rn.try2(file, [b'-', b'+']);
let mut count: usize = 0;
let hex = if rn.try2(file, [b'0', b'0']) {
if rn.try2(file, [b'x', b'X']) {
true
} else {
count = 1;
false
}
} else {
false
};
count += rn.read_digits(file, hex);
let dec_point = b'.';
if rn.try2(file, [dec_point, b'.']) {
count += rn.read_digits(file, hex);
}
if count > 0 {
let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
if rn.try2(file, exp_chars) {
rn.try2(file, [b'-', b'+']);
rn.read_digits(file, false);
}
}
file.unread_byte(rn.current);
rn.as_bytes().to_vec()
}
fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
let c = file.read_byte();
if c != EOF_SENTINEL {
file.unread_byte(c);
}
c != EOF_SENTINEL
}
fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
let mut buf: Vec<u8> = Vec::new();
let mut c: i32;
'outer: loop {
for _ in 0..LUAL_BUFFER_SIZE {
c = file.read_byte();
if c == EOF_SENTINEL || c == b'\n' as i32 {
break 'outer;
}
buf.push(c as u8);
}
}
if !chop && c == b'\n' as i32 {
buf.push(b'\n');
}
let had_content = c == b'\n' as i32 || !buf.is_empty();
(buf, had_content)
}
fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::new();
loop {
let mut chunk_read = 0usize;
for _ in 0..LUAL_BUFFER_SIZE {
let b = file.read_byte();
if b == EOF_SENTINEL {
break;
}
buf.push(b as u8);
chunk_read += 1;
}
if chunk_read < LUAL_BUFFER_SIZE {
break;
}
}
buf
}
fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
let mut buf = Vec::with_capacity(n);
for _ in 0..n {
let b = file.read_byte();
if b == EOF_SENTINEL {
break;
}
buf.push(b as u8);
}
let nr = buf.len();
(buf, nr > 0)
}
fn g_read(
state: &mut LuaState,
p_rc: &Rc<RefCell<LStream>>,
first: i32,
) -> Result<usize, LuaError> {
let nargs = (state.top() - first + 1).max(0);
let mut n = first;
let mut success = true;
{
let mut p = p_rc.borrow_mut();
let fh = p.file.as_mut().expect("open stream has no file handle");
fh.clear_error();
}
if nargs == 0 {
let (bytes, had) = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
read_line(fh, true)
};
state.push_string(&bytes)?;
success = had;
n = first + 1;
} else {
state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
let mut remaining = nargs;
while remaining > 0 && success {
if state.type_at(n) == LuaType::Number {
let l = state.check_arg_integer(n)? as usize;
if l == 0 {
let not_eof = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
test_eof(fh)
};
state.push_string(b"")?;
success = not_eof;
} else {
let (bytes, had) = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
read_chars(fh, l)
};
state.push_string(&bytes)?;
success = had;
}
} else {
let s: Vec<u8> = state.check_arg_string(n)?;
let pp: &[u8] = if s.first() == Some(&b'*') { &s[1..] } else { &s[..] };
match pp.first() {
Some(&b'n') => {
let bytes = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
read_number_bytes(fh)
};
let pushed = state.string_to_number_push(&bytes)?;
if pushed != 0 {
success = true;
} else {
state.push(LuaValue::Nil);
success = false;
}
}
Some(&b'l') => {
let (bytes, had) = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
read_line(fh, true)
};
state.push_string(&bytes)?;
success = had;
}
Some(&b'L') => {
let (bytes, had) = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
read_line(fh, false)
};
state.push_string(&bytes)?;
success = had;
}
Some(&b'a') => {
let bytes = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
read_all(fh)
};
state.push_string(&bytes)?;
success = true;
}
_ => {
return Err(LuaError::arg_error(n, "invalid format"));
}
}
}
n += 1;
remaining -= 1;
}
}
let has_err = {
let p = p_rc.borrow();
match p.file.as_deref() {
Some(fh) => fh.has_error(),
None => false,
}
};
if has_err {
let err = {
let p = p_rc.borrow();
match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
None => io::Error::new(io::ErrorKind::Other, "file read error"),
}
};
return file_result(
state,
false,
None,
err,
);
}
if !success {
state.pop_n(1);
state.push(LuaValue::Nil);
}
Ok((n - first) as usize)
}
fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
state.registry_get(key)?;
let ud_id = state
.test_arg_userdata(-1, LUA_FILE_HANDLE)
.map(|ud| ud.identity());
state.pop_n(1);
let label = &key[IO_PREFIX_LEN..];
let id = ud_id.ok_or_else(|| {
LuaError::runtime(format_args!(
"default {} file is invalid",
label.escape_ascii()
))
})?;
let rc = lookup_lstream(id).ok_or_else(|| {
LuaError::runtime(format_args!(
"default {} file is invalid",
label.escape_ascii()
))
})?;
if rc.borrow().is_closed() {
return Err(LuaError::runtime(format_args!(
"default {} file is closed",
label.escape_ascii()
)));
}
Ok(rc)
}
pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
g_read(state, &p_rc, 1)
}
pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = tofile(state)?;
g_read(state, &p_rc, 2)
}
#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
fn g_write(
state: &mut LuaState,
file: &mut dyn LuaFileHandle,
arg: i32,
) -> Result<usize, LuaError> {
let nargs = state.top() - arg;
let mut overall_ok = true;
for i in 0..nargs {
let idx = arg + i;
if state.type_at(idx) == LuaType::Number {
let s = if state.is_integer(idx) {
let ival = state.to_integer(idx).unwrap_or(0);
format!("{}", ival)
} else {
let fval = state.to_number(idx).unwrap_or(0.0);
format!("{:.14e}", fval)
};
match file.write_bytes(s.as_bytes()) {
Ok(n) => overall_ok = overall_ok && n == s.len(),
Err(_) => overall_ok = false,
}
} else {
let s: Vec<u8> = state.check_arg_string(idx)?;
match file.write_bytes(&s) {
Ok(n) => overall_ok = overall_ok && n == s.len(),
Err(_) => overall_ok = false,
}
}
}
if overall_ok {
Ok(1) } else {
file_result(
state,
false,
None,
io::Error::new(io::ErrorKind::Other, "write error"),
)
}
}
pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
let n = state.top();
let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
for i in 1..=(n as i32) {
if state.type_at(i) == LuaType::Number {
let s = if state.is_integer(i) {
let ival = state.to_integer(i).unwrap_or(0);
format!("{}", ival).into_bytes()
} else {
let fval = state.to_number(i).unwrap_or(0.0);
format!("{:.14e}", fval).into_bytes()
};
chunks.push(s);
} else {
let bytes: Vec<u8> = state.check_arg_string(i)?;
chunks.push(bytes);
}
}
let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
{
let mut p = p_rc.borrow_mut();
let fh = p.file.as_mut().expect("open stream has no file handle");
for chunk in &chunks {
fh.write_bytes(chunk).map_err(|e| {
LuaError::runtime(format_args!("io.write: {}", e))
})?;
}
}
state.registry_get(IO_OUTPUT_KEY)?;
Ok(1)
}
pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = tofile(state)?;
let n = state.top();
let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n.saturating_sub(1) as usize);
for i in 2..=(n as i32) {
if state.type_at(i) == LuaType::Number {
let s = if state.is_integer(i) {
let ival = state.to_integer(i).unwrap_or(0);
format!("{}", ival).into_bytes()
} else {
let fval = state.to_number(i).unwrap_or(0.0);
format!("{:.14e}", fval).into_bytes()
};
chunks.push(s);
} else {
let bytes: Vec<u8> = state.check_arg_string(i)?;
chunks.push(bytes);
}
}
let result: io::Result<()> = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_mut().expect("open stream has no file handle");
let mut r: io::Result<()> = Ok(());
for chunk in &chunks {
match fh.write_bytes(chunk) {
Ok(written) if written == chunk.len() => {}
Ok(_) => {
r = Err(io::Error::new(io::ErrorKind::Other, "short write"));
break;
}
Err(e) => {
r = Err(e);
break;
}
}
}
r
};
match result {
Ok(()) => {
state.push_value_at(1)?;
Ok(1)
}
Err(e) => file_result(state, false, None, e),
}
}
pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
let p_rc = tofile(state)?;
let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
let p3: i64 = state.opt_arg_integer(3, 0)?;
let seek_pos = match op {
0 => SeekFrom::Start(p3 as u64),
1 => SeekFrom::Current(p3),
2 => SeekFrom::End(p3),
_ => unreachable!(),
};
let result = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_mut().expect("open stream has no file handle");
fh.seek(seek_pos)
};
match result {
Ok(pos) => {
state.push(LuaValue::Int(pos as i64));
Ok(1)
}
Err(e) => file_result(state, false, None, e),
}
}
pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
let p_rc = tofile(state)?;
let op = state.check_arg_option(2, None, MODE_NAMES)?;
let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
let mode = match op {
0 => BufMode::No,
1 => BufMode::Full,
2 => BufMode::Line,
_ => unreachable!(),
};
let result = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_mut().expect("open stream has no file handle");
let mode_index = match mode {
BufMode::No => 0,
BufMode::Full => 1,
BufMode::Line => 2,
};
fh.set_buf_mode(mode_index, sz.max(0) as usize)
};
match result {
Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
Err(e) => file_result(state, false, None, e),
}
}
pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
let ud_id: Option<usize> = {
state.registry_get(IO_OUTPUT_KEY)?;
let id = state
.test_arg_userdata(-1, LUA_FILE_HANDLE)
.map(|ud| ud.identity());
state.pop_n(1);
id
};
if let Some(id) = ud_id {
if let Some(rc) = lookup_lstream(id) {
let result = {
let mut p = rc.borrow_mut();
if p.is_closed() {
return Err(LuaError::runtime(format_args!(
"default output file is closed"
)));
}
let fh = p.file.as_deref_mut().expect("open stream has no file handle");
fh.flush()
};
return match result {
Ok(()) => {
state.push(LuaValue::Bool(true));
Ok(1)
}
Err(e) => file_result(state, false, None, e),
};
}
}
state.push(LuaValue::Bool(true));
Ok(1)
}
pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
let p_rc = tofile(state)?;
let result = {
let mut p = p_rc.borrow_mut();
let fh = p.file.as_mut().expect("open stream has no file handle");
fh.flush()
};
match result {
Ok(()) => {
state.push(LuaValue::Bool(true));
Ok(1)
}
Err(e) => file_result(state, false, None, e),
}
}
fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
let n = state.top() - 1;
if n > MAX_ARG_LINE as i32 {
return Err(LuaError::arg_error(
MAX_ARG_LINE as i32 + 2,
"too many arguments",
));
}
state.push_value_at(1)?;
state.push(LuaValue::Int(n as i64));
state.push(LuaValue::Bool(toclose));
state.rotate(2, 3)?;
state.push_c_closure(io_readline, (3 + n) as i32)?;
Ok(())
}
pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
let _ = tofile(state)?; aux_lines(state, false)?;
Ok(1)
}
pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
if state.type_at(1) == LuaType::None {
state.push(LuaValue::Nil);
}
let toclose = if state.type_at(1) == LuaType::Nil {
state.registry_get(IO_INPUT_KEY)?;
state.replace(1)?;
let _ = tofile(state)?;
false
} else {
let filename = state.check_arg_string(1)?;
opencheck(state, &filename, b"r")?;
state.replace(1)?;
true
};
aux_lines(state, toclose)?;
if toclose {
state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
} else {
Ok(1)
}
}
fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
LuaValue::Int(i) => i as usize,
_ => 0,
};
let p_rc = lstream_from_upvalue(state, 1)?;
if p_rc.borrow().is_closed() {
return Err(LuaError::runtime(format_args!("file is already closed")));
}
lua_vm::api::set_top(state, 1)?;
state.ensure_stack(n as i32, "too many arguments")?;
for i in 1..=n {
let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
state.push(uv);
}
let result_n: usize = g_read(state, &p_rc, 2)?;
debug_assert!(result_n > 0, "g_read should return at least one value");
let top = state.top_idx().get() as i32;
let first_result_idx = top - result_n as i32;
let first_truthy = !matches!(
state.stack_at(first_result_idx),
LuaValue::Nil | LuaValue::Bool(false)
);
if first_truthy {
return Ok(result_n);
}
if result_n > 1 {
let err_val = state.stack_at(first_result_idx + 1).clone();
return Err(LuaError::from_value(err_val));
}
let toclose = !matches!(
state.value_at(crate::state_stub::upvalue_index(3)),
LuaValue::Nil | LuaValue::Bool(false)
);
if toclose {
lua_vm::api::set_top(state, 0)?;
state.push_upvalue(1)?;
aux_close(state)?;
}
Ok(0)
}
fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
state.new_metatable(LUA_FILE_HANDLE)?;
state.set_funcs(FILE_METAMETHODS, 0)?;
state.new_lib_table(FILE_METHODS)?;
state.set_funcs(FILE_METHODS, 0)?;
state.set_field(-2, b"__index")?;
state.pop_n(1);
Ok(())
}
fn create_std_file(
state: &mut LuaState,
std_kind: StdFileKind,
registry_key: Option<&[u8]>,
field_name: &[u8],
) -> Result<(), LuaError> {
let cell = new_pre_file(state)?;
let output_hook = match std_kind {
StdFileKind::Stdout => state.global().stdout_hook,
StdFileKind::Stderr => state.global().stderr_hook,
StdFileKind::Stdin => None,
};
let input_hook = match std_kind {
StdFileKind::Stdin => state.global().stdin_hook,
StdFileKind::Stdout | StdFileKind::Stderr => None,
};
{
let mut p = cell.borrow_mut();
p.file = Some(Box::new(StdStreamHandle::new(
std_kind,
input_hook,
output_hook,
)));
p.close_fn = Some(io_noclose);
}
if let Some(key) = registry_key {
state.push_value_at(-1)?;
state.registry_set(key)?;
}
state.set_field(-2, field_name)?;
Ok(())
}
pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
state.new_lib(IO_LIB)?;
create_meta(state)?;
create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
Ok(1)
}