1use std::collections::HashMap;
41use std::path::{Path, PathBuf};
42
43use mlua::{Lua, LuaSerdeExt, Result, Value};
44
45use crate::sandbox::{FsSandbox, InitError, ReadError, SandboxedFs};
46use crate::{ResolveError, Resolver};
47
48type NativeFactory = Box<dyn Fn(&Lua) -> Result<Value> + Send + Sync>;
49
50fn read_to_resolve_error(err: ReadError, name: &str, sanitized_path: &Path) -> ResolveError {
60 match err {
61 ReadError::Traversal { .. } => ResolveError::PathTraversal {
62 name: name.to_owned(),
63 },
64 ReadError::Io { source, .. } => ResolveError::Io {
65 path: sanitized_path.to_path_buf(),
66 source,
67 },
68 }
69}
70
71pub struct MemoryResolver {
89 modules: HashMap<String, String>,
90}
91
92impl Default for MemoryResolver {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl MemoryResolver {
99 pub fn new() -> Self {
100 Self {
101 modules: HashMap::new(),
102 }
103 }
104
105 pub fn add(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
107 self.modules.insert(name.into(), source.into());
108 self
109 }
110}
111
112impl Resolver for MemoryResolver {
113 fn resolve(&self, lua: &Lua, name: &str) -> Option<Result<Value>> {
114 let source = self.modules.get(name)?;
115 Some(lua.load(source.as_str()).set_name(name).eval())
116 }
117}
118
119pub struct NativeResolver {
136 modules: HashMap<String, NativeFactory>,
137}
138
139impl Default for NativeResolver {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl NativeResolver {
146 pub fn new() -> Self {
147 Self {
148 modules: HashMap::new(),
149 }
150 }
151
152 pub fn add(
154 mut self,
155 name: impl Into<String>,
156 factory: impl Fn(&Lua) -> Result<Value> + Send + Sync + 'static,
157 ) -> Self {
158 self.modules.insert(name.into(), Box::new(factory));
159 self
160 }
161}
162
163impl Resolver for NativeResolver {
164 fn resolve(&self, lua: &Lua, name: &str) -> Option<Result<Value>> {
165 let factory = self.modules.get(name)?;
166 Some(factory(lua))
167 }
168}
169
170pub struct FsResolver {
191 sandbox: Box<dyn SandboxedFs>,
192 extension: String,
193 init_name: String,
194 module_separator: char,
195}
196
197impl FsResolver {
198 pub fn new(root: impl Into<PathBuf>) -> std::result::Result<Self, InitError> {
200 let fs = FsSandbox::new(root)?;
201 Ok(Self::with_sandbox(fs))
202 }
203
204 pub fn with_sandbox(sandbox: impl SandboxedFs + 'static) -> Self {
206 let conv = crate::LuaConvention::default();
207 Self {
208 sandbox: Box::new(sandbox),
209 extension: conv.extension.to_owned(),
210 init_name: conv.init_name.to_owned(),
211 module_separator: conv.module_separator,
212 }
213 }
214
215 pub fn with_convention(self, conv: crate::LuaConvention) -> Self {
217 Self {
218 extension: conv.extension.to_owned(),
219 init_name: conv.init_name.to_owned(),
220 module_separator: conv.module_separator,
221 ..self
222 }
223 }
224
225 pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
227 self.extension = ext.into();
228 self
229 }
230
231 pub fn with_init_name(mut self, name: impl Into<String>) -> Self {
235 self.init_name = name.into();
236 self
237 }
238
239 pub fn with_module_separator(mut self, sep: char) -> Self {
243 self.module_separator = sep;
244 self
245 }
246}
247
248impl Resolver for FsResolver {
249 fn resolve(&self, lua: &Lua, name: &str) -> Option<Result<Value>> {
250 let relative = name.replace(self.module_separator, "/");
251
252 let candidates = [
253 PathBuf::from(format!("{relative}.{}", self.extension)),
254 PathBuf::from(format!("{relative}/{}.{}", self.init_name, self.extension)),
255 ];
256
257 for candidate in &candidates {
258 match self.sandbox.read(candidate) {
259 Ok(Some(file)) => {
260 let source_name = candidate.display().to_string();
261 return Some(lua.load(file.content).set_name(source_name).eval());
262 }
263 Ok(None) => continue,
264 Err(e) => {
265 return Some(Err(mlua::Error::external(read_to_resolve_error(
266 e, name, candidate,
267 ))));
268 }
269 }
270 }
271
272 None
273 }
274}
275
276type AssetParserFn = Box<dyn Fn(&Lua, &str) -> Result<Value> + Send + Sync>;
279
280pub struct AssetResolver {
344 sandbox: Box<dyn SandboxedFs>,
345 parsers: HashMap<String, AssetParserFn>,
346}
347
348impl AssetResolver {
349 pub fn new(root: impl Into<PathBuf>) -> std::result::Result<Self, InitError> {
351 let fs = FsSandbox::new(root)?;
352 Ok(Self::with_sandbox(fs))
353 }
354
355 pub fn with_sandbox(sandbox: impl SandboxedFs + 'static) -> Self {
357 Self {
358 sandbox: Box::new(sandbox),
359 parsers: HashMap::new(),
360 }
361 }
362
363 pub fn parser(
365 mut self,
366 ext: impl Into<String>,
367 f: impl Fn(&Lua, &str) -> Result<Value> + Send + Sync + 'static,
368 ) -> Self {
369 self.parsers.insert(ext.into(), Box::new(f));
370 self
371 }
372}
373
374pub fn json_parser() -> impl Fn(&Lua, &str) -> Result<Value> + Send + Sync {
379 |lua, content| {
380 let json: serde_json::Value = serde_json::from_str(content).map_err(|e| {
381 mlua::Error::external(ResolveError::AssetParse {
382 source: Box::new(e),
383 })
384 })?;
385 lua.to_value(&json)
386 }
387}
388
389pub fn text_parser() -> impl Fn(&Lua, &str) -> Result<Value> + Send + Sync {
394 |lua, content| lua.create_string(content).map(Value::String)
395}
396
397impl Resolver for AssetResolver {
398 fn resolve(&self, lua: &Lua, name: &str) -> Option<Result<Value>> {
399 let ext = Path::new(name).extension()?.to_str()?;
400 let parser = self.parsers.get(ext)?;
401
402 let asset_path = Path::new(name);
403 let file = match self.sandbox.read(asset_path) {
404 Ok(Some(file)) => file,
405 Ok(None) => return None,
406 Err(e) => {
407 return Some(Err(mlua::Error::external(read_to_resolve_error(
408 e, name, asset_path,
409 ))));
410 }
411 };
412
413 Some(parser(lua, &file.content))
414 }
415}
416
417pub struct PrefixResolver {
469 prefix: String,
470 separator: char,
471 inner: Box<dyn Resolver>,
472}
473
474impl PrefixResolver {
475 pub fn new(prefix: impl Into<String>, inner: impl Resolver + 'static) -> Self {
479 Self {
480 prefix: prefix.into(),
481 separator: crate::LuaConvention::default().module_separator,
482 inner: Box::new(inner),
483 }
484 }
485
486 pub fn with_convention(mut self, conv: crate::LuaConvention) -> Self {
488 self.separator = conv.module_separator;
489 self
490 }
491
492 pub fn with_separator(mut self, separator: char) -> Self {
494 self.separator = separator;
495 self
496 }
497}
498
499impl Resolver for PrefixResolver {
500 fn resolve(&self, lua: &Lua, name: &str) -> Option<Result<Value>> {
501 let mut prefix_with_sep = String::with_capacity(self.prefix.len() + 1);
502 prefix_with_sep.push_str(&self.prefix);
503 prefix_with_sep.push(self.separator);
504
505 let rest = name.strip_prefix(&prefix_with_sep)?;
506 self.inner.resolve(lua, rest)
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::sandbox::{FileContent, ReadError};
514
515 fn must_resolve(resolver: &dyn Resolver, lua: &Lua, name: &str) -> Value {
517 match resolver.resolve(lua, name) {
518 Some(Ok(v)) => v,
519 Some(Err(e)) => panic!("resolve('{name}') returned Err: {e}"),
520 None => panic!("resolve('{name}') returned None"),
521 }
522 }
523
524 fn must_resolve_err(resolver: &dyn Resolver, lua: &Lua, name: &str) -> String {
526 match resolver.resolve(lua, name) {
527 Some(Err(e)) => e.to_string(),
528 Some(Ok(_)) => panic!("resolve('{name}') returned Ok, expected Err"),
529 None => panic!("resolve('{name}') returned None, expected Some(Err)"),
530 }
531 }
532
533 fn get_field<V: mlua::FromLua>(value: &Value, key: impl mlua::IntoLua) -> V {
535 value
536 .as_table()
537 .expect("expected Table value")
538 .get::<V>(key)
539 .expect("table field access failed")
540 }
541
542 struct MockSandbox {
544 files: HashMap<PathBuf, String>,
545 }
546
547 impl MockSandbox {
548 fn new() -> Self {
549 Self {
550 files: HashMap::new(),
551 }
552 }
553
554 fn with_file(mut self, path: impl Into<PathBuf>, content: &str) -> Self {
555 self.files.insert(path.into(), content.to_owned());
556 self
557 }
558 }
559
560 impl SandboxedFs for MockSandbox {
561 fn read(&self, relative: &Path) -> std::result::Result<Option<FileContent>, ReadError> {
562 match self.files.get(relative) {
563 Some(content) => Ok(Some(FileContent {
564 content: content.clone(),
565 resolved_path: relative.to_path_buf(),
566 })),
567 None => Ok(None),
568 }
569 }
570 }
571
572 #[test]
573 fn fs_resolver_dot_to_path_conversion() {
574 let mock = MockSandbox::new().with_file("lib/helper.lua", "return { name = 'mocked' }");
575 let resolver = FsResolver::with_sandbox(mock);
576
577 let lua = mlua::Lua::new();
578 let value = must_resolve(&resolver, &lua, "lib.helper");
579 assert_eq!(get_field::<String>(&value, "name"), "mocked");
580 }
581
582 #[test]
583 fn fs_resolver_init_lua_fallback() {
584 let mock = MockSandbox::new().with_file("mypkg/init.lua", "return { from_init = true }");
585 let resolver = FsResolver::with_sandbox(mock);
586
587 let lua = mlua::Lua::new();
588 let value = must_resolve(&resolver, &lua, "mypkg");
589 assert!(get_field::<bool>(&value, "from_init"));
590 }
591
592 #[test]
593 fn fs_resolver_miss_returns_none() {
594 let mock = MockSandbox::new();
595 let resolver = FsResolver::with_sandbox(mock);
596
597 let lua = mlua::Lua::new();
598 assert!(resolver.resolve(&lua, "nonexistent").is_none());
599 }
600
601 #[test]
602 fn fs_resolver_custom_extension() {
603 let mock = MockSandbox::new().with_file("lib/helper.luau", "return { name = 'luau_mod' }");
604 let resolver = FsResolver::with_sandbox(mock).with_extension("luau");
605
606 let lua = mlua::Lua::new();
607 let value = must_resolve(&resolver, &lua, "lib.helper");
608 assert_eq!(get_field::<String>(&value, "name"), "luau_mod");
609 }
610
611 #[test]
612 fn fs_resolver_custom_init_name() {
613 let mock = MockSandbox::new().with_file("mypkg/mod.lua", "return { from_mod = true }");
614 let resolver = FsResolver::with_sandbox(mock).with_init_name("mod");
615
616 let lua = mlua::Lua::new();
617 let value = must_resolve(&resolver, &lua, "mypkg");
618 assert!(get_field::<bool>(&value, "from_mod"));
619 }
620
621 #[test]
622 fn fs_resolver_custom_extension_ignores_default() {
623 let mock = MockSandbox::new().with_file("helper.lua", "return 'wrong'");
625 let resolver = FsResolver::with_sandbox(mock).with_extension("luau");
626
627 let lua = mlua::Lua::new();
628 assert!(resolver.resolve(&lua, "helper").is_none());
629 }
630
631 #[test]
632 fn fs_resolver_with_convention_luau() {
633 let mock = MockSandbox::new()
634 .with_file("lib/helper.luau", "return { name = 'luau' }")
635 .with_file("pkg/init.luau", "return { pkg = true }");
636 let resolver = FsResolver::with_sandbox(mock).with_convention(crate::LuaConvention::LUAU);
637
638 let lua = mlua::Lua::new();
639
640 let value = must_resolve(&resolver, &lua, "lib.helper");
641 assert_eq!(get_field::<String>(&value, "name"), "luau");
642
643 let value = must_resolve(&resolver, &lua, "pkg");
644 assert!(get_field::<bool>(&value, "pkg"));
645 }
646
647 #[test]
648 fn convention_then_override() {
649 let mock = MockSandbox::new().with_file("pkg/mod.luau", "return { ok = true }");
651 let resolver = FsResolver::with_sandbox(mock)
652 .with_convention(crate::LuaConvention::LUAU)
653 .with_init_name("mod");
654
655 let lua = mlua::Lua::new();
656 let value = must_resolve(&resolver, &lua, "pkg");
657 assert!(get_field::<bool>(&value, "ok"));
658 }
659
660 #[test]
661 fn lua_convention_default_is_lua54() {
662 assert_eq!(crate::LuaConvention::default(), crate::LuaConvention::LUA54);
663 }
664
665 #[test]
666 fn asset_resolver_json_to_table() {
667 let mock = MockSandbox::new().with_file("config.json", r#"{"port": 8080}"#);
668 let resolver = AssetResolver::with_sandbox(mock).parser("json", json_parser());
669
670 let lua = mlua::Lua::new();
671 let value = must_resolve(&resolver, &lua, "config.json");
672 assert_eq!(get_field::<i32>(&value, "port"), 8080);
673 }
674
675 #[test]
676 fn asset_resolver_text_to_string() {
677 let mock = MockSandbox::new().with_file("query.sql", "SELECT 1");
678 let resolver = AssetResolver::with_sandbox(mock).parser("sql", text_parser());
679
680 let lua = mlua::Lua::new();
681 let value = must_resolve(&resolver, &lua, "query.sql");
682 let s: String = lua.unpack(value).expect("unpack String failed");
683 assert_eq!(s, "SELECT 1");
684 }
685
686 #[test]
687 fn asset_resolver_unregistered_ext_returns_none() {
688 let mock = MockSandbox::new().with_file("data.xyz", "stuff");
689 let resolver = AssetResolver::with_sandbox(mock).parser("json", json_parser());
690
691 let lua = mlua::Lua::new();
692 assert!(resolver.resolve(&lua, "data.xyz").is_none());
693 }
694
695 #[test]
696 fn asset_resolver_no_ext_returns_none() {
697 let mock = MockSandbox::new();
698 let resolver = AssetResolver::with_sandbox(mock);
699
700 let lua = mlua::Lua::new();
701 assert!(resolver.resolve(&lua, "noext").is_none());
702 }
703
704 #[test]
705 fn asset_resolver_custom_parser() {
706 let mock = MockSandbox::new().with_file("data.csv", "a,b,c");
707 let resolver = AssetResolver::with_sandbox(mock).parser("csv", |lua, content| {
708 let t = lua.create_table()?;
709 for (i, field) in content.split(',').enumerate() {
710 t.set(i + 1, lua.create_string(field)?)?;
711 }
712 Ok(Value::Table(t))
713 });
714
715 let lua = mlua::Lua::new();
716 let value = must_resolve(&resolver, &lua, "data.csv");
717 assert_eq!(get_field::<String>(&value, 1), "a");
718 }
719
720 struct IoErrorSandbox {
724 kind: std::io::ErrorKind,
725 }
726
727 impl SandboxedFs for IoErrorSandbox {
728 fn read(&self, relative: &Path) -> std::result::Result<Option<FileContent>, ReadError> {
729 Err(ReadError::Io {
730 path: relative.to_path_buf(),
731 source: std::io::Error::new(self.kind, "mock I/O error"),
732 })
733 }
734 }
735
736 #[test]
737 fn fs_resolver_propagates_io_error() {
738 let resolver = FsResolver::with_sandbox(IoErrorSandbox {
739 kind: std::io::ErrorKind::PermissionDenied,
740 });
741
742 let lua = mlua::Lua::new();
743 let msg = must_resolve_err(&resolver, &lua, "anything");
744 assert!(
745 msg.contains("I/O error"),
746 "expected ResolveError::Io message: {msg}"
747 );
748 }
749
750 #[test]
751 fn asset_resolver_propagates_io_error() {
752 let resolver = AssetResolver::with_sandbox(IoErrorSandbox {
753 kind: std::io::ErrorKind::PermissionDenied,
754 })
755 .parser("json", json_parser());
756
757 let lua = mlua::Lua::new();
758 let msg = must_resolve_err(&resolver, &lua, "data.json");
759 assert!(
760 msg.contains("I/O error"),
761 "expected ResolveError::Io message: {msg}"
762 );
763 }
764
765 #[test]
768 fn prefix_strips_and_delegates() {
769 let inner = MemoryResolver::new().add("helper", "return 'from helper'");
770 let resolver = PrefixResolver::new("sm", inner);
771
772 let lua = mlua::Lua::new();
773 let value = must_resolve(&resolver, &lua, "sm.helper");
774 let s: String = lua.unpack(value).expect("unpack String failed");
775 assert_eq!(s, "from helper");
776 }
777
778 #[test]
779 fn prefix_non_matching_returns_none() {
780 let inner = MemoryResolver::new().add("helper", "return 'x'");
781 let resolver = PrefixResolver::new("sm", inner);
782
783 let lua = mlua::Lua::new();
784 assert!(resolver.resolve(&lua, "other.helper").is_none());
785 }
786
787 #[test]
788 fn prefix_exact_match_without_separator_returns_none() {
789 let inner = MemoryResolver::new().add("helper", "return 'x'");
790 let resolver = PrefixResolver::new("sm", inner);
791
792 let lua = mlua::Lua::new();
793 assert!(resolver.resolve(&lua, "sm").is_none());
795 }
796
797 #[test]
798 fn prefix_no_substring_match() {
799 let inner = MemoryResolver::new().add("tp", "return 'x'");
800 let resolver = PrefixResolver::new("sm", inner);
801
802 let lua = mlua::Lua::new();
803 assert!(resolver.resolve(&lua, "smtp").is_none());
805 }
806
807 #[test]
808 fn prefix_custom_separator() {
809 let inner = MemoryResolver::new().add("http", "return 'http mod'");
810 let resolver = PrefixResolver::new("@std", inner).with_separator('/');
811
812 let lua = mlua::Lua::new();
813 let value = must_resolve(&resolver, &lua, "@std/http");
814 let s: String = lua.unpack(value).expect("unpack String failed");
815 assert_eq!(s, "http mod");
816 }
817
818 #[test]
819 fn prefix_nested_name() {
820 let mock = MockSandbox::new().with_file("ui/button.lua", "return { name = 'button' }");
821 let resolver = PrefixResolver::new("game", FsResolver::with_sandbox(mock));
822
823 let lua = mlua::Lua::new();
824 let value = must_resolve(&resolver, &lua, "game.ui.button");
826 assert_eq!(get_field::<String>(&value, "name"), "button");
827 }
828
829 #[test]
830 fn prefix_inner_miss_returns_none() {
831 let inner = MemoryResolver::new().add("helper", "return 'x'");
832 let resolver = PrefixResolver::new("sm", inner);
833
834 let lua = mlua::Lua::new();
835 assert!(resolver.resolve(&lua, "sm.nonexistent").is_none());
837 }
838}