Skip to main content

lua_stdlib/
loadlib.rs

1//! Dynamic library loader for the Lua `package` library.
2//!
3//! Ported from `reference/lua-5.4.7/src/loadlib.c` (758 lines, ~25 functions).
4//!
5//! Provides `require`, `package.loadlib`, `package.searchpath`, and the four
6//! built-in module searchers (preload, Lua-file, C-library, C-root).
7//!
8//! ## Platform-specific dynamic loading
9//!
10//! The three platform calls (`lsys_load`, `lsys_sym`, `lsys_unloadlib`) are
11//! dispatched through embedder hooks on [`lua_vm::state::GlobalState`]:
12//! `dynlib_load_hook`, `dynlib_symbol_hook`, `dynlib_unload_hook`. `lua-cli`
13//! installs a `libloading`-backed implementation; embeddings that omit the
14//! hooks behave like C-Lua's fallback platform stub (`LIB_FAIL = "absent"`).
15//!
16//! Keeping the platform calls behind hooks lets `lua-stdlib` stay free of
17//! `unsafe` per PORTING.md §1; `libloading` lives entirely in `lua-cli`.
18
19use crate::state_stub::{lua_CFunction, LuaState, LuaStateStubExt as _};
20use lua_types::{LuaError, LuaType, LuaValue};
21use lua_vm::state::{DynLibId, DynamicSymbol};
22
23// ── Module-level constants ────────────────────────────────────────────────────
24
25const LUA_POF: &[u8] = b"luaopen_";
26
27const LUA_OFSEP: &[u8] = b"_";
28
29const CLIBS: &[u8] = b"_CLIBS";
30
31// `lsys_load` chooses the tag at runtime: `"open"` when a load hook is
32// installed (matching POSIX/Windows behaviour) and `"absent"` when no hook
33// is registered (matching the fallback stub). The constant below carries the
34// fallback-stub spelling; the load-hook path uses `b"open"` directly.
35const LIB_FAIL_ABSENT: &[u8] = b"absent";
36
37const LUA_PATH_SEP: u8 = b';';
38
39const LUA_PATH_MARK: u8 = b'?';
40
41const LUA_IGMARK: u8 = b'-';
42
43#[cfg(target_os = "windows")]
44const LUA_DIRSEP: u8 = b'\\';
45#[cfg(not(target_os = "windows"))]
46const LUA_DIRSEP: u8 = b'/';
47
48// Both default to LUA_DIRSEP on all platforms.
49const LUA_CSUBSEP: u8 = LUA_DIRSEP;
50const LUA_LSUBSEP: u8 = LUA_DIRSEP;
51
52// In the Rust port these became enum variants of `LookForFuncStatus` so the
53// failure-tag string travels with the status (the C code always uses the
54// single compile-time `LIB_FAIL`). See `LookForFuncStatus` below.
55
56// is registered on `GlobalState`. The CLI backend supplies its own error
57// strings via the hook's `Err` return for "open" failures.
58const DLMSG: &[u8] = b"dynamic libraries not enabled; check your Lua installation";
59
60// Message returned via `(false, msg, "init")` when a hook resolves a symbol
61// against stock Lua 5.4's `lua_State *` C ABI. That ABI is not callable
62// against this build's `LuaState`; supporting it is a separate compatibility
63// project (see docs/LUA_PHASE_E_RUNTIME_SPEC.md Part 3).
64const C_ABI_UNSUPPORTED_MSG: &[u8] =
65    b"dynamic library loaded, but Lua C ABI modules are not supported by this build";
66
67const LUA_PATH_VAR: &[u8] = b"LUA_PATH";
68const LUA_CPATH_VAR: &[u8] = b"LUA_CPATH";
69
70// Matches C-Lua's luaconf.h defaults exactly: LUA_LDIR entries first, then
71// LUA_CDIR entries, then the local ./? fallback last.
72// TODO(port): These should come from a platform configuration crate, not be
73// hardcoded. Lua's build system inserts the actual install prefix here.
74#[cfg(not(target_os = "windows"))]
75const LUA_PATH_DEFAULT: &[u8] = b"/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua";
76#[cfg(target_os = "windows")]
77const LUA_PATH_DEFAULT: &[u8] = b"./?.lua;./?/init.lua";
78
79#[cfg(not(target_os = "windows"))]
80const LUA_CPATH_DEFAULT: &[u8] =
81    b"/usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;./?.so";
82#[cfg(target_os = "windows")]
83const LUA_CPATH_DEFAULT: &[u8] = b"./?.dll";
84
85// TODO(port): Centralise version constants; this is duplicated from luaconf.h.
86const LUA_VERSUFFIX: &[u8] = b"_5_4";
87
88fn getenv_bytes(state: &LuaState, name: &[u8]) -> Option<Vec<u8>> {
89    if let Some(env_fn) = state.global().env_hook {
90        return env_fn(name);
91    }
92
93    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
94    {
95        None
96    }
97
98    #[cfg(all(unix, not(all(target_arch = "wasm32", target_os = "unknown"))))]
99    {
100        use std::ffi::OsStr;
101        use std::os::unix::ffi::{OsStrExt, OsStringExt};
102
103        let os_name = OsStr::from_bytes(name);
104        std::env::var_os(os_name).map(|v| v.into_vec())
105    }
106
107    #[cfg(all(not(unix), not(all(target_arch = "wasm32", target_os = "unknown"))))]
108    {
109        std::str::from_utf8(name)
110            .ok()
111            .and_then(|name_str| std::env::var(name_str).ok())
112            .map(|s| s.into_bytes())
113    }
114}
115
116// ── Opaque library handle ─────────────────────────────────────────────────────
117//
118//
119// In this port, the library identity is the opaque `DynLibId(u64)` allocated
120// by the embedder-installed [`DynLibLoadHook`]. `lua-stdlib` never inspects
121// the value; it stashes the raw `u64` in `_CLIBS` as light userdata (cast
122// through `*mut c_void` to match C-Lua's representation) and hands it back to
123// the symbol and unload hooks.
124
125// ── Byte-string utilities ─────────────────────────────────────────────────────
126
127/// Append to `buf` the bytes of `s` with all non-overlapping occurrences of
128/// `pattern` replaced by `replacement`.
129///
130fn gsub_append(buf: &mut Vec<u8>, s: &[u8], pattern: &[u8], replacement: &[u8]) {
131    if pattern.is_empty() {
132        buf.extend_from_slice(s);
133        return;
134    }
135    let mut pos = 0;
136    while pos < s.len() {
137        if s[pos..].starts_with(pattern) {
138            buf.extend_from_slice(replacement);
139            pos += pattern.len();
140        } else {
141            buf.push(s[pos]);
142            pos += 1;
143        }
144    }
145}
146
147/// Return a new `Vec<u8>` with all non-overlapping occurrences of `pattern`
148/// in `s` replaced by `replacement`.
149fn gsub_bytes(s: &[u8], pattern: &[u8], replacement: &[u8]) -> Vec<u8> {
150    let mut out = Vec::new();
151    gsub_append(&mut out, s, pattern, replacement);
152    out
153}
154
155/// Find the byte offset of `needle` in `haystack`, or `None`.
156fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
157    if needle.is_empty() {
158        return Some(0);
159    }
160    haystack.windows(needle.len()).position(|w| w == needle)
161}
162
163// ── Platform-specific dynamic-loading dispatch ────────────────────────────────
164
165/// Unload a previously loaded C library.
166///
167///    — POSIX: `dlclose(lib)`; Windows: `FreeLibrary(lib)`.
168///
169/// Delegates to [`GlobalState::dynlib_unload_hook`]. When no hook is
170/// registered the library is leaked, which matches `libloading`'s safety
171/// model (the library must outlive every symbol it exports, and the simplest
172/// correct policy is to keep it alive for the state's lifetime).
173fn lsys_unloadlib(state: &mut LuaState, lib: DynLibId) {
174    if let Some(hook) = state.global().dynlib_unload_hook {
175        hook(lib);
176    }
177}
178
179/// Load a C library from `path`. If `see_glb` is true, make symbols globally
180/// visible (POSIX RTLD_GLOBAL). On failure, pushes an error string onto `state`.
181///
182///    — POSIX: `dlopen(path, RTLD_NOW | (seeglb ? RTLD_GLOBAL : RTLD_LOCAL))`
183///    — Windows: `LoadLibraryExA(path, NULL, LUA_LLE_FLAGS)`
184///
185/// PORT NOTE: returns `(handle, lib_fail_tag)`. The tag is `"absent"` when no
186/// hook is registered (matching C's fallback-stub `LIB_FAIL`) and `"open"`
187/// when the hook itself reports a failure (matching POSIX/Windows builds).
188fn lsys_load(
189    state: &mut LuaState,
190    path: &[u8],
191    see_glb: bool,
192) -> (Option<DynLibId>, &'static [u8]) {
193    let hook = state.global().dynlib_load_hook;
194    let Some(load_fn) = hook else {
195        let s = match state.intern_str(DLMSG) {
196            Ok(s) => s,
197            Err(_) => return (None, LIB_FAIL_ABSENT),
198        };
199        state.push(LuaValue::Str(s));
200        return (None, LIB_FAIL_ABSENT);
201    };
202    match load_fn(state, path, see_glb) {
203        Ok(id) => (Some(id), b"open"),
204        // PORT NOTE: `LuaError::File` is reserved for "no shared library at
205        // this path". Map it to the fallback-stub `"absent"` tag so that a
206        // probe like `package.loadlib("./nonexistent.so", ...)` reports
207        // `"absent"` regardless of whether a backend is installed. Every
208        // other `Err` is a true open-time failure → `"open"`.
209        Err(LuaError::File) => {
210            let mut msg = b"cannot find library '".to_vec();
211            msg.extend_from_slice(path);
212            msg.push(b'\'');
213            let s = match state.intern_str(&msg) {
214                Ok(s) => s,
215                Err(_) => return (None, LIB_FAIL_ABSENT),
216            };
217            state.push(LuaValue::Str(s));
218            (None, LIB_FAIL_ABSENT)
219        }
220        Err(err) => {
221            let msg = error_to_bytes(&err);
222            let s = match state.intern_str(&msg) {
223                Ok(s) => s,
224                Err(_) => return (None, b"open"),
225            };
226            state.push(LuaValue::Str(s));
227            (None, b"open")
228        }
229    }
230}
231
232/// Find symbol `sym` in library `lib` and either push it as a callable Lua
233/// function (returning `SymOutcome::Found`) or push an error message string
234/// and report which failure category the caller should propagate.
235///
236///    — POSIX: `cast_func(dlsym(lib, sym))`
237///    — Windows: `(lua_CFunction)(voidf)GetProcAddress(lib, sym)`
238fn lsys_sym(state: &mut LuaState, lib: DynLibId, sym: &[u8]) -> SymOutcome {
239    let hook = state.global().dynlib_symbol_hook;
240    let Some(sym_fn) = hook else {
241        let s = match state.intern_str(DLMSG) {
242            Ok(s) => s,
243            Err(_) => return SymOutcome::Missing,
244        };
245        state.push(LuaValue::Str(s));
246        return SymOutcome::Missing;
247    };
248    match sym_fn(state, lib, sym) {
249        Ok(DynamicSymbol::RustNative(f)) => SymOutcome::Found(f),
250        Ok(DynamicSymbol::LuaCAbi(_)) => {
251            let s = match state.intern_str(C_ABI_UNSUPPORTED_MSG) {
252                Ok(s) => s,
253                Err(_) => return SymOutcome::Missing,
254            };
255            state.push(LuaValue::Str(s));
256            SymOutcome::Missing
257        }
258        Ok(DynamicSymbol::Unsupported { reason }) => {
259            let s = match state.intern_str(&reason) {
260                Ok(s) => s,
261                Err(_) => return SymOutcome::Missing,
262            };
263            state.push(LuaValue::Str(s));
264            SymOutcome::Missing
265        }
266        Err(err) => {
267            let msg = error_to_bytes(&err);
268            let s = match state.intern_str(&msg) {
269                Ok(s) => s,
270                Err(_) => return SymOutcome::Missing,
271            };
272            state.push(LuaValue::Str(s));
273            SymOutcome::Missing
274        }
275    }
276}
277
278/// Outcome of `lsys_sym`.
279///
280/// `Missing` covers every non-success path (unknown symbol, ABI mismatch, hook
281/// absent, embedder-supplied refusal); in every case an error-message string
282/// has already been pushed onto the Lua stack, so the caller maps `Missing`
283/// to `ERRFUNC` / `"init"` without further work.
284enum SymOutcome {
285    /// Resolved to a Rust-native callable.
286    Found(lua_CFunction),
287    /// Resolution failed; an error-message string is on the stack.
288    Missing,
289}
290
291/// Extract a byte-string error message from a `LuaError`, falling back to a
292/// debug rendering for non-string variants.
293fn error_to_bytes(e: &LuaError) -> Vec<u8> {
294    match e {
295        LuaError::Runtime(LuaValue::Str(s)) | LuaError::Syntax(LuaValue::Str(s)) => {
296            s.as_bytes().to_vec()
297        }
298        other => format!("{:?}", other).into_bytes(),
299    }
300}
301
302/// Encode a [`DynLibId`] as a `*mut c_void` for storage in `_CLIBS` as light
303/// userdata. The cast is the inverse of [`decode_dynlib_id`]; neither side
304/// ever dereferences the pointer.
305fn encode_dynlib_id(id: DynLibId) -> *mut std::ffi::c_void {
306    id.0 as usize as *mut std::ffi::c_void
307}
308
309/// Decode a [`DynLibId`] previously stored via [`encode_dynlib_id`].
310fn decode_dynlib_id(p: *mut std::ffi::c_void) -> DynLibId {
311    DynLibId(p as usize as u64)
312}
313
314// ── Path helpers ──────────────────────────────────────────────────────────────
315
316/// Return `registry["LUA_NOENV"]` as a boolean.
317///
318fn noenv(state: &mut LuaState) -> bool {
319    let _ = state.get_field_registry(b"LUA_NOENV");
320    let b = state.to_boolean(-1);
321    state.pop_n(1);
322    b
323}
324
325/// Set `package[fieldname]` to the appropriate path value.
326///
327/// Priority: versioned env var (e.g. `LUA_PATH_5_4`) → unversioned env var
328/// (`LUA_PATH`) → compiled-in default. When the env var contains `;;`, the
329/// compiled-in default is spliced in place of `;;`.
330///
331/// const char *envname, const char *dft)`
332///
333/// PORT NOTE: C pushes the versioned env-var name string onto the Lua stack
334/// (via `lua_pushfstring`) and pops it at the end so that `setfield` uses index
335/// `-3`. In Rust we compute the versioned name without touching the Lua stack,
336/// so after pushing the final path value the package table is at `-2`. The
337/// caller must ensure the package table is at stack top when setpath is called.
338fn setpath(
339    state: &mut LuaState,
340    fieldname: &[u8],
341    envname: &[u8],
342    dft: &[u8],
343) -> Result<(), LuaError> {
344    let mut nver = envname.to_vec();
345    nver.extend_from_slice(LUA_VERSUFFIX);
346
347    let path_opt = if noenv(state) {
348        None
349    } else {
350        getenv_bytes(state, &nver).or_else(|| getenv_bytes(state, envname))
351    };
352
353    let final_path: Vec<u8> = if path_opt.is_none() {
354        dft.to_vec()
355    } else {
356        let path = path_opt.unwrap();
357        let double_sep = [LUA_PATH_SEP, LUA_PATH_SEP];
358        if let Some(dftmark_pos) = find_subslice(&path, &double_sep) {
359            // Path contains ";;": replace with default.
360            let mut buf = Vec::new();
361            if dftmark_pos > 0 {
362                buf.extend_from_slice(&path[..dftmark_pos]);
363                buf.push(LUA_PATH_SEP);
364            }
365            buf.extend_from_slice(dft);
366            let after = dftmark_pos + 2;
367            if after < path.len() {
368                buf.push(LUA_PATH_SEP);
369                buf.extend_from_slice(&path[after..]);
370            }
371            buf
372        } else {
373            path
374        }
375    };
376
377    // PORT NOTE: On Windows, setprogdir replaces LUA_EXEC_DIR in the path with
378    // the directory of the running executable (GetModuleFileNameA). On all other
379    // platforms it's a no-op ((void)0). Stubbed here; on Windows this would also
380    // require unsafe (Win32 API). The EXEC_DIR substitution is therefore skipped.
381
382    // PORT NOTE: In C the index is -3 because the versioned-name string is still
383    // on the stack. In Rust it is -2 because we did not push the versioned name.
384    let s = state.intern_str(&final_path)?;
385    state.push(LuaValue::Str(s));
386    state.set_field(-2, fieldname)?;
387
388    // PORT NOTE: No nver was pushed in Rust; nothing to pop here.
389
390    Ok(())
391}
392
393// ── CLIBS registry table ──────────────────────────────────────────────────────
394
395/// Return the library handle stored at `registry._CLIBS[path]`, or `None`.
396///
397fn checkclib(state: &mut LuaState, path: &[u8]) -> Option<DynLibId> {
398    let _ = state.get_field_registry(CLIBS);
399    let _ = state.get_field(-1, path);
400    let handle = state.to_light_userdata(-1).map(decode_dynlib_id);
401    state.pop_n(2);
402    handle
403}
404
405/// Register a library handle in the CLIBS table (both by path and sequentially).
406///
407fn addtoclib(state: &mut LuaState, path: &[u8], plib: DynLibId) -> Result<(), LuaError> {
408    state.get_field_registry(CLIBS)?;
409    state.push(LuaValue::LightUserData(encode_dynlib_id(plib)));
410    state.push_value(-1)?;
411    state.set_field(-3, path)?;
412    let n = state.len_at(-2);
413    state.raw_seti(-2, n + 1)?;
414    state.pop_n(1);
415    Ok(())
416}
417
418/// `__gc` metamethod for the CLIBS table: unloads all registered C libraries
419/// in reverse order when the Lua state closes.
420///
421fn gctm(state: &mut LuaState) -> Result<usize, LuaError> {
422    let n = state.len_at(1);
423    let mut i = n;
424    while i >= 1 {
425        state.raw_geti(1, i)?;
426        if let Some(handle) = state.to_light_userdata(-1).map(decode_dynlib_id) {
427            lsys_unloadlib(state, handle);
428        }
429        state.pop_n(1);
430        i -= 1;
431    }
432    Ok(0)
433}
434
435// ── Dynamic function lookup ───────────────────────────────────────────────────
436
437/// Look for a C function named `sym` in the dynamically loaded library at `path`.
438///
439/// On success, pushes the C function (or `true` for `*`-sentinel) and returns `Ok(0)`.
440/// On non-fatal failure, pushes an error message string and returns `Ok(ERRLIB)`
441/// or `Ok(ERRFUNC)`. Fatal errors (e.g. OOM) propagate via `Err`.
442///
443///
444/// PORT NOTE: C returns raw `int` error codes. Rust encodes them as `Ok(i32)`
445/// so the caller can distinguish "error code + message on stack" from "fatal Err".
446/// Status of `lookforfunc`. `Ok(0)` corresponds to C's `0` "success",
447/// `ErrLib(tag)` to C's `ERRLIB` (tag is the `LIB_FAIL` string the caller
448/// should attach: `"open"` for true dlopen failures, `"absent"` when no
449/// backend or the file doesn't exist), `ErrFunc` to C's `ERRFUNC`.
450enum LookForFuncStatus {
451    /// Loader successfully resolved a symbol (function pushed on stack).
452    Ok,
453    /// Library could not be opened. `tag` is the `LIB_FAIL` string.
454    ErrLib(&'static [u8]),
455    /// Library opened but symbol could not be resolved.
456    ErrFunc,
457}
458
459fn lookforfunc(
460    state: &mut LuaState,
461    path: &[u8],
462    sym: &[u8],
463) -> Result<LookForFuncStatus, LuaError> {
464    let reg = match checkclib(state, path) {
465        Some(handle) => handle,
466        None => {
467            let (loaded, tag) = lsys_load(state, path, sym.first() == Some(&b'*'));
468            match loaded {
469                Some(handle) => {
470                    addtoclib(state, path, handle)?;
471                    handle
472                }
473                None => return Ok(LookForFuncStatus::ErrLib(tag)),
474            }
475        }
476    };
477    if sym.first() == Some(&b'*') {
478        state.push(LuaValue::Bool(true));
479        return Ok(LookForFuncStatus::Ok);
480    }
481    match lsys_sym(state, reg, sym) {
482        SymOutcome::Found(func) => {
483            state.push_c_function(func)?;
484            Ok(LookForFuncStatus::Ok)
485        }
486        SymOutcome::Missing => Ok(LookForFuncStatus::ErrFunc),
487    }
488}
489
490// ── Lua-callable package functions ────────────────────────────────────────────
491
492/// `package.loadlib(filename, funcname)` — open a C library and return a
493/// Lua-callable wrapper for `funcname`.
494///
495/// Returns: on success, the loader function (1 value).
496/// On error: `false`, error-message string, and `"open"` or `"init"` (3 values).
497///
498pub fn ll_loadlib(state: &mut LuaState) -> Result<usize, LuaError> {
499    let path = state.check_arg_string(1)?.to_vec();
500    let init = state.check_arg_string(2)?.to_vec();
501    let stat = lookforfunc(state, &path, &init)?;
502    let where_bytes: &[u8] = match stat {
503        LookForFuncStatus::Ok => return Ok(1),
504        LookForFuncStatus::ErrLib(tag) => tag,
505        LookForFuncStatus::ErrFunc => b"init",
506    };
507    // PORT NOTE: luaL_pushfail pushes `false` in Lua 5.4 (changed from nil).
508    state.push(LuaValue::Bool(false));
509    state.insert(-2)?;
510    //
511    // PORT NOTE: the `LIB_FAIL` tag is chosen at run time. The CLI backend
512    // reports `LuaError::File` for a missing library → `"absent"` (matching
513    // C-Lua's no-dlfcn fallback); a true `dlopen` failure → `"open"`. The
514    // "init" branch (symbol resolution failed after the library opened) is
515    // identical in every build.
516    let where_s = state.intern_str(where_bytes)?;
517    state.push(LuaValue::Str(where_s));
518    Ok(3)
519}
520
521// ── File existence check ──────────────────────────────────────────────────────
522
523/// Try to open `filename` for reading; return `true` if it succeeds.
524///
525///    — `FILE *f = fopen(filename, "r"); if (f == NULL) return 0;`
526///
527/// PORT NOTE: `std::fs` is banned in `lua-stdlib`, so the actual file probe is
528/// delegated to the embedder-registered `file_loader_hook` on `GlobalState`.
529/// Without a hook installed, `readable` reports `false` (file system unreachable).
530fn readable(state: &LuaState, filename: &[u8]) -> bool {
531    match state.global().file_loader_hook {
532        Some(hook) => hook(filename).is_ok(),
533        None => false,
534    }
535}
536
537// ── Path-component iterator ───────────────────────────────────────────────────
538
539/// Iterator over `;`-separated path components.
540///
541/// through a buffer, temporarily zero-terminating each component. In Rust we
542/// advance a slice reference without mutation.
543///
544/// PORT NOTE: The C implementation restored each separator after use (mutating
545/// the buffer). This Rust version slices immutably, which changes the interface
546/// but produces the same sequence of filenames.
547struct PathComponents<'a> {
548    remaining: &'a [u8],
549}
550
551impl<'a> PathComponents<'a> {
552    fn new(path: &'a [u8]) -> Self {
553        PathComponents { remaining: path }
554    }
555}
556
557impl<'a> Iterator for PathComponents<'a> {
558    type Item = &'a [u8];
559
560    fn next(&mut self) -> Option<Self::Item> {
561        if self.remaining.is_empty() {
562            return None;
563        }
564        let component = match self.remaining.iter().position(|&b| b == LUA_PATH_SEP) {
565            Some(sep_pos) => {
566                let c = &self.remaining[..sep_pos];
567                self.remaining = &self.remaining[sep_pos + 1..];
568                c
569            }
570            None => {
571                let c = self.remaining;
572                self.remaining = &[];
573                c
574            }
575        };
576        Some(component)
577    }
578}
579
580// ── Error-message helpers ─────────────────────────────────────────────────────
581
582/// Push an error message listing all files in `path` that were not found.
583///
584/// Example output: `"no file 'a.lua'\n\tno file 'b.lua'"`
585///
586fn pusherrornotfound(state: &mut LuaState, path: &[u8]) -> Result<(), LuaError> {
587    let mut buf: Vec<u8> = Vec::new();
588    buf.extend_from_slice(b"no file '");
589    gsub_append(&mut buf, path, &[LUA_PATH_SEP], b"'\n\tno file '");
590    buf.push(b'\'');
591    let s = state.intern_str(&buf)?;
592    state.push(LuaValue::Str(s));
593    Ok(())
594}
595
596// ── Path search ───────────────────────────────────────────────────────────────
597
598/// Search for a readable file matching `name` in the `;`-separated `path`.
599///
600/// In each path template, `?` is replaced by `name` (with `sep` bytes replaced
601/// by `dirsep` first). Returns `Some(filename_bytes)` and pushes the filename
602/// string on the Lua stack if found. Returns `None` and pushes an error message
603/// string if not found.
604///
605/// const char *path, const char *sep,
606/// const char *dirsep)`
607fn searchpath(
608    state: &mut LuaState,
609    name: &[u8],
610    path: &[u8],
611    sep: &[u8],
612    dirsep: &[u8],
613) -> Result<Option<Vec<u8>>, LuaError> {
614    //        name = luaL_gsub(L, name, sep, dirsep);
615    let name_buf: Vec<u8> = if !sep.is_empty() && name.contains(&sep[0]) {
616        gsub_bytes(name, sep, dirsep)
617    } else {
618        name.to_vec()
619    };
620
621    // Build pathname list: replace every '?' in path with the (adjusted) name.
622    let pathname: Vec<u8> = gsub_bytes(path, &[LUA_PATH_MARK], &name_buf);
623
624    for filename in PathComponents::new(&pathname) {
625        if readable(state, filename) {
626            let s = state.intern_str(filename)?;
627            state.push(LuaValue::Str(s));
628            return Ok(Some(filename.to_vec()));
629        }
630    }
631
632    // PORT NOTE: C uses the Lua-stack string of the expanded pathname as the
633    // argument to pusherrornotfound. In Rust we have `pathname` already as a
634    // Vec<u8>; we pass it directly without the round-trip through the Lua stack.
635    pusherrornotfound(state, &pathname)?;
636    Ok(None)
637}
638
639/// `package.searchpath(name, path [, sep [, rep]])`.
640///
641/// Returns the first readable file in `path` with `sep` occurrences in `name`
642/// replaced by `rep`. On failure returns `false` plus the error message.
643///
644pub fn ll_searchpath(state: &mut LuaState) -> Result<usize, LuaError> {
645    let name = state.check_arg_string(1)?.to_vec();
646    let path = state.check_arg_string(2)?.to_vec();
647    let sep = state.opt_arg_string(3, b".")?;
648    let dirsep_default = [LUA_DIRSEP];
649    let dirsep = state.opt_arg_string(4, &dirsep_default)?;
650
651    let found = searchpath(state, &name, &path, &sep, &dirsep)?;
652    if found.is_some() {
653        return Ok(1);
654    }
655    state.push(LuaValue::Bool(false));
656    state.insert(-2)?;
657    Ok(2)
658}
659
660/// Find a module file using the path stored in `package[pname]`.
661///
662/// const char *pname, const char *dirsep)`
663fn findfile(
664    state: &mut LuaState,
665    name: &[u8],
666    pname: &[u8],
667    dirsep: u8,
668) -> Result<Option<Vec<u8>>, LuaError> {
669    // The package table is upvalue #1 for the searcher closures.
670    let uv = state.upvalue_index(1);
671    let _ = state.get_field(uv, pname);
672    let path_opt: Option<Vec<u8>> = state.to_bytes(-1);
673    let Some(path) = path_opt else {
674        state.pop_n(1);
675        return Err(LuaError::runtime(format_args!(
676            "'package.{}' must be a string",
677            String::from_utf8_lossy(pname)
678        )));
679    };
680    state.pop_n(1);
681    searchpath(state, name, &path, b".", &[dirsep])
682}
683
684/// Check whether a module load succeeded, returning the open function + filename
685/// (2 values) on success or raising an error on failure.
686///
687fn checkload(state: &mut LuaState, stat: bool, filename: &[u8]) -> Result<usize, LuaError> {
688    if stat {
689        let s = state.intern_str(filename)?;
690        state.push(LuaValue::Str(s));
691        Ok(2)
692    } else {
693        //                         lua_tostring(L, 1), filename, lua_tostring(L, -1));
694        // PORT NOTE: The error message in C embeds the module name (stack[1]) and
695        // the loader error message (stack top). In Rust we read those byte slices.
696        // TODO(port): state.to_bytes(1) and state.to_bytes(-1) borrow from the
697        //             stack simultaneously; in Phase B use index-snapshot clones.
698        let modname = state.to_bytes(1).unwrap_or_else(|| b"?".to_vec());
699        let loader_err = state.to_bytes(-1).unwrap_or_else(|| b"?".to_vec());
700
701        let mut msg = b"error loading module '".to_vec();
702        msg.extend_from_slice(&modname);
703        msg.extend_from_slice(b"' from file '");
704        msg.extend_from_slice(filename);
705        msg.extend_from_slice(b"':\n\t");
706        msg.extend_from_slice(&loader_err);
707
708        // PERF(port): builds a heap Vec then interns; in Phase B use push_fstring.
709        let s = state.intern_str(&msg)?;
710        return Err(LuaError::from_value(LuaValue::Str(s)));
711    }
712}
713
714// ── Searcher functions ────────────────────────────────────────────────────────
715
716/// Searcher that looks in `package.path` for a Lua source file.
717///
718/// Returns 1 value (error-message string) if not found, or 2 values (loader
719/// function, filename) if found and loaded successfully.
720///
721fn searcher_lua(state: &mut LuaState) -> Result<usize, LuaError> {
722    let name = state.check_arg_string(1)?.to_vec();
723    let filename = findfile(state, &name, b"path", LUA_LSUBSEP)?;
724    if filename.is_none() {
725        return Ok(1);
726    }
727    let filename = filename.unwrap();
728    //
729    // PORT NOTE: `std::fs` is banned in `lua-stdlib`, so file contents come in
730    // via the embedder-registered `file_loader_hook` on `GlobalState`. We then
731    // parse them through `state.load(...)` (which dispatches to the parser
732    // hook) and place the resulting closure on the stack so `checkload` can
733    // pair it with the filename.
734    let chunk = match state.global().file_loader_hook {
735        Some(hook) => hook(&filename),
736        None => Err(LuaError::runtime(format_args!(
737            "no file_loader_hook registered; cannot read '{}'",
738            String::from_utf8_lossy(&filename)
739        ))),
740    };
741    let load_ok = match chunk {
742        Ok(bytes) => {
743            // Use a chunk name of the form `@filename` matching C's luaL_loadfilex.
744            let mut chunkname = b"@".to_vec();
745            chunkname.extend_from_slice(&filename);
746            match state.load(&bytes, &chunkname, None) {
747                Ok(true) => true,
748                Ok(false) => false,
749                Err(e) => {
750                    let msg = match e {
751                        LuaError::Syntax(LuaValue::Str(ref s))
752                        | LuaError::Runtime(LuaValue::Str(ref s)) => s.as_bytes().to_vec(),
753                        other => format!("{:?}", other).into_bytes(),
754                    };
755                    let s = state.intern_str(&msg)?;
756                    state.push(LuaValue::Str(s));
757                    false
758                }
759            }
760        }
761        Err(e) => {
762            let msg = match e {
763                LuaError::Runtime(LuaValue::Str(ref s)) => s.as_bytes().to_vec(),
764                other => format!("{:?}", other).into_bytes(),
765            };
766            let s = state.intern_str(&msg)?;
767            state.push(LuaValue::Str(s));
768            false
769        }
770    };
771    checkload(state, load_ok, &filename)
772}
773
774/// Try to load `modname`'s open function from the C dynamic library at `filename`.
775///
776/// Handles the "ignore mark" (`-`) convention: `"foo-bar"` first tries
777/// `luaopen_foo`, then `luaopen_bar` as a fallback.
778///
779fn loadfunc(
780    state: &mut LuaState,
781    filename: &[u8],
782    modname: &[u8],
783) -> Result<LookForFuncStatus, LuaError> {
784    let modname: Vec<u8> = gsub_bytes(modname, b".", LUA_OFSEP);
785
786    if let Some(mark_pos) = modname.iter().position(|&b| b == LUA_IGMARK) {
787        let prefix = &modname[..mark_pos];
788        let mut openfunc = LUA_POF.to_vec();
789        openfunc.extend_from_slice(prefix);
790        let stat = lookforfunc(state, filename, &openfunc)?;
791        if !matches!(stat, LookForFuncStatus::ErrFunc) {
792            return Ok(stat);
793        }
794        let tail = &modname[mark_pos + 1..];
795        let mut openfunc2 = LUA_POF.to_vec();
796        openfunc2.extend_from_slice(tail);
797        return lookforfunc(state, filename, &openfunc2);
798    }
799
800    let mut openfunc = LUA_POF.to_vec();
801    openfunc.extend_from_slice(&modname);
802    lookforfunc(state, filename, &openfunc)
803}
804
805/// Searcher that looks in `package.cpath` for a C dynamic library.
806///
807fn searcher_c(state: &mut LuaState) -> Result<usize, LuaError> {
808    let name = state.check_arg_string(1)?.to_vec();
809    let filename = findfile(state, &name, b"cpath", LUA_CSUBSEP)?;
810    if filename.is_none() {
811        return Ok(1);
812    }
813    let filename = filename.unwrap();
814    let stat = loadfunc(state, &filename, &name)?;
815    let ok = matches!(stat, LookForFuncStatus::Ok);
816    checkload(state, ok, &filename)
817}
818
819/// Searcher that looks in `package.cpath` using only the root component
820/// (everything before the first `.`) of the module name.
821///
822fn searcher_croot(state: &mut LuaState) -> Result<usize, LuaError> {
823    let name = state.check_arg_string(1)?.to_vec();
824    let dot_pos = name.iter().position(|&b| b == b'.');
825    if dot_pos.is_none() {
826        return Ok(0);
827    }
828    let dot_pos = dot_pos.unwrap();
829
830    let root = &name[..dot_pos];
831    let root_s = state.intern_str(root)?;
832    state.push(LuaValue::Str(root_s));
833
834    // PORT NOTE: C reads the root string back from the stack; in Rust we use
835    // the slice directly and then pop the stack entry below.
836    let filename = findfile(state, root, b"cpath", LUA_CSUBSEP)?;
837    // Pop the root string we pushed above (findfile does not consume it).
838    state.pop_n(1);
839
840    if filename.is_none() {
841        return Ok(1);
842    }
843    let filename = filename.unwrap();
844
845    let stat = loadfunc(state, &filename, &name)?;
846    match stat {
847        LookForFuncStatus::Ok => {}
848        LookForFuncStatus::ErrFunc => {
849            let mut msg = b"no module '".to_vec();
850            msg.extend_from_slice(&name);
851            msg.extend_from_slice(b"' in file '");
852            msg.extend_from_slice(&filename);
853            msg.push(b'\'');
854            let s = state.intern_str(&msg)?;
855            state.push(LuaValue::Str(s));
856            return Ok(1);
857        }
858        LookForFuncStatus::ErrLib(_) => {
859            return checkload(state, false, &filename);
860        }
861    }
862
863    let s = state.intern_str(&filename)?;
864    state.push(LuaValue::Str(s));
865    Ok(2)
866}
867
868/// Searcher that looks in `package.preload` for a pre-registered loader.
869///
870fn searcher_preload(state: &mut LuaState) -> Result<usize, LuaError> {
871    let name = state.check_arg_string(1)?.to_vec();
872    state.get_field_registry(b"_PRELOAD")?;
873    let ty = state.get_field(-1, &name)?;
874    if ty == LuaType::Nil {
875        let mut msg = b"no field package.preload['".to_vec();
876        msg.extend_from_slice(&name);
877        msg.push(b'\'');
878        msg.push(b']');
879        let s = state.intern_str(&msg)?;
880        state.push(LuaValue::Str(s));
881        return Ok(1);
882    }
883    let tag = state.intern_str(b":preload:")?;
884    state.push(LuaValue::Str(tag));
885    Ok(2)
886}
887
888// ── require implementation ────────────────────────────────────────────────────
889
890/// Iterate through `package.searchers` to find a loader for module `name`.
891///
892/// On success, leaves `(loader_function, loader_data)` at the top of the stack
893/// (below the searchers table). On failure, raises a runtime error.
894///
895///
896/// TODO(port): The exact absolute stack indices used in C (index 3 for the
897/// searchers table) depend on the caller (`ll_require`) having set up the
898/// stack in a specific way. In Rust we use relative indices. The behaviour
899/// should match C but the index arithmetic must be verified in Phase B.
900fn findloader(state: &mut LuaState, name: &[u8]) -> Result<(), LuaError> {
901    //        luaL_error(L, "'package.searchers' must be a table");
902    let uv = state.upvalue_index(1);
903    // In 5.1 the searcher list lives in `package.loaders`; 5.2 renamed it to
904    // `package.searchers` (5.2 keeps `loaders` as an alias). Read the name this
905    // version exposes. See specs/followup/5.1-roster-syntax.md §1.
906    let field: &[u8] = if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
907        b"loaders"
908    } else {
909        b"searchers"
910    };
911    let ty = state.get_field(uv, field)?;
912    if ty != LuaType::Table {
913        return Err(LuaError::runtime(format_args!(
914            "'package.searchers' must be a table"
915        )));
916    }
917    // Searchers table is now at the top of the stack.
918
919    let mut msg_buf: Vec<u8> = Vec::new();
920
921    let mut i: i64 = 1;
922    loop {
923        msg_buf.extend_from_slice(b"\n\t");
924
925        // PORT NOTE: In C the searchers table is at absolute index 3. In Rust
926        // it is at -1 (relative to the top). TODO(port): verify this is correct
927        // after accounting for whatever else the caller left on the stack.
928        let item_ty = state.raw_geti(-1, i)?;
929        if item_ty == LuaType::Nil {
930            state.pop_n(1);
931            let len = msg_buf.len();
932            if len >= 2 {
933                msg_buf.truncate(len - 2);
934            }
935            // Build the error message as a Lua string then raise.
936            let mut err = b"module '".to_vec();
937            err.extend_from_slice(name);
938            err.extend_from_slice(b"' not found:");
939            err.extend_from_slice(&msg_buf);
940            let err_s = state.intern_str(&err)?;
941            return Err(LuaError::from_value(LuaValue::Str(err_s)));
942        }
943
944        let name_s = state.intern_str(name)?;
945        state.push(LuaValue::Str(name_s));
946
947        state.call(1, 2)?;
948
949        // After call: two return values r1 (at -2) and r2 (at -1) on top.
950        if state.type_at(-2) == LuaType::Function {
951            // Loader found; leave (r1=function, r2=data) on stack and return.
952            return Ok(());
953        }
954
955        if state.type_at(-2) == LuaType::String {
956            // r1 is an error-message string from the searcher.
957            state.pop_n(1);
958            if let Some(bytes) = state.to_bytes(-1) {
959                msg_buf.extend_from_slice(&bytes);
960            }
961            state.pop_n(1);
962        } else {
963            state.pop_n(2);
964            let len = msg_buf.len();
965            if len >= 2 {
966                msg_buf.truncate(len - 2);
967            }
968        }
969
970        i += 1;
971    }
972}
973
974/// `require(modname)` — load a module by name, using `package.loaded` as a
975/// cache and `package.searchers` to find and load it if not already cached.
976///
977/// Returns the module value (and optionally the loader data) — 2 values.
978///
979pub fn ll_require(state: &mut LuaState) -> Result<usize, LuaError> {
980    let name = state.check_arg_string(1)?.to_vec();
981
982    // PORT NOTE: must use the public-API `set_top` (relative to the current
983    // C-frame's `func`), not `LuaState::set_top` which is an inherent that
984    // sets an absolute stack index and would truncate the entire stack.
985    lua_vm::api::set_top(state, 1)?;
986
987    state.get_field_registry(b"_LOADED")?;
988
989    state.get_field(2, &name)?;
990
991    if state.to_boolean(-1) {
992        return Ok(1);
993    }
994
995    state.pop_n(1);
996
997    // After this, the stack has: [name(1), LOADED(2), searchers(3), loader(-2), loaderdata(-1)]
998    findloader(state, &name)?;
999
1000    // Swaps loader and loaderdata: [..., loaderdata, loader]
1001    state.rotate(-2, 1)?;
1002
1003    state.push_value(1)?;
1004
1005    // PORT NOTE: After the rotate, loaderdata is 3 from top (-3). In C this is
1006    // at absolute index 4 (but C uses the pre-rotate layout). TODO(port): verify.
1007    state.push_value(-3)?;
1008
1009    state.call(2, 1)?;
1010
1011    if state.type_at(-1) != LuaType::Nil {
1012        state.set_field(2, &name)?;
1013    } else {
1014        state.pop_n(1);
1015    }
1016
1017    let ty = state.get_field(2, &name)?;
1018    if ty == LuaType::Nil {
1019        state.push(LuaValue::Bool(true));
1020        state.copy_value(-1, -2)?;
1021        state.set_field(2, &name)?;
1022    }
1023
1024    state.rotate(-2, 1)?;
1025
1026    Ok(2)
1027}
1028
1029// ── Package library setup ─────────────────────────────────────────────────────
1030
1031/// Create the `searchers` table and install the four built-in searchers, each
1032/// with the `package` table as upvalue #1.
1033///
1034fn createsearcherstable(state: &mut LuaState) -> Result<(), LuaError> {
1035    //        searcher_Lua, searcher_C, searcher_Croot, NULL };
1036    let searchers: &[fn(&mut LuaState) -> Result<usize, LuaError>] =
1037        &[searcher_preload, searcher_lua, searcher_c, searcher_croot];
1038
1039    state.create_table(searchers.len() as i32, 0)?;
1040
1041    for (i, &f) in searchers.iter().enumerate() {
1042        state.push_value(-2)?;
1043        // TODO(port): push_c_closure takes the function and n upvalues from the
1044        //             stack. The package table upvalue must be correctly associated
1045        //             with each searcher closure so that findfile can access it
1046        //             via lua_upvalueindex(1). Verify in Phase B.
1047        state.push_c_closure(f, 1)?;
1048        state.raw_seti(-2, (i + 1) as i64)?;
1049    }
1050    // Roster name deltas for the searcher list:
1051    //  - 5.1: the table is named `package.loaders`; there is NO
1052    //    `package.searchers` (verified against lua5.1.5: `package.searchers` is
1053    //    nil, `package.loaders` is a table).
1054    //  - 5.2: renamed to `package.searchers` but kept `package.loaders` as a
1055    //    compat alias (both point at the same list).
1056    //  - 5.3+: `package.searchers` only.
1057    let version = state.global().lua_version;
1058    let has_loaders = matches!(
1059        version,
1060        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1061    );
1062    let has_searchers = !matches!(version, lua_types::LuaVersion::V51);
1063    if has_loaders {
1064        state.push_value(-1)?;
1065        state.set_field(-3, b"loaders")?;
1066    }
1067    if has_searchers {
1068        state.set_field(-2, b"searchers")?;
1069    } else {
1070        // No `searchers` field under 5.1; drop the table copy left on the stack.
1071        state.pop_n(1);
1072    }
1073    Ok(())
1074}
1075
1076/// Create the `_CLIBS` registry table with a `__gc` finalizer that closes all
1077/// loaded C libraries when the Lua state is closed.
1078///
1079fn createclibstable(state: &mut LuaState) -> Result<(), LuaError> {
1080    state.get_subtable_registry(CLIBS)?;
1081    state.create_table(0, 1)?;
1082    // TODO(phase-b): LuaClosure::LightC currently typed fn() -> i32 in lua-types; use push_c_function until widened.
1083    state.push_c_function(gctm)?;
1084    state.set_field(-2, b"__gc")?;
1085    state.set_metatable(-2)?;
1086    Ok(())
1087}
1088
1089// ── Lua 5.1 `module` / `package.seeall` (deprecated module system) ────────────
1090//
1091// These ship only in the default lua5.1.5 build (`loadlib.c`) and were removed
1092// in 5.2. Registered under the V51 backend; see
1093// specs/followup/5.1-roster-syntax.md §1. They lean on the 5.1 fenv globals
1094// model: `module` sets its caller's environment to the module table (via
1095// `crate::base::set_func_env_at_level`), and `package.seeall` points a module
1096// table's `__index` at `_G`.
1097
1098/// `package.seeall(module)` — make a module table inherit globals.
1099///
1100/// Sets (creating if absent) `module`'s metatable `__index` to the global
1101/// table. Mirrors `ll_seeall` in 5.1 `loadlib.c`. Verified against lua5.1.5.
1102fn ll_seeall(state: &mut LuaState) -> Result<usize, LuaError> {
1103    state.check_arg_type(1, LuaType::Table)?;
1104    if !state.get_metatable(1)? {
1105        state.create_table(0, 1)?;
1106        state.push_value(-1)?;
1107        state.set_metatable(1)?;
1108    }
1109    state.push_globals()?;
1110    state.set_field(-2, b"__index")?;
1111    Ok(0)
1112}
1113
1114/// Walk a dotted module name from a table on the stack, creating intermediate
1115/// tables as needed, leaving the final (sub)table on the stack top. A faithful
1116/// reduction of `luaL_findtable(L, idx, name, 1)`; returns `Err` on a name
1117/// conflict (an intermediate path component is a non-table, non-nil value).
1118fn findtable(state: &mut LuaState, table_idx: i32, name: &[u8]) -> Result<(), LuaError> {
1119    // Start from a copy of the base table on the stack top.
1120    state.push_value_at(table_idx)?;
1121    for part in name.split(|&b| b == b'.') {
1122        // Stack top holds the current table; fetch current[part].
1123        let ty = state.get_field(-1, part)?;
1124        if ty == LuaType::Nil {
1125            state.pop_n(1); // remove nil
1126            state.create_table(0, 1)?; // new subtable
1127            state.push_value(-1)?; // duplicate it
1128            state.set_field(-3, part)?; // current[part] = subtable
1129                                        // Stack: ..., current, subtable. Remove the parent, keep subtable.
1130            state.remove(-2)?;
1131        } else if ty == LuaType::Table {
1132            // Stack: ..., current, value. Remove the parent, keep value.
1133            state.remove(-2)?;
1134        } else {
1135            return Err(LuaError::runtime(format_args!(
1136                "name conflict for module '{}'",
1137                String::from_utf8_lossy(name)
1138            )));
1139        }
1140    }
1141    Ok(())
1142}
1143
1144/// `module(name [, ...])` — Lua 5.1 only.
1145///
1146/// Creates (or reuses) a module table named `name`, registers it in
1147/// `package.loaded`, initializes its `_NAME`/`_M`/`_PACKAGE` fields, applies any
1148/// option functions (e.g. `package.seeall`), and sets the calling chunk's
1149/// environment to the module table. Mirrors `ll_module` in 5.1 `loadlib.c`.
1150fn ll_module(state: &mut LuaState) -> Result<usize, LuaError> {
1151    let modname: Vec<u8> = state.check_arg_string(1)?;
1152    let n_opts = state.top() as i32;
1153
1154    // Fetch _LOADED[modname]; create the module table if absent.
1155    state.get_field_registry(b"_LOADED")?;
1156    let loaded_idx = state.top() as i32;
1157    state.get_field(loaded_idx, &modname)?;
1158    if state.type_at(-1) != LuaType::Table {
1159        state.pop_n(1); // remove non-table result
1160                        // Find/create a global table named `modname` (supporting dotted names).
1161        state.push_globals()?;
1162        let g_idx = state.top() as i32;
1163        findtable(state, g_idx, &modname)?;
1164        state.remove(g_idx)?; // drop the globals table copy, keep the module table
1165        state.push_value(-1)?;
1166        state.set_field(loaded_idx, &modname)?; // _LOADED[modname] = module
1167    }
1168
1169    // Initialize the module if it has no `_NAME` yet.
1170    let has_name = state.get_field(-1, b"_NAME")? != LuaType::Nil;
1171    state.pop_n(1);
1172    if !has_name {
1173        // module._M = module
1174        state.push_value(-1)?;
1175        state.set_field(-2, b"_M")?;
1176        // module._NAME = modname
1177        state.push_string(&modname)?;
1178        state.set_field(-2, b"_NAME")?;
1179        // module._PACKAGE = full name minus the last dotted component.
1180        let pkg: &[u8] = match modname.iter().rposition(|&b| b == b'.') {
1181            Some(dot) => &modname[..=dot],
1182            None => b"",
1183        };
1184        state.push_string(pkg)?;
1185        state.set_field(-2, b"_PACKAGE")?;
1186    }
1187
1188    // Set the caller's environment to the module table (the running closure that
1189    // invoked `module`, i.e. level 1 relative to this C function).
1190    let module_tbl = state.value_at(-1);
1191    crate::base::set_func_env_at_level(state, 1, module_tbl)?;
1192
1193    // Apply option functions: for each extra arg, call `option(module)`.
1194    let mut i = 2;
1195    while i <= n_opts {
1196        state.push_value_at(i)?; // option function
1197        state.push_value(-2)?; // module table
1198        state.call(1, 0)?;
1199        i += 1;
1200    }
1201    Ok(0)
1202}
1203
1204/// Open the `package` library and return the `package` table.
1205///
1206pub fn luaopen_package(state: &mut LuaState) -> Result<usize, LuaError> {
1207    createclibstable(state)?;
1208
1209    // PORT NOTE: The C pk_funcs table also contains placeholder entries for
1210    // "preload", "cpath", "path", "searchers", "loaded" (all NULL). In Rust
1211    // those fields are set explicitly below; only the real functions are here.
1212    state.new_lib(&[
1213        (
1214            b"loadlib" as &[u8],
1215            ll_loadlib as fn(&mut LuaState) -> Result<usize, LuaError>,
1216        ),
1217        (
1218            b"searchpath",
1219            ll_searchpath as fn(&mut LuaState) -> Result<usize, LuaError>,
1220        ),
1221    ])?;
1222
1223    createsearcherstable(state)?;
1224
1225    setpath(state, b"path", LUA_PATH_VAR, LUA_PATH_DEFAULT)?;
1226
1227    setpath(state, b"cpath", LUA_CPATH_VAR, LUA_CPATH_DEFAULT)?;
1228
1229    //                       LUA_EXEC_DIR "\n" LUA_IGMARK "\n");
1230    // The config string encodes platform separator characters, one per line.
1231    let mut config: Vec<u8> = Vec::new();
1232    config.push(LUA_DIRSEP);
1233    config.push(b'\n');
1234    config.push(LUA_PATH_SEP);
1235    config.push(b'\n');
1236    config.push(LUA_PATH_MARK);
1237    config.push(b'\n');
1238    config.push(b'!'); // LUA_EXEC_DIR
1239    config.push(b'\n');
1240    config.push(LUA_IGMARK);
1241    config.push(b'\n');
1242    let config_s = state.intern_str(&config)?;
1243    state.push(LuaValue::Str(config_s));
1244
1245    state.set_field(-2, b"config")?;
1246
1247    state.get_subtable_registry(b"_LOADED")?;
1248    state.set_field(-2, b"loaded")?;
1249
1250    state.get_subtable_registry(b"_PRELOAD")?;
1251    state.set_field(-2, b"preload")?;
1252
1253    state.push_globals()?;
1254    state.push_value(-2)?;
1255    state.set_funcs_with_upvalues(
1256        &[(
1257            b"require" as &[u8],
1258            ll_require as fn(&mut LuaState) -> Result<usize, LuaError>,
1259        )],
1260        1,
1261    )?;
1262    state.pop_n(1);
1263
1264    // The deprecated module system: `package.seeall` (a field on the package
1265    // table) and the `module` global. Present in 5.1 and kept in 5.2.4 via the
1266    // default-on `LUA_COMPAT_MODULE`; fully removed in 5.3. Verified against
1267    // lua5.1.5 and lua5.2.4. See specs/followup/5.1-roster-syntax.md §1.
1268    if matches!(
1269        state.global().lua_version,
1270        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1271    ) {
1272        // The package table is on top of the stack here.
1273        state.push_c_function(ll_seeall)?;
1274        state.set_field(-2, b"seeall")?;
1275        // `module` is a *global*, not a `package` field.
1276        state.push_c_function(ll_module)?;
1277        state.set_global(b"module")?;
1278    }
1279
1280    Ok(1)
1281}
1282
1283// ──────────────────────────────────────────────────────────────────────────────
1284// PORT STATUS
1285//   source:        src/loadlib.c  (758 lines, 25 functions)
1286//   target_crate:  lua-stdlib
1287//   confidence:    medium
1288//   todos:         8
1289//   port_notes:    7
1290//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
1291//   notes:         lsys_load/lsys_sym/lsys_unloadlib now dispatch through
1292//                  dynlib_*_hook on GlobalState (Phase D-3.5); lua-cli
1293//                  installs a libloading-backed backend. With no hook
1294//                  installed, LIB_FAIL is "absent" (matches the C fallback
1295//                  stub); with a hook installed it is "open". Stock Lua C
1296//                  ABI symbols resolve but fail with "init" + a clear
1297//                  unsupported-ABI message (DynamicSymbol::LuaCAbi case);
1298//                  full C-ABI compatibility is a separate project. readable()
1299//                  and searcher_lua are wired through file_loader_hook on
1300//                  GlobalState. Stack-index arithmetic in findloader /
1301//                  ll_require should be verified in Phase B. LUA_PATH_DEFAULT
1302//                  / LUA_CPATH_DEFAULT are hardcoded and must be replaced
1303//                  with platform configuration constants.
1304// ──────────────────────────────────────────────────────────────────────────────