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