luallaby 0.1.0

**Work in progress** A pure-Rust Lua interpreter/compiler
Documentation
use std::cell::RefCell;
use std::fs::File;
use std::process::{Command, Stdio};
use std::rc::Rc;

use super::os::tmpname;
use super::StdLib;
use crate::error::Result;
use crate::value::{ChildMode, FileDesc, FileHandle, FileMode, FuncBuiltin, Table, UserData};
use crate::{Error, LuaError, Value, VM};

pub(super) fn module(stdlib: &mut StdLib) -> Result<()> {
    fn stdio(desc: FileDesc) -> Result<Value> {
        Ok(Value::UserData(UserData::File(super::file::_create(
            desc,
            Rc::new(RefCell::new(Table::default())),
            Rc::new(RefCell::new(Table::default())),
        )?)))
    }

    stdlib
        .module("io")
        .func("close", close)?
        .func("flush", flush)?
        .func("input", input)?
        .func("lines", lines)?
        .func("open", open)?
        .func("output", output)?
        .func("popen", popen)?
        .func("read", read)?
        .cons("stdin", stdio(FileDesc::StdIn)?)?
        .cons("stdout", stdio(FileDesc::StdOut)?)?
        .cons("stderr", stdio(FileDesc::StdErr)?)?
        .func("tmpfile", tmpfile)?
        .func("type", r#type)?
        .func("write", write)?;

    Ok(())
}

fn close(vm: &mut VM) -> Result<Value> {
    Ok(super::file::_close_helper(
        vm.arg_opt(0)
            .as_ref()
            .map(Value::to_file)
            .transpose()?
            .unwrap_or_else(|| vm.out.clone()),
    ))
}

fn flush(vm: &mut VM) -> Result<Value> {
    if vm.out.borrow().is_closed() {
        err!(LuaError::FileDefaultOutputClosed)
    } else {
        Ok(super::file::_flush_helper(vm.out.clone()))
    }
}

fn input(vm: &mut VM) -> Result<Value> {
    match vm.arg_or_nil(0) {
        Value::Nil => {}
        Value::String(str) => {
            let filename = String::from_utf8_lossy(&str).into_owned();
            match File::options().read(true).open(&filename) {
                Ok(file) => vm.inp = super::file::create(vm, FileDesc::file(file, FileMode::Read))?,
                Err(e) => return err!(LuaError::FileOpen(filename, e.kind().to_string())),
            }
        }
        file => vm.inp = file.to_file().map_err(|e| vm.arg_error(0, e))?,
    }

    Ok(Value::UserData(UserData::File(vm.inp.clone())))
}

fn lines(vm: &mut VM) -> Result<Value> {
    let filename = vm.arg_opt(0);
    let args = vm.arg_split(1);
    if args.len() > 250 {
        return err!(LuaError::ArgumentTooMany);
    }

    let file = match &filename {
        None | Some(Value::Nil) => vm.inp.clone(),
        Some(v) => {
            let filename = String::from_utf8_lossy(v.to_string()?).into_owned();
            let file = FileDesc::file(File::options().read(true).open(&filename)?, FileMode::Read);
            super::file::create(vm, file)?
        }
    };
    let to_close = !matches!(filename, None | Some(Value::Nil));

    let file_func = file.clone();
    let func = Value::Func(vm.alloc_builtin(FuncBuiltin {
        module: "io",
        name: "__lines",
        func: Rc::new(move |_| __lines(file_func.clone(), args.clone(), to_close)),
    }));
    let mut ret = vec![func, Value::Nil, Value::Nil];
    if to_close {
        ret.push(Value::UserData(UserData::File(file)));
    }
    Ok(Value::Mult(ret))
}

fn __lines(file: Rc<RefCell<FileHandle>>, args: Vec<Value>, to_close: bool) -> Result<Value> {
    if file.borrow().is_closed() {
        err!(LuaError::FileAlreadyClosed)
    } else {
        let res = super::file::_read_helper(file.clone(), args)?;
        if res.to_single() == &Value::Nil && to_close {
            super::file::_close_helper(file);
        }
        Ok(res)
    }
}

fn open(vm: &mut VM) -> Result<Value> {
    let mode = match vm.arg_or_nil(1) {
        Value::Nil => vec![b'r'],
        v => v.into_string()?,
    };
    let mode = String::from_utf8(mode)
        .ok()
        .as_ref()
        .map(String::as_str)
        .and_then(FileMode::from_str)
        .ok_or_else(|| Error::from_lua(LuaError::FileOpenMode))?;
    let filename = vm.arg(0)?;
    let filename = String::from_utf8_lossy(filename.to_string()?);
    let file = mode.options().open(filename.into_owned());

    Ok(match file {
        Ok(file) => Value::UserData(UserData::File(super::file::create(
            vm,
            FileDesc::file(file, mode),
        )?)),
        Err(e) => e.into(),
    })
}

fn output(vm: &mut VM) -> Result<Value> {
    match vm.arg_or_nil(0) {
        Value::Nil => {}
        Value::String(str) => {
            let filename = String::from_utf8_lossy(&str).into_owned();
            match File::options()
                .write(true)
                .truncate(true)
                .create(true)
                .open(&filename)
            {
                Ok(file) => {
                    vm.out = super::file::create(vm, FileDesc::file(file, FileMode::Write))?
                }
                Err(e) => return err!(LuaError::FileOpen(filename, e.kind().to_string())),
            }
        }
        file => vm.out = file.to_file()?,
    }

    Ok(Value::UserData(UserData::File(vm.out.clone())))
}

fn popen(vm: &mut VM) -> Result<Value> {
    let prog = vm.arg_string_coerce(0)?;
    let prog = String::from_utf8_lossy(&prog).into_owned();
    let mode = match vm.arg_opt(1) {
        Some(mode) => {
            let mode = mode.to_string_coerce()?;
            match &mode[..] {
                &[b'r'] => ChildMode::Read,
                &[b'w'] => ChildMode::Write,
                _ => return err!(LuaError::FileOpenMode),
            }
        }
        None => ChildMode::Read,
    };

    let child = Command::new("/bin/sh")
        .arg("-c")
        .arg(prog)
        .stdin(match mode {
            ChildMode::Read => Stdio::inherit(),
            ChildMode::Write => Stdio::piped(),
        })
        .stdout(match mode {
            ChildMode::Read => Stdio::piped(),
            ChildMode::Write => Stdio::inherit(),
        })
        .stderr(Stdio::inherit())
        .spawn()?;

    Ok(Value::UserData(UserData::File(super::file::create(
        vm,
        FileDesc::Child(Some(child), mode),
    )?)))
}

fn read(vm: &mut VM) -> Result<Value> {
    if vm.inp.borrow().is_closed() {
        err!(LuaError::FileDefaultInputClosed)
    } else {
        super::file::_read_helper(vm.inp.clone(), vm.arg_split(0))
    }
}

fn tmpfile(vm: &mut VM) -> Result<Value> {
    let name = tmpname(vm)?;
    vm.frames.last_mut().unwrap().varargs = vec![name, Value::str("w+")];
    open(vm)
}

fn r#type(vm: &mut VM) -> Result<Value> {
    Ok(match vm.arg_or_nil(0) {
        Value::UserData(UserData::File(file)) => match file.borrow().desc() {
            FileDesc::File(None, ..) => Value::str("closed file"),
            _ => Value::str("file"),
        },
        _ => Value::Nil,
    })
}

fn write(vm: &mut VM) -> Result<Value> {
    if vm.out.borrow().is_closed() {
        err!(LuaError::FileDefaultOutputClosed)
    } else {
        let args = (0..vm.arg_len())
            .map(|idx| vm.arg_string_coerce(idx))
            .collect::<Result<Vec<_>>>()?;
        Ok(super::file::_write_helper(vm.out.clone(), args))
    }
}