#524: one kiwi value per compressed canvas byte (up to 128M), lower value and document.json ceilings on 32-bit targets

This commit is contained in:
bflatastic
2026-10-17 13:48:56 +07:00
parent cca1058b4a
commit 5fba7e5dba
4 changed files with 18 additions and 6 deletions
+9 -5
View File
@@ -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> {
+2
View File
@@ -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);
}
+3 -1
View File
@@ -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.
+4
View File
@@ -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).