mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 03:16:53 -06:00
fix(format): reopen large documents once saved
`document.json` was read under the 512 MB cap for any archive member, so a large imported document saved but didn't open again: 900 000 layers save as a 1.48 GB document.json, packed into 244 MB. It may now unpack to 16 times its packed size (counted up to the archive's own size), within 512 MB and 4 GB; images and other members keep 512 MB. A small archive still can't unpack into gigabytes. That saved file reopens in 13 s with a 5.9 GB peak. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
12d43b1f99
commit
0efddc3894
@@ -202,3 +202,18 @@ fn the_header_is_read_on_its_own() {
|
||||
let (_, _, index) = parse_json(&d.replacen("\"imageIndex\":[]", "\"imageIndex\":[{\"hash\":\"ab\"},{\"nope\":1}]", 1)).unwrap();
|
||||
assert_eq!(index, Some(["ab".to_string()].into_iter().collect()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_json_may_unpack_in_proportion_to_its_packed_size() {
|
||||
const MB: u64 = 1024 * 1024;
|
||||
// A small archive keeps the member cap: it can't unpack into gigabytes.
|
||||
assert_eq!(document_limit(0, 0), MAX_MEMBER);
|
||||
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);
|
||||
// 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.
|
||||
assert_eq!(document_limit(u64::MAX, u64::MAX), MAX_DOCUMENT);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ pub const FORMAT: &str = "figcraft";
|
||||
pub const VERSION: u32 = 1;
|
||||
/// Largest archive member we read (guards against zip bombs).
|
||||
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.
|
||||
const DOCUMENT_RATIO: u64 = 16;
|
||||
const MAX_DOCUMENT: u64 = 4 * 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).
|
||||
@@ -455,6 +460,11 @@ pub(crate) fn read_member<R: Read + Seek>(z: &mut zip::ZipArchive<R>, name: &str
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Largest `document.json` read from an archive of `len` bytes in which it takes `packed`.
|
||||
pub(crate) fn document_limit(packed: u64, len: u64) -> u64 {
|
||||
packed.min(len).saturating_mul(DOCUMENT_RATIO).clamp(MAX_MEMBER, MAX_DOCUMENT)
|
||||
}
|
||||
|
||||
/// How many images, and how many bytes of them together, one file may give.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ImageBudget {
|
||||
@@ -476,7 +486,9 @@ pub(crate) fn load_within(bytes: &[u8], budget: ImageBudget) -> Result<Loaded, F
|
||||
return Ok(Loaded { issues, ..Loaded::from(doc) });
|
||||
}
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).map_err(|e| FormatError::Corrupt(e.to_string()))?;
|
||||
let text = String::from_utf8(read_member(&mut z, "document.json", MAX_MEMBER)?).map_err(|e| FormatError::Corrupt(e.to_string()))?;
|
||||
let packed = z.by_name("document.json").map(|f| f.compressed_size()).unwrap_or(0);
|
||||
let limit = document_limit(packed, bytes.len() as u64);
|
||||
let text = String::from_utf8(read_member(&mut z, "document.json", limit)?).map_err(|e| FormatError::Corrupt(e.to_string()))?;
|
||||
let (mut doc, mut issues, own_images) = parse_json(&text)?;
|
||||
let (images, unread) = read_images(&mut z, own_images.as_ref(), budget);
|
||||
report_lost_images(&mut issues, &unread, own_images.as_ref(), &images);
|
||||
|
||||
Reference in New Issue
Block a user