diff --git a/crates/engine/src/templates.rs b/crates/engine/src/templates.rs index a8009f47..500308b7 100644 --- a/crates/engine/src/templates.rs +++ b/crates/engine/src/templates.rs @@ -486,10 +486,10 @@ pub fn encode_thumb(w: u32, h: u32, rgba: &[u8]) -> String { /// Decode a thumbnail to RGB bytes (THUMB_W×THUMB_H×3). pub fn decode_thumb(text: &str) -> Option> { let t = text.trim(); - if t.len() != THUMB_W * THUMB_H * 6 { + if t.len() != THUMB_W * THUMB_H * 6 || !t.is_ascii() { return None; } - (0..t.len()).step_by(2).map(|i| u8::from_str_radix(&t[i..i + 2], 16).ok()).collect() + (0..t.len()).step_by(2).map(|i| u8::from_str_radix(t.get(i..i + 2)?, 16).ok()).collect() } /// Render a project's main comp at its poster time into a thumbnail. @@ -943,6 +943,12 @@ mod tests { use super::*; use crate::config::{DirConfig, MemoryConfig}; + #[test] + fn non_ascii_thumbnail_is_rejected_without_panicking() { + let malformed = format!("aé{}", "0".repeat(THUMB_W * THUMB_H * 6 - 3)); + assert!(decode_thumb(&malformed).is_none()); + } + #[test] fn every_builtin_builds_with_controls_and_renders() { let s = Session::default(); diff --git a/crates/ui-egui/src/panels/home.rs b/crates/ui-egui/src/panels/home.rs index 269f2f44..fcfd0170 100644 --- a/crates/ui-egui/src/panels/home.rs +++ b/crates/ui-egui/src/panels/home.rs @@ -166,12 +166,13 @@ pub fn encode_thumb(img: &egui::ColorImage) -> String { out } +/// Decode a stored hex thumbnail, rejecting malformed or non-ASCII text. pub fn decode_thumb(text: &str) -> Option { let t = text.trim(); - if t.len() != THUMB_W * THUMB_H * 6 { + if t.len() != THUMB_W * THUMB_H * 6 || !t.is_ascii() { return None; } - let b: Vec = (0..t.len()).step_by(2).map(|i| u8::from_str_radix(&t[i..i + 2], 16)).collect::>().ok()?; + let b: Vec = (0..t.len()).step_by(2).map(|i| u8::from_str_radix(t.get(i..i + 2)?, 16).ok()).collect::>()?; let px = b.as_chunks::<3>().0.iter().map(|c| Color32::from_rgb(c[0], c[1], c[2])).collect(); Some(egui::ColorImage::new([THUMB_W, THUMB_H], px)) } @@ -496,6 +497,12 @@ fn run_actions(app: &mut EffectcraftApp, ctx: &egui::Context, actions: Vec<(&str mod tests { use super::*; + #[test] + fn non_ascii_thumbnail_is_rejected_without_panicking() { + let malformed = format!("aé{}", "0".repeat(THUMB_W * THUMB_H * 6 - 3)); + assert!(decode_thumb(&malformed).is_none()); + } + #[test] fn recent_list_comes_from_settings() { let mut prefs = effectcraft_engine::prefs::Prefs::default();