Krolyxon
Commit 74a9b56

Branches

Tags

Refs →

remagesrcdecodeexternal.rs

155 lines · 5.0 KB · Rust

1//! `[[decoders]]`: external programs that turn anything else into PNG, PPM or TIFF on stdout.
2
3use std::io::Read;
4use std::path::Path;
5use std::process::{Command, Stdio};
6use std::sync::RwLock;
7
8use anyhow::{Context, anyhow};
9
10use super::{DecodedImage, Options, image_rs};
11use crate::config::Decoder;
12
13static DECODERS: RwLock<Vec<Decoder>> = RwLock::new(Vec::new());
14
15/// Install the configured decoders (at startup and on every config reload).
16pub fn set_decoders(decoders: &[Decoder]) {
17 let mut d = DECODERS.write().unwrap_or_else(|e| e.into_inner());
18 *d = decoders.to_vec();
19}
20
21/// The first decoder whose patterns match `path`.
22pub fn find(path: &Path) -> Option<Decoder> {
23 let d = DECODERS.read().unwrap_or_else(|e| e.into_inner());
24 if d.is_empty() {
25 return None;
26 }
27 let name = path.file_name()?.to_string_lossy();
28 let full = path.to_string_lossy();
29 d.iter()
30 .find(|dec| {
31 dec.patterns.iter().any(|p| {
32 // Patterns with a slash match the whole path, others the file name.
33 let target = if p.contains('/') { &*full } else { &*name };
34 glob(&p.to_lowercase(), &target.to_lowercase())
35 })
36 })
37 .cloned()
38}
39
40/// `*` and `?` wildcards.
41fn glob(pattern: &str, text: &str) -> bool {
42 let (p, t): (Vec<char>, Vec<char>) = (pattern.chars().collect(), text.chars().collect());
43 let (mut pi, mut ti) = (0, 0);
44 let mut star: Option<(usize, usize)> = None;
45 while ti < t.len() {
46 if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
47 pi += 1;
48 ti += 1;
49 } else if pi < p.len() && p[pi] == '*' {
50 star = Some((pi, ti));
51 pi += 1;
52 } else if let Some((sp, st)) = star {
53 pi = sp + 1;
54 ti = st + 1;
55 star = Some((sp, st + 1));
56 } else {
57 return false;
58 }
59 }
60 p[pi..].iter().all(|&c| c == '*')
61}
62
63pub fn decode(path: &Path, d: &Decoder, opts: &Options) -> anyhow::Result<DecodedImage> {
64 let file = path.as_os_str();
65 let mut args = d.run.iter().map(|a| {
66 let a = match (a.strip_prefix("~/"), std::env::var_os("HOME")) {
67 (Some(rest), Some(home)) => format!("{}/{rest}", home.to_string_lossy()),
68 _ => a.clone(),
69 };
70 // `{file}` stands for the path, also inside a larger argument (`{file}[0]`).
71 match a.split_once("{file}") {
72 Some((pre, post)) => {
73 let mut s = std::ffi::OsString::from(pre);
74 s.push(file);
75 s.push(post);
76 s
77 }
78 None => a.into(),
79 }
80 });
81 let program = args
82 .next()
83 .ok_or_else(|| anyhow!("empty decoder command"))?;
84 let mut child = Command::new(&program)
85 .args(args)
86 .stdin(Stdio::null())
87 .stdout(Stdio::piped())
88 .stderr(Stdio::piped())
89 .spawn()
90 .with_context(|| program.to_string_lossy().into_owned())?;
91 let mut out = Vec::new();
92 if let Some(mut s) = child.stdout.take() {
93 s.read_to_end(&mut out)?;
94 }
95 let mut err = String::new();
96 if let Some(mut s) = child.stderr.take() {
97 let _ = s.read_to_string(&mut err);
98 }
99 let status = child.wait()?;
100 if !status.success() {
101 let first = err.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
102 return Err(anyhow!(
103 "{} failed: {status} {first}",
104 program.to_string_lossy()
105 ));
106 }
107 let mut img = image_rs::decode_bytes(&out, opts)?;
108 img.format = "EXT";
109 Ok(img)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn globbing() {
118 assert!(glob("*.psd", "a.psd"));
119 assert!(glob("*.psd", ".psd"));
120 assert!(!glob("*.psd", "a.psd.bak"));
121 assert!(glob("img-??.x*", "img-01.xcf"));
122 assert!(glob("*a*b*", "xaxxbx"));
123 assert!(!glob("a?", "a"));
124 }
125
126 #[test]
127 fn decodes_through_a_command() {
128 let dir = std::env::temp_dir().join(format!("remage-ext-{}", std::process::id()));
129 std::fs::create_dir_all(&dir).unwrap();
130 let png = dir.join("src.png");
131 image::RgbaImage::from_pixel(3, 2, image::Rgba([1, 2, 3, 255]))
132 .save(&png)
133 .unwrap();
134 let fake = dir.join("pic.fake");
135 std::fs::copy(&png, &fake).unwrap();
136 let d = Decoder {
137 patterns: vec!["*.FAKE".into()],
138 run: vec!["cat".into(), "{file}".into()],
139 };
140 set_decoders(std::slice::from_ref(&d));
141 assert!(find(&fake).is_some());
142 assert!(find(&png).is_none());
143 let img = decode(&fake, &d, &Options::default()).unwrap();
144 assert_eq!((img.size, img.format), ([3, 2], "EXT"));
145 let bad = Decoder {
146 patterns: vec![],
147 run: vec!["sh".into(), "-c".into(), "echo nope >&2; exit 2".into()],
148 };
149 let e = decode(&fake, &bad, &Options::default())
150 .unwrap_err()
151 .to_string();
152 assert!(e.contains("nope"), "{e}");
153 set_decoders(&[]);
154 }
155}