From 5fba7e5dba98a275e858713289ed4f9fd3363672 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 17 Oct 2026 13:48:56 +0700 Subject: [PATCH] #524: one kiwi value per compressed canvas byte (up to 128M), lower value and document.json ceilings on 32-bit targets --- crates/format/src/fig/kiwi.rs | 14 +++++++++----- crates/format/src/fig/tests.rs | 2 ++ crates/format/src/large_tests.rs | 4 +++- crates/format/src/lib.rs | 4 ++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/format/src/fig/kiwi.rs b/crates/format/src/fig/kiwi.rs index 46a9daacd..5c735b280 100644 --- a/crates/format/src/fig/kiwi.rs +++ b/crates/format/src/fig/kiwi.rs @@ -11,11 +11,15 @@ use std::fmt; /// Deepest struct/message nesting decoded. const MAX_DEPTH: usize = 64; /// Most values decoded from one message (every scalar, array element and field counts): at least -/// [`MIN_VALUES`], and one per [`PACKED_BYTES_PER_VALUE`] bytes of the file's compressed canvas, -/// so a large real file decodes while a small one can't expand into a huge tree, however far its -/// chunks inflate. +/// [`MIN_VALUES`], and one per byte of the file's compressed canvas up to [`MAX_VALUES`], so a +/// large real file decodes while a small one can't expand into a huge tree, however far its +/// chunks inflate. 32-bit targets (wasm32) have a 4 GB address space in all, so their ceiling is +/// lower: past it the file is refused cleanly instead of running out of memory. const MIN_VALUES: usize = 20_000_000; -const PACKED_BYTES_PER_VALUE: usize = 2; +#[cfg(not(target_pointer_width = "32"))] +const MAX_VALUES: usize = 128 << 20; +#[cfg(target_pointer_width = "32")] +const MAX_VALUES: usize = 32 << 20; /// Most definitions in a schema, and fields in one definition. const MAX_DEFS: usize = 10_000; const MAX_FIELDS: usize = 10_000; @@ -302,7 +306,7 @@ impl Schema { /// Most values decoded from a file whose compressed canvas is `packed` bytes. pub(crate) fn value_budget(packed: usize) -> usize { - MIN_VALUES.max(packed / PACKED_BYTES_PER_VALUE) + packed.clamp(MIN_VALUES, MAX_VALUES) } struct Decoder<'s, 'b> { diff --git a/crates/format/src/fig/tests.rs b/crates/format/src/fig/tests.rs index 574f860c2..ad9c85d05 100644 --- a/crates/format/src/fig/tests.rs +++ b/crates/format/src/fig/tests.rs @@ -629,6 +629,8 @@ fn large_files_get_a_larger_value_budget() { // A real 400 000-layer file: an 83.5 MB canvas whose message unpacks to 334 MB and holds // 30.5 million values was refused as "too many values". assert!(kiwi::value_budget(83_491_232) > 30_514_899); + // A real 194 MB design-system file (UX4G): a 39.5 MB canvas holding 29.7 million values. + assert!(kiwi::value_budget(39_528_420) > 29_703_730); // Still bounded by the largest canvas read. assert!(kiwi::value_budget(MAX_CANVAS as usize) <= 128 << 20); } diff --git a/crates/format/src/large_tests.rs b/crates/format/src/large_tests.rs index 2dc9eb4c1..889074f18 100644 --- a/crates/format/src/large_tests.rs +++ b/crates/format/src/large_tests.rs @@ -211,7 +211,9 @@ fn document_json_may_unpack_in_proportion_to_its_packed_size() { assert_eq!(document_limit(MB, 2 * MB), MAX_MEMBER); // A real 900 000-layer document saved by us packs 1 477 139 810 bytes into 244 362 923, and // didn't open again under the 512 MB cap. - assert!(document_limit(244_362_923, 260 * MB) > 1_477_139_810); + if cfg!(not(target_pointer_width = "32")) { + assert!(document_limit(244_362_923, 260 * MB) > 1_477_139_810); + } // The packed size an archive claims counts only up to the archive's own size. assert_eq!(document_limit(u64::MAX, MB), MAX_MEMBER); // Never more than the ceiling. diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index dd96b9323..9f7e51bc4 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -42,8 +42,12 @@ const MAX_MEMBER: u64 = 512 * 1024 * 1024; /// `document.json` may unpack to [`DOCUMENT_RATIO`] times what it takes in the archive, within /// [`MAX_MEMBER`] and [`MAX_DOCUMENT`]: a large document saved by us (a 900 000-layer file packs /// 1.48 GB into 244 MB) opens again, while a small archive still can't unpack into gigabytes. +/// 32-bit targets (wasm32) have a 4 GB address space in all, so their ceiling is lower. const DOCUMENT_RATIO: u64 = 16; +#[cfg(not(target_pointer_width = "32"))] const MAX_DOCUMENT: u64 = 4 * 1024 * 1024 * 1024; +#[cfg(target_pointer_width = "32")] +const MAX_DOCUMENT: u64 = 1024 * 1024 * 1024; /// Most versions a file keeps: saving more is refused, extra entries in a file are skipped. pub const MAX_VERSIONS: usize = 100; /// Longest version name and description kept (chars).