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
135pub fn host_pattern_matches(host_pattern: &str, alias: &str) -> bool {
139 let patterns: Vec<&str> = host_pattern.split_whitespace().collect();
140 if patterns.is_empty() {
141 return false;
142 }
143
144 let mut any_positive_match = false;
145 for pat in &patterns {
146 if let Some(neg) = pat.strip_prefix('!') {
147 if match_glob(neg, alias) {
148 return false;
149 }
150 } else if ssh_pattern_match(pat, alias) {
151 any_positive_match = true;
152 }
153 }
154
155 any_positive_match
156}
157
158pub fn proxy_jump_contains_self(proxy_jump: &str, alias: &str) -> bool {
163 proxy_jump.split(',').any(|hop| {
164 let h = hop.trim();
165 let h = h.split_once('@').map_or(h, |(_, host)| host);
167 let h = if let Some(bracketed) = h.strip_prefix('[') {
169 bracketed.split_once(']').map_or(h, |(host, _)| host)
170 } else {
171 h.rsplit_once(':').map_or(h, |(host, _)| host)
172 };
173 h == alias
174 })
175}
176
177pub(super) fn apply_first_match_fields(
181 proxy_jump: &mut String,
182 user: &mut String,
183 identity_file: &mut String,
184 p: &PatternEntry,
185) {
186 if proxy_jump.is_empty() && !p.proxy_jump.is_empty() {
187 proxy_jump.clone_from(&p.proxy_jump);
188 }
189 if user.is_empty() && !p.user.is_empty() {
190 user.clone_from(&p.user);
191 }
192 if identity_file.is_empty() && !p.identity_file.is_empty() {
193 identity_file.clone_from(&p.identity_file);
194 }
195}