freedesktop_desktop_entry/
exec.rs1use 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 #[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
140enum 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 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}