From 78c77ad45071159925b8901d23d24b2dd5ed2645 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:16:56 +0000 Subject: [PATCH] Reject invalid slide index in file.export, file.render, slide.colorAt Found while testing crates/engine/src/cmd/file.rs:581 (export); same pattern in render :539 and color_at :550 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/file.rs | 8 ++++---- crates/engine/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index f21f2a3a..24a52fca 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -7,7 +7,7 @@ use deckcraft_media::MediaInfo; use deckcraft_model::{MediaId, MediaItem, Presentation, Shape, ShapeKind, defaults}; use serde_json::{Value, json}; -use super::{CommandSpec, always, bad, base64_decode, base64_encode, bool_or, cmd, f64_or, has_doc, ok, str_param, usize_param}; +use super::{CommandSpec, always, bad, base64_decode, base64_encode, bool_or, cmd, f64_or, has_doc, ok, opt_usize_param, str_param, usize_param}; use crate::{DocState, EngineError, Result, Session}; pub fn specs() -> Vec { @@ -536,7 +536,7 @@ fn picture_bytes(img: &deckcraft_render::Image, format: &str, scale: f64) -> Vec fn render(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; - let i = usize_param(p, "slide").unwrap_or(st.selection.slide); + let i = opt_usize_param(p, "slide", "file.render")?.unwrap_or(st.selection.slide); if i >= st.doc.slides.len() { return Err(bad("file.render", format!("no slide {i}"))); } @@ -547,7 +547,7 @@ fn render(s: &mut Session, p: &Value) -> Result { /// The colour the slide is drawn with at one point: what the Eyedropper takes. fn color_at(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; - let i = usize_param(p, "slide").unwrap_or(st.selection.slide); + let i = opt_usize_param(p, "slide", "slide.colorAt")?.unwrap_or(st.selection.slide); if i >= st.doc.slides.len() { return Err(bad("slide.colorAt", format!("no slide {i}"))); } @@ -578,7 +578,7 @@ fn export(s: &mut Session, p: &Value) -> Result { } match format.as_str() { "png" | "jpeg" | "jpg" => { - let slides: Vec = match usize_param(p, "slide") { + let slides: Vec = match opt_usize_param(p, "slide", "file.export")? { Some(i) => vec![i], None if bool_or(p, "all", false) => (0..st.doc.slides.len()).collect(), None => vec![st.selection.slide], diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 4dfc21f4..b5c3ceb8 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5646,3 +5646,20 @@ fn invalid_ids_array_never_falls_back_to_the_selection() { assert!(s.doc().unwrap().shape(deckcraft_model::ShapeId(a as u32)).is_some(), "shape deleted for {ids}"); } } + +#[test] +fn invalid_slide_index_never_falls_back_to_the_selection_in_export_commands() { + let mut s = session(); + let dir = std::env::temp_dir().join(format!("deckcraft-badslide-{}", std::process::id())); + let path = dir.join("a.png"); + for slide in [json!(-1), json!(1.5), json!("two")] { + let r = s.execute("file.export", &json!({"path": path.to_string_lossy(), "slide": slide, "scale": 0.1})); + assert!(matches!(r, Err(crate::EngineError::BadParams { .. })), "export slide {slide} gave {r:?}"); + assert!(!path.exists(), "export wrote a file for {slide}"); + let r = s.execute("file.render", &json!({"slide": slide, "scale": 0.1})); + assert!(matches!(r, Err(crate::EngineError::BadParams { .. })), "render slide {slide} gave {r:?}"); + let r = s.execute("slide.colorAt", &json!({"slide": slide, "x": 1, "y": 1, "scale": 0.1})); + assert!(matches!(r, Err(crate::EngineError::BadParams { .. })), "colorAt slide {slide} gave {r:?}"); + } + let _ = std::fs::remove_dir_all(&dir); +}