diff --git a/Cargo.lock b/Cargo.lock index 3e3a753..6c67588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1651,6 +1651,9 @@ dependencies = [ "once_cell", "path-clean", "pathdiff", + "serde", + "serde_json", + "tokio", ] [[package]] diff --git a/crates/lune-std/src/globals/require/alias.rs b/crates/lune-std/src/globals/require/alias.rs index 38a822a..70e7aaf 100644 --- a/crates/lune-std/src/globals/require/alias.rs +++ b/crates/lune-std/src/globals/require/alias.rs @@ -1,8 +1,8 @@ use mlua::prelude::*; use lune_utils::{ - luaurc::LuauRc, path::{clean_path_and_make_absolute, diff_path, get_current_dir}, + LuauRc, }; use super::context::*; diff --git a/crates/lune-std/src/globals/require/context.rs b/crates/lune-std/src/globals/require/context.rs index 8e2ded3..0355d27 100644 --- a/crates/lune-std/src/globals/require/context.rs +++ b/crates/lune-std/src/globals/require/context.rs @@ -27,9 +27,9 @@ use crate::library::LuneStandardLibrary; */ #[derive(Debug, Clone)] pub(super) struct RequireContext { - cache_libraries: Arc>>>, - cache_results: Arc>>>, - cache_pending: Arc>>>, + libraries: Arc>>>, + results: Arc>>>, + pending: Arc>>>, } impl RequireContext { @@ -42,9 +42,9 @@ impl RequireContext { */ pub fn new() -> Self { Self { - cache_libraries: Arc::new(AsyncMutex::new(HashMap::new())), - cache_results: Arc::new(AsyncMutex::new(HashMap::new())), - cache_pending: Arc::new(AsyncMutex::new(HashMap::new())), + libraries: Arc::new(AsyncMutex::new(HashMap::new())), + results: Arc::new(AsyncMutex::new(HashMap::new())), + pending: Arc::new(AsyncMutex::new(HashMap::new())), } } @@ -57,7 +57,6 @@ impl RequireContext { absolute path by prepending the current working directory. */ pub fn resolve_paths( - &self, source: impl AsRef, path: impl AsRef, ) -> LuaResult<(PathBuf, PathBuf)> { @@ -66,7 +65,7 @@ impl RequireContext { .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))? .join(path.as_ref()); - let abs_path = clean_path_and_make_absolute(path); + let abs_path = clean_path_and_make_absolute(&path); let rel_path = clean_path(path); Ok((abs_path, rel_path)) @@ -77,7 +76,7 @@ impl RequireContext { */ pub fn is_cached(&self, abs_path: impl AsRef) -> LuaResult { let is_cached = self - .cache_results + .results .try_lock() .expect("RequireContext may not be used from multiple threads") .contains_key(abs_path.as_ref()); @@ -89,7 +88,7 @@ impl RequireContext { */ pub fn is_pending(&self, abs_path: impl AsRef) -> LuaResult { let is_pending = self - .cache_pending + .pending .try_lock() .expect("RequireContext may not be used from multiple threads") .contains_key(abs_path.as_ref()); @@ -107,7 +106,7 @@ impl RequireContext { abs_path: impl AsRef, ) -> LuaResult> { let results = self - .cache_results + .results .try_lock() .expect("RequireContext may not be used from multiple threads"); @@ -137,7 +136,7 @@ impl RequireContext { ) -> LuaResult> { let mut thread_recv = { let pending = self - .cache_pending + .pending .try_lock() .expect("RequireContext may not be used from multiple threads"); let thread_id = pending @@ -200,7 +199,7 @@ impl RequireContext { // Set this abs path as currently pending let (broadcast_tx, _) = broadcast::channel(1); - self.cache_pending + self.pending .try_lock() .expect("RequireContext may not be used from multiple threads") .insert(abs_path.to_path_buf(), broadcast_tx); @@ -220,7 +219,7 @@ impl RequireContext { // NOTE: We use the async lock and not try_lock here because // some other thread may be wanting to insert into the require // cache at the same time, and that's not an actual error case - self.cache_results + self.results .lock() .await .insert(abs_path.to_path_buf(), load_res); @@ -229,7 +228,7 @@ impl RequireContext { // broadcast a message to let any listeners know that this // path has now finished the require process and is cached let broadcast_tx = self - .cache_pending + .pending .try_lock() .expect("RequireContext may not be used from multiple threads") .remove(abs_path) @@ -253,7 +252,7 @@ impl RequireContext { }; let mut cache = self - .cache_libraries + .libraries .try_lock() .expect("RequireContext may not be used from multiple threads"); diff --git a/crates/lune-std/src/globals/require/builtin.rs b/crates/lune-std/src/globals/require/library.rs similarity index 83% rename from crates/lune-std/src/globals/require/builtin.rs rename to crates/lune-std/src/globals/require/library.rs index d75ddf5..b47ea92 100644 --- a/crates/lune-std/src/globals/require/builtin.rs +++ b/crates/lune-std/src/globals/require/library.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use super::context::*; -pub(super) async fn require<'lua, 'ctx>( +pub(super) fn require<'lua, 'ctx>( lua: &'lua Lua, ctx: &'ctx RequireContext, name: &str, diff --git a/crates/lune-std/src/globals/require/mod.rs b/crates/lune-std/src/globals/require/mod.rs index 0292797..3876e36 100644 --- a/crates/lune-std/src/globals/require/mod.rs +++ b/crates/lune-std/src/globals/require/mod.rs @@ -6,7 +6,7 @@ mod context; use context::RequireContext; mod alias; -mod builtin; +mod library; mod path; const REQUIRE_IMPL: &str = r" @@ -36,7 +36,7 @@ pub fn create(lua: &Lua) -> LuaResult { */ let require_fn = lua.create_async_function(require)?; - let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) { + let get_source_fn = lua.create_function(move |lua, (): ()| match lua.inspect_stack(2) { None => Err(LuaError::runtime( "Failed to get stack info for require source", )), @@ -80,11 +80,8 @@ async fn require<'lua>( .app_data_ref() .expect("Failed to get RequireContext from app data"); - if let Some(builtin_name) = path - .strip_prefix("@lune/") - .map(|name| name.to_ascii_lowercase()) - { - builtin::require(lua, &context, &builtin_name).await + if let Some(builtin_name) = path.strip_prefix("@lune/").map(str::to_ascii_lowercase) { + library::require(lua, &context, &builtin_name) } else if let Some(aliased_path) = path.strip_prefix('@') { let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime( "Require with custom alias must contain '/' delimiter", diff --git a/crates/lune-std/src/globals/require/path.rs b/crates/lune-std/src/globals/require/path.rs index 7e8084f..1fabebf 100644 --- a/crates/lune-std/src/globals/require/path.rs +++ b/crates/lune-std/src/globals/require/path.rs @@ -14,7 +14,7 @@ pub(super) async fn require<'lua, 'ctx>( where 'lua: 'ctx, { - let (abs_path, rel_path) = ctx.resolve_paths(source, path)?; + let (abs_path, rel_path) = RequireContext::resolve_paths(source, path)?; require_abs_rel(lua, ctx, abs_path, rel_path).await } diff --git a/crates/lune-utils/Cargo.toml b/crates/lune-utils/Cargo.toml index e3a2fed..05c7020 100644 --- a/crates/lune-utils/Cargo.toml +++ b/crates/lune-utils/Cargo.toml @@ -13,7 +13,12 @@ workspace = true [dependencies] mlua = { version = "0.9.7", features = ["async"] } +tokio = { version = "1", default-features = false, features = ["fs"] } + dunce = "1.0" once_cell = "1.17" path-clean = "1.0" pathdiff = "0.2" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/crates/lune-utils/src/lib.rs b/crates/lune-utils/src/lib.rs index 38738cc..4dd2ed9 100644 --- a/crates/lune-utils/src/lib.rs +++ b/crates/lune-utils/src/lib.rs @@ -1,9 +1,11 @@ #![allow(clippy::cargo_common_metadata)] +mod luaurc; mod table_builder; mod version_string; pub mod path; +pub use self::luaurc::LuauRc; pub use self::table_builder::TableBuilder; pub use self::version_string::get_version_string; diff --git a/crates/lune-utils/src/luaurc.rs b/crates/lune-utils/src/luaurc.rs new file mode 100644 index 0000000..415cc2d --- /dev/null +++ b/crates/lune-utils/src/luaurc.rs @@ -0,0 +1,164 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf, MAIN_SEPARATOR}, + sync::Arc, +}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use tokio::fs::read; + +use crate::path::{clean_path, clean_path_and_make_absolute}; + +const LUAURC_FILE: &str = ".luaurc"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum LuauLanguageMode { + NoCheck, + NonStrict, + Strict, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct LuauRcConfig { + #[serde(skip_serializing_if = "Option::is_none")] + language_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + lint: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + lint_errors: Option, + #[serde(skip_serializing_if = "Option::is_none")] + type_errors: Option, + #[serde(skip_serializing_if = "Option::is_none")] + globals: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + paths: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + aliases: Option>, +} + +/** + A deserialized `.luaurc` file. + + Contains utility methods for validating and searching for aliases. +*/ +#[derive(Debug, Clone)] +pub struct LuauRc { + dir: Arc, + config: LuauRcConfig, +} + +impl LuauRc { + /** + Reads a `.luaurc` file from the given directory. + + If the file does not exist, or if it is invalid, this function returns `None`. + */ + pub async fn read(dir: impl AsRef) -> Option { + let dir = clean_path_and_make_absolute(dir); + let path = dir.join(LUAURC_FILE); + let bytes = read(&path).await.ok()?; + let config = serde_json::from_slice(&bytes).ok()?; + Some(Self { + dir: dir.into(), + config, + }) + } + + /** + Reads a `.luaurc` file from the given directory, and then recursively searches + for a `.luaurc` file in the parent directories if a predicate is not satisfied. + + If no `.luaurc` file exists, or if they are invalid, this function returns `None`. + */ + pub async fn read_recursive( + dir: impl AsRef, + mut predicate: impl FnMut(&Self) -> bool, + ) -> Option { + let mut current = clean_path_and_make_absolute(dir); + loop { + if let Some(rc) = Self::read(¤t).await { + if predicate(&rc) { + return Some(rc); + } + } + if let Some(parent) = current.parent() { + current = parent.to_path_buf(); + } else { + return None; + } + } + } + + /** + Validates that the `.luaurc` file is correct. + + This primarily validates aliases since they are not + validated during creation of the [`LuauRc`] struct. + + # Errors + + If an alias key is invalid. + */ + pub fn validate(&self) -> Result<(), String> { + if let Some(aliases) = &self.config.aliases { + for alias in aliases.keys() { + if !is_valid_alias_key(alias) { + return Err(format!("invalid alias key: {alias}")); + } + } + } + Ok(()) + } + + /** + Gets a copy of all aliases in the `.luaurc` file. + + Will return an empty map if there are no aliases. + */ + #[must_use] + pub fn aliases(&self) -> HashMap { + self.config.aliases.clone().unwrap_or_default() + } + + /** + Finds an alias in the `.luaurc` file by name. + + If the alias does not exist, this function returns `None`. + */ + #[must_use] + pub fn find_alias(&self, name: &str) -> Option { + self.config.aliases.as_ref().and_then(|aliases| { + aliases.iter().find_map(|(alias, path)| { + if alias + .trim_end_matches(MAIN_SEPARATOR) + .eq_ignore_ascii_case(name) + && is_valid_alias_key(alias) + { + Some(clean_path(self.dir.join(path))) + } else { + None + } + }) + }) + } +} + +fn is_valid_alias_key(alias: impl AsRef) -> bool { + let alias = alias.as_ref(); + if alias.is_empty() + || alias.starts_with('.') + || alias.starts_with("..") + || alias.chars().any(|c| c == MAIN_SEPARATOR) + { + false // Paths are not valid alias keys + } else { + alias.chars().all(is_valid_alias_char) + } +} + +fn is_valid_alias_char(c: char) -> bool { + c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' +}