Skip to main content

zpdf_parser/
filters.rs

1use zpdf_core::{Error, ParseLimits, PdfDict, PdfName, PdfObject, Result};
2
3/// Tracks cumulative decoded bytes across a filter chain to prevent decompression bombs.
4/// Each filter in the chain consumes budget; once exhausted, decoding fails.
5pub(crate) struct DecodeBudget {
6    remaining: u64,
7    total_consumed: u64,
8}
9
10impl DecodeBudget {
11    pub(crate) fn new(limit: u64) -> Self {
12        Self {
13            remaining: limit,
14            total_consumed: 0,
15        }
16    }
17
18    /// Reserve bytes for output. Returns error if budget exhausted.
19    pub(crate) fn reserve(&mut self, bytes: u64) -> Result<()> {
20        if bytes > self.remaining {
21            return Err(Error::StreamDecode(format!(
22                "decoded stream exceeds budget: {} bytes already consumed, {} more requested, {} limit",
23                self.total_consumed, bytes, self.total_consumed.saturating_add(self.remaining)
24            )));
25        }
26        self.remaining = self.remaining.saturating_sub(bytes);
27        self.total_consumed = self.total_consumed.saturating_add(bytes);
28        Ok(())
29    }
30}
31
32/// Decode a PDF stream through its filter chain with explicit limits.
33///
34/// This is the primary implementation that enforces cumulative budget tracking
35/// to prevent decompression bombs (H1 security fix).
36pub fn decode_stream_with_limits(
37    data: &[u8],
38    dict: &PdfDict,
39    limits: &ParseLimits,
40) -> Result<Vec<u8>> {
41    let filters = match dict.get("Filter") {
42        Some(PdfObject::Name(n)) => vec![n.clone()],
43        Some(PdfObject::Array(arr)) => arr
44            .iter()
45            .map(|obj| match obj {
46                PdfObject::Name(n) => Ok(n.clone()),
47                _ => Err(Error::TypeMismatch {
48                    expected: "Name",
49                    actual: obj.type_name(),
50                }),
51            })
52            .collect::<Result<Vec<_>>>()?,
53        Some(_) => {
54            return Err(Error::TypeMismatch {
55                expected: "Name or Array",
56                actual: "other",
57            })
58        }
59        None => {
60            if data.len() as u64 > limits.max_decoded_stream_bytes {
61                return Err(Error::StreamSizeLimit(limits.max_decoded_stream_bytes));
62            }
63            return Ok(data.to_vec());
64        }
65    };
66
67    let decode_parms = extract_decode_parms(dict, filters.len());
68
69    // H1 Fix: Use cumulative budget from ParseLimits instead of per-filter 1 GiB cap
70    let mut budget = DecodeBudget::new(limits.max_decoded_stream_bytes);
71
72    // H1 Fix: Process filter chain properly, using previous output as next input
73    let mut current = data.to_vec();
74    for (i, filter) in filters.iter().enumerate() {
75        let params = decode_parms[i].as_ref();
76
77        // Apply filter to current data
78        let decoded = apply_filter(filter, &current, params, limits, &mut budget)?;
79
80        // Apply predictor if specified
81        current = if let Some(p) = params {
82            let predictor = p.get_i64("Predictor").unwrap_or(1);
83            if predictor == 2 || predictor >= 10 {
84                apply_predictor(&decoded, p, &mut budget)?
85            } else {
86                // Predictor 1 (and unknown values) is a no-op. Keep ownership of
87                // the filter output instead of cloning the entire stream.
88                decoded
89            }
90        } else {
91            decoded
92        };
93    }
94
95    Ok(current)
96}
97
98/// Decode a PDF stream through its filter chain using default limits.
99///
100/// **Temporary backward compatibility wrapper** - allows existing code to compile
101/// while we migrate call sites to pass explicit ParseLimits. This uses default
102/// limits (2 GiB decoded budget), which provides DoS protection but isn't customizable.
103///
104/// New code should use `decode_stream_with_limits` and pass the active ParseLimits.
105/// This wrapper will be removed once all call sites are migrated.
106pub fn decode_stream(data: &[u8], dict: &PdfDict) -> Result<Vec<u8>> {
107    decode_stream_with_limits(data, dict, &ParseLimits::default())
108}
109
110fn extract_decode_parms(dict: &PdfDict, filter_count: usize) -> Vec<Option<PdfDict>> {
111    match dict.get("DecodeParms").or_else(|| dict.get("DP")) {
112        Some(PdfObject::Dict(d)) => {
113            let mut v = vec![None; filter_count];
114            if !v.is_empty() {
115                v[0] = Some(d.clone());
116            }
117            v
118        }
119        Some(PdfObject::Array(arr)) => arr
120            .iter()
121            .map(|obj| match obj {
122                PdfObject::Dict(d) => Some(d.clone()),
123                _ => None,
124            })
125            .chain(std::iter::repeat(None))
126            .take(filter_count)
127            .collect(),
128        _ => vec![None; filter_count],
129    }
130}
131
132fn apply_predictor(data: &[u8], params: &PdfDict, budget: &mut DecodeBudget) -> Result<Vec<u8>> {
133    let predictor = params.get_i64("Predictor").unwrap_or(1) as u32;
134    if predictor == 1 {
135        return Ok(data.to_vec());
136    }
137
138    // H1 Fix: Reserve budget for predictor output before allocation
139    budget.reserve(data.len() as u64)?;
140
141    // Validate parameters with reasonable PDF limits to prevent overflow attacks.
142    // PDF spec typically uses modest values, but we allow generous bounds.
143    const MAX_COLORS: usize = 256; // PDF spec typically ≤32
144    const MAX_BPC: usize = 32; // PDF spec: 1,2,4,8,12,16,24,32
145    const MAX_COLUMNS: usize = 1 << 16; // 64K columns is generous
146
147    let colors = params
148        .get_i64("Colors")
149        .unwrap_or(1)
150        .clamp(1, MAX_COLORS as i64) as usize;
151    let bpc = params
152        .get_i64("BitsPerComponent")
153        .unwrap_or(8)
154        .clamp(1, MAX_BPC as i64) as usize;
155    let columns = params
156        .get_i64("Columns")
157        .unwrap_or(1)
158        .clamp(1, MAX_COLUMNS as i64) as usize;
159
160    if predictor == 2 {
161        decode_tiff_predictor(data, colors, bpc, columns)
162    } else if predictor >= 10 {
163        decode_png_predictor(data, colors, bpc, columns)
164    } else {
165        Ok(data.to_vec())
166    }
167}
168
169fn decode_tiff_predictor(
170    data: &[u8],
171    colors: usize,
172    bpc: usize,
173    columns: usize,
174) -> Result<Vec<u8>> {
175    // M4 Fix: Validate parameters before processing
176    // TIFF predictor (Predictor 2) in PDF spec only supports BPC=8
177    if bpc != 8 {
178        // Not an error per spec - just means predictor doesn't apply
179        return Ok(data.to_vec());
180    }
181
182    // M4 Fix: Explicit validation that parameters are non-zero
183    if colors == 0 || columns == 0 {
184        return Err(Error::StreamDecode(
185            "TIFF predictor: colors and columns must be non-zero".into(),
186        ));
187    }
188
189    // Check for overflow in row_bytes calculation
190    let row_bytes = columns
191        .checked_mul(colors)
192        .ok_or_else(|| Error::StreamDecode("TIFF predictor: columns * colors overflow".into()))?;
193
194    // M4 Fix: Validate data length is consistent with parameters
195    // Data should be a multiple of row_bytes (though we handle partial rows gracefully)
196    if data.is_empty() {
197        return Ok(data.to_vec());
198    }
199
200    let mut output = data.to_vec();
201    for row_start in (0..output.len()).step_by(row_bytes) {
202        let row_end = (row_start + row_bytes).min(output.len());
203        for i in (row_start + colors)..row_end {
204            output[i] = output[i].wrapping_add(output[i - colors]);
205        }
206    }
207    Ok(output)
208}
209
210fn decode_png_predictor(data: &[u8], colors: usize, bpc: usize, columns: usize) -> Result<Vec<u8>> {
211    // L1 Fix: Explicit validation of parameters before processing
212    if colors == 0 || columns == 0 {
213        return Err(Error::StreamDecode(
214            "PNG predictor: colors and columns must be non-zero".into(),
215        ));
216    }
217    if bpc == 0 || bpc > 16 {
218        return Err(Error::StreamDecode(
219            "PNG predictor: bits per component must be 1-16".into(),
220        ));
221    }
222
223    // Check all multiplications for overflow to prevent allocation bombs
224    let bits_per_row = colors
225        .checked_mul(bpc)
226        .and_then(|v| v.checked_mul(columns))
227        .ok_or_else(|| Error::StreamDecode("PNG predictor: row computation overflow".into()))?;
228    let row_bytes = bits_per_row.div_ceil(8);
229
230    let bits_per_pixel = colors
231        .checked_mul(bpc)
232        .ok_or_else(|| Error::StreamDecode("PNG predictor: pixel computation overflow".into()))?;
233    let bpp = bits_per_pixel.div_ceil(8); // bytes per pixel for Sub/Paeth
234
235    let stride = row_bytes
236        .checked_add(1)
237        .ok_or_else(|| Error::StreamDecode("PNG predictor: stride overflow".into()))?; // filter byte + row data
238
239    // L1 Fix: Validate stride is reasonable (not zero, not absurdly large)
240    if stride == 0 {
241        return Err(Error::StreamDecode(
242            "PNG predictor: stride cannot be zero".into(),
243        ));
244    }
245
246    if !data.len().is_multiple_of(stride) && !data.is_empty() {
247        // Try to process what we can
248        tracing::debug!(
249            "PNG predictor: data length {} not multiple of stride {stride}",
250            data.len()
251        );
252    }
253
254    let num_rows = data.len().div_ceil(stride);
255    let output_size = num_rows
256        .checked_mul(row_bytes)
257        .ok_or_else(|| Error::StreamDecode("PNG predictor: output size overflow".into()))?;
258    let mut output = Vec::with_capacity(output_size);
259    let mut prev_row = vec![0u8; row_bytes];
260
261    let mut pos = 0;
262    while pos < data.len() {
263        let filter_type = data[pos];
264        pos += 1;
265
266        let available = (data.len() - pos).min(row_bytes);
267        let cur = &data[pos..pos + available];
268
269        let mut row = vec![0u8; row_bytes];
270        row[..available].copy_from_slice(cur);
271
272        match filter_type {
273            0 => {} // None
274            1 => {
275                // Sub
276                for i in bpp..row_bytes {
277                    row[i] = row[i].wrapping_add(row[i - bpp]);
278                }
279            }
280            2 => {
281                // Up
282                for i in 0..row_bytes {
283                    row[i] = row[i].wrapping_add(prev_row[i]);
284                }
285            }
286            3 => {
287                // Average
288                for i in 0..row_bytes {
289                    let left = if i >= bpp { row[i - bpp] as u16 } else { 0 };
290                    let above = prev_row[i] as u16;
291                    row[i] = row[i].wrapping_add(((left + above) / 2) as u8);
292                }
293            }
294            4 => {
295                // Paeth
296                for i in 0..row_bytes {
297                    let left = if i >= bpp { row[i - bpp] as i32 } else { 0 };
298                    let above = prev_row[i] as i32;
299                    let upper_left = if i >= bpp {
300                        prev_row[i - bpp] as i32
301                    } else {
302                        0
303                    };
304                    row[i] = row[i].wrapping_add(paeth(left, above, upper_left));
305                }
306            }
307            _ => {
308                tracing::debug!("PNG predictor: unknown filter type {filter_type}");
309            }
310        }
311
312        output.extend_from_slice(&row);
313        prev_row.copy_from_slice(&row);
314        pos += available;
315    }
316
317    Ok(output)
318}
319
320fn paeth(a: i32, b: i32, c: i32) -> u8 {
321    let p = a + b - c;
322    let pa = (p - a).abs();
323    let pb = (p - b).abs();
324    let pc = (p - c).abs();
325    if pa <= pb && pa <= pc {
326        a as u8
327    } else if pb <= pc {
328        b as u8
329    } else {
330        c as u8
331    }
332}
333
334fn apply_filter(
335    filter: &PdfName,
336    data: &[u8],
337    params: Option<&PdfDict>,
338    limits: &ParseLimits,
339    budget: &mut DecodeBudget,
340) -> Result<Vec<u8>> {
341    match filter.as_str() {
342        "FlateDecode" | "Fl" => decode_flate(data, budget),
343        "LZWDecode" | "LZW" => {
344            // EarlyChange lives in DecodeParms; default 1 per ISO 32000.
345            let early_change = params
346                .and_then(|p| p.get_i64("EarlyChange").ok())
347                .unwrap_or(1);
348            lzw_decode(data, early_change, budget)
349        }
350        "ASCIIHexDecode" | "AHx" => decode_ascii_hex(data, budget),
351        "ASCII85Decode" | "A85" => decode_ascii85(data, budget),
352        "RunLengthDecode" | "RL" => decode_run_length(data, budget),
353        "DCTDecode" | "DCT" => decode_dct(data, limits.max_image_pixels, budget),
354        "CCITTFaxDecode" | "CCF" => {
355            let ccitt_params = crate::ccitt::CcittParams::from_dict(params);
356            // M1 Fix: Thread budget through to prevent decompression bombs
357            crate::ccitt::decode(data, &ccitt_params, budget)
358        }
359        "JBIG2Decode" => {
360            let jbig2_params = crate::jbig2::Jbig2Params::from_dict(params);
361            // M2 Fix: Thread budget through to prevent decompression bombs
362            crate::jbig2::decode(data, &jbig2_params, budget)
363        }
364        // JPXDecode output is decoded *pixels*, not raw samples, and JPEG 2000
365        // carries its own colour-space/alpha metadata that a bytes-only filter
366        // cannot return. Pass the codestream through unchanged; zpdf-image
367        // sniffs it (filter name + JP2/SOC magic) and runs the real decode.
368        "JPXDecode" => {
369            budget.reserve(data.len() as u64)?;
370            Ok(data.to_vec())
371        }
372        other => Err(Error::UnsupportedFilter(other.to_string())),
373    }
374}
375
376/// PDF/TIFF variable-width LZW decoder (ISO 32000-1, 7.4.4.2).
377///
378/// 8-bit input symbols; code width starts at 9 and grows to a max of 12.
379/// Code 256 = ClearTable (reset dictionary, width back to 9),
380/// code 257 = EOD. Codes 258+ are dictionary strings. `early_change` is the
381/// DecodeParms EarlyChange value (default 1); when 1 the code width is increased
382/// one code earlier than the natural boundary.
383fn lzw_decode(data: &[u8], early_change: i64, budget: &mut DecodeBudget) -> Result<Vec<u8>> {
384    const CLEAR: u32 = 256;
385    const EOD: u32 = 257;
386
387    // EarlyChange is effectively a flag: any nonzero -> 1, explicit 0 -> 0.
388    let early: u32 = if early_change == 0 { 0 } else { 1 };
389
390    // Dictionary: index = code, value = decoded byte string. Slots 0..=255 are
391    // single bytes; 256/257 are placeholders so the first dynamic code is 258.
392    let mut table: Vec<Vec<u8>> = Vec::with_capacity(4096);
393    let reset = |t: &mut Vec<Vec<u8>>| {
394        t.clear();
395        for i in 0..256u32 {
396            t.push(vec![i as u8]);
397        }
398        t.push(Vec::new()); // 256 CLEAR (unused as a string)
399        t.push(Vec::new()); // 257 EOD   (unused as a string)
400    };
401    reset(&mut table);
402
403    let mut width: u32 = 9;
404    let mut bit_pos: usize = 0;
405    let total_bits = data.len() * 8;
406
407    // MSB-first reader; returns None when fewer than `width` bits remain.
408    let read_code = |bit_pos: &mut usize, width: u32| -> Option<u32> {
409        if *bit_pos + width as usize > total_bits {
410            return None;
411        }
412        let mut code: u32 = 0;
413        for _ in 0..width {
414            let byte = data[*bit_pos / 8];
415            let bit = (byte >> (7 - (*bit_pos % 8))) & 1;
416            code = (code << 1) | bit as u32;
417            *bit_pos += 1;
418        }
419        Some(code)
420    };
421
422    let mut out: Vec<u8> = Vec::new();
423    let mut prev: Option<u32> = None;
424
425    // Stop when input is exhausted (some streams omit the EOD marker).
426    while let Some(code) = read_code(&mut bit_pos, width) {
427        if code == EOD {
428            break;
429        }
430        if code == CLEAR {
431            reset(&mut table);
432            width = 9;
433            prev = None;
434            continue;
435        }
436
437        // Resolve the output string for this code.
438        let entry: Vec<u8> = if (code as usize) < table.len() {
439            table[code as usize].clone()
440        } else if code as usize == table.len() {
441            // KwKwK: code refers to the entry we are about to define.
442            match prev {
443                Some(p) => {
444                    let mut e = table[p as usize].clone();
445                    e.push(table[p as usize][0]);
446                    e
447                }
448                None => {
449                    return Err(Error::StreamDecode(format!(
450                        "LZWDecode: code {code} before any literal"
451                    )))
452                }
453            }
454        } else {
455            return Err(Error::StreamDecode(format!(
456                "LZWDecode: invalid code {code} (table size {})",
457                table.len()
458            )));
459        };
460
461        // H1 Fix: Reserve budget before extending output
462        budget.reserve(entry.len() as u64)?;
463        out.extend_from_slice(&entry);
464
465        // Add new dictionary entry = previous string + first byte of this entry.
466        // (Skipped for the first code after a clear, when prev is None.)
467        if let Some(p) = prev {
468            // M5 Fix: Enforce the LZW table size limit of 4096 entries (codes 0-4095).
469            // The spec dictates max code width is 12 bits (2^12 = 4096), so adding
470            // beyond this would violate the protocol.
471            if table.len() >= 4096 {
472                // Table is full; don't add new entries until a CLEAR resets it.
473                // This is standard LZW behavior when the table maxes out.
474            } else {
475                let mut new_entry = table[p as usize].clone();
476                new_entry.push(entry[0]);
477                table.push(new_entry);
478            }
479        }
480        prev = Some(code);
481
482        // Width growth. After the push above, `table.len()` is the index that
483        // will be assigned to the NEXT dictionary entry, which is exactly the
484        // value to test against the current width's capacity. EarlyChange (=1)
485        // bumps the width one code earlier. Grow when `table.len() + early >=
486        // 2^width`. (Validated against weezl/TIFF LZW across the 9->10->11->12
487        // and 4096 boundaries; an earlier `+ 1` here desynced real streams.)
488        let next_code = table.len() as u32;
489        if width < 12 && next_code + early >= (1u32 << width) {
490            width += 1;
491        }
492    }
493
494    Ok(out)
495}
496
497/// Outcome of one chunked inflate attempt: either the reader ran to a clean
498/// EOF, or it failed partway with whatever bytes were recovered first.
499enum InflateOutcome {
500    Complete(Vec<u8>),
501    Failed(Vec<u8>, String),
502}
503
504/// Drive `reader` to completion in fixed-size chunks so that a mid-stream
505/// error still yields the bytes decoded before it. Output is capped by the
506/// budget; hitting the cap is a hard error (a decompression bomb is not salvageable data).
507fn inflate_chunked(
508    mut reader: impl std::io::Read,
509    budget: &mut DecodeBudget,
510) -> Result<InflateOutcome> {
511    let mut out = Vec::new();
512    let mut buf = [0u8; 16 * 1024];
513    loop {
514        match reader.read(&mut buf) {
515            Ok(0) => return Ok(InflateOutcome::Complete(out)),
516            Ok(n) => {
517                // H1 Fix: Reserve budget before extending output
518                budget.reserve(n as u64)?;
519                out.extend_from_slice(&buf[..n]);
520            }
521            Err(e) => return Ok(InflateOutcome::Failed(out, e.to_string())),
522        }
523    }
524}
525
526/// FlateDecode with real-world tolerance: salvages partial output from
527/// truncated/corrupt zlib streams, retries headerless data as raw deflate,
528/// and skips a bounded run of leading garbage before a plausible zlib header.
529fn decode_flate(data: &[u8], budget: &mut DecodeBudget) -> Result<Vec<u8>> {
530    use flate2::read::{DeflateDecoder, ZlibDecoder};
531
532    // Lenient: an empty stream decodes to nothing.
533    if data.is_empty() {
534        return Ok(Vec::new());
535    }
536
537    // Plausible zlib header at `i`: CM (low nibble of CMF) is 8 (deflate) and
538    // the FCHECK property holds (CMF<<8 | FLG divisible by 31).
539    let plausible_zlib = |i: usize| {
540        data.len() >= i + 2
541            && data[i] & 0x0f == 8
542            && ((data[i] as u32) << 8 | data[i + 1] as u32).is_multiple_of(31)
543    };
544
545    let mut zlib_err: Option<String> = None;
546    if plausible_zlib(0) {
547        match inflate_chunked(ZlibDecoder::new(data), budget)? {
548            InflateOutcome::Complete(out) => return Ok(out),
549            InflateOutcome::Failed(partial, err) if !partial.is_empty() => {
550                tracing::warn!(
551                    "FlateDecode: zlib stream failed after {} bytes ({err}); keeping partial output",
552                    partial.len()
553                );
554                return Ok(partial);
555            }
556            InflateOutcome::Failed(_, err) => zlib_err = Some(err),
557        }
558    }
559
560    // The header was implausible (or decoded to nothing): look for a plausible
561    // CMF/FLG pair after a bounded garbage/whitespace prefix.
562    const MAX_HEADER_SCAN: usize = 64;
563    if let Some(k) = (1..data.len().min(MAX_HEADER_SCAN)).find(|&k| plausible_zlib(k)) {
564        match inflate_chunked(ZlibDecoder::new(&data[k..]), budget)? {
565            InflateOutcome::Complete(out) => {
566                tracing::warn!("FlateDecode: skipped {k} bytes of leading garbage");
567                return Ok(out);
568            }
569            InflateOutcome::Failed(partial, err) if !partial.is_empty() => {
570                tracing::warn!(
571                    "FlateDecode: zlib stream at offset {k} failed ({err}); keeping {} partial bytes",
572                    partial.len()
573                );
574                return Ok(partial);
575            }
576            InflateOutcome::Failed(..) => {}
577        }
578    }
579
580    // Last resort: some writers emit raw deflate with no zlib wrapper.
581    match inflate_chunked(DeflateDecoder::new(data), budget)? {
582        InflateOutcome::Complete(out) => {
583            tracing::warn!("FlateDecode: decoded as raw deflate (missing zlib header)");
584            Ok(out)
585        }
586        InflateOutcome::Failed(partial, err) if !partial.is_empty() => {
587            tracing::warn!(
588                "FlateDecode: raw deflate failed ({err}); keeping {} partial bytes",
589                partial.len()
590            );
591            Ok(partial)
592        }
593        InflateOutcome::Failed(_, err) => Err(Error::StreamDecode(format!(
594            "FlateDecode: {}",
595            zlib_err.unwrap_or(err)
596        ))),
597    }
598}
599
600/// Lenient ASCIIHexDecode: whitespace is ignored anywhere, stray non-hex bytes
601/// are skipped (warned, not fatal), and anything after the `>` EOD marker is
602/// ignored, so partial/dirty streams still decode.
603fn decode_ascii_hex(data: &[u8], budget: &mut DecodeBudget) -> Result<Vec<u8>> {
604    // M7 Fix: Reserve budget upfront for worst-case output size (one byte per two input chars)
605    let max_output = data.len().saturating_add(1) / 2;
606    budget.reserve(max_output as u64)?;
607
608    let mut output = Vec::with_capacity(data.len() / 2);
609    let mut high: Option<u8> = None;
610    let mut stray = 0usize;
611
612    for &b in data {
613        if b == b'>' {
614            break; // EOD; bytes after it are ignored
615        }
616        if b.is_ascii_whitespace() || b == 0 {
617            continue;
618        }
619        let nibble = match b {
620            b'0'..=b'9' => b - b'0',
621            b'a'..=b'f' => b - b'a' + 10,
622            b'A'..=b'F' => b - b'A' + 10,
623            _ => {
624                stray += 1;
625                continue;
626            }
627        };
628
629        match high {
630            None => high = Some(nibble),
631            Some(h) => {
632                output.push((h << 4) | nibble);
633                high = None;
634            }
635        }
636    }
637
638    if let Some(h) = high {
639        output.push(h << 4);
640    }
641    if stray > 0 {
642        tracing::warn!("ASCIIHexDecode: ignored {stray} invalid byte(s)");
643    }
644
645    Ok(output)
646}
647
648/// Lenient ASCII85Decode: whitespace is ignored anywhere, stray bytes outside
649/// the alphabet are skipped (warned, not fatal), and everything from the `~`
650/// of the `~>` EOD marker on is ignored, salvaging partial output.
651fn decode_ascii85(data: &[u8], budget: &mut DecodeBudget) -> Result<Vec<u8>> {
652    // M6 Fix: Reserve budget upfront for worst-case output size.
653    // Each 5-char ASCII85 group decodes to 4 bytes, plus special 'z' → 4 bytes.
654    // Worst case: all 'z' chars → data.len() * 4 bytes. Over-reservation is safe.
655    let max_output = (data.len() as u64).saturating_mul(4);
656    budget.reserve(max_output)?;
657
658    let mut output = Vec::new();
659    // u64 accumulator: a 5-char group of bytes near 'u' encodes a value just
660    // above u32::MAX; the spec calls it invalid, but it must not overflow.
661    let mut tuple: u64 = 0;
662    let mut count = 0usize;
663    let mut stray = 0usize;
664
665    for &b in data {
666        if b == b'~' {
667            break; // start of the "~>" EOD marker; ignore it and the rest
668        }
669        if b.is_ascii_whitespace() || b == 0 {
670            continue;
671        }
672
673        if b == b'z' && count == 0 {
674            output.extend_from_slice(&[0, 0, 0, 0]);
675            continue;
676        }
677
678        if !(b'!'..=b'u').contains(&b) {
679            stray += 1;
680            continue;
681        }
682
683        tuple = tuple * 85 + (b - b'!') as u64;
684        count += 1;
685
686        if count == 5 {
687            let t = (tuple & 0xFFFF_FFFF) as u32;
688            output.extend_from_slice(&t.to_be_bytes());
689            tuple = 0;
690            count = 0;
691        }
692    }
693
694    // Handle remaining bytes
695    if count > 1 {
696        for _ in count..5 {
697            tuple = tuple * 85 + 84; // pad with 'u'
698        }
699        let t = (tuple & 0xFFFF_FFFF) as u32;
700        for i in 0..(count - 1) {
701            output.push((t >> (24 - i * 8)) as u8);
702        }
703    }
704    if stray > 0 {
705        tracing::warn!("ASCII85Decode: ignored {stray} invalid byte(s)");
706    }
707
708    Ok(output)
709}
710
711/// H2 Fix: Validate JPEG dimensions before zune-jpeg allocates the output buffer.
712/// A malformed JPEG header claiming 65535×65535×4 would trigger a ~16 GiB allocation
713/// in `decode()`, bypassing all limits. Pre-validate against the active
714/// `ParseLimits` pixel limit and reserve the decoded size from the budget before
715/// calling decode.
716fn validate_jpeg_dimensions(
717    width: u16,
718    height: u16,
719    components: u8,
720    max_image_pixels: u64,
721    budget: &mut DecodeBudget,
722) -> Result<()> {
723    let w = width as u64;
724    let h = height as u64;
725    let c = components as u64;
726
727    // The JPEG header is authoritative; the PDF image dictionary may disagree.
728    let pixel_count = w.saturating_mul(h);
729    if pixel_count > max_image_pixels {
730        return Err(Error::StreamDecode(format!(
731            "JPEG dimensions {w}×{h} exceed the {max_image_pixels}-pixel limit"
732        )));
733    }
734
735    // Check decoded byte size (width × height × channels) against budget
736    let decoded_size = pixel_count.saturating_mul(c);
737    if decoded_size > budget.remaining {
738        return Err(Error::StreamDecode(format!(
739            "JPEG output {w}×{h}×{c} = {decoded_size} bytes exceeds remaining budget {}",
740            budget.remaining
741        )));
742    }
743
744    // Reserve budget before zune-jpeg allocates
745    budget.reserve(decoded_size)?;
746    Ok(())
747}
748
749fn decode_dct(data: &[u8], max_image_pixels: u64, budget: &mut DecodeBudget) -> Result<Vec<u8>> {
750    use zune_jpeg::JpegDecoder;
751
752    // Adobe YCCK JPEGs (APP14 transform == 2, 4 components) are mis-handled by
753    // zune-jpeg's built-in YCCK->RGB: it applies a spurious `255 - x`, producing
754    // a colour-negative image (a white CMYK page reads back as black). zune has
755    // no YCCK->CMYK arm either, so we take the raw YCCK channels and convert them
756    // ourselves. Plain Adobe CMYK (transform 0) decodes correctly via zune's
757    // CMYK->RGB, so it stays on the default path.
758    if jpeg_is_adobe_ycck(data) {
759        use zune_jpeg::zune_core::colorspace::ColorSpace;
760        use zune_jpeg::zune_core::options::DecoderOptions;
761        let opts = DecoderOptions::default().jpeg_set_out_colorspace(ColorSpace::YCCK);
762        let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(data), opts);
763
764        // H2 Fix: Validate JPEG dimensions before decode to prevent allocation bomb
765        decoder
766            .decode_headers()
767            .map_err(|e| Error::StreamDecode(format!("DCTDecode header parse failed: {e}")))?;
768        if let Some(info) = decoder.info() {
769            validate_jpeg_dimensions(
770                info.width,
771                info.height,
772                info.components,
773                max_image_pixels,
774                budget,
775            )?;
776        }
777
778        match decoder.decode() {
779            Ok(ycck) if decoder.output_colorspace() == Some(ColorSpace::YCCK) => {
780                return Ok(ycck_to_rgb(&ycck));
781            }
782            // Unexpected (e.g. not actually 4-component): fall through to the
783            // default decode rather than mangle the data.
784            _ => {}
785        }
786    }
787
788    let mut decoder = JpegDecoder::new(std::io::Cursor::new(data));
789
790    // H2 Fix: Validate JPEG dimensions before decode to prevent allocation bomb
791    decoder
792        .decode_headers()
793        .map_err(|e| Error::StreamDecode(format!("DCTDecode header parse failed: {e}")))?;
794    if let Some(info) = decoder.info() {
795        validate_jpeg_dimensions(
796            info.width,
797            info.height,
798            info.components,
799            max_image_pixels,
800            budget,
801        )?;
802    }
803
804    decoder
805        .decode()
806        .map_err(|e| Error::StreamDecode(format!("DCTDecode: {e}")))
807}
808
809/// Convert raw upsampled Adobe YCCK samples (`Y, Cb, Cr, K` per pixel) to RGB.
810///
811/// In Adobe YCCK the chroma channels encode the *complement* of C/M/Y, so the
812/// JFIF YCbCr->RGB output is the transmitted (inverted) ink: `C = 1 − R'`,
813/// `M = 1 − G'`, `Y = 1 − B'`. The 4th channel is the black-ink amount
814/// (`K_raw = 255` ⇒ full black). The recovered DeviceCMYK is converted through
815/// the shared Adobe polynomial ([`zpdf_color::cmyk_to_rgb`]) so YCCK JPEGs match
816/// every other DeviceCMYK path — e.g. 100 % K is a dark near-black, not pure
817/// black. (The previous `channel * (255 − K_raw)` shortcut was the naïve
818/// `(1−c)(1−k)`, which over-saturated like a non-fidelity viewer.)
819fn ycck_to_rgb(ycck: &[u8]) -> Vec<u8> {
820    let mut out = Vec::with_capacity(ycck.len() / 4 * 3);
821    for px in ycck.chunks_exact(4) {
822        let (y, cb, cr) = (px[0] as f64, px[1] as f64, px[2] as f64);
823        // JFIF YCbCr -> R'G'B' (transmitted light = complement of C/M/Y ink).
824        let rp = (y + 1.402 * (cr - 128.0)).clamp(0.0, 255.0);
825        let gp = (y - 0.344_136 * (cb - 128.0) - 0.714_136 * (cr - 128.0)).clamp(0.0, 255.0);
826        let bp = (y + 1.772 * (cb - 128.0)).clamp(0.0, 255.0);
827        let (r, g, b) = zpdf_color::cmyk_to_rgb(
828            1.0 - rp / 255.0,
829            1.0 - gp / 255.0,
830            1.0 - bp / 255.0,
831            px[3] as f64 / 255.0,
832        );
833        out.push((r * 255.0).round() as u8);
834        out.push((g * 255.0).round() as u8);
835        out.push((b * 255.0).round() as u8);
836    }
837    out
838}
839
840/// Scan a JPEG for an Adobe APP14 marker with transform 2 (YCCK) over a SOF that
841/// declares 4 components. Cheap byte walk over the marker segments only.
842fn jpeg_is_adobe_ycck(data: &[u8]) -> bool {
843    let mut adobe_ycck = false;
844    let mut four_components = false;
845    let mut i = 2; // skip SOI (FFD8)
846    while i + 3 < data.len() {
847        if data[i] != 0xFF {
848            i += 1;
849            continue;
850        }
851        let marker = data[i + 1];
852        // Standalone markers (no length): padding fill, SOI/EOI, RSTn, TEM.
853        if marker == 0xFF || marker == 0x01 || (0xD0..=0xD9).contains(&marker) {
854            i += 2;
855            continue;
856        }
857        let seg_len = ((data[i + 2] as usize) << 8) | data[i + 3] as usize;
858        if seg_len < 2 {
859            break;
860        }
861        let payload_start = i + 4;
862        let payload_end = i + 2 + seg_len;
863        if payload_end > data.len() {
864            break;
865        }
866        let payload = &data[payload_start..payload_end];
867        match marker {
868            // APP14: "Adobe" + version(2) + flags0(2) + flags1(2) + transform(1).
869            0xEE => {
870                if payload.len() >= 12 && &payload[0..5] == b"Adobe" {
871                    adobe_ycck = payload[11] == 2;
872                }
873            }
874            // SOFn (baseline/progressive/etc.), excluding DHT(C4)/JPG(C8)/DAC(CC).
875            0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC => {
876                // precision(1) + height(2) + width(2) + Nf(1).
877                if payload.len() >= 6 {
878                    four_components = payload[5] == 4;
879                }
880            }
881            // Start of scan: header is done.
882            0xDA => break,
883            _ => {}
884        }
885        i = payload_end;
886    }
887    adobe_ycck && four_components
888}
889
890fn decode_run_length(data: &[u8], budget: &mut DecodeBudget) -> Result<Vec<u8>> {
891    let mut output = Vec::new();
892    let mut i = 0;
893
894    while i < data.len() {
895        let length_byte = data[i];
896        i += 1;
897
898        if length_byte == 128 {
899            break; // EOD
900        } else if length_byte < 128 {
901            // Copy next (length_byte + 1) bytes literally
902            let count = length_byte as usize + 1;
903            if i + count > data.len() {
904                return Err(Error::StreamDecode("RunLengthDecode: truncated".into()));
905            }
906            // H1 Fix: Reserve budget before extending
907            budget.reserve(count as u64)?;
908            output.extend_from_slice(&data[i..i + count]);
909            i += count;
910        } else {
911            // Repeat next byte (257 - length_byte) times
912            let count = 257 - length_byte as usize;
913            if i >= data.len() {
914                return Err(Error::StreamDecode("RunLengthDecode: truncated".into()));
915            }
916            // H1 Fix: Reserve budget before resizing
917            budget.reserve(count as u64)?;
918            let byte = data[i];
919            i += 1;
920            output.resize(output.len() + count, byte);
921        }
922    }
923
924    Ok(output)
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    #[test]
932    fn ycck_white_decodes_white_not_black() {
933        // Adobe white (no ink): Y=255, Cb=Cr=128 (neutral), K_raw=0 → CMYK all 0.
934        let rgb = ycck_to_rgb(&[255, 128, 128, 0]);
935        assert_eq!(rgb, vec![255, 255, 255], "Adobe YCCK white must stay white");
936    }
937
938    #[test]
939    fn ycck_full_black_ink_decodes_near_black() {
940        // K_raw=255 ⇒ CMYK (0,0,0,1). The Adobe DeviceCMYK polynomial renders
941        // 100% K as a dark near-black, not pure black (matches every other path).
942        let rgb = ycck_to_rgb(&[255, 128, 128, 255]);
943        assert_eq!(rgb, vec![44, 46, 53]);
944    }
945
946    #[test]
947    fn ycck_neutral_gray_via_polynomial() {
948        // No CMY (chroma neutral, luma full) with half black ink ⇒ CMYK
949        // (0,0,0,0.5); the polynomial maps it lighter than the naïve 127.
950        let rgb = ycck_to_rgb(&[255, 128, 128, 128]);
951        assert_eq!(rgb, vec![154, 156, 159]);
952    }
953
954    #[test]
955    fn ycck_colored_pixel_via_polynomial() {
956        // Non-neutral chroma exercises the C/M/Y recovery + the full polynomial.
957        let rgb = ycck_to_rgb(&[200, 100, 150, 50]);
958        assert_eq!(rgb, vec![198, 165, 131]);
959    }
960
961    #[test]
962    fn adobe_ycck_detection() {
963        // Minimal marker stream: SOI, APP14(Adobe, transform=2), SOF0(4 comp), SOS.
964        let mut j = vec![0xFF, 0xD8];
965        // APP14, len=16: "Adobe"(5)+ver(2)+f0(2)+f1(2)+transform(1) = 12 payload, +2 len = 14... use 16 with pad.
966        j.extend_from_slice(&[0xFF, 0xEE, 0x00, 0x0E]);
967        j.extend_from_slice(b"Adobe");
968        j.extend_from_slice(&[0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x02]); // version, flags, transform=2
969                                                                          // SOF0, len=17 (1 prec + 2 h + 2 w + 1 Nf=4 + 4*3 comp specs) -> payload 6+ needed.
970        j.extend_from_slice(&[0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x04]);
971        j.extend_from_slice(&[1, 0x11, 0, 2, 0x11, 0, 3, 0x11, 0, 4, 0x11, 0]);
972        j.extend_from_slice(&[0xFF, 0xDA, 0x00, 0x02]); // SOS
973        assert!(jpeg_is_adobe_ycck(&j));
974
975        // transform=0 (plain CMYK) must NOT take the YCCK path.
976        let mut j0 = j.clone();
977        // transform byte is at: 2 (SOI) + 4 (app14 hdr) + 11 = index 17.
978        j0[17] = 0;
979        assert!(!jpeg_is_adobe_ycck(&j0));
980    }
981
982    #[test]
983    fn flate_roundtrip() {
984        use flate2::write::ZlibEncoder;
985        use flate2::Compression;
986        use std::io::Write;
987
988        let original = b"Hello, zpdf! This is a test of FlateDecode.";
989        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
990        encoder.write_all(original).unwrap();
991        let compressed = encoder.finish().unwrap();
992
993        let decoded = {
994            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
995            decode_flate(&compressed, &mut budget).unwrap()
996        };
997        assert_eq!(decoded, original);
998    }
999
1000    #[test]
1001    fn flate_partial_salvage_on_truncation() {
1002        use flate2::write::ZlibEncoder;
1003        use flate2::Compression;
1004        use std::io::Write;
1005
1006        // Deterministic, mostly-incompressible data so the compressed stream
1007        // is long and a truncation still leaves plenty of decodable input.
1008        let mut state = 0x2545F491u64;
1009        let original: Vec<u8> = (0..64 * 1024)
1010            .map(|_| {
1011                state = state
1012                    .wrapping_mul(6364136223846793005)
1013                    .wrapping_add(1442695040888963407);
1014                (state >> 33) as u8
1015            })
1016            .collect();
1017        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1018        encoder.write_all(&original).unwrap();
1019        let compressed = encoder.finish().unwrap();
1020
1021        let truncated = &compressed[..compressed.len() / 2];
1022        let decoded = {
1023            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1024            decode_flate(truncated, &mut budget).unwrap()
1025        };
1026        assert!(!decoded.is_empty(), "partial output must be salvaged");
1027        assert!(decoded.len() < original.len());
1028        assert_eq!(
1029            &original[..decoded.len()],
1030            &decoded[..],
1031            "salvaged bytes are a prefix"
1032        );
1033    }
1034
1035    #[test]
1036    fn flate_raw_deflate_fallback() {
1037        use flate2::write::DeflateEncoder;
1038        use flate2::Compression;
1039        use std::io::Write;
1040
1041        let original = b"raw deflate stream without a zlib wrapper".to_vec();
1042        let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
1043        encoder.write_all(&original).unwrap();
1044        let compressed = encoder.finish().unwrap();
1045
1046        let decoded = {
1047            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1048            decode_flate(&compressed, &mut budget).unwrap()
1049        };
1050        assert_eq!(decoded, original);
1051    }
1052
1053    #[test]
1054    fn flate_skips_leading_garbage() {
1055        use flate2::write::ZlibEncoder;
1056        use flate2::Compression;
1057        use std::io::Write;
1058
1059        let original = b"zlib data behind a garbage prefix".to_vec();
1060        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1061        encoder.write_all(&original).unwrap();
1062        let compressed = encoder.finish().unwrap();
1063
1064        // \r\n\xff: no byte pair in the prefix forms a plausible zlib header.
1065        let mut data = b"\r\n\xff".to_vec();
1066        data.extend_from_slice(&compressed);
1067        let decoded = {
1068            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1069            decode_flate(&data, &mut budget).unwrap()
1070        };
1071        assert_eq!(decoded, original);
1072    }
1073
1074    #[test]
1075    fn flate_empty_input_is_empty_output() {
1076        assert_eq!(
1077            {
1078                let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1079                decode_flate(&[], &mut budget).unwrap()
1080            },
1081            Vec::<u8>::new()
1082        );
1083    }
1084
1085    #[test]
1086    fn flate_garbage_still_errors() {
1087        // Pure ASCII text: implausible zlib header, invalid deflate.
1088        assert!({
1089            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1090            decode_flate(b"this is not compressed data at all....", &mut budget).is_err()
1091        });
1092    }
1093
1094    #[test]
1095    fn ascii_hex() {
1096        let decoded = {
1097            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1098            decode_ascii_hex(b"48 65 6C 6C 6F>", &mut budget).unwrap()
1099        };
1100        assert_eq!(decoded, b"Hello");
1101    }
1102
1103    #[test]
1104    fn ascii_hex_tolerates_stray_bytes_and_data_after_eod() {
1105        // 'x'/'!' are not hex digits (skipped); '>' is EOD (rest ignored).
1106        let decoded = {
1107            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1108            decode_ascii_hex(b"48 65 x6C!6C 6F> trailing garbage \xff", &mut budget).unwrap()
1109        };
1110        assert_eq!(decoded, b"Hello");
1111    }
1112
1113    #[test]
1114    fn ascii85_basic() {
1115        // "Man " encodes to "9jqo^" in ASCII85
1116        let decoded = {
1117            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1118            decode_ascii85(b"9jqo^~>", &mut budget).unwrap()
1119        };
1120        assert_eq!(decoded, b"Man ");
1121    }
1122
1123    #[test]
1124    fn ascii85_ignores_bytes_after_eod() {
1125        let decoded = {
1126            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1127            decode_ascii85(b"9jqo^~> stray bytes \xff\xfe after EOD", &mut budget).unwrap()
1128        };
1129        assert_eq!(decoded, b"Man ");
1130    }
1131
1132    #[test]
1133    fn ascii85_skips_stray_bytes_and_whitespace() {
1134        // NUL and 0xFF are outside the alphabet: skipped, not fatal.
1135        // Whitespace inside a group is ignored.
1136        let decoded = {
1137            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1138            decode_ascii85(b"9j\x00qo\xff ^~>", &mut budget).unwrap()
1139        };
1140        assert_eq!(decoded, b"Man ");
1141    }
1142
1143    #[test]
1144    fn ascii85_overflowing_group_does_not_panic() {
1145        // "uuuuu" encodes a value above u32::MAX — invalid per spec, but must
1146        // decode leniently (truncated) instead of overflowing.
1147        assert!({
1148            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1149            decode_ascii85(b"uuuuu~>", &mut budget).is_ok()
1150        });
1151    }
1152
1153    #[test]
1154    fn run_length_literal_and_repeat() {
1155        // 2 literal bytes [0x41, 0x42], then repeat 0x43 three times, then EOD
1156        let data = [1, 0x41, 0x42, 254, 0x43, 128];
1157        let decoded = {
1158            let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1159            decode_run_length(&data, &mut budget).unwrap()
1160        };
1161        assert_eq!(decoded, vec![0x41, 0x42, 0x43, 0x43, 0x43]);
1162    }
1163
1164    #[test]
1165    fn png_predictor_none() {
1166        // 2 columns, 1 color, 8 bpc → row_bytes = 2, stride = 3
1167        // filter=0 (None): [0, 0x41, 0x42]
1168        let data = [0, 0x41, 0x42];
1169        let result = decode_png_predictor(&data, 1, 8, 2).unwrap();
1170        assert_eq!(result, vec![0x41, 0x42]);
1171    }
1172
1173    #[test]
1174    fn png_predictor_sub() {
1175        // filter=1 (Sub), bpp=1: each byte += left
1176        // row: [1, 10, 5, 3] → decoded: [10, 15, 18]
1177        let data = [1, 10, 5, 3];
1178        let result = decode_png_predictor(&data, 1, 8, 3).unwrap();
1179        assert_eq!(result, vec![10, 15, 18]);
1180    }
1181
1182    #[test]
1183    fn png_predictor_up() {
1184        // filter=2 (Up): each byte += above
1185        // row1: [0, 10, 20] → [10, 20]
1186        // row2: [2, 5, 3]   → [15, 23]
1187        let data = [0, 10, 20, 2, 5, 3];
1188        let result = decode_png_predictor(&data, 1, 8, 2).unwrap();
1189        assert_eq!(result, vec![10, 20, 15, 23]);
1190    }
1191
1192    #[test]
1193    fn png_predictor_paeth() {
1194        // filter=4 (Paeth), 1 color 8bpc 3 columns, bpp=1
1195        // row1: [0, 10, 20, 30]  → None: [10, 20, 30]
1196        // row2: [4, 5, 7, 3]     → Paeth reconstruction
1197        //   i=0: paeth(0, 10, 0)=10, 5+10=15
1198        //   i=1: paeth(15, 20, 10)=20, 7+20=27
1199        //   i=2: paeth(27, 30, 20)=30, 3+30=33
1200        let data = [0, 10, 20, 30, 4, 5, 7, 3];
1201        let result = decode_png_predictor(&data, 1, 8, 3).unwrap();
1202        assert_eq!(result, vec![10, 20, 30, 15, 27, 33]);
1203    }
1204
1205    #[test]
1206    fn tiff_predictor_basic() {
1207        // 3 colors (RGB), 8bpc, 2 columns → row = 6 bytes
1208        // [R0,G0,B0, dR1,dG1,dB1] → [R0,G0,B0, R0+dR1, G0+dG1, B0+dB1]
1209        let data = [100, 150, 200, 10, 20, 30];
1210        let result = decode_tiff_predictor(&data, 3, 8, 2).unwrap();
1211        assert_eq!(result, vec![100, 150, 200, 110, 170, 230]);
1212    }
1213
1214    // --- LZWDecode ---
1215
1216    #[test]
1217    fn lzw_canonical_vector() {
1218        // Classic ISO 32000 / Adobe LZW example. 9-bit codes, MSB-first:
1219        //   256 (Clear), 45 ('-'), 258 (KwKwK -> "--"), 259 ("---"),
1220        //   65 ('A'), 259 ("---"), 66 ('B'), 257 (EOD)
1221        let data = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
1222        let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1223        let decoded = lzw_decode(&data, 1, &mut budget).unwrap();
1224        assert_eq!(decoded, b"-----A---B");
1225    }
1226
1227    #[test]
1228    fn lzw_via_apply_filter_default_early_change() {
1229        let data = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
1230        let name = PdfName::new("LZWDecode");
1231        let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1232        let out = apply_filter(&name, &data, None, &ParseLimits::default(), &mut budget).unwrap();
1233        assert_eq!(out, b"-----A---B");
1234    }
1235
1236    #[test]
1237    fn lzw_stops_at_end_without_eod() {
1238        // Truncated before EOD; should decode the leading symbols and stop cleanly.
1239        let data = [0x80, 0x0B, 0x60, 0x50];
1240        let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1241        let out = lzw_decode(&data, 1, &mut budget).unwrap();
1242        assert!(out.starts_with(b"-"));
1243    }
1244
1245    #[test]
1246    fn lzw_empty_input() {
1247        let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1248        assert_eq!(lzw_decode(&[], 1, &mut budget).unwrap(), Vec::<u8>::new());
1249    }
1250
1251    /// Encode with weezl (an independent, spec-conformant LZW producer) so the
1252    /// decoder is validated against an EXTERNAL reference rather than its own
1253    /// paired encoder. weezl's TIFF size-switch == PDF EarlyChange=1; its plain
1254    /// MSB encoder == EarlyChange=0. Verified: weezl(tiff) output of the canonical
1255    /// vector decodes to "-----A---B" here.
1256    fn weezl_encode(data: &[u8], early_change: i64) -> Vec<u8> {
1257        use weezl::{encode::Encoder, BitOrder};
1258        let mut enc = if early_change == 0 {
1259            Encoder::new(BitOrder::Msb, 8)
1260        } else {
1261            Encoder::with_tiff_size_switch(BitOrder::Msb, 8)
1262        };
1263        enc.encode(data).expect("weezl encode")
1264    }
1265
1266    #[test]
1267    fn lzw_roundtrip_against_weezl() {
1268        // Cross every width boundary (9->10->11->12) and the 4096 auto-clear,
1269        // for both EarlyChange settings, against an external reference encoder.
1270        let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1271        for ec in [1i64, 0] {
1272            for &len in &[0usize, 1, 300, 600, 1200, 3000, 5000, 9000] {
1273                // Mix of low- and high-entropy bytes to grow the dictionary.
1274                let input: Vec<u8> = (0..len).map(|i| ((i * 7 + i / 11) % 251) as u8).collect();
1275                let encoded = weezl_encode(&input, ec);
1276                let decoded = lzw_decode(&encoded, ec, &mut budget).unwrap();
1277                assert_eq!(decoded, input, "ec={ec} len={len}");
1278            }
1279        }
1280    }
1281
1282    #[test]
1283    fn lzw_single_byte_run_roundtrip_against_weezl() {
1284        // A long single-symbol run exercises the KwKwK path heavily.
1285        let input = vec![b'A'; 5000];
1286        let mut budget = DecodeBudget::new(ParseLimits::default().max_decoded_stream_bytes);
1287        for ec in [1i64, 0] {
1288            let encoded = weezl_encode(&input, ec);
1289            assert_eq!(
1290                lzw_decode(&encoded, ec, &mut budget).unwrap(),
1291                input,
1292                "ec={ec}"
1293            );
1294        }
1295    }
1296
1297    #[test]
1298    fn decoded_limit_applies_without_filters() {
1299        let limits = ParseLimits {
1300            max_decoded_stream_bytes: 3,
1301            ..ParseLimits::default()
1302        };
1303        let err = decode_stream_with_limits(b"four", &PdfDict::new(), &limits).unwrap_err();
1304        assert!(matches!(err, Error::StreamSizeLimit(3)));
1305    }
1306
1307    #[test]
1308    fn jpeg_dimensions_honor_custom_pixel_limit() {
1309        let mut budget = DecodeBudget::new(1024);
1310        let err = validate_jpeg_dimensions(2, 2, 3, 3, &mut budget).unwrap_err();
1311        assert!(err.to_string().contains("3-pixel limit"));
1312
1313        let mut budget = DecodeBudget::new(1024);
1314        validate_jpeg_dimensions(2, 2, 3, 4, &mut budget).unwrap();
1315    }
1316
1317    #[test]
1318    fn jpx_passthrough_consumes_decode_budget() {
1319        let mut dict = PdfDict::new();
1320        dict.insert(
1321            PdfName::new("Filter"),
1322            PdfObject::Name(PdfName::new("JPXDecode")),
1323        );
1324        let limits = ParseLimits {
1325            max_decoded_stream_bytes: 3,
1326            ..ParseLimits::default()
1327        };
1328        assert!(decode_stream_with_limits(b"four", &dict, &limits).is_err());
1329    }
1330}