From 1812ccda0804f4edb52469b1ec48c70dedcf716f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 08:53:28 -0700 Subject: [PATCH] .ai open: keep the last zero byte of a Zstandard frame in the editing data The reader cut every trailing zero byte off the Zstandard data before decoding it, to drop the padding after the last frame. A frame can end in a zero byte itself, and cutting it left the frame short, so the file failed to open with "its Zstandard compression is damaged". The reader now decodes frame after frame and stops when only zeros are left. --- crates/eps/src/import/ai.rs | 8 ++++---- crates/eps/src/import/ai/tests.rs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index e71fc7db4..73e248ac5 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -141,11 +141,11 @@ impl Codec { flate2::read::ZlibDecoder::new(data).take(max + 1).read_to_end(&mut out).map_err(|_| damaged("its zlib compression"))?; } Codec::Zstd => { - // The data may be several frames one after the other, padded with zeros. - let end = data.iter().rposition(|b| *b != 0).map_or(0, |i| i + 1); - let mut src = data.get(..end).unwrap_or_default(); + // The data may be several frames one after the other, padded with zeros. A frame + // can itself end in a zero byte, so the padding is what is left after the last frame. + let mut src = data; let mut frames = 0usize; - while !src.is_empty() { + while src.iter().any(|b| *b != 0) { frames += 1; if frames > MAX_FRAMES { return Err(damaged("its Zstandard compression")); diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 721365c25..c9acad3ec 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -1219,6 +1219,19 @@ fn many_zstandard_frames_are_read_in_one_pass_and_capped() { assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed()); } +/// A Zstandard frame that ends in a zero byte keeps it: only the zeros after the last frame are +/// padding. +#[test] +fn a_zstandard_frame_ending_in_a_zero_byte_is_read_whole() { + // Magic number, a single-segment header with a 1-byte content size of 3, and one raw block + // (last, 3 bytes) holding `ab` and a zero byte. + let frame = [0x28, 0xB5, 0x2F, 0xFD, 0x20, 3, 0x19, 0, 0, b'a', b'b', 0]; + for padding in [0, 64] { + let raw = [&b"%AI24_ZStandard_Data"[..], &frame, &frame, &vec![0; padding]].concat(); + assert_eq!(decode_private(&raw).unwrap(), b"ab\0ab\0"); + } +} + #[test] fn closing_brackets_without_their_opening_dont_rescan_the_stack() { let n = 200_000;