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