Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parse the exec field #27

Merged
merged 4 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .vscode/settings.json

This file was deleted.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ strsim = "0.11.1"
thiserror = "1"
xdg = "2.4.0"
log = "0.4.21"

2 changes: 1 addition & 1 deletion examples/specific_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use freedesktop_desktop_entry::DesktopEntry;

fn main() {
let path = Path::new("tests/org.mozilla.firefox.desktop");
let path = Path::new("tests_entries/org.mozilla.firefox.desktop");
let locales = &["fr_FR", "en", "it"];

// if let Ok(bytes) = fs::read_to_string(path) {
Expand Down
260 changes: 260 additions & 0 deletions src/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright 2021 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use crate::DesktopEntry;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ExecError {
#[error("{0}")]
WrongFormat(String),

#[error("Exec field is empty")]
ExecFieldIsEmpty,

#[error("Exec key was not found")]
ExecFieldNotFound,
}

impl<'a> DesktopEntry<'a> {
pub fn parse_exec(&self) -> Result<Vec<String>, ExecError> {
self.get_args(self.exec(), &[], &[] as &[&str])
}

/// Macros like `%f` (cf [.desktop spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables)) will be subtitued using the `uris` parameter.
pub fn parse_exec_with_uris<L>(
&self,
uris: &[&'a str],
locales: &[L],
) -> Result<Vec<String>, ExecError>
where
L: AsRef<str>,
{
self.get_args(self.exec(), uris, locales)
}

pub fn parse_exec_action(&self, action_name: &str) -> Result<Vec<String>, ExecError> {
self.get_args(self.action_exec(action_name), &[], &[] as &[&str])
}

pub fn parse_exec_action_with_uris<L>(
&self,
action_name: &str,
uris: &[&'a str],
locales: &[L],
) -> Result<Vec<String>, ExecError>
where
L: AsRef<str>,
{
self.get_args(self.action_exec(action_name), uris, locales)
}

fn get_args<L>(
&'a self,
exec: Option<&'a str>,
uris: &[&'a str],
locales: &[L],
) -> Result<Vec<String>, ExecError>
where
L: AsRef<str>,
{
let Some(exec) = exec else {
return Err(ExecError::ExecFieldNotFound);
};

if exec.contains('=') {
return Err(ExecError::WrongFormat("equal sign detected".into()));
}

let exec = if let Some(without_prefix) = exec.strip_prefix('\"') {
without_prefix
.strip_suffix('\"')
.ok_or(ExecError::WrongFormat("unmatched quote".into()))?
} else {
exec
};

let mut args: Vec<String> = Vec::new();

for arg in exec.split_ascii_whitespace() {
match ArgOrFieldCode::try_from(arg) {
Ok(arg) => match arg {
ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => {
if let Some(arg) = uris.first() {
args.push(arg.to_string());
}
}
ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => {
uris.iter().for_each(|uri| args.push(uri.to_string()));
}
ArgOrFieldCode::IconKey => {
if let Some(icon) = self.icon() {
args.push(icon.to_string());
}
}
ArgOrFieldCode::TranslatedName => {
if let Some(name) = self.name(locales) {
args.push(name.to_string());
}
}
ArgOrFieldCode::DesktopFileLocation => {
args.push(self.path.to_string_lossy().to_string());
}
ArgOrFieldCode::Arg(arg) => {
args.push(arg.to_string());
}
},
Err(e) => {
log::error!("{}", e);
}
}
}

if args.is_empty() {
return Err(ExecError::ExecFieldIsEmpty);
}

Ok(args)
}
}

// either a command line argument or a field-code as described
// in https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
enum ArgOrFieldCode<'a> {
SingleFileName,
FileList,
SingleUrl,
UrlList,
IconKey,
TranslatedName,
DesktopFileLocation,
Arg(&'a str),
}

#[derive(Debug, Error)]
enum ExecErrorInternal<'a> {
#[error("Unknown field code: '{0}'")]
UnknownFieldCode(&'a str),

#[error("Deprecated field code: '{0}'")]
DeprecatedFieldCode(&'a str),
}

impl<'a> TryFrom<&'a str> for ArgOrFieldCode<'a> {
type Error = ExecErrorInternal<'a>;

// todo: handle escaping
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value {
"%f" => Ok(ArgOrFieldCode::SingleFileName),
"%F" => Ok(ArgOrFieldCode::FileList),
"%u" => Ok(ArgOrFieldCode::SingleUrl),
"%U" => Ok(ArgOrFieldCode::UrlList),
"%i" => Ok(ArgOrFieldCode::IconKey),
"%c" => Ok(ArgOrFieldCode::TranslatedName),
"%k" => Ok(ArgOrFieldCode::DesktopFileLocation),
"%d" | "%D" | "%n" | "%N" | "%v" | "%m" => {
Err(ExecErrorInternal::DeprecatedFieldCode(value))
}
other if other.starts_with('%') => Err(ExecErrorInternal::UnknownFieldCode(other)),
other => Ok(ArgOrFieldCode::Arg(other)),
}
}
}

#[cfg(test)]
mod test {

use std::path::PathBuf;

use crate::{get_languages_from_env, DesktopEntry};

use super::ExecError;

#[test]
fn should_return_unmatched_quote_error() {
let path = PathBuf::from("tests_entries/exec/unmatched-quotes.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&[], &locales);

assert!(matches!(result.unwrap_err(), ExecError::WrongFormat(..)));
}

#[test]
fn should_fail_if_exec_string_is_empty() {
let path = PathBuf::from("tests_entries/exec/empty-exec.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&[], &locales);

assert!(matches!(result.unwrap_err(), ExecError::ExecFieldIsEmpty));
}

#[test]
fn should_exec_simple_command() {
let path = PathBuf::from("tests_entries/exec/alacritty-simple.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&[], &locales);

assert!(result.is_ok());
}

#[test]
fn should_exec_complex_command() {
let path = PathBuf::from("tests_entries/exec/non-terminal-cmd.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&[], &locales);

assert!(result.is_ok());
}

#[test]
fn should_exec_terminal_command() {
let path = PathBuf::from("tests_entries/exec/non-terminal-cmd.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&[], &locales);

assert!(result.is_ok());
}

#[test]
#[ignore = "Needs a desktop environment with nvim installed, run locally only"]
fn should_parse_exec_with_field_codes() {
let path = PathBuf::from("/usr/share/applications/nvim.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&["src/lib.rs"], &locales);

assert!(result.is_ok());
}

#[test]
#[ignore = "Needs a desktop environment with gnome Books installed, run locally only"]
fn should_parse_exec_with_dbus() {
let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let result = de.parse_exec_with_uris(&["src/lib.rs"], &locales);

assert!(result.is_ok());
}

#[test]
#[ignore = "Needs a desktop environment with Nautilus installed, run locally only"]
fn should_parse_exec_with_dbus_and_field_codes() {
let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop");
let locales = get_languages_from_env();
let de = DesktopEntry::from_path(path, Some(&locales)).unwrap();
let _result = de.parse_exec_with_uris(&[], &locales);
let path = std::env::current_dir().unwrap();
let path = path.to_string_lossy();
let path = format!("file:///{path}");
let result = de.parse_exec_with_uris(&[path.as_str()], &locales);

assert!(result.is_ok());
}
}
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
mod decoder;
mod iter;

mod exec;
pub use exec::ExecError;

pub mod matching;
pub use decoder::DecodeError;

Expand Down Expand Up @@ -477,7 +480,7 @@ fn env_with_locale() {
let locales = &["fr_FR"];

let de = DesktopEntry::from_path(
PathBuf::from("tests/org.mozilla.firefox.desktop"),
PathBuf::from("tests_entries/org.mozilla.firefox.desktop"),
Some(locales),
)
.unwrap();
Expand Down
File renamed without changes.
19 changes: 19 additions & 0 deletions tests_entries/exec/alacritty-simple.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Desktop Entry]
Type=Application
TryExec=alacritty
Exec=alacritty
Icon=Alacritty
Terminal=false
Categories=System;TerminalEmulator;

Name=Alacritty
GenericName=Terminal
Comment=A fast, cross-platform, OpenGL terminal emulator
StartupWMClass=Alacritty
Actions=New;

X-Desktop-File-Install-Version=0.26

[Desktop Action New]
Name=New Terminal
Exec=alacritty
5 changes: 5 additions & 0 deletions tests_entries/exec/empty-exec.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Desktop Entry]
Exec=
Terminal=false
Type=Application
Name=NoExecKey
5 changes: 5 additions & 0 deletions tests_entries/exec/non-terminal-cmd.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Desktop Entry]
Exec=alacritty -e glxgears -info
Terminal=false
Type=Application
Name=GlxGearNoTerminal
5 changes: 5 additions & 0 deletions tests_entries/exec/terminal-cmd.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Desktop Entry]
Exec=glxgears -info
Terminal=true
Type=Application
Name=GlxGearTerminal
5 changes: 5 additions & 0 deletions tests_entries/exec/unmatched-quotes.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Desktop Entry]
Exec="alacritty -e
Terminal=false
Type=Application
Name=InvalidCommand
File renamed without changes.