use std::cell::Cell;
use std::io;
use omnilua::{HostHooks, Lua, LuaFileHandle, LuaVersion};
#[derive(Clone, Copy)]
enum FailMode {
RawErrno(i32),
NonRaw,
Success,
}
thread_local! {
static MODE: Cell<FailMode> = const { Cell::new(FailMode::RawErrno(0)) };
}
fn set_mode(mode: FailMode) {
MODE.with(|c| c.set(mode));
}
fn current_err() -> io::Result<()> {
match MODE.with(Cell::get) {
FailMode::RawErrno(code) => Err(io::Error::from_raw_os_error(code)),
FailMode::NonRaw => Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"synthetic capability failure",
)),
FailMode::Success => Ok(()),
}
}
fn failing_open(_filename: &[u8], _mode: &[u8]) -> io::Result<Box<dyn LuaFileHandle>> {
current_err()?;
unreachable!("Success mode is not exercised on the open path (needs a real handle)")
}
fn failing_remove(_filename: &[u8]) -> io::Result<()> {
current_err()
}
fn failing_rename(_from: &[u8], _to: &[u8]) -> io::Result<()> {
current_err()
}
fn lua_with_failing_fs(version: LuaVersion) -> Lua {
let hooks = HostHooks::new()
.file_open(failing_open)
.file_remove(failing_remove)
.file_rename(failing_rename);
Lua::with_hooks_versioned(hooks, version).expect("init lua with failing fs hooks")
}
fn eval_str(version: LuaVersion, code: &str) -> String {
let lua = lua_with_failing_fs(version);
lua.load(code)
.set_name(b"=errno_kit")
.eval()
.unwrap_or_else(|e| panic!("eval of `{code}` failed under {version:?}: {e:?}"))
}
const ALL: [LuaVersion; 5] = [
LuaVersion::V51,
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
];
#[cfg(unix)]
const ENOENT: i32 = 2;
#[cfg(unix)]
const EACCES: i32 = 13;
fn triple_code(call: &str) -> String {
format!(
"local ok, msg, errno = {call}; \
return tostring(ok) .. '|' .. tostring(msg) .. '|' .. tostring(errno) \
.. '|' .. tostring(select('#', {call}))"
)
}
#[cfg(unix)]
#[test]
fn io_open_reports_enoent_errno_and_clean_message() {
set_mode(FailMode::RawErrno(ENOENT));
let code = triple_code("io.open('/nonexistent/x')");
for v in ALL {
let got = eval_str(v, &code);
assert_eq!(
got, "nil|/nonexistent/x: No such file or directory|2|3",
"{v:?}: io.open ENOENT should be the faithful 3-value triple with errno 2 and a \
clean strerror message, got `{got}`"
);
}
}
#[cfg(unix)]
#[test]
fn io_open_reports_eacces_errno_and_clean_message() {
set_mode(FailMode::RawErrno(EACCES));
let code = triple_code("io.open('/root/protected')");
for v in ALL {
let got = eval_str(v, &code);
assert_eq!(
got, "nil|/root/protected: Permission denied|13|3",
"{v:?}: io.open EACCES should carry errno 13 and a clean strerror message, got `{got}`"
);
}
}
#[cfg(unix)]
#[test]
fn os_remove_reports_enoent_errno_and_clean_message() {
set_mode(FailMode::RawErrno(ENOENT));
let code = triple_code("os.remove('/nonexistent/x')");
for v in ALL {
let got = eval_str(v, &code);
assert_eq!(
got, "nil|/nonexistent/x: No such file or directory|2|3",
"{v:?}: os.remove ENOENT should carry errno 2 and a clean strerror message, got `{got}`"
);
}
}
#[cfg(unix)]
#[test]
fn os_rename_reports_enoent_errno_and_version_gated_message() {
set_mode(FailMode::RawErrno(ENOENT));
let code = triple_code("os.rename('/nonexistent/x', 'y')");
for v in ALL {
let got = eval_str(v, &code);
let expected = match v {
LuaVersion::V51 => "nil|/nonexistent/x: No such file or directory|2|3",
_ => "nil|No such file or directory|2|3",
};
assert_eq!(
got, expected,
"{v:?}: os.rename ENOENT should carry errno 2, got `{got}`"
);
}
}
#[cfg(unix)]
#[test]
fn io_input_open_failure_is_version_gated() {
set_mode(FailMode::RawErrno(ENOENT));
let code = "local ok, err = pcall(io.input, '/nonexistent/x'); return tostring(err)";
for v in ALL {
let got = eval_str(v, code);
let expected = match v {
LuaVersion::V51 => "bad argument #1 to '?' (/nonexistent/x: No such file or directory)",
_ => "cannot open file '/nonexistent/x' (No such file or directory)",
};
assert_eq!(
got, expected,
"{v:?}: io.input open-failure message was `{got}`"
);
}
}
#[test]
fn io_open_non_os_error_does_not_report_errno_zero() {
set_mode(FailMode::NonRaw);
let code = triple_code("io.open('/whatever')");
for v in ALL {
let got = eval_str(v, &code);
let (_ok_msg, tail) = got.rsplit_once('|').unwrap();
assert!(
tail == "2",
"{v:?}: a non-OS io.open failure must be the 2-value (nil, msg) result, got `{got}`"
);
let errno_field = got.split('|').nth(2).unwrap();
assert_eq!(
errno_field, "nil",
"{v:?}: a non-OS failure must NOT fabricate errno 0 — third value must be nil, \
got `{got}`"
);
}
}
#[test]
fn os_remove_non_os_error_does_not_report_errno_zero() {
set_mode(FailMode::NonRaw);
let code = triple_code("os.remove('/whatever')");
for v in ALL {
let got = eval_str(v, &code);
let errno_field = got.split('|').nth(2).unwrap();
let count_field = got.rsplit('|').next().unwrap();
assert_eq!(
(errno_field, count_field),
("nil", "2"),
"{v:?}: a non-OS os.remove failure must be (nil, msg) with no fabricated errno, \
got `{got}`"
);
}
}
#[test]
fn os_remove_success_returns_true() {
set_mode(FailMode::Success);
let code = "return tostring(os.remove('/whatever')) .. '|' .. \
tostring(select('#', os.remove('/whatever')))";
for v in ALL {
let got = eval_str(v, code);
assert_eq!(
got, "true|1",
"{v:?}: a successful os.remove must return exactly `true`, got `{got}`"
);
}
}