Skip to main content

mlua/luau/
require.rs

1use std::cell::RefCell;
2use std::ffi::CStr;
3use std::io::Result as IoResult;
4use std::ops::{Deref, DerefMut};
5use std::os::raw::{c_char, c_int, c_void};
6use std::result::Result as StdResult;
7use std::{fmt, mem, ptr};
8
9use crate::error::{Error, Result};
10use crate::function::Function;
11use crate::state::{Lua, callback_error_ext};
12use crate::table::Table;
13use crate::types::MaybeSend;
14
15pub use fs::FsRequirer;
16
17/// An error that can occur during navigation in the Luau `require-by-string` system.
18#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub enum NavigateError {
21    /// The path is ambiguous (more than one candidate matches).
22    Ambiguous,
23    /// The requested path could not be found.
24    NotFound,
25    /// Another error occurred during navigation.
26    Other(Error),
27}
28
29#[cfg(feature = "luau")]
30trait IntoNavigateResult {
31    fn into_nav_result(self) -> Result<ffi::luarequire_NavigateResult>;
32}
33
34#[cfg(feature = "luau")]
35impl IntoNavigateResult for StdResult<(), NavigateError> {
36    fn into_nav_result(self) -> Result<ffi::luarequire_NavigateResult> {
37        match self {
38            Ok(()) => Ok(ffi::luarequire_NavigateResult::Success),
39            Err(NavigateError::Ambiguous) => Ok(ffi::luarequire_NavigateResult::Ambiguous),
40            Err(NavigateError::NotFound) => Ok(ffi::luarequire_NavigateResult::NotFound),
41            Err(NavigateError::Other(err)) => Err(err),
42        }
43    }
44}
45
46impl From<Error> for NavigateError {
47    fn from(err: Error) -> Self {
48        NavigateError::Other(err)
49    }
50}
51
52#[cfg(feature = "luau")]
53type WriteResult = ffi::luarequire_WriteResult;
54
55#[cfg(feature = "luau")]
56type ConfigStatus = ffi::luarequire_ConfigStatus;
57
58/// A trait for handling modules loading and navigation in the Luau `require-by-string` system.
59pub trait Require {
60    /// Returns `true` if "require" is permitted for the given chunk name.
61    fn is_require_allowed(&self, chunk_name: &str) -> bool;
62
63    /// Resets the internal state to point at the requirer module.
64    fn reset(&mut self, chunk_name: &str) -> StdResult<(), NavigateError>;
65
66    /// Resets the internal state to point at an aliased module.
67    ///
68    /// This function receives an exact path from a configuration file.
69    /// It's only called when an alias's path cannot be resolved relative to its
70    /// configuration file.
71    fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError>;
72
73    /// Provides an initial alias override opportunity prior to searching for
74    /// configuration files.
75    ///
76    /// If `Ok(())` is returned, alias resolution stops here and the internal state
77    /// must point at the aliased location.
78    fn to_alias_override(&mut self, _alias: &str) -> StdResult<(), NavigateError> {
79        Err(NavigateError::NotFound)
80    }
81
82    /// Provides a final opportunity to resolve an alias if it cannot be found in
83    /// configuration files.
84    ///
85    /// If `Ok(())` is returned, alias resolution stops here and the internal state
86    /// must point at the aliased location.
87    fn to_alias_fallback(&mut self, _alias: &str) -> StdResult<(), NavigateError> {
88        Err(NavigateError::NotFound)
89    }
90
91    /// Navigates to the parent directory of the current requirer.
92    fn to_parent(&mut self) -> StdResult<(), NavigateError>;
93
94    /// Navigate to the given child directory.
95    fn to_child(&mut self, name: &str) -> StdResult<(), NavigateError>;
96
97    /// Returns whether the context is currently pointing at a module.
98    fn has_module(&self) -> bool;
99
100    /// Provides a cache key representing the current module.
101    ///
102    /// This function is only called if `has_module` returns true.
103    fn cache_key(&self) -> String;
104
105    /// Returns whether a configuration is present in the current context.
106    fn has_config(&self) -> bool;
107
108    /// Returns the contents of the configuration file in the current context.
109    ///
110    /// This function is only called if `has_config` returns true.
111    fn config(&self) -> IoResult<Vec<u8>>;
112
113    /// Returns a loader function for the current module, that when called, loads the module
114    /// and returns the result.
115    ///
116    /// Loader can be sync or async.
117    /// This function is only called if `has_module` returns true.
118    fn loader(&self, lua: &Lua) -> Result<Function>;
119}
120
121impl fmt::Debug for dyn Require {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "<dyn Require>")
124    }
125}
126
127struct Context {
128    require: Box<dyn Require>,
129    config_cache: Option<IoResult<Vec<u8>>>,
130}
131
132impl Deref for Context {
133    type Target = dyn Require;
134
135    fn deref(&self) -> &Self::Target {
136        &*self.require
137    }
138}
139
140impl DerefMut for Context {
141    fn deref_mut(&mut self) -> &mut Self::Target {
142        &mut *self.require
143    }
144}
145
146impl Context {
147    fn new(require: impl Require + MaybeSend + 'static) -> Self {
148        Context {
149            require: Box::new(require),
150            config_cache: None,
151        }
152    }
153}
154
155macro_rules! try_borrow {
156    ($state:expr, $ctx:expr) => {
157        match (*($ctx as *const RefCell<Context>)).try_borrow() {
158            Ok(ctx) => ctx,
159            Err(_) => ffi::luaL_error($state, cstr!("require context is already borrowed")),
160        }
161    };
162}
163
164macro_rules! try_borrow_mut {
165    ($state:expr, $ctx:expr) => {
166        match (*($ctx as *const RefCell<Context>)).try_borrow_mut() {
167            Ok(ctx) => ctx,
168            Err(_) => ffi::luaL_error($state, cstr!("require context is already borrowed")),
169        }
170    };
171}
172
173#[cfg(feature = "luau")]
174pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_Configuration) {
175    if config.is_null() {
176        return;
177    }
178
179    unsafe extern "C-unwind" fn is_require_allowed(
180        state: *mut ffi::lua_State,
181        ctx: *mut c_void,
182        requirer_chunkname: *const c_char,
183    ) -> bool {
184        if requirer_chunkname.is_null() {
185            return false;
186        }
187
188        let this = try_borrow!(state, ctx);
189        let chunk_name = CStr::from_ptr(requirer_chunkname).to_string_lossy();
190        this.is_require_allowed(&chunk_name)
191    }
192
193    unsafe extern "C-unwind" fn reset(
194        state: *mut ffi::lua_State,
195        ctx: *mut c_void,
196        requirer_chunkname: *const c_char,
197    ) -> ffi::luarequire_NavigateResult {
198        let mut this = try_borrow_mut!(state, ctx);
199        let chunk_name = CStr::from_ptr(requirer_chunkname).to_string_lossy();
200        callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
201            this.reset(&chunk_name).into_nav_result()
202        })
203    }
204
205    unsafe extern "C-unwind" fn jump_to_alias(
206        state: *mut ffi::lua_State,
207        ctx: *mut c_void,
208        path: *const c_char,
209    ) -> ffi::luarequire_NavigateResult {
210        let mut this = try_borrow_mut!(state, ctx);
211        let path = CStr::from_ptr(path).to_string_lossy();
212        callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
213            this.jump_to_alias(&path).into_nav_result()
214        })
215    }
216
217    unsafe extern "C-unwind" fn to_alias_override(
218        state: *mut ffi::lua_State,
219        ctx: *mut c_void,
220        alias_unprefixed: *const c_char,
221    ) -> ffi::luarequire_NavigateResult {
222        let mut this = try_borrow_mut!(state, ctx);
223        let alias = CStr::from_ptr(alias_unprefixed).to_string_lossy();
224        callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
225            this.to_alias_override(&alias).into_nav_result()
226        })
227    }
228
229    unsafe extern "C-unwind" fn to_alias_fallback(
230        state: *mut ffi::lua_State,
231        ctx: *mut c_void,
232        alias_unprefixed: *const c_char,
233    ) -> ffi::luarequire_NavigateResult {
234        let mut this = try_borrow_mut!(state, ctx);
235        let alias = CStr::from_ptr(alias_unprefixed).to_string_lossy();
236        callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
237            this.to_alias_fallback(&alias).into_nav_result()
238        })
239    }
240
241    unsafe extern "C-unwind" fn to_parent(
242        state: *mut ffi::lua_State,
243        ctx: *mut c_void,
244    ) -> ffi::luarequire_NavigateResult {
245        let mut this = try_borrow_mut!(state, ctx);
246        callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
247            this.to_parent().into_nav_result()
248        })
249    }
250
251    unsafe extern "C-unwind" fn to_child(
252        state: *mut ffi::lua_State,
253        ctx: *mut c_void,
254        name: *const c_char,
255    ) -> ffi::luarequire_NavigateResult {
256        let mut this = try_borrow_mut!(state, ctx);
257        let name = CStr::from_ptr(name).to_string_lossy();
258        callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
259            this.to_child(&name).into_nav_result()
260        })
261    }
262
263    unsafe extern "C-unwind" fn is_module_present(state: *mut ffi::lua_State, ctx: *mut c_void) -> bool {
264        let this = try_borrow!(state, ctx);
265        this.has_module()
266    }
267
268    unsafe extern "C-unwind" fn get_chunkname(
269        _state: *mut ffi::lua_State,
270        _ctx: *mut c_void,
271        buffer: *mut c_char,
272        buffer_size: usize,
273        size_out: *mut usize,
274    ) -> WriteResult {
275        write_to_buffer(buffer, buffer_size, size_out, &[])
276    }
277
278    unsafe extern "C-unwind" fn get_loadname(
279        _state: *mut ffi::lua_State,
280        _ctx: *mut c_void,
281        buffer: *mut c_char,
282        buffer_size: usize,
283        size_out: *mut usize,
284    ) -> WriteResult {
285        write_to_buffer(buffer, buffer_size, size_out, &[])
286    }
287
288    unsafe extern "C-unwind" fn get_cache_key(
289        state: *mut ffi::lua_State,
290        ctx: *mut c_void,
291        buffer: *mut c_char,
292        buffer_size: usize,
293        size_out: *mut usize,
294    ) -> WriteResult {
295        let this = try_borrow!(state, ctx);
296        let cache_key = this.cache_key();
297        write_to_buffer(buffer, buffer_size, size_out, cache_key.as_bytes())
298    }
299
300    unsafe extern "C-unwind" fn get_config_status(
301        state: *mut ffi::lua_State,
302        ctx: *mut c_void,
303    ) -> ConfigStatus {
304        let mut this = try_borrow_mut!(state, ctx);
305        if this.has_config() {
306            this.config_cache = Some(this.config());
307            if let Some(Ok(data)) = &this.config_cache {
308                return detect_config_format(data);
309            }
310        }
311        ConfigStatus::Absent
312    }
313
314    unsafe extern "C-unwind" fn get_config(
315        state: *mut ffi::lua_State,
316        ctx: *mut c_void,
317        buffer: *mut c_char,
318        buffer_size: usize,
319        size_out: *mut usize,
320    ) -> WriteResult {
321        let mut this = try_borrow_mut!(state, ctx);
322        let config = callback_error_ext(state, ptr::null_mut(), true, move |_, _| {
323            Ok(this.config_cache.take().unwrap_or_else(|| this.config())?)
324        });
325        write_to_buffer(buffer, buffer_size, size_out, &config)
326    }
327
328    unsafe extern "C-unwind" fn load(
329        state: *mut ffi::lua_State,
330        ctx: *mut c_void,
331        _path: *const c_char,
332        _chunkname: *const c_char,
333        _loadname: *const c_char,
334    ) -> c_int {
335        let this = try_borrow!(state, ctx);
336        callback_error_ext(state, ptr::null_mut(), true, move |extra, _| {
337            let rawlua = (*extra).raw_lua();
338            let loader = this.loader(rawlua.lua())?;
339            rawlua.push(loader)?;
340            Ok(1)
341        })
342    }
343
344    (*config).is_require_allowed = is_require_allowed;
345    (*config).reset = reset;
346    (*config).jump_to_alias = jump_to_alias;
347    (*config).to_alias_override = Some(to_alias_override);
348    (*config).to_alias_fallback = Some(to_alias_fallback);
349    (*config).to_parent = to_parent;
350    (*config).to_child = to_child;
351    (*config).is_module_present = is_module_present;
352    (*config).get_chunkname = get_chunkname;
353    (*config).get_loadname = get_loadname;
354    (*config).get_cache_key = get_cache_key;
355    (*config).get_config_status = get_config_status;
356    (*config).get_alias = None;
357    (*config).get_config = Some(get_config);
358    (*config).load = load;
359}
360
361/// Detect configuration file format (JSON or Luau)
362#[cfg(feature = "luau")]
363fn detect_config_format(data: &[u8]) -> ConfigStatus {
364    let data = data.trim_ascii();
365    if data.starts_with(b"{") {
366        let data = data[1..].trim_ascii_start();
367        if data.starts_with(b"\"") || data == b"}" {
368            return ConfigStatus::PresentJson;
369        }
370    }
371    ConfigStatus::PresentLuau
372}
373
374/// Helper function to write data to a buffer
375#[cfg(feature = "luau")]
376unsafe fn write_to_buffer(
377    buffer: *mut c_char,
378    buffer_size: usize,
379    size_out: *mut usize,
380    data: &[u8],
381) -> WriteResult {
382    // the buffer must be null terminated as it's a c++ `std::string` data() buffer
383    let is_null_terminated = data.last() == Some(&0);
384    *size_out = data.len() + if is_null_terminated { 0 } else { 1 };
385    if *size_out > buffer_size {
386        return WriteResult::BufferTooSmall;
387    }
388    ptr::copy_nonoverlapping(data.as_ptr(), buffer as *mut _, data.len());
389    if !is_null_terminated {
390        *buffer.add(data.len()) = 0;
391    }
392    WriteResult::Success
393}
394
395#[cfg(feature = "luau")]
396pub(super) fn create_require_function<R: Require + MaybeSend + 'static>(
397    lua: &Lua,
398    require: R,
399) -> Result<Function> {
400    unsafe extern "C-unwind" fn find_current_file(state: *mut ffi::lua_State) -> c_int {
401        let mut ar: ffi::lua_Debug = mem::zeroed();
402        for level in 2.. {
403            if ffi::lua_getinfo(state, level, cstr!("s"), &mut ar) == 0 {
404                ffi::luaL_error(state, cstr!("require is not supported in this context"));
405            }
406            if CStr::from_ptr(ar.what) != c"C" {
407                break;
408            }
409        }
410        ffi::lua_pushstring(state, ar.source);
411        1
412    }
413
414    unsafe extern "C-unwind" fn get_cache_key(state: *mut ffi::lua_State) -> c_int {
415        let ctx = ffi::lua_touserdata(state, ffi::lua_upvalueindex(1));
416        let ctx = try_borrow!(state, ctx);
417        let cache_key = ctx.cache_key();
418        ffi::lua_pushlstring(state, cache_key.as_ptr() as *const _, cache_key.len());
419        1
420    }
421
422    let (get_cache_key, find_current_file, proxyrequire, registered_modules, loader_cache) = unsafe {
423        lua.exec_raw::<(Function, Function, Function, Table, Table)>((), move |state| {
424            let context = Context::new(require);
425            let context_ptr = ffi::lua_newuserdata_t(state, RefCell::new(context));
426            ffi::lua_pushcclosured(state, get_cache_key, cstr!("get_cache_key"), 1);
427            ffi::lua_pushcfunctiond(state, find_current_file, cstr!("find_current_file"));
428            ffi::luarequire_pushproxyrequire(state, init_config, context_ptr as *mut _);
429            ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, ffi::LUA_REGISTERED_MODULES_TABLE);
430            ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, cstr!("__MLUA_LOADER_CACHE"));
431        })
432    }?;
433
434    unsafe extern "C-unwind" fn error(state: *mut ffi::lua_State) -> c_int {
435        ffi::luaL_where(state, 1);
436        ffi::lua_pushvalue(state, 1);
437        ffi::lua_concat(state, 2);
438        ffi::lua_error(state);
439    }
440
441    unsafe extern "C-unwind" fn r#type(state: *mut ffi::lua_State) -> c_int {
442        ffi::lua_pushstring(state, ffi::lua_typename(state, ffi::lua_type(state, 1)));
443        1
444    }
445
446    unsafe extern "C-unwind" fn to_lowercase(state: *mut ffi::lua_State) -> c_int {
447        let s = ffi::luaL_checkstring(state, 1);
448        let s = CStr::from_ptr(s);
449        if !s.to_bytes().iter().any(|&c| c.is_ascii_uppercase()) {
450            // If the string does not contain any uppercase ASCII letters, return it as is
451            return 1;
452        }
453        callback_error_ext(state, ptr::null_mut(), true, |extra, _| {
454            let s = (s.to_bytes().iter())
455                .map(|&c| c.to_ascii_lowercase())
456                .collect::<bstr::BString>();
457            (*extra).raw_lua().push(s).map(|_| 1)
458        })
459    }
460
461    let (error, r#type, to_lowercase) = unsafe {
462        lua.exec_raw::<(Function, Function, Function)>((), move |state| {
463            ffi::lua_pushcfunctiond(state, error, cstr!("error"));
464            ffi::lua_pushcfunctiond(state, r#type, cstr!("type"));
465            ffi::lua_pushcfunctiond(state, to_lowercase, cstr!("to_lowercase"));
466        })
467    }?;
468
469    // Prepare environment for the "require" function
470    let env = lua.create_table_with_capacity(0, 7)?;
471    env.raw_set("get_cache_key", get_cache_key)?;
472    env.raw_set("find_current_file", find_current_file)?;
473    env.raw_set("proxyrequire", proxyrequire)?;
474    env.raw_set("REGISTERED_MODULES", registered_modules)?;
475    env.raw_set("LOADER_CACHE", loader_cache)?;
476    env.raw_set("error", error)?;
477    env.raw_set("type", r#type)?;
478    env.raw_set("to_lowercase", to_lowercase)?;
479
480    lua.load(
481        r#"
482        local path = ...
483        if type(path) ~= "string" then
484            error("bad argument #1 to 'require' (string expected, got " .. type(path) .. ")")
485        end
486
487        -- Check if the module (path) is explicitly registered
488        local maybe_result = REGISTERED_MODULES[to_lowercase(path)]
489        if maybe_result ~= nil then
490            return maybe_result
491        end
492
493        local loader = proxyrequire(path, find_current_file())
494        local cache_key = get_cache_key()
495        -- Check if the loader result is already cached
496        local result = LOADER_CACHE[cache_key]
497        if result ~= nil then
498            return result
499        end
500
501        -- Call the loader function and cache the result
502        result = loader()
503        if result == nil then
504            result = true
505        end
506        LOADER_CACHE[cache_key] = result
507        return result
508        "#,
509    )
510    .try_cache()
511    .set_name("=__mlua_require")
512    .set_environment(env)
513    .into_function()
514}
515
516mod fs;