1use std::collections::HashMap;
41use std::path::{Path, PathBuf};
42
43use mlua::{Lua, LuaSerdeExt, Result, Value};
44
45use crate::sandbox::{FsSandbox, InitError, ReadError, SandboxedFs, SymlinkAwareSandbox};
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
510pub struct VendoredResolver {
541 inner: FsResolver,
542}
543
544impl std::fmt::Debug for VendoredResolver {
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 f.debug_struct("VendoredResolver").finish_non_exhaustive()
548 }
549}
550
551impl VendoredResolver {
552 pub fn new(
561 vendored_root: impl Into<PathBuf>,
562 ) -> std::result::Result<Self, crate::sandbox::InitError> {
563 let sandbox = SymlinkAwareSandbox::new(vendored_root)?;
564 let inner = FsResolver::with_sandbox(sandbox);
565 Ok(Self { inner })
566 }
567
568 pub fn from_lockfile(
589 lockfile_path: impl AsRef<Path>,
590 vendored_root: impl AsRef<Path>,
591 ) -> std::result::Result<Self, crate::PkgError> {
592 let vendored_root = vendored_root.as_ref();
593 let lockfile = crate::lockfile::Lockfile::read(lockfile_path)?;
594
595 if !vendored_root.exists() {
598 std::fs::create_dir_all(vendored_root)?;
599 }
600
601 for pkg in &lockfile.pkg {
605 let entry = vendored_root.join(&pkg.name);
606 if std::fs::symlink_metadata(&entry).is_err() {
607 eprintln!(
610 "mlua-pkg: vendored/{} not found — run `mlua-pkg install`",
611 pkg.name
612 );
613 }
614 }
615
616 let sandbox = SymlinkAwareSandbox::new(vendored_root).map_err(|e| {
622 crate::PkgError::Io {
625 source: std::io::Error::new(
626 std::io::ErrorKind::NotFound,
627 format!("vendored root init error: {e}"),
628 ),
629 }
630 })?;
631 let inner = FsResolver::with_sandbox(sandbox);
632
633 Ok(Self { inner })
634 }
635}
636
637impl Resolver for VendoredResolver {
638 fn resolve(&self, lua: &Lua, name: &str) -> Option<mlua::Result<mlua::Value>> {
643 self.inner.resolve(lua, name)
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use crate::sandbox::{FileContent, ReadError};
651
652 fn must_resolve(resolver: &dyn Resolver, lua: &Lua, name: &str) -> Value {
654 match resolver.resolve(lua, name) {
655 Some(Ok(v)) => v,
656 Some(Err(e)) => panic!("resolve('{name}') returned Err: {e}"),
657 None => panic!("resolve('{name}') returned None"),
658 }
659 }
660
661 fn must_resolve_err(resolver: &dyn Resolver, lua: &Lua, name: &str) -> String {
663 match resolver.resolve(lua, name) {
664 Some(Err(e)) => e.to_string(),
665 Some(Ok(_)) => panic!("resolve('{name}') returned Ok, expected Err"),
666 None => panic!("resolve('{name}') returned None, expected Some(Err)"),
667 }
668 }
669
670 fn get_field<V: mlua::FromLua>(value: &Value, key: impl mlua::IntoLua) -> V {
672 value
673 .as_table()
674 .expect("expected Table value")
675 .get::<V>(key)
676 .expect("table field access failed")
677 }
678
679 struct MockSandbox {
681 files: HashMap<PathBuf, String>,
682 }
683
684 impl MockSandbox {
685 fn new() -> Self {
686 Self {
687 files: HashMap::new(),
688 }
689 }
690
691 fn with_file(mut self, path: impl Into<PathBuf>, content: &str) -> Self {
692 self.files.insert(path.into(), content.to_owned());
693 self
694 }
695 }
696
697 impl SandboxedFs for MockSandbox {
698 fn read(&self, relative: &Path) -> std::result::Result<Option<FileContent>, ReadError> {
699 match self.files.get(relative) {
700 Some(content) => Ok(Some(FileContent {
701 content: content.clone(),
702 resolved_path: relative.to_path_buf(),
703 })),
704 None => Ok(None),
705 }
706 }
707 }
708
709 #[test]
710 fn fs_resolver_dot_to_path_conversion() {
711 let mock = MockSandbox::new().with_file("lib/helper.lua", "return { name = 'mocked' }");
712 let resolver = FsResolver::with_sandbox(mock);
713
714 let lua = mlua::Lua::new();
715 let value = must_resolve(&resolver, &lua, "lib.helper");
716 assert_eq!(get_field::<String>(&value, "name"), "mocked");
717 }
718
719 #[test]
720 fn fs_resolver_init_lua_fallback() {
721 let mock = MockSandbox::new().with_file("mypkg/init.lua", "return { from_init = true }");
722 let resolver = FsResolver::with_sandbox(mock);
723
724 let lua = mlua::Lua::new();
725 let value = must_resolve(&resolver, &lua, "mypkg");
726 assert!(get_field::<bool>(&value, "from_init"));
727 }
728
729 #[test]
730 fn fs_resolver_miss_returns_none() {
731 let mock = MockSandbox::new();
732 let resolver = FsResolver::with_sandbox(mock);
733
734 let lua = mlua::Lua::new();
735 assert!(resolver.resolve(&lua, "nonexistent").is_none());
736 }
737
738 #[test]
739 fn fs_resolver_custom_extension() {
740 let mock = MockSandbox::new().with_file("lib/helper.luau", "return { name = 'luau_mod' }");
741 let resolver = FsResolver::with_sandbox(mock).with_extension("luau");
742
743 let lua = mlua::Lua::new();
744 let value = must_resolve(&resolver, &lua, "lib.helper");
745 assert_eq!(get_field::<String>(&value, "name"), "luau_mod");
746 }
747
748 #[test]
749 fn fs_resolver_custom_init_name() {
750 let mock = MockSandbox::new().with_file("mypkg/mod.lua", "return { from_mod = true }");
751 let resolver = FsResolver::with_sandbox(mock).with_init_name("mod");
752
753 let lua = mlua::Lua::new();
754 let value = must_resolve(&resolver, &lua, "mypkg");
755 assert!(get_field::<bool>(&value, "from_mod"));
756 }
757
758 #[test]
759 fn fs_resolver_custom_extension_ignores_default() {
760 let mock = MockSandbox::new().with_file("helper.lua", "return 'wrong'");
762 let resolver = FsResolver::with_sandbox(mock).with_extension("luau");
763
764 let lua = mlua::Lua::new();
765 assert!(resolver.resolve(&lua, "helper").is_none());
766 }
767
768 #[test]
769 fn fs_resolver_with_convention_luau() {
770 let mock = MockSandbox::new()
771 .with_file("lib/helper.luau", "return { name = 'luau' }")
772 .with_file("pkg/init.luau", "return { pkg = true }");
773 let resolver = FsResolver::with_sandbox(mock).with_convention(crate::LuaConvention::LUAU);
774
775 let lua = mlua::Lua::new();
776
777 let value = must_resolve(&resolver, &lua, "lib.helper");
778 assert_eq!(get_field::<String>(&value, "name"), "luau");
779
780 let value = must_resolve(&resolver, &lua, "pkg");
781 assert!(get_field::<bool>(&value, "pkg"));
782 }
783
784 #[test]
785 fn convention_then_override() {
786 let mock = MockSandbox::new().with_file("pkg/mod.luau", "return { ok = true }");
788 let resolver = FsResolver::with_sandbox(mock)
789 .with_convention(crate::LuaConvention::LUAU)
790 .with_init_name("mod");
791
792 let lua = mlua::Lua::new();
793 let value = must_resolve(&resolver, &lua, "pkg");
794 assert!(get_field::<bool>(&value, "ok"));
795 }
796
797 #[test]
798 fn lua_convention_default_is_lua54() {
799 assert_eq!(crate::LuaConvention::default(), crate::LuaConvention::LUA54);
800 }
801
802 #[test]
803 fn asset_resolver_json_to_table() {
804 let mock = MockSandbox::new().with_file("config.json", r#"{"port": 8080}"#);
805 let resolver = AssetResolver::with_sandbox(mock).parser("json", json_parser());
806
807 let lua = mlua::Lua::new();
808 let value = must_resolve(&resolver, &lua, "config.json");
809 assert_eq!(get_field::<i32>(&value, "port"), 8080);
810 }
811
812 #[test]
813 fn asset_resolver_text_to_string() {
814 let mock = MockSandbox::new().with_file("query.sql", "SELECT 1");
815 let resolver = AssetResolver::with_sandbox(mock).parser("sql", text_parser());
816
817 let lua = mlua::Lua::new();
818 let value = must_resolve(&resolver, &lua, "query.sql");
819 let s: String = lua.unpack(value).expect("unpack String failed");
820 assert_eq!(s, "SELECT 1");
821 }
822
823 #[test]
824 fn asset_resolver_unregistered_ext_returns_none() {
825 let mock = MockSandbox::new().with_file("data.xyz", "stuff");
826 let resolver = AssetResolver::with_sandbox(mock).parser("json", json_parser());
827
828 let lua = mlua::Lua::new();
829 assert!(resolver.resolve(&lua, "data.xyz").is_none());
830 }
831
832 #[test]
833 fn asset_resolver_no_ext_returns_none() {
834 let mock = MockSandbox::new();
835 let resolver = AssetResolver::with_sandbox(mock);
836
837 let lua = mlua::Lua::new();
838 assert!(resolver.resolve(&lua, "noext").is_none());
839 }
840
841 #[test]
842 fn asset_resolver_custom_parser() {
843 let mock = MockSandbox::new().with_file("data.csv", "a,b,c");
844 let resolver = AssetResolver::with_sandbox(mock).parser("csv", |lua, content| {
845 let t = lua.create_table()?;
846 for (i, field) in content.split(',').enumerate() {
847 t.set(i + 1, lua.create_string(field)?)?;
848 }
849 Ok(Value::Table(t))
850 });
851
852 let lua = mlua::Lua::new();
853 let value = must_resolve(&resolver, &lua, "data.csv");
854 assert_eq!(get_field::<String>(&value, 1), "a");
855 }
856
857 struct IoErrorSandbox {
861 kind: std::io::ErrorKind,
862 }
863
864 impl SandboxedFs for IoErrorSandbox {
865 fn read(&self, relative: &Path) -> std::result::Result<Option<FileContent>, ReadError> {
866 Err(ReadError::Io {
867 path: relative.to_path_buf(),
868 source: std::io::Error::new(self.kind, "mock I/O error"),
869 })
870 }
871 }
872
873 #[test]
874 fn fs_resolver_propagates_io_error() {
875 let resolver = FsResolver::with_sandbox(IoErrorSandbox {
876 kind: std::io::ErrorKind::PermissionDenied,
877 });
878
879 let lua = mlua::Lua::new();
880 let msg = must_resolve_err(&resolver, &lua, "anything");
881 assert!(
882 msg.contains("I/O error"),
883 "expected ResolveError::Io message: {msg}"
884 );
885 }
886
887 #[test]
888 fn asset_resolver_propagates_io_error() {
889 let resolver = AssetResolver::with_sandbox(IoErrorSandbox {
890 kind: std::io::ErrorKind::PermissionDenied,
891 })
892 .parser("json", json_parser());
893
894 let lua = mlua::Lua::new();
895 let msg = must_resolve_err(&resolver, &lua, "data.json");
896 assert!(
897 msg.contains("I/O error"),
898 "expected ResolveError::Io message: {msg}"
899 );
900 }
901
902 #[test]
905 fn prefix_strips_and_delegates() {
906 let inner = MemoryResolver::new().add("helper", "return 'from helper'");
907 let resolver = PrefixResolver::new("sm", inner);
908
909 let lua = mlua::Lua::new();
910 let value = must_resolve(&resolver, &lua, "sm.helper");
911 let s: String = lua.unpack(value).expect("unpack String failed");
912 assert_eq!(s, "from helper");
913 }
914
915 #[test]
916 fn prefix_non_matching_returns_none() {
917 let inner = MemoryResolver::new().add("helper", "return 'x'");
918 let resolver = PrefixResolver::new("sm", inner);
919
920 let lua = mlua::Lua::new();
921 assert!(resolver.resolve(&lua, "other.helper").is_none());
922 }
923
924 #[test]
925 fn prefix_exact_match_without_separator_returns_none() {
926 let inner = MemoryResolver::new().add("helper", "return 'x'");
927 let resolver = PrefixResolver::new("sm", inner);
928
929 let lua = mlua::Lua::new();
930 assert!(resolver.resolve(&lua, "sm").is_none());
932 }
933
934 #[test]
935 fn prefix_no_substring_match() {
936 let inner = MemoryResolver::new().add("tp", "return 'x'");
937 let resolver = PrefixResolver::new("sm", inner);
938
939 let lua = mlua::Lua::new();
940 assert!(resolver.resolve(&lua, "smtp").is_none());
942 }
943
944 #[test]
945 fn prefix_custom_separator() {
946 let inner = MemoryResolver::new().add("http", "return 'http mod'");
947 let resolver = PrefixResolver::new("@std", inner).with_separator('/');
948
949 let lua = mlua::Lua::new();
950 let value = must_resolve(&resolver, &lua, "@std/http");
951 let s: String = lua.unpack(value).expect("unpack String failed");
952 assert_eq!(s, "http mod");
953 }
954
955 #[test]
956 fn prefix_nested_name() {
957 let mock = MockSandbox::new().with_file("ui/button.lua", "return { name = 'button' }");
958 let resolver = PrefixResolver::new("game", FsResolver::with_sandbox(mock));
959
960 let lua = mlua::Lua::new();
961 let value = must_resolve(&resolver, &lua, "game.ui.button");
963 assert_eq!(get_field::<String>(&value, "name"), "button");
964 }
965
966 #[test]
967 fn prefix_inner_miss_returns_none() {
968 let inner = MemoryResolver::new().add("helper", "return 'x'");
969 let resolver = PrefixResolver::new("sm", inner);
970
971 let lua = mlua::Lua::new();
972 assert!(resolver.resolve(&lua, "sm.nonexistent").is_none());
974 }
975
976 fn write_vendored_lockfile(dir: &Path, pkg_name: &str, entry: &str) -> PathBuf {
980 let content = format!(
981 "version = 1\n\n[[pkg]]\nname = {pkg_name:?}\nsource = \"git+https://github.com/x/{pkg_name}\"\nsha = \"{sha}\"\nentry = {entry:?}\n",
982 sha = "a".repeat(40),
983 );
984 let path = dir.join("mlua-pkg.lock");
985 std::fs::write(&path, content).unwrap();
986 path
987 }
988
989 #[test]
991 fn vendored_from_lockfile_missing_returns_error() {
992 let tmp = tempfile::tempdir().unwrap();
993 let lockfile = tmp.path().join("nonexistent.lock");
994 let vendored = tmp.path().join("vendored");
995
996 let err = VendoredResolver::from_lockfile(&lockfile, &vendored).unwrap_err();
997 assert!(
998 matches!(err, crate::PkgError::MissingLockfile { .. }),
999 "expected MissingLockfile, got: {err}"
1000 );
1001 }
1002
1003 #[test]
1005 fn vendored_resolver_single_pkg_init_lua() {
1006 let tmp = tempfile::tempdir().unwrap();
1007 let vendored = tmp.path().join("vendored");
1008 let lockfile = write_vendored_lockfile(tmp.path(), "foo", ".");
1009
1010 let foo_dir = vendored.join("foo");
1012 std::fs::create_dir_all(&foo_dir).unwrap();
1013 std::fs::write(foo_dir.join("init.lua"), "return { pkg = 'foo' }").unwrap();
1014
1015 let resolver = VendoredResolver::from_lockfile(&lockfile, &vendored).unwrap();
1016 let lua = mlua::Lua::new();
1017
1018 let value = must_resolve(&resolver, &lua, "foo");
1019 assert_eq!(get_field::<String>(&value, "pkg"), "foo");
1020 }
1021
1022 #[test]
1024 fn vendored_resolver_dot_to_path_sub_module() {
1025 let tmp = tempfile::tempdir().unwrap();
1026 let vendored = tmp.path().join("vendored");
1027 let lockfile = write_vendored_lockfile(tmp.path(), "foo", ".");
1028
1029 let foo_dir = vendored.join("foo");
1030 std::fs::create_dir_all(&foo_dir).unwrap();
1031 std::fs::write(foo_dir.join("bar.lua"), "return { sub = 'bar' }").unwrap();
1032
1033 let resolver = VendoredResolver::from_lockfile(&lockfile, &vendored).unwrap();
1034 let lua = mlua::Lua::new();
1035
1036 let value = must_resolve(&resolver, &lua, "foo.bar");
1038 assert_eq!(get_field::<String>(&value, "sub"), "bar");
1039 }
1040
1041 #[test]
1043 fn vendored_new_with_existing_dir() {
1044 let tmp = tempfile::tempdir().unwrap();
1045 let vendored = tmp.path().join("vendored");
1046 std::fs::create_dir_all(&vendored).unwrap();
1047
1048 std::fs::write(vendored.join("mypkg.lua"), "return 'direct'").unwrap();
1050
1051 let resolver = VendoredResolver::new(&vendored).unwrap();
1052 let lua = mlua::Lua::new();
1053
1054 let value = must_resolve(&resolver, &lua, "mypkg");
1055 let s: String = lua.unpack(value).unwrap();
1056 assert_eq!(s, "direct");
1057 }
1058
1059 #[test]
1061 fn vendored_resolver_is_send_sync() {
1062 fn assert_send_sync<T: Send + Sync>() {}
1063 assert_send_sync::<VendoredResolver>();
1064 }
1065}