1use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8
9use super::ast::{self, ConfigAst, ConfigNode};
10use toride_ssh_core::Result;
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub struct ResolvedHost {
15 pub alias: String,
17 pub host_name: Option<String>,
19 pub user: Option<String>,
21 pub port: Option<u16>,
23 pub identity_files: Vec<String>,
25 pub certificate_files: Vec<String>,
27 pub proxy_jump: Option<String>,
29 pub identity_agent: Option<String>,
31 pub forward_agent: Option<String>,
33 pub add_keys_to_agent: Option<String>,
35 pub use_keychain: Option<String>,
37 pub control_master: Option<String>,
39 pub control_path: Option<String>,
41 pub control_persist: Option<String>,
43 pub local_forwards: Vec<String>,
45 pub remote_forwards: Vec<String>,
47 pub dynamic_forwards: Vec<String>,
49 pub directives: Vec<(String, String)>,
51 pub user_known_hosts_file: Option<String>,
56 pub identities_only: Option<bool>,
63 pub canonicalized: bool,
67 pub unevaluated_match_warnings: Vec<String>,
70 pub gssapi_authentication: Option<String>,
72 pub gssapi_delegate_credentials: Option<String>,
74 pub gssapi_server_identity: Option<String>,
76 pub gssapi_client_identity: Option<String>,
78}
79
80const TOKEN_EXPANDABLE: &[&str] = &[
83 "certificatefile",
84 "controlmaster",
85 "controlpath",
86 "controlpersist",
87 "dynamicforward",
88 "forwardagent",
89 "identityagent",
90 "knownhostscommand",
91 "localforward",
92 "remoteforward",
93 "revokedhostkeys",
94 "usekeychain",
95 "userknownhostsfile",
96 "proxycommand",
97];
98
99pub async fn resolve(ssh_dir: &Path, host: &str, user: Option<&str>) -> Result<ResolvedHost> {
118 let config_path = ssh_dir.join("config");
119
120 let mut visited = HashSet::new();
122 let flat_ast = load_and_flatten(&config_path, &mut visited).await?;
123
124 let local_user = user.map_or_else(whoami, str::to_owned);
126 let mut resolved = resolve_pass(&flat_ast, host, host, &local_user);
127
128 expand_resolved(&mut resolved, host, ssh_dir);
130
131 if is_canonicalize_enabled(&resolved) {
133 let canonical_host = resolved.host_name.take().unwrap_or_else(|| host.to_owned());
134
135 let mut canon = resolve_pass(&flat_ast, &canonical_host, host, &local_user);
136
137 expand_resolved(&mut canon, &canonical_host, ssh_dir);
139
140 host.clone_into(&mut canon.alias);
141 canon.canonicalized = true;
142 return Ok(canon);
143 }
144
145 Ok(resolved)
146}
147
148fn resolve_pass(
155 flat_ast: &ConfigAst,
156 target_host: &str,
157 original_host: &str,
158 local_user: &str,
159) -> ResolvedHost {
160 let mut resolved = ResolvedHost {
161 alias: target_host.to_owned(),
162 host_name: None,
163 user: None,
164 port: None,
165 identity_files: Vec::new(),
166 certificate_files: Vec::new(),
167 proxy_jump: None,
168 identity_agent: None,
169 forward_agent: None,
170 add_keys_to_agent: None,
171 use_keychain: None,
172 control_master: None,
173 control_path: None,
174 control_persist: None,
175 local_forwards: Vec::new(),
176 remote_forwards: Vec::new(),
177 dynamic_forwards: Vec::new(),
178 directives: Vec::new(),
179 user_known_hosts_file: None,
180 identities_only: None,
181 canonicalized: false,
182 unevaluated_match_warnings: Vec::new(),
183 gssapi_authentication: None,
184 gssapi_delegate_credentials: None,
185 gssapi_server_identity: None,
186 gssapi_client_identity: None,
187 };
188
189 let mut seen_keys = HashSet::new();
190
191 for node in &flat_ast.nodes {
192 match node {
193 ConfigNode::HostBlock(b) => {
194 if host_matches(target_host, &b.patterns) {
195 resolve_block(&b.nodes, &mut resolved, &mut seen_keys);
196 }
197 }
198 ConfigNode::MatchBlock(b) => {
199 if contains_exec_criteria(&b.criteria) {
201 let warning = format!(
202 "Match block contains 'exec' criteria which are not evaluated: {}",
203 b.criteria,
204 );
205 tracing::warn!("{}", &warning);
206 resolved.unevaluated_match_warnings.push(warning);
207 }
208 if match_criteria_host(&b.criteria, target_host, local_user, original_host) {
209 resolve_block(&b.nodes, &mut resolved, &mut seen_keys);
210 }
211 }
212 _ => {}
213 }
214 }
215
216 resolved
217}
218
219fn load_and_flatten<'a>(
221 path: &'a Path,
222 visited: &'a mut HashSet<PathBuf>,
223) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ConfigAst>> + 'a>> {
224 Box::pin(async move {
225 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_owned());
229
230 if visited.contains(&canonical) {
231 return Err(toride_ssh_core::Error::ConfigIncludeCycle(
232 canonical.display().to_string(),
233 ));
234 }
235 visited.insert(canonical);
236
237 let content = if path.exists() {
238 tokio::fs::read_to_string(path).await?
239 } else {
240 return Ok(ConfigAst { nodes: Vec::new() });
241 };
242
243 let mut flat = ast::parse(&content);
244
245 let original_nodes = std::mem::take(&mut flat.nodes);
250 let mut new_nodes = Vec::with_capacity(original_nodes.len());
251
252 for node in original_nodes {
253 let pattern_value = match &node {
254 ConfigNode::Directive(d) if d.keyword.eq_ignore_ascii_case("include") => {
255 Some(d.value.clone())
256 }
257 _ => None,
258 };
259
260 if let Some(include_pattern) = pattern_value {
261 let expanded = expand_tilde_and_env(&include_pattern);
262
263 let base_dir = if Path::new(&expanded).is_absolute() {
265 PathBuf::new()
266 } else {
267 path.parent().unwrap_or_else(|| Path::new(".")).to_owned()
268 };
269
270 let full_pattern = base_dir.join(&expanded);
271 let pattern_str = full_pattern.display().to_string();
272
273 let matched_files = glob_paths(&pattern_str);
274
275 for inc_path in matched_files {
276 let included = load_and_flatten(&inc_path, visited).await?;
277 new_nodes.extend(included.nodes);
278 }
279 } else {
280 new_nodes.push(node);
281 }
282 }
283
284 flat.nodes = new_nodes;
285
286 Ok(flat)
287 })
288}
289
290fn expand_tilde_and_env(path: &str) -> String {
292 let mut result = path.to_owned();
293
294 if (result.starts_with("~/") || result == "~")
296 && let Some(home) = dirs::home_dir()
297 {
298 let home_str = home.display().to_string();
299 result = result.replacen('~', &home_str, 1);
300 }
301
302 result = expand_env_vars(&result);
304
305 result
306}
307
308fn expand_env_vars(s: &str) -> String {
314 let mut result = String::with_capacity(s.len());
315 let mut chars = s.char_indices().peekable();
316
317 while let Some((i, ch)) = chars.next() {
318 if ch == '$' {
319 if let Some((_, '{')) = chars.peek() {
320 chars.next(); let start = i + 2;
323 if let Some(end_offset) = s[start..].find('}') {
324 let var_name = &s[start..start + end_offset];
325 result.push_str(&std::env::var(var_name).unwrap_or_default());
326 for _ in 0..=end_offset {
328 chars.next();
329 }
330 continue;
331 }
332 result.push(ch);
334 result.push('{');
335 continue;
336 }
337 let rest = &s[i + 1..];
339 let end = rest
340 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
341 .unwrap_or(rest.len());
342 if end > 0 {
343 let var_name = &rest[..end];
344 result.push_str(&std::env::var(var_name).unwrap_or_default());
345 for _ in 0..end {
347 chars.next();
348 }
349 continue;
350 }
351 result.push(ch);
353 } else {
354 result.push(ch);
355 }
356 }
357
358 result
359}
360
361fn glob_paths(pattern: &str) -> Vec<PathBuf> {
369 if pattern.contains("**") {
371 return glob_paths_recursive(pattern);
372 }
373
374 let mut paths = Vec::new();
376
377 if let Some(parent) = Path::new(pattern).parent() {
378 let file_name = Path::new(pattern)
379 .file_name()
380 .map(|f| f.to_string_lossy().into_owned())
381 .unwrap_or_default();
382
383 if let Ok(entries) = std::fs::read_dir(parent) {
384 for entry in entries.flatten() {
385 let name = entry.file_name();
386 let name_str = name.to_string_lossy();
387 if simple_glob_match(&name_str, &file_name) {
388 paths.push(entry.path());
389 }
390 }
391 }
392 }
393
394 paths.sort();
395 paths
396}
397
398fn glob_paths_recursive(pattern: &str) -> Vec<PathBuf> {
406 let mut paths = Vec::new();
407
408 if let Some(delim) = pattern.find("**/") {
410 let prefix = &pattern[..delim];
411 let suffix = &pattern[delim + 3..];
413
414 let base = if prefix.is_empty() || prefix == "/" {
415 PathBuf::from(if prefix.is_empty() { "." } else { "/" })
416 } else {
417 PathBuf::from(prefix)
418 };
419
420 if base.is_dir() {
421 collect_recursive_glob(&base, suffix, &mut paths);
422 }
423 } else if let Some(prefix) = pattern.strip_suffix("**") {
424 let base = if prefix.is_empty() {
427 PathBuf::from(".")
428 } else {
429 PathBuf::from(prefix)
430 };
431
432 if base.is_dir() {
433 collect_recursive_glob(&base, "*", &mut paths);
434 }
435 }
436
437 paths.sort();
438 paths
439}
440
441fn collect_recursive_glob(dir: &Path, suffix: &str, out: &mut Vec<PathBuf>) {
449 let Ok(entries) = std::fs::read_dir(dir) else {
450 return;
451 };
452
453 let collected: Vec<_> = entries.flatten().collect();
454
455 for entry in &collected {
456 let name = entry.file_name();
457 let name_str = name.to_string_lossy();
458 let path = entry.path();
459
460 if let Some(slash) = suffix.find('/') {
463 let first = &suffix[..slash];
464 let rest = &suffix[slash + 1..];
465
466 if path.is_dir() && simple_glob_match(&name_str, first) {
469 walk_subpath(&path, rest, out);
470 }
471 } else if simple_glob_match(&name_str, suffix) {
472 out.push(path.clone());
473 }
474
475 if path.is_dir() {
477 collect_recursive_glob(&path, suffix, out);
478 }
479 }
480}
481
482fn walk_subpath(dir: &Path, pattern: &str, out: &mut Vec<PathBuf>) {
488 let (first, rest) = if let Some(slash) = pattern.find('/') {
489 (&pattern[..slash], Some(&pattern[slash + 1..]))
490 } else {
491 (pattern, None)
492 };
493
494 let Ok(entries) = std::fs::read_dir(dir) else {
495 return;
496 };
497
498 for entry in entries.flatten() {
499 let name = entry.file_name();
500 let name_str = name.to_string_lossy();
501
502 if !simple_glob_match(&name_str, first) {
503 continue;
504 }
505
506 if let Some(remaining) = rest {
507 if entry.path().is_dir() {
508 walk_subpath(&entry.path(), remaining, out);
509 }
510 } else {
511 out.push(entry.path());
512 }
513 }
514}
515
516fn simple_glob_match(name: &str, pattern: &str) -> bool {
520 if pattern == "*" {
521 return true;
522 }
523 if !pattern.contains('*') && !pattern.contains('?') {
524 return name == pattern;
525 }
526 super::directives::glob_matches(name, pattern)
528}
529
530fn resolve_block(nodes: &[ConfigNode], resolved: &mut ResolvedHost, seen: &mut HashSet<String>) {
536 for node in nodes {
537 if let ConfigNode::Directive(d) = node {
538 if super::directives::is_accumulative(&d.keyword) {
540 if d.keyword.eq_ignore_ascii_case("identityfile")
541 && !resolved.identity_files.iter().any(|f| f == &d.value)
542 {
543 resolved.identity_files.push(d.value.clone());
544 resolved
545 .directives
546 .push((d.keyword.clone(), d.value.clone()));
547 } else if d.keyword.eq_ignore_ascii_case("certificatefile")
548 && !resolved.certificate_files.iter().any(|f| f == &d.value)
549 {
550 resolved.certificate_files.push(d.value.clone());
551 resolved
552 .directives
553 .push((d.keyword.clone(), d.value.clone()));
554 } else if d.keyword.eq_ignore_ascii_case("localforward")
555 && !resolved.local_forwards.iter().any(|f| f == &d.value)
556 {
557 resolved.local_forwards.push(d.value.clone());
558 resolved
559 .directives
560 .push((d.keyword.clone(), d.value.clone()));
561 } else if d.keyword.eq_ignore_ascii_case("remoteforward")
562 && !resolved.remote_forwards.iter().any(|f| f == &d.value)
563 {
564 resolved.remote_forwards.push(d.value.clone());
565 resolved
566 .directives
567 .push((d.keyword.clone(), d.value.clone()));
568 } else if d.keyword.eq_ignore_ascii_case("dynamicforward")
569 && !resolved.dynamic_forwards.iter().any(|f| f == &d.value)
570 {
571 resolved.dynamic_forwards.push(d.value.clone());
572 resolved
573 .directives
574 .push((d.keyword.clone(), d.value.clone()));
575 }
576 continue;
577 }
578
579 let key_lower = d.keyword.to_ascii_lowercase();
582 if !seen.insert(key_lower) {
583 continue;
584 }
585
586 if d.keyword.eq_ignore_ascii_case("hostname") {
588 resolved.host_name = Some(d.value.clone());
589 } else if d.keyword.eq_ignore_ascii_case("user") {
590 resolved.user = Some(d.value.clone());
591 } else if d.keyword.eq_ignore_ascii_case("port") {
592 resolved.port = d.value.parse::<u16>().ok();
593 } else if d.keyword.eq_ignore_ascii_case("proxyjump") {
594 resolved.proxy_jump = Some(d.value.clone());
595 } else if d.keyword.eq_ignore_ascii_case("identityagent") {
596 resolved.identity_agent = Some(d.value.clone());
597 } else if d.keyword.eq_ignore_ascii_case("forwardagent") {
598 resolved.forward_agent = Some(d.value.clone());
599 } else if d.keyword.eq_ignore_ascii_case("addkeystoagent") {
600 resolved.add_keys_to_agent = Some(d.value.clone());
601 } else if d.keyword.eq_ignore_ascii_case("usekeychain") {
602 resolved.use_keychain = Some(d.value.clone());
603 } else if d.keyword.eq_ignore_ascii_case("controlmaster") {
604 resolved.control_master = Some(d.value.clone());
605 } else if d.keyword.eq_ignore_ascii_case("controlpath") {
606 resolved.control_path = Some(d.value.clone());
607 } else if d.keyword.eq_ignore_ascii_case("controlpersist") {
608 resolved.control_persist = Some(d.value.clone());
609 } else if d.keyword.eq_ignore_ascii_case("userknownhostsfile") {
610 resolved.user_known_hosts_file = Some(d.value.clone());
611 } else if d.keyword.eq_ignore_ascii_case("identitiesonly") {
612 let lv = d.value.to_ascii_lowercase();
613 if lv == "yes" {
614 resolved.identities_only = Some(true);
615 } else if lv == "no" {
616 resolved.identities_only = Some(false);
617 }
618 } else if d.keyword.eq_ignore_ascii_case("gssapiauthentication") {
619 resolved.gssapi_authentication = Some(d.value.clone());
620 } else if d.keyword.eq_ignore_ascii_case("gssapidelegatecredentials") {
621 resolved.gssapi_delegate_credentials = Some(d.value.clone());
622 } else if d.keyword.eq_ignore_ascii_case("gssapiserveridentity") {
623 resolved.gssapi_server_identity = Some(d.value.clone());
624 } else if d.keyword.eq_ignore_ascii_case("gssapiclientidentity") {
625 resolved.gssapi_client_identity = Some(d.value.clone());
626 }
627
628 resolved
629 .directives
630 .push((d.keyword.clone(), d.value.clone()));
631 }
632 }
633}
634
635struct TokenContext<'a> {
637 host: &'a str,
638 home_dir: &'a str,
639 local_hostname: &'a str,
640 remote_user: &'a str,
641 local_user: &'a str,
642 port: &'a str,
643 canonical_host: &'a str,
645 #[allow(
648 dead_code,
649 reason = "placeholder for `%i` token expansion, not yet wired"
650 )]
651 identity_file: Option<&'a str>,
652 local_host_key: &'a str,
654 jump_host: &'a str,
656 remote_host_key: &'a str,
658}
659
660#[expect(
668 clippy::too_many_lines,
669 reason = "serial field-by-field expansion over ResolvedHost"
670)]
671fn expand_resolved(resolved: &mut ResolvedHost, host: &str, _ssh_dir: &Path) {
672 let local_user = whoami();
673 let local_hostname = hostname();
674 let home_dir = dirs::home_dir()
675 .map(|p| p.display().to_string())
676 .unwrap_or_default();
677
678 let port_str = resolved
679 .port
680 .map_or_else(|| "22".to_owned(), |p| p.to_string());
681 let remote_user = resolved.user.as_deref().unwrap_or(&local_user).to_owned();
682
683 let ctx = TokenContext {
684 host,
685 home_dir: &home_dir,
686 local_hostname: &local_hostname,
687 remote_user: &remote_user,
688 local_user: &local_user,
689 port: &port_str,
690 canonical_host: host,
693 identity_file: None,
694 local_host_key: "",
696 jump_host: "",
697 remote_host_key: "",
698 };
699
700 for id_file in &mut resolved.identity_files {
702 *id_file = expand_tilde_and_env(id_file);
703 *id_file = expand_tokens(id_file, &ctx);
704 *id_file = collapse_double_percent(id_file);
705 }
706
707 for cert_file in &mut resolved.certificate_files {
708 *cert_file = expand_tilde_and_env(cert_file);
709 *cert_file = expand_tokens(cert_file, &ctx);
710 *cert_file = collapse_double_percent(cert_file);
711 }
712
713 if let Some(ref mut hn) = resolved.host_name {
714 *hn = expand_tilde_and_env(hn);
715 *hn = expand_tokens(hn, &ctx);
716 *hn = collapse_double_percent(hn);
717 }
718
719 if let Some(ref mut pj) = resolved.proxy_jump {
720 *pj = expand_tokens(pj, &ctx);
721 *pj = collapse_double_percent(pj);
722 }
723
724 if let Some(ref mut ia) = resolved.identity_agent {
725 *ia = expand_tilde_and_env(ia);
726 *ia = expand_tokens(ia, &ctx);
727 *ia = collapse_double_percent(ia);
728 }
729
730 if let Some(ref mut cp) = resolved.control_path {
731 *cp = expand_tilde_and_env(cp);
732 *cp = expand_tokens(cp, &ctx);
733 *cp = collapse_double_percent(cp);
734 }
735
736 if let Some(ref mut fa) = resolved.forward_agent {
737 *fa = expand_tilde_and_env(fa);
738 *fa = expand_tokens(fa, &ctx);
739 *fa = collapse_double_percent(fa);
740 }
741
742 if let Some(ref mut ata) = resolved.add_keys_to_agent {
743 *ata = expand_tilde_and_env(ata);
744 *ata = expand_tokens(ata, &ctx);
745 *ata = collapse_double_percent(ata);
746 }
747
748 if let Some(ref mut uk) = resolved.use_keychain {
749 *uk = expand_tilde_and_env(uk);
750 *uk = expand_tokens(uk, &ctx);
751 *uk = collapse_double_percent(uk);
752 }
753
754 if let Some(ref mut cm) = resolved.control_master {
755 *cm = expand_tilde_and_env(cm);
756 *cm = expand_tokens(cm, &ctx);
757 *cm = collapse_double_percent(cm);
758 }
759
760 if let Some(ref mut cpers) = resolved.control_persist {
761 *cpers = expand_tilde_and_env(cpers);
762 *cpers = expand_tokens(cpers, &ctx);
763 *cpers = collapse_double_percent(cpers);
764 }
765
766 for lf in &mut resolved.local_forwards {
767 *lf = expand_tilde_and_env(lf);
768 *lf = expand_tokens(lf, &ctx);
769 *lf = collapse_double_percent(lf);
770 }
771
772 for rf in &mut resolved.remote_forwards {
773 *rf = expand_tilde_and_env(rf);
774 *rf = expand_tokens(rf, &ctx);
775 *rf = collapse_double_percent(rf);
776 }
777
778 for df in &mut resolved.dynamic_forwards {
779 *df = expand_tilde_and_env(df);
780 *df = expand_tokens(df, &ctx);
781 *df = collapse_double_percent(df);
782 }
783
784 for (key, value) in &mut resolved.directives {
786 let key_lower = key.to_lowercase();
787 if TOKEN_EXPANDABLE.contains(&key_lower.as_str())
788 || key_lower == "identityfile"
789 || key_lower == "hostname"
790 || key_lower == "proxyjump"
791 {
792 let expanded = expand_tilde_and_env(value);
793 let expanded = expand_tokens(&expanded, &ctx);
794 *value = collapse_double_percent(&expanded);
795 }
796 }
797}
798
799fn is_canonicalize_enabled(resolved: &ResolvedHost) -> bool {
804 resolved
805 .directives
806 .iter()
807 .find(|(k, _)| k.eq_ignore_ascii_case("canonicalizehostname"))
808 .is_some_and(|(_, v)| {
809 let lv = v.to_lowercase();
810 lv == "yes" || lv == "always"
811 })
812}
813
814fn expand_tokens(s: &str, ctx: &TokenContext<'_>) -> String {
836 let mut result = String::with_capacity(s.len());
837 let mut chars = s.chars().peekable();
838
839 while let Some(ch) = chars.next() {
840 if ch == '%' {
841 match chars.peek().copied() {
842 Some('%') => {
843 result.push_str("%%");
845 chars.next();
846 }
847 Some('C') => {
848 chars.next();
850 let hash_input = format!("{}:{}:{}", ctx.host, ctx.port, ctx.local_user);
851 let hash = simple_hash(&hash_input);
852 result.push_str(&hash);
853 }
854 Some('d') => {
855 chars.next();
856 result.push_str(ctx.home_dir);
857 }
858 Some('H') => {
859 chars.next();
860 result.push_str(ctx.canonical_host);
861 }
862 Some('h' | 'n') => {
863 chars.next();
864 result.push_str(ctx.host);
865 }
866 Some('L') => {
867 chars.next();
869 let short = ctx
870 .local_hostname
871 .split('.')
872 .next()
873 .unwrap_or(ctx.local_hostname);
874 result.push_str(short);
875 }
876 Some('l') => {
877 chars.next();
878 result.push_str(ctx.local_hostname);
879 }
880 Some('p') => {
881 chars.next();
882 result.push_str(ctx.port);
883 }
884 Some('r' | 'T') => {
885 chars.next();
887 result.push_str(ctx.remote_user);
888 }
889 Some('i' | 'u') => {
890 chars.next();
896 result.push_str(ctx.local_user);
897 }
898 Some('k') => {
899 chars.next();
901 result.push_str(ctx.local_host_key);
902 }
903 Some('j') => {
904 chars.next();
906 result.push_str(ctx.jump_host);
907 }
908 Some('K') => {
909 chars.next();
911 result.push_str(ctx.remote_host_key);
912 }
913 Some('t') => {
914 chars.next();
916 result.push_str(ctx.port);
917 }
918 _ => {
919 result.push(ch);
921 }
922 }
923 } else {
924 result.push(ch);
925 }
926 }
927
928 result
929}
930
931fn simple_hash(s: &str) -> String {
938 let bytes = s.as_bytes();
939 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
941 let prime: u64 = 0x0100_0000_01b3;
942 for &b in bytes {
943 hash ^= u64::from(b);
944 hash = hash.wrapping_mul(prime);
945 }
946 format!("{hash:016x}")
947}
948
949fn collapse_double_percent(s: &str) -> String {
951 s.replace("%%", "%")
952}
953
954fn whoami() -> String {
956 std::env::var("USER")
957 .or_else(|_| std::env::var("USERNAME"))
958 .unwrap_or_else(|_| "unknown".to_owned())
959}
960
961fn hostname() -> String {
963 std::env::var("HOSTNAME")
964 .unwrap_or_else(|_| gethostname::gethostname().to_string_lossy().into_owned())
965}
966
967fn host_matches(host: &str, patterns: &[impl AsRef<str>]) -> bool {
969 super::directives::host_matches_patterns(host, patterns)
970}
971
972fn match_criteria_host(
992 criteria: &str,
993 target_host: &str,
994 target_user: &str,
995 original_host: &str,
996) -> bool {
997 let mut tokens = criteria.split_whitespace();
998
999 let mut has_host = false;
1002 let mut host_matched = false;
1003 let mut has_originalhost = false;
1004 let mut originalhost_matched = false;
1005 let mut has_user = false;
1006 let mut user_matched = false;
1007 let mut has_localuser = false;
1008 let mut localuser_matched = false;
1009
1010 let local_user = whoami();
1012
1013 while let Some(keyword) = tokens.next() {
1014 if keyword.eq_ignore_ascii_case("host") {
1015 if let Some(patterns_str) = tokens.next() {
1016 has_host = true;
1017 let patterns: Vec<&str> = patterns_str.split(',').collect();
1018 if host_matches(target_host, &patterns) {
1019 host_matched = true;
1020 }
1021 }
1022 } else if keyword.eq_ignore_ascii_case("originalhost") {
1023 if let Some(patterns_str) = tokens.next() {
1024 has_originalhost = true;
1025 let patterns: Vec<&str> = patterns_str.split(',').collect();
1026 if host_matches(original_host, &patterns) {
1027 originalhost_matched = true;
1028 }
1029 }
1030 } else if keyword.eq_ignore_ascii_case("user") {
1031 if let Some(names_str) = tokens.next() {
1032 has_user = true;
1033 let names: Vec<&str> = names_str.split(',').collect();
1035 if names.iter().any(|n| n.eq_ignore_ascii_case(target_user)) {
1036 user_matched = true;
1037 }
1038 }
1039 } else if keyword.eq_ignore_ascii_case("localuser") {
1040 if let Some(names_str) = tokens.next() {
1041 has_localuser = true;
1042 let names: Vec<&str> = names_str.split(',').collect();
1043 if names.iter().any(|n| n.eq_ignore_ascii_case(&local_user)) {
1044 localuser_matched = true;
1045 }
1046 }
1047 } else if keyword.eq_ignore_ascii_case("exec") {
1048 break;
1052 } else {
1053 tokens.next();
1056 }
1057 }
1058
1059 let any_known = has_host || has_originalhost || has_user || has_localuser;
1061 let all_matched = (!has_host || host_matched)
1063 && (!has_originalhost || originalhost_matched)
1064 && (!has_user || user_matched)
1065 && (!has_localuser || localuser_matched);
1066
1067 any_known && all_matched
1068}
1069
1070fn contains_exec_criteria(criteria: &str) -> bool {
1076 let mut tokens = criteria.split_whitespace();
1077 while let Some(keyword) = tokens.next() {
1078 if keyword.eq_ignore_ascii_case("exec") {
1079 return true;
1080 }
1081 tokens.next();
1084 }
1085 false
1086}
1087
1088#[cfg(test)]
1089#[path = "resolve.test.rs"]
1090mod tests;