dependencies: embed xdg-utils crate

No reason to have it out of the tree.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/299/head
Manos Pitsidianakis 2023-09-02 22:38:21 +03:00
parent 0a9c89b6b3
commit a615b4701b
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
8 changed files with 398 additions and 13 deletions

7
Cargo.lock generated
View File

@ -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"

View File

@ -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;

View File

@ -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::*;

View File

@ -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"] }

View File

@ -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;

View File

@ -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)]

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
//! 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 <a class="fn" href="fn.query_default_app.html"
//! title="xdg_utils::query_default_app fn">query_default_app</a> performs
//! like the xdg-utils function `binary_to_desktop_file`
//! * Function <a class="fn" href="fn.query_mime_info.html"
//! title="xdg_utils::query_mime_info fn">query_mime_info</a> 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
//! <https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html>
//!
//! # Reference implementation
//! <https://cgit.freedesktop.org/xdg/xdg-utils/tree/scripts/xdg-utils-common.in>
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<Self> {
let mut file: File = File::open(filename)?;
let mut contents: Vec<u8> = 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<Item = (&str, &str)> {
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<T: AsRef<str>>(query: T) -> Result<String> {
// Values are directory paths separated by : in case it's more than one.
let mut xdg_vars: HashMap<String, String> = 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<Vec<String>> = 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<T: AsRef<str>>(
filename: &Path,
xdg_vars: &HashMap<String, String>,
query: T,
) -> Result<Option<String>> {
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<String, String>,
) -> Result<Option<String>> {
for dir in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) {
let mut file_path: Option<PathBuf> = 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
/// <https://cgit.freedesktop.org/xdg/xdg-utils/tree/scripts/xdg-mime.in>
///
/// # 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<T: AsRef<Path>>(query: T) -> Result<Vec<u8>> {
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<u8>) -> Vec<u8> {
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");
}
}
}