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