1- use std:: collections:: HashSet ;
1+ use std:: collections:: { HashMap , HashSet } ;
22use std:: path:: Path ;
33use thiserror:: Error ;
44
@@ -9,6 +9,8 @@ pub struct Configuration {
99 pub ignore : HashSet < String > ,
1010 /// Maximum database age in days before warning.
1111 pub max_db_age_days : Option < u64 > ,
12+ /// Inline comments parsed from the YAML file (advisory ID → comment text).
13+ pub ignore_comments : HashMap < String , String > ,
1214}
1315
1416/// Errors that can occur when loading a configuration file.
@@ -115,8 +117,24 @@ impl Configuration {
115117 } )
116118 }
117119
120+ /// Extract inline `# comments` from YAML ignore entries.
121+ ///
122+ /// Matches lines in the format produced by [`save()`]: ` - ID # comment`.
123+ fn parse_ignore_comments ( yaml : & str ) -> HashMap < String , String > {
124+ yaml. lines ( )
125+ . filter_map ( |line| {
126+ let trimmed = line. trim ( ) ;
127+ let entry = trimmed. strip_prefix ( "- " ) ?;
128+ let ( id, comment) = entry. split_once ( " # " ) ?;
129+ Some ( ( id. trim ( ) . to_string ( ) , comment. to_string ( ) ) )
130+ } )
131+ . collect ( )
132+ }
133+
118134 /// Parse configuration from a YAML string.
119135 pub fn from_yaml ( yaml : & str ) -> Result < Self , ConfigError > {
136+ let ignore_comments = Self :: parse_ignore_comments ( yaml) ;
137+
120138 let value: serde_yml:: Value =
121139 serde_yml:: from_str ( yaml) . map_err ( |e| ConfigError :: InvalidYaml ( e. to_string ( ) ) ) ?;
122140
@@ -163,6 +181,7 @@ impl Configuration {
163181 Ok ( Configuration {
164182 ignore,
165183 max_db_age_days,
184+ ignore_comments,
166185 } )
167186 }
168187}
@@ -290,6 +309,7 @@ mod tests {
290309 let config = Configuration {
291310 ignore,
292311 max_db_age_days : Some ( 7 ) ,
312+ ..Configuration :: default ( )
293313 } ;
294314 config. save ( & path, None ) . unwrap ( ) ;
295315
@@ -326,6 +346,7 @@ mod tests {
326346 let config = Configuration {
327347 ignore,
328348 max_db_age_days : None ,
349+ ..Configuration :: default ( )
329350 } ;
330351 config. save ( & path, None ) . unwrap ( ) ;
331352
@@ -349,6 +370,7 @@ mod tests {
349370 let config = Configuration {
350371 ignore,
351372 max_db_age_days : None ,
373+ ..Configuration :: default ( )
352374 } ;
353375
354376 let mut comments = std:: collections:: HashMap :: new ( ) ;
@@ -436,4 +458,86 @@ mod tests {
436458 other => panic ! ( "expected InvalidConfiguration, got: {:?}" , other) ,
437459 }
438460 }
461+
462+ // ========== Comment Parsing ==========
463+
464+ #[ test]
465+ fn parse_ignore_comments_extracts_comments ( ) {
466+ let yaml = "---\n ignore:\n - CVE-2020-1234 # gem 1.0 (Critical) - Title\n - GHSA-aaaa-bbbb-cccc # rack 2.0 (Medium) - Other\n " ;
467+ let comments = Configuration :: parse_ignore_comments ( yaml) ;
468+ assert_eq ! ( comments. len( ) , 2 ) ;
469+ assert_eq ! (
470+ comments. get( "CVE-2020-1234" ) . unwrap( ) ,
471+ "gem 1.0 (Critical) - Title"
472+ ) ;
473+ assert_eq ! (
474+ comments. get( "GHSA-aaaa-bbbb-cccc" ) . unwrap( ) ,
475+ "rack 2.0 (Medium) - Other"
476+ ) ;
477+ }
478+
479+ #[ test]
480+ fn parse_ignore_comments_skips_uncommented_entries ( ) {
481+ let yaml = "---\n ignore:\n - CVE-2020-1234\n - GHSA-aaaa-bbbb-cccc # has comment\n " ;
482+ let comments = Configuration :: parse_ignore_comments ( yaml) ;
483+ assert_eq ! ( comments. len( ) , 1 ) ;
484+ assert ! ( comments. contains_key( "GHSA-aaaa-bbbb-cccc" ) ) ;
485+ assert ! ( !comments. contains_key( "CVE-2020-1234" ) ) ;
486+ }
487+
488+ #[ test]
489+ fn parse_ignore_comments_empty_yaml ( ) {
490+ let comments = Configuration :: parse_ignore_comments ( "---\n ignore: []\n " ) ;
491+ assert ! ( comments. is_empty( ) ) ;
492+ }
493+
494+ #[ test]
495+ fn from_yaml_preserves_comments ( ) {
496+ let yaml = "---\n ignore:\n - CVE-2020-1234 # gem 1.0 (Critical) - Title\n - GHSA-aaaa-bbbb-cccc\n " ;
497+ let config = Configuration :: from_yaml ( yaml) . unwrap ( ) ;
498+ assert_eq ! ( config. ignore. len( ) , 2 ) ;
499+ assert_eq ! ( config. ignore_comments. len( ) , 1 ) ;
500+ assert_eq ! (
501+ config. ignore_comments. get( "CVE-2020-1234" ) . unwrap( ) ,
502+ "gem 1.0 (Critical) - Title"
503+ ) ;
504+ }
505+
506+ #[ test]
507+ fn save_and_reload_preserves_comments ( ) {
508+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
509+ let path = tmp. path ( ) . join ( ".gem-audit.yml" ) ;
510+
511+ let mut ignore = HashSet :: new ( ) ;
512+ ignore. insert ( "CVE-2020-1234" . to_string ( ) ) ;
513+ ignore. insert ( "GHSA-aaaa-bbbb-cccc" . to_string ( ) ) ;
514+
515+ let mut comments = HashMap :: new ( ) ;
516+ comments. insert (
517+ "CVE-2020-1234" . to_string ( ) ,
518+ "gem 1.0 (Critical) - Title" . to_string ( ) ,
519+ ) ;
520+ comments. insert (
521+ "GHSA-aaaa-bbbb-cccc" . to_string ( ) ,
522+ "rack 2.0 (Medium) - Other" . to_string ( ) ,
523+ ) ;
524+
525+ let config = Configuration {
526+ ignore,
527+ max_db_age_days : None ,
528+ ..Configuration :: default ( )
529+ } ;
530+ config. save ( & path, Some ( & comments) ) . unwrap ( ) ;
531+
532+ let reloaded = Configuration :: load ( & path) . unwrap ( ) ;
533+ assert_eq ! ( reloaded. ignore_comments. len( ) , 2 ) ;
534+ assert_eq ! (
535+ reloaded. ignore_comments. get( "CVE-2020-1234" ) . unwrap( ) ,
536+ "gem 1.0 (Critical) - Title"
537+ ) ;
538+ assert_eq ! (
539+ reloaded. ignore_comments. get( "GHSA-aaaa-bbbb-cccc" ) . unwrap( ) ,
540+ "rack 2.0 (Medium) - Other"
541+ ) ;
542+ }
439543}
0 commit comments