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