use core::fmt;
#[derive(Debug, Clone)]
pub enum ProcessSelector {
All,
Id(u32),
Name(String),
Regex(regex::Regex),
Fold(String),
}
fn is_glob(input: &str) -> bool {
input.contains(['*', '?', '[', '{'])
}
fn glob_to_regex(input: &str) -> Result<String, SelectorError> {
let glob = globset::Glob::new(input).map_err(|e| SelectorError::BadGlob(e.to_string()))?;
let source = glob.regex().to_string();
Ok(source
.strip_prefix("(?-u)")
.map_or(source.clone(), ToString::to_string))
}
impl ProcessSelector {
pub fn parse(input: &str) -> Result<Self, SelectorError> {
if input.is_empty() {
return Err(SelectorError::Empty);
}
if input == "all" {
return Ok(Self::All);
}
if let Some(fold) = input.strip_prefix("fold:") {
if fold.is_empty() {
return Err(SelectorError::EmptyFold);
}
return Ok(Self::Fold(fold.to_string()));
}
if input.len() >= 2 && input.starts_with('/') && input.ends_with('/') {
let body = &input[1..input.len() - 1];
return regex::Regex::new(body)
.map(Self::Regex)
.map_err(|e| SelectorError::BadRegex(e.to_string()));
}
if input.bytes().all(|b| b.is_ascii_digit())
&& let Ok(id) = input.parse()
{
return Ok(Self::Id(id));
}
if is_glob(input) {
return glob_to_regex(input)
.and_then(|re| {
regex::Regex::new(&re).map_err(|e| SelectorError::BadRegex(e.to_string()))
})
.map(Self::Regex);
}
Ok(Self::Name(input.to_string()))
}
#[must_use]
pub const fn is_exact(&self) -> bool {
match self {
Self::Id(_) | Self::Name(_) => true,
Self::All | Self::Regex(_) | Self::Fold(_) => false,
}
}
#[must_use]
pub fn matches(&self, name: &str, id: u32, fold: Option<&str>) -> bool {
match self {
Self::All => true,
Self::Id(want) => *want == id,
Self::Name(want) => want == name,
Self::Regex(re) => re.is_match(name),
Self::Fold(want) => fold == Some(want.as_str()),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectorError {
Empty,
EmptyFold,
BadRegex(String),
BadGlob(String),
}
impl fmt::Display for SelectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("selector is empty"),
Self::EmptyFold => f.write_str("fold selector is missing a name"),
Self::BadRegex(m) => write!(f, "invalid selector regex: {m}"),
Self::BadGlob(m) => write!(f, "invalid selector glob: {m}"),
}
}
}
impl core::error::Error for SelectorError {}
impl std::convert::TryFrom<crate::protocol::SelectorSpec> for ProcessSelector {
type Error = SelectorError;
fn try_from(spec: crate::protocol::SelectorSpec) -> Result<Self, Self::Error> {
use crate::protocol::SelectorSpec;
Ok(match spec {
SelectorSpec::All => Self::All,
SelectorSpec::Id(id) => Self::Id(id),
SelectorSpec::Name(name) => Self::Name(name),
SelectorSpec::Fold(fold) => Self::Fold(fold),
SelectorSpec::Regex(src) => Self::Regex(
regex::RegexBuilder::new(&src)
.size_limit(1 << 20)
.build()
.map_err(|e| SelectorError::BadRegex(e.to_string()))?,
),
})
}
}
impl From<&ProcessSelector> for crate::protocol::SelectorSpec {
fn from(sel: &ProcessSelector) -> Self {
use crate::protocol::SelectorSpec;
match sel {
ProcessSelector::All => SelectorSpec::All,
ProcessSelector::Id(id) => SelectorSpec::Id(*id),
ProcessSelector::Name(name) => SelectorSpec::Name(name.clone()),
ProcessSelector::Regex(re) => SelectorSpec::Regex(re.as_str().to_string()),
ProcessSelector::Fold(fold) => SelectorSpec::Fold(fold.clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_name_without_a_metacharacter_is_still_an_exact_name() {
for plain in ["zeus-auth", "web.1", "api_v2", "a-b-c"] {
let parsed = ProcessSelector::parse(plain).unwrap();
assert!(
matches!(&parsed, ProcessSelector::Name(name) if name == plain),
"{plain} carries no glob metacharacter and is a name, got {parsed:?}"
);
}
}
#[test]
fn a_glob_matches_by_prefix_and_not_by_substring() {
let ProcessSelector::Regex(re) = ProcessSelector::parse("zeus-*").unwrap() else {
panic!("a pattern with `*` is compiled to a regex");
};
assert!(re.is_match("zeus-auth"));
assert!(re.is_match("zeus-create"));
assert!(!re.is_match("my-zeus-auth"), "anchored: no substring match");
assert!(!re.is_match("reactmap"));
}
#[test]
fn each_glob_metacharacter_compiles_and_matches() {
let cases = [
("*api*", "my-api-thing", "web"),
("zeus-?", "zeus-1", "zeus-auth"),
("zeus-[ab]*", "zeus-auth", "zeus-create"),
("{web,api}", "api", "worker"),
];
for (pattern, hit, miss) in cases {
let ProcessSelector::Regex(re) = ProcessSelector::parse(pattern).unwrap() else {
panic!("{pattern} must compile to a regex");
};
assert!(re.is_match(hit), "{pattern} must match {hit}");
assert!(!re.is_match(miss), "{pattern} must not match {miss}");
}
}
#[test]
fn the_earlier_forms_are_not_shadowed_by_the_glob_gate() {
assert!(matches!(
ProcessSelector::parse("all").unwrap(),
ProcessSelector::All
));
let fold = ProcessSelector::parse("fold:back*end").unwrap();
assert!(
matches!(&fold, ProcessSelector::Fold(name) if name == "back*end"),
"a fold name may contain a metacharacter and is still a fold, got {fold:?}"
);
let ProcessSelector::Regex(re) = ProcessSelector::parse("/^zeus-/").unwrap() else {
panic!("an explicit regex stays a regex");
};
assert!(re.is_match("zeus-auth"));
}
#[test]
fn an_unparseable_glob_is_refused() {
let err = ProcessSelector::parse("zeus-[").expect_err("an unclosed class is not a glob");
assert!(
matches!(err, SelectorError::BadGlob(_)),
"expected BadGlob, got {err:?}"
);
assert!(err.to_string().contains("glob"), "{err}");
}
#[test]
fn parse_rules() {
assert!(matches!(
ProcessSelector::parse("all").unwrap(),
ProcessSelector::All
));
assert!(matches!(
ProcessSelector::parse("3").unwrap(),
ProcessSelector::Id(3)
));
assert!(matches!(
ProcessSelector::parse("web").unwrap(),
ProcessSelector::Name(n) if n == "web"
));
assert!(matches!(
ProcessSelector::parse("/^w/").unwrap(),
ProcessSelector::Regex(_)
));
assert!(matches!(
ProcessSelector::parse("fold:backend").unwrap(),
ProcessSelector::Fold(fname) if fname == "backend"
));
}
#[test]
fn parse_errors() {
assert_eq!(
ProcessSelector::parse("").unwrap_err(),
SelectorError::Empty
);
assert_eq!(
ProcessSelector::parse("fold:").unwrap_err(),
SelectorError::EmptyFold
);
assert!(matches!(
ProcessSelector::parse("/((/").unwrap_err(),
SelectorError::BadRegex(_)
));
}
#[test]
fn matching() {
let by_name = ProcessSelector::parse("web").unwrap();
assert!(by_name.matches("web", 0, None));
assert!(!by_name.matches("worker", 0, None));
let by_regex = ProcessSelector::parse("/^w/").unwrap();
assert!(by_regex.matches("worker", 9, None));
assert!(!by_regex.matches("api", 9, None));
let by_fold = ProcessSelector::parse("fold:backend").unwrap();
assert!(by_fold.matches("anything", 0, Some("backend")));
assert!(!by_fold.matches("anything", 0, None));
assert!(
ProcessSelector::parse("all")
.unwrap()
.matches("x", 42, None)
);
assert!(ProcessSelector::parse("42").unwrap().matches("x", 42, None));
}
#[test]
fn only_a_name_or_an_id_names_one_entry_the_caller_knew_of() {
assert!(ProcessSelector::Name("bark".into()).is_exact());
assert!(ProcessSelector::Id(4).is_exact());
assert!(!ProcessSelector::All.is_exact());
assert!(!ProcessSelector::Fold("api".into()).is_exact());
assert!(!ProcessSelector::parse("/^bark$/").unwrap().is_exact());
}
#[test]
fn a_name_that_looks_numeric_is_an_id() {
assert!(matches!(
ProcessSelector::parse("42").unwrap(),
ProcessSelector::Id(42)
));
}
#[test]
fn selector_spec_bridges() {
use crate::protocol::SelectorSpec;
let sel: ProcessSelector = SelectorSpec::Regex("^w".to_string()).try_into().unwrap();
assert!(sel.matches("web", 1, None));
assert_eq!(
SelectorSpec::from(&sel),
SelectorSpec::Regex("^w".to_string())
);
for spec in [
SelectorSpec::All,
SelectorSpec::Id(3),
SelectorSpec::Name("web".to_string()),
SelectorSpec::Fold("backend".to_string()),
] {
let sel: ProcessSelector = spec.clone().try_into().unwrap();
assert_eq!(SelectorSpec::from(&sel), spec);
}
}
#[test]
fn selector_spec_bad_regex_is_typed_error() {
use crate::protocol::SelectorSpec;
assert!(matches!(
ProcessSelector::try_from(SelectorSpec::Regex("((".to_string())).unwrap_err(),
SelectorError::BadRegex(_)
));
}
#[test]
fn selector_spec_oversized_regex_is_rejected() {
use crate::protocol::SelectorSpec;
let huge = format!("(a{}){{10000}}", "|b".repeat(100_000));
assert!(ProcessSelector::try_from(SelectorSpec::Regex(huge)).is_err());
}
}