use super::lexer::wildcard_match;
use super::parser::{BlockKind, Directive, HostBlock, HostPattern};
pub(crate) fn directives_for_host<'a>(blocks: &'a [HostBlock], host: &str) -> Vec<&'a Directive> {
let mut out: Vec<&'a Directive> = Vec::new();
for block in blocks {
let applies = match &block.kind {
BlockKind::Global => true,
BlockKind::Host(patterns) => host_block_matches(patterns, host),
BlockKind::Match => false,
};
if applies {
out.extend(block.directives.iter());
}
}
out
}
fn host_block_matches(patterns: &[HostPattern], host: &str) -> bool {
let host_lc = host.to_ascii_lowercase();
let mut positive_match = false;
for p in patterns {
let pat_lc = p.pattern.to_ascii_lowercase();
if wildcard_match(&pat_lc, &host_lc) {
if p.negated {
return false;
}
positive_match = true;
}
}
positive_match
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssh_config::lexer::tokenize;
use crate::ssh_config::parser::parse;
use std::path::PathBuf;
fn parse_str(input: &str) -> Vec<HostBlock> {
let tokens = tokenize(input, &PathBuf::from("test")).expect("tokenize");
parse(tokens).expect("parse")
}
#[test]
fn global_directives_always_apply() {
let blocks = parse_str("User globaluser\nHost gh\n User ghuser\n");
let dirs = directives_for_host(&blocks, "anything.example.com");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].keyword, "user");
assert_eq!(dirs[0].args, vec!["globaluser"]);
}
#[test]
fn exact_host_match() {
let blocks = parse_str("Host gh\n User git\n");
let dirs = directives_for_host(&blocks, "gh");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].keyword, "user");
}
#[test]
fn no_match_yields_only_global() {
let blocks = parse_str("User defaultuser\nHost gh\n User git\n");
let dirs = directives_for_host(&blocks, "other");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].args, vec!["defaultuser"]);
}
#[test]
fn star_pattern_matches_anything() {
let blocks = parse_str("Host *\n User wild\n");
let dirs = directives_for_host(&blocks, "anything");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].args, vec!["wild"]);
}
#[test]
fn suffix_glob_matches() {
let blocks = parse_str("Host *.example.com\n User suf\n");
assert_eq!(directives_for_host(&blocks, "host.example.com").len(), 1);
assert_eq!(directives_for_host(&blocks, "host.example.org").len(), 0);
}
#[test]
fn question_pattern_matches_one_char() {
let blocks = parse_str("Host gh?\n User q\n");
assert_eq!(directives_for_host(&blocks, "gh1").len(), 1);
assert_eq!(directives_for_host(&blocks, "gh").len(), 0);
assert_eq!(directives_for_host(&blocks, "gh12").len(), 0);
}
#[test]
fn multiple_patterns_in_one_host_line() {
let blocks = parse_str("Host alpha beta gamma\n User multi\n");
assert_eq!(directives_for_host(&blocks, "alpha").len(), 1);
assert_eq!(directives_for_host(&blocks, "beta").len(), 1);
assert_eq!(directives_for_host(&blocks, "gamma").len(), 1);
assert_eq!(directives_for_host(&blocks, "delta").len(), 0);
}
#[test]
fn negation_excludes_match() {
let blocks = parse_str("Host * !work\n User general\n");
assert_eq!(directives_for_host(&blocks, "github.com").len(), 1);
assert_eq!(directives_for_host(&blocks, "work").len(), 0);
}
#[test]
fn negation_overrides_positive_match() {
let blocks = parse_str("Host *.com !evil.com\n User any_com\n");
assert_eq!(directives_for_host(&blocks, "good.com").len(), 1);
assert_eq!(directives_for_host(&blocks, "evil.com").len(), 0);
}
#[test]
fn only_negated_patterns_never_apply() {
let blocks = parse_str("Host !work\n User noop\n");
assert_eq!(directives_for_host(&blocks, "anything").len(), 0);
assert_eq!(directives_for_host(&blocks, "work").len(), 0);
}
#[test]
fn case_insensitive_match() {
let blocks = parse_str("Host GitHub.COM\n User upper\n");
assert_eq!(directives_for_host(&blocks, "github.com").len(), 1);
assert_eq!(directives_for_host(&blocks, "GITHUB.COM").len(), 1);
assert_eq!(directives_for_host(&blocks, "GitHub.com").len(), 1);
}
#[test]
fn case_insensitive_with_wildcard() {
let blocks = parse_str("Host *.GITHUB.com\n User u\n");
assert_eq!(directives_for_host(&blocks, "host.github.com").len(), 1);
assert_eq!(directives_for_host(&blocks, "host.GITHUB.COM").len(), 1);
}
#[test]
fn directives_concatenated_across_matching_blocks() {
let input = "User globaluser\n\
Host gh\n\
\x20\x20IdentityFile ~/.ssh/gh\n\
\x20\x20User ghuser\n\
Host *\n\
\x20\x20User wilduser\n";
let blocks = parse_str(input);
let dirs = directives_for_host(&blocks, "gh");
assert_eq!(dirs.len(), 4);
let kws: Vec<&str> = dirs.iter().map(|d| d.keyword.as_str()).collect();
assert_eq!(kws, vec!["user", "identityfile", "user", "user"]);
}
#[test]
fn match_blocks_never_match_in_m12() {
let input = "Match host gh\n\
\x20\x20User matched\n\
Host gh\n\
\x20\x20User direct\n";
let blocks = parse_str(input);
let dirs = directives_for_host(&blocks, "gh");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].args, vec!["direct"]);
}
#[test]
fn provenance_carried_through_match() {
let input = "Host gh\n User u\n";
let blocks = parse_str(input);
let dirs = directives_for_host(&blocks, "gh");
assert_eq!(dirs[0].line_no, 2);
assert_eq!(dirs[0].file, PathBuf::from("test"));
}
#[test]
fn empty_blocks_yields_empty_for_unknown_host() {
let blocks = parse_str("");
assert!(directives_for_host(&blocks, "anyhost").is_empty());
}
}