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