use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::glob::{host_matches, HostPattern};
use super::ConfigError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatchCondition {
Host(Vec<HostPattern>),
OriginalHost(Vec<HostPattern>),
User(Vec<HostPattern>),
LocalUser(Vec<HostPattern>),
Exec(String),
All,
Canonical,
Final,
}
#[derive(Debug, Clone, Default)]
pub struct MatchContext<'a> {
pub host: &'a str,
pub original_host: Option<&'a str>,
pub user: Option<&'a str>,
pub local_user: Option<&'a str>,
}
impl MatchContext<'_> {
pub fn original_host_or_host(&self) -> &str {
self.original_host.unwrap_or(self.host)
}
}
pub fn parse_match_line(
args: &[String],
line_no: usize,
) -> Result<Vec<MatchCondition>, ConfigError> {
let mut out = Vec::new();
let mut i = 0;
while i < args.len() {
let kw = args[i].to_ascii_lowercase();
match kw.as_str() {
"all" => {
out.push(MatchCondition::All);
i += 1;
}
"canonical" => {
out.push(MatchCondition::Canonical);
i += 1;
}
"final" => {
out.push(MatchCondition::Final);
i += 1;
}
"host" | "originalhost" | "user" | "localuser" => {
if i + 1 >= args.len() {
return Err(ConfigError::BadValue {
line: line_no,
keyword: "match".to_string(),
msg: alloc::format!("Match {kw} requires a pattern-list argument"),
});
}
let patterns = parse_match_pattern_list(&args[i + 1]);
let cond = match kw.as_str() {
"host" => MatchCondition::Host(patterns),
"originalhost" => MatchCondition::OriginalHost(patterns),
"user" => MatchCondition::User(patterns),
"localuser" => MatchCondition::LocalUser(patterns),
_ => unreachable!(),
};
out.push(cond);
i += 2;
}
"exec" => {
if i + 1 >= args.len() {
return Err(ConfigError::BadValue {
line: line_no,
keyword: "match".to_string(),
msg: "Match exec requires a command argument".into(),
});
}
let cmd = args[i + 1..].join(" ");
out.push(MatchCondition::Exec(cmd));
i = args.len();
}
other => {
return Err(ConfigError::BadValue {
line: line_no,
keyword: "match".to_string(),
msg: alloc::format!("unknown Match criterion: {other:?}"),
});
}
}
}
if out.is_empty() {
return Err(ConfigError::BadValue {
line: line_no,
keyword: "match".to_string(),
msg: "Match requires at least one criterion".into(),
});
}
Ok(out)
}
fn parse_match_pattern_list(s: &str) -> Vec<HostPattern> {
s.split(',')
.filter(|t| !t.is_empty())
.map(HostPattern::parse)
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecPolicy {
Deny,
Allow,
}
pub fn evaluate(cond: &MatchCondition, ctx: &MatchContext<'_>, policy: ExecPolicy) -> bool {
match cond {
MatchCondition::All => true,
MatchCondition::Canonical | MatchCondition::Final => false,
MatchCondition::Host(patterns) => host_matches(patterns, ctx.host),
MatchCondition::OriginalHost(patterns) => {
host_matches(patterns, ctx.original_host_or_host())
}
MatchCondition::User(patterns) => match ctx.user {
Some(u) => host_matches(patterns, u),
None => false,
},
MatchCondition::LocalUser(patterns) => match ctx.local_user {
Some(u) => host_matches(patterns, u),
None => false,
},
MatchCondition::Exec(cmd) => match policy {
ExecPolicy::Deny => false,
ExecPolicy::Allow => run_exec_match(cmd),
},
}
}
pub fn all_match(conds: &[MatchCondition], ctx: &MatchContext<'_>, policy: ExecPolicy) -> bool {
if conds.is_empty() {
return false;
}
conds.iter().all(|c| evaluate(c, ctx, policy))
}
#[cfg(feature = "std")]
fn run_exec_match(cmd: &str) -> bool {
use std::process::Command;
#[cfg(unix)]
let result = Command::new("/bin/sh").arg("-c").arg(cmd).status();
#[cfg(windows)]
let result = Command::new("cmd").arg("/C").arg(cmd).status();
#[cfg(not(any(unix, windows)))]
let result: Result<std::process::ExitStatus, std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Match exec is not supported on this platform",
));
match result {
Ok(status) => status.success(),
Err(_) => false,
}
}
#[cfg(not(feature = "std"))]
fn run_exec_match(_cmd: &str) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx<'a>(host: &'a str) -> MatchContext<'a> {
MatchContext {
host,
original_host: None,
user: None,
local_user: None,
}
}
#[test]
fn parse_all_alone() {
let args = vec!["all".to_string()];
let conds = parse_match_line(&args, 1).unwrap();
assert_eq!(conds, vec![MatchCondition::All]);
}
#[test]
fn parse_host_with_pattern_list() {
let args = vec!["host".to_string(), "*.example.com,!secret.*".to_string()];
let conds = parse_match_line(&args, 1).unwrap();
match &conds[0] {
MatchCondition::Host(p) => {
assert_eq!(p.len(), 2);
}
_ => panic!("wrong cond"),
}
}
#[test]
fn parse_missing_argument_errors() {
let args = vec!["host".to_string()];
let err = parse_match_line(&args, 7).unwrap_err();
match err {
ConfigError::BadValue { line, .. } => assert_eq!(line, 7),
_ => panic!("wrong err: {err:?}"),
}
}
#[test]
fn parse_unknown_criterion_errors() {
let args = vec!["address".to_string(), "1.2.3.4".to_string()];
let err = parse_match_line(&args, 3).unwrap_err();
match err {
ConfigError::BadValue { line, .. } => assert_eq!(line, 3),
_ => panic!("wrong err: {err:?}"),
}
}
#[test]
fn parse_empty_errors() {
let err = parse_match_line(&[], 1).unwrap_err();
match err {
ConfigError::BadValue { .. } => {}
_ => panic!("wrong err: {err:?}"),
}
}
#[test]
fn evaluate_all_matches() {
let c = ctx("anything");
assert!(evaluate(&MatchCondition::All, &c, ExecPolicy::Deny));
}
#[test]
fn evaluate_canonical_never_matches() {
let c = ctx("anything");
assert!(!evaluate(&MatchCondition::Canonical, &c, ExecPolicy::Deny));
assert!(!evaluate(&MatchCondition::Final, &c, ExecPolicy::Deny));
}
#[test]
fn evaluate_user_missing_in_context_is_no_match() {
let conds = parse_match_line(&["user".to_string(), "alice".to_string()], 1).unwrap();
let c = ctx("h"); assert!(!all_match(&conds, &c, ExecPolicy::Deny));
}
#[test]
fn evaluate_exec_denied_by_default() {
let conds = parse_match_line(&["exec".to_string(), "true".to_string()], 1).unwrap();
let c = ctx("h");
assert!(!all_match(&conds, &c, ExecPolicy::Deny));
}
#[test]
fn parse_match_pattern_list_skips_empties() {
let pats = parse_match_pattern_list("a,,b");
assert_eq!(pats.len(), 2);
}
}