1use core::fmt;
7
8#[derive(Debug, Clone)]
10pub enum ProcessSelector {
11 All,
13 Id(u32),
15 Name(String),
17 Regex(regex::Regex),
19 Fold(String),
21 Instance {
23 name: String,
25 slot: u32,
27 },
28}
29
30fn is_glob(input: &str) -> bool {
40 input.contains(['*', '?', '[', '{'])
41}
42
43fn glob_to_regex(input: &str) -> Result<regex::Regex, SelectorError> {
58 let glob = globset::Glob::new(input).map_err(|e| SelectorError::BadGlob(e.to_string()))?;
59 let source = glob.regex();
60 let source = source.strip_prefix("(?-u)").unwrap_or(source);
61 regex::Regex::new(source).map_err(|e| SelectorError::BadRegex(e.to_string()))
62}
63
64fn parse_u32_digits(s: &str) -> Option<u32> {
72 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
73 return None;
74 }
75 s.parse().ok()
76}
77
78impl ProcessSelector {
79 pub fn parse(input: &str) -> Result<Self, SelectorError> {
90 if input.is_empty() {
91 return Err(SelectorError::Empty);
92 }
93 if input == "all" {
94 return Ok(Self::All);
95 }
96 if let Some(fold) = input.strip_prefix("fold:") {
97 if fold.is_empty() {
98 return Err(SelectorError::EmptyFold);
99 }
100 return Ok(Self::Fold(fold.to_string()));
101 }
102 if input.len() >= 2 && input.starts_with('/') && input.ends_with('/') {
103 let body = &input[1..input.len() - 1];
104 return regex::Regex::new(body)
105 .map(Self::Regex)
106 .map_err(|e| SelectorError::BadRegex(e.to_string()));
107 }
108 if let Some(id) = parse_u32_digits(input) {
109 return Ok(Self::Id(id));
110 }
111 if is_glob(input) {
112 return glob_to_regex(input).map(Self::Regex);
113 }
114 if let Some((name, slot)) = input.rsplit_once(':')
118 && !name.is_empty()
119 && let Some(slot) = parse_u32_digits(slot)
120 {
121 return Ok(Self::Instance {
122 name: name.to_string(),
123 slot,
124 });
125 }
126
127 Ok(Self::Name(input.to_string()))
128 }
129
130 #[must_use]
139 pub const fn is_exact(&self) -> bool {
140 match self {
141 Self::Id(_) | Self::Name(_) | Self::Instance { .. } => true,
142 Self::All | Self::Regex(_) | Self::Fold(_) => false,
143 }
144 }
145
146 #[must_use]
148 pub fn matches(&self, name: &str, id: u32, fold: Option<&str>, instance: Option<u32>) -> bool {
149 match self {
150 Self::All => true,
151 Self::Id(want) => *want == id,
152 Self::Name(want) => want == name,
153 Self::Regex(re) => re.is_match(name),
154 Self::Fold(want) => fold == Some(want.as_str()),
155 Self::Instance { name: want, slot } => want == name && instance == Some(*slot),
160 }
161 }
162}
163
164#[non_exhaustive]
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum SelectorError {
174 Empty,
176 EmptyFold,
178 BadRegex(String),
180 BadGlob(String),
182}
183
184impl fmt::Display for SelectorError {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 match self {
187 Self::Empty => f.write_str("selector is empty"),
188 Self::EmptyFold => f.write_str("fold selector is missing a name"),
189 Self::BadRegex(m) => write!(f, "invalid selector regex: {m}"),
190 Self::BadGlob(m) => write!(f, "invalid selector glob: {m}"),
191 }
192 }
193}
194
195impl core::error::Error for SelectorError {}
196
197impl std::convert::TryFrom<crate::protocol::SelectorSpec> for ProcessSelector {
198 type Error = SelectorError;
199
200 fn try_from(spec: crate::protocol::SelectorSpec) -> Result<Self, Self::Error> {
207 use crate::protocol::SelectorSpec;
208 Ok(match spec {
209 SelectorSpec::All => Self::All,
210 SelectorSpec::Id(id) => Self::Id(id),
211 SelectorSpec::Name(name) => Self::Name(name),
212 SelectorSpec::Fold(fold) => Self::Fold(fold),
213 SelectorSpec::Instance { name, slot } => Self::Instance { name, slot },
214 SelectorSpec::Regex(src) => Self::Regex(
215 regex::RegexBuilder::new(&src)
217 .size_limit(1 << 20)
218 .build()
219 .map_err(|e| SelectorError::BadRegex(e.to_string()))?,
220 ),
221 })
222 }
223}
224
225impl From<&ProcessSelector> for crate::protocol::SelectorSpec {
226 fn from(sel: &ProcessSelector) -> Self {
227 use crate::protocol::SelectorSpec;
228 match sel {
229 ProcessSelector::All => SelectorSpec::All,
230 ProcessSelector::Id(id) => SelectorSpec::Id(*id),
231 ProcessSelector::Name(name) => SelectorSpec::Name(name.clone()),
232 ProcessSelector::Regex(re) => SelectorSpec::Regex(re.as_str().to_string()),
233 ProcessSelector::Fold(fold) => SelectorSpec::Fold(fold.clone()),
234 ProcessSelector::Instance { name, slot } => SelectorSpec::Instance {
235 name: name.clone(),
236 slot: *slot,
237 },
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn a_name_without_a_metacharacter_is_still_an_exact_name() {
248 for plain in ["api-auth", "web.1", "api_v2", "a-b-c"] {
249 let parsed = ProcessSelector::parse(plain).unwrap();
250 assert!(
251 matches!(&parsed, ProcessSelector::Name(name) if name == plain),
252 "{plain} carries no glob metacharacter and is a name, got {parsed:?}"
253 );
254 }
255 }
256
257 #[test]
258 fn a_glob_matches_by_prefix_and_not_by_substring() {
259 let ProcessSelector::Regex(re) = ProcessSelector::parse("api-*").unwrap() else {
260 panic!("a pattern with `*` is compiled to a regex");
261 };
262 assert!(re.is_match("api-auth"));
263 assert!(re.is_match("api-create"));
264 assert!(!re.is_match("my-api-auth"), "anchored: no substring match");
265 assert!(!re.is_match("reactmap"));
266 }
267
268 #[test]
271 fn each_glob_metacharacter_compiles_and_matches() {
272 let cases = [
273 ("*api*", "my-api-thing", "web"),
274 ("api-?", "api-1", "api-auth"),
275 ("api-[ab]*", "api-auth", "api-create"),
276 ("{web,api}", "api", "worker"),
277 ];
278 for (pattern, hit, miss) in cases {
279 let ProcessSelector::Regex(re) = ProcessSelector::parse(pattern).unwrap() else {
280 panic!("{pattern} must compile to a regex");
281 };
282 assert!(re.is_match(hit), "{pattern} must match {hit}");
283 assert!(!re.is_match(miss), "{pattern} must not match {miss}");
284 }
285 }
286
287 #[test]
288 fn the_earlier_forms_are_not_shadowed_by_the_glob_gate() {
289 assert!(matches!(
290 ProcessSelector::parse("all").unwrap(),
291 ProcessSelector::All
292 ));
293 let fold = ProcessSelector::parse("fold:back*end").unwrap();
294 assert!(
295 matches!(&fold, ProcessSelector::Fold(name) if name == "back*end"),
296 "a fold name may contain a metacharacter and is still a fold, got {fold:?}"
297 );
298 let ProcessSelector::Regex(re) = ProcessSelector::parse("/^api-/").unwrap() else {
299 panic!("an explicit regex stays a regex");
300 };
301 assert!(re.is_match("api-auth"));
302 }
303
304 #[test]
307 fn an_unparseable_glob_is_refused() {
308 let err = ProcessSelector::parse("api-[").expect_err("an unclosed class is not a glob");
309 assert!(
310 matches!(err, SelectorError::BadGlob(_)),
311 "expected BadGlob, got {err:?}"
312 );
313 assert!(err.to_string().contains("glob"), "{err}");
314 }
315
316 #[test]
317 fn parse_rules() {
318 assert!(matches!(
319 ProcessSelector::parse("all").unwrap(),
320 ProcessSelector::All
321 ));
322 assert!(matches!(
323 ProcessSelector::parse("3").unwrap(),
324 ProcessSelector::Id(3)
325 ));
326 assert!(matches!(
327 ProcessSelector::parse("web").unwrap(),
328 ProcessSelector::Name(n) if n == "web"
329 ));
330 assert!(matches!(
331 ProcessSelector::parse("/^w/").unwrap(),
332 ProcessSelector::Regex(_)
333 ));
334 assert!(matches!(
335 ProcessSelector::parse("fold:backend").unwrap(),
336 ProcessSelector::Fold(fname) if fname == "backend"
337 ));
338 }
339
340 #[test]
341 fn parse_errors() {
342 assert_eq!(
343 ProcessSelector::parse("").unwrap_err(),
344 SelectorError::Empty
345 );
346 assert_eq!(
347 ProcessSelector::parse("fold:").unwrap_err(),
348 SelectorError::EmptyFold
349 );
350 assert!(matches!(
351 ProcessSelector::parse("/((/").unwrap_err(),
352 SelectorError::BadRegex(_)
353 ));
354 }
355
356 #[test]
357 fn matching() {
358 let by_name = ProcessSelector::parse("web").unwrap();
359 assert!(by_name.matches("web", 0, None, None));
360 assert!(!by_name.matches("worker", 0, None, None));
361
362 let by_regex = ProcessSelector::parse("/^w/").unwrap();
363 assert!(by_regex.matches("worker", 9, None, None));
364 assert!(!by_regex.matches("api", 9, None, None));
365
366 let by_fold = ProcessSelector::parse("fold:backend").unwrap();
367 assert!(by_fold.matches("anything", 0, Some("backend"), None));
368 assert!(!by_fold.matches("anything", 0, None, None));
369
370 assert!(
371 ProcessSelector::parse("all")
372 .unwrap()
373 .matches("x", 42, None, None)
374 );
375 assert!(
376 ProcessSelector::parse("42")
377 .unwrap()
378 .matches("x", 42, None, None)
379 );
380 }
381
382 #[test]
385 fn only_a_name_or_an_id_names_one_entry_the_caller_knew_of() {
386 assert!(ProcessSelector::Name("bark".into()).is_exact());
387 assert!(ProcessSelector::Id(4).is_exact());
388 assert!(!ProcessSelector::All.is_exact());
389 assert!(!ProcessSelector::Fold("api".into()).is_exact());
390 assert!(!ProcessSelector::parse("/^bark$/").unwrap().is_exact());
393 }
394
395 #[test]
396 fn a_name_that_looks_numeric_is_an_id() {
397 assert!(matches!(
400 ProcessSelector::parse("42").unwrap(),
401 ProcessSelector::Id(42)
402 ));
403 }
404
405 #[test]
406 fn a_signed_number_is_a_name_at_both_sites_that_read_one() {
407 assert!(matches!(
411 ProcessSelector::parse("+5").unwrap(),
412 ProcessSelector::Name(name) if name == "+5"
413 ));
414 assert!(matches!(
415 ProcessSelector::parse("web:+2").unwrap(),
416 ProcessSelector::Name(name) if name == "web:+2"
417 ));
418 }
419
420 #[test]
421 fn selector_spec_bridges() {
422 use crate::protocol::SelectorSpec;
423 let sel: ProcessSelector = SelectorSpec::Regex("^w".to_string()).try_into().unwrap();
424 assert!(sel.matches("web", 1, None, None));
425 assert_eq!(
426 SelectorSpec::from(&sel),
427 SelectorSpec::Regex("^w".to_string())
428 );
429 for spec in [
430 SelectorSpec::All,
431 SelectorSpec::Id(3),
432 SelectorSpec::Name("web".to_string()),
433 SelectorSpec::Fold("backend".to_string()),
434 SelectorSpec::Instance {
435 name: "web".to_string(),
436 slot: 2,
437 },
438 ] {
439 let sel: ProcessSelector = spec.clone().try_into().unwrap();
440 assert_eq!(SelectorSpec::from(&sel), spec);
441 }
442 }
443
444 #[test]
445 fn selector_spec_bad_regex_is_typed_error() {
446 use crate::protocol::SelectorSpec;
447 assert!(matches!(
448 ProcessSelector::try_from(SelectorSpec::Regex("((".to_string())).unwrap_err(),
449 SelectorError::BadRegex(_)
450 ));
451 }
452
453 #[test]
454 fn an_instance_form_parses_and_matches_only_its_slot() {
455 let sel = ProcessSelector::parse("web:2").expect("parses");
456 assert!(matches!(
457 &sel,
458 ProcessSelector::Instance { name, slot } if name == "web" && *slot == 2
459 ));
460 assert!(sel.matches("web", 7, None, Some(2)));
461 assert!(!sel.matches("web", 7, None, Some(1)));
462 assert!(!sel.matches("api", 7, None, Some(2)));
463 assert!(
464 !sel.matches("web", 7, None, None),
465 "an older daemon's row carries no slot, so it cannot be the one asked for"
466 );
467 }
468
469 #[test]
470 fn an_instance_selector_names_one_entry_so_it_is_exact() {
471 assert!(
473 ProcessSelector::parse("metrics:0")
474 .expect("parses")
475 .is_exact()
476 );
477 }
478
479 #[test]
480 fn the_colon_forms_do_not_shadow_each_other() {
481 assert!(matches!(
482 ProcessSelector::parse("fold:web").expect("parses"),
483 ProcessSelector::Fold(_)
484 ));
485 assert!(matches!(
486 ProcessSelector::parse("web:2").expect("parses"),
487 ProcessSelector::Instance { .. }
488 ));
489 assert!(matches!(
492 ProcessSelector::parse("web:two").expect("parses"),
493 ProcessSelector::Name(_)
494 ));
495 assert!(matches!(
497 ProcessSelector::parse("web*:2").expect("parses"),
498 ProcessSelector::Regex(_)
499 ));
500 assert!(matches!(
502 ProcessSelector::parse("11").expect("parses"),
503 ProcessSelector::Id(11)
504 ));
505 }
506
507 #[test]
508 fn an_instance_selector_round_trips_through_the_wire_form() {
509 let sel = ProcessSelector::parse("web:2").expect("parses");
510 let spec = crate::protocol::SelectorSpec::from(&sel);
511 assert_eq!(
512 spec,
513 crate::protocol::SelectorSpec::Instance {
514 name: "web".to_string(),
515 slot: 2
516 }
517 );
518 let back = ProcessSelector::try_from(spec).expect("converts back");
519 assert!(matches!(back, ProcessSelector::Instance { .. }));
520 }
521
522 #[test]
523 fn selector_spec_oversized_regex_is_rejected() {
524 use crate::protocol::SelectorSpec;
529 let huge = format!("(a{}){{10000}}", "|b".repeat(100_000));
530 assert!(ProcessSelector::try_from(SelectorSpec::Regex(huge)).is_err());
531 }
532}