mediadecode_ffmpeg/channel_layout.rs
1//! Conversions from FFmpeg's [`ffmpeg_next::ChannelLayout`] /
2//! [`ffmpeg_next::ffi::AVChannelOrder`] to the channel-layout vocabulary
3//! [`mediaframe`] owns ([`ChannelLayout`], [`ChannelOrder`],
4//! [`ChannelSpec`], [`ChannelLayoutDescription`]).
5//!
6//! These live as **free functions** (not `From` trait impls) because of
7//! Rust's orphan rule: this crate owns neither `From` nor
8//! `mediaframe::audio::*`, so we can't write the `impl` here. Calling
9//! `mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(layout)`
10//! is the ergonomic boundary instead.
11//!
12//! FFmpeg's own type is imported as [`AvChannelLayout`] so the name
13//! [`ChannelLayout`] can stay with the vocabulary these functions
14//! produce.
15
16use core::{ffi::c_char, slice, str::FromStr};
17
18use ffmpeg_next::{ChannelLayout as AvChannelLayout, ffi};
19use mediaframe::audio::{ChannelLayout, ChannelLayoutDescription, ChannelOrder, ChannelSpec};
20use smol_bytes::Utf8Bytes;
21use std::vec::Vec;
22
23/// Maps an FFmpeg [`AvChannelLayout`] to the named
24/// [`ChannelLayout`] vocabulary.
25///
26/// Two rungs, in order:
27///
28/// 1. the **constant-arm table** — exactly `ffmpeg_next`'s
29/// `ChannelLayout` constant set, compared through
30/// `av_channel_layout_compare`;
31/// 2. the **describe rung** — for a layout that falls off the table,
32/// FFmpeg names it via `av_channel_layout_describe` and that name
33/// goes through [`ChannelLayout`]'s own total door (`FromStr`).
34///
35/// The second rung is what makes `binaural` / `5.1.2` / `9.1.6`
36/// reachable: [`ChannelLayout`] names all three, `ffmpeg_next` 9.0.0
37/// mints no constant for any of them, so the table alone can never
38/// produce them. It is also why a layout a *later* FFmpeg adds is
39/// reachable with no edit here, as long as the vocabulary already
40/// names it — FFmpeg speaks the name, the vocabulary reads the word,
41/// one source.
42///
43/// Returns [`ChannelLayout::default`] — the `Other("")` absent sentinel
44/// — when neither rung names the layout. The rendering itself is not
45/// smuggled into `Other`: an unrecognised layout stays *absent*, and
46/// [`ChannelLayoutDescription::text`] is where its FFmpeg rendering
47/// lives.
48pub fn channel_layout_from_ffmpeg(
49 value: &AvChannelLayout,
50) -> Result<ChannelLayout, ChannelLayoutFault> {
51 // One road for both exported conversions; see
52 // [`channel_layout_description_from_ffmpeg`] for what the safe one
53 // will and will not touch.
54 Ok(
55 channel_layout_description_from_ffmpeg(value)?
56 .known_kind()
57 .clone(),
58 )
59}
60
61/// The constant-arm table — the first and authoritative rung of
62/// [`channel_layout_from_ffmpeg`]. `None` means the layout fell off the
63/// table and the caller should try the describe rung.
64///
65/// The arm list is exactly `ffmpeg_next`'s `ChannelLayout` constant set:
66/// its `_7POINT1_TOP_BACK` is a `#define` alias of
67/// `AV_CH_LAYOUT_5POINT1POINT2_BACK` and so has no arm of its own, and
68/// its `BINAURAL` / `_5POINT1POINT2` / `_9POINT1POINT6` siblings — which
69/// [`ChannelLayout`] does name — have no constant to match against.
70fn mapped_constant(value: &AvChannelLayout) -> Option<ChannelLayout> {
71 let named = match () {
72 () if value.eq(&AvChannelLayout::MONO) => ChannelLayout::Mono,
73 () if value.eq(&AvChannelLayout::STEREO) => ChannelLayout::Stereo,
74 () if value.eq(&AvChannelLayout::STEREO_DOWNMIX) => ChannelLayout::StereoDownmix,
75 () if value.eq(&AvChannelLayout::SURROUND) => ChannelLayout::Ch3_0,
76 () if value.eq(&AvChannelLayout::QUAD) => ChannelLayout::Quad,
77 () if value.eq(&AvChannelLayout::HEXAGONAL) => ChannelLayout::Hexagonal,
78 () if value.eq(&AvChannelLayout::OCTAGONAL) => ChannelLayout::Octagonal,
79 () if value.eq(&AvChannelLayout::HEXADECAGONAL) => ChannelLayout::Hexadecagonal,
80 () if value.eq(&AvChannelLayout::CUBE) => ChannelLayout::Cube,
81 () if value.eq(&AvChannelLayout::_2POINT1) => ChannelLayout::Ch2_1,
82 () if value.eq(&AvChannelLayout::_2_1) => ChannelLayout::Ch3_0Back,
83 () if value.eq(&AvChannelLayout::_2_2) => ChannelLayout::QuadSide,
84 () if value.eq(&AvChannelLayout::_3POINT1) => ChannelLayout::Ch3_1,
85 () if value.eq(&AvChannelLayout::_3POINT1POINT2) => ChannelLayout::Ch3_1_2,
86 () if value.eq(&AvChannelLayout::_4POINT0) => ChannelLayout::Ch4_0,
87 () if value.eq(&AvChannelLayout::_4POINT1) => ChannelLayout::Ch4_1,
88 () if value.eq(&AvChannelLayout::_5POINT0) => ChannelLayout::Ch5_0,
89 () if value.eq(&AvChannelLayout::_5POINT0_BACK) => ChannelLayout::Ch5_0Back,
90 () if value.eq(&AvChannelLayout::_5POINT1) => ChannelLayout::Ch5_1,
91 () if value.eq(&AvChannelLayout::_5POINT1_BACK) => ChannelLayout::Ch5_1Back,
92 () if value.eq(&AvChannelLayout::_5POINT1POINT2_BACK) => ChannelLayout::Ch5_1_2Back,
93 () if value.eq(&AvChannelLayout::_5POINT1POINT4_BACK) => ChannelLayout::Ch5_1_4Back,
94 () if value.eq(&AvChannelLayout::_6POINT0) => ChannelLayout::Ch6_0,
95 () if value.eq(&AvChannelLayout::_6POINT0_FRONT) => ChannelLayout::Ch6_0Front,
96 () if value.eq(&AvChannelLayout::_6POINT1) => ChannelLayout::Ch6_1,
97 () if value.eq(&AvChannelLayout::_6POINT1_BACK) => ChannelLayout::Ch6_1Back,
98 () if value.eq(&AvChannelLayout::_6POINT1_FRONT) => ChannelLayout::Ch6_1Front,
99 () if value.eq(&AvChannelLayout::_7POINT0) => ChannelLayout::Ch7_0,
100 () if value.eq(&AvChannelLayout::_7POINT0_FRONT) => ChannelLayout::Ch7_0Front,
101 () if value.eq(&AvChannelLayout::_7POINT1) => ChannelLayout::Ch7_1,
102 () if value.eq(&AvChannelLayout::_7POINT1_WIDE) => ChannelLayout::Ch7_1Wide,
103 () if value.eq(&AvChannelLayout::_7POINT1_WIDE_BACK) => ChannelLayout::Ch7_1WideBack,
104 () if value.eq(&AvChannelLayout::_7POINT1POINT2) => ChannelLayout::Ch7_1_2,
105 () if value.eq(&AvChannelLayout::_7POINT1POINT4_BACK) => ChannelLayout::Ch7_1_4Back,
106 () if value.eq(&AvChannelLayout::_7POINT2POINT3) => ChannelLayout::Ch7_2_3,
107 () if value.eq(&AvChannelLayout::_9POINT1POINT4_BACK) => ChannelLayout::Ch9_1_4Back,
108 () if value.eq(&AvChannelLayout::_22POINT2) => ChannelLayout::Ch22_2,
109 () => return None,
110 };
111 Some(named)
112}
113
114/// The describe rung: read FFmpeg's own rendering of a layout
115/// (`av_channel_layout_describe`, e.g. `"binaural"`, `"5.1(side)"`)
116/// through [`ChannelLayout`]'s total `FromStr` door.
117///
118/// A **named** variant wins. Anything the vocabulary does not name —
119/// `FromStr`'s `Other` escape, which is where `"3 channels (FL+FR+TFL)"`
120/// and every custom-order rendering land — collapses to
121/// [`ChannelLayout::default`], the absent sentinel. That collapse is
122/// deliberate: `known_kind` answers *which named layout is this*, and
123/// "none of them" is `Other("")`; the rendering is already carried
124/// verbatim by [`ChannelLayoutDescription::text`], so letting it ride
125/// `Other` too would put a second, differently-shaped copy of the same
126/// string in the same struct.
127fn channel_layout_from_describe(rendered: &str) -> ChannelLayout {
128 // **A rendering too long to be a slug never reaches `from_str`.**
129 //
130 // `ChannelLayout::from_str` is total: what it does not recognise it
131 // wraps in `Other(Utf8Bytes::from(s))`, and because `s` is *borrowed*
132 // that constructor **copies** — infallibly — for anything past
133 // `smol_bytes::INLINE_CAP`. The line below then throws that copy
134 // away. So an unnamed layout whose rendering the container controls
135 // (a long `FL+FR+…` enumeration, a custom order's channel list) paid
136 // for an allocation this crate had no use for and could not refuse:
137 // an abort reachable from a file, on the road that exists to answer
138 // "is this one of the layouts we name?".
139 //
140 // Every slug the vocabulary recognises is short — the longest in
141 // mediaframe 0.11's roster is `7.1(wide-side)` at fourteen bytes,
142 // against an inline window of sixty-two — so a rendering past that
143 // window cannot be a match, and skipping the parse loses nothing.
144 // `renderings_of_named_layouts_fit_the_inline_window` pins that
145 // against FFmpeg's own standard-layout roster rather than against
146 // this comment.
147 //
148 // Within the window `from_str` allocates nothing: it folds into a
149 // stack buffer and compares byte slices, and the `Other` arm it can
150 // still reach stores its bytes inline.
151 //
152 // (A `ChannelLayout::parse_known(&str) -> Option<Self>` that never
153 // constructs `Other` is the real fix and is filed against mediaframe
154 // for 0.11.1; this is the surgical one, on this side of the seam.)
155 if rendered.len() > smol_bytes::INLINE_CAP {
156 return ChannelLayout::default();
157 }
158 ChannelLayout::from_str(rendered)
159 .ok()
160 .filter(|layout| !matches!(layout, ChannelLayout::Other(_)))
161 .unwrap_or_default()
162}
163
164/// Maps FFmpeg's [`AVChannelOrder`](ffi::AVChannelOrder) to the
165/// [`ChannelOrder`] tag.
166pub fn channel_order_from_ffmpeg(value: ffi::AVChannelOrder) -> ChannelOrder {
167 // Compare via integer rather than enum-matching: the caller often
168 // sources `value` from raw FFmpeg memory (`AVChannelLayout.order`),
169 // and an unknown variant would already be UB before reaching this
170 // function. Going through `as i32` here is sound because the caller
171 // is responsible for the up-conversion path; for the raw-pointer
172 // path use [`channel_order_from_raw`].
173 channel_order_from_raw(value as i32)
174}
175
176/// Variant of [`channel_order_from_ffmpeg`] that takes the raw integer
177/// directly. Use this when the caller has just read
178/// `AVChannelLayout.order` from FFmpeg memory and doesn't want to
179/// risk constructing an invalid bindgen enum value first.
180pub fn channel_order_from_raw(raw: i32) -> ChannelOrder {
181 match raw {
182 x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32 => ChannelOrder::Native,
183 x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 => ChannelOrder::Custom,
184 x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32 => ChannelOrder::Ambisonic,
185 _ => ChannelOrder::Unspecified,
186 }
187}
188
189/// Why a channel layout could not be described.
190///
191/// Three answers, and the first two are **memory-safety** ones rather
192/// than resource ones. `av_channel_layout_describe` walks `u.map[i]`
193/// for each of `nb_channels` when the order is `CUSTOM`, and FFmpeg's
194/// struct puts nothing between a caller and that walk: the map is a
195/// bare pointer and the count is a bare `int`. Refusing is not
196/// politeness, it is the precondition.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
198#[non_exhaustive]
199pub enum ChannelLayoutFault {
200 /// A `CUSTOM` order reached a **safe** conversion, which cannot
201 /// establish that the map it points at is as long as the count it
202 /// declares.
203 ///
204 /// `ffmpeg_next::ChannelLayout` is a public newtype over a public
205 /// `AVChannelLayout`, so safe Rust can write `nb_channels = 2` beside
206 /// a `u.map` that points at one entry — or at a dangling address, or
207 /// at something misaligned. Checking for null and for a terminator
208 /// inside each name does not help: **neither provenance nor extent is
209 /// observable from the pointer**, and the loop that would check the
210 /// names is itself the out-of-bounds read. There is no validation a
211 /// safe function can perform here, so it performs none and refuses.
212 ///
213 /// The extent has to come from the *caller* instead, which is what
214 /// [`channel_layout_description_from_raw_ptr`]'s `unsafe` contract
215 /// asks for. Inside this crate the demux and convert roads satisfy it
216 /// from FFmpeg's own `AVCodecParameters` and `AVFrame`, where
217 /// `av_channel_layout_copy` allocated the map and sized it — and they
218 /// argue exactly that at each call.
219 #[error(
220 "a custom channel layout declaring {channels} channels reached a safe conversion, which \
221 cannot verify that its map has that many entries"
222 )]
223 UnverifiableCustomMap {
224 /// `nb_channels`, as the layout declared it.
225 channels: i32,
226 },
227 /// A `CUSTOM` order whose `u.map` is null, or whose channel count is
228 /// not positive, or one of whose sixteen-byte names carries no NUL.
229 /// FFmpeg would dereference the map, or read past a name, while
230 /// describing it.
231 #[error("a custom channel layout declares {channels} channels and carries no usable map")]
232 MalformedCustomMap {
233 /// `nb_channels`, as the layout declared it.
234 channels: i32,
235 },
236 /// A layout whose **declared shape** is not one FFmpeg's own helpers
237 /// can be given, for an order other than `CUSTOM`.
238 ///
239 /// `av_channel_layout_describe` and `av_channel_layout_compare` both
240 /// assume the invariants `av_channel_layout_check` states, and
241 /// nothing between a caller and those helpers enforces them: a
242 /// `NATIVE` layout whose `nb_channels` disagrees with its mask's
243 /// population, an `AMBISONIC` layout whose channels do not form an
244 /// ambisonic order, and any layout declaring a count outside the
245 /// range every downstream calculation assumes. FFmpeg computes
246 /// `nb_channels - popcount(mask)` and takes an integer square root of
247 /// it in signed C arithmetic; a count a safe caller can simply write
248 /// into the public struct is enough to take that somewhere it was
249 /// never meant to go.
250 #[error(
251 "a channel layout of order {order} declaring {channels} channels is not a shape FFmpeg's \
252 own helpers can be given"
253 )]
254 MalformedLayout {
255 /// `order`, as the raw `c_int` it is on the wire.
256 order: i32,
257 /// `nb_channels`, as the layout declared it.
258 channels: i32,
259 },
260 /// The description could not be allocated.
261 #[error("out of memory describing a channel layout")]
262 Alloc,
263}
264
265/// Which arm of an `AVChannelLayout`'s union an order defines, if any —
266/// **the one dispatcher every union read in this crate goes through.**
267///
268/// It lives here rather than beside any one of its callers because the
269/// rule was discovered twice: R13 fixed the resampler's equality and
270/// `Debug`, and R14 found the codec-ticket mirror still reading
271/// `u.mask` for an unspecified layout. A rule written in two places
272/// becomes two rules, and this one governs whether a read is defined at
273/// all.
274///
275/// The distinction FFmpeg's header makes and this crate had been
276/// eliding: `NATIVE` and `AMBISONIC` define `u.mask`, `CUSTOM` defines
277/// `u.map`, and for `UNSPEC` the union is **undefined and must not be
278/// used**. An order this build does not name defines nothing either —
279/// a future FFmpeg may give it an arm, and until this crate is taught
280/// which, reading one would be a guess about storage a container
281/// supplied.
282#[derive(Copy, Clone, PartialEq, Eq, Debug)]
283pub(crate) enum LayoutArm {
284 /// `u.mask` — `NATIVE` and `AMBISONIC`.
285 Mask,
286 /// `u.map` — `CUSTOM`.
287 Map,
288 /// Neither: `UNSPEC`, and every order this build has never heard of.
289 Undefined,
290}
291
292impl LayoutArm {
293 /// Folds a raw `AVChannelOrder` — never an `AVChannelOrder` value,
294 /// which a container may hold outside this build's discriminant set.
295 pub(crate) const fn of(order_raw: i32) -> Self {
296 if order_raw == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32
297 || order_raw == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32
298 {
299 Self::Mask
300 } else if order_raw == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
301 Self::Map
302 } else {
303 Self::Undefined
304 }
305 }
306}
307
308/// The widest channel count this crate will hand to an FFmpeg layout
309/// helper.
310///
311/// Not a capacity and not a policy: a bound that keeps FFmpeg's own
312/// `int` arithmetic — `nb_channels - popcount(mask)`, and the integer
313/// square root taken of it for an ambisonic order — nowhere near where
314/// signed overflow lives. Nothing real comes close: a `NATIVE` layout
315/// is capped at sixty-four by its own mask, and the largest ambisonic
316/// order anyone records is a two-digit number of channels.
317const MAX_DECLARED_CHANNELS: i32 = 65_535;
318
319/// **The complete, allocation-free preflight for every channel-layout
320/// order — and every FFmpeg layout helper in this crate is behind it.**
321///
322/// # Why it is one function and not a check at each call
323///
324/// FFmpeg's layout helpers (`av_channel_layout_describe`,
325/// `av_channel_layout_compare`, `av_channel_layout_copy`, and
326/// `swr_alloc_set_opts2` / `swr_build_matrix2` through the contexts they
327/// configure) all assume the invariants `av_channel_layout_check`
328/// states, and none of them checks. `AVChannelLayout` is a public
329/// struct with public fields, and `ffmpeg_next::ChannelLayout` is a
330/// public newtype over it — so every one of those invariants is
331/// something *safe* Rust can break, and the helper that trips over it
332/// does so inside C.
333///
334/// Earlier rounds of review closed the custom-map road and left the
335/// others on the assumption that an order which describes its channels
336/// through a `uint64_t` mask cannot be malformed. It can: the mask is
337/// not the only field: `nb_channels` is an `int` a caller writes, and
338/// an `AMBISONIC` layout with an extreme one reaches arithmetic that
339/// was never given a bound. The lesson each round has taught is that a
340/// rule written in two places becomes two rules, so this one is written
341/// once and everything that calls a helper goes through it.
342///
343/// # What it decides, per order
344///
345/// - **any order**: a count below zero, or above
346/// [`MAX_DECLARED_CHANNELS`] — reported as the *map* fault for a
347/// `CUSTOM` layout, because that is the specific thing to say about
348/// one, and as the shape fault for the rest;
349/// - **any order but an unspecified one**: a count of zero, which
350/// `av_channel_layout_check` refuses for every order in its opening
351/// line. The exception is deliberate and narrow: the all-zero
352/// layout — `UNSPEC` with no channels — is how a container says it
353/// declared none, and no FFmpeg helper is ever called for it;
354/// - **`NATIVE`**: `popcount(u.mask) == nb_channels`, FFmpeg's own
355/// invariant — which also caps such a layout at sixty-four channels
356/// for free;
357/// - **`AMBISONIC`**: `av_channel_layout_check`'s own rule and only it —
358/// the non-diegetic channels named by `u.mask` must leave at least
359/// one channel for the ambisonic part, i.e. `popcount(mask) <
360/// nb_channels`. The remainder is deliberately **not** required to be
361/// a perfect square: that is
362/// `av_channel_layout_ambisonic_order`'s question, which answers
363/// `EINVAL` for what its own comment calls an "incomplete order", and
364/// an incomplete-order layout is one FFmpeg will still describe and
365/// convert;
366/// - **`CUSTOM`**: [`custom_map_fault`]'s rule, unchanged — null map,
367/// non-positive count, or a name with no NUL inside its sixteen
368/// bytes;
369/// - **`UNSPEC`**, and any order this build does not name: nothing
370/// beyond the count, because no helper is called for them.
371///
372/// # Safety
373///
374/// `ptr` must be a live `*const AVChannelLayout`. For a `CUSTOM` order
375/// with a non-null `u.map`, that map must hold `nb_channels` live
376/// `AVChannelCustom` entries — FFmpeg's own contract for a layout it
377/// filled, and [`custom_map_fault`]'s.
378pub(crate) unsafe fn layout_preflight(
379 ptr: *const ffi::AVChannelLayout,
380) -> Result<(), ChannelLayoutFault> {
381 use core::ptr::{addr_of, read_unaligned};
382
383 // SAFETY: `ptr` is live per the contract; `addr_of!` reaches `order`
384 // without forming a reference to it, and reading it as `i32` matches
385 // the bindgen enum's `c_int` storage — the field may hold a value no
386 // variant names.
387 let order = unsafe { read_unaligned(addr_of!((*ptr).order).cast::<i32>()) };
388 // SAFETY: a plain `int` field of a live struct.
389 let channels = unsafe { (*ptr).nb_channels };
390 let malformed = Err(ChannelLayoutFault::MalformedLayout { order, channels });
391
392 // **The order's own rule first, where it has one.** A `CUSTOM` layout
393 // declaring a count outside the bound is malformed as a *map* — that
394 // is the specific thing to say about it, and the count rule below is
395 // the general one. Reporting the general fault for a custom layout
396 // would tell a caller less than this crate knows.
397 if order == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
398 if !(0..=MAX_DECLARED_CHANNELS).contains(&channels) {
399 return Err(ChannelLayoutFault::MalformedCustomMap { channels });
400 }
401 // SAFETY: the caller's contract, forwarded.
402 return match unsafe { custom_map_fault(ptr, order) } {
403 Some(channels) => Err(ChannelLayoutFault::MalformedCustomMap { channels }),
404 None => Ok(()),
405 };
406 }
407 if !(0..=MAX_DECLARED_CHANNELS).contains(&channels) {
408 return malformed;
409 }
410 // **A positive count, as `av_channel_layout_check` requires of every
411 // order — with exactly one deliberate exception.**
412 //
413 // `check` opens with `if (nb_channels <= 0) return 0;`, so a `NATIVE`
414 // layout declaring zero channels and a zero mask is invalid to
415 // FFmpeg even though its own mask rule (`popcount == nb_channels`)
416 // would be satisfied by it. This crate admitted exactly that.
417 //
418 // The exception is the all-zero `AVChannelLayout`: `UNSPEC` with no
419 // channels is `ffmpeg_next::ChannelLayout::default()` and the state
420 // `avcodec_parameters_alloc` leaves behind, and this crate reads it
421 // as **"the container declared no layout"** rather than as a claim
422 // about one. Nothing is ever handed to an FFmpeg helper for it — the
423 // `Undefined` arm below returns before any call — so admitting it
424 // asserts nothing that could be wrong. Declaring it invalid here
425 // would refuse every stream that simply has no layout.
426 if channels == 0 && !matches!(LayoutArm::of(order), LayoutArm::Undefined) {
427 return malformed;
428 }
429 if matches!(LayoutArm::of(order), LayoutArm::Undefined) {
430 // `UNSPEC`, and any order this build does not name: no helper is
431 // called for them and the union is not theirs to read, so the count
432 // above is the whole of the rule.
433 return Ok(());
434 }
435 if order == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32 {
436 // SAFETY: the order names the `mask` arm of the union.
437 let mask = unsafe { (*ptr).u.mask };
438 return if i64::from(mask.count_ones()) == i64::from(channels) {
439 Ok(())
440 } else {
441 malformed
442 };
443 }
444 if order == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32 {
445 // SAFETY: the order names the `mask` arm of the union.
446 let mask = unsafe { (*ptr).u.mask };
447 // **FFmpeg's own validity rule, and nothing more than it.**
448 // `av_channel_layout_check` reads, in full:
449 //
450 // ```c
451 // case AV_CHANNEL_ORDER_AMBISONIC:
452 // /* If non-diegetic channels are present, ensure they are
453 // taken into account */
454 // return av_popcount64(channel_layout->u.mask) < channel_layout->nb_channels;
455 // ```
456 //
457 // The mask names the non-diegetic channels that follow the
458 // ambisonic ones, so at least one channel must be left for the
459 // ambisonic part. That is the whole of it — and the comparison
460 // also refuses a zero count, which is why no separate positivity
461 // test is needed here.
462 //
463 // This arm used to demand that the remainder be a perfect square,
464 // `(order + 1)²`. That is a real property and FFmpeg computes it —
465 // in `av_channel_layout_ambisonic_order`, whose own comment calls
466 // the failing case "incomplete order - some harmonics are missing"
467 // and which answers `AVERROR(EINVAL)` for it. **It is that
468 // function's question, not validity's**: `check` never consults it,
469 // an incomplete-order layout is a layout FFmpeg will describe and
470 // convert, and refusing one here turned a real file away. Asking a
471 // stricter question than the library asks is not caution; it is a
472 // different answer to a question nobody posed.
473 return if i64::from(mask.count_ones()) < i64::from(channels) {
474 Ok(())
475 } else {
476 malformed
477 };
478 }
479 // Unreachable: `LayoutArm` folds every order into one of the three
480 // arms and all three are answered above. Kept total rather than
481 // asserted, because an order that grows a fourth arm should reach a
482 // conservative answer rather than a panic.
483 Ok(())
484}
485
486/// Builds a fully-populated [`ChannelLayoutDescription`] from an FFmpeg
487/// [`AvChannelLayout`].
488///
489/// - Native / Ambisonic layouts populate `native_mask` from
490/// [`AvChannelLayout::bits`] (clearing it to `None` if zero).
491/// - Custom layouts populate `custom_channels` from FFmpeg's per-channel
492/// list (`AVChannelLayout.u.map`), with each label drawn from
493/// `AVChannelCustom.name`.
494/// - `text` carries the result of `av_channel_layout_describe`
495/// (FFmpeg's human-readable rendering — e.g. `"5.1(side)"`).
496/// - `known_kind` runs [`channel_layout_from_ffmpeg`]'s two rungs
497/// against that same single rendering: constant table first, then
498/// the describe rung.
499pub fn channel_layout_description_from_ffmpeg(
500 value: &AvChannelLayout,
501) -> Result<ChannelLayoutDescription, ChannelLayoutFault> {
502 use core::ptr::{addr_of, read_unaligned};
503
504 let ptr = &value.0 as *const ffi::AVChannelLayout;
505 // **A `CUSTOM` order is refused here rather than read.**
506 //
507 // This is the boundary between what a *safe* signature can promise
508 // and what it cannot. `AvChannelLayout` is `ffmpeg_next::ChannelLayout`,
509 // a public newtype over a public `AVChannelLayout`: safe Rust can set
510 // `nb_channels` to two and point `u.map` at one entry, at a dangling
511 // address, or at something misaligned, and nothing in the type says
512 // otherwise. Walking `nb_channels` entries to check them would *be*
513 // the out-of-bounds read, and every FFmpeg helper this module calls —
514 // `av_channel_layout_describe`, `av_channel_layout_compare` — makes
515 // the same indexing assumption. Provenance and extent are not
516 // observable from a pointer, so no check placed here can establish
517 // them.
518 //
519 // The extent comes from the caller instead, through
520 // [`channel_layout_description_from_raw_ptr`]'s `unsafe` contract.
521 // See [`ChannelLayoutFault::UnverifiableCustomMap`].
522 //
523 // SAFETY: `value` is a live reference, so `ptr` is a live
524 // `*const AVChannelLayout`; `addr_of!` reaches `order` without
525 // forming a reference to it, and reading it as `i32` matches the
526 // bindgen enum's `c_int` storage — the field may hold a value no
527 // variant names.
528 let order =
529 channel_order_from_raw(unsafe { read_unaligned(addr_of!((*ptr).order).cast::<i32>()) });
530 if matches!(order, ChannelOrder::Custom) {
531 // SAFETY: as above — a plain `int` field of a live struct.
532 let channels = unsafe { (*ptr).nb_channels };
533 return Err(ChannelLayoutFault::UnverifiableCustomMap { channels });
534 }
535
536 // SAFETY: the order is one of the non-`CUSTOM` variants, and for
537 // those the implementation below reads `u.mask` (a `uint64_t`, not a
538 // pointer) and never `u.map`, so the map's extent — the one thing a
539 // safe caller could lie about — is not consulted. `value` is a live
540 // reference for the duration of the call, which is the rest of the
541 // contract.
542 unsafe { channel_layout_description_from_raw_ptr(ptr) }
543}
544
545/// Pointer variant of [`channel_layout_description_from_ffmpeg`], and
546/// **the only road that reads a custom channel map**.
547///
548/// The safe form refuses a `CUSTOM` order outright, because nothing it
549/// is handed can establish the map's extent; this one requires that
550/// extent of its caller instead, and is therefore `unsafe`. The pointer
551/// shape also lets the convert path pass
552/// `addr_of!((*av_frame).ch_layout)` straight through without
553/// materializing a typed reference.
554///
555/// # Safety
556///
557/// 1. `ptr` must be a live, aligned `*const AVChannelLayout` for the
558/// duration of this call.
559/// 2. **If `(*ptr).order` is `AV_CHANNEL_ORDER_CUSTOM` and `u.map` is
560/// non-null, `u.map` must point at a live, aligned, initialised
561/// array of exactly `nb_channels` `AVChannelCustom` entries** — the
562/// invariant `av_channel_layout_copy` and every libavcodec road that
563/// fills a layout maintain, and the one this function's own walk and
564/// FFmpeg's helpers both index against. A shorter array, a dangling
565/// pointer, or a negative-but-nonzero count is undefined behaviour,
566/// and no check inside can recover it.
567///
568/// What the function *does* check, because those are content faults
569/// rather than extent ones: a null `u.map` beside a positive count, a
570/// non-positive count, and a sixteen-byte name with no NUL inside it
571/// (which `av_channel_layout_describe` would hand to `%s`). Each is
572/// refused as [`ChannelLayoutFault::MalformedCustomMap`] *before* any
573/// FFmpeg helper sees the layout.
574///
575/// `order` is read raw and folded before anything else, and no
576/// `&AVChannelLayout` is formed until it is known to be a discriminant
577/// this build names.
578pub unsafe fn channel_layout_description_from_raw_ptr(
579 ptr: *const ffi::AVChannelLayout,
580) -> Result<ChannelLayoutDescription, ChannelLayoutFault> {
581 use core::ptr::{addr_of, read_unaligned};
582 // Read `order` as a raw integer first — never let Rust assume
583 // the field is a valid `AVChannelOrder`.
584 // SAFETY: `ptr` is a valid `*const AVChannelLayout`; `addr_of!`
585 // computes the field address without forming a reference; reading
586 // as `i32` matches the bindgen enum's `c_int` storage.
587 let order_raw = unsafe { read_unaligned(addr_of!((*ptr).order) as *const i32) };
588 let order = channel_order_from_raw(order_raw);
589 let nb_channels = unsafe { (*ptr).nb_channels };
590
591 // Native / Ambisonic carry the bitmask in the union. Only read
592 // `u.mask` after the order is validated so we don't trip on an
593 // unknown order writing into a future variant of the union.
594 let native_mask = match LayoutArm::of(order_raw) {
595 // SAFETY: this arm names exactly the orders whose contract defines
596 // `u.mask`; see [`LayoutArm`], which is where that rule lives.
597 LayoutArm::Mask => {
598 let mask = unsafe { (*ptr).u.mask };
599 if mask != 0 { Some(mask) } else { None }
600 }
601 LayoutArm::Map | LayoutArm::Undefined => None,
602 };
603
604 // Build name / rendering through ffmpeg-next helpers. They take
605 // `&AvChannelLayout` (which is `repr(transparent)` over
606 // `AVChannelLayout`), but at this point we've already validated
607 // `order`, so forming the reference is sound: the only enum-typed
608 // field in `AVChannelLayout` is `order`, and it now holds a value
609 // that came back from `channel_order_from_raw` with the
610 // unknown bucket folded into a known variant — but the *underlying
611 // struct* still has the original raw bytes. We can't form `&AVChannelLayout`
612 // over an unknown order without UB, so for those helpers we
613 // explicitly only call them when order is one of the known variants.
614 // **The custom map is validated before FFmpeg is allowed to look at
615 // the layout at all**, and that ordering is the whole of this
616 // paragraph. `av_channel_layout_describe` renders a `CUSTOM` layout
617 // by walking `u.map[i]` for each of `nb_channels`; a layout that
618 // declares channels and carries a null map makes it read from null.
619 // The codec ticket's own null-map refusal happens *later* and
620 // therefore cannot protect this call — and this function's contract
621 // asks its caller for a live pointer and nothing more, so a layout
622 // like that is an input, not a caller error.
623 //
624 // SAFETY: `order` has been folded from the raw integer, so the
625 // `map` arm is read only for the order that names it; `addr_of!`
626 // reaches the union field without forming a reference to the layout.
627 // **Judged before FFmpeg is allowed to look at the layout at all** —
628 // every order, through [`layout_preflight`], which is the one place
629 // these rules are written. See its doc for what it decides and why
630 // the codec ticket, the decoder and the resampler share it rather
631 // than restating it.
632 //
633 // SAFETY: the caller's contract, forwarded: `ptr` is live, and for a
634 // `CUSTOM` order its map holds `nb_channels` entries.
635 unsafe { layout_preflight(ptr) }?;
636
637 let (known_kind, text) = if matches!(order, ChannelOrder::Unspecified) {
638 (ChannelLayout::default(), Utf8Bytes::default())
639 } else {
640 // SAFETY: `order` is one of {Native, Custom, Ambisonic} — all of
641 // which are valid `AVChannelOrder` discriminants present in our
642 // bindgen output, so `&*ptr` is sound to form here.
643 let layout_ref = unsafe { &*(ptr as *const AvChannelLayout) };
644 let text = describe_layout(layout_ref)?;
645 // Constant-arm table first, exactly as in
646 // `channel_layout_from_ffmpeg`; the rendering is consulted only
647 // when the layout falls off it. Describing once and feeding both
648 // fields from that one string keeps `known_kind` and `text`
649 // answering from the same FFmpeg call.
650 let known_kind =
651 mapped_constant(layout_ref).unwrap_or_else(|| channel_layout_from_describe(&text));
652 (known_kind, text)
653 };
654 let custom_channels_vec = unsafe { custom_channels_raw(ptr, order) }?;
655
656 Ok(
657 ChannelLayoutDescription::new(nb_channels.max(0) as u32)
658 .with_order(order)
659 .with_known_kind(known_kind)
660 .with_native_mask(native_mask)
661 .with_custom_channels(custom_channels_vec)
662 .with_text(text),
663 )
664}
665
666/// **Every structural fault a `CUSTOM` channel map can carry, decided
667/// without allocating** — `Some(nb_channels)` when FFmpeg must not be
668/// asked to read this layout, `None` when it may.
669///
670/// # The one place this rule is written
671///
672/// Two roads need it and they must not come to two answers. This
673/// module's own describe road refuses with
674/// [`ChannelLayoutFault::MalformedCustomMap`]; the codec ticket's
675/// admission pass refuses with `DemuxError::ParametersChannelMap`,
676/// *before the track table has allocated anything* — and a rule that
677/// admission applied less strictly than materialisation would mean a
678/// deterministic refusal arriving after the memory was already spent,
679/// which is the defect this function exists to make impossible.
680///
681/// # What it decides
682///
683/// - a null `u.map`, or a channel count that is not positive:
684/// `av_channel_layout_describe` walks `u.map[i]` for each of
685/// `nb_channels` and `av_channel_layout_copy` `memcpy`s from it, both
686/// with no null check of their own;
687/// - **a sixteen-byte name with no NUL inside it.** FFmpeg does not
688/// only walk the map: it tests `u.map[i].name[0]` and, when set,
689/// hands the fixed array to a `%s` conversion, which reads until a
690/// terminator. A full sixteen bytes of name is type-valid, safely
691/// constructible, and makes that read run off the end of the entry.
692/// `AVChannelCustom` documents the field as zeroed or
693/// NUL-terminated; nothing enforces it;
694/// - **an entry whose `id` is `AV_CHAN_NONE`.**
695/// `av_channel_layout_check` walks the map for exactly this and
696/// refuses the layout, because an entry that names no channel is a
697/// hole in the map rather than a channel with an unusual id. Admitted,
698/// it reached a codec ticket and was published as `u32::MAX`.
699///
700/// Everything but `CUSTOM` describes its channels through the union's
701/// `mask` arm, a `uint64_t` that cannot be malformed, so those orders
702/// are always `None`.
703///
704/// # Safety
705///
706/// `ptr` must be a live `*const AVChannelLayout`, and `order_raw` must
707/// be its own `order` field read as the `c_int` it is on the wire —
708/// never as an `AVChannelOrder`, which a container may hold a value
709/// outside. For a `CUSTOM` order with a non-null `u.map`, that map must
710/// hold `nb_channels` live `AVChannelCustom` entries — FFmpeg's own
711/// contract for a layout it filled.
712pub(crate) unsafe fn custom_map_fault(
713 ptr: *const ffi::AVChannelLayout,
714 order_raw: i32,
715) -> Option<i32> {
716 use core::ptr::{addr_of, read_unaligned};
717
718 // Compared raw rather than through the folded vocabulary: an order
719 // this build does not name folds to `Unspecified`, and "not custom"
720 // has to mean exactly that the `map` arm is not the live one.
721 if order_raw != ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
722 return None;
723 }
724 // SAFETY: `ptr` is live per the contract, and the order names the
725 // `map` arm of the union.
726 let (map_ptr, channels) = unsafe { ((*ptr).u.map, (*ptr).nb_channels) };
727 if map_ptr.is_null() || channels <= 0 {
728 return Some(channels);
729 }
730 for index in 0..channels as usize {
731 // SAFETY: the map is `nb_channels` entries long per the contract
732 // above and `index` is below that; `addr_of!` reaches the name
733 // array without forming a reference to the entry, whose `id` is an
734 // open enum.
735 let name = unsafe { read_unaligned(addr_of!((*map_ptr.add(index)).name).cast::<[u8; 16]>()) };
736 if !name.contains(&0) {
737 return Some(channels);
738 }
739 // **And no entry may be `AV_CHAN_NONE`.** `av_channel_layout_check`
740 // walks the map for exactly this and refuses the layout; an entry
741 // that names no channel is a hole in the map, not a channel with an
742 // unusual id. Read raw — `id` is an open enum and a container may
743 // write a value outside this build's discriminant set, which makes
744 // an `AVChannel`-typed read undefined before any comparison on it
745 // could run.
746 //
747 // SAFETY: as above — the map holds `channels` entries and `addr_of!`
748 // reaches the field without forming a reference to it.
749 let id = unsafe { read_unaligned(addr_of!((*map_ptr.add(index)).id).cast::<i32>()) };
750 if id == ffi::AVChannel::AV_CHAN_NONE as i32 {
751 return Some(channels);
752 }
753 }
754 None
755}
756
757/// Pointer-form of `custom_channels`. `order` must be the result of
758/// reading `(*ptr).order` as `i32` and folding through
759/// [`channel_order_from_raw`]; this skips re-reading it.
760///
761/// # Safety
762/// `ptr` must be a live `*const AVChannelLayout`. Reads only fields
763/// (`u.map`, `nb_channels`, and the per-channel array) — no `&AVChannelLayout`
764/// reference is ever formed.
765unsafe fn custom_channels_raw(
766 ptr: *const ffi::AVChannelLayout,
767 order: ChannelOrder,
768) -> Result<Vec<ChannelSpec>, ChannelLayoutFault> {
769 use core::ptr::{addr_of, read_unaligned};
770 if !matches!(order, ChannelOrder::Custom) {
771 return Ok(Vec::new());
772 }
773 let count = unsafe { (*ptr).nb_channels }.max(0) as usize;
774 if count == 0 {
775 return Ok(Vec::new());
776 }
777 // SAFETY: The `u` field is a union; reading `.map` is sound when
778 // `order == CUSTOM` per FFmpeg's documented contract. Guard
779 // explicitly for null.
780 let map_ptr = unsafe { (*ptr).u.map };
781 if map_ptr.is_null() {
782 return Ok(Vec::new());
783 }
784 // Iterate the AVChannelCustom array via raw pointers — never form
785 // `&[AVChannelCustom]` or `&AVChannelCustom`, because each entry
786 // contains `id: AVChannel`, a bindgen enum. If FFmpeg writes an
787 // unknown channel id (version skew / hostile decoder), the
788 // reference itself would be UB before the raw `id` read could
789 // sanitize it.
790 // `nb_channels` is the container's number, so the table it sizes is
791 // reserved fallibly: this runs after admission, and a count the
792 // caller's ceilings let through must come back as an error rather
793 // than an abort.
794 let mut out = Vec::new();
795 out
796 .try_reserve_exact(count)
797 .map_err(|_| ChannelLayoutFault::Alloc)?;
798 for index in 0..count {
799 // SAFETY: `map_ptr` points to `count == nb_channels` valid
800 // `AVChannelCustom` entries per FFmpeg's contract; `index < count`,
801 // so `entry_ptr` lies inside the allocation.
802 let entry_ptr: *const ffi::AVChannelCustom = unsafe { map_ptr.add(index) };
803 // SAFETY: `entry_ptr` is a valid pointer; `addr_of!((*p).field)`
804 // computes the field address without forming a reference.
805 let raw_id = unsafe { read_unaligned(addr_of!((*entry_ptr).id) as *const i32) };
806 // The label is built fallibly and carried whole; an allocator
807 // refusal on a file-declared channel count is reported rather than
808 // absorbed. SAFETY: as above.
809 let label = unsafe { custom_channel_label_raw(entry_ptr) }?;
810 out.push(ChannelSpec::new(index as u32, raw_id as u32).with_label(label));
811 }
812 Ok(out)
813}
814
815/// Pointer-form of `custom_channel_label` — never forms
816/// `&AVChannelCustom`, since the struct contains an enum-typed `id`.
817///
818/// # Safety
819/// `entry_ptr` must be a live `*const AVChannelCustom`.
820/// Decodes FFmpeg's bytes into the vocabulary's own carrier —
821/// **fallibly, and whole**.
822///
823/// # What this replaced, and why the replacement is the point
824///
825/// mediaframe's text seats took `SmolStr`, whose constructor is
826/// infallible and allocates past a twenty-three-byte inline window. On
827/// a road that runs once per channel of a file-declared count, that is
828/// an abort this crate could not report — so for several rounds a
829/// label or rendering too long to store inline was reported **absent**
830/// instead: a truthful refusal, but a lossy one, and a workaround
831/// rather than an answer.
832///
833/// mediaframe 0.11 moves those seats to [`Utf8Bytes`], whose road can
834/// be made fallible end to end: measure the decoding without producing
835/// it, reserve exactly that much, build it, and **move** the buffer
836/// into the carrier. Nothing is truncated and nothing is silently
837/// dropped; a label is either carried in full or the open is refused by
838/// name.
839///
840/// The measuring and building halves are [`crate::demuxer::lossy_len`]
841/// and [`crate::demuxer::lossy_text`] — the same two the metadata road
842/// uses, shared rather than restated, because a second copy of a
843/// lossy-decoding rule is a second rule.
844fn decode_text(bytes: &[u8]) -> Result<Utf8Bytes, ChannelLayoutFault> {
845 let decoded = crate::demuxer::lossy_len(bytes);
846 crate::demuxer::lossy_text(bytes, decoded).map_err(|_| ChannelLayoutFault::Alloc)
847}
848
849unsafe fn custom_channel_label_raw(
850 entry_ptr: *const ffi::AVChannelCustom,
851) -> Result<Utf8Bytes, ChannelLayoutFault> {
852 use core::ptr::addr_of;
853 // SAFETY: `name: [c_char; 16]` is an inline byte array — no
854 // validity invariant beyond initialization (FFmpeg guarantees that).
855 // `addr_of!` computes the address; we then re-interpret as `*const u8`
856 // for UTF-8 lossy decoding.
857 let name_ptr = unsafe { addr_of!((*entry_ptr).name) } as *const u8;
858 // SAFETY: `name` is exactly 16 bytes wide.
859 let bytes = unsafe { slice::from_raw_parts(name_ptr, 16) };
860 let end = bytes
861 .iter()
862 .position(|byte| *byte == 0)
863 .unwrap_or(bytes.len());
864 if end == 0 {
865 // An empty name is "there is none", which is the seat's own
866 // documented absent value — not a refusal and not a loss.
867 return Ok(Utf8Bytes::default());
868 }
869 decode_text(&bytes[..end])
870}
871
872// **`custom_channels` and `custom_channel_label` were deleted here, and
873// the deletion is the finding rather than tidying.** Both were dead
874// (`#[allow(dead_code)]`), superseded by [`custom_channels_raw`] — and
875// both formed `&[AVChannelCustom]` over FFmpeg's map, which is the very
876// undefined behaviour the raw walk was written to avoid: each entry
877// carries `id: AVChannel`, a bindgen enum, and a container that writes
878// a value outside this build's discriminant set makes the *reference*
879// UB before any `match` on it can run. Unreachable code that would be
880// unsound if revived is a landmine, not documentation.
881
882/// Renders a layout the way FFmpeg names it (`av_channel_layout_describe`).
883fn describe_layout(layout: &AvChannelLayout) -> Result<Utf8Bytes, ChannelLayoutFault> {
884 // `av_channel_layout_describe` returns the number of bytes needed
885 // (excluding the NUL terminator). Start with a 128-byte buffer —
886 // comfortably bigger than every named layout — and grow once if it
887 // wasn't enough. Use `c_char` for portability (signed on
888 // x86/aarch64-Apple, unsigned on aarch64-Linux).
889 // Reserved fallibly, both times: the second length is FFmpeg's
890 // rendering of a layout whose channel count came out of a file.
891 let mut buf: std::vec::Vec<c_char> = std::vec::Vec::new();
892 buf
893 .try_reserve_exact(128)
894 .map_err(|_| ChannelLayoutFault::Alloc)?;
895 buf.resize(128, 0 as c_char);
896 let mut needed =
897 unsafe { ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len()) };
898 if needed < 0 {
899 return Ok(Utf8Bytes::default());
900 }
901 if needed as usize >= buf.len() {
902 let want = needed as usize + 1;
903 buf
904 .try_reserve_exact(want - buf.len())
905 .map_err(|_| ChannelLayoutFault::Alloc)?;
906 buf.resize(want, 0 as c_char);
907 needed = unsafe {
908 ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len())
909 };
910 if needed < 0 {
911 return Ok(Utf8Bytes::default());
912 }
913 }
914 // SAFETY: buf is heap-allocated, NUL-terminated by FFmpeg's contract.
915 let bytes = unsafe { slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len()) };
916 // Clamped, because `needed` is FFmpeg's answer rather than this
917 // buffer's: the grow above makes `needed < buf.len()` true, and the
918 // `min` is what keeps that an argument rather than an assumption.
919 let end = bytes
920 .iter()
921 .position(|byte| *byte == 0)
922 .unwrap_or(needed as usize)
923 .min(bytes.len());
924 if end == 0 {
925 return Ok(Utf8Bytes::default());
926 }
927 // **Carried whole, and fallibly.** The rendering is FFmpeg's answer
928 // for a layout whose channel count came out of a file, so its size is
929 // the container's business; what this crate owes is an honest
930 // refusal rather than a truncation or an abort. See [`decode_text`]
931 // for the road, and for what the `SmolStr` seat used to force here.
932 decode_text(&bytes[..end])
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938
939 /// Builds a NATIVE-order layout from a channel mask, the way a decoder
940 /// hands one over. This is how a layout `ffmpeg_next` mints no constant
941 /// for can be reached at all: `av_channel_layout_from_mask` fills in the
942 /// order and the channel count, so nothing about the value is
943 /// hand-forged.
944 fn native(mask: u64) -> AvChannelLayout {
945 // SAFETY: an all-zero `AVChannelLayout` is `AV_CHANNEL_ORDER_UNSPEC`
946 // with no channels — a valid value, and the same starting point
947 // `ffmpeg_next`'s own `ChannelLayout::default` uses. The constructor
948 // then overwrites every field.
949 let mut raw: ffi::AVChannelLayout = unsafe { core::mem::zeroed() };
950 // SAFETY: `raw` is a live, writable `AVChannelLayout`.
951 let rc = unsafe { ffi::av_channel_layout_from_mask(&mut raw, mask) };
952 assert_eq!(rc, 0, "av_channel_layout_from_mask({mask:#x}) failed");
953 AvChannelLayout(raw)
954 }
955
956 /// `AV_CH_LAYOUT_BINAURAL`, spelled the way the FFmpeg header spells it
957 /// (`1ULL << AV_CHAN_BINAURAL_*`). The composed `AV_CH_BINAURAL_LEFT` /
958 /// `_RIGHT` macros do not survive bindgen's macro evaluation, but the
959 /// `AVChannel` enum they shift by does, so the mask is still derived
960 /// from FFmpeg's own numbers rather than typed out.
961 fn binaural_mask() -> u64 {
962 (1u64 << ffi::AVChannel::AV_CHAN_BINAURAL_LEFT as u64)
963 | (1u64 << ffi::AVChannel::AV_CHAN_BINAURAL_RIGHT as u64)
964 }
965
966 /// The three layouts [`ChannelLayout`] names but `ffmpeg_next` 9.0.0
967 /// mints no constant for. The constant table cannot reach them by
968 /// construction; the describe rung does, because FFmpeg's own layout
969 /// map names all three and the vocabulary reads that word.
970 #[test]
971 fn orphan_layouts_are_named_through_the_describe_rung() {
972 let cases = [
973 (binaural_mask(), "binaural", ChannelLayout::Binaural),
974 (
975 ffi::AV_CH_LAYOUT_5POINT1 | ffi::AV_CH_TOP_FRONT_LEFT | ffi::AV_CH_TOP_FRONT_RIGHT,
976 "5.1.2",
977 ChannelLayout::Ch5_1_2,
978 ),
979 (
980 ffi::AV_CH_LAYOUT_9POINT1POINT4_BACK | ffi::AV_CH_TOP_SIDE_LEFT | ffi::AV_CH_TOP_SIDE_RIGHT,
981 "9.1.6",
982 ChannelLayout::Ch9_1_6,
983 ),
984 ];
985 for (mask, slug, expected) in cases {
986 let layout = native(mask);
987 assert_eq!(
988 mapped_constant(&layout),
989 None,
990 "{slug} must fall off the constant table — that is what makes it an orphan"
991 );
992 assert_eq!(
993 describe_layout(&layout)
994 .expect("a well-formed layout describes")
995 .as_str(),
996 slug,
997 "FFmpeg must name {slug} for the rung to have a word to read"
998 );
999 assert_eq!(
1000 channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
1001 expected,
1002 "{slug} must reach its named variant through the rung"
1003 );
1004
1005 let described =
1006 channel_layout_description_from_ffmpeg(&layout).expect("a well-formed layout describes");
1007 assert_eq!(
1008 described.known_kind(),
1009 &expected,
1010 "{slug} must be named on the description path too"
1011 );
1012 assert_eq!(described.text(), slug, "{slug} rendering rides `text`");
1013 }
1014 }
1015
1016 /// FFmpeg 9's actual `5.1.4`: the *side*-surround mask
1017 /// (`FL+FR+FC+LFE+SL+SR` plus the four heights), which its layout map
1018 /// names and no constant here reaches.
1019 ///
1020 /// `ffmpeg_sys_next` 9.0.0 bundles a `channel_layout_fixed.h` that
1021 /// `#undef`s FFmpeg's layout macros and re-declares them as C
1022 /// constants, and its `AV_CH_LAYOUT_5POINT1POINT4_BACK` still carries
1023 /// FFmpeg 8's *back*-surround formula. So `ffmpeg_next`'s
1024 /// `_5POINT1POINT4_BACK` constant — the one the table compares against
1025 /// — is a mask FFmpeg 9 no longer names, and the mask FFmpeg 9 *does*
1026 /// name has no constant at all. This is the ruling's "a layout the
1027 /// vocabulary already names is reachable with zero adapter edits",
1028 /// arriving earlier than expected.
1029 ///
1030 /// Asserted through the public entry point alone, deliberately: if the
1031 /// upstream shim is ever refreshed the constant table will start
1032 /// answering this mask itself, and `5.1.4` must come out named either
1033 /// way.
1034 #[test]
1035 fn ffmpeg_nines_own_5_1_4_is_named() {
1036 let layout = native(
1037 ffi::AV_CH_LAYOUT_5POINT1
1038 | ffi::AV_CH_TOP_FRONT_LEFT
1039 | ffi::AV_CH_TOP_FRONT_RIGHT
1040 | ffi::AV_CH_TOP_BACK_LEFT
1041 | ffi::AV_CH_TOP_BACK_RIGHT,
1042 );
1043 assert_eq!(
1044 describe_layout(&layout)
1045 .expect("a well-formed layout describes")
1046 .as_str(),
1047 "5.1.4"
1048 );
1049 assert_eq!(
1050 channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
1051 ChannelLayout::Ch5_1_4Back
1052 );
1053 }
1054
1055 /// The constant table is the first rung and answers alone.
1056 ///
1057 /// `Some` here *is* the bypass proof: [`channel_layout_from_ffmpeg`] is
1058 /// `mapped_constant(..).unwrap_or_else(<describe rung>)`, and
1059 /// `unwrap_or_else` does not evaluate its closure on `Some` — so a
1060 /// mapped constant never renders, never parses, and cannot be
1061 /// re-answered by a word.
1062 ///
1063 /// The sample is the crossed-slug family (where FFmpeg qualifies the
1064 /// *side* layout in one place and the *back* one in another, so a
1065 /// name-based answer is the one that could plausibly differ), plus the
1066 /// `_7POINT1_TOP_BACK` alias that shares `_5POINT1POINT2_BACK`'s mask
1067 /// and therefore has no arm of its own.
1068 #[test]
1069 fn mapped_constants_are_answered_by_the_table_alone() {
1070 let table = [
1071 ("MONO", AvChannelLayout::MONO, ChannelLayout::Mono),
1072 ("STEREO", AvChannelLayout::STEREO, ChannelLayout::Stereo),
1073 (
1074 "STEREO_DOWNMIX",
1075 AvChannelLayout::STEREO_DOWNMIX,
1076 ChannelLayout::StereoDownmix,
1077 ),
1078 ("SURROUND", AvChannelLayout::SURROUND, ChannelLayout::Ch3_0),
1079 ("_5POINT0", AvChannelLayout::_5POINT0, ChannelLayout::Ch5_0),
1080 (
1081 "_5POINT0_BACK",
1082 AvChannelLayout::_5POINT0_BACK,
1083 ChannelLayout::Ch5_0Back,
1084 ),
1085 ("_5POINT1", AvChannelLayout::_5POINT1, ChannelLayout::Ch5_1),
1086 (
1087 "_5POINT1_BACK",
1088 AvChannelLayout::_5POINT1_BACK,
1089 ChannelLayout::Ch5_1Back,
1090 ),
1091 (
1092 "_5POINT1POINT2_BACK",
1093 AvChannelLayout::_5POINT1POINT2_BACK,
1094 ChannelLayout::Ch5_1_2Back,
1095 ),
1096 (
1097 "_7POINT1_TOP_BACK",
1098 AvChannelLayout::_7POINT1_TOP_BACK,
1099 ChannelLayout::Ch5_1_2Back,
1100 ),
1101 (
1102 "_7POINT1_WIDE",
1103 AvChannelLayout::_7POINT1_WIDE,
1104 ChannelLayout::Ch7_1Wide,
1105 ),
1106 (
1107 "_7POINT1_WIDE_BACK",
1108 AvChannelLayout::_7POINT1_WIDE_BACK,
1109 ChannelLayout::Ch7_1WideBack,
1110 ),
1111 (
1112 "_22POINT2",
1113 AvChannelLayout::_22POINT2,
1114 ChannelLayout::Ch22_2,
1115 ),
1116 ];
1117 for (name, layout, expected) in table {
1118 assert_eq!(
1119 mapped_constant(&layout),
1120 Some(expected.clone()),
1121 "{name} must be answered by the constant table, not by a rendering"
1122 );
1123 assert_eq!(
1124 channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
1125 expected,
1126 "{name}"
1127 );
1128 }
1129 }
1130
1131 /// **Every rendering FFmpeg gives a layout it names fits the inline
1132 /// window** — which is what makes the parse bypass lossless.
1133 ///
1134 /// `channel_layout_from_describe` skips `ChannelLayout::from_str`
1135 /// above `smol_bytes::INLINE_CAP`, because past that the `Other` arm
1136 /// copies a borrowed string on the heap, infallibly, only for the
1137 /// line below to discard it. The bypass is only sound if no
1138 /// *recognisable* slug is that long.
1139 ///
1140 /// This pins it against FFmpeg's own roster rather than against a
1141 /// list written here: `av_channel_layout_standard` iterates every
1142 /// standard layout the linked library knows, and each one's rendering
1143 /// is measured. A future FFmpeg that grows a sixty-three-byte layout
1144 /// name fails this lane rather than silently losing that layout.
1145 #[test]
1146 fn renderings_of_named_layouts_fit_the_inline_window() {
1147 let mut opaque: *mut core::ffi::c_void = core::ptr::null_mut();
1148 let mut seen = 0usize;
1149 loop {
1150 // SAFETY: `av_channel_layout_standard` walks a static table and
1151 // is documented to take the address of an opaque cursor, which
1152 // starts null and is advanced by the call.
1153 let layout = unsafe { ffi::av_channel_layout_standard(&mut opaque) };
1154 if layout.is_null() {
1155 break;
1156 }
1157 // SAFETY: the pointer is into libavutil's own static roster, live
1158 // for the process.
1159 let rendered = unsafe { describe_layout(&*(layout as *const AvChannelLayout)) }
1160 .expect("a standard layout describes");
1161 assert!(
1162 rendered.len() <= smol_bytes::INLINE_CAP,
1163 "{rendered:?} is {} bytes, past the {} the parse bypass assumes",
1164 rendered.len(),
1165 smol_bytes::INLINE_CAP,
1166 );
1167 seen += 1;
1168 }
1169 assert!(seen > 10, "the standard roster should not be nearly empty");
1170 }
1171
1172 /// A rendering past the inline window is answered *absent* without
1173 /// the parse — so the `Other` copy that would be discarded is never
1174 /// made.
1175 #[test]
1176 fn a_long_rendering_is_not_parsed_at_all() {
1177 let long = "FL+FR+FC+LFE+BL+BR+FLC+FRC+BC+SL+SR+TC+TFL+TFC+TFR+TBL+TBC+TBR".repeat(4);
1178 assert!(long.len() > smol_bytes::INLINE_CAP);
1179 assert_eq!(
1180 channel_layout_from_describe(&long),
1181 ChannelLayout::default(),
1182 "an unnameable rendering is absent, and nothing was copied to decide that",
1183 );
1184 // And a slug at the window's own width still parses, so the bound
1185 // is a bound rather than a shortcut.
1186 assert_eq!(
1187 channel_layout_from_describe("5.1(side)"),
1188 ChannelLayout::Ch5_1
1189 );
1190 }
1191
1192 /// A layout nobody names stays *absent*. The rung upgrades the sentinel
1193 /// to a named variant or leaves it alone; it never smuggles FFmpeg's
1194 /// rendering into `known_kind`'s escape, because `text` already carries
1195 /// that rendering verbatim.
1196 #[test]
1197 fn an_unnamed_layout_stays_absent_with_its_rendering_in_text() {
1198 // FL+FR+TFL: a native mask FFmpeg's layout map does not carry, so
1199 // `av_channel_layout_describe` falls back to listing the channels.
1200 let layout = native(ffi::AV_CH_FRONT_LEFT | ffi::AV_CH_FRONT_RIGHT | ffi::AV_CH_TOP_FRONT_LEFT);
1201 assert_eq!(mapped_constant(&layout), None);
1202
1203 let rendering = describe_layout(&layout).expect("a well-formed layout describes");
1204 assert!(
1205 rendering.contains("TFL"),
1206 "FFmpeg should list the channels it cannot name: {rendering:?}"
1207 );
1208 assert_eq!(
1209 channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
1210 ChannelLayout::default(),
1211 "an unnamed layout must land on the absent sentinel"
1212 );
1213
1214 let described =
1215 channel_layout_description_from_ffmpeg(&layout).expect("a well-formed layout describes");
1216 assert_eq!(described.known_kind(), &ChannelLayout::default());
1217 assert_eq!(
1218 described.text(),
1219 rendering.as_str(),
1220 "the rendering is what `text` carries"
1221 );
1222 }
1223
1224 /// The rung itself, on describe-shaped strings — the half of the door
1225 /// that needs no `AVChannelLayout` to exercise.
1226 #[test]
1227 fn the_describe_rung_reads_names_and_refuses_everything_else() {
1228 // The three orphans, as words.
1229 assert_eq!(
1230 channel_layout_from_describe("binaural"),
1231 ChannelLayout::Binaural
1232 );
1233 assert_eq!(
1234 channel_layout_from_describe("5.1.2"),
1235 ChannelLayout::Ch5_1_2
1236 );
1237 assert_eq!(
1238 channel_layout_from_describe("9.1.6"),
1239 ChannelLayout::Ch9_1_6
1240 );
1241 // The crossed slugs: unqualified `5.1` is the *back* layout and the
1242 // side one is qualified, so reading the word is the only way to tell
1243 // these two apart.
1244 assert_eq!(
1245 channel_layout_from_describe("5.1"),
1246 ChannelLayout::Ch5_1Back
1247 );
1248 assert_eq!(
1249 channel_layout_from_describe("5.1(side)"),
1250 ChannelLayout::Ch5_1
1251 );
1252 // Case folding is the vocabulary's, not ours.
1253 assert_eq!(
1254 channel_layout_from_describe("BINAURAL"),
1255 ChannelLayout::Binaural
1256 );
1257
1258 // Everything else is absent — never `Other(<the rendering>)`.
1259 for unnamed in [
1260 "",
1261 "3 channels",
1262 "3 channels (FL+FR+TFL)",
1263 "FL@Left+FR@Right",
1264 "ambisonic 2",
1265 "not-a-layout",
1266 ] {
1267 assert_eq!(
1268 channel_layout_from_describe(unnamed),
1269 ChannelLayout::default(),
1270 "{unnamed:?} must stay absent"
1271 );
1272 }
1273 }
1274}
1275
1276#[cfg(test)]
1277mod null_map_tests {
1278 use super::*;
1279
1280 /// **A custom layout with no map is refused before FFmpeg sees it.**
1281 ///
1282 /// `av_channel_layout_describe` renders a `CUSTOM` layout by walking
1283 /// `u.map[i]` for each of `nb_channels`. This function's contract
1284 /// asks its caller for a live pointer and nothing more, so a layout
1285 /// that declares channels and carries a null map is an *input* — and
1286 /// it used to reach `describe_layout` and `mapped_constant` before
1287 /// anything looked at the map, which is a read from null inside
1288 /// FFmpeg rather than an error out of this crate.
1289 ///
1290 /// The layout below is exactly that shape. If the validation is ever
1291 /// reordered behind the description again, this lane does not fail —
1292 /// it crashes, which is the honest signal for the defect it pins.
1293 #[test]
1294 fn a_custom_layout_without_a_map_is_refused_before_ffmpeg_is_called() {
1295 // SAFETY: a zeroed `AVChannelLayout` is a valid value; `order` is
1296 // then set to the CUSTOM discriminant and `nb_channels` to a
1297 // positive count, leaving `u.map` null — the shape under test.
1298 let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1299 layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1300 layout.nb_channels = 6;
1301
1302 // SAFETY: `layout` is live for the call and never escapes it.
1303 let described =
1304 unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
1305 assert_eq!(
1306 described,
1307 Err(ChannelLayoutFault::MalformedCustomMap { channels: 6 }),
1308 );
1309 }
1310
1311 /// **The safe road refuses a custom layout outright — it never reads
1312 /// the map, not even to check it.**
1313 ///
1314 /// The layout below is the shape no check can survive: `nb_channels`
1315 /// says two, the map holds one entry, and both the pointer and the
1316 /// count are things safe Rust set. A null check passes. A
1317 /// NUL-terminator walk over `nb_channels` entries passes the first
1318 /// one and then reads past the array — the check *is* the
1319 /// out-of-bounds read. FFmpeg's own helpers index the same way.
1320 ///
1321 /// So the safe conversions do not look. They fold `order`, see
1322 /// `CUSTOM`, and refuse; the extent has to come from an `unsafe`
1323 /// caller that can vouch for it. If this is ever "fixed" by
1324 /// validating instead of refusing, this lane does not fail — it
1325 /// reads a second `AVChannelCustom` that was never allocated, which
1326 /// under Miri is a hard error and in the wild is whatever happens to
1327 /// follow it in memory.
1328 #[test]
1329 fn a_safe_conversion_refuses_a_custom_layout_rather_than_trusting_its_count() {
1330 // One entry, correctly terminated: everything about it is valid
1331 // except that the layout beside it claims there are two.
1332 let mut name = [0 as core::ffi::c_char; 16];
1333 name[0] = b'F' as core::ffi::c_char;
1334 name[1] = b'L' as core::ffi::c_char;
1335 let map = [ffi::AVChannelCustom {
1336 id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
1337 name,
1338 opaque: core::ptr::null_mut(),
1339 }];
1340 // SAFETY: a zeroed `AVChannelLayout` is a valid value; the fields
1341 // below are set to the shape under test and `map` outlives the
1342 // call.
1343 let mut inner: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1344 inner.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1345 inner.nb_channels = 2;
1346 inner.u.map = map.as_ptr().cast_mut();
1347 // `ffmpeg_next::ChannelLayout` is a public newtype over that public
1348 // struct, which is the whole of why this is reachable from safe
1349 // code.
1350 let layout = AvChannelLayout(inner);
1351
1352 assert_eq!(
1353 channel_layout_description_from_ffmpeg(&layout),
1354 Err(ChannelLayoutFault::UnverifiableCustomMap { channels: 2 }),
1355 "the safe description road must refuse a custom layout, not validate it",
1356 );
1357 assert_eq!(
1358 channel_layout_from_ffmpeg(&layout),
1359 Err(ChannelLayoutFault::UnverifiableCustomMap { channels: 2 }),
1360 "and so must the safe naming road, which shares the implementation",
1361 );
1362 }
1363
1364 /// A layout the safe road *can* answer for: `u.map` is never read for
1365 /// a native order, so nothing about a custom map's extent is in
1366 /// question and the description is produced as before.
1367 #[test]
1368 fn a_safe_conversion_still_answers_for_a_native_layout() {
1369 let layout = AvChannelLayout::STEREO;
1370 let described =
1371 channel_layout_description_from_ffmpeg(&layout).expect("a native layout describes");
1372 assert_eq!(described.channels(), 2);
1373 assert_eq!(
1374 channel_layout_from_ffmpeg(&layout).expect("a native layout names"),
1375 ChannelLayout::Stereo,
1376 );
1377 }
1378
1379 /// **A layout that is not `CUSTOM` can still be malformed, and the
1380 /// preflight is what says so before FFmpeg is handed it.**
1381 ///
1382 /// For ten rounds the non-custom orders were admitted on the argument
1383 /// that a `uint64_t` mask cannot be wrong. The mask is not the only
1384 /// field: `nb_channels` is an `int` that safe Rust writes into a
1385 /// public struct, and FFmpeg's own arithmetic over it —
1386 /// `nb_channels - popcount(mask) - 1`, and an integer square root of
1387 /// that — was never given a bound.
1388 ///
1389 /// The rule the refusal applies is `av_channel_layout_check`'s and
1390 /// **only** its. A first cut demanded a complete ambisonic order as
1391 /// well, which is `av_channel_layout_ambisonic_order`'s question and
1392 /// refused layouts FFmpeg accepts. Both halves are asserted below:
1393 /// what the bound still guards, and what it must no longer refuse.
1394 #[test]
1395 fn a_non_custom_layout_with_a_broken_shape_is_refused() {
1396 // SAFETY: a zeroed `AVChannelLayout` is a valid value; each case
1397 // below sets only scalar fields and the `mask` arm of the union,
1398 // and the layout never leaves this function.
1399 let build = |order: ffi::AVChannelOrder, channels: i32, mask: u64| unsafe {
1400 let mut layout: ffi::AVChannelLayout = std::mem::zeroed();
1401 layout.order = order;
1402 layout.nb_channels = channels;
1403 layout.u.mask = mask;
1404 layout
1405 };
1406 let refused = |layout: &ffi::AVChannelLayout| {
1407 // SAFETY: `layout` is live for the call and never escapes it.
1408 unsafe { layout_preflight(layout as *const ffi::AVChannelLayout) }
1409 };
1410
1411 // An ambisonic layout with a count near `i32::MAX`: the arithmetic
1412 // FFmpeg does over it was never given a bound, so this is refused
1413 // on the count alone, before any helper sees it.
1414 let huge = build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC, i32::MAX, 0);
1415 assert_eq!(
1416 refused(&huge),
1417 Err(ChannelLayoutFault::MalformedLayout {
1418 order: ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32,
1419 channels: i32::MAX,
1420 }),
1421 );
1422
1423 // **An incomplete-order ambisonic layout is VALID**, and this crate
1424 // used to refuse it. Five channels are not `(order + 1)²` for any
1425 // order — `av_channel_layout_ambisonic_order` answers `EINVAL` and
1426 // its own comment calls the case "incomplete order - some harmonics
1427 // are missing" — but `av_channel_layout_check` never asks that
1428 // question, so FFmpeg describes and converts the layout and so must
1429 // this crate.
1430 assert_eq!(
1431 refused(&build(
1432 ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
1433 5,
1434 0
1435 )),
1436 Ok(()),
1437 "an incomplete order is not an invalid layout",
1438 );
1439 // Four is a complete first-order layout and is equally fine; the
1440 // validity rule does not distinguish them.
1441 assert_eq!(
1442 refused(&build(
1443 ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
1444 4,
1445 0
1446 )),
1447 Ok(()),
1448 );
1449 // What the rule *does* say, in full: the non-diegetic channels the
1450 // mask names must leave at least one for the ambisonic part.
1451 // Sixteen declared against two named is fine, square or not.
1452 let stereo_mask = ffi::AV_CH_FRONT_LEFT | ffi::AV_CH_FRONT_RIGHT;
1453 assert_eq!(
1454 refused(&build(
1455 ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
1456 16,
1457 stereo_mask,
1458 )),
1459 Ok(()),
1460 );
1461 // Two named and two declared leaves none, which is the one thing
1462 // `av_channel_layout_check` refuses on this arm — and one declared
1463 // against two named is the same refusal.
1464 for declared in [2, 1] {
1465 assert!(
1466 refused(&build(
1467 ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
1468 declared,
1469 stereo_mask,
1470 ))
1471 .is_err(),
1472 "{declared} channels against a two-channel mask leaves no ambisonic part",
1473 );
1474 }
1475
1476 // A native layout whose count disagrees with its mask: FFmpeg's own
1477 // invariant, and the one that also caps such a layout at sixty-four
1478 // channels for free.
1479 let lying = build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE, 7, stereo_mask);
1480 assert_eq!(
1481 refused(&lying),
1482 Err(ChannelLayoutFault::MalformedLayout {
1483 order: ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32,
1484 channels: 7,
1485 }),
1486 );
1487 assert_eq!(
1488 refused(&build(
1489 ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE,
1490 2,
1491 stereo_mask,
1492 )),
1493 Ok(()),
1494 "the same mask with its true count is a layout FFmpeg names",
1495 );
1496
1497 // A negative count is refused whatever the order.
1498 assert!(refused(&build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC, -1, 0)).is_err(),);
1499
1500 // **A zero count is refused for every order but an unspecified
1501 // one**, which is `av_channel_layout_check`'s opening line. A
1502 // `NATIVE` layout declaring no channels with an empty mask
1503 // satisfies that order's *own* rule — `popcount(0) == 0` — and is
1504 // invalid all the same; this crate used to admit it.
1505 for order in [
1506 ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE,
1507 ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
1508 ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM,
1509 ] {
1510 assert!(
1511 refused(&build(order, 0, 0)).is_err(),
1512 "{order:?} with no channels is not a layout",
1513 );
1514 }
1515 // The one exception, and it is a statement about *absence* rather
1516 // than about a layout: the all-zero `AVChannelLayout` is how a
1517 // container says it declared none, and no FFmpeg helper is ever
1518 // called for it.
1519 assert_eq!(
1520 refused(&build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC, 0, 0)),
1521 Ok(()),
1522 );
1523 }
1524
1525 /// A custom layout that declares **no** channels is not malformed —
1526 /// there is nothing for FFmpeg to walk — so it describes normally.
1527 #[test]
1528 fn a_custom_layout_with_no_channels_is_not_malformed() {
1529 // SAFETY: as above.
1530 let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1531 layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1532
1533 // SAFETY: `layout` is live for the call.
1534 let described =
1535 unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
1536 assert!(
1537 matches!(
1538 described,
1539 Err(ChannelLayoutFault::MalformedCustomMap { channels: 0 })
1540 ),
1541 "a zero-channel custom layout carries no map either, and is refused the same way",
1542 );
1543 }
1544
1545 /// **A name with no NUL inside its sixteen bytes is refused too.**
1546 ///
1547 /// FFmpeg does not only walk the map: it tests `u.map[i].name[0]`
1548 /// and hands the fixed array to a `%s` conversion, which reads until
1549 /// a NUL. Sixteen non-zero bytes are type-valid and safely
1550 /// constructible, and they make that read run off the end of the
1551 /// entry — so the map being present and correctly sized is not
1552 /// enough, and this is the only place the difference can be caught.
1553 #[test]
1554 fn a_custom_name_without_a_terminator_is_refused() {
1555 let entries = [ffi::AVChannelCustom {
1556 id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
1557 name: [b'x' as core::ffi::c_char; 16],
1558 opaque: core::ptr::null_mut(),
1559 }];
1560 // SAFETY: a zeroed layout is valid; the map below is live for the
1561 // call, correctly sized for the one channel declared, and its one
1562 // entry deliberately carries no terminator.
1563 let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1564 layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1565 layout.nb_channels = 1;
1566 layout.u.map = entries.as_ptr().cast_mut();
1567
1568 // SAFETY: `layout` and `entries` are live for the call.
1569 let described =
1570 unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
1571 assert_eq!(
1572 described,
1573 Err(ChannelLayoutFault::MalformedCustomMap { channels: 1 }),
1574 "a correctly sized map is not a describable one if a name never ends",
1575 );
1576 }
1577
1578 /// **A map entry that names no channel is a hole, and the layout is
1579 /// refused.**
1580 ///
1581 /// `av_channel_layout_check` walks a custom map for exactly this:
1582 /// `if (channel_layout->u.map[i].id == AV_CHAN_NONE) return 0;`. This
1583 /// crate checked the map's presence, its count and each name's
1584 /// terminator, and never looked at the id — so a layout with a hole
1585 /// in it passed admission, reached a codec ticket, and published
1586 /// `AV_CHAN_NONE` to a consumer as `u32::MAX`.
1587 ///
1588 /// The id is read raw because it is an open enum: a container may
1589 /// write a value outside this build's discriminant set, and an
1590 /// `AVChannel`-typed read would be undefined before any comparison
1591 /// could run.
1592 #[test]
1593 fn a_map_entry_that_names_no_channel_is_refused() {
1594 let mut name = [0 as core::ffi::c_char; 16];
1595 name[0] = b'F' as core::ffi::c_char;
1596 name[1] = b'L' as core::ffi::c_char;
1597 // Two entries, the *second* of which is the hole — a validation
1598 // that stops early passes anything else.
1599 let entries = [
1600 ffi::AVChannelCustom {
1601 id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
1602 name,
1603 opaque: core::ptr::null_mut(),
1604 },
1605 ffi::AVChannelCustom {
1606 id: ffi::AVChannel::AV_CHAN_NONE,
1607 name,
1608 opaque: core::ptr::null_mut(),
1609 },
1610 ];
1611 // SAFETY: a zeroed layout is valid; the map below is live for the
1612 // call and correctly sized for the two channels declared.
1613 let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1614 layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1615 layout.nb_channels = 2;
1616 layout.u.map = entries.as_ptr().cast_mut();
1617
1618 // SAFETY: `layout` and `entries` are live for the call.
1619 let described =
1620 unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
1621 assert_eq!(
1622 described,
1623 Err(ChannelLayoutFault::MalformedCustomMap { channels: 2 }),
1624 "an entry naming no channel is a hole in the map, not a channel",
1625 );
1626
1627 // The same map with both entries naming a channel describes, so the
1628 // refusal is about the hole rather than about custom layouts.
1629 let whole = [
1630 entries[0],
1631 ffi::AVChannelCustom {
1632 id: ffi::AVChannel::AV_CHAN_FRONT_RIGHT,
1633 name,
1634 opaque: core::ptr::null_mut(),
1635 },
1636 ];
1637 // SAFETY: as above.
1638 let mut ok_layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1639 ok_layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1640 ok_layout.nb_channels = 2;
1641 ok_layout.u.map = whole.as_ptr().cast_mut();
1642 // SAFETY: as above.
1643 unsafe { channel_layout_description_from_raw_ptr(&ok_layout as *const ffi::AVChannelLayout) }
1644 .expect("a map with no holes describes");
1645 }
1646
1647 /// The same map with a terminator describes normally, so the check
1648 /// above is about the terminator rather than about custom layouts.
1649 #[test]
1650 fn a_terminated_custom_name_describes() {
1651 let mut name = [0 as core::ffi::c_char; 16];
1652 name[0] = b'F' as core::ffi::c_char;
1653 name[1] = b'L' as core::ffi::c_char;
1654 let entries = [ffi::AVChannelCustom {
1655 id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
1656 name,
1657 opaque: core::ptr::null_mut(),
1658 }];
1659 // SAFETY: as above, with a terminated name.
1660 let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
1661 layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
1662 layout.nb_channels = 1;
1663 layout.u.map = entries.as_ptr().cast_mut();
1664
1665 // SAFETY: `layout` and `entries` are live for the call.
1666 let described =
1667 unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) }
1668 .expect("a terminated name is describable");
1669 assert_eq!(described.channels(), 1);
1670 }
1671
1672 /// **Text is carried whole and built fallibly** — the workaround
1673 /// that reported it *absent* is gone with the seat that forced it.
1674 ///
1675 /// Two lanes used to stand here, both asserting that a label or a
1676 /// rendering past `SmolStr`'s twenty-three-byte inline window came
1677 /// back empty. That was honest about what the crate did and dishonest
1678 /// about what the container said: mediaframe's seats took `SmolStr`,
1679 /// whose constructor is infallible, so on a road that runs once per
1680 /// channel of a file-declared count the only alternative to an
1681 /// unreportable abort was dropping the text. mediaframe 0.11 moves
1682 /// those seats to `Utf8Bytes`, and the road is fallible end to end.
1683 #[test]
1684 fn text_is_decoded_whole_and_fallibly() {
1685 for raw in [
1686 &b""[..],
1687 &b"FL"[..],
1688 &b"\xff"[..],
1689 &b"\xff\xfe\xfd"[..],
1690 &b"ok\xffafter"[..],
1691 &b"\xe2\x82"[..],
1692 &[0xffu8; 16][..],
1693 b"FrontLeftSurrnd",
1694 b"5.1(side)",
1695 // The two shapes the old rule dropped: a rendering longer than
1696 // the inline window, and one whose lossy expansion crossed it.
1697 b"FL+FR+FC+LFE+BL+BR+SL+SR",
1698 &b"a very long rendering\xff\xff that is not text either"[..],
1699 ] {
1700 let decoded = std::string::String::from_utf8_lossy(raw);
1701 assert_eq!(
1702 decode_text(raw)
1703 .expect("an allocator that is not refusing")
1704 .as_str(),
1705 decoded.as_ref(),
1706 "{raw:?} must be carried exactly as the lossy decoder reads it, whatever its length",
1707 );
1708 }
1709 }
1710}