mediadecode_ffmpeg/buffer.rs
1//! The **amputation seam**: FFmpeg's bytes leave here, copied once,
2//! as Rust-owned memory.
3//!
4//! An `AVPacket`'s payload and an `AVFrame`'s planes both live in
5//! `AVBufferRef`s — FFmpeg's own refcounted allocations. Through 0.8
6//! this crate handed those out directly, wrapped in an `FfmpegBuffer`
7//! whose `AsRef<[u8]>` pointed straight into libavcodec's memory. That
8//! type is gone. Every byte that crosses this boundary is now copied
9//! into an [`FfmpegBytes`], which is what the core's
10//! [D-seat amputation contract][law] requires: owned, `Send + Sync`,
11//! clone-is-a-refcount-bump, and with no FFmpeg lifetime riding along.
12//!
13//! What is left in this module is everything the copy still has to
14//! *judge*. A packet's payload has to be proved to lie inside the
15//! buffer that owns it before a byte of it is read, its side data has
16//! to be carried whole or refused, and its flags have to fit the
17//! portable set — so [`PacketBufferError`] and its payload structs
18//! outlive the buffer type they were written for. The bounds check in
19//! particular matters *more* now, not less: 0.8 formed a view over the
20//! claimed range, 0.9 reads it.
21//!
22//! # The one thing the amputation costs
23//!
24//! The `Arc<[u8]>` behind [`FfmpegBytes`] has no fallible constructor
25//! on stable Rust, so the copy itself aborts on allocation failure
26//! rather than returning an error.
27//! Everything that bounds *how much* can be asked for — the side-data
28//! entry and byte caps, the plane-geometry checks — is unchanged and
29//! still runs before any allocation, so a hostile stream cannot reach
30//! that abort by demanding memory; only a genuinely exhausted
31//! allocator can.
32//!
33//! [`payload_of`] is where the per-packet half of that bounding lives:
34//! every packet body this crate copies passes through it, and it
35//! refuses an over-budget claim before reading a byte. See
36//! [`crate::limits`] for the budgets and their defaults.
37//!
38//! # The funnel's accounting
39//!
40//! Every [`FfmpegBytes`] in this crate is built by
41//! [`FfmpegBytes::copy_from_slice`], [`FfmpegBytes::from_rows`] or
42//! [`FfmpegBytes::empty`], and **every one of those call sites is
43//! bounded before it allocates**. The table is kept here, beside the
44//! constructors, so that a new exit has to answer the question the
45//! existing ones already answered — the discipline is inherited by
46//! being written down where the next author will be standing.
47//!
48//! Three review rounds found bypasses that each looked like an
49//! exception: a plane path with no ceiling, an attachment whose
50//! payload was copied by `avcodec_parameters_copy` before its budget
51//! was charged, a resampler amplifying a small input into a huge
52//! output, and then `coded_side_data` — a third heap seat on the same
53//! wholesale parameter copy, where a MOV `prof` atom puts an ICC
54//! profile. None of them were exceptions. They were rows nobody had
55//! written down.
56//!
57//! The third one is why the table below has a section it did not need
58//! at first. `FfmpegBytes` is not the only place this crate copies
59//! attacker-sized bytes: `AVCodecParameters` has heap seats of its own,
60//! and the wholesale FFI copy that used to duplicate them took every
61//! one — including any this crate had never enumerated. That copy is
62//! gone; see
63//! [`bounded_clone_parameters`](crate::extras::bounded_clone_parameters),
64//! and [`CodecTicket`](crate::CodecTicket), which took the track row's
65//! copy one step further: those three seats are now owned Rust, and
66//! `extradata` lands in an `FfmpegBytes` like every other file-sized
67//! buffer here.
68//!
69//! | construction site | what it carries | what bounds it |
70//! |---|---|---|
71//! | [`payload_of`] | any packet's payload | its `budget` argument — [`PacketLimits::max_packet_bytes`](crate::PacketLimits::max_packet_bytes) for timed packets, [`DemuxLimits::max_attachment_bytes`](crate::DemuxLimits::max_attachment_bytes) for attachments — judged against the declared `size` before a byte is read |
72//! | `convert::copy_out_planes`, tight stride | one video or image plane | [`FrameLimits::max_pixels`](crate::FrameLimits::max_pixels) and [`FrameLimits::max_frame_bytes`](crate::FrameLimits::max_frame_bytes), both in a judge-pass that runs before any plane is allocated; `max_pixels` also reaches libavcodec |
73//! | `convert::copy_out_planes`, padded stride | one compacted plane, via [`FfmpegBytes::from_rows`] | the same pre-pass |
74//! | `convert::av_frame_to_audio_frame` | one audio plane | `max_frame_bytes`, checked over `plane_bytes × plane_count` before the loop |
75//! | `convert::collect_side_data` | one frame side-data entry | `SIDE_DATA_MAX_ENTRIES` (64) and `SIDE_DATA_MAX_TOTAL_BYTES` (256 KiB), plus `try_reserve_exact` |
76//! | `boundary::packet_side_data` | one packet side-data entry | the same two caps, as refusals rather than truncation |
77//! | `convert::av_subtitle_to_subtitle_frame`, text | concatenated cue text | `SUBTITLE_MAX_TEXT_BYTES_PER_RECT` (64 KiB), `SUBTITLE_MAX_TEXT_TOTAL_BYTES` (256 KiB), `SUBTITLE_MAX_RECTS` (64) |
78//! | …, bitmap | one paletted rect | `SUBTITLE_MAX_BITMAP_BYTES_PER_RECT` (16 MiB), `SUBTITLE_MAX_BITMAP_TOTAL_BYTES` (32 MiB), `SUBTITLE_MAX_RECTS` |
79//! | …, palette | an RGBA palette | structurally fixed at 256 × 4 bytes by the format |
80//! | `demuxer::extradata_payload` | a synthesized attachment (a font) | `demuxer::admit_attachments`, which charges every attachment in the file — per-attachment **and** aggregate — before the track loop allocates anything; re-checked here against the per-attachment ceiling |
81//! | `demuxer::attached_pic_payload` | a hoisted cover-art packet | the same admission pass, then `payload_of`'s budget |
82//! | `resampler::finish_output` | one converted audio plane | `FfmpegResampler::check_output_bytes`, against `max_frame_bytes`, run before the output `AVFrame` is allocated |
83//! | every [`FfmpegBytes::empty`] site | nothing | structurally zero: placeholder plane slots, a payload-less packet, a null palette, a marker side-data entry |
84//!
85//! # The other heap this crate copies
86//!
87//! `AVCodecParameters` is the same class of exposure — three heap
88//! seats, all sized by the file — so its rows belong in the same
89//! accounting. Since the track row went owned, one of those seats
90//! *is* an [`FfmpegBytes`]: [`CodecTicket`](crate::CodecTicket) holds
91//! `extradata` in one, and each `coded_side_data` payload in another.
92//!
93//! | construction site | what it carries | what bounds it |
94//! |---|---|---|
95//! | [`CodecTicket::mirror`](crate::CodecTicket::mirror), `extradata` | SPS/PPS and codec headers, into an [`FfmpegBytes`] | [`DemuxLimits::max_codec_parameter_bytes`](crate::DemuxLimits::max_codec_parameter_bytes), measured by `measure_parameters` before a byte is read |
96//! | …, `coded_side_data` | the descriptor array and each entry's payload — a MOV `prof` atom's ICC profile among them — into owned entries | the same seat, counting the array as well as the payloads |
97//! | …, `ch_layout` custom map | a channel map, into owned entries | the same seat |
98//! | [`CodecTicket::rebuild`](crate::CodecTicket::rebuild) | the same three seats, back into a fresh `AVCodecParameters` for a decoder to open from | nothing further, and nothing further is needed: it allocates exactly [`CodecTicket::footprint_bytes`](crate::CodecTicket::footprint_bytes), which is the number the mirror already admitted — no file-controlled input reaches it |
99//! | [`bounded_clone_parameters`](crate::extras::bounded_clone_parameters) | the same three seats, `AVCodecParameters` to `AVCodecParameters` | the same measurement. Off the demux road since the track row went owned; it is the decoder's own re-clone (`decoder::try_clone_parameters`), bounded by [`DecoderLimits::max_codec_parameter_bytes`](crate::DecoderLimits::max_codec_parameter_bytes) |
100//! | `demuxer::admit_streams` | nothing — it only measures | runs over **every** stream before the track loop mirrors anything, and charges the whole-file [`max_total_codec_parameter_bytes`](crate::DemuxLimits::max_total_codec_parameter_bytes) |
101//! | `decoder::build_codec_context` → `avcodec_parameters_to_context` | the same three seats, copied *into* an `AVCodecContext` | **the choke point**: measured and admitted against [`DecoderLimits::max_codec_parameter_bytes`](crate::DecoderLimits::max_codec_parameter_bytes) right there. Every decoder in this crate opens through this function — the four session `open`s, the HW probe's `build_state`, its per-backend advances, the software fallback — and none of them reaches `avcodec_parameters_to_context` any other way |
102//! | `image::FfmpegImageDecoder::decode` → `boundary::try_packet_copy` | the caller's compressed bytes, duplicated into an `AVPacket` | [`DecoderLimits::max_image_input_bytes`](crate::DecoderLimits::max_image_input_bytes), defaulting to the attachment family so the direct road is no more permissive than the demuxed one |
103//! | `boundary::ffmpeg_packet_from_{video,audio,subtitle}_packet` → `try_packet_copy` | the caller's compressed bytes, duplicated into an `AVPacket` — **the send leg** | [`DecoderLimits::max_packet_bytes`](crate::DecoderLimits::max_packet_bytes), judged before the allocation. The same seat the receive leg (`payload_of`) judges, so a byte count refused coming out of a container is refused going into a decoder |
104//! | still `pal8` palette plane | a fixed `AVPALETTE_SIZE` run | the **format**, not a seat: 256 × `AV_PIX_FMT_RGB32`, always, with no number a file gets to choose |
105//!
106//! # The rule
107//!
108//! **A carrier whose size comes from a file is bounded by a seat in
109//! [`crate::limits`]; a carrier whose size is a property of a format is
110//! bounded by that format.** There is no third kind, and a site that
111//! looks like one has not been thought about yet.
112//!
113//! And the corollary the third round bought: **no code path hands
114//! attacker-sized data to a wholesale FFI copy** — a copy that
115//! duplicates every field of a struct duplicates the fields nobody
116//! enumerated, which is a budget bypass that arrives with the next
117//! FFmpeg release rather than with the next commit.
118//!
119//! # The substrate's knobs, and where this crate stops
120//!
121//! Everything above is **tier one** of the [resource governance
122//! contract][gov]: allocations this crate makes itself, each bounded by
123//! a named seat or by a format. This table is that tier's proof.
124//!
125//! Tier two is the other half — FFmpeg's own resource knobs, set at
126//! every point libavcodec and libavformat offer one. They bound
127//! allocations this crate does not make and could not otherwise see:
128//!
129//! | knob | where it is set | what it bounds |
130//! |---|---|---|
131//! | `AVCodecContext.max_pixels` | every opened decoder | the caller's pixel limit, **verbatim**, applied by `ff_set_dimensions` to the raw dimensions. Extent, not cost: what a frame *costs* is the byte judge's question, two rows down |
132//! | the `get_format` coded-dims ask | the hardware road | the **pool's own declared extent**, asked of `avcodec_get_hw_frames_parameters` before the pool is initialised — `max_pixels` is applied to the *display* dims, which a cropped stream can make 2000x smaller |
133//! | the `get_format` byte judge | the hardware road | the **pool's** cost, priced through [`crate::footprint`] against `max_frame_bytes`. **Fails closed**: a pool that will not declare its dimensions and layout is a pool that cannot be judged, and the codec-alignment fallback that used to stand in could answer *smaller* than the pool it was standing in for |
134//! | the `get_buffer2` byte judge | every software decode | what the allocator will actually take for this frame — pictures and audio both, priced through [`crate::footprint`] against the caller's own `max_frame_bytes`, carried in the codec context's callback state |
135//! | the pre-transfer judge | every `av_hwframe_transfer_data` | the CPU destination a hardware download allocates, priced at the frames-context pool dims — folding **every** candidate format FFmpeg may pick, priceable or not, since FFmpeg does the picking |
136//! | `probesize` / `formatprobesize` | both demux entrypoints | what the format probe and stream analysis may consume |
137//! | `max_streams` | both demux entrypoints | the `AVStream` array a header can conjure |
138//! | the `AVIOContext` byte meter | the **reader** demux entrypoint | total bytes libavformat is handed, hard — past the budget the reader stops answering |
139//!
140//! Two of those knobs used to carry *translated* byte ceilings —
141//! `max_pixels` as `min(the caller's limit, bytes / 16)` and
142//! `max_samples` as `bytes / 8` — so that the byte budget could bite
143//! before libavcodec allocated. Both translations charged every stream
144//! the worst format in existence, and both over-refused ordinary media:
145//! a 1920x1080 `yuv420p` frame under a 4 MiB budget, a 6-channel `s16`
146//! frame under 64 KiB. They are gone. The byte budget is enforced by
147//! the `get_buffer2` judge, which is *itself* a pre-allocation seat —
148//! `get_buffer2` **is** the allocation — and prices the frame's real
149//! format at its real dimensions. An exact judge at the allocation
150//! beats an approximate one before it.
151//!
152//! Where a layout cannot be priced at all, these judges charge
153//! [`crate::footprint::video_frame_bytes_upper_bound`] — the same
154//! dimension alignment and per-plane overhead at the widest per-pixel
155//! rate the census finds — rather than a bare `w * h * rate`, which
156//! omits both and could land *below* the accurate path it was standing
157//! in for. A conservative fallback that can under-state is not
158//! conservative.
159//!
160//! **These are defense in depth, not a proof.** Each bounds what it was
161//! built to bound; together they cover every interposition point FFmpeg
162//! exposes, which is not the same as covering FFmpeg.
163//!
164//! ## What the demux seats cannot reach, and why they exist anyway
165//!
166//! `avformat_open_input` and `avformat_find_stream_info` build the
167//! attached picture, the extradata and the coded side data out of the
168//! file themselves. The attachment and parameter seats in the table
169//! above therefore measure this crate's *copies* of buffers libavformat
170//! has already allocated — too late, by construction, to have prevented
171//! the original.
172//!
173//! A parser cannot allocate from bytes it was never handed, so the
174//! input is bounded instead: that is what the probe knobs and the byte
175//! meter are for. What is **not** bounded is allocation *amplification*
176//! inside a parser — a container can describe, in a handful of bytes, a
177//! structure whose in-memory form is far larger, and nothing outside
178//! libavformat can observe it happen. Bounding that output is the
179//! substrate's own hardening territory; FFmpeg keeps `max_streams`,
180//! `max_index_size` and `max_picture_buffer` for it, and this crate
181//! sets the first.
182//!
183//! The hard meter also does not reach the **path** entrypoint: it needs
184//! an `AVIOContext` this crate owns, and a path is opened by
185//! libavformat's own protocol layer. The probe knobs still apply there;
186//! a caller who wants the meter on a file opens it as a reader.
187//!
188//! That gap is **tier three**, and it is named rather than hedged: see
189//! the [contract][gov] for the boundary and for the OS-level instrument
190//! a deployment needing a hard memory bound puts underneath all of
191//! this. This crate is not a hypervisor for FFmpeg, and its seats
192//! compose with that instrument rather than replacing it.
193//!
194//! [gov]: mediadecode::adapter#the-resource-governance-contract
195//!
196//! And the capstone, which is what every seat in this table is finally
197//! for: **a judge must dominate the allocator's arithmetic, not the
198//! payload's.** A budget compared against what the bytes weigh is not a
199//! budget on what will be spent — see [`crate::footprint`] for the
200//! measured gap and for the two judges that were caught paying it.
201//!
202//! And the corollary the ninth bought, which is about *whether* to
203//! carry at all rather than how much: **a payload that carries
204//! addresses instead of bytes is uncarriable.** `AV_PKT_FLAG_TRUSTED`
205//! marks one — the wrapped-`AVFrame` producers use it for a body that
206//! is an `AVFrame` pointer structure — and copying it mints a carrier
207//! that passes every property this table exists to guarantee and
208//! dangles the moment its source drops. It is refused on both legs
209//! ([`payload_of`] and the reverse builders), because either alone
210//! leaves the loop open. See [`TrustedPayload`].
211//!
212//! And the corollary the seventh bought, about the *inputs* to every
213//! guard above rather than the guards themselves: **a number a file
214//! chooses is judged or refused, never clipped.** A seat that bounds a
215//! byte product still trusts the fields the product is computed from,
216//! so a clamped sample count or channel count does not trip any budget
217//! — it produces a smaller, plausible frame that no ceiling has any
218//! reason to stop. Two of those were live on the audio path (a floored
219//! negative `nb_samples`, a channel count clipped to `u8::MAX`), and
220//! both turned a malformed header into a well-formed-looking frame,
221//! which is strictly worse than an error. The audio road now carries no
222//! lossy clamp; the one floor left, `sample_rate`, is censused at its
223//! site with the reason it is metadata and sizes nothing.
224//!
225//! [law]: mediadecode::adapter#the-d-seat-amputation-contract
226
227use std::fmt;
228
229use derive_more::{IsVariant, TryUnwrap, Unwrap};
230
231/// The bytes every packet and frame this crate produces are carried in.
232///
233/// Owned, `Send + Sync`, `'static`, and clone-is-a-refcount-bump: the
234/// core's [D-seat amputation contract][law], satisfied. Nothing inside
235/// reaches back into libavcodec.
236///
237/// # Why it is opaque
238///
239/// The obvious spelling was the bare `Arc<[u8]>` this type wraps, and
240/// 0.9.0's first cut used it. It is opaque for one reason, and the
241/// reason is not aesthetics:
242///
243/// **`Arc<[u8]>` is one storage strategy, and it is not going to be the
244/// only one.** Every exit currently allocates, copies, and frees per
245/// frame; a decode loop at 4K is asking the global allocator for eight
246/// megabytes sixty times a second and handing it back. The recorded
247/// answer is a plane pool — reusable slabs handed out at the boundary
248/// and returned when the last consumer drops them
249/// ([issue #35](https://github.com/findit-studio/mediadecode/issues/35)).
250/// A pooled slab is a different carrier with the same contract: still
251/// owned, still `Send + Sync`, still refcount-cloned, still holding no
252/// FFmpeg lifetime.
253///
254/// If the carrier were `Arc<[u8]>` in the public aliases, adding the
255/// pool would change the type of every frame and every packet in the
256/// crate — a breaking release for a change consumers cannot observe.
257/// Behind this newtype it is a new arm of a **private** enum: no
258/// signature moves, no consumer recompiles differently, and the
259/// `AsRef<[u8]>` a consumer actually programs against is unchanged.
260/// That extension point *is* this type's justification for existing.
261///
262/// The enum has exactly one arm today. It gains the second when the
263/// pool is built and not before — this codebase does not carry members
264/// nothing can produce.
265///
266/// [law]: mediadecode::adapter#the-d-seat-amputation-contract
267#[derive(Clone, Default)]
268pub struct FfmpegBytes(Inner);
269
270/// The storage behind [`FfmpegBytes`]. **Private, and the point.**
271///
272/// One arm today; see the type's own docs for the arm that is coming
273/// and why it can arrive without a breaking release.
274#[derive(Clone)]
275enum Inner {
276 /// No bytes, and no allocation either.
277 ///
278 /// The empty carrier is frequent — a video frame allocates four
279 /// plane slots and fills one to three, and a payload-less packet is
280 /// ordinary — and it used to be a process-wide `Arc` singleton
281 /// behind a `OnceLock`. A variant that holds nothing is simpler and
282 /// allocates nothing at all, which is what the fallible road wants
283 /// at its base case.
284 Empty,
285 /// A refcounted buffer, allocated by the copy at the boundary.
286 ///
287 /// **`triomphe::Arc`, and the reason is allocation failure.** The
288 /// bytes here come out of a container and are therefore
289 /// attacker-sized; every road into this carrier is downstream of a
290 /// budget that admitted a number, and a budget is worth nothing if
291 /// the allocation it admitted then aborts the process rather than
292 /// reporting the failure.
293 ///
294 /// `std::sync::Arc` cannot do it: there is no `try_new` for
295 /// `Arc<[u8]>` on stable and `Arc::new_uninit_slice` aborts like
296 /// every other infallible allocator call. Staging through a
297 /// `try_reserve_exact`ed `Vec` only moved the problem — the `Arc`
298 /// that took the `Vec` still allocated infallibly, and one of those
299 /// headers exists per coded-side-data entry, a count the file
300 /// controls. `triomphe::Arc` offers `try_new` and
301 /// `UniqueArc::try_new_uninit_slice`, so the whole allocation is
302 /// fallible and the payload lands in the `Arc` directly, with no
303 /// intermediate `Vec` and no second copy.
304 ///
305 /// An inline-capable carrier — `smol_bytes::Bytes`, which the text
306 /// seats use — was rejected rather than overlooked: inline storage
307 /// makes `Clone` copy small payloads, and [`FfmpegBytes::ptr_eq`],
308 /// the amputation contract's own instrument, asks whether a clone
309 /// was a refcount bump. A carrier that sometimes copies cannot
310 /// answer that question. `triomphe::Arc` keeps pointer identity.
311 Shared {
312 /// The allocation. May be longer than `len`: a producer that sizes
313 /// its output before a conversion runs cannot know the true length
314 /// until afterwards, and re-sizing then would be the very
315 /// allocation-after-the-point-of-no-return this carrier exists to
316 /// avoid.
317 bytes: triomphe::Arc<[u8]>,
318 /// How much of `bytes` is real output. A carrier's span is a span
319 /// a consumer may read, so everything past this is invisible.
320 len: usize,
321 },
322}
323
324impl Default for Inner {
325 #[inline]
326 fn default() -> Self {
327 Self::Empty
328 }
329}
330
331impl FfmpegBytes {
332 /// Copies `bytes` into a fresh carrier.
333 ///
334 /// **The copy site.** Every exit in this crate lands here or on
335 /// [`Self::empty`], so "one copy at the boundary" is a property of
336 /// one constructor rather than a promise thirty call sites keep —
337 /// and it is the one place a future pooled arm has to be taught
338 /// about.
339 ///
340 /// Public because the reverse direction needs it: a consumer
341 /// building a packet to feed back into a decoder has bytes and needs
342 /// a carrier, and the alternative is an opaque type nobody outside
343 /// this crate can construct.
344 ///
345 /// A zero-length copy lands on the shared empty allocation rather
346 /// than minting its own.
347 #[inline]
348 ///
349 /// **Test-only.** Its body is `try_copy_from_slice(..).expect(..)`,
350 /// and a `panic` on allocation failure is no better than the abort
351 /// it replaced — so no product path may reach it, and the `cfg`
352 /// below is what makes that a compile-time fact rather than a grep.
353 /// Everything that copies a container-controlled payload uses
354 /// [`Self::try_copy_from_slice`]: a fallible staging allocation does
355 /// not protect a subsequent infallible copy.
356 #[cfg(test)]
357 pub fn copy_from_slice(bytes: &[u8]) -> Self {
358 if bytes.is_empty() {
359 return Self::empty();
360 }
361 Self::try_copy_from_slice(bytes).expect("an infallible copy of an already-admitted payload")
362 }
363
364 /// [`Self::copy_from_slice`], reporting an allocation failure rather
365 /// than aborting on one.
366 ///
367 /// **The road every budgeted copy takes.** The payload is reserved
368 /// with `Vec::try_reserve_exact` before a byte is written, so a
369 /// container whose extradata, side data or attachment the caller's
370 /// ceilings admitted cannot terminate a safe `open` when the
371 /// allocator declines — it answers a named error instead. What is
372 /// left infallible afterwards is the `Arc` header described on
373 /// [`Inner`]: three words, and the same three whatever the payload
374 /// weighs.
375 #[inline]
376 pub fn try_copy_from_slice(bytes: &[u8]) -> Option<Self> {
377 if bytes.is_empty() {
378 return Some(Self::empty());
379 }
380 let mut uninit =
381 triomphe::UniqueArc::<[core::mem::MaybeUninit<u8>]>::try_new_uninit_slice(bytes.len())
382 .ok()?;
383 // SAFETY: the slice was just allocated with exactly `bytes.len()`
384 // slots, `uninit` is unique by type so nothing else can observe
385 // them, and the two regions cannot overlap — one is a fresh
386 // allocation.
387 unsafe {
388 core::ptr::copy_nonoverlapping(
389 bytes.as_ptr(),
390 uninit.as_mut_ptr().cast::<u8>(),
391 bytes.len(),
392 );
393 }
394 // SAFETY: every one of those slots was just written.
395 let filled = unsafe { triomphe::UniqueArc::assume_init_slice(uninit) };
396 let len = filled.len();
397 Some(Self(Inner::Shared {
398 bytes: filled.shareable(),
399 len,
400 }))
401 }
402
403 /// Claims `cap` bytes **now**, to be filled and named later.
404 ///
405 /// The reservation exists so that every fallible step happens on the
406 /// near side of a conversion. `swr` consumes its input as it runs, so
407 /// an allocation that fails afterwards leaves a caller with nothing
408 /// to retry and samples that are simply gone — which is what the
409 /// owned lane did until this seat existed.
410 ///
411 /// The bytes are zeroed rather than left uninitialised: a carrier
412 /// built from a partly-written reservation must still be a `[u8]` a
413 /// consumer may read, and the zeroing happens here, before the point
414 /// of no return, rather than in `commit` where nothing may fail.
415 #[inline]
416 pub(crate) fn reserve(cap: usize) -> Option<triomphe::UniqueArc<[u8]>> {
417 let mut uninit =
418 triomphe::UniqueArc::<[core::mem::MaybeUninit<u8>]>::try_new_uninit_slice(cap).ok()?;
419 // SAFETY: `cap` slots were just allocated and `uninit` is unique by
420 // type, so writing zeros over all of them initialises the whole
421 // slice and nothing else can observe the intermediate state.
422 unsafe {
423 core::ptr::write_bytes(uninit.as_mut_ptr().cast::<u8>(), 0, cap);
424 }
425 // SAFETY: every slot was just written.
426 Some(unsafe { triomphe::UniqueArc::assume_init_slice(uninit) })
427 }
428
429 /// Names how much of a [`Self::reserve`] is real output.
430 ///
431 /// **Infallible, and that is the point**: by the time this runs the
432 /// conversion has happened and there is nothing left to fail.
433 #[inline]
434 pub(crate) fn from_reservation(bytes: triomphe::UniqueArc<[u8]>, len: usize) -> Self {
435 let len = len.min(bytes.len());
436 if len == 0 {
437 return Self::empty();
438 }
439 Self(Inner::Shared {
440 bytes: bytes.shareable(),
441 len,
442 })
443 }
444
445 /// The zero-length carrier, shared.
446 ///
447 /// Placeholder plane slots and payload-less packets are frequent — a
448 /// video frame allocates four slots and populates one to three of
449 /// them — and each would otherwise be its own `Arc` header
450 /// allocation. One empty allocation for the process, cloned by
451 /// refcount, instead.
452 #[inline]
453 pub fn empty() -> Self {
454 Self(Inner::Empty)
455 }
456
457 /// Builds a carrier of `rows * row_bytes` bytes by writing each row
458 /// in turn — **one allocation, no staging buffer**.
459 ///
460 /// This is the road a padded plane takes. FFmpeg lays such a plane
461 /// out `linesize` bytes per row while only the first `row_bytes` of
462 /// each are the decoder's output, so the copy has to be row-wise and
463 /// the destination is contiguous. The obvious spelling — build a
464 /// `Vec`, then `Arc::from` it — allocates the whole plane **twice**
465 /// and copies it twice, so a 250 MiB frame peaks at 750 MiB counting
466 /// FFmpeg's own. Writing the rows straight into
467 /// `Arc::new_uninit_slice` leaves the unavoidable 2×: FFmpeg's plane
468 /// and ours.
469 ///
470 /// `row(i)` must answer a slice of exactly `row_bytes`; a shorter or
471 /// longer one is a bug in the caller's geometry and panics rather
472 /// than leaving the tail of the allocation uninitialised. That
473 /// assertion is what discharges the initialisation contract for the
474 /// `assume_init` below: the loop visits every row, each row fills its
475 /// full width, and `rows * row_bytes` is the whole allocation.
476 ///
477 /// Crate-internal: the public face is [`Self::copy_from_slice`], and
478 /// this shape only makes sense to a caller that already holds a
479 /// strided picture.
480 ///
481 /// # Panics
482 ///
483 /// If `rows * row_bytes` overflows `usize`, or if `row(i)` answers a
484 /// slice that is not `row_bytes` long. Callers reach this only after
485 /// the geometry has been validated and the total checked against
486 /// [`FrameLimits`](crate::FrameLimits), so both are unreachable from
487 /// input.
488 pub(crate) fn from_rows<'a>(
489 rows: usize,
490 row_bytes: usize,
491 mut row: impl FnMut(usize) -> &'a [u8],
492 ) -> Option<Self> {
493 let len = rows.checked_mul(row_bytes)?;
494 if len == 0 {
495 return Some(Self::empty());
496 }
497 // One reservation, made **fallibly**, and no staging buffer: the
498 // rows are appended straight into it. `None` therefore covers two
499 // refusals now — a row whose length disagrees with `row_bytes`,
500 // and an allocator that declined the plane — and both are answers
501 // this carrier's callers already had to handle.
502 //
503 // The `MaybeUninit` gather this replaced needed `unsafe` to view
504 // the source as uninitialised memory and a written-every-slot
505 // argument to discharge; appending cannot leave a hole, so the
506 // argument goes with it.
507 let mut uninit =
508 triomphe::UniqueArc::<[core::mem::MaybeUninit<u8>]>::try_new_uninit_slice(len).ok()?;
509 let destination = uninit.as_mut_ptr().cast::<u8>();
510 for index in 0..rows {
511 let source = row(index);
512 if source.len() != row_bytes {
513 // A length that arrives from a caller is an input, not a
514 // promise: refuse rather than copy `row_bytes` out of a
515 // shorter slice. The half-written allocation drops here.
516 return None;
517 }
518 // SAFETY: `rows * row_bytes == len` slots were allocated above,
519 // this row starts at `index * row_bytes` and is exactly
520 // `row_bytes` long, so the write stays inside them; the
521 // allocation is fresh, so it cannot overlap the source.
522 unsafe {
523 core::ptr::copy_nonoverlapping(
524 source.as_ptr(),
525 destination.add(index * row_bytes),
526 row_bytes,
527 );
528 }
529 }
530 // SAFETY: the loop wrote every one of the `rows * row_bytes` slots
531 // — `rows` iterations, each filling exactly `row_bytes`
532 // consecutive bytes starting at `index * row_bytes`, with any
533 // disagreeing row abandoning the whole gather before this point.
534 let filled = unsafe { triomphe::UniqueArc::assume_init_slice(uninit) };
535 let filled_len = filled.len();
536 Some(Self(Inner::Shared {
537 bytes: filled.shareable(),
538 len: filled_len,
539 }))
540 }
541
542 /// The bytes, as a slice.
543 ///
544 /// The same answer [`AsRef::as_ref`] gives; inherent so a caller
545 /// reaching through a `&FfmpegBytes` does not have to name the trait.
546 #[inline]
547 pub fn as_slice(&self) -> &[u8] {
548 match &self.0 {
549 Inner::Empty => &[],
550 Inner::Shared { bytes, len } => &bytes[..*len],
551 }
552 }
553
554 /// Number of bytes carried.
555 #[inline]
556 pub fn len(&self) -> usize {
557 self.as_slice().len()
558 }
559
560 /// `true` when this carries no bytes.
561 #[inline]
562 pub fn is_empty(&self) -> bool {
563 self.as_slice().is_empty()
564 }
565
566 /// `true` when both handles name the same allocation — a clone of
567 /// one another, rather than two copies that happen to be equal.
568 ///
569 /// The property the amputation contract is really about: `Clone` on
570 /// a message is a refcount bump. `PartialEq` answers a different
571 /// question (do these hold the same bytes), and a test that wants to
572 /// prove the clone did not copy has to ask this one.
573 #[inline]
574 pub fn ptr_eq(&self, other: &Self) -> bool {
575 match (&self.0, &other.0) {
576 // Two empties name the same nothing, which is what the
577 // process-wide empty singleton used to answer.
578 (Inner::Empty, Inner::Empty) => true,
579 // The allocation, not the span: two carriers naming different
580 // prefixes of one reservation are still the same allocation, and
581 // this question is about whether a clone copied.
582 (Inner::Shared { bytes: a, .. }, Inner::Shared { bytes: b, .. }) => {
583 triomphe::Arc::ptr_eq(a, b)
584 }
585 _ => false,
586 }
587 }
588}
589
590/// **Over the span, not the allocation.**
591///
592/// The derive compared `Inner` structurally, and `Inner` is not a
593/// structural type: a `Shared` arm holds an allocation that may be
594/// longer than the span, because a producer that sizes its output
595/// before a conversion runs cannot know the true length until
596/// afterwards. Two carriers whose [`FfmpegBytes::as_slice`] answers are
597/// byte-for-byte identical therefore compared **unequal** when one came
598/// off a reservation and the other was copied exactly — and `Empty`
599/// compared unequal to a zero-length `Shared`, though both carry
600/// nothing. Capacity a consumer cannot read is not part of the value.
601///
602/// [`FfmpegBytes::ptr_eq`] remains the instrument for the other
603/// question — *is this handle a refcount bump of that one* — and it is
604/// deliberately not what `==` answers.
605impl PartialEq for FfmpegBytes {
606 #[inline]
607 fn eq(&self, other: &Self) -> bool {
608 self.as_slice() == other.as_slice()
609 }
610}
611
612impl Eq for FfmpegBytes {}
613
614/// Hashes exactly what [`PartialEq`] compares, which is the contract's
615/// requirement rather than a preference: the derived hash mixed in the
616/// invisible capacity, so two equal carriers could land in different
617/// buckets and a payload-keyed map would miss.
618impl core::hash::Hash for FfmpegBytes {
619 #[inline]
620 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
621 core::hash::Hash::hash(self.as_slice(), state);
622 }
623}
624
625impl AsRef<[u8]> for FfmpegBytes {
626 #[inline]
627 fn as_ref(&self) -> &[u8] {
628 self.as_slice()
629 }
630}
631
632impl fmt::Debug for FfmpegBytes {
633 /// Length only, never the bytes.
634 ///
635 /// A derived `Debug` would print a decoded 4K plane one integer at a
636 /// time; this type is reached from the derived `Debug` of every
637 /// packet, frame and side-data entry in the crate, so the terse form
638 /// is the one that keeps those useful. Mirrors what `FfmpegBuffer`'s
639 /// own hand-written `Debug` did through 0.8.
640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641 f.debug_struct("FfmpegBytes")
642 .field("len", &self.len())
643 .finish()
644 }
645}
646
647/// Payload for [`PacketBufferError::PacketTooLarge`].
648///
649/// A packet's payload is larger than the budget in force.
650///
651/// Refused **before** the copy: 0.8 answered a claimed payload with a
652/// refcount, so an absurd `size` cost nothing; 0.9 answers it with an
653/// allocation, so the claim has to be judged first.
654#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
655#[error("a {bytes}-byte packet payload exceeds the {limit}-byte budget")]
656pub struct PacketTooLarge {
657 bytes: usize,
658 limit: usize,
659}
660
661impl PacketTooLarge {
662 /// Constructs a `PacketTooLarge` payload.
663 #[cfg_attr(not(tarpaulin), inline(always))]
664 pub const fn new(bytes: usize, limit: usize) -> Self {
665 Self { bytes, limit }
666 }
667 /// The payload length the packet declared.
668 #[cfg_attr(not(tarpaulin), inline(always))]
669 pub const fn bytes(&self) -> usize {
670 self.bytes
671 }
672 /// The budget in force.
673 #[cfg_attr(not(tarpaulin), inline(always))]
674 pub const fn limit(&self) -> usize {
675 self.limit
676 }
677}
678
679/// Payload for [`PacketBufferError::Bounds`].
680///
681/// The payload does not lie inside the packet's own buffer.
682/// `AVPacket` guarantees it does; a packet that says otherwise is
683/// malformed, and wrapping it would hand out a view over memory the
684/// buffer does not own.
685#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
686#[error("a {len}-byte payload at offset {offset} does not lie inside a {size}-byte buffer")]
687pub struct Bounds {
688 offset: usize,
689 len: usize,
690 size: usize,
691}
692
693impl Bounds {
694 /// Constructs a `Bounds` payload.
695 #[cfg_attr(not(tarpaulin), inline(always))]
696 pub const fn new(offset: usize, len: usize, size: usize) -> Self {
697 Self { offset, len, size }
698 }
699 /// Where the payload starts inside the buffer.
700 #[cfg_attr(not(tarpaulin), inline(always))]
701 pub const fn offset(&self) -> usize {
702 self.offset
703 }
704 /// The payload's length in bytes.
705 #[cfg_attr(not(tarpaulin), inline(always))]
706 pub const fn len(&self) -> usize {
707 self.len
708 }
709 /// `true` when the payload is zero bytes long.
710 #[cfg_attr(not(tarpaulin), inline(always))]
711 pub const fn is_empty(&self) -> bool {
712 self.len == 0
713 }
714 /// The buffer's own length in bytes.
715 #[cfg_attr(not(tarpaulin), inline(always))]
716 pub const fn size(&self) -> usize {
717 self.size
718 }
719}
720
721/// Payload for [`PacketBufferError::SideDataEntries`].
722///
723/// A packet declares more side-data entries than this crate will
724/// walk, or a negative count.
725///
726/// The cap bounds the work a crafted packet can demand *before* it is
727/// refused. It cannot trip on anything FFmpeg's own packet API
728/// produces: both `av_packet_new_side_data` and
729/// `av_packet_add_side_data` replace an entry of the same type, so a
730/// packet carries at most one entry per named type — forty-three in
731/// this build, and the cap tracks that number if it ever grows past
732/// the floor.
733#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
734#[error("a packet declaring {count} side-data entries cannot be carried (limit {cap})")]
735pub struct SideDataEntries {
736 count: i32,
737 cap: usize,
738}
739
740impl SideDataEntries {
741 /// Constructs a `SideDataEntries` payload.
742 #[cfg_attr(not(tarpaulin), inline(always))]
743 pub const fn new(count: i32, cap: usize) -> Self {
744 Self { count, cap }
745 }
746 /// The count the packet declared.
747 #[cfg_attr(not(tarpaulin), inline(always))]
748 pub const fn count(&self) -> i32 {
749 self.count
750 }
751 /// The most entries this crate will walk.
752 #[cfg_attr(not(tarpaulin), inline(always))]
753 pub const fn cap(&self) -> usize {
754 self.cap
755 }
756}
757
758/// Payload for [`PacketBufferError::SideDataArray`].
759///
760/// A packet declares side-data entries and carries no array to read
761/// them from.
762///
763/// Malformed, and named rather than read as "no side data": a null
764/// array with a positive count is the same silent loss as a truncated
765/// copy, reached through the pointer instead of the cap.
766#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
767#[error("a packet declaring {count} side-data entries carries no array")]
768pub struct SideDataArray {
769 count: i32,
770}
771
772impl SideDataArray {
773 /// Constructs a `SideDataArray` payload.
774 #[cfg_attr(not(tarpaulin), inline(always))]
775 pub const fn new(count: i32) -> Self {
776 Self { count }
777 }
778 /// The count the packet declared.
779 #[cfg_attr(not(tarpaulin), inline(always))]
780 pub const fn count(&self) -> i32 {
781 self.count
782 }
783}
784
785/// Payload for [`PacketBufferError::SideDataPayload`].
786#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
787#[error("side-data entry {index} declares {size} bytes and carries no data")]
788pub struct SideDataPayload {
789 index: usize,
790 size: usize,
791}
792
793impl SideDataPayload {
794 /// Constructs a `SideDataPayload` payload.
795 #[cfg_attr(not(tarpaulin), inline(always))]
796 pub const fn new(index: usize, size: usize) -> Self {
797 Self { index, size }
798 }
799 /// The entry's position in the packet's array.
800 #[cfg_attr(not(tarpaulin), inline(always))]
801 pub const fn index(&self) -> usize {
802 self.index
803 }
804 /// The length the entry declared.
805 #[cfg_attr(not(tarpaulin), inline(always))]
806 pub const fn size(&self) -> usize {
807 self.size
808 }
809}
810
811/// Payload for [`PacketBufferError::SideDataBytes`].
812///
813/// A packet's side data is larger than this crate will copy.
814#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
815#[error("{bytes} bytes of side data cannot be carried (limit {cap})")]
816pub struct SideDataBytes {
817 bytes: usize,
818 cap: usize,
819}
820
821impl SideDataBytes {
822 /// Constructs a `SideDataBytes` payload.
823 #[cfg_attr(not(tarpaulin), inline(always))]
824 pub const fn new(bytes: usize, cap: usize) -> Self {
825 Self { bytes, cap }
826 }
827 /// The total the packet's entries reached.
828 #[cfg_attr(not(tarpaulin), inline(always))]
829 pub const fn bytes(&self) -> usize {
830 self.bytes
831 }
832 /// The most bytes this crate will copy.
833 #[cfg_attr(not(tarpaulin), inline(always))]
834 pub const fn cap(&self) -> usize {
835 self.cap
836 }
837}
838
839/// Payload for [`PacketBufferError::UnrepresentableFlags`].
840///
841/// A packet carries flag bits the portable vocabulary cannot hold.
842///
843/// `mediadecode`'s `PacketFlags` is a `u8` bit set, and every packet
844/// flag FFmpeg names today lives in that byte — so this cannot fire
845/// against this build. It exists so that the day one does not, the
846/// packet is refused by name instead of arriving with a bit quietly
847/// missing: the same rule the rest of this boundary keeps.
848#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
849#[error("packet flags {raw:#x} do not fit the portable flag set")]
850pub struct UnrepresentableFlags {
851 raw: i32,
852}
853
854impl UnrepresentableFlags {
855 /// Constructs an `UnrepresentableFlags` payload.
856 #[cfg_attr(not(tarpaulin), inline(always))]
857 pub const fn new(raw: i32) -> Self {
858 Self { raw }
859 }
860 /// `AVPacket.flags` as FFmpeg wrote it.
861 #[cfg_attr(not(tarpaulin), inline(always))]
862 pub const fn raw(&self) -> i32 {
863 self.raw
864 }
865}
866
867/// Payload for [`PacketBufferError::SideDataAlloc`].
868///
869/// Out of memory copying a side-data entry.
870#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
871#[error("out of memory copying {size} bytes of side data")]
872pub struct SideDataAlloc {
873 size: usize,
874}
875
876impl SideDataAlloc {
877 /// Constructs a `SideDataAlloc` payload.
878 #[cfg_attr(not(tarpaulin), inline(always))]
879 pub const fn new(size: usize) -> Self {
880 Self { size }
881 }
882 /// The entry's length in bytes.
883 #[cfg_attr(not(tarpaulin), inline(always))]
884 pub const fn size(&self) -> usize {
885 self.size
886 }
887}
888/// Why a packet could not be carried across the boundary — its payload,
889/// or the side data that comes with it.
890///
891/// Every arm means the bytes are real and this crate could not carry
892/// them — never that there were none. "No payload" is `Ok(None)` from
893/// [`payload_of`], and keeping the two apart is the whole point of the
894/// type: a demuxer that reads a malformed packet as an empty marker
895/// drops a video packet and carries on as though the file said so. The
896/// side-data arms exist for the same reason one tier along — a packet
897/// whose side data cannot be carried whole is refused, never delivered
898/// with some of it.
899#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error, IsVariant, Unwrap, TryUnwrap)]
900#[unwrap(ref, ref_mut)]
901#[try_unwrap(ref, ref_mut)]
902pub enum PacketBufferError {
903 /// The payload is larger than the budget in force. Refused before
904 /// the copy.
905 #[error(transparent)]
906 PacketTooLarge(#[from] PacketTooLarge),
907
908 /// The payload does not lie inside the packet's own buffer.
909 #[error(transparent)]
910 Bounds(#[from] Bounds),
911
912 /// A packet declares more side-data entries than this crate will
913 /// walk, or a negative count.
914 #[error(transparent)]
915 SideDataEntries(#[from] SideDataEntries),
916
917 /// A packet declares side-data entries and carries no array to read
918 /// them from.
919 #[error(transparent)]
920 SideDataArray(#[from] SideDataArray),
921
922 /// A side-data entry declares bytes it does not carry.
923 #[error(transparent)]
924 SideDataPayload(#[from] SideDataPayload),
925
926 /// A packet's side data is larger than this crate will copy.
927 #[error(transparent)]
928 SideDataBytes(#[from] SideDataBytes),
929
930 /// A packet carries flag bits the portable vocabulary cannot hold.
931 #[error(transparent)]
932 UnrepresentableFlags(#[from] UnrepresentableFlags),
933
934 /// A packet is marked `AV_PKT_FLAG_TRUSTED`, so its payload may hold
935 /// pointers rather than bytes. See [`TrustedPayload`].
936 #[error(transparent)]
937 TrustedPayload(#[from] TrustedPayload),
938
939 /// The capture itself failed — an allocation on the owned lane, a
940 /// refcount on the view lane. See [`CaptureFailed`].
941 #[error(transparent)]
942 CaptureFailed(#[from] CaptureFailed),
943
944 /// The payload's buffer is referenced by something other than the
945 /// packet it came from. See [`SharedPayload`].
946 #[error(transparent)]
947 SharedPayload(#[from] SharedPayload),
948
949 /// Out of memory copying a side-data entry.
950 #[error(transparent)]
951 SideDataAlloc(#[from] SideDataAlloc),
952}
953
954impl PacketBufferError {
955 /// Whether the demux session should **park** the packet this refusal
956 /// came from and re-attempt it on the next pull.
957 ///
958 /// Deliberately not public, and deliberately named for the decision
959 /// rather than for a property of the error. It was briefly public as
960 /// `is_transient`, which promised more than an error enum can know:
961 /// whether retrying helps depends on *what was retried*.
962 /// `SharedPayload` is permanent for a caller who keeps their other
963 /// reference and retryable the moment they drop it;
964 /// `CaptureFailed` is worth another attempt only if the packet still
965 /// exists to attempt, which on a **consuming** conversion it does
966 /// not. Only the demux loop knows both halves — it still holds the
967 /// packet, and it knows nobody else does.
968 ///
969 /// So this answers one question for one caller. An allocation that
970 /// failed says nothing about the packet, and the demux loop is
971 /// holding the bytes; everything else is a fact about the packet
972 /// itself, and parking it would answer every later pull with the same
973 /// error instead of letting the session make progress.
974 ///
975 /// **The door left open:** a public retry signal would have to know
976 /// which operation produced the error and what the caller still
977 /// holds — an operation-aware answer, not a property of this enum.
978 /// If one is ever wanted it is designed then, not approximated now.
979 #[inline]
980 pub(crate) const fn parks_in_demux(&self) -> bool {
981 matches!(self, Self::CaptureFailed(_) | Self::SideDataAlloc(_))
982 }
983}
984
985/// `AV_PKT_FLAG_TRUSTED` as the bit the portable `PacketFlags` byte
986/// carries it in.
987///
988/// The core vocabulary deliberately does not *name* this flag — it is
989/// FFmpeg's, not a portable fact about packets — but `from_bits_retain`
990/// keeps the bit, so this crate can recognise its own flag coming back
991/// without the core growing a constant for it.
992pub(crate) const TRUSTED_BIT: u8 = ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED as u8;
993
994/// Compile-time proof that the flag really does fit the byte, so the
995/// cast above cannot silently become a different bit.
996const _: () = {
997 assert!(
998 ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED > 0
999 && ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED <= u8::MAX as std::ffi::c_int,
1000 "AV_PKT_FLAG_TRUSTED no longer fits the portable flag byte",
1001 );
1002};
1003
1004/// Payload for [`PacketBufferError::TrustedPayload`] and
1005/// [`crate::boundary::PacketBuildError::TrustedPayload`].
1006///
1007/// A packet carrying `AV_PKT_FLAG_TRUSTED`, refused on both legs.
1008///
1009/// # Why a flag makes a payload uncarriable
1010///
1011/// `AV_PKT_FLAG_TRUSTED` is FFmpeg's marker for a packet whose bytes
1012/// came from a source the *decoder* may treat as its own — and the
1013/// wrapped-AVFrame producers use it for exactly that: the payload is
1014/// not media, it is a **structure containing pointers to other live
1015/// objects** (an `AVFrame` and its buffers), passed by address between
1016/// components inside one FFmpeg pipeline.
1017///
1018/// This crate copies bytes. A pointer copied by value is not owned by
1019/// the copy — and that is not a gap this crate can close, because there
1020/// is no bound on what a payload's pointers might reach. So the
1021/// amputation has a corollary:
1022///
1023/// > **A payload that carries addresses instead of bytes cannot be
1024/// > carried.** Copying it produces a message that looks owned, is
1025/// > `Send + Sync + 'static` by every type-level test, and dangles the
1026/// > moment its source is dropped — a use-after-free reachable through
1027/// > entirely safe API.
1028///
1029/// Refusing is not conservatism, it is the only correct answer: the
1030/// contract this crate exists to keep says every byte leaving FFmpeg is
1031/// copied once into memory Rust owns, and a pointer cannot be.
1032///
1033/// Refused at **both** legs, because either one alone leaves the loop
1034/// open: copy-out ([`payload_of`]) is where such a packet would enter
1035/// the graph, and the reverse builders are where a flag that survived
1036/// some other route would be handed back to a decoder that trusts it.
1037#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1038pub struct TrustedPayload {
1039 len: usize,
1040}
1041
1042impl TrustedPayload {
1043 /// Constructs a `TrustedPayload` payload.
1044 #[inline]
1045 pub const fn new(len: usize) -> Self {
1046 Self { len }
1047 }
1048 /// How many bytes the packet declared.
1049 #[inline]
1050 pub const fn len(&self) -> usize {
1051 self.len
1052 }
1053 /// Whether the refused packet declared no bytes.
1054 #[inline]
1055 pub const fn is_empty(&self) -> bool {
1056 self.len == 0
1057 }
1058}
1059
1060impl core::fmt::Display for TrustedPayload {
1061 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1062 write!(
1063 f,
1064 "packet of {} bytes carries AV_PKT_FLAG_TRUSTED; a payload that may hold \
1065 pointers to other objects cannot be copied into an owned carrier",
1066 self.len,
1067 )
1068 }
1069}
1070
1071impl std::error::Error for TrustedPayload {}
1072
1073/// The payload of a raw `AVPacket`, copied out.
1074///
1075/// Shared by the four timed boundary conversions, the attachment
1076/// conversion, and the demuxer's capture of `AVStream.attached_pic` —
1077/// an `AVPacket` embedded in the stream by value, which no safe
1078/// wrapper reaches. One implementation, so the empty-versus-malformed
1079/// distinction cannot drift between them.
1080///
1081/// `Ok(None)` means the packet carries no payload at all: an empty
1082/// marker, which some demuxers emit. That is a fact about the packet
1083/// and is kept apart from [`PacketBufferError`], which is a failure to
1084/// take a payload that *is* there.
1085///
1086/// A packet whose `buf` is null — a stack- or arena-allocated
1087/// `AVPacket` — still reads as "no payload", exactly as it did before
1088/// the amputation. It is tempting now that the bytes are copied to
1089/// serve those from `data` / `size` directly, and that is precisely the
1090/// case with no owning buffer to bound the read against: the claim
1091/// would have to be taken on faith.
1092///
1093/// # Safety
1094///
1095/// `pkt` must be a live `*const AVPacket` for the duration of this
1096/// call.
1097pub(crate) unsafe fn payload_of<C: crate::FfmpegCarrier + crate::CarrierOps>(
1098 pkt: *const ffmpeg_next::ffi::AVPacket,
1099 budget: usize,
1100 provenance: PayloadProvenance,
1101) -> Result<Option<C::Buffer>, PacketBufferError> {
1102 // SAFETY: the caller's contract, forwarded unchanged.
1103 let Some(plan) = (unsafe { preflight_payload(pkt, budget, provenance) })? else {
1104 return Ok(None);
1105 };
1106 // SAFETY: the plan's extent was proved inside the preflight against
1107 // this same live packet, which the caller keeps alive for the call.
1108 unsafe { capture_payload::<C>(plan) }.map(Some)
1109}
1110
1111/// What [`preflight_payload`] decided, and everything the capture needs
1112/// to act on it — so the judging and the paying share one reading of
1113/// the packet rather than each performing their own.
1114#[derive(Clone, Copy)]
1115pub(crate) struct PayloadPlan {
1116 buf: *mut ffmpeg_next::ffi::AVBufferRef,
1117 buf_data: *const u8,
1118 offset: usize,
1119 len: usize,
1120 route: CaptureRoute,
1121}
1122
1123/// **Every refusal a packet payload can earn from what the packet
1124/// declares — decided without reading a byte of it and without
1125/// allocating.**
1126///
1127/// Split out of [`payload_of`] because the demux admission pass needs
1128/// exactly these answers about every parked attachment *before* the
1129/// track table has allocated anything. They are all deterministic facts
1130/// about the `AVPacket`: a `TRUSTED` payload this crate must not copy,
1131/// a declared size over the caller's ceiling, a `data`/`size` pair that
1132/// does not lie inside the buffer it claims, and a buffer somebody else
1133/// holds a reference to on a road where that is not acceptable. None of
1134/// them will be different on a second attempt, so none of them belongs
1135/// after the money is spent.
1136///
1137/// What is deliberately *not* here is [`CaptureFailed`] — the allocator
1138/// declining the carrier. That one cannot be foreseen by any amount of
1139/// reading, which is what makes it the only `PacketBuffer` arm that is
1140/// honestly unpredictable.
1141///
1142/// `Ok(None)` is the empty answer: a packet with no buffer, no data or
1143/// no size is not a fault, it is a packet with nothing in it.
1144///
1145/// # Safety
1146///
1147/// `pkt` must be a live `*const AVPacket` for the duration of the call,
1148/// and — if a plan comes back — for as long as that plan is used.
1149pub(crate) unsafe fn preflight_payload(
1150 pkt: *const ffmpeg_next::ffi::AVPacket,
1151 budget: usize,
1152 provenance: PayloadProvenance,
1153) -> Result<Option<PayloadPlan>, PacketBufferError> {
1154 // SAFETY: `pkt` is live per the contract above; `.buf`, `.data` and
1155 // `.size` are public fields on `AVPacket`, and `buf` may be null
1156 // (stack-allocated packets).
1157 let buf_ptr = unsafe { (*pkt).buf };
1158 let data_ptr = unsafe { (*pkt).data };
1159 let size_raw = unsafe { (*pkt).size };
1160 // **The uncarriable-payload refusal, ahead of everything.** See
1161 // [`TrustedPayload`]: this flag marks a payload that may be a
1162 // structure of pointers into other live objects rather than media
1163 // bytes, and copying those bytes would mint an owned-looking carrier
1164 // full of addresses that dangle as soon as the source is dropped.
1165 //
1166 // Judged before the empty-payload answer as well as before the copy:
1167 // "there is nothing to take here" is the wrong reply to a packet this
1168 // crate must not take *anything* from.
1169 //
1170 // SAFETY: `pkt` is live per the contract; `flags` is a public `c_int`
1171 // field, read as the integer it is.
1172 let flags_raw = unsafe { (*pkt).flags };
1173 if flags_raw & ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED != 0 {
1174 return Err(PacketBufferError::TrustedPayload(TrustedPayload::new(
1175 size_raw.max(0) as usize,
1176 )));
1177 }
1178 if buf_ptr.is_null() || data_ptr.is_null() || size_raw <= 0 {
1179 return Ok(None);
1180 }
1181 let len = size_raw as usize;
1182 // **The budget, before anything is read or allocated.** Judged on
1183 // the declared length rather than on what the copy turns out to
1184 // cost, because the point is to refuse without paying. Ahead of the
1185 // bounds check too: a forged `size` is exactly what both exist for,
1186 // and the cheaper judgement goes first.
1187 if len > budget {
1188 return Err(PacketBufferError::PacketTooLarge(PacketTooLarge::new(
1189 len, budget,
1190 )));
1191 }
1192 // SAFETY: `buf_ptr` is a live `AVBufferRef` owned by the packet.
1193 let buf_data = unsafe { (*buf_ptr).data };
1194 let size = unsafe { (*buf_ptr).size };
1195 if buf_data.is_null() {
1196 return Err(PacketBufferError::Bounds(Bounds::new(0, len, size)));
1197 }
1198 // `AVPacket` guarantees `data` lies within
1199 // `buf->data .. buf->data + buf->size`. Checked before the copy, not
1200 // instead of it: 0.8 formed a view over the claimed range and a
1201 // malformed `size` handed out a slice nobody read; 0.9 reads every
1202 // byte of it, so an unchecked claim is an out-of-bounds read rather
1203 // than a latent one.
1204 let offset = (data_ptr as usize).wrapping_sub(buf_data as usize);
1205 match offset.checked_add(len) {
1206 Some(end) if end <= size => {}
1207 _ => {
1208 return Err(PacketBufferError::Bounds(Bounds::new(offset, len, size)));
1209 }
1210 }
1211 // **The sharing question, asked before any byte is read.**
1212 //
1213 // Everything below reads the payload — the view lane by handing out a
1214 // span over it, the owned lane by copying it — so a buffer somebody
1215 // else references has to be classified before it is touched. What
1216 // matters is not the count but **who** the other holder is, and only
1217 // the caller of this function knows that: see [`PayloadProvenance`]
1218 // for the dichotomy and [`PayloadProvenance::route`] for the table.
1219 //
1220 // Placed here rather than inside the capture so the ordering is a
1221 // property of this function rather than of two lane impls: nothing
1222 // between the bounds proof and this decision touches the payload.
1223 //
1224 // SAFETY: `buf_ptr` is a live `AVBufferRef` owned by the packet;
1225 // `av_buffer_get_ref_count` only reads its atomic.
1226 let references = unsafe { ffmpeg_next::ffi::av_buffer_get_ref_count(buf_ptr.cast_const()) };
1227 let route = provenance.route(references != 1);
1228 if route == CaptureRoute::Refuse {
1229 return Err(PacketBufferError::SharedPayload(SharedPayload::new(
1230 references,
1231 )));
1232 }
1233
1234 // **The capture, and the only step the two lanes spell differently.**
1235 // Everything above — the `TRUSTED` refusal, the empty answer, the
1236 // budget, the extent proof — is shared, which is what keeps the view
1237 // lane from having to re-earn a single one of them.
1238 //
1239 // The **packet-payload** capture, not the general one: this range is
1240 // an `AVPacket`'s payload inside that packet's own buffer, which is
1241 // the one place libavformat's trailing-padding contract applies. The
1242 // view lane records that, and the send leg is the only thing that
1243 // reads it back — see `boundary::share_or_copy`.
1244 //
1245 // SAFETY: `offset + len` was just proved to lie inside `buf_ptr`'s
1246 // own `size`, and `buf_ptr` is a live `AVBufferRef` the packet owns.
1247 Ok(Some(PayloadPlan {
1248 buf: buf_ptr,
1249 buf_data: buf_data.cast_const(),
1250 offset,
1251 len,
1252 route,
1253 }))
1254}
1255
1256/// Acts on a [`PayloadPlan`] — **the only step that reads the payload,
1257/// and the only one that can fail for a reason the plan could not
1258/// foresee.**
1259///
1260/// # Safety
1261///
1262/// `plan` must come from [`preflight_payload`] over a packet still live
1263/// for this call.
1264unsafe fn capture_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1265 plan: PayloadPlan,
1266) -> Result<C::Buffer, PacketBufferError> {
1267 let PayloadPlan {
1268 buf: buf_ptr,
1269 buf_data,
1270 offset,
1271 len,
1272 route,
1273 } = plan;
1274 let carried = match route {
1275 CaptureRoute::Capture => unsafe { C::capture_packet_payload(buf_ptr, offset, len) },
1276 CaptureRoute::Copy => {
1277 // A demux-delivered packet whose buffer libavformat also holds.
1278 // Reading it here is race-free — every other reference is
1279 // C-owned, no `ffmpeg_next::Packet` wraps one, and this crate
1280 // holds the `AVFormatContext` exclusively for the duration of
1281 // the call — but the copy is what keeps that argument confined
1282 // to *this* call instead of to the carrier's whole life.
1283 //
1284 // SAFETY: the extent was proved above and `buf_data` is
1285 // non-null.
1286 let bytes = unsafe { core::slice::from_raw_parts(buf_data.add(offset), len) };
1287 C::from_bytes(bytes)
1288 }
1289 // Answered before the payload was touched.
1290 CaptureRoute::Refuse => unreachable!("a refusal returns from the preflight"),
1291 };
1292 carried.ok_or(PacketBufferError::CaptureFailed(CaptureFailed::new(len)))
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::*;
1298 use crate::limits::DEFAULT_MAX_PACKET_BYTES;
1299 use ffmpeg_next::{Packet, packet::Ref};
1300
1301 #[test]
1302 fn a_real_payload_is_copied_out_whole() {
1303 let packet = Packet::copy(&[1u8, 2, 3, 4]);
1304 // SAFETY: `packet` owns a live `AVPacket` for the call.
1305 let payload = unsafe {
1306 payload_of::<crate::Owned>(
1307 packet.as_ptr(),
1308 DEFAULT_MAX_PACKET_BYTES,
1309 PayloadProvenance::CallerSupplied,
1310 )
1311 }
1312 .expect("a well-formed packet is carriable")
1313 .expect("present");
1314 assert_eq!(payload.as_ref(), &[1, 2, 3, 4]);
1315 }
1316
1317 #[test]
1318 fn the_copy_outlives_the_packet_it_came_from() {
1319 // The whole point of the amputation: FFmpeg's allocation is gone
1320 // and the bytes are still here.
1321 let packet = Packet::copy(&[9u8, 8, 7]);
1322 // SAFETY: `packet` owns a live `AVPacket` for the call.
1323 let payload = unsafe {
1324 payload_of::<crate::Owned>(
1325 packet.as_ptr(),
1326 DEFAULT_MAX_PACKET_BYTES,
1327 PayloadProvenance::CallerSupplied,
1328 )
1329 }
1330 .expect("carriable")
1331 .expect("present");
1332 let shared = payload.clone();
1333 assert!(shared.ptr_eq(&payload), "the clone copied the bytes");
1334 drop(packet);
1335 drop(payload);
1336 assert_eq!(shared.as_ref(), &[9, 8, 7]);
1337 }
1338
1339 #[test]
1340 fn an_empty_packet_has_no_payload_rather_than_a_failure() {
1341 let packet = Packet::empty();
1342 // SAFETY: `packet` owns a live `AVPacket` for the call.
1343 assert!(
1344 unsafe {
1345 payload_of::<crate::Owned>(
1346 packet.as_ptr(),
1347 DEFAULT_MAX_PACKET_BYTES,
1348 PayloadProvenance::CallerSupplied,
1349 )
1350 }
1351 .expect("not a failure")
1352 .is_none()
1353 );
1354 }
1355
1356 #[test]
1357 fn a_payload_outside_its_own_buffer_is_refused_before_a_byte_is_read() {
1358 use ffmpeg_next::packet::Mut;
1359 let mut packet = Packet::copy(&[1u8, 2, 3, 4]);
1360 // SAFETY: `packet` owns a live `AVPacket`; `size` is a public
1361 // field. The forged claim is the read this check exists to stop.
1362 unsafe {
1363 (*packet.as_mut_ptr()).size = 1 << 20;
1364 }
1365 // SAFETY: `packet` owns a live `AVPacket` for the call.
1366 assert!(matches!(
1367 unsafe {
1368 payload_of::<crate::Owned>(
1369 packet.as_ptr(),
1370 DEFAULT_MAX_PACKET_BYTES,
1371 PayloadProvenance::CallerSupplied,
1372 )
1373 },
1374 Err(PacketBufferError::Bounds(_)),
1375 ));
1376 }
1377
1378 #[test]
1379 fn the_empty_carrier_costs_no_allocation() {
1380 let a = FfmpegBytes::empty();
1381 let b = FfmpegBytes::empty();
1382 assert!(a.is_empty());
1383 assert_eq!(a.len(), 0);
1384 assert!(
1385 a.ptr_eq(&b),
1386 "two empties name the same nothing, which is the answer the shared singleton used to give",
1387 );
1388 // And a zero-length copy is that same nothing rather than a
1389 // minted allocation.
1390 assert!(FfmpegBytes::copy_from_slice(&[]).ptr_eq(&a));
1391 assert!(
1392 FfmpegBytes::try_copy_from_slice(&[])
1393 .expect("an empty copy cannot fail")
1394 .ptr_eq(&a),
1395 );
1396 }
1397
1398 /// Equality and hashing read the span, and the two shapes that
1399 /// carry the same bytes with different allocations must agree.
1400 ///
1401 /// The derived implementations did not: a carrier built from a
1402 /// reservation keeps capacity past its span (a producer sizes its
1403 /// output before the conversion that fills it), so it compared and
1404 /// hashed differently from an exact copy of the very same bytes.
1405 /// Anything keyed on a payload — a cache, a dedup table — missed.
1406 #[test]
1407 fn equal_bytes_are_equal_and_hash_alike_whatever_the_allocation() {
1408 use core::hash::Hasher;
1409
1410 fn hash_of(value: &FfmpegBytes) -> u64 {
1411 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1412 core::hash::Hash::hash(value, &mut hasher);
1413 hasher.finish()
1414 }
1415
1416 // Reserved wide, committed short: the allocation is sixteen bytes
1417 // and the span is three.
1418 let mut reservation = FfmpegBytes::reserve(16).expect("a sixteen-byte reservation");
1419 reservation[..3].copy_from_slice(&[7u8, 8, 9]);
1420 let reserved = FfmpegBytes::from_reservation(reservation, 3);
1421 // Copied exactly: allocation and span are both three bytes.
1422 let exact = FfmpegBytes::try_copy_from_slice(&[7u8, 8, 9]).expect("a three-byte copy");
1423
1424 assert_eq!(reserved.as_slice(), exact.as_slice());
1425 assert_eq!(
1426 reserved, exact,
1427 "capacity a consumer cannot read is not part of the value",
1428 );
1429 assert_eq!(
1430 hash_of(&reserved),
1431 hash_of(&exact),
1432 "Hash must agree with Eq or a payload-keyed map misses",
1433 );
1434 assert!(
1435 !reserved.ptr_eq(&exact),
1436 "and ptr_eq still answers the other question: these are two allocations",
1437 );
1438
1439 // The empty carrier and a zero-length span are the same value too.
1440 // `from_reservation` folds a zero length to `Empty`, so this holds
1441 // by construction today; it is asserted because the `Eq` above is
1442 // what keeps it true if a second storage arm ever lands and stops
1443 // folding.
1444 let empty_reservation = FfmpegBytes::reserve(8).expect("an eight-byte reservation");
1445 let zero_span = FfmpegBytes::from_reservation(empty_reservation, 0);
1446 assert_eq!(zero_span, FfmpegBytes::empty());
1447 assert_eq!(hash_of(&zero_span), hash_of(&FfmpegBytes::empty()));
1448 }
1449
1450 #[test]
1451 fn copy_out_is_owned_and_shareable() {
1452 fn owned_and_shareable<T: Send + Sync + Clone + 'static>(_: &T) {}
1453 let carrier = FfmpegBytes::copy_from_slice(&[4u8, 5, 6]);
1454 owned_and_shareable(&carrier);
1455 assert_eq!(carrier.as_ref(), &[4, 5, 6]);
1456 // Terse `Debug` — the bytes never reach a log line through it.
1457 let rendered = format!("{carrier:?}");
1458 assert!(rendered.contains("len: 3"), "got {rendered}");
1459 assert!(!rendered.contains('4'), "got {rendered}");
1460 }
1461}
1462
1463/// Where the packet a payload is taken from came from — and therefore
1464/// what a second reference to its buffer can do.
1465///
1466/// The dichotomy is **delivered by libavformat** versus **handed over
1467/// by a caller**, and it is about who can *write*, not how many
1468/// references there are.
1469///
1470/// * A packet this crate's own read loop just took from
1471/// `av_read_frame` — or hoisted out of `AVStream.attached_pic` — may
1472/// well share its buffer, and every other reference to it is
1473/// libavformat's. FFmpeg writes through a buffer only after
1474/// `av_buffer_make_writable`, which *copies* when the buffer is
1475/// shared; and while this crate is reading, it holds the
1476/// `AVFormatContext` exclusively, so no libavformat code is running
1477/// at all. There is no safe-Rust `data_mut` on any of those
1478/// references, because no `ffmpeg_next::Packet` wraps them.
1479/// * A packet a **caller** hands over may share its buffer with
1480/// another `ffmpeg_next::Packet` — and that type's `data_mut` writes
1481/// in place from safe code, without consulting writability. That is
1482/// the writer the uniqueness rule exists for, and it may be on
1483/// another thread.
1484///
1485/// Spelled out as a parameter rather than assumed at the call sites,
1486/// because a blanket "refcount must be one" looked right and was twice
1487/// wrong: it refused every embedded cover picture, and then every
1488/// packet from a queue-backed subtitle demuxer, which delivers
1489/// `av_packet_ref`s of originals it keeps in its own queue.
1490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1491pub(crate) enum PayloadProvenance {
1492 /// A packet a caller handed to a public conversion. A second
1493 /// reference may be a `Packet` with a safe `data_mut`; shared is
1494 /// refused by name.
1495 CallerSupplied,
1496 /// A packet this crate's demux loop just received from
1497 /// `av_read_frame`. Secondary references are libavformat's own.
1498 DemuxDelivered,
1499 /// The container's parked picture, whether hoisted at open or queued
1500 /// as a stream's first packet. Written once while the container was
1501 /// opened and never again.
1502 AttachedPicture,
1503}
1504
1505/// How a payload of a given provenance may be captured once its extent
1506/// is proved.
1507#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1508pub(crate) enum CaptureRoute {
1509 /// The ordinary road: the view lane takes a window, the owned lane
1510 /// copies.
1511 Capture,
1512 /// Both lanes copy. The bytes are safe to *read* — no safe-Rust
1513 /// writer exists — but no long-lived window may be opened onto a
1514 /// buffer somebody else also holds unless something stronger than
1515 /// "nobody is writing right now" is true of it.
1516 Copy,
1517 /// Refuse without reading a byte.
1518 Refuse,
1519}
1520
1521impl PayloadProvenance {
1522 /// What to do with a payload whose buffer has `shared` other
1523 /// references.
1524 ///
1525 /// | provenance | unique | shared | the argument |
1526 /// |---|---|---|---|
1527 /// | [`Self::CallerSupplied`] | capture | **refuse** | a second `Packet`'s `data_mut` writes in place from safe code, possibly on another thread |
1528 /// | [`Self::DemuxDelivered`] | capture | **copy** | the read is race-free — every other reference is C-owned and the context is held exclusively — but a *window* would outlive that exclusivity, and FFmpeg's copy-on-write discipline is a weaker guarantee than this crate wants under a long-lived span |
1529 /// | [`Self::AttachedPicture`] | capture | **capture** | `AVStream.attached_pic` is written once while the container opens and never again, so a window onto it is as stable as one onto a private buffer |
1530 ///
1531 /// The middle row is the deliberate one. Sharing there would have
1532 /// rested on "libavformat honours its own writability rules
1533 /// forever"; copying rests on "nothing can be writing while we hold
1534 /// the context", which is a fact about *this* call and needs no
1535 /// promise about anyone's future behaviour. Subtitle queues — the
1536 /// shape that produced this row — carry payloads measured in bytes,
1537 /// so the copy is not a cost worth an argument.
1538 #[inline]
1539 pub(crate) const fn route(self, shared: bool) -> CaptureRoute {
1540 match (self, shared) {
1541 (_, false) | (Self::AttachedPicture, true) => CaptureRoute::Capture,
1542 (Self::DemuxDelivered, true) => CaptureRoute::Copy,
1543 (Self::CallerSupplied, true) => CaptureRoute::Refuse,
1544 }
1545 }
1546}
1547
1548/// A packet whose payload buffer somebody else still references.
1549///
1550/// **Refused without reading a byte of it, and that is the whole
1551/// point.** A refcount above one is exactly the state in which another
1552/// handle to the same allocation may exist — `ffmpeg_next::Packet`
1553/// hands out `&mut [u8]` through `data_mut` from entirely safe code,
1554/// and a `Packet` is `Send`, so that handle may be on another thread
1555/// writing right now. Forming a `&[u8]` over those bytes is a data race
1556/// whether the bytes are then viewed *or copied*: the copy needs the
1557/// read, and the read is the race.
1558///
1559/// An earlier round answered this shape with a silent copy, reasoning
1560/// that a copy is always sound and keeps the API total. That was wrong
1561/// in the direction that matters — it traded soundness for totality.
1562/// The refcount protects the allocation's *lifetime*; it says nothing
1563/// about who may be writing into it.
1564///
1565/// The ordinary roads never see this: a packet from `av_read_frame` is
1566/// uniquely referenced, and so is one the caller cloned successfully.
1567/// What produces it is a second reference the caller may not know they
1568/// have — see `ffmpeg_next::Packet::clone`, which ignores
1569/// `av_packet_make_writable`'s return code.
1570#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1571#[error(
1572 "packet payload buffer is shared ({references} references): its bytes cannot be read \
1573 without racing whoever else holds it"
1574)]
1575pub struct SharedPayload {
1576 references: i32,
1577}
1578
1579impl SharedPayload {
1580 /// Constructs a `SharedPayload` payload.
1581 #[inline]
1582 #[must_use]
1583 pub const fn new(references: i32) -> Self {
1584 Self { references }
1585 }
1586
1587 /// References the payload's buffer had when it was refused.
1588 #[inline]
1589 #[must_use]
1590 pub const fn references(&self) -> i32 {
1591 self.references
1592 }
1593}
1594
1595/// Payload for [`PacketBufferError::CaptureFailed`].
1596///
1597/// The proofs all passed and the carrier still could not be formed:
1598/// `av_buffer_alloc` returned null on the owned lane, or
1599/// `av_buffer_ref` did on the view lane. Distinct from
1600/// [`Bounds`] on purpose — a malformed packet and an exhausted
1601/// allocator are different facts, and 0.8 reported both as an absent
1602/// payload.
1603#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1604#[error("could not capture a {len}-byte payload: the allocator or the refcount refused")]
1605pub struct CaptureFailed {
1606 len: usize,
1607}
1608
1609impl CaptureFailed {
1610 /// Constructs a `CaptureFailed` payload.
1611 #[inline]
1612 pub const fn new(len: usize) -> Self {
1613 Self { len }
1614 }
1615 /// Bytes the capture was for.
1616 #[inline]
1617 pub const fn len(&self) -> usize {
1618 self.len
1619 }
1620 /// Whether the refused capture was of no bytes.
1621 #[inline]
1622 pub const fn is_empty(&self) -> bool {
1623 self.len == 0
1624 }
1625}