Skip to content

Commit 52d48f7

Browse files
authored
feat: parse the exec field
1 parent ee9a759 commit 52d48f7

14 files changed

+305
-6
lines changed

.vscode/settings.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ strsim = "0.11.1"
1919
thiserror = "1"
2020
xdg = "2.4.0"
2121
log = "0.4.21"
22+

examples/specific_file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
use freedesktop_desktop_entry::DesktopEntry;
44

55
fn main() {
6-
let path = Path::new("tests/org.mozilla.firefox.desktop");
6+
let path = Path::new("tests_entries/org.mozilla.firefox.desktop");
77
let locales = &["fr_FR", "en", "it"];
88

99
// if let Ok(bytes) = fs::read_to_string(path) {

src/exec.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// Copyright 2021 System76 <[email protected]>
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
use crate::DesktopEntry;
5+
use thiserror::Error;
6+
7+
#[derive(Debug, Error)]
8+
pub 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+
19+
impl<'a> DesktopEntry<'a> {
20+
pub fn parse_exec(&self) -> Result<Vec<String>, ExecError> {
21+
self.get_args(self.exec(), &[], &[] as &[&str])
22+
}
23+
24+
/// 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.
25+
pub fn parse_exec_with_uris<L>(
26+
&self,
27+
uris: &[&'a 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: &[&'a 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+
&'a self,
54+
exec: Option<&'a str>,
55+
uris: &[&'a 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+
if exec.contains('=') {
66+
return Err(ExecError::WrongFormat("equal sign detected".into()));
67+
}
68+
69+
let exec = if let Some(without_prefix) = exec.strip_prefix('\"') {
70+
without_prefix
71+
.strip_suffix('\"')
72+
.ok_or(ExecError::WrongFormat("unmatched quote".into()))?
73+
} else {
74+
exec
75+
};
76+
77+
let mut args: Vec<String> = Vec::new();
78+
79+
for arg in exec.split_ascii_whitespace() {
80+
match ArgOrFieldCode::try_from(arg) {
81+
Ok(arg) => match arg {
82+
ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => {
83+
if let Some(arg) = uris.first() {
84+
args.push(arg.to_string());
85+
}
86+
}
87+
ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => {
88+
uris.iter().for_each(|uri| args.push(uri.to_string()));
89+
}
90+
ArgOrFieldCode::IconKey => {
91+
if let Some(icon) = self.icon() {
92+
args.push(icon.to_string());
93+
}
94+
}
95+
ArgOrFieldCode::TranslatedName => {
96+
if let Some(name) = self.name(locales) {
97+
args.push(name.to_string());
98+
}
99+
}
100+
ArgOrFieldCode::DesktopFileLocation => {
101+
args.push(self.path.to_string_lossy().to_string());
102+
}
103+
ArgOrFieldCode::Arg(arg) => {
104+
args.push(arg.to_string());
105+
}
106+
},
107+
Err(e) => {
108+
log::error!("{}", e);
109+
}
110+
}
111+
}
112+
113+
if args.is_empty() {
114+
return Err(ExecError::ExecFieldIsEmpty);
115+
}
116+
117+
Ok(args)
118+
}
119+
}
120+
121+
// either a command line argument or a field-code as described
122+
// in https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
123+
enum 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)]
135+
enum 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+
143+
impl<'a> TryFrom<&'a str> for ArgOrFieldCode<'a> {
144+
type Error = ExecErrorInternal<'a>;
145+
146+
// todo: handle escaping
147+
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)]
166+
mod 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+
}

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
mod decoder;
55
mod iter;
66

7+
mod exec;
8+
pub use exec::ExecError;
9+
710
pub mod matching;
811
pub use decoder::DecodeError;
912

@@ -477,7 +480,7 @@ fn env_with_locale() {
477480
let locales = &["fr_FR"];
478481

479482
let de = DesktopEntry::from_path(
480-
PathBuf::from("tests/org.mozilla.firefox.desktop"),
483+
PathBuf::from("tests_entries/org.mozilla.firefox.desktop"),
481484
Some(locales),
482485
)
483486
.unwrap();
File renamed without changes.
File renamed without changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[Desktop Entry]
2+
Type=Application
3+
TryExec=alacritty
4+
Exec=alacritty
5+
Icon=Alacritty
6+
Terminal=false
7+
Categories=System;TerminalEmulator;
8+
9+
Name=Alacritty
10+
GenericName=Terminal
11+
Comment=A fast, cross-platform, OpenGL terminal emulator
12+
StartupWMClass=Alacritty
13+
Actions=New;
14+
15+
X-Desktop-File-Install-Version=0.26
16+
17+
[Desktop Action New]
18+
Name=New Terminal
19+
Exec=alacritty
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[Desktop Entry]
2+
Exec=
3+
Terminal=false
4+
Type=Application
5+
Name=NoExecKey
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[Desktop Entry]
2+
Exec=alacritty -e glxgears -info
3+
Terminal=false
4+
Type=Application
5+
Name=GlxGearNoTerminal

0 commit comments

Comments
 (0)