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.
28pub struct DiskFileSource;
29
30impl FileSource for DiskFileSource {
31    fn read(&self, path: &str) -> Option<Vec<u8>> {
32        std::fs::read(path).ok()
33    }
34}
35
36/// Type-erased handle stored in mlua's app-data so the Lua VM can
37/// retrieve the registered source from inside `fs.read` closures.
38pub(crate) type FileSourceHandle = Arc<dyn FileSource>;
39
40/// Register a `FileSource` with the given Lua state. Subsequent calls
41/// to `fs.read` / `fs.read_bytes` consult this source instead of
42/// reading from disk directly.
43pub fn set_file_source(lua: &mlua::Lua, source: Arc<dyn FileSource>) {
44    lua.set_app_data::<FileSourceHandle>(source);
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use std::sync::Arc;
51
52    struct InMemSource(Vec<(String, Vec<u8>)>);
53    impl FileSource for InMemSource {
54        fn read(&self, path: &str) -> Option<Vec<u8>> {
55            self.0
56                .iter()
57                .find(|(p, _)| p == path)
58                .map(|(_, b)| b.clone())
59        }
60    }
61
62    #[test]
63    fn disk_source_reads_real_file() {
64        let tmp = tempfile::NamedTempFile::new().unwrap();
65        std::fs::write(tmp.path(), b"hello").unwrap();
66        let src = DiskFileSource;
67        let bytes = src.read(tmp.path().to_str().unwrap()).unwrap();
68        assert_eq!(bytes, b"hello");
69    }
70
71    #[test]
72    fn disk_source_returns_none_for_missing() {
73        let src = DiskFileSource;
74        assert!(src.read("/definitely/does/not/exist").is_none());
75    }
76
77    #[test]
78    fn in_mem_source_returns_registered_path() {
79        let src = InMemSource(vec![("hi.txt".into(), b"hello".to_vec())]);
80        assert_eq!(src.read("hi.txt").unwrap(), b"hello");
81        assert!(src.read("missing").is_none());
82    }
83
84    #[test]
85    fn set_file_source_stores_handle_in_app_data() {
86        let lua = mlua::Lua::new();
87        let src: Arc<dyn FileSource> = Arc::new(DiskFileSource);
88        set_file_source(&lua, src);
89        assert!(lua.app_data_ref::<FileSourceHandle>().is_some());
90    }
91}