Skip to main content

mlua_batteries/
lib.rs

1//! Batteries-included standard library modules for mlua.
2//!
3//! Each module exposes a single `module(lua) -> LuaResult<LuaTable>` entry point.
4//! Register individually or use [`register_all`] for convenience.
5//!
6//! # Platform support
7//!
8//! This crate targets **Unix server platforms** (Linux, macOS).
9//! Windows is not a supported target.
10//!
11//! # Encoding — UTF-8 only (by design)
12//!
13//! All path arguments are received as Rust [`String`] (UTF-8).
14//! Non-UTF-8 Lua strings are rejected at the `FromLua` boundary.
15//! Returned paths use [`to_string_lossy`](std::path::Path::to_string_lossy),
16//! replacing any non-UTF-8 bytes with U+FFFD.
17//!
18//! ## Why not raw bytes / `OsStr`?
19//!
20//! mlua's `FromLua` for `String` performs UTF-8 validation — non-UTF-8
21//! values produce `FromLuaConversionError` before reaching handler code.
22//! Bypassing this would require accepting `mlua::String` + `as_bytes()`
23//! in every function, converting through `OsStr::from_bytes()`, and
24//! returning `OsStr::as_bytes()` back to Lua.  This adds complexity
25//! across all path-accepting functions for a scenario (non-UTF-8
26//! filenames) that is rare on modern systems.
27//!
28//! References:
29//! - mlua `String::to_str()`: <https://docs.rs/mlua/latest/mlua/struct.String.html>
30//! - mlua string internals: <https://deepwiki.com/mlua-rs/mlua/2.3.4-strings>
31//!
32//! # Quick start
33//!
34//! ```rust,no_run
35//! use mlua::prelude::*;
36//!
37//! let lua = Lua::new();
38//! mlua_batteries::register_all(&lua, "std").unwrap();
39//! // Lua: std.json.encode({a = 1})
40//! // Lua: std.env.get("HOME")
41//! ```
42//!
43//! # `require` instead of a global (Teal / htl)
44//!
45//! [`register_all`] installs a global table, which is the convenient
46//! shape for plain Lua.  A Teal project (htl lints `global` away) reaches
47//! the same modules through `require`: [`preload_all`] registers every
48//! enabled module in `package.preload` under `<prefix>.<name>`, plus the
49//! namespace itself under `<prefix>`, and touches no global.  The
50//! declarations that let the Teal checker see them are in
51//! [`dts`](crate::dts), written to a project's `types/` with the same
52//! prefix.
53//!
54//! ```rust,no_run
55//! use mlua::prelude::*;
56//!
57//! let lua = Lua::new();
58//! mlua_batteries::preload_all(&lua, mlua_batteries::PRELOAD_PREFIX).unwrap();
59//! // Lua / Teal: local json = require("mlua_batteries.json")
60//! //             local std  = require("mlua_batteries")   -- every module in one table
61//! ```
62//!
63//! # Async
64//!
65//! The modules above are synchronous and need no runtime.  Two opt-in
66//! pieces are async, and both want a tokio current-thread runtime driving
67//! a `LocalSet`:
68//!
69//! - `task` — structured concurrency primitives (`std.task.*`).
70//! - `async_overrides` — replaces the blocking entries of an
71//!   already-registered namespace (`std.time.sleep`, `std.proc.pipeline`,
72//!   `std.http.*`, `std.fs.*`) with async ones, so they no longer park the
73//!   VM thread.  Same Lua-side API; opt in by calling it after
74//!   [`register_all`].
75//!
76//! # Custom configuration
77//!
78//! ```rust,ignore
79//! // Requires the `sandbox` feature.
80//! use mlua::prelude::*;
81//! use mlua_batteries::config::Config;
82//! use mlua_batteries::policy::Sandboxed;
83//!
84//! let lua = Lua::new();
85//! let config = Config::builder()
86//!     .path_policy(Sandboxed::new(["/app/data"]).unwrap().read_only())
87//!     .max_walk_depth(50)
88//!     .build()
89//!     .expect("invalid config");
90//! mlua_batteries::register_all_with(&lua, "std", config).unwrap();
91//! ```
92
93pub mod config;
94pub mod dts;
95pub mod policy;
96
97#[cfg(feature = "argparse")]
98pub mod argparse;
99#[cfg(feature = "task")]
100pub mod async_overrides;
101#[cfg(feature = "base64")]
102pub mod base64;
103#[cfg(feature = "env")]
104pub mod env;
105#[cfg(feature = "fs")]
106pub mod fs;
107#[cfg(feature = "hash")]
108pub mod hash;
109#[cfg(feature = "http")]
110pub mod http;
111#[cfg(feature = "json")]
112pub mod json;
113#[cfg(feature = "llm")]
114pub mod llm;
115#[cfg(feature = "log")]
116pub mod log;
117#[cfg(feature = "path")]
118pub mod path;
119#[cfg(feature = "pretty")]
120pub mod pretty;
121#[cfg(feature = "proc")]
122pub mod proc;
123#[cfg(feature = "regex")]
124pub mod regex;
125#[cfg(feature = "string")]
126pub mod string;
127#[cfg(feature = "task")]
128pub mod task;
129#[cfg(feature = "time")]
130pub mod time;
131#[cfg(feature = "uuid")]
132pub mod uuid;
133#[cfg(feature = "validate")]
134pub mod validate;
135#[cfg(feature = "watch")]
136pub mod watch;
137
138pub(crate) mod util;
139
140use config::Config;
141use mlua::prelude::*;
142
143/// Module factory function type.
144pub type ModuleFactory = fn(&Lua) -> LuaResult<LuaTable>;
145
146/// Register all enabled modules with default configuration.
147///
148/// Equivalent to `register_all_with(lua, namespace, Config::default())`.
149///
150/// # Warning
151///
152/// The default configuration uses [`policy::Unrestricted`], which allows
153/// Lua scripts to access **any** file on the filesystem.  For untrusted
154/// scripts, use [`register_all_with`] with a [`policy::Sandboxed`] policy.
155pub fn register_all(lua: &Lua, namespace: &str) -> LuaResult<LuaTable> {
156    register_all_with(lua, namespace, Config::default())
157}
158
159/// Register all enabled modules with custom configuration.
160///
161/// The [`Config`] is stored in `lua.app_data` and consulted by each
162/// module for policy checks and limit values.
163///
164/// # Calling multiple times
165///
166/// Calling this function again on the same [`Lua`] instance **replaces**
167/// the previous [`Config`] (and the shared HTTP agent, if the `http`
168/// feature is enabled).  Functions registered by earlier calls remain
169/// in the namespace table but will use the **new** Config for all
170/// subsequent invocations.  This is intentional — it allows
171/// reconfiguration — but callers should be aware that there is no
172/// "merge" behaviour.
173pub fn register_all_with(lua: &Lua, namespace: &str, config: Config) -> LuaResult<LuaTable> {
174    lua.set_app_data(config);
175
176    let ns = lua.create_table()?;
177
178    macro_rules! register {
179        ($name:literal, $mod:ident) => {{
180            #[cfg(feature = $name)]
181            ns.set($name, $mod::module(lua)?)?;
182        }};
183    }
184
185    register!("json", json);
186    register!("env", env);
187    register!("path", path);
188    register!("string", string);
189    register!("regex", regex);
190    register!("validate", validate);
191    register!("pretty", pretty);
192    register!("argparse", argparse);
193    register!("log", log);
194    register!("uuid", uuid);
195    register!("base64", base64);
196    register!("time", time);
197    register!("fs", fs);
198    register!("http", http);
199    register!("llm", llm);
200    register!("hash", hash);
201    register!("proc", proc);
202    register!("watch", watch);
203
204    lua.globals().set(namespace, ns.clone())?;
205    Ok(ns)
206}
207
208/// The `require` prefix this crate's shipped Teal declarations are named
209/// under (`require("mlua_batteries.json")`), and the one to pass
210/// [`preload_all`] unless the host composes its own namespace.
211///
212/// It is the crate's own name rather than `std` on purpose: `std` is the
213/// host's namespace to assemble (a host may keep `std.fs` for its own
214/// sandboxed module and take only `std.json` from here), so nothing this
215/// crate ships claims it.
216pub const PRELOAD_PREFIX: &str = "mlua_batteries";
217
218/// Register every enabled module in `package.preload` with default
219/// configuration.
220///
221/// Equivalent to `preload_all_with(lua, prefix, Config::default())`; the
222/// warning on [`register_all`] about the unrestricted default policy
223/// applies here too.
224pub fn preload_all(lua: &Lua, prefix: &str) -> LuaResult<()> {
225    preload_all_with(lua, prefix, Config::default())
226}
227
228/// Register every enabled module in `package.preload` with custom
229/// configuration.
230///
231/// After this call `require("<prefix>.json")` (and so on for each module
232/// in [`module_entries`], plus `task` when that feature is on) returns
233/// the module table, and
234/// `require("<prefix>")` returns a namespace table holding all of them —
235/// the same table instances, since the namespace loader goes through
236/// `require` itself.  Modules are built lazily on first `require` and
237/// cached by `package.loaded` as usual.  No global is set, which is what a
238/// Teal / htl project wants (see the crate docs); the two entry points are
239/// independent, so a host may call [`register_all_with`] as well.
240///
241/// The [`Config`] goes into `lua.app_data` exactly as in
242/// [`register_all_with`], with the same replace-on-repeat semantics.
243///
244/// The prefix is the host's choice.  [`PRELOAD_PREFIX`] matches the
245/// module names the shipped Teal declarations use; a host that names its
246/// namespace differently writes the declarations under that prefix
247/// instead (`dts::write_to(dir, prefix)`), so the two stay aligned.
248pub fn preload_all_with(lua: &Lua, prefix: &str, config: Config) -> LuaResult<()> {
249    lua.set_app_data(config);
250
251    let preload: LuaTable = lua
252        .globals()
253        .get::<LuaTable>("package")?
254        .get::<LuaTable>("preload")?;
255
256    // `task` is async-first and stays out of the synchronous namespace
257    // `register_all` builds, but a preload entry costs nothing until it is
258    // required, so a Teal host reaches it the same way as the others.
259    #[cfg(feature = "task")]
260    let task_entry = Some(("task", task::module as ModuleFactory));
261    #[cfg(not(feature = "task"))]
262    let task_entry: Option<(&'static str, ModuleFactory)> = None;
263
264    let mut names: Vec<&'static str> = Vec::new();
265    for (name, factory) in module_entries().into_iter().chain(task_entry) {
266        names.push(name);
267        // A preload loader receives (modname, extra); neither is needed.
268        let loader = lua.create_function(move |lua, _: LuaMultiValue| factory(lua))?;
269        preload.set(format!("{prefix}.{name}"), loader)?;
270    }
271
272    let prefix_owned = prefix.to_string();
273    let namespace_loader = lua.create_function(move |lua, _: LuaMultiValue| {
274        let require: LuaFunction = lua.globals().get("require")?;
275        let ns = lua.create_table()?;
276        for name in &names {
277            let module: LuaTable = require.call(format!("{prefix_owned}.{name}"))?;
278            ns.set(*name, module)?;
279        }
280        Ok(ns)
281    })?;
282    preload.set(prefix, namespace_loader)?;
283
284    Ok(())
285}
286
287/// Returns a list of `(name, factory)` pairs for all enabled modules.
288///
289/// Each entry is a `(&'static str, fn(&Lua) -> LuaResult<LuaTable>)`.
290/// The list only includes modules whose cargo features are active.
291///
292/// # When to use
293///
294/// Use this when you need per-module registration instead of the
295/// all-in-one [`register_all`]. Common case: integration with
296/// `mlua-pkg`'s `NativeResolver`:
297///
298/// ```rust,ignore
299/// // `ignore`: NativeResolver is from the `mlua-pkg` crate, which is
300/// // not a dependency of this crate. Cannot be compiled in-tree.
301/// let mut resolver = NativeResolver::new();
302/// for (name, factory) in mlua_batteries::module_entries() {
303///     resolver = resolver.add(name, |lua| factory(lua).map(mlua::Value::Table));
304/// }
305/// ```
306pub fn module_entries() -> Vec<(&'static str, ModuleFactory)> {
307    let mut entries: Vec<(&'static str, ModuleFactory)> = Vec::new();
308
309    macro_rules! entry {
310        ($name:literal, $mod:ident) => {{
311            #[cfg(feature = $name)]
312            entries.push(($name, $mod::module));
313        }};
314    }
315
316    entry!("json", json);
317    entry!("env", env);
318    entry!("path", path);
319    entry!("string", string);
320    entry!("regex", regex);
321    entry!("validate", validate);
322    entry!("pretty", pretty);
323    entry!("argparse", argparse);
324    entry!("log", log);
325    entry!("uuid", uuid);
326    entry!("base64", base64);
327    entry!("time", time);
328    entry!("fs", fs);
329    entry!("http", http);
330    entry!("llm", llm);
331    entry!("hash", hash);
332    entry!("proc", proc);
333    entry!("watch", watch);
334
335    entries
336}