wyvern/extensions/
match_logic.rs1use std::path::Path;
4
5use super::{ExtensionDef, ExtensionRegistry, MatchSpec, MatchToken};
6
7#[derive(Debug, Clone)]
9pub enum ExtensionMatch<'a> {
10 Suffix {
12 ext: &'a ExtensionDef,
14 path: &'a str,
16 },
17 Prefix {
19 ext: &'a ExtensionDef,
21 args_after_prefix: &'a [String],
23 },
24 PrefixSuffix {
26 ext: &'a ExtensionDef,
28 path: &'a str,
30 args_after_prefix: &'a [String],
32 },
33}
34
35impl<'a> ExtensionMatch<'a> {
36 #[must_use]
38 pub fn extension(&self) -> &'a ExtensionDef {
39 match self {
40 Self::Suffix { ext, .. }
41 | Self::Prefix { ext, .. }
42 | Self::PrefixSuffix { ext, .. } => ext,
43 }
44 }
45
46 #[must_use]
48 pub fn path(&self) -> Option<&'a str> {
49 match self {
50 Self::Suffix { path, .. } | Self::PrefixSuffix { path, .. } => Some(*path),
51 Self::Prefix { .. } => None,
52 }
53 }
54
55 #[must_use]
57 pub fn args_after_prefix(&self) -> &'a [String] {
58 match self {
59 Self::Prefix {
60 args_after_prefix, ..
61 }
62 | Self::PrefixSuffix {
63 args_after_prefix, ..
64 } => args_after_prefix,
65 Self::Suffix { .. } => &[],
66 }
67 }
68}
69
70impl ExtensionDef {
71 pub(super) fn match_spec_argv<'a>(&'a self, argv: &'a [String]) -> Option<ExtensionMatch<'a>> {
72 let spec = &self.match_spec;
73 if let Some(prefix) = &spec.argv_prefix {
74 if argv.len() < prefix.len()
75 || !prefix
76 .iter()
77 .zip(argv.iter())
78 .all(|(expected, got)| expected.as_str() == got)
79 {
80 return None;
81 }
82 let rest = &argv[prefix.len()..];
83 if let Some(suffix) = &spec.arg_suffix {
84 let path = rest
85 .iter()
86 .find(|token| ends_with_suffix(token, suffix.as_str()))?;
87 return Some(ExtensionMatch::PrefixSuffix {
88 ext: self,
89 path: path.as_str(),
90 args_after_prefix: rest,
91 });
92 }
93 return Some(ExtensionMatch::Prefix {
94 ext: self,
95 args_after_prefix: rest,
96 });
97 }
98 if argv.len() != 1 {
99 return None;
100 }
101 let token = argv[0].as_str();
102 if let Some(filename) = &spec.filename {
103 let base = Path::new(token).file_name()?.to_str()?;
104 if base == filename.as_str() {
105 return Some(ExtensionMatch::Suffix {
106 ext: self,
107 path: token,
108 });
109 }
110 return None;
111 }
112 if let Some(suffix) = &spec.positional_suffix {
113 if ends_with_suffix(token, suffix.as_str()) {
114 return Some(ExtensionMatch::Suffix {
115 ext: self,
116 path: token,
117 });
118 }
119 }
120 None
121 }
122}
123
124pub(crate) fn ends_with_suffix(token: &str, suffix: &str) -> bool {
125 let Some(start) = token.len().checked_sub(suffix.len()) else {
126 return false;
127 };
128 token
131 .get(start..)
132 .is_some_and(|tail| tail.eq_ignore_ascii_case(suffix))
133}
134
135#[must_use]
141pub fn match_extension_help<'a>(
142 registry: &'a ExtensionRegistry,
143 argv: &'a [String],
144) -> Option<&'a ExtensionDef> {
145 if let Some(ext) = match_prefix_help(registry, argv) {
146 return Some(ext);
147 }
148 match_suffix_help(registry, argv)
149}
150
151fn match_prefix_help<'a>(
152 registry: &'a ExtensionRegistry,
153 argv: &'a [String],
154) -> Option<&'a ExtensionDef> {
155 let mut best: Option<(&'a ExtensionDef, usize)> = None;
156 for ext in registry.extensions() {
157 let Some(prefix) = &ext.match_spec.argv_prefix else {
158 continue;
159 };
160 if prefix.is_empty() || argv.len() < prefix.len() {
161 continue;
162 }
163 if !prefix
164 .iter()
165 .zip(argv.iter())
166 .all(|(expected, got)| expected.as_str() == got)
167 {
168 continue;
169 }
170 if !is_help_only_tokens(&argv[prefix.len()..]) {
171 continue;
172 }
173 let len = prefix.len();
174 if best.is_none_or(|(_, best_len)| len > best_len) {
175 best = Some((ext, len));
176 }
177 }
178 best.map(|(ext, _)| ext)
179}
180
181fn match_suffix_help<'a>(
182 registry: &'a ExtensionRegistry,
183 argv: &'a [String],
184) -> Option<&'a ExtensionDef> {
185 if argv.len() < 2 || !is_help_only_tokens(&argv[1..]) {
186 return None;
187 }
188 let path = argv[0].as_str();
189 registry
190 .extensions()
191 .iter()
192 .find(|ext| ext.match_spec.argv_prefix.is_none() && matches_suffix_or_filename(ext, path))
193}
194
195fn matches_suffix_or_filename(ext: &ExtensionDef, token: &str) -> bool {
196 let spec = &ext.match_spec;
197 if let Some(filename) = &spec.filename {
198 return Path::new(token)
199 .file_name()
200 .and_then(|name| name.to_str())
201 .is_some_and(|base| base == filename.as_str());
202 }
203 spec.positional_suffix
204 .as_ref()
205 .is_some_and(|suffix| ends_with_suffix(token, suffix.as_str()))
206}
207
208#[must_use]
210pub fn is_help_only_tokens(tokens: &[String]) -> bool {
211 !tokens.is_empty()
212 && tokens
213 .iter()
214 .all(|token| token == "--help" || token == "-h")
215}
216
217#[must_use]
218pub fn match_kind_summary(spec: &MatchSpec) -> String {
219 if let Some(prefix) = &spec.argv_prefix {
220 let prefix_s = prefix
221 .iter()
222 .map(MatchToken::as_str)
223 .collect::<Vec<_>>()
224 .join(" ");
225 if let Some(suffix) = &spec.arg_suffix {
226 return format!("prefix+suffix: {prefix_s} {suffix}");
227 }
228 return format!("prefix: {prefix_s}");
229 }
230 if let Some(filename) = &spec.filename {
231 return format!("filename: {filename}");
232 }
233 if let Some(suffix) = &spec.positional_suffix {
234 return format!("suffix: {suffix}");
235 }
236 "match: (none)".to_string()
237}