From 23e16be7f5de8f2f295ed0f0d64ec32dc98b6317 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 29 Oct 2026 09:36:10 +0700 Subject: [PATCH] #1398: share the member size check between JSON and images (capped_member) # Conflicts: # crates/format/src/sketch.rs --- crates/format/src/sketch.rs | 52 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/crates/format/src/sketch.rs b/crates/format/src/sketch.rs index 21cb7d2b..8186ce26 100644 --- a/crates/format/src/sketch.rs +++ b/crates/format/src/sketch.rs @@ -56,10 +56,10 @@ pub const NEWEST_TESTED_VERSION: u64 = 139; pub const TOO_OLD: &str = "saved by a Sketch older than 48; open it in a newer Sketch and save it again"; /// Largest JSON member read (a real page of a few thousand layers takes 33 MB). const MAX_JSON_MEMBER: u64 = 512 * 1024 * 1024; -/// Most bytes JSON members may unpack to, per compressed byte (real files pack 5–20 times; a -/// zero-filled array a thousand times), with a floor for small members. -const MAX_JSON_RATIO: u64 = 100; -const MIN_JSON_ALLOWANCE: u64 = 16 * 1024 * 1024; +/// Most bytes a member may unpack to, per compressed byte (real JSON packs 5–20 times and images +/// barely; a zero-filled member a thousand times), with a floor for small members. +const MAX_RATIO: u64 = 100; +const MIN_ALLOWANCE: u64 = 16 * 1024 * 1024; /// Most JSON read from one file in all. #[cfg(not(target_pointer_width = "32"))] const MAX_JSON_TOTAL: u64 = 2 * 1024 * 1024 * 1024; @@ -67,10 +67,6 @@ const MAX_JSON_TOTAL: u64 = 2 * 1024 * 1024 * 1024; const MAX_JSON_TOTAL: u64 = 512 * 1024 * 1024; /// Largest image member read. const MAX_IMAGE: u64 = figcraft_doc::ImageData::MAX_BYTES; -/// Most bytes an image member may unpack to, per compressed byte (images are already compressed, -/// a zero-filled one packs a thousand times), with a floor for small members. -const MAX_IMAGE_RATIO: u64 = 100; -const MIN_IMAGE_ALLOWANCE: u64 = 16 * 1024 * 1024; /// Most image bytes read from one file in all. #[cfg(not(target_pointer_width = "32"))] const MAX_IMAGE_TOTAL: u64 = 1024 * 1024 * 1024; @@ -256,37 +252,39 @@ impl<'b> Archive<'b> { } // A clone shares the parsed directory and the bytes; the resolver only has shared access. let mut z = self.zip.clone(); - let member = format!("images/{name}"); - let (size, packed) = { - let f = z.by_name(&member).ok()?; - (f.size(), f.compressed_size()) - }; - let allowed = MAX_IMAGE - .min(packed.saturating_mul(MAX_IMAGE_RATIO).max(MIN_IMAGE_ALLOWANCE)) - .min(MAX_IMAGE_TOTAL.saturating_sub(self.image_total.get())); - if size > allowed { - return None; - } - let data = crate::read_member(&mut z, &member, allowed).ok()?; - self.image_total.set(self.image_total.get() + data.len() as u64); - Some(data) + let mut total = self.image_total.get(); + let data = capped_member(&mut z, &format!("images/{name}"), MAX_IMAGE, &mut total, MAX_IMAGE_TOTAL).ok(); + self.image_total.set(total); + data } } -/// JSON member `name`, refused before unpacking when it would be larger than `limit`, than -/// [`MAX_JSON_RATIO`] times what it takes in the archive (past a floor), or than what is left of -/// the file's [`MAX_JSON_TOTAL`] (`total`: read so far). -fn json_member(z: &mut zip::ZipArchive>, name: &str, limit: u64, total: &mut u64) -> Result { +/// Member `name`, refused before unpacking when it would be larger than `limit`, than +/// [`MAX_RATIO`] times what it takes in the archive (past a floor), or than what is left of +/// `max_total` (`total`: read so far, counted up). +fn capped_member( + z: &mut zip::ZipArchive>, + name: &str, + limit: u64, + total: &mut u64, + max_total: u64, +) -> Result, FormatError> { let (size, packed) = { let f = z.by_name(name).map_err(|e| corrupt(format!("{name}: {e}")))?; (f.size(), f.compressed_size()) }; - let allowed = limit.min(packed.saturating_mul(MAX_JSON_RATIO).max(MIN_JSON_ALLOWANCE)).min(MAX_JSON_TOTAL.saturating_sub(*total)); + let allowed = limit.min(packed.saturating_mul(MAX_RATIO).max(MIN_ALLOWANCE)).min(max_total.saturating_sub(*total)); if size > allowed { return Err(corrupt(format!("{name} is too large"))); } let data = crate::read_member(z, name, allowed)?; *total += data.len() as u64; + Ok(data) +} + +/// JSON member `name`, read by [`capped_member`] within the file's [`MAX_JSON_TOTAL`]. +fn json_member(z: &mut zip::ZipArchive>, name: &str, limit: u64, total: &mut u64) -> Result { + let data = capped_member(z, name, limit, total, MAX_JSON_TOTAL)?; serde_json::from_slice(&data).map_err(|e| corrupt(format!("{name}: {e}"))) }