From a615b4701b7e852a9112b317e2e31997c6cbe82e Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 2 Sep 2023 22:38:21 +0300 Subject: [PATCH] dependencies: embed xdg-utils crate No reason to have it out of the tree. Signed-off-by: Manos Pitsidianakis --- Cargo.lock | 7 - meli/src/mail/view/envelope.rs | 2 +- meli/src/mail/view/html.rs | 2 +- melib/Cargo.toml | 1 - melib/src/email/compose.rs | 3 +- melib/src/lib.rs | 1 - melib/src/utils/mod.rs | 1 + melib/src/utils/xdg/mod.rs | 394 +++++++++++++++++++++++++++++++++ 8 files changed, 398 insertions(+), 13 deletions(-) create mode 100644 melib/src/utils/xdg/mod.rs diff --git a/Cargo.lock b/Cargo.lock index db0ce410..a8276c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1311,7 +1311,6 @@ dependencies = [ "unicode-segmentation", "uuid", "xdg", - "xdg-utils", ] [[package]] @@ -2652,9 +2651,3 @@ name = "xdg" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" - -[[package]] -name = "xdg-utils" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9fefe62d5969721e2cfc529e6a760901cc0da422b6d67e7bfd18e69490dba6" diff --git a/meli/src/mail/view/envelope.rs b/meli/src/mail/view/envelope.rs index 631bcde1..8f397cbd 100644 --- a/meli/src/mail/view/envelope.rs +++ b/meli/src/mail/view/envelope.rs @@ -22,7 +22,7 @@ use std::process::{Command, Stdio}; use linkify::LinkFinder; -use melib::xdg_utils::query_default_app; +use melib::utils::xdg::query_default_app; use super::*; use crate::ThreadEvent; diff --git a/meli/src/mail/view/html.rs b/meli/src/mail/view/html.rs index dda48070..3d8c4674 100644 --- a/meli/src/mail/view/html.rs +++ b/meli/src/mail/view/html.rs @@ -24,7 +24,7 @@ use std::{ process::{Command, Stdio}, }; -use melib::xdg_utils::query_default_app; +use melib::utils::xdg::query_default_app; use super::*; diff --git a/melib/Cargo.toml b/melib/Cargo.toml index 5fb23fb6..b9ba6624 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -54,7 +54,6 @@ socket2 = { version = "0.4", features = [] } unicode-segmentation = { version = "1.2.1", default-features = false, optional = true } uuid = { version = "^1", features = ["serde", "v4", "v5"] } xdg = "2.1.0" -xdg-utils = "^0.4.0" [dev-dependencies] mailin-embedded = { version = "0.7", features = ["rtls"] } diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index b7e93b7f..00f8ce22 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -29,7 +29,6 @@ use std::{ }; use data_encoding::BASE64_MIME; -use xdg_utils::query_mime_info; use super::*; use crate::{ @@ -37,7 +36,7 @@ use crate::{ attachment_types::{Charset, ContentTransferEncoding, ContentType, MultipartType}, attachments::AttachmentBuilder, }, - utils::{datetime, shellexpand::ShellExpandTrait}, + utils::{datetime, shellexpand::ShellExpandTrait, xdg::query_mime_info}, }; pub mod mime; diff --git a/melib/src/lib.rs b/melib/src/lib.rs index 510fd715..42f112bb 100644 --- a/melib/src/lib.rs +++ b/melib/src/lib.rs @@ -190,7 +190,6 @@ pub extern crate indexmap; pub extern crate smallvec; pub extern crate smol; pub extern crate uuid; -pub extern crate xdg_utils; #[derive(Debug, Copy, Clone)] #[repr(transparent)] diff --git a/melib/src/utils/mod.rs b/melib/src/utils/mod.rs index bb62e10f..970e7f6a 100644 --- a/melib/src/utils/mod.rs +++ b/melib/src/utils/mod.rs @@ -32,6 +32,7 @@ pub mod percent_encoding; pub mod shellexpand; #[cfg(feature = "sqlite3")] pub mod sqlite3; +pub mod xdg; pub mod html_escape { //! HTML Coded Character Set diff --git a/melib/src/utils/xdg/mod.rs b/melib/src/utils/xdg/mod.rs new file mode 100644 index 00000000..8f4b7ec5 --- /dev/null +++ b/melib/src/utils/xdg/mod.rs @@ -0,0 +1,394 @@ +/* xdg-utils library + * + * Copyright 2019-2020 Manos Pitsidianakis + * + * xdg-utils is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * xdg-utils is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with xdg-utils. If not, see . + */ + +//! Query system for default apps using XDG MIME databases. +//! +//! The xdg-utils library provides dependency-free (except for `std`) Rust +//! implementations of some common functions in the freedesktop project +//! `xdg-utils`. +//! +//! # What is implemented? +//! * Function query_default_app performs +//! like the xdg-utils function `binary_to_desktop_file` +//! * Function query_mime_info launches the +//! `mimetype` or else the `file` command. +//! +//! Some of the utils may be implemented by combining these functions with other +//! functions in the Rust standard library. +//! +//! | Name | Function | Implemented functionalities| +//! |-----------------|--------------------------------------------------------|----------------------------| +//! |`xdg-desktop-menu`| Install desktop menu items | no +//! |`xdg-desktop-icon`| Install icons to the desktop | no +//! |`xdg-icon-resource`| Install icon resources | no +//! |`xdg-mime` | Query information about file type handling and install descriptions for new file types| queries only +//! |`xdg-open` | Open a file or URL in the user's preferred application | all (combine crate functions with `std::process::Command`) +//! |`xdg-email` | Send mail using the user's preferred e-mail composer | no +//! |`xdg-screensaver` | Control the screensaver | no +//! +//! # Specification +//! +//! +//! # Reference implementation +//! + +use std::{ + collections::HashMap, + env, fs, + fs::File, + io::{Error, ErrorKind, Read, Result}, + path::{Path, PathBuf}, + process::{Command, Stdio}, + str, +}; + +macro_rules! split_and_chain { + ($xdg_vars:ident[$key:literal]) => { + $xdg_vars.get($key).map(String::as_str).unwrap_or("").split(':') + }; + ($xdg_vars:ident[$key:literal], $($tail_xdg_vars:ident[$tail_key:literal]),+$(,)*) => { + + split_and_chain!($xdg_vars[$key]).chain(split_and_chain!($($tail_xdg_vars[$tail_key]),+)) + } +} + +struct Ini(String); + +impl Ini { + fn from_filename(filename: &Path) -> Result { + let mut file: File = File::open(filename)?; + + let mut contents: Vec = vec![]; + file.read_to_end(&mut contents)?; + + let contents_str = + String::from_utf8(contents).map_err(|err| Error::new(ErrorKind::InvalidData, err))?; + Ok(Self(contents_str)) + } + + fn iter_section(&self, section: &str) -> impl Iterator { + let section = format!("[{}]", section); + let mut lines = self.0.lines(); + + // Eat lines until we find the beginning of our section. + loop { + let line = lines.next(); + if let Some(line) = line { + if line == section { + break; + } + } else { + break; + } + } + + // Then take all foo=bar lines until the next section. + lines + .filter(|line| !line.starts_with('#')) + .take_while(|line| !line.starts_with('[')) + .filter_map(|line| { + let split: Vec<_> = line.splitn(2, '=').collect(); + if split.len() != 2 { + None + } else { + Some((split[0], split[1])) + } + }) + } +} + +/// Returns the command string of the desktop file that is the default +/// application of given MIME type `query` +/// +/// # Example +/// ```no_run +/// use xdg_utils::query_default_app; +/// +/// // The crate author recommends firefox. +/// assert_eq!( +/// Ok("firefox".into()), +/// query_default_app("text/html").map_err(|_| ()) +/// ); +/// ``` +pub fn query_default_app>(query: T) -> Result { + // Values are directory paths separated by : in case it's more than one. + let mut xdg_vars: HashMap = HashMap::new(); + let env_vars: env::Vars = env::vars(); + + for (k, v) in env_vars { + if k.starts_with("XDG_CONFIG") + || k.starts_with("XDG_DATA") + || k.starts_with("XDG_CURRENT_DESKTOP") + || k == "HOME" + { + xdg_vars.insert(k.to_string(), v.to_string()); + } + } + + // Insert defaults if variables are missing + if xdg_vars.contains_key("HOME") && !xdg_vars.contains_key("XDG_DATA_HOME") { + let h = xdg_vars["HOME"].clone(); + xdg_vars.insert("XDG_DATA_HOME".to_string(), format!("{}/.local/share", h)); + } + + if xdg_vars.contains_key("HOME") && !xdg_vars.contains_key("XDG_CONFIG_HOME") { + let h = xdg_vars["HOME"].clone(); + xdg_vars.insert("XDG_CONFIG_HOME".to_string(), format!("{}/.config", h)); + } + + if !xdg_vars.contains_key("XDG_DATA_DIRS") { + xdg_vars.insert( + "XDG_DATA_DIRS".to_string(), + "/usr/local/share:/usr/share".to_string(), + ); + } + + if !xdg_vars.contains_key("XDG_CONFIG_DIRS") { + xdg_vars.insert("XDG_CONFIG_DIRS".to_string(), "/etc/xdg".to_string()); + } + + let desktops: Option> = if xdg_vars.contains_key("XDG_CURRENT_DESKTOP") { + let list = xdg_vars["XDG_CURRENT_DESKTOP"] + .trim() + .split(':') + .map(str::to_ascii_lowercase) + .collect(); + Some(list) + } else { + None + }; + + // Search for mime entry in files. + for p in split_and_chain!( + xdg_vars["XDG_CONFIG_HOME"], + xdg_vars["XDG_CONFIG_DIRS"], + xdg_vars["XDG_DATA_HOME"], + xdg_vars["XDG_DATA_DIRS"], + ) { + if let Some(ref d) = desktops { + for desktop in d { + let pb: PathBuf = PathBuf::from(format!( + "{var_value}/{desktop_val}-mimeapps.list", + var_value = p, + desktop_val = desktop + )); + if pb.exists() { + if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? { + return Ok(ret); + } + } + } + } + let pb: PathBuf = PathBuf::from(format!("{var_value}/mimeapps.list", var_value = p)); + if pb.exists() { + if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? { + return Ok(ret); + } + } + } + + // Search again but for different paths. + for p in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) { + if let Some(ref d) = desktops { + for desktop in d { + let pb: PathBuf = PathBuf::from(format!( + "{var_value}/applications/{desktop_val}-mimeapps.list", + var_value = p, + desktop_val = desktop + )); + if pb.exists() { + if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? { + return Ok(ret); + } + } + } + } + let pb: PathBuf = PathBuf::from(format!( + "{var_value}/applications/mimeapps.list", + var_value = p + )); + if pb.exists() { + if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? { + return Ok(ret); + } + } + } + + Err(Error::new( + ErrorKind::NotFound, + format!("No results for mime query: {}", query.as_ref()), + )) +} + +fn check_mimeapps_list>( + filename: &Path, + xdg_vars: &HashMap, + query: T, +) -> Result> { + let ini = Ini::from_filename(filename)?; + for (key, value) in ini + .iter_section("Added Associations") + .chain(ini.iter_section("Default Applications")) + { + if key != query.as_ref() { + continue; + } + for v in value.split(';') { + if v.trim().is_empty() { + continue; + } + + if let Some(b) = desktop_file_to_command(v, xdg_vars)? { + return Ok(Some(b)); + } + } + } + + Ok(None) +} + +/// Find the desktop file in the filesystem, then find the binary it uses from +/// its "Exec=..." line entry. +pub fn desktop_file_to_command( + desktop_name: &str, + xdg_vars: &HashMap, +) -> Result> { + for dir in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) { + let mut file_path: Option = None; + let mut p; + if desktop_name.contains('-') { + let v: Vec<&str> = desktop_name.split('-').collect(); + let (vendor, app): (&str, &str) = (v[0], v[1]); + + p = PathBuf::from(format!( + "{dir}/applications/{vendor}/{app}", + dir = dir, + vendor = vendor, + app = app + )); + if p.exists() { + file_path = Some(p); + } + } + + if file_path.is_none() { + 'indir: for indir in &[format!("{}/applications", dir)] { + p = PathBuf::from(format!( + "{indir}/{desktop}", + indir = indir, + desktop = desktop_name + )); + if p.exists() { + file_path = Some(p); + break 'indir; + } + p.pop(); // Remove {desktop} from path. + if p.is_dir() { + for entry in fs::read_dir(&p)? { + let mut p = entry?.path().to_owned(); + p.push(desktop_name); + if p.exists() { + file_path = Some(p); + break 'indir; + } + } + } + } + } + if let Some(file_path) = file_path { + let ini = Ini::from_filename(&file_path)?; + for (key, value) in ini.iter_section("Desktop Entry") { + if key != "Exec" { + continue; + } + return Ok(Some(String::from(value))); + } + } + } + + Ok(None) +} + +/// Returns the MIME type of given file +/// +/// +/// # Example +/// ``` +/// use xdg_utils::query_mime_info; +/// let result = query_mime_info("/bin/sh") +/// .map_err(|_| ()) +/// .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()); +/// let result_str = result.as_ref().map(|s| s.as_str()); +/// assert!( +/// Ok("application/x-pie-executable") == result_str +/// || Ok("application/x-sharedlib") == result_str +/// ) +/// ``` +pub fn query_mime_info>(query: T) -> Result> { + let command_obj = Command::new("mimetype") + .args(["--brief", "--dereference"]) + .arg(query.as_ref()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .or_else(|_| { + Command::new("file") + .args(["--brief", "--dereference", "--mime-type"]) + .arg(query.as_ref()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + })?; + + Ok(drop_right_whitespace( + command_obj.wait_with_output()?.stdout, + )) +} + +#[inline(always)] +fn drop_right_whitespace(mut vec: Vec) -> Vec { + while vec.last() == Some(&b'\n') { + vec.pop(); + } + vec +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_various_mimes() { + /* Run with `cargo test -- --nocapture` to see output. */ + println!("{:?}", query_default_app("image/jpeg")); + println!("{:?}", query_default_app("text/html")); + println!("{:?}", query_default_app("video/mp4")); + println!("{:?}", query_default_app("application/pdf")); + } + + #[test] + fn test_ini_works() { + let ini = Ini(String::from("[foo]\n# comment\nbar=baz\n\n[bar]\nbar=foo")); + for (key, value) in ini.iter_section("foo") { + assert_eq!(key, "bar"); + assert_eq!(value, "baz"); + } + } +}