~zethra/poki-launcher

e68e0ad281d8d7701a603c7336c3afe8f919faff — Sashanoraa 6 months ago e015127
lib: Correctly parse Exec value in desktop files

Signed-off-by: Sashanoraa <sasha@sashanoraa.gay>
2 files changed, 190 insertions(+), 45 deletions(-)

M lib-poki-launcher/src/db.rs
M lib-poki-launcher/src/desktop_entry.rs
M lib-poki-launcher/src/db.rs => lib-poki-launcher/src/db.rs +6 -6
@@ 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());

M lib-poki-launcher/src/desktop_entry.rs => lib-poki-launcher/src/desktop_entry.rs +184 -39
@@ 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