205 lines
7.2 KiB
Rust
205 lines
7.2 KiB
Rust
//! Malformed input must produce errors, never panics or huge allocations.
|
|
|
|
mod common;
|
|
use common::*;
|
|
use photocraft_codecs::*;
|
|
use proptest::prelude::*;
|
|
|
|
fn tight() -> DecodeOptions {
|
|
DecodeOptions { limits: Limits { max_width: 4096, max_height: 4096, max_pixels: 1 << 22, max_alloc: 64 << 20 } }
|
|
}
|
|
|
|
fn samples() -> Vec<(Format, Vec<u8>)> {
|
|
let mut v = Vec::new();
|
|
for f in rw_formats() {
|
|
for (l, s) in [(ChannelLayout::Rgba, SampleType::U8), (ChannelLayout::Gray, SampleType::U16), (ChannelLayout::Rgb, SampleType::F32)] {
|
|
let mut img = synth(19, 13, l, s, 11, 0.3);
|
|
img.icc = Some(sample_icc(300));
|
|
img.meta.exif = Some(sample_exif());
|
|
img.meta.xmp = Some(SAMPLE_XMP.into());
|
|
v.push((f, encode(&img, f, &EncodeOptions::default()).unwrap()));
|
|
}
|
|
}
|
|
// Extra variants: interlaced PNG, ASCII PNM, CMYK JPEG.
|
|
let img = synth(19, 13, ChannelLayout::Rgb, SampleType::U8, 1, 0.3);
|
|
v.push((Format::Png, encode(&img, Format::Png, &EncodeOptions { png_interlaced: true, ..Default::default() }).unwrap()));
|
|
v.push((Format::Pnm, b"P3\n2 2\n255\n0 1 2 3 4 5 6 7 8 9 10 11\n".to_vec()));
|
|
v.push((Format::Jpeg, encode(&synth(19, 13, ChannelLayout::Cmyk, SampleType::U8, 1, 0.3), Format::Jpeg, &EncodeOptions::default()).unwrap()));
|
|
v
|
|
}
|
|
|
|
fn no_panic(f: Format, bytes: &[u8]) {
|
|
let _ = decode_with(bytes, &tight());
|
|
let _ = decode_as_with(f, bytes, &tight());
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_at_many_offsets() {
|
|
for (f, bytes) in samples() {
|
|
let n = bytes.len();
|
|
let step = (n / 97).max(1);
|
|
let mut cuts: Vec<usize> = (0..n.min(80)).collect();
|
|
cuts.extend((0..n).step_by(step));
|
|
cuts.extend(n.saturating_sub(40)..n);
|
|
for cut in cuts {
|
|
no_panic(f, &bytes[..cut]);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_files_mostly_error() {
|
|
// Chopping off the second half must not silently succeed for formats
|
|
// with length-checked rasters.
|
|
for (f, bytes) in samples() {
|
|
if matches!(f, Format::Png | Format::Pnm | Format::Qoi | Format::OpenExr | Format::Tiff | Format::Bmp) {
|
|
let r = decode_as_with(f, &bytes[..bytes.len() / 2], &tight());
|
|
assert!(r.is_err(), "{f:?} decoded half a file");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bit_flips_never_panic() {
|
|
let mut rng = common::Rng::new(42);
|
|
for (f, bytes) in samples() {
|
|
for _ in 0..150 {
|
|
let mut b = bytes.clone();
|
|
let flips = 1 + (rng.next_u64() % 4) as usize;
|
|
for _ in 0..flips {
|
|
let i = (rng.next_u64() as usize) % b.len();
|
|
b[i] ^= 1 << (rng.next_u64() % 8);
|
|
}
|
|
no_panic(f, &b);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn byte_overwrites_in_header_never_panic() {
|
|
for (f, bytes) in samples() {
|
|
for i in 0..bytes.len().min(64) {
|
|
for v in [0x00, 0x7F, 0xFF] {
|
|
let mut b = bytes.clone();
|
|
b[i] = v;
|
|
no_panic(f, &b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_input_errors_for_every_format() {
|
|
for f in Format::ALL {
|
|
assert!(decode_as(f, &[]).is_err(), "{f:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pnm_specific_malformations() {
|
|
let cases: &[&[u8]] = &[
|
|
b"P6\n",
|
|
b"P6\n2 2\n",
|
|
b"P6\n2 2\n255",
|
|
b"P6\n2 2\n0\n",
|
|
b"P6\n2 2\n70000\n",
|
|
b"P6\n-2 2\n255\n",
|
|
b"P6\n2 2\n255\n\x01\x02",
|
|
b"P5\n2 2\n65535\n\x00\x00\x00",
|
|
b"P2\n2 2\n10\n1 2 3 11\n",
|
|
b"P1\n2 2\n0 1 2 0\n",
|
|
b"P4\n9 2\n\x00",
|
|
b"P7\nWIDTH 2\nHEIGHT 2\nDEPTH 9\nMAXVAL 255\nENDHDR\n",
|
|
b"P7\nWIDTH 2\nHEIGHT 2\nMAXVAL 255\nENDHDR\n",
|
|
b"P7\nWIDTH 2\nBOGUS 3\nENDHDR\n",
|
|
b"PF\n2 2\n0.0\n",
|
|
b"PF\n2 2\nnan\n",
|
|
b"PF\n2 2\n-1.0\n\0\0\0\0",
|
|
b"P6 4294967295 4294967295 255\n",
|
|
];
|
|
for c in cases {
|
|
assert!(decode(c).is_err(), "{:?}", String::from_utf8_lossy(c));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pnm_valid_variants() {
|
|
let img = decode(b"P1\n# comment\n3 2\n1 0 1\n0 1 0\n").unwrap();
|
|
assert_eq!(img.data(), &[0, 255, 0, 255, 0, 255]);
|
|
let img = decode(b"P1\n3 2\n101010").unwrap();
|
|
assert_eq!(img.data(), &[0, 255, 0, 255, 0, 255]);
|
|
let img = decode(b"P4\n3 2\n\xA0\x40").unwrap();
|
|
assert_eq!(img.data(), &[0, 255, 0, 255, 0, 255]);
|
|
let img = decode(b"P2\n2 1\n15\n0 15\n").unwrap();
|
|
assert_eq!(img.data(), &[0, 255]);
|
|
let img = decode(b"P2\n2 1\n1023\n0 1023\n").unwrap();
|
|
assert_eq!(img.sample_type(), SampleType::U16);
|
|
assert_eq!(img.to_u16_samples().unwrap(), vec![0, 65535]);
|
|
let img = decode(b"P3\n1 1\n255\n1 2 3\n").unwrap();
|
|
assert_eq!((img.layout(), img.data()), (ChannelLayout::Rgb, &[1u8, 2, 3][..]));
|
|
let img = decode(b"P7\nWIDTH 1\nHEIGHT 1\nDEPTH 2\nMAXVAL 255\nTUPLTYPE GRAYSCALE_ALPHA\nENDHDR\n\x10\x20").unwrap();
|
|
assert_eq!((img.layout(), img.data()), (ChannelLayout::GrayA, &[0x10u8, 0x20][..]));
|
|
let mut pfm = b"PF\n1 2\n1.0\n".to_vec(); // big-endian, bottom-up
|
|
for v in [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0] {
|
|
pfm.extend_from_slice(&v.to_be_bytes());
|
|
}
|
|
let img = decode(&pfm).unwrap();
|
|
assert_eq!(img.to_f32_samples().unwrap(), vec![4.0, 5.0, 6.0, 1.0, 2.0, 3.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn jpeg_marker_garbage() {
|
|
let cases: &[&[u8]] = &[
|
|
&[0xFF, 0xD8, 0xFF],
|
|
&[0xFF, 0xD8, 0xFF, 0xE0, 0x00],
|
|
&[0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x01],
|
|
&[0xFF, 0xD8, 0xFF, 0xE2, 0x00, 0x10, b'I', b'C', b'C', b'_', b'P', b'R', b'O', b'F', b'I', b'L', b'E', 0, 1, 1],
|
|
&[0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B, 8, 0xFF, 0xFF, 0xFF, 0xFF, 3, 0, 0, 0],
|
|
&[0xFF, 0xD8, 0xFF, 0xD9],
|
|
];
|
|
for c in cases {
|
|
assert!(decode(c).is_err());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig { cases: 400, .. ProptestConfig::default() })]
|
|
|
|
#[test]
|
|
fn random_bytes_never_panic(data in proptest::collection::vec(any::<u8>(), 0..2048)) {
|
|
let _ = decode_with(&data, &tight());
|
|
}
|
|
|
|
#[test]
|
|
fn random_bytes_with_magic_never_panic(idx in 0usize..13, data in proptest::collection::vec(any::<u8>(), 0..1024)) {
|
|
let magics: [&[u8]; 13] = [
|
|
b"\x89PNG\r\n\x1a\n", &[0xFF, 0xD8, 0xFF], b"II*\0", b"RIFF\0\0\0\0WEBP", b"GIF89a", b"BM",
|
|
&[0, 0, 1, 0, 1, 0], b"P6\n", b"qoif", &[0x76, 0x2F, 0x31, 0x01], b"#?RADIANCE\n", b"P7\n", b"PF\n",
|
|
];
|
|
let mut b = magics[idx].to_vec();
|
|
b.extend_from_slice(&data);
|
|
let _ = decode_with(&b, &tight());
|
|
}
|
|
|
|
#[test]
|
|
fn random_bytes_each_format_never_panic(idx in 0usize..13, data in proptest::collection::vec(any::<u8>(), 0..512)) {
|
|
let _ = decode_as_with(Format::ALL[idx], &data, &tight());
|
|
}
|
|
|
|
#[test]
|
|
fn mutated_valid_files_never_panic(which in 0usize..40, pos in any::<usize>(), val in any::<u8>(), cut in any::<usize>()) {
|
|
let s = samples_cached();
|
|
let (f, bytes) = &s[which % s.len()];
|
|
let mut b = bytes.clone();
|
|
let p = pos % b.len();
|
|
b[p] = val;
|
|
let c = cut % (b.len() + 1);
|
|
let _ = decode_as_with(*f, &b[..c.max(p + 1).min(b.len())], &tight());
|
|
}
|
|
}
|
|
|
|
fn samples_cached() -> &'static [(Format, Vec<u8>)] {
|
|
static S: std::sync::OnceLock<Vec<(Format, Vec<u8>)>> = std::sync::OnceLock::new();
|
|
S.get_or_init(samples)
|
|
}
|