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/// Build the `package.config` string for `version`.
83///
84/// Five lines encoding the platform separators: directory separator, path
85/// separator, the `?` substitution mark, the `!` exec-dir mark, and the `-`
86/// ignore mark. The trailing newline after the ignore mark is a **5.2 addition**
87/// (`LUA_IGMARK "\n"` in 5.2+ `loadlib.c`); 5.1's string ends at `-`, so 5.1 is
88/// 9 bytes and 5.2+ are 10 (pinned in `tests/loadlib_strengthen.rs`).
89fn package_config(version: lua_types::LuaVersion) -> Vec<u8> {
90 let mut config = vec![
91 LUA_DIRSEP,
92 b'\n',
93 LUA_PATH_SEP,
94 b'\n',
95 LUA_PATH_MARK,
96 b'\n',
97 b'!',
98 b'\n',
99 LUA_IGMARK,
100 ];
101 if !matches!(version, lua_types::LuaVersion::V51) {
102 config.push(b'\n');
103 }
104 config
105}
106
107// ── Version-derived package-path defaults (issue #273) ───────────────────────
108//
109// `GlobalState::lua_version` is the single source of truth for every byte
110// below; nothing here is a per-lookup computation — `luaopen_package` (the
111// library-init cold path, run once per `Lua` instance) is the only caller.
112//
113// Every default was captured directly from the unmodified upstream `make
114// macosx` build of each version (`specs/oracle/CONTRACT.md`,
115// `/tmp/lua-refs/bin/lua5.x`), not read off of `luaconf.h` — see
116// `tests/loadlib_strengthen.rs` for the pinned assertions and this crate's
117// PR for the per-version diff transcript.
118
119/// The version-directory segment baked into default package paths and into
120/// versioned environment-variable names (e.g. `5.4` in
121/// `/usr/local/share/lua/5.4/?.lua` and in `LUA_PATH_5_4`). Mirrors upstream's
122/// `LUA_VDIR` (`luaconf.h`).
123///
124/// The trailing wildcard arm is unreachable in practice: every public
125/// constructor (`Lua::with_hooks_versioned` and friends) refuses to build a
126/// `LuaVersion` for which [`lua_types::LuaVersion::is_supported`] is false,
127/// and today's five supported variants are all matched above it. It exists
128/// only because `LuaVersion` is `#[non_exhaustive]` from this crate's point
129/// of view.
130fn lua_vdir(version: lua_types::LuaVersion) -> &'static [u8] {
131 match version {
132 lua_types::LuaVersion::V51 => b"5.1",
133 lua_types::LuaVersion::V52 => b"5.2",
134 lua_types::LuaVersion::V53 => b"5.3",
135 lua_types::LuaVersion::V54 => b"5.4",
136 lua_types::LuaVersion::V55 => b"5.5",
137 _ => b"5.4",
138 }
139}
140
141/// The `LUA_VERSUFFIX` value for `version` (e.g. `_5_4`), appended to
142/// `LUA_PATH`/`LUA_CPATH` to build the versioned environment-variable name an
143/// instance consults first (`LUA_PATH_5_4`, `LUA_CPATH_5_3`, ...). See
144/// [`lua_vdir`] for the wildcard-arm note.
145fn lua_versuffix(version: lua_types::LuaVersion) -> &'static [u8] {
146 match version {
147 lua_types::LuaVersion::V51 => b"_5_1",
148 lua_types::LuaVersion::V52 => b"_5_2",
149 lua_types::LuaVersion::V53 => b"_5_3",
150 lua_types::LuaVersion::V54 => b"_5_4",
151 lua_types::LuaVersion::V55 => b"_5_5",
152 _ => b"_5_4",
153 }
154}
155
156/// Whether `version` consults a versioned environment variable
157/// (`LUA_PATH_5_x`/`LUA_CPATH_5_x`) at all before falling back to the
158/// unversioned `LUA_PATH`/`LUA_CPATH`. Versioned env vars are a **5.2+**
159/// addition; 5.1 only ever reads the unversioned name — verified against
160/// `lua5.1.5`, where setting `LUA_PATH_5_1` has no effect on `package.path`.
161fn has_versioned_env_vars(version: lua_types::LuaVersion) -> bool {
162 !matches!(version, lua_types::LuaVersion::V51)
163}
164
165/// `LUA_LDIR` for `version`: where installed pure-Lua modules live
166/// (`/usr/local/share/lua/<vdir>/`).
167fn lua_ldir(version: lua_types::LuaVersion) -> Vec<u8> {
168 let mut dir = b"/usr/local/share/lua/".to_vec();
169 dir.extend_from_slice(lua_vdir(version));
170 dir.push(b'/');
171 dir
172}
173
174/// `LUA_CDIR` for `version`: where installed C modules live
175/// (`/usr/local/lib/lua/<vdir>/`).
176fn lua_cdir(version: lua_types::LuaVersion) -> Vec<u8> {
177 let mut dir = b"/usr/local/lib/lua/".to_vec();
178 dir.extend_from_slice(lua_vdir(version));
179 dir.push(b'/');
180 dir
181}
182
183/// The compiled-in `package.path` default for `version`. The entry SHAPE, not
184/// just the version segment, differs by era:
185/// - **5.1**: `./?.lua` FIRST, then `LDIR`/`CDIR`, no trailing `./?/init.lua`.
186/// - **5.2**: `LDIR`/`CDIR` first, `./?.lua` LAST, no `./?/init.lua` at all.
187/// - **5.3/5.4/5.5**: `LDIR`/`CDIR` first, then BOTH `./?.lua` and
188/// `./?/init.lua` last (the `LUA_SHRDIR`-derived shape 5.3 introduced).
189#[cfg(not(target_os = "windows"))]
190fn lua_path_default(version: lua_types::LuaVersion) -> Vec<u8> {
191 let ldir = lua_ldir(version);
192 let cdir = lua_cdir(version);
193 let mut path = Vec::new();
194 match version {
195 lua_types::LuaVersion::V51 => {
196 path.extend_from_slice(b"./?.lua;");
197 path.extend_from_slice(&ldir);
198 path.extend_from_slice(b"?.lua;");
199 path.extend_from_slice(&ldir);
200 path.extend_from_slice(b"?/init.lua;");
201 path.extend_from_slice(&cdir);
202 path.extend_from_slice(b"?.lua;");
203 path.extend_from_slice(&cdir);
204 path.extend_from_slice(b"?/init.lua");
205 }
206 lua_types::LuaVersion::V52 => {
207 path.extend_from_slice(&ldir);
208 path.extend_from_slice(b"?.lua;");
209 path.extend_from_slice(&ldir);
210 path.extend_from_slice(b"?/init.lua;");
211 path.extend_from_slice(&cdir);
212 path.extend_from_slice(b"?.lua;");
213 path.extend_from_slice(&cdir);
214 path.extend_from_slice(b"?/init.lua;./?.lua");
215 }
216 _ => {
217 path.extend_from_slice(&ldir);
218 path.extend_from_slice(b"?.lua;");
219 path.extend_from_slice(&ldir);
220 path.extend_from_slice(b"?/init.lua;");
221 path.extend_from_slice(&cdir);
222 path.extend_from_slice(b"?.lua;");
223 path.extend_from_slice(&cdir);
224 path.extend_from_slice(b"?/init.lua;./?.lua;./?/init.lua");
225 }
226 }
227 path
228}
229
230/// The Windows fallback default is version-flat, unchanged by this fix: the
231/// `!`-relative (`LUA_EXEC_DIR`) substitution real Windows builds perform via
232/// `setprogdir` is not implemented on this platform (see `setpath`'s doc
233/// comment on the same gap), so there is no faithful versioned
234/// installed-module directory to build here either.
235#[cfg(target_os = "windows")]
236fn lua_path_default(_version: lua_types::LuaVersion) -> Vec<u8> {
237 b"./?.lua;./?/init.lua".to_vec()
238}
239
240/// The compiled-in `package.cpath` default for `version` — same era split as
241/// [`lua_path_default`]: 5.1 puts `./?.so` FIRST with no `./` alternative
242/// after `CDIR`'s entries; 5.2+ share one shape (`CDIR` entries, then
243/// `./?.so` last).
244#[cfg(not(target_os = "windows"))]
245fn lua_cpath_default(version: lua_types::LuaVersion) -> Vec<u8> {
246 let cdir = lua_cdir(version);
247 let mut path = Vec::new();
248 match version {
249 lua_types::LuaVersion::V51 => {
250 path.extend_from_slice(b"./?.so;");
251 path.extend_from_slice(&cdir);
252 path.extend_from_slice(b"?.so;");
253 path.extend_from_slice(&cdir);
254 path.extend_from_slice(b"loadall.so");
255 }
256 _ => {
257 path.extend_from_slice(&cdir);
258 path.extend_from_slice(b"?.so;");
259 path.extend_from_slice(&cdir);
260 path.extend_from_slice(b"loadall.so;./?.so");
261 }
262 }
263 path
264}
265
266/// See [`lua_path_default`]'s Windows note — unchanged, version-flat.
267#[cfg(target_os = "windows")]
268fn lua_cpath_default(_version: lua_types::LuaVersion) -> Vec<u8> {
269 b"./?.dll".to_vec()
270}
271
272fn getenv_bytes(state: &LuaState, name: &[u8]) -> Option<Vec<u8>> {
273 if let Some(env_fn) = state.global().env_hook {
274 return env_fn(name);
275 }
276
277 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
278 {
279 None
280 }
281
282 #[cfg(all(unix, not(all(target_arch = "wasm32", target_os = "unknown"))))]
283 {
284 use std::ffi::OsStr;
285 use std::os::unix::ffi::{OsStrExt, OsStringExt};
286
287 let os_name = OsStr::from_bytes(name);
288 std::env::var_os(os_name).map(|v| v.into_vec())
289 }
290
291 #[cfg(all(not(unix), not(all(target_arch = "wasm32", target_os = "unknown"))))]
292 {
293 std::str::from_utf8(name)
294 .ok()
295 .and_then(|name_str| std::env::var(name_str).ok())
296 .map(|s| s.into_bytes())
297 }
298}
299
300// ── Opaque library handle ─────────────────────────────────────────────────────
301//
302//
303// In this port, the library identity is the opaque `DynLibId(u64)` allocated
304// by the embedder-installed [`DynLibLoadHook`]. `lua-stdlib` never inspects
305// the value; it stashes the raw `u64` in `_CLIBS` as light userdata (cast
306// through `*mut c_void` to match C-Lua's representation) and hands it back to
307// the symbol and unload hooks.
308
309// ── Byte-string utilities ─────────────────────────────────────────────────────
310
311/// Append to `buf` the bytes of `s` with all non-overlapping occurrences of
312/// `pattern` replaced by `replacement`.
313///
314fn gsub_append(buf: &mut Vec<u8>, s: &[u8], pattern: &[u8], replacement: &[u8]) {
315 if pattern.is_empty() {
316 buf.extend_from_slice(s);
317 return;
318 }
319 let mut pos = 0;
320 while pos < s.len() {
321 if s[pos..].starts_with(pattern) {
322 buf.extend_from_slice(replacement);
323 pos += pattern.len();
324 } else {
325 buf.push(s[pos]);
326 pos += 1;
327 }
328 }
329}
330
331/// Return a new `Vec<u8>` with all non-overlapping occurrences of `pattern`
332/// in `s` replaced by `replacement`.
333fn gsub_bytes(s: &[u8], pattern: &[u8], replacement: &[u8]) -> Vec<u8> {
334 let mut out = Vec::new();
335 gsub_append(&mut out, s, pattern, replacement);
336 out
337}
338
339/// Find the byte offset of `needle` in `haystack`, or `None`.
340fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
341 if needle.is_empty() {
342 return Some(0);
343 }
344 haystack.windows(needle.len()).position(|w| w == needle)
345}
346
347// ── Platform-specific dynamic-loading dispatch ────────────────────────────────
348
349/// Unload a previously loaded C library.
350///
351/// — POSIX: `dlclose(lib)`; Windows: `FreeLibrary(lib)`.
352///
353/// Delegates to [`GlobalState::dynlib_unload_hook`]. When no hook is
354/// registered the library is leaked, which matches `libloading`'s safety
355/// model (the library must outlive every symbol it exports, and the simplest
356/// correct policy is to keep it alive for the state's lifetime).
357fn lsys_unloadlib(state: &mut LuaState, lib: DynLibId) {
358 if let Some(hook) = state.global().dynlib_unload_hook {
359 hook(lib);
360 }
361}
362
363/// Load a C library from `path`. If `see_glb` is true, make symbols globally
364/// visible (POSIX RTLD_GLOBAL). On failure, pushes an error string onto `state`.
365///
366/// — POSIX: `dlopen(path, RTLD_NOW | (seeglb ? RTLD_GLOBAL : RTLD_LOCAL))`
367/// — Windows: `LoadLibraryExA(path, NULL, LUA_LLE_FLAGS)`
368///
369/// Returns `(handle, lib_fail_tag)`. The tag is `"absent"` when no hook is
370/// registered (matching C's fallback-stub `LIB_FAIL`) and `"open"` when the
371/// hook itself reports a failure (matching POSIX/Windows builds).
372fn lsys_load(
373 state: &mut LuaState,
374 path: &[u8],
375 see_glb: bool,
376) -> (Option<DynLibId>, &'static [u8]) {
377 let hook = state.global().dynlib_load_hook;
378 let Some(load_fn) = hook else {
379 let s = match state.intern_str(DLMSG) {
380 Ok(s) => s,
381 Err(_) => return (None, LIB_FAIL_ABSENT),
382 };
383 state.push(LuaValue::Str(s));
384 return (None, LIB_FAIL_ABSENT);
385 };
386 match load_fn(state, path, see_glb) {
387 Ok(id) => (Some(id), b"open"),
388 // `LuaError::File` is reserved for "no shared library at this path":
389 // map it to the fallback-stub `"absent"` tag so a probe like
390 // `package.loadlib("./nonexistent.so", ...)` reports `"absent"`
391 // regardless of whether a backend is installed. Every other `Err` is a
392 // true open-time failure → `"open"`.
393 Err(LuaError::File) => {
394 let mut msg = b"cannot find library '".to_vec();
395 msg.extend_from_slice(path);
396 msg.push(b'\'');
397 let s = match state.intern_str(&msg) {
398 Ok(s) => s,
399 Err(_) => return (None, LIB_FAIL_ABSENT),
400 };
401 state.push(LuaValue::Str(s));
402 (None, LIB_FAIL_ABSENT)
403 }
404 Err(err) => {
405 let msg = error_to_bytes(&err);
406 let s = match state.intern_str(&msg) {
407 Ok(s) => s,
408 Err(_) => return (None, b"open"),
409 };
410 state.push(LuaValue::Str(s));
411 (None, b"open")
412 }
413 }
414}
415
416/// Find symbol `sym` in library `lib` and either push it as a callable Lua
417/// function (returning `SymOutcome::Found`) or push an error message string
418/// and report which failure category the caller should propagate.
419///
420/// — POSIX: `cast_func(dlsym(lib, sym))`
421/// — Windows: `(lua_CFunction)(voidf)GetProcAddress(lib, sym)`
422fn lsys_sym(state: &mut LuaState, lib: DynLibId, sym: &[u8]) -> SymOutcome {
423 let hook = state.global().dynlib_symbol_hook;
424 let Some(sym_fn) = hook else {
425 let s = match state.intern_str(DLMSG) {
426 Ok(s) => s,
427 Err(_) => return SymOutcome::Missing,
428 };
429 state.push(LuaValue::Str(s));
430 return SymOutcome::Missing;
431 };
432 match sym_fn(state, lib, sym) {
433 Ok(DynamicSymbol::RustNative(f)) => SymOutcome::Found(f),
434 Ok(DynamicSymbol::LuaCAbi(_)) => {
435 let s = match state.intern_str(C_ABI_UNSUPPORTED_MSG) {
436 Ok(s) => s,
437 Err(_) => return SymOutcome::Missing,
438 };
439 state.push(LuaValue::Str(s));
440 SymOutcome::Missing
441 }
442 Ok(DynamicSymbol::Unsupported { reason }) => {
443 let s = match state.intern_str(&reason) {
444 Ok(s) => s,
445 Err(_) => return SymOutcome::Missing,
446 };
447 state.push(LuaValue::Str(s));
448 SymOutcome::Missing
449 }
450 Err(err) => {
451 let msg = error_to_bytes(&err);
452 let s = match state.intern_str(&msg) {
453 Ok(s) => s,
454 Err(_) => return SymOutcome::Missing,
455 };
456 state.push(LuaValue::Str(s));
457 SymOutcome::Missing
458 }
459 }
460}
461
462/// Outcome of `lsys_sym`.
463///
464/// `Missing` covers every non-success path (unknown symbol, ABI mismatch, hook
465/// absent, embedder-supplied refusal); in every case an error-message string
466/// has already been pushed onto the Lua stack, so the caller maps `Missing`
467/// to `ERRFUNC` / `"init"` without further work.
468enum SymOutcome {
469 /// Resolved to a Rust-native callable.
470 Found(lua_CFunction),
471 /// Resolution failed; an error-message string is on the stack.
472 Missing,
473}
474
475/// Extract a byte-string error message from a `LuaError`, falling back to a
476/// debug rendering for non-string variants.
477fn error_to_bytes(e: &LuaError) -> Vec<u8> {
478 match e.message_bytes() {
479 Some(b) => b.to_vec(),
480 None => format!("{:?}", e).into_bytes(),
481 }
482}
483
484/// Encode a [`DynLibId`] as a `*mut c_void` for storage in `_CLIBS` as light
485/// userdata. The cast is the inverse of [`decode_dynlib_id`]; neither side
486/// ever dereferences the pointer.
487fn encode_dynlib_id(id: DynLibId) -> *mut std::ffi::c_void {
488 id.0 as usize as *mut std::ffi::c_void
489}
490
491/// Decode a [`DynLibId`] previously stored via [`encode_dynlib_id`].
492fn decode_dynlib_id(p: *mut std::ffi::c_void) -> DynLibId {
493 DynLibId(p as usize as u64)
494}
495
496// ── Path helpers ──────────────────────────────────────────────────────────────
497
498/// Return `registry["LUA_NOENV"]` as a boolean.
499///
500fn noenv(state: &mut LuaState) -> bool {
501 let _ = state.get_field_registry(b"LUA_NOENV");
502 let b = state.to_boolean(-1);
503 state.pop_n(1);
504 b
505}
506
507/// Set `package[fieldname]` to the appropriate path value.
508///
509/// Priority: versioned env var (e.g. `LUA_PATH_5_4`, only consulted when
510/// [`has_versioned_env_vars`] is true for this instance's version — 5.1 has
511/// no versioned env vars at all) → unversioned env var (`LUA_PATH`) →
512/// compiled-in default. When the env var contains `;;`, the compiled-in
513/// default is spliced in place of `;;`. The caller must leave the `package`
514/// table at the stack top; the path value is set on it directly (the
515/// versioned env-var name is computed off-stack, so no index bookkeeping is
516/// needed).
517fn setpath(
518 state: &mut LuaState,
519 fieldname: &[u8],
520 envname: &[u8],
521 dft: &[u8],
522) -> Result<(), LuaError> {
523 let version = state.global().lua_version;
524
525 let path_opt = if noenv(state) {
526 None
527 } else if has_versioned_env_vars(version) {
528 let mut nver = envname.to_vec();
529 nver.extend_from_slice(lua_versuffix(version));
530 getenv_bytes(state, &nver).or_else(|| getenv_bytes(state, envname))
531 } else {
532 getenv_bytes(state, envname)
533 };
534
535 let final_path: Vec<u8> = match path_opt {
536 None => dft.to_vec(),
537 Some(path) if double_semicolon_splice_is_legacy(version) => {
538 legacy_double_semicolon_splice(&path, dft)
539 }
540 Some(path) => modern_double_semicolon_splice(&path, dft),
541 };
542
543 // The Windows `setprogdir` step (replace `LUA_EXEC_DIR` with the running
544 // executable's directory via `GetModuleFileNameA`, a Win32/`unsafe` call) is
545 // a no-op on every other platform and is not yet implemented here, so the
546 // `LUA_EXEC_DIR` substitution is skipped.
547 let s = state.intern_str(&final_path)?;
548 state.push(LuaValue::Str(s));
549 state.set_field(-2, fieldname)?;
550
551 Ok(())
552}
553
554/// Whether `version`'s `;;`-in-env-var default splice is the LEGACY,
555/// position-independent `gsub` (5.1/5.2/5.3's `luaL_gsub(path, ";;",
556/// ";AUXMARK;")` then `luaL_gsub(_, AUXMARK, dft)`: EVERY non-overlapping
557/// `;;` occurrence gets the default spliced in, wrapped in separators on
558/// both sides regardless of position) rather than the single-shot,
559/// position-aware splice **5.4** introduced (only the FIRST `;;` is
560/// replaced, and a boundary separator is omitted when `;;` sits at the very
561/// start or end of the string). Verified against `lua5.1.5`/`lua5.4.7`:
562/// `LUA_PATH="/a?;;;;/b"` splices the default TWICE on 5.1 (once per `;;`
563/// pair) but only once on 5.4, leaving the remaining `;;` literal.
564fn double_semicolon_splice_is_legacy(version: lua_types::LuaVersion) -> bool {
565 matches!(
566 version,
567 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
568 )
569}
570
571/// The 5.1/5.2/5.3 `;;` default splice: every non-overlapping occurrence of
572/// `;;` in `path` becomes `;` + `dft` + `;`, unconditionally. Collapses
573/// upstream's two-step `gsub(path, ";;", ";AUXMARK;")` then
574/// `gsub(_, AUXMARK, dft)` into one pass — the `AUXMARK` indirection exists
575/// upstream only to avoid re-scanning `dft` for the first gsub's pattern, and
576/// a single combined replacement already has that property (`dft` is
577/// inserted verbatim into the output, never re-scanned for `;;`).
578fn legacy_double_semicolon_splice(path: &[u8], dft: &[u8]) -> Vec<u8> {
579 let double_sep = [LUA_PATH_SEP, LUA_PATH_SEP];
580 if find_subslice(path, &double_sep).is_none() {
581 return path.to_vec();
582 }
583 let mut replacement = vec![LUA_PATH_SEP];
584 replacement.extend_from_slice(dft);
585 replacement.push(LUA_PATH_SEP);
586 gsub_bytes(path, &double_sep, &replacement)
587}
588
589/// The 5.4/5.5 `;;` default splice: only the FIRST occurrence of `;;` is
590/// replaced; the leading separator is omitted when `;;` starts the string
591/// (no prefix) and the trailing separator is omitted when `;;` ends it (no
592/// suffix).
593fn modern_double_semicolon_splice(path: &[u8], dft: &[u8]) -> Vec<u8> {
594 let double_sep = [LUA_PATH_SEP, LUA_PATH_SEP];
595 let Some(dftmark_pos) = find_subslice(path, &double_sep) else {
596 return path.to_vec();
597 };
598 let mut buf = Vec::new();
599 if dftmark_pos > 0 {
600 buf.extend_from_slice(&path[..dftmark_pos]);
601 buf.push(LUA_PATH_SEP);
602 }
603 buf.extend_from_slice(dft);
604 let after = dftmark_pos + 2;
605 if after < path.len() {
606 buf.push(LUA_PATH_SEP);
607 buf.extend_from_slice(&path[after..]);
608 }
609 buf
610}
611
612// ── CLIBS registry table ──────────────────────────────────────────────────────
613
614/// Return the library handle stored at `registry._CLIBS[path]`, or `None`.
615///
616fn checkclib(state: &mut LuaState, path: &[u8]) -> Option<DynLibId> {
617 let _ = state.get_field_registry(CLIBS);
618 let _ = state.get_field(-1, path);
619 let handle = state.to_light_userdata(-1).map(decode_dynlib_id);
620 state.pop_n(2);
621 handle
622}
623
624/// Register a library handle in the CLIBS table (both by path and sequentially).
625///
626fn addtoclib(state: &mut LuaState, path: &[u8], plib: DynLibId) -> Result<(), LuaError> {
627 state.get_field_registry(CLIBS)?;
628 state.push(LuaValue::LightUserData(encode_dynlib_id(plib)));
629 state.push_value(-1)?;
630 state.set_field(-3, path)?;
631 let n = state.len_at(-2);
632 state.raw_seti(-2, n + 1)?;
633 state.pop_n(1);
634 Ok(())
635}
636
637/// `__gc` metamethod for the CLIBS table: unloads all registered C libraries
638/// in reverse order when the Lua state closes.
639///
640fn gctm(state: &mut LuaState) -> Result<usize, LuaError> {
641 let n = state.len_at(1);
642 let mut i = n;
643 while i >= 1 {
644 state.raw_geti(1, i)?;
645 if let Some(handle) = state.to_light_userdata(-1).map(decode_dynlib_id) {
646 lsys_unloadlib(state, handle);
647 }
648 state.pop_n(1);
649 i -= 1;
650 }
651 Ok(0)
652}
653
654// ── Dynamic function lookup ───────────────────────────────────────────────────
655
656/// Outcome of looking for a C function in a dynamically loaded library.
657///
658/// On success the function (or `true` for the `*` sentinel) is on the stack;
659/// on a non-fatal failure an error-message string is on the stack and the
660/// variant tells the caller what to report. Fatal errors propagate via `Err`.
661/// `Ok` is C's success; `ErrLib(tag)` is C's `ERRLIB` carrying the `LIB_FAIL`
662/// string (`"open"` for a true dlopen failure, `"absent"` when no backend is
663/// installed or the file does not exist); `ErrFunc` is C's `ERRFUNC` (the
664/// library opened but the symbol was not found).
665enum LookForFuncStatus {
666 /// Loader successfully resolved a symbol (function pushed on stack).
667 Ok,
668 /// Library could not be opened. `tag` is the `LIB_FAIL` string.
669 ErrLib(&'static [u8]),
670 /// Library opened but symbol could not be resolved.
671 ErrFunc,
672}
673
674fn lookforfunc(
675 state: &mut LuaState,
676 path: &[u8],
677 sym: &[u8],
678) -> Result<LookForFuncStatus, LuaError> {
679 let reg = match checkclib(state, path) {
680 Some(handle) => handle,
681 None => {
682 let (loaded, tag) = lsys_load(state, path, sym.first() == Some(&b'*'));
683 match loaded {
684 Some(handle) => {
685 addtoclib(state, path, handle)?;
686 handle
687 }
688 None => return Ok(LookForFuncStatus::ErrLib(tag)),
689 }
690 }
691 };
692 if sym.first() == Some(&b'*') {
693 state.push(LuaValue::Bool(true));
694 return Ok(LookForFuncStatus::Ok);
695 }
696 match lsys_sym(state, reg, sym) {
697 SymOutcome::Found(func) => {
698 state.push_c_function(func)?;
699 Ok(LookForFuncStatus::Ok)
700 }
701 SymOutcome::Missing => Ok(LookForFuncStatus::ErrFunc),
702 }
703}
704
705// ── Lua-callable package functions ────────────────────────────────────────────
706
707/// `package.loadlib(filename, funcname)` — open a C library and return a
708/// Lua-callable wrapper for `funcname`.
709///
710/// Returns: on success, the loader function (1 value).
711/// On error: `false`, error-message string, and `"open"` or `"init"` (3 values).
712///
713pub fn ll_loadlib(state: &mut LuaState) -> Result<usize, LuaError> {
714 let path = state.check_arg_string(1)?.to_vec();
715 let init = state.check_arg_string(2)?.to_vec();
716 let stat = lookforfunc(state, &path, &init)?;
717 let where_bytes: &[u8] = match stat {
718 LookForFuncStatus::Ok => return Ok(1),
719 LookForFuncStatus::ErrLib(tag) => tag,
720 LookForFuncStatus::ErrFunc => b"init",
721 };
722 // `luaL_pushfail` is `lua_pushnil` on every version (5.4 included); the fail
723 // value is `nil`, not `false`. The `LIB_FAIL` tag is chosen at run time: the
724 // CLI backend reports `LuaError::File` for a missing library → `"absent"`
725 // (matching C-Lua's no-dlfcn fallback), a true `dlopen` failure → `"open"`,
726 // and the "init" branch (symbol resolution failed after the library opened)
727 // is identical in every build.
728 state.push(LuaValue::Nil);
729 state.insert(-2)?;
730 let where_s = state.intern_str(where_bytes)?;
731 state.push(LuaValue::Str(where_s));
732 Ok(3)
733}
734
735// ── File existence check ──────────────────────────────────────────────────────
736
737/// Whether `filename` can be opened for reading.
738///
739/// `std::fs` is banned in `lua-stdlib`, so the probe is delegated to the
740/// embedder-registered `file_loader_hook` on `GlobalState`. Without a hook
741/// installed, `readable` reports `false` (the file system is unreachable) — so
742/// the in-process searcher tests deterministically see every path as not-found.
743fn readable(state: &LuaState, filename: &[u8]) -> bool {
744 match state.global().file_loader_hook {
745 Some(hook) => hook(filename).is_ok(),
746 None => false,
747 }
748}
749
750// ── Path-component iterator ───────────────────────────────────────────────────
751
752/// Iterator over `;`-separated path-template components, yielding each as an
753/// immutable slice (the C original walked one mutable buffer, swapping each
754/// separator for a NUL and back; this produces the identical sequence).
755struct PathComponents<'a> {
756 remaining: &'a [u8],
757}
758
759impl<'a> PathComponents<'a> {
760 fn new(path: &'a [u8]) -> Self {
761 PathComponents { remaining: path }
762 }
763}
764
765impl<'a> Iterator for PathComponents<'a> {
766 type Item = &'a [u8];
767
768 fn next(&mut self) -> Option<Self::Item> {
769 if self.remaining.is_empty() {
770 return None;
771 }
772 let component = match self.remaining.iter().position(|&b| b == LUA_PATH_SEP) {
773 Some(sep_pos) => {
774 let c = &self.remaining[..sep_pos];
775 self.remaining = &self.remaining[sep_pos + 1..];
776 c
777 }
778 None => {
779 let c = self.remaining;
780 self.remaining = &[];
781 c
782 }
783 };
784 Some(component)
785 }
786}
787
788// ── Error-message helpers ─────────────────────────────────────────────────────
789
790/// Push an error message listing all files in `path` that were not found.
791///
792/// Example output: `"no file 'a.lua'\n\tno file 'b.lua'"`
793///
794fn pusherrornotfound(state: &mut LuaState, path: &[u8]) -> Result<(), LuaError> {
795 let mut buf: Vec<u8> = Vec::new();
796 buf.extend_from_slice(b"no file '");
797 gsub_append(&mut buf, path, &[LUA_PATH_SEP], b"'\n\tno file '");
798 buf.push(b'\'');
799 let s = state.intern_str(&buf)?;
800 state.push(LuaValue::Str(s));
801 Ok(())
802}
803
804// ── Path search ───────────────────────────────────────────────────────────────
805
806/// Search for a readable file matching `name` in the `;`-separated `path`.
807///
808/// `sep` bytes in `name` are first replaced by `dirsep`; then each template's
809/// `?` is replaced by the adjusted name. On the first readable match, pushes the
810/// filename string and returns `Some(filename_bytes)`; otherwise pushes the
811/// not-found message and returns `None`.
812fn searchpath(
813 state: &mut LuaState,
814 name: &[u8],
815 path: &[u8],
816 sep: &[u8],
817 dirsep: &[u8],
818) -> Result<Option<Vec<u8>>, LuaError> {
819 let name_buf: Vec<u8> = if !sep.is_empty() && name.contains(&sep[0]) {
820 gsub_bytes(name, sep, dirsep)
821 } else {
822 name.to_vec()
823 };
824
825 let pathname: Vec<u8> = gsub_bytes(path, &[LUA_PATH_MARK], &name_buf);
826
827 for filename in PathComponents::new(&pathname) {
828 if readable(state, filename) {
829 let s = state.intern_str(filename)?;
830 state.push(LuaValue::Str(s));
831 return Ok(Some(filename.to_vec()));
832 }
833 }
834
835 pusherrornotfound(state, &pathname)?;
836 Ok(None)
837}
838
839/// `package.searchpath(name, path [, sep [, rep]])`.
840///
841/// Returns the first readable file in `path` with `sep` occurrences in `name`
842/// replaced by `rep`. On failure returns `luaL_pushfail` (a `nil`, NOT `false`,
843/// on every version) plus the error message. See [`ll_loadlib`] for the same
844/// `luaL_pushfail` = `lua_pushnil` translation.
845pub fn ll_searchpath(state: &mut LuaState) -> Result<usize, LuaError> {
846 let name = state.check_arg_string(1)?.to_vec();
847 let path = state.check_arg_string(2)?.to_vec();
848 let sep = state.opt_arg_string(3, b".")?;
849 let dirsep_default = [LUA_DIRSEP];
850 let dirsep = state.opt_arg_string(4, &dirsep_default)?;
851
852 let found = searchpath(state, &name, &path, &sep, &dirsep)?;
853 if found.is_some() {
854 return Ok(1);
855 }
856 if searchpath_error_has_leading_separator(state.global().lua_version) {
857 prepend_searchpath_separator(state)?;
858 }
859 state.push(LuaValue::Nil);
860 state.insert(-2)?;
861 Ok(2)
862}
863
864/// Whether the standalone `package.searchpath` error message carries a leading
865/// `\n\t` separator before its first `no file '…'` line.
866///
867/// In 5.2/5.3 the `searchpath` helper builds each entry as `"\n\tno file '%s'"`,
868/// so the first line is prefixed too; 5.4 moved that prefix into `findloader`'s
869/// per-iteration accumulator and made `searchpath`'s own message bare (the form
870/// this port's `pusherrornotfound` produces). The `require` trace is unaffected
871/// either way — there `findloader` supplies the single `\n\t` per searcher — so
872/// the seam is observable ONLY through the standalone Lua function. (5.1 has no
873/// `package.searchpath`.) Pinned in `tests/loadlib_strengthen.rs`.
874fn searchpath_error_has_leading_separator(version: lua_types::LuaVersion) -> bool {
875 matches!(version, lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53)
876}
877
878/// Replace the not-found message on the stack top with one carrying a leading
879/// `\n\t` (the 5.2/5.3 `searchpath` form). The message produced by
880/// `pusherrornotfound` is bare; this restores the legacy prefix.
881fn prepend_searchpath_separator(state: &mut LuaState) -> Result<(), LuaError> {
882 let Some(bare) = state.to_bytes(-1) else {
883 return Ok(());
884 };
885 state.pop_n(1);
886 let mut prefixed = b"\n\t".to_vec();
887 prefixed.extend_from_slice(&bare);
888 let s = state.intern_str(&prefixed)?;
889 state.push(LuaValue::Str(s));
890 Ok(())
891}
892
893/// Find a module file using the path stored in `package[pname]` (e.g.
894/// `package.path` / `package.cpath`), read from upvalue #1 of the searcher
895/// closure. Errors if that field is not a string.
896fn findfile(
897 state: &mut LuaState,
898 name: &[u8],
899 pname: &[u8],
900 dirsep: u8,
901) -> Result<Option<Vec<u8>>, LuaError> {
902 let uv = state.upvalue_index(1);
903 let _ = state.get_field(uv, pname);
904 let path_opt: Option<Vec<u8>> = state.to_bytes(-1);
905 let Some(path) = path_opt else {
906 state.pop_n(1);
907 return Err(LuaError::runtime(format_args!(
908 "'package.{}' must be a string",
909 String::from_utf8_lossy(pname)
910 )));
911 };
912 state.pop_n(1);
913 searchpath(state, name, &path, b".", &[dirsep])
914}
915
916/// Check whether a module load succeeded, returning the open function + filename
917/// (2 values) on success or raising an error on failure.
918///
919fn checkload(state: &mut LuaState, stat: bool, filename: &[u8]) -> Result<usize, LuaError> {
920 if stat {
921 let s = state.intern_str(filename)?;
922 state.push(LuaValue::Str(s));
923 Ok(2)
924 } else {
925 // The error embeds the module name (the `require` arg at stack[1]) and
926 // the loader's own error message (the searcher's pushed string at the
927 // stack top). Both are owned byte copies, so there is no aliasing.
928 let modname = state.to_bytes(1).unwrap_or_else(|| b"?".to_vec());
929 let loader_err = state.to_bytes(-1).unwrap_or_else(|| b"?".to_vec());
930
931 let mut msg = b"error loading module '".to_vec();
932 msg.extend_from_slice(&modname);
933 msg.extend_from_slice(b"' from file '");
934 msg.extend_from_slice(filename);
935 msg.extend_from_slice(b"':\n\t");
936 msg.extend_from_slice(&loader_err);
937
938 let s = state.intern_str(&msg)?;
939 return Err(LuaError::from_value(LuaValue::Str(s)));
940 }
941}
942
943// ── Searcher functions ────────────────────────────────────────────────────────
944
945/// Searcher that looks in `package.path` for a Lua source file.
946///
947/// Returns 1 value (error-message string) if not found, or 2 values (loader
948/// function, filename) if found and loaded successfully.
949///
950fn searcher_lua(state: &mut LuaState) -> Result<usize, LuaError> {
951 let name = state.check_arg_string(1)?.to_vec();
952 let filename = findfile(state, &name, b"path", LUA_LSUBSEP)?;
953 if filename.is_none() {
954 return Ok(1);
955 }
956 let filename = filename.unwrap();
957 // `std::fs` is banned in `lua-stdlib`, so file contents arrive via the
958 // embedder-registered `file_loader_hook` on `GlobalState`; the bytes are then
959 // parsed through `state.load(...)` (which dispatches to the parser hook) and
960 // the resulting closure is left on the stack for `checkload` to pair with the
961 // filename.
962 let chunk = match state.global().file_loader_hook {
963 Some(hook) => hook(&filename),
964 None => Err(LuaError::runtime(format_args!(
965 "no file_loader_hook registered; cannot read '{}'",
966 String::from_utf8_lossy(&filename)
967 ))),
968 };
969 let load_ok = match chunk {
970 Ok(bytes) => {
971 // Use a chunk name of the form `@filename` matching C's luaL_loadfilex.
972 let mut chunkname = b"@".to_vec();
973 chunkname.extend_from_slice(&filename);
974 match state.load(&bytes, &chunkname, None) {
975 Ok(true) => true,
976 Ok(false) => false,
977 Err(e) => {
978 let msg = match e.message_bytes() {
979 Some(b) => b.to_vec(),
980 None => format!("{:?}", &e).into_bytes(),
981 };
982 let s = state.intern_str(&msg)?;
983 state.push(LuaValue::Str(s));
984 false
985 }
986 }
987 }
988 Err(e) => {
989 let msg = match e.message_bytes() {
990 Some(b) => b.to_vec(),
991 None => format!("{:?}", &e).into_bytes(),
992 };
993 let s = state.intern_str(&msg)?;
994 state.push(LuaValue::Str(s));
995 false
996 }
997 };
998 checkload(state, load_ok, &filename)
999}
1000
1001/// Try to load `modname`'s open function from the C dynamic library at `filename`.
1002///
1003/// Handles the "ignore mark" (`-`) convention: `"foo-bar"` first tries
1004/// `luaopen_foo`, then `luaopen_bar` as a fallback.
1005///
1006fn loadfunc(
1007 state: &mut LuaState,
1008 filename: &[u8],
1009 modname: &[u8],
1010) -> Result<LookForFuncStatus, LuaError> {
1011 let modname: Vec<u8> = gsub_bytes(modname, b".", LUA_OFSEP);
1012
1013 if let Some(mark_pos) = modname.iter().position(|&b| b == LUA_IGMARK) {
1014 let prefix = &modname[..mark_pos];
1015 let mut openfunc = LUA_POF.to_vec();
1016 openfunc.extend_from_slice(prefix);
1017 let stat = lookforfunc(state, filename, &openfunc)?;
1018 if !matches!(stat, LookForFuncStatus::ErrFunc) {
1019 return Ok(stat);
1020 }
1021 let tail = &modname[mark_pos + 1..];
1022 let mut openfunc2 = LUA_POF.to_vec();
1023 openfunc2.extend_from_slice(tail);
1024 return lookforfunc(state, filename, &openfunc2);
1025 }
1026
1027 let mut openfunc = LUA_POF.to_vec();
1028 openfunc.extend_from_slice(&modname);
1029 lookforfunc(state, filename, &openfunc)
1030}
1031
1032/// Searcher that looks in `package.cpath` for a C dynamic library.
1033///
1034fn searcher_c(state: &mut LuaState) -> Result<usize, LuaError> {
1035 let name = state.check_arg_string(1)?.to_vec();
1036 let filename = findfile(state, &name, b"cpath", LUA_CSUBSEP)?;
1037 if filename.is_none() {
1038 return Ok(1);
1039 }
1040 let filename = filename.unwrap();
1041 let stat = loadfunc(state, &filename, &name)?;
1042 let ok = matches!(stat, LookForFuncStatus::Ok);
1043 checkload(state, ok, &filename)
1044}
1045
1046/// Searcher that looks in `package.cpath` using only the root component
1047/// (everything before the first `.`) of the module name.
1048///
1049fn searcher_croot(state: &mut LuaState) -> Result<usize, LuaError> {
1050 let name = state.check_arg_string(1)?.to_vec();
1051 let dot_pos = name.iter().position(|&b| b == b'.');
1052 if dot_pos.is_none() {
1053 return Ok(0);
1054 }
1055 let dot_pos = dot_pos.unwrap();
1056
1057 let root = &name[..dot_pos];
1058
1059 let filename = findfile(state, root, b"cpath", LUA_CSUBSEP)?;
1060
1061 if filename.is_none() {
1062 return Ok(1);
1063 }
1064 let filename = filename.unwrap();
1065
1066 let stat = loadfunc(state, &filename, &name)?;
1067 match stat {
1068 LookForFuncStatus::Ok => {}
1069 LookForFuncStatus::ErrFunc => {
1070 let mut msg = b"no module '".to_vec();
1071 msg.extend_from_slice(&name);
1072 msg.extend_from_slice(b"' in file '");
1073 msg.extend_from_slice(&filename);
1074 msg.push(b'\'');
1075 let s = state.intern_str(&msg)?;
1076 state.push(LuaValue::Str(s));
1077 return Ok(1);
1078 }
1079 LookForFuncStatus::ErrLib(_) => {
1080 return checkload(state, false, &filename);
1081 }
1082 }
1083
1084 let s = state.intern_str(&filename)?;
1085 state.push(LuaValue::Str(s));
1086 Ok(2)
1087}
1088
1089/// Searcher that looks in `package.preload` for a pre-registered loader.
1090///
1091/// On a hit, every version leaves the loader function on the stack. From **5.4**
1092/// the searcher also returns the `:preload:` sentinel as loader data (a 2nd
1093/// value); 5.1/5.2/5.3 return only the function. See [`require_returns_loader_data`].
1094fn searcher_preload(state: &mut LuaState) -> Result<usize, LuaError> {
1095 let name = state.check_arg_string(1)?.to_vec();
1096 state.get_field_registry(b"_PRELOAD")?;
1097 let ty = state.get_field(-1, &name)?;
1098 if ty == LuaType::Nil {
1099 let mut msg = b"no field package.preload['".to_vec();
1100 msg.extend_from_slice(&name);
1101 msg.push(b'\'');
1102 msg.push(b']');
1103 let s = state.intern_str(&msg)?;
1104 state.push(LuaValue::Str(s));
1105 return Ok(1);
1106 }
1107 if !require_returns_loader_data(state.global().lua_version) {
1108 return Ok(1);
1109 }
1110 let tag = state.intern_str(b":preload:")?;
1111 state.push(LuaValue::Str(tag));
1112 Ok(2)
1113}
1114
1115// ── require implementation ────────────────────────────────────────────────────
1116
1117/// Iterate through `package.searchers` to find a loader for module `name`.
1118///
1119/// On success, leaves `(loader_function, loader_data)` at the top of the stack
1120/// (below the searchers table). On failure, raises a runtime error.
1121///
1122/// The accumulated `module '<name>' not found:` message lists one searcher per
1123/// line; the per-iteration `\n\t` prefix matches 5.4+ `findloader`, while the
1124/// pre-5.4 searchers prepend their own separator (the two regimes converge on
1125/// the identical trace, pinned in `tests/loadlib_strengthen.rs`).
1126fn findloader(state: &mut LuaState, name: &[u8]) -> Result<(), LuaError> {
1127 let uv = state.upvalue_index(1);
1128 // In 5.1 the searcher list lives in `package.loaders`; 5.2 renamed it to
1129 // `package.searchers` (5.2 keeps `loaders` as an alias). Read the name this
1130 // version exposes. See specs/followup/5.1-roster-syntax.md §1.
1131 let field: &[u8] = if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1132 b"loaders"
1133 } else {
1134 b"searchers"
1135 };
1136 let ty = state.get_field(uv, field)?;
1137 if ty != LuaType::Table {
1138 return Err(LuaError::runtime(format_args!(
1139 "'package.searchers' must be a table"
1140 )));
1141 }
1142
1143 let mut msg_buf: Vec<u8> = Vec::new();
1144
1145 let mut i: i64 = 1;
1146 loop {
1147 msg_buf.extend_from_slice(b"\n\t");
1148
1149 let item_ty = state.raw_geti(-1, i)?;
1150 if item_ty == LuaType::Nil {
1151 state.pop_n(1);
1152 let len = msg_buf.len();
1153 if len >= 2 {
1154 msg_buf.truncate(len - 2);
1155 }
1156 // Build the error message as a Lua string then raise.
1157 let mut err = b"module '".to_vec();
1158 err.extend_from_slice(name);
1159 err.extend_from_slice(b"' not found:");
1160 err.extend_from_slice(&msg_buf);
1161 let err_s = state.intern_str(&err)?;
1162 return Err(LuaError::from_value(LuaValue::Str(err_s)));
1163 }
1164
1165 let name_s = state.intern_str(name)?;
1166 state.push(LuaValue::Str(name_s));
1167
1168 state.call(1, 2)?;
1169
1170 // After call: two return values r1 (at -2) and r2 (at -1) on top.
1171 if state.type_at(-2) == LuaType::Function {
1172 // Loader found; leave (r1=function, r2=data) on stack and return.
1173 return Ok(());
1174 }
1175
1176 if state.type_at(-2) == LuaType::String {
1177 // r1 is an error-message string from the searcher.
1178 state.pop_n(1);
1179 if let Some(bytes) = state.to_bytes(-1) {
1180 msg_buf.extend_from_slice(&bytes);
1181 }
1182 state.pop_n(1);
1183 } else {
1184 state.pop_n(2);
1185 let len = msg_buf.len();
1186 if len >= 2 {
1187 msg_buf.truncate(len - 2);
1188 }
1189 }
1190
1191 i += 1;
1192 }
1193}
1194
1195/// `require(modname)` — load a module by name, using `package.loaded` as a
1196/// cache and `package.searchers` to find and load it if not already cached.
1197///
1198/// Returns the module value (and optionally the loader data) — 2 values.
1199///
1200pub fn ll_require(state: &mut LuaState) -> Result<usize, LuaError> {
1201 let name = state.check_arg_string(1)?.to_vec();
1202 let version = state.global().lua_version;
1203
1204 // Use the public-API `set_top` (relative to the current C-frame's `func`),
1205 // not the inherent `LuaState::set_top`, which sets an absolute index and
1206 // would truncate the whole stack.
1207 lua_vm::api::set_top(state, 1)?;
1208
1209 state.get_field_registry(b"_LOADED")?;
1210
1211 state.get_field(2, &name)?;
1212
1213 if state.to_boolean(-1) {
1214 return Ok(1);
1215 }
1216
1217 state.pop_n(1);
1218
1219 // `findloader` leaves (loader function, loader data) at the top.
1220 findloader(state, &name)?;
1221
1222 if require_passes_loader_data(version) {
1223 // 5.2+: the loader receives (name, loader data). 5.4+ additionally
1224 // returns the loader data as `require`'s 2nd value, so the data is kept
1225 // below the function (rotate) and re-pushed; 5.2/5.3 pass it but discard
1226 // it (return 1).
1227 state.rotate(-2, 1)?;
1228 state.push_value(1)?;
1229 state.push_value(-3)?;
1230 state.call(2, 1)?;
1231 } else {
1232 // 5.1: the loader receives only the name; there is no loader data.
1233 state.pop_n(1);
1234 state.push_value(1)?;
1235 state.call(1, 1)?;
1236 }
1237
1238 if state.type_at(-1) != LuaType::Nil {
1239 state.set_field(2, &name)?;
1240 } else {
1241 state.pop_n(1);
1242 }
1243
1244 let ty = state.get_field(2, &name)?;
1245 if ty == LuaType::Nil {
1246 state.push(LuaValue::Bool(true));
1247 state.copy_value(-1, -2)?;
1248 state.set_field(2, &name)?;
1249 }
1250
1251 if require_returns_loader_data(version) {
1252 // 5.4+: return (module result, loader data). The loader data is still on
1253 // the stack below the module result; swap them to module-result-first.
1254 state.rotate(-2, 1)?;
1255 Ok(2)
1256 } else {
1257 // 5.1/5.2/5.3: `ll_require` returns only the module (return 1). On the
1258 // 5.2/5.3 path the loader data is still on the stack below the result;
1259 // drop it so the single return value is the module.
1260 if require_passes_loader_data(version) {
1261 state.remove(-2)?;
1262 }
1263 Ok(1)
1264 }
1265}
1266
1267/// Whether `require` passes the searcher's loader data to the module loader as
1268/// a SECOND argument (after the module name).
1269///
1270/// 5.1's `ll_require` calls the loader with one argument (`lua_call(L, 1, 1)`);
1271/// 5.2 widened it to two (`lua_call(L, 2, 1)`), so every later version passes the
1272/// loader data too. Pinned in `tests/loadlib_strengthen.rs`.
1273fn require_passes_loader_data(version: lua_types::LuaVersion) -> bool {
1274 !matches!(version, lua_types::LuaVersion::V51)
1275}
1276
1277/// Whether `require` returns the searcher's loader data as a SECOND result.
1278///
1279/// This is a **5.4** addition (`ll_require`'s `return 2`); 5.1/5.2/5.3 return only
1280/// the module (`return 1`), so `local _, d = require(m)` yields `d == nil` there.
1281/// It is the same seam the preload searcher's `:preload:` sentinel rides on
1282/// (a searcher only bothers returning loader data on a version that surfaces it).
1283/// Pinned in `tests/loadlib_strengthen.rs`.
1284fn require_returns_loader_data(version: lua_types::LuaVersion) -> bool {
1285 matches!(version, lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55)
1286}
1287
1288// ── Package library setup ─────────────────────────────────────────────────────
1289
1290/// Create the `searchers` table and install the four built-in searchers, each
1291/// with the `package` table as upvalue #1.
1292///
1293fn createsearcherstable(state: &mut LuaState) -> Result<(), LuaError> {
1294 let searchers: &[fn(&mut LuaState) -> Result<usize, LuaError>] =
1295 &[searcher_preload, searcher_lua, searcher_c, searcher_croot];
1296
1297 state.create_table(searchers.len() as i32, 0)?;
1298
1299 for (i, &f) in searchers.iter().enumerate() {
1300 // Each searcher closes over the `package` table (upvalue #1) so
1301 // `findfile` can read `package.path`/`package.cpath` via
1302 // `lua_upvalueindex(1)`.
1303 state.push_value(-2)?;
1304 state.push_c_closure(f, 1)?;
1305 state.raw_seti(-2, (i + 1) as i64)?;
1306 }
1307 // Roster name deltas for the searcher list:
1308 // - 5.1: the table is named `package.loaders`; there is NO
1309 // `package.searchers` (verified against lua5.1.5: `package.searchers` is
1310 // nil, `package.loaders` is a table).
1311 // - 5.2: renamed to `package.searchers` but kept `package.loaders` as a
1312 // compat alias (both point at the same list).
1313 // - 5.3+: `package.searchers` only.
1314 let version = state.global().lua_version;
1315 let has_loaders = matches!(
1316 version,
1317 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1318 );
1319 let has_searchers = !matches!(version, lua_types::LuaVersion::V51);
1320 if has_loaders {
1321 state.push_value(-1)?;
1322 state.set_field(-3, b"loaders")?;
1323 }
1324 if has_searchers {
1325 state.set_field(-2, b"searchers")?;
1326 } else {
1327 // No `searchers` field under 5.1; drop the table copy left on the stack.
1328 state.pop_n(1);
1329 }
1330 Ok(())
1331}
1332
1333/// Create the `_CLIBS` registry table with a `__gc` finalizer that closes all
1334/// loaded C libraries when the Lua state is closed.
1335///
1336fn createclibstable(state: &mut LuaState) -> Result<(), LuaError> {
1337 state.get_subtable_registry(CLIBS)?;
1338 state.create_table(0, 1)?;
1339 state.push_c_function(gctm)?;
1340 state.set_field(-2, b"__gc")?;
1341 state.set_metatable(-2)?;
1342 Ok(())
1343}
1344
1345// ── Lua 5.1 `module` / `package.seeall` (deprecated module system) ────────────
1346//
1347// These ship only in the default lua5.1.5 build (`loadlib.c`) and were removed
1348// in 5.2. Registered under the V51 backend; see
1349// specs/followup/5.1-roster-syntax.md §1. They lean on the 5.1 fenv globals
1350// model: `module` sets its caller's environment to the module table (via
1351// `crate::base::set_func_env_at_level`), and `package.seeall` points a module
1352// table's `__index` at `_G`.
1353
1354/// `package.seeall(module)` — make a module table inherit globals.
1355///
1356/// Sets (creating if absent) `module`'s metatable `__index` to the global
1357/// table. Mirrors `ll_seeall` in 5.1 `loadlib.c`. Verified against lua5.1.5.
1358fn ll_seeall(state: &mut LuaState) -> Result<usize, LuaError> {
1359 state.check_arg_type(1, LuaType::Table)?;
1360 if !state.get_metatable(1)? {
1361 state.create_table(0, 1)?;
1362 state.push_value(-1)?;
1363 state.set_metatable(1)?;
1364 }
1365 state.push_globals()?;
1366 state.set_field(-2, b"__index")?;
1367 Ok(0)
1368}
1369
1370/// Walk a dotted module name from a table on the stack, creating intermediate
1371/// tables as needed, leaving the final (sub)table on the stack top. A faithful
1372/// reduction of `luaL_findtable(L, idx, name, 1)`; returns `Err` on a name
1373/// conflict (an intermediate path component is a non-table, non-nil value).
1374fn findtable(state: &mut LuaState, table_idx: i32, name: &[u8]) -> Result<(), LuaError> {
1375 // Start from a copy of the base table on the stack top.
1376 state.push_value_at(table_idx)?;
1377 for part in name.split(|&b| b == b'.') {
1378 // Stack top holds the current table; fetch current[part].
1379 let ty = state.get_field(-1, part)?;
1380 if ty == LuaType::Nil {
1381 state.pop_n(1); // remove nil
1382 state.create_table(0, 1)?; // new subtable
1383 state.push_value(-1)?; // duplicate it
1384 state.set_field(-3, part)?; // current[part] = subtable
1385 // Stack: ..., current, subtable. Remove the parent, keep subtable.
1386 state.remove(-2)?;
1387 } else if ty == LuaType::Table {
1388 // Stack: ..., current, value. Remove the parent, keep value.
1389 state.remove(-2)?;
1390 } else {
1391 return Err(LuaError::runtime(format_args!(
1392 "name conflict for module '{}'",
1393 String::from_utf8_lossy(name)
1394 )));
1395 }
1396 }
1397 Ok(())
1398}
1399
1400/// `module(name [, ...])` — Lua 5.1 only.
1401///
1402/// Creates (or reuses) a module table named `name`, registers it in
1403/// `package.loaded`, initializes its `_NAME`/`_M`/`_PACKAGE` fields, applies any
1404/// option functions (e.g. `package.seeall`), and sets the calling chunk's
1405/// environment to the module table. Mirrors `ll_module` in 5.1 `loadlib.c`.
1406fn ll_module(state: &mut LuaState) -> Result<usize, LuaError> {
1407 let modname: Vec<u8> = state.check_arg_string(1)?;
1408 let n_opts = state.top() as i32;
1409
1410 // Fetch _LOADED[modname]; create the module table if absent.
1411 state.get_field_registry(b"_LOADED")?;
1412 let loaded_idx = state.top() as i32;
1413 state.get_field(loaded_idx, &modname)?;
1414 if state.type_at(-1) != LuaType::Table {
1415 state.pop_n(1); // remove non-table result
1416 // Find/create a global table named `modname` (supporting dotted names).
1417 state.push_globals()?;
1418 let g_idx = state.top() as i32;
1419 findtable(state, g_idx, &modname)?;
1420 state.remove(g_idx)?; // drop the globals table copy, keep the module table
1421 state.push_value(-1)?;
1422 state.set_field(loaded_idx, &modname)?; // _LOADED[modname] = module
1423 }
1424
1425 // Initialize the module if it has no `_NAME` yet.
1426 let has_name = state.get_field(-1, b"_NAME")? != LuaType::Nil;
1427 state.pop_n(1);
1428 if !has_name {
1429 // module._M = module
1430 state.push_value(-1)?;
1431 state.set_field(-2, b"_M")?;
1432 // module._NAME = modname
1433 state.push_string(&modname)?;
1434 state.set_field(-2, b"_NAME")?;
1435 // module._PACKAGE = full name minus the last dotted component.
1436 let pkg: &[u8] = match modname.iter().rposition(|&b| b == b'.') {
1437 Some(dot) => &modname[..=dot],
1438 None => b"",
1439 };
1440 state.push_string(pkg)?;
1441 state.set_field(-2, b"_PACKAGE")?;
1442 }
1443
1444 // Set the caller's environment to the module table (the running closure that
1445 // invoked `module`, i.e. level 1 relative to this C function).
1446 let module_tbl = state.value_at(-1);
1447 crate::base::set_func_env_at_level(state, 1, module_tbl)?;
1448
1449 // Apply option functions: for each extra arg, call `option(module)`.
1450 let mut i = 2;
1451 while i <= n_opts {
1452 state.push_value_at(i)?; // option function
1453 state.push_value(-2)?; // module table
1454 state.call(1, 0)?;
1455 i += 1;
1456 }
1457 Ok(0)
1458}
1459
1460/// Open the `package` library and return the `package` table.
1461///
1462pub fn luaopen_package(state: &mut LuaState) -> Result<usize, LuaError> {
1463 createclibstable(state)?;
1464
1465 // The C `pk_funcs` table also has placeholder entries for "preload",
1466 // "cpath", "path", "searchers", "loaded" (all NULL); those fields are set
1467 // explicitly below. Only `loadlib` is unconditional — `package.searchpath`
1468 // was added in 5.2 (absent on 5.1), so it is registered separately below.
1469 state.new_lib(&[(
1470 b"loadlib" as &[u8],
1471 ll_loadlib as fn(&mut LuaState) -> Result<usize, LuaError>,
1472 )])?;
1473
1474 if !matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1475 state.push_c_function(ll_searchpath)?;
1476 state.set_field(-2, b"searchpath")?;
1477 }
1478
1479 createsearcherstable(state)?;
1480
1481 let path_default = lua_path_default(state.global().lua_version);
1482 setpath(state, b"path", LUA_PATH_VAR, &path_default)?;
1483
1484 let cpath_default = lua_cpath_default(state.global().lua_version);
1485 setpath(state, b"cpath", LUA_CPATH_VAR, &cpath_default)?;
1486
1487 let config = package_config(state.global().lua_version);
1488 let config_s = state.intern_str(&config)?;
1489 state.push(LuaValue::Str(config_s));
1490
1491 state.set_field(-2, b"config")?;
1492
1493 state.get_subtable_registry(b"_LOADED")?;
1494 state.set_field(-2, b"loaded")?;
1495
1496 state.get_subtable_registry(b"_PRELOAD")?;
1497 state.set_field(-2, b"preload")?;
1498
1499 state.push_globals()?;
1500 state.push_value(-2)?;
1501 state.set_funcs_with_upvalues(
1502 &[(
1503 b"require" as &[u8],
1504 ll_require as fn(&mut LuaState) -> Result<usize, LuaError>,
1505 )],
1506 1,
1507 )?;
1508 state.pop_n(1);
1509
1510 // The deprecated module system: `package.seeall` (a field on the package
1511 // table) and the `module` global. Present in 5.1 and kept in 5.2.4 via the
1512 // default-on `LUA_COMPAT_MODULE`; fully removed in 5.3. Verified against
1513 // lua5.1.5 and lua5.2.4. See specs/followup/5.1-roster-syntax.md §1.
1514 if matches!(
1515 state.global().lua_version,
1516 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1517 ) {
1518 // The package table is on top of the stack here.
1519 state.push_c_function(ll_seeall)?;
1520 state.set_field(-2, b"seeall")?;
1521 // `module` is a *global*, not a `package` field.
1522 state.push_c_function(ll_module)?;
1523 state.set_global(b"module")?;
1524 }
1525
1526 Ok(1)
1527}
1528
1529#[cfg(test)]
1530mod tests {
1531 use super::*;
1532 use lua_types::LuaVersion;
1533
1534 const ALL: [LuaVersion; 5] =
1535 [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55];
1536
1537 /// `lua_vdir` is the literal version-directory segment used both in
1538 /// default package paths and to build the versioned env-var name — one
1539 /// value per version, never `5.4` for anything else.
1540 #[test]
1541 fn vdir_is_version_exact() {
1542 assert_eq!(lua_vdir(LuaVersion::V51), b"5.1");
1543 assert_eq!(lua_vdir(LuaVersion::V52), b"5.2");
1544 assert_eq!(lua_vdir(LuaVersion::V53), b"5.3");
1545 assert_eq!(lua_vdir(LuaVersion::V54), b"5.4");
1546 assert_eq!(lua_vdir(LuaVersion::V55), b"5.5");
1547 }
1548
1549 #[test]
1550 fn versuffix_is_version_exact() {
1551 assert_eq!(lua_versuffix(LuaVersion::V51), b"_5_1");
1552 assert_eq!(lua_versuffix(LuaVersion::V52), b"_5_2");
1553 assert_eq!(lua_versuffix(LuaVersion::V53), b"_5_3");
1554 assert_eq!(lua_versuffix(LuaVersion::V54), b"_5_4");
1555 assert_eq!(lua_versuffix(LuaVersion::V55), b"_5_5");
1556 }
1557
1558 /// 5.1 is the one version with NO versioned environment variables at
1559 /// all — confirmed against `lua5.1.5`, where `LUA_PATH_5_1` has no
1560 /// effect on `package.path`. 5.2 onward all have them.
1561 #[test]
1562 fn only_5_1_lacks_versioned_env_vars() {
1563 assert!(!has_versioned_env_vars(LuaVersion::V51));
1564 for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1565 assert!(has_versioned_env_vars(v), "{v:?}");
1566 }
1567 }
1568
1569 /// Byte-for-byte against the unmodified upstream `make macosx` build of
1570 /// each version (`specs/oracle/CONTRACT.md`), captured from
1571 /// `/tmp/lua-refs/bin/lua5.x -e 'print(package.path)'`. The entry SHAPE
1572 /// (not just the `5.x` segment) differs by era — see `lua_path_default`'s
1573 /// doc comment.
1574 #[test]
1575 #[cfg(not(target_os = "windows"))]
1576 fn path_default_is_version_exact() {
1577 assert_eq!(
1578 lua_path_default(LuaVersion::V51),
1579 b"./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;\
1580 /usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua"
1581 .to_vec()
1582 );
1583 assert_eq!(
1584 lua_path_default(LuaVersion::V52),
1585 b"/usr/local/share/lua/5.2/?.lua;/usr/local/share/lua/5.2/?/init.lua;\
1586 /usr/local/lib/lua/5.2/?.lua;/usr/local/lib/lua/5.2/?/init.lua;./?.lua"
1587 .to_vec()
1588 );
1589 for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1590 let vdir = lua_vdir(v);
1591 let mut expected = Vec::new();
1592 expected.extend_from_slice(b"/usr/local/share/lua/");
1593 expected.extend_from_slice(vdir);
1594 expected.extend_from_slice(b"/?.lua;/usr/local/share/lua/");
1595 expected.extend_from_slice(vdir);
1596 expected.extend_from_slice(b"/?/init.lua;/usr/local/lib/lua/");
1597 expected.extend_from_slice(vdir);
1598 expected.extend_from_slice(b"/?.lua;/usr/local/lib/lua/");
1599 expected.extend_from_slice(vdir);
1600 expected.extend_from_slice(b"/?/init.lua;./?.lua;./?/init.lua");
1601 assert_eq!(lua_path_default(v), expected, "{v:?}");
1602 }
1603 }
1604
1605 #[test]
1606 #[cfg(not(target_os = "windows"))]
1607 fn cpath_default_is_version_exact() {
1608 assert_eq!(
1609 lua_cpath_default(LuaVersion::V51),
1610 b"./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so".to_vec()
1611 );
1612 for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
1613 let vdir = lua_vdir(v);
1614 let mut expected = Vec::new();
1615 expected.extend_from_slice(b"/usr/local/lib/lua/");
1616 expected.extend_from_slice(vdir);
1617 expected.extend_from_slice(b"/?.so;/usr/local/lib/lua/");
1618 expected.extend_from_slice(vdir);
1619 expected.extend_from_slice(b"/loadall.so;./?.so");
1620 assert_eq!(lua_cpath_default(v), expected, "{v:?}");
1621 }
1622 }
1623
1624 /// No two versions may collide on their directory segment or defaults —
1625 /// a regression here would silently point every version at the same
1626 /// installed-module directory again (the shape of issue #273).
1627 #[test]
1628 #[cfg(not(target_os = "windows"))]
1629 fn every_version_has_a_distinct_path_default() {
1630 for (i, a) in ALL.iter().enumerate() {
1631 for b in &ALL[i + 1..] {
1632 assert_ne!(lua_path_default(*a), lua_path_default(*b), "{a:?} vs {b:?}");
1633 assert_ne!(lua_cpath_default(*a), lua_cpath_default(*b), "{a:?} vs {b:?}");
1634 }
1635 }
1636 }
1637
1638 /// Only 5.1/5.2/5.3 use the legacy gsub-based `;;` splice; 5.4/5.5 use
1639 /// the position-aware one.
1640 #[test]
1641 fn double_semicolon_splice_is_legacy_matrix() {
1642 for v in [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53] {
1643 assert!(double_semicolon_splice_is_legacy(v), "{v:?}");
1644 }
1645 for v in [LuaVersion::V54, LuaVersion::V55] {
1646 assert!(!double_semicolon_splice_is_legacy(v), "{v:?}");
1647 }
1648 }
1649
1650 /// The legacy splice replaces EVERY non-overlapping `;;` pair,
1651 /// unconditionally wrapping the default in separators regardless of
1652 /// where the pair sits — verified against `lua5.1.5`.
1653 #[test]
1654 fn legacy_splice_replaces_every_occurrence_both_sides() {
1655 assert_eq!(
1656 legacy_double_semicolon_splice(b"/a/?.lua;;;;/b/?.lua", b"DEFAULT"),
1657 b"/a/?.lua;DEFAULT;;DEFAULT;/b/?.lua".to_vec()
1658 );
1659 assert_eq!(
1660 legacy_double_semicolon_splice(b";;/b/?.lua", b"DEFAULT"),
1661 b";DEFAULT;/b/?.lua".to_vec()
1662 );
1663 assert_eq!(
1664 legacy_double_semicolon_splice(b"/a/?.lua;;", b"DEFAULT"),
1665 b"/a/?.lua;DEFAULT;".to_vec()
1666 );
1667 assert_eq!(
1668 legacy_double_semicolon_splice(b"/a/?.lua", b"DEFAULT"),
1669 b"/a/?.lua".to_vec()
1670 );
1671 }
1672
1673 /// The modern splice replaces only the FIRST `;;` pair and omits a
1674 /// boundary separator when the pair sits at the very start or end —
1675 /// verified against `lua5.4.7`.
1676 #[test]
1677 fn modern_splice_replaces_first_occurrence_and_omits_boundary_separators() {
1678 assert_eq!(
1679 modern_double_semicolon_splice(b"/a/?.lua;;;;/b/?.lua", b"DEFAULT"),
1680 b"/a/?.lua;DEFAULT;;;/b/?.lua".to_vec()
1681 );
1682 assert_eq!(
1683 modern_double_semicolon_splice(b";;/b/?.lua", b"DEFAULT"),
1684 b"DEFAULT;/b/?.lua".to_vec()
1685 );
1686 assert_eq!(
1687 modern_double_semicolon_splice(b"/a/?.lua;;", b"DEFAULT"),
1688 b"/a/?.lua;DEFAULT".to_vec()
1689 );
1690 assert_eq!(
1691 modern_double_semicolon_splice(b"/a/?.lua", b"DEFAULT"),
1692 b"/a/?.lua".to_vec()
1693 );
1694 }
1695}