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//! `Arc<TrackInfo<Ffmpeg>>` handles — see
11//! [`Demuxer::TrackHandle`](mediadecode::demuxer::Demuxer::TrackHandle).
12//!
13//! # What normalization this layer does
14//!
15//! libavformat's track table is not quite the one the demux tier
16//! promises, and the gap is entirely about attachments:
17//!
18//! - **Cover art is an attachment, not video.** A still image in an
19//!   MP3, FLAC or MP4 arrives as a video stream carrying
20//!   `AV_DISPOSITION_ATTACHED_PIC`. This layer maps it to
21//!   [`TrackKind::Attachment`], so the `Video` arm carries true motion
22//!   video and nothing else.
23//! - **A font's bytes are not in the packet stream at all.** An
24//!   `AVMEDIA_TYPE_ATTACHMENT` stream never produces a packet; its
25//!   payload lives in `AVCodecParameters.extradata`. This layer
26//!   synthesizes the packet at open time.
27//! - **Cover art's packet is hoisted.** libavformat parks the real
28//!   packet in `AVStream.attached_pic`; some demuxers also emit it in
29//!   the packet stream, some do not. This layer takes it from
30//!   `attached_pic` at open time and drops the duplicate if it ever
31//!   arrives, so the count is exactly one either way.
32//!
33//! Both kinds are queued at open — every attachment track, without
34//! exception, or the open fails. That is what makes the face's "exactly
35//! one packet, before any timed packet" true *by construction* here:
36//! the queue is complete and drains before the first `av_read_frame`
37//! call ever runs, so no packet on an attachment track can be anything
38//! but a duplicate, and no seek can move a packet that was never on the
39//! timeline.
40//!
41//! # Seeking
42//!
43//! `seek` converts the target to `AV_TIME_BASE` units and calls
44//! `avformat_seek_file` over the window `[i64::MIN, target]`, which is
45//! FFmpeg's backward convention: the landing point is the nearest
46//! keyframe at or before the target, never after. `avformat_seek_file`
47//! flushes libavformat's own buffers; this layer clears the EOF latch
48//! it set itself, and deliberately does **not** touch the attachment
49//! bookkeeping — an attachment already handed out is never handed out
50//! again, and one not yet handed out is still owed.
51
52use std::{
53  collections::VecDeque,
54  ffi::{CStr, c_int},
55  io::{Read, Seek},
56  num::NonZeroI32,
57  path::Path,
58  ptr::{addr_of, read_unaligned},
59  sync::Arc,
60};
61
62use derive_more::{IsVariant, TryUnwrap, Unwrap};
63use ffmpeg_next::{
64  Packet, Rational,
65  ffi::{
66    AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_TIMED_THUMBNAILS, AV_NOPTS_VALUE, AVDictionary,
67    AVStream, av_dict_get,
68  },
69  format::{self, context::Input},
70};
71use mediadecode::{
72  Timebase, Timestamp,
73  demuxer::{
74    AttachmentPacket, AttachmentTrackPacket, AttachmentTrackParams, AudioTrackPacket,
75    AudioTrackParams, DataTrackPacket, DataTrackParams, DemuxedPacket, Demuxer,
76    SubtitleTrackPacket, SubtitleTrackParams, TrackIndex, TrackInfo, TrackKind, TrackParams,
77    UnknownTrackParams, VideoTrackPacket, VideoTrackParams,
78  },
79};
80use smol_str::SmolStr;
81
82use crate::{
83  Ffmpeg, boundary,
84  buffer::PacketBufferError,
85  codec_id::CodecId,
86  extras::{AttachmentPacketExtra, TrackExtra},
87  limits::DemuxLimits,
88  reader_guard::{GuardedReader, PanicLatch},
89  sample_format::SampleFormat,
90};
91
92/// One microsecond — the timebase `avformat_seek_file` expects when no
93/// reference stream is named (`stream_index == -1`).
94fn av_time_base_q() -> Timebase {
95  Timebase::new(1, NonZeroI32::new(1_000_000).expect("1e6 is non-zero"))
96}
97
98/// `mediadecode::demuxer::Demuxer` impl wrapping `ffmpeg::format::context::Input`.
99///
100/// Construction is deliberately not on the trait — see [`Self::open`]
101/// and [`Self::open_reader`].
102pub struct CarrierDemuxer<C: crate::FfmpegCarrier> {
103  input: Input,
104  /// The track table, built once at open and held for the life of the
105  /// session — **this is the table `next_packet` classifies against**,
106  /// so nothing may take it away.
107  ///
108  /// Rows are `Arc`-wrapped at the door rather than by each consumer:
109  /// [`TrackInfo`] is not `Clone` (the message-carrier law), so a
110  /// consumer that needs a row past a borrow of this session needs a
111  /// shared handle, and one allocation per track at open is the whole
112  /// cost of every fan-out afterwards. `Arc` and not `Rc` because
113  /// [`CodecTicket`](crate::ticket::CodecTicket) made these rows
114  /// `Send + Sync` by construction precisely so a track table could
115  /// cross tasks.
116  tracks: Vec<Arc<TrackInfo<Ffmpeg>>>,
117  /// What libavformat decided the bytes are wrapped in, read once at
118  /// open — see [`CarrierDemuxer::format`].
119  ///
120  /// Held rather than re-derived because it is a property of the
121  /// session: `avformat_open_input` picks the demuxer and never changes
122  /// it, so the answer cannot move and a second read could only cost
123  /// more. `None` only where libavformat left `iformat` null or its
124  /// name is not readable text — neither of which a successful open
125  /// produces.
126  format: Option<crate::ContainerFormat>,
127  pending: VecDeque<(
128    TrackIndex,
129    AttachmentPacket<AttachmentPacketExtra, C::Buffer>,
130  )>,
131  /// `true` once this session has answered `Ok(None)`. Only then does
132  /// [`Self::seek`] clear the `AVIOContext`'s EOF latch — clearing it
133  /// unconditionally would also erase a genuine sticky I/O error, which
134  /// `Input::seek` goes out of its way to preserve.
135  eof: bool,
136  /// `true` once this session has reported a packet whose stream the
137  /// track table does not describe.
138  ///
139  /// The diagnostic is **once per session, not once per packet**. A
140  /// format that adds an `AVStream` mid-read (`AVFMTCTX_NOHEADER`:
141  /// MPEG-TS, RTP) then delivers packets on it at the wire's own rate,
142  /// and a line each would be an unbounded log on healthy input — a
143  /// live stream could fill a disk with it. One line names the
144  /// condition; the rest of the session stays quiet. Never cleared,
145  /// including across a seek: it records that this session has said
146  /// its piece, which a seek does not undo.
147  unplaceable_reported: bool,
148  /// Set for a session opened over a caller's reader: where a panic
149  /// raised inside that reader is recorded. `None` for a path-opened
150  /// session, which runs no caller code.
151  reader_panic: Option<Arc<PanicLatch>>,
152  /// The budgets this session spends: on any one timed packet, and —
153  /// already spent, at open — on the file's attachments.
154  limits: DemuxLimits,
155  /// A packet `av_read_frame` has already handed over and whose
156  /// conversion has **not committed**, with the provenance that was
157  /// observed for it.
158  ///
159  /// `av_read_frame` advances the container: once it returns, that
160  /// packet is off the wire and nothing brings it back. A conversion
161  /// that then fails on an *allocation* — a refcount the view lane
162  /// could not take, a copy the middle row could not make — used to
163  /// drop it, leaving a live session that answered the next pull with
164  /// the **following** packet. Compressed data and subtitle cues went
165  /// missing under memory pressure, quietly.
166  ///
167  /// So the read and the conversion are one transaction with a seat
168  /// between them: a transient refusal parks the packet here and the
169  /// next pull re-attempts *this* packet before reading another. It is
170  /// the same park-then-replay the decode household already runs —
171  /// `CarrierVideoStreamDecoder` holds `sw_replay_frames`, and the
172  /// probe holds its rescue history — for the same reason: a byte C
173  /// has already given up is not re-askable.
174  ///
175  /// The provenance is parked **with** the packet rather than re-probed
176  /// on replay. It is an observation about the moment of delivery, and
177  /// a queue that has moved on could answer it differently.
178  unconverted: Option<(Packet, crate::buffer::PayloadProvenance)>,
179}
180
181// The generic bodies. Crate-private, because their bound is: they are
182// the implementation, and the public faces below are written per lane
183// so that no signature a consumer reads names a trait they cannot.
184impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierDemuxer<C> {
185  /// Opens a container from a filesystem path.
186  ///
187  /// Runs `avformat_open_input` followed by
188  /// `avformat_find_stream_info`, then builds the track table and
189  /// captures every attachment payload.
190  ///
191  /// Call [`ffmpeg_next::init`] once before the first open if you want
192  /// FFmpeg's logging and network protocols configured; probing a local
193  /// container does not require it.
194  pub(crate) fn open_impl<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
195    Self::open_with_impl(path, DemuxLimits::default())
196  }
197
198  /// [`Self::open`], with the session's resource budgets named.
199  ///
200  /// The budgets are taken **at open** rather than through a `with_*`
201  /// builder because the attachment half of them is spent here: every
202  /// attachment payload in the file is captured before this call
203  /// returns, which is what makes the demux tier's "exactly one packet,
204  /// before any timed packet" contract true by construction. A budget
205  /// set afterwards would arrive after the spending.
206  ///
207  /// A file whose attachments exceed the budget **fails to open**, with
208  /// [`DemuxError::AttachmentTooLarge`] or
209  /// [`DemuxError::AttachmentBudgetExhausted`] naming the track that
210  /// crossed the line.
211  pub(crate) fn open_with_impl<P: AsRef<Path> + ?Sized>(
212    path: &P,
213    limits: DemuxLimits,
214  ) -> Result<Self, DemuxError> {
215    // **The probe knobs, set before libavformat reads a byte.** See
216    // [`DemuxLimits::max_probe_bytes`]: `avformat_open_input` and
217    // `avformat_find_stream_info` build the attachment, extradata and
218    // coded-side-data buffers themselves, so every budget that measures
219    // *this crate's* copies arrives after the original allocation. The
220    // instrument that reaches behind that is the one bounding what the
221    // parser is handed in the first place.
222    //
223    // On this entrypoint that is `probesize` / `formatprobesize` /
224    // `max_streams` only: the hard byte meter needs an `AVIOContext`
225    // this crate owns, and a path is opened by libavformat's own
226    // protocol layer. The reader entrypoint gets both.
227    Self::from_input(
228      format::input_with_dictionary(path, probe_options(limits))?,
229      limits,
230    )
231  }
232
233  /// Opens a container from any `Read + Seek` byte source, through a
234  /// custom `AVIOContext`.
235  ///
236  /// `Seek` is mandatory and not negotiable: MP4 files routinely put
237  /// `moov` at the end, so a reader that cannot go backwards cannot be
238  /// probed at all — and the seek law on the face would be
239  /// unimplementable.
240  ///
241  /// `filename` is a probe hint, not a path: libavformat uses its
242  /// extension to break ties between formats whose byte signatures are
243  /// ambiguous. Pass `None` when there is nothing to hint with.
244  ///
245  /// # A panicking reader
246  ///
247  /// libavformat drives the reader from `extern "C"` callbacks, where a
248  /// panic would abort the process rather than unwind. Every call into
249  /// `reader` therefore runs under `catch_unwind`: a panic becomes an
250  /// I/O error for libavformat and surfaces here — or from the next
251  /// [`next_packet`](Demuxer::next_packet) / [`seek`](Demuxer::seek) —
252  /// as [`DemuxError::ReaderPanic`], carrying the panic's message. The
253  /// session is terminal from that point: the `AVIOContext`'s error
254  /// state is sticky and the reader's own state is unknown.
255  pub(crate) fn open_reader_impl<R: Read + Seek + Send + 'static>(
256    reader: R,
257    filename: Option<&str>,
258  ) -> Result<Self, DemuxError> {
259    Self::open_reader_with_impl(reader, filename, DemuxLimits::default())
260  }
261
262  /// [`Self::open_reader`], with the session's resource budgets named.
263  /// See [`Self::open_with`] for why they are taken at open.
264  pub(crate) fn open_reader_with_impl<R: Read + Seek + Send + 'static>(
265    reader: R,
266    filename: Option<&str>,
267    limits: DemuxLimits,
268  ) -> Result<Self, DemuxError> {
269    let (guarded, latch, meter) = GuardedReader::new(reader, limits.max_probe_bytes());
270    let io = format::context::StreamIo::from_read_seek(guarded)?;
271    let input =
272      format::input_from_stream(io, filename, Some(probe_options(limits))).map_err(|e| {
273        // Three ways this can fail, and they must not be confused: a
274        // panicked reader, a probe budget reached, or libavformat's own
275        // verdict. The meter is consulted before the errno because
276        // libavformat folds the reader's I/O error into whatever it was
277        // doing at the time — usually "invalid data" — which would
278        // report a refusal this crate made as a malformed file.
279        reader_panic(&latch)
280          .or_else(|| {
281            meter.tripped().then(|| {
282              DemuxError::ProbeBudgetExhausted(ProbeBudgetExhausted::new(
283                meter.read(),
284                meter.budget(),
285              ))
286            })
287          })
288          .unwrap_or(DemuxError::Ffmpeg(e))
289      })?;
290    if meter.tripped() {
291      return Err(DemuxError::ProbeBudgetExhausted(ProbeBudgetExhausted::new(
292        meter.read(),
293        meter.budget(),
294      )));
295    }
296    // Open and analysed: the seat bounds *probing*, and reading the
297    // media itself afterwards is the caller's business, packet by
298    // packet, already bounded by the packet seats.
299    meter.release();
300    // A panic libavformat tolerated (a failed probe it recovered from)
301    // still poisoned the reader; the session must not open over it.
302    if let Some(panicked) = reader_panic(&latch) {
303      return Err(panicked);
304    }
305    let mut demuxer = Self::from_input(input, limits)?;
306    demuxer.reader_panic = Some(latch);
307    Ok(demuxer)
308  }
309
310  /// Borrows the wrapped `ffmpeg::format::context::Input` — for
311  /// `av_dump_format`, container-level metadata, chapters, and anything
312  /// else the portable track table has no seat for.
313  #[cfg_attr(not(tarpaulin), inline(always))]
314  pub(crate) const fn input_impl(&self) -> &Input {
315    &self.input
316  }
317
318  /// The budgets this session was opened with.
319  #[cfg_attr(not(tarpaulin), inline(always))]
320  pub(crate) const fn limits_impl(&self) -> DemuxLimits {
321    self.limits
322  }
323
324  /// What libavformat decided this session's bytes are wrapped in.
325  #[cfg_attr(not(tarpaulin), inline(always))]
326  pub(crate) const fn format_impl(&self) -> Option<&crate::ContainerFormat> {
327    self.format.as_ref()
328  }
329
330  fn from_input(input: Input, limits: DemuxLimits) -> Result<Self, DemuxError> {
331    let (tracks, pending) = build_tracks::<C>(&input, limits)?;
332    // One allocation per track, here and never again: the session
333    // keeps these handles and hands out clones of them.
334    let tracks = tracks.into_iter().map(Arc::new).collect();
335    // SAFETY: `input` owns a live `AVFormatContext` for the whole of
336    // this call, and the read takes copies of the two static-table
337    // strings rather than borrowing from it.
338    let format = unsafe { crate::ContainerFormat::from_context(input.as_ptr()) };
339    Ok(Self {
340      input,
341      tracks,
342      format,
343      pending,
344      unconverted: None,
345      eof: false,
346      unplaceable_reported: false,
347      reader_panic: None,
348      limits,
349    })
350  }
351
352  /// The error a panicked reader owes this session, if one panicked.
353  fn panicked(&self) -> Option<DemuxError> {
354    self.reader_panic.as_deref().and_then(reader_panic)
355  }
356}
357
358/// The libavformat options this crate sets before a container is
359/// opened.
360///
361/// Passed as an `AVDictionary` because that is the only route to these
362/// fields that works for both entrypoints: `avformat_open_input`
363/// applies the dictionary to the context it allocates itself, and the
364/// same names reach the context behind a custom `AVIOContext`.
365///
366/// * `probesize` / `formatprobesize` bound what the format probe and
367///   the stream analysis are allowed to consume;
368/// * `max_streams` bounds the `AVStream` array a header can conjure —
369///   a container claiming a hundred thousand streams is an allocation
370///   this crate's per-track budgets are downstream of.
371fn probe_options(limits: DemuxLimits) -> ffmpeg_next::Dictionary<'static> {
372  let mut options = ffmpeg_next::Dictionary::new();
373  let probe = limits.max_probe_bytes().to_string();
374  options.set("probesize", &probe);
375  options.set("formatprobesize", &probe);
376  options.set("max_streams", &limits.max_streams().to_string());
377  options
378}
379
380/// Payload for [`DemuxError::ProbeBudgetExhausted`].
381///
382/// libavformat wanted more of the file than the probe budget allows.
383///
384/// # What this bounds
385///
386/// This is the only seat in the crate that reaches *behind*
387/// libavformat: `avformat_open_input` and `avformat_find_stream_info`
388/// build the attached picture, the extradata and the coded side data
389/// out of the file themselves, so every budget measuring this crate's
390/// own copies necessarily arrives after those allocations happened.
391///
392/// A parser cannot allocate from bytes it was never handed, so the
393/// input is bounded instead. What is **not** bounded is amplification
394/// inside a parser — a container can describe, in a few bytes, a
395/// structure whose in-memory form is far larger, and nothing outside
396/// libavformat can observe that. Bounding the output of that is the
397/// substrate's own hardening territory; FFmpeg keeps `max_streams`,
398/// `max_index_size` and `max_picture_buffer` for it, and this crate
399/// sets the first.
400#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
401#[error("libavformat read {read} bytes probing the container, over a budget of {budget}")]
402pub struct ProbeBudgetExhausted {
403  read: u64,
404  budget: u64,
405}
406
407impl ProbeBudgetExhausted {
408  /// Constructs a `ProbeBudgetExhausted` payload.
409  #[inline]
410  pub const fn new(read: u64, budget: u64) -> Self {
411    Self { read, budget }
412  }
413  /// Bytes libavformat was handed before the budget was reached.
414  #[inline]
415  pub const fn read(&self) -> u64 {
416    self.read
417  }
418  /// The budget in force.
419  #[inline]
420  pub const fn budget(&self) -> u64 {
421    self.budget
422  }
423}
424
425/// Turns a latched reader panic into the error that names it.
426fn reader_panic(latch: &PanicLatch) -> Option<DemuxError> {
427  latch
428    .message()
429    .map(|message| DemuxError::ReaderPanic(ReaderPanic::new(message)))
430}
431
432impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierDemuxer<C> {
433  pub(crate) fn tracks_impl(&self) -> &[Arc<TrackInfo<Ffmpeg>>] {
434    &self.tracks
435  }
436
437  pub(crate) fn next_packet_impl(
438    &mut self,
439  ) -> Result<Option<DemuxedPacket<Ffmpeg, C::Buffer>>, DemuxError> {
440    // A latched reader panic is terminal, and terminal starts here. The
441    // queue is filled at open and owes nothing to the reader, so a pull
442    // that drained it would answer `Ok` to a caller the session has
443    // already told the truth to — `seek` can latch a panic while
444    // attachments are still queued.
445    if let Some(panicked) = self.panicked() {
446      return Err(panicked);
447    }
448
449    // The attachment queue drains first and drains completely, which is
450    // the whole of "exactly one packet, before any timed packet": no
451    // `av_read_frame` has run yet when the last one leaves.
452    if let Some((track, packet)) = self.pending.pop_front() {
453      return Ok(Some(DemuxedPacket::Attachment(AttachmentTrackPacket::new(
454        track, packet,
455      ))));
456    }
457
458    loop {
459      // **A parked packet is re-attempted before another is read.**
460      // See [`Self::unconverted`]: `av_read_frame` has already given
461      // this one up, so reading past it would lose it.
462      let (packet, parked_provenance) = match self.unconverted.take() {
463        Some((packet, provenance)) => (packet, Some(provenance)),
464        None => {
465          let mut packet = Packet::empty();
466          let read = packet.read(&mut self.input);
467          // A panicking reader reported an ordinary I/O error to C, and
468          // libavformat may answer that with the error, with EOF (a
469          // stream it cannot read looks finished), or with a packet it
470          // had already buffered. None of those are the file's word, so
471          // the latch is consulted whatever the outcome was.
472          if let Some(panicked) = self.panicked() {
473            return Err(panicked);
474          }
475          match read {
476            Ok(()) => {}
477            Err(ffmpeg_next::Error::Eof) => {
478              self.eof = true;
479              return Ok(None);
480            }
481            // A demuxer can resync past a corrupt packet, and
482            // `AVERROR_INVALIDDATA` is not latched into the
483            // `AVIOContext`, so reading again makes progress. Every
484            // other error is sticky and is surfaced.
485            Err(ffmpeg_next::Error::InvalidData) => continue,
486            Err(e) => return Err(DemuxError::Ffmpeg(e)),
487          }
488          (packet, None)
489        }
490      };
491
492      let index = packet.stream();
493      // A packet for a stream the table does not describe cannot be
494      // placed, so it is passed by — the same answer the `Unknown` arm
495      // below gives a track nothing can name.
496      //
497      // **Neither an assertion nor an error.** The arm is reachable on
498      // healthy input: a format flagged `AVFMTCTX_NOHEADER` — MPEG-TS,
499      // RTP and the rest that carry no up-front stream list — may add
500      // an `AVStream` in the middle of `av_read_frame`, and this
501      // session's table was fixed at open, which is the contract
502      // `TrackIndex` needs (position in `tracks()`, dense and stable
503      // for the life of the session). A `debug_assert` would fire on a
504      // transport stream, and an `Err` would end a session over a
505      // stream the caller never asked about.
506      //
507      // It is no longer the *every* packet path. It was, for one
508      // release: the take-the-table door emptied this very `Vec`, so
509      // every index fell out of range at once and a healthy file
510      // demuxed to nothing (issue #51). The table cannot be taken
511      // away any more; what is left here is the genuinely
512      // out-of-range index the arm was written for. Reported rather
513      // than silent, because silence is what made the old failure
514      // invisible — and reported **once**, because the very case that
515      // makes the arm reachable is a live stream that would otherwise
516      // log a line per packet for as long as it runs. See
517      // [`Self::unplaceable_reported`].
518      let Some(info) = self.tracks.get(index) else {
519        if !self.unplaceable_reported {
520          self.unplaceable_reported = true;
521          tracing::debug!(
522            stream = index,
523            tracks = self.tracks.len(),
524            "demux: no track row describes this packet's stream; passing it and any further \
525             such packet by, without repeating this line",
526          );
527        }
528        continue;
529      };
530      let track = TrackIndex::new(index);
531      let time_base = info.timebase();
532
533      // A payload that is there and cannot be referenced is an error,
534      // never a silently dropped packet: `Ok(None)` below means the
535      // packet carried nothing, and that is the only thing that reads
536      // the next one.
537      // **Everything this loop delivers is demux-delivered**, whatever
538      // its refcount: libavformat just handed it over, so any other
539      // reference to its buffer is libavformat's own and no
540      // `ffmpeg_next::Packet` wraps one. That is not the hazard a
541      // caller's second handle is — see
542      // [`crate::buffer::PayloadProvenance`].
543      //
544      // Sharing is ordinary here. A queue-backed demuxer — SubRip,
545      // SubViewer and the rest of the `FFDemuxSubtitlesQueue` family —
546      // keeps its parsed cues and delivers `av_packet_ref`s of them,
547      // so *every* packet it produces arrives with two references.
548      //
549      // The one sub-case that is stronger still is the container's
550      // parked picture, which a stream carrying
551      // `ATTACHED_PIC | TIMED_THUMBNAILS` delivers as its first packet:
552      // written once while the container opened, so the view lane may
553      // window it rather than copy. See [`is_streams_attached_pic`] for
554      // the identity proof.
555      //
556      // SAFETY: both the session's `AVFormatContext` and `packet` are
557      // live here.
558      let provenance = match parked_provenance {
559        // Observed when this packet was delivered, and kept with it.
560        Some(provenance) => provenance,
561        None if unsafe { is_streams_attached_pic(&self.input, index, &packet) } => {
562          crate::buffer::PayloadProvenance::AttachedPicture
563        }
564        None => crate::buffer::PayloadProvenance::DemuxDelivered,
565      };
566
567      // The packet this loop just read is **handed over**, not lent: the
568      // view lane's carrier is a window into its buffer, and a source
569      // that survived the conversion would be a mutable alias of it.
570      // Exactly one arm runs, so exactly one move happens.
571      // **The conversion borrows what this session owns.** The packet
572      // stays in hand until the carrier exists, which is what lets a
573      // failure park it instead of dropping it; on success it falls out
574      // of scope at the end of the iteration and the carrier keeps its
575      // buffer alive by refcount, exactly as when the conversion
576      // consumed it. Nothing outside this loop ever sees the packet, so
577      // the borrow cannot become the aliasing shape the public faces
578      // refuse.
579      let converted = match info.kind() {
580        TrackKind::Video => boundary::video_packet_from_borrowed::<C>(
581          &packet,
582          time_base,
583          self.limits.packet(),
584          provenance,
585        )
586        .map(|built| built.map(|p| DemuxedPacket::Video(VideoTrackPacket::new(track, p)))),
587        TrackKind::Audio => boundary::audio_packet_from_borrowed::<C>(
588          &packet,
589          time_base,
590          self.limits.packet(),
591          provenance,
592        )
593        .map(|built| built.map(|p| DemuxedPacket::Audio(AudioTrackPacket::new(track, p)))),
594        TrackKind::Subtitle => boundary::subtitle_packet_from_borrowed::<C>(
595          &packet,
596          time_base,
597          self.limits.packet(),
598          provenance,
599        )
600        .map(|built| built.map(|p| DemuxedPacket::Subtitle(SubtitleTrackPacket::new(track, p)))),
601        TrackKind::Data => boundary::data_packet_from_borrowed::<C>(
602          &packet,
603          time_base,
604          self.limits.packet(),
605          provenance,
606        )
607        .map(|built| built.map(|p| DemuxedPacket::Data(DataTrackPacket::new(track, p)))),
608        // Every attachment track's one packet was queued at open time,
609        // so anything arriving on one now is the duplicate some
610        // demuxers emit for cover art. Drop it — the contract is
611        // exactly one, and the one has already left. Nothing is
612        // converted here, so there is nothing to park.
613        TrackKind::Attachment => continue,
614        // The roster of arms is five; a track nothing can name has no
615        // arm and its packets are not delivered.
616        TrackKind::Unknown => continue,
617      };
618
619      let built = match converted {
620        Ok(built) => built,
621        Err(source) => {
622          // **Park a refusal that another attempt could survive.** An
623          // allocation that failed says nothing about the packet, and
624          // the packet is off the wire either way. Anything else is a
625          // fact about the packet itself — a malformed one is not made
626          // well-formed by retrying, and parking it would answer every
627          // later pull with the same error instead of letting the
628          // session make progress.
629          if source.parks_in_demux() {
630            self.unconverted = Some((packet, provenance));
631          }
632          return Err(DemuxError::PacketBuffer(PacketBuffer::new(index, source)));
633        }
634      };
635
636      // `None` here means the packet carried no payload — an empty
637      // packet, which some demuxers emit as a marker. Nothing to
638      // deliver; read the next one.
639      if let Some(out) = built {
640        return Ok(Some(out));
641      }
642    }
643  }
644
645  pub(crate) fn seek_impl(&mut self, target: Timestamp) -> Result<(), DemuxError> {
646    let ts = target.rescale_to(av_time_base_q()).pts();
647    // Only our own EOF latch is cleared, and only before the seek —
648    // the seek machinery gates on `eof_reached`, so clearing it
649    // afterwards would be too late.
650    if self.eof {
651      self.input.clear_eof();
652      self.eof = false;
653    }
654    // `..ts` is how ffmpeg-next spells the seek window: it reads only
655    // the endpoint, and `avformat_seek_file`'s `max_ts` is inclusive,
656    // so the window is `[i64::MIN, ts]`. FFmpeg picks the closest seek
657    // point inside it — the nearest keyframe at or before the target.
658    // Never after: a decoder started past the target has no reference
659    // frame.
660    let sought = self.input.seek(ts, ..ts);
661    if let Some(panicked) = self.panicked() {
662      return Err(panicked);
663    }
664    sought?;
665    // **The seat is cleared by a seek that happened, not by one that
666    // was attempted.** A parked packet belongs to the position the
667    // session is leaving, so a successful seek discards it. A *failed*
668    // one leaves the session where it was — and that packet is off the
669    // wire, so dropping it here would be the same silent loss the seat
670    // exists to prevent, with no re-read able to recover it.
671    //
672    // FFmpeg does not specify where a container sits after a seek that
673    // returned an error, and this crate does not guess: it keeps a
674    // packet the container really did deliver, and a caller who saw the
675    // seek fail already knows the position is not the one they asked
676    // for. Every timestamp needed to tell is on the packet.
677    self.unconverted = None;
678    Ok(())
679  }
680}
681
682macro_rules! demuxer_lane_face {
683  ($($lane:ty),+ $(,)?) => { $(
684    impl CarrierDemuxer<$lane> {
685      /// Opens a container from a filesystem path.
686      ///
687      /// Runs `avformat_open_input` followed by
688      /// `avformat_find_stream_info`, then builds the track table and
689      /// captures every attachment payload.
690      ///
691      /// Call [`ffmpeg_next::init`] once before the first open if you
692      /// want FFmpeg's logging and network protocols configured;
693      /// probing a local container does not require it.
694      pub fn open<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
695        Self::open_impl(path)
696      }
697
698      /// [`Self::open`], with the session's resource budgets named.
699      ///
700      /// The budgets are taken **at open** rather than through a
701      /// `with_*` builder because the attachment half of them is spent
702      /// here: every attachment payload is captured during this call.
703      pub fn open_with<P: AsRef<Path> + ?Sized>(
704        path: &P,
705        limits: DemuxLimits,
706      ) -> Result<Self, DemuxError> {
707        Self::open_with_impl(path, limits)
708      }
709
710      /// Opens a container from any `Read + Seek` source.
711      pub fn open_reader<R: Read + Seek + Send + 'static>(
712        reader: R,
713        url: Option<&str>,
714      ) -> Result<Self, DemuxError> {
715        Self::open_reader_impl(reader, url)
716      }
717
718      /// [`Self::open_reader`], with the session's budgets named.
719      pub fn open_reader_with<R: Read + Seek + Send + 'static>(
720        reader: R,
721        url: Option<&str>,
722        limits: DemuxLimits,
723      ) -> Result<Self, DemuxError> {
724        Self::open_reader_with_impl(reader, url, limits)
725      }
726
727      /// The wrapped `AVFormatContext`.
728      pub const fn input(&self) -> &Input {
729        self.input_impl()
730      }
731
732      /// The budgets this session was opened with.
733      pub const fn limits(&self) -> DemuxLimits {
734        self.limits_impl()
735      }
736
737      /// **What the container IS**, as libavformat identified it from
738      /// the bytes — the demuxer it chose, with the short names that
739      /// demuxer handles and its description.
740      ///
741      /// Decided during the open and fixed for the life of the
742      /// session, so this answers the same thing at any point and
743      /// costs nothing to ask.
744      ///
745      /// `None` only where libavformat left `iformat` null or its name
746      /// is not readable text; neither happens on a session that
747      /// opened successfully.
748      ///
749      /// **Nothing here looked at a path.** A file's extension is a
750      /// claim about its bytes, and this is a reading of them — which
751      /// is what makes the answer usable on a content-addressed row,
752      /// where the same bytes under two names are one content. See
753      /// [`ContainerFormat`](crate::ContainerFormat) for what the
754      /// demuxer's name does and does not narrow to.
755      pub const fn format(&self) -> Option<&crate::ContainerFormat> {
756        self.format_impl()
757      }
758    }
759
760    impl Demuxer for CarrierDemuxer<$lane> {
761      type Adapter = Ffmpeg;
762      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
763      type TrackHandle = Arc<TrackInfo<Ffmpeg>>;
764      type Error = DemuxError;
765
766      /// The track table, held for the life of the session.
767      ///
768      /// Reading it takes nothing away — clone the handles worth
769      /// keeping. `Arc` is the carrier because
770      /// [`CodecTicket`](crate::ticket::CodecTicket) mirrors an
771      /// `AVCodecParameters` into owned Rust, which is what makes a
772      /// row `Send + Sync` and a table shareable across tasks.
773      fn tracks(&self) -> &[Arc<TrackInfo<Ffmpeg>>] {
774        self.tracks_impl()
775      }
776
777      /// Pulls the next packet.
778      ///
779      /// **A refusal that another attempt could survive costs no
780      /// packet.** `av_read_frame` advances the container, so a
781      /// conversion that then fails on an allocation would otherwise
782      /// drop bytes nothing can ask for again. Such a packet is parked
783      /// instead, and this method re-attempts *it* before reading
784      /// another — so a caller who pulls again loses nothing. A refusal
785      /// about the packet itself is not parked: retrying a malformed
786      /// packet forever would be worse than passing it by.
787      fn next_packet(
788        &mut self,
789      ) -> Result<Option<DemuxedPacket<Ffmpeg, Self::Buffer>>, DemuxError> {
790        self.next_packet_impl()
791      }
792
793      fn seek(&mut self, target: Timestamp) -> Result<(), DemuxError> {
794        self.seek_impl(target)
795      }
796    }
797  )+ };
798}
799
800demuxer_lane_face!(crate::View, crate::Owned);
801
802/// Payload for [`DemuxError::AttachmentTooLarge`].
803///
804/// One attachment's payload exceeds
805/// [`DemuxLimits::max_attachment_bytes`].
806#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
807#[error(
808  "the attachment on stream {stream_index} is {bytes} bytes, over the {limit}-byte per-attachment budget"
809)]
810pub struct AttachmentTooLarge {
811  stream_index: usize,
812  bytes: usize,
813  limit: usize,
814}
815
816impl AttachmentTooLarge {
817  /// Constructs an `AttachmentTooLarge` payload.
818  #[cfg_attr(not(tarpaulin), inline(always))]
819  pub const fn new(stream_index: usize, bytes: usize, limit: usize) -> Self {
820    Self {
821      stream_index,
822      bytes,
823      limit,
824    }
825  }
826  /// The `AVStream.index` carrying the oversized attachment.
827  #[cfg_attr(not(tarpaulin), inline(always))]
828  pub const fn stream_index(&self) -> usize {
829    self.stream_index
830  }
831  /// The attachment's payload length.
832  #[cfg_attr(not(tarpaulin), inline(always))]
833  pub const fn bytes(&self) -> usize {
834    self.bytes
835  }
836  /// The per-attachment budget in force.
837  #[cfg_attr(not(tarpaulin), inline(always))]
838  pub const fn limit(&self) -> usize {
839    self.limit
840  }
841}
842
843/// Payload for [`DemuxError::AttachmentBudgetExhausted`].
844///
845/// The file's attachments, together, exceed
846/// [`DemuxLimits::max_total_attachment_bytes`].
847///
848/// Separate from [`AttachmentTooLarge`] because it is a different
849/// attack: every attachment can be modest and there can still be four
850/// hundred of them. This arm names the track that ran the total past
851/// the line, not the track that was individually at fault — there
852/// need not be one.
853#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
854#[error(
855  "the attachment on stream {stream_index} brings the file's attachments to {total} bytes, over the {limit}-byte budget"
856)]
857pub struct AttachmentBudgetExhausted {
858  stream_index: usize,
859  total: usize,
860  limit: usize,
861}
862
863impl AttachmentBudgetExhausted {
864  /// Constructs an `AttachmentBudgetExhausted` payload.
865  #[cfg_attr(not(tarpaulin), inline(always))]
866  pub const fn new(stream_index: usize, total: usize, limit: usize) -> Self {
867    Self {
868      stream_index,
869      total,
870      limit,
871    }
872  }
873  /// The `AVStream.index` whose attachment crossed the line.
874  #[cfg_attr(not(tarpaulin), inline(always))]
875  pub const fn stream_index(&self) -> usize {
876    self.stream_index
877  }
878  /// The running total, including this attachment.
879  #[cfg_attr(not(tarpaulin), inline(always))]
880  pub const fn total(&self) -> usize {
881    self.total
882  }
883  /// The whole-file budget in force.
884  #[cfg_attr(not(tarpaulin), inline(always))]
885  pub const fn limit(&self) -> usize {
886    self.limit
887  }
888}
889
890/// Payload for [`DemuxError::ParametersTooLarge`].
891///
892/// One stream's codec parameters hold more heap bytes than
893/// [`DemuxLimits::max_codec_parameter_bytes`] allows.
894///
895/// The bytes are `extradata` plus every `coded_side_data` entry plus a
896/// custom channel map — the three seats `AVCodecParameters` reaches the
897/// heap through. A MOV `prof` atom lands in the second of those as an
898/// ICC profile, which is where the honest large values live and where
899/// the forged ones do too.
900#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
901#[error(
902  "the codec parameters on stream {stream_index} hold {bytes} heap bytes, over the {limit}-byte budget"
903)]
904pub struct ParametersTooLarge {
905  stream_index: usize,
906  bytes: usize,
907  limit: usize,
908}
909
910impl ParametersTooLarge {
911  /// Constructs a `ParametersTooLarge` payload.
912  #[cfg_attr(not(tarpaulin), inline(always))]
913  pub const fn new(stream_index: usize, bytes: usize, limit: usize) -> Self {
914    Self {
915      stream_index,
916      bytes,
917      limit,
918    }
919  }
920  /// The `AVStream.index` whose parameters were refused.
921  #[cfg_attr(not(tarpaulin), inline(always))]
922  pub const fn stream_index(&self) -> usize {
923    self.stream_index
924  }
925  /// The heap bytes the parameters declared.
926  #[cfg_attr(not(tarpaulin), inline(always))]
927  pub const fn bytes(&self) -> usize {
928    self.bytes
929  }
930  /// The budget in force.
931  #[cfg_attr(not(tarpaulin), inline(always))]
932  pub const fn limit(&self) -> usize {
933    self.limit
934  }
935}
936
937/// Payload for [`DemuxError::ParametersBudgetExhausted`].
938///
939/// Every stream's codec parameters, together, hold more heap bytes than
940/// [`DemuxLimits::max_total_codec_parameter_bytes`] allows.
941///
942/// A separate attack from [`ParametersTooLarge`], and separate for the
943/// same reason the attachment pair are: each stream's parameters can be
944/// individually modest and a container can still declare two hundred
945/// streams. The arm names the stream that ran the total past the line,
946/// which need not be one that was individually at fault.
947#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
948#[error(
949  "the codec parameters on stream {stream_index} bring the file's to {total} heap bytes, over the {limit}-byte budget"
950)]
951pub struct ParametersBudgetExhausted {
952  stream_index: usize,
953  total: usize,
954  limit: usize,
955}
956
957impl ParametersBudgetExhausted {
958  /// Constructs a `ParametersBudgetExhausted` payload.
959  #[cfg_attr(not(tarpaulin), inline(always))]
960  pub const fn new(stream_index: usize, total: usize, limit: usize) -> Self {
961    Self {
962      stream_index,
963      total,
964      limit,
965    }
966  }
967  /// The `AVStream.index` whose parameters crossed the line.
968  #[cfg_attr(not(tarpaulin), inline(always))]
969  pub const fn stream_index(&self) -> usize {
970    self.stream_index
971  }
972  /// The running total, including this stream.
973  #[cfg_attr(not(tarpaulin), inline(always))]
974  pub const fn total(&self) -> usize {
975    self.total
976  }
977  /// The whole-file budget in force.
978  #[cfg_attr(not(tarpaulin), inline(always))]
979  pub const fn limit(&self) -> usize {
980    self.limit
981  }
982}
983
984/// Payload for [`DemuxError::ParametersMissing`].
985///
986/// Codec parameters arrived that were never allocated.
987///
988/// `ffmpeg_next::codec::Parameters` has safe constructors that hand
989/// back a null-backed value when FFmpeg's allocation failed, and they
990/// report nothing. Copying from one dereferences null, so it is
991/// refused where it arrives — at construction, and again in the
992/// copier — rather than crashing later somewhere that has forgotten
993/// the allocator ever failed.
994#[derive(thiserror::Error, Debug, Clone)]
995#[error("the codec parameters for stream {stream_index} were never allocated")]
996pub struct ParametersMissing {
997  stream_index: usize,
998}
999
1000impl ParametersMissing {
1001  /// Constructs a `ParametersMissing` payload.
1002  #[cfg_attr(not(tarpaulin), inline(always))]
1003  pub const fn new(stream_index: usize) -> Self {
1004    Self { stream_index }
1005  }
1006  /// The `AVStream.index` the parameters were offered for.
1007  #[cfg_attr(not(tarpaulin), inline(always))]
1008  pub const fn stream_index(&self) -> usize {
1009    self.stream_index
1010  }
1011}
1012
1013/// Payload for [`DemuxError::ParametersAlloc`].
1014///
1015/// Codec parameters for a track could not be allocated.
1016#[derive(thiserror::Error, Debug, Clone)]
1017#[error("out of memory allocating the codec parameters for stream {stream_index}")]
1018pub struct ParametersAlloc {
1019  stream_index: usize,
1020}
1021
1022impl ParametersAlloc {
1023  /// Constructs a `ParametersAlloc` payload.
1024  #[cfg_attr(not(tarpaulin), inline(always))]
1025  pub const fn new(stream_index: usize) -> Self {
1026    Self { stream_index }
1027  }
1028  /// The `AVStream.index` whose parameters could not be copied.
1029  #[cfg_attr(not(tarpaulin), inline(always))]
1030  pub const fn stream_index(&self) -> usize {
1031    self.stream_index
1032  }
1033}
1034
1035/// Payload for [`DemuxError::ParametersCopy`].
1036///
1037/// Copying a track's codec parameters failed part way.
1038#[derive(thiserror::Error, Debug, Clone)]
1039#[error("the codec parameters for stream {stream_index} could not be copied: {source}")]
1040pub struct ParametersCopy {
1041  stream_index: usize,
1042  #[source]
1043  source: ffmpeg_next::Error,
1044}
1045
1046impl ParametersCopy {
1047  /// Constructs a `ParametersCopy` payload.
1048  #[cfg_attr(not(tarpaulin), inline(always))]
1049  pub const fn new(stream_index: usize, source: ffmpeg_next::Error) -> Self {
1050    Self {
1051      stream_index,
1052      source,
1053    }
1054  }
1055  /// The `AVStream.index` whose parameters could not be copied.
1056  #[cfg_attr(not(tarpaulin), inline(always))]
1057  pub const fn stream_index(&self) -> usize {
1058    self.stream_index
1059  }
1060  /// What FFmpeg said.
1061  #[cfg_attr(not(tarpaulin), inline(always))]
1062  pub const fn source(&self) -> &ffmpeg_next::Error {
1063    &self.source
1064  }
1065}
1066
1067/// Payload for [`DemuxError::ParametersOpaque`].
1068///
1069/// A channel layout arrived carrying `opaque` — a raw pointer FFmpeg
1070/// documents as "private data of the user".
1071///
1072/// [`CodecTicket`](crate::ticket::CodecTicket) is an **owned** mirror:
1073/// it outlives the `AVCodecParameters` it was read from, and it may
1074/// cross threads, so a pointer into somebody else's data is exactly
1075/// what it cannot carry. libavformat sets neither
1076/// `AVChannelLayout::opaque` nor `AVChannelCustom::opaque`, so no
1077/// demuxed stream reaches the mirror with one; if one ever does, the
1078/// mirror refuses rather than dropping the pointer in silence. That is
1079/// the same fail-closed answer `extras::measure_parameters` gives a
1080/// channel order it has never heard of, and for the same reason:
1081/// carrying on would be a guess about memory nobody here owns.
1082#[derive(thiserror::Error, Debug, Clone)]
1083#[error(
1084  "the channel layout for stream {stream_index} carries user-private data \
1085   ({}) that an owned codec ticket cannot mirror",
1086  match channel { Some(i) => format!("custom channel {i}"), None => "the layout".to_owned() },
1087)]
1088pub struct ParametersOpaque {
1089  stream_index: usize,
1090  channel: Option<usize>,
1091}
1092
1093impl ParametersOpaque {
1094  /// Constructs a `ParametersOpaque` payload. `channel` names the
1095  /// custom-map entry when the pointer was on one, and is `None` when
1096  /// it was on the layout itself.
1097  #[cfg_attr(not(tarpaulin), inline(always))]
1098  pub const fn new(stream_index: usize, channel: Option<usize>) -> Self {
1099    Self {
1100      stream_index,
1101      channel,
1102    }
1103  }
1104  /// The `AVStream.index` whose layout carried the pointer.
1105  #[cfg_attr(not(tarpaulin), inline(always))]
1106  pub const fn stream_index(&self) -> usize {
1107    self.stream_index
1108  }
1109  /// The custom-map entry the pointer was on, or `None` when it was on
1110  /// the layout itself.
1111  #[cfg_attr(not(tarpaulin), inline(always))]
1112  pub const fn channel(&self) -> Option<usize> {
1113    self.channel
1114  }
1115}
1116
1117/// Payload for [`DemuxError::ParametersChannelMap`].
1118///
1119/// A channel layout declared `AV_CHANNEL_ORDER_CUSTOM` without the map
1120/// that order requires.
1121///
1122/// **This one is a crash, not a curiosity.** For a custom order,
1123/// `av_channel_layout_copy` — which is how
1124/// `avcodec_parameters_to_context` moves a layout into a decoder's
1125/// context — does
1126///
1127/// ```c
1128/// dst->u.map = av_malloc_array(src->nb_channels, sizeof(*dst->u.map));
1129/// if (!dst->u.map)
1130///     return AVERROR(ENOMEM);
1131/// memcpy(dst->u.map, src->u.map, src->nb_channels * sizeof(*src->u.map));
1132/// ```
1133///
1134/// with **no null check on `src->u.map`** — verified against FFmpeg
1135/// n9.0. A layout that names channels it has no map for therefore makes
1136/// libavcodec `memcpy` from a null pointer the moment a decoder opens
1137/// from it.
1138///
1139/// So the mirror refuses such a layout at the door rather than
1140/// reproducing it. An earlier draft carried it through, on the argument
1141/// that a malformed layout in should be a malformed layout out — the
1142/// round trip is faithful either way, and the parity comparator agreed.
1143/// That symmetry was the wrong test: faithfully reproducing a shape
1144/// whose only consumer dereferences null is not fidelity, it is
1145/// forwarding a crash. Refusing is the same fail-closed answer
1146/// `extras::measure_parameters` gives a channel order it has never
1147/// heard of.
1148#[derive(thiserror::Error, Debug, Clone)]
1149#[error(
1150  "the custom channel layout for stream {stream_index} declares {channels} channels \
1151   but carries no map for them"
1152)]
1153pub struct ParametersChannelMap {
1154  stream_index: usize,
1155  channels: i32,
1156}
1157
1158impl ParametersChannelMap {
1159  /// Constructs a `ParametersChannelMap` payload.
1160  #[cfg_attr(not(tarpaulin), inline(always))]
1161  pub const fn new(stream_index: usize, channels: i32) -> Self {
1162    Self {
1163      stream_index,
1164      channels,
1165    }
1166  }
1167  /// The `AVStream.index` whose layout was malformed.
1168  #[cfg_attr(not(tarpaulin), inline(always))]
1169  pub const fn stream_index(&self) -> usize {
1170    self.stream_index
1171  }
1172  /// The `nb_channels` the layout declared with no map to describe them.
1173  #[cfg_attr(not(tarpaulin), inline(always))]
1174  pub const fn channels(&self) -> i32 {
1175    self.channels
1176  }
1177}
1178
1179/// Payload for [`DemuxError::PacketBuffer`].
1180///
1181/// A packet's payload could not be referenced — the bytes are there
1182/// and this layer could not carry them.
1183///
1184/// Never raised for a packet that simply has no payload: an empty
1185/// packet is a marker some demuxers emit, and it is skipped in
1186/// silence. Distinguishing the two is what keeps a refcount failure
1187/// under memory pressure from looking like the file's own word and
1188/// dropping real compressed bytes.
1189#[derive(thiserror::Error, Debug, Clone)]
1190#[error("stream {stream_index}: {source}")]
1191pub struct PacketBuffer {
1192  stream_index: usize,
1193  #[source]
1194  source: PacketBufferError,
1195}
1196
1197impl PacketBuffer {
1198  /// Constructs a `PacketBuffer` payload.
1199  #[cfg_attr(not(tarpaulin), inline(always))]
1200  pub const fn new(stream_index: usize, source: PacketBufferError) -> Self {
1201    Self {
1202      stream_index,
1203      source,
1204    }
1205  }
1206  /// The `AVStream.index` the packet belongs to.
1207  #[cfg_attr(not(tarpaulin), inline(always))]
1208  pub const fn stream_index(&self) -> usize {
1209    self.stream_index
1210  }
1211  /// What went wrong.
1212  #[cfg_attr(not(tarpaulin), inline(always))]
1213  pub const fn source(&self) -> &PacketBufferError {
1214    &self.source
1215  }
1216}
1217
1218/// Payload for [`DemuxError::ReaderPanic`].
1219///
1220/// The `Read + Seek` source given to [`FfmpegDemuxer::open_reader`]
1221/// panicked inside a libavformat callback.
1222///
1223/// The panic was caught before it could cross the `extern "C"`
1224/// boundary and abort the process; this is what it said. The session
1225/// is terminal — every later call reports the same panic.
1226#[derive(thiserror::Error, Debug, Clone)]
1227#[error("the reader panicked: {message}")]
1228pub struct ReaderPanic {
1229  message: SmolStr,
1230}
1231
1232impl ReaderPanic {
1233  /// Constructs a `ReaderPanic` payload.
1234  #[cfg_attr(not(tarpaulin), inline(always))]
1235  pub const fn new(message: SmolStr) -> Self {
1236    Self { message }
1237  }
1238  /// What the panic payload said.
1239  #[cfg_attr(not(tarpaulin), inline(always))]
1240  pub fn message(&self) -> &str {
1241    self.message.as_str()
1242  }
1243}
1244
1245/// Errors from [`FfmpegDemuxer`].
1246///
1247/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
1248/// fail are discovered — a backend, a ceiling, a corruption a codec
1249/// learns to report — and a consumer that meets one it has never heard
1250/// of should take its generic-fault path. That is exactly what the
1251/// wildcard arm this attribute forces is for. The two status
1252/// vocabularies opposite it,
1253/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
1254/// are exhaustive for the mirror-image reason: their arms are the
1255/// substrate's fixed state set, and there the wildcard would be dead
1256/// weight hiding a state a consumer forgot.
1257#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
1258#[unwrap(ref, ref_mut)]
1259#[try_unwrap(ref, ref_mut)]
1260#[non_exhaustive]
1261pub enum DemuxError {
1262  /// The wrapped libavformat call reported an error — open, read or
1263  /// seek.
1264  #[error(transparent)]
1265  Ffmpeg(#[from] ffmpeg_next::Error),
1266
1267  /// libavformat asked for more bytes than the probe budget allows
1268  /// while opening and analysing the container. See
1269  /// [`ProbeBudgetExhausted`].
1270  #[error(transparent)]
1271  ProbeBudgetExhausted(#[from] ProbeBudgetExhausted),
1272
1273  /// One attachment's payload is over the per-attachment budget.
1274  /// Refused at open, before the copy.
1275  #[error(transparent)]
1276  AttachmentTooLarge(#[from] AttachmentTooLarge),
1277
1278  /// The file's attachments, together, are over the whole-file budget.
1279  /// Refused at open, before the copy that would have crossed it.
1280  #[error(transparent)]
1281  AttachmentBudgetExhausted(#[from] AttachmentBudgetExhausted),
1282
1283  /// One stream's codec parameters hold more heap bytes than the
1284  /// budget allows. Refused at open, before the clone.
1285  #[error(transparent)]
1286  ParametersTooLarge(#[from] ParametersTooLarge),
1287
1288  /// Every stream's codec parameters together are over the whole-file
1289  /// budget. Refused at open, before the clone that would have crossed
1290  /// it.
1291  #[error(transparent)]
1292  ParametersBudgetExhausted(#[from] ParametersBudgetExhausted),
1293
1294  /// Codec parameters arrived that were never allocated.
1295  #[error(transparent)]
1296  ParametersMissing(#[from] ParametersMissing),
1297
1298  /// Codec parameters for a track could not be allocated.
1299  #[error(transparent)]
1300  ParametersAlloc(#[from] ParametersAlloc),
1301
1302  /// Copying a track's codec parameters failed part way.
1303  #[error(transparent)]
1304  ParametersCopy(#[from] ParametersCopy),
1305
1306  /// A channel layout arrived carrying user-private data an owned
1307  /// codec ticket cannot mirror.
1308  #[error(transparent)]
1309  ParametersOpaque(#[from] ParametersOpaque),
1310
1311  /// A channel layout declared a custom order without the map that
1312  /// order requires — a shape `av_channel_layout_copy` would `memcpy`
1313  /// from null.
1314  #[error(transparent)]
1315  ParametersChannelMap(#[from] ParametersChannelMap),
1316
1317  /// A packet's payload could not be referenced — the bytes are there
1318  /// and this layer could not carry them.
1319  #[error(transparent)]
1320  PacketBuffer(#[from] PacketBuffer),
1321
1322  /// The `Read + Seek` source given to
1323  /// [`FfmpegDemuxer::open_reader`] panicked inside a libavformat
1324  /// callback.
1325  #[error(transparent)]
1326  ReaderPanic(#[from] ReaderPanic),
1327}
1328
1329// ---------------------------------------------------------------------------
1330//  Track-table construction.
1331// ---------------------------------------------------------------------------
1332
1333type BuiltTracks<C> = (
1334  Vec<TrackInfo<Ffmpeg>>,
1335  VecDeque<(
1336    TrackIndex,
1337    AttachmentPacket<AttachmentPacketExtra, <C as crate::FfmpegCarrier>::Buffer>,
1338  )>,
1339);
1340
1341fn build_tracks<C: crate::FfmpegCarrier + crate::CarrierOps>(
1342  input: &Input,
1343  limits: DemuxLimits,
1344) -> Result<BuiltTracks<C>, DemuxError> {
1345  // **Admission before allocation.** Every attachment in the file is
1346  // judged here, in full, before the loop below allocates anything at
1347  // all — see [`admit_streams`] for why the charge cannot live
1348  // inside the capture.
1349  admit_streams(input, limits)?;
1350
1351  let count = input.streams().len();
1352  let mut tracks = Vec::with_capacity(count);
1353  let mut pending = VecDeque::new();
1354
1355  for stream in input.streams() {
1356    let index = stream.index();
1357    // `AVStream.index` is the stream's position in `ic->streams[]` and
1358    // libavformat keeps the two identical. The demux tier makes
1359    // `TrackIndex` mean "position in `tracks()`", so the two agree by
1360    // construction — but only if they really are dense and in order,
1361    // which is cheap to insist on rather than assume.
1362    debug_assert_eq!(
1363      index,
1364      tracks.len(),
1365      "AVStream indices are dense and ordered"
1366    );
1367
1368    let parameters = stream.parameters();
1369    let par = unsafe { parameters.as_ptr() };
1370    // Never read `AVCodecParameters.codec_type` / `.codec_id` as their
1371    // bindgen enums: a value outside this build's discriminant set is
1372    // UB the moment it exists. Both are read as the raw integers they
1373    // are on the wire — the medium through [`boundary::media_kind_of`],
1374    // which folds anything unnamed into `Unknown`.
1375    //
1376    // The medium used to go through `Parameters::medium()` on the
1377    // argument that `AVMediaType`'s set is tiny and stable. It is; that
1378    // made the read unlikely to bite, not sound. The exception is gone
1379    // rather than defended, so no attacker-reachable path in this crate
1380    // forms a bindgen enum out of FFmpeg memory.
1381    let medium = boundary::media_kind_of(&parameters);
1382    let codec =
1383      CodecId::from_raw(unsafe { read_unaligned(addr_of!((*par).codec_id).cast::<i32>()) });
1384
1385    let disposition = unsafe { (*stream.as_ptr()).disposition };
1386    let attached_pic = is_attachment_disposition(disposition);
1387
1388    let time_base = rational_to_timebase(stream.time_base());
1389    let raw_duration = stream.duration();
1390    let duration = (raw_duration != AV_NOPTS_VALUE && raw_duration > 0)
1391      .then(|| Timestamp::new(raw_duration, time_base));
1392    let raw_start = stream.start_time();
1393    let frames = stream.frames();
1394
1395    let params = if attached_pic {
1396      // Cover art. A still image in a video-shaped slot is an
1397      // attachment by every property that matters, and the `Video` arm
1398      // is reserved for motion video.
1399      TrackParams::Attachment(AttachmentTrackParams::new(codec))
1400    } else {
1401      match medium {
1402        boundary::MediaKind::Video => TrackParams::Video(VideoTrackParams::new(
1403          codec,
1404          unsafe { (*par).width }.max(0) as u32,
1405          unsafe { (*par).height }.max(0) as u32,
1406          boundary::from_av_pixel_format(unsafe { (*par).format }),
1407          rate_to_timebase(stream.avg_frame_rate()),
1408        )),
1409        boundary::MediaKind::Audio => {
1410          let ch_layout = unsafe { std::ptr::addr_of!((*par).ch_layout) };
1411          // SAFETY: `par` is a live `*const AVCodecParameters` for the
1412          // life of `parameters`; the helper validates `order` as an
1413          // `i32` before constructing any `AVChannelOrder`.
1414          let channel_layout =
1415            unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout) };
1416          TrackParams::Audio(AudioTrackParams::new(
1417            codec,
1418            unsafe { (*par).sample_rate }.max(0) as u32,
1419            channel_layout.channels().min(255) as u8,
1420            SampleFormat::from_raw(unsafe { (*par).format }),
1421            channel_layout,
1422          ))
1423        }
1424        boundary::MediaKind::Subtitle => TrackParams::Subtitle(SubtitleTrackParams::new(codec)),
1425        boundary::MediaKind::Data => TrackParams::Data(DataTrackParams::new(codec)),
1426        boundary::MediaKind::Attachment => {
1427          TrackParams::Attachment(AttachmentTrackParams::new(codec))
1428        }
1429        boundary::MediaKind::Unknown => TrackParams::Unknown(UnknownTrackParams::new(codec)),
1430      }
1431    };
1432
1433    // The parameter mirror. For an `AVMEDIA_TYPE_ATTACHMENT` stream its
1434    // `extradata` **is** the attachment's payload — the same bytes the
1435    // carrier below already holds — so it is left behind rather than
1436    // copied. Censused before it was: nothing can use it. libavcodec
1437    // has no decoder for a font (`avcodec_find_decoder` answers null
1438    // for `AV_CODEC_ID_TTF` and its siblings), so no road in this crate
1439    // or downstream of it opens a codec context from these parameters;
1440    // the payload reaches a consumer as the attachment packet, which is
1441    // the delivery the demux tier promises.
1442    //
1443    // **Omitted, not stripped.** An earlier shape copied the extradata
1444    // and freed it immediately afterwards, which allocated the payload
1445    // for no reason and — worse — charged it against the *parameter*
1446    // ceiling on the way past. A font between the two ceilings passed
1447    // the admission pass and then failed inside the clone. See
1448    // [`ExtradataPolicy`](crate::extras::ExtradataPolicy).
1449    //
1450    // Cover art keeps its extradata: there the payload is the parked
1451    // `AVPacket`, extradata is *not* a copy of it, and a still codec
1452    // can legitimately need it (MJPEG with an external Huffman table).
1453    // Measured on this build: a cover-art stream carries none anyway.
1454    let extradata_policy = if medium.is_attachment() {
1455      crate::extras::ExtradataPolicy::Omit
1456    } else {
1457      crate::extras::ExtradataPolicy::Copy
1458    };
1459    // Straight from the stream's own parameters into the owned ticket.
1460    // The row used to reach here through an intermediate
1461    // `avcodec_parameters_copy` — one ffmpeg-native deep copy per
1462    // track, whose only purpose was to sever the tie to the format
1463    // context. The mirror severs it by being owned Rust, so that copy
1464    // is gone rather than moved.
1465    let ticket = crate::ticket::CodecTicket::mirror_with(
1466      &parameters,
1467      index,
1468      limits.max_codec_parameter_bytes(),
1469      extradata_policy,
1470    )?;
1471    let extra = TrackExtra::new(index as i32, ticket)
1472      .with_disposition(disposition)
1473      .with_start_time((raw_start != AV_NOPTS_VALUE).then_some(raw_start))
1474      .with_frame_count((frames > 0).then_some(frames));
1475
1476    // SAFETY: `stream` keeps the `AVStream` — and so its metadata
1477    // dictionary — live across both reads. The dictionary is read
1478    // through `av_dict_get` rather than through
1479    // `DictionaryRef::get`: see [`metadata_text`].
1480    let metadata = unsafe { (*stream.as_ptr()).metadata };
1481    let info = TrackInfo::new(time_base, params, extra)
1482      .with_duration(duration)
1483      .with_filename(unsafe { metadata_text(metadata, c"filename") })
1484      .with_mime_type(unsafe { metadata_text(metadata, c"mimetype") })
1485      // **`language` is where every container's tag lands.** libavformat
1486      // normalises the *key*, not the value: Matroska's `Language`
1487      // element, MP4's `mdhd` language code and an `elng`/ISO 639-2
1488      // atom, Matroska's BCP 47 `LanguageBCP47`, an ASF descriptor and
1489      // an ID3 `TLAN` frame all arrive on this one entry. What each
1490      // wrote is what is read — see
1491      // [`TrackInfo::language`](mediadecode::demuxer::TrackInfo::language)
1492      // for why nothing folds it here.
1493      .with_language(unsafe { metadata_text(metadata, c"language") });
1494
1495    // Capture the attachment payload now, so the queue is complete
1496    // before a single timed packet has been read. Every attachment
1497    // track leaves this loop with exactly one packet queued, or the
1498    // open fails: that is what makes "exactly one packet, before any
1499    // timed packet" a property of the construction rather than a
1500    // promise the pull loop has to keep.
1501    if info.kind() == TrackKind::Attachment {
1502      let packet = if attached_pic {
1503        // SAFETY: `stream` keeps the format context (and so the
1504        // `AVStream`) live; `attached_pic` is an `AVPacket` embedded by
1505        // value, and `addr_of!` reaches it without forming a reference
1506        // to the stream.
1507        let pkt = unsafe { std::ptr::addr_of!((*stream.as_ptr()).attached_pic) };
1508        unsafe { attached_pic_payload::<C>(pkt, index, limits) }?
1509      } else {
1510        extradata_payload::<C>(&stream, limits)?
1511      };
1512      pending.push_back((TrackIndex::new(index), packet));
1513    }
1514
1515    tracks.push(info);
1516  }
1517
1518  Ok((tracks, pending))
1519}
1520
1521/// Whether `packet`'s payload is the very allocation the container has
1522/// parked in `AVStream.attached_pic` for stream `index`.
1523///
1524/// # Why this exists
1525///
1526/// libavformat queues a stream's attached picture as its **first
1527/// packet** — `read_frame_internal` does `av_packet_ref(pkt,
1528/// &st->attached_pic)` and keeps its own reference — so that packet
1529/// arrives with two references through nobody's fault. A pure cover-art
1530/// stream never reaches this road (it is an attachment, hoisted at
1531/// open), but a stream carrying `ATTACHED_PIC | TIMED_THUMBNAILS` is
1532/// deliberately classified as **video** by
1533/// [`is_attachment_disposition`], so its first pull comes through here
1534/// and would be refused as a shared payload. Every packet after it is
1535/// an ordinary timed one with a buffer of its own.
1536///
1537/// # The probe, and why it is a proof rather than a guess
1538///
1539/// `av_buffer_ref` sets the new reference's `buffer` field to the
1540/// source's, so two `AVBufferRef`s name one allocation **iff** their
1541/// `buffer` pointers are equal — the same identity
1542/// [`crate::FfmpegBuffer::ptr_eq`] rests on. Comparing them therefore
1543/// establishes the fact the carve-out needs: this payload's allocation
1544/// *is* `AVStream.attached_pic`'s, so one of its outstanding references
1545/// is the container's own.
1546///
1547/// The alternatives were heuristics and are not used: the disposition
1548/// bits say a stream *has* an attached picture, not that this packet is
1549/// it; "the first packet on the stream" is an ordering assumption that
1550/// nothing in libavformat's contract fixes.
1551///
1552/// # The soundness argument, restated for this packet
1553///
1554/// It is the same one the hoisted-attachment road rests on, and it
1555/// holds here for the same reason. `AVStream.attached_pic` is written
1556/// once, while the container is being opened, and never again; the
1557/// reference this crate is looking at is the container's, held for the
1558/// lifetime of the `AVFormatContext`, and there is no
1559/// `ffmpeg_next::Packet` wrapping it for anyone to call `data_mut` on.
1560/// What the uniqueness rule guards against is a *safe Rust* handle that
1561/// may write while this crate reads, and the container's reference is
1562/// not one.
1563///
1564/// # Safety
1565///
1566/// `input` and `packet` must both be live for the duration of the call.
1567unsafe fn is_streams_attached_pic(input: &Input, index: usize, packet: &Packet) -> bool {
1568  // SAFETY: `input` owns a live `AVFormatContext`; `streams` is an
1569  // array of `nb_streams` pointers, and `index` is checked against it.
1570  let stream = unsafe {
1571    let context = input.as_ptr();
1572    if index >= (*context).nb_streams as usize {
1573      return false;
1574    }
1575    *(*context).streams.add(index)
1576  };
1577  if stream.is_null() {
1578    return false;
1579  }
1580  // SAFETY: `stream` is one of the context's own live `AVStream`s and
1581  // `packet` is live per this function's contract.
1582  unsafe { packet_is_parked_picture(stream, packet) }
1583}
1584
1585/// The identity itself: whether `packet`'s payload allocation is the
1586/// one `stream` has parked in `attached_pic`.
1587///
1588/// Split out from [`is_streams_attached_pic`] so the comparison can be
1589/// tested against a hand-built pair without forging an
1590/// `AVFormatContext` — see `a_queued_attached_picture_is_recognised`.
1591///
1592/// # Safety
1593///
1594/// `stream` must be a live `AVStream` and `packet` a live `AVPacket`.
1595unsafe fn packet_is_parked_picture(stream: *const AVStream, packet: &Packet) -> bool {
1596  use ffmpeg_next::packet::Ref;
1597
1598  // SAFETY: both are live per the contract; `attached_pic` is an inline
1599  // `AVPacket` and both `buf` fields may be null, which is answered
1600  // before either is read through.
1601  unsafe {
1602    let parked = (*stream).attached_pic.buf;
1603    let carried = (*packet.as_ptr()).buf;
1604    if parked.is_null() || carried.is_null() {
1605      return false;
1606    }
1607    // The shared `AVBuffer`, not the `AVBufferRef`: `av_packet_ref`
1608    // mints a new reference struct around the same allocation, so
1609    // comparing the references themselves would answer "no" to exactly
1610    // the case this is for.
1611    (*parked).buffer == (*carried).buffer
1612  }
1613}
1614
1615/// Whether a stream's disposition makes it an **attachment** — a
1616/// payload with no place on the timeline — rather than a timed track.
1617///
1618/// `AV_DISPOSITION_ATTACHED_PIC` alone says "cover art": one still
1619/// image, parked in `AVStream.attached_pic`, no timeline. But FFmpeg
1620/// pairs it with `AV_DISPOSITION_TIMED_THUMBNAILS` for a different
1621/// thing entirely — "the stream is sparse, and contains thumbnail
1622/// images, often corresponding to chapter markers", a flag its own
1623/// header documents as *only ever* used together with `ATTACHED_PIC`.
1624/// Such a stream has many images and every one of them has a
1625/// timestamp.
1626///
1627/// Classifying that as an attachment loses all but the first: the
1628/// attachment contract is exactly one packet, so the queue takes the
1629/// parked copy and the delivery loop drops every timed packet on the
1630/// track. It goes to the **`Video`** arm instead. That does not
1631/// contradict "cover art is an attachment, not video" — the reason
1632/// behind that ruling is that a single still with no timeline must not
1633/// look like a motion track, and a timed-thumbnail stream *is* on the
1634/// timeline. It is sparse video: a codec id, a frame size, a pixel
1635/// format and packets with timestamps, which is everything a consumer
1636/// needs to decode the images. The `Data` arm was the alternative and
1637/// is worse: it would strand encoded pictures in an arm that names no
1638/// decoder.
1639///
1640/// The bits are tested against the raw `AVStream.disposition` rather
1641/// than through `ffmpeg_next`'s `Disposition`, which mints no
1642/// `TIMED_THUMBNAILS` constant at all — its `from_bits_truncate` drops
1643/// every bit this build of the wrapper has no name for, which is how
1644/// the distinction went missing in the first place.
1645const fn is_attachment_disposition(disposition: c_int) -> bool {
1646  disposition & AV_DISPOSITION_ATTACHED_PIC != 0
1647    && disposition & AV_DISPOSITION_TIMED_THUMBNAILS == 0
1648}
1649
1650/// Upper bound on the NUL search in [`metadata_text`].
1651///
1652/// Generous by four orders of magnitude for a filename or a MIME type,
1653/// and there only so that a value libavutil did not terminate cannot
1654/// turn the walk into an unbounded read — the same discipline
1655/// [`crate::channel_layout`] and the pixel-format namer follow. A value
1656/// longer than this is refused rather than truncated: a truncated
1657/// filename is a different filename.
1658const METADATA_VALUE_MAX_BYTES: usize = 64 * 1024;
1659
1660/// Reads one entry out of a container's metadata dictionary as text
1661/// this crate can own.
1662///
1663/// **Why not `DictionaryRef::get`.** ffmpeg-next 9.0.0 builds its
1664/// `&str` with `from_utf8_unchecked`
1665/// (`src/util/dictionary/immutable.rs`), and FFmpeg does not validate
1666/// demuxed metadata as UTF-8 — an ID3 frame, a Matroska attachment
1667/// name or a MOV atom carries whatever bytes the file carries. A
1668/// `filename` holding a stray `0x80` would therefore have produced a
1669/// `&str` that is not UTF-8: undefined behaviour the moment it exists,
1670/// before `SmolStr` ever copies it.
1671///
1672/// Invalid bytes are replaced (`U+FFFD`), not refused. This is
1673/// *identity* metadata — the name a font was attached under, the MIME
1674/// type declared for a cover — and a file that names its attachment in
1675/// some legacy codepage is still a file worth opening. The replacement
1676/// characters say plainly that the container's bytes were not text.
1677///
1678/// # Safety
1679///
1680/// `dict` must be null or a live `*const AVDictionary` for the
1681/// duration of this call.
1682unsafe fn metadata_text(dict: *const AVDictionary, key: &CStr) -> Option<SmolStr> {
1683  if dict.is_null() {
1684    return None;
1685  }
1686  // SAFETY: `dict` is live per the contract above and `key` is a
1687  // NUL-terminated C string by construction; `av_dict_get` reads both
1688  // and returns a borrowed entry owned by the dictionary.
1689  let entry = unsafe { av_dict_get(dict, key.as_ptr(), std::ptr::null(), 0) };
1690  if entry.is_null() {
1691    return None;
1692  }
1693  // SAFETY: a non-null entry is a live `AVDictionaryEntry` for as long
1694  // as the dictionary is not modified, which it is not here.
1695  let value = unsafe { (*entry).value };
1696  if value.is_null() {
1697    return None;
1698  }
1699  for len in 0..METADATA_VALUE_MAX_BYTES {
1700    // SAFETY: `value` is a NUL-terminated string libavutil allocated
1701    // with `av_strdup`; the walk reads at most one byte past the last
1702    // value byte and stops at the terminator.
1703    if unsafe { *value.add(len).cast::<u8>() } == 0 {
1704      // SAFETY: the `len` bytes below the terminator were just walked,
1705      // so the slice is in bounds and initialised.
1706      let bytes = unsafe { std::slice::from_raw_parts(value.cast::<u8>(), len) };
1707      return Some(SmolStr::new(std::string::String::from_utf8_lossy(bytes)));
1708    }
1709  }
1710  None
1711}
1712
1713/// Wraps `AVStream.attached_pic` — the real packet libavformat parsed
1714/// for a cover-art stream — as this track's one attachment packet.
1715///
1716/// A stream that declares cover art but parks no payload still gets a
1717/// packet: an empty one, marked `synthesized`, because the contract is
1718/// one packet per attachment track and a consumer that sees an empty
1719/// payload learns something true about the file. The alternative shipped
1720/// once — waiting for the payload to arrive as a packet later — and it
1721/// cannot hold: nothing stops a timed packet, or a seek, from coming
1722/// first, so the track's packet would arrive out of order or never.
1723///
1724/// Measured before it was written: across MP3 (ID3 APIC), M4A (`covr`),
1725/// FLAC (`METADATA_BLOCK_PICTURE`) and Matroska (an `image/*`
1726/// attachment), every stream libavformat gives
1727/// `AV_DISPOSITION_ATTACHED_PIC` also carries the parked packet —
1728/// `ff_add_attached_pic` sets the disposition and fills
1729/// `attached_pic` in the same breath. The empty case is the honest
1730/// answer to a state this build's demuxers do not produce, not a
1731/// fallback anything relies on.
1732///
1733/// # Safety
1734///
1735/// `pkt` must be a live `*const AVPacket` — in practice the
1736/// `attached_pic` embedded in the `AVStream` at `index` — for the
1737/// duration of this call.
1738unsafe fn attached_pic_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1739  pkt: *const ffmpeg_next::ffi::AVPacket,
1740  index: usize,
1741  limits: DemuxLimits,
1742) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
1743  // Already admitted: [`admit_streams`] charged this payload — and
1744  // every other attachment in the file — before `build_tracks`
1745  // allocated anything. The per-attachment budget is passed down as
1746  // this packet's own ceiling anyway, so the funnel is guarded even if
1747  // a future caller reaches it without the admission pass.
1748  //
1749  // SAFETY: `pkt` is live per the contract above.
1750  // **The container's own cover art**, whose buffer libavformat also
1751  // holds — see [`crate::buffer::PayloadProvenance`] for why that
1752  // second reference is not the hazard a caller's second `Packet` is.
1753  let captured = unsafe {
1754    crate::buffer::payload_of::<C>(
1755      pkt,
1756      limits.max_attachment_bytes(),
1757      crate::buffer::PayloadProvenance::AttachedPicture,
1758    )
1759  }
1760  .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
1761  let extra = AttachmentPacketExtra::new(index as i32);
1762  Ok(match captured {
1763    Some(payload) => {
1764      // The hoisted packet's own flags, through the same raw reader the
1765      // five boundary conversions use. FFmpeg marks an attached picture
1766      // `AV_PKT_FLAG_KEY` — a still image is a keyframe if anything is
1767      // — and building this one with empty flags dropped that, along
1768      // with `CORRUPT` and every other bit the packet really carried.
1769      // SAFETY: `pkt` points at the live embedded `AVPacket`.
1770      let flags = unsafe { boundary::md_flags_from_av_packet(pkt) }
1771        .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
1772      AttachmentPacket::new(payload, extra).with_flags(flags)
1773    }
1774    // Nothing was parked, so there are no flags to read: an empty set
1775    // is the honest answer for a packet this layer invented.
1776    None => AttachmentPacket::new(C::empty(), extra.with_synthesized(true)),
1777  })
1778}
1779
1780/// Builds an attachment payload out of a track's codec extradata — the
1781/// only place a font's bytes ever live, since an
1782/// `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets at all.
1783///
1784/// A track with no extradata still gets a packet, with an empty
1785/// payload: the contract is one packet per attachment track, and a
1786/// consumer that sees an empty one learns something true about the
1787/// file. Only an allocation failure is an error.
1788fn extradata_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1789  stream: &ffmpeg_next::format::stream::Stream<'_>,
1790  limits: DemuxLimits,
1791) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
1792  let index = stream.index();
1793  let parameters = stream.parameters();
1794  // SAFETY: `parameters` keeps the `AVCodecParameters` live;
1795  // `extradata` / `extradata_size` are public fields.
1796  let par = unsafe { parameters.as_ptr() };
1797  let ptr = unsafe { (*par).extradata };
1798  let len = unsafe { (*par).extradata_size }.max(0) as usize;
1799  // Already admitted, exactly as on the hoisted cover-art path — see
1800  // [`admit_streams`]. Re-judged here against the per-attachment
1801  // ceiling alone, so the helper is safe to call on its own.
1802  if len > limits.max_attachment_bytes() {
1803    return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
1804      index,
1805      len,
1806      limits.max_attachment_bytes(),
1807    )));
1808  }
1809  let bytes: &[u8] = if ptr.is_null() || len == 0 {
1810    &[]
1811  } else {
1812    // SAFETY: libavformat guarantees `extradata` is readable for
1813    // `extradata_size` bytes (plus its padding) while the parameters
1814    // live, and the slice is consumed before this function returns.
1815    unsafe { std::slice::from_raw_parts(ptr, len) }
1816  };
1817  // Extradata is a plain allocation with no `AVBufferRef` behind it —
1818  // an `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets, so a
1819  // font's bytes never live in a refcounted buffer. **Both** lanes copy
1820  // here, which is what `from_bytes` is for.
1821  Ok(AttachmentPacket::new(
1822    C::from_bytes(bytes).ok_or_else(|| {
1823      DemuxError::PacketBuffer(PacketBuffer::new(
1824        index,
1825        crate::buffer::PacketBufferError::CaptureFailed(crate::buffer::CaptureFailed::new(len)),
1826      ))
1827    })?,
1828    AttachmentPacketExtra::new(index as i32).with_synthesized(true),
1829  ))
1830}
1831
1832/// **The admission pass**: judges every stream in the file before the
1833/// track table allocates anything at all.
1834///
1835/// # Why this cannot live inside the capture
1836///
1837/// It used to, and that was a bypass. `build_tracks` deep-copies each
1838/// stream's `AVCodecParameters` on its way to building a `TrackExtra`,
1839/// and for an `AVMEDIA_TYPE_ATTACHMENT` stream **the extradata inside
1840/// those parameters is the attachment's payload**. So the loop paid for
1841/// the payload — a full `avcodec_parameters_copy` — one statement
1842/// before asking whether it was allowed to. A file declaring a gigabyte
1843/// of "font" allocated the gigabyte and then reported that a gigabyte
1844/// was too much.
1845///
1846/// The fix is not a check moved a few lines earlier: any per-track
1847/// interleaving of judging and paying has the same shape, because the
1848/// aggregate budget is only knowable once every track has been *seen*.
1849/// So the whole file is admitted here, in a pass that allocates
1850/// nothing — it reads two integers per stream — and only a container
1851/// that passes in full reaches the loop that builds carriers and
1852/// parameter copies.
1853///
1854/// # Why it is every stream, not every attachment
1855///
1856/// Because the track table copies **every** stream's codec parameters,
1857/// and `AVCodecParameters` reaches the heap three ways — `extradata`,
1858/// every `coded_side_data` entry, a custom channel map — all of them
1859/// sized by the file. A pass that walked only attachment streams left
1860/// the other road wide open: a MOV puts an ICC profile in
1861/// `coded_side_data`, on an ordinary video track, and the wholesale
1862/// copy took it before anything asked how big it was. That was the same
1863/// class of defect three review rounds running, which is why the copy
1864/// itself is gone (see
1865/// [`bounded_clone_parameters`](crate::extras::bounded_clone_parameters))
1866/// and why this pass sees everything.
1867///
1868/// # What is charged
1869///
1870/// The bytes this session will **retain**, which is not always the
1871/// declared size:
1872///
1873/// - every stream is charged its parameter clone's footprint against
1874///   the per-stream and whole-file codec-parameter budgets;
1875/// - a synthesized attachment's `extradata` is charged to the
1876///   *attachment* budget and left out of the parameter one, because the
1877///   clone strips it and the carrier holds it — one set of bytes, one
1878///   charge;
1879/// - the attachment budgets then see:
1880///
1881/// - a hoisted cover-art track retains its parked `AVPacket`'s payload
1882///   *and* the extradata in its parameter copy, which the still decoder
1883///   may need and which is not a duplicate of the payload;
1884/// - a synthesized `AVMEDIA_TYPE_ATTACHMENT` track retains only the
1885///   carrier, because `build_tracks` strips the duplicate extradata out
1886///   of the parameter copy (see the comment there for the census).
1887///
1888/// Charging residency rather than payload is what keeps the budget an
1889/// honest statement about memory instead of about file structure.
1890///
1891/// The per-attachment ceiling is judged first for each track: when a
1892/// single payload is itself over the line, that is the more specific
1893/// fact, and naming the aggregate instead would send a reader looking
1894/// for four hundred attachments that are not there.
1895fn admit_streams(input: &Input, limits: DemuxLimits) -> Result<(), DemuxError> {
1896  let mut attachment_spent: usize = 0;
1897  let mut parameter_spent: usize = 0;
1898
1899  for stream in input.streams() {
1900    let index = stream.index();
1901    let parameters = stream.parameters();
1902    // SAFETY: `parameters` keeps the `AVCodecParameters` live for this
1903    // measurement, which allocates nothing and dereferences only what
1904    // it counts.
1905    let par = unsafe { parameters.as_ptr() };
1906    if par.is_null() {
1907      return Err(DemuxError::ParametersMissing(ParametersMissing::new(index)));
1908    }
1909    let footprint =
1910      unsafe { crate::extras::measure_parameters(par) }.ok_or(DemuxError::ParametersTooLarge(
1911        ParametersTooLarge::new(index, usize::MAX, limits.max_codec_parameter_bytes()),
1912      ))?;
1913
1914    // SAFETY: `stream` keeps the `AVStream` live; `disposition` is a
1915    // public field.
1916    let disposition = unsafe { (*stream.as_ptr()).disposition };
1917    let cover_art = is_attachment_disposition(disposition);
1918    let synthesized = !cover_art && boundary::media_kind_of(&parameters).is_attachment();
1919
1920    // What the *parameter clone* will retain for this stream. The
1921    // synthesized-attachment road strips `extradata` — the font's
1922    // payload rides the carrier instead — so counting it here would
1923    // charge the same bytes twice and make the budget a statement about
1924    // the file rather than about memory.
1925    let retained_parameters = if synthesized {
1926      footprint.total_without_extradata()
1927    } else {
1928      footprint.total()
1929    }
1930    .ok_or(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1931      index,
1932      usize::MAX,
1933      limits.max_codec_parameter_bytes(),
1934    )))?;
1935
1936    if retained_parameters > limits.max_codec_parameter_bytes() {
1937      return Err(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1938        index,
1939        retained_parameters,
1940        limits.max_codec_parameter_bytes(),
1941      )));
1942    }
1943    parameter_spent = parameter_spent.saturating_add(retained_parameters);
1944    if parameter_spent > limits.max_total_codec_parameter_bytes() {
1945      return Err(DemuxError::ParametersBudgetExhausted(
1946        ParametersBudgetExhausted::new(
1947          index,
1948          parameter_spent,
1949          limits.max_total_codec_parameter_bytes(),
1950        ),
1951      ));
1952    }
1953
1954    // And what the *carrier* will hold, for the two attachment roads.
1955    let carrier = if cover_art {
1956      // SAFETY: `attached_pic` is an `AVPacket` embedded in the
1957      // `AVStream` by value; `addr_of!` reaches its `size` without
1958      // forming a reference to the stream.
1959      unsafe {
1960        let pkt = std::ptr::addr_of!((*stream.as_ptr()).attached_pic);
1961        (*pkt).size
1962      }
1963      .max(0) as usize
1964    } else if synthesized {
1965      // The **payload**, not the padded clone figure. The carrier is
1966      // an `FfmpegBytes` over exactly these bytes and the clone omits
1967      // extradata entirely on this road, so nothing here allocates the
1968      // padding — charging it would bill sixty-four bytes that are
1969      // never spent, reject a payload in the last sixty-four below the
1970      // ceiling, and disagree with the image road about the same file
1971      // at exactly the cap.
1972      footprint.extradata_payload()
1973    } else {
1974      // Not an attachment: nothing is captured eagerly for it, so
1975      // nothing more is charged.
1976      continue;
1977    };
1978
1979    charge_attachment(index, carrier, limits, &mut attachment_spent)?;
1980  }
1981  Ok(())
1982}
1983
1984/// Charges `declared` bytes against both attachment budgets, refusing
1985/// before anything is copied. The one place a file's attachment
1986/// spending is decided; see [`admit_streams`] for when it runs.
1987fn charge_attachment(
1988  index: usize,
1989  declared: usize,
1990  limits: DemuxLimits,
1991  spent: &mut usize,
1992) -> Result<(), DemuxError> {
1993  if declared > limits.max_attachment_bytes() {
1994    return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
1995      index,
1996      declared,
1997      limits.max_attachment_bytes(),
1998    )));
1999  }
2000  let total = spent.saturating_add(declared);
2001  if total > limits.max_total_attachment_bytes() {
2002    return Err(DemuxError::AttachmentBudgetExhausted(
2003      AttachmentBudgetExhausted::new(index, total, limits.max_total_attachment_bytes()),
2004    ));
2005  }
2006  *spent = total;
2007  Ok(())
2008}
2009
2010/// A stream's `AVRational` timebase as a [`Timebase`]. A zero or
2011/// negative denominator is clamped to 1 rather than refused: a
2012/// malformed timebase makes the track's timestamps meaningless, not the
2013/// file unreadable, and every other track still demuxes.
2014fn rational_to_timebase(value: Rational) -> Timebase {
2015  Timebase::new(
2016    value.numerator(),
2017    NonZeroI32::new(value.denominator().max(1)).expect("clamped to at least 1"),
2018  )
2019}
2020
2021/// A frame *rate* as a rate-shaped [`Timebase`] (`30000/1001` for
2022/// 29.97 fps), or `None` when the container declares none.
2023fn rate_to_timebase(value: Rational) -> Option<Timebase> {
2024  let (num, den) = (value.numerator(), value.denominator());
2025  (num > 0 && den > 0).then(|| Timebase::new(num, NonZeroI32::new(den).expect("checked above")))
2026}
2027
2028#[cfg(test)]
2029mod tests {
2030  use ffmpeg_next::ffi::{av_dict_free, av_dict_set};
2031
2032  use ffmpeg_next::codec::Parameters;
2033
2034  use super::*;
2035  use crate::extras::TrackExtra;
2036
2037  /// Builds a dictionary holding one entry whose *value* is the given
2038  /// raw bytes. The bytes go in as a C string, which is all
2039  /// `av_dict_set` promises to copy — FFmpeg never asks whether they
2040  /// are UTF-8, which is the whole point of the lane below.
2041  fn dict_with(key: &CStr, value: &[u8]) -> *mut AVDictionary {
2042    let mut dict: *mut AVDictionary = std::ptr::null_mut();
2043    let mut terminated = value.to_vec();
2044    terminated.push(0);
2045    let rc = unsafe {
2046      av_dict_set(
2047        &mut dict,
2048        key.as_ptr(),
2049        terminated.as_ptr().cast::<std::ffi::c_char>(),
2050        0,
2051      )
2052    };
2053    assert!(rc >= 0, "av_dict_set failed: {rc}");
2054    dict
2055  }
2056
2057  #[test]
2058  fn metadata_that_is_not_utf8_is_read_lossily_not_unsoundly() {
2059    // The bytes a real container can hold: a Latin-1 "café.ttf" whose
2060    // 0xE9 is not valid UTF-8 on its own. Read through
2061    // `DictionaryRef::get` this produced a `&str` that violates the
2062    // type's invariant — undefined behaviour before `SmolStr` ever
2063    // copied it.
2064    let raw = b"caf\xE9.ttf".to_vec();
2065    assert!(
2066      std::str::from_utf8(&raw).is_err(),
2067      "the source bytes really are not UTF-8",
2068    );
2069    let dict = dict_with(c"filename", &raw);
2070    let text = unsafe { metadata_text(dict, c"filename") }.expect("the entry exists");
2071    assert_eq!(text.as_str(), "caf\u{FFFD}.ttf");
2072    // A key the dictionary does not hold, and a null dictionary, are
2073    // both simply absent.
2074    assert_eq!(unsafe { metadata_text(dict, c"mimetype") }, None);
2075    assert_eq!(
2076      unsafe { metadata_text(std::ptr::null(), c"filename") },
2077      None
2078    );
2079    unsafe { av_dict_free(&mut { dict }) };
2080  }
2081
2082  #[test]
2083  fn valid_metadata_survives_unchanged() {
2084    let dict = dict_with(c"mimetype", b"application/x-truetype-font");
2085    assert_eq!(
2086      unsafe { metadata_text(dict, c"mimetype") }.as_deref(),
2087      Some("application/x-truetype-font"),
2088    );
2089    unsafe { av_dict_free(&mut { dict }) };
2090  }
2091
2092  #[test]
2093  fn an_unterminated_length_is_refused_rather_than_truncated() {
2094    // Nothing libavutil produces is this long; the cap exists so a
2095    // value it did not terminate cannot walk off the end. A value that
2096    // reaches the cap is absent, never a prefix of itself.
2097    let long = vec![b'a'; METADATA_VALUE_MAX_BYTES + 1];
2098    let dict = dict_with(c"filename", &long);
2099    assert_eq!(unsafe { metadata_text(dict, c"filename") }, None);
2100    unsafe { av_dict_free(&mut { dict }) };
2101  }
2102
2103  /// A reader that panics with a payload whose destructor panics in
2104  /// turn. Both panics are safe code; the second one is what used to
2105  /// leave the guard and enter the `extern "C"` AVIO callback.
2106  struct PanicsWithAHostilePayload;
2107
2108  struct PanicOnDrop;
2109
2110  impl Drop for PanicOnDrop {
2111    fn drop(&mut self) {
2112      panic!("and the payload went too");
2113    }
2114  }
2115
2116  impl std::io::Read for PanicsWithAHostilePayload {
2117    fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
2118      std::panic::panic_any(PanicOnDrop);
2119    }
2120  }
2121
2122  impl std::io::Seek for PanicsWithAHostilePayload {
2123    fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
2124      std::panic::panic_any(PanicOnDrop);
2125    }
2126  }
2127
2128  #[test]
2129  fn a_reader_panic_with_a_hostile_payload_does_not_abort_the_process() {
2130    // In its own process, because the assertion *is* the process: a
2131    // parent that sees the child exit cleanly has seen the abort not
2132    // happen. The guard caught the reader's panic and then dropped its
2133    // payload outside `catch_unwind`, so a payload whose `Drop` panics
2134    // sent that second panic straight out of `read` and into C —
2135    // through the very guard that exists to stop it.
2136    crate::fault_subprocess::in_subprocess(
2137      "demuxer::tests::a_reader_panic_with_a_hostile_payload_does_not_abort_the_process",
2138      || {
2139        let previous = std::panic::take_hook();
2140        std::panic::set_hook(Box::new(|_| {}));
2141        let opened =
2142          CarrierDemuxer::<crate::Owned>::open_reader(PanicsWithAHostilePayload, Some("x.mkv"));
2143        std::panic::set_hook(previous);
2144        match opened {
2145          Err(DemuxError::ReaderPanic(_)) => {}
2146          Err(other) => panic!("expected ReaderPanic, got {other:?}"),
2147          Ok(_) => panic!("a reader that only panics cannot open a container"),
2148        }
2149      },
2150    );
2151  }
2152
2153  #[test]
2154  fn codec_parameters_that_cannot_be_allocated_are_named() {
2155    // `Parameters::new` does not check `avcodec_parameters_alloc`, and
2156    // `clone_from` dereferences the result immediately: under a failed
2157    // allocation the shipped clone would write through null.
2158    crate::fault_subprocess::in_subprocess(
2159      "demuxer::tests::codec_parameters_that_cannot_be_allocated_are_named",
2160      || {
2161        let source = Parameters::new();
2162        assert!(
2163          !unsafe { source.as_ptr() }.is_null(),
2164          "the source allocates before the cap goes on",
2165        );
2166        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2167        let refused = crate::extras::bounded_clone_parameters(&source, 4, usize::MAX);
2168        crate::fault_subprocess::uncap_ffmpeg_allocations();
2169        assert!(
2170          matches!(
2171            refused,
2172            Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 4
2173          ),
2174          "expected ParametersAlloc, got {:?}",
2175          refused.map(|_| ()),
2176        );
2177        // And with the cap lifted the same copy succeeds, so the
2178        // refusal was the allocator's answer and not a broken helper.
2179        crate::extras::bounded_clone_parameters(&source, 4, usize::MAX).expect("an uncapped copy");
2180      },
2181    );
2182  }
2183
2184  #[test]
2185  fn the_public_track_extra_handoffs_still_answer_the_allocator() {
2186    // The lane this replaces guarded a hazard that no longer exists:
2187    // `TrackExtra` derived `Clone` over `ffmpeg_next`'s `Parameters`,
2188    // whose clone dereferences an unchecked allocation, so safe public
2189    // code reached a SIGSEGV by copying a track row. The row holds no
2190    // `Parameters` at all now, and the two public handoffs have split
2191    // in kind because of it:
2192    //
2193    // * `Clone` allocates **nothing from FFmpeg** — it copies an
2194    //   owned mirror, which is a `Vec` spine and a refcount bump — so
2195    //   it survives a capped allocator rather than reporting through
2196    //   one. That is what makes the derive honest under the carrier
2197    //   law, and a capped allocator is the only way to pin it.
2198    // * `clone_parameters` is the rebuild, and it is where FFmpeg
2199    //   allocation moved to. It still answers.
2200    crate::fault_subprocess::in_subprocess(
2201      "demuxer::tests::the_public_track_extra_handoffs_still_answer_the_allocator",
2202      || {
2203        let source = Parameters::new();
2204        assert!(!unsafe { source.as_ptr() }.is_null(), "allocated uncapped");
2205        let extra = TrackExtra::new(
2206          6,
2207          crate::ticket::CodecTicket::mirror(&source, 6, usize::MAX).expect("uncapped"),
2208        );
2209
2210        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2211        let cloned = extra.clone();
2212        let handed = extra.clone_parameters().map(|_| ());
2213        crate::fault_subprocess::uncap_ffmpeg_allocations();
2214
2215        assert_eq!(
2216          cloned.parameter_bytes(),
2217          extra.parameter_bytes(),
2218          "the row cloned under an allocator that refuses everything",
2219        );
2220        assert!(
2221          matches!(handed, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
2222          "TrackExtra::clone_parameters: {handed:?}",
2223        );
2224
2225        // And the rebuild works once the allocator does.
2226        extra.clone_parameters().expect("an uncapped handoff");
2227      },
2228    );
2229  }
2230
2231  #[test]
2232  fn parameters_that_never_allocated_are_refused_at_the_door() {
2233    // The route the destination check could not see. A safe
2234    // `Parameters::new()` under a failed allocation hands back a
2235    // null-backed value and says nothing; the copier then allocated its
2236    // own destination happily — the allocator having recovered by
2237    // then — and called `avcodec_parameters_copy(out, NULL)`, which
2238    // dereferences its source. Same crash, one recovery later, still
2239    // from safe public code.
2240    crate::fault_subprocess::in_subprocess(
2241      "demuxer::tests::parameters_that_never_allocated_are_refused_at_the_door",
2242      || {
2243        // The cap is on *while the source is built* — that is the whole
2244        // difference from the destination lane.
2245        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2246        let never_allocated = Parameters::new();
2247        crate::fault_subprocess::uncap_ffmpeg_allocations();
2248        assert!(
2249          unsafe { never_allocated.as_ptr() }.is_null(),
2250          "the safe constructor really does hand back a null-backed value",
2251        );
2252
2253        // The door moved with the handle. `TrackExtra::new` no longer
2254        // takes a `Parameters` at all, so the only way a null-backed
2255        // one reaches a track row is through the mirror — which is
2256        // where the check now lives, and where it belongs: beside the
2257        // raw pointer rather than one type downstream of it.
2258        let refused = crate::ticket::CodecTicket::mirror(&never_allocated, 9, usize::MAX);
2259        let Err(DemuxError::ParametersMissing(p)) = refused.map(|_| ()) else {
2260          panic!("a null-backed source must not become a codec ticket");
2261        };
2262        assert_eq!(p.stream_index(), 9);
2263
2264        // And the copier refuses it too, so the invariant is not the
2265        // only thing standing between this and a null dereference.
2266        let never_allocated = {
2267          crate::fault_subprocess::cap_ffmpeg_allocations(1);
2268          let p = Parameters::new();
2269          crate::fault_subprocess::uncap_ffmpeg_allocations();
2270          p
2271        };
2272        assert!(matches!(
2273          crate::extras::bounded_clone_parameters(&never_allocated, 9, usize::MAX).map(|_| ()),
2274          Err(DemuxError::ParametersMissing(p)) if p.stream_index() == 9,
2275        ));
2276
2277        // A row built over real parameters still hands off both ways,
2278        // so the refusal is about the null and nothing else.
2279        let real = Parameters::new();
2280        let extra = TrackExtra::new(
2281          9,
2282          crate::ticket::CodecTicket::mirror(&real, 9, usize::MAX).expect("real parameters"),
2283        );
2284        let _ = extra.clone();
2285        extra.clone_parameters().expect("handoff");
2286      },
2287    );
2288  }
2289
2290  #[cfg(feature = "resample")]
2291  #[test]
2292  fn a_spec_read_from_parameters_that_never_allocated_is_absent() {
2293    // The same trap at another public door, found by the sweep:
2294    // `ResampleSpec::from_parameters` asks `parameters.medium()`
2295    // first, and *that* dereferences the pointer inside ffmpeg-next
2296    // before any code of ours runs.
2297    crate::fault_subprocess::in_subprocess(
2298      "demuxer::tests::a_spec_read_from_parameters_that_never_allocated_is_absent",
2299      || {
2300        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2301        let never_allocated = Parameters::new();
2302        crate::fault_subprocess::uncap_ffmpeg_allocations();
2303        assert!(unsafe { never_allocated.as_ptr() }.is_null());
2304        assert_eq!(
2305          crate::ResampleSpec::from_parameters(&never_allocated),
2306          None,
2307          "parameters that do not exist describe no audio",
2308        );
2309      },
2310    );
2311  }
2312
2313  #[test]
2314  fn codec_parameters_whose_copy_fails_are_named() {
2315    // The other leg: the destination allocates, and the deep copy of
2316    // the extradata does not. `clone_from` discards that return value,
2317    // so the shipped clone handed back parameters missing the very
2318    // bytes a decoder needs to open — and said nothing.
2319    crate::fault_subprocess::in_subprocess(
2320      "demuxer::tests::codec_parameters_whose_copy_fails_are_named",
2321      || {
2322        const EXTRADATA: usize = 8 * 1024 * 1024;
2323        let mut source = Parameters::new();
2324        // SAFETY: `source` owns a live `AVCodecParameters`; the buffer
2325        // comes from FFmpeg's allocator and is handed to it, so
2326        // `avcodec_parameters_free` releases it with the rest.
2327        unsafe {
2328          let par = source.as_mut_ptr();
2329          let extradata = ffmpeg_next::ffi::av_mallocz(EXTRADATA) as *mut u8;
2330          assert!(!extradata.is_null(), "av_mallocz");
2331          (*par).extradata = extradata;
2332          (*par).extradata_size = EXTRADATA as i32;
2333        }
2334
2335        // Big enough for the destination `AVCodecParameters`, far too
2336        // small for its extradata.
2337        crate::fault_subprocess::cap_ffmpeg_allocations(64 * 1024);
2338        let refused = crate::extras::bounded_clone_parameters(&source, 2, usize::MAX);
2339        crate::fault_subprocess::uncap_ffmpeg_allocations();
2340        match refused {
2341          Err(DemuxError::ParametersCopy(p)) => assert_eq!(p.stream_index(), 2),
2342          Err(other) => panic!("expected ParametersCopy, got {other:?}"),
2343          Ok(_) => panic!("a copy that could not copy the extradata must not succeed"),
2344        }
2345        crate::extras::bounded_clone_parameters(&source, 2, usize::MAX).expect("an uncapped copy");
2346      },
2347    );
2348  }
2349
2350  /// A stream whose `attached_pic` is `parked`, and the packet
2351  /// libavformat would queue for it.
2352  ///
2353  /// # On the fixture road
2354  ///
2355  /// The container shape this guards — a stream carrying
2356  /// `ATTACHED_PIC | TIMED_THUMBNAILS` — **cannot be minted by the
2357  /// ffmpeg CLI**, and that was censused rather than assumed: no muxer
2358  /// has a field for those bits (`-disposition:v
2359  /// attached_pic+timed_thumbnails` round-trips to nothing through
2360  /// mp4, mov and matroska alike), because the mov *demuxer* derives
2361  /// them from a chapter-track reference its own muxer does not write
2362  /// in that direction.
2363  ///
2364  /// What is reproducible, and what actually matters, is the **packet
2365  /// shape**: `read_frame_internal` queues a stream's parked picture
2366  /// with `av_packet_ref` while keeping its own reference, which is
2367  /// exactly what `av_packet_ref` builds here. The classification half
2368  /// — that such a stream is video rather than an attachment — is
2369  /// pinned separately by
2370  /// [`a_timed_thumbnail_stream_is_not_an_attachment`].
2371  fn parked_picture_stream(parked: &Packet) -> (Box<AVStream>, Packet) {
2372    use ffmpeg_next::packet::{Mut, Ref};
2373
2374    let mut stream: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
2375    let mut queued = Packet::empty();
2376    // SAFETY: `parked` is a live refcounted packet; `av_packet_ref`
2377    // takes a reference to its buffer, which is precisely what
2378    // libavformat does when it queues an attached picture. The stream
2379    // is zeroed apart from the one field the probe reads.
2380    unsafe {
2381      assert_eq!(
2382        ffmpeg_next::ffi::av_packet_ref(queued.as_mut_ptr(), parked.as_ptr()),
2383        0,
2384      );
2385      stream.attached_pic.buf = (*parked.as_ptr()).buf;
2386      stream.attached_pic.data = (*parked.as_ptr()).data;
2387      stream.attached_pic.size = (*parked.as_ptr()).size;
2388    }
2389    (stream, queued)
2390  }
2391
2392  #[test]
2393  fn a_queued_attached_picture_is_recognised() {
2394    use ffmpeg_next::packet::Ref;
2395
2396    let parked = Packet::copy(&[9u8; 2048]);
2397    let (stream, queued) = parked_picture_stream(&parked);
2398
2399    // The two references are different structs around one allocation —
2400    // which is the whole reason the probe compares `buffer` and not the
2401    // `AVBufferRef`. Asserting the difference is what makes this a test
2402    // of the right comparison rather than of a lucky one.
2403    // SAFETY: both packets are live.
2404    unsafe {
2405      assert_ne!(
2406        (*queued.as_ptr()).buf,
2407        (*parked.as_ptr()).buf,
2408        "av_packet_ref must mint a new reference struct",
2409      );
2410    }
2411    // SAFETY: the stream is a zeroed `AVStream` whose only populated
2412    // fields are the ones the probe reads, and `queued` is live.
2413    assert!(unsafe { packet_is_parked_picture(&*stream, &queued) });
2414
2415    // An ordinary timed packet — the shape every pull after the first
2416    // one has — is not the parked picture.
2417    let ordinary = Packet::copy(&[1u8; 2048]);
2418    // SAFETY: as above.
2419    assert!(!unsafe { packet_is_parked_picture(&*stream, &ordinary) });
2420
2421    // And a stream that parks nothing recognises nothing.
2422    let bare: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
2423    // SAFETY: as above.
2424    assert!(!unsafe { packet_is_parked_picture(&*bare, &queued) });
2425  }
2426
2427  #[test]
2428  fn the_queued_picture_is_admitted_and_later_packets_take_the_ordinary_road() {
2429    use crate::buffer::{PacketBufferError, PayloadProvenance, payload_of};
2430    use ffmpeg_next::packet::Ref;
2431
2432    let parked = Packet::copy(&[9u8; 2048]);
2433    let (_stream, queued) = parked_picture_stream(&parked);
2434    // SAFETY: the packet is live; `buf` is a public field.
2435    let parked_buffer = unsafe { (*parked.as_ptr()).buf };
2436
2437    // **The first pull.** Two references, one of them the container's.
2438    // From a *caller* that shape is refused, because a caller's second
2439    // reference may be a `Packet` with a safe `data_mut`.
2440    // SAFETY: `queued` is live for every call in this test.
2441    assert!(matches!(
2442      unsafe {
2443        payload_of::<crate::View>(
2444          queued.as_ptr(),
2445          usize::MAX,
2446          PayloadProvenance::CallerSupplied,
2447        )
2448      },
2449      Err(PacketBufferError::SharedPayload(_)),
2450    ));
2451
2452    // Delivered by the demux loop, the same shape is carried — by copy,
2453    // because a window would outlive the exclusivity the read rests on.
2454    // SAFETY: as above.
2455    let copied = unsafe {
2456      payload_of::<crate::View>(
2457        queued.as_ptr(),
2458        usize::MAX,
2459        PayloadProvenance::DemuxDelivered,
2460      )
2461    }
2462    .expect("a demux-delivered shared payload is carriable")
2463    .expect("it has a payload");
2464    assert_eq!(copied.as_ref(), &[9u8; 2048][..]);
2465    // SAFETY: the packet is live; `data` is a public field.
2466    unsafe {
2467      assert_ne!(
2468        copied.as_ref().as_ptr() as usize,
2469        (*queued.as_ptr()).data as usize,
2470        "a shared demux-delivered payload is copied, not windowed",
2471      );
2472    }
2473
2474    // With the provenance the probe establishes, both lanes carry it.
2475    // SAFETY: as above.
2476    let viewed = unsafe {
2477      payload_of::<crate::View>(
2478        queued.as_ptr(),
2479        usize::MAX,
2480        PayloadProvenance::AttachedPicture,
2481      )
2482    }
2483    .expect("the container's own picture is carriable")
2484    .expect("it has a payload");
2485    assert_eq!(viewed.as_ref(), &[9u8; 2048][..]);
2486    // And on the view lane it is a window into the parked allocation
2487    // rather than a copy of it.
2488    // SAFETY: both are live; `data`/`size` are public fields.
2489    unsafe {
2490      let start = (*parked_buffer).data as usize;
2491      let end = start + (*parked_buffer).size;
2492      let at = viewed.as_ref().as_ptr() as usize;
2493      assert!(
2494        at >= start && at + viewed.len() <= end,
2495        "the queued picture must be viewed, not copied",
2496      );
2497    }
2498    // SAFETY: as above.
2499    let owned = unsafe {
2500      payload_of::<crate::Owned>(
2501        queued.as_ptr(),
2502        usize::MAX,
2503        PayloadProvenance::AttachedPicture,
2504      )
2505    }
2506    .expect("the owned lane carries it too")
2507    .expect("it has a payload");
2508    assert_eq!(owned.as_ref(), &[9u8; 2048][..]);
2509
2510    // **Every pull after it.** A timed packet has a buffer of its own,
2511    // so it stays on the `Delivered` road, is unique, and the view lane
2512    // shares it.
2513    let later = Packet::copy(&[4u8; 1024]);
2514    // SAFETY: `later` is live.
2515    let shared = unsafe {
2516      payload_of::<crate::View>(
2517        later.as_ptr(),
2518        usize::MAX,
2519        PayloadProvenance::DemuxDelivered,
2520      )
2521    }
2522    .expect("an ordinary packet is carriable")
2523    .expect("it has a payload");
2524    // SAFETY: as above.
2525    unsafe {
2526      assert_eq!(
2527        shared.as_ref().as_ptr() as usize,
2528        (*later.as_ptr()).data as usize,
2529        "a uniquely-referenced packet is still shared, not copied",
2530      );
2531    }
2532  }
2533
2534  #[test]
2535  fn a_timed_thumbnail_stream_is_not_an_attachment() {
2536    // `TIMED_THUMBNAILS` is documented as only ever appearing beside
2537    // `ATTACHED_PIC`, so testing the picture bit alone reads a sparse
2538    // chapter-thumbnail track as cover art — and the attachment
2539    // contract then delivers exactly one of its images and drops the
2540    // rest, every one of which had a timestamp.
2541    assert!(
2542      is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC),
2543      "a plain attached picture is still an attachment",
2544    );
2545    assert!(
2546      !is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC | AV_DISPOSITION_TIMED_THUMBNAILS),
2547      "a timed-thumbnail stream is a timed track, whatever else it is flagged",
2548    );
2549    // Neither bit, and the other bits that ride along, change nothing.
2550    assert!(!is_attachment_disposition(0));
2551    assert!(!is_attachment_disposition(AV_DISPOSITION_TIMED_THUMBNAILS));
2552    assert!(is_attachment_disposition(
2553      AV_DISPOSITION_ATTACHED_PIC | ffmpeg_next::ffi::AV_DISPOSITION_DEFAULT
2554    ));
2555    // And the reason the raw bits are read at all: the wrapper's own
2556    // flag set cannot express the distinction.
2557    assert!(
2558      ffmpeg_next::format::stream::Disposition::from_bits(AV_DISPOSITION_TIMED_THUMBNAILS)
2559        .is_none(),
2560      "ffmpeg_next mints no TIMED_THUMBNAILS bit — from_bits_truncate would drop it silently",
2561    );
2562  }
2563
2564  #[test]
2565  fn an_uncapturable_cover_still_gets_its_one_packet() {
2566    // The state the shipped `AwaitingPacket` fallback existed for: a
2567    // stream that declares cover art and parks no payload. The fallback
2568    // waited for a packet that may never come, and let timed packets —
2569    // and seeks — go first, which the face forbids. The track now gets
2570    // its one packet at open like every other attachment track: empty,
2571    // and marked as this layer's own work.
2572    //
2573    // Not reachable from a file: across MP3, M4A, FLAC and Matroska,
2574    // every ATTACHED_PIC stream libavformat produces carries the parked
2575    // packet, because `ff_add_attached_pic` sets the disposition and
2576    // fills it in the same call. A zeroed `AVPacket` is exactly what
2577    // `attached_pic` would hold if one ever did not.
2578    let empty: ffmpeg_next::ffi::AVPacket = unsafe { std::mem::zeroed() };
2579    let packet = unsafe { attached_pic_payload::<crate::Owned>(&empty, 7, DemuxLimits::default()) }
2580      .expect("an unparked cover is a degenerate track, not an unreadable file");
2581    assert!(packet.data().as_ref().is_empty());
2582    assert!(
2583      packet.extra().synthesized(),
2584      "nothing in the container handed this payload over",
2585    );
2586    assert_eq!(packet.extra().stream_index(), 7);
2587  }
2588
2589  #[test]
2590  fn a_zero_denominator_timebase_is_clamped_not_refused() {
2591    // A malformed timebase makes one track's timestamps meaningless.
2592    // It must not make the file unreadable — every other track still
2593    // demuxes, and the caller can see the 1/1 for what it is.
2594    let tb = rational_to_timebase(Rational::new(1, 0));
2595    assert_eq!(tb.den().get(), 1);
2596    assert_eq!(tb.num(), 1);
2597  }
2598
2599  #[test]
2600  fn a_declared_frame_rate_becomes_a_rate_shaped_timebase() {
2601    let ntsc = rate_to_timebase(Rational::new(30_000, 1001)).expect("declared");
2602    assert_eq!((ntsc.num(), ntsc.den().get()), (30_000, 1001));
2603    assert_eq!(
2604      rate_to_timebase(Rational::new(0, 1)),
2605      None,
2606      "0 fps is absent"
2607    );
2608    assert_eq!(
2609      rate_to_timebase(Rational::new(30, 0)),
2610      None,
2611      "no denominator"
2612    );
2613  }
2614
2615  #[test]
2616  fn the_seek_timebase_is_microseconds() {
2617    // `avformat_seek_file` with `stream_index == -1` takes AV_TIME_BASE
2618    // units; a target expressed in anything else has to arrive there.
2619    let tb = av_time_base_q();
2620    assert_eq!((tb.num(), tb.den().get()), (1, 1_000_000));
2621    let target = Timestamp::new(1_500, Timebase::new(1, NonZeroI32::new(1000).expect("ms")));
2622    assert_eq!(target.rescale_to(tb).pts(), 1_500_000);
2623  }
2624}