purple_ssh/ssh_config/
pattern.rs1use super::model::PatternEntry;
9
10pub fn is_host_pattern(pattern: &str) -> bool {
12 pattern.contains('*')
13 || pattern.contains('?')
14 || pattern.contains('[')
15 || pattern.starts_with('!')
16 || pattern.contains(' ')
17 || pattern.contains('\t')
18}
19
20pub fn ssh_pattern_match(pattern: &str, text: &str) -> bool {
24 if let Some(rest) = pattern.strip_prefix('!') {
25 return !match_glob(rest, text);
26 }
27 match_glob(pattern, text)
28}
29
30fn match_glob(pattern: &str, text: &str) -> bool {
33 if text.is_empty() {
34 return pattern.is_empty();
35 }
36 if pattern.is_empty() {
37 return false;
38 }
39 let pat: Vec<char> = pattern.chars().collect();
40 let txt: Vec<char> = text.chars().collect();
41 glob_match(&pat, &txt)
42}
43
44fn glob_match(pat: &[char], txt: &[char]) -> bool {
46 let mut pi = 0;
47 let mut ti = 0;
48 let mut star: Option<(usize, usize)> = None; while ti < txt.len() {
51 if pi < pat.len() && pat[pi] == '?' {
52 pi += 1;
53 ti += 1;
54 } else if pi < pat.len() && pat[pi] == '*' {
55 star = Some((pi + 1, ti));
56 pi += 1;
57 } else if pi < pat.len() && pat[pi] == '[' {
58 if let Some((matches, end)) = match_char_class(pat, pi, txt[ti]) {
59 if matches {
60 pi = end;
61 ti += 1;
62 } else if let Some((spi, sti)) = star {
63 let sti = sti + 1;
64 star = Some((spi, sti));
65 pi = spi;
66 ti = sti;
67 } else {
68 return false;
69 }
70 } else if let Some((spi, sti)) = star {
71 let sti = sti + 1;
73 star = Some((spi, sti));
74 pi = spi;
75 ti = sti;
76 } else {
77 return false;
78 }
79 } else if pi < pat.len() && pat[pi] == txt[ti] {
80 pi += 1;
81 ti += 1;
82 } else if let Some((spi, sti)) = star {
83 let sti = sti + 1;
84 star = Some((spi, sti));
85 pi = spi;
86 ti = sti;
87 } else {
88 return false;
89 }
90 }
91
92 while pi < pat.len() && pat[pi] == '*' {
93 pi += 1;
94 }
95 pi == pat.len()
96}
97
98fn match_char_class(pat: &[char], start: usize, ch: char) -> Option<(bool, usize)> {
102 let mut i = start + 1;
103 if i >= pat.len() {
104 return None;
105 }
106
107 let negate = pat[i] == '!' || pat[i] == '^';
108 if negate {
109 i += 1;
110 }
111
112 let mut matched = false;
113 while i < pat.len() && pat[i] != ']' {
114 if i + 2 < pat.len() && pat[i + 1] == '-' && pat[i + 2] != ']' {
115 let lo = pat[i];
116 let hi = pat[i + 2];
117 if ch >= lo && ch <= hi {
118 matched = true;
119 }
120 i += 3;
121 } else {
122 matched |= pat[i] == ch;
123 i += 1;
124 }
125 }
126
127 if i >= pat.len() {
128 return None;
129 }
130
131 let result = if negate { !matched } else { matched };
132 Some((result, i + 1))
133}
134
135fn unquote_pattern_token(token: &str) -> &str {
140 if token.len() >= 2 && token.starts_with('"') && token.ends_with('"') {
141 &token[1..token.len() - 1]
142 } else {
143 token
144 }
145}
146
147pub fn host_pattern_matches(host_pattern: &str, alias: &str) -> bool {
151 let patterns: Vec<&str> = host_pattern.split_whitespace().collect();
152 if patterns.is_empty() {
153 return false;
154 }
155
156 let mut any_positive_match = false;
157 for pat in &patterns {
158 let pat = unquote_pattern_token(pat);
159 if let Some(neg) = pat.strip_prefix('!') {
160 if match_glob(neg, alias) {
161 return false;
162 }
163 } else if ssh_pattern_match(pat, alias) {
164 any_positive_match = true;
165 }
166 }
167
168 any_positive_match
169}
170
171pub fn proxy_jump_contains_self(proxy_jump: &str, alias: &str) -> bool {
176 proxy_jump.split(',').any(|hop| {
177 let h = hop.trim();
178 let h = h.split_once('@').map_or(h, |(_, host)| host);
180 let h = if let Some(bracketed) = h.strip_prefix('[') {
182 bracketed.split_once(']').map_or(h, |(host, _)| host)
183 } else {
184 h.rsplit_once(':').map_or(h, |(host, _)| host)
185 };
186 h == alias
187 })
188}
189
190pub(super) fn apply_first_match_fields(
194 proxy_jump: &mut String,
195 user: &mut String,
196 identity_file: &mut String,
197 p: &PatternEntry,
198) {
199 if proxy_jump.is_empty() && !p.proxy_jump.is_empty() {
200 proxy_jump.clone_from(&p.proxy_jump);
201 }
202 if user.is_empty() && !p.user.is_empty() {
203 user.clone_from(&p.user);
204 }
205 if identity_file.is_empty() && !p.identity_file.is_empty() {
206 identity_file.clone_from(&p.identity_file);
207 }
208}