Skip to content

Commit

Permalink
feat: parse the exec field
Browse files Browse the repository at this point in the history
  • Loading branch information
wiiznokes authored Aug 20, 2024
1 parent ee9a759 commit 52d48f7
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 6 deletions.
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.
File renamed without changes.

0 comments on commit 52d48f7

Please sign in to comment.