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