Skip to main content

lua_stdlib/
string_lib.rs

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