mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Part of #637: the editing data's readers stay linear on hostile input
Zstandard frames: the trailing zero padding is trimmed once, not looked for again before each frame, and at most 4096 frames are read (more is damaged data). A `]` finds its `[` from a list of where they are rather than by scanning the operand stack back. Images are read up to 32 million pixels. Tests: thousands of tiny frames, and hundreds of thousands of unmatched brackets, read in one pass. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0be7640088
commit
9b4166ed5e
3 files changed
+46
-10
No files matched your search
@@ -37,6 +37,8 @@ mod tests;
|
||||
pub const MAX_DATA: u64 = 256 << 20;
|
||||
/// Largest Zstandard window accepted (a frame asking for more is damaged data).
|
||||
const MAX_ZSTD_WINDOW: u64 = 64 << 20;
|
||||
/// Most Zstandard frames read (the app writes one).
|
||||
const MAX_FRAMES: usize = 4096;
|
||||
|
||||
const EPS_BEGIN: &[u8] = b"%AI9_PrivateDataBegin";
|
||||
const EPS_END: &[u8] = b"%AI9_PrivateDataEnd";
|
||||
@@ -126,20 +128,22 @@ impl Codec {
|
||||
flate2::read::ZlibDecoder::new(data).take(max + 1).read_to_end(&mut out).map_err(|_| damaged("its zlib compression"))?;
|
||||
}
|
||||
Codec::Zstd => {
|
||||
let mut src = data;
|
||||
// The data may be several frames one after the other.
|
||||
while src.iter().any(|b| *b != 0) {
|
||||
// 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();
|
||||
let mut frames = 0usize;
|
||||
while !src.is_empty() {
|
||||
frames += 1;
|
||||
if frames > MAX_FRAMES {
|
||||
return Err(damaged("its Zstandard compression"));
|
||||
}
|
||||
let left = (max + 1).saturating_sub(out.len() as u64);
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
let d = ruzstd::decoding::StreamingDecoder::new_with_max_window_size(&mut src, MAX_ZSTD_WINDOW)
|
||||
.map_err(|_| damaged("its Zstandard compression"))?;
|
||||
let before = out.len();
|
||||
d.take(left).read_to_end(&mut out).map_err(|_| damaged("its Zstandard compression"))?;
|
||||
if out.len() == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const MAX_NODES: usize = 4_000_000;
|
||||
/// Most points in one path.
|
||||
const MAX_POINTS: usize = 4_000_000;
|
||||
/// Largest image read (pixels).
|
||||
const MAX_PIXELS: u64 = 1 << 28;
|
||||
const MAX_PIXELS: u64 = 1 << 25;
|
||||
/// Largest side of the document and its artboards (points).
|
||||
const MAX_SIDE: f64 = 1e6;
|
||||
|
||||
@@ -200,6 +200,8 @@ struct Raster {
|
||||
struct Reader<'a> {
|
||||
lex: Lexer<'a>,
|
||||
stack: Vec<V>,
|
||||
/// Where the `[` marks are on the stack, innermost last (some may have gone with operands).
|
||||
marks: Vec<usize>,
|
||||
gs: GState,
|
||||
frames: Vec<Frame>,
|
||||
path: BezPath,
|
||||
@@ -323,6 +325,7 @@ impl<'a> Reader<'a> {
|
||||
Self {
|
||||
lex: Lexer::new(data),
|
||||
stack: Vec::new(),
|
||||
marks: Vec::new(),
|
||||
gs: GState::default(),
|
||||
frames: Vec::new(),
|
||||
path: BezPath::new(),
|
||||
@@ -622,9 +625,19 @@ impl<'a> Reader<'a> {
|
||||
|
||||
fn op(&mut self, w: &str, hidden: bool) -> Result<(), String> {
|
||||
match w {
|
||||
"[" => return self.push(V::Mark),
|
||||
"[" => {
|
||||
self.marks.push(self.stack.len());
|
||||
return self.push(V::Mark);
|
||||
}
|
||||
"]" => {
|
||||
let at = self.stack.iter().rposition(|v| matches!(v, V::Mark)).unwrap_or(self.stack.len());
|
||||
// The innermost `[` still on the stack (the stack is cut back by operators).
|
||||
let mut at = self.stack.len();
|
||||
while let Some(m) = self.marks.pop() {
|
||||
if matches!(self.stack.get(m), Some(V::Mark)) {
|
||||
at = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let items: Vec<V> = self.stack.split_off(at).into_iter().skip(1).collect();
|
||||
return self.push(V::Arr(items));
|
||||
}
|
||||
|
||||
@@ -407,3 +407,22 @@ fn ai_private_data() {
|
||||
}
|
||||
assert!(decode_private(b"%AI12_CompressedDataxx").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_zstandard_frames_are_read_in_one_pass_and_capped() {
|
||||
let frame = zstd(b" ");
|
||||
let frames = |n: usize| -> Vec<u8> { [&b"%AI24_ZStandard_Data"[..], &frame.repeat(n), &[0; 64]].concat() };
|
||||
let started = std::time::Instant::now();
|
||||
assert_eq!(decode_private(&frames(4000)).unwrap().len(), 4000);
|
||||
assert!(decode_private(&frames(50_000)).unwrap_err().contains("Zstandard"));
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_brackets_without_their_opening_dont_rescan_the_stack() {
|
||||
let n = 200_000;
|
||||
let body = format!("{}\n{}\n", "1 ".repeat(n), "] ".repeat(n));
|
||||
let started = std::time::Instant::now();
|
||||
let _ = read(stream(&layer(&body)).as_bytes());
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed());
|
||||
}
|
||||
Reference in new issue
Block a user