#1398: share the member size check between JSON and images (capped_member)

# Conflicts:
#	crates/format/src/sketch.rs
This commit is contained in:
bflatastic
2026-10-29 10:30:56 +07:00
parent ca84502ffd
commit 23e16be7f5
+25 -27
View File
@@ -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<std::io::Cursor<&[u8]>>, name: &str, limit: u64, total: &mut u64) -> Result<Value, FormatError> {
/// 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<std::io::Cursor<&[u8]>>,
name: &str,
limit: u64,
total: &mut u64,
max_total: u64,
) -> Result<Vec<u8>, 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<std::io::Cursor<&[u8]>>, name: &str, limit: u64, total: &mut u64) -> Result<Value, FormatError> {
let data = capped_member(z, name, limit, total, MAX_JSON_TOTAL)?;
serde_json::from_slice(&data).map_err(|e| corrupt(format!("{name}: {e}")))
}