From 7e09989ce45ce2dcf4f09066b88b4306171245d4 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 06:26:24 -0500 Subject: [PATCH] craft-xcf: allow cancellation between layer records * xcf: stop parsing layer metadata on cancellation --- crates/xcf/src/lib.rs | 9 +++++++++ crates/xcf/src/tests.rs | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/crates/xcf/src/lib.rs b/crates/xcf/src/lib.rs index fa226d1f1..9d714b47b 100644 --- a/crates/xcf/src/lib.rs +++ b/crates/xcf/src/lib.rs @@ -566,6 +566,12 @@ impl<'a> Xcf<'a> { /// Reads the header, the image properties and every layer structure of an XCF file /// (already inflated when it was gzipped). pub fn parse(bytes: &'a [u8]) -> Result> { + Self::parse_with(bytes, &|| true) + } + + /// Parses an XCF file like [`Self::parse`], checking `keep_going` between layer records. + /// This lets callers stop large metadata parses when their operation is cancelled. + pub fn parse_with(bytes: &'a [u8], keep_going: &dyn Fn() -> bool) -> Result> { if !is_xcf(bytes) { return Err(invalid("not an XCF file")); } @@ -639,6 +645,9 @@ impl<'a> Xcf<'a> { let c = c.at(0); let mut meta_budget = MAX_METADATA_BYTES; for ptr in layer_ptrs { + if !keep_going() { + return Err(Error::Cancelled); + } let layer = layer(c.at(ptr), &image, &mut meta_budget)?; image.layers.push(layer); } diff --git a/crates/xcf/src/tests.rs b/crates/xcf/src/tests.rs index 5ec45620c..e2f4701b5 100644 --- a/crates/xcf/src/tests.rs +++ b/crates/xcf/src/tests.rs @@ -1003,6 +1003,21 @@ fn the_pixel_budget_is_charged_before_decoding_and_decoding_can_be_cancelled() { assert_eq!(xcf.layer_pixels(0, &mut MAX_PIXEL_BYTES.clone(), &|| false), Err(Error::Cancelled)); } +#[test] +fn parsing_layer_metadata_can_be_cancelled_between_records() { + let mut img = TestImage::rgb8(3, 3); + img.layers.push(TestLayer::rgba("first", 3, 3, rgba(3, 3, |_, _| [1, 2, 3, 255]))); + img.layers.push(TestLayer::rgba("second", 3, 3, rgba(3, 3, |_, _| [4, 5, 6, 255]))); + let bytes = img.write(); + let checks = Cell::new(0); + let keep_going = || { + checks.set(checks.get() + 1); + checks.get() <= 1 + }; + assert_eq!(Xcf::parse_with(&bytes, &keep_going).err(), Some(Error::Cancelled)); + assert_eq!(checks.get(), 2, "the second layer is not parsed after cancellation"); +} + #[test] fn gzipped_files_are_recognised_by_name_and_inflated() { use std::io::Write;