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