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