use crate::parse::{Token, WordSet};
use crate::verdict::{SafetyLevel, Verdict};
static MAGICK_SAFE_SUBS: WordSet = WordSet::new(&[
"combine", "compare", "composite", "convert", "identify",
"mogrify", "montage", "stream",
]);
pub fn is_safe_magick(tokens: &[Token]) -> Verdict {
if tokens.len() == 2
&& matches!(tokens[1].as_str(), "--help" | "-h" | "--version" | "-V")
{
return Verdict::Allowed(SafetyLevel::Inert);
}
if tokens[1..].iter().any(|t| t.as_str() == "-script") {
return Verdict::Denied;
}
let Some(first) = tokens.get(1) else {
return Verdict::Denied;
};
let first_str = first.as_str();
if matches!(first_str, "animate" | "conjure" | "display" | "import") {
return Verdict::Denied;
}
if first_str == "convert" || first_str == "identify" {
let inner = shell_words::join(tokens[1..].iter().map(|t| t.as_str()));
return crate::command_verdict(&inner);
}
if MAGICK_SAFE_SUBS.contains(first_str) {
return Verdict::Allowed(SafetyLevel::SafeWrite);
}
let mut parts: Vec<&str> = Vec::with_capacity(tokens.len());
parts.push("convert");
for t in &tokens[1..] {
parts.push(t.as_str());
}
let inner = shell_words::join(parts);
crate::command_verdict(&inner)
}
#[cfg(test)]
mod tests {
use crate::is_safe_command;
use crate::verdict::{SafetyLevel, Verdict};
fn check(cmd: &str) -> bool {
is_safe_command(cmd)
}
fn verdict(cmd: &str) -> Verdict {
crate::command_verdict(cmd)
}
safe! {
magick_help: "magick --help",
magick_version: "magick --version",
magick_convert_explicit: "magick convert in.png out.png",
magick_convert_resize: "magick convert in.png -resize 1200x out.png",
magick_identify_explicit: "magick identify photo.jpg",
magick_implicit_convert: "magick in.png out.png",
magick_implicit_with_resize: "magick in.png -resize 1200x out.png",
magick_implicit_avif_to_png: "magick /Users/me/Downloads/x.avif -resize 1200x /tmp/out.png",
magick_implicit_with_quality: "magick in.jpg -quality 85 out.jpg",
magick_mogrify: "magick mogrify -resize 50% photo.jpg",
magick_compare: "magick compare a.png b.png diff.png",
magick_montage: "magick montage *.png montage.png",
magick_combine: "magick combine a.png b.png combined.png",
magick_stream: "magick stream image.png pixels.gray",
}
denied! {
magick_conjure_msl: "magick conjure script.msl",
magick_display_window: "magick display photo.jpg",
magick_animate_gif: "magick animate animation.gif",
magick_import_screen: "magick import screen.png",
magick_script_flag: "magick -script attack.msl",
magick_script_after_input: "magick in.png -script attack.msl out.png",
magick_convert_with_script: "magick convert -script attack.msl",
}
#[test]
fn magick_implicit_is_safewrite() {
assert_eq!(
verdict("magick in.png -resize 1200x out.png"),
Verdict::Allowed(SafetyLevel::SafeWrite),
);
}
#[test]
fn magick_identify_routes_through_identify_top_level() {
assert_eq!(
verdict("magick identify photo.jpg"),
Verdict::Allowed(SafetyLevel::Inert),
);
}
#[test]
fn magick_help_is_inert() {
assert_eq!(
verdict("magick --help"),
Verdict::Allowed(SafetyLevel::Inert),
);
}
}