mlua-pkg 0.1.0

Composable Lua module loader for mlua
Documentation

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 mlua::Lua;
use mlua_pkg::{Registry, resolvers::*};

fn setup(lua: &Lua) -> Result<(), Box<dyn std::error::Error>> {
    let mut reg = Registry::new();

    // Rust-native module (highest priority)
    reg.add(NativeResolver::new().add("@std/http", |lua| {
        let t = lua.create_table()?;
        t.set("version", 1)?;
        Ok(mlua::Value::Table(t))
    }));

    // Embedded Lua source
    reg.add(MemoryResolver::new().add("utils", "return { pi = 3.14 }"));

    // Filesystem with sandbox (dot-separated -> path)
    reg.add(FsResolver::new("./scripts")?);

    // Non-Lua assets with pluggable parsers
    reg.add(AssetResolver::new("./assets")?
        .parser("json", json_parser())
        .parser("sql", text_parser()));

    reg.install(lua)?;
    Ok(())
}

// 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 mlua_pkg::{LuaConvention, resolvers::FsResolver};

// Luau convention
let r = FsResolver::new("./src")?
    .with_convention(LuaConvention::LUAU);

// Custom
let r = FsResolver::new("./src")?
    .with_extension("lua")
    .with_init_name("mod")
    .with_module_separator('/');

Asset parsing

AssetResolver dispatches to registered parsers by file extension:

use mlua_pkg::resolvers::{AssetResolver, json_parser, text_parser};

let r = AssetResolver::new("./assets")?
    .parser("json", json_parser())   // JSON -> Lua Table
    .parser("sql", text_parser())    // raw text -> Lua String
    .parser("csv", |lua, content| {  // custom parser
        let t = lua.create_table()?;
        for (i, line) in content.lines().enumerate() {
            t.set(i + 1, lua.create_string(line)?)?;
        }
        Ok(mlua::Value::Table(t))
    });

Namespace mounting

PrefixResolver creates mount points for module namespaces:

use mlua_pkg::{Registry, resolvers::*};

let mut reg = Registry::new();

// "game.engine" -> strip "game." -> FsResolver resolves "engine"
reg.add(PrefixResolver::new("game",
    FsResolver::new("./game_modules")?));

// "game" (init.lua) -> outer FsResolver
reg.add(FsResolver::new("./scripts")?);

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
mlua-pkg = { version = "0.1", features = ["sandbox-cap-std"] }
use mlua_pkg::{resolvers::FsResolver, sandbox::CapSandbox};

let resolver = FsResolver::with_sandbox(CapSandbox::new("./scripts")?);

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.