use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
use std::time::Duration;
#[derive(Debug)]
pub enum DecodeOutcome {
Ok,
ParseError(String),
Timeout,
}
pub const INTERACTIVE_TIMEOUT: Duration = Duration::from_secs(1);
const MAX_CONCURRENT_SVG_WORKERS: usize = 8;
static ACTIVE_SVG_WORKERS: AtomicUsize = AtomicUsize::new(0);
pub fn parse_with_timeout(source: String, deadline: Duration) -> DecodeOutcome {
if ACTIVE_SVG_WORKERS.fetch_add(1, Ordering::Relaxed) >= MAX_CONCURRENT_SVG_WORKERS {
ACTIVE_SVG_WORKERS.fetch_sub(1, Ordering::Relaxed);
return DecodeOutcome::Timeout;
}
let (tx, rx) = mpsc::channel();
std::thread::Builder::new()
.name("plushie-svg-guard".into())
.spawn(move || {
let opt = usvg::Options::default();
let result = usvg::Tree::from_str(&source, &opt).map_err(|e| e.to_string());
let _ = tx.send(result);
ACTIVE_SVG_WORKERS.fetch_sub(1, Ordering::Relaxed);
})
.map(|_| ())
.unwrap_or_else(|e| {
log::error!("svg_guard: failed to spawn worker: {e}");
ACTIVE_SVG_WORKERS.fetch_sub(1, Ordering::Relaxed);
});
match rx.recv_timeout(deadline) {
Ok(Ok(_tree)) => DecodeOutcome::Ok,
Ok(Err(msg)) => DecodeOutcome::ParseError(msg),
Err(mpsc::RecvTimeoutError::Timeout) => DecodeOutcome::Timeout,
Err(mpsc::RecvTimeoutError::Disconnected) => {
DecodeOutcome::ParseError("svg_guard: worker panicked".into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_svg() {
let src = r#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>"#;
let out = parse_with_timeout(src.to_string(), INTERACTIVE_TIMEOUT);
matches!(out, DecodeOutcome::Ok).then_some(()).expect("ok");
}
#[test]
fn returns_parse_error_on_garbage() {
let out = parse_with_timeout("not xml at all".to_string(), INTERACTIVE_TIMEOUT);
assert!(
matches!(out, DecodeOutcome::ParseError(_)),
"expected parse error"
);
}
#[test]
fn timeout_short_circuits() {
let src = r#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#;
let out = parse_with_timeout(src.to_string(), Duration::from_nanos(1));
assert!(
matches!(
out,
DecodeOutcome::Ok | DecodeOutcome::Timeout | DecodeOutcome::ParseError(_)
),
"unexpected outcome"
);
}
}