use std::ffi::c_void;
use std::io::{ErrorKind, SeekFrom};
use std::os::raw::{c_int, c_uchar};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
use crate::io_traits::{IoSink, IoSource};
use crate::{AVIOContext, AvError, av_freep as ffi_av_freep, av_malloc as ffi_av_malloc};
unsafe extern "C" {
fn avio_alloc_context(
buffer: *mut c_uchar,
buffer_size: c_int,
write_flag: c_int,
opaque: *mut c_void,
read_packet: Option<unsafe extern "C" fn(*mut c_void, *mut u8, c_int) -> c_int>,
write_packet: Option<unsafe extern "C" fn(*mut c_void, *const u8, c_int) -> c_int>,
seek: Option<unsafe extern "C" fn(*mut c_void, i64, c_int) -> i64>,
) -> *mut AVIOContext;
fn avio_context_free(s: *mut *mut AVIOContext);
}
#[allow(dead_code)]
fn assert_callback_abi(ctx: &AVIOContext) {
let _: Option<unsafe extern "C" fn(*mut c_void, *mut u8, c_int) -> c_int> = ctx.read_packet;
let _: Option<unsafe extern "C" fn(*mut c_void, *const u8, c_int) -> c_int> = ctx.write_packet;
let _: Option<unsafe extern "C" fn(*mut c_void, i64, c_int) -> i64> = ctx.seek;
}
const BUFFER_SIZE: usize = 4096;
const AVSEEK_SIZE: c_int = crate::AVSEEK_SIZE as c_int;
const AVSEEK_FORCE: c_int = 0x2_0000;
enum IoState {
Read(Box<dyn IoSource>),
Write(Box<dyn IoSink>),
}
pub(crate) struct IoContext {
ptr: NonNull<AVIOContext>,
state: NonNull<IoState>,
}
impl IoContext {
pub(crate) fn reader(source: impl IoSource + 'static) -> Result<Self, AvError> {
Self::alloc(IoState::Read(Box::new(source)), 0)
}
pub(crate) fn writer(sink: impl IoSink + 'static) -> Result<Self, AvError> {
Self::alloc(IoState::Write(Box::new(sink)), 1)
}
pub(crate) fn as_ptr(&self) -> *mut AVIOContext {
self.ptr.as_ptr()
}
fn alloc(state: IoState, write_flag: c_int) -> Result<Self, AvError> {
crate::ensure_initialized();
let buffer = unsafe { alloc_buffer() };
let Some(buffer) = NonNull::new(buffer) else {
return Err(AvError::new(crate::error_codes::ENOMEM));
};
let state = NonNull::from(Box::leak(Box::new(state)));
let (read, write) = match write_flag {
0 => (
Some(read_packet as unsafe extern "C" fn(*mut c_void, *mut u8, c_int) -> c_int),
None,
),
_ => (
None,
Some(write_packet as unsafe extern "C" fn(*mut c_void, *const u8, c_int) -> c_int),
),
};
let ptr = unsafe {
avio_alloc_context(
buffer.as_ptr(),
c_int::try_from(BUFFER_SIZE).unwrap_or(c_int::MAX),
write_flag,
state.as_ptr().cast::<c_void>(),
read,
write,
Some(seek),
)
};
match NonNull::new(ptr) {
Some(ptr) => Ok(Self { ptr, state }),
None => {
unsafe {
let mut raw = buffer.as_ptr().cast::<c_void>();
ffi_av_freep(std::ptr::addr_of_mut!(raw).cast::<c_void>());
drop(Box::from_raw(state.as_ptr()));
}
Err(AvError::new(crate::error_codes::ENOMEM))
}
}
}
}
unsafe fn alloc_buffer() -> *mut c_uchar {
unsafe { ffi_av_malloc(BUFFER_SIZE).cast::<c_uchar>() }
}
impl std::fmt::Debug for IoContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IoContext").finish_non_exhaustive()
}
}
impl Drop for IoContext {
fn drop(&mut self) {
unsafe {
let ctx = self.ptr.as_ptr();
ffi_av_freep(std::ptr::addr_of_mut!((*ctx).buffer).cast::<c_void>());
let mut raw = ctx;
avio_context_free(std::ptr::addr_of_mut!(raw));
drop(Box::from_raw(self.state.as_ptr()));
}
}
}
unsafe impl Send for IoContext {}
unsafe fn with_state<T>(opaque: *mut c_void, err: T, f: impl FnOnce(&mut IoState) -> T) -> T {
if opaque.is_null() {
return err;
}
let state = unsafe { &mut *opaque.cast::<IoState>() };
catch_unwind(AssertUnwindSafe(|| f(state))).unwrap_or(err)
}
unsafe extern "C" fn read_packet(opaque: *mut c_void, buf: *mut u8, buf_size: c_int) -> c_int {
let eio = crate::error_codes::EIO;
if buf.is_null() || buf_size <= 0 {
return crate::error_codes::EINVAL;
}
let len = buf_size as usize;
unsafe { std::ptr::write_bytes(buf, 0, len) };
let out = unsafe { std::slice::from_raw_parts_mut(buf, len) };
unsafe {
with_state(opaque, eio, |state| {
let IoState::Read(source) = state else {
return eio;
};
loop {
match source.read(out) {
Ok(0) => return crate::error_codes::EOF,
Ok(n) if n > len => return eio,
Ok(n) => return c_int::try_from(n).unwrap_or(eio),
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(_) => return eio,
}
}
})
}
}
unsafe extern "C" fn write_packet(opaque: *mut c_void, buf: *const u8, buf_size: c_int) -> c_int {
let eio = crate::error_codes::EIO;
if buf.is_null() || buf_size < 0 {
return crate::error_codes::EINVAL;
}
let data = unsafe { std::slice::from_raw_parts(buf, buf_size as usize) };
unsafe {
with_state(opaque, eio, |state| {
let IoState::Write(sink) = state else {
return eio;
};
match sink.write_all(data) {
Ok(()) => buf_size,
Err(_) => eio,
}
})
}
}
unsafe extern "C" fn seek(opaque: *mut c_void, offset: i64, whence: c_int) -> i64 {
let eio = i64::from(crate::error_codes::EIO);
unsafe {
with_state(opaque, eio, |state| {
let whence = whence & !AVSEEK_FORCE;
if whence == AVSEEK_SIZE {
return stream_len(state).map_or(eio, |len| len);
}
let pos = match whence {
0 => match u64::try_from(offset) {
Ok(pos) => SeekFrom::Start(pos),
Err(_) => return i64::from(crate::error_codes::EINVAL),
},
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => return i64::from(crate::error_codes::EINVAL),
};
let seeked = match state {
IoState::Read(source) => source.seek(pos),
IoState::Write(sink) => sink.seek(pos),
};
seeked.map_or(eio, |p| i64::try_from(p).unwrap_or(eio))
})
}
}
fn stream_len(state: &mut IoState) -> Option<i64> {
fn measure(
current: std::io::Result<u64>,
mut seek: impl FnMut(SeekFrom) -> std::io::Result<u64>,
) -> Option<i64> {
let here = current.ok()?;
let end = seek(SeekFrom::End(0)).ok()?;
seek(SeekFrom::Start(here)).ok()?;
i64::try_from(end).ok()
}
match state {
IoState::Read(source) => {
let here = source.stream_position();
measure(here, |p| source.seek(p))
}
IoState::Write(sink) => {
let here = sink.stream_position();
measure(here, |p| sink.seek(p))
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{AVSEEK_FORCE, AVSEEK_SIZE, IoContext, read_packet, seek, write_packet};
struct CountingSource {
inner: Cursor<Vec<u8>>,
drops: Arc<AtomicUsize>,
}
impl std::io::Read for CountingSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl std::io::Seek for CountingSource {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
impl Drop for CountingSource {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
}
}
struct PanickingSource;
impl std::io::Read for PanickingSource {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
panic!("the source panicked");
}
}
impl std::io::Seek for PanickingSource {
fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
Ok(0)
}
}
#[test]
fn dropping_the_context_should_drop_the_source_exactly_once() {
let drops = Arc::new(AtomicUsize::new(0));
let source = CountingSource {
inner: Cursor::new(vec![1u8, 2, 3, 4]),
drops: Arc::clone(&drops),
};
let ctx = IoContext::reader(source).expect("allocation should succeed");
assert_eq!(
drops.load(Ordering::SeqCst),
0,
"still owned by the context"
);
drop(ctx);
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"the source must be dropped exactly once with the context"
);
}
#[test]
fn dropping_a_writer_context_should_drop_the_sink_exactly_once() {
let drops = Arc::new(AtomicUsize::new(0));
struct CountingSink(Arc<AtomicUsize>, Cursor<Vec<u8>>);
impl std::io::Write for CountingSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.1.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl std::io::Seek for CountingSink {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.1.seek(pos)
}
}
impl Drop for CountingSink {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let ctx = IoContext::writer(CountingSink(Arc::clone(&drops), Cursor::new(Vec::new())))
.expect("allocation should succeed");
drop(ctx);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[test]
fn read_packet_should_report_eof_rather_than_zero() {
let ctx = IoContext::reader(Cursor::new(vec![7u8, 8])).expect("allocation");
let mut buf = [0u8; 8];
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let first = unsafe { read_packet(opaque, buf.as_mut_ptr(), 8) };
assert_eq!(first, 2, "the whole source is two bytes");
assert_eq!(&buf[..2], &[7, 8]);
let second = unsafe { read_packet(opaque, buf.as_mut_ptr(), 8) };
assert_eq!(
second,
crate::error_codes::EOF,
"a drained source must report AVERROR_EOF, not 0"
);
}
struct OverreportingSource;
impl std::io::Read for OverreportingSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
Ok(buf.len() + 4096)
}
}
impl std::io::Seek for OverreportingSource {
fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
Ok(0)
}
}
struct InterruptOnceSource {
interrupted: bool,
inner: Cursor<Vec<u8>>,
}
impl std::io::Read for InterruptOnceSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if !self.interrupted {
self.interrupted = true;
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted));
}
self.inner.read(buf)
}
}
impl std::io::Seek for InterruptOnceSource {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
#[test]
fn read_packet_should_reject_a_source_that_reports_more_than_it_was_given() {
let ctx = IoContext::reader(OverreportingSource).expect("allocation");
let mut buf = [0u8; 8];
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let ret = unsafe { read_packet(opaque, buf.as_mut_ptr(), 8) };
assert_eq!(
ret,
crate::error_codes::EIO,
"an over-reported length must be refused, not passed to FFmpeg"
);
}
#[test]
fn read_packet_should_retry_an_interrupted_read_rather_than_report_zero() {
let source = InterruptOnceSource {
interrupted: false,
inner: Cursor::new(vec![5u8, 6, 7]),
};
let ctx = IoContext::reader(source).expect("allocation");
let mut buf = [0u8; 8];
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let ret = unsafe { read_packet(opaque, buf.as_mut_ptr(), 8) };
assert_eq!(ret, 3, "the interrupt must be retried, not reported");
assert_eq!(&buf[..3], &[5, 6, 7]);
}
#[test]
fn seek_should_reject_a_negative_absolute_position() {
let ctx = IoContext::reader(Cursor::new(vec![0u8; 16])).expect("allocation");
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let ret = unsafe { seek(opaque, -8, 0) };
assert_eq!(ret, i64::from(crate::error_codes::EINVAL));
}
#[test]
fn a_panicking_source_should_return_an_error_instead_of_unwinding() {
let ctx = IoContext::reader(PanickingSource).expect("allocation");
let mut buf = [0u8; 4];
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let ret = unsafe { read_packet(opaque, buf.as_mut_ptr(), 4) };
assert_eq!(
ret,
crate::error_codes::EIO,
"a panicking source must surface as EIO"
);
}
#[test]
fn seek_should_answer_avseek_size_without_moving_the_position() {
let ctx = IoContext::reader(Cursor::new(vec![0u8; 40])).expect("allocation");
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let moved = unsafe { seek(opaque, 10, 0) };
assert_eq!(moved, 10);
let size = unsafe { seek(opaque, 0, AVSEEK_SIZE) };
assert_eq!(size, 40, "AVSEEK_SIZE must report the stream length");
let still_there = unsafe { seek(opaque, 0, 1) };
assert_eq!(
still_there, 10,
"answering AVSEEK_SIZE must leave the position untouched"
);
}
#[test]
fn seek_should_mask_avseek_force_off_the_whence() {
let ctx = IoContext::reader(Cursor::new(vec![0u8; 16])).expect("allocation");
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let forced = unsafe { seek(opaque, 4, AVSEEK_FORCE) };
assert_eq!(forced, 4, "SEEK_SET | AVSEEK_FORCE must still seek");
}
#[test]
fn write_packet_should_forward_bytes_to_the_sink() {
let ctx = IoContext::writer(Cursor::new(Vec::new())).expect("allocation");
let data = [1u8, 2, 3];
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let written = unsafe { write_packet(opaque, data.as_ptr(), 3) };
assert_eq!(written, 3);
}
#[test]
fn a_reader_context_should_reject_a_write() {
let ctx = IoContext::reader(Cursor::new(vec![0u8; 4])).expect("allocation");
let data = [1u8];
let opaque = unsafe { (*ctx.as_ptr()).opaque };
let ret = unsafe { write_packet(opaque, data.as_ptr(), 1) };
assert_eq!(ret, crate::error_codes::EIO);
}
}