@@ -85,7 +85,7 @@ impl SshdConfigParser {
8585 }
8686 match node. kind ( ) {
8787 "keyword" => {
88- Self :: parse_and_insert_keyword ( node, input, input_bytes, Some ( & mut self . map ) , false ) ?;
88+ Self :: parse_and_insert_keyword ( node, input, input_bytes, Some ( & mut self . map ) ) ?;
8989 Ok ( ( ) )
9090 } ,
9191 "comment" | "empty_line" => Ok ( ( ) ) ,
@@ -101,10 +101,8 @@ impl SshdConfigParser {
101101 ) ) ;
102102 } ;
103103
104- // Parse criteria without inserting into a map (force_array=true for criteria)
105- let ( criteria_key, criteria_value) = Self :: parse_and_insert_keyword ( criteria_node, input, input_bytes, None , true ) ?;
106- let mut criteria_map = Map :: new ( ) ;
107- criteria_map. insert ( criteria_key, criteria_value) ;
104+ // Parse criteria by extracting the entire line and parsing key-value pairs
105+ let criteria_map = Self :: parse_match_criteria ( criteria_node, input, input_bytes) ?;
108106
109107 let mut match_object = Map :: new ( ) ;
110108 match_object. insert ( "criteria" . to_string ( ) , Value :: Object ( criteria_map) ) ;
@@ -124,11 +122,9 @@ impl SshdConfigParser {
124122 if child_node. id ( ) == criteria_node. id ( ) {
125123 continue ;
126124 }
127- Self :: parse_and_insert_keyword ( child_node, input, input_bytes, Some ( & mut match_object) , false ) ?;
128- }
129- "comment" => {
130- continue ;
125+ Self :: parse_and_insert_keyword ( child_node, input, input_bytes, Some ( & mut match_object) ) ?;
131126 }
127+ "comment" => { }
132128 _ => {
133129 return Err ( SshdConfigError :: ParserError ( t ! ( "parser.unknownNodeType" , node = child_node. kind( ) ) . to_string ( ) ) ) ;
134130 }
@@ -140,22 +136,51 @@ impl SshdConfigParser {
140136 Ok ( ( ) )
141137 }
142138
139+ /// Parse match criteria which can contain multiple key-value pairs on a single line.
140+ /// Example: "user alice,bob address *.*.0.1 localport 22"
141+ /// Returns a Map with each criterion as a key with an array value.
142+ fn parse_match_criteria ( criteria_node : tree_sitter:: Node , input : & str , input_bytes : & [ u8 ] ) -> Result < Map < String , Value > , SshdConfigError > {
143+ let Ok ( criteria_text) = criteria_node. utf8_text ( input_bytes) else {
144+ return Err ( SshdConfigError :: ParserError ( t ! ( "parser.failedToParseChildNode" , input = input) . to_string ( ) ) ) ;
145+ } ;
146+
147+ let criteria_text = criteria_text. trim_end ( ) ;
148+ let tokens: Vec < & str > = criteria_text. split_whitespace ( ) . collect ( ) ;
149+ let mut criteria_map = Map :: new ( ) ;
150+ let mut i = 0 ;
151+
152+ while i < tokens. len ( ) {
153+ let key = tokens[ i] . to_lowercase ( ) ;
154+ i += 1 ;
155+ if i >= tokens. len ( ) {
156+ return Err ( SshdConfigError :: ParserError (
157+ t ! ( "parser.missingValueInChildNode" , input = input) . to_string ( )
158+ ) ) ;
159+ }
160+
161+ let value_str = tokens[ i] ;
162+ let values: Vec < Value > = value_str. split ( ',' ) . map ( |s| Value :: String ( s. to_string ( ) ) ) . collect ( ) ;
163+
164+ criteria_map. insert ( key, Value :: Array ( values) ) ;
165+ i += 1 ;
166+ }
167+ Ok ( criteria_map)
168+ }
169+
143170 /// Parse a keyword node and optionally insert it into a map.
144171 /// If `target_map` is provided, the keyword will be inserted into that map with repeatability handling.
145172 /// If `target_map` is None, returns the key-value pair without inserting.
146- /// If `force_array` is true, the value will always be an array (used for criteria).
147173 fn parse_and_insert_keyword (
148174 keyword_node : tree_sitter:: Node ,
149175 input : & str ,
150176 input_bytes : & [ u8 ] ,
151- target_map : Option < & mut Map < String , Value > > ,
152- force_array : bool
177+ target_map : Option < & mut Map < String , Value > >
153178 ) -> Result < ( String , Value ) , SshdConfigError > {
154179 let mut cursor = keyword_node. walk ( ) ;
155180 let mut key = None ;
156181 let mut value = Value :: Null ;
157182 let mut operator: Option < String > = None ;
158- let mut is_vec = force_array ;
183+ let mut is_vec = false ;
159184 let mut is_repeatable = false ;
160185 let mut keyword_type = KeywordType :: Unseparated ;
161186
@@ -170,15 +195,13 @@ impl SshdConfigParser {
170195 debug ! ( "{}" , t!( "parser.keywordDebug" , text = text) . to_string( ) ) ;
171196 }
172197
173- if !force_array {
174- if MULTI_ARG_KEYWORDS_SPACE_SEP . contains ( & text) {
175- keyword_type = KeywordType :: SpaceSeparated ;
176- } else if MULTI_ARG_KEYWORDS_COMMA_SEP . contains ( & text) {
177- keyword_type = KeywordType :: CommaSeparated ;
178- }
179- is_repeatable = REPEATABLE_KEYWORDS . contains ( & text) ;
180- is_vec = is_repeatable || keyword_type != KeywordType :: Unseparated ;
198+ if MULTI_ARG_KEYWORDS_SPACE_SEP . contains ( & text) {
199+ keyword_type = KeywordType :: SpaceSeparated ;
200+ } else if MULTI_ARG_KEYWORDS_COMMA_SEP . contains ( & text) {
201+ keyword_type = KeywordType :: CommaSeparated ;
181202 }
203+ is_repeatable = REPEATABLE_KEYWORDS . contains ( & text) ;
204+ is_vec = is_repeatable || keyword_type != KeywordType :: Unseparated ;
182205 key = Some ( text. to_string ( ) ) ;
183206 }
184207
@@ -309,7 +332,7 @@ fn parse_arguments_node(arg_node: tree_sitter::Node, input: &str, input_bytes: &
309332 return Err ( SshdConfigError :: ParserError ( t ! ( "parser.invalidValue" ) . to_string ( ) ) ) ;
310333 } ,
311334 _ => return Err ( SshdConfigError :: ParserError ( t ! ( "parser.unknownNode" , kind = node. kind( ) ) . to_string ( ) ) )
312- } ;
335+ }
313336 }
314337
315338 // Always return array if is_vec is true (for MULTI_ARG_KEYWORDS_COMMA_SEP, MULTI_ARG_KEYWORDS_SPACE_SEP, and REPEATABLE_KEYWORDS)
@@ -370,9 +393,6 @@ mod tests {
370393 fn multiarg_string_with_spaces_no_quotes_keyword ( ) {
371394 let input = "allowgroups administrators developers\n " ;
372395 let result: Map < String , Value > = parse_text_to_map ( input) . unwrap ( ) ;
373-
374- eprintln ! ( "Top-level allowgroups: {:?}" , result. get( "allowgroups" ) ) ;
375-
376396 let allowgroups = result. get ( "allowgroups" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
377397 assert_eq ! ( allowgroups. len( ) , 2 ) ;
378398 assert_eq ! ( allowgroups[ 0 ] , Value :: String ( "administrators" . to_string( ) ) ) ;
@@ -496,12 +516,8 @@ match user testuser
496516 allowgroups administrators developers
497517"# ;
498518 let result: Map < String , Value > = parse_text_to_map ( input) . unwrap ( ) ;
499-
500519 let match_array = result. get ( "match" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
501520 let match_obj = match_array[ 0 ] . as_object ( ) . unwrap ( ) ;
502-
503- // Debug output
504- eprintln ! ( "Match object keys: {:?}" , match_obj. keys( ) . collect:: <Vec <_>>( ) ) ;
505521 for ( k, v) in match_obj. iter ( ) {
506522 eprintln ! ( " {}: {:?}" , k, v) ;
507523 }
@@ -516,7 +532,6 @@ match user testuser
516532
517533 #[ test]
518534 fn match_with_repeated_multiarg_keyword ( ) {
519- // Test that repeatable multi-arg keywords append all values to flat array
520535 let input = r#"
521536match user testuser
522537 allowgroups administrators developers
@@ -539,7 +554,6 @@ match user testuser
539554
540555 #[ test]
541556 fn match_with_repeated_single_value_keyword ( ) {
542- // Test that repeatable single-value keywords also work correctly
543557 let input = r#"
544558match user testuser
545559 port 2222
@@ -565,13 +579,36 @@ match user developer
565579 passwordauthentication yes
566580"# ;
567581 let result: Map < String , Value > = parse_text_to_map ( input) . unwrap ( ) ;
582+ let match_array = result. get ( "match" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
583+ let match_obj = match_array[ 0 ] . as_object ( ) . unwrap ( ) ;
584+ assert_eq ! ( match_obj. get( "passwordauthentication" ) . unwrap( ) , & Value :: String ( "yes" . to_string( ) ) ) ;
585+ assert_eq ! ( match_obj. len( ) , 2 ) ;
586+ }
568587
569- // Comments should be ignored, only the keyword should be present
588+ #[ test]
589+ fn match_with_multiple_criteria_types ( ) {
590+ let input = r#"
591+ match user alice,bob address 1.2.3.4/56
592+ passwordauthentication yes
593+ allowtcpforwarding no
594+ "# ;
595+ let result: Map < String , Value > = parse_text_to_map ( input) . unwrap ( ) ;
570596 let match_array = result. get ( "match" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
597+ assert_eq ! ( match_array. len( ) , 1 ) ;
571598 let match_obj = match_array[ 0 ] . as_object ( ) . unwrap ( ) ;
572599
600+ let criteria = match_obj. get ( "criteria" ) . unwrap ( ) . as_object ( ) . unwrap ( ) ;
601+
602+ let user_array = criteria. get ( "user" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
603+ assert_eq ! ( user_array. len( ) , 2 ) ;
604+ assert_eq ! ( user_array[ 0 ] , Value :: String ( "alice" . to_string( ) ) ) ;
605+ assert_eq ! ( user_array[ 1 ] , Value :: String ( "bob" . to_string( ) ) ) ;
606+
607+ let address_array = criteria. get ( "address" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
608+ assert_eq ! ( address_array. len( ) , 1 ) ;
609+ assert_eq ! ( address_array[ 0 ] , Value :: String ( "1.2.3.4/56" . to_string( ) ) ) ;
610+
573611 assert_eq ! ( match_obj. get( "passwordauthentication" ) . unwrap( ) , & Value :: String ( "yes" . to_string( ) ) ) ;
574- // Should only have criteria and passwordauthentication keys
575- assert_eq ! ( match_obj. len( ) , 2 ) ;
612+ assert_eq ! ( match_obj. get( "allowtcpforwarding" ) . unwrap( ) , & Value :: String ( "no" . to_string( ) ) ) ;
576613 }
577614}
0 commit comments