diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..4b6c79f5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/crates/psd/src/descriptor.rs b/crates/psd/src/descriptor.rs index d426f714..b2cebbf4 100644 --- a/crates/psd/src/descriptor.rs +++ b/crates/psd/src/descriptor.rs @@ -118,7 +118,7 @@ pub(crate) fn read_id(r: &mut Reader) -> Result { pub(crate) fn read_unicode(r: &mut Reader) -> Result { let n = r.u32()? as usize; - let mut v = Vec::with_capacity(n); + let mut v = Vec::with_capacity(n.min(r.remaining() / 2)); for _ in 0..n { v.push(r.u16()?); } diff --git a/crates/psd/src/lib.rs b/crates/psd/src/lib.rs index de3ec182..10d4ebbe 100644 --- a/crates/psd/src/lib.rs +++ b/crates/psd/src/lib.rs @@ -7,6 +7,8 @@ //! out as straight (unpremultiplied) RGBA `f32` in 0..1, display-encoded (32-bit documents, //! which are linear, are encoded to sRGB). +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + pub mod descriptor; pub mod engine_data; pub mod placed; @@ -41,6 +43,19 @@ impl std::error::Error for Error {} pub type Result = std::result::Result; +/// The most pixels one decoded image (the composite, a layer or a mask) may have: 2^28, e.g. +/// 16 384 × 16 384, 4 GiB as float RGBA. Bounds read from a file are untrusted, so a few bytes +/// can't make the reader allocate more than that. +const MAX_DECODE_PIXELS: u64 = 1 << 28; + +/// `w × h` as a pixel count, or an error when it exceeds [`MAX_DECODE_PIXELS`]. +fn decode_pixels(w: usize, h: usize) -> Result { + match (w as u64).checked_mul(h as u64) { + Some(n) if n <= MAX_DECODE_PIXELS => Ok(n as usize), + _ => Err(Error::Unsupported(format!("{w}×{h} pixels is too large to decode"))), + } +} + /// Whether `bytes` start like a Photoshop document. pub fn is_psd(bytes: &[u8]) -> bool { bytes.len() >= 6 && &bytes[..4] == b"8BPS" && matches!(u16::from_be_bytes([bytes[4], bytes[5]]), 1 | 2) @@ -108,10 +123,10 @@ impl Rect { Rect { top, left, bottom: top + height, right: left + width } } pub fn width(&self) -> u32 { - (self.right - self.left).max(0) as u32 + (i64::from(self.right) - i64::from(self.left)).clamp(0, i64::from(u32::MAX)) as u32 } pub fn height(&self) -> u32 { - (self.bottom - self.top).max(0) as u32 + (i64::from(self.bottom) - i64::from(self.top)).clamp(0, i64::from(u32::MAX)) as u32 } pub fn is_empty(&self) -> bool { self.width() == 0 || self.height() == 0 @@ -780,11 +795,12 @@ impl Psd { let end = offset.checked_add(len).filter(|&e| e <= self.data.len()).ok_or(Error::Truncated)?; let mut r = Reader::at(&self.data[..end], offset); let comp = r.u16()?; + decode_pixels(w, h)?; let rowb = self.row_bytes(w); let raw = match comp { 0 => r.bytes(rowb * h)?.to_vec(), 1 => { - let mut counts = Vec::with_capacity(h); + let mut counts = Vec::with_capacity(h.min(r.remaining() / 2)); for _ in 0..h { counts.push(if self.psb { r.u32()? as usize } else { r.u16()? as usize }); } @@ -796,7 +812,7 @@ impl Psd { } 2 | 3 => { let rest = r.bytes(r.remaining())?; - let mut v = miniz_oxide::inflate::decompress_to_vec_zlib(rest).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; + let mut v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; v.resize(rowb * h, 0); if comp == 3 { self.unpredict(&mut v, w, h); @@ -891,6 +907,7 @@ impl Psd { /// The merged (flattened) image. pub fn composite(&self) -> Result { let (w, h) = (self.width as usize, self.height as usize); + decode_pixels(w, h)?; let mut r = Reader::at(&self.data, self.merged_offset); let comp = r.u16()?; let nch = self.channels as usize; @@ -905,7 +922,7 @@ impl Psd { } } 1 => { - let mut counts = Vec::with_capacity(nch * h); + let mut counts = Vec::with_capacity((nch * h).min(r.remaining() / 2)); for _ in 0..nch * h { counts.push(if self.psb { r.u32()? as usize } else { r.u16()? as usize }); } @@ -919,7 +936,7 @@ impl Psd { } 2 | 3 => { let rest = r.bytes(r.remaining())?; - let v = miniz_oxide::inflate::decompress_to_vec_zlib(rest).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; + let v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?; for (c, p) in planes.iter_mut().enumerate().take(want) { let mut plane = v.get(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?.to_vec(); if comp == 3 { @@ -956,7 +973,7 @@ impl Psd { fill_rect }; let (ow, oh) = (out_rect.width() as usize, out_rect.height() as usize); - let mut out = vec![[0.0f32; 4]; ow * oh]; + let mut out = vec![[0.0f32; 4]; decode_pixels(ow, oh)?]; let src = fill_rect; let (sw, sh) = (src.width() as usize, src.height() as usize); if sw > 0 && sh > 0 { @@ -972,22 +989,26 @@ impl Psd { } } let fill = l.fill.map(|c| c.map(|v| v as f32)); - for y in 0..sh { - let dy = src.top + y as i32 - out_rect.top; - if dy < 0 || dy >= oh as i32 { - continue; - } - for x in 0..sw { - let dx = src.left + x as i32 - out_rect.left; - if dx < 0 || dx >= ow as i32 { - continue; - } + // The source rows / columns that land inside the output (64-bit: file bounds can span + // the whole i32 range). + let span = |s0: i32, n: usize, o0: i32, on: usize| { + let off = i64::from(o0) - i64::from(s0); + let lo = off.clamp(0, n as i64); + let hi = (off + on as i64).clamp(lo, n as i64); + (lo as usize..hi as usize, off) + }; + let (ys, oy) = span(src.top, sh, out_rect.top, oh); + let (xs, ox) = span(src.left, sw, out_rect.left, ow); + for y in ys { + let dy = (y as i64 - oy) as usize; + for x in xs.clone() { + let dx = (x as i64 - ox) as usize; let i = y * sw + x; let (rgb, a) = match fill { Some(f) if !l.has_pixels() => (f, 1.0), _ => (self.to_rgb(&planes, i), alpha.as_ref().map_or(1.0, |a| a[i])), }; - out[dy as usize * ow + dx as usize] = [rgb[0], rgb[1], rgb[2], a]; + out[dy * ow + dx] = [rgb[0], rgb[1], rgb[2], a]; } } } diff --git a/crates/psd/src/tests.rs b/crates/psd/src/tests.rs index cd2f9293..235163b6 100644 --- a/crates/psd/src/tests.rs +++ b/crates/psd/src/tests.rs @@ -218,3 +218,37 @@ fn smart_objects_and_embedded_files() { assert_eq!((e.width, e.height), (20, 10)); assert!(p.layers[0].smart_object.is_none()); } + +/// Replace the first layer record's bounds (`top, left, bottom, right`) in a written document. +fn patch_layer_rect(bytes: &mut [u8], from: Rect, to: Rect) { + let enc = |r: Rect| [r.top, r.left, r.bottom, r.right].iter().flat_map(|v| v.to_be_bytes()).collect::>(); + let (old, new) = (enc(from), enc(to)); + let at = bytes.windows(16).position(|w| w == old.as_slice()).expect("layer rect in the written file"); + bytes[at..at + 16].copy_from_slice(&new); +} + +#[test] +fn extreme_layer_bounds_error_instead_of_overflowing() { + // A layer spanning the whole i32 range: its width overflowed i32 subtraction. + let r = Rect::new(2, 3, 4, 5); + let mut d = WDoc::new(8, 8); + d.layers = vec![WLayer::solid("L", r, [1.0, 0.0, 0.0, 1.0])]; + let mut bytes = write(&d); + patch_layer_rect(&mut bytes, r, Rect { top: i32::MIN, left: i32::MIN, bottom: i32::MAX, right: i32::MAX }); + let p = Psd::parse(bytes).unwrap(); + assert_eq!(p.layers[0].rect.width(), u32::MAX); + assert!(p.layer_pixels(0, false).is_err()); + assert!(p.layer_pixels(0, true).is_err()); +} + +#[test] +fn huge_layer_bounds_error_instead_of_allocating() { + // A few bytes claiming a 2-billion-pixel-square layer must not try to allocate it. + let r = Rect::new(2, 3, 4, 5); + let mut d = WDoc::new(8, 8); + d.layers = vec![WLayer::solid("L", r, [1.0, 0.0, 0.0, 1.0])]; + let mut bytes = write(&d); + patch_layer_rect(&mut bytes, r, Rect { top: 0, left: 0, bottom: i32::MAX, right: i32::MAX }); + let p = Psd::parse(bytes).unwrap(); + assert!(matches!(p.layer_pixels(0, false), Err(Error::Unsupported(_)))); +}