use std::io::{IsTerminal, Read, Write};
use lua_stdlib::auxlib::{self, load_buffer};
use lua_stdlib::init::open_libs;
use lua_types::error::LuaError;
use lua_types::value::LuaValue;
use lua_types::LuaType;
use lua_vm::api;
use lua_vm::state::LuaState;
const MULTRET: i32 = -1;
const PROGNAME_DEFAULT: &[u8] = b"lua";
const LUA_COPYRIGHT: &[u8] = b"Lua 5.4.7 Copyright (C) 1994-2024 Lua.org, PUC-Rio";
const LUA_REGISTRYINDEX: i32 = -1_001_000;
const HAS_ERROR: i32 = 1;
const HAS_I: i32 = 2;
const HAS_V: i32 = 4;
const HAS_E: i32 = 8;
const HAS_BIG_E: i32 = 16;
pub(crate) struct Cli {
pub progname: Option<Vec<u8>>,
}
pub(crate) fn l_message(progname: Option<&[u8]>, msg: &[u8]) {
let mut err = std::io::stderr();
if let Some(p) = progname {
let _ = err.write_all(p);
let _ = err.write_all(b": ");
}
let _ = err.write_all(msg);
let _ = err.write_all(b"\n");
let _ = err.flush();
}
pub(crate) fn error_bytes(e: &LuaError) -> Vec<u8> {
match e {
LuaError::Runtime(v) | LuaError::Syntax(v) => match v {
LuaValue::Str(s) => s.as_bytes().to_vec(),
_ => b"(error object is not a string)".to_vec(),
},
_ => b"(error message not a string)".to_vec(),
}
}
fn lua_remove(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
api::rotate(state, idx, -1);
api::set_top(state, -2)
}
fn msghandler(state: &mut LuaState) -> Result<usize, LuaError> {
let direct = match api::to_lua_string(state, 1) {
Ok(Some(s)) => Some(s.as_bytes().to_vec()),
_ => None,
};
let msg = match direct {
Some(m) => m,
None => {
if auxlib::call_meta(state, 1, b"__tostring")?
&& api::lua_type_at(state, -1) == LuaType::String
{
return Ok(1);
}
let tn = api::type_name(state, api::lua_type_at(state, 1));
format!(
"(error object is a {} value)",
String::from_utf8_lossy(tn)
)
.into_bytes()
}
};
auxlib::traceback(state, None, Some(&msg), 1)?;
Ok(1)
}
fn print_version() {
let mut out = std::io::stdout();
let _ = out.write_all(LUA_COPYRIGHT);
let _ = out.write_all(b"\n");
let _ = out.flush();
}
fn stdin_is_tty() -> bool {
std::io::stdin().is_terminal()
}
fn strip_shebang(mut data: Vec<u8>) -> Vec<u8> {
if data.starts_with(b"\xEF\xBB\xBF") {
data.drain(0..3);
}
if data.first() == Some(&b'#') {
match data.iter().position(|&b| b == b'\n') {
Some(pos) => {
data.drain(0..pos);
}
None => data.clear(),
}
}
data
}
fn io_error_message(e: &std::io::Error) -> String {
let s = e.to_string();
match s.rfind(" (os error ") {
Some(idx) => s[..idx].to_string(),
None => s,
}
}
#[cfg(unix)]
fn path_from_bytes(b: &[u8]) -> std::path::PathBuf {
use std::os::unix::ffi::OsStrExt;
std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b))
}
#[cfg(not(unix))]
fn path_from_bytes(b: &[u8]) -> std::path::PathBuf {
std::path::PathBuf::from(String::from_utf8_lossy(b).into_owned())
}
#[cfg(unix)]
fn env_bytes(key: &str) -> Option<Vec<u8>> {
use std::os::unix::ffi::OsStrExt;
std::env::var_os(key).map(|v| v.as_bytes().to_vec())
}
#[cfg(not(unix))]
fn env_bytes(key: &str) -> Option<Vec<u8>> {
std::env::var_os(key).map(|v| v.to_string_lossy().into_owned().into_bytes())
}
fn createargtable(state: &mut LuaState, argv: &[Vec<u8>], script: i32) -> Result<(), LuaError> {
let argc = argv.len() as i32;
let narg = argc - (script + 1);
api::create_table(state, narg.max(0), script + 1)?;
for i in 0..argc {
api::push_lstring(state, &argv[i as usize])?;
api::raw_set_i(state, -2, (i - script) as i64)?;
}
api::set_global(state, b"arg")
}
fn pushargs(state: &mut LuaState) -> Result<i32, LuaError> {
if api::get_global(state, b"arg")? != LuaType::Table {
return Err(LuaError::runtime(format_args!("'arg' is not a table")));
}
let n = auxlib::lua_len(state, -1)? as i32;
if !api::check_stack(state, n + 3) {
return Err(LuaError::runtime(format_args!(
"too many arguments to script"
)));
}
for i in 1..=n {
api::raw_get_i(state, -i, i as i64);
}
lua_remove(state, -(n + 1))?;
Ok(n)
}
fn collectargs(argv: &[Vec<u8>]) -> (i32, i32) {
let mut args = 0;
let mut first;
let mut i = 1usize;
while i < argv.len() {
first = i as i32;
let a = &argv[i];
if a.is_empty() || a[0] != b'-' {
return (args, first);
}
match a.get(1).copied() {
None => return (args, first),
Some(b'-') => {
if a.len() > 2 {
return (HAS_ERROR, first);
}
return (args, (i + 1) as i32);
}
Some(b'E') => {
if a.len() > 2 {
return (HAS_ERROR, first);
}
args |= HAS_BIG_E;
}
Some(b'W') => {
if a.len() > 2 {
return (HAS_ERROR, first);
}
}
Some(b'i') => {
if a.len() > 2 {
return (HAS_ERROR, first);
}
args |= HAS_I | HAS_V;
}
Some(b'v') => {
if a.len() > 2 {
return (HAS_ERROR, first);
}
args |= HAS_V;
}
Some(b'e') => {
args |= HAS_E;
if a.len() == 2 {
i += 1;
if i >= argv.len() || argv[i].first() == Some(&b'-') {
return (HAS_ERROR, first);
}
}
}
Some(b'l') => {
if a.len() == 2 {
i += 1;
if i >= argv.len() || argv[i].first() == Some(&b'-') {
return (HAS_ERROR, first);
}
}
}
_ => return (HAS_ERROR, first),
}
i += 1;
}
(args, 0)
}
impl Cli {
pub(crate) fn report(&self, r: Result<(), LuaError>) -> bool {
match r {
Ok(()) => true,
Err(e) => {
l_message(self.progname.as_deref(), &error_bytes(&e));
false
}
}
}
pub(crate) fn report_stack_error(&self, state: &mut LuaState) {
let bytes = match api::to_lua_string(state, -1) {
Ok(Some(s)) => s.as_bytes().to_vec(),
_ => b"(error message not a string)".to_vec(),
};
state.pop_n(1);
l_message(self.progname.as_deref(), &bytes);
}
pub(crate) fn docall(&self, state: &mut LuaState, nargs: i32, nres: i32) -> Result<(), LuaError> {
let base = api::get_top(state) - nargs;
api::push_cclosure(state, msghandler, 0)?;
state.insert(base)?;
let r = api::pcall_k(state, nargs, nres, base, 0, None);
let _ = lua_remove(state, base);
r.map(|_| ())
}
fn dostring(&self, state: &mut LuaState, s: &[u8], name: &[u8]) -> bool {
match load_buffer(state, s, name) {
Ok(0) => self.report(self.docall(state, 0, 0)),
Ok(_) => {
self.report_stack_error(state);
false
}
Err(e) => self.report(Err(e)),
}
}
fn dofile(&self, state: &mut LuaState, name: Option<&[u8]>) -> bool {
match self.load_file(state, name) {
Ok(0) => self.report(self.docall(state, 0, 0)),
Ok(_) => {
self.report_stack_error(state);
false
}
Err(e) => self.report(Err(e)),
}
}
fn load_file(&self, state: &mut LuaState, name: Option<&[u8]>) -> Result<i32, LuaError> {
let (data, chunkname) = match name {
Some(path) => {
let raw = std::fs::read(path_from_bytes(path)).map_err(|e| {
LuaError::runtime(format_args!(
"cannot open {}: {}",
String::from_utf8_lossy(path),
io_error_message(&e)
))
})?;
let mut cn = vec![b'@'];
cn.extend_from_slice(path);
(strip_shebang(raw), cn)
}
None => {
let mut raw = Vec::new();
std::io::stdin()
.lock()
.read_to_end(&mut raw)
.map_err(|e| LuaError::runtime(format_args!("stdin read error: {}", e)))?;
(strip_shebang(raw), b"=stdin".to_vec())
}
};
load_buffer(state, &data, &chunkname)
}
fn dolibrary(&self, state: &mut LuaState, globname: &[u8]) -> bool {
let eq = globname.iter().position(|&b| b == b'=');
let (glob_full, modname): (&[u8], &[u8]) = match eq {
Some(p) => (&globname[..p], &globname[p + 1..]),
None => (globname, globname),
};
let effective_glob: &[u8] = if eq.is_none() {
match glob_full.iter().position(|&b| b == b'-') {
Some(p) => &glob_full[..p],
None => glob_full,
}
} else {
glob_full
};
let load = (|| {
if api::get_global(state, b"require")? != LuaType::Function {
return Err(LuaError::runtime(format_args!("'require' is not a function")));
}
api::push_lstring(state, modname)?;
Ok(())
})();
if let Err(e) = load {
return self.report(Err(e));
}
match self.docall(state, 1, 1) {
Ok(()) => match api::set_global(state, effective_glob) {
Ok(()) => true,
Err(e) => self.report(Err(e)),
},
Err(e) => self.report(Err(e)),
}
}
fn handle_luainit(&self, state: &mut LuaState) -> bool {
let (init, name): (Vec<u8>, &[u8]) = match env_bytes("LUA_INIT_5_4") {
Some(v) => (v, b"=LUA_INIT_5_4" as &[u8]),
None => match env_bytes("LUA_INIT") {
Some(v) => (v, b"=LUA_INIT" as &[u8]),
None => return true,
},
};
if init.first() == Some(&b'@') {
self.dofile(state, Some(&init[1..]))
} else {
self.dostring(state, &init, name)
}
}
fn handle_script(&self, state: &mut LuaState, argv: &[Vec<u8>], script: i32) -> bool {
let fname = &argv[script as usize];
let prev = argv.get((script - 1) as usize).map(|v| v.as_slice());
let use_stdin = fname.as_slice() == b"-" && prev != Some(b"--");
let loaded = if use_stdin {
self.load_file(state, None)
} else {
self.load_file(state, Some(fname))
};
match loaded {
Ok(0) => {
let n = match pushargs(state) {
Ok(n) => n,
Err(e) => return self.report(Err(e)),
};
self.report(self.docall(state, n, MULTRET))
}
Ok(_) => {
self.report_stack_error(state);
false
}
Err(e) => self.report(Err(e)),
}
}
fn runargs(&self, state: &mut LuaState, argv: &[Vec<u8>], n: i32) -> bool {
let mut i = 1usize;
while (i as i32) < n {
let a = &argv[i];
let option = a.get(1).copied();
match option {
Some(opt @ b'e') | Some(opt @ b'l') => {
let extra: Vec<u8> = if a.len() > 2 {
a[2..].to_vec()
} else {
i += 1;
argv[i].clone()
};
let ok = if opt == b'e' {
self.dostring(state, &extra, b"=(command line)")
} else {
self.dolibrary(state, &extra)
};
if !ok {
return false;
}
}
Some(b'W') => api::warning(state, b"@on", false),
_ => {}
}
i += 1;
}
true
}
fn print_usage(&self, badoption: &[u8]) {
let mut err = std::io::stderr();
let prog = self.progname.as_deref().unwrap_or(PROGNAME_DEFAULT);
let _ = err.write_all(prog);
let _ = err.write_all(b": ");
if badoption.get(1) == Some(&b'e') || badoption.get(1) == Some(&b'l') {
let _ = writeln!(err, "'{}' needs argument", String::from_utf8_lossy(badoption));
} else {
let _ = writeln!(
err,
"unrecognized option '{}'",
String::from_utf8_lossy(badoption)
);
}
let _ = err.write_all(
b"usage: lua [options] [script [args]]\n\
Available options are:\n\
\x20 -e stat execute string 'stat'\n\
\x20 -i enter interactive mode after executing 'script'\n\
\x20 -l mod require library 'mod' into global 'mod'\n\
\x20 -l g=mod require library 'mod' into global 'g'\n\
\x20 -v show version information\n\
\x20 -E ignore environment variables\n\
\x20 -W turn warnings on\n\
\x20 -- stop handling options\n\
\x20 - stop handling options and execute stdin\n",
);
let _ = err.flush();
}
}
pub(crate) fn run(
state: &mut LuaState,
argv: &[Vec<u8>],
preload: fn(&mut LuaState) -> Result<(), LuaError>,
) -> i32 {
let (args, script) = collectargs(argv);
let mut cli = Cli {
progname: argv
.first()
.filter(|p| !p.is_empty())
.map(|p| p.to_vec()),
};
if args == HAS_ERROR {
let bad = argv.get(script as usize).map(|v| v.as_slice()).unwrap_or(b"");
cli.print_usage(bad);
return 1;
}
if args & HAS_V != 0 {
print_version();
}
if args & HAS_BIG_E != 0 {
api::push_boolean(state, true);
if let Err(e) = api::set_field(state, LUA_REGISTRYINDEX, b"LUA_NOENV") {
return if cli.report(Err(e)) { 0 } else { 1 };
}
}
if script > 0 {
if let Some(dir) = path_from_bytes(&argv[script as usize])
.parent()
.filter(|d| !d.as_os_str().is_empty())
{
crate::prepend_lua_path(dir);
}
}
if let Err(e) = open_libs(state) {
cli.report(Err(e));
return 1;
}
if let Err(e) = preload(state) {
cli.report(Err(e));
return 1;
}
if let Err(e) = createargtable(state, argv, script) {
cli.report(Err(e));
return 1;
}
if args & HAS_BIG_E == 0 && !cli.handle_luainit(state) {
return 1;
}
let optlim = if script > 0 { script } else { argv.len() as i32 };
if !cli.runargs(state, argv, optlim) {
return 1;
}
if script > 0 && !cli.handle_script(state, argv, script) {
return 1;
}
if args & HAS_I != 0 {
crate::repl::do_repl(state, &mut cli);
} else if script < 1 && args & (HAS_E | HAS_V) == 0 {
if stdin_is_tty() {
print_version();
crate::repl::do_repl(state, &mut cli);
} else {
cli.dofile(state, None);
}
}
0
}