use std::ffi::{OsStr, OsString};
use std::path::Path;
#[cfg(test)]
mod tests;
pub(super) struct Fields<'a> {
pub(super) file: &'a Path,
pub(super) name: &'a str,
pub(super) icon: Option<&'a str>,
pub(super) entry: &'a Path,
}
#[derive(Debug, PartialEq)]
enum Piece {
Text(String),
Code(char, bool),
}
#[derive(Debug, Default)]
struct Word {
pieces: Vec<Piece>,
quoted: bool,
}
impl Word {
fn text(&mut self, c: char) {
if let Some(Piece::Text(text)) = self.pieces.last_mut() {
text.push(c);
} else {
self.pieces.push(Piece::Text(c.to_string()));
}
}
}
pub(super) fn expand(exec: &str, fields: &Fields<'_>) -> Option<Vec<OsString>> {
let mut args = Vec::new();
let mut takes_file = false;
for word in words(exec)? {
if word.pieces == [Piece::Code('i', false)] {
if let Some(icon) = fields.icon {
args.push(OsString::from("--icon"));
args.push(OsString::from(icon));
}
continue;
}
let mut arg = OsString::new();
let mut keep = word.quoted;
for piece in word.pieces {
match piece {
Piece::Text(text) => {
arg.push(text);
keep = true;
}
Piece::Code(code, quoted) => {
let value = match code {
'f' | 'F' | 'u' | 'U' => {
takes_file = true;
fields.file.as_os_str().to_owned()
}
'c' => OsString::from(fields.name),
'k' => fields.entry.as_os_str().to_owned(),
'i' | 'd' | 'D' | 'n' | 'N' | 'v' | 'm' => continue,
_ => return None,
};
arg.push(if quoted { shell_quoted(&value) } else { value });
keep = true;
}
}
}
if keep {
args.push(arg);
}
}
if args.first().is_none_or(|program| program.is_empty()) {
return None;
}
if !takes_file {
args.push(fields.file.as_os_str().to_owned());
}
Some(args)
}
fn words(exec: &str) -> Option<Vec<Word>> {
let mut words = Vec::new();
let mut word: Option<Word> = None;
let mut chars = exec.chars();
while let Some(c) = chars.next() {
match c {
' ' | '\t' | '\n' => words.extend(word.take()),
'"' => {
let word = word.get_or_insert_with(Word::default);
word.quoted = true;
loop {
match chars.next()? {
'"' => break,
'\\' => {
let next = chars.next()?;
if !matches!(next, '"' | '`' | '$' | '\\') {
word.text('\\');
}
word.text(next);
}
'%' => match chars.next()? {
'%' => word.text('%'),
code => word.pieces.push(Piece::Code(code, true)),
},
other => word.text(other),
}
}
}
'%' => {
let word = word.get_or_insert_with(Word::default);
match chars.next()? {
'%' => word.text('%'),
code => word.pieces.push(Piece::Code(code, false)),
}
}
other => word.get_or_insert_with(Word::default).text(other),
}
}
words.extend(word);
Some(words)
}
#[cfg(unix)]
fn shell_quoted(value: &OsStr) -> OsString {
use std::os::unix::ffi::{OsStrExt, OsStringExt};
let mut out = vec![b'\''];
for &byte in value.as_bytes() {
if byte == b'\'' {
out.extend_from_slice(b"'\\''");
} else {
out.push(byte);
}
}
out.push(b'\'');
OsString::from_vec(out)
}
#[cfg(not(unix))]
fn shell_quoted(value: &OsStr) -> OsString {
OsString::from(format!("'{}'", value.to_string_lossy().replace('\'', "'\\''")))
}