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