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