freedesktop_desktop_entry/
exec.rs

1// Copyright 2021 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use crate::DesktopEntry;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum ExecError {
9    #[error("{0}")]
10    WrongFormat(String),
11
12    #[error("Exec field is empty")]
13    ExecFieldIsEmpty,
14
15    #[error("Exec key was not found")]
16    ExecFieldNotFound,
17}
18
19impl DesktopEntry {
20    #[inline]
21    pub fn parse_exec(&self) -> Result<Vec<String>, ExecError> {
22        self.get_args(self.exec(), &[], &[] as &[&str])
23    }
24
25    /// Macros like `%f` (cf [.desktop spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables)) will be subtitued using the `uris` parameter.
26    #[inline]
27    pub fn parse_exec_with_uris<L>(
28        &self,
29        uris: &[&str],
30        locales: &[L],
31    ) -> Result<Vec<String>, ExecError>
32    where
33        L: AsRef<str>,
34    {
35        self.get_args(self.exec(), uris, locales)
36    }
37
38    #[inline]
39    pub fn parse_exec_action(&self, action_name: &str) -> Result<Vec<String>, ExecError> {
40        self.get_args(self.action_exec(action_name), &[], &[] as &[&str])
41    }
42
43    #[inline]
44    pub fn parse_exec_action_with_uris<L>(
45        &self,
46        action_name: &str,
47        uris: &[&str],
48        locales: &[L],
49    ) -> Result<Vec<String>, ExecError>
50    where
51        L: AsRef<str>,
52    {
53        self.get_args(self.action_exec(action_name), uris, locales)
54    }
55
56    fn get_args<L>(
57        &self,
58        exec: Option<&str>,
59        uris: &[&str],
60        locales: &[L],
61    ) -> Result<Vec<String>, ExecError>
62    where
63        L: AsRef<str>,
64    {
65        #[inline(never)]
66        fn inner<'a>(
67            this: &'a DesktopEntry,
68            exec: Option<&str>,
69            uris: &[&str],
70            locales: &mut dyn Iterator<Item = &str>,
71        ) -> Result<Vec<String>, ExecError> {
72            let Some(exec) = exec else {
73                return Err(ExecError::ExecFieldNotFound);
74            };
75
76            let exec = if let Some(without_prefix) = exec.strip_prefix('\"') {
77                without_prefix
78                    .strip_suffix('\"')
79                    .ok_or(ExecError::WrongFormat("unmatched quote".into()))?
80            } else {
81                exec
82            };
83
84            let mut args: Vec<String> = Vec::new();
85
86            for arg in exec.split_ascii_whitespace() {
87                match ArgOrFieldCode::try_from(arg) {
88                    Ok(arg) => match arg {
89                        ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => {
90                            if let Some(arg) = uris.first() {
91                                args.push(arg.to_string());
92                            }
93                        }
94                        ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => {
95                            uris.iter().for_each(|uri| args.push(uri.to_string()));
96                        }
97                        ArgOrFieldCode::IconKey => {
98                            if let Some(icon) = this.icon() {
99                                args.push(icon.to_string());
100                            }
101                        }
102                        ArgOrFieldCode::TranslatedName => {
103                            if let Some(name) = DesktopEntry::localized_entry(
104                                this.ubuntu_gettext_domain.as_deref(),
105                                this.groups.desktop_entry(),
106                                "Name",
107                                locales,
108                            ) {
109                                args.push(name.to_string());
110                            }
111                        }
112                        ArgOrFieldCode::DesktopFileLocation => {
113                            args.push(this.path.to_string_lossy().to_string());
114                        }
115                        ArgOrFieldCode::Arg(arg) => {
116                            args.push(arg.to_string());
117                        }
118                    },
119                    Err(e) => {
120                        log::error!("{}", e);
121                    }
122                }
123            }
124
125            if args.is_empty() {
126                return Err(ExecError::ExecFieldIsEmpty);
127            }
128
129            if args.first().unwrap().contains('=') {
130                return Err(ExecError::WrongFormat("equal sign detected".into()));
131            }
132
133            Ok(args)
134        }
135
136        inner(self, exec, uris, &mut locales.iter().map(AsRef::as_ref))
137    }
138}
139
140// either a command line argument or a field-code as described
141// in https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
142enum ArgOrFieldCode<'a> {
143    SingleFileName,
144    FileList,
145    SingleUrl,
146    UrlList,
147    IconKey,
148    TranslatedName,
149    DesktopFileLocation,
150    Arg(&'a str),
151}
152
153#[derive(Debug, Error)]
154enum ExecErrorInternal<'a> {
155    #[error("Unknown field code: '{0}'")]
156    UnknownFieldCode(&'a str),
157
158    #[error("Deprecated field code: '{0}'")]
159    DeprecatedFieldCode(&'a str),
160}
161
162impl<'a> TryFrom<&'a str> for ArgOrFieldCode<'a> {
163    type Error = ExecErrorInternal<'a>;
164
165    // todo: handle escaping
166    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
167        match value {
168            "%f" => Ok(ArgOrFieldCode::SingleFileName),
169            "%F" => Ok(ArgOrFieldCode::FileList),
170            "%u" => Ok(ArgOrFieldCode::SingleUrl),
171            "%U" => Ok(ArgOrFieldCode::UrlList),
172            "%i" => Ok(ArgOrFieldCode::IconKey),
173            "%c" => Ok(ArgOrFieldCode::TranslatedName),
174            "%k" => Ok(ArgOrFieldCode::DesktopFileLocation),
175            "%d" | "%D" | "%n" | "%N" | "%v" | "%m" => {
176                Err(ExecErrorInternal::DeprecatedFieldCode(value))
177            }
178            other if other.starts_with('%') => Err(ExecErrorInternal::UnknownFieldCode(other)),
179            other => Ok(ArgOrFieldCode::Arg(other)),
180        }
181    }
182}
183
184#[cfg(test)]
185mod test {
186
187    use std::path::PathBuf;
188
189    use crate::{DesktopEntry, get_languages_from_env};
190
191    use super::ExecError;
192
193    #[test]
194    fn should_return_unmatched_quote_error() {
195        let path = PathBuf::from("tests_entries/exec/unmatched-quotes.desktop");
196        let locales = get_languages_from_env();
197        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
198        let result = de.parse_exec_with_uris(&[], &locales);
199
200        assert!(matches!(result.unwrap_err(), ExecError::WrongFormat(..)));
201    }
202
203    #[test]
204    fn should_fail_if_exec_string_is_empty() {
205        let path = PathBuf::from("tests_entries/exec/empty-exec.desktop");
206        let locales = get_languages_from_env();
207        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
208        let result = de.parse_exec_with_uris(&[], &locales);
209
210        assert!(matches!(result.unwrap_err(), ExecError::ExecFieldIsEmpty));
211    }
212
213    #[test]
214    fn should_exec_simple_command() {
215        let path = PathBuf::from("tests_entries/exec/alacritty-simple.desktop");
216        let locales = get_languages_from_env();
217        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
218        let result = de.parse_exec_with_uris(&[], &locales);
219
220        assert!(result.is_ok());
221    }
222
223    #[test]
224    fn should_exec_complex_command() {
225        let path = PathBuf::from("tests_entries/exec/non-terminal-cmd.desktop");
226        let locales = get_languages_from_env();
227        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
228        let result = de.parse_exec_with_uris(&[], &locales);
229
230        assert!(result.is_ok());
231    }
232
233    #[test]
234    fn should_exec_terminal_command() {
235        let path = PathBuf::from("tests_entries/exec/non-terminal-cmd.desktop");
236        let locales = get_languages_from_env();
237        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
238        let result = de.parse_exec_with_uris(&[], &locales);
239
240        assert!(result.is_ok());
241    }
242
243    #[test]
244    #[ignore = "Needs a desktop environment with nvim installed, run locally only"]
245    fn should_parse_exec_with_field_codes() {
246        let path = PathBuf::from("/usr/share/applications/nvim.desktop");
247        let locales = get_languages_from_env();
248        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
249        let result = de.parse_exec_with_uris(&["src/lib.rs"], &locales);
250
251        assert!(result.is_ok());
252    }
253
254    #[test]
255    #[ignore = "Needs a desktop environment with gnome Books installed, run locally only"]
256    fn should_parse_exec_with_dbus() {
257        let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop");
258        let locales = get_languages_from_env();
259        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
260        let result = de.parse_exec_with_uris(&["src/lib.rs"], &locales);
261
262        assert!(result.is_ok());
263    }
264
265    #[test]
266    #[ignore = "Needs a desktop environment with Nautilus installed, run locally only"]
267    fn should_parse_exec_with_dbus_and_field_codes() {
268        let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop");
269        let locales = get_languages_from_env();
270        let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
271        let _result = de.parse_exec_with_uris(&[], &locales);
272        let path = std::env::current_dir().unwrap();
273        let path = path.to_string_lossy();
274        let path = format!("file:///{path}");
275        let result = de.parse_exec_with_uris(&[path.as_str()], &locales);
276
277        assert!(result.is_ok());
278    }
279}