Skip to main content

mediadecode_ffmpeg/
limits.rs

1//! Resource ceilings — the finite budgets every copy across the FFmpeg
2//! boundary is checked against **before** it allocates.
3//!
4//! These seats are tier one and tier two of the [resource governance
5//! contract][gov]: what this crate allocates itself, and the FFmpeg
6//! knobs it sets on the caller's behalf. The contract also states what
7//! they do **not** bound, and what a deployment needing a hard memory
8//! bound puts underneath them — read it before sizing these for a
9//! hostile-input service.
10//!
11//! # Why these exist
12//!
13//! 0.9 made every exit copy (see [the amputation contract][law]). A copy
14//! is a decision to allocate whatever the file asks for, and a container
15//! is untrusted input: a header claiming 100000×100000 pixels, a packet
16//! claiming a gigabyte, a Matroska with a thousand attached "fonts" all
17//! cost nothing to write and everything to honour. Through 0.8 the
18//! frame and packet payloads were *views*, so an absurd claim cost a
19//! refcount; from 0.9 it costs memory, and the claim has to be judged
20//! before it is paid.
21//!
22//! Every seat here is a **finite default**, not an `Option`. There is no
23//! "unlimited" spelling on purpose: the shape that lets a caller ask for
24//! no ceiling is the shape a caller reaches for once, in a hurry, and
25//! never revisits. A caller who needs more says how much more.
26//!
27//! # Two layers, one number
28//!
29//! [`FrameLimits::max_pixels`] is enforced twice: once here, against the
30//! frame this crate is about to copy, and once inside libavcodec, by
31//! writing the same number to `AVCodecContext.max_pixels` when a decoder
32//! is opened. The second is the one that matters most — it makes the
33//! decoder refuse before allocating *its* huge frame, which this crate
34//! would otherwise only get to reject after FFmpeg had already paid for
35//! it.
36//!
37//! # The house shape
38//!
39//! `DEFAULT_*` consts, `Copy` options structs with `new` / getters /
40//! `with_*` / `set_*`, and a `with_*` seat on each session — the same
41//! shape [`crate::VideoDecoder::with_max_probe_pending_bytes`] and its
42//! [`DEFAULT_MAX_PROBE_PENDING_BYTES`](crate::decoder::DEFAULT_MAX_PROBE_PENDING_BYTES)
43//! already established for the probe-replay budget.
44//!
45//! [law]: mediadecode::adapter#the-d-seat-amputation-contract
46//! [gov]: mediadecode::adapter#the-resource-governance-contract
47
48/// Default ceiling on a decoded frame's pixel count — 256 mebipixels.
49///
50/// **Why this number.** The largest picture anything ships is 8K UHD
51/// (7680×4320 ≈ 33 Mpx); 16K×16K, which nothing does, is 268 Mpx. This
52/// default sits exactly there: every real frame passes, and the
53/// hand-written header claiming 100000×100000 (10 Gpx) is refused
54/// before a byte is allocated — by libavcodec first, since the same
55/// number is written to `AVCodecContext.max_pixels`, and by this crate
56/// second.
57///
58/// FFmpeg's own default for that option is `INT_MAX`, i.e. no ceiling
59/// worth the name. Overriding it is the point.
60pub const DEFAULT_MAX_PIXELS: u64 = 256 * 1024 * 1024;
61
62/// Default ceiling on the bytes one decoded frame may export — 512 MiB.
63///
64/// **Why this number.** Pixels alone do not bound the copy: bit depth,
65/// plane count and stride padding all multiply it. The widest realistic
66/// frame is 8K 4:4:4 16-bit with alpha (7680×4320×8 bytes ≈ 253 MiB);
67/// 8K P010 is ~96 MiB and 4K P010 ~24 MiB. 512 MiB clears the worst of
68/// those by 2× and still bounds a single frame to something a process
69/// can survive.
70///
71/// Checked against the sum of what the planes will actually export —
72/// after the stride decision, so it is the number this crate is about
73/// to allocate rather than an estimate of it.
74pub const DEFAULT_MAX_FRAME_BYTES: usize = 512 * 1024 * 1024;
75
76/// Default ceiling on one packet's payload — 1 GiB.
77///
78/// **Why this number.** Deliberately ceiling-class rather than tuned:
79/// `AVPacket.size` is a `c_int`, so 2 GiB is the structural maximum and
80/// this halves it. Real packets are nowhere near — an intra-only 8K
81/// ProRes 4444 XQ frame is ~10 MB, an uncompressed v210 8K frame ~88
82/// MB, and a whole-file attachment (the largest packet shape that
83/// exists) is bounded far below by
84/// [`DEFAULT_MAX_ATTACHMENT_BYTES`]. The job here is to refuse the
85/// forged `size` field, not to second-guess a codec.
86pub const DEFAULT_MAX_PACKET_BYTES: usize = 1024 * 1024 * 1024;
87
88/// Default ceiling on one attachment's payload — 64 MiB.
89///
90/// **Why this number.** An attachment is a whole file: cover art or a
91/// font. A generous cover is a 4000×4000 PNG at ~20 MB; the largest
92/// fonts in circulation are CJK families at ~30 MB. 64 MiB clears both
93/// and is two orders of magnitude under the packet ceiling, which is
94/// right — an attachment is the one payload captured *eagerly*, at
95/// open, before a caller has asked for anything.
96pub const DEFAULT_MAX_ATTACHMENT_BYTES: usize = 64 * 1024 * 1024;
97
98/// Default ceiling on **all** attachments in one file, together — 256
99/// MiB.
100///
101/// **Why this number, and why it is separate.** The per-attachment
102/// ceiling bounds one payload; nothing in it bounds a container that
103/// attaches four hundred of them. A subtitled release with a full ASS
104/// font set attaches perhaps ten to thirty fonts of a few MB each —
105/// call it 100 MB at the high end. 256 MiB clears that and refuses the
106/// file whose attachment table is the attack.
107///
108/// This budget is spent at **open**, because that is when this crate
109/// captures every attachment (the demux tier's "exactly one packet,
110/// before any timed packet" contract is kept by construction, and the
111/// construction is eager). A file that exhausts it fails to open, with
112/// the arm naming which track ran the total past the line.
113pub const DEFAULT_MAX_TOTAL_ATTACHMENT_BYTES: usize = 256 * 1024 * 1024;
114
115/// Default ceiling on one stream's codec-parameter heap — 16 MiB.
116///
117/// **What it bounds.** `AVCodecParameters` has three heap seats and all
118/// three come from the file: `extradata`, every entry of
119/// `coded_side_data`, and a custom `ch_layout` channel map. Copying a
120/// track row's parameters copies all of them.
121///
122/// **Why this number.** The honest end of the range is small — H.264
123/// SPS/PPS extradata is tens of bytes, HEVC's a few hundred, FLAC and
124/// ALAC headers a couple of kilobytes. What sets the ceiling is
125/// `coded_side_data`: a MOV `prof` atom carries an **ICC profile**, and
126/// those are legitimately large — a few kilobytes for sRGB, half a
127/// megabyte to two megabytes for a real camera or display profile, and
128/// the largest device-link profiles in circulation reach roughly ten.
129/// 16 MiB clears all of that and still refuses the forged atom.
130pub const DEFAULT_MAX_CODEC_PARAMETER_BYTES: usize = 16 * 1024 * 1024;
131
132/// Default ceiling on **every** stream's codec-parameter heap in one
133/// file, together — 64 MiB.
134///
135/// **Why this number, and why it is separate.** The per-stream ceiling
136/// bounds one track's parameters; nothing in it bounds a container that
137/// declares two hundred tracks each carrying a two-megabyte profile.
138/// Four tracks with a large ICC profile apiece is the realistic high
139/// end, so 64 MiB clears it and refuses the stream table that is the
140/// attack.
141///
142/// Charged over **all** streams, not just the ones a caller will
143/// decode: the track table is built eagerly at open, so every stream's
144/// parameters are copied whether or not anybody asks for them.
145pub const DEFAULT_MAX_TOTAL_CODEC_PARAMETER_BYTES: usize = 64 * 1024 * 1024;
146
147/// The defaults have to hold together, and these say how — at compile
148/// time, because every term is a constant and a fact a build can check
149/// is a fact no test run has to.
150///
151/// Each clause is a claim the doc comments above make in prose:
152/// - every ceiling is finite and non-zero (a zero ceiling refuses
153///   everything, which is the opposite failure and just as bad);
154/// - 8K UHD, and the widest realistic frame, pass;
155/// - the 100000×100000 header does not;
156/// - a whole-file attachment budget below the per-attachment one, or a
157///   per-packet ceiling below the per-attachment one, would be
158///   incoherent — the narrower seat could never fire;
159/// - a per-packet ceiling above `c_int::MAX` could never fire either,
160///   since `AVPacket.size` cannot express it.
161const _: () = {
162  assert!(DEFAULT_MAX_PIXELS > 0 && DEFAULT_MAX_PIXELS < u64::MAX);
163  assert!(DEFAULT_MAX_FRAME_BYTES > 0 && DEFAULT_MAX_FRAME_BYTES < usize::MAX);
164  assert!(DEFAULT_MAX_PACKET_BYTES > 0 && DEFAULT_MAX_PACKET_BYTES < usize::MAX);
165  assert!(DEFAULT_MAX_ATTACHMENT_BYTES > 0);
166  assert!(DEFAULT_MAX_TOTAL_ATTACHMENT_BYTES > 0);
167
168  // 8K UHD — the largest picture anything ships — must decode.
169  assert!(7680 * 4320 < DEFAULT_MAX_PIXELS);
170  // And the widest realistic frame: 8K 4:4:4 16-bit with alpha.
171  assert!(7680 * 4320 * 8 < DEFAULT_MAX_FRAME_BYTES);
172
173  // **8K must decode in the widest format that exists, not just the
174  // widest realistic one.** The byte ceiling is pushed into libavcodec
175  // as a pixel ceiling priced at the worst per-pixel cost any format
176  // this build can emit — 16 bytes, reached by `rgbaf32` and its seven
177  // siblings — because a container's declared format is not an upper
178  // bound on what its decoder produces. That makes the effective pixel
179  // ceiling `max_frame_bytes / 16`, and this is the assertion that
180  // keeps 8K inside it: at 33.18 Mpx and 16 bytes an 8K `rgbaf32` frame
181  // is 506 MiB, which 512 MiB clears with about 1% to spare.
182  //
183  // If `DEFAULT_MAX_FRAME_BYTES` is ever lowered, or a future FFmpeg
184  // adds a format wider than 16 bytes per pixel, this fails the build
185  // rather than quietly refusing 8K at run time.
186  assert!(7680 * 4320 * 16 < DEFAULT_MAX_FRAME_BYTES);
187  // The header a fuzzer writes must not.
188  assert!(100_000 * 100_000 > DEFAULT_MAX_PIXELS);
189
190  assert!(DEFAULT_MAX_TOTAL_ATTACHMENT_BYTES >= DEFAULT_MAX_ATTACHMENT_BYTES);
191  assert!(DEFAULT_MAX_PACKET_BYTES >= DEFAULT_MAX_ATTACHMENT_BYTES);
192  assert!(DEFAULT_MAX_PACKET_BYTES <= i32::MAX as usize);
193
194  assert!(DEFAULT_MAX_CODEC_PARAMETER_BYTES > 0);
195  assert!(DEFAULT_MAX_TOTAL_CODEC_PARAMETER_BYTES >= DEFAULT_MAX_CODEC_PARAMETER_BYTES);
196  // A ten-megabyte device-link ICC profile is real media and must pass.
197  assert!(10 * 1024 * 1024 < DEFAULT_MAX_CODEC_PARAMETER_BYTES);
198
199  // **The two ICC policies agree, and this is what keeps them agreeing.**
200  // The same profile can arrive as a track parameter (`coded_side_data`)
201  // or as a decoded still's frame side data, and a ceiling that admits
202  // it on one road and drops it on the other is not a policy, it is an
203  // accident of which road the file took.
204  assert!(DEFAULT_MAX_IMAGE_SIDE_DATA_BYTES >= DEFAULT_MAX_CODEC_PARAMETER_BYTES);
205};
206
207/// What one decoded frame may cost.
208///
209/// Carried by every session that decodes frames and handed to the
210/// conversion that copies them. See the [module docs](self) for why the
211/// seats are finite and how [`Self::max_pixels`] reaches libavcodec as
212/// well as this crate.
213#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
214pub struct FrameLimits {
215  max_pixels: u64,
216  max_frame_bytes: usize,
217  max_image_side_data_bytes: usize,
218}
219
220impl Default for FrameLimits {
221  #[inline]
222  fn default() -> Self {
223    Self::new()
224  }
225}
226
227impl FrameLimits {
228  /// The defaults: [`DEFAULT_MAX_PIXELS`] and
229  /// [`DEFAULT_MAX_FRAME_BYTES`].
230  #[cfg_attr(not(tarpaulin), inline(always))]
231  pub const fn new() -> Self {
232    Self {
233      max_pixels: DEFAULT_MAX_PIXELS,
234      max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
235      max_image_side_data_bytes: DEFAULT_MAX_IMAGE_SIDE_DATA_BYTES,
236    }
237  }
238
239  /// Most pixels one decoded frame may have.
240  ///
241  /// Also written to `AVCodecContext.max_pixels` when a decoder is
242  /// opened from these limits, so libavcodec refuses an oversized
243  /// picture before allocating it.
244  #[cfg_attr(not(tarpaulin), inline(always))]
245  pub const fn max_pixels(&self) -> u64 {
246    self.max_pixels
247  }
248  /// Most bytes one decoded frame's planes may export, together.
249  #[cfg_attr(not(tarpaulin), inline(always))]
250  pub const fn max_frame_bytes(&self) -> usize {
251    self.max_frame_bytes
252  }
253  /// The ceiling on side data one decoded **still** may carry.
254  #[cfg_attr(not(tarpaulin), inline(always))]
255  pub const fn max_image_side_data_bytes(&self) -> usize {
256    self.max_image_side_data_bytes
257  }
258
259  /// Sets the pixel ceiling (consuming builder).
260  #[cfg_attr(not(tarpaulin), inline(always))]
261  #[must_use]
262  pub const fn with_max_pixels(mut self, value: u64) -> Self {
263    self.max_pixels = value;
264    self
265  }
266  /// Sets the per-frame byte ceiling (consuming builder).
267  #[cfg_attr(not(tarpaulin), inline(always))]
268  #[must_use]
269  pub const fn with_max_frame_bytes(mut self, value: usize) -> Self {
270    self.max_frame_bytes = value;
271    self
272  }
273  /// Sets the decoded-still side-data ceiling (consuming builder).
274  #[cfg_attr(not(tarpaulin), inline(always))]
275  #[must_use]
276  pub const fn with_max_image_side_data_bytes(mut self, value: usize) -> Self {
277    self.max_image_side_data_bytes = value;
278    self
279  }
280
281  /// Sets the pixel ceiling in place.
282  #[cfg_attr(not(tarpaulin), inline(always))]
283  pub const fn set_max_pixels(&mut self, value: u64) -> &mut Self {
284    self.max_pixels = value;
285    self
286  }
287  /// Sets the per-frame byte ceiling in place.
288  #[cfg_attr(not(tarpaulin), inline(always))]
289  pub const fn set_max_frame_bytes(&mut self, value: usize) -> &mut Self {
290    self.max_frame_bytes = value;
291    self
292  }
293  /// Sets the decoded-still side-data ceiling in place.
294  #[cfg_attr(not(tarpaulin), inline(always))]
295  pub const fn set_max_image_side_data_bytes(&mut self, value: usize) -> &mut Self {
296    self.max_image_side_data_bytes = value;
297    self
298  }
299}
300
301/// Default ceiling on the bytes libavformat may **read** while probing
302/// and analysing a container — 5 MiB, which is FFmpeg's own
303/// `probesize` default.
304///
305/// **What this seat is for, and what it is not.** Every other budget in
306/// this crate bounds a copy *this crate* makes. This one bounds work
307/// **libavformat does before this crate is handed anything**:
308/// `avformat_open_input` and `avformat_find_stream_info` build the
309/// attached-picture, extradata and coded-side-data buffers themselves,
310/// so the attachment budgets — which measure this crate's copies —
311/// arrive after the original allocation has already happened.
312///
313/// A parser cannot allocate from bytes it was never given, so bounding
314/// the read is the instrument that reaches furthest back. See
315/// [`DemuxLimits::max_probe_bytes`] for how far it actually reaches and
316/// what it does not.
317pub const DEFAULT_MAX_PROBE_BYTES: u64 = 5 * 1024 * 1024;
318
319/// Default ceiling on the number of streams a container may declare —
320/// FFmpeg's own `max_streams` default.
321///
322/// Each declared stream costs an `AVStream` and its `AVCodecParameters`
323/// inside libavformat, before this crate sees a track table, so a
324/// header claiming a hundred thousand streams is an allocation this
325/// crate's per-track budgets are downstream of.
326pub const DEFAULT_MAX_STREAMS: u32 = 1000;
327
328/// Default ceiling on the side data one decoded **still** may carry —
329/// the same 16 MiB as [`DEFAULT_MAX_CODEC_PARAMETER_BYTES`], and the
330/// same reason.
331///
332/// **Why the still road needs its own number.** The shared stream
333/// collector caps frame side data at 256 KiB in total and *silently
334/// drops* whatever does not fit. On a video stream that is defensible:
335/// side data there is small, per-frame, and repeated. On a still it is
336/// wrong twice over. A decoded image's side data is dominated by the
337/// one thing that is legitimately megabytes — an **ICC profile** — and
338/// the parameter budget next door already admits those up to 16 MiB, so
339/// the same profile was admitted as a track parameter and swallowed as
340/// a frame annotation. Worse, the drop is positional: entries after the
341/// cap are skipped, and `AV_FRAME_DATA_DISPLAYMATRIX` — the orientation
342/// this crate reads off a still — is a small entry that a large ICC
343/// profile ahead of it pushed out. A picture came back silently rotated
344/// wrong.
345///
346/// So the still road gets a seat sized to what it actually carries, and
347/// over-budget is a **named refusal** rather than a quiet truncation:
348/// side data that cannot be carried whole is a fact about the picture,
349/// not a detail to drop.
350pub const DEFAULT_MAX_IMAGE_SIDE_DATA_BYTES: usize = DEFAULT_MAX_CODEC_PARAMETER_BYTES;
351
352/// Default ceiling on the compressed bytes one **image** decode may be
353/// handed — 64 MiB, the attachment family.
354///
355/// **Why the attachment family and not the packet one.** What
356/// [`crate::FfmpegImageDecoder`] decodes *is* an attachment: a whole
357/// file a container handed over eagerly. When it arrives through the
358/// demuxer it has already been charged against
359/// [`DEFAULT_MAX_ATTACHMENT_BYTES`], and this seat is what keeps the
360/// same ceiling in force when a caller builds the packet itself — the
361/// one road that skips the demux tier entirely. A 1 GiB packet ceiling
362/// here would mean the direct road was a gigabyte more permissive than
363/// the demuxed one for the same bytes.
364pub const DEFAULT_MAX_IMAGE_INPUT_BYTES: usize = DEFAULT_MAX_ATTACHMENT_BYTES;
365
366/// What one packet's payload may cost.
367///
368/// Carried by the boundary conversions and by an open demux session.
369#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
370pub struct PacketLimits {
371  max_packet_bytes: usize,
372}
373
374impl Default for PacketLimits {
375  #[inline]
376  fn default() -> Self {
377    Self::new()
378  }
379}
380
381impl PacketLimits {
382  /// The default: [`DEFAULT_MAX_PACKET_BYTES`].
383  #[cfg_attr(not(tarpaulin), inline(always))]
384  pub const fn new() -> Self {
385    Self {
386      max_packet_bytes: DEFAULT_MAX_PACKET_BYTES,
387    }
388  }
389
390  /// Most bytes one packet's payload may carry.
391  #[cfg_attr(not(tarpaulin), inline(always))]
392  pub const fn max_packet_bytes(&self) -> usize {
393    self.max_packet_bytes
394  }
395
396  /// Sets the per-packet ceiling (consuming builder).
397  #[cfg_attr(not(tarpaulin), inline(always))]
398  #[must_use]
399  pub const fn with_max_packet_bytes(mut self, value: usize) -> Self {
400    self.max_packet_bytes = value;
401    self
402  }
403  /// Sets the per-packet ceiling in place.
404  #[cfg_attr(not(tarpaulin), inline(always))]
405  pub const fn set_max_packet_bytes(&mut self, value: usize) -> &mut Self {
406    self.max_packet_bytes = value;
407    self
408  }
409}
410
411/// What opening and running one **decoder** may spend.
412///
413/// Composes [`FrameLimits`] — what the frames it produces may cost —
414/// with the two things a decoder spends before it has produced
415/// anything: copying the caller's codec parameters into an
416/// `AVCodecContext`, and copying the caller's compressed bytes into an
417/// `AVPacket`.
418///
419/// Taken at `open` by every decoder session in this crate, for the
420/// reason [`FrameLimits`] gives: half of it is written into an
421/// `AVCodecContext` whose ceilings cannot move after `avcodec_open2`.
422#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
423pub struct DecoderLimits {
424  frame: FrameLimits,
425  max_codec_parameter_bytes: usize,
426  max_packet_bytes: usize,
427  max_image_input_bytes: usize,
428}
429
430impl Default for DecoderLimits {
431  #[inline]
432  fn default() -> Self {
433    Self::new()
434  }
435}
436
437impl DecoderLimits {
438  /// The defaults: [`FrameLimits::new`],
439  /// [`DEFAULT_MAX_CODEC_PARAMETER_BYTES`], [`DEFAULT_MAX_PACKET_BYTES`]
440  /// and [`DEFAULT_MAX_IMAGE_INPUT_BYTES`].
441  #[cfg_attr(not(tarpaulin), inline(always))]
442  pub const fn new() -> Self {
443    Self {
444      frame: FrameLimits::new(),
445      max_codec_parameter_bytes: DEFAULT_MAX_CODEC_PARAMETER_BYTES,
446      max_packet_bytes: DEFAULT_MAX_PACKET_BYTES,
447      max_image_input_bytes: DEFAULT_MAX_IMAGE_INPUT_BYTES,
448    }
449  }
450
451  /// What one decoded frame may cost.
452  #[cfg_attr(not(tarpaulin), inline(always))]
453  pub const fn frame(&self) -> FrameLimits {
454    self.frame
455  }
456  /// Most heap bytes the codec parameters this decoder is opened from
457  /// may hold.
458  ///
459  /// Enforced at the choke point every road into libavcodec passes
460  /// through, so a decoder cannot be opened over parameters nobody
461  /// measured.
462  #[cfg_attr(not(tarpaulin), inline(always))]
463  pub const fn max_codec_parameter_bytes(&self) -> usize {
464    self.max_codec_parameter_bytes
465  }
466  /// Most compressed bytes one packet handed to a **stream** decoder
467  /// may carry.
468  #[cfg_attr(not(tarpaulin), inline(always))]
469  pub const fn max_packet_bytes(&self) -> usize {
470    self.max_packet_bytes
471  }
472
473  /// [`Self::max_packet_bytes`] as the [`PacketLimits`] the boundary
474  /// conversions take, so the send leg and the receive leg are handed
475  /// the same seat rather than two numbers that could drift.
476  #[cfg_attr(not(tarpaulin), inline(always))]
477  pub const fn packet_limits(&self) -> PacketLimits {
478    PacketLimits::new().with_max_packet_bytes(self.max_packet_bytes)
479  }
480  /// Most compressed bytes one **image** decode may be handed. See
481  /// [`DEFAULT_MAX_IMAGE_INPUT_BYTES`] for why this is its own seat.
482  #[cfg_attr(not(tarpaulin), inline(always))]
483  pub const fn max_image_input_bytes(&self) -> usize {
484    self.max_image_input_bytes
485  }
486
487  /// Sets the frame ceilings (consuming builder).
488  #[cfg_attr(not(tarpaulin), inline(always))]
489  #[must_use]
490  pub const fn with_frame(mut self, value: FrameLimits) -> Self {
491    self.frame = value;
492    self
493  }
494  /// Sets the codec-parameter ceiling (consuming builder).
495  #[cfg_attr(not(tarpaulin), inline(always))]
496  #[must_use]
497  pub const fn with_max_codec_parameter_bytes(mut self, value: usize) -> Self {
498    self.max_codec_parameter_bytes = value;
499    self
500  }
501  /// Sets the per-packet ceiling (consuming builder).
502  #[cfg_attr(not(tarpaulin), inline(always))]
503  #[must_use]
504  pub const fn with_max_packet_bytes(mut self, value: usize) -> Self {
505    self.max_packet_bytes = value;
506    self
507  }
508  /// Sets the image-input ceiling (consuming builder).
509  #[cfg_attr(not(tarpaulin), inline(always))]
510  #[must_use]
511  pub const fn with_max_image_input_bytes(mut self, value: usize) -> Self {
512    self.max_image_input_bytes = value;
513    self
514  }
515
516  /// Sets the frame ceilings in place.
517  #[cfg_attr(not(tarpaulin), inline(always))]
518  pub const fn set_frame(&mut self, value: FrameLimits) -> &mut Self {
519    self.frame = value;
520    self
521  }
522  /// Sets the codec-parameter ceiling in place.
523  #[cfg_attr(not(tarpaulin), inline(always))]
524  pub const fn set_max_codec_parameter_bytes(&mut self, value: usize) -> &mut Self {
525    self.max_codec_parameter_bytes = value;
526    self
527  }
528  /// Sets the per-packet ceiling in place.
529  #[cfg_attr(not(tarpaulin), inline(always))]
530  pub const fn set_max_packet_bytes(&mut self, value: usize) -> &mut Self {
531    self.max_packet_bytes = value;
532    self
533  }
534  /// Sets the image-input ceiling in place.
535  #[cfg_attr(not(tarpaulin), inline(always))]
536  pub const fn set_max_image_input_bytes(&mut self, value: usize) -> &mut Self {
537    self.max_image_input_bytes = value;
538    self
539  }
540}
541
542/// What one demux session may spend: on any single packet, on any
543/// single attachment, and on every attachment in the file together.
544///
545/// Handed to [`FfmpegDemuxer::open_with`](crate::FfmpegDemuxer::open_with)
546/// rather than set afterwards, because the attachment budget is spent
547/// *during* the open — every attachment payload is captured before the
548/// first timed packet is read, which is what makes the demux tier's
549/// delivery contract true by construction.
550#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
551pub struct DemuxLimits {
552  packet: PacketLimits,
553  max_attachment_bytes: usize,
554  max_total_attachment_bytes: usize,
555  max_codec_parameter_bytes: usize,
556  max_total_codec_parameter_bytes: usize,
557  max_probe_bytes: u64,
558  max_streams: u32,
559}
560
561impl Default for DemuxLimits {
562  #[inline]
563  fn default() -> Self {
564    Self::new()
565  }
566}
567
568impl DemuxLimits {
569  /// The defaults: [`PacketLimits::new`],
570  /// [`DEFAULT_MAX_ATTACHMENT_BYTES`] and
571  /// [`DEFAULT_MAX_TOTAL_ATTACHMENT_BYTES`].
572  #[cfg_attr(not(tarpaulin), inline(always))]
573  pub const fn new() -> Self {
574    Self {
575      packet: PacketLimits::new(),
576      max_attachment_bytes: DEFAULT_MAX_ATTACHMENT_BYTES,
577      max_total_attachment_bytes: DEFAULT_MAX_TOTAL_ATTACHMENT_BYTES,
578      max_codec_parameter_bytes: DEFAULT_MAX_CODEC_PARAMETER_BYTES,
579      max_total_codec_parameter_bytes: DEFAULT_MAX_TOTAL_CODEC_PARAMETER_BYTES,
580      max_probe_bytes: DEFAULT_MAX_PROBE_BYTES,
581      max_streams: DEFAULT_MAX_STREAMS,
582    }
583  }
584
585  /// The ceiling on bytes libavformat may read while probing and
586  /// analysing a container.
587  ///
588  /// # What this bounds, and what it does not
589  ///
590  /// **Bounded:** the total bytes libavformat is handed during
591  /// `avformat_open_input` and `avformat_find_stream_info`. It reaches
592  /// two ways — as `probesize` and `formatprobesize`, which every
593  /// entrypoint sets before the open, and, on the reader entrypoint, as
594  /// a hard byte meter on the `AVIOContext` itself: past the budget the
595  /// reader answers an I/O error, so the parser gets nothing more
596  /// whatever it asks for.
597  ///
598  /// **Not bounded:** allocation *amplification* inside a parser. A
599  /// container can describe, in a handful of bytes, a structure whose
600  /// in-memory form is much larger, and nothing outside libavformat can
601  /// see that happen. What this seat guarantees is that the input to
602  /// that amplification is finite and small; bounding its output is the
603  /// substrate's own hardening territory, and FFmpeg has its own
604  /// `max_streams` / `max_index_size` / `max_picture_buffer` seats for
605  /// exactly that — [`Self::max_streams`] sets the first of them.
606  ///
607  /// **Not bounded on the path entrypoint:** the byte meter needs an
608  /// `AVIOContext` this crate owns, and a path is opened by
609  /// libavformat's own protocol layer. `probesize` and
610  /// `formatprobesize` still apply there; the hard meter does not.
611  /// A caller who wants the meter on a file can open it as a reader.
612  #[cfg_attr(not(tarpaulin), inline(always))]
613  pub const fn max_probe_bytes(&self) -> u64 {
614    self.max_probe_bytes
615  }
616  /// The ceiling on streams a container may declare. See
617  /// [`Self::max_probe_bytes`] for why a seat inside libavformat is
618  /// worth setting at all.
619  #[cfg_attr(not(tarpaulin), inline(always))]
620  pub const fn max_streams(&self) -> u32 {
621    self.max_streams
622  }
623  /// Sets the probe-read ceiling (consuming builder).
624  #[cfg_attr(not(tarpaulin), inline(always))]
625  #[must_use]
626  pub const fn with_max_probe_bytes(mut self, value: u64) -> Self {
627    self.max_probe_bytes = value;
628    self
629  }
630  /// Sets the declared-stream ceiling (consuming builder).
631  #[cfg_attr(not(tarpaulin), inline(always))]
632  #[must_use]
633  pub const fn with_max_streams(mut self, value: u32) -> Self {
634    self.max_streams = value;
635    self
636  }
637
638  /// The per-packet budget timed packets are checked against.
639  #[cfg_attr(not(tarpaulin), inline(always))]
640  pub const fn packet(&self) -> PacketLimits {
641    self.packet
642  }
643  /// Most bytes one attachment may carry.
644  #[cfg_attr(not(tarpaulin), inline(always))]
645  pub const fn max_attachment_bytes(&self) -> usize {
646    self.max_attachment_bytes
647  }
648  /// Most bytes every attachment in the file may carry together.
649  #[cfg_attr(not(tarpaulin), inline(always))]
650  pub const fn max_total_attachment_bytes(&self) -> usize {
651    self.max_total_attachment_bytes
652  }
653  /// Most heap bytes one stream's codec parameters may hold —
654  /// `extradata`, `coded_side_data` and a custom channel map together.
655  #[cfg_attr(not(tarpaulin), inline(always))]
656  pub const fn max_codec_parameter_bytes(&self) -> usize {
657    self.max_codec_parameter_bytes
658  }
659  /// Most heap bytes every stream's codec parameters may hold together.
660  #[cfg_attr(not(tarpaulin), inline(always))]
661  pub const fn max_total_codec_parameter_bytes(&self) -> usize {
662    self.max_total_codec_parameter_bytes
663  }
664
665  /// Sets the per-packet budget (consuming builder).
666  #[cfg_attr(not(tarpaulin), inline(always))]
667  #[must_use]
668  pub const fn with_packet(mut self, value: PacketLimits) -> Self {
669    self.packet = value;
670    self
671  }
672  /// Sets the per-attachment ceiling (consuming builder).
673  #[cfg_attr(not(tarpaulin), inline(always))]
674  #[must_use]
675  pub const fn with_max_attachment_bytes(mut self, value: usize) -> Self {
676    self.max_attachment_bytes = value;
677    self
678  }
679  /// Sets the whole-file attachment budget (consuming builder).
680  #[cfg_attr(not(tarpaulin), inline(always))]
681  #[must_use]
682  pub const fn with_max_total_attachment_bytes(mut self, value: usize) -> Self {
683    self.max_total_attachment_bytes = value;
684    self
685  }
686  /// Sets the per-stream codec-parameter ceiling (consuming builder).
687  #[cfg_attr(not(tarpaulin), inline(always))]
688  #[must_use]
689  pub const fn with_max_codec_parameter_bytes(mut self, value: usize) -> Self {
690    self.max_codec_parameter_bytes = value;
691    self
692  }
693  /// Sets the whole-file codec-parameter budget (consuming builder).
694  #[cfg_attr(not(tarpaulin), inline(always))]
695  #[must_use]
696  pub const fn with_max_total_codec_parameter_bytes(mut self, value: usize) -> Self {
697    self.max_total_codec_parameter_bytes = value;
698    self
699  }
700
701  /// Sets the per-packet budget in place.
702  #[cfg_attr(not(tarpaulin), inline(always))]
703  pub const fn set_packet(&mut self, value: PacketLimits) -> &mut Self {
704    self.packet = value;
705    self
706  }
707  /// Sets the per-attachment ceiling in place.
708  #[cfg_attr(not(tarpaulin), inline(always))]
709  pub const fn set_max_attachment_bytes(&mut self, value: usize) -> &mut Self {
710    self.max_attachment_bytes = value;
711    self
712  }
713  /// Sets the whole-file attachment budget in place.
714  #[cfg_attr(not(tarpaulin), inline(always))]
715  pub const fn set_max_total_attachment_bytes(&mut self, value: usize) -> &mut Self {
716    self.max_total_attachment_bytes = value;
717    self
718  }
719  /// Sets the per-stream codec-parameter ceiling in place.
720  #[cfg_attr(not(tarpaulin), inline(always))]
721  pub const fn set_max_codec_parameter_bytes(&mut self, value: usize) -> &mut Self {
722    self.max_codec_parameter_bytes = value;
723    self
724  }
725  /// Sets the whole-file codec-parameter budget in place.
726  #[cfg_attr(not(tarpaulin), inline(always))]
727  pub const fn set_max_total_codec_parameter_bytes(&mut self, value: usize) -> &mut Self {
728    self.max_total_codec_parameter_bytes = value;
729    self
730  }
731}
732
733#[cfg(test)]
734mod tests;