kairo_core/
exec.rs

1use freedesktop_desktop_entry as fde;
2
3use crate::Result;
4
5#[derive(thiserror::Error, Debug)]
6pub enum ExecParseError {
7    #[error("invalid format in {path}: {reason}")]
8    InvalidFormat {
9        reason: String,
10        path: Box<std::path::Path>,
11    },
12
13    #[error("invalid Exec arguments in {path}")]
14    InvalidExecArgs { path: Box<std::path::Path> },
15
16    #[error("Exec key was not found in {path}")]
17    ExecFieldNotFound { path: Box<std::path::Path> },
18}
19
20pub struct ExecParser<'a, L>
21where
22    L: AsRef<str>,
23{
24    de: &'a fde::DesktopEntry,
25    locales: &'a [L],
26}
27
28impl<'a, L> ExecParser<'a, L>
29where
30    L: AsRef<str>,
31{
32    pub fn new(de: &'a fde::DesktopEntry, locales: &'a [L]) -> ExecParser<'a, L>
33    where
34        L: AsRef<str>,
35    {
36        ExecParser { de, locales }
37    }
38
39    pub fn parse_with_uris(&self, uris: &[&str]) -> Result<(String, Vec<String>)> {
40        let exec = self.de.exec().ok_or(ExecParseError::ExecFieldNotFound {
41            path: self.de.path.clone().into(),
42        })?;
43
44        let exec = if let Some(without_prefix) = exec.strip_prefix('\"') {
45            without_prefix
46                .strip_suffix('\"')
47                .ok_or(ExecParseError::InvalidFormat {
48                    reason: "unmatched quote".into(),
49                    path: self.de.path.clone().into(),
50                })?
51        } else {
52            exec
53        };
54
55        let exec_args = shell_words::split(exec)?
56            .iter()
57            .flat_map(|arg| self.parse_arg(arg, uris))
58            .flatten()
59            .collect::<Vec<_>>();
60
61        match exec_args.as_slice() {
62            [cmd, args @ ..] => Ok((cmd.to_string(), args.to_vec())),
63            _ => Err(ExecParseError::InvalidExecArgs {
64                path: self.de.path.clone().into(),
65            })?,
66        }
67    }
68
69    fn parse_arg(&self, arg: &str, uris: &[&str]) -> Option<Vec<String>> {
70        match ArgOrFieldCode::try_from(arg) {
71            Ok(arg) => match arg {
72                ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => {
73                    uris.first().map(|uri| vec![uri.to_string()])
74                }
75                ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => {
76                    let args = uris.iter().map(|uri| uri.to_string()).collect();
77                    Some(args)
78                }
79                ArgOrFieldCode::IconKey => self.de.icon().map(|icon| vec![icon.to_string()]),
80                ArgOrFieldCode::TranslatedName => self
81                    .de
82                    .name(self.locales)
83                    .map(|name| vec![name.to_string()]),
84                ArgOrFieldCode::DesktopFileLocation => {
85                    Some(vec![self.de.path.to_string_lossy().to_string()])
86                }
87                ArgOrFieldCode::Arg(arg) => Some(vec![arg.to_string()]),
88            },
89            Err(e) => {
90                log::error!("{}", e);
91                None
92            }
93        }
94    }
95}
96
97// either a command line argument or a field-code as described
98// in https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
99enum ArgOrFieldCode<'a> {
100    SingleFileName,
101    FileList,
102    SingleUrl,
103    UrlList,
104    IconKey,
105    TranslatedName,
106    DesktopFileLocation,
107    Arg(&'a str),
108}
109
110#[derive(Debug, thiserror::Error)]
111enum ExecErrorInternal<'a> {
112    #[error("Unknown field code: '{0}'")]
113    UnknownFieldCode(&'a str),
114
115    #[error("Deprecated field code: '{0}'")]
116    DeprecatedFieldCode(&'a str),
117}
118
119impl<'a> TryFrom<&'a str> for ArgOrFieldCode<'a> {
120    type Error = ExecErrorInternal<'a>;
121
122    // todo: handle escaping
123    fn try_from(value: &'a str) -> std::result::Result<Self, Self::Error> {
124        match value {
125            "%f" => Ok(ArgOrFieldCode::SingleFileName),
126            "%F" => Ok(ArgOrFieldCode::FileList),
127            "%u" => Ok(ArgOrFieldCode::SingleUrl),
128            "%U" => Ok(ArgOrFieldCode::UrlList),
129            "%i" => Ok(ArgOrFieldCode::IconKey),
130            "%c" => Ok(ArgOrFieldCode::TranslatedName),
131            "%k" => Ok(ArgOrFieldCode::DesktopFileLocation),
132            "%d" | "%D" | "%n" | "%N" | "%v" | "%m" => {
133                Err(ExecErrorInternal::DeprecatedFieldCode(value))
134            }
135            other if other.starts_with('%') => Err(ExecErrorInternal::UnknownFieldCode(other)),
136            other => Ok(ArgOrFieldCode::Arg(other)),
137        }
138    }
139}