Skip to main content

mediadecode_ffmpeg/
demuxer.rs

1//! [`mediadecode::demuxer::Demuxer`] impl backed by `libavformat`.
2//!
3//! Opens a container — from a path, or from any `Read + Seek` reader
4//! through a custom `AVIOContext` — builds its track table once, and
5//! then hands packets out one at a time in interleaved file order.
6//!
7//! The table is built at open and **kept for the life of the session**:
8//! it is what every packet is classified against, so reading it takes
9//! nothing away and may happen at any point. Rows are handed out as
10//! `triomphe::Arc<TrackInfo<Ffmpeg>>` handles — see
11//! [`Demuxer::TrackHandle`](mediadecode::demuxer::Demuxer::TrackHandle).
12//!
13//! A container's **table of contents** is read at that same moment and
14//! kept the same way: `AVFormatContext.chapters`, mirrored into
15//! [`Chapter`] rows and answered by
16//! [`Demuxer::chapters`](mediadecode::demuxer::Demuxer::chapters). See
17//! [`build_chapters`] for what is read, what is bounded before it is
18//! allocated, and what is deliberately left as the container wrote it.
19//!
20//! # What normalization this layer does
21//!
22//! libavformat's track table is not quite the one the demux tier
23//! promises, and the gap is entirely about attachments:
24//!
25//! - **Cover art is an attachment, not video.** A still image in an
26//!   MP3, FLAC or MP4 arrives as a video stream carrying
27//!   `AV_DISPOSITION_ATTACHED_PIC`. This layer maps it to
28//!   [`TrackKind::Attachment`], so the `Video` arm carries true motion
29//!   video and nothing else.
30//! - **A font's bytes are not in the packet stream at all.** An
31//!   `AVMEDIA_TYPE_ATTACHMENT` stream never produces a packet; its
32//!   payload lives in `AVCodecParameters.extradata`. This layer
33//!   synthesizes the packet at open time.
34//! - **Cover art's packet is hoisted.** libavformat parks the real
35//!   packet in `AVStream.attached_pic`; some demuxers also emit it in
36//!   the packet stream, some do not. This layer takes it from
37//!   `attached_pic` at open time and drops the duplicate if it ever
38//!   arrives, so the count is exactly one either way.
39//!
40//! Both kinds are queued at open — every attachment track, without
41//! exception, or the open fails. That is what makes the face's "exactly
42//! one packet, before any timed packet" true *by construction* here:
43//! the queue is complete and drains before the first `av_read_frame`
44//! call ever runs, so no packet on an attachment track can be anything
45//! but a duplicate, and no seek can move a packet that was never on the
46//! timeline.
47//!
48//! # Seeking
49//!
50//! `seek` converts the target to `AV_TIME_BASE` units and calls
51//! `avformat_seek_file` over the window `[i64::MIN, target]`, which is
52//! FFmpeg's backward convention: the landing point is the nearest
53//! keyframe at or before the target, never after. `avformat_seek_file`
54//! flushes libavformat's own buffers; this layer clears the EOF latch
55//! it set itself, and deliberately does **not** touch the attachment
56//! bookkeeping — an attachment already handed out is never handed out
57//! again, and one not yet handed out is still owed.
58
59use std::collections::{TryReserveError, VecDeque};
60use std::{
61  ffi::{CStr, c_int},
62  io::{Read, Seek},
63  num::NonZeroI32,
64  path::Path,
65  ptr::{addr_of, read_unaligned},
66  sync::Arc,
67};
68
69// **The track handle's refcount is triomphe's, not `std`'s**, for the
70// reason [`crate::buffer`] gives at length: `std::sync::Arc::new`
71// aborts when the allocator declines, and the number of these headers
72// is the container's stream count. `triomphe::Arc::try_new` reports
73// it. `std::sync::Arc` stays for the reader-panic latch, whose one
74// allocation is per session rather than per stream.
75use triomphe::Arc as TrackArc;
76
77use derive_more::{IsVariant, TryUnwrap, Unwrap};
78use ffmpeg_next::{
79  Packet, Rational,
80  ffi::{
81    AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_TIMED_THUMBNAILS, AV_NOPTS_VALUE, AVDictionary,
82    AVStream, av_dict_get,
83  },
84  format::{self, context::Input},
85};
86use mediadecode::{
87  Timebase, Timestamp,
88  demuxer::{
89    AttachmentPacket, AttachmentTrackPacket, AttachmentTrackParams, AudioTrackPacket,
90    AudioTrackParams, Chapter, DataTrackPacket, DataTrackParams, DemuxedPacket, Demuxer,
91    SubtitleTrackPacket, SubtitleTrackParams, TrackIndex, TrackInfo, TrackKind, TrackParams,
92    UnknownTrackParams, VideoTrackPacket, VideoTrackParams,
93  },
94};
95use smol_bytes::Utf8Bytes;
96
97use crate::{
98  Ffmpeg, boundary,
99  buffer::PacketBufferError,
100  codec_id::CodecId,
101  extras::{AttachmentPacketExtra, TrackExtra},
102  limits::DemuxLimits,
103  reader_guard::{GuardedReader, PanicLatch},
104  sample_format::SampleFormat,
105};
106
107/// One microsecond — the timebase `avformat_seek_file` expects when no
108/// reference stream is named (`stream_index == -1`).
109fn av_time_base_q() -> Timebase {
110  Timebase::new(1, NonZeroI32::new(1_000_000).expect("1e6 is non-zero"))
111}
112
113/// `mediadecode::demuxer::Demuxer` impl wrapping `ffmpeg::format::context::Input`.
114///
115/// Construction is deliberately not on the trait — see [`Self::open`]
116/// and [`Self::open_reader`].
117pub struct CarrierDemuxer<C: crate::FfmpegCarrier> {
118  input: Input,
119  /// The track table, built once at open and held for the life of the
120  /// session — **this is the table `next_packet` classifies against**,
121  /// so nothing may take it away.
122  ///
123  /// Rows are `Arc`-wrapped at the door rather than by each consumer:
124  /// [`TrackInfo`] is not `Clone` (the message-carrier law), so a
125  /// consumer that needs a row past a borrow of this session needs a
126  /// shared handle, and one allocation per track at open is the whole
127  /// cost of every fan-out afterwards. `Arc` and not `Rc` because
128  /// [`CodecTicket`](crate::ticket::CodecTicket) made these rows
129  /// `Send + Sync` by construction precisely so a track table could
130  /// cross tasks.
131  tracks: Vec<TrackArc<TrackInfo<Ffmpeg>>>,
132  /// The container's table of contents, mirrored once at open and held
133  /// for the life of the session — see [`build_chapters`].
134  ///
135  /// Not `Arc`-wrapped, unlike the track rows above, and for the
136  /// reason [`Chapter`] gives: a chapter is four scalars and a title,
137  /// so a consumer that wants one past a borrow of this session clones
138  /// the row itself and there is no carrier choice worth making.
139  ///
140  /// Empty for the overwhelming majority of files — nothing allocates
141  /// where a container declares no chapters.
142  chapters: Vec<Chapter<Ffmpeg>>,
143  /// What libavformat decided the bytes are wrapped in, read once at
144  /// open — see [`CarrierDemuxer::format`].
145  ///
146  /// Held rather than re-derived because it is a property of the
147  /// session: `avformat_open_input` picks the demuxer and never changes
148  /// it, so the answer cannot move and a second read could only cost
149  /// more. `None` only where libavformat left `iformat` null or its
150  /// name is not readable text — neither of which a successful open
151  /// produces.
152  format: Option<crate::ContainerFormat>,
153  pending: VecDeque<(
154    TrackIndex,
155    AttachmentPacket<AttachmentPacketExtra, C::Buffer>,
156  )>,
157  /// `true` once this session has answered `Ok(None)`. Only then does
158  /// [`Self::seek`] clear the `AVIOContext`'s EOF latch — clearing it
159  /// unconditionally would also erase a genuine sticky I/O error, which
160  /// `Input::seek` goes out of its way to preserve.
161  eof: bool,
162  /// `true` once this session has reported a packet whose stream the
163  /// track table does not describe.
164  ///
165  /// The diagnostic is **once per session, not once per packet**. A
166  /// format that adds an `AVStream` mid-read (`AVFMTCTX_NOHEADER`:
167  /// MPEG-TS, RTP) then delivers packets on it at the wire's own rate,
168  /// and a line each would be an unbounded log on healthy input — a
169  /// live stream could fill a disk with it. One line names the
170  /// condition; the rest of the session stays quiet. Never cleared,
171  /// including across a seek: it records that this session has said
172  /// its piece, which a seek does not undo.
173  unplaceable_reported: bool,
174  /// Set for a session opened over a caller's reader: where a panic
175  /// raised inside that reader is recorded. `None` for a path-opened
176  /// session, which runs no caller code.
177  reader_panic: Option<Arc<PanicLatch>>,
178  /// The budgets this session spends: on any one timed packet, and —
179  /// already spent, at open — on the file's attachments.
180  limits: DemuxLimits,
181  /// A packet `av_read_frame` has already handed over and whose
182  /// conversion has **not committed**, with the provenance that was
183  /// observed for it.
184  ///
185  /// `av_read_frame` advances the container: once it returns, that
186  /// packet is off the wire and nothing brings it back. A conversion
187  /// that then fails on an *allocation* — a refcount the view lane
188  /// could not take, a copy the middle row could not make — used to
189  /// drop it, leaving a live session that answered the next pull with
190  /// the **following** packet. Compressed data and subtitle cues went
191  /// missing under memory pressure, quietly.
192  ///
193  /// So the read and the conversion are one transaction with a seat
194  /// between them: a transient refusal parks the packet here and the
195  /// next pull re-attempts *this* packet before reading another. It is
196  /// the same park-then-replay the decode household already runs —
197  /// `CarrierVideoStreamDecoder` holds `sw_replay_frames`, and the
198  /// probe holds its rescue history — for the same reason: a byte C
199  /// has already given up is not re-askable.
200  ///
201  /// The provenance is parked **with** the packet rather than re-probed
202  /// on replay. It is an observation about the moment of delivery, and
203  /// a queue that has moved on could answer it differently.
204  unconverted: Option<(Packet, crate::buffer::PayloadProvenance)>,
205}
206
207// The generic bodies. Crate-private, because their bound is: they are
208// the implementation, and the public faces below are written per lane
209// so that no signature a consumer reads names a trait they cannot.
210impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierDemuxer<C> {
211  /// Opens a container from a filesystem path.
212  ///
213  /// Runs `avformat_open_input` followed by
214  /// `avformat_find_stream_info`, then builds the track table and
215  /// captures every attachment payload.
216  ///
217  /// Call [`ffmpeg_next::init`] once before the first open if you want
218  /// FFmpeg's logging and network protocols configured; probing a local
219  /// container does not require it.
220  pub(crate) fn open_impl<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
221    Self::open_with_impl(path, DemuxLimits::default())
222  }
223
224  /// [`Self::open`], with the session's resource budgets named.
225  ///
226  /// The budgets are taken **at open** rather than through a `with_*`
227  /// builder because the attachment half of them is spent here: every
228  /// attachment payload in the file is captured before this call
229  /// returns, which is what makes the demux tier's "exactly one packet,
230  /// before any timed packet" contract true by construction. A budget
231  /// set afterwards would arrive after the spending.
232  ///
233  /// A file whose attachments exceed the budget **fails to open**, with
234  /// [`DemuxError::AttachmentTooLarge`] or
235  /// [`DemuxError::AttachmentBudgetExhausted`] naming the track that
236  /// crossed the line.
237  pub(crate) fn open_with_impl<P: AsRef<Path> + ?Sized>(
238    path: &P,
239    limits: DemuxLimits,
240  ) -> Result<Self, DemuxError> {
241    // **The probe knobs, set before libavformat reads a byte.** See
242    // [`DemuxLimits::max_probe_bytes`]: `avformat_open_input` and
243    // `avformat_find_stream_info` build the attachment, extradata and
244    // coded-side-data buffers themselves, so every budget that measures
245    // *this crate's* copies arrives after the original allocation. The
246    // instrument that reaches behind that is the one bounding what the
247    // parser is handed in the first place.
248    //
249    // On this entrypoint that is `probesize` / `formatprobesize` /
250    // `max_streams` only: the hard byte meter needs an `AVIOContext`
251    // this crate owns, and a path is opened by libavformat's own
252    // protocol layer. The reader entrypoint gets both.
253    Self::from_input(
254      format::input_with_dictionary(path, probe_options(limits))?,
255      limits,
256    )
257  }
258
259  /// Opens a container from any `Read + Seek` byte source, through a
260  /// custom `AVIOContext`.
261  ///
262  /// `Seek` is mandatory and not negotiable: MP4 files routinely put
263  /// `moov` at the end, so a reader that cannot go backwards cannot be
264  /// probed at all — and the seek law on the face would be
265  /// unimplementable.
266  ///
267  /// `filename` is a probe hint, not a path: libavformat uses its
268  /// extension to break ties between formats whose byte signatures are
269  /// ambiguous. Pass `None` when there is nothing to hint with.
270  ///
271  /// # A panicking reader
272  ///
273  /// libavformat drives the reader from `extern "C"` callbacks, where a
274  /// panic would abort the process rather than unwind. Every call into
275  /// `reader` therefore runs under `catch_unwind`: a panic becomes an
276  /// I/O error for libavformat and surfaces here — or from the next
277  /// [`next_packet`](Demuxer::next_packet) / [`seek`](Demuxer::seek) —
278  /// as [`DemuxError::ReaderPanic`], carrying the panic's message. The
279  /// session is terminal from that point: the `AVIOContext`'s error
280  /// state is sticky and the reader's own state is unknown.
281  pub(crate) fn open_reader_impl<R: Read + Seek + Send + 'static>(
282    reader: R,
283    filename: Option<&str>,
284  ) -> Result<Self, DemuxError> {
285    Self::open_reader_with_impl(reader, filename, DemuxLimits::default())
286  }
287
288  /// [`Self::open_reader`], with the session's resource budgets named.
289  /// See [`Self::open_with`] for why they are taken at open.
290  pub(crate) fn open_reader_with_impl<R: Read + Seek + Send + 'static>(
291    reader: R,
292    filename: Option<&str>,
293    limits: DemuxLimits,
294  ) -> Result<Self, DemuxError> {
295    let (guarded, latch, meter) = GuardedReader::new(reader, limits.max_probe_bytes());
296    let io = format::context::StreamIo::from_read_seek(guarded)?;
297    let input =
298      format::input_from_stream(io, filename, Some(probe_options(limits))).map_err(|e| {
299        // Three ways this can fail, and they must not be confused: a
300        // panicked reader, a probe budget reached, or libavformat's own
301        // verdict. The meter is consulted before the errno because
302        // libavformat folds the reader's I/O error into whatever it was
303        // doing at the time — usually "invalid data" — which would
304        // report a refusal this crate made as a malformed file.
305        reader_panic(&latch)
306          .or_else(|| {
307            meter.tripped().then(|| {
308              DemuxError::ProbeBudgetExhausted(ProbeBudgetExhausted::new(
309                meter.read(),
310                meter.budget(),
311              ))
312            })
313          })
314          .unwrap_or(DemuxError::Ffmpeg(e))
315      })?;
316    if meter.tripped() {
317      return Err(DemuxError::ProbeBudgetExhausted(ProbeBudgetExhausted::new(
318        meter.read(),
319        meter.budget(),
320      )));
321    }
322    // Open and analysed: the seat bounds *probing*, and reading the
323    // media itself afterwards is the caller's business, packet by
324    // packet, already bounded by the packet seats.
325    meter.release();
326    // A panic libavformat tolerated (a failed probe it recovered from)
327    // still poisoned the reader; the session must not open over it.
328    if let Some(panicked) = reader_panic(&latch) {
329      return Err(panicked);
330    }
331    let mut demuxer = Self::from_input(input, limits)?;
332    demuxer.reader_panic = Some(latch);
333    Ok(demuxer)
334  }
335
336  /// Borrows the wrapped `ffmpeg::format::context::Input` — for
337  /// `av_dump_format`, container-level metadata, and anything else the
338  /// portable track and chapter tables have no seat for.
339  #[cfg_attr(not(tarpaulin), inline(always))]
340  pub(crate) const fn input_impl(&self) -> &Input {
341    &self.input
342  }
343
344  /// The budgets this session was opened with.
345  #[cfg_attr(not(tarpaulin), inline(always))]
346  pub(crate) const fn limits_impl(&self) -> DemuxLimits {
347    self.limits
348  }
349
350  /// What libavformat decided this session's bytes are wrapped in.
351  #[cfg_attr(not(tarpaulin), inline(always))]
352  pub(crate) const fn format_impl(&self) -> Option<&crate::ContainerFormat> {
353    self.format.as_ref()
354  }
355
356  fn from_input(input: Input, limits: DemuxLimits) -> Result<Self, DemuxError> {
357    // **Everything the container declares is judged before anything it
358    // declares is paid for**, and that is a property of the *whole*
359    // open rather than of either table.
360    //
361    // The order is: [`admit_chapters`] here, then [`admit_streams`] as
362    // `build_tracks`' first statement, and only then a reservation. Both
363    // passes allocate nothing — they read integers, and price metadata
364    // through a borrow of libavutil's own buffer — so every budgeted
365    // quantity this open can refuse is refused while the process has
366    // spent nothing on the file.
367    //
368    // The class, stated once so it can be checked rather than
369    // rediscovered: probe bytes are metered during the read;
370    // `max_streams` is libavformat's own option, set before the header
371    // is parsed; per-stream and whole-file codec parameters — which
372    // includes extradata, every coded-side-data payload and a custom
373    // channel map, all through
374    // [`measure_parameters`](crate::extras::measure_parameters) — the
375    // per-attachment and whole-file attachment payloads, and the
376    // whole-file stream metadata are charged by `admit_streams`; the
377    // chapter count and the chapter titles by `admit_chapters`; and a
378    // single metadata value is bounded by [`metadata_value`]'s own
379    // walk, which stops at [`METADATA_VALUE_MAX_BYTES`] rather than
380    // reading past it. Three rounds of review found three instances of
381    // one defect here — a correct check placed after the memory it was
382    // meant to protect — which is why the list is written down.
383    admit_chapters(&input, limits)?;
384    let (tracks, pending) = build_tracks::<C>(&input, limits)?;
385    // One allocation per track, here and never again: the session
386    // keeps these handles and hands out clones of them.
387    //
388    // The table is reserved fallibly and so is each row's handle:
389    // `triomphe::Arc::try_new` reports an allocator refusal where
390    // `std::sync::Arc::new` would abort, and the count is the
391    // container's. See [`crate::buffer`] for why this crate's
392    // refcount is triomphe's.
393    let count = tracks.len();
394    let mut handles: Vec<TrackArc<TrackInfo<Ffmpeg>>> = Vec::new();
395    handles
396      .try_reserve_exact(count)
397      .map_err(|_| DemuxError::TrackTableAlloc(TrackTableAlloc::new(count)))?;
398    for row in tracks {
399      handles.push(
400        TrackArc::try_new(row)
401          .map_err(|_| DemuxError::TrackTableAlloc(TrackTableAlloc::new(count)))?,
402      );
403    }
404    let tracks = handles;
405    // The chapter table is read here for the same reason the track
406    // table is: `avformat_find_stream_info` has run, so the container's
407    // answer is final and a session that holds it can be asked at any
408    // point without touching the file again.
409    let chapters = build_chapters(&input, limits)?;
410    // SAFETY: `input` owns a live `AVFormatContext` for the whole of
411    // this call, and the read takes copies of the two static-table
412    // strings rather than borrowing from it.
413    let format = unsafe { crate::ContainerFormat::from_context(input.as_ptr()) };
414    Ok(Self {
415      input,
416      tracks,
417      chapters,
418      format,
419      pending,
420      unconverted: None,
421      eof: false,
422      unplaceable_reported: false,
423      reader_panic: None,
424      limits,
425    })
426  }
427
428  /// The error a panicked reader owes this session, if one panicked.
429  fn panicked(&self) -> Option<DemuxError> {
430    self.reader_panic.as_deref().and_then(reader_panic)
431  }
432}
433
434/// The libavformat options this crate sets before a container is
435/// opened.
436///
437/// Passed as an `AVDictionary` because that is the only route to these
438/// fields that works for both entrypoints: `avformat_open_input`
439/// applies the dictionary to the context it allocates itself, and the
440/// same names reach the context behind a custom `AVIOContext`.
441///
442/// * `probesize` / `formatprobesize` bound what the format probe and
443///   the stream analysis are allowed to consume;
444/// * `max_streams` bounds the `AVStream` array a header can conjure —
445///   a container claiming a hundred thousand streams is an allocation
446///   this crate's per-track budgets are downstream of.
447fn probe_options(limits: DemuxLimits) -> ffmpeg_next::Dictionary<'static> {
448  let mut options = ffmpeg_next::Dictionary::new();
449  let probe = limits.max_probe_bytes().to_string();
450  options.set("probesize", &probe);
451  options.set("formatprobesize", &probe);
452  options.set("max_streams", &limits.max_streams().to_string());
453  options
454}
455
456/// Payload for [`DemuxError::ProbeBudgetExhausted`].
457///
458/// libavformat wanted more of the file than the probe budget allows.
459///
460/// # What this bounds
461///
462/// This is the only seat in the crate that reaches *behind*
463/// libavformat: `avformat_open_input` and `avformat_find_stream_info`
464/// build the attached picture, the extradata and the coded side data
465/// out of the file themselves, so every budget measuring this crate's
466/// own copies necessarily arrives after those allocations happened.
467///
468/// A parser cannot allocate from bytes it was never handed, so the
469/// input is bounded instead. What is **not** bounded is amplification
470/// inside a parser — a container can describe, in a few bytes, a
471/// structure whose in-memory form is far larger, and nothing outside
472/// libavformat can observe that. Bounding the output of that is the
473/// substrate's own hardening territory; FFmpeg keeps `max_streams`,
474/// `max_index_size` and `max_picture_buffer` for it, and this crate
475/// sets the first.
476#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
477#[error("libavformat read {read} bytes probing the container, over a budget of {budget}")]
478pub struct ProbeBudgetExhausted {
479  read: u64,
480  budget: u64,
481}
482
483impl ProbeBudgetExhausted {
484  /// Constructs a `ProbeBudgetExhausted` payload.
485  #[inline]
486  pub const fn new(read: u64, budget: u64) -> Self {
487    Self { read, budget }
488  }
489  /// Bytes libavformat was handed before the budget was reached.
490  #[inline]
491  pub const fn read(&self) -> u64 {
492    self.read
493  }
494  /// The budget in force.
495  #[inline]
496  pub const fn budget(&self) -> u64 {
497    self.budget
498  }
499}
500
501/// Turns a latched reader panic into the error that names it.
502fn reader_panic(latch: &PanicLatch) -> Option<DemuxError> {
503  latch
504    .message()
505    .map(|message| DemuxError::ReaderPanic(ReaderPanic::new(message)))
506}
507
508impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierDemuxer<C> {
509  pub(crate) fn tracks_impl(&self) -> &[TrackArc<TrackInfo<Ffmpeg>>] {
510    &self.tracks
511  }
512
513  pub(crate) fn chapters_impl(&self) -> &[Chapter<Ffmpeg>] {
514    &self.chapters
515  }
516
517  pub(crate) fn next_packet_impl(
518    &mut self,
519  ) -> Result<Option<DemuxedPacket<Ffmpeg, C::Buffer>>, DemuxError> {
520    // A latched reader panic is terminal, and terminal starts here. The
521    // queue is filled at open and owes nothing to the reader, so a pull
522    // that drained it would answer `Ok` to a caller the session has
523    // already told the truth to — `seek` can latch a panic while
524    // attachments are still queued.
525    if let Some(panicked) = self.panicked() {
526      return Err(panicked);
527    }
528
529    // The attachment queue drains first and drains completely, which is
530    // the whole of "exactly one packet, before any timed packet": no
531    // `av_read_frame` has run yet when the last one leaves.
532    if let Some((track, packet)) = self.pending.pop_front() {
533      return Ok(Some(DemuxedPacket::Attachment(AttachmentTrackPacket::new(
534        track, packet,
535      ))));
536    }
537
538    loop {
539      // **A parked packet is re-attempted before another is read.**
540      // See [`Self::unconverted`]: `av_read_frame` has already given
541      // this one up, so reading past it would lose it.
542      let (packet, parked_provenance) = match self.unconverted.take() {
543        Some((packet, provenance)) => (packet, Some(provenance)),
544        None => {
545          let mut packet = Packet::empty();
546          let read = packet.read(&mut self.input);
547          // A panicking reader reported an ordinary I/O error to C, and
548          // libavformat may answer that with the error, with EOF (a
549          // stream it cannot read looks finished), or with a packet it
550          // had already buffered. None of those are the file's word, so
551          // the latch is consulted whatever the outcome was.
552          if let Some(panicked) = self.panicked() {
553            return Err(panicked);
554          }
555          match read {
556            Ok(()) => {}
557            Err(ffmpeg_next::Error::Eof) => {
558              self.eof = true;
559              return Ok(None);
560            }
561            // A demuxer can resync past a corrupt packet, and
562            // `AVERROR_INVALIDDATA` is not latched into the
563            // `AVIOContext`, so reading again makes progress. Every
564            // other error is sticky and is surfaced.
565            Err(ffmpeg_next::Error::InvalidData) => continue,
566            Err(e) => return Err(DemuxError::Ffmpeg(e)),
567          }
568          (packet, None)
569        }
570      };
571
572      let index = packet.stream();
573      // A packet for a stream the table does not describe cannot be
574      // placed, so it is passed by — the same answer the `Unknown` arm
575      // below gives a track nothing can name.
576      //
577      // **Neither an assertion nor an error.** The arm is reachable on
578      // healthy input: a format flagged `AVFMTCTX_NOHEADER` — MPEG-TS,
579      // RTP and the rest that carry no up-front stream list — may add
580      // an `AVStream` in the middle of `av_read_frame`, and this
581      // session's table was fixed at open, which is the contract
582      // `TrackIndex` needs (position in `tracks()`, dense and stable
583      // for the life of the session). A `debug_assert` would fire on a
584      // transport stream, and an `Err` would end a session over a
585      // stream the caller never asked about.
586      //
587      // It is no longer the *every* packet path. It was, for one
588      // release: the take-the-table door emptied this very `Vec`, so
589      // every index fell out of range at once and a healthy file
590      // demuxed to nothing (issue #51). The table cannot be taken
591      // away any more; what is left here is the genuinely
592      // out-of-range index the arm was written for. Reported rather
593      // than silent, because silence is what made the old failure
594      // invisible — and reported **once**, because the very case that
595      // makes the arm reachable is a live stream that would otherwise
596      // log a line per packet for as long as it runs. See
597      // [`Self::unplaceable_reported`].
598      let Some(info) = self.tracks.get(index) else {
599        if !self.unplaceable_reported {
600          self.unplaceable_reported = true;
601          tracing::debug!(
602            stream = index,
603            tracks = self.tracks.len(),
604            "demux: no track row describes this packet's stream; passing it and any further \
605             such packet by, without repeating this line",
606          );
607        }
608        continue;
609      };
610      let track = TrackIndex::new(index);
611      let time_base = info.timebase();
612
613      // A payload that is there and cannot be referenced is an error,
614      // never a silently dropped packet: `Ok(None)` below means the
615      // packet carried nothing, and that is the only thing that reads
616      // the next one.
617      // **Everything this loop delivers is demux-delivered**, whatever
618      // its refcount: libavformat just handed it over, so any other
619      // reference to its buffer is libavformat's own and no
620      // `ffmpeg_next::Packet` wraps one. That is not the hazard a
621      // caller's second handle is — see
622      // [`crate::buffer::PayloadProvenance`].
623      //
624      // Sharing is ordinary here. A queue-backed demuxer — SubRip,
625      // SubViewer and the rest of the `FFDemuxSubtitlesQueue` family —
626      // keeps its parsed cues and delivers `av_packet_ref`s of them,
627      // so *every* packet it produces arrives with two references.
628      //
629      // The one sub-case that is stronger still is the container's
630      // parked picture, which a stream carrying
631      // `ATTACHED_PIC | TIMED_THUMBNAILS` delivers as its first packet:
632      // written once while the container opened, so the view lane may
633      // window it rather than copy. See [`is_streams_attached_pic`] for
634      // the identity proof.
635      //
636      // SAFETY: both the session's `AVFormatContext` and `packet` are
637      // live here.
638      let provenance = match parked_provenance {
639        // Observed when this packet was delivered, and kept with it.
640        Some(provenance) => provenance,
641        None if unsafe { is_streams_attached_pic(&self.input, index, &packet) } => {
642          crate::buffer::PayloadProvenance::AttachedPicture
643        }
644        None => crate::buffer::PayloadProvenance::DemuxDelivered,
645      };
646
647      // The packet this loop just read is **handed over**, not lent: the
648      // view lane's carrier is a window into its buffer, and a source
649      // that survived the conversion would be a mutable alias of it.
650      // Exactly one arm runs, so exactly one move happens.
651      // **The conversion borrows what this session owns.** The packet
652      // stays in hand until the carrier exists, which is what lets a
653      // failure park it instead of dropping it; on success it falls out
654      // of scope at the end of the iteration and the carrier keeps its
655      // buffer alive by refcount, exactly as when the conversion
656      // consumed it. Nothing outside this loop ever sees the packet, so
657      // the borrow cannot become the aliasing shape the public faces
658      // refuse.
659      let converted = match info.kind() {
660        TrackKind::Video => boundary::video_packet_from_borrowed::<C>(
661          &packet,
662          time_base,
663          self.limits.packet(),
664          provenance,
665        )
666        .map(|built| built.map(|p| DemuxedPacket::Video(VideoTrackPacket::new(track, p)))),
667        TrackKind::Audio => boundary::audio_packet_from_borrowed::<C>(
668          &packet,
669          time_base,
670          self.limits.packet(),
671          provenance,
672        )
673        .map(|built| built.map(|p| DemuxedPacket::Audio(AudioTrackPacket::new(track, p)))),
674        TrackKind::Subtitle => boundary::subtitle_packet_from_borrowed::<C>(
675          &packet,
676          time_base,
677          self.limits.packet(),
678          provenance,
679        )
680        .map(|built| built.map(|p| DemuxedPacket::Subtitle(SubtitleTrackPacket::new(track, p)))),
681        TrackKind::Data => boundary::data_packet_from_borrowed::<C>(
682          &packet,
683          time_base,
684          self.limits.packet(),
685          provenance,
686        )
687        .map(|built| built.map(|p| DemuxedPacket::Data(DataTrackPacket::new(track, p)))),
688        // Every attachment track's one packet was queued at open time,
689        // so anything arriving on one now is the duplicate some
690        // demuxers emit for cover art. Drop it — the contract is
691        // exactly one, and the one has already left. Nothing is
692        // converted here, so there is nothing to park.
693        TrackKind::Attachment => continue,
694        // The roster of arms is five; a track nothing can name has no
695        // arm and its packets are not delivered.
696        TrackKind::Unknown => continue,
697      };
698
699      let built = match converted {
700        Ok(built) => built,
701        Err(source) => {
702          // **Park a refusal that another attempt could survive.** An
703          // allocation that failed says nothing about the packet, and
704          // the packet is off the wire either way. Anything else is a
705          // fact about the packet itself — a malformed one is not made
706          // well-formed by retrying, and parking it would answer every
707          // later pull with the same error instead of letting the
708          // session make progress.
709          if source.parks_in_demux() {
710            self.unconverted = Some((packet, provenance));
711          }
712          return Err(DemuxError::PacketBuffer(PacketBuffer::new(index, source)));
713        }
714      };
715
716      // `None` here means the packet carried no payload — an empty
717      // packet, which some demuxers emit as a marker. Nothing to
718      // deliver; read the next one.
719      if let Some(out) = built {
720        return Ok(Some(out));
721      }
722    }
723  }
724
725  pub(crate) fn seek_impl(&mut self, target: Timestamp) -> Result<(), DemuxError> {
726    let ts = target.rescale_to(av_time_base_q()).pts();
727    // Only our own EOF latch is cleared, and only before the seek —
728    // the seek machinery gates on `eof_reached`, so clearing it
729    // afterwards would be too late.
730    if self.eof {
731      self.input.clear_eof();
732      self.eof = false;
733    }
734    // `..ts` is how ffmpeg-next spells the seek window: it reads only
735    // the endpoint, and `avformat_seek_file`'s `max_ts` is inclusive,
736    // so the window is `[i64::MIN, ts]`. FFmpeg picks the closest seek
737    // point inside it — the nearest keyframe at or before the target.
738    // Never after: a decoder started past the target has no reference
739    // frame.
740    let sought = self.input.seek(ts, ..ts);
741    if let Some(panicked) = self.panicked() {
742      return Err(panicked);
743    }
744    sought?;
745    // **The seat is cleared by a seek that happened, not by one that
746    // was attempted.** A parked packet belongs to the position the
747    // session is leaving, so a successful seek discards it. A *failed*
748    // one leaves the session where it was — and that packet is off the
749    // wire, so dropping it here would be the same silent loss the seat
750    // exists to prevent, with no re-read able to recover it.
751    //
752    // FFmpeg does not specify where a container sits after a seek that
753    // returned an error, and this crate does not guess: it keeps a
754    // packet the container really did deliver, and a caller who saw the
755    // seek fail already knows the position is not the one they asked
756    // for. Every timestamp needed to tell is on the packet.
757    self.unconverted = None;
758    Ok(())
759  }
760}
761
762macro_rules! demuxer_lane_face {
763  ($($lane:ty),+ $(,)?) => { $(
764    impl CarrierDemuxer<$lane> {
765      /// Opens a container from a filesystem path.
766      ///
767      /// Runs `avformat_open_input` followed by
768      /// `avformat_find_stream_info`, then builds the track table and
769      /// captures every attachment payload.
770      ///
771      /// Call [`ffmpeg_next::init`] once before the first open if you
772      /// want FFmpeg's logging and network protocols configured;
773      /// probing a local container does not require it.
774      pub fn open<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
775        Self::open_impl(path)
776      }
777
778      /// [`Self::open`], with the session's resource budgets named.
779      ///
780      /// The budgets are taken **at open** rather than through a
781      /// `with_*` builder because the attachment half of them is spent
782      /// here: every attachment payload is captured during this call.
783      pub fn open_with<P: AsRef<Path> + ?Sized>(
784        path: &P,
785        limits: DemuxLimits,
786      ) -> Result<Self, DemuxError> {
787        Self::open_with_impl(path, limits)
788      }
789
790      /// Opens a container from any `Read + Seek` source.
791      pub fn open_reader<R: Read + Seek + Send + 'static>(
792        reader: R,
793        url: Option<&str>,
794      ) -> Result<Self, DemuxError> {
795        Self::open_reader_impl(reader, url)
796      }
797
798      /// [`Self::open_reader`], with the session's budgets named.
799      pub fn open_reader_with<R: Read + Seek + Send + 'static>(
800        reader: R,
801        url: Option<&str>,
802        limits: DemuxLimits,
803      ) -> Result<Self, DemuxError> {
804        Self::open_reader_with_impl(reader, url, limits)
805      }
806
807      /// The wrapped `AVFormatContext`.
808      pub const fn input(&self) -> &Input {
809        self.input_impl()
810      }
811
812      /// The budgets this session was opened with.
813      pub const fn limits(&self) -> DemuxLimits {
814        self.limits_impl()
815      }
816
817      /// **What the container IS**, as libavformat identified it from
818      /// the bytes — the demuxer it chose, with the short names that
819      /// demuxer handles and its description.
820      ///
821      /// Decided during the open and fixed for the life of the
822      /// session, so this answers the same thing at any point and
823      /// costs nothing to ask.
824      ///
825      /// `None` only where libavformat left `iformat` null or its name
826      /// is not readable text; neither happens on a session that
827      /// opened successfully.
828      ///
829      /// **Nothing here looked at a path.** A file's extension is a
830      /// claim about its bytes, and this is a reading of them — which
831      /// is what makes the answer usable on a content-addressed row,
832      /// where the same bytes under two names are one content. See
833      /// [`ContainerFormat`](crate::ContainerFormat) for what the
834      /// demuxer's name does and does not narrow to.
835      pub const fn format(&self) -> Option<&crate::ContainerFormat> {
836        self.format_impl()
837      }
838    }
839
840    impl Demuxer for CarrierDemuxer<$lane> {
841      type Adapter = Ffmpeg;
842      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
843      type TrackHandle = TrackArc<TrackInfo<Ffmpeg>>;
844      type Error = DemuxError;
845
846      /// The track table, held for the life of the session.
847      ///
848      /// Reading it takes nothing away — clone the handles worth
849      /// keeping. `Arc` is the carrier because
850      /// [`CodecTicket`](crate::ticket::CodecTicket) mirrors an
851      /// `AVCodecParameters` into owned Rust, which is what makes a
852      /// row `Send + Sync` and a table shareable across tasks.
853      fn tracks(&self) -> &[TrackArc<TrackInfo<Ffmpeg>>] {
854        self.tracks_impl()
855      }
856
857      /// The container's chapter table, in the order
858      /// `AVFormatContext.chapters` holds it, mirrored at open and
859      /// held for the life of the session.
860      ///
861      /// Empty where the file declares no chapters — which is the
862      /// provided answer too, so the override changes nothing for a
863      /// container that has none.
864      ///
865      /// **Nothing is repaired.** A chapter whose `end` precedes its
866      /// `start`, and one whose end libavformat left at its
867      /// no-timestamp sentinel because the file declared none, are
868      /// mirrored exactly as written; see [`Chapter`] for why this
869      /// layer reports rather than clamps.
870      fn chapters(&self) -> &[Chapter<Ffmpeg>] {
871        self.chapters_impl()
872      }
873
874      /// Pulls the next packet.
875      ///
876      /// **A refusal that another attempt could survive costs no
877      /// packet.** `av_read_frame` advances the container, so a
878      /// conversion that then fails on an allocation would otherwise
879      /// drop bytes nothing can ask for again. Such a packet is parked
880      /// instead, and this method re-attempts *it* before reading
881      /// another — so a caller who pulls again loses nothing. A refusal
882      /// about the packet itself is not parked: retrying a malformed
883      /// packet forever would be worse than passing it by.
884      fn next_packet(
885        &mut self,
886      ) -> Result<Option<DemuxedPacket<Ffmpeg, Self::Buffer>>, DemuxError> {
887        self.next_packet_impl()
888      }
889
890      fn seek(&mut self, target: Timestamp) -> Result<(), DemuxError> {
891        self.seek_impl(target)
892      }
893    }
894  )+ };
895}
896
897demuxer_lane_face!(crate::View, crate::Owned);
898
899/// Payload for [`DemuxError::AttachmentTooLarge`].
900///
901/// One attachment's payload exceeds
902/// [`DemuxLimits::max_attachment_bytes`].
903#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
904#[error(
905  "the attachment on stream {stream_index} is {bytes} bytes, over the {limit}-byte per-attachment budget"
906)]
907pub struct AttachmentTooLarge {
908  stream_index: usize,
909  bytes: usize,
910  limit: usize,
911}
912
913impl AttachmentTooLarge {
914  /// Constructs an `AttachmentTooLarge` payload.
915  #[cfg_attr(not(tarpaulin), inline(always))]
916  pub const fn new(stream_index: usize, bytes: usize, limit: usize) -> Self {
917    Self {
918      stream_index,
919      bytes,
920      limit,
921    }
922  }
923  /// The `AVStream.index` carrying the oversized attachment.
924  #[cfg_attr(not(tarpaulin), inline(always))]
925  pub const fn stream_index(&self) -> usize {
926    self.stream_index
927  }
928  /// The attachment's payload length.
929  #[cfg_attr(not(tarpaulin), inline(always))]
930  pub const fn bytes(&self) -> usize {
931    self.bytes
932  }
933  /// The per-attachment budget in force.
934  #[cfg_attr(not(tarpaulin), inline(always))]
935  pub const fn limit(&self) -> usize {
936    self.limit
937  }
938}
939
940/// Payload for [`DemuxError::AttachmentBudgetExhausted`].
941///
942/// The file's attachments, together, exceed
943/// [`DemuxLimits::max_total_attachment_bytes`].
944///
945/// Separate from [`AttachmentTooLarge`] because it is a different
946/// attack: every attachment can be modest and there can still be four
947/// hundred of them. This arm names the track that ran the total past
948/// the line, not the track that was individually at fault — there
949/// need not be one.
950#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
951#[error(
952  "the attachment on stream {stream_index} brings the file's attachments to {total} bytes, over the {limit}-byte budget"
953)]
954pub struct AttachmentBudgetExhausted {
955  stream_index: usize,
956  total: usize,
957  limit: usize,
958}
959
960impl AttachmentBudgetExhausted {
961  /// Constructs an `AttachmentBudgetExhausted` payload.
962  #[cfg_attr(not(tarpaulin), inline(always))]
963  pub const fn new(stream_index: usize, total: usize, limit: usize) -> Self {
964    Self {
965      stream_index,
966      total,
967      limit,
968    }
969  }
970  /// The `AVStream.index` whose attachment crossed the line.
971  #[cfg_attr(not(tarpaulin), inline(always))]
972  pub const fn stream_index(&self) -> usize {
973    self.stream_index
974  }
975  /// The running total, including this attachment.
976  #[cfg_attr(not(tarpaulin), inline(always))]
977  pub const fn total(&self) -> usize {
978    self.total
979  }
980  /// The whole-file budget in force.
981  #[cfg_attr(not(tarpaulin), inline(always))]
982  pub const fn limit(&self) -> usize {
983    self.limit
984  }
985}
986
987/// Payload for [`DemuxError::ParametersTooLarge`].
988///
989/// One stream's codec parameters hold more heap bytes than
990/// [`DemuxLimits::max_codec_parameter_bytes`] allows.
991///
992/// The bytes are `extradata` plus every `coded_side_data` entry plus a
993/// custom channel map — the three seats `AVCodecParameters` reaches the
994/// heap through. A MOV `prof` atom lands in the second of those as an
995/// ICC profile, which is where the honest large values live and where
996/// the forged ones do too.
997#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
998#[error(
999  "the codec parameters on stream {stream_index} hold {bytes} heap bytes, over the {limit}-byte budget"
1000)]
1001pub struct ParametersTooLarge {
1002  stream_index: usize,
1003  bytes: usize,
1004  limit: usize,
1005}
1006
1007impl ParametersTooLarge {
1008  /// Constructs a `ParametersTooLarge` payload.
1009  #[cfg_attr(not(tarpaulin), inline(always))]
1010  pub const fn new(stream_index: usize, bytes: usize, limit: usize) -> Self {
1011    Self {
1012      stream_index,
1013      bytes,
1014      limit,
1015    }
1016  }
1017  /// The `AVStream.index` whose parameters were refused.
1018  #[cfg_attr(not(tarpaulin), inline(always))]
1019  pub const fn stream_index(&self) -> usize {
1020    self.stream_index
1021  }
1022  /// The heap bytes the parameters declared.
1023  #[cfg_attr(not(tarpaulin), inline(always))]
1024  pub const fn bytes(&self) -> usize {
1025    self.bytes
1026  }
1027  /// The budget in force.
1028  #[cfg_attr(not(tarpaulin), inline(always))]
1029  pub const fn limit(&self) -> usize {
1030    self.limit
1031  }
1032}
1033
1034/// Payload for [`DemuxError::ParametersBudgetExhausted`].
1035///
1036/// Every stream's codec parameters, together, hold more heap bytes than
1037/// [`DemuxLimits::max_total_codec_parameter_bytes`] allows.
1038///
1039/// A separate attack from [`ParametersTooLarge`], and separate for the
1040/// same reason the attachment pair are: each stream's parameters can be
1041/// individually modest and a container can still declare two hundred
1042/// streams. The arm names the stream that ran the total past the line,
1043/// which need not be one that was individually at fault.
1044#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1045#[error(
1046  "the codec parameters on stream {stream_index} bring the file's to {total} heap bytes, over the {limit}-byte budget"
1047)]
1048pub struct ParametersBudgetExhausted {
1049  stream_index: usize,
1050  total: usize,
1051  limit: usize,
1052}
1053
1054impl ParametersBudgetExhausted {
1055  /// Constructs a `ParametersBudgetExhausted` payload.
1056  #[cfg_attr(not(tarpaulin), inline(always))]
1057  pub const fn new(stream_index: usize, total: usize, limit: usize) -> Self {
1058    Self {
1059      stream_index,
1060      total,
1061      limit,
1062    }
1063  }
1064  /// The `AVStream.index` whose parameters crossed the line.
1065  #[cfg_attr(not(tarpaulin), inline(always))]
1066  pub const fn stream_index(&self) -> usize {
1067    self.stream_index
1068  }
1069  /// The running total, including this stream.
1070  #[cfg_attr(not(tarpaulin), inline(always))]
1071  pub const fn total(&self) -> usize {
1072    self.total
1073  }
1074  /// The whole-file budget in force.
1075  #[cfg_attr(not(tarpaulin), inline(always))]
1076  pub const fn limit(&self) -> usize {
1077    self.limit
1078  }
1079}
1080
1081/// Payload for [`DemuxError::ParametersMissing`].
1082///
1083/// Codec parameters arrived that were never allocated.
1084///
1085/// `ffmpeg_next::codec::Parameters` has safe constructors that hand
1086/// back a null-backed value when FFmpeg's allocation failed, and they
1087/// report nothing. Copying from one dereferences null, so it is
1088/// refused where it arrives — at construction, and again in the
1089/// copier — rather than crashing later somewhere that has forgotten
1090/// the allocator ever failed.
1091#[derive(thiserror::Error, Debug, Clone)]
1092#[error("the codec parameters for stream {stream_index} were never allocated")]
1093pub struct ParametersMissing {
1094  stream_index: usize,
1095}
1096
1097impl ParametersMissing {
1098  /// Constructs a `ParametersMissing` payload.
1099  #[cfg_attr(not(tarpaulin), inline(always))]
1100  pub const fn new(stream_index: usize) -> Self {
1101    Self { stream_index }
1102  }
1103  /// The `AVStream.index` the parameters were offered for.
1104  #[cfg_attr(not(tarpaulin), inline(always))]
1105  pub const fn stream_index(&self) -> usize {
1106    self.stream_index
1107  }
1108}
1109
1110/// Payload for [`DemuxError::ParametersAlloc`].
1111///
1112/// Codec parameters for a track could not be allocated.
1113#[derive(thiserror::Error, Debug, Clone)]
1114#[error("out of memory allocating the codec parameters for stream {stream_index}")]
1115pub struct ParametersAlloc {
1116  stream_index: usize,
1117}
1118
1119impl ParametersAlloc {
1120  /// Constructs a `ParametersAlloc` payload.
1121  #[cfg_attr(not(tarpaulin), inline(always))]
1122  pub const fn new(stream_index: usize) -> Self {
1123    Self { stream_index }
1124  }
1125  /// The `AVStream.index` whose parameters could not be copied.
1126  #[cfg_attr(not(tarpaulin), inline(always))]
1127  pub const fn stream_index(&self) -> usize {
1128    self.stream_index
1129  }
1130}
1131
1132/// Payload for [`DemuxError::TrackTimebaseInvalid`].
1133///
1134/// A stream declares an `AVRational` timebase that is not a
1135/// [`Timebase`]: a zero or negative denominator, or a negative
1136/// numerator.
1137///
1138/// **Refused rather than repaired.** The value is what every timestamp
1139/// on that track would be measured against, and there is no honest
1140/// substitute — a ruler invented here is indistinguishable downstream
1141/// from one the file declared. `0/1`, libavformat's own "not set yet"
1142/// default, is **not** this error: see
1143/// [`TrackInfo::timebase`](mediadecode::demuxer::TrackInfo::timebase).
1144#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1145#[error("stream {stream_index} declares the timebase {num}/{den}, which is not a usable one")]
1146pub struct TrackTimebaseInvalid {
1147  stream_index: usize,
1148  num: i32,
1149  den: i32,
1150}
1151
1152impl TrackTimebaseInvalid {
1153  /// Constructs a `TrackTimebaseInvalid` payload.
1154  #[cfg_attr(not(tarpaulin), inline(always))]
1155  pub const fn new(stream_index: usize, num: i32, den: i32) -> Self {
1156    Self {
1157      stream_index,
1158      num,
1159      den,
1160    }
1161  }
1162  /// The `AVStream.index` that declared it.
1163  #[cfg_attr(not(tarpaulin), inline(always))]
1164  pub const fn stream_index(&self) -> usize {
1165    self.stream_index
1166  }
1167  /// The numerator the container wrote.
1168  #[cfg_attr(not(tarpaulin), inline(always))]
1169  pub const fn num(&self) -> i32 {
1170    self.num
1171  }
1172  /// The denominator the container wrote.
1173  #[cfg_attr(not(tarpaulin), inline(always))]
1174  pub const fn den(&self) -> i32 {
1175    self.den
1176  }
1177}
1178
1179/// Payload for [`DemuxError::ChapterTimebaseInvalid`].
1180///
1181/// A chapter declares an `AVRational` timebase that cannot rule its
1182/// span: a zero or negative denominator, a negative numerator, or a
1183/// **zero** numerator.
1184///
1185/// Stricter than [`TrackTimebaseInvalid`] by that last case, and
1186/// deliberately: a chapter's ruler is written by whatever wrote the
1187/// chapter, so `0/den` there is not an absence but a claim that every
1188/// boundary in the table falls on one instant.
1189///
1190/// **The whole open fails, rather than the row being dropped.** A
1191/// chapter table is a table: a reader that quietly returned the other
1192/// eleven rows would be handing a consumer something that disagrees
1193/// with the file and says nothing about it. This names the row, its
1194/// container id and the rational, so a caller learns exactly what the
1195/// file wrote — and repairing a container is a job for something that
1196/// rewrites containers, not for a reader that would have to invent the
1197/// number it repaired with.
1198#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1199#[error(
1200  "chapter {index} (container id {id}) declares the timebase {num}/{den}, which cannot rule its span"
1201)]
1202pub struct ChapterTimebaseInvalid {
1203  index: usize,
1204  id: i64,
1205  num: i32,
1206  den: i32,
1207}
1208
1209impl ChapterTimebaseInvalid {
1210  /// Constructs a `ChapterTimebaseInvalid` payload.
1211  #[cfg_attr(not(tarpaulin), inline(always))]
1212  pub const fn new(index: usize, id: i64, num: i32, den: i32) -> Self {
1213    Self {
1214      index,
1215      id,
1216      num,
1217      den,
1218    }
1219  }
1220  /// The chapter's position in `AVFormatContext.chapters`.
1221  #[cfg_attr(not(tarpaulin), inline(always))]
1222  pub const fn index(&self) -> usize {
1223    self.index
1224  }
1225  /// The id the container assigned that chapter.
1226  #[cfg_attr(not(tarpaulin), inline(always))]
1227  pub const fn id(&self) -> i64 {
1228    self.id
1229  }
1230  /// The numerator the container wrote.
1231  #[cfg_attr(not(tarpaulin), inline(always))]
1232  pub const fn num(&self) -> i32 {
1233    self.num
1234  }
1235  /// The denominator the container wrote.
1236  #[cfg_attr(not(tarpaulin), inline(always))]
1237  pub const fn den(&self) -> i32 {
1238    self.den
1239  }
1240}
1241
1242/// Payload for [`DemuxError::TooManyChapters`].
1243///
1244/// The container declares more chapters than
1245/// [`DemuxLimits::max_chapters`] allows. Refused at open, before the
1246/// table is reserved.
1247#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1248#[error("the container declares {declared} chapters, over the ceiling of {limit}")]
1249pub struct TooManyChapters {
1250  declared: usize,
1251  limit: u32,
1252}
1253
1254impl TooManyChapters {
1255  /// Constructs a `TooManyChapters` payload.
1256  #[cfg_attr(not(tarpaulin), inline(always))]
1257  pub const fn new(declared: usize, limit: u32) -> Self {
1258    Self { declared, limit }
1259  }
1260  /// `AVFormatContext.nb_chapters`, as the container declared it.
1261  #[cfg_attr(not(tarpaulin), inline(always))]
1262  pub const fn declared(&self) -> usize {
1263    self.declared
1264  }
1265  /// The ceiling in force.
1266  #[cfg_attr(not(tarpaulin), inline(always))]
1267  pub const fn limit(&self) -> u32 {
1268    self.limit
1269  }
1270}
1271
1272/// Payload for [`DemuxError::ChapterTitleBudgetExhausted`].
1273///
1274/// The file's chapter titles, together, are over
1275/// [`DemuxLimits::max_total_chapter_title_bytes`].
1276#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1277#[error(
1278  "the chapter titles reach {bytes} bytes at chapter {index}, over the {limit}-byte whole-file budget"
1279)]
1280pub struct ChapterTitleBudgetExhausted {
1281  index: usize,
1282  bytes: usize,
1283  limit: usize,
1284}
1285
1286impl ChapterTitleBudgetExhausted {
1287  /// Constructs a `ChapterTitleBudgetExhausted` payload.
1288  #[cfg_attr(not(tarpaulin), inline(always))]
1289  pub const fn new(index: usize, bytes: usize, limit: usize) -> Self {
1290    Self {
1291      index,
1292      bytes,
1293      limit,
1294    }
1295  }
1296  /// The chapter whose title ran the total past the line.
1297  #[cfg_attr(not(tarpaulin), inline(always))]
1298  pub const fn index(&self) -> usize {
1299    self.index
1300  }
1301  /// The running total at that chapter.
1302  #[cfg_attr(not(tarpaulin), inline(always))]
1303  pub const fn bytes(&self) -> usize {
1304    self.bytes
1305  }
1306  /// The whole-file budget in force.
1307  #[cfg_attr(not(tarpaulin), inline(always))]
1308  pub const fn limit(&self) -> usize {
1309    self.limit
1310  }
1311}
1312
1313/// Payload for [`DemuxError::ChapterTitleTooLong`].
1314///
1315/// A chapter's `title` has no terminator inside
1316/// [`METADATA_VALUE_MAX_BYTES`].
1317///
1318/// **Distinct from the budget error, and deliberately so.**
1319/// [`ChapterTitleBudgetExhausted`] means the file's titles together
1320/// exceed what the caller allowed, and a caller answers it by raising
1321/// [`DemuxLimits::max_total_chapter_title_bytes`](crate::DemuxLimits::max_total_chapter_title_bytes).
1322/// This one means a single value runs past a structural cap this crate
1323/// owns and no seat can move — folding the two together would offer a
1324/// knob that cannot fix it.
1325///
1326/// Refused rather than truncated (a truncated title is a different
1327/// title), and refused *visibly*: reporting it as an absent title, as
1328/// this road used to, made the mirrored table silently disagree with
1329/// the container and slipped the value past the budget entirely.
1330#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1331#[error("the title on chapter {index} runs past the {limit}-byte cap on one metadata value")]
1332pub struct ChapterTitleTooLong {
1333  index: usize,
1334  limit: usize,
1335}
1336
1337impl ChapterTitleTooLong {
1338  /// Constructs a `ChapterTitleTooLong` payload.
1339  #[cfg_attr(not(tarpaulin), inline(always))]
1340  pub const fn new(index: usize, limit: usize) -> Self {
1341    Self { index, limit }
1342  }
1343  /// The chapter's position in `AVFormatContext.chapters`.
1344  #[cfg_attr(not(tarpaulin), inline(always))]
1345  pub const fn index(&self) -> usize {
1346    self.index
1347  }
1348  /// The per-value cap in force.
1349  #[cfg_attr(not(tarpaulin), inline(always))]
1350  pub const fn limit(&self) -> usize {
1351    self.limit
1352  }
1353}
1354
1355/// Payload for [`DemuxError::ChapterTitleAlloc`].
1356///
1357/// A chapter title that passed the budget could not be decoded into
1358/// owned text. The size had already been charged, so this is the
1359/// allocator refusing rather than the file asking for too much.
1360#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1361#[error("out of memory decoding the {bytes}-byte title on chapter {index}")]
1362pub struct ChapterTitleAlloc {
1363  index: usize,
1364  bytes: usize,
1365}
1366
1367impl ChapterTitleAlloc {
1368  /// Constructs a `ChapterTitleAlloc` payload.
1369  #[cfg_attr(not(tarpaulin), inline(always))]
1370  pub const fn new(index: usize, bytes: usize) -> Self {
1371    Self { index, bytes }
1372  }
1373  /// The chapter's position in `AVFormatContext.chapters`.
1374  #[cfg_attr(not(tarpaulin), inline(always))]
1375  pub const fn index(&self) -> usize {
1376    self.index
1377  }
1378  /// The decoded size that could not be reserved.
1379  #[cfg_attr(not(tarpaulin), inline(always))]
1380  pub const fn bytes(&self) -> usize {
1381    self.bytes
1382  }
1383}
1384
1385/// Payload for [`DemuxError::TrackMetadataTooLong`].
1386///
1387/// One of a stream's retained metadata values — `filename`,
1388/// `mimetype` or `language` — has no terminator inside
1389/// [`METADATA_VALUE_MAX_BYTES`]. `key` says which.
1390#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1391#[error("the {key} on stream {stream_index} runs past the {limit}-byte cap on one metadata value")]
1392pub struct TrackMetadataTooLong {
1393  stream_index: usize,
1394  key: &'static str,
1395  limit: usize,
1396}
1397
1398impl TrackMetadataTooLong {
1399  /// Constructs a `TrackMetadataTooLong` payload.
1400  #[cfg_attr(not(tarpaulin), inline(always))]
1401  pub const fn new(stream_index: usize, key: &'static str, limit: usize) -> Self {
1402    Self {
1403      stream_index,
1404      key,
1405      limit,
1406    }
1407  }
1408  /// The `AVStream.index` carrying it.
1409  #[cfg_attr(not(tarpaulin), inline(always))]
1410  pub const fn stream_index(&self) -> usize {
1411    self.stream_index
1412  }
1413  /// The dictionary key that was being read.
1414  #[cfg_attr(not(tarpaulin), inline(always))]
1415  pub const fn key(&self) -> &'static str {
1416    self.key
1417  }
1418  /// The per-value cap in force.
1419  #[cfg_attr(not(tarpaulin), inline(always))]
1420  pub const fn limit(&self) -> usize {
1421    self.limit
1422  }
1423}
1424
1425/// Payload for [`DemuxError::TrackMetadataBudgetExhausted`].
1426///
1427/// The file's stream metadata, together, is over
1428/// [`DemuxLimits::max_total_stream_metadata_bytes`].
1429///
1430/// The budget is whole-file rather than per-stream because the
1431/// exposure is: `max_streams` bounds how many streams a header may
1432/// declare and says nothing about what each may carry, and every
1433/// admitted stream's three values are mirrored eagerly at open.
1434#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1435#[error(
1436  "the stream metadata reaches {bytes} bytes at the {key} on stream {stream_index}, over the {limit}-byte whole-file budget"
1437)]
1438pub struct TrackMetadataBudgetExhausted {
1439  stream_index: usize,
1440  key: &'static str,
1441  bytes: usize,
1442  limit: usize,
1443}
1444
1445impl TrackMetadataBudgetExhausted {
1446  /// Constructs a `TrackMetadataBudgetExhausted` payload.
1447  #[cfg_attr(not(tarpaulin), inline(always))]
1448  pub const fn new(stream_index: usize, key: &'static str, bytes: usize, limit: usize) -> Self {
1449    Self {
1450      stream_index,
1451      key,
1452      bytes,
1453      limit,
1454    }
1455  }
1456  /// The `AVStream.index` whose value ran the total past the line.
1457  #[cfg_attr(not(tarpaulin), inline(always))]
1458  pub const fn stream_index(&self) -> usize {
1459    self.stream_index
1460  }
1461  /// The dictionary key that was being read.
1462  #[cfg_attr(not(tarpaulin), inline(always))]
1463  pub const fn key(&self) -> &'static str {
1464    self.key
1465  }
1466  /// The running total at that value.
1467  #[cfg_attr(not(tarpaulin), inline(always))]
1468  pub const fn bytes(&self) -> usize {
1469    self.bytes
1470  }
1471  /// The whole-file budget in force.
1472  #[cfg_attr(not(tarpaulin), inline(always))]
1473  pub const fn limit(&self) -> usize {
1474    self.limit
1475  }
1476}
1477
1478/// Payload for [`DemuxError::TrackMetadataAlloc`].
1479///
1480/// A stream metadata value that passed the budget could not be decoded
1481/// into owned text.
1482#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1483#[error("out of memory decoding the {bytes}-byte {key} on stream {stream_index}")]
1484pub struct TrackMetadataAlloc {
1485  stream_index: usize,
1486  key: &'static str,
1487  bytes: usize,
1488}
1489
1490impl TrackMetadataAlloc {
1491  /// Constructs a `TrackMetadataAlloc` payload.
1492  #[cfg_attr(not(tarpaulin), inline(always))]
1493  pub const fn new(stream_index: usize, key: &'static str, bytes: usize) -> Self {
1494    Self {
1495      stream_index,
1496      key,
1497      bytes,
1498    }
1499  }
1500  /// The `AVStream.index` carrying it.
1501  #[cfg_attr(not(tarpaulin), inline(always))]
1502  pub const fn stream_index(&self) -> usize {
1503    self.stream_index
1504  }
1505  /// The dictionary key that was being read.
1506  #[cfg_attr(not(tarpaulin), inline(always))]
1507  pub const fn key(&self) -> &'static str {
1508    self.key
1509  }
1510  /// The decoded size that could not be reserved.
1511  #[cfg_attr(not(tarpaulin), inline(always))]
1512  pub const fn bytes(&self) -> usize {
1513    self.bytes
1514  }
1515}
1516
1517/// Payload for [`DemuxError::TrackTableAlloc`].
1518///
1519/// The track table, or the attachment queue built beside it, could not
1520/// be reserved. The stream count had already passed
1521/// [`DemuxLimits::max_streams`](crate::DemuxLimits::max_streams), so
1522/// this is the allocator declining rather than the file asking for too
1523/// much — reported instead of aborting, which is what an infallible
1524/// reservation would have done.
1525#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1526#[error("out of memory reserving the table of {streams} streams")]
1527pub struct TrackTableAlloc {
1528  streams: usize,
1529}
1530
1531impl TrackTableAlloc {
1532  /// Constructs a `TrackTableAlloc` payload.
1533  #[cfg_attr(not(tarpaulin), inline(always))]
1534  pub const fn new(streams: usize) -> Self {
1535    Self { streams }
1536  }
1537  /// The stream count the reservation was for.
1538  #[cfg_attr(not(tarpaulin), inline(always))]
1539  pub const fn streams(&self) -> usize {
1540    self.streams
1541  }
1542}
1543
1544/// Payload for [`DemuxError::ChapterAlloc`].
1545///
1546/// The chapter table could not be reserved. The count had already
1547/// passed [`DemuxLimits::max_chapters`], so this is the allocator
1548/// refusing rather than the file asking for too much — reported
1549/// instead of aborting, which is what an infallible reservation would
1550/// have done.
1551#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1552#[error("out of memory reserving the table of {declared} chapters")]
1553pub struct ChapterAlloc {
1554  declared: usize,
1555}
1556
1557impl ChapterAlloc {
1558  /// Constructs a `ChapterAlloc` payload.
1559  #[cfg_attr(not(tarpaulin), inline(always))]
1560  pub const fn new(declared: usize) -> Self {
1561    Self { declared }
1562  }
1563  /// The chapter count the reservation was for.
1564  #[cfg_attr(not(tarpaulin), inline(always))]
1565  pub const fn declared(&self) -> usize {
1566    self.declared
1567  }
1568}
1569
1570/// Payload for [`DemuxError::ParametersCopy`].
1571///
1572/// Copying a track's codec parameters failed part way.
1573#[derive(thiserror::Error, Debug, Clone)]
1574#[error("the codec parameters for stream {stream_index} could not be copied: {source}")]
1575pub struct ParametersCopy {
1576  stream_index: usize,
1577  #[source]
1578  source: ffmpeg_next::Error,
1579}
1580
1581impl ParametersCopy {
1582  /// Constructs a `ParametersCopy` payload.
1583  #[cfg_attr(not(tarpaulin), inline(always))]
1584  pub const fn new(stream_index: usize, source: ffmpeg_next::Error) -> Self {
1585    Self {
1586      stream_index,
1587      source,
1588    }
1589  }
1590  /// The `AVStream.index` whose parameters could not be copied.
1591  #[cfg_attr(not(tarpaulin), inline(always))]
1592  pub const fn stream_index(&self) -> usize {
1593    self.stream_index
1594  }
1595  /// What FFmpeg said.
1596  #[cfg_attr(not(tarpaulin), inline(always))]
1597  pub const fn source(&self) -> &ffmpeg_next::Error {
1598    &self.source
1599  }
1600}
1601
1602/// Payload for [`DemuxError::ParametersOpaque`].
1603///
1604/// A channel layout arrived carrying `opaque` — a raw pointer FFmpeg
1605/// documents as "private data of the user".
1606///
1607/// [`CodecTicket`](crate::ticket::CodecTicket) is an **owned** mirror:
1608/// it outlives the `AVCodecParameters` it was read from, and it may
1609/// cross threads, so a pointer into somebody else's data is exactly
1610/// what it cannot carry. libavformat sets neither
1611/// `AVChannelLayout::opaque` nor `AVChannelCustom::opaque`, so no
1612/// demuxed stream reaches the mirror with one; if one ever does, the
1613/// mirror refuses rather than dropping the pointer in silence. That is
1614/// the same fail-closed answer `extras::measure_parameters` gives a
1615/// channel order it has never heard of, and for the same reason:
1616/// carrying on would be a guess about memory nobody here owns.
1617#[derive(thiserror::Error, Debug, Clone)]
1618#[error(
1619  "the channel layout for stream {stream_index} carries user-private data \
1620   ({}) that an owned codec ticket cannot mirror",
1621  match channel { Some(i) => format!("custom channel {i}"), None => "the layout".to_owned() },
1622)]
1623pub struct ParametersOpaque {
1624  stream_index: usize,
1625  channel: Option<usize>,
1626}
1627
1628impl ParametersOpaque {
1629  /// Constructs a `ParametersOpaque` payload. `channel` names the
1630  /// custom-map entry when the pointer was on one, and is `None` when
1631  /// it was on the layout itself.
1632  #[cfg_attr(not(tarpaulin), inline(always))]
1633  pub const fn new(stream_index: usize, channel: Option<usize>) -> Self {
1634    Self {
1635      stream_index,
1636      channel,
1637    }
1638  }
1639  /// The `AVStream.index` whose layout carried the pointer.
1640  #[cfg_attr(not(tarpaulin), inline(always))]
1641  pub const fn stream_index(&self) -> usize {
1642    self.stream_index
1643  }
1644  /// The custom-map entry the pointer was on, or `None` when it was on
1645  /// the layout itself.
1646  #[cfg_attr(not(tarpaulin), inline(always))]
1647  pub const fn channel(&self) -> Option<usize> {
1648    self.channel
1649  }
1650}
1651
1652/// Payload for [`DemuxError::ParametersChannelMap`].
1653///
1654/// A channel layout declared `AV_CHANNEL_ORDER_CUSTOM` without the map
1655/// that order requires.
1656///
1657/// **This one is a crash, not a curiosity.** For a custom order,
1658/// `av_channel_layout_copy` — which is how
1659/// `avcodec_parameters_to_context` moves a layout into a decoder's
1660/// context — does
1661///
1662/// ```c
1663/// dst->u.map = av_malloc_array(src->nb_channels, sizeof(*dst->u.map));
1664/// if (!dst->u.map)
1665///     return AVERROR(ENOMEM);
1666/// memcpy(dst->u.map, src->u.map, src->nb_channels * sizeof(*src->u.map));
1667/// ```
1668///
1669/// with **no null check on `src->u.map`** — verified against FFmpeg
1670/// n9.0. A layout that names channels it has no map for therefore makes
1671/// libavcodec `memcpy` from a null pointer the moment a decoder opens
1672/// from it.
1673///
1674/// So the mirror refuses such a layout at the door rather than
1675/// reproducing it. An earlier draft carried it through, on the argument
1676/// that a malformed layout in should be a malformed layout out — the
1677/// round trip is faithful either way, and the parity comparator agreed.
1678/// That symmetry was the wrong test: faithfully reproducing a shape
1679/// whose only consumer dereferences null is not fidelity, it is
1680/// forwarding a crash. Refusing is the same fail-closed answer
1681/// `extras::measure_parameters` gives a channel order it has never
1682/// heard of.
1683#[derive(thiserror::Error, Debug, Clone)]
1684#[error(
1685  "the custom channel layout for stream {stream_index} declares {channels} channels \
1686   and carries no usable map for them"
1687)]
1688pub struct ParametersChannelMap {
1689  stream_index: usize,
1690  channels: i32,
1691}
1692
1693impl ParametersChannelMap {
1694  /// Constructs a `ParametersChannelMap` payload.
1695  #[cfg_attr(not(tarpaulin), inline(always))]
1696  pub const fn new(stream_index: usize, channels: i32) -> Self {
1697    Self {
1698      stream_index,
1699      channels,
1700    }
1701  }
1702  /// The `AVStream.index` whose layout was malformed.
1703  #[cfg_attr(not(tarpaulin), inline(always))]
1704  pub const fn stream_index(&self) -> usize {
1705    self.stream_index
1706  }
1707  /// The `nb_channels` the layout declared with no map to describe them.
1708  #[cfg_attr(not(tarpaulin), inline(always))]
1709  pub const fn channels(&self) -> i32 {
1710    self.channels
1711  }
1712}
1713
1714/// Payload for [`DemuxError::ParametersLayoutShape`].
1715///
1716/// The non-custom half of what a channel layout can be wrong about, and
1717/// the half that went unchecked for eleven rounds of review on the
1718/// argument that an order describing its channels through a `uint64_t`
1719/// mask cannot be malformed. The mask is not the only field:
1720/// `nb_channels` is an `int` a caller writes, and FFmpeg's helpers
1721/// compute `nb_channels - popcount(mask)` and take an integer square
1722/// root of it without checking either.
1723///
1724/// See
1725/// [`layout_preflight`](crate::channel_layout::layout_preflight) for
1726/// the complete rule and why it is one function.
1727#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
1728#[error(
1729  "the channel layout for stream {stream_index} declares order {order} with {channels} \
1730   channels, which is not a shape FFmpeg's own helpers can be given"
1731)]
1732pub struct ParametersLayoutShape {
1733  stream_index: usize,
1734  order: i32,
1735  channels: i32,
1736}
1737
1738impl ParametersLayoutShape {
1739  /// Constructs a `ParametersLayoutShape` payload.
1740  #[cfg_attr(not(tarpaulin), inline(always))]
1741  pub const fn new(stream_index: usize, order: i32, channels: i32) -> Self {
1742    Self {
1743      stream_index,
1744      order,
1745      channels,
1746    }
1747  }
1748  /// The `AVStream.index` whose layout declared it.
1749  #[cfg_attr(not(tarpaulin), inline(always))]
1750  pub const fn stream_index(&self) -> usize {
1751    self.stream_index
1752  }
1753  /// `AVChannelLayout.order`, as the raw `c_int` it is on the wire.
1754  #[cfg_attr(not(tarpaulin), inline(always))]
1755  pub const fn order(&self) -> i32 {
1756    self.order
1757  }
1758  /// `nb_channels`, as the layout declared it.
1759  #[cfg_attr(not(tarpaulin), inline(always))]
1760  pub const fn channels(&self) -> i32 {
1761    self.channels
1762  }
1763}
1764
1765/// Names a channel-layout fault for a demux caller — **one mapper, so
1766/// the roads that share the preflight also share its report**.
1767///
1768/// `Alloc` is the only arm that is not a statement about the container:
1769/// it is the allocator declining a rendering buffer, and it keeps the
1770/// name every other allocator refusal on this road has.
1771pub(crate) fn layout_fault_to_demux(
1772  stream_index: usize,
1773  fault: crate::channel_layout::ChannelLayoutFault,
1774) -> DemuxError {
1775  use crate::channel_layout::ChannelLayoutFault as Fault;
1776  match fault {
1777    // The second cannot arrive from a pointer road — it is how the
1778    // *safe* conversion refuses a custom layout whose extent it cannot
1779    // establish — but both say the same thing about this stream.
1780    Fault::MalformedCustomMap { channels } | Fault::UnverifiableCustomMap { channels } => {
1781      DemuxError::ParametersChannelMap(ParametersChannelMap::new(stream_index, channels))
1782    }
1783    Fault::MalformedLayout { order, channels } => {
1784      DemuxError::ParametersLayoutShape(ParametersLayoutShape::new(stream_index, order, channels))
1785    }
1786    Fault::Alloc => DemuxError::ParametersAlloc(ParametersAlloc::new(stream_index)),
1787  }
1788}
1789
1790/// Payload for [`DemuxError::PacketBuffer`].
1791///
1792/// A packet's payload could not be referenced — the bytes are there
1793/// and this layer could not carry them.
1794///
1795/// Never raised for a packet that simply has no payload: an empty
1796/// packet is a marker some demuxers emit, and it is skipped in
1797/// silence. Distinguishing the two is what keeps a refcount failure
1798/// under memory pressure from looking like the file's own word and
1799/// dropping real compressed bytes.
1800#[derive(thiserror::Error, Debug, Clone)]
1801#[error("stream {stream_index}: {source}")]
1802pub struct PacketBuffer {
1803  stream_index: usize,
1804  #[source]
1805  source: PacketBufferError,
1806}
1807
1808impl PacketBuffer {
1809  /// Constructs a `PacketBuffer` payload.
1810  #[cfg_attr(not(tarpaulin), inline(always))]
1811  pub const fn new(stream_index: usize, source: PacketBufferError) -> Self {
1812    Self {
1813      stream_index,
1814      source,
1815    }
1816  }
1817  /// The `AVStream.index` the packet belongs to.
1818  #[cfg_attr(not(tarpaulin), inline(always))]
1819  pub const fn stream_index(&self) -> usize {
1820    self.stream_index
1821  }
1822  /// What went wrong.
1823  #[cfg_attr(not(tarpaulin), inline(always))]
1824  pub const fn source(&self) -> &PacketBufferError {
1825    &self.source
1826  }
1827}
1828
1829/// Payload for [`DemuxError::ReaderPanic`].
1830///
1831/// The `Read + Seek` source given to [`FfmpegDemuxer::open_reader`]
1832/// panicked inside a libavformat callback.
1833///
1834/// The panic was caught before it could cross the `extern "C"`
1835/// boundary and abort the process; this is what it said. The session
1836/// is terminal — every later call reports the same panic.
1837#[derive(thiserror::Error, Debug, Clone)]
1838#[error("the reader panicked: {message}")]
1839pub struct ReaderPanic {
1840  message: Utf8Bytes,
1841}
1842
1843impl ReaderPanic {
1844  /// Constructs a `ReaderPanic` payload.
1845  #[cfg_attr(not(tarpaulin), inline(always))]
1846  pub const fn new(message: Utf8Bytes) -> Self {
1847    Self { message }
1848  }
1849  /// What the panic payload said.
1850  #[cfg_attr(not(tarpaulin), inline(always))]
1851  pub fn message(&self) -> &str {
1852    self.message.as_str()
1853  }
1854}
1855
1856/// Errors from [`FfmpegDemuxer`].
1857///
1858/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
1859/// fail are discovered — a backend, a ceiling, a corruption a codec
1860/// learns to report — and a consumer that meets one it has never heard
1861/// of should take its generic-fault path. That is exactly what the
1862/// wildcard arm this attribute forces is for. The two status
1863/// vocabularies opposite it,
1864/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
1865/// are exhaustive for the mirror-image reason: their arms are the
1866/// substrate's fixed state set, and there the wildcard would be dead
1867/// weight hiding a state a consumer forgot.
1868#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
1869#[unwrap(ref, ref_mut)]
1870#[try_unwrap(ref, ref_mut)]
1871#[non_exhaustive]
1872pub enum DemuxError {
1873  /// The wrapped libavformat call reported an error — open, read or
1874  /// seek.
1875  #[error(transparent)]
1876  Ffmpeg(#[from] ffmpeg_next::Error),
1877
1878  /// libavformat asked for more bytes than the probe budget allows
1879  /// while opening and analysing the container. See
1880  /// [`ProbeBudgetExhausted`].
1881  #[error(transparent)]
1882  ProbeBudgetExhausted(#[from] ProbeBudgetExhausted),
1883
1884  /// One attachment's payload is over the per-attachment budget.
1885  /// Refused at open, before the copy.
1886  #[error(transparent)]
1887  AttachmentTooLarge(#[from] AttachmentTooLarge),
1888
1889  /// The file's attachments, together, are over the whole-file budget.
1890  /// Refused at open, before the copy that would have crossed it.
1891  #[error(transparent)]
1892  AttachmentBudgetExhausted(#[from] AttachmentBudgetExhausted),
1893
1894  /// One stream's codec parameters hold more heap bytes than the
1895  /// budget allows. Refused at open, before the clone.
1896  #[error(transparent)]
1897  ParametersTooLarge(#[from] ParametersTooLarge),
1898
1899  /// Every stream's codec parameters together are over the whole-file
1900  /// budget. Refused at open, before the clone that would have crossed
1901  /// it.
1902  #[error(transparent)]
1903  ParametersBudgetExhausted(#[from] ParametersBudgetExhausted),
1904
1905  /// Codec parameters arrived that were never allocated.
1906  #[error(transparent)]
1907  ParametersMissing(#[from] ParametersMissing),
1908
1909  /// Codec parameters for a track could not be allocated.
1910  #[error(transparent)]
1911  ParametersAlloc(#[from] ParametersAlloc),
1912
1913  /// Copying a track's codec parameters failed part way.
1914  #[error(transparent)]
1915  ParametersCopy(#[from] ParametersCopy),
1916
1917  /// A channel layout arrived carrying user-private data an owned
1918  /// codec ticket cannot mirror.
1919  #[error(transparent)]
1920  ParametersOpaque(#[from] ParametersOpaque),
1921
1922  /// A channel layout declared a custom order without the map that
1923  /// order requires — a shape `av_channel_layout_copy` would `memcpy`
1924  /// from null.
1925  #[error(transparent)]
1926  ParametersChannelMap(#[from] ParametersChannelMap),
1927
1928  /// A stream's channel layout declares a shape FFmpeg's own helpers
1929  /// cannot be given — see [`ParametersLayoutShape`].
1930  #[error(transparent)]
1931  ParametersLayoutShape(#[from] ParametersLayoutShape),
1932
1933  /// A stream declares a timebase that is not one. Refused at open,
1934  /// rather than repaired with a number this crate would have had to
1935  /// invent.
1936  #[error(transparent)]
1937  TrackTimebaseInvalid(#[from] TrackTimebaseInvalid),
1938
1939  /// A chapter declares a timebase that cannot rule its span. Refused
1940  /// at open, for the reason [`ChapterTimebaseInvalid`] gives.
1941  #[error(transparent)]
1942  ChapterTimebaseInvalid(#[from] ChapterTimebaseInvalid),
1943
1944  /// The container declares more chapters than the ceiling allows.
1945  /// Refused at open, before the table is reserved.
1946  #[error(transparent)]
1947  TooManyChapters(#[from] TooManyChapters),
1948
1949  /// The file's chapter titles, together, are over the whole-file
1950  /// budget. Refused at the title that crossed it, before it was
1951  /// copied.
1952  #[error(transparent)]
1953  ChapterTitleBudgetExhausted(#[from] ChapterTitleBudgetExhausted),
1954
1955  /// One chapter title runs past the cap on a single metadata value —
1956  /// refused, rather than reported as an absent title.
1957  #[error(transparent)]
1958  ChapterTitleTooLong(#[from] ChapterTitleTooLong),
1959
1960  /// A chapter title the budget admitted could not be decoded.
1961  #[error(transparent)]
1962  ChapterTitleAlloc(#[from] ChapterTitleAlloc),
1963
1964  /// One of a stream's retained metadata values runs past the cap on a
1965  /// single metadata value.
1966  #[error(transparent)]
1967  TrackMetadataTooLong(#[from] TrackMetadataTooLong),
1968
1969  /// The file's stream metadata, together, is over the whole-file
1970  /// budget. Refused at the value that crossed it, before it was
1971  /// copied.
1972  #[error(transparent)]
1973  TrackMetadataBudgetExhausted(#[from] TrackMetadataBudgetExhausted),
1974
1975  /// A stream metadata value the budget admitted could not be decoded.
1976  #[error(transparent)]
1977  TrackMetadataAlloc(#[from] TrackMetadataAlloc),
1978
1979  /// The chapter table could not be reserved.
1980  #[error(transparent)]
1981  ChapterAlloc(#[from] ChapterAlloc),
1982
1983  /// The track table, or the attachment queue beside it, could not be
1984  /// reserved.
1985  #[error(transparent)]
1986  TrackTableAlloc(#[from] TrackTableAlloc),
1987
1988  /// A packet's payload could not be referenced — the bytes are there
1989  /// and this layer could not carry them.
1990  #[error(transparent)]
1991  PacketBuffer(#[from] PacketBuffer),
1992
1993  /// The `Read + Seek` source given to
1994  /// [`FfmpegDemuxer::open_reader`] panicked inside a libavformat
1995  /// callback.
1996  #[error(transparent)]
1997  ReaderPanic(#[from] ReaderPanic),
1998}
1999
2000// ---------------------------------------------------------------------------
2001//  Track-table construction.
2002// ---------------------------------------------------------------------------
2003
2004type BuiltTracks<C> = (
2005  Vec<TrackInfo<Ffmpeg>>,
2006  VecDeque<(
2007    TrackIndex,
2008    AttachmentPacket<AttachmentPacketExtra, <C as crate::FfmpegCarrier>::Buffer>,
2009  )>,
2010);
2011
2012fn build_tracks<C: crate::FfmpegCarrier + crate::CarrierOps>(
2013  input: &Input,
2014  limits: DemuxLimits,
2015) -> Result<BuiltTracks<C>, DemuxError> {
2016  // **Admission before allocation.** Every attachment in the file is
2017  // judged here, in full, before the loop below allocates anything at
2018  // all — see [`admit_streams`] for why the charge cannot live
2019  // inside the capture.
2020  admit_streams(input, limits)?;
2021
2022  let count = input.streams().len();
2023  // **Reserved fallibly, both of them.** The stream count is the
2024  // container's; `max_streams` bounds it, and a bound the caller chose
2025  // is exactly the number that must come back as an error rather than
2026  // an abort when the allocator declines it.
2027  let mut tracks = Vec::new();
2028  tracks
2029    .try_reserve_exact(count)
2030    .map_err(|_| DemuxError::TrackTableAlloc(TrackTableAlloc::new(count)))?;
2031  let mut pending = VecDeque::new();
2032  pending
2033    .try_reserve(count)
2034    .map_err(|_| DemuxError::TrackTableAlloc(TrackTableAlloc::new(count)))?;
2035  // The whole file's stream-metadata budget, spent across every
2036  // admitted stream rather than per stream: `max_streams` bounds how
2037  // many streams a header may declare, and nothing bounded what each
2038  // could carry.
2039  let mut metadata_spent: usize = 0;
2040
2041  for stream in input.streams() {
2042    let index = stream.index();
2043    // `AVStream.index` is the stream's position in `ic->streams[]` and
2044    // libavformat keeps the two identical. The demux tier makes
2045    // `TrackIndex` mean "position in `tracks()`", so the two agree by
2046    // construction — but only if they really are dense and in order,
2047    // which is cheap to insist on rather than assume.
2048    debug_assert_eq!(
2049      index,
2050      tracks.len(),
2051      "AVStream indices are dense and ordered"
2052    );
2053
2054    let parameters = stream.parameters();
2055    let par = unsafe { parameters.as_ptr() };
2056    // Never read `AVCodecParameters.codec_type` / `.codec_id` as their
2057    // bindgen enums: a value outside this build's discriminant set is
2058    // UB the moment it exists. Both are read as the raw integers they
2059    // are on the wire — the medium through [`boundary::media_kind_of`],
2060    // which folds anything unnamed into `Unknown`.
2061    //
2062    // The medium used to go through `Parameters::medium()` on the
2063    // argument that `AVMediaType`'s set is tiny and stable. It is; that
2064    // made the read unlikely to bite, not sound. The exception is gone
2065    // rather than defended, so no attacker-reachable path in this crate
2066    // forms a bindgen enum out of FFmpeg memory.
2067    let medium = boundary::media_kind_of(&parameters);
2068    let codec =
2069      CodecId::from_raw(unsafe { read_unaligned(addr_of!((*par).codec_id).cast::<i32>()) });
2070
2071    let disposition = unsafe { (*stream.as_ptr()).disposition };
2072    let attached_pic = is_attachment_disposition(disposition);
2073
2074    // Already judged by [`admit_streams`], which refuses a malformed
2075    // ruler before this loop allocates anything; the same function is
2076    // called here because this is where the value is actually needed,
2077    // and one function is how the two passes are kept from becoming
2078    // two rules.
2079    let time_base = stream_timebase(index, stream.time_base())?;
2080    let raw_duration = stream.duration();
2081    let duration = (raw_duration != AV_NOPTS_VALUE && raw_duration > 0)
2082      .then(|| Timestamp::new(raw_duration, time_base));
2083    let raw_start = stream.start_time();
2084    let frames = stream.frames();
2085
2086    let params = if attached_pic {
2087      // Cover art. A still image in a video-shaped slot is an
2088      // attachment by every property that matters, and the `Video` arm
2089      // is reserved for motion video.
2090      TrackParams::Attachment(AttachmentTrackParams::new(codec))
2091    } else {
2092      match medium {
2093        boundary::MediaKind::Video => TrackParams::Video(VideoTrackParams::new(
2094          codec,
2095          unsafe { (*par).width }.max(0) as u32,
2096          unsafe { (*par).height }.max(0) as u32,
2097          boundary::from_av_pixel_format(unsafe { (*par).format }),
2098          rate_to_timebase(stream.avg_frame_rate()),
2099        )),
2100        boundary::MediaKind::Audio => {
2101          let ch_layout = unsafe { std::ptr::addr_of!((*par).ch_layout) };
2102          // **This is the trusted road, and here is why it is trusted.**
2103          //
2104          // The `unsafe` form is the only one that reads a custom
2105          // channel map, because its contract asks the caller for the
2106          // map's extent — the one thing a pointer cannot be asked.
2107          // This call site can supply it: `par` is the
2108          // `AVCodecParameters` libavformat itself built for this
2109          // stream, and libavformat fills `ch_layout` through
2110          // `av_channel_layout_copy`, which allocates the map and sizes
2111          // it to `nb_channels` in the same operation. The layout is
2112          // never handed in by a caller of this crate, so there is no
2113          // road by which `nb_channels` and the map can disagree.
2114          //
2115          // SAFETY: (1) `par` is a live `*const AVCodecParameters` for
2116          // the life of `parameters`, so `ch_layout` is a live, aligned
2117          // `*const AVChannelLayout`. (2) For a `CUSTOM` order, `u.map`
2118          // is FFmpeg's own allocation of exactly `nb_channels`
2119          // `AVChannelCustom` entries, per the paragraph above. The
2120          // helper validates `order` as an `i32` before constructing any
2121          // `AVChannelOrder`, and refuses a null map or an
2122          // unterminated name before FFmpeg is allowed to render the
2123          // layout — a precondition, not a courtesy.
2124          let channel_layout =
2125            unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout) }
2126              .map_err(|fault| layout_fault_to_demux(index, fault))?;
2127          TrackParams::Audio(AudioTrackParams::new(
2128            codec,
2129            unsafe { (*par).sample_rate }.max(0) as u32,
2130            channel_layout.channels().min(255) as u8,
2131            SampleFormat::from_raw(unsafe { (*par).format }),
2132            channel_layout,
2133          ))
2134        }
2135        boundary::MediaKind::Subtitle => TrackParams::Subtitle(SubtitleTrackParams::new(codec)),
2136        boundary::MediaKind::Data => TrackParams::Data(DataTrackParams::new(codec)),
2137        boundary::MediaKind::Attachment => {
2138          TrackParams::Attachment(AttachmentTrackParams::new(codec))
2139        }
2140        boundary::MediaKind::Unknown => TrackParams::Unknown(UnknownTrackParams::new(codec)),
2141      }
2142    };
2143
2144    // The parameter mirror. For an `AVMEDIA_TYPE_ATTACHMENT` stream its
2145    // `extradata` **is** the attachment's payload — the same bytes the
2146    // carrier below already holds — so it is left behind rather than
2147    // copied. Censused before it was: nothing can use it. libavcodec
2148    // has no decoder for a font (`avcodec_find_decoder` answers null
2149    // for `AV_CODEC_ID_TTF` and its siblings), so no road in this crate
2150    // or downstream of it opens a codec context from these parameters;
2151    // the payload reaches a consumer as the attachment packet, which is
2152    // the delivery the demux tier promises.
2153    //
2154    // **Omitted, not stripped.** An earlier shape copied the extradata
2155    // and freed it immediately afterwards, which allocated the payload
2156    // for no reason and — worse — charged it against the *parameter*
2157    // ceiling on the way past. A font between the two ceilings passed
2158    // the admission pass and then failed inside the clone. See
2159    // [`ExtradataPolicy`](crate::extras::ExtradataPolicy).
2160    //
2161    // Cover art keeps its extradata: there the payload is the parked
2162    // `AVPacket`, extradata is *not* a copy of it, and a still codec
2163    // can legitimately need it (MJPEG with an external Huffman table).
2164    // Measured on this build: a cover-art stream carries none anyway.
2165    let extradata_policy = if medium.is_attachment() {
2166      crate::extras::ExtradataPolicy::Omit
2167    } else {
2168      crate::extras::ExtradataPolicy::Copy
2169    };
2170    // Straight from the stream's own parameters into the owned ticket.
2171    // The row used to reach here through an intermediate
2172    // `avcodec_parameters_copy` — one ffmpeg-native deep copy per
2173    // track, whose only purpose was to sever the tie to the format
2174    // context. The mirror severs it by being owned Rust, so that copy
2175    // is gone rather than moved.
2176    let ticket = crate::ticket::CodecTicket::mirror_with(
2177      &parameters,
2178      index,
2179      limits.max_codec_parameter_bytes(),
2180      extradata_policy,
2181    )?;
2182    let extra = TrackExtra::new(index as i32, ticket)
2183      .with_disposition(disposition)
2184      .with_start_time((raw_start != AV_NOPTS_VALUE).then_some(raw_start))
2185      .with_frame_count((frames > 0).then_some(frames));
2186
2187    // SAFETY: `stream` keeps the `AVStream` — and so its metadata
2188    // dictionary — live across every read below. The dictionary is
2189    // read through `av_dict_get` rather than through
2190    // `DictionaryRef::get`: see [`metadata_value`].
2191    let metadata = unsafe { (*stream.as_ptr()).metadata };
2192    // **Every retained value is measured, charged and only then
2193    // built**, against one whole-file budget. Three values a stream
2194    // apiece, mirrored eagerly for every admitted stream, is an
2195    // open-time allocation a container controls the size of — and
2196    // `max_streams` bounds the count of streams, not the bytes each
2197    // one can carry.
2198    let mut text = |key: &'static CStr| -> Result<Option<Utf8Bytes>, DemuxError> {
2199      // SAFETY: as above — `stream` keeps the dictionary live, and the
2200      // borrow does not outlive this call.
2201      let raw = unsafe { metadata_value(metadata, key) };
2202      retain_metadata(
2203        raw,
2204        &mut metadata_spent,
2205        limits.max_total_stream_metadata_bytes(),
2206      )
2207      .map_err(|fault| stream_metadata_error(index, key, fault, limits))
2208    };
2209    let info = TrackInfo::new(time_base, params, extra)
2210      .with_duration(duration)
2211      // The same three keys `RETAINED_STREAM_METADATA` names, in the
2212      // same order — `admit_streams` has already charged every one of
2213      // them across the whole file, so nothing here can refuse a value
2214      // the pre-pass admitted. The charge stays as defence in depth:
2215      // it is what makes `metadata_spent` real rather than notional,
2216      // and it is the arm that reports an allocator refusal, which no
2217      // measurement can foresee.
2218      .with_filename(text(c"filename")?)
2219      .with_mime_type(text(c"mimetype")?)
2220      // **`language` is where every container's tag lands.** libavformat
2221      // normalises the *key*, not the value: Matroska's `Language`
2222      // element, MP4's `mdhd` language code and an `elng`/ISO 639-2
2223      // atom, Matroska's BCP 47 `LanguageBCP47`, an ASF descriptor and
2224      // an ID3 `TLAN` frame all arrive on this one entry. What each
2225      // wrote is what is read — see
2226      // [`TrackInfo::language`](mediadecode::demuxer::TrackInfo::language)
2227      // for why nothing folds it here.
2228      .with_language(text(c"language")?);
2229
2230    // Capture the attachment payload now, so the queue is complete
2231    // before a single timed packet has been read. Every attachment
2232    // track leaves this loop with exactly one packet queued, or the
2233    // open fails: that is what makes "exactly one packet, before any
2234    // timed packet" a property of the construction rather than a
2235    // promise the pull loop has to keep.
2236    if info.kind() == TrackKind::Attachment {
2237      let packet = if attached_pic {
2238        // SAFETY: `stream` keeps the format context (and so the
2239        // `AVStream`) live; `attached_pic` is an `AVPacket` embedded by
2240        // value, and `addr_of!` reaches it without forming a reference
2241        // to the stream.
2242        let pkt = unsafe { std::ptr::addr_of!((*stream.as_ptr()).attached_pic) };
2243        unsafe { attached_pic_payload::<C>(pkt, index, limits) }?
2244      } else {
2245        extradata_payload::<C>(&stream, limits)?
2246      };
2247      pending.push_back((TrackIndex::new(index), packet));
2248    }
2249
2250    tracks.push(info);
2251  }
2252
2253  Ok((tracks, pending))
2254}
2255
2256// ---------------------------------------------------------------------------
2257//  Chapter-table construction.
2258// ---------------------------------------------------------------------------
2259
2260/// Mirrors `AVFormatContext.chapters` into owned rows.
2261///
2262/// libavformat fills the array while it parses the header — the MOV
2263/// `chpl`/chapter track, Matroska's `Chapters` element, an Ogg
2264/// `CHAPTER` comment — and `avformat_find_stream_info` has returned by
2265/// the time this runs, so the answer is final. That is what lets the
2266/// session hold the mirror beside the track table and answer
2267/// [`Demuxer::chapters`] at any point without touching the file again.
2268///
2269/// # Read raw, for the reasons the rest of this file is
2270///
2271/// The fields come off the `AVChapter` directly rather than through
2272/// `ffmpeg_next`'s own chapter wrapper. Two reasons, both already
2273/// standing here: that wrapper reads the metadata dictionary through
2274/// `DictionaryRef`, whose `&str` is built with `from_utf8_unchecked`
2275/// over bytes no container validates — see [`metadata_value`], the
2276/// measured road every metadata value in this file takes — and its
2277/// `as_ptr` dereferences the array entry without checking it, where
2278/// this walk answers a null entry by skipping it.
2279///
2280/// Nothing read here is a bindgen enum: an id, an `AVRational`, two
2281/// tick counts and a dictionary pointer.
2282///
2283/// # Nothing is repaired
2284///
2285/// A chapter whose `end` precedes its `start`, and one whose end
2286/// libavformat left at `AV_NOPTS_VALUE` because the file declared
2287/// none, are both mirrored exactly as written. This layer reports what
2288/// the container says; see [`Chapter`]'s own doc for why a clamp here
2289/// would be worse than the inverted row it replaced.
2290///
2291/// # Admission before allocation
2292///
2293/// The judging happens twice, and the first time is
2294/// [`admit_chapters`] — which runs before the *track* table is built,
2295/// because that table is where an open spends its memory and a file
2296/// certain to be refused should not pay for it. What follows here is
2297/// the same judgement repeated on the way to a row it has to build
2298/// anyway; see that function for why repeating it is how the two
2299/// passes stay one rule.
2300///
2301/// `nb_chapters` is file-controlled and libavformat has no
2302/// `max_chapters` knob to bound it with, so this crate judges the count
2303/// itself before reserving anything —
2304/// [`DemuxLimits::max_chapters`](crate::DemuxLimits::max_chapters), the
2305/// same posture [`admit_streams`] takes for the track table. Past the
2306/// ceiling the open fails with [`TooManyChapters`]; the reservation
2307/// that follows is **fallible** (`try_reserve_exact`), so an allocator
2308/// that refuses a count inside the ceiling is a named error rather than
2309/// an abort.
2310///
2311/// Titles are charged as they are read, against
2312/// [`DemuxLimits::max_total_chapter_title_bytes`](crate::DemuxLimits::max_total_chapter_title_bytes),
2313/// and the charge is made after each single title rather than before —
2314/// exactly as [`charge_attachment`] does. What bounds the overshoot is
2315/// [`METADATA_VALUE_MAX_BYTES`], which [`metadata_value`] refuses any
2316/// one dictionary value past: the worst this can hold before refusing
2317/// is the budget plus one 64 KiB title.
2318///
2319/// Note that libavformat having already built its own chapter array
2320/// does **not** bound this one. That array is four scalars and a
2321/// pointer per entry; a row here owns a title as well, so the mirror is
2322/// the larger of the two and a table that parsed successfully can still
2323/// be one this process should not pay for.
2324/// Reads the chapter array's shape and **allocates nothing**.
2325///
2326/// Shared by [`admit_chapters`] and [`build_chapters`] so the two
2327/// passes cannot come to disagree about what the container declared.
2328/// Returns `None` when there is no table to walk at all.
2329///
2330/// Safe because the borrow is the whole precondition: `input` owns the
2331/// `AVFormatContext` these two fields belong to, and it is live for as
2332/// long as the reference is.
2333fn chapter_array(input: &Input) -> Option<(usize, *mut *mut ffmpeg_next::ffi::AVChapter)> {
2334  // SAFETY: `input` owns a live `AVFormatContext` for the whole of
2335  // this call; `nb_chapters` and `chapters` are public fields of it.
2336  let (count, array) = unsafe {
2337    let context = input.as_ptr();
2338    ((*context).nb_chapters as usize, (*context).chapters)
2339  };
2340  if array.is_null() || count == 0 {
2341    None
2342  } else {
2343    Some((count, array))
2344  }
2345}
2346
2347/// Judges the whole chapter table **before anything is materialised**,
2348/// allocating not one byte.
2349///
2350/// # Why this is a pass of its own
2351///
2352/// The judgement used to live inside [`build_chapters`], which runs
2353/// after the track table — and the track table is where this open
2354/// spends its memory: attachment carriers up to
2355/// [`DemuxLimits::max_total_attachment_bytes`](crate::DemuxLimits::max_total_attachment_bytes),
2356/// codec-parameter mirrors, retained stream metadata, and one `Arc` per
2357/// row. A file whose chapter count is a hundred times the ceiling was
2358/// therefore *certain* to be refused, and paid for hundreds of
2359/// megabytes of track material first. Repeating the open repeats the
2360/// bill, which is a resource-exhaustion road built out of two correct
2361/// checks in the wrong order.
2362///
2363/// So the cheap, allocation-free judgement runs beside stream
2364/// admission, before either table exists. What it judges is exactly
2365/// what [`build_chapters`] would have: the declared count against
2366/// [`DemuxLimits::max_chapters`](crate::DemuxLimits::max_chapters),
2367/// every chapter's timebase, and every title's decoded length against
2368/// the aggregate title budget.
2369///
2370/// # Why the second pass still judges
2371///
2372/// [`build_chapters`] repeats these checks rather than trusting this
2373/// one. It needs the timebase and the title anyway to build a row, and
2374/// re-deriving them is how the two passes stay one rule: nothing
2375/// between the two can change the container's answer —
2376/// `avformat_find_stream_info` has long returned — so a divergence
2377/// would be a bug in this file rather than a state to handle. The
2378/// repeat costs a pointer walk and a `strlen`; it allocates nothing.
2379fn admit_chapters(input: &Input, limits: DemuxLimits) -> Result<(), DemuxError> {
2380  let Some((count, array)) = chapter_array(input) else {
2381    return Ok(());
2382  };
2383  if count > limits.max_chapters() as usize {
2384    return Err(DemuxError::TooManyChapters(TooManyChapters::new(
2385      count,
2386      limits.max_chapters(),
2387    )));
2388  }
2389
2390  let mut title_spent: usize = 0;
2391  for index in 0..count {
2392    // SAFETY: `array` is the context's own array of `count` chapter
2393    // pointers and `index` is below `count`.
2394    let chapter = unsafe { *array.add(index) };
2395    if chapter.is_null() {
2396      continue;
2397    }
2398    // SAFETY: a non-null entry is an `AVChapter` the context owns for
2399    // its whole life. Every field read is a plain scalar, an
2400    // `AVRational` or a dictionary pointer — never a bindgen enum.
2401    let (id, time_base, metadata) = unsafe {
2402      (
2403        (*chapter).id,
2404        Rational::from((*chapter).time_base),
2405        (*chapter).metadata,
2406      )
2407    };
2408    if positive_rational_to_timebase(time_base).is_none() {
2409      return Err(DemuxError::ChapterTimebaseInvalid(
2410        ChapterTimebaseInvalid::new(index, id, time_base.numerator(), time_base.denominator()),
2411      ));
2412    }
2413    // The title is **measured, not built**: `metadata_value` returns a
2414    // borrow of libavutil's own buffer and `lossy_len` prices the
2415    // decoding without producing it, so an over-budget table is
2416    // refused having touched no heap at all.
2417    //
2418    // SAFETY: `metadata` is the chapter's own dictionary, owned by the
2419    // context and live for the whole of this call.
2420    let raw_title = unsafe { metadata_value(metadata, c"title") };
2421    // Measured and charged; the row itself is built by
2422    // [`build_chapters`], which is where the bytes are actually spent.
2423    let _admitted = charge_metadata(
2424      raw_title,
2425      &mut title_spent,
2426      limits.max_total_chapter_title_bytes(),
2427    )
2428    .map_err(|fault| chapter_title_error(index, fault, limits))?;
2429  }
2430  Ok(())
2431}
2432
2433fn build_chapters(input: &Input, limits: DemuxLimits) -> Result<Vec<Chapter<Ffmpeg>>, DemuxError> {
2434  let Some((count, array)) = chapter_array(input) else {
2435    return Ok(Vec::new());
2436  };
2437  if count > limits.max_chapters() as usize {
2438    return Err(DemuxError::TooManyChapters(TooManyChapters::new(
2439      count,
2440      limits.max_chapters(),
2441    )));
2442  }
2443
2444  let mut out = Vec::new();
2445  out
2446    .try_reserve_exact(count)
2447    .map_err(|_| DemuxError::ChapterAlloc(ChapterAlloc::new(count)))?;
2448  let mut title_spent: usize = 0;
2449
2450  for index in 0..count {
2451    // SAFETY: `array` is the context's own array of `count` chapter
2452    // pointers and `index` is below `count`.
2453    let chapter = unsafe { *array.add(index) };
2454    if chapter.is_null() {
2455      continue;
2456    }
2457    // SAFETY: a non-null entry is an `AVChapter` the context owns for
2458    // its whole life. Every field below is a plain scalar, an
2459    // `AVRational` or a dictionary pointer — never a bindgen enum.
2460    let (id, time_base, start, end, metadata) = unsafe {
2461      (
2462        (*chapter).id,
2463        Rational::from((*chapter).time_base),
2464        (*chapter).start,
2465        (*chapter).end,
2466        (*chapter).metadata,
2467      )
2468    };
2469    // **The ruler is judged before the row is built.** A chapter's
2470    // timebase is file-controlled and libavformat does not validate it
2471    // — the FFMETADATA parser stores `TIMEBASE=-1/1000` as written —
2472    // so this is where a container's malformed rational becomes a
2473    // refusal instead of a fabricated ruler or a panic.
2474    let timebase =
2475      positive_rational_to_timebase(time_base).ok_or(DemuxError::ChapterTimebaseInvalid(
2476        ChapterTimebaseInvalid::new(index, id, time_base.numerator(), time_base.denominator()),
2477      ))?;
2478
2479    // **`title` is where every container's chapter name lands.**
2480    // libavformat normalises the key, not the value: a Matroska
2481    // `ChapterDisplay`'s `ChapString`, a MOV chapter track's text
2482    // sample and an FFMETADATA `title=` all arrive on this one entry,
2483    // and what each wrote is what is read.
2484    //
2485    // **Measured and charged before a byte of it is copied.** The
2486    // reading below borrows libavutil's buffer, so a title the budget
2487    // refuses costs no heap at all — see [`retain_metadata`].
2488    //
2489    // SAFETY: `metadata` is the chapter's own dictionary, owned by the
2490    // context and live for the whole of this call.
2491    let raw_title = unsafe { metadata_value(metadata, c"title") };
2492    let title = retain_metadata(
2493      raw_title,
2494      &mut title_spent,
2495      limits.max_total_chapter_title_bytes(),
2496    )
2497    .map_err(|fault| chapter_title_error(index, fault, limits))?;
2498
2499    // Inside the reservation above, which was for `count` rows and is
2500    // never pushed past — so no growth, fallible or otherwise, happens
2501    // here.
2502    out.push(
2503      Chapter::new(
2504        id,
2505        timebase,
2506        Timestamp::new(start, timebase),
2507        Timestamp::new(end, timebase),
2508      )
2509      .with_title(title),
2510    );
2511  }
2512  Ok(out)
2513}
2514
2515/// Whether `packet`'s payload is the very allocation the container has
2516/// parked in `AVStream.attached_pic` for stream `index`.
2517///
2518/// # Why this exists
2519///
2520/// libavformat queues a stream's attached picture as its **first
2521/// packet** — `read_frame_internal` does `av_packet_ref(pkt,
2522/// &st->attached_pic)` and keeps its own reference — so that packet
2523/// arrives with two references through nobody's fault. A pure cover-art
2524/// stream never reaches this road (it is an attachment, hoisted at
2525/// open), but a stream carrying `ATTACHED_PIC | TIMED_THUMBNAILS` is
2526/// deliberately classified as **video** by
2527/// [`is_attachment_disposition`], so its first pull comes through here
2528/// and would be refused as a shared payload. Every packet after it is
2529/// an ordinary timed one with a buffer of its own.
2530///
2531/// # The probe, and why it is a proof rather than a guess
2532///
2533/// `av_buffer_ref` sets the new reference's `buffer` field to the
2534/// source's, so two `AVBufferRef`s name one allocation **iff** their
2535/// `buffer` pointers are equal — the same identity
2536/// [`crate::FfmpegBuffer::ptr_eq`] rests on. Comparing them therefore
2537/// establishes the fact the carve-out needs: this payload's allocation
2538/// *is* `AVStream.attached_pic`'s, so one of its outstanding references
2539/// is the container's own.
2540///
2541/// The alternatives were heuristics and are not used: the disposition
2542/// bits say a stream *has* an attached picture, not that this packet is
2543/// it; "the first packet on the stream" is an ordering assumption that
2544/// nothing in libavformat's contract fixes.
2545///
2546/// # The soundness argument, restated for this packet
2547///
2548/// It is the same one the hoisted-attachment road rests on, and it
2549/// holds here for the same reason. `AVStream.attached_pic` is written
2550/// once, while the container is being opened, and never again; the
2551/// reference this crate is looking at is the container's, held for the
2552/// lifetime of the `AVFormatContext`, and there is no
2553/// `ffmpeg_next::Packet` wrapping it for anyone to call `data_mut` on.
2554/// What the uniqueness rule guards against is a *safe Rust* handle that
2555/// may write while this crate reads, and the container's reference is
2556/// not one.
2557///
2558/// # Safety
2559///
2560/// `input` and `packet` must both be live for the duration of the call.
2561unsafe fn is_streams_attached_pic(input: &Input, index: usize, packet: &Packet) -> bool {
2562  // SAFETY: `input` owns a live `AVFormatContext`; `streams` is an
2563  // array of `nb_streams` pointers, and `index` is checked against it.
2564  let stream = unsafe {
2565    let context = input.as_ptr();
2566    if index >= (*context).nb_streams as usize {
2567      return false;
2568    }
2569    *(*context).streams.add(index)
2570  };
2571  if stream.is_null() {
2572    return false;
2573  }
2574  // SAFETY: `stream` is one of the context's own live `AVStream`s and
2575  // `packet` is live per this function's contract.
2576  unsafe { packet_is_parked_picture(stream, packet) }
2577}
2578
2579/// The identity itself: whether `packet`'s payload allocation is the
2580/// one `stream` has parked in `attached_pic`.
2581///
2582/// Split out from [`is_streams_attached_pic`] so the comparison can be
2583/// tested against a hand-built pair without forging an
2584/// `AVFormatContext` — see `a_queued_attached_picture_is_recognised`.
2585///
2586/// # Safety
2587///
2588/// `stream` must be a live `AVStream` and `packet` a live `AVPacket`.
2589unsafe fn packet_is_parked_picture(stream: *const AVStream, packet: &Packet) -> bool {
2590  use ffmpeg_next::packet::Ref;
2591
2592  // SAFETY: both are live per the contract; `attached_pic` is an inline
2593  // `AVPacket` and both `buf` fields may be null, which is answered
2594  // before either is read through.
2595  unsafe {
2596    let parked = (*stream).attached_pic.buf;
2597    let carried = (*packet.as_ptr()).buf;
2598    if parked.is_null() || carried.is_null() {
2599      return false;
2600    }
2601    // The shared `AVBuffer`, not the `AVBufferRef`: `av_packet_ref`
2602    // mints a new reference struct around the same allocation, so
2603    // comparing the references themselves would answer "no" to exactly
2604    // the case this is for.
2605    (*parked).buffer == (*carried).buffer
2606  }
2607}
2608
2609/// Whether a stream's disposition makes it an **attachment** — a
2610/// payload with no place on the timeline — rather than a timed track.
2611///
2612/// `AV_DISPOSITION_ATTACHED_PIC` alone says "cover art": one still
2613/// image, parked in `AVStream.attached_pic`, no timeline. But FFmpeg
2614/// pairs it with `AV_DISPOSITION_TIMED_THUMBNAILS` for a different
2615/// thing entirely — "the stream is sparse, and contains thumbnail
2616/// images, often corresponding to chapter markers", a flag its own
2617/// header documents as *only ever* used together with `ATTACHED_PIC`.
2618/// Such a stream has many images and every one of them has a
2619/// timestamp.
2620///
2621/// Classifying that as an attachment loses all but the first: the
2622/// attachment contract is exactly one packet, so the queue takes the
2623/// parked copy and the delivery loop drops every timed packet on the
2624/// track. It goes to the **`Video`** arm instead. That does not
2625/// contradict "cover art is an attachment, not video" — the reason
2626/// behind that ruling is that a single still with no timeline must not
2627/// look like a motion track, and a timed-thumbnail stream *is* on the
2628/// timeline. It is sparse video: a codec id, a frame size, a pixel
2629/// format and packets with timestamps, which is everything a consumer
2630/// needs to decode the images. The `Data` arm was the alternative and
2631/// is worse: it would strand encoded pictures in an arm that names no
2632/// decoder.
2633///
2634/// The bits are tested against the raw `AVStream.disposition` rather
2635/// than through `ffmpeg_next`'s `Disposition`, which mints no
2636/// `TIMED_THUMBNAILS` constant at all — its `from_bits_truncate` drops
2637/// every bit this build of the wrapper has no name for, which is how
2638/// the distinction went missing in the first place.
2639const fn is_attachment_disposition(disposition: c_int) -> bool {
2640  disposition & AV_DISPOSITION_ATTACHED_PIC != 0
2641    && disposition & AV_DISPOSITION_TIMED_THUMBNAILS == 0
2642}
2643
2644/// Upper bound on the NUL search in [`metadata_value`].
2645///
2646/// Generous by four orders of magnitude for a filename or a MIME type,
2647/// and there only so that a value libavutil did not terminate cannot
2648/// turn the walk into an unbounded read — the same discipline
2649/// [`crate::channel_layout`] and the pixel-format namer follow. A value
2650/// longer than this is refused rather than truncated: a truncated
2651/// filename is a different filename.
2652const METADATA_VALUE_MAX_BYTES: usize = 64 * 1024;
2653
2654/// What a metadata dictionary holds for one key — **measured, and not
2655/// yet copied**.
2656///
2657/// Three outcomes rather than an `Option`, because the answer that
2658/// used to go missing is the third one: a value with no terminator
2659/// inside [`METADATA_VALUE_MAX_BYTES`] is not an absent value, and
2660/// reporting it as one made a container's declaration vanish silently
2661/// *and* escape every budget charged against it.
2662///
2663/// `Present` borrows libavutil's own buffer. That is the load-bearing
2664/// property of this function and the reason it exists at all: a borrow
2665/// allocates nothing, so a caller can learn a value's size and refuse
2666/// it **before** any owning conversion exists.
2667enum MetadataValue<'a> {
2668  /// The dictionary has no such key, or the entry's value is null.
2669  Absent,
2670  /// The value, borrowed from the dictionary. Not NUL-terminated here:
2671  /// the terminator is what bounded the walk.
2672  Present(&'a [u8]),
2673  /// No terminator below [`METADATA_VALUE_MAX_BYTES`]. Refused rather
2674  /// than truncated — a truncated filename is a different filename —
2675  /// and now refused *visibly*, unlike an absent one.
2676  NotTerminated,
2677}
2678
2679/// Measures one dictionary entry without copying it.
2680///
2681/// # Safety
2682///
2683/// `dict` must be null or a live `*const AVDictionary` for the
2684/// duration of this call, and the returned borrow is valid only while
2685/// that dictionary is neither modified nor freed.
2686unsafe fn metadata_value<'a>(dict: *const AVDictionary, key: &CStr) -> MetadataValue<'a> {
2687  if dict.is_null() {
2688    return MetadataValue::Absent;
2689  }
2690  // SAFETY: `dict` is live per the contract above and `key` is a
2691  // NUL-terminated C string by construction; `av_dict_get` reads both
2692  // and returns a borrowed entry owned by the dictionary.
2693  let entry = unsafe { av_dict_get(dict, key.as_ptr(), std::ptr::null(), 0) };
2694  if entry.is_null() {
2695    return MetadataValue::Absent;
2696  }
2697  // SAFETY: a non-null entry is a live `AVDictionaryEntry` for as long
2698  // as the dictionary is not modified, which it is not here.
2699  let value = unsafe { (*entry).value };
2700  if value.is_null() {
2701    return MetadataValue::Absent;
2702  }
2703  for len in 0..METADATA_VALUE_MAX_BYTES {
2704    // SAFETY: `value` is a NUL-terminated string libavutil allocated
2705    // with `av_strdup`; the walk reads at most one byte past the last
2706    // value byte and stops at the terminator.
2707    if unsafe { *value.add(len).cast::<u8>() } == 0 {
2708      // SAFETY: the `len` bytes below the terminator were just walked,
2709      // so the slice is in bounds and initialised. The borrow lives as
2710      // long as the dictionary does, which this function's contract
2711      // requires of its caller.
2712      return MetadataValue::Present(unsafe {
2713        std::slice::from_raw_parts(value.cast::<u8>(), len)
2714      });
2715    }
2716  }
2717  MetadataValue::NotTerminated
2718}
2719
2720/// The length `String::from_utf8_lossy` would produce for `bytes`,
2721/// **without producing it**.
2722///
2723/// The charge has to be the decoded size rather than the raw one:
2724/// lossy decoding replaces each invalid sequence with `U+FFFD`, three
2725/// bytes, so a value of invalid single bytes triples on the way in. It
2726/// also has to be knowable before anything is allocated, which rules
2727/// out decoding first and measuring afterwards.
2728///
2729/// Exactness is not decorative — an approximation would either
2730/// under-charge the budget or refuse ordinary text — so agreement with
2731/// `from_utf8_lossy` is asserted directly in the unit lanes rather
2732/// than argued here.
2733pub(crate) fn lossy_len(bytes: &[u8]) -> usize {
2734  const REPLACEMENT: usize = char::REPLACEMENT_CHARACTER.len_utf8();
2735
2736  let mut rest = bytes;
2737  let mut total = 0usize;
2738  loop {
2739    match std::str::from_utf8(rest) {
2740      Ok(valid) => return total + valid.len(),
2741      Err(fault) => {
2742        total += fault.valid_up_to() + REPLACEMENT;
2743        match fault.error_len() {
2744          // An invalid sequence of `skip` bytes becomes one `U+FFFD`.
2745          Some(skip) => rest = &rest[fault.valid_up_to() + skip..],
2746          // A truncated trailing sequence: one `U+FFFD`, and the end.
2747          None => return total,
2748        }
2749      }
2750    }
2751  }
2752}
2753
2754/// Decodes measured bytes into owned text, through a buffer reserved
2755/// **fallibly**.
2756///
2757/// `decoded` is [`lossy_len`]'s answer for the same bytes, so the one
2758/// reservation here is exact and the pushes that follow cannot grow
2759/// it.
2760///
2761/// # Nothing is copied, and nothing allocates after the charge
2762///
2763/// The buffer is reserved once, fallibly, at the exact decoded size,
2764/// and then **moved** into the carrier: `Utf8Bytes::from(String)`
2765/// keeps a short value inline (`smol_bytes::INLINE_CAP`, no allocation
2766/// at all) and hands a longer one to `bytes::Bytes::from(Vec<u8>)`,
2767/// which takes the vector's own allocation over. The `Utf8Bytes` this
2768/// replaced copied into a fresh `Arc<str>` instead — a second
2769/// allocation, infallible, of an attacker-sized value, made while the
2770/// first was still live, so failing it aborted the process that the
2771/// budget above existed to keep alive.
2772///
2773/// **The one residue, stated exactly.** `Bytes::from(Vec<u8>)` moves
2774/// the buffer outright when the vector's length equals its capacity,
2775/// which `try_reserve_exact` followed by exactly `decoded` bytes is
2776/// what produces; should an allocator hand back more capacity than was
2777/// asked for, `bytes` allocates a fixed-size reference-count header —
2778/// thirty-two bytes, the same for a ten-byte title and a sixty-four
2779/// kibibyte one. What is gone is the part an attacker could scale.
2780pub(crate) fn lossy_text(bytes: &[u8], decoded: usize) -> Result<Utf8Bytes, TryReserveError> {
2781  let mut buffer = std::string::String::new();
2782  buffer.try_reserve_exact(decoded)?;
2783
2784  let mut rest = bytes;
2785  loop {
2786    match std::str::from_utf8(rest) {
2787      Ok(valid) => {
2788        buffer.push_str(valid);
2789        break;
2790      }
2791      Err(fault) => {
2792        let (valid, after) = rest.split_at(fault.valid_up_to());
2793        buffer.push_str(
2794          std::str::from_utf8(valid).expect("valid_up_to bounds a valid prefix by definition"),
2795        );
2796        buffer.push(char::REPLACEMENT_CHARACTER);
2797        match fault.error_len() {
2798          Some(skip) => rest = &after[skip..],
2799          None => break,
2800        }
2801      }
2802    }
2803  }
2804  debug_assert_eq!(
2805    buffer.len(),
2806    decoded,
2807    "lossy_len must price exactly what lossy_text builds",
2808  );
2809  // The move. Nothing past this point copies the value.
2810  Ok(Utf8Bytes::from(buffer))
2811}
2812
2813/// Names the fault a stream's metadata ran into, for the key it was
2814/// reading.
2815///
2816/// One mapper rather than three copies of the same `match`: the three
2817/// values a track row retains — `filename`, `mimetype` and `language`
2818/// — share one budget and one road, and differ only in which key is
2819/// reported.
2820/// Names the fault a chapter's title ran into, the way
2821/// [`stream_metadata_error`] does for a stream's — one mapper so the
2822/// admission pass and the materialisation cannot report the same fault
2823/// two different ways.
2824fn chapter_title_error(index: usize, fault: MetadataFault, limits: DemuxLimits) -> DemuxError {
2825  match fault {
2826    MetadataFault::TooLong => {
2827      DemuxError::ChapterTitleTooLong(ChapterTitleTooLong::new(index, METADATA_VALUE_MAX_BYTES))
2828    }
2829    MetadataFault::BudgetExhausted(total) => DemuxError::ChapterTitleBudgetExhausted(
2830      ChapterTitleBudgetExhausted::new(index, total, limits.max_total_chapter_title_bytes()),
2831    ),
2832    MetadataFault::Alloc(bytes) => {
2833      DemuxError::ChapterTitleAlloc(ChapterTitleAlloc::new(index, bytes))
2834    }
2835  }
2836}
2837
2838fn stream_metadata_error(
2839  index: usize,
2840  key: &'static CStr,
2841  fault: MetadataFault,
2842  limits: DemuxLimits,
2843) -> DemuxError {
2844  let key = key.to_str().unwrap_or("<non-utf8 key>");
2845  match fault {
2846    MetadataFault::TooLong => DemuxError::TrackMetadataTooLong(TrackMetadataTooLong::new(
2847      index,
2848      key,
2849      METADATA_VALUE_MAX_BYTES,
2850    )),
2851    MetadataFault::BudgetExhausted(total) => {
2852      DemuxError::TrackMetadataBudgetExhausted(TrackMetadataBudgetExhausted::new(
2853        index,
2854        key,
2855        total,
2856        limits.max_total_stream_metadata_bytes(),
2857      ))
2858    }
2859    MetadataFault::Alloc(bytes) => {
2860      DemuxError::TrackMetadataAlloc(TrackMetadataAlloc::new(index, key, bytes))
2861    }
2862  }
2863}
2864
2865/// Why a metadata value this crate meant to retain was not retained.
2866///
2867/// Crate-private on purpose: each call site maps it to an error that
2868/// names *what* was being read, because "the chapter titles are over
2869/// budget" and "this stream's language is over budget" are different
2870/// things to a caller even though the mechanism is one.
2871#[derive(Debug)]
2872enum MetadataFault {
2873  /// [`MetadataValue::NotTerminated`].
2874  TooLong,
2875  /// The running total, which is over the budget.
2876  BudgetExhausted(usize),
2877  /// The decoded size that could not be reserved.
2878  Alloc(usize),
2879}
2880
2881/// **Measure, charge, then materialise — in that order.**
2882///
2883/// The order is the whole of it. Reading the size is a borrow of
2884/// libavutil's buffer and allocates nothing, so a value the budget
2885/// refuses costs no heap at all; only a value already admitted is
2886/// built, and it is built through [`lossy_text`]'s fallible
2887/// reservation.
2888///
2889/// `spent` advances only for a value actually retained.
2890fn retain_metadata(
2891  value: MetadataValue<'_>,
2892  spent: &mut usize,
2893  limit: usize,
2894) -> Result<Option<Utf8Bytes>, MetadataFault> {
2895  let Some((bytes, decoded)) = charge_metadata(value, spent, limit)? else {
2896    return Ok(None);
2897  };
2898  lossy_text(bytes, decoded)
2899    .map(Some)
2900    .map_err(|_| MetadataFault::Alloc(decoded))
2901}
2902
2903/// **The judging half of [`retain_metadata`], on its own** — measure
2904/// and charge, build nothing.
2905///
2906/// It is separate because the judging has to happen in a place the
2907/// building cannot: an admission pass that runs before any table is
2908/// materialised. Sharing one function is what stops the two passes
2909/// from drifting into two rules, which for a budget would mean a file
2910/// admitted by one and refused by the other after the memory was
2911/// already spent.
2912///
2913/// Returns the admitted bytes with [`lossy_len`]'s price for them, so a
2914/// caller that *is* going to build can hand both straight to
2915/// [`lossy_text`] without measuring twice. `spent` advances only for a
2916/// value that was admitted.
2917fn charge_metadata<'a>(
2918  value: MetadataValue<'a>,
2919  spent: &mut usize,
2920  limit: usize,
2921) -> Result<Option<(&'a [u8], usize)>, MetadataFault> {
2922  let bytes = match value {
2923    MetadataValue::Absent => return Ok(None),
2924    MetadataValue::NotTerminated => return Err(MetadataFault::TooLong),
2925    MetadataValue::Present(bytes) => bytes,
2926  };
2927  let decoded = lossy_len(bytes);
2928  let total = spent.saturating_add(decoded);
2929  if total > limit {
2930    return Err(MetadataFault::BudgetExhausted(total));
2931  }
2932  *spent = total;
2933  Ok(Some((bytes, decoded)))
2934}
2935
2936/// The three metadata keys a track row retains, in the order
2937/// [`build_tracks`] reads them — so the admission pass and the
2938/// materialisation refuse on the *same* value and name the same key.
2939const RETAINED_STREAM_METADATA: [&CStr; 3] = [c"filename", c"mimetype", c"language"];
2940
2941/// Wraps `AVStream.attached_pic` — the real packet libavformat parsed
2942/// for a cover-art stream — as this track's one attachment packet.
2943///
2944/// A stream that declares cover art but parks no payload still gets a
2945/// packet: an empty one, marked `synthesized`, because the contract is
2946/// one packet per attachment track and a consumer that sees an empty
2947/// payload learns something true about the file. The alternative shipped
2948/// once — waiting for the payload to arrive as a packet later — and it
2949/// cannot hold: nothing stops a timed packet, or a seek, from coming
2950/// first, so the track's packet would arrive out of order or never.
2951///
2952/// Measured before it was written: across MP3 (ID3 APIC), M4A (`covr`),
2953/// FLAC (`METADATA_BLOCK_PICTURE`) and Matroska (an `image/*`
2954/// attachment), every stream libavformat gives
2955/// `AV_DISPOSITION_ATTACHED_PIC` also carries the parked packet —
2956/// `ff_add_attached_pic` sets the disposition and fills
2957/// `attached_pic` in the same breath. The empty case is the honest
2958/// answer to a state this build's demuxers do not produce, not a
2959/// fallback anything relies on.
2960///
2961/// # Safety
2962///
2963/// `pkt` must be a live `*const AVPacket` — in practice the
2964/// `attached_pic` embedded in the `AVStream` at `index` — for the
2965/// duration of this call.
2966unsafe fn attached_pic_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
2967  pkt: *const ffmpeg_next::ffi::AVPacket,
2968  index: usize,
2969  limits: DemuxLimits,
2970) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
2971  // Already admitted: [`admit_streams`] charged this payload — and
2972  // every other attachment in the file — before `build_tracks`
2973  // allocated anything. The per-attachment budget is passed down as
2974  // this packet's own ceiling anyway, so the funnel is guarded even if
2975  // a future caller reaches it without the admission pass.
2976  //
2977  // SAFETY: `pkt` is live per the contract above.
2978  // **The container's own cover art**, whose buffer libavformat also
2979  // holds — see [`crate::buffer::PayloadProvenance`] for why that
2980  // second reference is not the hazard a caller's second `Packet` is.
2981  let captured = unsafe {
2982    crate::buffer::payload_of::<C>(
2983      pkt,
2984      limits.max_attachment_bytes(),
2985      crate::buffer::PayloadProvenance::AttachedPicture,
2986    )
2987  }
2988  .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
2989  let extra = AttachmentPacketExtra::new(index as i32);
2990  Ok(match captured {
2991    Some(payload) => {
2992      // The hoisted packet's own flags, through the same raw reader the
2993      // five boundary conversions use. FFmpeg marks an attached picture
2994      // `AV_PKT_FLAG_KEY` — a still image is a keyframe if anything is
2995      // — and building this one with empty flags dropped that, along
2996      // with `CORRUPT` and every other bit the packet really carried.
2997      // SAFETY: `pkt` points at the live embedded `AVPacket`.
2998      let flags = unsafe { boundary::md_flags_from_av_packet(pkt) }
2999        .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
3000      AttachmentPacket::new(payload, extra).with_flags(flags)
3001    }
3002    // Nothing was parked, so there are no flags to read: an empty set
3003    // is the honest answer for a packet this layer invented.
3004    None => AttachmentPacket::new(C::empty(), extra.with_synthesized(true)),
3005  })
3006}
3007
3008/// Builds an attachment payload out of a track's codec extradata — the
3009/// only place a font's bytes ever live, since an
3010/// `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets at all.
3011///
3012/// A track with no extradata still gets a packet, with an empty
3013/// payload: the contract is one packet per attachment track, and a
3014/// consumer that sees an empty one learns something true about the
3015/// file. Only an allocation failure is an error.
3016fn extradata_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
3017  stream: &ffmpeg_next::format::stream::Stream<'_>,
3018  limits: DemuxLimits,
3019) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
3020  let index = stream.index();
3021  let parameters = stream.parameters();
3022  // SAFETY: `parameters` keeps the `AVCodecParameters` live;
3023  // `extradata` / `extradata_size` are public fields.
3024  let par = unsafe { parameters.as_ptr() };
3025  let ptr = unsafe { (*par).extradata };
3026  let len = unsafe { (*par).extradata_size }.max(0) as usize;
3027  // Already admitted, exactly as on the hoisted cover-art path — see
3028  // [`admit_streams`]. Re-judged here against the per-attachment
3029  // ceiling alone, so the helper is safe to call on its own.
3030  if len > limits.max_attachment_bytes() {
3031    return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
3032      index,
3033      len,
3034      limits.max_attachment_bytes(),
3035    )));
3036  }
3037  let bytes: &[u8] = if ptr.is_null() || len == 0 {
3038    &[]
3039  } else {
3040    // SAFETY: libavformat guarantees `extradata` is readable for
3041    // `extradata_size` bytes (plus its padding) while the parameters
3042    // live, and the slice is consumed before this function returns.
3043    unsafe { std::slice::from_raw_parts(ptr, len) }
3044  };
3045  // Extradata is a plain allocation with no `AVBufferRef` behind it —
3046  // an `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets, so a
3047  // font's bytes never live in a refcounted buffer. **Both** lanes copy
3048  // here, which is what `from_bytes` is for.
3049  Ok(AttachmentPacket::new(
3050    C::from_bytes(bytes).ok_or_else(|| {
3051      DemuxError::PacketBuffer(PacketBuffer::new(
3052        index,
3053        crate::buffer::PacketBufferError::CaptureFailed(crate::buffer::CaptureFailed::new(len)),
3054      ))
3055    })?,
3056    AttachmentPacketExtra::new(index as i32).with_synthesized(true),
3057  ))
3058}
3059
3060/// **The admission pass**: judges every stream in the file before the
3061/// track table allocates anything at all.
3062///
3063/// # Why this cannot live inside the capture
3064///
3065/// It used to, and that was a bypass. `build_tracks` deep-copies each
3066/// stream's `AVCodecParameters` on its way to building a `TrackExtra`,
3067/// and for an `AVMEDIA_TYPE_ATTACHMENT` stream **the extradata inside
3068/// those parameters is the attachment's payload**. So the loop paid for
3069/// the payload — a full `avcodec_parameters_copy` — one statement
3070/// before asking whether it was allowed to. A file declaring a gigabyte
3071/// of "font" allocated the gigabyte and then reported that a gigabyte
3072/// was too much.
3073///
3074/// The fix is not a check moved a few lines earlier: any per-track
3075/// interleaving of judging and paying has the same shape, because the
3076/// aggregate budget is only knowable once every track has been *seen*.
3077/// So the whole file is admitted here, in a pass that allocates
3078/// nothing — it reads two integers per stream — and only a container
3079/// that passes in full reaches the loop that builds carriers and
3080/// parameter copies.
3081///
3082/// # Why it is every stream, not every attachment
3083///
3084/// Because the track table copies **every** stream's codec parameters,
3085/// and `AVCodecParameters` reaches the heap three ways — `extradata`,
3086/// every `coded_side_data` entry, a custom channel map — all of them
3087/// sized by the file. A pass that walked only attachment streams left
3088/// the other road wide open: a MOV puts an ICC profile in
3089/// `coded_side_data`, on an ordinary video track, and the wholesale
3090/// copy took it before anything asked how big it was. That was the same
3091/// class of defect three review rounds running, which is why the copy
3092/// itself is gone (see
3093/// [`bounded_clone_parameters`](crate::extras::bounded_clone_parameters))
3094/// and why this pass sees everything.
3095///
3096/// # What is charged
3097///
3098/// The bytes this session will **retain**, which is not always the
3099/// declared size:
3100///
3101/// - every stream is charged its parameter clone's footprint against
3102///   the per-stream and whole-file codec-parameter budgets;
3103/// - a synthesized attachment's `extradata` is charged to the
3104///   *attachment* budget and left out of the parameter one, because the
3105///   clone strips it and the carrier holds it — one set of bytes, one
3106///   charge;
3107/// - the attachment budgets then see:
3108///
3109/// - a hoisted cover-art track retains its parked `AVPacket`'s payload
3110///   *and* the extradata in its parameter copy, which the still decoder
3111///   may need and which is not a duplicate of the payload;
3112/// - a synthesized `AVMEDIA_TYPE_ATTACHMENT` track retains only the
3113///   carrier, because `build_tracks` strips the duplicate extradata out
3114///   of the parameter copy (see the comment there for the census).
3115///
3116/// Charging residency rather than payload is what keeps the budget an
3117/// honest statement about memory instead of about file structure.
3118///
3119/// The per-attachment ceiling is judged first for each track: when a
3120/// single payload is itself over the line, that is the more specific
3121/// fact, and naming the aggregate instead would send a reader looking
3122/// for four hundred attachments that are not there.
3123fn admit_streams(input: &Input, limits: DemuxLimits) -> Result<(), DemuxError> {
3124  let mut attachment_spent: usize = 0;
3125  let mut parameter_spent: usize = 0;
3126  let mut metadata_spent: usize = 0;
3127
3128  for stream in input.streams() {
3129    let index = stream.index();
3130    let parameters = stream.parameters();
3131    // SAFETY: `parameters` keeps the `AVCodecParameters` live for this
3132    // measurement, which allocates nothing and dereferences only what
3133    // it counts.
3134    let par = unsafe { parameters.as_ptr() };
3135    if par.is_null() {
3136      return Err(DemuxError::ParametersMissing(ParametersMissing::new(index)));
3137    }
3138
3139    // **The ruler, judged here rather than during materialisation.**
3140    //
3141    // This is the judge-before-pay invariant for a refusal that is not
3142    // a budget, and it was the third place the invariant failed. A
3143    // malformed `AVStream.time_base` is a permanent, deterministic fact
3144    // about the container — it does not depend on how much memory the
3145    // machine has — so it can and must be decided while nothing has
3146    // been spent. It used to be decided inside `build_tracks`' loop,
3147    // which meant a malformed ruler on the *last* stream was refused
3148    // only after every earlier stream's codec ticket, metadata and
3149    // attachment carrier had been materialised.
3150    //
3151    // Structure before budget, deliberately: a stream that is malformed
3152    // is a more specific thing to say than a file that is too large,
3153    // and the two can be true of one container at once.
3154    let _ruler = stream_timebase(index, stream.time_base())?;
3155
3156    // **And the layout's structure, for the same reason.** A non-null
3157    // `ch_layout.opaque`, a custom order with no map or a non-positive
3158    // count, and a non-null `opaque` on any map entry are all
3159    // permanent facts about the container that the codec ticket would
3160    // otherwise discover mid-materialisation — after every earlier
3161    // stream had been paid for. Deciding them costs a pointer walk and
3162    // no allocation; see
3163    // [`validate_channel_layout`](crate::ticket::validate_channel_layout),
3164    // which the ticket builder calls again as its own first statement.
3165    //
3166    // SAFETY: `par` is the live `AVCodecParameters` checked non-null
3167    // above, owned by `parameters` for this iteration, and for a custom
3168    // order libavformat filled its map with `nb_channels` entries
3169    // through `av_channel_layout_copy` — the same argument the demux
3170    // road's own channel-layout read makes.
3171    unsafe { crate::ticket::validate_channel_layout(par, index) }?;
3172
3173    let footprint =
3174      unsafe { crate::extras::measure_parameters(par) }.ok_or(DemuxError::ParametersTooLarge(
3175        ParametersTooLarge::new(index, usize::MAX, limits.max_codec_parameter_bytes()),
3176      ))?;
3177
3178    // SAFETY: `stream` keeps the `AVStream` live; `disposition` is a
3179    // public field.
3180    let disposition = unsafe { (*stream.as_ptr()).disposition };
3181    let cover_art = is_attachment_disposition(disposition);
3182    let synthesized = !cover_art && boundary::media_kind_of(&parameters).is_attachment();
3183
3184    // What the *parameter clone* will retain for this stream. The
3185    // synthesized-attachment road strips `extradata` — the font's
3186    // payload rides the carrier instead — so counting it here would
3187    // charge the same bytes twice and make the budget a statement about
3188    // the file rather than about memory.
3189    let retained_parameters = if synthesized {
3190      footprint.total_without_extradata()
3191    } else {
3192      footprint.total()
3193    }
3194    .ok_or(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
3195      index,
3196      usize::MAX,
3197      limits.max_codec_parameter_bytes(),
3198    )))?;
3199
3200    if retained_parameters > limits.max_codec_parameter_bytes() {
3201      return Err(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
3202        index,
3203        retained_parameters,
3204        limits.max_codec_parameter_bytes(),
3205      )));
3206    }
3207    parameter_spent = parameter_spent.saturating_add(retained_parameters);
3208    if parameter_spent > limits.max_total_codec_parameter_bytes() {
3209      return Err(DemuxError::ParametersBudgetExhausted(
3210        ParametersBudgetExhausted::new(
3211          index,
3212          parameter_spent,
3213          limits.max_total_codec_parameter_bytes(),
3214        ),
3215      ));
3216    }
3217
3218    // **And the three metadata values this row will retain**, measured
3219    // here for the same reason everything else in this pass is: a
3220    // budget checked during materialisation is a budget that has
3221    // already been paid. The charge used to live in `build_tracks`'s
3222    // loop, which meant an over-budget value on the *last* stream was
3223    // refused only after every earlier stream's parameter clone and
3224    // attachment carrier had been retained and this stream's ticket
3225    // copied — so a file certain to be refused could first be made to
3226    // cost the whole aggregate, on every open.
3227    //
3228    // Nothing here allocates: `metadata_value` hands back a borrow of
3229    // libavutil's own buffer and `lossy_len` prices the decoding
3230    // without producing it. The keys are read in `build_tracks`' own
3231    // order so both passes refuse on the same value and name the same
3232    // key, and both call `charge_metadata`, so there is one rule
3233    // rather than two.
3234    //
3235    // SAFETY: `stream` keeps the `AVStream` — and so its metadata
3236    // dictionary — live across the reads below, and no borrow outlives
3237    // this loop iteration.
3238    let metadata = unsafe { (*stream.as_ptr()).metadata };
3239    for key in RETAINED_STREAM_METADATA {
3240      // SAFETY: as above.
3241      let raw = unsafe { metadata_value(metadata, key) };
3242      let _admitted = charge_metadata(
3243        raw,
3244        &mut metadata_spent,
3245        limits.max_total_stream_metadata_bytes(),
3246      )
3247      .map_err(|fault| stream_metadata_error(index, key, fault, limits))?;
3248    }
3249
3250    // And what the *carrier* will hold, for the two attachment roads.
3251    let carrier = if cover_art {
3252      // SAFETY: `attached_pic` is an `AVPacket` embedded in the
3253      // `AVStream` by value; `addr_of!` reaches it without forming a
3254      // reference to the stream.
3255      let pkt = unsafe { std::ptr::addr_of!((*stream.as_ptr()).attached_pic) };
3256
3257      // **The parked packet's own structure, judged here.**
3258      //
3259      // `PacketBuffer` is not one fault: it carries a `TRUSTED` payload
3260      // this crate must not copy, a `data`/`size` pair that does not lie
3261      // inside the buffer it claims, a buffer somebody else holds a
3262      // reference to, flags outside the portable set — **and** the
3263      // allocator declining the carrier. Only the last of those is
3264      // unforeseeable; the rest are permanent facts about an `AVPacket`
3265      // that is already parked and already readable. Deciding them in
3266      // the capture meant a bad final attachment was refused after every
3267      // earlier stream's ticket, metadata and carrier had been retained.
3268      //
3269      // The budget passed here is deliberately `usize::MAX`: the size
3270      // question belongs to `charge_attachment` below, which answers it
3271      // as `AttachmentTooLarge` against the attachment seats rather than
3272      // as a packet's own ceiling. This call is asked only for the
3273      // structural answers.
3274      //
3275      // SAFETY: `pkt` points at the live embedded `AVPacket` for the
3276      // whole of this call, and no plan outlives it — it is discarded
3277      // here, the capture happens later against the same packet.
3278      let _plan = unsafe {
3279        crate::buffer::preflight_payload(
3280          pkt,
3281          usize::MAX,
3282          crate::buffer::PayloadProvenance::AttachedPicture,
3283        )
3284      }
3285      .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
3286      // And the flags the packet will be rebuilt with, which is the one
3287      // remaining deterministic `PacketBuffer` arm.
3288      //
3289      // SAFETY: as above.
3290      unsafe { boundary::md_flags_from_av_packet(pkt) }
3291        .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
3292
3293      // SAFETY: a plain `int` field of the live embedded packet.
3294      unsafe { (*pkt).size }.max(0) as usize
3295    } else if synthesized {
3296      // The **payload**, not the padded clone figure. The carrier is
3297      // an `FfmpegBytes` over exactly these bytes and the clone omits
3298      // extradata entirely on this road, so nothing here allocates the
3299      // padding — charging it would bill sixty-four bytes that are
3300      // never spent, reject a payload in the last sixty-four below the
3301      // ceiling, and disagree with the image road about the same file
3302      // at exactly the cap.
3303      footprint.extradata_payload()
3304    } else {
3305      // Not an attachment: nothing is captured eagerly for it, so
3306      // nothing more is charged.
3307      continue;
3308    };
3309
3310    charge_attachment(index, carrier, limits, &mut attachment_spent)?;
3311  }
3312  Ok(())
3313}
3314
3315/// Charges `declared` bytes against both attachment budgets, refusing
3316/// before anything is copied. The one place a file's attachment
3317/// spending is decided; see [`admit_streams`] for when it runs.
3318fn charge_attachment(
3319  index: usize,
3320  declared: usize,
3321  limits: DemuxLimits,
3322  spent: &mut usize,
3323) -> Result<(), DemuxError> {
3324  if declared > limits.max_attachment_bytes() {
3325    return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
3326      index,
3327      declared,
3328      limits.max_attachment_bytes(),
3329    )));
3330  }
3331  let total = spent.saturating_add(declared);
3332  if total > limits.max_total_attachment_bytes() {
3333    return Err(DemuxError::AttachmentBudgetExhausted(
3334      AttachmentBudgetExhausted::new(index, total, limits.max_total_attachment_bytes()),
3335    ));
3336  }
3337  *spent = total;
3338  Ok(())
3339}
3340
3341/// A stream's own ruler, refused rather than repaired — **one
3342/// conversion and one error, for the two passes that need it**.
3343///
3344/// A timebase is file-controlled and there is no honest substitute for
3345/// it: it is what every timestamp on the track is measured against, so
3346/// a malformed one makes the track's whole timeline a fabrication
3347/// rather than a detail. `0/1` — libavformat's own "not set" — is not
3348/// malformed and is admitted; see [`rational_to_timebase`].
3349///
3350/// This exists as a function because the judgement has to happen twice
3351/// and must not become two judgements. [`admit_streams`] calls it while
3352/// nothing has been allocated, which is what makes a malformed ruler on
3353/// the *last* stream refuse the open before the first stream's codec
3354/// ticket is copied; [`build_tracks`] calls it again where the value is
3355/// actually used. Two call sites, one rule, and no way for the pass
3356/// that pays to refuse something the pass that judges admitted.
3357fn stream_timebase(index: usize, declared: Rational) -> Result<Timebase, DemuxError> {
3358  rational_to_timebase(declared).ok_or(DemuxError::TrackTimebaseInvalid(TrackTimebaseInvalid::new(
3359    index,
3360    declared.numerator(),
3361    declared.denominator(),
3362  )))
3363}
3364
3365/// A file-controlled `AVRational` as a [`Timebase`], or `None` where it
3366/// is not one.
3367///
3368/// **Nothing is clamped and nothing is invented.** `None` means exactly
3369/// that [`Timebase`] has no such value: a denominator that is zero or
3370/// negative, or a negative numerator. A zero numerator *is* admitted,
3371/// because it is not malformed — `0/1` is libavformat's own "this
3372/// stream has no timebase yet" default, which ordinary containers carry
3373/// on untimed streams, and passing it through is reporting rather than
3374/// guessing. See [`positive_rational_to_timebase`] for the stricter
3375/// rule the seats that cannot mean *absent* take.
3376///
3377/// # Why this is fallible now
3378///
3379/// It used to clamp the denominator up to 1 and hand the numerator to
3380/// `Timebase::new` unexamined, on the argument that a malformed
3381/// timebase makes one track's timestamps meaningless rather than the
3382/// file unreadable. The first half of that was a fabrication — a `1/1`
3383/// invented here is indistinguishable downstream from a `1/1` the file
3384/// really declared — and the second half was a **panic**:
3385/// `Timebase::new` asserts a non-negative numerator, and an
3386/// `AVRational` out of a container can be negative. libavformat's
3387/// FFMETADATA parser stores `TIMEBASE=-1/1000` verbatim, so sixty bytes
3388/// of text were enough to abort a safe `open`. Every caller now answers
3389/// a `None` with a named error instead.
3390fn rational_to_timebase(value: Rational) -> Option<Timebase> {
3391  Timebase::try_new(value.numerator(), NonZeroI32::new(value.denominator())?)
3392}
3393
3394/// [`rational_to_timebase`], and the numerator must be positive too.
3395///
3396/// The rule for a seat where a zero numerator cannot mean "absent".
3397///
3398/// A chapter's `time_base` is one such seat: `avpriv_new_chapter` takes
3399/// it as an argument, so whatever wrote the chapter wrote its ruler
3400/// too, and `0/den` there is not an unset default but a declaration
3401/// that every boundary in the table is the same instant — which is the
3402/// whole content of the row, malformed. A declared frame *rate* is the
3403/// other: zero frames per second is not a rate.
3404fn positive_rational_to_timebase(value: Rational) -> Option<Timebase> {
3405  (value.numerator() > 0)
3406    .then(|| rational_to_timebase(value))
3407    .flatten()
3408}
3409
3410/// A frame *rate* as a rate-shaped [`Timebase`] (`30000/1001` for
3411/// 29.97 fps), or `None` when the container declares none.
3412fn rate_to_timebase(value: Rational) -> Option<Timebase> {
3413  positive_rational_to_timebase(value)
3414}
3415
3416#[cfg(test)]
3417mod tests {
3418  use ffmpeg_next::ffi::{av_dict_free, av_dict_set};
3419
3420  use ffmpeg_next::codec::Parameters;
3421
3422  use super::*;
3423  use crate::extras::TrackExtra;
3424
3425  /// Builds a dictionary holding one entry whose *value* is the given
3426  /// raw bytes. The bytes go in as a C string, which is all
3427  /// `av_dict_set` promises to copy — FFmpeg never asks whether they
3428  /// are UTF-8, which is the whole point of the lane below.
3429  fn dict_with(key: &CStr, value: &[u8]) -> *mut AVDictionary {
3430    let mut dict: *mut AVDictionary = std::ptr::null_mut();
3431    let mut terminated = value.to_vec();
3432    terminated.push(0);
3433    let rc = unsafe {
3434      av_dict_set(
3435        &mut dict,
3436        key.as_ptr(),
3437        terminated.as_ptr().cast::<std::ffi::c_char>(),
3438        0,
3439      )
3440    };
3441    assert!(rc >= 0, "av_dict_set failed: {rc}");
3442    dict
3443  }
3444
3445  #[test]
3446  fn metadata_that_is_not_utf8_is_read_lossily_not_unsoundly() {
3447    // The bytes a real container can hold: a Latin-1 "café.ttf" whose
3448    // 0xE9 is not valid UTF-8 on its own. Read through
3449    // `DictionaryRef::get` this produced a `&str` that violates the
3450    // type's invariant — undefined behaviour before anything ever
3451    // copied it.
3452    let raw = b"caf\xE9.ttf".to_vec();
3453    assert!(
3454      std::str::from_utf8(&raw).is_err(),
3455      "the source bytes really are not UTF-8",
3456    );
3457    let dict = dict_with(c"filename", &raw);
3458    let mut spent = 0usize;
3459    let text = retain_metadata(
3460      unsafe { metadata_value(dict, c"filename") },
3461      &mut spent,
3462      usize::MAX,
3463    )
3464    .expect("a readable value")
3465    .expect("the entry exists");
3466    assert_eq!(text.as_str(), "caf\u{FFFD}.ttf");
3467    assert_eq!(
3468      spent,
3469      "caf\u{FFFD}.ttf".len(),
3470      "the charge is the decoded size, which the replacement made longer than the raw bytes",
3471    );
3472    // A key the dictionary does not hold, and a null dictionary, are
3473    // both simply absent.
3474    assert!(matches!(
3475      unsafe { metadata_value(dict, c"mimetype") },
3476      MetadataValue::Absent,
3477    ));
3478    assert!(matches!(
3479      unsafe { metadata_value(std::ptr::null(), c"filename") },
3480      MetadataValue::Absent,
3481    ));
3482    unsafe { av_dict_free(&mut { dict }) };
3483  }
3484
3485  #[test]
3486  fn valid_metadata_survives_unchanged() {
3487    let dict = dict_with(c"mimetype", b"application/x-truetype-font");
3488    let mut spent = 0usize;
3489    assert_eq!(
3490      retain_metadata(
3491        unsafe { metadata_value(dict, c"mimetype") },
3492        &mut spent,
3493        usize::MAX,
3494      )
3495      .expect("a readable value")
3496      .as_deref(),
3497      Some("application/x-truetype-font"),
3498    );
3499    unsafe { av_dict_free(&mut { dict }) };
3500  }
3501
3502  #[test]
3503  fn an_unterminated_length_is_refused_rather_than_truncated() {
3504    // Nothing libavutil produces is this long; the cap exists so a
3505    // value it did not terminate cannot walk off the end. A value that
3506    // reaches the cap is refused — and, since this shape was fixed,
3507    // refused *visibly*: it is no longer the same answer as absent.
3508    let long = vec![b'a'; METADATA_VALUE_MAX_BYTES + 1];
3509    let dict = dict_with(c"filename", &long);
3510    assert!(matches!(
3511      unsafe { metadata_value(dict, c"filename") },
3512      MetadataValue::NotTerminated,
3513    ));
3514    unsafe { av_dict_free(&mut { dict }) };
3515  }
3516
3517  /// A reader that panics with a payload whose destructor panics in
3518  /// turn. Both panics are safe code; the second one is what used to
3519  /// leave the guard and enter the `extern "C"` AVIO callback.
3520  struct PanicsWithAHostilePayload;
3521
3522  struct PanicOnDrop;
3523
3524  impl Drop for PanicOnDrop {
3525    fn drop(&mut self) {
3526      panic!("and the payload went too");
3527    }
3528  }
3529
3530  impl std::io::Read for PanicsWithAHostilePayload {
3531    fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
3532      std::panic::panic_any(PanicOnDrop);
3533    }
3534  }
3535
3536  impl std::io::Seek for PanicsWithAHostilePayload {
3537    fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
3538      std::panic::panic_any(PanicOnDrop);
3539    }
3540  }
3541
3542  #[test]
3543  fn a_reader_panic_with_a_hostile_payload_does_not_abort_the_process() {
3544    // In its own process, because the assertion *is* the process: a
3545    // parent that sees the child exit cleanly has seen the abort not
3546    // happen. The guard caught the reader's panic and then dropped its
3547    // payload outside `catch_unwind`, so a payload whose `Drop` panics
3548    // sent that second panic straight out of `read` and into C —
3549    // through the very guard that exists to stop it.
3550    crate::fault_subprocess::in_subprocess(
3551      "demuxer::tests::a_reader_panic_with_a_hostile_payload_does_not_abort_the_process",
3552      || {
3553        let previous = std::panic::take_hook();
3554        std::panic::set_hook(Box::new(|_| {}));
3555        let opened =
3556          CarrierDemuxer::<crate::Owned>::open_reader(PanicsWithAHostilePayload, Some("x.mkv"));
3557        std::panic::set_hook(previous);
3558        match opened {
3559          Err(DemuxError::ReaderPanic(_)) => {}
3560          Err(other) => panic!("expected ReaderPanic, got {other:?}"),
3561          Ok(_) => panic!("a reader that only panics cannot open a container"),
3562        }
3563      },
3564    );
3565  }
3566
3567  #[test]
3568  fn codec_parameters_that_cannot_be_allocated_are_named() {
3569    // `Parameters::new` does not check `avcodec_parameters_alloc`, and
3570    // `clone_from` dereferences the result immediately: under a failed
3571    // allocation the shipped clone would write through null.
3572    crate::fault_subprocess::in_subprocess(
3573      "demuxer::tests::codec_parameters_that_cannot_be_allocated_are_named",
3574      || {
3575        let source = Parameters::new();
3576        assert!(
3577          !unsafe { source.as_ptr() }.is_null(),
3578          "the source allocates before the cap goes on",
3579        );
3580        crate::fault_subprocess::cap_ffmpeg_allocations(1);
3581        let refused = crate::extras::bounded_clone_parameters(&source, 4, usize::MAX);
3582        crate::fault_subprocess::uncap_ffmpeg_allocations();
3583        assert!(
3584          matches!(
3585            refused,
3586            Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 4
3587          ),
3588          "expected ParametersAlloc, got {:?}",
3589          refused.map(|_| ()),
3590        );
3591        // And with the cap lifted the same copy succeeds, so the
3592        // refusal was the allocator's answer and not a broken helper.
3593        crate::extras::bounded_clone_parameters(&source, 4, usize::MAX).expect("an uncapped copy");
3594      },
3595    );
3596  }
3597
3598  #[test]
3599  fn the_public_track_extra_handoffs_still_answer_the_allocator() {
3600    // The lane this replaces guarded a hazard that no longer exists:
3601    // `TrackExtra` derived `Clone` over `ffmpeg_next`'s `Parameters`,
3602    // whose clone dereferences an unchecked allocation, so safe public
3603    // code reached a SIGSEGV by copying a track row. The row holds no
3604    // `Parameters` at all now, and the two public handoffs have split
3605    // in kind because of it:
3606    //
3607    // * `Clone` allocates **nothing from FFmpeg** — it copies an
3608    //   owned mirror, which is a `Vec` spine and a refcount bump — so
3609    //   it survives a capped allocator rather than reporting through
3610    //   one. That is what makes the derive honest under the carrier
3611    //   law, and a capped allocator is the only way to pin it.
3612    // * `clone_parameters` is the rebuild, and it is where FFmpeg
3613    //   allocation moved to. It still answers.
3614    crate::fault_subprocess::in_subprocess(
3615      "demuxer::tests::the_public_track_extra_handoffs_still_answer_the_allocator",
3616      || {
3617        let source = Parameters::new();
3618        assert!(!unsafe { source.as_ptr() }.is_null(), "allocated uncapped");
3619        let extra = TrackExtra::new(
3620          6,
3621          crate::ticket::CodecTicket::mirror(&source, 6, usize::MAX).expect("uncapped"),
3622        );
3623
3624        crate::fault_subprocess::cap_ffmpeg_allocations(1);
3625        let cloned = extra.clone();
3626        let handed = extra.clone_parameters().map(|_| ());
3627        crate::fault_subprocess::uncap_ffmpeg_allocations();
3628
3629        assert_eq!(
3630          cloned.parameter_bytes(),
3631          extra.parameter_bytes(),
3632          "the row cloned under an allocator that refuses everything",
3633        );
3634        assert!(
3635          matches!(handed, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
3636          "TrackExtra::clone_parameters: {handed:?}",
3637        );
3638
3639        // And the rebuild works once the allocator does.
3640        extra.clone_parameters().expect("an uncapped handoff");
3641      },
3642    );
3643  }
3644
3645  #[test]
3646  fn parameters_that_never_allocated_are_refused_at_the_door() {
3647    // The route the destination check could not see. A safe
3648    // `Parameters::new()` under a failed allocation hands back a
3649    // null-backed value and says nothing; the copier then allocated its
3650    // own destination happily — the allocator having recovered by
3651    // then — and called `avcodec_parameters_copy(out, NULL)`, which
3652    // dereferences its source. Same crash, one recovery later, still
3653    // from safe public code.
3654    crate::fault_subprocess::in_subprocess(
3655      "demuxer::tests::parameters_that_never_allocated_are_refused_at_the_door",
3656      || {
3657        // The cap is on *while the source is built* — that is the whole
3658        // difference from the destination lane.
3659        crate::fault_subprocess::cap_ffmpeg_allocations(1);
3660        let never_allocated = Parameters::new();
3661        crate::fault_subprocess::uncap_ffmpeg_allocations();
3662        assert!(
3663          unsafe { never_allocated.as_ptr() }.is_null(),
3664          "the safe constructor really does hand back a null-backed value",
3665        );
3666
3667        // The door moved with the handle. `TrackExtra::new` no longer
3668        // takes a `Parameters` at all, so the only way a null-backed
3669        // one reaches a track row is through the mirror — which is
3670        // where the check now lives, and where it belongs: beside the
3671        // raw pointer rather than one type downstream of it.
3672        let refused = crate::ticket::CodecTicket::mirror(&never_allocated, 9, usize::MAX);
3673        let Err(DemuxError::ParametersMissing(p)) = refused.map(|_| ()) else {
3674          panic!("a null-backed source must not become a codec ticket");
3675        };
3676        assert_eq!(p.stream_index(), 9);
3677
3678        // And the copier refuses it too, so the invariant is not the
3679        // only thing standing between this and a null dereference.
3680        let never_allocated = {
3681          crate::fault_subprocess::cap_ffmpeg_allocations(1);
3682          let p = Parameters::new();
3683          crate::fault_subprocess::uncap_ffmpeg_allocations();
3684          p
3685        };
3686        assert!(matches!(
3687          crate::extras::bounded_clone_parameters(&never_allocated, 9, usize::MAX).map(|_| ()),
3688          Err(DemuxError::ParametersMissing(p)) if p.stream_index() == 9,
3689        ));
3690
3691        // A row built over real parameters still hands off both ways,
3692        // so the refusal is about the null and nothing else.
3693        let real = Parameters::new();
3694        let extra = TrackExtra::new(
3695          9,
3696          crate::ticket::CodecTicket::mirror(&real, 9, usize::MAX).expect("real parameters"),
3697        );
3698        let _ = extra.clone();
3699        extra.clone_parameters().expect("handoff");
3700      },
3701    );
3702  }
3703
3704  #[cfg(feature = "resample")]
3705  #[test]
3706  fn a_spec_read_from_parameters_that_never_allocated_is_absent() {
3707    // The same trap at another public door, found by the sweep:
3708    // `ResampleSpec::from_parameters` asks `parameters.medium()`
3709    // first, and *that* dereferences the pointer inside ffmpeg-next
3710    // before any code of ours runs.
3711    crate::fault_subprocess::in_subprocess(
3712      "demuxer::tests::a_spec_read_from_parameters_that_never_allocated_is_absent",
3713      || {
3714        crate::fault_subprocess::cap_ffmpeg_allocations(1);
3715        let never_allocated = Parameters::new();
3716        crate::fault_subprocess::uncap_ffmpeg_allocations();
3717        assert!(unsafe { never_allocated.as_ptr() }.is_null());
3718        assert_eq!(
3719          crate::ResampleSpec::from_parameters(&never_allocated),
3720          None,
3721          "parameters that do not exist describe no audio",
3722        );
3723      },
3724    );
3725  }
3726
3727  #[test]
3728  fn codec_parameters_whose_copy_fails_are_named() {
3729    // The other leg: the destination allocates, and the deep copy of
3730    // the extradata does not. `clone_from` discards that return value,
3731    // so the shipped clone handed back parameters missing the very
3732    // bytes a decoder needs to open — and said nothing.
3733    crate::fault_subprocess::in_subprocess(
3734      "demuxer::tests::codec_parameters_whose_copy_fails_are_named",
3735      || {
3736        const EXTRADATA: usize = 8 * 1024 * 1024;
3737        let mut source = Parameters::new();
3738        // SAFETY: `source` owns a live `AVCodecParameters`; the buffer
3739        // comes from FFmpeg's allocator and is handed to it, so
3740        // `avcodec_parameters_free` releases it with the rest.
3741        unsafe {
3742          let par = source.as_mut_ptr();
3743          let extradata = ffmpeg_next::ffi::av_mallocz(EXTRADATA) as *mut u8;
3744          assert!(!extradata.is_null(), "av_mallocz");
3745          (*par).extradata = extradata;
3746          (*par).extradata_size = EXTRADATA as i32;
3747        }
3748
3749        // Big enough for the destination `AVCodecParameters`, far too
3750        // small for its extradata.
3751        crate::fault_subprocess::cap_ffmpeg_allocations(64 * 1024);
3752        let refused = crate::extras::bounded_clone_parameters(&source, 2, usize::MAX);
3753        crate::fault_subprocess::uncap_ffmpeg_allocations();
3754        match refused {
3755          Err(DemuxError::ParametersCopy(p)) => assert_eq!(p.stream_index(), 2),
3756          Err(other) => panic!("expected ParametersCopy, got {other:?}"),
3757          Ok(_) => panic!("a copy that could not copy the extradata must not succeed"),
3758        }
3759        crate::extras::bounded_clone_parameters(&source, 2, usize::MAX).expect("an uncapped copy");
3760      },
3761    );
3762  }
3763
3764  /// A stream whose `attached_pic` is `parked`, and the packet
3765  /// libavformat would queue for it.
3766  ///
3767  /// # On the fixture road
3768  ///
3769  /// The container shape this guards — a stream carrying
3770  /// `ATTACHED_PIC | TIMED_THUMBNAILS` — **cannot be minted by the
3771  /// ffmpeg CLI**, and that was censused rather than assumed: no muxer
3772  /// has a field for those bits (`-disposition:v
3773  /// attached_pic+timed_thumbnails` round-trips to nothing through
3774  /// mp4, mov and matroska alike), because the mov *demuxer* derives
3775  /// them from a chapter-track reference its own muxer does not write
3776  /// in that direction.
3777  ///
3778  /// What is reproducible, and what actually matters, is the **packet
3779  /// shape**: `read_frame_internal` queues a stream's parked picture
3780  /// with `av_packet_ref` while keeping its own reference, which is
3781  /// exactly what `av_packet_ref` builds here. The classification half
3782  /// — that such a stream is video rather than an attachment — is
3783  /// pinned separately by
3784  /// [`a_timed_thumbnail_stream_is_not_an_attachment`].
3785  fn parked_picture_stream(parked: &Packet) -> (Box<AVStream>, Packet) {
3786    use ffmpeg_next::packet::{Mut, Ref};
3787
3788    let mut stream: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
3789    let mut queued = Packet::empty();
3790    // SAFETY: `parked` is a live refcounted packet; `av_packet_ref`
3791    // takes a reference to its buffer, which is precisely what
3792    // libavformat does when it queues an attached picture. The stream
3793    // is zeroed apart from the one field the probe reads.
3794    unsafe {
3795      assert_eq!(
3796        ffmpeg_next::ffi::av_packet_ref(queued.as_mut_ptr(), parked.as_ptr()),
3797        0,
3798      );
3799      stream.attached_pic.buf = (*parked.as_ptr()).buf;
3800      stream.attached_pic.data = (*parked.as_ptr()).data;
3801      stream.attached_pic.size = (*parked.as_ptr()).size;
3802    }
3803    (stream, queued)
3804  }
3805
3806  #[test]
3807  fn a_queued_attached_picture_is_recognised() {
3808    use ffmpeg_next::packet::Ref;
3809
3810    let parked = Packet::copy(&[9u8; 2048]);
3811    let (stream, queued) = parked_picture_stream(&parked);
3812
3813    // The two references are different structs around one allocation —
3814    // which is the whole reason the probe compares `buffer` and not the
3815    // `AVBufferRef`. Asserting the difference is what makes this a test
3816    // of the right comparison rather than of a lucky one.
3817    // SAFETY: both packets are live.
3818    unsafe {
3819      assert_ne!(
3820        (*queued.as_ptr()).buf,
3821        (*parked.as_ptr()).buf,
3822        "av_packet_ref must mint a new reference struct",
3823      );
3824    }
3825    // SAFETY: the stream is a zeroed `AVStream` whose only populated
3826    // fields are the ones the probe reads, and `queued` is live.
3827    assert!(unsafe { packet_is_parked_picture(&*stream, &queued) });
3828
3829    // An ordinary timed packet — the shape every pull after the first
3830    // one has — is not the parked picture.
3831    let ordinary = Packet::copy(&[1u8; 2048]);
3832    // SAFETY: as above.
3833    assert!(!unsafe { packet_is_parked_picture(&*stream, &ordinary) });
3834
3835    // And a stream that parks nothing recognises nothing.
3836    let bare: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
3837    // SAFETY: as above.
3838    assert!(!unsafe { packet_is_parked_picture(&*bare, &queued) });
3839  }
3840
3841  #[test]
3842  fn the_queued_picture_is_admitted_and_later_packets_take_the_ordinary_road() {
3843    use crate::buffer::{PacketBufferError, PayloadProvenance, payload_of};
3844    use ffmpeg_next::packet::Ref;
3845
3846    let parked = Packet::copy(&[9u8; 2048]);
3847    let (_stream, queued) = parked_picture_stream(&parked);
3848    // SAFETY: the packet is live; `buf` is a public field.
3849    let parked_buffer = unsafe { (*parked.as_ptr()).buf };
3850
3851    // **The first pull.** Two references, one of them the container's.
3852    // From a *caller* that shape is refused, because a caller's second
3853    // reference may be a `Packet` with a safe `data_mut`.
3854    // SAFETY: `queued` is live for every call in this test.
3855    assert!(matches!(
3856      unsafe {
3857        payload_of::<crate::View>(
3858          queued.as_ptr(),
3859          usize::MAX,
3860          PayloadProvenance::CallerSupplied,
3861        )
3862      },
3863      Err(PacketBufferError::SharedPayload(_)),
3864    ));
3865
3866    // Delivered by the demux loop, the same shape is carried — by copy,
3867    // because a window would outlive the exclusivity the read rests on.
3868    // SAFETY: as above.
3869    let copied = unsafe {
3870      payload_of::<crate::View>(
3871        queued.as_ptr(),
3872        usize::MAX,
3873        PayloadProvenance::DemuxDelivered,
3874      )
3875    }
3876    .expect("a demux-delivered shared payload is carriable")
3877    .expect("it has a payload");
3878    assert_eq!(copied.as_ref(), &[9u8; 2048][..]);
3879    // SAFETY: the packet is live; `data` is a public field.
3880    unsafe {
3881      assert_ne!(
3882        copied.as_ref().as_ptr() as usize,
3883        (*queued.as_ptr()).data as usize,
3884        "a shared demux-delivered payload is copied, not windowed",
3885      );
3886    }
3887
3888    // With the provenance the probe establishes, both lanes carry it.
3889    // SAFETY: as above.
3890    let viewed = unsafe {
3891      payload_of::<crate::View>(
3892        queued.as_ptr(),
3893        usize::MAX,
3894        PayloadProvenance::AttachedPicture,
3895      )
3896    }
3897    .expect("the container's own picture is carriable")
3898    .expect("it has a payload");
3899    assert_eq!(viewed.as_ref(), &[9u8; 2048][..]);
3900    // And on the view lane it is a window into the parked allocation
3901    // rather than a copy of it.
3902    // SAFETY: both are live; `data`/`size` are public fields.
3903    unsafe {
3904      let start = (*parked_buffer).data as usize;
3905      let end = start + (*parked_buffer).size;
3906      let at = viewed.as_ref().as_ptr() as usize;
3907      assert!(
3908        at >= start && at + viewed.len() <= end,
3909        "the queued picture must be viewed, not copied",
3910      );
3911    }
3912    // SAFETY: as above.
3913    let owned = unsafe {
3914      payload_of::<crate::Owned>(
3915        queued.as_ptr(),
3916        usize::MAX,
3917        PayloadProvenance::AttachedPicture,
3918      )
3919    }
3920    .expect("the owned lane carries it too")
3921    .expect("it has a payload");
3922    assert_eq!(owned.as_ref(), &[9u8; 2048][..]);
3923
3924    // **Every pull after it.** A timed packet has a buffer of its own,
3925    // so it stays on the `Delivered` road, is unique, and the view lane
3926    // shares it.
3927    let later = Packet::copy(&[4u8; 1024]);
3928    // SAFETY: `later` is live.
3929    let shared = unsafe {
3930      payload_of::<crate::View>(
3931        later.as_ptr(),
3932        usize::MAX,
3933        PayloadProvenance::DemuxDelivered,
3934      )
3935    }
3936    .expect("an ordinary packet is carriable")
3937    .expect("it has a payload");
3938    // SAFETY: as above.
3939    unsafe {
3940      assert_eq!(
3941        shared.as_ref().as_ptr() as usize,
3942        (*later.as_ptr()).data as usize,
3943        "a uniquely-referenced packet is still shared, not copied",
3944      );
3945    }
3946  }
3947
3948  #[test]
3949  fn a_timed_thumbnail_stream_is_not_an_attachment() {
3950    // `TIMED_THUMBNAILS` is documented as only ever appearing beside
3951    // `ATTACHED_PIC`, so testing the picture bit alone reads a sparse
3952    // chapter-thumbnail track as cover art — and the attachment
3953    // contract then delivers exactly one of its images and drops the
3954    // rest, every one of which had a timestamp.
3955    assert!(
3956      is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC),
3957      "a plain attached picture is still an attachment",
3958    );
3959    assert!(
3960      !is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC | AV_DISPOSITION_TIMED_THUMBNAILS),
3961      "a timed-thumbnail stream is a timed track, whatever else it is flagged",
3962    );
3963    // Neither bit, and the other bits that ride along, change nothing.
3964    assert!(!is_attachment_disposition(0));
3965    assert!(!is_attachment_disposition(AV_DISPOSITION_TIMED_THUMBNAILS));
3966    assert!(is_attachment_disposition(
3967      AV_DISPOSITION_ATTACHED_PIC | ffmpeg_next::ffi::AV_DISPOSITION_DEFAULT
3968    ));
3969    // And the reason the raw bits are read at all: the wrapper's own
3970    // flag set cannot express the distinction.
3971    assert!(
3972      ffmpeg_next::format::stream::Disposition::from_bits(AV_DISPOSITION_TIMED_THUMBNAILS)
3973        .is_none(),
3974      "ffmpeg_next mints no TIMED_THUMBNAILS bit — from_bits_truncate would drop it silently",
3975    );
3976  }
3977
3978  #[test]
3979  fn an_uncapturable_cover_still_gets_its_one_packet() {
3980    // The state the shipped `AwaitingPacket` fallback existed for: a
3981    // stream that declares cover art and parks no payload. The fallback
3982    // waited for a packet that may never come, and let timed packets —
3983    // and seeks — go first, which the face forbids. The track now gets
3984    // its one packet at open like every other attachment track: empty,
3985    // and marked as this layer's own work.
3986    //
3987    // Not reachable from a file: across MP3, M4A, FLAC and Matroska,
3988    // every ATTACHED_PIC stream libavformat produces carries the parked
3989    // packet, because `ff_add_attached_pic` sets the disposition and
3990    // fills it in the same call. A zeroed `AVPacket` is exactly what
3991    // `attached_pic` would hold if one ever did not.
3992    let empty: ffmpeg_next::ffi::AVPacket = unsafe { std::mem::zeroed() };
3993    let packet = unsafe { attached_pic_payload::<crate::Owned>(&empty, 7, DemuxLimits::default()) }
3994      .expect("an unparked cover is a degenerate track, not an unreadable file");
3995    assert!(packet.data().as_ref().is_empty());
3996    assert!(
3997      packet.extra().synthesized(),
3998      "nothing in the container handed this payload over",
3999    );
4000    assert_eq!(packet.extra().stream_index(), 7);
4001  }
4002
4003  /// **`lossy_len` prices exactly what `from_utf8_lossy` builds.**
4004  ///
4005  /// The budget charge is made from this number *before* anything is
4006  /// decoded, so a disagreement would either under-charge the budget —
4007  /// the hostile case, where bytes that are not UTF-8 triple on the way
4008  /// through — or refuse ordinary text. Asserted against the real
4009  /// decoder rather than argued, over the shapes that differ: valid
4010  /// ASCII and multi-byte text, a lone invalid byte, a run of them, an
4011  /// invalid sequence between valid text, and a truncated trailing
4012  /// sequence (which `error_len() == None` reports and which becomes
4013  /// exactly one replacement).
4014  #[test]
4015  fn lossy_len_prices_exactly_what_lossy_text_builds() {
4016    let cases: [&[u8]; 9] = [
4017      b"",
4018      b"Opening",
4019      "héllo wörld".as_bytes(),
4020      b"\xff",
4021      b"\xff\xfe\xfd",
4022      b"before\xffafter",
4023      b"\xe2\x82",           // truncated three-byte sequence
4024      b"ok\xe2\x82",         // ... after valid text
4025      b"\xf0\x9f\x92\xa9ok", // a real four-byte sequence, untouched
4026    ];
4027    for raw in cases {
4028      let built = std::string::String::from_utf8_lossy(raw);
4029      assert_eq!(
4030        lossy_len(raw),
4031        built.len(),
4032        "{raw:?} must be priced at what from_utf8_lossy produces",
4033      );
4034      let text = lossy_text(raw, lossy_len(raw)).expect("a small reservation");
4035      assert_eq!(
4036        text.as_str(),
4037        built.as_ref(),
4038        "{raw:?} must decode identically"
4039      );
4040    }
4041  }
4042
4043  /// **Measure, charge, materialise — and a value the budget refuses is
4044  /// never materialised at all.**
4045  ///
4046  /// The ordering is a property of the types rather than of the
4047  /// control flow, which is what makes it hold: [`metadata_value`]
4048  /// hands back a **borrow** of libavutil's buffer, so the size is
4049  /// known before any owning conversion exists, and the refusal below
4050  /// happens with nothing on the heap. A zero budget therefore costs
4051  /// nothing however long the value is.
4052  #[test]
4053  fn a_refused_metadata_value_is_never_materialised() {
4054    let long = vec![b'x'; 4096];
4055    let mut spent = 0usize;
4056    match retain_metadata(MetadataValue::Present(&long), &mut spent, 0) {
4057      Err(MetadataFault::BudgetExhausted(total)) => assert_eq!(total, 4096),
4058      _ => panic!("a zero budget must refuse a 4096-byte value"),
4059    }
4060    assert_eq!(spent, 0, "a refused value does not advance the budget");
4061
4062    // Under a budget that admits it, the same value is retained and
4063    // charged its decoded size — once.
4064    let mut spent = 0usize;
4065    let text = retain_metadata(MetadataValue::Present(&long), &mut spent, 8192)
4066      .expect("admitted")
4067      .expect("present");
4068    assert_eq!(text.len(), 4096);
4069    assert_eq!(spent, 4096);
4070
4071    // And the three outcomes stay apart.
4072    let mut spent = 0usize;
4073    assert!(
4074      retain_metadata(MetadataValue::Absent, &mut spent, 0)
4075        .expect("absent is not a fault")
4076        .is_none(),
4077    );
4078    assert!(matches!(
4079      retain_metadata(MetadataValue::NotTerminated, &mut spent, usize::MAX),
4080      Err(MetadataFault::TooLong),
4081    ));
4082    assert_eq!(spent, 0);
4083  }
4084
4085  /// **A value with no terminator is not an absent value.**
4086  ///
4087  /// The shape that used to erase it: the reader answered `None` for
4088  /// both, so a declared title of exactly 65,536 bytes reached a
4089  /// consumer as an untitled chapter, uncharged against any budget.
4090  #[test]
4091  fn the_unterminated_case_is_distinct_from_the_absent_one() {
4092    let mut spent = 0usize;
4093    assert!(matches!(
4094      retain_metadata(MetadataValue::NotTerminated, &mut spent, usize::MAX),
4095      Err(MetadataFault::TooLong),
4096    ));
4097    assert!(matches!(
4098      retain_metadata(MetadataValue::Absent, &mut spent, usize::MAX),
4099      Ok(None),
4100    ));
4101  }
4102
4103  /// **A malformed rational is refused, never clamped and never a
4104  /// panic.**
4105  ///
4106  /// This replaces a lane that asserted the opposite — that `1/0` came
4107  /// back as `1/1`. That clamp was a fabrication a consumer could not
4108  /// tell from a declaration, and it did not cover the case that
4109  /// actually bites: `Timebase::new` asserts a non-negative numerator,
4110  /// so a negative one panicked a safe `open`. libavformat stores
4111  /// `TIMEBASE=-1/1000` out of an FFMETADATA sidecar verbatim, which
4112  /// made sixty bytes of text enough to abort the process.
4113  #[test]
4114  fn a_malformed_rational_is_refused_rather_than_clamped() {
4115    for (num, den) in [(1, 0), (1, -1000), (-1, 1000), (-1, -1000)] {
4116      assert_eq!(
4117        rational_to_timebase(Rational::new(num, den)),
4118        None,
4119        "{num}/{den} is not a timebase, and inventing one for it would be indistinguishable \
4120         downstream from a file that declared it",
4121      );
4122    }
4123  }
4124
4125  /// **One rule for a stream's ruler, and both passes hold it.**
4126  ///
4127  /// The refusal used to live inside `build_tracks`' materialisation
4128  /// loop, so a malformed ruler on the *last* stream was decided only
4129  /// after every earlier stream's codec ticket, metadata and attachment
4130  /// carrier had been paid for. It is decided in `admit_streams` now,
4131  /// where nothing has been allocated — and by *this* function, which
4132  /// is the only place the conversion and the error are written, so the
4133  /// pass that pays cannot refuse something the pass that judges
4134  /// admitted.
4135  ///
4136  /// **On reachability, stated rather than implied.** Unlike
4137  /// `AVChapter.time_base` — which libavformat stores exactly as an
4138  /// FFMETADATA sidecar wrote it, `TIMEBASE=-1/1000` included, and
4139  /// which the lane above pins against a real container — a stream's
4140  /// timebase normally arrives through `avpriv_set_pts_info`, which
4141  /// refuses a non-positive value itself. No container was found that
4142  /// reaches this refusal, so it is a defensive one: demuxers that
4143  /// assign `st->time_base` directly are not obliged to go through that
4144  /// helper, and a check whose cost is one comparison is not worth
4145  /// trading for an assumption about every demuxer in libavformat. What
4146  /// this lane can pin is the rule and the report; what the ordering
4147  /// rests on is that the pass it now lives in allocates nothing at
4148  /// all.
4149  #[test]
4150  fn a_stream_ruler_is_refused_by_one_rule_that_names_what_was_declared() {
4151    for (num, den) in [(1, 0), (1, -1000), (-1, 1000), (-1, -1000)] {
4152      match stream_timebase(7, Rational::new(num, den)) {
4153        Err(DemuxError::TrackTimebaseInvalid(fault)) => {
4154          assert_eq!(fault.stream_index(), 7, "the refusal names the stream");
4155          assert_eq!(
4156            (fault.num(), fault.den()),
4157            (num, den),
4158            "{num}/{den} is reported as the container wrote it, not as a repair",
4159          );
4160        }
4161        // Split so nothing here relies on `Timebase` being `Debug`.
4162        Err(other) => panic!("{num}/{den} must be a timebase fault, got {other:?}"),
4163        Ok(_) => panic!("{num}/{den} must be refused, and it was admitted"),
4164      }
4165    }
4166
4167    // And the two shapes a stream may legitimately carry are admitted:
4168    // an ordinary ruler, and libavformat's own "never set".
4169    assert_eq!(
4170      stream_timebase(0, Rational::new(1, 90_000))
4171        .map(|tb| (tb.num(), tb.den().get()))
4172        .ok(),
4173      Some((1, 90_000)),
4174    );
4175    assert_eq!(
4176      stream_timebase(0, Rational::new(0, 1))
4177        .map(|tb| (tb.num(), tb.den().get()))
4178        .ok(),
4179      Some((0, 1)),
4180      "`0/1` is not malformed for a track seat — see the lane below",
4181    );
4182  }
4183
4184  /// A zero numerator is the one rational the two rules disagree on,
4185  /// and the disagreement is the point.
4186  ///
4187  /// `0/1` is libavformat's own default for a stream whose demuxer
4188  /// never set one, so a track seat reads it as the container declaring
4189  /// nothing. A chapter's ruler is written by whatever wrote the
4190  /// chapter, so the same value there is a claim that every boundary in
4191  /// the table falls on one instant — malformed, and refused.
4192  #[test]
4193  fn a_zero_numerator_is_absent_for_a_track_and_malformed_for_a_chapter() {
4194    let zero = Rational::new(0, 1);
4195    assert_eq!(
4196      rational_to_timebase(zero).map(|tb| (tb.num(), tb.den().get())),
4197      Some((0, 1)),
4198      "the track rule passes libavformat's own unset default through",
4199    );
4200    assert_eq!(
4201      positive_rational_to_timebase(zero),
4202      None,
4203      "the strict rule refuses it",
4204    );
4205  }
4206
4207  #[test]
4208  fn a_declared_frame_rate_becomes_a_rate_shaped_timebase() {
4209    let ntsc = rate_to_timebase(Rational::new(30_000, 1001)).expect("declared");
4210    assert_eq!((ntsc.num(), ntsc.den().get()), (30_000, 1001));
4211    assert_eq!(
4212      rate_to_timebase(Rational::new(0, 1)),
4213      None,
4214      "0 fps is absent"
4215    );
4216    assert_eq!(
4217      rate_to_timebase(Rational::new(30, 0)),
4218      None,
4219      "no denominator"
4220    );
4221  }
4222
4223  #[test]
4224  fn the_seek_timebase_is_microseconds() {
4225    // `avformat_seek_file` with `stream_index == -1` takes AV_TIME_BASE
4226    // units; a target expressed in anything else has to arrive there.
4227    let tb = av_time_base_q();
4228    assert_eq!((tb.num(), tb.den().get()), (1, 1_000_000));
4229    let target = Timestamp::new(1_500, Timebase::new(1, NonZeroI32::new(1000).expect("ms")));
4230    assert_eq!(target.rescale_to(tb).pts(), 1_500_000);
4231  }
4232}