ebe0b272d0
UI texture loading and widget method calls now follow 1.12.1. - A UI texture's decoder is chosen by its extension (`0x449e16`), not by the file's content. A refused BLP draws green (`0x44a50e`). A TGA side below 8 fails (`0x58ab40`). A refused TGA minimap mask draws white. - Portraits, `SetCursor` and drag icons open only `.blp` (`0x525e0f`, `0x449610`), and so do the tabard emblem cells (`0x50338a`). - `SetPortraitToTexture` takes a Texture object and ignores a bad path (`0x48d780`). - Every widget method checks its receiver first: a table, then an object, then the class. A bad receiver raises the reference's own lines. The 8 of 481 methods that skip it at the bytes are exempt. Texture arguments to the portrait and taxi globals, `AddFontStrings`' FontStrings and `SetBuybackItem`'s slot are checked the same way. A test asserts that every reference method is wrapped. Fixes #1685 Fixes #2100 Fixes #2099 Fixes #2203 Fixes #2125 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
8.5 KiB
Rust
230 lines
8.5 KiB
Rust
//! The API answers as mlua 0.11's does over the C Lua build, for the calls benilla makes.
|
|
|
|
use benilla_mlua::{Function, Lua};
|
|
|
|
/// `Debug::function` is the function running at the level: `lua_getinfo(L, "f", ar)`.
|
|
#[test]
|
|
fn a_stack_level_names_its_function() {
|
|
let lua = Lua::new();
|
|
let seen = lua
|
|
.create_function(|lua, ()| {
|
|
let mine = lua.inspect_stack(0, |d| d.function()).unwrap();
|
|
let caller = lua.inspect_stack(1, |d| d.function()).unwrap();
|
|
Ok((mine, caller))
|
|
})
|
|
.unwrap();
|
|
lua.globals().set("seen", seen.clone()).unwrap();
|
|
let (mine, caller, f): (Function, Function, Function) = lua
|
|
.load("function F() local a, b = seen() return a, b, F end return F()")
|
|
.eval()
|
|
.unwrap();
|
|
assert_eq!(mine, seen);
|
|
assert_eq!(caller, f);
|
|
}
|
|
|
|
/// An index the host makes with no Lua running raises a plain type error, as `lua_gettable`
|
|
/// does with no function to name a variable in.
|
|
#[test]
|
|
fn a_host_index_through_a_bad_index_metamethod_raises() {
|
|
let lua = Lua::new();
|
|
lua.load("setmetatable(getfenv(0), {__index = true})")
|
|
.exec()
|
|
.unwrap();
|
|
let err = lua.globals().get::<benilla_mlua::Value>("x").unwrap_err();
|
|
assert!(
|
|
err.to_string().contains("attempt to index a boolean value"),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
/// A number reads as `Integer` when `num_traits::cast` to `i64` round-trips its bits, as mlua
|
|
/// reads a 5.1 number: `2^63` is past `i64` and `-0` is not `0`.
|
|
#[test]
|
|
fn numbers_read_as_mlua_reads_them() {
|
|
use benilla_mlua::Value;
|
|
let lua = Lua::new();
|
|
let (a, b, c, d): (Value, Value, Value, Value) = lua
|
|
.load("return 9223372036854775808, -9223372036854775808, -(0), 9007199254740992")
|
|
.eval()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(a, Value::Number(n) if n == 9_223_372_036_854_775_808.0),
|
|
"{a:?}"
|
|
);
|
|
assert!(matches!(b, Value::Integer(i64::MIN)), "{b:?}");
|
|
assert!(
|
|
matches!(c, Value::Number(n) if n == 0.0 && n.is_sign_negative()),
|
|
"{c:?}"
|
|
);
|
|
assert!(matches!(d, Value::Integer(9_007_199_254_740_992)), "{d:?}");
|
|
|
|
let int = |v: Value| lua.coerce_integer(v).unwrap();
|
|
assert_eq!(int(Value::Integer(5)), Some(5));
|
|
assert_eq!(int(Value::Number(1e-17)), Some(0), "within EPSILON of 0");
|
|
assert_eq!(int(Value::Number(0.5)), None);
|
|
assert_eq!(int(Value::Number(-3.0)), Some(-3));
|
|
assert_eq!(
|
|
int(Value::String(lua.create_string(" 7 ").unwrap())),
|
|
Some(7)
|
|
);
|
|
assert_eq!(int(Value::Boolean(true)), None);
|
|
}
|
|
|
|
/// `len`, `push` and `pop` go through `__len`, `__index` and `__newindex` when the table has a
|
|
/// metatable, as mlua's do over `luaL_len`.
|
|
#[test]
|
|
fn length_push_and_pop_use_the_metatable() {
|
|
use benilla_mlua::Table;
|
|
let lua = Lua::new();
|
|
let t: Table = lua
|
|
.load(
|
|
"return setmetatable({1, 2, 3}, {__len = function() return 10 end, \
|
|
__newindex = function(t, k, v) Log = k rawset(t, k, v) end})",
|
|
)
|
|
.eval()
|
|
.unwrap();
|
|
assert_eq!(t.len().unwrap(), 10);
|
|
assert_eq!(t.raw_len(), 3);
|
|
t.push("x").unwrap();
|
|
assert_eq!(lua.globals().get::<i64>("Log").unwrap(), 11);
|
|
let bad: Table = lua
|
|
.load("return setmetatable({}, {__len = function() return 2.5 end})")
|
|
.eval()
|
|
.unwrap();
|
|
assert_eq!(
|
|
bad.len().unwrap_err().to_string(),
|
|
"runtime error: object length is not an integer"
|
|
);
|
|
let empty: Table = lua
|
|
.load("return setmetatable({}, {__index = function(t, k) return 'idx' .. k end})")
|
|
.eval()
|
|
.unwrap();
|
|
assert_eq!(empty.pop::<String>().unwrap(), "idx0");
|
|
let plain = lua.create_table().unwrap();
|
|
assert_eq!(plain.pop::<Option<i64>>().unwrap(), None);
|
|
}
|
|
|
|
/// `raw_insert` and `raw_remove` take an index in the sequence and shift the rest, as mlua's.
|
|
#[test]
|
|
fn raw_insert_and_remove_shift_within_bounds() {
|
|
let lua = Lua::new();
|
|
let t = lua.create_sequence_from([1, 2, 3]).unwrap();
|
|
assert!(t.raw_insert(0, 9).is_err());
|
|
assert!(t.raw_insert(5, 9).is_err());
|
|
t.raw_insert(1, 0).unwrap();
|
|
t.raw_insert(5, 4).unwrap();
|
|
assert!(t.raw_remove(6).is_err());
|
|
t.raw_remove(2).unwrap();
|
|
let got: Vec<i64> = t.sequence_values().map(Result::unwrap).collect();
|
|
assert_eq!(got, [0, 2, 3, 4]);
|
|
t.raw_set("k", 1).unwrap();
|
|
t.raw_remove("k").unwrap();
|
|
assert_eq!(t.raw_get::<Option<i64>>("k").unwrap(), None);
|
|
}
|
|
|
|
/// A table made from an iterator starts empty and grows by rehash, as the client's C code builds
|
|
/// one: five values make an array of eight, and five integer pairs move into it too, the sizes
|
|
/// the client gives `t = {} t[1] = 1 ... t[5] = 5`.
|
|
#[test]
|
|
fn a_table_from_an_iterator_grows_from_empty() {
|
|
let lua = Lua::new();
|
|
let seq = lua.create_sequence_from([1, 2, 3, 4, 5]).unwrap();
|
|
let map = lua
|
|
.create_table_from([(1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e")])
|
|
.unwrap();
|
|
lua.globals().set("Seq", seq).unwrap();
|
|
lua.globals().set("Map", map).unwrap();
|
|
lua.with_vm(|vm| {
|
|
let seq = vm.get_global("Seq").as_table().unwrap();
|
|
let map = vm.get_global("Map").as_table().unwrap();
|
|
assert_eq!(vm.heap.table(seq).array.len(), 8);
|
|
assert_eq!(
|
|
(vm.heap.table(map).array.len(), vm.heap.table(map).lsizenode),
|
|
(8, 0)
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Every chunk compiles as source, whatever its mode (`luaD_protectedparser` zeroes the binary
|
|
/// flag, `0x6f6a15`): a binary chunk meets the lexer's control-byte error (`0x6ffabb`).
|
|
#[test]
|
|
fn a_binary_chunk_is_lexed_as_source() {
|
|
let lua = Lua::new();
|
|
for mode in [None, Some(benilla_mlua::ChunkMode::Text)] {
|
|
let mut chunk = lua.load(&b"\x1bLuaP\x01\x04"[..]).set_name("=bin");
|
|
if let Some(m) = mode {
|
|
chunk = chunk.set_mode(m);
|
|
}
|
|
let Err(benilla_mlua::Error::SyntaxError { message, .. }) = chunk.into_function() else {
|
|
panic!("a binary chunk compiled");
|
|
};
|
|
assert_eq!(message, "bin:1: invalid control char near `char(27)'");
|
|
}
|
|
}
|
|
|
|
/// `Value::to_string` of a table or function is the text Lua's `tostring` gives it: the type and
|
|
/// MSVC's `%p`, eight uppercase hex digits with no `0x`.
|
|
#[test]
|
|
fn an_objects_text_is_the_vms_tostring() {
|
|
let lua = Lua::new();
|
|
let (t, f, ts, fs): (benilla_mlua::Value, benilla_mlua::Value, String, String) = lua
|
|
.load("local t, f = {}, function() end return t, f, tostring(t), tostring(f)")
|
|
.eval()
|
|
.unwrap();
|
|
assert_eq!(t.to_string().unwrap(), ts);
|
|
assert_eq!(f.to_string().unwrap(), fs);
|
|
let hex = ts.strip_prefix("table: ").unwrap();
|
|
assert_eq!(hex.len(), 8, "{ts}");
|
|
assert!(
|
|
hex.bytes()
|
|
.all(|b| b.is_ascii_digit() || (b'A'..=b'F').contains(&b)),
|
|
"{ts}"
|
|
);
|
|
}
|
|
|
|
/// A guarded host function reads its receiver as the 1.12 member prologue does, raises the
|
|
/// check's error before the function converts an argument, and otherwise runs the function itself
|
|
/// with the call's arguments.
|
|
#[test]
|
|
fn a_guarded_function_checks_its_receiver_then_runs() {
|
|
use benilla_mlua::{LightUserData, Receiver, Table, Value};
|
|
let lua = Lua::new();
|
|
let f = lua
|
|
.create_function(|_, (this, n): (Table, i64)| Ok(this.raw_get::<i64>("k")? + n))
|
|
.unwrap();
|
|
let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
|
let log = seen.clone();
|
|
let g = lua
|
|
.guard_receiver(&f, move |_, r| {
|
|
log.borrow_mut().push(r);
|
|
match r {
|
|
Receiver::Object(7) => Ok(()),
|
|
_ => Err(benilla_mlua::Error::runtime("bad receiver")),
|
|
}
|
|
})
|
|
.unwrap()
|
|
.expect("a host function");
|
|
lua.globals().set("g", g).unwrap();
|
|
let t = lua.create_table().unwrap();
|
|
t.raw_set(0, Value::LightUserData(LightUserData(7 as *mut _)))
|
|
.unwrap();
|
|
t.raw_set("k", 40).unwrap();
|
|
lua.globals().set("t", t).unwrap();
|
|
assert_eq!(lua.load("return g(t, 2)").eval::<i64>().unwrap(), 42);
|
|
for (chunk, read) in [
|
|
("return g()", Receiver::NotTable),
|
|
("return g(5, 2)", Receiver::NotTable),
|
|
("return g({}, 'x')", Receiver::NoObject),
|
|
] {
|
|
let err = lua.load(chunk).eval::<i64>().unwrap_err().to_string();
|
|
assert!(err.contains("bad receiver"), "{chunk}: {err}");
|
|
assert_eq!(seen.borrow().last(), Some(&read), "{chunk}");
|
|
}
|
|
// A Lua function is no host closure.
|
|
let lua_fn: Function = lua.load("return function() end").eval().unwrap();
|
|
assert!(lua
|
|
.guard_receiver(&lua_fn, |_, _| Ok(()))
|
|
.unwrap()
|
|
.is_none());
|
|
}
|