Skip to main content

alpha2_extensions/
budget.rs

1//! What one provider call may cost, in one place: the limits every provider
2//! checks a source against before it reads, decodes, or draws it. A source
3//! past one is refused with [`ExtensionError::Budget`], the same way natively
4//! and in the browser module, as both run this code.
5//!
6//! The limits are on different axes and are not folded into one number:
7//! - **document size**: the most bytes of a source's own file a provider
8//!   reads, for the formats it reads whole ([`document_limit`]);
9//! - **canvas**: the largest canvas a provider opens ([`check_canvas`]);
10//! - **decoded pixels**: what an animated image's frames cover together
11//!   ([`MAX_ANIMATION_PIXELS`]) and what the images inside one source decode
12//!   to ([`MAX_SOURCE_IMAGE_PIXELS`]), and the decoded bytes a source keeps
13//!   between requests ([`DEFAULT_FRAME_BUDGET`]);
14//! - **drawing**: what drawing one SVG render or one Lottie frame costs, in
15//!   canvases of blended fill ([`MAX_DRAWING`]).
16//!
17//! Each provider also counts what its own format can run away on (an SVG's
18//! elements and path segments, a Lottie frame's draws and nesting, a
19//! dotLottie package's files) next to its parser, and refuses past those
20//! limits with the same error.
21
22use alpha2_model::{
23    MAX_LOTTIE_DOCUMENT_BYTES, MAX_PROVIDER_DOCUMENT_BYTES, MAX_SVG_DOCUMENT_BYTES, ProviderSource,
24};
25
26use crate::api::ExtensionError;
27
28/// Largest canvas side a provider opens, in pixels.
29pub const MAX_CANVAS_SIDE: u32 = 16_384;
30/// Largest canvas a provider opens, in pixels: 8K UHD.
31pub const MAX_CANVAS_PIXELS: u64 = 7_680 * 4_320;
32/// Most frames an animated image may have.
33pub const MAX_ANIMATION_FRAMES: usize = 10_000;
34/// Most pixels an animated image's frames may cover, all frames together:
35/// what one pass through it decodes and composites, whatever its canvas.
36/// About a thousand whole 1080p frames, which one pass decodes in about ten
37/// seconds natively.
38pub const MAX_ANIMATION_PIXELS: u64 = 1 << 31;
39/// Decoded bytes an animated source or an image sequence keeps between
40/// requests.
41pub const DEFAULT_FRAME_BUDGET: usize = 256 * 1024 * 1024;
42/// Most pixels the raster images inside one source may decode to, all of
43/// them together: four whole canvases of [`MAX_CANVAS_PIXELS`]. An SVG
44/// decodes every image it draws on each render (an image drawn twice, as
45/// through `<use>`, counts twice) and a Lottie animation decodes its image
46/// assets once, when it opens, and holds them; each image is also within
47/// [`check_canvas`] on its own. A source past it is refused.
48pub const MAX_SOURCE_IMAGE_PIXELS: u64 = 4 * MAX_CANVAS_PIXELS;
49
50/// Most canvases' worth of drawing one provider call may cost: each pixel of
51/// every fill, stroke, layer, mask, clip, tile, and filter primitive an SVG
52/// render draws, and each fill and stroke a Lottie frame paints, weighted by
53/// what it costs against a fill blended over the canvas. A 1080p canvas of
54/// blended fill (a gradient, or a solid fill made translucent) costs about
55/// 4 to 10 ms natively, so this is about 2 s. An opaque solid fill is not
56/// counted for less, as whether something above it fades is not known before
57/// it is drawn.
58#[cfg(any(feature = "svg", feature = "lottie"))]
59pub(crate) const MAX_DRAWING: f64 = 256.0;
60
61/// The least an SVG's canvas counts as for [`MAX_DRAWING`]: 1920x1080, as
62/// what is bounded is time and a small icon may draw many blurs. A Lottie
63/// frame counts in its own canvas, as everything it paints is drawn onto
64/// layers the size of the frame.
65#[cfg(feature = "svg")]
66pub(crate) const FLOOR_CANVAS: f64 = 1920.0 * 1080.0;
67
68/// Refuses a canvas larger than a provider opens.
69///
70/// # Errors
71/// [`ExtensionError::Budget`] or, for an empty canvas,
72/// [`ExtensionError::Malformed`].
73pub fn check_canvas(width: u32, height: u32) -> Result<(), ExtensionError> {
74    if width == 0 || height == 0 {
75        return Err(ExtensionError::Malformed("the canvas is empty".into()));
76    }
77    if width > MAX_CANVAS_SIDE
78        || height > MAX_CANVAS_SIDE
79        || u64::from(width) * u64::from(height) > MAX_CANVAS_PIXELS
80    {
81        return Err(ExtensionError::Budget(format!(
82            "a {width}x{height} canvas is larger than {MAX_CANVAS_PIXELS} pixels"
83        )));
84    }
85    Ok(())
86}
87
88/// The most bytes a provider reads of a source's own file, for the formats
89/// it reads whole: a dotLottie package ([`MAX_PROVIDER_DOCUMENT_BYTES`]), a
90/// Lottie JSON file ([`MAX_LOTTIE_DOCUMENT_BYTES`]), and an SVG document
91/// ([`MAX_SVG_DOCUMENT_BYTES`]), with what the file is called; `None` for
92/// the formats a provider decodes as it goes. `head` is the file's start: a
93/// Lottie source's file is a package when it starts as a ZIP file does.
94///
95/// A host checks a file's size against it before reading it, and every
96/// provider checks the bytes it is given before parsing them.
97#[must_use]
98pub fn document_limit(source: &ProviderSource, head: &[u8]) -> Option<(usize, &'static str)> {
99    match source {
100        ProviderSource::Svg => Some((MAX_SVG_DOCUMENT_BYTES, "SVG document")),
101        ProviderSource::Lottie { .. } if head.starts_with(b"PK\x03\x04") => {
102            Some((MAX_PROVIDER_DOCUMENT_BYTES, "dotLottie package"))
103        }
104        ProviderSource::Lottie { .. } => Some((MAX_LOTTIE_DOCUMENT_BYTES, "Lottie file")),
105        _ => None,
106    }
107}
108
109/// Refuses a source's file of `length` bytes larger than its
110/// [`document_limit`], before it is read or parsed.
111///
112/// # Errors
113/// [`ExtensionError::Budget`].
114pub fn check_document_size(
115    source: &ProviderSource,
116    head: &[u8],
117    length: u64,
118) -> Result<(), ExtensionError> {
119    match document_limit(source, head) {
120        Some((limit, what)) if length > limit as u64 => Err(ExtensionError::Budget(format!(
121            "the {what} is {length} bytes, more than the {limit} a provider reads"
122        ))),
123        _ => Ok(()),
124    }
125}
126
127/// Refuses images that together decode to more than
128/// [`MAX_SOURCE_IMAGE_PIXELS`].
129///
130/// # Errors
131/// [`ExtensionError::Budget`].
132pub fn check_image_pixels(pixels: u64) -> Result<(), ExtensionError> {
133    if pixels > MAX_SOURCE_IMAGE_PIXELS {
134        return Err(ExtensionError::Budget(format!(
135            "its images decode to {pixels} pixels, more than the {MAX_SOURCE_IMAGE_PIXELS} one source may"
136        )));
137    }
138    Ok(())
139}