diff --git a/src/enforcer.rs b/src/enforcer.rs index a997e5f1..c63cf36c 100644 --- a/src/enforcer.rs +++ b/src/enforcer.rs @@ -14,6 +14,8 @@ use crate::{ Result, }; +use crate::model::DefaultModel; + #[cfg(any(feature = "logging", feature = "watcher"))] use crate::emitter::notify_logger_and_watcher; @@ -149,10 +151,21 @@ impl Enforcer { let mut eft_stream = self.eft.new_stream(&e_ast.value, max(policy_len, 1)); - let m_ast_compiled = self - .engine - .compile_expression(escape_eval(&m_ast.value)) - .map_err(Into::>::into)?; + let m_ast_compiled = if let Some(default_model) = + self.model.as_any().downcast_ref::() + { + default_model.get_compiled_matcher("m").ok_or_else(|| { + crate::error::Error::ModelError(crate::error::ModelError::M( + "Matcher 'm' not compiled".to_string(), + )) + })? + } else { + // Fallback to original compilation (for other Model implementations) + &self + .engine + .compile_expression(escape_eval(&m_ast.value)) + .map_err(Into::>::into)? + }; if policy_len == 0 { for token in p_ast.tokens.iter() { @@ -161,7 +174,7 @@ impl Enforcer { let eval_result = self .engine - .eval_ast_with_scope::(&mut scope, &m_ast_compiled)?; + .eval_ast_with_scope::(&mut scope, m_ast_compiled)?; let eft = if eval_result { EffectKind::Allow } else { @@ -189,7 +202,7 @@ impl Enforcer { let eval_result = self .engine - .eval_ast_with_scope::(&mut scope, &m_ast_compiled)?; + .eval_ast_with_scope::(&mut scope, m_ast_compiled)?; let eft = match p_ast.tokens.iter().position(|x| x == "p_eft") { Some(j) if eval_result => { let p_eft = &pvals[j]; @@ -278,10 +291,26 @@ impl Enforcer { let mut eft_stream = self.eft.new_stream(&e_ast.value, max(policy_len, 1)); - let m_ast_compiled = self - .engine - .compile_expression(escape_eval(&m_ast.value)) - .map_err(Into::>::into)?; + let m_ast_compiled = if let Some(default_model) = + self.model.as_any().downcast_ref::() + { + default_model.get_compiled_matcher(&ctx.m_type).ok_or_else( + || { + crate::error::Error::ModelError( + crate::error::ModelError::M(format!( + "Matcher '{}' not compiled", + ctx.m_type + )), + ) + }, + )? + } else { + // Fallback to original compilation (for other Model implementations) + &self + .engine + .compile_expression(escape_eval(&m_ast.value)) + .map_err(Into::>::into)? + }; if policy_len == 0 { for token in p_ast.tokens.iter() { @@ -290,7 +319,7 @@ impl Enforcer { let eval_result = self .engine - .eval_ast_with_scope::(&mut scope, &m_ast_compiled)?; + .eval_ast_with_scope::(&mut scope, m_ast_compiled)?; let eft = if eval_result { EffectKind::Allow } else { @@ -318,7 +347,7 @@ impl Enforcer { let eval_result = self .engine - .eval_ast_with_scope::(&mut scope, &m_ast_compiled)?; + .eval_ast_with_scope::(&mut scope, m_ast_compiled)?; let eft = match p_ast.tokens.iter().position(|x| x == "p_eft") { Some(j) if eval_result => { let p_eft = &pvals[j]; @@ -433,6 +462,13 @@ impl CoreApi for Enforcer { e.register_g_functions()?; + // If using DefaultModel, compile matcher expressions + if let Some(default_model) = + e.model.as_any_mut().downcast_mut::() + { + default_model.compile_matchers(&e.engine)?; + } + Ok(e) } @@ -534,6 +570,14 @@ impl CoreApi for Enforcer { async fn set_model(&mut self, m: M) -> Result<()> { self.model = m.try_into_model().await?; + + // If using DefaultModel, recompile matcher expressions + if let Some(default_model) = + self.model.as_any_mut().downcast_mut::() + { + default_model.compile_matchers(&self.engine)?; + } + self.load_policy().await?; Ok(()) } diff --git a/src/model/default_model.rs b/src/model/default_model.rs index 4f6cfeac..afcc6d69 100644 --- a/src/model/default_model.rs +++ b/src/model/default_model.rs @@ -19,14 +19,51 @@ use async_std::path::Path as ioPath; #[cfg(feature = "runtime-tokio")] use std::path::Path as ioPath; +use rhai::{Engine, AST}; use std::{collections::HashMap, sync::Arc}; #[derive(Clone, Default)] pub struct DefaultModel { pub(crate) model: HashMap, + // Precompiled matcher expressions - compiled during Model initialization + // Keys are full matcher names (e.g., "m", "m2", "m3") + compiled_matchers: HashMap, } impl DefaultModel { + // Compiles all matcher expressions after Model loading is complete + // Should be called before Enforcer is used + pub fn compile_matchers(&mut self, engine: &Engine) -> Result<()> { + self.compiled_matchers.clear(); + + // Only compile matchers from the 'm' section + if let Some(assertions) = self.model.get("m") { + for (key, assertion) in assertions { + let compiled = engine + .compile_expression(crate::util::escape_eval( + &assertion.value, + )) + .map_err(|e| { + crate::error::Error::ModelError( + crate::error::ModelError::M(format!( + "Failed to compile matcher '{}': {}", + key, e + )), + ) + })?; + + // Directly use the complete matcher key as HashMap key + self.compiled_matchers.insert(key.clone(), compiled); + } + } + Ok(()) + } + + // Gets the precompiled matcher - O(1) lookup, simple and direct + #[inline] + pub fn get_compiled_matcher(&self, key: &str) -> Option<&AST> { + self.compiled_matchers.get(key) + } #[cfg(not(target_arch = "wasm32"))] pub async fn from_file>(p: P) -> Result { let cfg = Config::from_file(p).await?; @@ -444,6 +481,14 @@ impl Model for DefaultModel { s } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/src/model/mod.rs b/src/model/mod.rs index 30a2540d..bc32b8b9 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -73,4 +73,7 @@ pub trait Model: Send + Sync { field_values: Vec, ) -> (bool, Vec>); fn to_text(&self) -> String; + // Downcast support for performance + fn as_any(&self) -> &dyn std::any::Any; + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; }