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
97enum 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 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}