// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// How much image a caller is willing to be handed.
//
// Every reader in the library takes this, because every format lets the file
// declare its own size before any pixels arrive, and a decoder that trusts the
// declaration is a decoder that allocates whatever a hostile file asks for. The
// ceiling is checked before the allocation, not after.
///|
/// The ceilings a decoder works under.
///
/// The defaults are the ones a desktop application can live with: a texture no
/// larger than a modern GPU takes, and a megabyte of pixels at the very most.
/// A caller reading files from the network should set them lower; a caller
/// reading its own assets can leave them alone.
pub(all) struct Limits {
/// The widest image accepted, in pixels.
width : Int32
/// The tallest image accepted, in pixels.
height : Int32
/// The most pixels accepted, whatever the shape: this is the one that stops a
/// long thin image from slipping past the two above.
pixels : Int32
/// The most bytes a decoder may produce while unpacking: a compressed stream
/// can expand without limit, so the expansion is capped separately.
bytes : Int32
}
///|
/// The desktop defaults: a million pixels a side, 256 megapixels in all, and a
/// gigabyte of expansion.
pub fn Limits::default() -> Limits {
{
width: 1_048_576,
height: 1_048_576,
pixels: 268_435_456,
bytes: 1_073_741_824,
}
}
///|
/// What a viewer of untrusted uploads would choose: 8192 square, 64 megapixels,
/// 256 MiB of expansion.
pub fn Limits::strict() -> Limits {
{ width: 8192, height: 8192, pixels: 67_108_864, bytes: 268_435_456, }
}
///|
/// Whether an image of this shape is inside the limits.
pub fn Limits::allows(self : Limits, width : Int32, height : Int32) -> Bool {
width >= 0 &&
height >= 0 &&
width <= self.width &&
height <= self.height &&
width.to_int64() * height.to_int64() <= self.pixels.to_int64()
}
///|
/// Refuse an image of this shape unless the limits allow it.
///
/// The pixel count is multiplied in 64 bits so that a declaration of
/// 65535 × 65535 cannot wrap into a small number and pass.
pub fn Limits::check(
self : Limits,
width : Int32,
height : Int32,
) -> Unit raise Refused {
if width < 0 || height < 0 {
raise Unexpected(at=0, got="negative size", want="a width and a height")
}
if width > self.width {
raise OverLimit(limit=self.width, got=width)
}
if height > self.height {
raise OverLimit(limit=self.height, got=height)
}
let pixels = width.to_int64() * height.to_int64()
if pixels > self.pixels.to_int64() {
raise OverLimit(
limit=self.pixels,
got=if pixels > 2_147_483_647L { 2_147_483_647 } else { pixels.to_int() },
)
}
}
///|
/// Refuse a buffer of this many bytes unless the limits allow it.
pub fn Limits::check_bytes(self : Limits, bytes : Int32) -> Unit raise Refused {
if bytes > self.bytes {
raise OverLimit(limit=self.bytes, got=bytes)
}
}