Skip to main content

luna_core/vm/
lib_io.rs

1//! io library: `FILE*` handles over OS files, the default input/output
2//! streams, and `io.popen`. Shaped per dialect after liolib.c 5.1–5.5 — the
3//! file metatable's layout, what the operations return, how read formats
4//! are spelled and how numbers are written all changed between versions.
5//!
6//! A handle's payload plays the part of C stdio's `FILE`: `write_buf` is its
7//! output buffer, `read_buf[read_pos..]` its input buffer (which also holds
8//! pushed-back bytes).
9
10use std::io::{Read, Seek, SeekFrom, Write};
11
12use crate::numeric::{self, FloatFmt, Num};
13use crate::runtime::value::f2i_exact;
14use crate::runtime::{FileHandle, Gc, Table, Userdata, UserdataPayload, Value};
15use crate::version::LuaVersion;
16use crate::vm::argcheck::{self, Args};
17use crate::vm::builtins::{arg_error, raise_str};
18use crate::vm::error::LuaError;
19use crate::vm::exec::Vm;
20
21/// `EINVAL` / `EBADF` / `ESPIPE`: the errno values stdio reports for the
22/// failures luna detects itself rather than receiving from the OS.
23const EINVAL: i32 = 22;
24const ENOMEM: i32 = 12;
25const EBADF: i32 = 9;
26#[cfg(not(unix))]
27const ESPIPE: i32 = 29;
28
29/// `LUAL_BUFFERSIZE` for `setvbuf`'s default size; luna's buffers do not
30/// take a size, but the argument is still checked.
31const LUAL_BUFFERSIZE: i64 = 1024;
32
33/// Bytes pulled from the OS per refill of a handle's input buffer.
34const READ_CHUNK: usize = 4096;
35
36pub(crate) fn open_io(vm: &mut Vm) {
37    let v = vm.version();
38    let io = vm.heap.new_table();
39    for (name, f) in [
40        ("close", io_close as crate::runtime::value::NativeFn),
41        ("flush", io_flush),
42        ("input", io_input),
43        ("lines", io_lines),
44        ("open", io_open),
45        ("output", io_output),
46        ("popen", io_popen),
47        ("read", io_read),
48        ("tmpfile", io_tmpfile),
49        ("type", io_type),
50        ("write", io_write),
51    ] {
52        put_native(vm, io, name, f);
53    }
54    // The method `close`: 5.1 and 5.2 register `io_close` itself (5.1 with an
55    // environment that has no default output, so a missing argument checks
56    // nil); 5.3 split it into `f_close`.
57    let close: crate::runtime::value::NativeFn = match v {
58        LuaVersion::Lua51 => f_close_51,
59        LuaVersion::Lua52 => io_close,
60        _ => f_close,
61    };
62    let methods: [(&str, crate::runtime::value::NativeFn); 7] = [
63        ("close", close),
64        ("flush", f_flush),
65        ("lines", f_lines),
66        ("read", f_read),
67        ("seek", f_seek),
68        ("setvbuf", f_setvbuf),
69        ("write", f_write),
70    ];
71    // ≤5.3 keep the methods in the metatable itself (`__index = mt`); 5.4
72    // moved them to a table of their own and added `__close`. `__name` comes
73    // from `luaL_newmetatable`, which only sets it from 5.3 on.
74    let mt = vm.heap.new_table();
75    let index = if v >= LuaVersion::Lua54 {
76        vm.heap.new_table()
77    } else {
78        mt
79    };
80    for (name, f) in methods {
81        put_native(vm, index, name, f);
82    }
83    put(vm, mt, "__index", Value::Table(index));
84    put_native(vm, mt, "__gc", f_gc);
85    put_native(vm, mt, "__tostring", f_tostring);
86    if v >= LuaVersion::Lua53 {
87        let n = Value::Str(vm.heap.intern(b"FILE*"));
88        put(vm, mt, "__name", n);
89    }
90    if v >= LuaVersion::Lua54 {
91        // a to-be-closed file already shut by the user must not re-error,
92        // hence the __gc body rather than f_close
93        put_native(vm, mt, "__close", f_gc);
94    }
95    vm.barrier_back_table(index);
96    vm.barrier_back_table(mt);
97    vm.file_mt = Some(mt);
98
99    for (name, fh) in [
100        ("stdin", FileHandle::Stdin),
101        ("stdout", FileHandle::Stdout),
102        ("stderr", FileHandle::Stderr),
103    ] {
104        let writable = !matches!(fh, FileHandle::Stdin);
105        let h = new_file(vm, fh, writable);
106        put(vm, io, name, Value::Userdata(h));
107        match name {
108            "stdin" => {
109                vm.io_input = Some(h);
110                vm.io_stdin = Some(h);
111            }
112            "stdout" => vm.io_output = Some(h),
113            _ => {}
114        }
115    }
116    vm.set_global("io", Value::Table(io))
117        .expect("stdlib registration");
118    vm.barrier_back_table(io);
119}
120
121impl Vm {
122    /// C `fgets(buf, size, stdin)`, for a host that reads lines as lua.c's
123    /// REPL does: the bytes up to and including the next newline, at most
124    /// `size - 1` of them; `None` at the end of input. The bytes come
125    /// through the io library's `io.stdin` buffer, so Lua code reading
126    /// stdin in between sees what follows, as it shares C's `stdin`.
127    pub fn read_stdin_line(&mut self, size: usize) -> std::io::Result<Option<Vec<u8>>> {
128        let mut line = Vec::new();
129        if let Some(u) = self.io_stdin {
130            while line.len() + 1 < size {
131                let Some(b) = getc(u)? else { break };
132                line.push(b);
133                if b == b'\n' {
134                    break;
135                }
136            }
137        } else {
138            // no io library: nothing else buffers stdin
139            use std::io::BufRead;
140            let mut input = std::io::stdin().lock();
141            while line.len() + 1 < size && line.last() != Some(&b'\n') {
142                let buf = input.fill_buf()?;
143                if buf.is_empty() {
144                    break;
145                }
146                let room = &buf[..buf.len().min(size - 1 - line.len())];
147                let n = room
148                    .iter()
149                    .position(|&b| b == b'\n')
150                    .map_or(room.len(), |i| i + 1);
151                line.extend_from_slice(&room[..n]);
152                input.consume(n);
153            }
154        }
155        Ok((!line.is_empty()).then_some(line))
156    }
157}
158
159fn put(vm: &mut Vm, t: Gc<Table>, k: &str, v: Value) {
160    let k = Value::Str(vm.heap.intern(k.as_bytes()));
161    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
162    unsafe { t.as_mut() }
163        .set(&mut vm.heap, k, v)
164        .expect("valid key");
165}
166
167fn put_native(vm: &mut Vm, t: Gc<Table>, k: &str, f: crate::runtime::value::NativeFn) {
168    let fv = vm.native(f);
169    put(vm, t, k, fv);
170}
171
172// ---- errors in the shape of luaL_fileresult / luaL_execresult ----
173
174/// An errno luna reports itself. std reads a raw code as a Win32 error on
175/// Windows, where 22 is "The device does not recognize the command."; the C
176/// runtime's `strerror`, which PUC prints, says "Invalid argument".
177fn posix_error(code: i32) -> std::io::Error {
178    #[cfg(windows)]
179    {
180        std::io::Error::other(PosixErrno(code))
181    }
182    #[cfg(not(windows))]
183    {
184        std::io::Error::from_raw_os_error(code)
185    }
186}
187
188#[cfg(windows)]
189#[derive(Debug)]
190struct PosixErrno(i32);
191
192#[cfg(windows)]
193impl std::fmt::Display for PosixErrno {
194    // the MSVC C runtime's strerror texts for the codes luna raises
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.write_str(match self.0 {
197            EBADF => "Bad file descriptor",
198            ENOMEM => "Not enough space",
199            EINVAL => "Invalid argument",
200            ESPIPE => "Invalid seek",
201            _ => "Unknown error",
202        })
203    }
204}
205
206#[cfg(windows)]
207impl std::error::Error for PosixErrno {}
208
209/// The errno of an error, as `luaL_fileresult` returns it.
210fn errno(e: &std::io::Error) -> Option<i32> {
211    #[cfg(windows)]
212    if let Some(p) = e.get_ref().and_then(|r| r.downcast_ref::<PosixErrno>()) {
213        return Some(p.0);
214    }
215    e.raw_os_error()
216}
217
218/// C `strerror` text of an OS error (std appends " (os error N)").
219pub(crate) fn strerror(e: &std::io::Error) -> String {
220    let s = e.to_string();
221    match e.raw_os_error() {
222        Some(code) => s
223            .strip_suffix(&format!(" (os error {code})"))
224            .expect("std renders OS errors with an '(os error N)' suffix")
225            .to_string(),
226        None => s,
227    }
228}
229
230/// `luaL_fileresult` for a failure: `(nil, "[fname: ]strerror", errno)`.
231pub(crate) fn file_fail(vm: &mut Vm, fs: u32, fname: Option<&[u8]>, e: &std::io::Error) -> u32 {
232    let vals = file_fail_values(vm, fname, e);
233    vm.nat_return(fs, &vals)
234}
235
236fn file_fail_values(vm: &mut Vm, fname: Option<&[u8]>, e: &std::io::Error) -> [Value; 3] {
237    let mut msg = Vec::new();
238    if let Some(n) = fname {
239        msg.extend_from_slice(c_str(n));
240        msg.extend_from_slice(b": ");
241    }
242    msg.extend_from_slice(strerror(e).as_bytes());
243    let code = errno(e).map_or(0, i64::from);
244    let m = Value::Str(vm.heap.intern(&msg));
245    [Value::Nil, m, Value::Int(code)]
246}
247
248/// `luaL_fileresult` for success.
249fn file_ok(vm: &mut Vm, fs: u32) -> u32 {
250    vm.nat_return(fs, &[Value::Bool(true)])
251}
252
253/// `luaL_execresult` on a waited-for child (5.2+): `(true|nil, "exit"|
254/// "signal", code)`, or the file-result triple when waiting failed.
255#[cfg(any(unix, windows))]
256pub(crate) fn exec_result(
257    vm: &mut Vm,
258    fs: u32,
259    status: std::io::Result<std::process::ExitStatus>,
260) -> u32 {
261    let status = match status {
262        Ok(s) => s,
263        Err(e) => return file_fail(vm, fs, None, &e),
264    };
265    let (what, code) = exit_status_breakdown(&status);
266    let ok = if what == "exit" && code == 0 {
267        Value::Bool(true)
268    } else {
269        Value::Nil
270    };
271    let w = Value::Str(vm.heap.intern(what.as_bytes()));
272    vm.nat_return(fs, &[ok, w, Value::Int(code as i64)])
273}
274
275/// `l_inspectstat`: `WIFEXITED` → ("exit", status), `WIFSIGNALED` →
276/// ("signal", signal). Windows has no signals.
277#[cfg(any(unix, windows))]
278pub(crate) fn exit_status_breakdown(status: &std::process::ExitStatus) -> (&'static str, i32) {
279    #[cfg(unix)]
280    {
281        use std::os::unix::process::ExitStatusExt;
282        if let Some(sig) = status.signal() {
283            return ("signal", sig);
284        }
285    }
286    (
287        "exit",
288        status
289            .code()
290            .expect("a status that is not a signal carries a code"),
291    )
292}
293
294// ---- paths ----
295
296/// The part of a Lua string a C function sees as a file name: up to the
297/// first NUL.
298pub(crate) fn c_str(b: &[u8]) -> &[u8] {
299    b.split(|&c| c == 0)
300        .next()
301        .expect("split yields a first piece")
302}
303
304/// A C file name (bytes up to the first NUL) as an OS path.
305pub(crate) fn os_path(b: &[u8]) -> std::path::PathBuf {
306    #[cfg(unix)]
307    {
308        use std::os::unix::ffi::OsStrExt;
309        std::ffi::OsStr::from_bytes(c_str(b)).into()
310    }
311    #[cfg(not(unix))]
312    {
313        String::from_utf8_lossy(c_str(b)).into_owned().into()
314    }
315}
316
317// ---- handles ----
318
319/// A new handle carrying the `FILE*` metatable. Like `luaL_setmetatable` on
320/// a metatable with `__gc`, this registers it for finalization, so a file
321/// nobody closed is still flushed and closed when collected or at state
322/// close. `writable` gives it a user-space output buffer.
323fn new_file(vm: &mut Vm, fh: FileHandle, writable: bool) -> Gc<Userdata> {
324    let u = vm.heap.new_userdata(UserdataPayload::File(fh), writable);
325    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
326    unsafe { u.as_mut() }.set_metatable(vm.file_mt);
327    vm.heap.register_finalizable_userdata(u);
328    u
329}
330
331/// `luaL_testudata(L, i, LUA_FILEHANDLE)`: a userdata whose metatable is the
332/// `FILE*` one (luna only gives that metatable to file payloads, but
333/// `debug.setmetatable` can hand it to any userdata).
334fn test_file(vm: &Vm, v: Value) -> Option<Gc<Userdata>> {
335    match v {
336        Value::Userdata(u)
337            if matches!(u.payload, UserdataPayload::File(_))
338                && u.metatable()
339                    .zip(vm.file_mt)
340                    .is_some_and(|(a, b)| a.ptr_eq(b)) =>
341        {
342            Some(u)
343        }
344        _ => None,
345    }
346}
347
348/// `tolstream`: argument `i` must be a `FILE*`.
349fn check_stream(vm: &mut Vm, a: Args, i: u32) -> Result<Gc<Userdata>, LuaError> {
350    match test_file(vm, a.get(vm, i)) {
351        Some(u) if !a.is_none(i) => Ok(u),
352        _ => Err(argcheck::type_error(vm, a, i, "FILE*")),
353    }
354}
355
356/// `tofile`: argument `i` must be an open `FILE*`.
357fn check_open(vm: &mut Vm, a: Args, i: u32) -> Result<Gc<Userdata>, LuaError> {
358    let u = check_stream(vm, a, i)?;
359    if u.file().is_closed() {
360        return Err(raise_str(vm, "attempt to use a closed file"));
361    }
362    Ok(u)
363}
364
365/// Drain every open handle's output buffer and stdout (C `fflush(NULL)`).
366pub(crate) fn flush_all(vm: &mut Vm) {
367    for u in vm.heap.finalizable_userdata() {
368        if matches!(u.payload, UserdataPayload::File(ref fh) if !fh.is_closed()) {
369            // like fflush(NULL), a stream that fails to flush does not stop
370            // the others and reports nothing
371            let _ = drain_write_buf(u);
372        }
373    }
374    let _ = std::io::stdout().flush(); // same: fflush(NULL) reports nothing
375}
376
377// ---- closing ----
378
379/// What closing a stream did (`aux_close` → `io_noclose` / `io_fclose` /
380/// `io_pclose`).
381enum Closed {
382    /// a standard stream, left open
383    Std,
384    /// a regular file: the flush-on-close result
385    File(std::io::Result<()>),
386    /// a popen stream: the wait result
387    #[cfg(any(unix, windows))]
388    Pipe(std::io::Result<std::process::ExitStatus>),
389}
390
391fn close_stream(u: Gc<Userdata>) -> Closed {
392    if u.file().is_std() {
393        return Closed::Std;
394    }
395    let flushed = drain_write_buf(u);
396    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
397    let m = unsafe { u.as_mut() };
398    // dropping the handle closes the descriptor; for a pipe the child then
399    // sees EOF before we wait for it
400    *m.file_mut() = FileHandle::Closed;
401    m.read_buf = Vec::new();
402    m.read_pos = 0;
403    #[cfg(any(unix, windows))]
404    if let Some(mut child) = m.popen_child.take() {
405        return Closed::Pipe(child.wait());
406    }
407    Closed::File(flushed)
408}
409
410fn push_closed(vm: &mut Vm, fs: u32, c: Closed) -> u32 {
411    match c {
412        Closed::Std => {
413            let m = Value::Str(vm.heap.intern(b"cannot close standard file"));
414            vm.nat_return(fs, &[Value::Nil, m])
415        }
416        Closed::File(Ok(())) => file_ok(vm, fs),
417        Closed::File(Err(e)) => file_fail(vm, fs, None, &e),
418        // 5.1's `lua_pclose` only tells whether pclose itself worked
419        #[cfg(any(unix, windows))]
420        Closed::Pipe(r) if vm.version() == LuaVersion::Lua51 => match r {
421            Ok(_) => file_ok(vm, fs),
422            Err(e) => file_fail(vm, fs, None, &e),
423        },
424        #[cfg(any(unix, windows))]
425        Closed::Pipe(r) => exec_result(vm, fs, r),
426    }
427}
428
429/// `io.close([file])`, also the 5.2 method: no argument means the default
430/// output.
431fn io_close(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
432    let u = if nargs == 0 {
433        let d = default_file(vm, Io::Output);
434        if d.file().is_closed() {
435            return Err(raise_str(vm, "attempt to use a closed file"));
436        }
437        d
438    } else {
439        check_open(vm, Args::new(fs, nargs), 0)?
440    };
441    let c = close_stream(u);
442    Ok(push_closed(vm, fs, c))
443}
444
445/// 5.1's method `close` is `io_close` with an environment lacking the
446/// default output, so without an argument it checks a nil.
447fn f_close_51(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
448    if nargs == 0 {
449        return Err(arg_error(vm, 1, "FILE* expected, got nil"));
450    }
451    f_close(vm, fs, nargs)
452}
453
454fn f_close(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
455    let u = check_open(vm, Args::new(fs, nargs), 0)?;
456    let c = close_stream(u);
457    Ok(push_closed(vm, fs, c))
458}
459
460/// `__gc` (and 5.4's `__close`): close an open handle, ignoring the outcome.
461fn f_gc(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
462    let u = check_stream(vm, Args::new(fs, nargs), 0)?;
463    if !u.file().is_closed() {
464        let _ = close_stream(u); // PUC's f_gc drops aux_close's results
465    }
466    Ok(vm.nat_return(fs, &[]))
467}
468
469fn f_tostring(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
470    let u = check_stream(vm, Args::new(fs, nargs), 0)?;
471    let s = if u.file().is_closed() {
472        "file (closed)".to_string()
473    } else {
474        format!("file ({:p})", u.as_ptr())
475    };
476    let v = Value::Str(vm.heap.intern(s.as_bytes()));
477    Ok(vm.nat_return(fs, &[v]))
478}
479
480fn io_type(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
481    let v = argcheck::check_any(vm, Args::new(fs, nargs), 0)?;
482    let r = match test_file(vm, v) {
483        None => Value::Nil,
484        Some(u) if u.file().is_closed() => Value::Str(vm.heap.intern(b"closed file")),
485        Some(_) => Value::Str(vm.heap.intern(b"file")),
486    };
487    Ok(vm.nat_return(fs, &[r]))
488}
489
490// ---- opening ----
491
492/// How `fopen` opens a mode; `None` when the mode is not one `fopen`
493/// accepts (EINVAL).
494fn fopen_options(mode: &[u8]) -> Option<(std::fs::OpenOptions, bool)> {
495    let mut o = std::fs::OpenOptions::new();
496    let first = *mode.first()?;
497    let rest = &mode[1..];
498    let plus = rest.contains(&b'+');
499    // macOS fopen honours 'x' (O_EXCL) and ignores the other extra letters
500    let excl = rest.contains(&b'x');
501    let writable = match first {
502        b'r' => {
503            o.read(true).write(plus);
504            plus
505        }
506        b'w' => {
507            o.write(true).read(plus).truncate(true);
508            if excl {
509                o.create_new(true);
510            } else {
511                o.create(true);
512            }
513            true
514        }
515        b'a' => {
516            o.append(true).read(plus);
517            if excl {
518                o.create_new(true);
519            } else {
520                o.create(true);
521            }
522            true
523        }
524        _ => return None,
525    };
526    Some((o, writable))
527}
528
529/// `l_checkmode`: 5.2 accepts `[rwa]%+?b?`, 5.3+ `[rwa]%+?b*`; 5.1 checks
530/// nothing and lets `fopen` decide.
531fn mode_ok(v: LuaVersion, mode: &[u8]) -> bool {
532    let Some((&first, rest)) = mode.split_first() else {
533        return false;
534    };
535    if !b"rwa".contains(&first) {
536        return false;
537    }
538    let rest = rest.strip_prefix(b"+").unwrap_or(rest);
539    match v {
540        LuaVersion::Lua52 => rest.is_empty() || rest == b"b",
541        _ => rest.iter().all(|&c| c == b'b'),
542    }
543}
544
545fn open_file(name: &[u8], mode: &[u8]) -> std::io::Result<(std::fs::File, bool)> {
546    let (o, writable) = fopen_options(c_str(mode)).ok_or_else(|| posix_error(EINVAL))?;
547    Ok((o.open(os_path(name))?, writable))
548}
549
550fn io_open(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
551    let a = Args::new(fs, nargs);
552    let name = argcheck::check_string(vm, a, 0)?.as_bytes().to_vec();
553    let mode = match argcheck::opt_string(vm, a, 1)? {
554        Some(m) => m.as_bytes().to_vec(),
555        None => b"r".to_vec(),
556    };
557    if vm.version() >= LuaVersion::Lua52 && !mode_ok(vm.version(), &mode) {
558        return Err(arg_error(vm, 2, "invalid mode"));
559    }
560    match open_file(&name, &mode) {
561        Ok((f, writable)) => {
562            let u = new_file(vm, FileHandle::File(f), writable);
563            Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
564        }
565        Err(e) => Ok(file_fail(vm, fs, Some(&name), &e)),
566    }
567}
568
569fn io_tmpfile(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
570    use std::sync::atomic::{AtomicU64, Ordering};
571    static CTR: AtomicU64 = AtomicU64::new(0);
572    let n = CTR.fetch_add(1, Ordering::Relaxed);
573    let mut path = std::env::temp_dir();
574    path.push(format!("lua_tmp_{}_{n}", std::process::id()));
575    let file = match std::fs::OpenOptions::new()
576        .read(true)
577        .write(true)
578        .create_new(true)
579        .open(&path)
580    {
581        Ok(f) => f,
582        Err(e) => return Ok(file_fail(vm, fs, None, &e)),
583    };
584    // tmpfile(3) leaves no name behind; the open handle keeps the file.
585    if let Err(e) = std::fs::remove_file(&path) {
586        return Ok(file_fail(vm, fs, None, &e));
587    }
588    let u = new_file(vm, FileHandle::File(file), true);
589    Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
590}
591
592/// `io.popen(prog [, mode])`: a `/bin/sh -c prog` child with its stdout
593/// (`"r"`) or stdin (`"w"`) as the stream.
594#[cfg(any(unix, windows))]
595fn io_popen(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
596    let a = Args::new(fs, nargs);
597    let prog = argcheck::check_string(vm, a, 0)?.as_bytes().to_vec();
598    let mode = match argcheck::opt_string(vm, a, 1)? {
599        Some(m) => m.as_bytes().to_vec(),
600        None => b"r".to_vec(),
601    };
602    // 5.3+ check the mode (`l_checkmodep`); before that popen(3) did, and
603    // refuses anything but r/w with EINVAL. ("r+"/"w+", a two-way stream on
604    // BSD popen, is not provided.)
605    let read = match c_str(&mode) {
606        b"r" => true,
607        b"w" => false,
608        _ if vm.version() >= LuaVersion::Lua53 => return Err(arg_error(vm, 2, "invalid mode")),
609        _ => {
610            let e = posix_error(EINVAL);
611            return Ok(file_fail(vm, fs, Some(&prog), &e));
612        }
613    };
614    // `l_popen` flushes every output stream first (`fflush(NULL)`), so the
615    // child sees what the parent wrote before it.
616    flush_all(vm);
617    let mut cmd = shell_command(&prog);
618    if read {
619        cmd.stdout(std::process::Stdio::piped());
620    } else {
621        cmd.stdin(std::process::Stdio::piped());
622    }
623    let mut child = match cmd.spawn() {
624        Ok(c) => c,
625        Err(e) => return Ok(file_fail(vm, fs, Some(&prog), &e)),
626    };
627    let file = if read {
628        pipe_file(child.stdout.take().expect("stdout was piped"))
629    } else {
630        pipe_file(child.stdin.take().expect("stdin was piped"))
631    };
632    let u = new_file(vm, FileHandle::File(file), !read);
633    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
634    unsafe { u.as_mut() }.popen_child = Some(child);
635    Ok(vm.nat_return(fs, &[Value::Userdata(u)]))
636}
637
638/// A child's pipe end as a plain file, so reads and writes share one path.
639#[cfg(unix)]
640fn pipe_file(p: impl Into<std::os::fd::OwnedFd>) -> std::fs::File {
641    std::fs::File::from(p.into())
642}
643
644#[cfg(windows)]
645fn pipe_file(p: impl Into<std::os::windows::io::OwnedHandle>) -> std::fs::File {
646    std::fs::File::from(p.into())
647}
648
649/// Targets without processes (`wasm32-wasip1`): the ISO C `l_popen`, which
650/// raises "'popen' not supported" after the argument checks.
651#[cfg(not(any(unix, windows)))]
652fn io_popen(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
653    let a = Args::new(fs, nargs);
654    argcheck::check_string(vm, a, 0)?;
655    argcheck::opt_string(vm, a, 1)?;
656    Err(raise_str(vm, "'popen' not supported"))
657}
658
659/// The shell `system(3)` and `popen(3)` run a command through.
660#[cfg(any(unix, windows))]
661pub(crate) fn shell_command(cmd: &[u8]) -> std::process::Command {
662    #[cfg(unix)]
663    {
664        use std::os::unix::ffi::OsStrExt;
665        let mut c = std::process::Command::new("/bin/sh");
666        c.arg("-c").arg(std::ffi::OsStr::from_bytes(c_str(cmd)));
667        c
668    }
669    #[cfg(windows)]
670    {
671        let mut c = std::process::Command::new("cmd");
672        c.arg("/C")
673            .arg(String::from_utf8_lossy(c_str(cmd)).into_owned());
674        c
675    }
676}
677
678// ---- default streams ----
679
680#[derive(Clone, Copy)]
681enum Io {
682    Input,
683    Output,
684}
685
686fn default_file(vm: &Vm, which: Io) -> Gc<Userdata> {
687    match which {
688        Io::Input => vm.io_input,
689        Io::Output => vm.io_output,
690    }
691    .expect("default streams are set when io opens")
692}
693
694/// `getiofile`: the default stream, which must still be open.
695fn get_io_file(vm: &mut Vm, which: Io) -> Result<Gc<Userdata>, LuaError> {
696    let u = default_file(vm, which);
697    if u.file().is_closed() {
698        let what = match which {
699            Io::Input => "input",
700            Io::Output => "output",
701        };
702        let adj = if vm.version() >= LuaVersion::Lua54 {
703            "default"
704        } else {
705            "standard"
706        };
707        return Err(raise_str(vm, &format!("{adj} {what} file is closed")));
708    }
709    Ok(u)
710}
711
712/// `g_iofile`: set the default stream from a file name or a handle, then
713/// return it.
714fn g_iofile(vm: &mut Vm, fs: u32, nargs: u32, which: Io) -> Result<u32, LuaError> {
715    let a = Args::new(fs, nargs);
716    if !a.is_none_or_nil(vm, 0) {
717        let v = a.get(vm, 0);
718        let u = match argcheck::to_str_bytes(vm, v) {
719            Some(name) => {
720                let mode: &[u8] = match which {
721                    Io::Input => b"r",
722                    Io::Output => b"w",
723                };
724                open_checked(vm, &name, mode)?
725            }
726            None => check_open(vm, a, 0)?,
727        };
728        match which {
729            Io::Input => vm.io_input = Some(u),
730            Io::Output => vm.io_output = Some(u),
731        }
732    }
733    let cur = default_file(vm, which);
734    Ok(vm.nat_return(fs, &[Value::Userdata(cur)]))
735}
736
737/// `opencheck` (5.2+) / 5.1's `fileerror`: open or raise.
738fn open_checked(vm: &mut Vm, name: &[u8], mode: &[u8]) -> Result<Gc<Userdata>, LuaError> {
739    match open_file(name, mode) {
740        Ok((f, writable)) => Ok(new_file(vm, FileHandle::File(f), writable)),
741        Err(e) => {
742            let n = String::from_utf8_lossy(c_str(name)).into_owned();
743            let err = strerror(&e);
744            Err(if vm.version() == LuaVersion::Lua51 {
745                arg_error(vm, 1, &format!("{n}: {err}"))
746            } else {
747                raise_str(vm, &format!("cannot open file '{n}' ({err})"))
748            })
749        }
750    }
751}
752
753fn io_input(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
754    g_iofile(vm, fs, nargs, Io::Input)
755}
756
757fn io_output(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
758    g_iofile(vm, fs, nargs, Io::Output)
759}
760
761// ---- the stream: buffered bytes over the OS handle ----
762
763/// Refill the input buffer; `false` at end of file.
764fn fill(u: Gc<Userdata>) -> std::io::Result<bool> {
765    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
766    let m = unsafe { u.as_mut() };
767    let mut chunk = vec![0u8; READ_CHUNK];
768    let n = match m.file_mut() {
769        FileHandle::File(f) => f.read(&mut chunk)?,
770        FileHandle::Stdin => std::io::stdin().read(&mut chunk)?,
771        // stdout/stderr are write-only streams
772        FileHandle::Stdout | FileHandle::Stderr => {
773            return Err(posix_error(EBADF));
774        }
775        FileHandle::Closed => unreachable!("reads check the stream is open"),
776    };
777    chunk.truncate(n);
778    m.read_buf = chunk;
779    m.read_pos = 0;
780    Ok(n > 0)
781}
782
783/// `getc`.
784fn getc(u: Gc<Userdata>) -> std::io::Result<Option<u8>> {
785    if u.read_pos >= u.read_buf.len() && !fill(u)? {
786        return Ok(None);
787    }
788    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
789    let m = unsafe { u.as_mut() };
790    let b = m.read_buf[m.read_pos];
791    m.read_pos += 1;
792    Ok(Some(b))
793}
794
795/// `ungetc` of any number of bytes: the next reads return `bytes` first.
796fn unget(u: Gc<Userdata>, bytes: &[u8]) {
797    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
798    let m = unsafe { u.as_mut() };
799    if m.read_pos >= bytes.len() && m.read_buf[m.read_pos - bytes.len()..m.read_pos] == *bytes {
800        m.read_pos -= bytes.len();
801        return;
802    }
803    let mut buf = bytes.to_vec();
804    buf.extend_from_slice(&m.read_buf[m.read_pos..]);
805    m.read_buf = buf;
806    m.read_pos = 0;
807}
808
809/// Bytes buffered ahead of the logical position.
810fn read_ahead(u: Gc<Userdata>) -> i64 {
811    (u.read_buf.len() - u.read_pos) as i64
812}
813
814/// Give back read-ahead before the position is used for something else
815/// (a write, a seek): the OS position is that far past the logical one.
816fn unread_ahead(u: Gc<Userdata>) -> std::io::Result<()> {
817    let ahead = read_ahead(u);
818    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
819    let m = unsafe { u.as_mut() };
820    if ahead > 0
821        && let FileHandle::File(f) = m.file_mut()
822    {
823        f.seek(SeekFrom::Current(-ahead))?;
824    }
825    m.read_buf = Vec::new();
826    m.read_pos = 0;
827    Ok(())
828}
829
830/// Write `bytes` straight to the OS handle.
831fn write_to(u: Gc<Userdata>, bytes: &[u8]) -> std::io::Result<()> {
832    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
833    match unsafe { u.as_mut() }.file_mut() {
834        FileHandle::File(f) => f.write_all(bytes),
835        FileHandle::Stdout => std::io::stdout().write_all(bytes),
836        FileHandle::Stderr => std::io::stderr().write_all(bytes),
837        FileHandle::Stdin => Err(posix_error(EBADF)),
838        FileHandle::Closed => unreachable!("writes check the stream is open"),
839    }
840}
841
842/// Drain the output buffer to the OS. The buffer is emptied either way: C
843/// stdio drops what it failed to write and reports the error.
844fn drain_write_buf(u: Gc<Userdata>) -> std::io::Result<()> {
845    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
846    let buf = std::mem::take(&mut unsafe { u.as_mut() }.write_buf);
847    if buf.is_empty() {
848        return Ok(());
849    }
850    write_to(u, &buf)
851}
852
853/// Put `bytes` on the stream through its buffering mode.
854fn put_bytes(u: Gc<Userdata>, bytes: &[u8]) -> std::io::Result<()> {
855    if !matches!(u.file(), FileHandle::File(_)) || !u.writable {
856        // standard streams are buffered by std; a read-only file fails here
857        return write_to(u, bytes);
858    }
859    unread_ahead(u)?;
860    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
861    let m = unsafe { u.as_mut() };
862    match m.buf_mode {
863        BUF_NO => write_to(u, bytes),
864        BUF_LINE => {
865            m.write_buf.extend_from_slice(bytes);
866            match m.write_buf.iter().rposition(|&b| b == b'\n') {
867                Some(nl) => {
868                    let out: Vec<u8> = m.write_buf.drain(..=nl).collect();
869                    write_to(u, &out)
870                }
871                None => Ok(()),
872            }
873        }
874        _ => {
875            m.write_buf.extend_from_slice(bytes);
876            Ok(())
877        }
878    }
879}
880
881/// `setvbuf` modes as kept in `Userdata::buf_mode`.
882const BUF_FULL: u8 = 0;
883const BUF_LINE: u8 = 1;
884const BUF_NO: u8 = 2;
885
886fn flush_stream(u: Gc<Userdata>) -> std::io::Result<()> {
887    drain_write_buf(u)?;
888    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
889    match unsafe { u.as_mut() }.file_mut() {
890        FileHandle::File(f) => f.flush(),
891        FileHandle::Stdout => std::io::stdout().flush(),
892        FileHandle::Stderr => std::io::stderr().flush(),
893        FileHandle::Stdin | FileHandle::Closed => Ok(()),
894    }
895}
896
897// ---- writing ----
898
899/// How the dialect writes a number: ≤5.2 `%.14g`; 5.3/5.4 `%lld` for an
900/// integer and `%.14g` for a float (so no ".0"); 5.5 converts as tostring.
901fn number_text(vm: &Vm, n: Num) -> Vec<u8> {
902    let fmt = match vm.version() {
903        LuaVersion::Lua55 => vm.float_fmt(),
904        _ => FloatFmt::Legacy14,
905    };
906    numeric::num_to_string_for(n, fmt).into_bytes()
907}
908
909/// `g_write`: write `vals` in order and give the dialect's result.
910fn g_write(vm: &mut Vm, fs: u32, u: Gc<Userdata>, args: Args, first: u32) -> Result<u32, LuaError> {
911    let v = vm.version();
912    let mut total: i64 = 0;
913    let mut failure: Option<std::io::Error> = None;
914    for i in first..args.n {
915        let bytes = match args.get(vm, i) {
916            Value::Int(x) => number_text(vm, Num::Int(x)),
917            Value::Float(f) => number_text(vm, Num::Float(f)),
918            _ => argcheck::check_string(vm, args, i)?.as_bytes().to_vec(),
919        };
920        // ≤5.4 stop writing after a failure but still check the remaining
921        // arguments; 5.5 returns at the first failure
922        if failure.is_some() {
923            continue;
924        }
925        match put_bytes(u, &bytes) {
926            Ok(()) => total += bytes.len() as i64,
927            Err(e) if v >= LuaVersion::Lua55 => {
928                let mut vals = file_fail_values(vm, None, &e).to_vec();
929                vals.push(Value::Int(total));
930                return Ok(vm.nat_return(fs, &vals));
931            }
932            Err(e) => failure = Some(e),
933        }
934    }
935    Ok(match failure {
936        Some(e) => file_fail(vm, fs, None, &e),
937        // 5.1 reports success as true; 5.2+ return the file
938        None if v == LuaVersion::Lua51 => file_ok(vm, fs),
939        None => vm.nat_return(fs, &[Value::Userdata(u)]),
940    })
941}
942
943fn io_write(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
944    let u = get_io_file(vm, Io::Output)?;
945    g_write(vm, fs, u, Args::new(fs, nargs), 0)
946}
947
948fn f_write(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
949    let a = Args::new(fs, nargs);
950    let u = check_open(vm, a, 0)?;
951    g_write(vm, fs, u, a, 1)
952}
953
954fn io_flush(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
955    let u = get_io_file(vm, Io::Output)?;
956    Ok(match flush_stream(u) {
957        Ok(()) => file_ok(vm, fs),
958        Err(e) => file_fail(vm, fs, None, &e),
959    })
960}
961
962fn f_flush(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
963    let u = check_open(vm, Args::new(fs, nargs), 0)?;
964    Ok(match flush_stream(u) {
965        Ok(()) => file_ok(vm, fs),
966        Err(e) => file_fail(vm, fs, None, &e),
967    })
968}
969
970// ---- seek / setvbuf ----
971
972fn f_seek(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
973    let a = Args::new(fs, nargs);
974    let u = check_open(vm, a, 0)?;
975    let op = argcheck::check_option(vm, a, 1, Some("cur"), &["set", "cur", "end"])?;
976    let offset = if vm.version() == LuaVersion::Lua52 {
977        // 5.2 reads a float and requires it to survive the cast to off_t
978        let p3 = argcheck::opt_number(vm, a, 2, 0.0)?;
979        let off = p3 as i64;
980        if off as f64 != p3 {
981            return Err(arg_error(vm, 3, "not an integer in proper range"));
982        }
983        off
984    } else {
985        argcheck::opt_integer(vm, a, 2, 0)?
986    };
987    match seek_stream(u, op, offset) {
988        // ≤5.2 has one number type: `lua_pushnumber(ftell(f))`
989        Ok(pos) if vm.version() <= LuaVersion::Lua52 => {
990            Ok(vm.nat_return(fs, &[Value::Float(pos as f64)]))
991        }
992        Ok(pos) => Ok(vm.nat_return(fs, &[Value::Int(pos as i64)])),
993        Err(e) => Ok(file_fail(vm, fs, None, &e)),
994    }
995}
996
997/// `fseek` + `ftell`: flush pending output, give back read-ahead, move.
998fn seek_stream(u: Gc<Userdata>, op: usize, offset: i64) -> std::io::Result<u64> {
999    drain_write_buf(u)?;
1000    let ahead = read_ahead(u);
1001    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1002    let m = unsafe { u.as_mut() };
1003    let from = match op {
1004        0 if offset < 0 => return Err(posix_error(EINVAL)),
1005        0 => SeekFrom::Start(offset as u64),
1006        1 => match offset.checked_sub(ahead) {
1007            Some(off) => SeekFrom::Current(off),
1008            None => return Err(posix_error(EINVAL)),
1009        },
1010        _ => SeekFrom::End(offset),
1011    };
1012    let pos = match m.file_mut() {
1013        FileHandle::File(f) => f.seek(from)?,
1014        std_stream => seek_std(std_stream, from)?,
1015    };
1016    m.read_buf = Vec::new();
1017    m.read_pos = 0;
1018    Ok(pos)
1019}
1020
1021/// Seek a standard stream through a duplicate of its descriptor, which
1022/// shares the offset (and fails with ESPIPE on a terminal or pipe).
1023#[cfg(unix)]
1024fn seek_std(fh: &FileHandle, from: SeekFrom) -> std::io::Result<u64> {
1025    use std::os::fd::AsFd;
1026    let fd = match fh {
1027        FileHandle::Stdin => std::io::stdin().as_fd().try_clone_to_owned()?,
1028        FileHandle::Stdout => std::io::stdout().as_fd().try_clone_to_owned()?,
1029        FileHandle::Stderr => std::io::stderr().as_fd().try_clone_to_owned()?,
1030        FileHandle::File(_) | FileHandle::Closed => unreachable!("only standard streams"),
1031    };
1032    std::fs::File::from(fd).seek(from)
1033}
1034
1035#[cfg(not(unix))]
1036fn seek_std(_fh: &FileHandle, _from: SeekFrom) -> std::io::Result<u64> {
1037    Err(posix_error(ESPIPE))
1038}
1039
1040fn f_setvbuf(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
1041    let a = Args::new(fs, nargs);
1042    let u = check_open(vm, a, 0)?;
1043    let op = argcheck::check_option(vm, a, 1, None, &["no", "full", "line"])?;
1044    argcheck::opt_integer(vm, a, 2, LUAL_BUFFERSIZE)?;
1045    let mode = [BUF_NO, BUF_FULL, BUF_LINE][op];
1046    // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1047    unsafe { u.as_mut() }.buf_mode = mode;
1048    if mode == BUF_NO
1049        && let Err(e) = drain_write_buf(u)
1050    {
1051        return Ok(file_fail(vm, fs, None, &e));
1052    }
1053    Ok(file_ok(vm, fs))
1054}
1055
1056// ---- reading ----
1057
1058/// Outcome of `g_read`: the values (the last one nil when a format failed),
1059/// or the I/O error that ends a read (`ferror`).
1060enum ReadOut {
1061    Values(Vec<Value>),
1062    Error(std::io::Error),
1063}
1064
1065/// One read format, parsed.
1066enum Fmt {
1067    Count(i64),
1068    Number,
1069    Line { keep_nl: bool },
1070    All,
1071}
1072
1073/// Parse read format `fmt`, the argument numbered `argno` in errors.
1074fn parse_format(vm: &mut Vm, fmt: Value, argno: u32) -> Result<Fmt, LuaError> {
1075    let v = vm.version();
1076    match fmt {
1077        Value::Int(n) => return Ok(Fmt::Count(n)),
1078        Value::Float(f) if v <= LuaVersion::Lua52 => return Ok(Fmt::Count(f as i64)),
1079        Value::Float(f) => {
1080            return f2i_exact(f)
1081                .map(Fmt::Count)
1082                .ok_or_else(|| arg_error(vm, argno, "number has no integer representation"));
1083        }
1084        _ => {}
1085    }
1086    let spec = match fmt {
1087        Value::Str(s) => s.as_bytes().to_vec(),
1088        _ if v <= LuaVersion::Lua52 => return Err(arg_error(vm, argno, "invalid option")),
1089        _ => {
1090            let tn = argcheck::typename_of(vm, fmt);
1091            return Err(arg_error(vm, argno, &format!("string expected, got {tn}")));
1092        }
1093    };
1094    // ≤5.2 require the '*'; 5.3 made it optional
1095    let body = match spec.strip_prefix(b"*") {
1096        Some(b) => b,
1097        None if v <= LuaVersion::Lua52 => return Err(arg_error(vm, argno, "invalid option")),
1098        None => &spec,
1099    };
1100    Ok(match body.first() {
1101        Some(b'n') => Fmt::Number,
1102        Some(b'l') => Fmt::Line { keep_nl: false },
1103        Some(b'L') if v >= LuaVersion::Lua52 => Fmt::Line { keep_nl: true },
1104        Some(b'a') => Fmt::All,
1105        _ => return Err(arg_error(vm, argno, "invalid format")),
1106    })
1107}
1108
1109/// `g_read`: apply `fmts` in order until one fails. With no formats, read a
1110/// line. `argno0` numbers the first format in argument errors.
1111fn g_read(vm: &mut Vm, u: Gc<Userdata>, fmts: &[Value], argno0: u32) -> Result<ReadOut, LuaError> {
1112    // stdio needs a flush between writing and reading the same stream
1113    if let Err(e) = drain_write_buf(u) {
1114        return Ok(ReadOut::Error(e));
1115    }
1116    if fmts.is_empty() {
1117        return Ok(match read_line(vm, u, false) {
1118            Ok(v) => ReadOut::Values(vec![v]),
1119            Err(e) => ReadOut::Error(e),
1120        });
1121    }
1122    let mut out = Vec::with_capacity(fmts.len());
1123    for (i, &f) in fmts.iter().enumerate() {
1124        let fmt = parse_format(vm, f, argno0 + i as u32)?;
1125        let r = match fmt {
1126            Fmt::Count(n) => read_count(vm, u, n)?,
1127            Fmt::Number => read_number(vm, u),
1128            Fmt::Line { keep_nl } => read_line(vm, u, keep_nl),
1129            Fmt::All => read_all(vm, u),
1130        };
1131        match r {
1132            Ok(v) => {
1133                let stop = v.is_nil();
1134                out.push(v);
1135                if stop {
1136                    break;
1137                }
1138            }
1139            Err(e) => return Ok(ReadOut::Error(e)),
1140        }
1141    }
1142    Ok(ReadOut::Values(out))
1143}
1144
1145fn push_read(vm: &mut Vm, fs: u32, r: ReadOut) -> u32 {
1146    match r {
1147        ReadOut::Values(vals) => vm.nat_return(fs, &vals),
1148        ReadOut::Error(e) => file_fail(vm, fs, None, &e),
1149    }
1150}
1151
1152fn io_read(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
1153    let u = get_io_file(vm, Io::Input)?;
1154    let fmts: Vec<Value> = (0..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
1155    let r = g_read(vm, u, &fmts, 1)?;
1156    Ok(push_read(vm, fs, r))
1157}
1158
1159fn f_read(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
1160    let u = check_open(vm, Args::new(fs, nargs), 0)?;
1161    let fmts: Vec<Value> = (1..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
1162    let r = g_read(vm, u, &fmts, 2)?;
1163    Ok(push_read(vm, fs, r))
1164}
1165
1166/// `BUFSIZ`, the size of the chunks ≤5.2 read a line in (`LUAL_BUFFERSIZE`).
1167#[cfg(any(
1168    target_os = "macos",
1169    target_os = "ios",
1170    target_os = "freebsd",
1171    target_os = "netbsd",
1172    target_os = "openbsd",
1173    target_os = "dragonfly"
1174))]
1175const BUFSIZ: usize = 1024;
1176#[cfg(not(any(
1177    target_os = "macos",
1178    target_os = "ios",
1179    target_os = "freebsd",
1180    target_os = "netbsd",
1181    target_os = "openbsd",
1182    target_os = "dragonfly"
1183)))]
1184const BUFSIZ: usize = 8192;
1185
1186/// `read_line`: up to and excluding (`keep_nl`: including) the newline; nil
1187/// when nothing at all was read.
1188fn read_line(vm: &mut Vm, u: Gc<Userdata>, keep_nl: bool) -> std::io::Result<Value> {
1189    if vm.version() <= LuaVersion::Lua52 {
1190        return read_line_fgets(vm, u, keep_nl);
1191    }
1192    let mut buf = Vec::new();
1193    let mut got_nl = false;
1194    while let Some(c) = getc(u)? {
1195        if c == b'\n' {
1196            got_nl = true;
1197            if keep_nl {
1198                buf.push(c);
1199            }
1200            break;
1201        }
1202        buf.push(c);
1203    }
1204    if got_nl || !buf.is_empty() {
1205        read_str(vm, &buf)
1206    } else {
1207        Ok(Value::Nil)
1208    }
1209}
1210
1211/// ≤5.2's `read_line` reads with `fgets` and measures each chunk with
1212/// `strlen`: a NUL cuts the chunk short there, and a newline after it is
1213/// lost, so the line runs on into the next.
1214fn read_line_fgets(vm: &mut Vm, u: Gc<Userdata>, keep_nl: bool) -> std::io::Result<Value> {
1215    let mut out = Vec::new();
1216    loop {
1217        let mut chunk = Vec::new();
1218        while chunk.len() < BUFSIZ - 1 {
1219            match getc(u)? {
1220                Some(c) => {
1221                    chunk.push(c);
1222                    if c == b'\n' {
1223                        break;
1224                    }
1225                }
1226                None => break,
1227            }
1228        }
1229        if chunk.is_empty() {
1230            return if out.is_empty() {
1231                Ok(Value::Nil)
1232            } else {
1233                read_str(vm, &out)
1234            };
1235        }
1236        // strlen: up to the first NUL, the whole chunk when there is none
1237        let len = chunk.iter().position(|&b| b == 0).unwrap_or(chunk.len());
1238        if len == 0 || chunk[len - 1] != b'\n' {
1239            out.extend_from_slice(&chunk[..len]);
1240        } else {
1241            let end = if keep_nl { len } else { len - 1 };
1242            out.extend_from_slice(&chunk[..end]);
1243            return read_str(vm, &out);
1244        }
1245    }
1246}
1247
1248/// A string read from a file. One longer than a string can hold is an
1249/// allocation failure, which PUC's buffer would meet first.
1250fn read_str(vm: &mut Vm, bytes: &[u8]) -> std::io::Result<Value> {
1251    if bytes.len() > crate::runtime::string::MAX_LEN {
1252        return Err(posix_error(ENOMEM));
1253    }
1254    Ok(Value::Str(vm.heap.intern(bytes)))
1255}
1256
1257/// `read_all`: never fails (an empty string at end of file).
1258fn read_all(vm: &mut Vm, u: Gc<Userdata>) -> std::io::Result<Value> {
1259    let mut buf = Vec::new();
1260    loop {
1261        buf.extend_from_slice(&u.read_buf[u.read_pos..]);
1262        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1263        unsafe { u.as_mut() }.read_pos = u.read_buf.len();
1264        if !fill(u)? {
1265            break;
1266        }
1267    }
1268    read_str(vm, &buf)
1269}
1270
1271/// Sizes no allocator grants; PUC's buffer for them fails before reading.
1272const UNALLOCATABLE: u64 = 1 << 47;
1273
1274/// A byte-count format: `0` tests for end of file; `n` reads up to `n`
1275/// bytes (nil when none were left). A negative count is a huge `size_t`.
1276fn read_count(vm: &mut Vm, u: Gc<Userdata>, n: i64) -> Result<std::io::Result<Value>, LuaError> {
1277    let size = n as u64;
1278    if size == 0 {
1279        return Ok(test_eof(vm, u));
1280    }
1281    // 5.1 reads in chunks, so any size works; 5.2+ size one buffer for the
1282    // whole request, which the allocator refuses for absurd sizes
1283    if size >= UNALLOCATABLE && vm.version() >= LuaVersion::Lua52 {
1284        return Err(match vm.version() {
1285            LuaVersion::Lua52 if size > u64::MAX - 64 => {
1286                vm.plain_err("memory allocation error: block too big")
1287            }
1288            LuaVersion::Lua53 => raise_str(vm, "not enough memory for buffer allocation"),
1289            LuaVersion::Lua55 if size >= i64::MAX as u64 => {
1290                raise_str(vm, "resulting string too large")
1291            }
1292            _ => vm.plain_err("not enough memory"),
1293        });
1294    }
1295    let mut buf = Vec::new();
1296    while (buf.len() as u64) < size {
1297        let want = (size - buf.len() as u64) as usize;
1298        if u.read_pos >= u.read_buf.len() {
1299            match fill(u) {
1300                Ok(true) => {}
1301                Ok(false) => break,
1302                Err(e) => return Ok(Err(e)),
1303            }
1304        }
1305        let take = want.min(u.read_buf.len() - u.read_pos);
1306        buf.extend_from_slice(&u.read_buf[u.read_pos..u.read_pos + take]);
1307        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1308        unsafe { u.as_mut() }.read_pos += take;
1309    }
1310    Ok(if buf.is_empty() {
1311        Ok(Value::Nil)
1312    } else {
1313        read_str(vm, &buf)
1314    })
1315}
1316
1317/// `test_eof`: "" if a byte is left, nil at end of file.
1318fn test_eof(vm: &mut Vm, u: Gc<Userdata>) -> std::io::Result<Value> {
1319    Ok(match getc(u)? {
1320        Some(c) => {
1321            unget(u, &[c]);
1322            Value::Str(vm.heap.intern(b""))
1323        }
1324        None => Value::Nil,
1325    })
1326}
1327
1328fn read_number(vm: &mut Vm, u: Gc<Userdata>) -> std::io::Result<Value> {
1329    if vm.version() <= LuaVersion::Lua52 {
1330        return scan_double(u);
1331    }
1332    let buf = read_numeral(u)?;
1333    Ok(match numeric::str2num(&buf, true, true) {
1334        Some(Num::Int(i)) => Value::Int(i),
1335        Some(Num::Float(f)) => Value::Float(f),
1336        None => Value::Nil,
1337    })
1338}
1339
1340/// ≤5.2 read numbers with `fscanf("%lf")`. The BSD scanner converts the
1341/// longest prefix that is a valid floating-point numeral and pushes back
1342/// every byte after it; with no valid prefix, it pushes everything back.
1343fn scan_double(u: Gc<Userdata>) -> std::io::Result<Value> {
1344    let mut c = getc(u)?;
1345    while matches!(c, Some(b) if is_c_space(b)) {
1346        c = getc(u)?;
1347    }
1348    let mut buf: Vec<u8> = Vec::new();
1349    let mut commit = 0; // length of the longest complete numeral in buf
1350    let mut state = Scan::Start;
1351    while let Some(b) = c {
1352        let next = scan_step(state, b, &buf);
1353        let Some((st, complete)) = next else { break };
1354        buf.push(b);
1355        if complete {
1356            commit = buf.len();
1357        }
1358        state = st;
1359        c = getc(u)?;
1360    }
1361    if let Some(b) = c {
1362        buf.push(b);
1363    }
1364    unget(u, &buf[commit..]);
1365    if commit == 0 {
1366        return Ok(Value::Nil);
1367    }
1368    Ok(Value::Float(parse_c_double(&buf[..commit])))
1369}
1370
1371fn is_c_space(b: u8) -> bool {
1372    matches!(b, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r')
1373}
1374
1375#[derive(Clone, Copy, PartialEq)]
1376enum Scan {
1377    Start,
1378    Sign,
1379    Zero,
1380    Int,
1381    Dot,
1382    Frac,
1383    ExpMark,
1384    ExpSign,
1385    ExpDigits,
1386    HexX,
1387    HexInt,
1388    HexDot,
1389    HexFrac,
1390    Word,
1391    NanOpen,
1392    Done,
1393}
1394
1395/// One step of the `%lf` scanner: the state after `b`, and whether the
1396/// bytes so far form a complete numeral. `None` when `b` cannot continue.
1397fn scan_step(s: Scan, b: u8, buf: &[u8]) -> Option<(Scan, bool)> {
1398    let digits = |st| Some((st, true));
1399    match s {
1400        Scan::Start | Scan::Sign => match b {
1401            b'+' | b'-' if s == Scan::Start => Some((Scan::Sign, false)),
1402            b'0' => digits(Scan::Zero),
1403            b'1'..=b'9' => digits(Scan::Int),
1404            b'.' => Some((Scan::Dot, false)),
1405            b'i' | b'I' | b'n' | b'N' => Some((Scan::Word, false)),
1406            _ => None,
1407        },
1408        Scan::Zero if matches!(b, b'x' | b'X') => Some((Scan::HexX, false)),
1409        Scan::Zero | Scan::Int => match b {
1410            b'0'..=b'9' => digits(Scan::Int),
1411            b'.' => digits(Scan::Frac),
1412            b'e' | b'E' => Some((Scan::ExpMark, false)),
1413            _ => None,
1414        },
1415        Scan::Dot => match b {
1416            b'0'..=b'9' => digits(Scan::Frac),
1417            _ => None,
1418        },
1419        Scan::Frac => match b {
1420            b'0'..=b'9' => digits(Scan::Frac),
1421            b'e' | b'E' => Some((Scan::ExpMark, false)),
1422            _ => None,
1423        },
1424        Scan::ExpMark => match b {
1425            b'+' | b'-' => Some((Scan::ExpSign, false)),
1426            b'0'..=b'9' => digits(Scan::ExpDigits),
1427            _ => None,
1428        },
1429        Scan::ExpSign | Scan::ExpDigits => match b {
1430            b'0'..=b'9' => digits(Scan::ExpDigits),
1431            _ => None,
1432        },
1433        Scan::HexX => match b {
1434            b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => digits(Scan::HexInt),
1435            b'.' => Some((Scan::HexDot, false)),
1436            _ => None,
1437        },
1438        Scan::HexInt => match b {
1439            b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => digits(Scan::HexInt),
1440            b'.' => digits(Scan::HexFrac),
1441            b'p' | b'P' => Some((Scan::ExpMark, false)),
1442            _ => None,
1443        },
1444        Scan::HexDot | Scan::HexFrac => match b {
1445            b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => digits(Scan::HexFrac),
1446            b'p' | b'P' if s == Scan::HexFrac => Some((Scan::ExpMark, false)),
1447            _ => None,
1448        },
1449        Scan::Word => {
1450            // "inf", "infinity", "nan", compared case-insensitively
1451            let word: Vec<u8> = buf
1452                .iter()
1453                .skip_while(|&&c| c == b'+' || c == b'-')
1454                .map(u8::to_ascii_lowercase)
1455                .chain(std::iter::once(b.to_ascii_lowercase()))
1456                .collect();
1457            if b"infinity".starts_with(&word) {
1458                Some((Scan::Word, word == b"inf" || word == b"infinity"))
1459            } else if b"nan".starts_with(&word) {
1460                Some((Scan::Word, word == b"nan"))
1461            } else if word == b"nan(" {
1462                Some((Scan::NanOpen, false))
1463            } else {
1464                None
1465            }
1466        }
1467        Scan::NanOpen => match b {
1468            b')' => Some((Scan::Done, true)),
1469            b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' => Some((Scan::NanOpen, false)),
1470            _ => None,
1471        },
1472        Scan::Done => None,
1473    }
1474}
1475
1476/// `strtod` on a complete numeral from `scan_double`.
1477fn parse_c_double(s: &[u8]) -> f64 {
1478    let (neg, body) = match s.split_first() {
1479        Some((b'-', rest)) => (true, rest),
1480        Some((b'+', rest)) => (false, rest),
1481        _ => (false, s),
1482    };
1483    let lower = body.to_ascii_lowercase();
1484    let mag = if lower.starts_with(b"inf") {
1485        f64::INFINITY
1486    } else if lower.starts_with(b"nan") {
1487        f64::NAN
1488    } else if lower.starts_with(b"0x") {
1489        match numeric::str2num(body, false, true) {
1490            Some(n) => n.as_f64(),
1491            None => unreachable!("the scanner only commits valid hex numerals"),
1492        }
1493    } else {
1494        std::str::from_utf8(body)
1495            .expect("decimal numerals are ASCII")
1496            .parse::<f64>()
1497            .expect("the scanner only commits valid decimal numerals")
1498    };
1499    if neg { -mag } else { mag }
1500}
1501
1502/// Cap on a numeral's length for 5.3+'s reader (`L_MAXLENNUM`).
1503const L_MAXLENNUM: usize = 200;
1504
1505/// 5.3+ `read_number`'s state: the look-ahead byte and the saved prefix.
1506struct Rn {
1507    buf: Vec<u8>,
1508    c: Option<u8>,
1509}
1510
1511/// `nextc`: keep the look-ahead byte and read the next. Past
1512/// `L_MAXLENNUM` the numeral is invalidated and reading stops.
1513fn rn_next(rn: &mut Rn, u: Gc<Userdata>) -> std::io::Result<bool> {
1514    if rn.buf.len() >= L_MAXLENNUM {
1515        rn.buf.clear();
1516        return Ok(false);
1517    }
1518    if let Some(b) = rn.c {
1519        rn.buf.push(b);
1520    }
1521    rn.c = getc(u)?;
1522    Ok(true)
1523}
1524
1525/// `test2`: take the look-ahead byte if it is one of `set`.
1526fn rn_test(rn: &mut Rn, u: Gc<Userdata>, set: &[u8]) -> std::io::Result<bool> {
1527    if matches!(rn.c, Some(c) if set.contains(&c)) {
1528        return rn_next(rn, u);
1529    }
1530    Ok(false)
1531}
1532
1533/// `readdigits`.
1534fn rn_digits(rn: &mut Rn, u: Gc<Userdata>, hex: bool) -> std::io::Result<u32> {
1535    let mut count = 0;
1536    while matches!(rn.c, Some(c) if if hex { c.is_ascii_hexdigit() } else { c.is_ascii_digit() })
1537        && rn_next(rn, u)?
1538    {
1539        count += 1;
1540    }
1541    Ok(count)
1542}
1543
1544/// 5.3+ `read_number`'s scan: the longest prefix following a fixed numeral
1545/// grammar, with the first byte that does not fit pushed back.
1546fn read_numeral(u: Gc<Userdata>) -> std::io::Result<Vec<u8>> {
1547    let mut c = getc(u)?;
1548    while matches!(c, Some(b) if is_c_space(b)) {
1549        c = getc(u)?;
1550    }
1551    let mut rn = Rn { buf: Vec::new(), c };
1552    let mut count = 0;
1553    let mut hex = false;
1554    rn_test(&mut rn, u, b"-+")?;
1555    if rn_test(&mut rn, u, b"0")? {
1556        if rn_test(&mut rn, u, b"xX")? {
1557            hex = true;
1558        } else {
1559            count = 1;
1560        }
1561    }
1562    count += rn_digits(&mut rn, u, hex)?;
1563    if rn_test(&mut rn, u, b".")? {
1564        count += rn_digits(&mut rn, u, hex)?;
1565    }
1566    if count > 0 && rn_test(&mut rn, u, if hex { b"pP" } else { b"eE" })? {
1567        rn_test(&mut rn, u, b"-+")?;
1568        rn_digits(&mut rn, u, false)?;
1569    }
1570    if let Some(b) = rn.c {
1571        unget(u, &[b]);
1572    }
1573    Ok(rn.buf)
1574}
1575
1576// ---- lines ----
1577
1578/// Most read formats a line iterator may carry: 5.2 `LUA_MINSTACK - 3`,
1579/// 5.3+ `MAXARGLINE`. The argument number in the error is the limit's own
1580/// (5.2) or two past it (5.3+).
1581fn check_line_formats(vm: &mut Vm, n: u32) -> Result<(), LuaError> {
1582    let (max, argno, msg) = match vm.version() {
1583        LuaVersion::Lua51 => return Ok(()),
1584        LuaVersion::Lua52 => (17, 17, "too many options"),
1585        _ => (250, 252, "too many arguments"),
1586    };
1587    if n > max {
1588        return Err(arg_error(vm, argno, msg));
1589    }
1590    Ok(())
1591}
1592
1593/// `aux_lines`: an iterator over `u` with upvalues [file, toclose, fmt...].
1594/// 5.1's iterator takes no formats.
1595fn make_lines(vm: &mut Vm, u: Gc<Userdata>, toclose: bool, fmts: &[Value]) -> Value {
1596    let mut up = vec![Value::Userdata(u), Value::Bool(toclose)];
1597    if vm.version() >= LuaVersion::Lua52 {
1598        up.extend_from_slice(fmts);
1599    }
1600    vm.native_with(io_readline, up.into_boxed_slice())
1601}
1602
1603fn f_lines(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
1604    let u = check_open(vm, Args::new(fs, nargs), 0)?;
1605    check_line_formats(vm, nargs.saturating_sub(1))?;
1606    let fmts: Vec<Value> = (1..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
1607    let it = make_lines(vm, u, false, &fmts);
1608    Ok(vm.nat_return(fs, &[it]))
1609}
1610
1611fn io_lines(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
1612    let a = Args::new(fs, nargs);
1613    // (5.1 checks index 1 after pushing the default input above it, so an
1614    // explicit nil fails there; luna treats nil as "no file name", as the
1615    // manual and every later version do.)
1616    let (u, toclose) = if a.is_none_or_nil(vm, 0) {
1617        let d = default_file(vm, Io::Input);
1618        if d.file().is_closed() {
1619            return Err(raise_str(vm, "attempt to use a closed file"));
1620        }
1621        (d, false)
1622    } else {
1623        let name = argcheck::check_string(vm, a, 0)?.as_bytes().to_vec();
1624        (open_checked(vm, &name, b"r")?, true)
1625    };
1626    let nfmt = nargs.saturating_sub(1);
1627    check_line_formats(vm, nfmt)?;
1628    let fmts: Vec<Value> = (1..nargs).map(|i| vm.nat_arg(fs, nargs, i)).collect();
1629    let it = make_lines(vm, u, toclose, &fmts);
1630    // 5.4+ return the file as the generic for's closing value
1631    if toclose && vm.version() >= LuaVersion::Lua54 {
1632        return Ok(vm.nat_return(fs, &[it, Value::Nil, Value::Nil, Value::Userdata(u)]));
1633    }
1634    Ok(vm.nat_return(fs, &[it]))
1635}
1636
1637/// `io_readline`: one step of a line iterator.
1638fn io_readline(vm: &mut Vm, fs: u32, _nargs: u32) -> Result<u32, LuaError> {
1639    let Value::Userdata(u) = vm.nat_upval(fs, 0) else {
1640        unreachable!("line iterator upvalue 0 is its file");
1641    };
1642    if u.file().is_closed() {
1643        return Err(raise_str(vm, "file is already closed"));
1644    }
1645    let fmts: Vec<Value> = (2..vm.nat_upcount(fs))
1646        .map(|i| vm.nat_upval(fs, i))
1647        .collect();
1648    let vals = match g_read(vm, u, &fmts, 2)? {
1649        ReadOut::Values(v) => v,
1650        // the read's error message is raised
1651        ReadOut::Error(e) => return Err(raise_str(vm, &strerror(&e))),
1652    };
1653    // ≤5.2 continue on a non-nil first value, 5.3+ on a true one; the only
1654    // false-ish value a read produces is nil, so the tests agree
1655    if !vals[0].is_nil() {
1656        return Ok(vm.nat_return(fs, &vals));
1657    }
1658    if let Value::Bool(true) = vm.nat_upval(fs, 1) {
1659        let _ = close_stream(u); // aux_close's results are dropped here too
1660    }
1661    Ok(vm.nat_return(fs, &[]))
1662}