Skip to main content

tiff_reader/
filters.rs

1//! Compression filter pipeline for TIFF strip/tile decompression.
2
3#[cfg(any(feature = "jpeg", feature = "zstd", feature = "webp"))]
4use std::io::Cursor;
5use std::io::Read;
6#[cfg(feature = "jpeg")]
7use std::panic::{self, AssertUnwindSafe};
8
9use crate::error::{Error, Result};
10use crate::header::ByteOrder;
11use tiff_core::{Compression, Predictor};
12
13#[derive(Debug, Clone, Copy)]
14#[cfg_attr(not(any(feature = "jpeg", feature = "webp")), allow(dead_code))]
15pub(crate) struct ImageDecodeLayout {
16    pub width: usize,
17    pub height: usize,
18    pub samples_per_pixel: usize,
19}
20
21/// Decompress a strip or tile according to the TIFF compression scheme.
22pub fn decompress(
23    compression: u16,
24    data: &[u8],
25    index: usize,
26    _jpeg_tables: Option<&[u8]>,
27    decoded_len_limit: usize,
28) -> Result<Vec<u8>> {
29    decompress_with_layout(
30        compression,
31        data,
32        index,
33        _jpeg_tables,
34        decoded_len_limit,
35        None,
36    )
37}
38
39pub(crate) fn decompress_with_layout(
40    compression: u16,
41    data: &[u8],
42    index: usize,
43    _jpeg_tables: Option<&[u8]>,
44    decoded_len_limit: usize,
45    expected_layout: Option<ImageDecodeLayout>,
46) -> Result<Vec<u8>> {
47    // The value is consumed only by feature-gated image codecs.
48    let _ = expected_layout;
49    match Compression::from_code(compression) {
50        Some(Compression::None) => {
51            if data.len() > decoded_len_limit {
52                return Err(decoded_block_too_large(
53                    index,
54                    "uncompressed",
55                    decoded_len_limit,
56                ));
57            }
58            Ok(data.to_vec())
59        }
60        Some(Compression::Deflate | Compression::DeflateOld) => {
61            decompress_deflate(data, index, decoded_len_limit)
62        }
63        Some(Compression::Lzw) => decompress_lzw(data, index, decoded_len_limit),
64        Some(Compression::PackBits) => decompress_packbits(data, index, decoded_len_limit),
65        Some(Compression::Lerc) => Err(Error::UnsupportedCompression(compression)),
66        #[cfg(feature = "jpeg")]
67        Some(Compression::OldJpeg) => Err(Error::UnsupportedCompression(compression)),
68        #[cfg(feature = "jpeg")]
69        Some(Compression::Jpeg) => decompress_jpeg(
70            data,
71            index,
72            _jpeg_tables,
73            decoded_len_limit,
74            expected_layout,
75        ),
76        #[cfg(not(feature = "jpeg"))]
77        Some(Compression::OldJpeg | Compression::Jpeg) => {
78            Err(Error::UnsupportedCompression(compression))
79        }
80        #[cfg(feature = "zstd")]
81        Some(Compression::Zstd) => decompress_zstd(data, index, decoded_len_limit),
82        #[cfg(not(feature = "zstd"))]
83        Some(Compression::Zstd) => Err(Error::UnsupportedCompression(compression)),
84        #[cfg(feature = "webp")]
85        Some(Compression::WebP) => decompress_webp(data, index, decoded_len_limit, expected_layout),
86        #[cfg(not(feature = "webp"))]
87        Some(Compression::WebP) => Err(Error::UnsupportedCompression(compression)),
88        None => Err(Error::UnsupportedCompression(compression)),
89    }
90}
91
92/// Normalize row bytes into native-endian decoded samples and reverse any TIFF predictor.
93pub fn fix_endianness_and_predict(
94    row: &mut [u8],
95    bit_depth: u16,
96    samples: u16,
97    byte_order: ByteOrder,
98    predictor: u16,
99) -> Result<()> {
100    fix_endianness_and_predict_with_scratch(
101        row,
102        bit_depth,
103        samples,
104        byte_order,
105        predictor,
106        &mut Vec::new(),
107    )
108}
109
110/// Scratch-buffer variant used by per-row decode loops so the floating-point
111/// predictor's working copy is not reallocated for every row.
112pub(crate) fn fix_endianness_and_predict_with_scratch(
113    row: &mut [u8],
114    bit_depth: u16,
115    samples: u16,
116    byte_order: ByteOrder,
117    predictor: u16,
118    scratch: &mut Vec<u8>,
119) -> Result<()> {
120    match Predictor::from_code(predictor) {
121        Some(Predictor::None) => {
122            fix_endianness(row, byte_order, bit_depth);
123            Ok(())
124        }
125        Some(Predictor::Horizontal) => {
126            fix_endianness(row, byte_order, bit_depth);
127            reverse_horizontal_predictor(row, bit_depth, samples);
128            Ok(())
129        }
130        Some(Predictor::FloatingPoint) => match bit_depth {
131            16 | 32 | 64 => {
132                scratch.clear();
133                scratch.extend_from_slice(row);
134                match bit_depth {
135                    16 => predict_f16(scratch, row, samples),
136                    32 => predict_f32(scratch, row, samples),
137                    _ => predict_f64(scratch, row, samples),
138                }
139                Ok(())
140            }
141            _ => Err(Error::UnsupportedPredictor(3)),
142        },
143        None => Err(Error::UnsupportedPredictor(predictor)),
144    }
145}
146
147fn decompress_deflate(data: &[u8], index: usize, decoded_len_limit: usize) -> Result<Vec<u8>> {
148    use flate2::read::ZlibDecoder;
149
150    let decoder = ZlibDecoder::new(data);
151    read_bounded_to_end(decoder, index, "deflate", decoded_len_limit)
152}
153
154fn decompress_lzw(data: &[u8], index: usize, decoded_len_limit: usize) -> Result<Vec<u8>> {
155    use weezl::decode::Configuration;
156    use weezl::{BitOrder, LzwStatus};
157
158    let mut decoder = Configuration::with_tiff_size_switch(BitOrder::Msb, 8)
159        .with_yield_on_full_buffer(true)
160        .build();
161    let probe_limit = decoded_len_probe_limit(index, "LZW", decoded_len_limit)?;
162    let mut out = Vec::with_capacity(decoded_len_limit.min(8192));
163    let mut input_offset = 0usize;
164    let mut scratch = [0u8; 8192];
165
166    loop {
167        let remaining = probe_limit.saturating_sub(out.len());
168        if remaining == 0 {
169            return Err(decoded_block_too_large(index, "LZW", decoded_len_limit));
170        }
171
172        let output_len = remaining.min(scratch.len());
173        let result = decoder.decode_bytes(&data[input_offset..], &mut scratch[..output_len]);
174        input_offset += result.consumed_in;
175        out.extend_from_slice(&scratch[..result.consumed_out]);
176        if out.len() > decoded_len_limit {
177            return Err(decoded_block_too_large(index, "LZW", decoded_len_limit));
178        }
179
180        match result.status {
181            Err(e) => {
182                return Err(Error::DecompressionFailed {
183                    index,
184                    reason: format!("LZW: {e}"),
185                })
186            }
187            Ok(LzwStatus::Done) => return Ok(out),
188            Ok(LzwStatus::Ok) => {
189                if result.consumed_in == 0 && result.consumed_out == 0 {
190                    return Err(Error::DecompressionFailed {
191                        index,
192                        reason: "LZW: decoder made no progress".into(),
193                    });
194                }
195            }
196            Ok(LzwStatus::NoProgress) => {
197                if result.consumed_out == output_len {
198                    continue;
199                }
200                return Err(Error::DecompressionFailed {
201                    index,
202                    reason: "LZW: stream ended before end marker".into(),
203                });
204            }
205        }
206    }
207}
208
209fn decompress_packbits(data: &[u8], index: usize, decoded_len_limit: usize) -> Result<Vec<u8>> {
210    let probe_limit = decoded_len_probe_limit(index, "PackBits", decoded_len_limit)?;
211    let mut out = Vec::new();
212    let mut cursor = 0usize;
213
214    while cursor < data.len() {
215        let header = data[cursor] as i8;
216        cursor += 1;
217
218        if header >= 0 {
219            let count = header as usize + 1;
220            let end = cursor + count;
221            if end > data.len() {
222                return Err(Error::DecompressionFailed {
223                    index,
224                    reason: "PackBits literal run is truncated".into(),
225                });
226            }
227            append_bounded_bytes(
228                &mut out,
229                &data[cursor..end],
230                index,
231                "PackBits",
232                decoded_len_limit,
233                probe_limit,
234            )?;
235            cursor = end;
236        } else if header != -128 {
237            if cursor >= data.len() {
238                return Err(Error::DecompressionFailed {
239                    index,
240                    reason: "PackBits repeat run is truncated".into(),
241                });
242            }
243            let count = (1i16 - header as i16) as usize;
244            let byte = data[cursor];
245            cursor += 1;
246            append_bounded_repeat(
247                &mut out,
248                byte,
249                count,
250                index,
251                "PackBits",
252                decoded_len_limit,
253                probe_limit,
254            )?;
255        }
256    }
257
258    Ok(out)
259}
260
261#[cfg(feature = "jpeg")]
262fn decompress_jpeg(
263    data: &[u8],
264    index: usize,
265    jpeg_tables: Option<&[u8]>,
266    decoded_len_limit: usize,
267    expected_layout: Option<ImageDecodeLayout>,
268) -> Result<Vec<u8>> {
269    let stream = merge_jpeg_stream(jpeg_tables, data);
270    panic::catch_unwind(AssertUnwindSafe(|| {
271        let mut decoder = jpeg_decoder::Decoder::new(Cursor::new(stream));
272        decoder.set_max_decoding_buffer_size(decoded_len_limit);
273        decoder.read_info()?;
274        validate_jpeg_metadata(&decoder, decoded_len_limit, expected_layout)?;
275        decoder.decode()
276    }))
277    .map_err(|payload| Error::DecompressionFailed {
278        index,
279        reason: format!(
280            "JPEG decoder panicked: {}",
281            panic_payload_message(payload.as_ref())
282        ),
283    })?
284    .map_err(|e| Error::DecompressionFailed {
285        index,
286        reason: format!("JPEG: {e}"),
287    })
288}
289
290#[cfg(feature = "jpeg")]
291fn validate_jpeg_metadata<R: std::io::Read>(
292    decoder: &jpeg_decoder::Decoder<R>,
293    decoded_len_limit: usize,
294    expected_layout: Option<ImageDecodeLayout>,
295) -> std::result::Result<(), jpeg_decoder::Error> {
296    let info = decoder.info().ok_or_else(|| {
297        jpeg_decoder::Error::Format("JPEG metadata missing after read_info".into())
298    })?;
299    let decoded_len = usize::from(info.width)
300        .checked_mul(usize::from(info.height))
301        .and_then(|pixels| pixels.checked_mul(info.pixel_format.pixel_bytes()))
302        .ok_or_else(|| jpeg_decoder::Error::Format("JPEG decoded size overflow".into()))?;
303    if decoded_len > decoded_len_limit {
304        return Err(jpeg_decoder::Error::Format(format!(
305            "JPEG decoded size {decoded_len} exceeds TIFF block budget {decoded_len_limit}"
306        )));
307    }
308    if let Some(expected) = expected_layout {
309        if usize::from(info.width) != expected.width || usize::from(info.height) != expected.height
310        {
311            return Err(jpeg_decoder::Error::Format(format!(
312                "JPEG dimensions {}x{} do not match TIFF block {}x{}",
313                info.width, info.height, expected.width, expected.height
314            )));
315        }
316        if info.pixel_format.pixel_bytes() != expected.samples_per_pixel {
317            return Err(jpeg_decoder::Error::Format(format!(
318                "JPEG channel count {} does not match TIFF block channel count {}",
319                info.pixel_format.pixel_bytes(),
320                expected.samples_per_pixel
321            )));
322        }
323    }
324    Ok(())
325}
326
327#[cfg(feature = "zstd")]
328fn decompress_zstd(data: &[u8], index: usize, decoded_len_limit: usize) -> Result<Vec<u8>> {
329    let decoder = ruzstd::decoding::StreamingDecoder::new(Cursor::new(data)).map_err(|error| {
330        Error::DecompressionFailed {
331            index,
332            reason: format!("ZSTD: {error}"),
333        }
334    })?;
335    read_bounded_to_end(decoder, index, "ZSTD", decoded_len_limit)
336}
337
338#[cfg(feature = "webp")]
339fn decompress_webp(
340    data: &[u8],
341    index: usize,
342    decoded_len_limit: usize,
343    expected_layout: Option<ImageDecodeLayout>,
344) -> Result<Vec<u8>> {
345    let mut decoder = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
346        image_webp::WebPDecoder::new(Cursor::new(data))
347    }))
348    .map_err(|payload| Error::DecompressionFailed {
349        index,
350        reason: format!(
351            "WebP decoder panicked: {}",
352            webp_panic_payload_message(payload.as_ref())
353        ),
354    })?
355    .map_err(|error| Error::DecompressionFailed {
356        index,
357        reason: format!("WebP: {error}"),
358    })?;
359
360    if decoder.is_animated() {
361        return Err(Error::DecompressionFailed {
362            index,
363            reason: "WebP: animated payloads are not valid TIFF blocks".into(),
364        });
365    }
366    if let Some(expected) = expected_layout {
367        let (actual_width, actual_height) = decoder.dimensions();
368        if actual_width as usize != expected.width || actual_height as usize != expected.height {
369            return Err(Error::DecompressionFailed {
370                index,
371                reason: format!(
372                    "WebP dimensions {actual_width}x{actual_height} do not match TIFF block {}x{}",
373                    expected.width, expected.height
374                ),
375            });
376        }
377        let actual_samples = if decoder.has_alpha() { 4 } else { 3 };
378        if actual_samples != expected.samples_per_pixel {
379            return Err(Error::DecompressionFailed {
380                index,
381                reason: format!(
382                    "WebP channel count {actual_samples} does not match TIFF block channel count {}",
383                    expected.samples_per_pixel
384                ),
385            });
386        }
387    }
388
389    let output_len = decoder
390        .output_buffer_size()
391        .ok_or_else(|| Error::DecompressionFailed {
392            index,
393            reason: "WebP output size overflows usize".into(),
394        })?;
395    if output_len > decoded_len_limit {
396        return Err(decoded_block_too_large(index, "WebP", decoded_len_limit));
397    }
398
399    let mut out = vec![0u8; output_len];
400    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
401        decoder.read_image(&mut out)
402    }))
403    .map_err(|payload| Error::DecompressionFailed {
404        index,
405        reason: format!(
406            "WebP decoder panicked: {}",
407            webp_panic_payload_message(payload.as_ref())
408        ),
409    })?
410    .map_err(|error| Error::DecompressionFailed {
411        index,
412        reason: format!("WebP: {error}"),
413    })?;
414    Ok(out)
415}
416
417#[cfg(feature = "webp")]
418fn webp_panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
419    if let Some(message) = payload.downcast_ref::<&'static str>() {
420        (*message).to_string()
421    } else if let Some(message) = payload.downcast_ref::<String>() {
422        message.clone()
423    } else {
424        "unknown panic payload".into()
425    }
426}
427
428fn read_bounded_to_end<R: Read>(
429    reader: R,
430    index: usize,
431    codec: &'static str,
432    decoded_len_limit: usize,
433) -> Result<Vec<u8>> {
434    let probe_limit = decoded_len_probe_limit(index, codec, decoded_len_limit)?;
435    let mut reader = reader.take(probe_limit as u64);
436    let mut out = Vec::with_capacity(decoded_len_limit.min(64 * 1024));
437    let mut scratch = vec![0u8; probe_limit.min(64 * 1024)];
438
439    loop {
440        let remaining = probe_limit.saturating_sub(out.len());
441        if remaining == 0 {
442            break;
443        }
444        let read_len = remaining.min(scratch.len());
445        let bytes_read =
446            reader
447                .read(&mut scratch[..read_len])
448                .map_err(|e| Error::DecompressionFailed {
449                    index,
450                    reason: format!("{codec}: {e}"),
451                })?;
452        if bytes_read == 0 {
453            break;
454        }
455        out.extend_from_slice(&scratch[..bytes_read]);
456        if out.len() > decoded_len_limit {
457            return Err(decoded_block_too_large(index, codec, decoded_len_limit));
458        }
459    }
460
461    Ok(out)
462}
463
464fn decoded_len_probe_limit(
465    index: usize,
466    codec: &'static str,
467    decoded_len_limit: usize,
468) -> Result<usize> {
469    decoded_len_limit
470        .checked_add(1)
471        .ok_or_else(|| Error::DecompressionFailed {
472            index,
473            reason: format!("{codec}: TIFF block budget is too large to probe safely"),
474        })
475}
476
477fn decoded_block_too_large(index: usize, codec: &'static str, decoded_len_limit: usize) -> Error {
478    Error::DecompressionFailed {
479        index,
480        reason: format!("{codec}: decoded block exceeds TIFF block budget {decoded_len_limit}"),
481    }
482}
483
484fn append_bounded_bytes(
485    out: &mut Vec<u8>,
486    bytes: &[u8],
487    index: usize,
488    codec: &'static str,
489    decoded_len_limit: usize,
490    probe_limit: usize,
491) -> Result<()> {
492    let remaining = probe_limit.saturating_sub(out.len());
493    let copy_len = bytes.len().min(remaining);
494    out.extend_from_slice(&bytes[..copy_len]);
495    if copy_len < bytes.len() || out.len() > decoded_len_limit {
496        return Err(decoded_block_too_large(index, codec, decoded_len_limit));
497    }
498    Ok(())
499}
500
501fn append_bounded_repeat(
502    out: &mut Vec<u8>,
503    byte: u8,
504    count: usize,
505    index: usize,
506    codec: &'static str,
507    decoded_len_limit: usize,
508    probe_limit: usize,
509) -> Result<()> {
510    let remaining = probe_limit.saturating_sub(out.len());
511    let copy_len = count.min(remaining);
512    out.resize(out.len() + copy_len, byte);
513    if copy_len < count || out.len() > decoded_len_limit {
514        return Err(decoded_block_too_large(index, codec, decoded_len_limit));
515    }
516    Ok(())
517}
518
519#[cfg(feature = "jpeg")]
520fn merge_jpeg_stream(jpeg_tables: Option<&[u8]>, scan_data: &[u8]) -> Vec<u8> {
521    if jpeg_tables.is_none() {
522        return scan_data.to_vec();
523    }
524
525    let tables = jpeg_tables.unwrap_or_default();
526    let table_body = match tables.strip_suffix(&[0xff, 0xd9]) {
527        Some(without_eoi) => without_eoi,
528        None => tables,
529    };
530    let scan_body = match scan_data.strip_prefix(&[0xff, 0xd8]) {
531        Some(without_soi) => without_soi,
532        None => scan_data,
533    };
534
535    let mut merged = Vec::with_capacity(table_body.len() + scan_body.len() + 2);
536    if table_body.starts_with(&[0xff, 0xd8]) {
537        merged.extend_from_slice(table_body);
538    } else {
539        merged.extend_from_slice(&[0xff, 0xd8]);
540        merged.extend_from_slice(table_body);
541    }
542    merged.extend_from_slice(scan_body);
543    if !merged.ends_with(&[0xff, 0xd9]) {
544        merged.extend_from_slice(&[0xff, 0xd9]);
545    }
546    merged
547}
548
549#[cfg(feature = "jpeg")]
550fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
551    if let Some(message) = payload.downcast_ref::<&'static str>() {
552        (*message).to_string()
553    } else if let Some(message) = payload.downcast_ref::<String>() {
554        message.clone()
555    } else {
556        "unknown panic payload".into()
557    }
558}
559
560fn fix_endianness(buf: &mut [u8], byte_order: ByteOrder, bit_depth: u16) {
561    let host_is_little_endian = cfg!(target_endian = "little");
562    let data_is_little_endian = matches!(byte_order, ByteOrder::LittleEndian);
563    if host_is_little_endian == data_is_little_endian {
564        return;
565    }
566
567    let chunk = match bit_depth {
568        0..=8 => 1,
569        9..=16 => 2,
570        17..=32 => 4,
571        _ => 8,
572    };
573    if chunk == 1 {
574        return;
575    }
576
577    for value in buf.chunks_exact_mut(chunk) {
578        value.reverse();
579    }
580}
581
582fn reverse_horizontal_predictor(buf: &mut [u8], bit_depth: u16, samples: u16) {
583    let bytes_per_value = match bit_depth {
584        0..=8 => 1,
585        9..=16 => 2,
586        17..=32 => 4,
587        _ => 8,
588    };
589    let lookback = usize::from(samples) * bytes_per_value;
590
591    match bytes_per_value {
592        1 => {
593            for index in lookback..buf.len() {
594                buf[index] = buf[index].wrapping_add(buf[index - lookback]);
595            }
596        }
597        2 => {
598            for index in (lookback..buf.len()).step_by(2) {
599                let current = u16::from_ne_bytes(buf[index..index + 2].try_into().unwrap());
600                let previous = u16::from_ne_bytes(
601                    buf[index - lookback..index - lookback + 2]
602                        .try_into()
603                        .unwrap(),
604                );
605                buf[index..index + 2]
606                    .copy_from_slice(&current.wrapping_add(previous).to_ne_bytes());
607            }
608        }
609        4 => {
610            for index in (lookback..buf.len()).step_by(4) {
611                let current = u32::from_ne_bytes(buf[index..index + 4].try_into().unwrap());
612                let previous = u32::from_ne_bytes(
613                    buf[index - lookback..index - lookback + 4]
614                        .try_into()
615                        .unwrap(),
616                );
617                buf[index..index + 4]
618                    .copy_from_slice(&current.wrapping_add(previous).to_ne_bytes());
619            }
620        }
621        _ => {
622            for index in (lookback..buf.len()).step_by(8) {
623                let current = u64::from_ne_bytes(buf[index..index + 8].try_into().unwrap());
624                let previous = u64::from_ne_bytes(
625                    buf[index - lookback..index - lookback + 8]
626                        .try_into()
627                        .unwrap(),
628                );
629                buf[index..index + 8]
630                    .copy_from_slice(&current.wrapping_add(previous).to_ne_bytes());
631            }
632        }
633    }
634}
635
636fn predict_f16(input: &mut [u8], output: &mut [u8], samples: u16) {
637    let samples = usize::from(samples);
638    for i in samples..input.len() {
639        input[i] = input[i].wrapping_add(input[i - samples]);
640    }
641    for (i, chunk) in output.chunks_mut(2).enumerate() {
642        chunk.copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes([
643            input[i],
644            input[input.len() / 2 + i],
645        ])));
646    }
647}
648
649fn predict_f32(input: &mut [u8], output: &mut [u8], samples: u16) {
650    let samples = usize::from(samples);
651    for i in samples..input.len() {
652        input[i] = input[i].wrapping_add(input[i - samples]);
653    }
654    for (i, chunk) in output.chunks_mut(4).enumerate() {
655        chunk.copy_from_slice(&u32::to_ne_bytes(u32::from_be_bytes([
656            input[i],
657            input[input.len() / 4 + i],
658            input[input.len() / 2 + i],
659            input[input.len() / 4 * 3 + i],
660        ])));
661    }
662}
663
664fn predict_f64(input: &mut [u8], output: &mut [u8], samples: u16) {
665    let samples = usize::from(samples);
666    for i in samples..input.len() {
667        input[i] = input[i].wrapping_add(input[i - samples]);
668    }
669    for (i, chunk) in output.chunks_mut(8).enumerate() {
670        chunk.copy_from_slice(&u64::to_ne_bytes(u64::from_be_bytes([
671            input[i],
672            input[input.len() / 8 + i],
673            input[input.len() / 8 * 2 + i],
674            input[input.len() / 8 * 3 + i],
675            input[input.len() / 8 * 4 + i],
676            input[input.len() / 8 * 5 + i],
677            input[input.len() / 8 * 6 + i],
678            input[input.len() / 8 * 7 + i],
679        ])));
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use std::path::Path;
686
687    #[cfg(not(feature = "jpeg"))]
688    use super::decompress;
689    #[cfg(feature = "jpeg")]
690    use super::{decompress, merge_jpeg_stream};
691    use super::{decompress_lzw, decompress_packbits, fix_endianness_and_predict};
692    use crate::header::ByteOrder;
693    use std::io::Write;
694    use tiff_core::Compression;
695
696    #[test]
697    fn horizontal_predictor_restores_u16_rows() {
698        let mut row = vec![1, 0, 1, 0, 2, 0];
699        fix_endianness_and_predict(&mut row, 16, 1, ByteOrder::LittleEndian, 2).unwrap();
700        assert_eq!(row, vec![1, 0, 2, 0, 4, 0]);
701    }
702
703    #[test]
704    fn packbits_decoder_rejects_truncated_repeat_run() {
705        let err = decompress_packbits(&[0xff], 0, 1).unwrap_err();
706        assert!(err.to_string().contains("PackBits"));
707    }
708
709    #[test]
710    fn deflate_decoder_rejects_blocks_that_exceed_budget() {
711        let payload = [0x2a; 128];
712        let mut encoder =
713            flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
714        encoder.write_all(&payload).unwrap();
715        let compressed = encoder.finish().unwrap();
716
717        let err =
718            decompress(Compression::Deflate.to_code(), &compressed, 0, None, 127).unwrap_err();
719        assert!(err.to_string().contains("block budget"));
720    }
721
722    #[test]
723    fn lzw_decoder_rejects_blocks_that_exceed_budget() {
724        let payload = [0x2a; 128];
725        let compressed = weezl::encode::Encoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8)
726            .encode(&payload)
727            .unwrap();
728
729        let err = decompress_lzw(&compressed, 0, 127).unwrap_err();
730        assert!(err.to_string().contains("block budget"));
731    }
732
733    #[test]
734    fn packbits_decoder_rejects_blocks_that_exceed_budget() {
735        let err = decompress_packbits(&[0x81, 0x2a], 0, 127).unwrap_err();
736        assert!(err.to_string().contains("block budget"));
737    }
738
739    #[test]
740    fn uncompressed_decoder_rejects_blocks_that_exceed_budget() {
741        let err =
742            decompress(Compression::None.to_code(), &[1, 2, 3, 4, 5], 0, None, 4).unwrap_err();
743        assert!(err.to_string().contains("block budget"));
744    }
745
746    #[test]
747    fn lzw_real_cog_tile_requires_repeated_trailer_bytes() {
748        let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
749            .join("../testdata/interoperability/gdal/gcore/data/cog/byte_little_endian_golden.tif");
750        let bytes = std::fs::read(fixture).unwrap();
751
752        let without_trailer = &bytes[570..570 + 1223];
753        let with_trailer = &bytes[570..570 + 1227];
754
755        assert!(decompress_lzw(without_trailer, 0, 1_000_000).is_ok());
756        assert!(decompress_lzw(with_trailer, 0, 1_000_000).is_ok());
757    }
758
759    #[cfg(feature = "zstd")]
760    #[test]
761    fn zstd_decoder_rejects_blocks_that_exceed_budget() {
762        let payload = [0x2a; 128];
763        let compressed = ruzstd::encoding::compress_to_vec(
764            &payload[..],
765            ruzstd::encoding::CompressionLevel::Fastest,
766        );
767
768        let err = decompress(Compression::Zstd.to_code(), &compressed, 0, None, 127).unwrap_err();
769        assert!(err.to_string().contains("block budget"));
770    }
771
772    #[cfg(feature = "jpeg")]
773    #[test]
774    fn merges_jpeg_tables_with_abbreviated_scan() {
775        let merged = merge_jpeg_stream(
776            Some(&[0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0xff, 0xd9]),
777            &[0xff, 0xda, 0x00, 0x08, 0x00],
778        );
779        assert_eq!(&merged[..6], &[0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43]);
780        assert!(merged.ends_with(&[0xff, 0xd9]));
781    }
782
783    #[cfg(feature = "jpeg")]
784    #[test]
785    fn jpeg_decoder_rejects_frame_sizes_that_exceed_tiff_budget() {
786        let mut jpeg = vec![
787            0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x14, 0x00, 0x14, 0x01, 0x01, 0x11,
788            0x00, 0xff, 0xc4, 0x00, 0x17, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
789            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x04, 0x06, 0xff, 0xc4,
790            0x00, 0x2a, 0x10, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x05, 0x05, 0x00, 0x00, 0x00,
791            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x11, 0x03, 0x04, 0x00, 0x18, 0x31, 0x41,
792            0x13, 0x21, 0x51, 0x71, 0x05, 0x22, 0x61, 0x91, 0xb1, 0x14, 0x42, 0x62, 0xc1, 0xf0,
793            0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x75, 0xc5, 0xb7, 0xd2,
794            0x31, 0x4a, 0x75, 0x51, 0xe0, 0x65, 0xf2, 0x19, 0xd8, 0x8d, 0x7d, 0xfe, 0x71, 0x19,
795            0x2b, 0x94, 0x54, 0x2c, 0x33, 0x38, 0x20, 0x2f, 0x7d, 0xf5, 0xd2, 0x40, 0x18, 0x6b,
796            0xdc, 0x3d, 0xa0, 0x44, 0x15, 0xc9, 0x2c, 0xa1, 0xc8, 0x5c, 0xa4, 0x2c, 0xed, 0xcc,
797            0x74, 0x83, 0xcb, 0xaf, 0x59, 0xc2, 0xaf, 0x0f, 0x02, 0xb3, 0x2e, 0x57, 0xfc, 0x79,
798            0x15, 0x9f, 0x58, 0xee, 0x3f, 0x7b, 0xe0, 0x59, 0x95, 0x84, 0x26, 0x56, 0xac, 0xc2,
799            0x62, 0xa0, 0x8c, 0xa4, 0x91, 0xc9, 0x44, 0xed, 0xa4, 0x9e, 0x9a, 0x08, 0xc1, 0x8a,
800            0x54, 0x9d, 0x41, 0xe3, 0xa4, 0xe8, 0x65, 0x01, 0xe7, 0xdc, 0xff, 0x00, 0x6d, 0x8d,
801            0x2f, 0x89, 0x5b, 0x50, 0xbe, 0xb9, 0x4a, 0x0d, 0x4c, 0x53, 0x51, 0x01, 0x8a, 0x31,
802            0x9a, 0x92, 0x22, 0x5a, 0x49, 0xe7, 0xda, 0x37, 0xeb, 0x8c, 0xc5, 0xc7, 0x0a, 0xd5,
803            0x87, 0x0a, 0x85, 0x30, 0xc7, 0xee, 0x69, 0x27, 0x40, 0x77, 0x3e, 0xbf, 0x18, 0x99,
804            0xae, 0x1c, 0xb6, 0xc0, 0x0d, 0x02, 0xf9, 0x47, 0xb0, 0x81, 0x8f, 0xff, 0xd9,
805        ];
806        jpeg[7] = 0x9b;
807        jpeg[8] = 0x43;
808        jpeg[9] = 0xee;
809        jpeg[10] = 0x23;
810
811        let error = decompress(Compression::Jpeg.to_code(), &jpeg, 0, None, 20 * 20).unwrap_err();
812        assert!(error.to_string().contains("block budget"));
813    }
814}