Skip to main content

lua_stdlib/
loadlib.rs

1//! The Lua `package` library: `require`, `package.loadlib`,
2//! `package.searchpath`, and the four built-in module searchers (preload,
3//! Lua-file, C-library, C-root).
4//!
5//! ## Graduation (Idiomatization Sprint 2 / Phase 2 — cold, platform-FFI module)
6//!
7//! Split cleanly into two regimes, and treated as such:
8//!
9//! * **Deterministic pure-Lua package logic** — now guarded by
10//!   `tests/loadlib_strengthen.rs` (16 reference-pinned cross-version
11//!   assertions). Strengthening that net FIRST caught **seven** divergences our
12//!   weaker net hid: the 5.1 `package.config` trailing newline, `require`'s 5.4+
13//!   2nd return value, the 5.1 preload-loader arg count, a C-root searcher
14//!   message truncation, the `nil`-vs-`false` `luaL_pushfail` value, the 5.1
15//!   absence of `package.searchpath`, and the 5.2/5.3 searchpath-error leading
16//!   separator. All were fixed via single-source version helpers; the version
17//!   gates are explicit and load-bearing. See `GRADUATED.md` "loadlib".
18//! * **Platform / dynamic-loading FFI** — left LOAD-BEARING and untouched. The
19//!   three platform calls (`lsys_load`, `lsys_sym`, `lsys_unloadlib`) dispatch
20//!   through embedder hooks on [`lua_vm::state::GlobalState`]
21//!   (`dynlib_load_hook`, `dynlib_symbol_hook`, `dynlib_unload_hook`); `lua-cli`
22//!   installs a `libloading`-backed (genuinely `unsafe`) implementation, while
23//!   embeddings that omit the hooks behave like C-Lua's fallback stub
24//!   (`LIB_FAIL = "absent"`). This indirection keeps `lua-stdlib` itself
25//!   `unsafe`-free (`unsafe_code = "forbid"`); the real FFI bridge lives in
26//!   `lua-cli`. Its behavior — the dlopen/dlsym path, the platform error
27//!   strings, the `"open"`/`"absent"`/`"init"` tags — needs a real shared
28//!   object and host loader, so it is NOT reference-pinnable and is a documented
29//!   honest-negative (the analogue of math's platform `rand()`).
30
31use crate::state_stub::{lua_CFunction, LuaState, LuaStateStubExt as _};
32use lua_types::{LuaError, LuaType, LuaValue};
33use lua_vm::state::{DynLibId, DynamicSymbol};
34
35// ── Module-level constants ────────────────────────────────────────────────────
36
37const LUA_POF: &[u8] = b"luaopen_";
38
39const LUA_OFSEP: &[u8] = b"_";
40
41const CLIBS: &[u8] = b"_CLIBS";
42
43// `lsys_load` chooses the tag at runtime: `"open"` when a load hook is
44// installed (matching POSIX/Windows behaviour) and `"absent"` when no hook
45// is registered (matching the fallback stub). The constant below carries the
46// fallback-stub spelling; the load-hook path uses `b"open"` directly.
47const LIB_FAIL_ABSENT: &[u8] = b"absent";
48
49const LUA_PATH_SEP: u8 = b';';
50
51const LUA_PATH_MARK: u8 = b'?';
52
53const LUA_IGMARK: u8 = b'-';
54
55#[cfg(target_os = "windows")]
56const LUA_DIRSEP: u8 = b'\\';
57#[cfg(not(target_os = "windows"))]
58const LUA_DIRSEP: u8 = b'/';
59
60// Both default to LUA_DIRSEP on all platforms.
61const LUA_CSUBSEP: u8 = LUA_DIRSEP;
62const LUA_LSUBSEP: u8 = LUA_DIRSEP;
63
64// The fail-tag spelling travels with `LookForFuncStatus` (below) rather than a
65// single compile-time `LIB_FAIL` constant, so each failure carries its own tag.
66
67// Pushed when no `dynlib_load_hook`/`dynlib_symbol_hook` is registered on
68// `GlobalState`. With a backend installed the CLI supplies its own error
69// strings via the hook's `Err` return for "open" failures.
70const DLMSG: &[u8] = b"dynamic libraries not enabled; check your Lua installation";
71
72// Message returned via `(false, msg, "init")` when a hook resolves a symbol
73// against stock Lua 5.4's `lua_State *` C ABI. That ABI is not callable
74// against this build's `LuaState`; supporting it is a separate compatibility
75// project (see docs/LUA_PHASE_E_RUNTIME_SPEC.md Part 3).
76const C_ABI_UNSUPPORTED_MSG: &[u8] =
77    b"dynamic library loaded, but Lua C ABI modules are not supported by this build";
78
79const LUA_PATH_VAR: &[u8] = b"LUA_PATH";
80const LUA_CPATH_VAR: &[u8] = b"LUA_CPATH";
81
82/// Build the `package.config` string for `version`.
83///
84/// Five lines encoding the platform separators: directory separator, path
85/// separator, the `?` substitution mark, the `!` exec-dir mark, and the `-`
86/// ignore mark. The trailing newline after the ignore mark is a **5.2 addition**
87/// (`LUA_IGMARK "\n"` in 5.2+ `loadlib.c`); 5.1's string ends at `-`, so 5.1 is
88/// 9 bytes and 5.2+ are 10 (pinned in `tests/loadlib_strengthen.rs`).
89fn package_config(version: lua_types::LuaVersion) -> Vec<u8> {
90    let mut config = vec![
91        LUA_DIRSEP,
92        b'\n',
93        LUA_PATH_SEP,
94        b'\n',
95        LUA_PATH_MARK,
96        b'\n',
97        b'!',
98        b'\n',
99        LUA_IGMARK,
100    ];
101    if !matches!(version, lua_types::LuaVersion::V51) {
102        config.push(b'\n');
103    }
104    config
105}
106
107// ── Version-derived package-path defaults (issue #273) ───────────────────────
108//
109// `GlobalState::lua_version` is the single source of truth for every byte
110// below; nothing here is a per-lookup computation — `luaopen_package` (the
111// library-init cold path, run once per `Lua` instance) is the only caller.
112//
113// Every default was captured directly from the unmodified upstream `make
114// macosx` build of each version (`specs/oracle/CONTRACT.md`,
115// `/tmp/lua-refs/bin/lua5.x`), not read off of `luaconf.h` — see
116// `tests/loadlib_strengthen.rs` for the pinned assertions and this crate's
117// PR for the per-version diff transcript.
118
119/// The version-directory segment baked into default package paths and into
120/// versioned environment-variable names (e.g. `5.4` in
121/// `/usr/local/share/lua/5.4/?.lua` and in `LUA_PATH_5_4`). Mirrors upstream's
122/// `LUA_VDIR` (`luaconf.h`).
123///
124/// The trailing wildcard arm is unreachable in practice: every public
125/// constructor (`Lua::with_hooks_versioned` and friends) refuses to build a
126/// `LuaVersion` for which [`lua_types::LuaVersion::is_supported`] is false,
127/// and today's five supported variants are all matched above it. It exists
128/// only because `LuaVersion` is `#[non_exhaustive]` from this crate's point
129/// of view.
130fn lua_vdir(version: lua_types::LuaVersion) -> &'static [u8] {
131    match version {
132        lua_types::LuaVersion::V51 => b"5.1",
133        lua_types::LuaVersion::V52 => b"5.2",
134        lua_types::LuaVersion::V53 => b"5.3",
135        lua_types::LuaVersion::V54 => b"5.4",
136        lua_types::LuaVersion::V55 => b"5.5",
137        _ => b"5.4",
138    }
139}
140
141/// The `LUA_VERSUFFIX` value for `version` (e.g. `_5_4`), appended to
142/// `LUA_PATH`/`LUA_CPATH` to build the versioned environment-variable name an
143/// instance consults first (`LUA_PATH_5_4`, `LUA_CPATH_5_3`, ...). See
144/// [`lua_vdir`] for the wildcard-arm note.
145fn lua_versuffix(version: lua_types::LuaVersion) -> &'static [u8] {
146    match version {
147        lua_types::LuaVersion::V51 => b"_5_1",
148        lua_types::LuaVersion::V52 => b"_5_2",
149        lua_types::LuaVersion::V53 => b"_5_3",
150        lua_types::LuaVersion::V54 => b"_5_4",
151        lua_types::LuaVersion::V55 => b"_5_5",
152        _ => b"_5_4",
153    }
154}
155
156/// Whether `version` consults a versioned environment variable
157/// (`LUA_PATH_5_x`/`LUA_CPATH_5_x`) at all before falling back to the
158/// unversioned `LUA_PATH`/`LUA_CPATH`. Versioned env vars are a **5.2+**
159/// addition; 5.1 only ever reads the unversioned name — verified against
160/// `lua5.1.5`, where setting `LUA_PATH_5_1` has no effect on `package.path`.
161fn has_versioned_env_vars(version: lua_types::LuaVersion) -> bool {
162    !matches!(version, lua_types::LuaVersion::V51)
163}
164
165/// `LUA_LDIR` for `version`: where installed pure-Lua modules live
166/// (`/usr/local/share/lua/<vdir>/`).
167fn lua_ldir(version: lua_types::LuaVersion) -> Vec<u8> {
168    let mut dir = b"/usr/local/share/lua/".to_vec();
169    dir.extend_from_slice(lua_vdir(version));
170    dir.push(b'/');
171    dir
172}
173
174/// `LUA_CDIR` for `version`: where installed C modules live
175/// (`/usr/local/lib/lua/<vdir>/`).
176fn lua_cdir(version: lua_types::LuaVersion) -> Vec<u8> {
177    let mut dir = b"/usr/local/lib/lua/".to_vec();
178    dir.extend_from_slice(lua_vdir(version));
179    dir.push(b'/');
180    dir
181}
182
183/// The compiled-in non-Windows `package.path` default for `version`. The
184/// entry SHAPE, not just the version segment, differs by era:
185/// - **5.1**: `./?.lua` FIRST, then `LDIR`/`CDIR`, no trailing `./?/init.lua`.
186/// - **5.2**: `LDIR`/`CDIR` first, `./?.lua` LAST, no `./?/init.lua` at all.
187/// - **5.3/5.4/5.5**: `LDIR`/`CDIR` first, then BOTH `./?.lua` and
188///   `./?/init.lua` last (the `LUA_SHRDIR`-derived shape 5.3 introduced).
189///
190/// Compiled (and unit-tested) on every platform; [`lua_path_default`] selects
191/// it on non-Windows targets.
192fn unix_path_default(version: lua_types::LuaVersion) -> Vec<u8> {
193    let ldir = lua_ldir(version);
194    let cdir = lua_cdir(version);
195    let mut path = Vec::new();
196    match version {
197        lua_types::LuaVersion::V51 => {
198            path.extend_from_slice(b"./?.lua;");
199            path.extend_from_slice(&ldir);
200            path.extend_from_slice(b"?.lua;");
201            path.extend_from_slice(&ldir);
202            path.extend_from_slice(b"?/init.lua;");
203            path.extend_from_slice(&cdir);
204            path.extend_from_slice(b"?.lua;");
205            path.extend_from_slice(&cdir);
206            path.extend_from_slice(b"?/init.lua");
207        }
208        lua_types::LuaVersion::V52 => {
209            path.extend_from_slice(&ldir);
210            path.extend_from_slice(b"?.lua;");
211            path.extend_from_slice(&ldir);
212            path.extend_from_slice(b"?/init.lua;");
213            path.extend_from_slice(&cdir);
214            path.extend_from_slice(b"?.lua;");
215            path.extend_from_slice(&cdir);
216            path.extend_from_slice(b"?/init.lua;./?.lua");
217        }
218        _ => {
219            path.extend_from_slice(&ldir);
220            path.extend_from_slice(b"?.lua;");
221            path.extend_from_slice(&ldir);
222            path.extend_from_slice(b"?/init.lua;");
223            path.extend_from_slice(&cdir);
224            path.extend_from_slice(b"?.lua;");
225            path.extend_from_slice(&cdir);
226            path.extend_from_slice(b"?/init.lua;./?.lua;./?/init.lua");
227        }
228    }
229    path
230}
231
232/// The compiled-in Windows `package.path` default for `version`, transcribed
233/// from each release's `luaconf.h` `_WIN32` branch (`LUA_LDIR` = `!\lua\`,
234/// `LUA_CDIR` = `!\`, and from 5.3 `LUA_SHRDIR` = `!\..\share\lua\<vdir>\`).
235/// `!` is `LUA_EXEC_DIR`, replaced by the running executable's directory in
236/// [`setprogdir`]. The era split mirrors [`unix_path_default`]:
237/// - **5.1**: `.\?.lua` FIRST, then `LDIR`/`CDIR` entries.
238/// - **5.2**: `LDIR`/`CDIR` first, `.\?.lua` LAST, no `.\?\init.lua`.
239/// - **5.3/5.4/5.5**: `LDIR`/`CDIR`, then `SHRDIR`, then both `.\` entries.
240///
241/// Compiled (and unit-tested) on every platform; [`lua_path_default`] selects
242/// it on Windows targets.
243fn windows_path_default(version: lua_types::LuaVersion) -> Vec<u8> {
244    match version {
245        lua_types::LuaVersion::V51 => {
246            b".\\?.lua;!\\lua\\?.lua;!\\lua\\?\\init.lua;!\\?.lua;!\\?\\init.lua".to_vec()
247        }
248        lua_types::LuaVersion::V52 => {
249            b"!\\lua\\?.lua;!\\lua\\?\\init.lua;!\\?.lua;!\\?\\init.lua;.\\?.lua".to_vec()
250        }
251        _ => {
252            let vdir = lua_vdir(version);
253            let mut path = Vec::new();
254            path.extend_from_slice(b"!\\lua\\?.lua;!\\lua\\?\\init.lua;");
255            path.extend_from_slice(b"!\\?.lua;!\\?\\init.lua;");
256            path.extend_from_slice(b"!\\..\\share\\lua\\");
257            path.extend_from_slice(vdir);
258            path.extend_from_slice(b"\\?.lua;");
259            path.extend_from_slice(b"!\\..\\share\\lua\\");
260            path.extend_from_slice(vdir);
261            path.extend_from_slice(b"\\?\\init.lua;");
262            path.extend_from_slice(b".\\?.lua;.\\?\\init.lua");
263            path
264        }
265    }
266}
267
268/// The compiled-in `package.path` default for `version` on THIS platform.
269fn lua_path_default(version: lua_types::LuaVersion) -> Vec<u8> {
270    if cfg!(target_os = "windows") {
271        windows_path_default(version)
272    } else {
273        unix_path_default(version)
274    }
275}
276
277/// The compiled-in non-Windows `package.cpath` default for `version` — same
278/// era split as [`unix_path_default`]: 5.1 puts `./?.so` FIRST with no `./`
279/// alternative after `CDIR`'s entries; 5.2+ share one shape (`CDIR` entries,
280/// then `./?.so` last).
281fn unix_cpath_default(version: lua_types::LuaVersion) -> Vec<u8> {
282    let cdir = lua_cdir(version);
283    let mut path = Vec::new();
284    match version {
285        lua_types::LuaVersion::V51 => {
286            path.extend_from_slice(b"./?.so;");
287            path.extend_from_slice(&cdir);
288            path.extend_from_slice(b"?.so;");
289            path.extend_from_slice(&cdir);
290            path.extend_from_slice(b"loadall.so");
291        }
292        _ => {
293            path.extend_from_slice(&cdir);
294            path.extend_from_slice(b"?.so;");
295            path.extend_from_slice(&cdir);
296            path.extend_from_slice(b"loadall.so;./?.so");
297        }
298    }
299    path
300}
301
302/// The compiled-in Windows `package.cpath` default for `version`, transcribed
303/// from each release's `luaconf.h` `_WIN32` branch — see
304/// [`windows_path_default`] for the `!` (`LUA_EXEC_DIR`) convention:
305/// - **5.1**: `.\?.dll` FIRST, then `!\?.dll;!\loadall.dll`.
306/// - **5.2**: `!\?.dll;!\loadall.dll`, then `.\?.dll` LAST.
307/// - **5.3/5.4/5.5**: adds `!\..\lib\lua\<vdir>\?.dll` after `!\?.dll`.
308fn windows_cpath_default(version: lua_types::LuaVersion) -> Vec<u8> {
309    match version {
310        lua_types::LuaVersion::V51 => b".\\?.dll;!\\?.dll;!\\loadall.dll".to_vec(),
311        lua_types::LuaVersion::V52 => b"!\\?.dll;!\\loadall.dll;.\\?.dll".to_vec(),
312        _ => {
313            let vdir = lua_vdir(version);
314            let mut path = Vec::new();
315            path.extend_from_slice(b"!\\?.dll;!\\..\\lib\\lua\\");
316            path.extend_from_slice(vdir);
317            path.extend_from_slice(b"\\?.dll;");
318            path.extend_from_slice(b"!\\loadall.dll;.\\?.dll");
319            path
320        }
321    }
322}
323
324/// The compiled-in `package.cpath` default for `version` on THIS platform.
325fn lua_cpath_default(version: lua_types::LuaVersion) -> Vec<u8> {
326    if cfg!(target_os = "windows") {
327        windows_cpath_default(version)
328    } else {
329        unix_cpath_default(version)
330    }
331}
332
333fn getenv_bytes(state: &LuaState, name: &[u8]) -> Option<Vec<u8>> {
334    if let Some(env_fn) = state.global().env_hook {
335        return env_fn(name);
336    }
337
338    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
339    {
340        None
341    }
342
343    #[cfg(all(unix, not(all(target_arch = "wasm32", target_os = "unknown"))))]
344    {
345        use std::ffi::OsStr;
346        use std::os::unix::ffi::{OsStrExt, OsStringExt};
347
348        let os_name = OsStr::from_bytes(name);
349        std::env::var_os(os_name).map(|v| v.into_vec())
350    }
351
352    #[cfg(all(not(unix), not(all(target_arch = "wasm32", target_os = "unknown"))))]
353    {
354        std::str::from_utf8(name)
355            .ok()
356            .and_then(|name_str| std::env::var(name_str).ok())
357            .map(|s| s.into_bytes())
358    }
359}
360
361// ── Opaque library handle ─────────────────────────────────────────────────────
362//
363//
364// In this port, the library identity is the opaque `DynLibId(u64)` allocated
365// by the embedder-installed [`DynLibLoadHook`]. `lua-stdlib` never inspects
366// the value; it stashes the raw `u64` in `_CLIBS` as light userdata (cast
367// through `*mut c_void` to match C-Lua's representation) and hands it back to
368// the symbol and unload hooks.
369
370// ── Byte-string utilities ─────────────────────────────────────────────────────
371
372/// Append to `buf` the bytes of `s` with all non-overlapping occurrences of
373/// `pattern` replaced by `replacement`.
374///
375fn gsub_append(buf: &mut Vec<u8>, s: &[u8], pattern: &[u8], replacement: &[u8]) {
376    if pattern.is_empty() {
377        buf.extend_from_slice(s);
378        return;
379    }
380    let mut pos = 0;
381    while pos < s.len() {
382        if s[pos..].starts_with(pattern) {
383            buf.extend_from_slice(replacement);
384            pos += pattern.len();
385        } else {
386            buf.push(s[pos]);
387            pos += 1;
388        }
389    }
390}
391
392/// Return a new `Vec<u8>` with all non-overlapping occurrences of `pattern`
393/// in `s` replaced by `replacement`.
394fn gsub_bytes(s: &[u8], pattern: &[u8], replacement: &[u8]) -> Vec<u8> {
395    let mut out = Vec::new();
396    gsub_append(&mut out, s, pattern, replacement);
397    out
398}
399
400/// Find the byte offset of `needle` in `haystack`, or `None`.
401fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
402    if needle.is_empty() {
403        return Some(0);
404    }
405    haystack.windows(needle.len()).position(|w| w == needle)
406}
407
408// ── Platform-specific dynamic-loading dispatch ────────────────────────────────
409
410/// Unload a previously loaded C library.
411///
412///    — POSIX: `dlclose(lib)`; Windows: `FreeLibrary(lib)`.
413///
414/// Delegates to [`GlobalState::dynlib_unload_hook`]. When no hook is
415/// registered the library is leaked, which matches `libloading`'s safety
416/// model (the library must outlive every symbol it exports, and the simplest
417/// correct policy is to keep it alive for the state's lifetime).
418fn lsys_unloadlib(state: &mut LuaState, lib: DynLibId) {
419    if let Some(hook) = state.global().dynlib_unload_hook {
420        hook(lib);
421    }
422}
423
424/// Load a C library from `path`. If `see_glb` is true, make symbols globally
425/// visible (POSIX RTLD_GLOBAL). On failure, pushes an error string onto `state`.
426///
427///    — POSIX: `dlopen(path, RTLD_NOW | (seeglb ? RTLD_GLOBAL : RTLD_LOCAL))`
428///    — Windows: `LoadLibraryExA(path, NULL, LUA_LLE_FLAGS)`
429///
430/// Returns `(handle, lib_fail_tag)`. The tag is `"absent"` when no hook is
431/// registered (matching C's fallback-stub `LIB_FAIL`) and `"open"` when the
432/// hook itself reports a failure (matching POSIX/Windows builds).
433fn lsys_load(
434    state: &mut LuaState,
435    path: &[u8],
436    see_glb: bool,
437) -> (Option<DynLibId>, &'static [u8]) {
438    let hook = state.global().dynlib_load_hook;
439    let Some(load_fn) = hook else {
440        let s = match state.intern_str(DLMSG) {
441            Ok(s) => s,
442            Err(_) => return (None, LIB_FAIL_ABSENT),
443        };
444        state.push(LuaValue::Str(s));
445        return (None, LIB_FAIL_ABSENT);
446    };
447    match load_fn(state, path, see_glb) {
448        Ok(id) => (Some(id), b"open"),
449        // `LuaError::File` is reserved for "no shared library at this path":
450        // map it to the fallback-stub `"absent"` tag so a probe like
451        // `package.loadlib("./nonexistent.so", ...)` reports `"absent"`
452        // regardless of whether a backend is installed. Every other `Err` is a
453        // true open-time failure → `"open"`.
454        Err(LuaError::File) => {
455            let mut msg = b"cannot find library '".to_vec();
456            msg.extend_from_slice(path);
457            msg.push(b'\'');
458            let s = match state.intern_str(&msg) {
459                Ok(s) => s,
460                Err(_) => return (None, LIB_FAIL_ABSENT),
461            };
462            state.push(LuaValue::Str(s));
463            (None, LIB_FAIL_ABSENT)
464        }
465        Err(err) => {
466            let msg = error_to_bytes(&err);
467            let s = match state.intern_str(&msg) {
468                Ok(s) => s,
469                Err(_) => return (None, b"open"),
470            };
471            state.push(LuaValue::Str(s));
472            (None, b"open")
473        }
474    }
475}
476
477/// Find symbol `sym` in library `lib` and either push it as a callable Lua
478/// function (returning `SymOutcome::Found`) or push an error message string
479/// and report which failure category the caller should propagate.
480///
481///    — POSIX: `cast_func(dlsym(lib, sym))`
482///    — Windows: `(lua_CFunction)(voidf)GetProcAddress(lib, sym)`
483fn lsys_sym(state: &mut LuaState, lib: DynLibId, sym: &[u8]) -> SymOutcome {
484    let hook = state.global().dynlib_symbol_hook;
485    let Some(sym_fn) = hook else {
486        let s = match state.intern_str(DLMSG) {
487            Ok(s) => s,
488            Err(_) => return SymOutcome::Missing,
489        };
490        state.push(LuaValue::Str(s));
491        return SymOutcome::Missing;
492    };
493    match sym_fn(state, lib, sym) {
494        Ok(DynamicSymbol::RustNative(f)) => SymOutcome::Found(f),
495        Ok(DynamicSymbol::LuaCAbi(_)) => {
496            let s = match state.intern_str(C_ABI_UNSUPPORTED_MSG) {
497                Ok(s) => s,
498                Err(_) => return SymOutcome::Missing,
499            };
500            state.push(LuaValue::Str(s));
501            SymOutcome::Missing
502        }
503        Ok(DynamicSymbol::Unsupported { reason }) => {
504            let s = match state.intern_str(&reason) {
505                Ok(s) => s,
506                Err(_) => return SymOutcome::Missing,
507            };
508            state.push(LuaValue::Str(s));
509            SymOutcome::Missing
510        }
511        Err(err) => {
512            let msg = error_to_bytes(&err);
513            let s = match state.intern_str(&msg) {
514                Ok(s) => s,
515                Err(_) => return SymOutcome::Missing,
516            };
517            state.push(LuaValue::Str(s));
518            SymOutcome::Missing
519        }
520    }
521}
522
523/// Outcome of `lsys_sym`.
524///
525/// `Missing` covers every non-success path (unknown symbol, ABI mismatch, hook
526/// absent, embedder-supplied refusal); in every case an error-message string
527/// has already been pushed onto the Lua stack, so the caller maps `Missing`
528/// to `ERRFUNC` / `"init"` without further work.
529enum SymOutcome {
530    /// Resolved to a Rust-native callable.
531    Found(lua_CFunction),
532    /// Resolution failed; an error-message string is on the stack.
533    Missing,
534}
535
536/// Extract a byte-string error message from a `LuaError`, falling back to a
537/// debug rendering for non-string variants.
538fn error_to_bytes(e: &LuaError) -> Vec<u8> {
539    match e.message_bytes() {
540        Some(b) => b.to_vec(),
541        None => format!("{:?}", e).into_bytes(),
542    }
543}
544
545/// Encode a [`DynLibId`] as a `*mut c_void` for storage in `_CLIBS` as light
546/// userdata. The cast is the inverse of [`decode_dynlib_id`]; neither side
547/// ever dereferences the pointer.
548fn encode_dynlib_id(id: DynLibId) -> *mut std::ffi::c_void {
549    id.0 as usize as *mut std::ffi::c_void
550}
551
552/// Decode a [`DynLibId`] previously stored via [`encode_dynlib_id`].
553fn decode_dynlib_id(p: *mut std::ffi::c_void) -> DynLibId {
554    DynLibId(p as usize as u64)
555}
556
557// ── Path helpers ──────────────────────────────────────────────────────────────
558
559/// Return `registry["LUA_NOENV"]` as a boolean.
560///
561fn noenv(state: &mut LuaState) -> bool {
562    let _ = state.get_field_registry(b"LUA_NOENV");
563    let b = state.to_boolean(-1);
564    state.pop_n(1);
565    b
566}
567
568/// Set `package[fieldname]` to the appropriate path value.
569///
570/// Priority: versioned env var (e.g. `LUA_PATH_5_4`, only consulted when
571/// [`has_versioned_env_vars`] is true for this instance's version — 5.1 has
572/// no versioned env vars at all) → unversioned env var (`LUA_PATH`) →
573/// compiled-in default. When the env var contains `;;`, the compiled-in
574/// default is spliced in place of `;;`. On Windows the composed value then
575/// goes through [`setprogdir`] (`!` → executable directory), matching C's
576/// ordering: the substitution applies to environment-provided paths too, not
577/// only to the defaults. The caller must leave the `package` table at the
578/// stack top; the path value is set on it directly (the versioned env-var
579/// name is computed off-stack, so no index bookkeeping is needed).
580fn setpath(
581    state: &mut LuaState,
582    fieldname: &[u8],
583    envname: &[u8],
584    dft: &[u8],
585) -> Result<(), LuaError> {
586    let version = state.global().lua_version;
587
588    let path_opt = if noenv(state) {
589        None
590    } else if has_versioned_env_vars(version) {
591        let mut nver = envname.to_vec();
592        nver.extend_from_slice(lua_versuffix(version));
593        getenv_bytes(state, &nver).or_else(|| getenv_bytes(state, envname))
594    } else {
595        getenv_bytes(state, envname)
596    };
597
598    let final_path: Vec<u8> = match path_opt {
599        None => dft.to_vec(),
600        Some(path) if double_semicolon_splice_is_legacy(version) => {
601            legacy_double_semicolon_splice(&path, dft)
602        }
603        Some(path) => modern_double_semicolon_splice(&path, dft),
604    };
605
606    let final_path = setprogdir(final_path)?;
607    let s = state.intern_str(&final_path)?;
608    state.push(LuaValue::Str(s));
609    state.set_field(-2, fieldname)?;
610
611    Ok(())
612}
613
614/// Whether `version`'s `;;`-in-env-var default splice is the LEGACY,
615/// position-independent `gsub` (5.1/5.2/5.3's `luaL_gsub(path, ";;",
616/// ";AUXMARK;")` then `luaL_gsub(_, AUXMARK, dft)`: EVERY non-overlapping
617/// `;;` occurrence gets the default spliced in, wrapped in separators on
618/// both sides regardless of position) rather than the single-shot,
619/// position-aware splice **5.4** introduced (only the FIRST `;;` is
620/// replaced, and a boundary separator is omitted when `;;` sits at the very
621/// start or end of the string). Verified against `lua5.1.5`/`lua5.4.7`:
622/// `LUA_PATH="/a?;;;;/b"` splices the default TWICE on 5.1 (once per `;;`
623/// pair) but only once on 5.4, leaving the remaining `;;` literal.
624fn double_semicolon_splice_is_legacy(version: lua_types::LuaVersion) -> bool {
625    matches!(
626        version,
627        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
628    )
629}
630
631/// The 5.1/5.2/5.3 `;;` default splice: every non-overlapping occurrence of
632/// `;;` in `path` becomes `;` + `dft` + `;`, unconditionally. Collapses
633/// upstream's two-step `gsub(path, ";;", ";AUXMARK;")` then
634/// `gsub(_, AUXMARK, dft)` into one pass — the `AUXMARK` indirection exists
635/// upstream only to avoid re-scanning `dft` for the first gsub's pattern, and
636/// a single combined replacement already has that property (`dft` is
637/// inserted verbatim into the output, never re-scanned for `;;`).
638fn legacy_double_semicolon_splice(path: &[u8], dft: &[u8]) -> Vec<u8> {
639    let double_sep = [LUA_PATH_SEP, LUA_PATH_SEP];
640    if find_subslice(path, &double_sep).is_none() {
641        return path.to_vec();
642    }
643    let mut replacement = vec![LUA_PATH_SEP];
644    replacement.extend_from_slice(dft);
645    replacement.push(LUA_PATH_SEP);
646    gsub_bytes(path, &double_sep, &replacement)
647}
648
649/// The 5.4/5.5 `;;` default splice: only the FIRST occurrence of `;;` is
650/// replaced; the leading separator is omitted when `;;` starts the string
651/// (no prefix) and the trailing separator is omitted when `;;` ends it (no
652/// suffix).
653fn modern_double_semicolon_splice(path: &[u8], dft: &[u8]) -> Vec<u8> {
654    let double_sep = [LUA_PATH_SEP, LUA_PATH_SEP];
655    let Some(dftmark_pos) = find_subslice(path, &double_sep) else {
656        return path.to_vec();
657    };
658    let mut buf = Vec::new();
659    if dftmark_pos > 0 {
660        buf.extend_from_slice(&path[..dftmark_pos]);
661        buf.push(LUA_PATH_SEP);
662    }
663    buf.extend_from_slice(dft);
664    let after = dftmark_pos + 2;
665    if after < path.len() {
666        buf.push(LUA_PATH_SEP);
667        buf.extend_from_slice(&path[after..]);
668    }
669    buf
670}
671
672/// Upstream `setprogdir` (loadlib.c, Windows-only): replace every `!`
673/// (`LUA_EXEC_DIR`; 5.1 spells it `LUA_EXECDIR`, same value) in the composed
674/// path with the running executable's directory — `GetModuleFileNameA`
675/// truncated at its last `\`, here `std::env::current_exe().parent()`. C
676/// raises `"unable to get ModuleFileName"` via `luaL_error` when the lookup
677/// fails; the `current_exe` error and missing-parent cases map onto the same
678/// message. The directory bytes come from `to_string_lossy`, so a non-Unicode
679/// executable path degrades the same lossy way as C's ANSI-codepage `A` call.
680#[cfg(target_os = "windows")]
681fn setprogdir(path: Vec<u8>) -> Result<Vec<u8>, LuaError> {
682    let exe = std::env::current_exe()
683        .map_err(|_| LuaError::runtime(format_args!("unable to get ModuleFileName")))?;
684    let Some(dir) = exe.parent() else {
685        return Err(LuaError::runtime(format_args!(
686            "unable to get ModuleFileName"
687        )));
688    };
689    let dir = dir.to_string_lossy();
690    Ok(gsub_bytes(&path, b"!", dir.as_bytes()))
691}
692
693/// Non-Windows `setprogdir` is the identity, mirroring C's
694/// `#define setprogdir(L) ((void)0)`: a literal `!` in a POSIX path is kept.
695#[cfg(not(target_os = "windows"))]
696fn setprogdir(path: Vec<u8>) -> Result<Vec<u8>, LuaError> {
697    Ok(path)
698}
699
700// ── CLIBS registry table ──────────────────────────────────────────────────────
701
702/// Return the library handle stored at `registry._CLIBS[path]`, or `None`.
703///
704fn checkclib(state: &mut LuaState, path: &[u8]) -> Option<DynLibId> {
705    let _ = state.get_field_registry(CLIBS);
706    let _ = state.get_field(-1, path);
707    let handle = state.to_light_userdata(-1).map(decode_dynlib_id);
708    state.pop_n(2);
709    handle
710}
711
712/// Register a library handle in the CLIBS table (both by path and sequentially).
713///
714fn addtoclib(state: &mut LuaState, path: &[u8], plib: DynLibId) -> Result<(), LuaError> {
715    state.get_field_registry(CLIBS)?;
716    state.push(LuaValue::LightUserData(encode_dynlib_id(plib)));
717    state.push_value(-1)?;
718    state.set_field(-3, path)?;
719    let n = state.len_at(-2);
720    state.raw_seti(-2, n + 1)?;
721    state.pop_n(1);
722    Ok(())
723}
724
725/// `__gc` metamethod for the CLIBS table: unloads all registered C libraries
726/// in reverse order when the Lua state closes.
727///
728fn gctm(state: &mut LuaState) -> Result<usize, LuaError> {
729    let n = state.len_at(1);
730    let mut i = n;
731    while i >= 1 {
732        state.raw_geti(1, i)?;
733        if let Some(handle) = state.to_light_userdata(-1).map(decode_dynlib_id) {
734            lsys_unloadlib(state, handle);
735        }
736        state.pop_n(1);
737        i -= 1;
738    }
739    Ok(0)
740}
741
742// ── Dynamic function lookup ───────────────────────────────────────────────────
743
744/// Outcome of looking for a C function in a dynamically loaded library.
745///
746/// On success the function (or `true` for the `*` sentinel) is on the stack;
747/// on a non-fatal failure an error-message string is on the stack and the
748/// variant tells the caller what to report. Fatal errors propagate via `Err`.
749/// `Ok` is C's success; `ErrLib(tag)` is C's `ERRLIB` carrying the `LIB_FAIL`
750/// string (`"open"` for a true dlopen failure, `"absent"` when no backend is
751/// installed or the file does not exist); `ErrFunc` is C's `ERRFUNC` (the
752/// library opened but the symbol was not found).
753enum LookForFuncStatus {
754    /// Loader successfully resolved a symbol (function pushed on stack).
755    Ok,
756    /// Library could not be opened. `tag` is the `LIB_FAIL` string.
757    ErrLib(&'static [u8]),
758    /// Library opened but symbol could not be resolved.
759    ErrFunc,
760}
761
762fn lookforfunc(
763    state: &mut LuaState,
764    path: &[u8],
765    sym: &[u8],
766) -> Result<LookForFuncStatus, LuaError> {
767    let reg = match checkclib(state, path) {
768        Some(handle) => handle,
769        None => {
770            let (loaded, tag) = lsys_load(state, path, sym.first() == Some(&b'*'));
771            match loaded {
772                Some(handle) => {
773                    addtoclib(state, path, handle)?;
774                    handle
775                }
776                None => return Ok(LookForFuncStatus::ErrLib(tag)),
777            }
778        }
779    };
780    if sym.first() == Some(&b'*') {
781        state.push(LuaValue::Bool(true));
782        return Ok(LookForFuncStatus::Ok);
783    }
784    match lsys_sym(state, reg, sym) {
785        SymOutcome::Found(func) => {
786            state.push_c_function(func)?;
787            Ok(LookForFuncStatus::Ok)
788        }
789        SymOutcome::Missing => Ok(LookForFuncStatus::ErrFunc),
790    }
791}
792
793// ── Lua-callable package functions ────────────────────────────────────────────
794
795/// `package.loadlib(filename, funcname)` — open a C library and return a
796/// Lua-callable wrapper for `funcname`.
797///
798/// Returns: on success, the loader function (1 value).
799/// On error: `false`, error-message string, and `"open"` or `"init"` (3 values).
800///
801pub fn ll_loadlib(state: &mut LuaState) -> Result<usize, LuaError> {
802    let path = state.check_arg_string(1)?.to_vec();
803    let init = state.check_arg_string(2)?.to_vec();
804    let stat = lookforfunc(state, &path, &init)?;
805    let where_bytes: &[u8] = match stat {
806        LookForFuncStatus::Ok => return Ok(1),
807        LookForFuncStatus::ErrLib(tag) => tag,
808        LookForFuncStatus::ErrFunc => b"init",
809    };
810    // `luaL_pushfail` is `lua_pushnil` on every version (5.4 included); the fail
811    // value is `nil`, not `false`. The `LIB_FAIL` tag is chosen at run time: the
812    // CLI backend reports `LuaError::File` for a missing library → `"absent"`
813    // (matching C-Lua's no-dlfcn fallback), a true `dlopen` failure → `"open"`,
814    // and the "init" branch (symbol resolution failed after the library opened)
815    // is identical in every build.
816    state.push(LuaValue::Nil);
817    state.insert(-2)?;
818    let where_s = state.intern_str(where_bytes)?;
819    state.push(LuaValue::Str(where_s));
820    Ok(3)
821}
822
823// ── File existence check ──────────────────────────────────────────────────────
824
825/// Whether `filename` can be opened for reading.
826///
827/// `std::fs` is banned in `lua-stdlib`, so the probe is delegated to the
828/// embedder-registered `file_loader_hook` on `GlobalState`. Without a hook
829/// installed, `readable` reports `false` (the file system is unreachable) — so
830/// the in-process searcher tests deterministically see every path as not-found.
831fn readable(state: &LuaState, filename: &[u8]) -> bool {
832    match state.global().file_loader_hook {
833        Some(hook) => hook(filename).is_ok(),
834        None => false,
835    }
836}
837
838// ── Path-component iterator ───────────────────────────────────────────────────
839
840/// Iterator over `;`-separated path-template components, yielding each as an
841/// immutable slice (the C original walked one mutable buffer, swapping each
842/// separator for a NUL and back; this produces the identical sequence).
843struct PathComponents<'a> {
844    remaining: &'a [u8],
845}
846
847impl<'a> PathComponents<'a> {
848    fn new(path: &'a [u8]) -> Self {
849        PathComponents { remaining: path }
850    }
851}
852
853impl<'a> Iterator for PathComponents<'a> {
854    type Item = &'a [u8];
855
856    fn next(&mut self) -> Option<Self::Item> {
857        if self.remaining.is_empty() {
858            return None;
859        }
860        let component = match self.remaining.iter().position(|&b| b == LUA_PATH_SEP) {
861            Some(sep_pos) => {
862                let c = &self.remaining[..sep_pos];
863                self.remaining = &self.remaining[sep_pos + 1..];
864                c
865            }
866            None => {
867                let c = self.remaining;
868                self.remaining = &[];
869                c
870            }
871        };
872        Some(component)
873    }
874}
875
876// ── Error-message helpers ─────────────────────────────────────────────────────
877
878/// Push an error message listing all files in `path` that were not found.
879///
880/// Example output: `"no file 'a.lua'\n\tno file 'b.lua'"`
881///
882fn pusherrornotfound(state: &mut LuaState, path: &[u8]) -> Result<(), LuaError> {
883    let mut buf: Vec<u8> = Vec::new();
884    buf.extend_from_slice(b"no file '");
885    gsub_append(&mut buf, path, &[LUA_PATH_SEP], b"'\n\tno file '");
886    buf.push(b'\'');
887    let s = state.intern_str(&buf)?;
888    state.push(LuaValue::Str(s));
889    Ok(())
890}
891
892// ── Path search ───────────────────────────────────────────────────────────────
893
894/// Search for a readable file matching `name` in the `;`-separated `path`.
895///
896/// `sep` bytes in `name` are first replaced by `dirsep`; then each template's
897/// `?` is replaced by the adjusted name. On the first readable match, pushes the
898/// filename string and returns `Some(filename_bytes)`; otherwise pushes the
899/// not-found message and returns `None`.
900fn searchpath(
901    state: &mut LuaState,
902    name: &[u8],
903    path: &[u8],
904    sep: &[u8],
905    dirsep: &[u8],
906) -> Result<Option<Vec<u8>>, LuaError> {
907    let name_buf: Vec<u8> = if !sep.is_empty() && name.contains(&sep[0]) {
908        gsub_bytes(name, sep, dirsep)
909    } else {
910        name.to_vec()
911    };
912
913    let pathname: Vec<u8> = gsub_bytes(path, &[LUA_PATH_MARK], &name_buf);
914
915    for filename in PathComponents::new(&pathname) {
916        if readable(state, filename) {
917            let s = state.intern_str(filename)?;
918            state.push(LuaValue::Str(s));
919            return Ok(Some(filename.to_vec()));
920        }
921    }
922
923    pusherrornotfound(state, &pathname)?;
924    Ok(None)
925}
926
927/// `package.searchpath(name, path [, sep [, rep]])`.
928///
929/// Returns the first readable file in `path` with `sep` occurrences in `name`
930/// replaced by `rep`. On failure returns `luaL_pushfail` (a `nil`, NOT `false`,
931/// on every version) plus the error message. See [`ll_loadlib`] for the same
932/// `luaL_pushfail` = `lua_pushnil` translation.
933pub fn ll_searchpath(state: &mut LuaState) -> Result<usize, LuaError> {
934    let name = state.check_arg_string(1)?.to_vec();
935    let path = state.check_arg_string(2)?.to_vec();
936    let sep = state.opt_arg_string(3, b".")?;
937    let dirsep_default = [LUA_DIRSEP];
938    let dirsep = state.opt_arg_string(4, &dirsep_default)?;
939
940    let found = searchpath(state, &name, &path, &sep, &dirsep)?;
941    if found.is_some() {
942        return Ok(1);
943    }
944    if searchpath_error_has_leading_separator(state.global().lua_version) {
945        prepend_searchpath_separator(state)?;
946    }
947    state.push(LuaValue::Nil);
948    state.insert(-2)?;
949    Ok(2)
950}
951
952/// Whether the standalone `package.searchpath` error message carries a leading
953/// `\n\t` separator before its first `no file '…'` line.
954///
955/// In 5.2/5.3 the `searchpath` helper builds each entry as `"\n\tno file '%s'"`,
956/// so the first line is prefixed too; 5.4 moved that prefix into `findloader`'s
957/// per-iteration accumulator and made `searchpath`'s own message bare (the form
958/// this port's `pusherrornotfound` produces). The `require` trace is unaffected
959/// either way — there `findloader` supplies the single `\n\t` per searcher — so
960/// the seam is observable ONLY through the standalone Lua function. (5.1 has no
961/// `package.searchpath`.) Pinned in `tests/loadlib_strengthen.rs`.
962fn searchpath_error_has_leading_separator(version: lua_types::LuaVersion) -> bool {
963    matches!(version, lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53)
964}
965
966/// Replace the not-found message on the stack top with one carrying a leading
967/// `\n\t` (the 5.2/5.3 `searchpath` form). The message produced by
968/// `pusherrornotfound` is bare; this restores the legacy prefix.
969fn prepend_searchpath_separator(state: &mut LuaState) -> Result<(), LuaError> {
970    let Some(bare) = state.to_bytes(-1) else {
971        return Ok(());
972    };
973    state.pop_n(1);
974    let mut prefixed = b"\n\t".to_vec();
975    prefixed.extend_from_slice(&bare);
976    let s = state.intern_str(&prefixed)?;
977    state.push(LuaValue::Str(s));
978    Ok(())
979}
980
981/// Find a module file using the path stored in `package[pname]` (e.g.
982/// `package.path` / `package.cpath`), read from upvalue #1 of the searcher
983/// closure. Errors if that field is not a string.
984fn findfile(
985    state: &mut LuaState,
986    name: &[u8],
987    pname: &[u8],
988    dirsep: u8,
989) -> Result<Option<Vec<u8>>, LuaError> {
990    let uv = state.upvalue_index(1);
991    let _ = state.get_field(uv, pname);
992    let path_opt: Option<Vec<u8>> = state.to_bytes(-1);
993    let Some(path) = path_opt else {
994        state.pop_n(1);
995        return Err(LuaError::runtime(format_args!(
996            "'package.{}' must be a string",
997            String::from_utf8_lossy(pname)
998        )));
999    };
1000    state.pop_n(1);
1001    searchpath(state, name, &path, b".", &[dirsep])
1002}
1003
1004/// Check whether a module load succeeded, returning the open function + filename
1005/// (2 values) on success or raising an error on failure.
1006///
1007fn checkload(state: &mut LuaState, stat: bool, filename: &[u8]) -> Result<usize, LuaError> {
1008    if stat {
1009        let s = state.intern_str(filename)?;
1010        state.push(LuaValue::Str(s));
1011        Ok(2)
1012    } else {
1013        // The error embeds the module name (the `require` arg at stack[1]) and
1014        // the loader's own error message (the searcher's pushed string at the
1015        // stack top). Both are owned byte copies, so there is no aliasing.
1016        let modname = state.to_bytes(1).unwrap_or_else(|| b"?".to_vec());
1017        let loader_err = state.to_bytes(-1).unwrap_or_else(|| b"?".to_vec());
1018
1019        let mut msg = b"error loading module '".to_vec();
1020        msg.extend_from_slice(&modname);
1021        msg.extend_from_slice(b"' from file '");
1022        msg.extend_from_slice(filename);
1023        msg.extend_from_slice(b"':\n\t");
1024        msg.extend_from_slice(&loader_err);
1025
1026        let s = state.intern_str(&msg)?;
1027        return Err(LuaError::from_value(LuaValue::Str(s)));
1028    }
1029}
1030
1031// ── Searcher functions ────────────────────────────────────────────────────────
1032
1033/// Searcher that looks in `package.path` for a Lua source file.
1034///
1035/// Returns 1 value (error-message string) if not found, or 2 values (loader
1036/// function, filename) if found and loaded successfully.
1037///
1038fn searcher_lua(state: &mut LuaState) -> Result<usize, LuaError> {
1039    let name = state.check_arg_string(1)?.to_vec();
1040    let filename = findfile(state, &name, b"path", LUA_LSUBSEP)?;
1041    if filename.is_none() {
1042        return Ok(1);
1043    }
1044    let filename = filename.unwrap();
1045    // `std::fs` is banned in `lua-stdlib`, so file contents arrive via the
1046    // embedder-registered `file_loader_hook` on `GlobalState`; the bytes are then
1047    // parsed through `state.load(...)` (which dispatches to the parser hook) and
1048    // the resulting closure is left on the stack for `checkload` to pair with the
1049    // filename.
1050    let chunk = match state.global().file_loader_hook {
1051        Some(hook) => hook(&filename),
1052        None => Err(LuaError::runtime(format_args!(
1053            "no file_loader_hook registered; cannot read '{}'",
1054            String::from_utf8_lossy(&filename)
1055        ))),
1056    };
1057    let load_ok = match chunk {
1058        Ok(bytes) => {
1059            // Use a chunk name of the form `@filename` matching C's luaL_loadfilex.
1060            let mut chunkname = b"@".to_vec();
1061            chunkname.extend_from_slice(&filename);
1062            match state.load(&bytes, &chunkname, None) {
1063                Ok(true) => true,
1064                Ok(false) => false,
1065                Err(e) => {
1066                    let msg = match e.message_bytes() {
1067                        Some(b) => b.to_vec(),
1068                        None => format!("{:?}", &e).into_bytes(),
1069                    };
1070                    let s = state.intern_str(&msg)?;
1071                    state.push(LuaValue::Str(s));
1072                    false
1073                }
1074            }
1075        }
1076        Err(e) => {
1077            let msg = match e.message_bytes() {
1078                Some(b) => b.to_vec(),
1079                None => format!("{:?}", &e).into_bytes(),
1080            };
1081            let s = state.intern_str(&msg)?;
1082            state.push(LuaValue::Str(s));
1083            false
1084        }
1085    };
1086    checkload(state, load_ok, &filename)
1087}
1088
1089/// Try to load `modname`'s open function from the C dynamic library at `filename`.
1090///
1091/// Handles the "ignore mark" (`-`) convention: `"foo-bar"` first tries
1092/// `luaopen_foo`, then `luaopen_bar` as a fallback.
1093///
1094fn loadfunc(
1095    state: &mut LuaState,
1096    filename: &[u8],
1097    modname: &[u8],
1098) -> Result<LookForFuncStatus, LuaError> {
1099    let modname: Vec<u8> = gsub_bytes(modname, b".", LUA_OFSEP);
1100
1101    if let Some(mark_pos) = modname.iter().position(|&b| b == LUA_IGMARK) {
1102        let prefix = &modname[..mark_pos];
1103        let mut openfunc = LUA_POF.to_vec();
1104        openfunc.extend_from_slice(prefix);
1105        let stat = lookforfunc(state, filename, &openfunc)?;
1106        if !matches!(stat, LookForFuncStatus::ErrFunc) {
1107            return Ok(stat);
1108        }
1109        let tail = &modname[mark_pos + 1..];
1110        let mut openfunc2 = LUA_POF.to_vec();
1111        openfunc2.extend_from_slice(tail);
1112        return lookforfunc(state, filename, &openfunc2);
1113    }
1114
1115    let mut openfunc = LUA_POF.to_vec();
1116    openfunc.extend_from_slice(&modname);
1117    lookforfunc(state, filename, &openfunc)
1118}
1119
1120/// Searcher that looks in `package.cpath` for a C dynamic library.
1121///
1122fn searcher_c(state: &mut LuaState) -> Result<usize, LuaError> {
1123    let name = state.check_arg_string(1)?.to_vec();
1124    let filename = findfile(state, &name, b"cpath", LUA_CSUBSEP)?;
1125    if filename.is_none() {
1126        return Ok(1);
1127    }
1128    let filename = filename.unwrap();
1129    let stat = loadfunc(state, &filename, &name)?;
1130    let ok = matches!(stat, LookForFuncStatus::Ok);
1131    checkload(state, ok, &filename)
1132}
1133
1134/// Searcher that looks in `package.cpath` using only the root component
1135/// (everything before the first `.`) of the module name.
1136///
1137fn searcher_croot(state: &mut LuaState) -> Result<usize, LuaError> {
1138    let name = state.check_arg_string(1)?.to_vec();
1139    let dot_pos = name.iter().position(|&b| b == b'.');
1140    if dot_pos.is_none() {
1141        return Ok(0);
1142    }
1143    let dot_pos = dot_pos.unwrap();
1144
1145    let root = &name[..dot_pos];
1146
1147    let filename = findfile(state, root, b"cpath", LUA_CSUBSEP)?;
1148
1149    if filename.is_none() {
1150        return Ok(1);
1151    }
1152    let filename = filename.unwrap();
1153
1154    let stat = loadfunc(state, &filename, &name)?;
1155    match stat {
1156        LookForFuncStatus::Ok => {}
1157        LookForFuncStatus::ErrFunc => {
1158            let mut msg = b"no module '".to_vec();
1159            msg.extend_from_slice(&name);
1160            msg.extend_from_slice(b"' in file '");
1161            msg.extend_from_slice(&filename);
1162            msg.push(b'\'');
1163            let s = state.intern_str(&msg)?;
1164            state.push(LuaValue::Str(s));
1165            return Ok(1);
1166        }
1167        LookForFuncStatus::ErrLib(_) => {
1168            return checkload(state, false, &filename);
1169        }
1170    }
1171
1172    let s = state.intern_str(&filename)?;
1173    state.push(LuaValue::Str(s));
1174    Ok(2)
1175}
1176
1177/// Searcher that looks in `package.preload` for a pre-registered loader.
1178///
1179/// On a hit, every version leaves the loader function on the stack. From **5.4**
1180/// the searcher also returns the `:preload:` sentinel as loader data (a 2nd
1181/// value); 5.1/5.2/5.3 return only the function. See [`require_returns_loader_data`].
1182fn searcher_preload(state: &mut LuaState) -> Result<usize, LuaError> {
1183    let name = state.check_arg_string(1)?.to_vec();
1184    state.get_field_registry(b"_PRELOAD")?;
1185    let ty = state.get_field(-1, &name)?;
1186    if ty == LuaType::Nil {
1187        let mut msg = b"no field package.preload['".to_vec();
1188        msg.extend_from_slice(&name);
1189        msg.push(b'\'');
1190        msg.push(b']');
1191        let s = state.intern_str(&msg)?;
1192        state.push(LuaValue::Str(s));
1193        return Ok(1);
1194    }
1195    if !require_returns_loader_data(state.global().lua_version) {
1196        return Ok(1);
1197    }
1198    let tag = state.intern_str(b":preload:")?;
1199    state.push(LuaValue::Str(tag));
1200    Ok(2)
1201}
1202
1203// ── require implementation ────────────────────────────────────────────────────
1204
1205/// Iterate through `package.searchers` to find a loader for module `name`.
1206///
1207/// On success, leaves `(loader_function, loader_data)` at the top of the stack
1208/// (below the searchers table). On failure, raises a runtime error.
1209///
1210/// The accumulated `module '<name>' not found:` message lists one searcher per
1211/// line; the per-iteration `\n\t` prefix matches 5.4+ `findloader`, while the
1212/// pre-5.4 searchers prepend their own separator (the two regimes converge on
1213/// the identical trace, pinned in `tests/loadlib_strengthen.rs`).
1214fn findloader(state: &mut LuaState, name: &[u8]) -> Result<(), LuaError> {
1215    let uv = state.upvalue_index(1);
1216    // In 5.1 the searcher list lives in `package.loaders`; 5.2 renamed it to
1217    // `package.searchers` (5.2 keeps `loaders` as an alias). Read the name this
1218    // version exposes. See specs/followup/5.1-roster-syntax.md §1.
1219    let field: &[u8] = if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1220        b"loaders"
1221    } else {
1222        b"searchers"
1223    };
1224    let ty = state.get_field(uv, field)?;
1225    if ty != LuaType::Table {
1226        return Err(LuaError::runtime(format_args!(
1227            "'package.searchers' must be a table"
1228        )));
1229    }
1230
1231    let mut msg_buf: Vec<u8> = Vec::new();
1232
1233    let mut i: i64 = 1;
1234    loop {
1235        msg_buf.extend_from_slice(b"\n\t");
1236
1237        let item_ty = state.raw_geti(-1, i)?;
1238        if item_ty == LuaType::Nil {
1239            state.pop_n(1);
1240            let len = msg_buf.len();
1241            if len >= 2 {
1242                msg_buf.truncate(len - 2);
1243            }
1244            // Build the error message as a Lua string then raise.
1245            let mut err = b"module '".to_vec();
1246            err.extend_from_slice(name);
1247            err.extend_from_slice(b"' not found:");
1248            err.extend_from_slice(&msg_buf);
1249            let err_s = state.intern_str(&err)?;
1250            return Err(LuaError::from_value(LuaValue::Str(err_s)));
1251        }
1252
1253        let name_s = state.intern_str(name)?;
1254        state.push(LuaValue::Str(name_s));
1255
1256        state.call(1, 2)?;
1257
1258        // After call: two return values r1 (at -2) and r2 (at -1) on top.
1259        if state.type_at(-2) == LuaType::Function {
1260            // Loader found; leave (r1=function, r2=data) on stack and return.
1261            return Ok(());
1262        }
1263
1264        if state.type_at(-2) == LuaType::String {
1265            // r1 is an error-message string from the searcher.
1266            state.pop_n(1);
1267            if let Some(bytes) = state.to_bytes(-1) {
1268                msg_buf.extend_from_slice(&bytes);
1269            }
1270            state.pop_n(1);
1271        } else {
1272            state.pop_n(2);
1273            let len = msg_buf.len();
1274            if len >= 2 {
1275                msg_buf.truncate(len - 2);
1276            }
1277        }
1278
1279        i += 1;
1280    }
1281}
1282
1283/// `require(modname)` — load a module by name, using `package.loaded` as a
1284/// cache and `package.searchers` to find and load it if not already cached.
1285///
1286/// Returns the module value (and optionally the loader data) — 2 values.
1287///
1288pub fn ll_require(state: &mut LuaState) -> Result<usize, LuaError> {
1289    let name = state.check_arg_string(1)?.to_vec();
1290    let version = state.global().lua_version;
1291
1292    // Use the public-API `set_top` (relative to the current C-frame's `func`),
1293    // not the inherent `LuaState::set_top`, which sets an absolute index and
1294    // would truncate the whole stack.
1295    lua_vm::api::set_top(state, 1)?;
1296
1297    state.get_field_registry(b"_LOADED")?;
1298
1299    state.get_field(2, &name)?;
1300
1301    if state.to_boolean(-1) {
1302        return Ok(1);
1303    }
1304
1305    state.pop_n(1);
1306
1307    // `findloader` leaves (loader function, loader data) at the top.
1308    findloader(state, &name)?;
1309
1310    if require_passes_loader_data(version) {
1311        // 5.2+: the loader receives (name, loader data). 5.4+ additionally
1312        // returns the loader data as `require`'s 2nd value, so the data is kept
1313        // below the function (rotate) and re-pushed; 5.2/5.3 pass it but discard
1314        // it (return 1).
1315        state.rotate(-2, 1)?;
1316        state.push_value(1)?;
1317        state.push_value(-3)?;
1318        state.call(2, 1)?;
1319    } else {
1320        // 5.1: the loader receives only the name; there is no loader data.
1321        state.pop_n(1);
1322        state.push_value(1)?;
1323        state.call(1, 1)?;
1324    }
1325
1326    if state.type_at(-1) != LuaType::Nil {
1327        state.set_field(2, &name)?;
1328    } else {
1329        state.pop_n(1);
1330    }
1331
1332    let ty = state.get_field(2, &name)?;
1333    if ty == LuaType::Nil {
1334        state.push(LuaValue::Bool(true));
1335        state.copy_value(-1, -2)?;
1336        state.set_field(2, &name)?;
1337    }
1338
1339    if require_returns_loader_data(version) {
1340        // 5.4+: return (module result, loader data). The loader data is still on
1341        // the stack below the module result; swap them to module-result-first.
1342        state.rotate(-2, 1)?;
1343        Ok(2)
1344    } else {
1345        // 5.1/5.2/5.3: `ll_require` returns only the module (return 1). On the
1346        // 5.2/5.3 path the loader data is still on the stack below the result;
1347        // drop it so the single return value is the module.
1348        if require_passes_loader_data(version) {
1349            state.remove(-2)?;
1350        }
1351        Ok(1)
1352    }
1353}
1354
1355/// Whether `require` passes the searcher's loader data to the module loader as
1356/// a SECOND argument (after the module name).
1357///
1358/// 5.1's `ll_require` calls the loader with one argument (`lua_call(L, 1, 1)`);
1359/// 5.2 widened it to two (`lua_call(L, 2, 1)`), so every later version passes the
1360/// loader data too. Pinned in `tests/loadlib_strengthen.rs`.
1361fn require_passes_loader_data(version: lua_types::LuaVersion) -> bool {
1362    !matches!(version, lua_types::LuaVersion::V51)
1363}
1364
1365/// Whether `require` returns the searcher's loader data as a SECOND result.
1366///
1367/// This is a **5.4** addition (`ll_require`'s `return 2`); 5.1/5.2/5.3 return only
1368/// the module (`return 1`), so `local _, d = require(m)` yields `d == nil` there.
1369/// It is the same seam the preload searcher's `:preload:` sentinel rides on
1370/// (a searcher only bothers returning loader data on a version that surfaces it).
1371/// Pinned in `tests/loadlib_strengthen.rs`.
1372fn require_returns_loader_data(version: lua_types::LuaVersion) -> bool {
1373    matches!(version, lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55)
1374}
1375
1376// ── Package library setup ─────────────────────────────────────────────────────
1377
1378/// Create the `searchers` table and install the four built-in searchers, each
1379/// with the `package` table as upvalue #1.
1380///
1381fn createsearcherstable(state: &mut LuaState) -> Result<(), LuaError> {
1382    let searchers: &[fn(&mut LuaState) -> Result<usize, LuaError>] =
1383        &[searcher_preload, searcher_lua, searcher_c, searcher_croot];
1384
1385    state.create_table(searchers.len() as i32, 0)?;
1386
1387    for (i, &f) in searchers.iter().enumerate() {
1388        // Each searcher closes over the `package` table (upvalue #1) so
1389        // `findfile` can read `package.path`/`package.cpath` via
1390        // `lua_upvalueindex(1)`.
1391        state.push_value(-2)?;
1392        state.push_c_closure(f, 1)?;
1393        state.raw_seti(-2, (i + 1) as i64)?;
1394    }
1395    // Roster name deltas for the searcher list:
1396    //  - 5.1: the table is named `package.loaders`; there is NO
1397    //    `package.searchers` (verified against lua5.1.5: `package.searchers` is
1398    //    nil, `package.loaders` is a table).
1399    //  - 5.2: renamed to `package.searchers` but kept `package.loaders` as a
1400    //    compat alias (both point at the same list).
1401    //  - 5.3+: `package.searchers` only.
1402    let version = state.global().lua_version;
1403    let has_loaders = matches!(
1404        version,
1405        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1406    );
1407    let has_searchers = !matches!(version, lua_types::LuaVersion::V51);
1408    if has_loaders {
1409        state.push_value(-1)?;
1410        state.set_field(-3, b"loaders")?;
1411    }
1412    if has_searchers {
1413        state.set_field(-2, b"searchers")?;
1414    } else {
1415        // No `searchers` field under 5.1; drop the table copy left on the stack.
1416        state.pop_n(1);
1417    }
1418    Ok(())
1419}
1420
1421/// Create the `_CLIBS` registry table with a `__gc` finalizer that closes all
1422/// loaded C libraries when the Lua state is closed.
1423///
1424fn createclibstable(state: &mut LuaState) -> Result<(), LuaError> {
1425    state.get_subtable_registry(CLIBS)?;
1426    state.create_table(0, 1)?;
1427    state.push_c_function(gctm)?;
1428    state.set_field(-2, b"__gc")?;
1429    state.set_metatable(-2)?;
1430    Ok(())
1431}
1432
1433// ── Lua 5.1 `module` / `package.seeall` (deprecated module system) ────────────
1434//
1435// These ship only in the default lua5.1.5 build (`loadlib.c`) and were removed
1436// in 5.2. Registered under the V51 backend; see
1437// specs/followup/5.1-roster-syntax.md §1. They lean on the 5.1 fenv globals
1438// model: `module` sets its caller's environment to the module table (via
1439// `crate::base::set_func_env_at_level`), and `package.seeall` points a module
1440// table's `__index` at `_G`.
1441
1442/// `package.seeall(module)` — make a module table inherit globals.
1443///
1444/// Sets (creating if absent) `module`'s metatable `__index` to the global
1445/// table. Mirrors `ll_seeall` in 5.1 `loadlib.c`. Verified against lua5.1.5.
1446fn ll_seeall(state: &mut LuaState) -> Result<usize, LuaError> {
1447    state.check_arg_type(1, LuaType::Table)?;
1448    if !state.get_metatable(1)? {
1449        state.create_table(0, 1)?;
1450        state.push_value(-1)?;
1451        state.set_metatable(1)?;
1452    }
1453    state.push_globals()?;
1454    state.set_field(-2, b"__index")?;
1455    Ok(0)
1456}
1457
1458/// Walk a dotted module name from a table on the stack, creating intermediate
1459/// tables as needed, leaving the final (sub)table on the stack top. A faithful
1460/// reduction of `luaL_findtable(L, idx, name, 1)`; returns `Err` on a name
1461/// conflict (an intermediate path component is a non-table, non-nil value).
1462fn findtable(state: &mut LuaState, table_idx: i32, name: &[u8]) -> Result<(), LuaError> {
1463    // Start from a copy of the base table on the stack top.
1464    state.push_value_at(table_idx)?;
1465    for part in name.split(|&b| b == b'.') {
1466        // Stack top holds the current table; fetch current[part].
1467        let ty = state.get_field(-1, part)?;
1468        if ty == LuaType::Nil {
1469            state.pop_n(1); // remove nil
1470            state.create_table(0, 1)?; // new subtable
1471            state.push_value(-1)?; // duplicate it
1472            state.set_field(-3, part)?; // current[part] = subtable
1473                                        // Stack: ..., current, subtable. Remove the parent, keep subtable.
1474            state.remove(-2)?;
1475        } else if ty == LuaType::Table {
1476            // Stack: ..., current, value. Remove the parent, keep value.
1477            state.remove(-2)?;
1478        } else {
1479            return Err(LuaError::runtime(format_args!(
1480                "name conflict for module '{}'",
1481                String::from_utf8_lossy(name)
1482            )));
1483        }
1484    }
1485    Ok(())
1486}
1487
1488/// `module(name [, ...])` — Lua 5.1 only.
1489///
1490/// Creates (or reuses) a module table named `name`, registers it in
1491/// `package.loaded`, initializes its `_NAME`/`_M`/`_PACKAGE` fields, applies any
1492/// option functions (e.g. `package.seeall`), and sets the calling chunk's
1493/// environment to the module table. Mirrors `ll_module` in 5.1 `loadlib.c`.
1494fn ll_module(state: &mut LuaState) -> Result<usize, LuaError> {
1495    let modname: Vec<u8> = state.check_arg_string(1)?;
1496    let n_opts = state.top() as i32;
1497
1498    // Fetch _LOADED[modname]; create the module table if absent.
1499    state.get_field_registry(b"_LOADED")?;
1500    let loaded_idx = state.top() as i32;
1501    state.get_field(loaded_idx, &modname)?;
1502    if state.type_at(-1) != LuaType::Table {
1503        state.pop_n(1); // remove non-table result
1504                        // Find/create a global table named `modname` (supporting dotted names).
1505        state.push_globals()?;
1506        let g_idx = state.top() as i32;
1507        findtable(state, g_idx, &modname)?;
1508        state.remove(g_idx)?; // drop the globals table copy, keep the module table
1509        state.push_value(-1)?;
1510        state.set_field(loaded_idx, &modname)?; // _LOADED[modname] = module
1511    }
1512
1513    // Initialize the module if it has no `_NAME` yet.
1514    let has_name = state.get_field(-1, b"_NAME")? != LuaType::Nil;
1515    state.pop_n(1);
1516    if !has_name {
1517        // module._M = module
1518        state.push_value(-1)?;
1519        state.set_field(-2, b"_M")?;
1520        // module._NAME = modname
1521        state.push_string(&modname)?;
1522        state.set_field(-2, b"_NAME")?;
1523        // module._PACKAGE = full name minus the last dotted component.
1524        let pkg: &[u8] = match modname.iter().rposition(|&b| b == b'.') {
1525            Some(dot) => &modname[..=dot],
1526            None => b"",
1527        };
1528        state.push_string(pkg)?;
1529        state.set_field(-2, b"_PACKAGE")?;
1530    }
1531
1532    // Set the caller's environment to the module table (the running closure that
1533    // invoked `module`, i.e. level 1 relative to this C function).
1534    let module_tbl = state.value_at(-1);
1535    crate::base::set_func_env_at_level(state, 1, module_tbl)?;
1536
1537    // Apply option functions: for each extra arg, call `option(module)`.
1538    let mut i = 2;
1539    while i <= n_opts {
1540        state.push_value_at(i)?; // option function
1541        state.push_value(-2)?; // module table
1542        state.call(1, 0)?;
1543        i += 1;
1544    }
1545    Ok(0)
1546}
1547
1548/// Open the `package` library and return the `package` table.
1549///
1550pub fn luaopen_package(state: &mut LuaState) -> Result<usize, LuaError> {
1551    createclibstable(state)?;
1552
1553    // The C `pk_funcs` table also has placeholder entries for "preload",
1554    // "cpath", "path", "searchers", "loaded" (all NULL); those fields are set
1555    // explicitly below. Only `loadlib` is unconditional — `package.searchpath`
1556    // was added in 5.2 (absent on 5.1), so it is registered separately below.
1557    state.new_lib(&[(
1558        b"loadlib" as &[u8],
1559        ll_loadlib as fn(&mut LuaState) -> Result<usize, LuaError>,
1560    )])?;
1561
1562    if !matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1563        state.push_c_function(ll_searchpath)?;
1564        state.set_field(-2, b"searchpath")?;
1565    }
1566
1567    createsearcherstable(state)?;
1568
1569    let path_default = lua_path_default(state.global().lua_version);
1570    setpath(state, b"path", LUA_PATH_VAR, &path_default)?;
1571
1572    let cpath_default = lua_cpath_default(state.global().lua_version);
1573    setpath(state, b"cpath", LUA_CPATH_VAR, &cpath_default)?;
1574
1575    let config = package_config(state.global().lua_version);
1576    let config_s = state.intern_str(&config)?;
1577    state.push(LuaValue::Str(config_s));
1578
1579    state.set_field(-2, b"config")?;
1580
1581    state.get_subtable_registry(b"_LOADED")?;
1582    state.set_field(-2, b"loaded")?;
1583
1584    state.get_subtable_registry(b"_PRELOAD")?;
1585    state.set_field(-2, b"preload")?;
1586
1587    state.push_globals()?;
1588    state.push_value(-2)?;
1589    state.set_funcs_with_upvalues(
1590        &[(
1591            b"require" as &[u8],
1592            ll_require as fn(&mut LuaState) -> Result<usize, LuaError>,
1593        )],
1594        1,
1595    )?;
1596    state.pop_n(1);
1597
1598    // The deprecated module system: `package.seeall` (a field on the package
1599    // table) and the `module` global. Present in 5.1 and kept in 5.2.4 via the
1600    // default-on `LUA_COMPAT_MODULE`; fully removed in 5.3. Verified against
1601    // lua5.1.5 and lua5.2.4. See specs/followup/5.1-roster-syntax.md §1.
1602    if matches!(
1603        state.global().lua_version,
1604        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1605    ) {
1606        // The package table is on top of the stack here.
1607        state.push_c_function(ll_seeall)?;
1608        state.set_field(-2, b"seeall")?;
1609        // `module` is a *global*, not a `package` field.
1610        state.push_c_function(ll_module)?;
1611        state.set_global(b"module")?;
1612    }
1613
1614    Ok(1)
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619    use super::*;
1620    use lua_types::LuaVersion;
1621
1622    const ALL: [LuaVersion; 5] =
1623        [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55];
1624
1625    /// `lua_vdir` is the literal version-directory segment used both in
1626    /// default package paths and to build the versioned env-var name — one
1627    /// value per version, never `5.4` for anything else.
1628    #[test]
1629    fn vdir_is_version_exact() {
1630        assert_eq!(lua_vdir(LuaVersion::V51), b"5.1");
1631        assert_eq!(lua_vdir(LuaVersion::V52), b"5.2");
1632        assert_eq!(lua_vdir(LuaVersion::V53), b"5.3");
1633        assert_eq!(lua_vdir(LuaVersion::V54), b"5.4");
1634        assert_eq!(lua_vdir(LuaVersion::V55), b"5.5");
1635    }
1636
1637    #[test]
1638    fn versuffix_is_version_exact() {
1639        assert_eq!(lua_versuffix(LuaVersion::V51), b"_5_1");
1640        assert_eq!(lua_versuffix(LuaVersion::V52), b"_5_2");
1641        assert_eq!(lua_versuffix(LuaVersion::V53), b"_5_3");
1642        assert_eq!(lua_versuffix(LuaVersion::V54), b"_5_4");
1643        assert_eq!(lua_versuffix(LuaVersion::V55), b"_5_5");
1644    }
1645
1646    /// 5.1 is the one version with NO versioned environment variables at
1647    /// all — confirmed against `lua5.1.5`, where `LUA_PATH_5_1` has no
1648    /// effect on `package.path`. 5.2 onward all have them.
1649    #[test]
1650    fn only_5_1_lacks_versioned_env_vars() {
1651        assert!(!has_versioned_env_vars(LuaVersion::V51));
1652        for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1653            assert!(has_versioned_env_vars(v), "{v:?}");
1654        }
1655    }
1656
1657    /// Byte-for-byte against the unmodified upstream `make macosx` build of
1658    /// each version (`specs/oracle/CONTRACT.md`), captured from
1659    /// `/tmp/lua-refs/bin/lua5.x -e 'print(package.path)'`. The entry SHAPE
1660    /// (not just the `5.x` segment) differs by era — see `lua_path_default`'s
1661    /// doc comment.
1662    #[test]
1663    fn path_default_is_version_exact() {
1664        assert_eq!(
1665            unix_path_default(LuaVersion::V51),
1666            b"./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;\
1667              /usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua"
1668                .to_vec()
1669        );
1670        assert_eq!(
1671            unix_path_default(LuaVersion::V52),
1672            b"/usr/local/share/lua/5.2/?.lua;/usr/local/share/lua/5.2/?/init.lua;\
1673              /usr/local/lib/lua/5.2/?.lua;/usr/local/lib/lua/5.2/?/init.lua;./?.lua"
1674                .to_vec()
1675        );
1676        for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1677            let vdir = lua_vdir(v);
1678            let mut expected = Vec::new();
1679            expected.extend_from_slice(b"/usr/local/share/lua/");
1680            expected.extend_from_slice(vdir);
1681            expected.extend_from_slice(b"/?.lua;/usr/local/share/lua/");
1682            expected.extend_from_slice(vdir);
1683            expected.extend_from_slice(b"/?/init.lua;/usr/local/lib/lua/");
1684            expected.extend_from_slice(vdir);
1685            expected.extend_from_slice(b"/?.lua;/usr/local/lib/lua/");
1686            expected.extend_from_slice(vdir);
1687            expected.extend_from_slice(b"/?/init.lua;./?.lua;./?/init.lua");
1688            assert_eq!(unix_path_default(v), expected, "{v:?}");
1689        }
1690    }
1691
1692    #[test]
1693    fn cpath_default_is_version_exact() {
1694        assert_eq!(
1695            unix_cpath_default(LuaVersion::V51),
1696            b"./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so".to_vec()
1697        );
1698        for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1699            let vdir = lua_vdir(v);
1700            let mut expected = Vec::new();
1701            expected.extend_from_slice(b"/usr/local/lib/lua/");
1702            expected.extend_from_slice(vdir);
1703            expected.extend_from_slice(b"/?.so;/usr/local/lib/lua/");
1704            expected.extend_from_slice(vdir);
1705            expected.extend_from_slice(b"/loadall.so;./?.so");
1706            assert_eq!(unix_cpath_default(v), expected, "{v:?}");
1707        }
1708    }
1709
1710    /// Byte-for-byte expansion of each release's `luaconf.h` `_WIN32`
1711    /// `LUA_PATH_DEFAULT` (`LUA_LDIR` = `!\lua\`, `LUA_CDIR` = `!\`, 5.3+
1712    /// `LUA_SHRDIR` = `!\..\share\lua\<vdir>\`), pre-`setprogdir` — the `!`
1713    /// marks are still literal here.
1714    #[test]
1715    fn windows_path_default_is_version_exact() {
1716        assert_eq!(
1717            windows_path_default(LuaVersion::V51),
1718            b".\\?.lua;!\\lua\\?.lua;!\\lua\\?\\init.lua;!\\?.lua;!\\?\\init.lua".to_vec()
1719        );
1720        assert_eq!(
1721            windows_path_default(LuaVersion::V52),
1722            b"!\\lua\\?.lua;!\\lua\\?\\init.lua;!\\?.lua;!\\?\\init.lua;.\\?.lua".to_vec()
1723        );
1724        for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1725            let vdir = lua_vdir(v);
1726            let mut expected = Vec::new();
1727            expected.extend_from_slice(b"!\\lua\\?.lua;!\\lua\\?\\init.lua;");
1728            expected.extend_from_slice(b"!\\?.lua;!\\?\\init.lua;");
1729            expected.extend_from_slice(b"!\\..\\share\\lua\\");
1730            expected.extend_from_slice(vdir);
1731            expected.extend_from_slice(b"\\?.lua;!\\..\\share\\lua\\");
1732            expected.extend_from_slice(vdir);
1733            expected.extend_from_slice(b"\\?\\init.lua;.\\?.lua;.\\?\\init.lua");
1734            assert_eq!(windows_path_default(v), expected, "{v:?}");
1735        }
1736    }
1737
1738    /// Byte-for-byte expansion of each release's `luaconf.h` `_WIN32`
1739    /// `LUA_CPATH_DEFAULT`, pre-`setprogdir`.
1740    #[test]
1741    fn windows_cpath_default_is_version_exact() {
1742        assert_eq!(
1743            windows_cpath_default(LuaVersion::V51),
1744            b".\\?.dll;!\\?.dll;!\\loadall.dll".to_vec()
1745        );
1746        assert_eq!(
1747            windows_cpath_default(LuaVersion::V52),
1748            b"!\\?.dll;!\\loadall.dll;.\\?.dll".to_vec()
1749        );
1750        for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1751            let vdir = lua_vdir(v);
1752            let mut expected = b"!\\?.dll;!\\..\\lib\\lua\\".to_vec();
1753            expected.extend_from_slice(vdir);
1754            expected.extend_from_slice(b"\\?.dll;!\\loadall.dll;.\\?.dll");
1755            assert_eq!(windows_cpath_default(v), expected, "{v:?}");
1756        }
1757    }
1758
1759    /// The string step of `setprogdir`: `gsub_bytes(path, b"!", dir)` must
1760    /// replace EVERY `!` occurrence (C uses `luaL_gsub`), leave `!`-free
1761    /// paths untouched, and compose with the version defaults.
1762    #[test]
1763    fn exec_dir_substitution_replaces_every_mark() {
1764        assert_eq!(
1765            gsub_bytes(
1766                &windows_cpath_default(LuaVersion::V54),
1767                b"!",
1768                b"C:\\Lua\\bin"
1769            ),
1770            b"C:\\Lua\\bin\\?.dll;C:\\Lua\\bin\\..\\lib\\lua\\5.4\\?.dll;\
1771              C:\\Lua\\bin\\loadall.dll;.\\?.dll"
1772                .to_vec()
1773        );
1774        assert_eq!(
1775            gsub_bytes(b".\\?.lua;.\\?\\init.lua", b"!", b"C:\\Lua\\bin"),
1776            b".\\?.lua;.\\?\\init.lua".to_vec()
1777        );
1778    }
1779
1780    /// No two versions may collide on their directory segment or defaults —
1781    /// a regression here would silently point every version at the same
1782    /// installed-module directory again (the shape of issue #273). Checked
1783    /// for BOTH platform flavors, on every platform.
1784    #[test]
1785    fn every_version_has_a_distinct_path_default() {
1786        for (i, a) in ALL.iter().enumerate() {
1787            for b in &ALL[i + 1..] {
1788                assert_ne!(unix_path_default(*a), unix_path_default(*b), "{a:?} vs {b:?}");
1789                assert_ne!(
1790                    unix_cpath_default(*a),
1791                    unix_cpath_default(*b),
1792                    "{a:?} vs {b:?}"
1793                );
1794                assert_ne!(
1795                    windows_path_default(*a),
1796                    windows_path_default(*b),
1797                    "{a:?} vs {b:?}"
1798                );
1799                assert_ne!(
1800                    windows_cpath_default(*a),
1801                    windows_cpath_default(*b),
1802                    "{a:?} vs {b:?}"
1803                );
1804            }
1805        }
1806    }
1807
1808    /// Only 5.1/5.2/5.3 use the legacy gsub-based `;;` splice; 5.4/5.5 use
1809    /// the position-aware one.
1810    #[test]
1811    fn double_semicolon_splice_is_legacy_matrix() {
1812        for v in [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53] {
1813            assert!(double_semicolon_splice_is_legacy(v), "{v:?}");
1814        }
1815        for v in [LuaVersion::V54, LuaVersion::V55] {
1816            assert!(!double_semicolon_splice_is_legacy(v), "{v:?}");
1817        }
1818    }
1819
1820    /// The legacy splice replaces EVERY non-overlapping `;;` pair,
1821    /// unconditionally wrapping the default in separators regardless of
1822    /// where the pair sits — verified against `lua5.1.5`.
1823    #[test]
1824    fn legacy_splice_replaces_every_occurrence_both_sides() {
1825        assert_eq!(
1826            legacy_double_semicolon_splice(b"/a/?.lua;;;;/b/?.lua", b"DEFAULT"),
1827            b"/a/?.lua;DEFAULT;;DEFAULT;/b/?.lua".to_vec()
1828        );
1829        assert_eq!(
1830            legacy_double_semicolon_splice(b";;/b/?.lua", b"DEFAULT"),
1831            b";DEFAULT;/b/?.lua".to_vec()
1832        );
1833        assert_eq!(
1834            legacy_double_semicolon_splice(b"/a/?.lua;;", b"DEFAULT"),
1835            b"/a/?.lua;DEFAULT;".to_vec()
1836        );
1837        assert_eq!(
1838            legacy_double_semicolon_splice(b"/a/?.lua", b"DEFAULT"),
1839            b"/a/?.lua".to_vec()
1840        );
1841    }
1842
1843    /// The modern splice replaces only the FIRST `;;` pair and omits a
1844    /// boundary separator when the pair sits at the very start or end —
1845    /// verified against `lua5.4.7`.
1846    #[test]
1847    fn modern_splice_replaces_first_occurrence_and_omits_boundary_separators() {
1848        assert_eq!(
1849            modern_double_semicolon_splice(b"/a/?.lua;;;;/b/?.lua", b"DEFAULT"),
1850            b"/a/?.lua;DEFAULT;;;/b/?.lua".to_vec()
1851        );
1852        assert_eq!(
1853            modern_double_semicolon_splice(b";;/b/?.lua", b"DEFAULT"),
1854            b"DEFAULT;/b/?.lua".to_vec()
1855        );
1856        assert_eq!(
1857            modern_double_semicolon_splice(b"/a/?.lua;;", b"DEFAULT"),
1858            b"/a/?.lua;DEFAULT".to_vec()
1859        );
1860        assert_eq!(
1861            modern_double_semicolon_splice(b"/a/?.lua", b"DEFAULT"),
1862            b"/a/?.lua".to_vec()
1863        );
1864    }
1865}