1#[cfg(test)]
48mod tests {
49 use super::*;
50 #[test]
51 fn it_works() {
52 println!("{:?}", query_default_app("image/jpeg"));
54 println!("{:?}", query_default_app("text/html"));
55 println!("{:?}", query_default_app("video/mp4"));
56 println!("{:?}", query_default_app("application/pdf"));
57 }
58
59 #[test]
60 fn ini() {
61 let ini = Ini(String::from("[foo]\n# comment\nbar=baz\n\n[bar]\nbar=foo"));
62 for (key, value) in ini.iter_section("foo") {
63 assert_eq!(key, "bar");
64 assert_eq!(value, "baz");
65 }
66 }
67}
68
69use std::collections::HashMap;
70use std::env;
71use std::fs;
72use std::fs::File;
73use std::io::{Error, ErrorKind, Read, Result};
74use std::path::{Path, PathBuf};
75use std::process::{Command, Stdio};
76use std::str;
77
78macro_rules! split_and_chain {
79 ($xdg_vars:ident[$key:literal]) => {
80 $xdg_vars.get($key).map(String::as_str).unwrap_or("").split(':')
81 };
82 ($xdg_vars:ident[$key:literal], $($tail_xdg_vars:ident[$tail_key:literal]),+$(,)*) => {
83
84 split_and_chain!($xdg_vars[$key]).chain(split_and_chain!($($tail_xdg_vars[$tail_key]),+))
85 }
86}
87
88struct Ini(String);
89
90impl Ini {
91 fn from_filename(filename: &Path) -> Result<Ini> {
92 let mut file: File = File::open(filename)?;
93
94 let mut contents: Vec<u8> = vec![];
95 file.read_to_end(&mut contents)?;
96
97 let contents_str =
98 String::from_utf8(contents).map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
99 Ok(Ini(contents_str))
100 }
101
102 fn iter_section(&self, section: &str) -> impl Iterator<Item = (&str, &str)> {
103 let section = format!("[{}]", section);
104 let mut lines = self.0.lines();
105
106 loop {
108 let line = lines.next();
109 if let Some(line) = line {
110 if line == section {
111 break;
112 }
113 } else {
114 break;
115 }
116 }
117
118 lines
120 .filter(|line| !line.starts_with('#'))
121 .take_while(|line| !line.starts_with('['))
122 .filter_map(|line| {
123 let split: Vec<_> = line.splitn(2, '=').collect();
124 if split.len() != 2 {
125 None
126 } else {
127 Some((split[0], split[1]))
128 }
129 })
130 }
131}
132
133pub fn query_default_app<T: AsRef<str>>(query: T) -> Result<String> {
143 let mut xdg_vars: HashMap<String, String> = HashMap::new();
145 let env_vars: env::Vars = env::vars();
146
147 for (k, v) in env_vars {
148 if k.starts_with("XDG_CONFIG")
149 || k.starts_with("XDG_DATA")
150 || k.starts_with("XDG_CURRENT_DESKTOP")
151 || k == "HOME"
152 {
153 xdg_vars.insert(k.to_string(), v.to_string());
154 }
155 }
156
157 if xdg_vars.contains_key("HOME") && !xdg_vars.contains_key("XDG_DATA_HOME") {
159 let h = xdg_vars["HOME"].clone();
160 xdg_vars.insert("XDG_DATA_HOME".to_string(), format!("{}/.local/share", h));
161 }
162
163 if xdg_vars.contains_key("HOME") && !xdg_vars.contains_key("XDG_CONFIG_HOME") {
164 let h = xdg_vars["HOME"].clone();
165 xdg_vars.insert("XDG_CONFIG_HOME".to_string(), format!("{}/.config", h));
166 }
167
168 if !xdg_vars.contains_key("XDG_DATA_DIRS") {
169 xdg_vars.insert(
170 "XDG_DATA_DIRS".to_string(),
171 "/usr/local/share:/usr/share".to_string(),
172 );
173 }
174
175 if !xdg_vars.contains_key("XDG_CONFIG_DIRS") {
176 xdg_vars.insert("XDG_CONFIG_DIRS".to_string(), "/etc/xdg".to_string());
177 }
178
179 let desktops: Option<Vec<String>> = if xdg_vars.contains_key("XDG_CURRENT_DESKTOP") {
180 let list = xdg_vars["XDG_CURRENT_DESKTOP"]
181 .trim()
182 .split(':')
183 .map(str::to_ascii_lowercase)
184 .collect();
185 Some(list)
186 } else {
187 None
188 };
189
190 for p in split_and_chain!(
192 xdg_vars["XDG_CONFIG_HOME"],
193 xdg_vars["XDG_CONFIG_DIRS"],
194 xdg_vars["XDG_DATA_HOME"],
195 xdg_vars["XDG_DATA_DIRS"],
196 ) {
197 if let Some(ref d) = desktops {
198 for desktop in d {
199 let pb: PathBuf = PathBuf::from(format!(
200 "{var_value}/{desktop_val}-mimeapps.list",
201 var_value = p,
202 desktop_val = desktop
203 ));
204 if pb.exists() {
205 if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
206 return Ok(ret);
207 }
208 }
209 }
210 }
211 let pb: PathBuf = PathBuf::from(format!("{var_value}/mimeapps.list", var_value = p));
212 if pb.exists() {
213 if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
214 return Ok(ret);
215 }
216 }
217 }
218
219 for p in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) {
221 if let Some(ref d) = desktops {
222 for desktop in d {
223 let pb: PathBuf = PathBuf::from(format!(
224 "{var_value}/applications/{desktop_val}-mimeapps.list",
225 var_value = p,
226 desktop_val = desktop
227 ));
228 if pb.exists() {
229 if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
230 return Ok(ret);
231 }
232 }
233 }
234 }
235 let pb: PathBuf = PathBuf::from(format!(
236 "{var_value}/applications/mimeapps.list",
237 var_value = p
238 ));
239 if pb.exists() {
240 if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
241 return Ok(ret);
242 }
243 }
244 }
245
246 Err(Error::new(
247 ErrorKind::NotFound,
248 format!("No results for mime query: {}", query.as_ref()),
249 ))
250}
251
252fn check_mimeapps_list<T: AsRef<str>>(
253 filename: &Path,
254 xdg_vars: &HashMap<String, String>,
255 query: T,
256) -> Result<Option<String>> {
257 let ini = Ini::from_filename(filename)?;
258 for (key, value) in ini
259 .iter_section("Added Associations")
260 .chain(ini.iter_section("Default Applications"))
261 {
262 if key != query.as_ref() {
263 continue;
264 }
265 for v in value.split(';') {
266 if v.trim().is_empty() {
267 continue;
268 }
269
270 if let Some(b) = desktop_file_to_command(v, xdg_vars)? {
271 return Ok(Some(b));
272 }
273 }
274 }
275
276 Ok(None)
277}
278
279fn desktop_file_to_command(
282 desktop_name: &str,
283 xdg_vars: &HashMap<String, String>,
284) -> Result<Option<String>> {
285 for dir in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) {
286 let mut file_path: Option<PathBuf> = None;
287 let mut p;
288 if desktop_name.contains('-') {
289 let v: Vec<&str> = desktop_name.split('-').collect();
290 let (vendor, app): (&str, &str) = (v[0], v[1]);
291
292 p = PathBuf::from(format!(
293 "{dir}/applications/{vendor}/{app}",
294 dir = dir,
295 vendor = vendor,
296 app = app
297 ));
298 if p.exists() {
299 file_path = Some(p);
300 }
301 }
302
303 if file_path.is_none() {
304 'indir: for indir in &[format!("{}/applications", dir)] {
305 p = PathBuf::from(format!(
306 "{indir}/{desktop}",
307 indir = indir,
308 desktop = desktop_name
309 ));
310 if p.exists() {
311 file_path = Some(p);
312 break 'indir;
313 }
314 p.pop(); if p.is_dir() {
316 for entry in fs::read_dir(&p)? {
317 let mut p = entry?.path().to_owned();
318 p.push(desktop_name);
319 if p.exists() {
320 file_path = Some(p);
321 break 'indir;
322 }
323 }
324 }
325 }
326 }
327 if let Some(file_path) = file_path {
328 let ini = Ini::from_filename(&file_path)?;
329 for (key, value) in ini.iter_section("Desktop Entry") {
330 if key != "Exec" {
331 continue;
332 }
333 return Ok(Some(String::from(value)));
334 }
335 }
336 }
337
338 Ok(None)
339}
340
341pub fn query_mime_info<T: AsRef<Path>>(query: T) -> Result<Vec<u8>> {
354 let command_obj = Command::new("mimetype")
355 .args(&["--brief", "--dereference"])
356 .arg(query.as_ref())
357 .stdin(Stdio::piped())
358 .stdout(Stdio::piped())
359 .spawn()
360 .or_else(|_| {
361 Command::new("file")
362 .args(&["--brief", "--dereference", "--mime-type"])
363 .arg(query.as_ref())
364 .stdin(Stdio::piped())
365 .stdout(Stdio::piped())
366 .spawn()
367 })?;
368
369 Ok(drop_right_whitespace(
370 command_obj.wait_with_output()?.stdout,
371 ))
372}
373
374#[inline(always)]
375fn drop_right_whitespace(mut vec: Vec<u8>) -> Vec<u8> {
376 while vec.last() == Some(&b'\n') {
377 vec.pop();
378 }
379 vec
380}