@@ -223,6 +223,23 @@ impl JsRuntime {
223223 self . rule_cache . borrow_mut ( ) . remove ( rule_name) . is_some ( )
224224 }
225225
226+ /// Drops the cached [v8::UnboundScript] for the given `rule_name` if its `rule_code` doesn't match the provided.
227+ /// Returns `true` if an entry was evicted, or `false` if not.
228+ ///
229+ /// # Panics
230+ /// Panics if the `rule_cache` has an existing borrow.
231+ pub fn evict_script_if_stale ( & self , rule_name : & str , rule_code : & str ) -> bool {
232+ let mut cache = self . rule_cache . borrow_mut ( ) ;
233+ let Some ( entry) = cache. get ( rule_name) else {
234+ return false ;
235+ } ;
236+ if entry. code_hash != CompiledRule :: get_code_hash ( rule_code) {
237+ cache. remove ( rule_name) ;
238+ return true ;
239+ }
240+ false
241+ }
242+
226243 #[ allow( clippy:: too_many_arguments) ]
227244 fn execute_rule_internal (
228245 & mut self ,
@@ -482,6 +499,8 @@ pub struct CompiledRule {
482499 script : v8:: Global < v8:: UnboundScript > ,
483500 /// The number of lines in the rule code before it was interpolated into the rule template.
484501 original_line_count : usize ,
502+ /// A fingerprint of the rule code (before interpolation) that produced this script.
503+ code_hash : [ u8 ; 16 ] ,
485504}
486505
487506impl CompiledRule {
@@ -509,12 +528,20 @@ impl CompiledRule {
509528 ) ;
510529 let script = compile_script ( scope, & rule_script, Some ( & origin) ) ?;
511530 let original_line_count = rule_code. lines ( ) . count ( ) ;
531+ let code_hash = Self :: get_code_hash ( rule_code) ;
512532 Ok ( Self {
513533 script,
514534 original_line_count,
535+ code_hash,
515536 } )
516537 }
517538
539+ fn get_code_hash ( rule_code : & str ) -> [ u8 ; 16 ] {
540+ use sha2:: Digest ;
541+ let digest = sha2:: Sha256 :: digest ( rule_code. as_bytes ( ) ) ;
542+ digest[ ..16 ] . try_into ( ) . expect ( "slice len should be 16" )
543+ }
544+
518545 /// Mutates the provided stack trace to remove lines that reference code outside the template.
519546 pub fn filter_stack_trace ( & self , stack_trace_frames : & mut Vec < JsCallSite > ) {
520547 stack_trace_frames. retain ( |cs| {
@@ -1689,4 +1716,35 @@ function visit(captures) {
16891716 vec![ " at someFunction (rule:2:5)" , " at visit (rule:6:5)" ]
16901717 ) ;
16911718 }
1719+
1720+ /// [`JsRuntime::evict_script_if_stale`] removes a cached rule if the code hash doesn't match.
1721+ #[ test]
1722+ fn evict_script_if_stale ( ) {
1723+ const RULE_NAME : & str = "ruleset/rule-name" ;
1724+ const CODE_V1 : & str = "function visit(captures) { console.log('rule_v1'); }" ;
1725+ const CODE_V2 : & str = "function visit(captures) { console.log('rule_v2'); }" ;
1726+
1727+ let mut rt = cfg_test_v8 ( ) . new_runtime ( ) ;
1728+
1729+ let compiled = CompiledRule :: new ( & mut rt. v8_handle_scope ( ) , CODE_V1 ) . unwrap ( ) ;
1730+ rt. rule_cache
1731+ . borrow_mut ( )
1732+ . insert ( RULE_NAME . to_string ( ) , compiled) ;
1733+ assert_eq ! (
1734+ rt. rule_cache. borrow( ) . get( RULE_NAME ) . unwrap( ) . code_hash,
1735+ CompiledRule :: get_code_hash( CODE_V1 )
1736+ ) ;
1737+
1738+ let did_evict = rt. evict_script_if_stale ( RULE_NAME , CODE_V1 ) ;
1739+ assert ! ( !did_evict) ;
1740+ assert ! ( rt. rule_cache. borrow( ) . contains_key( RULE_NAME ) ) ;
1741+
1742+ let did_evict = rt. evict_script_if_stale ( "ruleset/rule-without-a-cached-script" , CODE_V2 ) ;
1743+ assert ! ( !did_evict) ;
1744+ assert ! ( rt. rule_cache. borrow( ) . contains_key( RULE_NAME ) ) ;
1745+
1746+ let did_evict = rt. evict_script_if_stale ( RULE_NAME , CODE_V2 ) ;
1747+ assert ! ( did_evict) ;
1748+ assert ! ( !rt. rule_cache. borrow( ) . contains_key( RULE_NAME ) ) ;
1749+ }
16921750}
0 commit comments