33//! This module contains utility functions used across the labeling system,
44//! including JSON extraction, integrity determination, and common operations.
55
6+ use std:: borrow:: Cow ;
67use std:: sync:: atomic:: { AtomicBool , Ordering } ;
78
89use serde_json:: Value ;
@@ -156,10 +157,10 @@ pub struct PolicyContext {
156157 pub demotion_label : String ,
157158}
158159
159- fn normalize_scope ( scope : & str , ctx : & PolicyContext ) -> String {
160+ fn normalize_scope < ' a > ( scope : & ' a str , ctx : & ' a PolicyContext ) -> Cow < ' a , str > {
160161 let token = policy_scope_token ( & ctx. scopes ) ;
161162 if token. is_empty ( ) {
162- scope . to_string ( )
163+ Cow :: Borrowed ( scope )
163164 } else if ctx
164165 . scopes
165166 . iter ( )
@@ -176,10 +177,10 @@ fn normalize_scope(scope: &str, ctx: &PolicyContext) -> String {
176177 if matches_any_scope {
177178 token
178179 } else {
179- scope . to_string ( )
180+ Cow :: Borrowed ( scope )
180181 }
181182 } else {
182- scope . to_string ( )
183+ Cow :: Borrowed ( scope )
183184 }
184185}
185186
@@ -191,16 +192,24 @@ fn split_repo_id(repo_id: &str) -> Option<(&str, &str)> {
191192 Some ( ( owner, repo) )
192193}
193194
194- pub ( crate ) fn policy_scope_token ( scopes : & [ PolicyScopeEntry ] ) -> String {
195- let labels : Vec < & str > = scopes
195+ pub ( crate ) fn policy_scope_token ( scopes : & [ PolicyScopeEntry ] ) -> Cow < ' _ , str > {
196+ let mut labels = scopes
196197 . iter ( )
197198 . map ( |s| s. scope_label . as_str ( ) )
198- . filter ( |s| !s. is_empty ( ) )
199- . collect ( ) ;
200- if labels. is_empty ( ) {
201- String :: new ( )
202- } else {
203- labels. join ( " | " )
199+ . filter ( |s| !s. is_empty ( ) ) ;
200+ match ( labels. next ( ) , labels. next ( ) ) {
201+ ( None , _) => Cow :: Borrowed ( "" ) ,
202+ ( Some ( first) , None ) => Cow :: Borrowed ( first) ,
203+ ( Some ( first) , Some ( second) ) => {
204+ let mut value = String :: from ( first) ;
205+ value. push_str ( " | " ) ;
206+ value. push_str ( second) ;
207+ for rest in labels {
208+ value. push_str ( " | " ) ;
209+ value. push_str ( rest) ;
210+ }
211+ Cow :: Owned ( value)
212+ }
204213 }
205214}
206215
@@ -1284,16 +1293,30 @@ fn integrity_rank(scope: &str, labels: &[String], ctx: &PolicyContext) -> u8 {
12841293}
12851294
12861295fn integrity_rank_normalized ( normalized_scope : & str , labels : & [ String ] ) -> u8 {
1287- // Check from highest to lowest, allocating one label at a time .
1296+ // Check from highest to lowest.
12881297 for ( rank, ( prefix, base) ) in INTEGRITY_LEVELS . iter ( ) . enumerate ( ) . rev ( ) {
1289- let tag = format_integrity_label ( prefix, normalized_scope, base) ;
1290- if labels. iter ( ) . any ( |l| l == & tag) {
1298+ if labels
1299+ . iter ( )
1300+ . any ( |label| label_matches_normalized ( label, prefix, normalized_scope, base) )
1301+ {
12911302 return ( rank + 1 ) as u8 ;
12921303 }
12931304 }
12941305 0
12951306}
12961307
1308+ #[ inline]
1309+ fn label_matches_normalized ( label : & str , prefix : & str , scope : & str , base : & str ) -> bool {
1310+ if scope. is_empty ( ) {
1311+ label == base
1312+ } else if scope. contains ( '|' ) {
1313+ // Multi-scope uses canonical "integrity=<base>;scopes=..." encoding.
1314+ label == format_integrity_label ( prefix, scope, base)
1315+ } else {
1316+ label. strip_prefix ( prefix) == Some ( scope)
1317+ }
1318+ }
1319+
12971320/// Elevate integrity to the max of current and candidate levels for a scope.
12981321pub fn max_integrity (
12991322 scope : & str ,
@@ -1917,11 +1940,54 @@ pub(crate) fn is_any_trusted_actor(username: &str, ctx: &PolicyContext) -> bool
19171940#[ cfg( test) ]
19181941mod tests {
19191942 use super :: * ;
1943+ use std:: borrow:: Cow ;
19201944
19211945 fn test_ctx ( ) -> PolicyContext {
19221946 PolicyContext :: default ( )
19231947 }
19241948
1949+ #[ test]
1950+ fn test_policy_scope_token_borrows_for_single_scope ( ) {
1951+ let scopes = vec ! [ PolicyScopeEntry {
1952+ scope_kind: ScopeKind :: Owner ,
1953+ scope_owner: Some ( "octocat" . to_string( ) ) ,
1954+ scope_repo: None ,
1955+ scope_label: "octocat" . to_string( ) ,
1956+ } ] ;
1957+ let token = policy_scope_token ( & scopes) ;
1958+ assert_eq ! ( token, "octocat" ) ;
1959+ assert ! ( matches!( token, Cow :: Borrowed ( _) ) ) ;
1960+ }
1961+
1962+ #[ test]
1963+ fn test_policy_scope_token_owns_for_multi_scope ( ) {
1964+ let scopes = vec ! [
1965+ PolicyScopeEntry {
1966+ scope_kind: ScopeKind :: Owner ,
1967+ scope_owner: Some ( "octocat" . to_string( ) ) ,
1968+ scope_repo: None ,
1969+ scope_label: "octocat" . to_string( ) ,
1970+ } ,
1971+ PolicyScopeEntry {
1972+ scope_kind: ScopeKind :: RepoPrefix ,
1973+ scope_owner: Some ( "octocat" . to_string( ) ) ,
1974+ scope_repo: Some ( "hello" . to_string( ) ) ,
1975+ scope_label: "octocat/hello*" . to_string( ) ,
1976+ } ,
1977+ ] ;
1978+ let token = policy_scope_token ( & scopes) ;
1979+ assert_eq ! ( token, "octocat | octocat/hello*" ) ;
1980+ assert ! ( matches!( token, Cow :: Owned ( _) ) ) ;
1981+ }
1982+
1983+ #[ test]
1984+ fn test_normalize_scope_borrows_input_when_no_scopes ( ) {
1985+ let ctx = test_ctx ( ) ;
1986+ let normalized = normalize_scope ( "owner/repo" , & ctx) ;
1987+ assert_eq ! ( normalized, "owner/repo" ) ;
1988+ assert ! ( matches!( normalized, Cow :: Borrowed ( _) ) ) ;
1989+ }
1990+
19251991 #[ test]
19261992 fn test_is_any_trusted_actor_tiers_and_negative ( ) {
19271993 let ctx = PolicyContext {
@@ -2317,6 +2383,34 @@ mod tests {
23172383 assert_eq ! ( result, candidate, "max_integrity should keep the higher integrity rank" ) ;
23182384 }
23192385
2386+ #[ test]
2387+ fn test_label_matches_normalized_common_and_multiscope_paths ( ) {
2388+ assert ! ( label_matches_normalized(
2389+ "approved:owner/repo" ,
2390+ label_constants:: WRITER_PREFIX ,
2391+ "owner/repo" ,
2392+ label_constants:: WRITER_BASE
2393+ ) ) ;
2394+ assert ! ( label_matches_normalized(
2395+ "integrity=approved;scopes=owner/repo,owner/tool*" ,
2396+ label_constants:: WRITER_PREFIX ,
2397+ "owner/repo | owner/tool*" ,
2398+ label_constants:: WRITER_BASE
2399+ ) ) ;
2400+ assert ! ( label_matches_normalized(
2401+ label_constants:: READER_BASE ,
2402+ label_constants:: READER_PREFIX ,
2403+ "" ,
2404+ label_constants:: READER_BASE
2405+ ) ) ;
2406+ assert ! ( !label_matches_normalized(
2407+ "approved:owner/repo" ,
2408+ label_constants:: MERGED_PREFIX ,
2409+ "owner/repo" ,
2410+ label_constants:: MERGED_BASE
2411+ ) ) ;
2412+ }
2413+
23202414 #[ test]
23212415 fn test_integrity_for_level_mapping ( ) {
23222416 let ctx = test_ctx ( ) ;
0 commit comments