Skip to main content

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}