diff --git a/crates/plugins/src/objects.rs b/crates/plugins/src/objects.rs index 70193028c..4808f3d22 100644 --- a/crates/plugins/src/objects.rs +++ b/crates/plugins/src/objects.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex}; use serde_json::{Value, json}; use vectorcraft_color::cms::{self, Cms, CmykLut, lab}; use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::cmyk::oversized_tiff; use vectorcraft_doc::{ Appearance, AppearanceItem, Document, Effect, FillLayer, ImageBlob, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeLayer, SymbolBoxes, TextKind, TextObject, @@ -437,8 +438,12 @@ fn text_json(t: &TextObject, left: Option<&mut usize>) -> (Value, bool) { } /// How image `b` stores its colors, read from its header: `"cmyk"`, `"gray"`, `"rgb"`, or -/// `"unknown"` when it can't be read or only a linked image's preview is there. +/// `"unknown"` when it can't be read, is a TIFF too large for the decoder to count, or only a +/// linked image's preview is there. fn color_mode(b: &ImageBlob) -> &'static str { + if oversized_tiff(&b.bytes).is_some() { + return "unknown"; + } if b.proxy.as_ref().is_some_and(|p| Arc::ptr_eq(p, &b.bytes) || **p == *b.bytes) { return "unknown"; } @@ -457,7 +462,7 @@ fn color_mode(b: &ImageBlob) -> &'static str { /// characters of base64; the size is checked, and read from the header, before anything is decoded. fn rgba_pixels(b: &ImageBlob, w: u32, h: u32, max: usize) -> Option> { let bytes = u64::from(w).checked_mul(u64::from(h))?.checked_mul(4)?; - if bytes == 0 || bytes.div_ceil(3).saturating_mul(4) > max as u64 { + if bytes == 0 || bytes.div_ceil(3).saturating_mul(4) > max as u64 || oversized_tiff(&b.bytes).is_some() { return None; } if b.is_cmyk() { @@ -802,19 +807,16 @@ fn decode_objects(v: &Value, read_only: &HashMap) -> Result { - let at = format!("objects[{i}] (id {})", id.0); - if !returned.insert(id) { - return Err(out_err(&at, "is listed twice")); - } - check_unchanged(o, given, &at)?; - Ok(OutObject { id: Some(id), kept: true, ..OutObject::default() }) + .map(|(i, o)| match output_id(o).and_then(|id| Some((id, read_only.get(&id)?))) { + Some((id, given)) => { + let at = format!("objects[{i}] (id {})", id.0); + if !returned.insert(id) { + return Err(out_err(&at, "is listed twice")); } - None => decode_object(o, &format!("objects[{i}]"), &mut anchors), + check_unchanged(o, given, &at)?; + Ok(OutObject { id: Some(id), kept: true, ..OutObject::default() }) } + None => decode_object(o, &format!("objects[{i}]"), &mut anchors), }) .collect::>>()?; let mut missing: Vec = read_only.keys().filter(|id| !returned.contains(id)).copied().collect(); @@ -828,6 +830,12 @@ fn decode_objects(v: &Value, read_only: &HashMap) -> Result Option { + let id = o.get("id")?; + id.as_u64().or_else(|| id.as_f64().filter(|f| f.fract() == 0.0 && (0.0..=9_007_199_254_740_992.0).contains(f)).map(|f| f as u64)).map(NodeId) +} + /// Checks entry `o` of a filter's output for a read-only object it was `given`: `{"id": …}` alone, /// or `given` itself, numbers compared by value. Otherwise the error names the first field that /// differs (`text.runs[0].text`): a field the entry gives before one it leaves out, shown as diff --git a/crates/plugins/src/objects/tests.rs b/crates/plugins/src/objects/tests.rs index 01e248a46..0a7b1d0d1 100644 --- a/crates/plugins/src/objects/tests.rs +++ b/crates/plugins/src/objects/tests.rs @@ -653,6 +653,31 @@ fn objects_that_clip_a_clip_group_are_not_inputs() { assert_eq!(filter_targets(&d, &[by_paths]), [clipped_by_paths.id], "a filter without inputs"); } +/// A TIFF with more strips and planes than the decoder can count comes as `"unknown"`, without its +/// pixels, and never reaches the decoder. +#[test] +fn a_tiff_too_large_to_count_is_unknown_and_has_no_pixels() { + let entries: [(u16, u16, u32); 9] = + [(256, 4, 1), (257, 4, 1 << 31 | 1), (258, 3, 8), (262, 3, 1), (273, 4, 0), (277, 3, 2), (278, 4, 1), (279, 4, 0), (284, 3, 2)]; + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend(u16::try_from(entries.len()).unwrap().to_le_bytes()); + for (tag, ty, v) in entries { + tiff.extend(tag.to_le_bytes()); + tiff.extend(ty.to_le_bytes()); + tiff.extend(1u32.to_le_bytes()); + tiff.extend(v.to_le_bytes()); + } + tiff.extend([0; 4]); + let (mut d, _, layer, _) = mixed(); + d.images.insert("big".into(), ImageBlob::new("image/tiff", tiff)); + let big = image_node(&mut d, "big", (1, 1), None); + let big = d.insert(Some(layer), usize::MAX, big).unwrap(); + let (bytes, _) = encode_input_with(&d, &[big], &[InputKind::Image], usize::MAX).unwrap(); + let input: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(input["objects"][0]["colorMode"], "unknown"); + assert_eq!(input["objects"][0]["pixelsOmitted"], true); +} + /// A field a plug-in adds to a read-only object is named in the refusal with its control and /// direction characters escaped, and cut short. #[test] @@ -692,6 +717,14 @@ fn read_only_kind_field_and_omission_refuse() { let mut echo = input.clone(); echo["objects"][3]["transform"] = json!([1, 0, 0, 1, 5, 50]); assert!(decode_output_with(echo.to_string().as_bytes(), &read_only).is_ok()); + // So is an id written as a float; a fractional one names no object. + let id = input["objects"][3]["id"].as_u64().unwrap(); + let mut echo = input.clone(); + echo["objects"][3]["id"] = json!(id as f64); + let out = decode_output_with(echo.to_string().as_bytes(), &read_only).unwrap(); + assert_eq!(out.objects.iter().filter(|o| o.kept).count(), 5); + echo["objects"][3]["id"] = json!(id as f64 + 0.5); + assert!(decode_output_with(echo.to_string().as_bytes(), &read_only).is_err()); // `{"id"}` alone keeps it; the paths left out are deleted and a new path goes above the type. let ro = |id: NodeId| json!({"id": id.0}); let out = json!({"objects": [ro(text), {"name": "new", "path": square(0.0)}, ro(embedded), ro(linked), ro(star), ro(ghost)]});