feature/prod-ready #1
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user