mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +00:00
* feat(ps): add craft-ps with an EPS reader (read feature) The reading half of craft-ps (#115): a clean-room PostScript interpreter that runs an EPS and returns what it paints as a one-page vector PDF of its bounding box, colours kept, so apps can draw and export EPS as a placed PDF. Moved from DesignCraft (storytold/designcraft, where it was written for placed EPS), with its app-specific names made neutral. Written from the PostScript Language Reference (3rd ed.), the EPS specification, the Type 1 Font Format and Adobe TN 5012/5176/5177. Covers the language, paths, clipping, strokepath, device/Separation/ DeviceN/Indexed/ICCBased/Lab colour, images with lazily decoded filters, shadings 1-7 and patterns, and Type 1/3/42/CFF/CID text through CMaps, horizontal and vertical. Bounded: each operator charges work proportional to its operands against the step limit as it goes, every byte read or decoded counts, copies count against the memory cap; defaults 50 M steps / 1 GiB, and 20 M / 256 MiB on wasm32. No panics (deny lints, fuzzed in debug builds). 34 tests, the Type 42 one on a TrueType font built in the test. No new external crates: pdf-writer, miniz_oxide, weezl, skrifa and kurbo are already locked at these versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * clippy(ps): document stateful decoder complexity * fix(ps): bound huge arc angle normalization * fix(ps): preserve EffectCraft EPS text and limits parity * fix(ps): address interpreter review findings * fix(ps): preserve composite glyph paint order * fix(ps): address interpreter review findings * fix(ps): bound filter buffers and preserve file aliases * fix(ps): make predictor allocation fallible --------- Co-authored-by: markus.doebele <markus.doebele@top21.sh> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
987 lines
38 KiB
Rust
987 lines
38 KiB
Rust
//! Files: the program itself, strings run as code, decoding filters and `eexec` sections.
|
|
//!
|
|
//! Filters decode as they are read, so a filter made over `currentfile` before the operator
|
|
//! that consumes it (`currentfile /ASCII85Decode filter … image`) reads the data that follows
|
|
//! that operator. A filter takes from the file beneath only what its encoding needs: bytes a
|
|
//! decompressor fetched past its end are pushed back, so the program carries on after the data.
|
|
|
|
use std::collections::VecDeque;
|
|
use std::rc::Rc;
|
|
|
|
/// The most bytes one filter may decode to, and one reusable stream may hold.
|
|
const MAX_DECODED: usize = 256 << 20;
|
|
/// How deep files may sit on one another.
|
|
const MAX_CHAIN: usize = 16;
|
|
|
|
pub enum Dec {
|
|
Hex {
|
|
hi: Option<u8>,
|
|
},
|
|
A85 {
|
|
group: [u8; 5],
|
|
n: usize,
|
|
first: bool,
|
|
},
|
|
RunLength,
|
|
Lzw {
|
|
dec: Box<weezl::decode::Decoder>,
|
|
pending: Vec<u8>,
|
|
},
|
|
Flate {
|
|
state: Box<miniz_oxide::inflate::stream::InflateState>,
|
|
pending: Vec<u8>,
|
|
},
|
|
SubFile {
|
|
eod: Vec<u8>,
|
|
count: usize,
|
|
left: Option<usize>,
|
|
skip_on_pop: bool,
|
|
window: VecDeque<u8>,
|
|
},
|
|
/// JPEG data: only images take it (whole, as it is).
|
|
Dct,
|
|
}
|
|
|
|
pub enum Kind {
|
|
Bytes(Rc<Vec<u8>>),
|
|
/// A window onto a string's storage, read in place (`token`, executing a string).
|
|
Shared {
|
|
buf: Rc<std::cell::RefCell<Vec<u8>>>,
|
|
off: usize,
|
|
len: usize,
|
|
},
|
|
/// Decrypting `eexec` over another file. `hex` is decided by the first four bytes.
|
|
Eexec {
|
|
up: usize,
|
|
r: u16,
|
|
hex: Option<bool>,
|
|
skipped: u8,
|
|
},
|
|
Filter {
|
|
up: usize,
|
|
dec: Dec,
|
|
out: VecDeque<u8>,
|
|
done: bool,
|
|
produced: usize,
|
|
},
|
|
}
|
|
|
|
pub struct Source {
|
|
pub kind: Kind,
|
|
pub pos: usize,
|
|
pub closed: bool,
|
|
/// Raw JPEG data for a DCTDecode filter, once taken.
|
|
pub dct: Option<Rc<Vec<u8>>>,
|
|
/// The last byte given, and bytes pushed back (read again first; the last pushed first).
|
|
pub last: Option<u8>,
|
|
pub back: Vec<u8>,
|
|
/// A PNG or TIFF predictor under Flate or LZW.
|
|
pub predictor: Option<Predictor>,
|
|
}
|
|
|
|
impl Source {
|
|
pub fn bytes(data: Vec<u8>) -> Self {
|
|
Self::shared(Rc::new(data))
|
|
}
|
|
|
|
pub fn shared(data: Rc<Vec<u8>>) -> Self {
|
|
Self::of(Kind::Bytes(data))
|
|
}
|
|
|
|
/// A string read in place, without copying it.
|
|
pub fn string(s: &crate::read::obj::PStr) -> Self {
|
|
Self::of(Kind::Shared { buf: s.buf.clone(), off: s.off, len: s.len })
|
|
}
|
|
|
|
pub fn eexec(up: usize) -> Self {
|
|
Self::of(Kind::Eexec { up, r: 55665, hex: None, skipped: 0 })
|
|
}
|
|
|
|
fn of(kind: Kind) -> Self {
|
|
Source { kind, pos: 0, closed: false, dct: None, last: None, back: Vec::new(), predictor: None }
|
|
}
|
|
}
|
|
|
|
fn is_ws(c: u8) -> bool {
|
|
matches!(c, 0 | b'\t' | b'\n' | 0x0c | b'\r' | b' ')
|
|
}
|
|
|
|
fn hex_val(c: u8) -> Option<u8> {
|
|
match c {
|
|
b'0'..=b'9' => Some(c - b'0'),
|
|
b'a'..=b'f' => Some(c - b'a' + 10),
|
|
b'A'..=b'F' => Some(c - b'A' + 10),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct FilterParams {
|
|
pub early_change: bool,
|
|
pub predictor: i64,
|
|
pub colors: i64,
|
|
pub bpc: i64,
|
|
pub columns: i64,
|
|
pub eod_count: usize,
|
|
pub eod_string: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Predictor {
|
|
kind: i64,
|
|
bpp: usize,
|
|
row: usize,
|
|
prev: Vec<u8>,
|
|
cur: Vec<u8>,
|
|
}
|
|
|
|
/// The sources, read through one another (`eexec` over the program, filters over both).
|
|
pub struct Sources {
|
|
pub list: Vec<Source>,
|
|
/// Bytes read so far, at every level (a filter's input and its output both count): the
|
|
/// interpreter charges them as work.
|
|
pub read: u64,
|
|
/// The most bytes that may be read; past it every file is at its end and `exhausted` is set.
|
|
pub max_read: u64,
|
|
pub exhausted: bool,
|
|
}
|
|
|
|
impl Default for Sources {
|
|
fn default() -> Self {
|
|
Sources { list: Vec::new(), read: 0, max_read: u64::MAX, exhausted: false }
|
|
}
|
|
}
|
|
|
|
impl Sources {
|
|
pub fn add(&mut self, s: Source) -> usize {
|
|
self.list.push(s);
|
|
self.list.len() - 1
|
|
}
|
|
|
|
pub fn is_subfile_filter(&self, id: usize) -> bool {
|
|
self.list.get(id).is_some_and(|source| matches!(&source.kind, Kind::Filter { dec: Dec::SubFile { skip_on_pop: true, .. }, .. }))
|
|
}
|
|
|
|
pub fn read(&mut self, id: usize) -> Option<u8> {
|
|
self.read_depth(id, 0)
|
|
}
|
|
|
|
fn read_depth(&mut self, id: usize, depth: usize) -> Option<u8> {
|
|
if self.read >= self.max_read {
|
|
self.exhausted = true;
|
|
return None;
|
|
}
|
|
let c = self.read_inner(id, depth);
|
|
if c.is_some() {
|
|
self.read += 1;
|
|
}
|
|
c
|
|
}
|
|
|
|
fn read_inner(&mut self, id: usize, depth: usize) -> Option<u8> {
|
|
if depth > MAX_CHAIN {
|
|
return None;
|
|
}
|
|
let src = self.list.get_mut(id)?;
|
|
if src.closed {
|
|
return None;
|
|
}
|
|
if let Some(c) = src.back.pop() {
|
|
src.last = Some(c);
|
|
return Some(c);
|
|
}
|
|
let c = match &mut src.kind {
|
|
Kind::Bytes(data) => {
|
|
let c = data.get(src.pos).copied();
|
|
if c.is_some() {
|
|
src.pos += 1;
|
|
}
|
|
return c;
|
|
}
|
|
Kind::Shared { buf, off, len } => {
|
|
let c = if src.pos < *len { off.checked_add(src.pos).and_then(|i| buf.borrow().get(i).copied()) } else { None };
|
|
if c.is_some() {
|
|
src.pos += 1;
|
|
}
|
|
return c;
|
|
}
|
|
Kind::Eexec { .. } => self.read_eexec(id, depth),
|
|
Kind::Filter { .. } => self.read_filter(id, depth),
|
|
};
|
|
if let Some(s) = self.list.get_mut(id) {
|
|
s.last = c;
|
|
if c.is_some() {
|
|
s.pos += 1;
|
|
}
|
|
}
|
|
c
|
|
}
|
|
|
|
fn read_eexec(&mut self, id: usize, depth: usize) -> Option<u8> {
|
|
let Some(Source { kind: Kind::Eexec { up, hex, .. }, .. }) = self.list.get(id) else { return None };
|
|
let (up, hex) = (*up, *hex);
|
|
let hex = match hex {
|
|
Some(h) => h,
|
|
None => {
|
|
let h = self.leading_hex(up, depth)?;
|
|
if let Some(Source { kind: Kind::Eexec { hex, .. }, .. }) = self.list.get_mut(id) {
|
|
*hex = Some(h);
|
|
}
|
|
h
|
|
}
|
|
};
|
|
loop {
|
|
let cipher = if hex {
|
|
let mut digits = [0u8; 2];
|
|
let mut n = 0;
|
|
while n < 2 {
|
|
let c = self.read_depth(up, depth + 1)?;
|
|
if is_ws(c) {
|
|
continue;
|
|
}
|
|
digits[n] = hex_val(c)?;
|
|
n += 1;
|
|
}
|
|
digits[0] << 4 | digits[1]
|
|
} else {
|
|
self.read_depth(up, depth + 1)?
|
|
};
|
|
let Some(Source { kind: Kind::Eexec { r, skipped, .. }, .. }) = self.list.get_mut(id) else { return None };
|
|
let plain = cipher ^ (*r >> 8) as u8;
|
|
*r = (u16::from(cipher).wrapping_add(*r)).wrapping_mul(52845).wrapping_add(22719);
|
|
// The first four plain bytes are random.
|
|
if *skipped < 4 {
|
|
*skipped += 1;
|
|
continue;
|
|
}
|
|
return Some(plain);
|
|
}
|
|
}
|
|
|
|
/// Whether the next four bytes after whitespace are hex digits (the whitespace is consumed
|
|
/// when they are; the four bytes are pushed back either way).
|
|
fn leading_hex(&mut self, up: usize, depth: usize) -> Option<bool> {
|
|
let mut skipped = Vec::new();
|
|
let first = loop {
|
|
let c = self.read_depth(up, depth + 1)?;
|
|
if !is_ws(c) {
|
|
break c;
|
|
}
|
|
skipped.push(c);
|
|
};
|
|
let mut four = vec![first];
|
|
while four.len() < 4 {
|
|
match self.read_depth(up, depth + 1) {
|
|
Some(c) => four.push(c),
|
|
None => break,
|
|
}
|
|
}
|
|
let hex = four.len() == 4 && four.iter().all(|c| hex_val(*c).is_some());
|
|
self.unread_many(up, &four);
|
|
if !hex {
|
|
self.unread_many(up, &skipped);
|
|
}
|
|
Some(hex)
|
|
}
|
|
|
|
/// Push bytes back so they are read again, in order.
|
|
pub fn unread_many(&mut self, id: usize, bytes: &[u8]) {
|
|
let Some(src) = self.list.get_mut(id) else { return };
|
|
match src.kind {
|
|
Kind::Bytes(_) | Kind::Shared { .. } if src.back.is_empty() => src.pos = src.pos.saturating_sub(bytes.len()),
|
|
_ => src.back.extend(bytes.iter().rev()),
|
|
}
|
|
}
|
|
|
|
/// Step back over the byte just read (the scanner's look-ahead).
|
|
pub fn unread(&mut self, id: usize) {
|
|
let Some(src) = self.list.get_mut(id) else { return };
|
|
match src.kind {
|
|
Kind::Bytes(_) | Kind::Shared { .. } if src.back.is_empty() => src.pos = src.pos.saturating_sub(1),
|
|
_ => {
|
|
if let Some(c) = src.last {
|
|
src.back.push(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn peek(&mut self, id: usize) -> Option<u8> {
|
|
let c = self.read(id)?;
|
|
self.unread(id);
|
|
Some(c)
|
|
}
|
|
|
|
/// Build a decoding source without retaining it, so the interpreter can charge its memory
|
|
/// budget before registering the source.
|
|
pub fn filter(&mut self, up: usize, name: &[u8], p: &FilterParams, max_owned: usize) -> Result<(Source, usize), String> {
|
|
let dec = match name {
|
|
b"ASCIIHexDecode" | b"AHx" => Dec::Hex { hi: None },
|
|
b"ASCII85Decode" | b"A85" => Dec::A85 { group: [0; 5], n: 0, first: true },
|
|
b"RunLengthDecode" | b"RL" => Dec::RunLength,
|
|
b"LZWDecode" | b"LZW" => {
|
|
let dec = if p.early_change {
|
|
weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
|
|
} else {
|
|
weezl::decode::Decoder::new(weezl::BitOrder::Msb, 8)
|
|
};
|
|
Dec::Lzw { dec: Box::new(dec), pending: Vec::new() }
|
|
}
|
|
b"FlateDecode" | b"Fl" => {
|
|
Dec::Flate { state: miniz_oxide::inflate::stream::InflateState::new_boxed(miniz_oxide::DataFormat::Zlib), pending: Vec::new() }
|
|
}
|
|
b"DCTDecode" | b"DCT" => Dec::Dct,
|
|
b"SubFileDecode" => Dec::SubFile {
|
|
eod: p.eod_string.clone(),
|
|
count: p.eod_count,
|
|
// A byte count; with no count and no end string, everything passes through.
|
|
left: (p.eod_string.is_empty() && p.eod_count > 0).then_some(p.eod_count),
|
|
skip_on_pop: true,
|
|
window: VecDeque::new(),
|
|
},
|
|
b"NullEncode" => Dec::SubFile { eod: Vec::new(), count: 0, left: None, skip_on_pop: false, window: VecDeque::new() },
|
|
b"ReusableStreamDecode" => {
|
|
// Reads its data when made, and can be read again (`resetfile`, positions).
|
|
let mut data = Vec::new();
|
|
while let Some(c) = self.read(up) {
|
|
let next = data.len().saturating_add(1);
|
|
if next > MAX_DECODED || next > max_owned {
|
|
return Err("out of memory".into());
|
|
}
|
|
if next > data.capacity() {
|
|
let target = next.saturating_add(4095).min(max_owned).min(MAX_DECODED);
|
|
data.try_reserve_exact(target.saturating_sub(data.len())).map_err(|_| "out of memory")?;
|
|
}
|
|
data.push(c);
|
|
}
|
|
// The capacity is retained by the reusable byte source, so charge it rather
|
|
// than only the initialized bytes.
|
|
let bytes = data.capacity();
|
|
return Ok((Source::bytes(data), bytes));
|
|
}
|
|
_ => return Err(format!("unsupported filter {}", String::from_utf8_lossy(name))),
|
|
};
|
|
let mut s = Source::of(Kind::Filter { up, dec, out: VecDeque::new(), done: false, produced: 0 });
|
|
if p.predictor > 1 && matches!(name, b"FlateDecode" | b"Fl" | b"LZWDecode" | b"LZW") {
|
|
let colors = usize::try_from(p.colors.clamp(1, 32)).unwrap_or(1);
|
|
let bpc = usize::try_from(p.bpc.clamp(1, 16)).unwrap_or(8);
|
|
let columns = usize::try_from(p.columns.clamp(1, 1 << 20)).unwrap_or(1);
|
|
let row = (colors * bpc * columns).div_ceil(8);
|
|
if row > max_owned {
|
|
return Err("out of memory".into());
|
|
}
|
|
let mut prev = Vec::new();
|
|
prev.try_reserve_exact(row).map_err(|_| "out of memory")?;
|
|
prev.resize(row, 0);
|
|
s.predictor = Some(Predictor { kind: p.predictor, bpp: (colors * bpc).div_ceil(8).max(1), row, prev, cur: Vec::new() });
|
|
return Ok((s, row));
|
|
}
|
|
Ok((s, 0))
|
|
}
|
|
|
|
fn read_filter(&mut self, id: usize, depth: usize) -> Option<u8> {
|
|
loop {
|
|
let src = self.list.get_mut(id)?;
|
|
let Kind::Filter { out, done, .. } = &mut src.kind else { return None };
|
|
if let Some(c) = out.pop_front() {
|
|
return Some(c);
|
|
}
|
|
if *done {
|
|
return None;
|
|
}
|
|
if !self.fill(id, depth)
|
|
&& let Some(Source { kind: Kind::Filter { done, .. }, .. }) = self.list.get_mut(id)
|
|
{
|
|
// What the last step produced is still read first.
|
|
*done = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Decode some more of filter `id`; false once its data has ended.
|
|
#[allow(clippy::cognitive_complexity, reason = "one streaming decoder state machine preserves filter read-ahead and predictor ordering")]
|
|
fn fill(&mut self, id: usize, depth: usize) -> bool {
|
|
let Some(Source { kind: Kind::Filter { up, dec, .. }, .. }) = self.list.get(id) else { return false };
|
|
let up = *up;
|
|
let mut produced: Vec<u8> = Vec::new();
|
|
let mut more = true;
|
|
match dec {
|
|
Dec::Dct => more = false,
|
|
Dec::Hex { hi } => {
|
|
let mut hi = *hi;
|
|
while produced.is_empty() {
|
|
let Some(c) = self.read_depth(up, depth + 1) else {
|
|
more = false;
|
|
break;
|
|
};
|
|
if c == b'>' {
|
|
more = false;
|
|
break;
|
|
}
|
|
let Some(v) = hex_val(c) else { continue };
|
|
match hi.take() {
|
|
Some(h) => produced.push(h << 4 | v),
|
|
None => hi = Some(v),
|
|
}
|
|
}
|
|
if !more && let Some(h) = hi.take() {
|
|
produced.push(h << 4);
|
|
}
|
|
if let Some(Source { kind: Kind::Filter { dec: Dec::Hex { hi: slot }, .. }, .. }) = self.list.get_mut(id) {
|
|
*slot = hi;
|
|
}
|
|
}
|
|
Dec::A85 { group, n, first } => {
|
|
let (mut group, mut n, mut first) = (*group, *n, *first);
|
|
while produced.is_empty() {
|
|
let Some(c) = self.read_depth(up, depth + 1) else {
|
|
more = false;
|
|
break;
|
|
};
|
|
if is_ws(c) {
|
|
continue;
|
|
}
|
|
if c == b'<' && first {
|
|
// A leading `<~` is optional; `<` is itself a valid ASCII85 digit.
|
|
let next = self.read_depth(up, depth + 1);
|
|
if next == Some(b'~') {
|
|
first = false;
|
|
continue;
|
|
}
|
|
if next.is_some() {
|
|
self.unread(up);
|
|
}
|
|
}
|
|
first = false;
|
|
if c == b'~' {
|
|
let _ = self.read_depth(up, depth + 1);
|
|
more = false;
|
|
break;
|
|
}
|
|
if c == b'z' && n == 0 {
|
|
produced.extend([0; 4]);
|
|
continue;
|
|
}
|
|
if !(b'!'..=b'u').contains(&c) {
|
|
more = false;
|
|
break;
|
|
}
|
|
if let Some(slot) = group.get_mut(n) {
|
|
*slot = c - b'!';
|
|
}
|
|
n += 1;
|
|
if n == 5 {
|
|
let v = group.iter().fold(0u64, |a, &d| a * 85 + u64::from(d)).min(u64::from(u32::MAX));
|
|
produced.extend(u32::try_from(v).unwrap_or(u32::MAX).to_be_bytes());
|
|
n = 0;
|
|
}
|
|
}
|
|
if !more && n > 1 {
|
|
for slot in group.iter_mut().skip(n) {
|
|
*slot = 84;
|
|
}
|
|
let v = group.iter().fold(0u64, |a, &d| a * 85 + u64::from(d)).min(u64::from(u32::MAX));
|
|
produced.extend(u32::try_from(v).unwrap_or(u32::MAX).to_be_bytes().iter().take(n - 1));
|
|
n = 0;
|
|
}
|
|
if let Some(Source { kind: Kind::Filter { dec: Dec::A85 { group: g, n: nn, first: f }, .. }, .. }) = self.list.get_mut(id) {
|
|
*g = group;
|
|
*nn = n;
|
|
*f = first;
|
|
}
|
|
}
|
|
Dec::RunLength => match self.read_depth(up, depth + 1) {
|
|
None | Some(128) => more = false,
|
|
Some(len @ 0..=127) => {
|
|
for _ in 0..=len {
|
|
match self.read_depth(up, depth + 1) {
|
|
Some(c) => produced.push(c),
|
|
None => {
|
|
more = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(len) => match self.read_depth(up, depth + 1) {
|
|
Some(c) => produced.extend(std::iter::repeat_n(c, 257 - usize::from(len))),
|
|
None => more = false,
|
|
},
|
|
},
|
|
Dec::SubFile { eod, count, left, window, .. } => {
|
|
let (eod, mut count, mut left, mut window) = (eod.clone(), *count, *left, window.clone());
|
|
if let Some(n) = left.as_mut() {
|
|
if *n == 0 {
|
|
more = false;
|
|
} else if let Some(c) = self.read_depth(up, depth + 1) {
|
|
*n -= 1;
|
|
produced.push(c);
|
|
} else {
|
|
more = false;
|
|
}
|
|
} else if eod.is_empty() {
|
|
match self.read_depth(up, depth + 1) {
|
|
Some(c) => produced.push(c),
|
|
None => more = false,
|
|
}
|
|
} else {
|
|
while produced.is_empty() && more {
|
|
let Some(c) = self.read_depth(up, depth + 1) else {
|
|
produced.extend(window.drain(..));
|
|
more = false;
|
|
break;
|
|
};
|
|
window.push_back(c);
|
|
if window.len() >= eod.len() && window.iter().skip(window.len() - eod.len()).eq(eod.iter()) {
|
|
if count == 0 {
|
|
let keep = window.len() - eod.len();
|
|
produced.extend(window.drain(..keep));
|
|
window.clear();
|
|
more = false;
|
|
break;
|
|
}
|
|
count -= 1;
|
|
if count == 0 {
|
|
// Positive EODCount includes the Nth terminating occurrence.
|
|
produced.extend(window.drain(..));
|
|
more = false;
|
|
break;
|
|
}
|
|
}
|
|
while window.len() > eod.len() {
|
|
if let Some(b) = window.pop_front() {
|
|
produced.push(b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(Source { kind: Kind::Filter { dec: Dec::SubFile { count: c, left: l, window: w, .. }, .. }, .. }) = self.list.get_mut(id)
|
|
{
|
|
*c = count;
|
|
*l = left;
|
|
*w = window;
|
|
}
|
|
}
|
|
Dec::Flate { .. } | Dec::Lzw { .. } => {
|
|
// Feed what is pending, or a chunk from beneath; keep what wasn't used.
|
|
let mut input = match self.list.get_mut(id).map(|s| &mut s.kind) {
|
|
Some(Kind::Filter { dec: Dec::Flate { pending, .. } | Dec::Lzw { pending, .. }, .. }) => std::mem::take(pending),
|
|
_ => Vec::new(),
|
|
};
|
|
let mut eof = false;
|
|
if input.is_empty() {
|
|
while input.len() < 4096 {
|
|
match self.read_depth(up, depth + 1) {
|
|
Some(c) => input.push(c),
|
|
None => {
|
|
eof = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let mut buf = vec![0u8; 64 * 1024];
|
|
let (used, written, finished) = match self.list.get_mut(id).map(|s| &mut s.kind) {
|
|
Some(Kind::Filter { dec: Dec::Flate { state, .. }, .. }) => {
|
|
let r = miniz_oxide::inflate::stream::inflate(state, &input, &mut buf, miniz_oxide::MZFlush::None);
|
|
let finished = matches!(r.status, Ok(miniz_oxide::MZStatus::StreamEnd) | Err(_));
|
|
(r.bytes_consumed, r.bytes_written, finished)
|
|
}
|
|
Some(Kind::Filter { dec: Dec::Lzw { dec, .. }, .. }) => {
|
|
// One byte at a time: the decoder must not read past its end code.
|
|
let (mut used, mut written, mut finished) = (0, 0, false);
|
|
while used < input.len() && written + 4096 < buf.len() {
|
|
let r = dec.decode_bytes(input.get(used..used + 1).unwrap_or(&[]), buf.get_mut(written..).unwrap_or(&mut []));
|
|
used += r.consumed_in;
|
|
written += r.consumed_out;
|
|
if matches!(r.status, Ok(weezl::LzwStatus::Done) | Err(_)) {
|
|
finished = true;
|
|
break;
|
|
}
|
|
if r.consumed_in == 0 && r.consumed_out == 0 {
|
|
break;
|
|
}
|
|
}
|
|
(used, written, finished)
|
|
}
|
|
_ => (0, 0, true),
|
|
};
|
|
produced.extend_from_slice(buf.get(..written).unwrap_or(&[]));
|
|
let rest = input.get(used..).unwrap_or(&[]).to_vec();
|
|
if finished {
|
|
// The stream ended inside this chunk: what follows belongs to the file.
|
|
self.unread_many(up, &rest);
|
|
more = false;
|
|
} else if used == 0 && written == 0 && (eof || input.is_empty()) {
|
|
more = false;
|
|
} else if let Some(Source { kind: Kind::Filter { dec: Dec::Flate { pending, .. } | Dec::Lzw { pending, .. }, .. }, .. }) =
|
|
self.list.get_mut(id)
|
|
{
|
|
*pending = rest;
|
|
}
|
|
}
|
|
}
|
|
// Predictors work on whole rows.
|
|
let predicted = match self.list.get_mut(id).and_then(|s| s.predictor.as_mut()) {
|
|
Some(pr) => unpredict(pr, &produced, !more),
|
|
None => produced,
|
|
};
|
|
let Some(Source { kind: Kind::Filter { out, produced: total, .. }, .. }) = self.list.get_mut(id) else { return false };
|
|
*total = total.saturating_add(predicted.len());
|
|
if *total > MAX_DECODED {
|
|
return false;
|
|
}
|
|
out.extend(predicted);
|
|
more
|
|
}
|
|
|
|
/// The JPEG a DCTDecode filter reads: through its end marker when it sits on the program
|
|
/// itself, all of the filter beneath otherwise.
|
|
pub fn dct(&mut self, id: usize) -> Option<Rc<Vec<u8>>> {
|
|
let src = self.list.get(id)?;
|
|
if let Some(d) = &src.dct {
|
|
return Some(d.clone());
|
|
}
|
|
let Kind::Filter { up, dec: Dec::Dct, .. } = &src.kind else { return None };
|
|
let up = *up;
|
|
let direct = match self.list.get(up) {
|
|
Some(Source { kind: Kind::Bytes(data), pos, back, .. }) if back.is_empty() => Some((data.clone(), *pos)),
|
|
_ => None,
|
|
};
|
|
let raw = match direct {
|
|
Some((data, pos)) => {
|
|
let rest = data.get(pos..).unwrap_or(&[]);
|
|
let end = jpeg_end(rest).unwrap_or(rest.len());
|
|
if let Some(s) = self.list.get_mut(up) {
|
|
s.pos = s.pos.saturating_add(end);
|
|
}
|
|
rest.get(..end).unwrap_or(&[]).to_vec()
|
|
}
|
|
None => {
|
|
let mut raw = Vec::new();
|
|
while let Some(c) = self.read(up) {
|
|
raw.push(c);
|
|
if raw.len() > MAX_DECODED {
|
|
break;
|
|
}
|
|
}
|
|
raw
|
|
}
|
|
};
|
|
let raw = Rc::new(raw);
|
|
if let Some(s) = self.list.get_mut(id) {
|
|
s.dct = Some(raw.clone());
|
|
}
|
|
Some(raw)
|
|
}
|
|
|
|
pub fn is_filter(&self, id: usize) -> bool {
|
|
matches!(self.list.get(id).map(|s| &s.kind), Some(Kind::Filter { .. }))
|
|
}
|
|
|
|
/// Read a filter to its end marker, so the file beneath carries on after the data.
|
|
pub fn drain(&mut self, id: usize) {
|
|
let mut n = 0usize;
|
|
while n < MAX_DECODED && self.read(id).is_some() {
|
|
n += 1;
|
|
}
|
|
}
|
|
|
|
pub fn close(&mut self, id: usize) {
|
|
if let Some(s) = self.list.get_mut(id) {
|
|
s.closed = true;
|
|
}
|
|
}
|
|
|
|
pub fn reset(&mut self, id: usize) {
|
|
if let Some(s) = self.list.get_mut(id)
|
|
&& matches!(s.kind, Kind::Bytes(_) | Kind::Shared { .. })
|
|
{
|
|
s.pos = 0;
|
|
s.back.clear();
|
|
s.closed = false;
|
|
}
|
|
}
|
|
|
|
pub fn position(&self, id: usize) -> Option<usize> {
|
|
let s = self.list.get(id)?;
|
|
Some(s.pos.saturating_sub(s.back.len()))
|
|
}
|
|
|
|
pub fn set_position(&mut self, id: usize, pos: usize) -> bool {
|
|
match self.list.get_mut(id) {
|
|
Some(s) if matches!(&s.kind, Kind::Bytes(d) if pos <= d.len()) || matches!(&s.kind, Kind::Shared { len, .. } if pos <= *len) => {
|
|
s.pos = pos;
|
|
s.back.clear();
|
|
s.closed = false;
|
|
true
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn available(&self, id: usize) -> i64 {
|
|
match self.list.get(id) {
|
|
Some(s) if s.closed => -1,
|
|
Some(Source { kind: Kind::Bytes(d), pos, back, .. }) => i64::try_from(d.len().saturating_sub(*pos) + back.len()).unwrap_or(i64::MAX),
|
|
Some(Source { kind: Kind::Shared { len, .. }, pos, back, .. }) => {
|
|
i64::try_from(len.saturating_sub(*pos) + back.len()).unwrap_or(i64::MAX)
|
|
}
|
|
Some(_) => 1,
|
|
None => -1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Undo a predictor on newly decoded bytes (`last`: no more will come).
|
|
fn unpredict(p: &mut Predictor, data: &[u8], last: bool) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
if p.kind == 2 {
|
|
// TIFF: 8-bit horizontal differencing.
|
|
for &b in data {
|
|
p.cur.push(b);
|
|
if p.cur.len() == p.row {
|
|
for i in p.bpp..p.row {
|
|
let prev = p.cur.get(i - p.bpp).copied().unwrap_or(0);
|
|
if let Some(c) = p.cur.get_mut(i) {
|
|
*c = c.wrapping_add(prev);
|
|
}
|
|
}
|
|
out.append(&mut p.cur);
|
|
}
|
|
}
|
|
if last {
|
|
out.append(&mut p.cur);
|
|
}
|
|
return out;
|
|
}
|
|
// PNG: a filter byte before each row.
|
|
for &b in data {
|
|
p.cur.push(b);
|
|
if p.cur.len() == p.row + 1 {
|
|
let kind = p.cur.first().copied().unwrap_or(0);
|
|
let mut row: Vec<u8> = p.cur.get(1..).unwrap_or(&[]).to_vec();
|
|
for i in 0..row.len() {
|
|
let a = if i >= p.bpp { row.get(i - p.bpp).copied().unwrap_or(0) } else { 0 };
|
|
let up = p.prev.get(i).copied().unwrap_or(0);
|
|
let c = if i >= p.bpp { p.prev.get(i - p.bpp).copied().unwrap_or(0) } else { 0 };
|
|
let add = match kind {
|
|
1 => a,
|
|
2 => up,
|
|
3 => ((u16::from(a) + u16::from(up)) / 2) as u8,
|
|
4 => {
|
|
let pp = i16::from(a) + i16::from(up) - i16::from(c);
|
|
let (pa, pb, pc) = ((pp - i16::from(a)).abs(), (pp - i16::from(up)).abs(), (pp - i16::from(c)).abs());
|
|
if pa <= pb && pa <= pc {
|
|
a
|
|
} else if pb <= pc {
|
|
up
|
|
} else {
|
|
c
|
|
}
|
|
}
|
|
_ => 0,
|
|
};
|
|
if let Some(v) = row.get_mut(i) {
|
|
*v = v.wrapping_add(add);
|
|
}
|
|
}
|
|
out.extend_from_slice(&row);
|
|
p.prev = row;
|
|
p.cur.clear();
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The length of the JPEG at the start of `d` (through its EOI marker).
|
|
fn jpeg_end(d: &[u8]) -> Option<usize> {
|
|
if d.get(..2)? != [0xFF, 0xD8] {
|
|
return None;
|
|
}
|
|
let mut i = 2;
|
|
loop {
|
|
while d.get(i)? == &0xFF && d.get(i + 1)? == &0xFF {
|
|
i += 1;
|
|
}
|
|
if d.get(i)? != &0xFF {
|
|
return None;
|
|
}
|
|
let m = *d.get(i + 1)?;
|
|
i += 2;
|
|
match m {
|
|
0xD9 => return Some(i),
|
|
0x01 | 0xD0..=0xD7 => continue,
|
|
_ => {
|
|
let len = usize::from(u16::from_be_bytes([*d.get(i)?, *d.get(i + 1)?]));
|
|
i = i.checked_add(len)?;
|
|
if m == 0xDA {
|
|
// Entropy-coded data: up to the next marker that isn't a stuffed byte or restart.
|
|
loop {
|
|
if d.get(i)? == &0xFF {
|
|
let n = *d.get(i + 1)?;
|
|
if n != 0 && !(0xD0..=0xD7).contains(&n) {
|
|
break;
|
|
}
|
|
i += 2;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn all(s: &mut Sources, id: usize) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
while let Some(c) = s.read(id) {
|
|
out.push(c);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn add_filter(s: &mut Sources, up: usize, name: &[u8], params: &FilterParams) -> usize {
|
|
let (source, _) = s.filter(up, name, params, usize::MAX).unwrap();
|
|
s.add(source)
|
|
}
|
|
|
|
#[test]
|
|
fn filter_owned_buffers_are_preflighted_against_memory_budget() {
|
|
let mut sources = Sources::default();
|
|
let id = sources.add(Source::bytes(Vec::new()));
|
|
let predictor = FilterParams { predictor: 12, colors: 32, bpc: 16, columns: 1 << 20, ..Default::default() };
|
|
assert!(matches!(sources.filter(id, b"FlateDecode", &predictor, 4096), Err(e) if e == "out of memory"));
|
|
|
|
let id = sources.add(Source::bytes(vec![b'x'; 10]));
|
|
assert!(matches!(sources.filter(id, b"ReusableStreamDecode", &FilterParams::default(), 9), Err(e) if e == "out of memory"));
|
|
let id = sources.add(Source::bytes(vec![b'x'; 10]));
|
|
let (_, owned) = sources.filter(id, b"ReusableStreamDecode", &FilterParams::default(), 10).unwrap();
|
|
assert_eq!(owned, 10);
|
|
}
|
|
|
|
#[test]
|
|
fn filters_stop_at_their_end_markers() {
|
|
let mut s = Sources::default();
|
|
let id = s.add(Source::bytes(b"48656c6c6f>rest".to_vec()));
|
|
let f = add_filter(&mut s, id, b"ASCIIHexDecode", &FilterParams::default());
|
|
assert_eq!(all(&mut s, f), b"Hello");
|
|
assert_eq!(all(&mut s, id), b"rest");
|
|
let id = s.add(Source::bytes(b"87cURD]i,\"Ebo80~>rest".to_vec()));
|
|
let f = add_filter(&mut s, id, b"ASCII85Decode", &FilterParams::default());
|
|
assert_eq!(all(&mut s, f), b"Hello World!");
|
|
assert_eq!(all(&mut s, id), b"rest");
|
|
let id = s.add(Source::bytes(b"<!!!!~>rest".to_vec()));
|
|
let f = add_filter(&mut s, id, b"ASCII85Decode", &FilterParams::default());
|
|
let value = 27u32 * 85u32.pow(4);
|
|
assert_eq!(all(&mut s, f), value.to_be_bytes());
|
|
assert_eq!(all(&mut s, id), b"rest");
|
|
let id = s.add(Source::bytes(vec![2, b'a', b'b', b'c', 254, b'x', 128, b'!']));
|
|
let f = add_filter(&mut s, id, b"RunLengthDecode", &FilterParams::default());
|
|
assert_eq!(all(&mut s, f), b"abcxxx");
|
|
assert_eq!(all(&mut s, id), b"!");
|
|
let id = s.add(Source::bytes(b"data%%EndBinary tail".to_vec()));
|
|
let p = FilterParams { eod_string: b"%%EndBinary".to_vec(), ..Default::default() };
|
|
let f = add_filter(&mut s, id, b"SubFileDecode", &p);
|
|
assert_eq!(all(&mut s, f), b"data");
|
|
assert_eq!(all(&mut s, id), b" tail");
|
|
let id = s.add(Source::bytes(b"aENDtailENDrest".to_vec()));
|
|
let p = FilterParams { eod_count: 1, eod_string: b"END".to_vec(), ..Default::default() };
|
|
let f = add_filter(&mut s, id, b"SubFileDecode", &p);
|
|
assert_eq!(all(&mut s, f), b"aEND");
|
|
assert_eq!(all(&mut s, id), b"tailENDrest");
|
|
}
|
|
|
|
fn ascii85(data: &[u8]) -> String {
|
|
let mut out = String::new();
|
|
for chunk in data.chunks(4) {
|
|
let mut b = [0u8; 4];
|
|
b[..chunk.len()].copy_from_slice(chunk);
|
|
let mut v = u32::from_be_bytes(b);
|
|
let mut digits = [0u8; 5];
|
|
for d in digits.iter_mut().rev() {
|
|
*d = (v % 85) as u8 + b'!';
|
|
v /= 85;
|
|
}
|
|
out.push_str(std::str::from_utf8(&digits[..chunk.len() + 1]).unwrap());
|
|
}
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn flate_and_lzw_read_only_their_streams() {
|
|
let mut z = miniz_oxide::deflate::compress_to_vec_zlib(&b"abcabcabc".repeat(2000), 6);
|
|
z.extend_from_slice(b"tail");
|
|
let mut s = Sources::default();
|
|
let id = s.add(Source::bytes(z));
|
|
let f = add_filter(&mut s, id, b"FlateDecode", &FilterParams::default());
|
|
assert_eq!(all(&mut s, f), b"abcabcabc".repeat(2000));
|
|
assert_eq!(all(&mut s, id), b"tail");
|
|
// Flate over ASCII85 over the program: bytes pushed back return to the filter beneath.
|
|
let z = miniz_oxide::deflate::compress_to_vec_zlib(b"xyz", 6);
|
|
let mut s = Sources::default();
|
|
let id = s.add(Source::bytes(format!("{}~> after", ascii85(&z)).into_bytes()));
|
|
let a = add_filter(&mut s, id, b"ASCII85Decode", &FilterParams::default());
|
|
let f = add_filter(&mut s, a, b"FlateDecode", &FilterParams::default());
|
|
assert_eq!(all(&mut s, f), b"xyz");
|
|
assert_eq!(all(&mut s, id), b" after");
|
|
let mut enc = weezl::encode::Encoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8);
|
|
let mut l = enc.encode(b"lzw lzw lzw").unwrap();
|
|
l.extend_from_slice(b"!");
|
|
let mut s = Sources::default();
|
|
let id = s.add(Source::bytes(l));
|
|
let f = add_filter(&mut s, id, b"LZWDecode", &FilterParams { early_change: true, ..Default::default() });
|
|
assert_eq!(all(&mut s, f), b"lzw lzw lzw");
|
|
assert_eq!(all(&mut s, id), b"!");
|
|
}
|
|
|
|
#[test]
|
|
fn eexec_decrypts_binary_and_hex() {
|
|
let encrypt = |plain: &[u8]| {
|
|
let mut r: u16 = 55665;
|
|
let mut out = Vec::new();
|
|
for &p in [0u8, 0, 0, 0].iter().chain(plain) {
|
|
let c = p ^ (r >> 8) as u8;
|
|
r = (u16::from(c).wrapping_add(r)).wrapping_mul(52845).wrapping_add(22719);
|
|
out.push(c);
|
|
}
|
|
out
|
|
};
|
|
let enc = encrypt(b"hello");
|
|
for hex in [false, true] {
|
|
let data = if hex { enc.iter().map(|b| format!("{b:02x}")).collect::<String>().into_bytes() } else { enc.clone() };
|
|
let mut s = Sources::default();
|
|
let up = s.add(Source::bytes(data));
|
|
let e = s.add(Source::eexec(up));
|
|
assert_eq!(s.peek(e), Some(b'h'), "look-ahead inside the section");
|
|
let got: Vec<u8> = (0..5).filter_map(|_| s.read(e)).collect();
|
|
assert_eq!(got, b"hello", "hex {hex}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn broken_filter_data_ends_the_filter_not_the_program() {
|
|
for (name, data) in [
|
|
(&b"ASCIIHexDecode"[..], &b"zz>"[..]),
|
|
(b"ASCII85Decode", b"\x7f~>"),
|
|
(b"FlateDecode", b"\x78\x9c\xff\xff"),
|
|
(b"RunLengthDecode", b"\x05a"),
|
|
(b"LZWDecode", b"\xff\xff\xff"),
|
|
] {
|
|
let mut s = Sources::default();
|
|
let id = s.add(Source::bytes(data.to_vec()));
|
|
let f = add_filter(&mut s, id, name, &FilterParams::default());
|
|
let _ = all(&mut s, f);
|
|
}
|
|
let mut s = Sources::default();
|
|
let id = s.add(Source::bytes(b"\xff\xd8\xff".to_vec()));
|
|
let f = add_filter(&mut s, id, b"DCTDecode", &FilterParams::default());
|
|
assert!(s.dct(f).is_some());
|
|
}
|
|
}
|