Skip to content

Commit a1807dd

Browse files
committed
fix match criteria parsing
1 parent d1d0658 commit a1807dd

File tree

1 file changed

+71
-34
lines changed

1 file changed

+71
-34
lines changed

resources/sshdconfig/src/parser.rs

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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#"
521536
match 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#"
544558
match 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

Comments
 (0)