mlua-pkg
Composable Lua module loader for mlua.
Turns require("name") into a composable name -> Value resolution chain,
unifying in-memory sources, filesystem, Rust-native modules, and non-Lua assets
under a single Resolver trait.
Quick start
use Lua;
use ;
// Lua side: require("@std/http"), require("utils"), etc.
Resolver chain
Resolvers are tried in registration order. First Some wins.
require("name")
|
v
package.searchers[1] <- Registry hook
|
+- Resolver A: resolve(lua, "name") -> None (skip)
+- Resolver B: resolve(lua, "name") -> Some(Ok(Value)) (done)
|
v
package.loaded["name"] = Value <- Lua standard cache
| Return value | Meaning | Next resolver? |
|---|---|---|
None |
Not my responsibility | Tried |
Some(Ok(v)) |
Resolved | Skipped |
Some(Err(e)) |
Responsible but failed | Skipped |
Some(Err) intentionally does not fall through.
"Found but broken" should not silently resolve to something else.
Built-in resolvers
Leaf resolvers
| Resolver | Source | Match condition |
|---|---|---|
MemoryResolver |
HashMap<String, String> |
Name is registered |
NativeResolver |
Fn(&Lua) -> Result<Value> |
Name is registered |
FsResolver |
Filesystem (sandboxed) | File exists |
AssetResolver |
Filesystem (sandboxed) | Known extension + file exists |
Combinators
| Combinator | Behavior |
|---|---|
Registry (chain) |
Try resolvers in order, take first Some |
PrefixResolver |
Strip prefix, delegate to inner resolver |
Filesystem resolution
FsResolver converts dot-separated module names to paths:
require("lib.helper") -> lib/helper.lua
require("mypkg") -> mypkg.lua, then mypkg/init.lua
Configurable via LuaConvention or individual methods:
use ;
// Luau convention
let r = new?
.with_convention;
// Custom
let r = new?
.with_extension
.with_init_name
.with_module_separator;
Asset parsing
AssetResolver dispatches to registered parsers by file extension:
use ;
let r = new?
.parser // JSON -> Lua Table
.parser // raw text -> Lua String
.parser;
Namespace mounting
PrefixResolver creates mount points for module namespaces:
use ;
let mut reg = new;
// "game.engine" -> strip "game." -> FsResolver resolves "engine"
reg.add;
// "game" (init.lua) -> outer FsResolver
reg.add;
Sandbox
All filesystem access goes through the SandboxedFs trait.
Two implementations are provided:
| Implementation | TOCTOU safe | Dependency |
|---|---|---|
FsSandbox (default) |
No | None |
CapSandbox |
Yes | cap-std (opt-in) |
FsSandbox canonicalizes paths and blocks traversal, but has a TOCTOU gap
between canonicalize() and read_to_string().
CapSandbox eliminates the TOCTOU gap via OS-level capability-based file
access (openat2 / RESOLVE_BENEATH on Linux, equivalent on other platforms).
# Enable CapSandbox
= { = "0.1", = ["sandbox-cap-std"] }
use ;
let resolver = with_sandbox;
For test mocking, implement SandboxedFs on your own type
and inject it via FsResolver::with_sandbox() / AssetResolver::with_sandbox().
License
Licensed under either of
at your option.