fix(apps): proper exec field-code stripping with quote-awareness

This commit is contained in:
2026-03-15 19:56:02 +01:00
parent b68aef83ba
commit d1122ff4f0

View File

@@ -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<String> = 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::<Vec<_>>()
.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<String> { pub fn resolve_icon_path(name: &str) -> Option<String> {
if name.starts_with('/') && Path::new(name).exists() { if name.starts_with('/') && Path::new(name).exists() {
return Some(name.to_string()); return Some(name.to_string());
@@ -111,16 +165,7 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopEntry> {
return None; return None;
} }
let exec_clean: String = exec? let exec_clean: String = clean_exec(&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
});
Some(DesktopEntry { Some(DesktopEntry {
name: AppName::new(name?), name: AppName::new(name?),
@@ -130,3 +175,28 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopEntry> {
keywords, 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");
}
}