Skip to main content

lua_stdlib/
string_lib.rs

1//! Standard library for string operations and pattern-matching.
2//!
3//! Port of `lstrlib.c` (Lua 5.4.7, 1875 lines, 46 functions).
4//!
5//! Sections:
6//!   1. Basic string operations (byte, char, find, format, gmatch, gsub, len,
7//!      lower, match, rep, reverse, sub, upper)
8//!   2. Pattern-matching engine (MatchState + recursive matcher)
9//!   3. String format (`string.format`)
10//!   4. Pack / unpack (`string.pack`, `string.packsize`, `string.unpack`)
11//!   5. Module registration (`luaopen_string`)
12
13use lua_types::error::LuaError;
14use lua_types::value::LuaValue;
15use lua_types::arith::ArithOp;
16use lua_types::{LuaType};
17use lua_vm::state::LuaTableRefExt as _;
18use crate::state_stub::{LuaState, LuaStateStubExt as _, lua_CFunction, upvalue_index};
19
20// ────────────────────────────────────────────────────────────────────────────
21// Constants
22// ────────────────────────────────────────────────────────────────────────────
23
24const LUA_MAX_CAPTURES: usize = 32;
25
26const MAX_CC_CALLS: i32 = 200;
27
28const L_ESC: u8 = b'%';
29
30const SPECIALS: &[u8] = b"^$*+?.([%-";
31
32const CAP_UNFINISHED: isize = -1;
33
34const CAP_POSITION: isize = -2;
35
36#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
37const MAX_ITEM: usize = 120;
38
39#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
40const MAX_ITEM_F: usize = 418;
41
42#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
43const MAX_FORMAT: usize = 32;
44
45const MAX_INT_SIZE: usize = 16;
46
47// On platforms where size_t is at least as wide as int (all our targets), this
48// collapses to INT_MAX so that packed sizes round-trip through a Lua integer
49// without ambiguity.
50const PACK_MAXSIZE: usize = i32::MAX as usize;
51
52const NB: u32 = 8;
53
54const MC: u8 = 0xFF;
55
56const SZINT: usize = 8; // sizeof(i64) == 8
57
58const PACK_PAD_BYTE: u8 = 0x00;
59
60// ────────────────────────────────────────────────────────────────────────────
61// Pattern-matching types
62// ────────────────────────────────────────────────────────────────────────────
63
64/// One capture record inside MatchState.
65///
66/// In Rust, `init` is an index into `MatchState::src`; `len` is either a
67/// non-negative actual length, `CAP_UNFINISHED`, or `CAP_POSITION`.
68#[derive(Copy, Clone)]
69struct Capture {
70    /// Index into the source slice where this capture started.
71    init: usize,
72    /// CAP_UNFINISHED, CAP_POSITION, or non-negative byte count.
73    len: isize,
74}
75
76impl Default for Capture {
77    fn default() -> Self {
78        Capture { init: 0, len: CAP_UNFINISHED }
79    }
80}
81
82/// State threaded through the recursive pattern-matcher.
83///
84/// Raw C pointers replaced by indices into `src` / `pat` slices.
85struct MatchState<'a> {
86    /// Source string being searched.
87    src: &'a [u8],
88    /// Pattern string.
89    pat: &'a [u8],
90    /// Recursion depth counter; decremented on entry, incremented on return.
91    matchdepth: i32,
92    /// Number of capture records currently in use.
93    level: u8,
94    /// Capture records indexed `0..level`.
95    captures: [Capture; LUA_MAX_CAPTURES],
96    /// Total `match_pat` invocations across the whole operation. Used to bound
97    /// catastrophic backtracking under a sandbox; charged against the
98    /// instruction budget by the caller.
99    steps: u64,
100    /// Maximum `steps` before the matcher stops. `0` means unlimited (no active
101    /// instruction budget), preserving non-sandboxed behavior exactly.
102    step_limit: u64,
103    /// Set when `step_limit` is reached; the matcher then unwinds to the caller,
104    /// which charges the budget and raises the uncatchable sandbox abort.
105    aborted: bool,
106}
107
108impl<'a> MatchState<'a> {
109    fn new(src: &'a [u8], pat: &'a [u8], step_limit: u64) -> Self {
110        MatchState {
111            src,
112            pat,
113            matchdepth: MAX_CC_CALLS,
114            level: 0,
115            captures: [Capture::default(); LUA_MAX_CAPTURES],
116            steps: 0,
117            step_limit,
118            aborted: false,
119        }
120    }
121
122    fn reset_level(&mut self) {
123        self.level = 0;
124        debug_assert!(self.matchdepth == MAX_CC_CALLS);
125    }
126}
127
128/// Iterator state for `string.gmatch`.
129///
130/// Stored as userdata on the Lua stack in the C implementation; in Phase A we
131/// represent it as a plain Rust struct.
132///
133/// TODO(port): In the real port, this needs to live in a Lua userdata object
134/// so that Lua GC can see it. For now it's a plain struct passed by
135/// `state.to_userdata()`.
136#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
137struct GMatchState {
138    /// Current position in `src` (index into the source slice).
139    src_pos: usize,
140    /// The pattern string (owned copy so it survives the closure).
141    pat: Vec<u8>,
142    /// End of the last match (to avoid zero-length infinite loops).
143    last_match: Option<usize>,
144    /// Source string (owned copy).
145    src: Vec<u8>,
146}
147
148// ────────────────────────────────────────────────────────────────────────────
149// Pack/unpack types
150// ────────────────────────────────────────────────────────────────────────────
151
152/// Pack/unpack format option.
153///
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155enum KOption {
156    Int,        // signed integers
157    Uint,       // unsigned integers
158    Float,      // single-precision float (C float)
159    Number,     // Lua native float (lua_Number = f64)
160    Double,     // double-precision float (C double)
161    Char,       // fixed-length string
162    Kstring,    // string with length prefix
163    Zstr,       // zero-terminated string
164    Padding,    // padding byte (x)
165    Paddalign,  // padding to alignment (X)
166    Nop,        // no-op (space, <, >, =, !)
167}
168
169/// Header state for pack/unpack format parsing.
170///
171struct Header {
172    is_little: bool,
173    max_align: usize,
174}
175
176impl Header {
177    fn new() -> Self {
178        Header {
179            is_little: cfg!(target_endian = "little"),
180            max_align: 1,
181        }
182    }
183}
184
185// ────────────────────────────────────────────────────────────────────────────
186// §1  Basic string helpers
187// ────────────────────────────────────────────────────────────────────────────
188
189/// Translate a relative initial string position: negative means back from end;
190/// result is clipped to `[1, ∞)`.
191///
192fn pos_relat_i(pos: i64, len: usize) -> usize {
193    if pos > 0 {
194        pos as usize
195    } else if pos == 0 {
196        1
197    } else if pos < -(len as i64) {
198        1
199    } else {
200        len.wrapping_add(pos as usize).wrapping_add(1)
201    }
202}
203
204/// Get an optional ending string position from argument `arg`, default `def`.
205/// Negative means back from end; clipped to `[0, len]`.
206///
207fn get_end_pos(pos: i64, len: usize) -> usize {
208    if pos > len as i64 {
209        len
210    } else if pos >= 0 {
211        pos as usize
212    } else if pos < -(len as i64) {
213        0
214    } else {
215        len.wrapping_add(pos as usize).wrapping_add(1)
216    }
217}
218
219// ────────────────────────────────────────────────────────────────────────────
220// §2  Exported string functions (registered in strlib[])
221// ────────────────────────────────────────────────────────────────────────────
222
223/// `string.len(s)` — return byte-length of `s`.
224///
225///
226/// Reads only the byte-length, never the bytes themselves, so go through
227/// `to_lua_string_len` (which never copies) rather than `check_arg_string`
228/// (which `to_vec`s the entire payload only for `.len()` to throw it away).
229pub fn str_len(state: &mut LuaState) -> Result<usize, LuaError> {
230    let l = match state.to_lua_string_len(1) {
231        Some(n) => n,
232        None => {
233            state.check_arg_string(1)?;
234            unreachable!("check_arg_string raises when arg #1 is not a string");
235        }
236    };
237    state.push(LuaValue::Int(l as i64));
238    Ok(1)
239}
240
241/// `string.sub(s, i [, j])` — return substring.
242///
243///
244/// Borrow through `to_lua_string` so the full source string is not copied just
245/// to slice a (typically small) substring out of it. The `GcRef` keeps the
246/// bytes rooted across the `check_arg_integer` / `opt_arg_integer` calls (none
247/// of which can collect the string at arg #1).
248pub fn str_sub(state: &mut LuaState) -> Result<usize, LuaError> {
249    let s_ref = match state.to_lua_string(1) {
250        Some(r) => r,
251        None => {
252            state.check_arg_string(1)?;
253            unreachable!("check_arg_string raises when arg #1 is not a string");
254        }
255    };
256    let s: &[u8] = s_ref.as_bytes();
257    let l = s.len();
258    let start = pos_relat_i(state.check_arg_integer(2)?, l);
259    let end_pos_raw = state.opt_arg_integer(3, -1)?;
260    let end = get_end_pos(end_pos_raw, l);
261    if start <= end {
262        let slice = &s[(start - 1)..end];
263        state.push_string(slice)?;
264    } else {
265        state.push_string(b"")?;
266    }
267    Ok(1)
268}
269
270/// `string.reverse(s)` — return string with bytes reversed.
271///
272///
273/// Borrow the source bytes; the previous `check_arg_string` made a full owned
274/// copy that was discarded after the single iteration.
275pub fn str_reverse(state: &mut LuaState) -> Result<usize, LuaError> {
276    let s_ref = match state.to_lua_string(1) {
277        Some(r) => r,
278        None => {
279            state.check_arg_string(1)?;
280            unreachable!("check_arg_string raises when arg #1 is not a string");
281        }
282    };
283    let s: &[u8] = s_ref.as_bytes();
284    let buf: Vec<u8> = s.iter().copied().rev().collect();
285    state.push_bytes(&buf)?;
286    Ok(1)
287}
288
289/// `string.lower(s)` — return lowercase copy.
290///
291///
292/// Borrow the source bytes; one allocation (the output `Vec`) is unavoidable,
293/// but the intermediate copy from `check_arg_string` was not.
294pub fn str_lower(state: &mut LuaState) -> Result<usize, LuaError> {
295    let s_ref = match state.to_lua_string(1) {
296        Some(r) => r,
297        None => {
298            state.check_arg_string(1)?;
299            unreachable!("check_arg_string raises when arg #1 is not a string");
300        }
301    };
302    let s: &[u8] = s_ref.as_bytes();
303    let buf: Vec<u8> = s.iter().map(|&c| c.to_ascii_lowercase()).collect();
304    state.push_bytes(&buf)?;
305    Ok(1)
306}
307
308/// `string.upper(s)` — return uppercase copy.
309///
310///
311/// Borrow the source bytes; called as the `string.gsub` replacement function
312/// in `string_ops_long` ~700k times against `%w+` matches, so the intermediate
313/// copy from `check_arg_string` added up.
314pub fn str_upper(state: &mut LuaState) -> Result<usize, LuaError> {
315    let s_ref = match state.to_lua_string(1) {
316        Some(r) => r,
317        None => {
318            state.check_arg_string(1)?;
319            unreachable!("check_arg_string raises when arg #1 is not a string");
320        }
321    };
322    let s: &[u8] = s_ref.as_bytes();
323    let buf: Vec<u8> = s.iter().map(|&c| c.to_ascii_uppercase()).collect();
324    state.push_bytes(&buf)?;
325    Ok(1)
326}
327
328/// `string.rep(s, n [, sep])` — return `n` copies of `s` separated by `sep`.
329///
330///
331/// Borrow `s` through `to_lua_string`. The previous version did the
332/// `check_arg_string` copy and then a second redundant `s.to_vec()` inside the
333/// build loop — that double-copy is gone too.
334pub fn str_rep(state: &mut LuaState) -> Result<usize, LuaError> {
335    let s_ref = match state.to_lua_string(1) {
336        Some(r) => r,
337        None => {
338            state.check_arg_string(1)?;
339            unreachable!("check_arg_string raises when arg #1 is not a string");
340        }
341    };
342    let s: &[u8] = s_ref.as_bytes();
343    let l = s.len();
344    let n = state.check_arg_integer(2)?;
345    let sep_owned = state.opt_arg_string(3, b"")?;
346    let sep: &[u8] = &sep_owned;
347    let lsep = sep.len();
348
349    if n <= 0 {
350        state.push_string(b"")?;
351    } else {
352        const MAXSIZE: usize = i32::MAX as usize;
353        let per = l.checked_add(lsep)
354            .ok_or_else(|| LuaError::runtime(format_args!("resulting string too large")))?;
355        if per > MAXSIZE / (n as usize) {
356            return Err(LuaError::runtime(format_args!("resulting string too large")));
357        }
358        let total = per * (n as usize) - lsep;
359
360        if let Some(err) = state.sandbox_reserve(total) {
361            return Err(err);
362        }
363
364        let mut buf: Vec<u8> = Vec::with_capacity(total);
365        for i in 0..(n as usize) {
366            buf.extend_from_slice(s);
367            if i < (n as usize - 1) && lsep > 0 {
368                buf.extend_from_slice(sep);
369            }
370        }
371        state.push_bytes(&buf)?;
372    }
373    Ok(1)
374}
375
376/// `string.byte(s [, i [, j]])` — return numeric codes of characters.
377///
378///
379/// Borrow the source bytes through `to_lua_string` (returns a `GcRef<LuaString>`)
380/// instead of `check_arg_string` (which copies the entire string into a fresh
381/// `Vec<u8>`). On the `string_ops_long` workload `string.byte` is called 700k
382/// times against the same ~14 KB string, so the previous copy was on the order
383/// of 10 GB of memcpy. The `GcRef` keeps the bytes rooted while the borrow lives.
384pub fn str_byte(state: &mut LuaState) -> Result<usize, LuaError> {
385    let s_ref = match state.to_lua_string(1) {
386        Some(r) => r,
387        None => {
388            state.check_arg_string(1)?;
389            unreachable!("check_arg_string raises when arg #1 is not a string");
390        }
391    };
392    let s: &[u8] = s_ref.as_bytes();
393    let l = s.len();
394    let pi = state.opt_arg_integer(2, 1)?;
395    let posi = pos_relat_i(pi, l);
396    let pose_raw = state.opt_arg_integer(3, pi)?;
397    let pose = get_end_pos(pose_raw, l);
398
399    if posi > pose {
400        return Ok(0);
401    }
402    let count = pose.saturating_sub(posi - 1) + 1;
403    if count > i32::MAX as usize {
404        return Err(LuaError::runtime(format_args!("string slice too long")));
405    }
406    let n = (pose - posi + 1) as usize;
407    state.ensure_stack(n as i32, "string slice too long")?;
408
409    for i in 0..n {
410        state.push(LuaValue::Int(s[posi - 1 + i] as i64));
411    }
412    Ok(n)
413}
414
415/// `string.char(...)` — return string built from character codes.
416///
417pub fn str_char(state: &mut LuaState) -> Result<usize, LuaError> {
418    let n = state.get_top();
419    let mut buf = Vec::with_capacity(n as usize);
420    for i in 1..=n {
421        let c = state.check_arg_integer(i)? as u64;
422        if c > u8::MAX as u64 {
423            return Err(LuaError::arg_error(i, "value out of range"));
424        }
425        buf.push(c as u8);
426    }
427    state.push_bytes(&buf)?;
428    Ok(1)
429}
430
431/// `string.dump(function [, strip])` — serialize a function as binary chunk.
432///
433/// Uses `lua_dump` internally; the writer callback builds a buffer.
434pub fn str_dump(state: &mut LuaState) -> Result<usize, LuaError> {
435    state.check_arg_type(1, LuaType::Function)?;
436    let strip = state.arg_to_bool(2);
437    // PORT NOTE: `state.set_top` (inherent) takes an absolute StackIdx and
438    // would wipe the call frame. `lua_settop` is frame-relative.
439    lua_vm::api::set_top(state, 1)?;
440    // TODO(port): state.dump_function(strip) needs to produce &[u8].
441    // In the C code, lua_dump writes to a writer callback that fills a luaL_Buffer.
442    // In Rust, state.dump() should return Vec<u8> or write to a &mut Vec<u8>.
443    let bytes = state.dump_function(strip)
444        .map_err(|_| LuaError::runtime(format_args!("unable to dump given function")))?;
445    state.push_bytes(&bytes)?;
446    Ok(1)
447}
448
449// ────────────────────────────────────────────────────────────────────────────
450// §3  String metamethods (arithmetic coercion)
451// ────────────────────────────────────────────────────────────────────────────
452
453/// Try to coerce the argument at `arg` to a number, pushing it on the stack.
454/// Returns true on success.
455///
456fn tonum(state: &mut LuaState, arg: i32) -> Result<bool, LuaError> {
457    if state.type_at(arg) == LuaType::Number {
458        state.push_value_at(arg)?;
459        Ok(true)
460    } else {
461        // check whether it is a numerical string
462        //    return (s != NULL && lua_stringtonumber(L, s) == len + 1);
463        if let Some(s) = state.to_lua_string_bytes(arg) {
464            let len = s.len();
465            // PORT NOTE: string_to_number pushes the number if successful
466            let pushed = state.string_to_number_push(&s)?;
467            let ok = pushed == len + 1;
468            // Lua 5.1–5.3: a string coerced in an arithmetic operation always
469            // yields a float (`('16') + 0` is a float in 5.3, an integer in
470            // 5.4). This metamethod path is arithmetic-only, so the promotion
471            // never touches bitwise ops. Verified vs the 5.3.6/5.4.7 oracle.
472            if ok
473                && matches!(
474                    state.global().lua_version,
475                    lua_types::LuaVersion::V51
476                        | lua_types::LuaVersion::V52
477                        | lua_types::LuaVersion::V53
478                )
479            {
480                if let Some(f) = lua_vm::api::to_number_x(state, -1) {
481                    state.pop();
482                    state.push(LuaValue::Float(f));
483                }
484            }
485            Ok(ok)
486        } else {
487            Ok(false)
488        }
489    }
490}
491
492/// Try to invoke the metamethod `mtname` on the two operands.
493///
494fn trymt(state: &mut LuaState, mtname: &[u8]) -> Result<(), LuaError> {
495    // PORT NOTE: `state.set_top` (inherent) takes an absolute StackIdx and
496    // would wipe the call frame's arguments. `lua_settop` is frame-relative
497    // — keep the first two args of the current C function.
498    lua_vm::api::set_top(state, 2)?;
499    //        luaL_error(...)
500    let t2_is_string = state.type_at(2) == LuaType::String;
501    let has_mm = state.get_meta_field(2, mtname)?;
502    if t2_is_string || !has_mm {
503        let op = &mtname[2..]; // skip "__"
504        return Err(LuaError::runtime(format_args!(
505            "attempt to {} a '{}' with a '{}'",
506            op.escape_ascii(),
507            state.type_name_at(-2).escape_ascii(),
508            state.type_name_at(-1).escape_ascii(),
509        )));
510    }
511    state.insert(-3)?;
512    state.call(2, 1)?;
513    Ok(())
514}
515
516/// Generic arithmetic helper: coerce both args and call `op`, else try metamethod.
517///
518fn arith(state: &mut LuaState, op: ArithOp, mtname: &[u8]) -> Result<usize, LuaError> {
519    if tonum(state, 1)? && tonum(state, 2)? {
520        state.arith(op)?;
521    } else {
522        trymt(state, mtname)?;
523    }
524    Ok(1)
525}
526
527pub fn arith_add(state: &mut LuaState) -> Result<usize, LuaError> {
528    arith(state, ArithOp::Add, b"__add")
529}
530pub fn arith_sub(state: &mut LuaState) -> Result<usize, LuaError> {
531    arith(state, ArithOp::Sub, b"__sub")
532}
533pub fn arith_mul(state: &mut LuaState) -> Result<usize, LuaError> {
534    arith(state, ArithOp::Mul, b"__mul")
535}
536pub fn arith_mod(state: &mut LuaState) -> Result<usize, LuaError> {
537    arith(state, ArithOp::Mod, b"__mod")
538}
539pub fn arith_pow(state: &mut LuaState) -> Result<usize, LuaError> {
540    arith(state, ArithOp::Pow, b"__pow")
541}
542pub fn arith_div(state: &mut LuaState) -> Result<usize, LuaError> {
543    arith(state, ArithOp::Div, b"__div")
544}
545pub fn arith_idiv(state: &mut LuaState) -> Result<usize, LuaError> {
546    arith(state, ArithOp::Idiv, b"__idiv")
547}
548pub fn arith_unm(state: &mut LuaState) -> Result<usize, LuaError> {
549    arith(state, ArithOp::Unm, b"__unm")
550}
551
552// ────────────────────────────────────────────────────────────────────────────
553// §4  Pattern-matching engine
554// ────────────────────────────────────────────────────────────────────────────
555
556/// Return `true` if `c` belongs to the character class `cl` (a `%x` letter).
557///
558#[inline]
559fn match_class(c: u8, cl: u8) -> bool {
560    let res = match cl.to_ascii_lowercase() {
561        b'a' => c.is_ascii_alphabetic(),
562        b'c' => c.is_ascii_control(),
563        b'd' => c.is_ascii_digit(),
564        b'g' => c.is_ascii_graphic(),
565        b'l' => c.is_ascii_lowercase(),
566        b'p' => c.is_ascii_punctuation(),
567        b's' => c.is_ascii_whitespace(),
568        b'u' => c.is_ascii_uppercase(),
569        b'w' => c.is_ascii_alphanumeric(),
570        b'x' => c.is_ascii_hexdigit(),
571        b'z' => c == 0,
572        _    => return cl == c,
573    };
574    if cl.is_ascii_lowercase() { res } else { !res }
575}
576
577/// Match character `c` against a bracket class `[p .. ec-1]`.
578///
579/// `p` and `ec` are indices into `pat`.
580#[inline]
581fn matchbracketclass(pat: &[u8], c: u8, mut p: usize, ec: usize) -> bool {
582    let sig = if p + 1 < pat.len() && pat[p + 1] == b'^' {
583        p += 1; // skip '^'
584        false
585    } else {
586        true
587    };
588    p += 1; // advance past '[' or '^'
589    while p < ec {
590        if pat[p] == L_ESC {
591            p += 1;
592            if p < ec && match_class(c, pat[p]) {
593                return sig;
594            }
595        } else if p + 1 < ec && pat[p + 1] == b'-' && p + 2 < ec {
596            let lo = pat[p];
597            p += 2;
598            let hi = pat[p];
599            if lo <= c && c <= hi {
600                return sig;
601            }
602        } else if pat[p] == c {
603            return sig;
604        }
605        p += 1;
606    }
607    !sig
608}
609
610/// Return `true` if the single character at `src[s]` matches the pattern
611/// element starting at `pat[p]` with class end at `ep`.
612///
613#[inline]
614fn singlematch(ms: &MatchState, s: usize, p: usize, ep: usize) -> bool {
615    if s >= ms.src.len() {
616        return false;
617    }
618    let c = ms.src[s];
619    match ms.pat[p] {
620        b'.' => true,
621        L_ESC => match_class(c, ms.pat[p + 1]),
622        b'[' => matchbracketclass(ms.pat, c, p, ep - 1),
623        pc   => pc == c,
624    }
625}
626
627/// Find the end of the pattern element starting at `pat[p]`.
628/// Returns the index one past the element, or an error for malformed patterns.
629///
630fn classend(ms: &MatchState, p: usize) -> Result<usize, LuaError> {
631    let pat = ms.pat;
632    match pat.get(p).copied() {
633        Some(L_ESC) => {
634            if p + 1 >= pat.len() {
635                return Err(LuaError::runtime(format_args!(
636                    "malformed pattern (ends with '%')"
637                )));
638            }
639            Ok(p + 2)
640        }
641        Some(b'[') => {
642            let mut q = p + 1;
643            if q < pat.len() && pat[q] == b'^' {
644                q += 1;
645            }
646            loop {
647                if q >= pat.len() {
648                    return Err(LuaError::runtime(format_args!(
649                        "malformed pattern (missing ']')"
650                    )));
651                }
652                let ch = pat[q];
653                q += 1;
654                if ch == L_ESC && q < pat.len() {
655                    q += 1;
656                }
657                if q < pat.len() && pat[q] == b']' {
658                    return Ok(q + 1);
659                }
660            }
661        }
662        Some(_) => Ok(p + 1),
663        None => Ok(p),
664    }
665}
666
667/// Check that capture `l` (1-based char digit from pattern) is valid.
668/// Returns the 0-based capture index.
669///
670fn check_capture(ms: &MatchState, l: u8) -> Result<usize, LuaError> {
671    let signed = (l as i32) - (b'1' as i32);
672    if signed < 0
673        || signed >= ms.level as i32
674        || ms.captures[signed as usize].len == CAP_UNFINISHED
675    {
676        return Err(LuaError::runtime(format_args!(
677            "invalid capture index %{}",
678            signed + 1
679        )));
680    }
681    Ok(signed as usize)
682}
683
684/// Find the most recent unfinished capture to close.
685///
686fn capture_to_close(ms: &MatchState) -> Result<usize, LuaError> {
687    let mut level = ms.level as usize;
688    while level > 0 {
689        level -= 1;
690        if ms.captures[level].len == CAP_UNFINISHED {
691            return Ok(level);
692        }
693    }
694    Err(LuaError::runtime(format_args!("invalid pattern capture")))
695}
696
697/// Match a balanced string `%bxy` starting at `src[s]`.
698///
699/// Returns the new `s` position after the match, or `None`.
700fn matchbalance(ms: &MatchState, s: usize, p: usize) -> Result<Option<usize>, LuaError> {
701    if p + 1 >= ms.pat.len() {
702        return Err(LuaError::runtime(format_args!(
703            "malformed pattern (missing arguments to '%b')"
704        )));
705    }
706    let b = ms.pat[p];
707    let e = ms.pat[p + 1];
708    if s >= ms.src.len() || ms.src[s] != b {
709        return Ok(None);
710    }
711    let mut cont = 1i32;
712    let mut s = s + 1;
713    while s < ms.src.len() {
714        if ms.src[s] == e {
715            cont -= 1;
716            if cont == 0 {
717                return Ok(Some(s + 1));
718            }
719        } else if ms.src[s] == b {
720            cont += 1;
721        }
722        s += 1;
723    }
724    Ok(None)
725}
726
727/// Greedy match: match as many as possible, then try the rest of the pattern.
728///
729fn max_expand(
730    ms: &mut MatchState,
731    s: usize,
732    p: usize,
733    ep: usize,
734) -> Result<Option<usize>, LuaError> {
735    let mut count: isize = 0;
736    while singlematch(ms, s + count as usize, p, ep) {
737        count += 1;
738    }
739    while count >= 0 {
740        let res = match_pat(ms, s + count as usize, ep + 1)?;
741        if res.is_some() {
742            return Ok(res);
743        }
744        count -= 1;
745    }
746    Ok(None)
747}
748
749/// Lazy match: try the rest of the pattern first, then expand by one.
750///
751fn min_expand(
752    ms: &mut MatchState,
753    mut s: usize,
754    p: usize,
755    ep: usize,
756) -> Result<Option<usize>, LuaError> {
757    loop {
758        let res = match_pat(ms, s, ep + 1)?;
759        if res.is_some() {
760            return Ok(res);
761        } else if singlematch(ms, s, p, ep) {
762            s += 1;
763        } else {
764            return Ok(None);
765        }
766    }
767}
768
769/// Open a new capture at `src[s]`.
770///
771fn start_capture(
772    ms: &mut MatchState,
773    s: usize,
774    p: usize,
775    what: isize,
776) -> Result<Option<usize>, LuaError> {
777    let level = ms.level as usize;
778    if level >= LUA_MAX_CAPTURES {
779        return Err(LuaError::runtime(format_args!("too many captures")));
780    }
781    ms.captures[level].init = s;
782    ms.captures[level].len = what;
783    ms.level += 1;
784    let res = match_pat(ms, s, p)?;
785    if res.is_none() {
786        ms.level -= 1; // undo capture
787    }
788    Ok(res)
789}
790
791/// Close the most recent open capture at `src[s]`.
792///
793fn end_capture(ms: &mut MatchState, s: usize, p: usize) -> Result<Option<usize>, LuaError> {
794    let l = capture_to_close(ms)?;
795    ms.captures[l].len = (s - ms.captures[l].init) as isize;
796    let res = match_pat(ms, s, p)?;
797    if res.is_none() {
798        ms.captures[l].len = CAP_UNFINISHED; // undo
799    }
800    Ok(res)
801}
802
803/// Match a back-reference `%n` against `src[s]`.
804///
805fn match_capture(ms: &MatchState, s: usize, l: u8) -> Result<Option<usize>, LuaError> {
806    let idx = check_capture(ms, l)?;
807    let cap_len = ms.captures[idx].len as usize;
808    let cap_init = ms.captures[idx].init;
809    if ms.src.len() - s >= cap_len
810        && &ms.src[s..s + cap_len] == &ms.src[cap_init..cap_init + cap_len]
811    {
812        Ok(Some(s + cap_len))
813    } else {
814        Ok(None)
815    }
816}
817
818/// Core recursive pattern matcher.
819/// Returns `Ok(Some(new_s))` on match, `Ok(None)` on failure, `Err` on error.
820///
821/// The C code uses `goto init` for tail calls; here we use a loop.
822fn match_pat(ms: &mut MatchState, mut s: usize, mut p: usize) -> Result<Option<usize>, LuaError> {
823    if ms.aborted {
824        return Ok(None);
825    }
826    ms.steps += 1;
827    if ms.step_limit != 0 && ms.steps > ms.step_limit {
828        ms.aborted = true;
829        return Ok(None);
830    }
831    ms.matchdepth -= 1;
832    if ms.matchdepth < 0 {
833        ms.matchdepth = 0;
834        return Err(LuaError::runtime(format_args!("pattern too complex")));
835    }
836
837    // Use a loop to simulate `goto init` (tail-call optimization).
838    let result = 'outer: loop {
839        if p >= ms.pat.len() {
840            // end of pattern — full match up to current s
841            break 'outer Ok(Some(s));
842        }
843
844        match ms.pat[p] {
845            b'(' => {
846                let s2 = if p + 1 < ms.pat.len() && ms.pat[p + 1] == b')' {
847                    // position capture
848                    start_capture(ms, s, p + 2, CAP_POSITION)?
849                } else {
850                    start_capture(ms, s, p + 1, CAP_UNFINISHED)?
851                };
852                break 'outer Ok(s2);
853            }
854            b')' => {
855                let s2 = end_capture(ms, s, p + 1)?;
856                break 'outer Ok(s2);
857            }
858            b'$' => {
859                if p + 1 != ms.pat.len() {
860                    // fall through to default
861                    let ep = classend(ms, p)?;
862                    let s2 = handle_class_with_suffix(ms, s, p, ep)?;
863                    break 'outer Ok(s2);
864                }
865                break 'outer Ok(if s == ms.src.len() { Some(s) } else { None });
866            }
867            L_ESC => {
868                match ms.pat.get(p + 1).copied().unwrap_or(0) {
869                    b'b' => {
870                        let s2 = matchbalance(ms, s, p + 2)?;
871                        if let Some(ns) = s2 {
872                            s = ns;
873                            p += 4;
874                            continue 'outer; // tail call: match(ms, s, p+4)
875                        }
876                        break 'outer Ok(None);
877                    }
878                    b'f' => {
879                        p += 2;
880                        if ms.pat.get(p).copied() != Some(b'[') {
881                            return Err(LuaError::runtime(format_args!(
882                                "missing '[' after '%f' in pattern"
883                            )));
884                        }
885                        let ep = classend(ms, p)?;
886                        let previous = if s == 0 { 0u8 } else { ms.src[s - 1] };
887                        let current = ms.src.get(s).copied().unwrap_or(0);
888                        if !matchbracketclass(ms.pat, previous, p, ep - 1)
889                            && matchbracketclass(ms.pat, current, p, ep - 1)
890                        {
891                            p = ep;
892                            continue 'outer; // tail call: match(ms, s, ep)
893                        }
894                        break 'outer Ok(None);
895                    }
896                    c @ b'0'..=b'9' => {
897                        let s2 = match_capture(ms, s, c)?;
898                        if let Some(ns) = s2 {
899                            s = ns;
900                            p += 2;
901                            continue 'outer; // tail call: match(ms, s, p+2)
902                        }
903                        break 'outer Ok(None);
904                    }
905                    _ => {
906                        // fall through to default class handling
907                        let ep = classend(ms, p)?;
908                        let s2 = handle_class_with_suffix(ms, s, p, ep)?;
909                        break 'outer Ok(s2);
910                    }
911                }
912            }
913            _ => {
914                // default: pattern class plus optional suffix
915                let ep = classend(ms, p)?;
916                let s2 = handle_class_with_suffix(ms, s, p, ep)?;
917                break 'outer Ok(s2);
918            }
919        }
920    };
921
922    ms.matchdepth += 1;
923    result
924}
925
926/// Handle a pattern class element with an optional repetition suffix (`*`, `+`, `?`, `-`).
927///
928/// PORT NOTE: Factored out from `match_pat`'s `default/dflt` label to share
929/// code between the ESC-default and plain-default paths.
930fn handle_class_with_suffix(
931    ms: &mut MatchState,
932    s: usize,
933    p: usize,
934    ep: usize,
935) -> Result<Option<usize>, LuaError> {
936    let matched_once = singlematch(ms, s, p, ep);
937    if !matched_once {
938        //    else s = NULL;
939        match ms.pat.get(ep).copied() {
940            Some(b'*') | Some(b'?') | Some(b'-') => {
941                // Accept zero occurrences: tail-call match(ms, s, ep+1)
942                // We can't do a tail call into match_pat because we're returning
943                // from handle_class_with_suffix, but we can call it directly.
944                return match_pat(ms, s, ep + 1);
945            }
946            _ => return Ok(None),
947        }
948    }
949
950    // Matched at least once
951    match ms.pat.get(ep).copied() {
952        Some(b'?') => {
953            // Optional: try matching with s+1, fall back to ep+1
954            let res = match_pat(ms, s + 1, ep + 1)?;
955            if res.is_some() {
956                Ok(res)
957            } else {
958                match_pat(ms, s, ep + 1)
959            }
960        }
961        Some(b'+') => {
962            // 1 or more: greedy from s+1
963            max_expand(ms, s + 1, p, ep)
964        }
965        Some(b'*') => {
966            // 0 or more: greedy from s
967            max_expand(ms, s, p, ep)
968        }
969        Some(b'-') => {
970            // 0 or more: lazy from s
971            min_expand(ms, s, p, ep)
972        }
973        _ => {
974            // No suffix: match one, advance both s and p
975            match_pat(ms, s + 1, ep)
976        }
977    }
978}
979
980// ────────────────────────────────────────────────────────────────────────────
981// §5  Pattern-matching public API helpers
982// ────────────────────────────────────────────────────────────────────────────
983
984/// Find `needle` in `haystack` using a plain memmem-style search.
985///
986/// Returns the byte-offset of the first occurrence, or `None`.
987fn lmemfind(haystack: &[u8], needle: &[u8]) -> Option<usize> {
988    if needle.is_empty() {
989        return Some(0);
990    }
991    if needle.len() > haystack.len() {
992        return None;
993    }
994    let first = needle[0];
995    let rest = &needle[1..];
996    let limit = haystack.len() - rest.len();
997    let mut s = 0;
998    while s <= limit {
999        if let Some(pos) = haystack[s..].iter().position(|&b| b == first) {
1000            let pos = s + pos;
1001            if pos + 1 + rest.len() <= haystack.len()
1002                && &haystack[pos + 1..pos + 1 + rest.len()] == rest
1003            {
1004                return Some(pos);
1005            }
1006            s = pos + 1;
1007        } else {
1008            break;
1009        }
1010    }
1011    None
1012}
1013
1014/// Check whether the pattern `pat` has no special characters (for plain search).
1015///
1016fn nospecials(pat: &[u8]) -> bool {
1017    !pat.iter().any(|b| SPECIALS.contains(b))
1018}
1019
1020/// Information about one capture result.
1021enum CaptureInfo<'a> {
1022    /// A position capture; value is 1-based index.
1023    Position(i64),
1024    /// A string capture (slice of source).
1025    Bytes(&'a [u8]),
1026}
1027
1028/// Get information about the `i`-th capture.
1029/// If there are no captures and `i == 0`, returns the whole match `s..e`.
1030///
1031fn get_one_capture<'a>(
1032    ms: &'a MatchState,
1033    i: usize,
1034    s: usize,
1035    e: usize,
1036) -> Result<CaptureInfo<'a>, LuaError> {
1037    if i >= ms.level as usize {
1038        if i != 0 {
1039            return Err(LuaError::runtime(format_args!(
1040                "invalid capture index %{}",
1041                i + 1
1042            )));
1043        }
1044        // Return whole match
1045        return Ok(CaptureInfo::Bytes(&ms.src[s..e]));
1046    }
1047    let cap = &ms.captures[i];
1048    if cap.len == CAP_UNFINISHED {
1049        return Err(LuaError::runtime(format_args!("unfinished capture")));
1050    }
1051    if cap.len == CAP_POSITION {
1052        return Ok(CaptureInfo::Position((cap.init + 1) as i64));
1053    }
1054    let len = cap.len as usize;
1055    Ok(CaptureInfo::Bytes(&ms.src[cap.init..cap.init + len]))
1056}
1057
1058/// Push all captures (or whole match if none) onto the stack.
1059/// Returns the number of values pushed.
1060///
1061fn push_captures(
1062    state: &mut LuaState,
1063    ms: &MatchState,
1064    s: usize,
1065    e: usize,
1066) -> Result<usize, LuaError> {
1067    let nlevels = if ms.level == 0 { 1 } else { ms.level as usize };
1068    state.ensure_stack(nlevels as i32, "too many captures")?;
1069    for i in 0..nlevels {
1070        match get_one_capture(ms, i, s, e)? {
1071            CaptureInfo::Position(n) => state.push(LuaValue::Int(n)),
1072            CaptureInfo::Bytes(b) => state.push_bytes(b)?,
1073        }
1074    }
1075    Ok(nlevels)
1076}
1077
1078// ────────────────────────────────────────────────────────────────────────────
1079// §6  str_find / str_match / gmatch / gsub
1080// ────────────────────────────────────────────────────────────────────────────
1081
1082/// Shared implementation of `string.find` and `string.match`.
1083///
1084fn str_find_aux(state: &mut LuaState, find: bool) -> Result<usize, LuaError> {
1085    let s_ref = match state.to_lua_string(1) {
1086        Some(r) => r,
1087        None => {
1088            state.check_arg_string(1)?;
1089            unreachable!("check_arg_string raises when arg #1 is not a string");
1090        }
1091    };
1092    let p_ref = match state.to_lua_string(2) {
1093        Some(r) => r,
1094        None => {
1095            state.check_arg_string(2)?;
1096            unreachable!("check_arg_string raises when arg #2 is not a string");
1097        }
1098    };
1099    let s: &[u8] = s_ref.as_bytes();
1100    let p: &[u8] = p_ref.as_bytes();
1101    let ls = s.len();
1102    let lp = p.len();
1103    let init_raw = state.opt_arg_integer(3, 1)?;
1104    let init = pos_relat_i(init_raw, ls).saturating_sub(1);
1105
1106    if init > ls {
1107        state.push(LuaValue::Nil);
1108        return Ok(1);
1109    }
1110
1111    if find && (state.arg_to_bool(4) || nospecials(p)) {
1112        // plain search
1113        if let Some(pos) = lmemfind(&s[init..], p) {
1114            let abs = init + pos;
1115            state.push(LuaValue::Int((abs + 1) as i64));
1116            state.push(LuaValue::Int((abs + lp) as i64));
1117            return Ok(2);
1118        }
1119    } else {
1120        let step_limit = state.sandbox_match_step_limit();
1121        let mut ms = MatchState::new(s, p, step_limit);
1122        let anchor = p.first() == Some(&b'^');
1123        let p_slice = if anchor { &p[1..] } else { p };
1124        ms.pat = p_slice;
1125
1126        let mut s1 = init;
1127        let mut matched: Option<usize> = None;
1128        loop {
1129            ms.reset_level();
1130            if let Some(res) = match_pat(&mut ms, s1, 0)? {
1131                matched = Some(res);
1132                break;
1133            }
1134            if ms.aborted || s1 >= ms.src.len() || anchor {
1135                break;
1136            }
1137            s1 += 1;
1138        }
1139
1140        if let Some(err) = state.sandbox_charge(ms.steps) {
1141            return Err(err);
1142        }
1143
1144        if let Some(res) = matched {
1145            if find {
1146                state.push(LuaValue::Int((s1 + 1) as i64));
1147                state.push(LuaValue::Int(res as i64));
1148                let nc = push_captures(state, &ms, 0, 0)?;
1149                return Ok(nc + 2);
1150            } else {
1151                return push_captures(state, &ms, s1, res);
1152            }
1153        }
1154    }
1155
1156    state.push(LuaValue::Nil);
1157    Ok(1)
1158}
1159
1160/// `string.find(s, pattern [, init [, plain]])` — find pattern in `s`.
1161///
1162pub fn str_find(state: &mut LuaState) -> Result<usize, LuaError> {
1163    str_find_aux(state, true)
1164}
1165
1166/// `string.match(s, pattern [, init])` — match pattern against `s`.
1167///
1168pub fn str_match(state: &mut LuaState) -> Result<usize, LuaError> {
1169    str_find_aux(state, false)
1170}
1171
1172/// Continuation function for `string.gmatch` iterator closure.
1173///
1174///
1175/// PORT NOTE: The C version stores `GMatchState` inside a heap-allocated
1176/// userdata referenced by upvalue 3, then mutates fields via the raw pointer
1177/// each iteration. Our Phase-A `LuaCClosure.upvalues` is immutable, so the
1178/// iterator state lives in a Lua table referenced by upvalue 1 with
1179/// integer-keyed slots:
1180///   t[1] = source bytes (string), t[2] = pattern bytes (string),
1181///   t[3] = current source position (1-based; equals `lastmatch` after a
1182///   successful match), t[4] = end of last match (`0` ≡ NULL in C, meaning
1183///   "no match yet").
1184///
1185/// PERF NOTE: The previous version pushed the upvalue table onto the stack
1186/// and then issued six `raw_geti` / `raw_seti` calls plus four `to_lua_string`
1187/// / `to_integer_x` reads — each of which re-resolves the stack index via
1188/// `index_to_value`. That made `index_to_value` the #1 non-algorithm frame in
1189/// `string_ops_long` at 9.4% of wall. The current version resolves the
1190/// upvalue once via `value_at`, extracts the `GcRef<LuaTable>`, and reads /
1191/// writes its integer-keyed slots directly through `LuaTableRefExt`. This is
1192/// the same shape as C-Lua's `luaH_getint` / `luaH_setint` direct table ops
1193/// against the embedded `GMatchState` struct fields — no stack roundtrip
1194/// per probe.
1195pub fn gmatch_aux(state: &mut LuaState) -> Result<usize, LuaError> {
1196    let upval = state.value_at(upvalue_index(1));
1197    let tbl = match upval {
1198        LuaValue::Table(t) => t,
1199        _ => return Ok(0),
1200    };
1201
1202    let s_val = tbl.get_int(1);
1203    let p_val = tbl.get_int(2);
1204    let (LuaValue::Str(s_str), LuaValue::Str(p_str)) = (&s_val, &p_val) else {
1205        return Ok(0);
1206    };
1207    let s: &[u8] = s_str.as_bytes();
1208    let p: &[u8] = p_str.as_bytes();
1209
1210    let pos = match tbl.get_int(3) {
1211        LuaValue::Int(n) => n,
1212        _ => 1,
1213    };
1214    let lastmatch_raw = match tbl.get_int(4) {
1215        LuaValue::Int(n) => n,
1216        _ => 0,
1217    };
1218    let last_match: Option<usize> = if lastmatch_raw <= 0 {
1219        None
1220    } else {
1221        Some((lastmatch_raw - 1) as usize)
1222    };
1223
1224    let ls = s.len();
1225    let start_pos = if pos < 1 { 0usize } else { (pos - 1) as usize };
1226
1227    let step_limit = state.sandbox_match_step_limit();
1228    let mut ms = MatchState::new(s, p, step_limit);
1229
1230    let mut src = start_pos;
1231    let mut hit: Option<(usize, usize)> = None;
1232    while src <= ls {
1233        ms.reset_level();
1234        if let Some(e) = match_pat(&mut ms, src, 0)? {
1235            if Some(e) != last_match {
1236                hit = Some((src, e));
1237                break;
1238            }
1239        }
1240        if ms.aborted {
1241            break;
1242        }
1243        src += 1;
1244    }
1245
1246    if let Some(err) = state.sandbox_charge(ms.steps) {
1247        return Err(err);
1248    }
1249
1250    if let Some((src, e)) = hit {
1251        let e_val = LuaValue::Int((e + 1) as i64);
1252        tbl.raw_set_int(state, 3, e_val.clone())?;
1253        tbl.raw_set_int(state, 4, e_val)?;
1254        return push_captures(state, &ms, src, e);
1255    }
1256
1257    Ok(0)
1258}
1259
1260/// `string.gmatch(s, pattern [, init])` — return an iterator for all matches.
1261///
1262///
1263/// PORT NOTE: C uses `lua_newuserdatauv` for the GMatchState plus a 3-upvalue
1264/// C closure. Phase-A LuaCClosure upvalues are immutable, so we collapse the
1265/// state into a 4-element Lua table held in a single upvalue (see
1266/// `gmatch_aux`).
1267pub fn gmatch(state: &mut LuaState) -> Result<usize, LuaError> {
1268    let s_ref = match state.to_lua_string(1) {
1269        Some(r) => r,
1270        None => {
1271            state.check_arg_string(1)?;
1272            unreachable!("check_arg_string raises when arg #1 is not a string");
1273        }
1274    };
1275    let ls = s_ref.len();
1276    match state.to_lua_string(2) {
1277        Some(_) => {}
1278        None => {
1279            state.check_arg_string(2)?;
1280            unreachable!("check_arg_string raises when arg #2 is not a string");
1281        }
1282    };
1283    let init_raw = state.opt_arg_integer(3, 1)?;
1284    let mut init = pos_relat_i(init_raw, ls).saturating_sub(1);
1285    if init > ls {
1286        init = ls + 1;
1287    }
1288
1289    lua_vm::api::set_top(state, 2)?;
1290
1291    state.create_table(4, 0)?;
1292    let tbl_idx = state.top();
1293    state.push_value_at(1)?;
1294    state.raw_seti(tbl_idx, 1)?;
1295    state.push_value_at(2)?;
1296    state.raw_seti(tbl_idx, 2)?;
1297    state.push(LuaValue::Int((init + 1) as i64));
1298    state.raw_seti(tbl_idx, 3)?;
1299    state.push(LuaValue::Int(0));
1300    state.raw_seti(tbl_idx, 4)?;
1301
1302    state.push_c_closure(gmatch_aux, 1)?;
1303    Ok(1)
1304}
1305
1306/// Add a replacement string with `%n` capture references to `buf`.
1307///
1308fn add_s(
1309    state: &mut LuaState,
1310    ms: &MatchState,
1311    buf: &mut Vec<u8>,
1312    s: usize,
1313    e: usize,
1314) -> Result<(), LuaError> {
1315    let news_bytes = state.to_lua_string_bytes(3).unwrap_or_default();
1316    let mut i = 0usize;
1317    while i < news_bytes.len() {
1318        if news_bytes[i] != L_ESC {
1319            buf.push(news_bytes[i]);
1320            i += 1;
1321        } else {
1322            i += 1; // skip ESC
1323            if i >= news_bytes.len() {
1324                break;
1325            }
1326            let c = news_bytes[i];
1327            if c == L_ESC {
1328                buf.push(L_ESC);
1329            } else if c == b'0' {
1330                buf.extend_from_slice(&ms.src[s..e]);
1331            } else if c.is_ascii_digit() {
1332                match get_one_capture(ms, (c - b'1') as usize, s, e)? {
1333                    CaptureInfo::Position(n) => {
1334                        // push position then pop into buf
1335                        let formatted = format!("{}", n).into_bytes();
1336                        buf.extend_from_slice(&formatted);
1337                    }
1338                    CaptureInfo::Bytes(b) => {
1339                        buf.extend_from_slice(b);
1340                    }
1341                }
1342            } else {
1343                return Err(LuaError::runtime(format_args!(
1344                    "invalid use of '{}' in replacement string",
1345                    L_ESC as char
1346                )));
1347            }
1348            i += 1;
1349        }
1350    }
1351    Ok(())
1352}
1353
1354/// Add the replacement value (string, table lookup, or function call) to `buf`.
1355/// Returns `true` if the original text was changed.
1356///
1357fn add_value(
1358    state: &mut LuaState,
1359    ms: &MatchState,
1360    buf: &mut Vec<u8>,
1361    s: usize,
1362    e: usize,
1363    tr: LuaType,
1364) -> Result<bool, LuaError> {
1365    match tr {
1366        LuaType::Function => {
1367            state.push_value_at(3)?;
1368            let n = push_captures(state, ms, s, e)?;
1369            state.call(n as i32, 1)?;
1370        }
1371        LuaType::Table => {
1372            match get_one_capture(ms, 0, s, e)? {
1373                CaptureInfo::Position(n) => state.push(LuaValue::Int(n)),
1374                CaptureInfo::Bytes(b) => state.push_bytes(b)?,
1375            }
1376            state.get_table(3)?;
1377        }
1378        _ => {
1379            // LUA_TNUMBER or LUA_TSTRING: add replacement string directly
1380            add_s(state, ms, buf, s, e)?;
1381            return Ok(true);
1382        }
1383    }
1384
1385    let top_bool = state.arg_to_bool(-1);
1386    if !top_bool {
1387        state.pop_n(1);
1388        buf.extend_from_slice(&ms.src[s..e]);
1389        return Ok(false);
1390    }
1391    if state.type_at(-1) != LuaType::String {
1392        let tname = state.type_name_at(-1).to_owned();
1393        return Err(LuaError::runtime(format_args!(
1394            "invalid replacement value (a {})", tname.escape_ascii()
1395        )));
1396    }
1397    let v = state.to_bytes(-1).unwrap_or_default();
1398    state.pop();
1399    buf.extend_from_slice(&v);
1400    Ok(true)
1401}
1402
1403/// `string.gsub(s, pattern, repl [, n])` — global substitution.
1404///
1405pub fn str_gsub(state: &mut LuaState) -> Result<usize, LuaError> {
1406    let src_bytes = state.check_arg_string(1)?;
1407    let pat_bytes = state.check_arg_string(2)?;
1408    let src_len = src_bytes.len();
1409    let max_s = state.opt_arg_integer(4, (src_len + 1) as i64)?;
1410    let tr = state.type_at(3);
1411
1412    if !matches!(tr, LuaType::Number | LuaType::String | LuaType::Function | LuaType::Table) {
1413        let v = state.arg(3);
1414        return Err(LuaError::type_arg_error(3, "string/function/table", &v));
1415    }
1416
1417    let src_owned = src_bytes;
1418    let pat_owned = pat_bytes;
1419
1420    let anchor = pat_owned.first() == Some(&b'^');
1421    let pat_slice = if anchor { &pat_owned[1..] } else { &pat_owned[..] };
1422
1423    let step_limit = state.sandbox_match_step_limit();
1424    let mut ms = MatchState::new(&src_owned, pat_slice, step_limit);
1425    let mut buf: Vec<u8> = Vec::new();
1426    let mut src_pos = 0usize;
1427    let mut last_match: Option<usize> = None;
1428    let mut n: i64 = 0;
1429    let mut changed = false;
1430
1431    while n < max_s {
1432        ms.reset_level();
1433        let maybe_e = match_pat(&mut ms, src_pos, 0)?;
1434        if let Some(e) = maybe_e {
1435            if last_match != Some(e) {
1436                n += 1;
1437                let delta = add_value(state, &ms, &mut buf, src_pos, e, tr)?;
1438                changed |= delta;
1439                src_pos = e;
1440                last_match = Some(e);
1441            } else if src_pos < ms.src.len() {
1442                buf.push(ms.src[src_pos]);
1443                src_pos += 1;
1444            } else {
1445                break;
1446            }
1447        } else if src_pos < ms.src.len() {
1448            buf.push(ms.src[src_pos]);
1449            src_pos += 1;
1450        } else {
1451            break;
1452        }
1453        if ms.aborted || anchor {
1454            break;
1455        }
1456    }
1457
1458    if let Some(err) = state.sandbox_charge(ms.steps) {
1459        return Err(err);
1460    }
1461
1462    if !changed {
1463        state.push_value_at(1)?;
1464    } else {
1465        buf.extend_from_slice(&ms.src[src_pos..]);
1466        state.push_bytes(&buf)?;
1467    }
1468    state.push(LuaValue::Int(n));
1469    Ok(2)
1470}
1471
1472// ────────────────────────────────────────────────────────────────────────────
1473// §7  String format (`string.format`)
1474// ────────────────────────────────────────────────────────────────────────────
1475
1476/// Add a hex-float digit to buffer and return the fractional remainder.
1477///
1478fn adddigit(buf: &mut Vec<u8>, x: f64) -> f64 {
1479    let dd = x.floor();
1480    let d = dd as i32;
1481    let c = if d < 10 { b'0' + d as u8 } else { b'a' + (d - 10) as u8 };
1482    buf.push(c);
1483    x - dd
1484}
1485
1486/// Convert a float to a hex-float string body (digits only, no sign, no `0x` prefix).
1487///
1488/// Returns `(frac_digits, exponent_string)` for use by `format_hex_float`.
1489///
1490fn num2straux(x: f64) -> Vec<u8> {
1491    format_hex_float(x, None)
1492}
1493
1494/// Produce a hex-float string for `x` with optional precision (digits after the point).
1495///
1496/// When `precision` is `None` the minimum number of digits needed for a round-trip
1497/// is emitted (C's default `%a` behaviour). When `precision` is `Some(p)` exactly `p`
1498/// digits follow the radix point; trailing zeros are added as needed, and excess
1499/// digits are discarded (C truncates rather than rounds, matching the C `printf`
1500/// behaviour on the tested platforms).
1501fn format_hex_float(x: f64, precision: Option<usize>) -> Vec<u8> {
1502    if x.is_nan() {
1503        return b"nan".to_vec();
1504    }
1505    if x.is_infinite() {
1506        return if x < 0.0 { b"-inf".to_vec() } else { b"inf".to_vec() };
1507    }
1508    if x == 0.0 {
1509        let sign: &[u8] = if x.is_sign_negative() { b"-" } else { b"" };
1510        return match precision {
1511            None => [sign, b"0x0p+0"].concat(),
1512            Some(0) => [sign, b"0x0p+0"].concat(),
1513            Some(p) => {
1514                let zeros = "0".repeat(p);
1515                [sign, b"0x0.", zeros.as_bytes(), b"p+0"].concat()
1516            }
1517        };
1518    }
1519
1520    let (m_raw, exp) = frexp(x);
1521    let mut buf: Vec<u8> = Vec::new();
1522    let mut m = m_raw;
1523    if m < 0.0 {
1524        buf.push(b'-');
1525        m = -m;
1526    }
1527    buf.extend_from_slice(b"0x");
1528
1529    let nbfd = 1;
1530    m = adddigit(&mut buf, m * (1 << nbfd) as f64);
1531    let e = exp - nbfd;
1532
1533    match precision {
1534        None => {
1535            if m > 0.0 {
1536                buf.push(b'.');
1537                while m > 0.0 {
1538                    m = adddigit(&mut buf, m * 16.0);
1539                }
1540            }
1541        }
1542        Some(0) => {}
1543        Some(p) => {
1544            buf.push(b'.');
1545            for _ in 0..p {
1546                if m > 0.0 {
1547                    m = adddigit(&mut buf, m * 16.0);
1548                } else {
1549                    buf.push(b'0');
1550                }
1551            }
1552        }
1553    }
1554
1555    let exp_str = format!("p{:+}", e);
1556    buf.extend_from_slice(exp_str.as_bytes());
1557    buf
1558}
1559
1560/// Decompose `x` into mantissa in `[-1.0, -0.5] ∪ [0.5, 1.0)` and exponent.
1561///
1562/// Equivalent to C's `frexp`. The sign of `x` is preserved in the returned mantissa
1563/// so that `num2straux` can emit the leading `-` correctly for negative inputs.
1564fn frexp(x: f64) -> (f64, i32) {
1565    if x == 0.0 || x.is_nan() || x.is_infinite() {
1566        return (x, 0);
1567    }
1568    let bits = x.to_bits();
1569    let sign_bit = bits & 0x8000_0000_0000_0000u64;
1570    let exp_bits = ((bits >> 52) & 0x7FF) as i32;
1571    if exp_bits == 0 {
1572        let (m, e) = frexp(x * (1u64 << 52) as f64);
1573        return (m, e - 52);
1574    }
1575    let exp = exp_bits - 1022;
1576    let mantissa_bits = sign_bit | (bits & 0x000F_FFFF_FFFF_FFFF) | 0x3FE0_0000_0000_0000;
1577    (f64::from_bits(mantissa_bits), exp)
1578}
1579
1580/// Convert float `n` to a Lua-readable literal (hex or special representation).
1581///
1582fn quotefloat(n: f64) -> Vec<u8> {
1583    if n == f64::INFINITY {
1584        return b"1e9999".to_vec();
1585    } else if n == f64::NEG_INFINITY {
1586        return b"-1e9999".to_vec();
1587    } else if n.is_nan() {
1588        return b"(0/0)".to_vec();
1589    }
1590    // hex float, ensuring dot separator
1591    let buf = num2straux(n);
1592    if !buf.contains(&b'.') && !buf.contains(&b'p') {
1593        // try to find locale decimal point and replace with '.'
1594        // PORT NOTE: We always produce '.' so this branch is not taken.
1595    }
1596    buf
1597}
1598
1599/// Add a quoted Lua string literal to `buf`.
1600///
1601fn addquoted(buf: &mut Vec<u8>, s: &[u8]) {
1602    buf.push(b'"');
1603    for (idx, &c) in s.iter().enumerate() {
1604        if c == b'"' || c == b'\\' || c == b'\n' {
1605            buf.push(b'\\');
1606            buf.push(c);
1607        } else if c.is_ascii_control() {
1608            let next_is_digit = s.get(idx + 1).map_or(false, |n| n.is_ascii_digit());
1609            let formatted = if next_is_digit {
1610                format!("\\{:03}", c)
1611            } else {
1612                format!("\\{}", c)
1613            };
1614            buf.extend_from_slice(formatted.as_bytes());
1615        } else {
1616            buf.push(c);
1617        }
1618    }
1619    buf.push(b'"');
1620}
1621
1622/// Add a Lua literal representation of arg `n` to `buf`.
1623///
1624fn addliteral(state: &mut LuaState, buf: &mut Vec<u8>, arg: i32) -> Result<(), LuaError> {
1625    match state.type_at(arg) {
1626        LuaType::String => {
1627            let s = state.check_arg_string(arg)?.to_vec();
1628            addquoted(buf, &s);
1629        }
1630        LuaType::Number => {
1631            if state.is_integer(arg) {
1632                let n = state.to_integer(arg).unwrap_or(0);
1633                let formatted = if n == i64::MIN {
1634                    format!("0x{:016x}", n as u64)
1635                } else {
1636                    format!("{}", n)
1637                };
1638                buf.extend_from_slice(formatted.as_bytes());
1639            } else {
1640                let n = state.to_number(arg).unwrap_or(0.0);
1641                let hex = quotefloat(n);
1642                buf.extend_from_slice(&hex);
1643            }
1644        }
1645        LuaType::Nil => {
1646            buf.extend_from_slice(b"nil");
1647        }
1648        LuaType::Boolean => {
1649            buf.extend_from_slice(if state.to_boolean(arg) { b"true" } else { b"false" });
1650        }
1651        _ => {
1652            return Err(LuaError::arg_error(arg, "value has no literal form"));
1653        }
1654    }
1655    Ok(())
1656}
1657
1658
1659/// Flags allowed per conversion type (matches lstrlib.c constants).
1660const FMT_FLAGS_F: &[u8] = b"-+#0 ";
1661const FMT_FLAGS_X: &[u8] = b"-#0";
1662const FMT_FLAGS_I: &[u8] = b"-+0 ";
1663const FMT_FLAGS_U: &[u8] = b"-0";
1664const FMT_FLAGS_C: &[u8] = b"-";
1665
1666/// Validate a format specifier against allowed flags and width/precision digit counts.
1667///
1668/// `form` is the full specifier slice including the leading `%` and the trailing
1669/// conversion character (e.g. `b"%100.3d"`). `flags` is the allowed-flags byte set for
1670/// this conversion type. `allow_precision` is false for conversions that forbid `.`.
1671///
1672/// Mirrors C `checkformat` in lstrlib.c: consumes flags, then up to 2 width digits,
1673/// then (if allowed) `.` + up to 2 precision digits, then asserts we are at the
1674/// conversion character. Returns `Err("invalid conversion specification")` on failure.
1675fn check_conv_spec(form: &[u8], flags: &[u8], allow_precision: bool) -> Result<(), LuaError> {
1676    let mut i = 1usize; // skip '%'
1677    while i < form.len() && flags.contains(&form[i]) {
1678        i += 1;
1679    }
1680    if i < form.len() && form[i] == b'0' {
1681        return Err(LuaError::runtime(format_args!("invalid conversion specification")));
1682    }
1683    if i < form.len() && form[i].is_ascii_digit() {
1684        i += 1;
1685        if i < form.len() && form[i].is_ascii_digit() {
1686            i += 1;
1687        }
1688    }
1689    if allow_precision && i < form.len() && form[i] == b'.' {
1690        i += 1;
1691        if i < form.len() && form[i].is_ascii_digit() {
1692            i += 1;
1693            if i < form.len() && form[i].is_ascii_digit() {
1694                i += 1;
1695            }
1696        }
1697    }
1698    if i != form.len() - 1 {
1699        return Err(LuaError::runtime(format_args!("invalid conversion specification")));
1700    }
1701    Ok(())
1702}
1703
1704/// Parsed printf-style format specifier (flags, width, precision).
1705#[derive(Default)]
1706struct FmtSpec {
1707    left_align: bool,
1708    plus_sign: bool,
1709    space_sign: bool,
1710    alt_form: bool,
1711    zero_pad: bool,
1712    width: usize,
1713    precision: Option<usize>,
1714}
1715
1716fn parse_fmt_spec(spec: &[u8]) -> FmtSpec {
1717    let mut s = FmtSpec::default();
1718    let mut i = 0;
1719    while i < spec.len() {
1720        match spec[i] {
1721            b'-' => s.left_align = true,
1722            b'+' => s.plus_sign = true,
1723            b' ' => s.space_sign = true,
1724            b'#' => s.alt_form = true,
1725            b'0' => s.zero_pad = true,
1726            _ => break,
1727        }
1728        i += 1;
1729    }
1730    while i < spec.len() && spec[i].is_ascii_digit() {
1731        s.width = s.width * 10 + (spec[i] - b'0') as usize;
1732        i += 1;
1733    }
1734    if i < spec.len() && spec[i] == b'.' {
1735        i += 1;
1736        let mut p = 0usize;
1737        while i < spec.len() && spec[i].is_ascii_digit() {
1738            p = p * 10 + (spec[i] - b'0') as usize;
1739            i += 1;
1740        }
1741        s.precision = Some(p);
1742    }
1743    s
1744}
1745
1746fn pad_str(buf: &mut Vec<u8>, body: &[u8], spec: &FmtSpec) {
1747    let body = match spec.precision {
1748        Some(p) if body.len() > p => &body[..p],
1749        _ => body,
1750    };
1751    if body.len() >= spec.width {
1752        buf.extend_from_slice(body);
1753        return;
1754    }
1755    let pad = spec.width - body.len();
1756    if spec.left_align {
1757        buf.extend_from_slice(body);
1758        for _ in 0..pad { buf.push(b' '); }
1759    } else {
1760        for _ in 0..pad { buf.push(b' '); }
1761        buf.extend_from_slice(body);
1762    }
1763}
1764
1765fn pad_int(buf: &mut Vec<u8>, sign_prefix: &[u8], digits: &[u8], spec: &FmtSpec) {
1766    let min_digits = spec.precision.unwrap_or(0);
1767    let zeroes_for_prec = if digits.len() < min_digits { min_digits - digits.len() } else { 0 };
1768    let core_len = sign_prefix.len() + zeroes_for_prec + digits.len();
1769    if core_len >= spec.width {
1770        buf.extend_from_slice(sign_prefix);
1771        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1772        buf.extend_from_slice(digits);
1773        return;
1774    }
1775    let pad = spec.width - core_len;
1776    let use_zero_pad = spec.zero_pad && !spec.left_align && spec.precision.is_none();
1777    if spec.left_align {
1778        buf.extend_from_slice(sign_prefix);
1779        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1780        buf.extend_from_slice(digits);
1781        for _ in 0..pad { buf.push(b' '); }
1782    } else if use_zero_pad {
1783        buf.extend_from_slice(sign_prefix);
1784        for _ in 0..pad { buf.push(b'0'); }
1785        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1786        buf.extend_from_slice(digits);
1787    } else {
1788        for _ in 0..pad { buf.push(b' '); }
1789        buf.extend_from_slice(sign_prefix);
1790        for _ in 0..zeroes_for_prec { buf.push(b'0'); }
1791        buf.extend_from_slice(digits);
1792    }
1793}
1794
1795fn signed_int_parts(n: i64, spec: &FmtSpec) -> (Vec<u8>, Vec<u8>) {
1796    if n == 0 && spec.precision == Some(0) {
1797        return (Vec::new(), Vec::new());
1798    }
1799    let (sign, abs_digits) = if n < 0 {
1800        (b"-".to_vec(), {
1801            let u = (n as i128).unsigned_abs();
1802            format!("{}", u).into_bytes()
1803        })
1804    } else {
1805        let s: Vec<u8> = if spec.plus_sign {
1806            b"+".to_vec()
1807        } else if spec.space_sign {
1808            b" ".to_vec()
1809        } else {
1810            Vec::new()
1811        };
1812        (s, format!("{}", n).into_bytes())
1813    };
1814    (sign, abs_digits)
1815}
1816
1817fn unsigned_int_parts(n: u64, base: u32, upper: bool, spec: &FmtSpec) -> (Vec<u8>, Vec<u8>) {
1818    let digits = if n == 0 && spec.precision == Some(0) {
1819        Vec::new()
1820    } else {
1821        match base {
1822            8 => format!("{:o}", n).into_bytes(),
1823            16 if upper => format!("{:X}", n).into_bytes(),
1824            16 => format!("{:x}", n).into_bytes(),
1825            _ => format!("{}", n).into_bytes(),
1826        }
1827    };
1828    let prefix: Vec<u8> = if spec.alt_form && n != 0 {
1829        match base {
1830            8 => b"0".to_vec(),
1831            16 if upper => b"0X".to_vec(),
1832            16 => b"0x".to_vec(),
1833            _ => Vec::new(),
1834        }
1835    } else {
1836        Vec::new()
1837    };
1838    (prefix, digits)
1839}
1840
1841fn format_float(n: f64, conv: u8, spec: &FmtSpec) -> Vec<u8> {
1842    let prec = spec.precision.unwrap_or(6);
1843    if n.is_nan() {
1844        return if conv.is_ascii_uppercase() { b"NAN".to_vec() } else { b"nan".to_vec() };
1845    }
1846    if n.is_infinite() {
1847        let s: &[u8] = if conv.is_ascii_uppercase() {
1848            if n < 0.0 { b"-INF" } else { b"INF" }
1849        } else if n < 0.0 { b"-inf" } else { b"inf" };
1850        return s.to_vec();
1851    }
1852    match conv {
1853        b'f' | b'F' => {
1854            let mut result = format!("{:.*}", prec, n).into_bytes();
1855            if spec.alt_form && !result.contains(&b'.') {
1856                result.push(b'.');
1857            }
1858            result
1859        }
1860        b'e' => format_exp(n, prec, false, spec.alt_form),
1861        b'E' => {
1862            let mut v = format_exp(n, prec, false, spec.alt_form);
1863            for b in v.iter_mut() { if *b == b'e' { *b = b'E'; } }
1864            v
1865        }
1866        b'g' | b'G' => {
1867            let p = if prec == 0 { 1 } else { prec };
1868            let v = format_g(n, p, spec.alt_form);
1869            if conv == b'G' {
1870                v.into_iter().map(|b| if b == b'e' { b'E' } else { b }).collect()
1871            } else { v }
1872        }
1873        _ => format!("{}", n).into_bytes(),
1874    }
1875}
1876
1877fn format_exp(n: f64, prec: usize, _upper: bool, alt: bool) -> Vec<u8> {
1878    if n == 0.0 {
1879        let mantissa: String = if prec == 0 {
1880            if alt { "0.".to_string() } else { "0".to_string() }
1881        } else {
1882            format!("0.{}", "0".repeat(prec))
1883        };
1884        return format!("{}e+00", mantissa).into_bytes();
1885    }
1886    let abs = n.abs();
1887    let exp = abs.log10().floor() as i32;
1888    let mantissa = n / 10f64.powi(exp);
1889    let mantissa_str = format!("{:.*}", prec, mantissa);
1890    let (mant_final, exp_final) = if let Some(dot_pos) = mantissa_str.find('.') {
1891        let int_part = &mantissa_str[..dot_pos];
1892        let abs_int = int_part.trim_start_matches('-');
1893        if abs_int.len() > 1 {
1894            let new_mant = if prec == 0 {
1895                mantissa_str[..mantissa_str.len()-1].to_string()
1896            } else {
1897                let neg = if int_part.starts_with('-') { "-" } else { "" };
1898                let frac = &mantissa_str[dot_pos+1..];
1899                format!("{}{}.{}{}", neg, &abs_int[..1], &abs_int[1..], frac)
1900            };
1901            (new_mant, exp + (abs_int.len() as i32 - 1))
1902        } else {
1903            (mantissa_str, exp)
1904        }
1905    } else if mantissa_str.trim_start_matches('-').len() > 1 {
1906        let neg = if mantissa_str.starts_with('-') { "-" } else { "" };
1907        let body = mantissa_str.trim_start_matches('-');
1908        let bumped = format!("{}{}.{}", neg, &body[..1], &body[1..]);
1909        (bumped, exp + (body.len() as i32 - 1))
1910    } else {
1911        (mantissa_str, exp)
1912    };
1913    let sign = if exp_final < 0 { '-' } else { '+' };
1914    let mant_out = if alt && !mant_final.contains('.') {
1915        format!("{}.", mant_final)
1916    } else { mant_final };
1917    format!("{}e{}{:02}", mant_out, sign, exp_final.abs()).into_bytes()
1918}
1919
1920fn format_g(n: f64, prec: usize, alt: bool) -> Vec<u8> {
1921    if n == 0.0 {
1922        return if alt { format!("0.{}", "0".repeat(prec.saturating_sub(1))).into_bytes() } else { b"0".to_vec() };
1923    }
1924    let abs = n.abs();
1925    let exp = abs.log10().floor() as i32;
1926    if exp < -4 || exp >= prec as i32 {
1927        let ep = if prec == 0 { 0 } else { prec - 1 };
1928        let mut v = format_exp(n, ep, false, alt);
1929        if !alt {
1930            v = strip_trailing_zeros_exp(&v);
1931        }
1932        v
1933    } else {
1934        let dec_places = (prec as i32 - 1 - exp).max(0) as usize;
1935        let mut v = format!("{:.*}", dec_places, n).into_bytes();
1936        if !alt {
1937            v = strip_trailing_zeros_fixed(&v);
1938        }
1939        v
1940    }
1941}
1942
1943fn strip_trailing_zeros_fixed(s: &[u8]) -> Vec<u8> {
1944    if !s.contains(&b'.') { return s.to_vec(); }
1945    let mut end = s.len();
1946    while end > 0 && s[end-1] == b'0' { end -= 1; }
1947    if end > 0 && s[end-1] == b'.' { end -= 1; }
1948    s[..end].to_vec()
1949}
1950
1951fn strip_trailing_zeros_exp(s: &[u8]) -> Vec<u8> {
1952    let e_pos = match s.iter().position(|&b| b == b'e' || b == b'E') {
1953        Some(p) => p,
1954        None => return s.to_vec(),
1955    };
1956    let mantissa = &s[..e_pos];
1957    let exp_part = &s[e_pos..];
1958    if !mantissa.contains(&b'.') {
1959        let mut out = mantissa.to_vec();
1960        out.extend_from_slice(exp_part);
1961        return out;
1962    }
1963    let mut end = mantissa.len();
1964    while end > 0 && mantissa[end-1] == b'0' { end -= 1; }
1965    if end > 0 && mantissa[end-1] == b'.' { end -= 1; }
1966    let mut out = mantissa[..end].to_vec();
1967    out.extend_from_slice(exp_part);
1968    out
1969}
1970
1971/// `string.format(fmt, ...)` — C-style string formatting.
1972///
1973pub fn str_format(state: &mut LuaState) -> Result<usize, LuaError> {
1974    let top = state.get_top();
1975    let mut arg = 1i32;
1976    let fmt_bytes = state.check_arg_string(1)?.to_vec();
1977    let mut buf: Vec<u8> = Vec::new();
1978    let mut i = 0usize;
1979
1980    while i < fmt_bytes.len() {
1981        let c = fmt_bytes[i];
1982        if c != L_ESC {
1983            buf.push(c);
1984            i += 1;
1985            continue;
1986        }
1987        i += 1;
1988        if i >= fmt_bytes.len() {
1989            break;
1990        }
1991        if fmt_bytes[i] == L_ESC {
1992            buf.push(L_ESC);
1993            i += 1;
1994            continue;
1995        }
1996
1997        // Parse a format specifier
1998        arg += 1;
1999        if arg > top {
2000            return Err(LuaError::arg_error(arg, "no value"));
2001        }
2002
2003        // Collect flags, width, precision
2004        let spec_start = i - 1; // includes the initial '%'
2005        // Skip flags: -, +, #, 0, space
2006        while i < fmt_bytes.len() && b"-+#0 ".contains(&fmt_bytes[i]) {
2007            i += 1;
2008        }
2009        // Skip width digits
2010        if i < fmt_bytes.len() && fmt_bytes[i] != b'0' {
2011            while i < fmt_bytes.len() && fmt_bytes[i].is_ascii_digit() {
2012                i += 1;
2013            }
2014        }
2015        // Skip precision
2016        if i < fmt_bytes.len() && fmt_bytes[i] == b'.' {
2017            i += 1;
2018            while i < fmt_bytes.len() && fmt_bytes[i].is_ascii_digit() {
2019                i += 1;
2020            }
2021        }
2022
2023        if i >= fmt_bytes.len() {
2024            return Err(LuaError::runtime(format_args!("invalid conversion specification")));
2025        }
2026
2027        let conv = fmt_bytes[i];
2028        i += 1;
2029
2030        let spec_slice = &fmt_bytes[spec_start + 1..i - 1];
2031        let form = &fmt_bytes[spec_start..i];
2032
2033        // Must check before parse_fmt_spec to avoid overflow on huge widths.
2034        if spec_slice.len() + 1 >= 22 {
2035            return Err(LuaError::runtime(format_args!("invalid format (too long)")));
2036        }
2037
2038        let spec = parse_fmt_spec(spec_slice);
2039
2040        match conv {
2041            b'c' => {
2042                check_conv_spec(form, FMT_FLAGS_C, false)?;
2043                let n = state.check_arg_integer(arg)?;
2044                let body = vec![n as u8];
2045                pad_str(&mut buf, &body, &spec);
2046            }
2047            b'd' | b'i' => {
2048                check_conv_spec(form, FMT_FLAGS_I, true)?;
2049                let n = state.check_arg_integer(arg)?;
2050                let (sign, digits) = signed_int_parts(n, &spec);
2051                pad_int(&mut buf, &sign, &digits, &spec);
2052            }
2053            b'u' => {
2054                check_conv_spec(form, FMT_FLAGS_U, true)?;
2055                let n = state.check_arg_integer(arg)? as u64;
2056                let (prefix, digits) = unsigned_int_parts(n, 10, false, &spec);
2057                pad_int(&mut buf, &prefix, &digits, &spec);
2058            }
2059            b'o' => {
2060                check_conv_spec(form, FMT_FLAGS_X, true)?;
2061                let n = state.check_arg_integer(arg)? as u64;
2062                let (prefix, digits) = unsigned_int_parts(n, 8, false, &spec);
2063                pad_int(&mut buf, &prefix, &digits, &spec);
2064            }
2065            b'x' => {
2066                check_conv_spec(form, FMT_FLAGS_X, true)?;
2067                let n = state.check_arg_integer(arg)? as u64;
2068                let (prefix, digits) = unsigned_int_parts(n, 16, false, &spec);
2069                pad_int(&mut buf, &prefix, &digits, &spec);
2070            }
2071            b'X' => {
2072                check_conv_spec(form, FMT_FLAGS_X, true)?;
2073                let n = state.check_arg_integer(arg)? as u64;
2074                let (prefix, digits) = unsigned_int_parts(n, 16, true, &spec);
2075                pad_int(&mut buf, &prefix, &digits, &spec);
2076            }
2077            b'a' | b'A' => {
2078                check_conv_spec(form, FMT_FLAGS_F, true)?;
2079                let n = state.check_arg_number(arg)?;
2080                let body = format_hex_float(n, spec.precision);
2081                let body: Vec<u8> = if conv == b'A' {
2082                    body.into_iter().map(|b| b.to_ascii_uppercase()).collect()
2083                } else {
2084                    body
2085                };
2086                let (sign, digits): (Vec<u8>, Vec<u8>) =
2087                    if !body.is_empty() && (body[0] == b'-' || body[0] == b'+') {
2088                        (vec![body[0]], body[1..].to_vec())
2089                    } else if spec.plus_sign {
2090                        (b"+".to_vec(), body)
2091                    } else if spec.space_sign {
2092                        (b" ".to_vec(), body)
2093                    } else {
2094                        (Vec::new(), body)
2095                    };
2096                let no_prec_spec = FmtSpec {
2097                    left_align: spec.left_align,
2098                    plus_sign: spec.plus_sign,
2099                    space_sign: spec.space_sign,
2100                    alt_form: spec.alt_form,
2101                    zero_pad: spec.zero_pad,
2102                    width: spec.width,
2103                    precision: None,
2104                };
2105                pad_int(&mut buf, &sign, &digits, &no_prec_spec);
2106            }
2107            b'f' | b'e' | b'E' | b'g' | b'G' => {
2108                check_conv_spec(form, FMT_FLAGS_F, true)?;
2109                let n = state.check_arg_number(arg)?;
2110                let body = format_float(n, conv, &spec);
2111                let (sign, digits): (Vec<u8>, Vec<u8>) = if !body.is_empty() && (body[0] == b'-' || body[0] == b'+') {
2112                    (vec![body[0]], body[1..].to_vec())
2113                } else if n >= 0.0 && spec.plus_sign {
2114                    (b"+".to_vec(), body)
2115                } else if n >= 0.0 && spec.space_sign {
2116                    (b" ".to_vec(), body)
2117                } else {
2118                    (Vec::new(), body)
2119                };
2120                let no_prec_spec = FmtSpec {
2121                    left_align: spec.left_align,
2122                    plus_sign: spec.plus_sign,
2123                    space_sign: spec.space_sign,
2124                    alt_form: spec.alt_form,
2125                    zero_pad: spec.zero_pad,
2126                    width: spec.width,
2127                    precision: None,
2128                };
2129                pad_int(&mut buf, &sign, &digits, &no_prec_spec);
2130            }
2131            b'p' => {
2132                check_conv_spec(form, FMT_FLAGS_C, false)?;
2133                let s: Vec<u8> = match lua_vm::api::to_pointer(state, arg) {
2134                    Some(p) => format!("0x{:x}", p).into_bytes(),
2135                    None => b"(null)".to_vec(),
2136                };
2137                pad_str(&mut buf, &s, &FmtSpec { precision: None, ..spec });
2138            }
2139            b'q' => {
2140                if form.len() > 2 {
2141                    return Err(LuaError::runtime(format_args!(
2142                        "specifier '%q' cannot have modifiers"
2143                    )));
2144                }
2145                addliteral(state, &mut buf, arg)?;
2146            }
2147            b's' => {
2148                check_conv_spec(form, FMT_FLAGS_C, true)?;
2149                let s = state.to_display_string(arg)?;
2150                let has_modifiers = spec.width != 0 || spec.precision.is_some();
2151                if has_modifiers && s.contains(&0u8) {
2152                    return Err(LuaError::arg_error(
2153                        arg,
2154                        "string contains zeros",
2155                    ));
2156                }
2157                pad_str(&mut buf, &s, &spec);
2158                state.pop_n(1);
2159            }
2160            _ => {
2161                return Err(LuaError::runtime(format_args!(
2162                    "invalid conversion '%{}' to 'format'", conv as char
2163                )));
2164            }
2165        }
2166    }
2167
2168    state.push_bytes(&buf)?;
2169    Ok(1)
2170}
2171
2172// ────────────────────────────────────────────────────────────────────────────
2173// §8  Pack / unpack
2174// ────────────────────────────────────────────────────────────────────────────
2175
2176/// Return `true` if `c` is an ASCII digit.
2177fn is_digit(c: u8) -> bool {
2178    c.is_ascii_digit()
2179}
2180
2181/// Read an optional integer from the format string, returning `df` if absent.
2182///
2183fn getnum(fmt: &[u8], pos: &mut usize, df: i32) -> i32 {
2184    if *pos >= fmt.len() || !is_digit(fmt[*pos]) {
2185        return df;
2186    }
2187    let mut a = 0i32;
2188    while *pos < fmt.len() && is_digit(fmt[*pos]) {
2189        a = a * 10 + (fmt[*pos] - b'0') as i32;
2190        *pos += 1;
2191        if a > (i32::MAX - 9) / 10 {
2192            break;
2193        }
2194    }
2195    a
2196}
2197
2198/// Read an integer from the format string, error if out of `[1, MAXINTSIZE]`.
2199///
2200fn getnumlimit(fmt: &[u8], pos: &mut usize, df: i32) -> Result<usize, LuaError> {
2201    let sz = getnum(fmt, pos, df);
2202    if sz > MAX_INT_SIZE as i32 || sz <= 0 {
2203        return Err(LuaError::runtime(format_args!(
2204            "integral size ({}) out of limits [1,{}]",
2205            sz, MAX_INT_SIZE
2206        )));
2207    }
2208    Ok(sz as usize)
2209}
2210
2211/// Read and classify the next pack format option, filling `size`.
2212///
2213fn getoption(h: &mut Header, fmt: &[u8], pos: &mut usize, size: &mut usize) -> Result<KOption, LuaError> {
2214    // In Rust, the native max-align of a union of f64/void*/size_t is 8 on 64-bit.
2215    const NATIVE_MAX_ALIGN: usize = std::mem::align_of::<f64>();
2216
2217    if *pos >= fmt.len() {
2218        return Ok(KOption::Nop);
2219    }
2220    let opt = fmt[*pos];
2221    *pos += 1;
2222    *size = 0;
2223
2224    match opt {
2225        b'b' => { *size = 1; Ok(KOption::Int) }
2226        b'B' => { *size = 1; Ok(KOption::Uint) }
2227        b'h' => { *size = 2; Ok(KOption::Int) }
2228        b'H' => { *size = 2; Ok(KOption::Uint) }
2229        b'l' => { *size = 8; Ok(KOption::Int) }  // sizeof(long) on 64-bit
2230        b'L' => { *size = 8; Ok(KOption::Uint) }
2231        b'j' => { *size = SZINT; Ok(KOption::Int) }
2232        b'J' => { *size = SZINT; Ok(KOption::Uint) }
2233        b'T' => { *size = std::mem::size_of::<usize>(); Ok(KOption::Uint) }
2234        b'f' => { *size = 4; Ok(KOption::Float) }
2235        b'n' => { *size = 8; Ok(KOption::Number) }  // sizeof(lua_Number) = sizeof(f64) = 8
2236        b'd' => { *size = 8; Ok(KOption::Double) }  // sizeof(double) = 8
2237        b'i' => { *size = getnumlimit(fmt, pos, 4)?; Ok(KOption::Int) }
2238        b'I' => { *size = getnumlimit(fmt, pos, 4)?; Ok(KOption::Uint) }
2239        b's' => { *size = getnumlimit(fmt, pos, std::mem::size_of::<usize>()  as i32)?; Ok(KOption::Kstring) }
2240        b'c' => {
2241            let n = getnum(fmt, pos, -1);
2242            if n == -1 {
2243                return Err(LuaError::runtime(format_args!("missing size for format option 'c'")));
2244            }
2245            *size = n as usize;
2246            Ok(KOption::Char)
2247        }
2248        b'z' => Ok(KOption::Zstr),
2249        b'x' => { *size = 1; Ok(KOption::Padding) }
2250        b'X' => Ok(KOption::Paddalign),
2251        b' ' => Ok(KOption::Nop),
2252        b'<' => { h.is_little = true; Ok(KOption::Nop) }
2253        b'>' => { h.is_little = false; Ok(KOption::Nop) }
2254        b'=' => { h.is_little = cfg!(target_endian = "little"); Ok(KOption::Nop) }
2255        b'!' => {
2256            let n = getnum(fmt, pos, NATIVE_MAX_ALIGN as i32);
2257            h.max_align = getnumlimit(fmt, pos, n)?;
2258            Ok(KOption::Nop)
2259        }
2260        _ => Err(LuaError::runtime(format_args!("invalid format option '{}'", opt as char)))
2261    }
2262}
2263
2264/// Get full details about the next format option, including alignment padding.
2265///
2266fn getdetails(
2267    h: &mut Header,
2268    total_size: usize,
2269    fmt: &[u8],
2270    pos: &mut usize,
2271    psize: &mut usize,
2272    ntoalign: &mut usize,
2273) -> Result<KOption, LuaError> {
2274    let opt = getoption(h, fmt, pos, psize)?;
2275    let mut align = *psize;
2276
2277    if opt == KOption::Paddalign {
2278        if *pos >= fmt.len() {
2279            return Err(LuaError::arg_error(1, "invalid next option for option 'X'"));
2280        }
2281        let mut dummy_size = 0usize;
2282        let next_opt = getoption(h, fmt, pos, &mut dummy_size)?;
2283        align = dummy_size;
2284        if next_opt == KOption::Char || align == 0 {
2285            return Err(LuaError::arg_error(1, "invalid next option for option 'X'"));
2286        }
2287    }
2288
2289    if align <= 1 || opt == KOption::Char {
2290        *ntoalign = 0;
2291    } else {
2292        if align > h.max_align {
2293            align = h.max_align;
2294        }
2295        if (align & (align - 1)) != 0 {
2296            return Err(LuaError::arg_error(1, "format asks for alignment not power of 2"));
2297        }
2298        *ntoalign = (align - (total_size & (align - 1))) & (align - 1);
2299    }
2300    Ok(opt)
2301}
2302
2303/// Pack integer `n` with `size` bytes into `buf` with given endianness.
2304///
2305fn packint(buf: &mut Vec<u8>, mut n: u64, is_little: bool, size: usize, neg: bool) {
2306    let start = buf.len();
2307    buf.resize(start + size, 0);
2308    let slice = &mut buf[start..start + size];
2309    // Write LSB first (little-endian), then swap if big-endian
2310    for i in 0..size {
2311        slice[if is_little { i } else { size - 1 - i }] = (n & MC as u64) as u8;
2312        n >>= NB;
2313    }
2314    // Sign extension for negative numbers larger than lua_Integer
2315    if neg && size > SZINT {
2316        for i in SZINT..size {
2317            slice[if is_little { i } else { size - 1 - i }] = MC;
2318        }
2319    }
2320}
2321
2322/// Copy bytes with endianness correction.
2323///
2324fn copywithendian(dest: &mut [u8], src: &[u8], is_little: bool) {
2325    debug_assert_eq!(dest.len(), src.len());
2326    if is_little == cfg!(target_endian = "little") {
2327        dest.copy_from_slice(src);
2328    } else {
2329        for (d, s) in dest.iter_mut().zip(src.iter().rev()) {
2330            *d = *s;
2331        }
2332    }
2333}
2334
2335/// Unpack a (possibly signed) integer from `data[0..size]`.
2336///
2337fn unpackint(_state: &LuaState, data: &[u8], is_little: bool, size: usize, is_signed: bool) -> Result<i64, LuaError> {
2338    let limit = size.min(SZINT);
2339    let mut res: u64 = 0;
2340    for i in (0..limit).rev() {
2341        res <<= NB;
2342        let byte_idx = if is_little { i } else { size - 1 - i };
2343        res |= data[byte_idx] as u64;
2344    }
2345
2346    if size < SZINT {
2347        if is_signed {
2348            let mask: u64 = 1u64 << (size * NB as usize - 1);
2349            res = (res ^ mask).wrapping_sub(mask);
2350        }
2351    } else if size > SZINT {
2352        let mask = if !is_signed || (res as i64) >= 0 { 0u8 } else { MC };
2353        for i in limit..size {
2354            let byte_idx = if is_little { i } else { size - 1 - i };
2355            if data[byte_idx] != mask {
2356                return Err(LuaError::runtime(format_args!(
2357                    "{}-byte integer does not fit into Lua Integer", size
2358                )));
2359            }
2360        }
2361    }
2362    Ok(res as i64)
2363}
2364
2365/// `string.pack(fmt, ...)` — pack values into a binary string.
2366///
2367pub fn str_pack(state: &mut LuaState) -> Result<usize, LuaError> {
2368    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2369    let fmt = &fmt_bytes[..];
2370    let mut h = Header::new();
2371    let mut arg = 1i32;
2372    let mut total_size = 0usize;
2373    let mut buf: Vec<u8> = Vec::new();
2374    let mut pos = 0usize;
2375
2376    while pos < fmt.len() {
2377        let mut size = 0usize;
2378        let mut ntoalign = 0usize;
2379        let opt = getdetails(&mut h, total_size, fmt, &mut pos, &mut size, &mut ntoalign)?;
2380        total_size += ntoalign + size;
2381        for _ in 0..ntoalign {
2382            buf.push(PACK_PAD_BYTE);
2383        }
2384        arg += 1;
2385
2386        match opt {
2387            KOption::Int => {
2388                let n = state.check_arg_integer(arg)?;
2389                if size < SZINT {
2390                    let lim: i64 = 1i64 << (size * NB as usize - 1);
2391                    if !(-lim <= n && n < lim) {
2392                        return Err(LuaError::arg_error(arg, "integer overflow"));
2393                    }
2394                }
2395                packint(&mut buf, n as u64, h.is_little, size, n < 0);
2396            }
2397            KOption::Uint => {
2398                let n = state.check_arg_integer(arg)?;
2399                if size < SZINT {
2400                    let lim: u64 = 1u64 << (size * NB as usize);
2401                    if (n as u64) >= lim {
2402                        return Err(LuaError::arg_error(arg, "unsigned overflow"));
2403                    }
2404                }
2405                packint(&mut buf, n as u64, h.is_little, size, false);
2406            }
2407            KOption::Float => {
2408                let f = state.check_arg_number(arg)? as f32;
2409                let start = buf.len();
2410                buf.resize(start + 4, 0);
2411                copywithendian(&mut buf[start..start + 4], &f.to_bits().to_ne_bytes(), h.is_little);
2412            }
2413            KOption::Number => {
2414                let f = state.check_arg_number(arg)?;
2415                let start = buf.len();
2416                buf.resize(start + 8, 0);
2417                copywithendian(&mut buf[start..start + 8], &f.to_bits().to_ne_bytes(), h.is_little);
2418            }
2419            KOption::Double => {
2420                let f = state.check_arg_number(arg)? as f64;
2421                let start = buf.len();
2422                buf.resize(start + 8, 0);
2423                copywithendian(&mut buf[start..start + 8], &f.to_bits().to_ne_bytes(), h.is_little);
2424            }
2425            KOption::Char => {
2426                let s = state.check_arg_string(arg)?.to_vec();
2427                if s.len() > size {
2428                    return Err(LuaError::arg_error(arg, "string longer than given size"));
2429                }
2430                buf.extend_from_slice(&s);
2431                let pad = size - s.len();
2432                for _ in 0..pad {
2433                    buf.push(PACK_PAD_BYTE);
2434                }
2435            }
2436            KOption::Kstring => {
2437                let s = state.check_arg_string(arg)?.to_vec();
2438                let len = s.len();
2439                if size < SZINT && len >= (1usize << (size * 8)) {
2440                    return Err(LuaError::arg_error(arg, "string length does not fit in given size"));
2441                }
2442                packint(&mut buf, len as u64, h.is_little, size, false);
2443                buf.extend_from_slice(&s);
2444                total_size += len;
2445            }
2446            KOption::Zstr => {
2447                let s = state.check_arg_string(arg)?.to_vec();
2448                if s.contains(&0) {
2449                    return Err(LuaError::arg_error(arg, "string contains zeros"));
2450                }
2451                buf.extend_from_slice(&s);
2452                buf.push(0);
2453                total_size += s.len() + 1;
2454            }
2455            KOption::Padding => {
2456                buf.push(PACK_PAD_BYTE);
2457                arg -= 1; // undo increment
2458            }
2459            KOption::Paddalign | KOption::Nop => {
2460                arg -= 1; // undo increment
2461            }
2462        }
2463    }
2464
2465    state.push_bytes(&buf)?;
2466    Ok(1)
2467}
2468
2469/// `string.packsize(fmt)` — return the byte-size the format would produce.
2470///
2471pub fn str_packsize(state: &mut LuaState) -> Result<usize, LuaError> {
2472    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2473    let fmt = &fmt_bytes[..];
2474    let mut h = Header::new();
2475    let mut total_size = 0usize;
2476    let mut pos = 0usize;
2477
2478    while pos < fmt.len() {
2479        let mut size = 0usize;
2480        let mut ntoalign = 0usize;
2481        let opt = getdetails(&mut h, total_size, fmt, &mut pos, &mut size, &mut ntoalign)?;
2482        if opt == KOption::Kstring || opt == KOption::Zstr {
2483            return Err(LuaError::arg_error(1, "variable-length format"));
2484        }
2485        let space = ntoalign + size;
2486        if total_size > PACK_MAXSIZE - space {
2487            return Err(LuaError::arg_error(1, "format result too large"));
2488        }
2489        total_size += space;
2490    }
2491    state.push(LuaValue::Int(total_size as i64));
2492    Ok(1)
2493}
2494
2495/// `string.unpack(fmt, s [, pos])` — unpack binary data from string.
2496///
2497pub fn str_unpack(state: &mut LuaState) -> Result<usize, LuaError> {
2498    let fmt_bytes = state.check_arg_string(1)?.to_vec();
2499    let data_bytes = state.check_arg_string(2)?.to_vec();
2500    let ld = data_bytes.len();
2501    let pos_raw = state.opt_arg_integer(3, 1)?;
2502    let mut pos = pos_relat_i(pos_raw, ld).saturating_sub(1);
2503
2504    if pos > ld {
2505        return Err(LuaError::arg_error(3, "initial position out of string"));
2506    }
2507
2508    let fmt = &fmt_bytes[..];
2509    let data = &data_bytes[..];
2510    let mut h = Header::new();
2511    let mut fmt_pos = 0usize;
2512    let mut n = 0usize;
2513
2514    while fmt_pos < fmt.len() {
2515        let mut size = 0usize;
2516        let mut ntoalign = 0usize;
2517        let opt = getdetails(&mut h, pos, fmt, &mut fmt_pos, &mut size, &mut ntoalign)?;
2518
2519        if ntoalign + size > ld - pos {
2520            return Err(LuaError::arg_error(2, "data string too short"));
2521        }
2522        pos += ntoalign;
2523        state.ensure_stack(2, "too many results")?;
2524        n += 1;
2525
2526        match opt {
2527            KOption::Int => {
2528                let v = unpackint(state, &data[pos..pos + size], h.is_little, size, true)?;
2529                state.push(LuaValue::Int(v));
2530            }
2531            KOption::Uint => {
2532                let v = unpackint(state, &data[pos..pos + size], h.is_little, size, false)?;
2533                state.push(LuaValue::Int(v));
2534            }
2535            KOption::Float => {
2536                let mut bytes = [0u8; 4];
2537                copywithendian(&mut bytes, &data[pos..pos + 4], h.is_little);
2538                let f = f32::from_bits(u32::from_ne_bytes(bytes));
2539                state.push(LuaValue::Float(f as f64));
2540            }
2541            KOption::Number => {
2542                let mut bytes = [0u8; 8];
2543                copywithendian(&mut bytes, &data[pos..pos + 8], h.is_little);
2544                let f = f64::from_bits(u64::from_ne_bytes(bytes));
2545                state.push(LuaValue::Float(f));
2546            }
2547            KOption::Double => {
2548                let mut bytes = [0u8; 8];
2549                copywithendian(&mut bytes, &data[pos..pos + 8], h.is_little);
2550                let f = f64::from_bits(u64::from_ne_bytes(bytes));
2551                state.push(LuaValue::Float(f));
2552            }
2553            KOption::Char => {
2554                state.push_bytes(&data[pos..pos + size])?;
2555            }
2556            KOption::Kstring => {
2557                let len = unpackint(state, &data[pos..pos + size], h.is_little, size, false)? as usize;
2558                if len > ld - pos - size {
2559                    return Err(LuaError::arg_error(2, "data string too short"));
2560                }
2561                state.push_bytes(&data[pos + size..pos + size + len])?;
2562                pos += len;
2563            }
2564            KOption::Zstr => {
2565                let end = data[pos..].iter().position(|&b| b == 0)
2566                    .ok_or_else(|| LuaError::arg_error(2, "unfinished string for format 'z'"))?;
2567                if pos + end >= ld {
2568                    return Err(LuaError::arg_error(2, "unfinished string for format 'z'"));
2569                }
2570                state.push_bytes(&data[pos..pos + end])?;
2571                pos += end + 1;
2572            }
2573            KOption::Paddalign | KOption::Padding | KOption::Nop => {
2574                n -= 1; // undo increment
2575            }
2576        }
2577        pos += size;
2578    }
2579
2580    state.push(LuaValue::Int((pos + 1) as i64));
2581    Ok(n + 1)
2582}
2583
2584// ────────────────────────────────────────────────────────────────────────────
2585// §9  Module registration
2586// ────────────────────────────────────────────────────────────────────────────
2587
2588/// Function table for `string` library.
2589///
2590pub const STRING_LIB: &[(&[u8], lua_CFunction)] = &[
2591    (b"byte",     str_byte),
2592    (b"char",     str_char),
2593    (b"dump",     str_dump),
2594    (b"find",     str_find),
2595    (b"format",   str_format),
2596    (b"gmatch",   gmatch),
2597    (b"gsub",     str_gsub),
2598    (b"len",      str_len),
2599    (b"lower",    str_lower),
2600    (b"match",    str_match),
2601    (b"rep",      str_rep),
2602    (b"reverse",  str_reverse),
2603    (b"sub",      str_sub),
2604    (b"upper",    str_upper),
2605    (b"pack",     str_pack),
2606    (b"packsize", str_packsize),
2607    (b"unpack",   str_unpack),
2608];
2609
2610/// Metamethods to install on the string metatable.
2611///
2612pub const STRING_META_METHODS: &[(&[u8], lua_CFunction)] = &[
2613    (b"__add",  arith_add),
2614    (b"__sub",  arith_sub),
2615    (b"__mul",  arith_mul),
2616    (b"__mod",  arith_mod),
2617    (b"__pow",  arith_pow),
2618    (b"__div",  arith_div),
2619    (b"__idiv", arith_idiv),
2620    (b"__unm",  arith_unm),
2621];
2622
2623/// Create the string metatable and set it as the metatable for all strings.
2624///
2625pub fn createmetatable(state: &mut LuaState) -> Result<(), LuaError> {
2626    state.new_lib_table(STRING_META_METHODS)?;
2627    state.set_funcs(STRING_META_METHODS, 0)?;
2628    state.push_string(b"")?;
2629    let mt_idx = state.top_idx() - 2;
2630    let mt = state.get_at(mt_idx);
2631    state.push(mt);
2632    state.set_metatable(-2)?;
2633    state.pop_n(1);
2634    let strlib_idx = state.top_idx() - 2;
2635    let strlib = state.get_at(strlib_idx);
2636    state.push(strlib);
2637    state.set_field(-2, b"__index")?;
2638    state.pop_n(1);
2639    Ok(())
2640}
2641
2642/// `luaopen_string` — open the string library.
2643///
2644pub fn luaopen_string(state: &mut LuaState) -> Result<usize, LuaError> {
2645    state.new_lib(STRING_LIB)?;
2646    createmetatable(state)?;
2647    Ok(1)
2648}
2649
2650// ────────────────────────────────────────────────────────────────────────────
2651// PORT STATUS
2652//   source:        src/lstrlib.c  (1875 lines, 46 functions)
2653//   target_crate:  lua-stdlib
2654//   confidence:    medium
2655//   todos:         13
2656//   port_notes:    6
2657//   unsafe_blocks: 0
2658//   notes:         Pattern engine uses index-based MatchState (not raw ptrs).
2659//                  string.format delegates numeric widths/precision/flags to
2660//                  Phase B (a sprintf-compatible crate or manual impl).
2661//                  gmatch iterator state holds a 4-element Lua table in the
2662//                  closure's single upvalue (src, pat, pos, lastmatch) instead
2663//                  of the C-Lua GMatchState userdata, because Phase-A
2664//                  LuaCClosure upvalues are immutable. See gmatch_aux.
2665//                  copywithendian uses safe byte-level swapping (no transmute).
2666//                  unpackint sign-extension uses two's-complement bit tricks;
2667//                  logic review needed in Phase B.
2668//                  str_dump requires state.dump_function() which is not yet
2669//                  defined; Phase B wires up the ldump.c port.
2670//                  addquoted uses 3-digit escape for all control chars (slight
2671//                  deviation from C which uses 1-digit when safe); benign.
2672//                  str_len/str_sub/str_byte/str_reverse/str_lower/str_upper/
2673//                  str_rep/gmatch/str_find_aux borrow source bytes through
2674//                  to_lua_string (GcRef) instead of copying via
2675//                  check_arg_string, mirroring the gmatch_aux fix (685482d).
2676//                  string_ops 3.00x→2.00x, string_ops_long 2.25x→1.48x on
2677//                  best-of-5 (Apple M3 Max).
2678//                  gmatch_aux reads / writes its 4-slot state table directly
2679//                  through LuaTableRefExt::{get_int, raw_set_int} after a
2680//                  single value_at(upvalue_index(1)) resolution, replacing
2681//                  six raw_geti / raw_seti + four to_lua_string / to_integer_x
2682//                  calls that each re-resolved the stack index via
2683//                  index_to_value. Drops string_ops_long 1.58x→1.38x
2684//                  (below the 1.5x parity threshold) and index_to_value share
2685//                  9.4%→2.0% on Apple M3 Max best-of-5.
2686// ────────────────────────────────────────────────────────────────────────────