use crate::error::AllocFrameError;
use ffmpeg_sys_next::AVMediaType::{
AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
AVMEDIA_TYPE_VIDEO,
};
#[cfg(ffmpeg_8_0)]
use ffmpeg_sys_next::av_frame_side_data_clone;
#[cfg(not(docsrs))]
use ffmpeg_sys_next::av_frame_side_data_free;
use ffmpeg_sys_next::{
av_freep, av_gettime_relative, avcodec_free_context, avformat_close_input,
avformat_free_context, avio_closep, avio_context_free, AVCodecContext, AVFormatContext,
AVFrameSideData, AVIOContext, AVMediaType, AVRational, AVStream, AVFMT_NOFILE,
};
use std::ffi::c_void;
use std::ptr::null_mut;
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use std::sync::Arc;
const OUTPUT_END_GRACE_US: i64 = 500_000;
pub(crate) const DEFAULT_CUSTOM_IO_BUFFER_SIZE: usize = 64 * 1024;
pub(crate) struct InterruptState {
scheduler_status: Arc<AtomicUsize>,
end_grace_start_us: AtomicI64,
finalizing_outputs: AtomicUsize,
}
impl InterruptState {
pub(crate) fn new(scheduler_status: Arc<AtomicUsize>) -> Self {
Self {
scheduler_status,
end_grace_start_us: AtomicI64::new(0),
finalizing_outputs: AtomicUsize::new(0),
}
}
pub(crate) fn begin_output_finalize(self: &Arc<Self>) -> OutputFinalizeGuard {
self.finalizing_outputs.fetch_add(1, Ordering::AcqRel);
OutputFinalizeGuard {
state: Arc::clone(self),
}
}
fn should_interrupt_input(&self) -> bool {
crate::core::scheduler::ffmpeg_scheduler::is_stopping(
self.scheduler_status.load(Ordering::Acquire),
)
}
fn should_interrupt_output(&self) -> bool {
let status = self.scheduler_status.load(Ordering::Acquire);
if status == crate::core::scheduler::ffmpeg_scheduler::STATUS_ABORT {
return true;
}
if status != crate::core::scheduler::ffmpeg_scheduler::STATUS_END {
return false;
}
if self.finalizing_outputs.load(Ordering::Acquire) > 0 {
return false;
}
let now = unsafe { av_gettime_relative() };
let start = match self.end_grace_start_us.compare_exchange(
0,
now,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => now,
Err(previous) => previous,
};
now - start > OUTPUT_END_GRACE_US
}
}
pub(crate) struct OutputFinalizeGuard {
state: Arc<InterruptState>,
}
impl Drop for OutputFinalizeGuard {
fn drop(&mut self) {
self.state.finalizing_outputs.fetch_sub(1, Ordering::AcqRel);
}
}
pub(crate) unsafe extern "C" fn input_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
let state = &*(opaque as *const InterruptState);
state.should_interrupt_input() as libc::c_int
}
pub(crate) unsafe extern "C" fn output_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
let state = &*(opaque as *const InterruptState);
let interrupt = state.should_interrupt_output();
#[cfg(test)]
if interrupt {
crate::core::scheduler::tcp_write_probe::note_cut_election();
}
interrupt as libc::c_int
}
pub(crate) struct MuxStartGate {
started: std::sync::atomic::AtomicBool,
lock: std::sync::Mutex<()>,
}
pub(crate) enum PreSendOutcome {
Sent,
Started(PacketBox),
Full(PacketBox),
Disconnected(PacketBox),
}
impl MuxStartGate {
pub(crate) fn new() -> Self {
Self {
started: std::sync::atomic::AtomicBool::new(false),
lock: std::sync::Mutex::new(()),
}
}
pub(crate) fn is_started(&self) -> bool {
self.started.load(Ordering::Acquire)
}
pub(crate) fn start_with(&self, drain: impl FnOnce()) {
let _guard = self.lock.lock().unwrap();
drain();
self.started.store(true, Ordering::Release);
}
pub(crate) fn send_pre(
&self,
pre_sender: &pre_mux_queue::PreMuxQueueSender,
packet_box: PacketBox,
) -> PreSendOutcome {
let _guard = self.lock.lock().unwrap();
if self.started.load(Ordering::Acquire) {
return PreSendOutcome::Started(packet_box);
}
match pre_sender.try_push(packet_box) {
pre_mux_queue::PreQueueTryPush::Sent => PreSendOutcome::Sent,
pre_mux_queue::PreQueueTryPush::Full(pb) => PreSendOutcome::Full(pb),
pre_mux_queue::PreQueueTryPush::Disconnected(pb) => PreSendOutcome::Disconnected(pb),
}
}
}
use ffmpeg_context::{InputOpaque, OutputOpaque};
pub mod ffmpeg_context;
pub mod ffmpeg_context_builder;
pub mod input;
pub mod output;
pub mod filter_complex;
pub(super) mod attachment;
pub(super) mod decoder_stream;
pub(super) mod demuxer;
pub(super) mod encoder_stream;
pub(super) mod filter_graph;
pub(super) mod frame_source;
pub(super) mod input_filter;
pub(super) mod muxer;
pub(super) mod obj_pool;
pub(super) mod output_filter;
pub(super) mod pre_mux_queue;
pub mod null_output;
pub(crate) struct CodecContext {
inner: *mut AVCodecContext,
}
unsafe impl Send for CodecContext {}
impl CodecContext {
pub(crate) fn new(avcodec_context: *mut AVCodecContext) -> Self {
Self {
inner: avcodec_context,
}
}
pub(crate) fn as_mut_ptr(&self) -> *mut AVCodecContext {
self.inner
}
pub(crate) fn as_ptr(&self) -> *const AVCodecContext {
self.inner as *const AVCodecContext
}
}
impl Drop for CodecContext {
fn drop(&mut self) {
unsafe {
avcodec_free_context(&mut self.inner);
}
}
}
#[derive(Copy, Clone)]
pub(crate) struct Stream {
pub(crate) inner: *mut AVStream,
}
unsafe impl Send for Stream {}
pub(crate) struct FrameBox {
pub(crate) frame: ffmpeg_next::Frame,
pub(crate) frame_data: FrameData,
}
unsafe impl Send for FrameBox {}
pub fn frame_alloc() -> crate::error::Result<ffmpeg_next::Frame> {
unsafe {
let frame = ffmpeg_next::Frame::empty();
if frame.as_ptr().is_null() {
return Err(AllocFrameError::OutOfMemory.into());
}
Ok(frame)
}
}
pub fn null_frame() -> ffmpeg_next::Frame {
unsafe { ffmpeg_next::Frame::wrap(null_mut()) }
}
pub(crate) struct SideDataList {
entries: *mut *mut AVFrameSideData,
count: i32,
}
impl SideDataList {
pub(crate) fn new() -> Self {
Self {
entries: null_mut(),
count: 0,
}
}
#[cfg(ffmpeg_8_0)]
pub(crate) fn push_clone(&mut self, sd: *const AVFrameSideData, flags: u32) -> i32 {
unsafe { av_frame_side_data_clone(&mut self.entries, &mut self.count, sd, flags) }
}
pub(crate) fn clear(&mut self) {
#[cfg(not(docsrs))]
unsafe {
av_frame_side_data_free(&mut self.entries, &mut self.count)
}
}
#[cfg(ffmpeg_8_0)]
pub(crate) fn len(&self) -> i32 {
self.count
}
#[cfg(ffmpeg_8_0)]
pub(crate) fn as_mut_ptr(&mut self) -> *mut *mut AVFrameSideData {
self.entries
}
#[cfg(ffmpeg_8_0)]
pub(crate) fn iter(&self) -> impl Iterator<Item = *const AVFrameSideData> + '_ {
(0..self.count)
.map(|i| unsafe { *self.entries.offset(i as isize) as *const AVFrameSideData })
}
}
impl Drop for SideDataList {
fn drop(&mut self) {
self.clear();
}
}
unsafe impl Send for SideDataList {}
unsafe impl Sync for SideDataList {}
#[derive(Clone)]
pub(crate) struct FrameData {
pub(crate) framerate: Option<AVRational>,
pub(crate) bits_per_raw_sample: i32,
pub(crate) input_stream_width: i32,
pub(crate) input_stream_height: i32,
pub(crate) subtitle_header: Option<Arc<[u8]>>,
pub(crate) fg_input_index: usize,
#[cfg_attr(not(ffmpeg_8_0), allow(dead_code))]
pub(crate) side_data: Option<Arc<SideDataList>>,
}
pub(crate) struct PacketBox {
pub(crate) packet: ffmpeg_next::Packet,
pub(crate) packet_data: PacketData,
}
unsafe impl Send for PacketBox {}
#[derive(Clone, Copy)]
pub(crate) struct PacketData {
pub(crate) dts_est: i64,
pub(crate) codec_type: AVMediaType,
pub(crate) output_stream_index: i32,
pub(crate) is_copy: bool,
}
pub(crate) fn out_fmt_ctx_free(out_fmt_ctx: *mut AVFormatContext, is_set_write_callback: bool) {
if out_fmt_ctx.is_null() {
return;
}
unsafe {
let custom_io_teardown_panic = if is_set_write_callback {
let avio_ctx = (*out_fmt_ctx).pb;
(*out_fmt_ctx).pb = null_mut();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
free_output_opaque(avio_ctx)
}))
.err()
} else if (*(*out_fmt_ctx).oformat).flags & AVFMT_NOFILE == 0 {
let mut pb = (*out_fmt_ctx).pb;
if !pb.is_null() {
avio_closep(&mut pb);
}
None
} else {
None
};
avformat_free_context(out_fmt_ctx);
if let Some(payload) = custom_io_teardown_panic {
std::panic::resume_unwind(payload);
}
}
}
pub(crate) unsafe fn input_custom_io_is_poisoned(avio_ctx: *mut AVIOContext) -> bool {
if avio_ctx.is_null() {
return false;
}
let opaque_ptr = (*avio_ctx).opaque as *const InputOpaque;
!opaque_ptr.is_null() && (*opaque_ptr).poisoned
}
pub(crate) unsafe fn output_custom_io_is_poisoned(avio_ctx: *mut AVIOContext) -> bool {
if avio_ctx.is_null() {
return false;
}
let opaque_ptr = (*avio_ctx).opaque as *const OutputOpaque;
!opaque_ptr.is_null() && (*opaque_ptr).poisoned
}
fn drop_custom_io_callback_contained<T>(callback: T) -> bool {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(callback))) {
Ok(()) => false,
Err(payload) => {
crate::core::packet_sink::dispose_panic_payload(payload);
true
}
}
}
unsafe fn dispose_output_opaque_callbacks(opaque_ptr: *mut OutputOpaque) -> bool {
if opaque_ptr.is_null() {
return false;
}
let OutputOpaque {
write,
seek,
poisoned: _,
} = *Box::from_raw(opaque_ptr);
let mut panicked = drop_custom_io_callback_contained(write);
if let Some(seek) = seek {
panicked |= drop_custom_io_callback_contained(seek);
}
panicked
}
unsafe fn dispose_input_opaque_callbacks(opaque_ptr: *mut InputOpaque) -> bool {
if opaque_ptr.is_null() {
return false;
}
let InputOpaque {
read,
seek,
poisoned: _,
} = *Box::from_raw(opaque_ptr);
let mut panicked = drop_custom_io_callback_contained(read);
if let Some(seek) = seek {
panicked |= drop_custom_io_callback_contained(seek);
}
panicked
}
pub(crate) unsafe fn free_output_opaque(mut avio_ctx: *mut AVIOContext) {
if avio_ctx.is_null() {
return;
}
let already_panicking = std::thread::panicking();
let mut callback_state_panicked = false;
let opaque_ptr = (*avio_ctx).opaque as *mut OutputOpaque;
(*avio_ctx).opaque = null_mut();
callback_state_panicked |= dispose_output_opaque_callbacks(opaque_ptr);
if !(*avio_ctx).buffer.is_null() {
av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
}
avio_context_free(&mut avio_ctx);
if callback_state_panicked && !already_panicking {
panic!("custom-IO output callback state panicked during teardown");
}
}
pub(crate) fn in_fmt_ctx_free(mut in_fmt_ctx: *mut AVFormatContext, is_set_read_callback: bool) {
if in_fmt_ctx.is_null() {
return;
}
unsafe {
let avio_ctx = if is_set_read_callback {
(*in_fmt_ctx).pb
} else {
null_mut()
};
avformat_close_input(&mut in_fmt_ctx);
free_input_opaque(avio_ctx);
}
}
pub(crate) unsafe fn free_input_opaque(mut avio_ctx: *mut AVIOContext) {
if avio_ctx.is_null() {
return;
}
let already_panicking = std::thread::panicking();
let mut callback_state_panicked = false;
let opaque_ptr = (*avio_ctx).opaque as *mut InputOpaque;
(*avio_ctx).opaque = null_mut();
callback_state_panicked |= dispose_input_opaque_callbacks(opaque_ptr);
if !(*avio_ctx).buffer.is_null() {
av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
}
avio_context_free(&mut avio_ctx);
if callback_state_panicked && !already_panicking {
panic!("custom-IO input callback state panicked during teardown");
}
}
pub(crate) struct FmtCtxGuard {
ctx: *mut AVFormatContext,
mode: crate::raw::Mode,
}
impl FmtCtxGuard {
pub(crate) fn disarmed() -> Self {
Self {
ctx: null_mut(),
mode: crate::raw::Mode::Input,
}
}
pub(crate) fn arm(&mut self, ctx: *mut AVFormatContext, mode: crate::raw::Mode) {
self.ctx = ctx;
self.mode = mode;
}
pub(crate) fn release(&mut self) -> *mut AVFormatContext {
let ctx = self.ctx;
self.ctx = null_mut();
ctx
}
pub(crate) fn release_into(&mut self) -> crate::raw::FormatContext {
let mode = self.mode;
let ctx = self.release();
assert!(!ctx.is_null(), "release_into on a disarmed FmtCtxGuard");
unsafe { crate::raw::FormatContext::from_mode(ctx, mode) }
}
}
impl Drop for FmtCtxGuard {
fn drop(&mut self) {
if self.ctx.is_null() {
return;
}
match self.mode {
crate::raw::Mode::Input => in_fmt_ctx_free(self.ctx, false),
crate::raw::Mode::InputCustomIo => in_fmt_ctx_free(self.ctx, true),
crate::raw::Mode::Output => out_fmt_ctx_free(self.ctx, false),
crate::raw::Mode::OutputCustomIo => out_fmt_ctx_free(self.ctx, true),
}
}
}
#[allow(dead_code)]
pub(crate) fn type_to_linklabel(media_type: AVMediaType, index: usize) -> Option<String> {
match media_type {
AVMediaType::AVMEDIA_TYPE_UNKNOWN => None,
AVMEDIA_TYPE_VIDEO => Some(format!("{index}:v")),
AVMEDIA_TYPE_AUDIO => Some(format!("{index}:a")),
AVMEDIA_TYPE_DATA => Some(format!("{index}:d")),
AVMEDIA_TYPE_SUBTITLE => Some(format!("{index}:s")),
AVMEDIA_TYPE_ATTACHMENT => Some(format!("{index}:t")),
AVMediaType::AVMEDIA_TYPE_NB => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::scheduler::ffmpeg_scheduler::{STATUS_ABORT, STATUS_END, STATUS_RUN};
unsafe fn test_avio(opaque: *mut c_void, write_flag: libc::c_int) -> *mut AVIOContext {
const BUFFER_SIZE: usize = 128;
let buffer = ffmpeg_sys_next::av_malloc(BUFFER_SIZE) as *mut u8;
assert!(!buffer.is_null(), "test AVIO buffer allocation failed");
let avio_ctx = ffmpeg_sys_next::avio_alloc_context(
buffer,
BUFFER_SIZE as libc::c_int,
write_flag,
opaque,
None,
None,
None,
);
if avio_ctx.is_null() {
ffmpeg_sys_next::av_free(buffer.cast());
panic!("test AVIO context allocation failed");
}
avio_ctx
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> &str {
payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("non-string panic payload")
}
struct CallbackDropBomb(Arc<AtomicUsize>);
impl Drop for CallbackDropBomb {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
panic!("test callback capture destructor");
}
}
struct FreeInputAvioOnDrop(*mut AVIOContext);
impl Drop for FreeInputAvioOnDrop {
fn drop(&mut self) {
unsafe { free_input_opaque(self.0) };
}
}
struct FreeOutputAvioOnDrop(*mut AVIOContext);
impl Drop for FreeOutputAvioOnDrop {
fn drop(&mut self) {
unsafe { free_output_opaque(self.0) };
}
}
fn state_with(status: usize) -> Arc<InterruptState> {
Arc::new(InterruptState::new(Arc::new(AtomicUsize::new(status))))
}
fn expire_grace(state: &InterruptState) {
let past = unsafe { av_gettime_relative() } - OUTPUT_END_GRACE_US - 1;
state.end_grace_start_us.store(past, Ordering::Release);
}
#[test]
fn output_grace_cut_fires_after_the_window() {
let state = state_with(STATUS_END);
expire_grace(&state);
assert!(state.should_interrupt_output());
}
#[test]
fn finalize_guard_holds_the_grace_cut_open() {
let state = state_with(STATUS_END);
expire_grace(&state);
let guard = state.begin_output_finalize();
assert!(
!state.should_interrupt_output(),
"a finalizing output must not be cut by the STATUS_END grace"
);
drop(guard);
assert!(
state.should_interrupt_output(),
"after the last finalize guard drops, the expired grace cuts again"
);
}
#[test]
fn abort_cuts_through_a_finalize_window() {
let state = state_with(STATUS_ABORT);
let _guard = state.begin_output_finalize();
assert!(
state.should_interrupt_output(),
"abort() is the hard cancel: it must cut even a finalizing output"
);
}
#[test]
fn running_scheduler_never_cuts_output() {
let state = state_with(STATUS_RUN);
assert!(!state.should_interrupt_output());
}
#[test]
fn overlapping_finalize_windows_hold_until_the_last_drops() {
let state = state_with(STATUS_END);
expire_grace(&state);
let g1 = state.begin_output_finalize();
let g2 = state.begin_output_finalize();
drop(g1);
assert!(
!state.should_interrupt_output(),
"one of two overlapping finalize windows dropping must keep the hold"
);
drop(g2);
assert!(state.should_interrupt_output());
}
#[test]
fn custom_io_poison_helpers_read_live_opaque_state() {
unsafe {
assert!(!input_custom_io_is_poisoned(null_mut()));
assert!(!output_custom_io_is_poisoned(null_mut()));
let input_opaque = Box::into_raw(Box::new(InputOpaque {
read: Box::new(|buf| buf.len() as i32),
seek: None,
poisoned: false,
}));
let input_avio = test_avio(input_opaque.cast(), 0);
assert!(!input_custom_io_is_poisoned(input_avio));
(*input_opaque).poisoned = true;
assert!(input_custom_io_is_poisoned(input_avio));
free_input_opaque(input_avio);
let output_opaque = Box::into_raw(Box::new(OutputOpaque {
write: Box::new(|buf| buf.len() as i32),
seek: None,
poisoned: false,
}));
let output_avio = test_avio(output_opaque.cast(), 1);
assert!(!output_custom_io_is_poisoned(output_avio));
(*output_opaque).poisoned = true;
assert!(output_custom_io_is_poisoned(output_avio));
free_output_opaque(output_avio);
}
}
#[test]
fn input_opaque_teardown_drops_each_callback_before_stable_repanic() {
let drops = Arc::new(AtomicUsize::new(0));
let read_bomb = CallbackDropBomb(Arc::clone(&drops));
let seek_bomb = CallbackDropBomb(Arc::clone(&drops));
let opaque = Box::into_raw(Box::new(InputOpaque {
read: Box::new(move |_buf| {
let _hold = &read_bomb;
0
}),
seek: Some(Box::new(move |_offset, _whence| {
let _hold = &seek_bomb;
0
})),
poisoned: false,
}));
let avio_ctx = unsafe { test_avio(opaque.cast(), 0) };
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
free_input_opaque(avio_ctx)
}))
.expect_err("normal teardown must publish a stable panic after reclamation");
assert_eq!(
panic_message(payload.as_ref()),
"custom-IO input callback state panicked during teardown"
);
assert_eq!(
drops.load(Ordering::SeqCst),
2,
"read and seek callback boxes must both be disposed"
);
}
#[test]
fn output_opaque_teardown_drops_each_callback_before_stable_repanic() {
let drops = Arc::new(AtomicUsize::new(0));
let write_bomb = CallbackDropBomb(Arc::clone(&drops));
let seek_bomb = CallbackDropBomb(Arc::clone(&drops));
let opaque = Box::into_raw(Box::new(OutputOpaque {
write: Box::new(move |_buf| {
let _hold = &write_bomb;
0
}),
seek: Some(Box::new(move |_offset, _whence| {
let _hold = &seek_bomb;
0
})),
poisoned: false,
}));
let avio_ctx = unsafe { test_avio(opaque.cast(), 1) };
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
free_output_opaque(avio_ctx)
}))
.expect_err("normal teardown must publish a stable panic after reclamation");
assert_eq!(
panic_message(payload.as_ref()),
"custom-IO output callback state panicked during teardown"
);
assert_eq!(
drops.load(Ordering::SeqCst),
2,
"write and seek callback boxes must both be disposed"
);
}
#[test]
fn output_format_teardown_preserves_the_deferred_stable_panic() {
let drops = Arc::new(AtomicUsize::new(0));
let write_bomb = CallbackDropBomb(Arc::clone(&drops));
let opaque = Box::into_raw(Box::new(OutputOpaque {
write: Box::new(move |_buf| {
let _hold = &write_bomb;
0
}),
seek: None,
poisoned: false,
}));
let avio_ctx = unsafe { test_avio(opaque.cast(), 1) };
let format_ctx = unsafe { ffmpeg_sys_next::avformat_alloc_context() };
assert!(
!format_ctx.is_null(),
"test format context allocation failed"
);
unsafe {
(*format_ctx).pb = avio_ctx;
}
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
out_fmt_ctx_free(format_ctx, true)
}))
.expect_err("the owner-level teardown must preserve the stable panic");
assert_eq!(
panic_message(payload.as_ref()),
"custom-IO output callback state panicked during teardown"
);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[test]
fn custom_io_teardown_preserves_an_existing_unwind() {
let input_drops = Arc::new(AtomicUsize::new(0));
let read_bomb = CallbackDropBomb(Arc::clone(&input_drops));
let seek_bomb = CallbackDropBomb(Arc::clone(&input_drops));
let input_opaque = Box::into_raw(Box::new(InputOpaque {
read: Box::new(move |_buf| {
let _hold = &read_bomb;
0
}),
seek: Some(Box::new(move |_offset, _whence| {
let _hold = &seek_bomb;
0
})),
poisoned: false,
}));
let input_avio = unsafe { test_avio(input_opaque.cast(), 0) };
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _free_on_unwind = FreeInputAvioOnDrop(input_avio);
panic!("primary input unwind");
}))
.expect_err("the primary input panic must propagate");
assert_eq!(panic_message(payload.as_ref()), "primary input unwind");
assert_eq!(input_drops.load(Ordering::SeqCst), 2);
let output_drops = Arc::new(AtomicUsize::new(0));
let write_bomb = CallbackDropBomb(Arc::clone(&output_drops));
let seek_bomb = CallbackDropBomb(Arc::clone(&output_drops));
let output_opaque = Box::into_raw(Box::new(OutputOpaque {
write: Box::new(move |_buf| {
let _hold = &write_bomb;
0
}),
seek: Some(Box::new(move |_offset, _whence| {
let _hold = &seek_bomb;
0
})),
poisoned: false,
}));
let output_avio = unsafe { test_avio(output_opaque.cast(), 1) };
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _free_on_unwind = FreeOutputAvioOnDrop(output_avio);
panic!("primary output unwind");
}))
.expect_err("the primary output panic must propagate");
assert_eq!(panic_message(payload.as_ref()), "primary output unwind");
assert_eq!(output_drops.load(Ordering::SeqCst), 2);
}
}