Support regex for application names

pull/138/head
Frederick Zhang 2 years ago
parent 41462c123f
commit 008fabaf80
No known key found for this signature in database
GPG Key ID: 980A192C361BE1AE

9
Cargo.lock generated

@ -714,9 +714,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "regex"
version = "1.5.6"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"aho-corasick",
"memchr",
@ -725,9 +725,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.6.26"
version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "ryu"
@ -1099,6 +1099,7 @@ dependencies = [
"lazy_static",
"log",
"nix 0.24.2",
"regex",
"serde",
"serde_json",
"serde_with",

@ -17,6 +17,7 @@ indoc = "1.0"
lazy_static = "1.4.0"
log = "0.4.17"
nix = "0.24.2"
regex = "1.6.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "2.0", features = ["chrono"] }

@ -1,13 +1,91 @@
use std::str::FromStr;
use anyhow::anyhow;
use regex::Regex;
use serde::{Deserialize, Deserializer};
// TODO: Use trait to allow only either `only` or `not`
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Application {
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub only: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub not: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_matchers")]
pub only: Option<Vec<ApplicationMatcher>>,
#[serde(default, deserialize_with = "deserialize_matchers")]
pub not: Option<Vec<ApplicationMatcher>>,
}
#[derive(Debug)]
pub enum ApplicationMatcher {
Literal(String),
Regex(Regex),
}
impl ApplicationMatcher {
pub fn matches(&self, name: &str) -> bool {
match &self {
ApplicationMatcher::Literal(s) => s == name,
ApplicationMatcher::Regex(r) => r.is_match(name),
}
}
}
impl FromStr for ApplicationMatcher {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let first_char = s.chars().next();
match first_char {
None => Err(anyhow!("Empty application name")),
Some('/') if s.len() < 3 => Err(anyhow!("Application name regex format must be /<regex>/")),
Some('/') => Ok(ApplicationMatcher::Regex(Regex::new(&slash_unescape(s)?)?)),
Some(_) => Ok(ApplicationMatcher::Literal(s.to_owned())),
}
}
}
fn slash_unescape(s: &str) -> anyhow::Result<String> {
let mut result = String::with_capacity(s.len());
let mut escaping = false;
let mut finished = false;
for c in s.chars().skip(1) {
if finished {
return Err(anyhow!("Unexpected trailing string after closing / in application name regex"));
}
if escaping {
escaping = false;
if c != '/' {
result.push('\\');
}
result.push(c);
continue;
}
match c {
'/' => finished = true,
'\\' => escaping = true,
_ => result.push(c),
}
}
if !finished {
return Err(anyhow!("Missing closing / in application name regex"));
}
Ok(result)
}
fn deserialize_matchers<'de, D>(deserializer: D) -> Result<Option<Vec<ApplicationMatcher>>, D::Error>
where
D: Deserializer<'de>,
{
let v = deserialize_string_or_vec(deserializer)?;
match v {
None => Ok(None),
Some(strings) => {
let mut result: Vec<ApplicationMatcher> = vec![];
for s in strings {
result.push(ApplicationMatcher::from_str(&s).map_err(serde::de::Error::custom)?);
}
Ok(Some(result))
}
}
}
pub fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
@ -27,3 +105,50 @@ where
};
Ok(Some(vec))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_literal_application_name_matcher() {
let matcher = ApplicationMatcher::from_str(r"Minecraft").unwrap();
assert!(matcher.matches("Minecraft"), "Failed to match exact Minecraft");
assert!(!matcher.matches("Minecraft Launcher"), "Literal matcher should not be used as substring");
}
#[test]
fn test_regex_application_name_matcher() {
let matcher = ApplicationMatcher::from_str(r"/^Minecraft\*? \d+\.\d+(\.\d+)?$/").unwrap();
assert!(matcher.matches(r"Minecraft 1.19.2"), "Failed to match Minecraft 1.19.2 using regex");
assert!(matcher.matches(r"Minecraft* 1.19"), "Failed to match Minecraft* 1.19 using regex");
assert!(matcher.matches(r"Minecraft* 1.19.2"), "Failed to match Minecraft* 1.19.2 using regex");
}
#[test]
fn test_regex_unescape_application_name_matcher() {
let matcher = ApplicationMatcher::from_str(r"/^\/$/").unwrap();
assert!(matcher.matches(r"/"), "Failed to match single slash using regex");
}
#[test]
fn test_unescape_slash_correct_regex() {
let given = r"/^Mine\d\/craft\\/";
let got = slash_unescape(given).unwrap();
assert_eq!(r"^Mine\d/craft\\", got);
}
#[test]
fn test_unescape_slash_missing_closing_slash() {
let given = r"/^Minecraft\/";
let got = slash_unescape(given).unwrap_err();
assert_eq!("Missing closing / in application name regex", got.to_string());
}
#[test]
fn test_unescape_slash_excessive_string_after_closing() {
let given = r"/^Minecraft/i";
let got = slash_unescape(given).unwrap_err();
assert_eq!("Unexpected trailing string after closing / in application name regex", got.to_string());
}
}

@ -32,6 +32,24 @@ fn test_modmap_application() {
"})
}
#[test]
fn test_modmap_application_regex() {
assert_parse(indoc! {r"
modmap:
- remap:
Alt_L: Ctrl_L
application:
not:
- /^Minecraft/
- /^Minecraft\//
- /^Minecraft\d/
- remap:
Shift_R: Win_R
application:
only: /^Miencraft\\/
"})
}
#[test]
fn test_modmap_multi_purpose_key() {
assert_parse(indoc! {"

@ -436,10 +436,10 @@ impl EventHandler {
if let Some(application) = &self.application_cache {
if let Some(application_only) = &application_matcher.only {
return application_only.contains(application);
return application_only.iter().any(|m| m.matches(application));
}
if let Some(application_not) = &application_matcher.not {
return !application_not.contains(application);
return application_not.iter().all(|m| !m.matches(application));
}
}
false

Loading…
Cancel
Save