b18d08f64f
Lua 5.0 behaviours of the client that an addon can observe, which benilla-lua's 5.1 base got differently: - string.sub and string.find keep a position's low 32 bits (`0x7fbd55`). - An escaped byte at or above 0x80 matches nothing (`0x7fcbc6`). - `error()` checks its argument (`0x702993`) and adds the position at any level but 0 (`0x7029a8`). - `__unm` gets the operand and nil (`0x6f8edc`). - `__index` chains reach 102 tables and `__newindex` 101. - math.deg multiplies by 180/pi (`0x7fb14d`). The bare aliases run as their own `compat.lua` chunk (`0x703b21`), each on the client's line. Its closures keep a local `math`, so their error lines match and replacing the global `math` leaves them working. `seterrorhandler` keeps the top value, and nil clears it (`0x702930`). Each fix has a test. Fixes #2632 Fixes #2633 Fixes #2634 Fixes #2635 Fixes #2636 Fixes #2637 Fixes #2638 Fixes #2639 Fixes #2640 Fixes #2654 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1073 lines
37 KiB
Rust
1073 lines
37 KiB
Rust
//! The string library: 5.1's `lstrlib.c`, whose messages the script host carries (the pattern
|
|
//! matcher with `%f` and `%b`). A pattern ends at its first NUL, as the C matcher reads a C
|
|
//! string.
|
|
|
|
use crate::api::R;
|
|
use crate::cfmt::{self, Spec};
|
|
use crate::value::{Value, V};
|
|
use crate::vm::{LuaError, Vm};
|
|
|
|
pub fn open(vm: &mut Vm) {
|
|
// `luaL_openlib` makes a library's table empty, by `lua_newtable` (`0x6f4dff`).
|
|
let t = vm.heap.new_table(0, 0);
|
|
let fns: [(&'static str, crate::heap::NativeFn); 12] = [
|
|
("len", len),
|
|
("sub", sub),
|
|
("lower", lower),
|
|
("upper", upper),
|
|
("char", char_),
|
|
("rep", rep),
|
|
("byte", byte),
|
|
("format", format),
|
|
("dump", dump),
|
|
("find", find),
|
|
("gfind", gfind),
|
|
("gsub", gsub),
|
|
];
|
|
for (name, f) in fns {
|
|
let v = vm.new_native(name, f);
|
|
let k = vm.str(name.as_bytes());
|
|
vm.heap.table_mut(t).set(k, v).unwrap();
|
|
}
|
|
vm.set_global("string", Value::Table(t));
|
|
}
|
|
|
|
/// The client's `posrelat` (`0x7fbde0`): a negative position counts back from the end, added in
|
|
/// 32 bits (`0x7fbde6`), and one that is still negative stays so; the callers clamp it.
|
|
fn posrelat(pos: i32, len: usize) -> i32 {
|
|
if pos < 0 {
|
|
pos.wrapping_add(len as i32).wrapping_add(1)
|
|
} else {
|
|
pos
|
|
}
|
|
}
|
|
|
|
fn len(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let n = vm.heap.str(s).len();
|
|
Ok(vm.ret1(Value::Num(n as f64)))
|
|
}
|
|
|
|
fn sub(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let l = vm.heap.str(s).len();
|
|
// `str_sub` reads both positions through `__ftol`'s low dword (`0x7fbd55`, `0x7fbd79`) and
|
|
// clamps them as signed 32-bit values (`0x7fbd88`-`0x7fbd9f`).
|
|
let start = vm.check_int(nargs, 2)?;
|
|
let end = vm.opt_int(nargs, 3, -1)?;
|
|
let mut start = posrelat(start, l);
|
|
let mut end = posrelat(end, l);
|
|
if start < 1 {
|
|
start = 1;
|
|
}
|
|
if end > l as i32 {
|
|
end = l as i32;
|
|
}
|
|
if start <= end {
|
|
let (a, e) = (start as usize - 1, end as usize);
|
|
let r = vm
|
|
.heap
|
|
.intern_with(|h, b| b.extend_from_slice(&h.str(s)[a..e]));
|
|
Ok(vm.ret1(Value::Str(r)))
|
|
} else {
|
|
Ok(vm.ret_str(b""))
|
|
}
|
|
}
|
|
|
|
/// The client's `str_lower` (`0x7fbdf0`): each UTF-8 code point through `LowerCodepoint`
|
|
/// (`0x7fc0a0`), `A`-`Z` and U+00C0-U+00DE up by 32.
|
|
fn lower(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
case_map(vm, nargs, |c| match c {
|
|
0x41..=0x5a | 0xc0..=0xde => c + 0x20,
|
|
_ => c,
|
|
})
|
|
}
|
|
|
|
/// The client's `str_upper` (`0x7fc0c0`): the same walk through `UpperCodepoint` (`0x7fc180`),
|
|
/// `a`-`z` and U+00E0-U+00FE down by 32.
|
|
fn upper(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
case_map(vm, nargs, |c| match c {
|
|
0x61..=0x7a | 0xe0..=0xfe => c - 0x20,
|
|
_ => c,
|
|
})
|
|
}
|
|
|
|
/// `str_lower`'s loop: decode a code point, map it, encode it, advance by the bytes the decoder
|
|
/// read (`0x7fbe8a`). A string of ASCII without a NUL maps byte by byte, as the loop would.
|
|
///
|
|
/// Deviation: the decoder reads a NUL as no bytes (`0x7fbeda`), so the client's loop never
|
|
/// passes one and the call never returns; here the result ends there.
|
|
fn case_map(vm: &mut Vm, nargs: usize, map: fn(u32) -> u32) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let r = vm.heap.intern_with(|h, b| {
|
|
let src = h.str(s);
|
|
if src.iter().all(|&c| c != 0 && c < 0x80) {
|
|
b.extend(src.iter().map(|&c| map(c as u32) as u8));
|
|
return;
|
|
}
|
|
let mut i = 0;
|
|
while i < src.len() {
|
|
let (c, n) = utf8_decode(src, i);
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
utf8_encode(map(c), b);
|
|
i += n;
|
|
}
|
|
});
|
|
Ok(vm.ret1(Value::Str(r)))
|
|
}
|
|
|
|
/// The client's `Utf8DecodeChar` (`0x7fbeb0`) at `s[i]`: the code point and the bytes read, the
|
|
/// string ending in the NUL a C string ends in. A lead byte gives its count of continuations by
|
|
/// its masks (`0x7fbef2`-`0x7fbf4e`), up to five, with no check of the value; a byte that leads
|
|
/// nothing, or a continuation that is not `10xxxxxx` (`0x7fbf7c`, counted first at `0x7fbf75`),
|
|
/// is `0x80000000`; a NUL is `0xffffffff`, its own byte not read.
|
|
fn utf8_decode(s: &[u8], i: usize) -> (u32, usize) {
|
|
let at = |k: usize| s.get(k).copied().unwrap_or(0);
|
|
let lead = at(i);
|
|
let (bits, n) = match lead {
|
|
0 => return (u32::MAX, 0),
|
|
_ if lead & 0xfe == 0xfc => (lead & 0x01, 5),
|
|
_ if lead & 0xfc == 0xf8 => (lead & 0x03, 4),
|
|
_ if lead & 0xf8 == 0xf0 => (lead & 0x07, 3),
|
|
_ if lead & 0xf0 == 0xe0 => (lead & 0x0f, 2),
|
|
_ if lead & 0xe0 == 0xc0 => (lead & 0x1f, 1),
|
|
_ if lead & 0x80 != 0 => return (0x8000_0000, 1),
|
|
_ => return (lead as u32, 1),
|
|
};
|
|
let mut c = bits as u32;
|
|
for k in 1..=n {
|
|
let x = at(i + k);
|
|
if x == 0 {
|
|
return (u32::MAX, k);
|
|
}
|
|
if x & 0xc0 != 0x80 {
|
|
return (0x8000_0000, k + 1);
|
|
}
|
|
c = c << 6 | (x & 0x3f) as u32;
|
|
}
|
|
(c, n + 1)
|
|
}
|
|
|
|
/// The client's `Utf8EncodeChar` (`0x7fbfb0`), by its limits (`0x7fbfb6`-`0x7fc05a`): one byte
|
|
/// below 0x80, two below 0x800, three below 0x10000, four below 0x200000, five below 0x400000,
|
|
/// six below 0x80000000, and none from there.
|
|
fn utf8_encode(c: u32, out: &mut Vec<u8>) {
|
|
let cont = |shift: u32| 0x80 | (c >> shift) as u8 & 0x3f;
|
|
match c {
|
|
0..0x80 => out.push(c as u8),
|
|
0x80..0x800 => out.extend([0xc0 | (c >> 6) as u8, cont(0)]),
|
|
0x800..0x1_0000 => out.extend([0xe0 | (c >> 12) as u8, cont(6), cont(0)]),
|
|
0x1_0000..0x20_0000 => out.extend([0xf0 | (c >> 18) as u8, cont(12), cont(6), cont(0)]),
|
|
0x20_0000..0x40_0000 => {
|
|
out.extend([0xf8 | (c >> 24) as u8, cont(18), cont(12), cont(6), cont(0)])
|
|
}
|
|
0x40_0000..0x8000_0000 => out.extend([
|
|
0xfc | (c >> 30) as u8,
|
|
cont(24),
|
|
cont(18),
|
|
cont(12),
|
|
cont(6),
|
|
cont(0),
|
|
]),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn char_(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let mut out = Vec::with_capacity(nargs);
|
|
for i in 1..=nargs {
|
|
let c = vm.check_int(nargs, i)?;
|
|
// `uchar(c) == c`.
|
|
if !(0..=255).contains(&c) {
|
|
return Err(vm.arg_error(i, "invalid value"));
|
|
}
|
|
out.push(c as u8);
|
|
}
|
|
Ok(vm.ret_str(&out))
|
|
}
|
|
|
|
/// 5.1's `rep`: `n` copies; a result past [`crate::api::MAX_STRING`] is `not enough memory`.
|
|
fn rep(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let n = vm.check_int(nargs, 2)?;
|
|
let l = vm.heap.str(s).len();
|
|
if n <= 0 || l == 0 {
|
|
return Ok(vm.ret_str(b""));
|
|
}
|
|
let total = l.saturating_mul(n as usize);
|
|
let [mut out] = vm.take_bufs::<1>();
|
|
let r = vm.reserve(&mut out, total);
|
|
if r.is_ok() {
|
|
// Doubling copies, as `slice::repeat` makes them.
|
|
out.extend_from_slice(vm.heap.str(s));
|
|
while out.len() <= total / 2 {
|
|
out.extend_from_within(..);
|
|
}
|
|
out.extend_from_within(..total - out.len());
|
|
}
|
|
let r = r.map(|()| vm.ret_str(&out));
|
|
vm.give_bufs([out]);
|
|
r
|
|
}
|
|
|
|
/// The client's `str_byte` (`0x7fc210`): the byte at one position, `luaL_optnumber` (default 1,
|
|
/// `0x7fc238`) truncated to 32 bits by `__ftol` and counted from the end when negative
|
|
/// (`0x7fc247`), or nothing outside 1 to the length (`0x7fc24c`-`0x7fc252`). A third argument is
|
|
/// never read.
|
|
fn byte(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let l = vm.heap.str(s).len();
|
|
let pos = crate::api::c_int(vm.opt_number(nargs, 2, 1.0)?) as i64;
|
|
let pos = if pos < 0 { pos + l as i64 + 1 } else { pos };
|
|
if pos <= 0 || pos > l as i64 {
|
|
return Ok(0);
|
|
}
|
|
let b = vm.heap.str(s)[pos as usize - 1];
|
|
Ok(vm.ret1(Value::Num(b as f64)))
|
|
}
|
|
|
|
/// `str_dump` (`0x7fc330`): argument 1 must be a function (`0x7fc343`), but `lua_dump`
|
|
/// (`0x6f4370`) dumps the stack's top (`0x6f4373`), the last argument, and takes only a Lua
|
|
/// function with no upvalues (`0x6f4383`, `0x6f438a`), else `unable to dump given function`
|
|
/// (`0x882284`). Deviation: the
|
|
/// client writes 5.0 bytecode (`luaU_dump`, `0x6fb600`); benilla writes 5.1's header and the
|
|
/// function's source name. The client loads no binary chunk (`0x6f6a15`), so the bytes are only
|
|
/// data, and matching them would need 5.0's code generator.
|
|
fn dump(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
vm.check_function(nargs, 1)?;
|
|
let source = match vm.arg(nargs, nargs - 1).unpack() {
|
|
V::Func(f) => match vm.heap.func(f) {
|
|
crate::heap::FuncObj::Lua(l) if l.proto().upvals.is_empty() => {
|
|
Some(l.proto().source.clone())
|
|
}
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
};
|
|
let Some(source) = source else {
|
|
return Err(vm.error_at(1, "unable to dump given function"));
|
|
};
|
|
let mut out = b"\x1bLuaQ\x00\x01\x04\x08\x04\x08\x00".to_vec();
|
|
out.extend_from_slice(&source);
|
|
Ok(vm.ret_str(&out))
|
|
}
|
|
|
|
// ── string.format ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// The client's `str_format` (`0x7fd3d0`): in its Lua 5.0 library, this one function is Lua 4.0's
|
|
/// `lstrlib.c` code. The format is a C string, read up to its first NUL. Each item may name its
|
|
/// argument with one digit and `$` (`0x7fd4ac`), which sets the cursor the next items go on from;
|
|
/// its flags, width and precision are matched as `[-+ #0]*(%d*)%.?(%d*)` (`0x8823ec`), two digits
|
|
/// at most each and 18 bytes in all (`0x7fd50c`-`0x7fd51c`), and go to `sprintf` with the
|
|
/// conversion.
|
|
fn format(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let mut b = vm.take_bufs::<2>();
|
|
let r = format_in(vm, nargs, &mut b);
|
|
vm.give_bufs(b);
|
|
r
|
|
}
|
|
|
|
fn format_in(vm: &mut Vm, nargs: usize, [f, out]: &mut [Vec<u8>; 2]) -> R<usize> {
|
|
let fs = vm.check_str(nargs, 1)?;
|
|
f.extend_from_slice(crate::lex::cstr(vm.heap.str(fs)));
|
|
let f = &f[..];
|
|
let at = |j: usize| f.get(j).copied().unwrap_or(0);
|
|
let mut arg = 1;
|
|
let mut i = 0;
|
|
while i < f.len() {
|
|
if f[i] != b'%' {
|
|
out.push(f[i]);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
i += 1;
|
|
if at(i) == b'%' {
|
|
out.push(b'%');
|
|
i += 1;
|
|
continue;
|
|
}
|
|
// A number's text is short: the bound on the whole is enough for it.
|
|
vm.check_len(out)?;
|
|
let mut initf = i;
|
|
if at(i).is_ascii_digit() && at(i + 1) == b'$' {
|
|
arg = (at(i) - b'0') as usize;
|
|
initf = i + 2;
|
|
}
|
|
arg += 1;
|
|
let mut spec = Spec::default();
|
|
let mut j = initf;
|
|
while at(j) != 0 && b"-+ #0".contains(&at(j)) {
|
|
match at(j) {
|
|
b'-' => spec.minus = true,
|
|
b'+' => spec.plus = true,
|
|
b' ' => spec.space = true,
|
|
b'#' => spec.alt = true,
|
|
_ => spec.zero = true,
|
|
}
|
|
j += 1;
|
|
}
|
|
let width_at = j;
|
|
while at(j).is_ascii_digit() {
|
|
spec.width = spec.width.saturating_mul(10) + (at(j) - b'0') as usize;
|
|
j += 1;
|
|
}
|
|
let width_len = j - width_at;
|
|
if at(j) == b'.' {
|
|
j += 1;
|
|
spec.precision = Some(0);
|
|
}
|
|
let prec_at = j;
|
|
while at(j).is_ascii_digit() {
|
|
spec.precision = spec
|
|
.precision
|
|
.map(|p| p.saturating_mul(10) + (at(j) - b'0') as usize);
|
|
j += 1;
|
|
}
|
|
let prec_len = j - prec_at;
|
|
if width_len > 2 || prec_len > 2 || j - initf > 18 {
|
|
return Err(vm.error_at(1, "invalid format (width or precision too long)"));
|
|
}
|
|
let conv = at(j);
|
|
i = j + 1;
|
|
match conv {
|
|
b'c' | b'd' | b'i' | b'o' | b'u' | b'x' | b'X' => {
|
|
// `luaL_checknumber` and `__ftol` (`0x7fd570`, `0x7fd575`): the low 32 bits.
|
|
let n = crate::api::c_int(vm.check_number(nargs, arg)?);
|
|
match conv {
|
|
b'c' => {
|
|
let start = out.len();
|
|
let spec = Spec {
|
|
precision: None,
|
|
..spec
|
|
};
|
|
cfmt::write_str(out, &[n as u8], &spec);
|
|
// `luaL_addlstring(b, buff, strlen(buff))`: a `%c` of 0 ends the item.
|
|
if let Some(end) = out[start..].iter().position(|&b| b == 0) {
|
|
out.truncate(start + end);
|
|
}
|
|
}
|
|
b'd' | b'i' => cfmt::write_int(out, n as i64, &spec),
|
|
_ => cfmt::write_uint(out, n as u32, conv, &spec),
|
|
}
|
|
}
|
|
b'e' | b'E' | b'f' | b'g' | b'G' => {
|
|
let n = vm.check_number(nargs, arg)?;
|
|
cfmt::write_float(out, n, conv, &spec);
|
|
}
|
|
b'q' => {
|
|
let s = vm.check_str(nargs, arg)?;
|
|
// `luaI_addquoted` (`0x7fd6d0`), by its table at `0x7fd7b0`: `"`, `\` and a
|
|
// newline take a backslash, a NUL is `\000`, and every other byte is itself.
|
|
let len = vm.heap.str(s).len();
|
|
vm.reserve(out, len.saturating_mul(4).saturating_add(2))?;
|
|
out.push(b'"');
|
|
for &c in vm.heap.str(s) {
|
|
match c {
|
|
b'"' | b'\\' | b'\n' => out.extend_from_slice(&[b'\\', c]),
|
|
0 => out.extend_from_slice(b"\\000"),
|
|
_ => out.push(c),
|
|
}
|
|
}
|
|
out.push(b'"');
|
|
}
|
|
b's' => {
|
|
let s = vm.check_str(nargs, arg)?;
|
|
// At most the string and a two-digit width.
|
|
let len = vm.heap.str(s).len();
|
|
vm.reserve(out, len.saturating_add(99))?;
|
|
let b = vm.heap.str(s);
|
|
// No precision digits and 100 bytes or more: the string whole (`0x7fd609`).
|
|
if prec_len == 0 && b.len() >= 100 {
|
|
out.extend_from_slice(b);
|
|
} else {
|
|
// `sprintf` reads a C string.
|
|
let c = crate::lex::cstr(b);
|
|
if spec.is_plain() {
|
|
out.extend_from_slice(c);
|
|
} else {
|
|
cfmt::write_str(out, c, &spec);
|
|
}
|
|
}
|
|
}
|
|
_ => return Err(vm.error_at(1, "invalid option in `format'")),
|
|
}
|
|
}
|
|
Ok(vm.ret_str(out))
|
|
}
|
|
|
|
// ── the matcher ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
const MAX_CAPTURES: usize = 32;
|
|
|
|
const CAP_UNFINISHED: isize = -1;
|
|
const CAP_POSITION: isize = -2;
|
|
const SPECIALS: &[u8] = b"^$*+?.([%-";
|
|
|
|
struct MatchState<'a> {
|
|
src: &'a [u8],
|
|
/// The pattern, NUL-terminated as 5.0's C string is.
|
|
pat: &'a [u8],
|
|
level: usize,
|
|
capture: [(usize, isize); MAX_CAPTURES],
|
|
/// The Rust stack address past which a nested match raises `pattern too complex`.
|
|
stack_limit: usize,
|
|
}
|
|
|
|
/// A matcher error: the message `luaL_error` raises.
|
|
struct PatErr(&'static str);
|
|
|
|
type M<T> = Result<T, PatErr>;
|
|
|
|
/// `match_class` (`0x7fc930`). Both callers pass the pattern's letter sign-extended (`0x7fc8c6`,
|
|
/// `0x7fcbc6`), `tolower` (`0x40f92b`) leaves a negative alone, so a byte of 0x80 or more misses
|
|
/// the class switch and the default compares it with the zero-extended subject byte (`0x7fcb38`):
|
|
/// it never matches.
|
|
fn class_match(c: u8, cl: u8) -> bool {
|
|
let res = match cl.to_ascii_lowercase() {
|
|
b'a' => c.is_ascii_alphabetic(),
|
|
b'c' => c < 32 || c == 127,
|
|
b'd' => c.is_ascii_digit(),
|
|
b'l' => c.is_ascii_lowercase(),
|
|
b'p' => c.is_ascii_punctuation(),
|
|
b's' => matches!(c, 9..=13 | 32),
|
|
b'u' => c.is_ascii_uppercase(),
|
|
b'w' => c.is_ascii_alphanumeric(),
|
|
b'x' => c.is_ascii_hexdigit(),
|
|
b'z' => c == 0,
|
|
_ => return cl == c && cl < 0x80,
|
|
};
|
|
if cl.is_ascii_lowercase() {
|
|
res
|
|
} else {
|
|
!res
|
|
}
|
|
}
|
|
|
|
impl MatchState<'_> {
|
|
fn p(&self, i: usize) -> u8 {
|
|
self.pat.get(i).copied().unwrap_or(0)
|
|
}
|
|
|
|
fn class_end(&self, p: usize) -> M<usize> {
|
|
let mut p = p;
|
|
let c = self.p(p);
|
|
p += 1;
|
|
match c {
|
|
b'%' => {
|
|
if self.p(p) == 0 {
|
|
return Err(PatErr("malformed pattern (ends with `%')"));
|
|
}
|
|
Ok(p + 1)
|
|
}
|
|
b'[' => {
|
|
if self.p(p) == b'^' {
|
|
p += 1;
|
|
}
|
|
loop {
|
|
if self.p(p) == 0 {
|
|
return Err(PatErr("malformed pattern (missing `]')"));
|
|
}
|
|
let c = self.p(p);
|
|
p += 1;
|
|
if c == b'%' && self.p(p) != 0 {
|
|
p += 1;
|
|
}
|
|
if self.p(p) == b']' {
|
|
break;
|
|
}
|
|
}
|
|
Ok(p + 1)
|
|
}
|
|
_ => Ok(p),
|
|
}
|
|
}
|
|
|
|
/// `matchbracketclass`: `p` at the `[`, `ec` at the `]`.
|
|
fn bracket(&self, c: u8, p: usize, ec: usize) -> bool {
|
|
let mut sig = true;
|
|
let mut p = p;
|
|
if self.p(p + 1) == b'^' {
|
|
sig = false;
|
|
p += 1;
|
|
}
|
|
loop {
|
|
p += 1;
|
|
if p >= ec {
|
|
break;
|
|
}
|
|
if self.p(p) == b'%' {
|
|
p += 1;
|
|
if class_match(c, self.p(p)) {
|
|
return sig;
|
|
}
|
|
} else if self.p(p + 1) == b'-' && p + 2 < ec {
|
|
p += 2;
|
|
if self.p(p - 2) <= c && c <= self.p(p) {
|
|
return sig;
|
|
}
|
|
} else if self.p(p) == c {
|
|
return sig;
|
|
}
|
|
}
|
|
!sig
|
|
}
|
|
|
|
fn single(&self, c: u8, p: usize, ep: usize) -> bool {
|
|
match self.p(p) {
|
|
b'.' => true,
|
|
b'%' => class_match(c, self.p(p + 1)),
|
|
b'[' => self.bracket(c, p, ep - 1),
|
|
pc => pc == c,
|
|
}
|
|
}
|
|
|
|
/// A nested `do_match`, within the Rust stack the VM allows its calls. The first level,
|
|
/// which `find`, `gsub` and `gfind` call, is not checked, so a pattern that never nests pays
|
|
/// nothing. Deviation: 5.1's `match` recurses without a bound and overflows the C stack
|
|
/// (near 32000 levels on 8 MB); this raises 5.2's `pattern too complex` instead. The bound
|
|
/// is stack measured, not levels counted, so it holds at any optimisation level: a level
|
|
/// takes about 110 bytes at opt-level 1 and about 1.5 KB at opt-level 0.
|
|
#[inline(never)]
|
|
fn sub_match(&mut self, s: usize, p: usize) -> M<Option<usize>> {
|
|
let marker = 0u8;
|
|
if (std::ptr::addr_of!(marker) as usize) < self.stack_limit {
|
|
return Err(PatErr("pattern too complex"));
|
|
}
|
|
self.do_match(s, p)
|
|
}
|
|
|
|
fn do_match(&mut self, s: usize, p: usize) -> M<Option<usize>> {
|
|
let (mut s, mut p) = (s, p);
|
|
loop {
|
|
match self.p(p) {
|
|
b'(' => {
|
|
return if self.p(p + 1) == b')' {
|
|
self.start_capture(s, p + 2, CAP_POSITION)
|
|
} else {
|
|
self.start_capture(s, p + 1, CAP_UNFINISHED)
|
|
};
|
|
}
|
|
b')' => return self.end_capture(s, p + 1),
|
|
0 => return Ok(Some(s)),
|
|
b'$' if self.p(p + 1) == 0 => {
|
|
return Ok(if s == self.src.len() { Some(s) } else { None });
|
|
}
|
|
b'%' if self.p(p + 1) == b'b' => match self.balance(s, p + 2)? {
|
|
Some(e) => {
|
|
s = e;
|
|
p += 4;
|
|
continue;
|
|
}
|
|
None => return Ok(None),
|
|
},
|
|
b'%' if self.p(p + 1) == b'f' => {
|
|
p += 2;
|
|
if self.p(p) != b'[' {
|
|
return Err(PatErr("missing `[' after `%f' in pattern"));
|
|
}
|
|
let ep = self.class_end(p)?;
|
|
let prev = if s == 0 { 0 } else { self.src[s - 1] };
|
|
let cur = self.src.get(s).copied().unwrap_or(0);
|
|
if !self.bracket(prev, p, ep - 1) && self.bracket(cur, p, ep - 1) {
|
|
p = ep;
|
|
continue;
|
|
}
|
|
return Ok(None);
|
|
}
|
|
b'%' if self.p(p + 1).is_ascii_digit() => {
|
|
match self.match_capture(s, self.p(p + 1))? {
|
|
Some(e) => {
|
|
s = e;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
None => return Ok(None),
|
|
}
|
|
}
|
|
_ => {
|
|
let ep = self.class_end(p)?;
|
|
let m = s < self.src.len() && self.single(self.src[s], p, ep);
|
|
match self.p(ep) {
|
|
b'?' => {
|
|
if m {
|
|
if let Some(r) = self.sub_match(s + 1, ep + 1)? {
|
|
return Ok(Some(r));
|
|
}
|
|
}
|
|
p = ep + 1;
|
|
}
|
|
b'*' => return self.max_expand(s, p, ep),
|
|
b'+' => {
|
|
return if m {
|
|
self.max_expand(s + 1, p, ep)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
b'-' => return self.min_expand(s, p, ep),
|
|
_ => {
|
|
if !m {
|
|
return Ok(None);
|
|
}
|
|
s += 1;
|
|
p = ep;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn balance(&self, s: usize, p: usize) -> M<Option<usize>> {
|
|
if self.p(p) == 0 || self.p(p + 1) == 0 {
|
|
return Err(PatErr("unbalanced pattern"));
|
|
}
|
|
if s >= self.src.len() || self.src[s] != self.p(p) {
|
|
return Ok(None);
|
|
}
|
|
let (b, e) = (self.p(p), self.p(p + 1));
|
|
let mut cont = 1;
|
|
let mut s = s + 1;
|
|
while s < self.src.len() {
|
|
let c = self.src[s];
|
|
if c == e {
|
|
cont -= 1;
|
|
if cont == 0 {
|
|
return Ok(Some(s + 1));
|
|
}
|
|
} else if c == b {
|
|
cont += 1;
|
|
}
|
|
s += 1;
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
// Inlined into `do_match`: one frame per pattern level, so a deep match stays within the
|
|
// CPU's return-address predictor.
|
|
#[inline(always)]
|
|
fn max_expand(&mut self, s: usize, p: usize, ep: usize) -> M<Option<usize>> {
|
|
let mut i = 0;
|
|
while s + i < self.src.len() && self.single(self.src[s + i], p, ep) {
|
|
i += 1;
|
|
}
|
|
loop {
|
|
if let Some(r) = self.sub_match(s + i, ep + 1)? {
|
|
return Ok(Some(r));
|
|
}
|
|
if i == 0 {
|
|
return Ok(None);
|
|
}
|
|
i -= 1;
|
|
}
|
|
}
|
|
|
|
// Inlined into `do_match`: one frame per pattern level, so a deep match stays within the
|
|
// CPU's return-address predictor.
|
|
#[inline(always)]
|
|
fn min_expand(&mut self, s: usize, p: usize, ep: usize) -> M<Option<usize>> {
|
|
let mut s = s;
|
|
loop {
|
|
if let Some(r) = self.sub_match(s, ep + 1)? {
|
|
return Ok(Some(r));
|
|
}
|
|
if s < self.src.len() && self.single(self.src[s], p, ep) {
|
|
s += 1;
|
|
} else {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inlined into `do_match`: one frame per pattern level, so a deep match stays within the
|
|
// CPU's return-address predictor.
|
|
#[inline(always)]
|
|
fn start_capture(&mut self, s: usize, p: usize, what: isize) -> M<Option<usize>> {
|
|
if self.level >= MAX_CAPTURES {
|
|
return Err(PatErr("too many captures"));
|
|
}
|
|
self.capture[self.level] = (s, what);
|
|
self.level += 1;
|
|
let r = self.sub_match(s, p)?;
|
|
if r.is_none() {
|
|
self.level -= 1;
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
// Inlined into `do_match`: one frame per pattern level, so a deep match stays within the
|
|
// CPU's return-address predictor.
|
|
#[inline(always)]
|
|
fn end_capture(&mut self, s: usize, p: usize) -> M<Option<usize>> {
|
|
let l = self.capture_to_close()?;
|
|
self.capture[l].1 = (s - self.capture[l].0) as isize;
|
|
let r = self.sub_match(s, p)?;
|
|
if r.is_none() {
|
|
self.capture[l].1 = CAP_UNFINISHED;
|
|
}
|
|
Ok(r)
|
|
}
|
|
|
|
fn capture_to_close(&self) -> M<usize> {
|
|
(0..self.level)
|
|
.rev()
|
|
.find(|&l| self.capture[l].1 == CAP_UNFINISHED)
|
|
.ok_or(PatErr("invalid pattern capture"))
|
|
}
|
|
|
|
fn check_capture(&self, l: u8) -> M<usize> {
|
|
let l = l as isize - b'1' as isize;
|
|
if l < 0 || l as usize >= self.level || self.capture[l as usize].1 == CAP_UNFINISHED {
|
|
return Err(PatErr("invalid capture index"));
|
|
}
|
|
Ok(l as usize)
|
|
}
|
|
|
|
fn match_capture(&self, s: usize, l: u8) -> M<Option<usize>> {
|
|
let l = self.check_capture(l)?;
|
|
let (init, len) = self.capture[l];
|
|
let len = len as usize;
|
|
if self.src.len() - s >= len && self.src[init..init + len] == self.src[s..s + len] {
|
|
Ok(Some(s + len))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// `push_onecapture`.
|
|
fn capture_value(&self, vm: &mut Vm, i: usize) -> M<Value> {
|
|
let (init, len) = self.capture[i];
|
|
if len == CAP_UNFINISHED {
|
|
return Err(PatErr("unfinished capture"));
|
|
}
|
|
Ok(if len == CAP_POSITION {
|
|
Value::Num((init + 1) as f64)
|
|
} else {
|
|
vm.str(&self.src[init..init + len as usize])
|
|
})
|
|
}
|
|
|
|
/// `push_captures`: the captures, or the whole match `s..e` when there are none.
|
|
fn captures(&self, vm: &mut Vm, whole: Option<(usize, usize)>) -> M<Vec<Value>> {
|
|
if self.level == 0 {
|
|
if let Some((s, e)) = whole {
|
|
return Ok(vec![vm.str(&self.src[s..e])]);
|
|
}
|
|
}
|
|
(0..self.level).map(|i| self.capture_value(vm, i)).collect()
|
|
}
|
|
}
|
|
|
|
fn pat_err(vm: &mut Vm, e: PatErr) -> LuaError {
|
|
vm.error_at(1, e.0)
|
|
}
|
|
|
|
/// The pattern as 5.0 reads it: up to its first NUL, then a NUL.
|
|
/// The pattern as 5.0's matcher reads it, a C string: up to its first NUL, then one.
|
|
/// The byte a match of `pat` from `p` must start with, when its first item is one literal byte
|
|
/// that must occur (no `*`, `?` or `-` after it): `do_match` returns no match at once at any
|
|
/// position without it, before it reads the rest of the pattern, so skipping such positions
|
|
/// changes no result and no error.
|
|
fn first_literal(pat: &[u8], p: usize) -> Option<u8> {
|
|
let (c, ep) = match *pat.get(p)? {
|
|
b'%' => {
|
|
let c = *pat.get(p + 1)?;
|
|
// An escaped byte of 0x80 or more matches nothing, so it is no literal to look for.
|
|
if c == 0 || c >= 0x80 || c.is_ascii_alphanumeric() {
|
|
return None;
|
|
}
|
|
(c, p + 2)
|
|
}
|
|
c if c == 0 || c == b')' || SPECIALS.contains(&c) => return None,
|
|
c => (c, p + 1),
|
|
};
|
|
match pat.get(ep) {
|
|
Some(b'*' | b'?' | b'-') => None,
|
|
_ => Some(c),
|
|
}
|
|
}
|
|
|
|
fn c_pattern(p: &[u8], out: &mut Vec<u8>) {
|
|
out.clear();
|
|
out.extend_from_slice(&p[..p.iter().position(|&b| b == 0).unwrap_or(p.len())]);
|
|
out.push(0);
|
|
}
|
|
|
|
fn new_state<'a>(vm: &Vm, src: &'a [u8], pat: &'a [u8]) -> MatchState<'a> {
|
|
MatchState {
|
|
src,
|
|
pat,
|
|
level: 0,
|
|
capture: [(0, 0); MAX_CAPTURES],
|
|
stack_limit: vm.rust_stack_limit(),
|
|
}
|
|
}
|
|
|
|
// The pattern functions copy the subject and pattern out of the heap (a match calls back into
|
|
// the VM); the copies go into buffers the VM keeps, not fresh allocations.
|
|
fn find(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let mut b = vm.take_bufs::<3>();
|
|
let r = find_in(vm, nargs, &mut b);
|
|
vm.give_bufs(b);
|
|
r
|
|
}
|
|
|
|
fn find_in(vm: &mut Vm, nargs: usize, [src, pat, cpat]: &mut [Vec<u8>; 3]) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let p = vm.check_str(nargs, 2)?;
|
|
let l1 = vm.heap.str(s).len();
|
|
// `str_find` reads init through `__ftol`'s low dword (`0x7fc3f7`), then `posrelat` and the
|
|
// clamp (`0x7fc401`-`0x7fc413`) in 32 bits.
|
|
let init = posrelat(vm.opt_int(nargs, 3, 1)?, l1).wrapping_sub(1);
|
|
let init = if init < 0 { 0 } else { (init as usize).min(l1) };
|
|
let plain = vm.arg(nargs, 3).truthy();
|
|
src.extend_from_slice(vm.heap.str(s));
|
|
pat.extend_from_slice(vm.heap.str(p));
|
|
c_pattern(pat, cpat);
|
|
let (src, pat, cpat) = (&src[..], &pat[..], &cpat[..]);
|
|
if plain || !cpat[..cpat.len() - 1].iter().any(|c| SPECIALS.contains(c)) {
|
|
// `lmemfind` over the whole pattern, NULs included.
|
|
let hay = &src[init..];
|
|
let found = if pat.is_empty() {
|
|
Some(0)
|
|
} else if pat.len() > hay.len() {
|
|
None
|
|
} else {
|
|
hay.windows(pat.len()).position(|w| w == pat)
|
|
};
|
|
if let Some(i) = found {
|
|
vm.push(Value::Num((init + i + 1) as f64));
|
|
vm.push(Value::Num((init + i + pat.len()) as f64));
|
|
return Ok(2);
|
|
}
|
|
return Ok(vm.ret1(Value::Nil));
|
|
}
|
|
let (anchor, start) = if cpat[0] == b'^' {
|
|
(true, 1)
|
|
} else {
|
|
(false, 0)
|
|
};
|
|
let mut ms = new_state(vm, src, cpat);
|
|
let mut s1 = init;
|
|
loop {
|
|
ms.level = 0;
|
|
match ms.do_match(s1, start) {
|
|
Err(e) => return Err(pat_err(vm, e)),
|
|
Ok(Some(e)) => {
|
|
let caps = match ms.captures(vm, None) {
|
|
Ok(c) => c,
|
|
Err(e) => return Err(pat_err(vm, e)),
|
|
};
|
|
vm.push(Value::Num((s1 + 1) as f64));
|
|
vm.push(Value::Num(e as f64));
|
|
for c in &caps {
|
|
vm.push(*c);
|
|
}
|
|
return Ok(2 + caps.len());
|
|
}
|
|
Ok(None) => {}
|
|
}
|
|
s1 += 1;
|
|
if s1 > src.len() || anchor {
|
|
break;
|
|
}
|
|
}
|
|
Ok(vm.ret1(Value::Nil))
|
|
}
|
|
|
|
/// `gfind`: an iterator closure over `(s, p, position)`.
|
|
fn gfind(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let p = vm.check_str(nargs, 2)?;
|
|
let f = vm.new_closure(
|
|
"gfind_aux",
|
|
gfind_aux,
|
|
vec![Value::Str(s), Value::Str(p), Value::Num(0.0)],
|
|
);
|
|
Ok(vm.ret1(f))
|
|
}
|
|
|
|
fn gfind_aux(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let mut b = vm.take_bufs::<2>();
|
|
let r = gfind_in(vm, nargs, &mut b);
|
|
vm.give_bufs(b);
|
|
r
|
|
}
|
|
|
|
fn gfind_in(vm: &mut Vm, _nargs: usize, [src, cpat]: &mut [Vec<u8>; 2]) -> R<usize> {
|
|
let (V::Str(s), V::Str(p)) = ((vm.upvalue(0)).unpack(), (vm.upvalue(1)).unpack()) else {
|
|
return Ok(0);
|
|
};
|
|
let pos = vm.tonumber(vm.upvalue(2)).unwrap_or(0.0) as usize;
|
|
src.extend_from_slice(vm.heap.str(s));
|
|
c_pattern(vm.heap.str(p), cpat);
|
|
let (src, cpat) = (&src[..], &cpat[..]);
|
|
let mut ms = new_state(vm, src, cpat);
|
|
let mut at = pos;
|
|
while at <= src.len() {
|
|
ms.level = 0;
|
|
match ms.do_match(at, 0) {
|
|
Err(e) => return Err(pat_err(vm, e)),
|
|
Ok(Some(e)) => {
|
|
let newstart = if e == at { e + 1 } else { e };
|
|
vm.set_upvalue(2, Value::Num(newstart as f64));
|
|
let caps = match ms.captures(vm, Some((at, e))) {
|
|
Ok(c) => c,
|
|
Err(e) => return Err(pat_err(vm, e)),
|
|
};
|
|
for c in &caps {
|
|
vm.push(*c);
|
|
}
|
|
return Ok(caps.len());
|
|
}
|
|
Ok(None) => at += 1,
|
|
}
|
|
}
|
|
Ok(0)
|
|
}
|
|
|
|
/// The client's `str_gsub` (`0x7fd0e0`): 5.0's, a string or a function replacement.
|
|
fn gsub(vm: &mut Vm, nargs: usize) -> R<usize> {
|
|
let mut b = vm.take_bufs::<4>();
|
|
let r = gsub_in(vm, nargs, &mut b);
|
|
vm.give_bufs(b);
|
|
r
|
|
}
|
|
|
|
fn gsub_in(vm: &mut Vm, nargs: usize, [src, cpat, out, repl_buf]: &mut [Vec<u8>; 4]) -> R<usize> {
|
|
let s = vm.check_str(nargs, 1)?;
|
|
let p = vm.check_str(nargs, 2)?;
|
|
let srcl = vm.heap.str(s).len();
|
|
let repl = vm.arg(nargs, 2);
|
|
// `luaL_optint(L, 4, srcl+1)`: the default cast to `int` as C casts it.
|
|
let max_s = vm.opt_int(nargs, 4, (srcl as i64 + 1) as i32)?;
|
|
// `lua_isstring(L, 3) || lua_type(L, 3) == LUA_TFUNCTION`, else `string or function expected`
|
|
// (`0x7fd163`-`0x7fd195`): no table.
|
|
let kind = match repl.unpack() {
|
|
_ if nargs < 3 => None,
|
|
V::Str(_) | V::Num(_) => Some(0),
|
|
V::Func(_) => Some(1),
|
|
_ => None,
|
|
};
|
|
let Some(kind) = kind else {
|
|
return Err(vm.arg_error(3, "string or function expected"));
|
|
};
|
|
if kind == 0 {
|
|
// `lua_tolstring(L, 3)`: a number replacement is its text.
|
|
let r = vm.tostr(repl).unwrap();
|
|
repl_buf.extend_from_slice(vm.heap.str(r));
|
|
}
|
|
src.extend_from_slice(vm.heap.str(s));
|
|
c_pattern(vm.heap.str(p), cpat);
|
|
let (src, cpat, repl_bytes) = (&src[..], &cpat[..], &repl_buf[..]);
|
|
let (anchor, pstart) = if cpat[0] == b'^' {
|
|
(true, 1)
|
|
} else {
|
|
(false, 0)
|
|
};
|
|
let mut ms = new_state(vm, src, cpat);
|
|
let mut at = 0usize;
|
|
let mut n = 0;
|
|
let lit = if anchor {
|
|
None
|
|
} else {
|
|
first_literal(cpat, pstart)
|
|
};
|
|
while n < max_s {
|
|
// Straight to the next place the first byte occurs: `do_match` fails at once anywhere
|
|
// else, and those bytes are copied as the loop below would copy them one by one.
|
|
if let Some(c) = lit {
|
|
match src[at..].iter().position(|&b| b == c) {
|
|
Some(k) => {
|
|
out.extend_from_slice(&src[at..at + k]);
|
|
at += k;
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
ms.level = 0;
|
|
let e = match ms.do_match(at, pstart) {
|
|
Ok(e) => e,
|
|
Err(e) => return Err(pat_err(vm, e)),
|
|
};
|
|
if let Some(e) = e {
|
|
n += 1;
|
|
add_value(vm, &ms, out, kind, repl, repl_bytes, at, e)?;
|
|
}
|
|
match e {
|
|
Some(e) if e > at => at = e,
|
|
_ if at < src.len() => {
|
|
out.push(src[at]);
|
|
at += 1;
|
|
}
|
|
_ => break,
|
|
}
|
|
if anchor {
|
|
break;
|
|
}
|
|
}
|
|
out.extend_from_slice(&src[at..]);
|
|
let r = vm.str(out);
|
|
vm.push(r);
|
|
vm.push(Value::Num(n as f64));
|
|
Ok(2)
|
|
}
|
|
|
|
/// The client's `add_s` (`0x7fd2a0`): one match's replacement onto `out`.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn add_value(
|
|
vm: &mut Vm,
|
|
ms: &MatchState,
|
|
out: &mut Vec<u8>,
|
|
kind: u8,
|
|
repl: Value,
|
|
repl_bytes: &[u8],
|
|
s: usize,
|
|
e: usize,
|
|
) -> R<()> {
|
|
if kind == 0 {
|
|
// `%` and a digit is a capture through `check_capture` (`0x7fd34d`), so `%0`, and `%1`
|
|
// with no capture, raise `invalid capture index`; `%` and anything else is that byte.
|
|
vm.check_len(out)?;
|
|
let mut i = 0;
|
|
while i < repl_bytes.len() {
|
|
let c = repl_bytes[i];
|
|
if c != b'%' {
|
|
out.push(c);
|
|
} else {
|
|
i += 1;
|
|
let d = repl_bytes.get(i).copied().unwrap_or(0);
|
|
if !d.is_ascii_digit() {
|
|
out.push(d);
|
|
} else {
|
|
let v = ms
|
|
.check_capture(d)
|
|
.and_then(|l| ms.capture_value(vm, l))
|
|
.map_err(|err| pat_err(vm, err))?;
|
|
let sv = vm.tostr(v).unwrap();
|
|
vm.reserve(out, vm.heap.str(sv).len())?;
|
|
out.extend_from_slice(vm.heap.str(sv));
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
return Ok(());
|
|
}
|
|
let caps = ms
|
|
.captures(vm, Some((s, e)))
|
|
.map_err(|err| pat_err(vm, err))?;
|
|
let v = vm.call_value(repl, &caps, 1)?[0];
|
|
// A string or number result is added, anything else dropped with the match
|
|
// (`0x7fd392`-`0x7fd3b7`).
|
|
if let Some(sv) = matches!(v.unpack(), V::Str(_) | V::Num(_))
|
|
.then(|| vm.tostr(v))
|
|
.flatten()
|
|
{
|
|
vm.reserve(out, vm.heap.str(sv).len())?;
|
|
out.extend_from_slice(vm.heap.str(sv));
|
|
}
|
|
Ok(())
|
|
}
|