use std::{
env,
ffi::{OsStr, OsString},
io,
os::unix::ffi::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
};
use freedesktop_entry_parser::{Entry as FreeDesktopEntry, parse_entry};
use once_cell_regex::{byte_regex, exports::regex::bytes::Regex};
pub fn query_mime_entry(mime_type: &str) -> Option<PathBuf> {
duct::cmd("xdg-mime", ["query", "default", mime_type])
.read()
.map(|out_str| {
log::debug!("query_mime_entry got output {:?}", out_str);
if !out_str.is_empty() {
Some(PathBuf::from(out_str.trim()))
} else {
None
}
})
.ok()?
}
pub fn find_entry_in_dir(dir_path: &Path, target: &Path) -> std::io::Result<Option<PathBuf>> {
for entry in dir_path.read_dir()?.flatten() {
if entry.path().is_file() && entry.file_name() == target {
return Ok(Some(entry.path()));
} else if entry.path().is_dir() {
if let Some(result) = find_entry_in_dir(&entry.path(), target)? {
return Ok(Some(result));
}
}
}
Ok(None)
}
pub fn parse(entry: impl AsRef<Path>) -> io::Result<FreeDesktopEntry> {
parse_entry(entry.as_ref())
}
pub fn find_entry_by_app_name(
dir_path: &Path,
app_name: &OsStr,
) -> Option<(FreeDesktopEntry, PathBuf)> {
for entry in dir_path.read_dir().ok()?.filter_map(Result::ok) {
let entry_path = entry.path();
if entry_path.is_file() {
if let Ok(parsed) = parse_entry(&entry_path) {
if parsed
.section("Desktop Entry")
.attr("Name")
.map(str::as_ref)
== Some(app_name)
{
return Some((parsed, entry_path));
}
}
} else if entry.path().is_dir() {
if let Some(result) = find_entry_by_app_name(&entry_path, app_name) {
return Some(result);
}
}
}
None
}
fn replace_on_pattern(
text: impl AsRef<OsStr>,
replace_by: impl AsRef<OsStr>,
regex: &Regex,
) -> OsString {
let text = text.as_ref();
let replace_by = replace_by.as_ref();
let mut result_text = Vec::new();
let mut last_index_read = 0;
for mat in regex.find_iter(text.as_bytes()) {
let start = mat.start();
let end = mat.end();
result_text.extend_from_slice(&text.as_bytes()[last_index_read..start]);
result_text.extend_from_slice(replace_by.as_bytes());
last_index_read = end;
}
result_text.extend_from_slice(&text.as_bytes()[last_index_read..]);
OsString::from_vec(result_text)
}
fn parse_quoted_text(
text: &OsStr,
argument: &OsStr,
icon: Option<&OsStr>,
desktop_entry_path: Option<&Path>,
) -> OsString {
let mut result = Vec::new();
let mut escaping = false;
for &c in text.as_bytes() {
if escaping {
result.push(c);
escaping = false;
} else {
if c == b'\\' {
escaping = true;
} else {
result.push(c);
}
}
}
let result = OsString::from_vec(result);
parse_unquoted_text(&result, argument, icon, desktop_entry_path)
}
fn parse_unquoted_text(
text: &OsStr,
argument: &OsStr,
icon: Option<&OsStr>,
desktop_entry_path: Option<&Path>,
) -> OsString {
let arg_re = byte_regex!(r"%u|%U|%f|%F");
let result = replace_on_pattern(text, argument, arg_re);
let icon_replace = icon.unwrap_or_else(|| "".as_ref());
let result = replace_on_pattern(result, icon_replace, byte_regex!("%i"));
let desktop_entry_replace = desktop_entry_path.unwrap_or_else(|| "".as_ref());
let result = replace_on_pattern(result, desktop_entry_replace, byte_regex!("%k"));
let result = replace_on_pattern(result, "", byte_regex!(r"%[^%]"));
replace_on_pattern(result, "%", byte_regex!("%%"))
}
pub fn parse_command(
command: &OsStr,
argument: &OsStr,
icon: Option<&OsStr>,
desktop_entry_path: Option<&Path>,
) -> Vec<OsString> {
log::debug!(
"Parsing XDG Exec command {:?}, with argument {:?}",
command,
argument
);
let mut escape_char = false;
let mut reading_quoted = false;
let mut reading_singlequoted = false;
let mut parsed_command_parts = Vec::new();
let mut text_atom = Vec::new();
for &c in command.as_bytes() {
if escape_char {
text_atom.push(c);
escape_char = false;
} else if c == b'\\' {
escape_char = true;
} else if reading_quoted {
if c != b'"' {
text_atom.push(c);
} else {
if !text_atom.is_empty() {
let text_atom_string = parse_quoted_text(
OsStr::from_bytes(&text_atom),
argument,
icon,
desktop_entry_path,
);
parsed_command_parts.push(text_atom_string);
text_atom.clear();
}
reading_quoted = false;
}
} else if reading_singlequoted {
if c != b'\'' {
text_atom.push(c);
} else {
if !text_atom.is_empty() {
let text_atom_string = parse_quoted_text(
OsStr::from_bytes(&text_atom),
argument,
icon,
desktop_entry_path,
);
parsed_command_parts.push(text_atom_string);
text_atom.clear();
}
reading_singlequoted = false;
}
} else if [b' ', b'\t', b'\n'].contains(&c) {
if !text_atom.is_empty() {
let text_atom_string = parse_unquoted_text(
OsStr::from_bytes(&text_atom),
argument,
icon,
desktop_entry_path,
);
parsed_command_parts.push(text_atom_string);
text_atom.clear();
}
} else {
match c {
b'"' => reading_quoted = true,
b'\'' => reading_singlequoted = true,
anything_else => text_atom.push(anything_else),
}
}
}
if !text_atom.is_empty() {
let text_atom_string = parse_unquoted_text(
OsStr::from_bytes(&text_atom),
argument,
icon,
desktop_entry_path,
);
parsed_command_parts.push(text_atom_string);
text_atom.clear();
}
log::debug!(
"XDG parsed command {:?} to {:?}",
command,
parsed_command_parts
);
parsed_command_parts
}
pub fn get_xdg_data_dirs() -> Vec<PathBuf> {
let mut result = Vec::new();
if let Ok(home) = crate::util::home_dir() {
let xdg_data_home = env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home.join(".local/share")); result.push(xdg_data_home);
}
if let Ok(var) = env::var("XDG_DATA_DIRS") {
let entries = var.split(':').map(PathBuf::from);
result.extend(entries);
} else {
result.push(PathBuf::from("/usr/local/share"));
result.push(PathBuf::from("/usr/share"));
};
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_command_simple() {
assert_eq!(
parse_command(
r#"simple.sh %u"#.as_ref(),
"~/myfolder/src".as_ref(),
None,
None,
),
["simple.sh", "~/myfolder/src"]
);
}
#[test]
fn parse_command_simple_quote_test() {
assert_eq!(
parse_command(
r#"simple.sh "%u" "single 'quotes' inside" 'double "quotes" inside' \"not quoted\""#.as_ref(),
"~/my folder/src".as_ref(),
None,
None,
),
["simple.sh", "~/my folder/src", "single 'quotes' inside", r#"double "quotes" inside"#, "\"not", "quoted\""]
);
}
#[test]
fn parse_command_escape_test() {
assert_eq!(
parse_command(
r#"cargo run -- these are separated these\ are\ together "This is a dollar sign: \\$" %u \\ \$ \`"#.as_ref(),
"filename.txt".as_ref(),
None,
None,
),
["cargo", "run", "--", "these", "are", "separated", "these are together", "This is a dollar sign: $", "filename.txt", r"\", "$", "`"]
);
}
#[test]
fn parse_command_complex_test() {
assert_eq!(
parse_command(
r#"test_command --flag %u --another "thing \\\\" %i %% %k My\ Work\ Place"#
.as_ref(),
"/my/file/folder/file.rs".as_ref(),
Some("/foo/bar/something/myicon.xpg".as_ref()),
Some("/foo/bar/applications/test.desktop".as_ref()),
),
[
"test_command",
"--flag",
"/my/file/folder/file.rs",
"--another",
r"thing \",
"/foo/bar/something/myicon.xpg",
"%",
"/foo/bar/applications/test.desktop",
"My Work Place"
]
);
}
}