diff --git a/crates/plugins/plugin-apps/src/linux.rs b/crates/plugins/plugin-apps/src/linux.rs index dd69c1a..acbed48 100644 --- a/crates/plugins/plugin-apps/src/linux.rs +++ b/crates/plugins/plugin-apps/src/linux.rs @@ -45,6 +45,60 @@ impl DesktopEntrySource for FsDesktopEntrySource { } } +pub(crate) fn clean_exec(exec: &str) -> String { + // Tokenize respecting double-quoted strings, then filter field codes. + let mut tokens: Vec = Vec::new(); + let mut chars = exec.chars().peekable(); + + while let Some(&ch) = chars.peek() { + if ch.is_whitespace() { + chars.next(); + continue; + } + if ch == '"' { + // Consume opening quote + chars.next(); + let mut token = String::from('"'); + while let Some(&c) = chars.peek() { + chars.next(); + if c == '"' { + token.push('"'); + break; + } + token.push(c); + } + // Strip embedded field codes like %f inside the quoted string + // (between the quotes, before re-assembling) + let inner = &token[1..token.len().saturating_sub(1)]; + let cleaned_inner: String = inner + .split_whitespace() + .filter(|s| !is_field_code(s)) + .collect::>() + .join(" "); + tokens.push(format!("\"{cleaned_inner}\"")); + } else { + let mut token = String::new(); + while let Some(&c) = chars.peek() { + if c.is_whitespace() { + break; + } + chars.next(); + token.push(c); + } + if !is_field_code(&token) { + tokens.push(token); + } + } + } + + tokens.join(" ") +} + +fn is_field_code(s: &str) -> bool { + let b = s.as_bytes(); + b.len() == 2 && b[0] == b'%' && b[1].is_ascii_alphabetic() +} + pub fn resolve_icon_path(name: &str) -> Option { if name.starts_with('/') && Path::new(name).exists() { return Some(name.to_string()); @@ -111,16 +165,7 @@ fn parse_desktop_file(path: &Path) -> Option { return None; } - let exec_clean: String = exec? - .split_whitespace() - .filter(|s| !s.starts_with('%')) - .fold(String::new(), |mut acc, s| { - if !acc.is_empty() { - acc.push(' '); - } - acc.push_str(s); - acc - }); + let exec_clean: String = clean_exec(&exec?); Some(DesktopEntry { name: AppName::new(name?), @@ -130,3 +175,28 @@ fn parse_desktop_file(path: &Path) -> Option { keywords, }) } + +#[cfg(test)] +mod exec_tests { + use super::clean_exec; + + #[test] + fn strips_bare_field_code() { + assert_eq!(clean_exec("app --file %f"), "app --file"); + } + + #[test] + fn strips_multiple_field_codes() { + assert_eq!(clean_exec("app %U --flag"), "app --flag"); + } + + #[test] + fn preserves_quoted_value() { + assert_eq!(clean_exec(r#"app --arg="value" %U"#), r#"app --arg="value""#); + } + + #[test] + fn handles_plain_exec() { + assert_eq!(clean_exec("firefox"), "firefox"); + } +}