mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
Merge PR #3015: Never crash: an oversize export size is an error, not a multiply overflow
crates/engine/src/media.rs: main moved render_now and export_job to media/session.rs; the two size checks ported there crates/engine/src/tests_export.rs: the test is appended to tests/export.rs
This commit is contained in:
@@ -399,6 +399,7 @@ impl crate::Session {
|
||||
|
||||
/// Synchronous render (CLI, MCP, tests).
|
||||
pub fn render_now(&mut self, id: PhotoId, max_w: usize, max_h: usize) -> Result<Rendered, String> {
|
||||
lightcraft_pipeline::check_output_size(max_w, max_h)?;
|
||||
let job = self.render_job(id, max_w, max_h, false, true).ok_or("no such photo")?;
|
||||
let r = job.run();
|
||||
self.accept(&r);
|
||||
@@ -429,6 +430,7 @@ impl crate::Session {
|
||||
space: lightcraft_pipeline::OutputSpace,
|
||||
depth: lightcraft_pipeline::OutputDepth,
|
||||
) -> Result<RenderJob, String> {
|
||||
lightcraft_pipeline::check_output_size(max_w, max_h)?;
|
||||
let mut job = self.render_job(id, max_w, max_h, false, true).ok_or("no such photo")?;
|
||||
// An export renders the full image: never a thumbnail's source developed from a smaller copy of the raw
|
||||
// image that the file keeps (Lightroom's merged DNGs)
|
||||
|
||||
@@ -1396,3 +1396,16 @@ fn sixteen_bit_zip_tiff_export_is_never_larger_than_uncompressed() {
|
||||
eprintln!("demo photo, 16-bit TIFF: none {none} B, ZIP {zip} B, LZW {lzw} B");
|
||||
assert!(zip <= none, "16-bit ZIP {zip} B is larger than uncompressed {none} B");
|
||||
}
|
||||
|
||||
/// An absurd export size is refused, not a multiply overflow (it panicked before the fix).
|
||||
#[test]
|
||||
fn huge_export_size_is_an_error() {
|
||||
use lightcraft_pipeline::{OutputDepth, OutputSpace};
|
||||
let mut s = Session::with_demo();
|
||||
let id = s.active().unwrap();
|
||||
let huge = usize::MAX >> 8;
|
||||
assert!(s.render_export(id, huge, huge, OutputSpace::Srgb, OutputDepth::U8).is_err());
|
||||
assert!(s.render_export(id, 70_000, 10, OutputSpace::Srgb, OutputDepth::U8).is_err());
|
||||
assert!(s.render_now(id, huge, huge).is_err());
|
||||
assert!(s.render_now(id, 10, 70_000).is_err());
|
||||
}
|
||||
|
||||
@@ -599,7 +599,10 @@ pub fn render(
|
||||
};
|
||||
let plan = lightcraft_pipeline::plan(src, info, s, req);
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
let n = w * h;
|
||||
let Some(n) = w.checked_mul(h).filter(|n| n.checked_mul(12).is_some()) else {
|
||||
fail(FailKind::Limit, format!("{w}×{h} output is too large"));
|
||||
return None;
|
||||
};
|
||||
let _limit = match fault {
|
||||
Some(crate::Fault::Limit(bytes)) => Some(crate::ctx::LimitOverride::new(bytes)),
|
||||
_ => None,
|
||||
|
||||
@@ -173,6 +173,21 @@ impl PixelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
/// The longest output side a render may be asked for (TIFF/PNG/JPEG all fit within it).
|
||||
pub const MAX_OUTPUT_SIDE: usize = 65_535;
|
||||
/// The most output pixels a render may be asked for (the box it fits into, ≈ 1.07 Gpx).
|
||||
pub const MAX_OUTPUT_PIXELS: usize = 1 << 30;
|
||||
|
||||
/// Rejects an output box past [`MAX_OUTPUT_SIDE`] or [`MAX_OUTPUT_PIXELS`] before anything is
|
||||
/// sized from it.
|
||||
pub fn check_output_size(max_w: usize, max_h: usize) -> Result<(), String> {
|
||||
let pixels = max_w.checked_mul(max_h);
|
||||
if max_w > MAX_OUTPUT_SIDE || max_h > MAX_OUTPUT_SIDE || pixels.is_none_or(|n| n > MAX_OUTPUT_PIXELS) {
|
||||
return Err(format!("output size {max_w}×{max_h} is too large (at most {MAX_OUTPUT_SIDE} px per side and {MAX_OUTPUT_PIXELS} pixels)"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl RenderRequest {
|
||||
pub fn fit(max_w: usize, max_h: usize) -> Self {
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user