Skip to main content

assay/lua/
file_source.rs

1//! Pluggable file source for Lua's `fs.read` / `fs.read_bytes`.
2//!
3//! Consumers (e.g. embedded-app binaries) implement `FileSource` to
4//! redirect the runtime's filesystem reads through their own backing
5//! store — typically a `rust-embed` virtual FS plus optional disk
6//! overlays for operator config.
7//!
8//! When no FileSource is registered, `fs.read` falls back to direct
9//! disk reads (preserving the standalone `assay run script.lua`
10//! behaviour).
11
12use std::sync::Arc;
13
14/// Anything that can resolve a path string to bytes. Implementations
15/// MUST be cheap to clone (we hold `Arc<dyn FileSource>`) and `Send +
16/// Sync` so they can live in mlua's app-data across async tasks.
17pub trait FileSource: Send + Sync {
18    /// Return the bytes at `path`, or `None` if the path is unknown.
19    /// Errors during read are surfaced as `None`; this matches Lua's
20    /// existing "missing file" behaviour where `fs.read` raises a
21    /// runtime error.
22    fn read(&self, path: &str) -> Option<Vec<u8>>;
23}
24
25/// Default FileSource that reads directly from disk. Available for
26/// callers who want to register a source explicitly but keep
27/// disk-backed semantics.
28#[allow(dead_code)]
29pub struct DiskFileSource;
30
31impl FileSource for DiskFileSource {
32    fn read(&self, path: &str) -> Option<Vec<u8>> {
33        std::fs::read(path).ok()
34    }
35}
36
37/// Type-erased handle stored in mlua's app-data so the Lua VM can
38/// retrieve the registered source from inside `fs.read` closures.
39pub(crate) type FileSourceHandle = Arc<dyn FileSource>;
40
41/// Register a `FileSource` with the given Lua state. Subsequent calls
42/// to `fs.read` / `fs.read_bytes` consult this source instead of
43/// reading from disk directly.
44#[allow(dead_code)]
45pub fn set_file_source(lua: &mlua::Lua, source: Arc<dyn FileSource>) {
46    lua.set_app_data::<FileSourceHandle>(source);
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use std::sync::Arc;
53
54    struct InMemSource(Vec<(String, Vec<u8>)>);
55    impl FileSource for InMemSource {
56        fn read(&self, path: &str) -> Option<Vec<u8>> {
57            self.0
58                .iter()
59                .find(|(p, _)| p == path)
60                .map(|(_, b)| b.clone())
61        }
62    }
63
64    #[test]
65    fn disk_source_reads_real_file() {
66        let tmp = tempfile::NamedTempFile::new().unwrap();
67        std::fs::write(tmp.path(), b"hello").unwrap();
68        let src = DiskFileSource;
69        let bytes = src.read(tmp.path().to_str().unwrap()).unwrap();
70        assert_eq!(bytes, b"hello");
71    }
72
73    #[test]
74    fn disk_source_returns_none_for_missing() {
75        let src = DiskFileSource;
76        assert!(src.read("/definitely/does/not/exist").is_none());
77    }
78
79    #[test]
80    fn in_mem_source_returns_registered_path() {
81        let src = InMemSource(vec![("hi.txt".into(), b"hello".to_vec())]);
82        assert_eq!(src.read("hi.txt").unwrap(), b"hello");
83        assert!(src.read("missing").is_none());
84    }
85
86    #[test]
87    fn set_file_source_stores_handle_in_app_data() {
88        let lua = mlua::Lua::new();
89        let src: Arc<dyn FileSource> = Arc::new(DiskFileSource);
90        set_file_source(&lua, src);
91        assert!(lua.app_data_ref::<FileSourceHandle>().is_some());
92    }
93}