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