@@ 342,13 342,13 @@ mod tests {
App::new(
"Test1".to_owned(),
"icon".to_owned(),
- "/bin/test".to_owned(),
+ vec!["/bin/test".to_owned()],
false,
),
App::new(
"Test2".to_owned(),
"icon".to_owned(),
- "/bin/test".to_owned(),
+ vec!["/bin/test".to_owned()],
false,
),
];
@@ 363,13 363,13 @@ mod tests {
App::new(
"Test1".to_owned(),
"icon".to_owned(),
- "/bin/test".to_owned(),
+ vec!["/bin/test".to_owned()],
false,
),
App::new(
"Test2".to_owned(),
"icon".to_owned(),
- "/bin/test".to_owned(),
+ vec!["/bin/test".to_owned()],
false,
),
];
@@ 384,14 384,14 @@ mod tests {
let mut apps = vec![App::new(
"Test1".to_owned(),
"icon".to_owned(),
- "/bin/test".to_owned(),
+ vec!["/bin/test".to_owned()],
false,
)];
let mut apps_db = AppsDB::new(Config::default(), apps.clone());
apps.push(App::new(
"Test2".to_owned(),
"icon".to_owned(),
- "/bin/test".to_owned(),
+ vec!["/bin/test".to_owned()],
false,
));
apps_db.merge_new_entries(apps.clone());
@@ 70,14 70,7 @@ fn prop_is_true(item: Option<&str>) -> Result<bool, ParseBoolError> {
}
}
-fn strip_entry_args(exec: &str) -> Vec<String> {
- exec.split(' ')
- .filter(|item| !item.starts_with('%'))
- .map(|s| s.to_owned())
- .collect()
-}
-
-fn unescape_string(s: &str) -> Result<String, char> {
+fn unescape_string(s: &str) -> String {
let mut unescaped = String::with_capacity(s.len());
let mut iter = s.chars();
while let Some(c) = iter.next() {
@@ 88,14 81,86 @@ fn unescape_string(s: &str) -> Result<String, char> {
Some('s') => unescaped.push(' '),
Some('t') => unescaped.push('\t'),
Some('r') => unescaped.push('\r'),
- Some(invalid) => return Err(invalid),
+ Some(other) => {
+ unescaped.push('\\');
+ unescaped.push(other);
+ }
None => {}
}
} else {
unescaped.push(c);
}
}
- Ok(unescaped)
+ unescaped
+}
+
+fn parse_exec(s: &str, name: &str, icon: &str, file_path: &str) -> Vec<String> {
+ let mut output = Vec::new();
+ let mut iter = s.chars();
+ let mut in_quote = false;
+ let mut part = String::new();
+ fn push(v: &mut Vec<String>, item: String) {
+ if !item.is_empty() {
+ v.push(item);
+ }
+ }
+ while let Some(c) = iter.next() {
+ if in_quote {
+ if c == '"' {
+ push(&mut output, part);
+ part = String::new();
+ in_quote = false;
+ } else if c == '\\' {
+ match iter.next() {
+ Some('"') => part.push('"'),
+ Some('`') => part.push('`'),
+ Some('$') => part.push('$'),
+ Some('\\') => part.push('\\'),
+ Some(other) => {
+ part.push('\\');
+ part.push(other);
+ }
+ None => {}
+ }
+ } else {
+ part.push(c);
+ }
+ } else {
+ if c == '"' {
+ in_quote = true;
+ } else if c == ' ' {
+ push(&mut output, part);
+ part = String::new();
+ } else if c == '%' {
+ match iter.next() {
+ Some('%') => part.push('%'),
+ Some('i') => {
+ if !icon.is_empty() {
+ push(&mut output, part);
+ output.push("--icon".to_owned());
+ output.push(icon.to_owned());
+ part = String::new();
+ }
+ }
+ Some('c') => {
+ push(&mut output, part);
+ output.push(name.to_owned());
+ part = String::new();
+ }
+ Some('k') => {
+ push(&mut output, part);
+ output.push(file_path.to_owned());
+ part = String::new();
+ }
+ Some(_) | None => {}
+ }
+ } else {
+ part.push(c);
+ }
+ }
+ }
+ push(&mut output, part);
+ output
}
/// Parse a desktop entry
@@ 160,28 225,18 @@ pub fn parse_desktop_file(
if not_display || hidden {
return Ok(None);
}
- let name = section.attr("Name").ok_or(EntryParseError::MissingName {
- file_path: path.to_owned(),
- })?;
- let name = unescape_string(name).map_err(|value| {
- EntryParseError::InvalidEscape {
- file_path: path.to_owned(),
- name: "Name".to_owned(),
- value,
- }
- })?;
- let exec = section.attr("Exec").ok_or(EntryParseError::MissingExec {
- file_path: path.to_owned(),
- })?;
- let exec = unescape_string(exec).map_err(|value| {
- EntryParseError::InvalidEscape {
+ let name = unescape_string(section.attr("Name").ok_or(
+ EntryParseError::MissingName {
file_path: path.to_owned(),
- name: "Exec".to_owned(),
- value,
- }
- })?;
- let exec = strip_entry_args(&exec);
+ },
+ )?);
let icon = section.attr("Icon").unwrap_or("");
+ let exec_str =
+ section.attr("Exec").ok_or(EntryParseError::MissingExec {
+ file_path: path.to_owned(),
+ })?;
+ let exec =
+ parse_exec(exec_str, &name, icon, path.to_string_lossy().as_ref());
let terminal = {
if let Some(value) = section.attr("Terminal") {
value.parse().map_err(|_| EntryParseError::InvalidPropVal {
@@ 205,20 260,82 @@ pub fn parse_desktop_file(
mod test {
use super::*;
- mod strip_entry_args {
+ fn ovec(v: &[&str]) -> Vec<String> {
+ v.iter().map(|s| (*s).to_owned()).collect()
+ }
+
+ mod parse_exec {
use super::*;
#[test]
- fn no_args() {
- let exec = "/usr/bin/cat --flag".to_owned();
- assert_eq!(strip_entry_args(&exec), exec);
+ fn basic() {
+ let exec = "/usr/bin/cat --flag";
+ let expected = ovec(&["/usr/bin/cat", "--flag"]);
+ assert_eq!(
+ parse_exec(
+ &unescape_string(exec),
+ "cat",
+ "cat",
+ "/cat.desktop"
+ ),
+ expected
+ );
}
#[test]
- fn has_args() {
- let exec = "/usr/bin/cat %f --flag";
- let exec_no_args = "/usr/bin/cat --flag".to_owned();
- assert_eq!(strip_entry_args(&exec), exec_no_args);
+ fn quoted() {
+ let exec = "\"/usr/bin/cat\" --flag";
+ let expected = ovec(&["/usr/bin/cat", "--flag"]);
+ assert_eq!(
+ parse_exec(
+ &unescape_string(exec),
+ "cat",
+ "cat",
+ "/cat.desktop"
+ ),
+ expected
+ );
+ }
+
+ #[test]
+ fn args() {
+ let exec = "\"/usr/bin/cat\" --flag %k %i %c %f %%";
+ let expected = ovec(&[
+ "/usr/bin/cat",
+ "--flag",
+ "/cat.desktop",
+ "--icon",
+ "cat",
+ "cat",
+ "%",
+ ]);
+ assert_eq!(
+ parse_exec(
+ &unescape_string(exec),
+ "cat",
+ "cat",
+ "/cat.desktop"
+ ),
+ expected
+ );
+ }
+
+ #[test]
+ fn complex() {
+ let exec = r#""/usr/bin folder/cat" --flag "a very weird \\\\ \" string \\$ <>`" "#;
+
+ let first_pass = r#""/usr/bin folder/cat" --flag "a very weird \\ \" string \$ <>`" "#;
+ assert_eq!(unescape_string(exec), first_pass);
+ let exec = unescape_string(exec);
+ let expected = ovec(&[
+ "/usr/bin folder/cat",
+ "--flag",
+ r#"a very weird \ " string $ <>`"#,
+ ]);
+ assert_eq!(
+ parse_exec(&exec, "cat", "cat", "/cat icon.png"),
+ expected
+ );
}
}
@@ 245,7 362,35 @@ mod test {
let other_app = App::new(
"Test".to_owned(),
"testicon".to_owned(),
- "/usr/bin/test --with-flag".to_owned(),
+ vec!["/usr/bin/test".to_owned(), "--with-flag".to_owned()],
+ false,
+ );
+ // Note, apps will have different uuids but Eq doesn't consider them
+ assert_eq!(app, other_app);
+ remove_file(&path).unwrap();
+ }
+
+ #[test]
+ fn file_with_args() {
+ use crate::App;
+ use std::fs::{remove_file, File};
+ use std::io::prelude::*;
+ use std::path::Path;
+
+ let path = Path::new("./test2.desktop");
+ let mut file = File::create(&path).unwrap();
+ file.write_all(
+ b"[Desktop Entry]
+ Name=Test
+ Icon=testicon
+ Exec=/usr/bin/test --with-flag %c %i %k %f",
+ )
+ .unwrap();
+ let app = parse_desktop_file(&path).unwrap().unwrap();
+ let other_app = App::new(
+ "Test".to_owned(),
+ "testicon".to_owned(),
+ ovec(&["/usr/bin/test", "--with-flag", "Test", "--icon", "testicon", "./test2.desktop"]),
false,
);
// Note, apps will have different uuids but Eq doesn't consider them