mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge pull request #2608 from storytold/fix/2141-header-sized-allocations
Media: header-sized EXR windows and audio sample rates are bounded (#2141)
This commit is contained in:
@@ -13,6 +13,10 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use effectcraft_raster::AuxChannels;
|
||||
use effectcraft_raster::channels3d::{BACKGROUND_DEPTH, split_channel};
|
||||
|
||||
/// The largest display window decoded (16384²; vector footage and Continuously Rasterize
|
||||
/// stop at the same size).
|
||||
const MAX_DISPLAY_PIXELS: usize = 1 << 28;
|
||||
|
||||
fn is_depth(name: &str) -> bool {
|
||||
let last = name.rsplit('.').next().unwrap_or(name);
|
||||
last.eq_ignore_ascii_case("z") || name.to_ascii_lowercase().contains("depth")
|
||||
@@ -75,7 +79,9 @@ pub fn read_exr_channels(bytes: &[u8]) -> Option<AuxChannels> {
|
||||
}
|
||||
};
|
||||
collect_meta(&img.attributes.other);
|
||||
let n = w.checked_mul(h)?;
|
||||
// Each channel is a display-window plane: a 60000² window over a 1×1 data window was a
|
||||
// multi-gigabyte allocation per channel (#2141).
|
||||
let n = w.checked_mul(h).filter(|n| *n <= MAX_DISPLAY_PIXELS)?;
|
||||
// The decoded image is consumed channel by channel, so each channel's samples are freed (or
|
||||
// moved) as its plane is made instead of the whole file being held twice (#474).
|
||||
for layer in img.layer_data {
|
||||
@@ -337,6 +343,18 @@ mod tests {
|
||||
assert_eq!(aux.get("GlossDir.R").and_then(|p| p.first()).copied(), Some(0.25));
|
||||
}
|
||||
|
||||
/// #2141: one pixel of data in a 60000² display window is refused, not allocated per channel.
|
||||
#[test]
|
||||
fn a_huge_display_window_is_refused_not_allocated() {
|
||||
use exr::prelude::*;
|
||||
let channels = AnyChannels::sort(vec![AnyChannel::new("R", FlatSamples::F32(vec![0.5]))].into());
|
||||
let mut image = Image::from_layer(Layer::new((1, 1), LayerAttributes::default(), Encoding::FAST_LOSSLESS, channels));
|
||||
image.attributes.display_window = IntegerBounds::new((0, 0), (60000, 60000));
|
||||
let mut bytes = std::io::Cursor::new(Vec::new());
|
||||
image.write().to_buffered(&mut bytes).unwrap();
|
||||
assert!(read_exr_channels(&bytes.into_inner()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layered_exr_without_an_unnamed_rgb_layer_imports() {
|
||||
use exr::prelude::*;
|
||||
|
||||
@@ -67,6 +67,10 @@ const CONFORM_VERSION: u32 = 3;
|
||||
/// conform thread allocate all of it at once (#408).
|
||||
const MAX_CONFORM_SECONDS: i64 = 24 * 3600;
|
||||
|
||||
/// The sample rates audio is read at; a header outside them is damaged or hostile (#2141).
|
||||
const MIN_SAMPLE_RATE: u32 = 1_000;
|
||||
const MAX_SAMPLE_RATE: u32 = 768_000;
|
||||
|
||||
/// The sample frames of `footage`'s audio at `rate` Hz that a conformed file holds, `None` past
|
||||
/// [`MAX_CONFORM_SECONDS`].
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
@@ -513,6 +517,11 @@ impl MediaPool {
|
||||
let src = self.inner.source(&path).ok_or("the file can't be opened")?;
|
||||
let s0 = start.to_units_floor(rate as i64);
|
||||
let native = src.info().audio.as_ref().map_or(rate, |a| a.sample_rate);
|
||||
// The header's rate scales the samples read: 0xFFFFFFFF Hz made one conform chunk about
|
||||
// 47 GB (#2141). Real audio is 1–768 kHz.
|
||||
if native != 0 && !(MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&native) {
|
||||
return Err(format!("a sample rate of {native} Hz is outside {MIN_SAMPLE_RATE}–{MAX_SAMPLE_RATE} Hz"));
|
||||
}
|
||||
if native == 0 || rate == 0 || native == rate {
|
||||
return read_stereo(&src, s0, frames, rate);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user