use core::fmt;
use cstream::{
AsCStream, AsRawCStream, BorrowedCStream, FromRawCStream, IntoRawCStream, OwnedCStream,
};
use fopencookie_sys as sys;
use libc::{c_char, c_int, c_long, c_void, off_t, size_t};
use std::{
ffi::CStr,
fmt::Write,
io::{self, SeekFrom},
mem,
num::TryFromIntError,
ptr::{self, NonNull},
slice,
str::FromStr,
};
type ReadFnPtr<T = ()> = fn(&mut T, &mut [u8]) -> io::Result<usize>;
type WriteFnPtr<T = ()> = fn(&mut T, &[u8]) -> io::Result<usize>;
type FlushFnPtr<T = ()> = fn(&mut T) -> io::Result<()>;
type SeekFnPtr<T = ()> = fn(&mut T, SeekFrom) -> io::Result<u64>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct VTable<T> {
read: Action<ReadFnPtr<T>>,
write: Action<WriteFnPtr<T>>,
flush: Option<FlushFnPtr<T>>,
seek: Option<SeekFnPtr<T>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
enum Action<T> {
Do(T),
#[default]
Ignore,
Unsupported,
}
#[must_use = "Call `build` to actually create the IoStream"]
pub struct Builder<T> {
vtable: VTable<T>,
}
impl<T> Builder<T> {
pub const fn new() -> Self {
Self {
vtable: VTable {
read: Action::Ignore,
write: Action::Ignore,
flush: None,
seek: None,
},
}
}
pub const fn read(mut self) -> Self
where
T: io::Read,
{
self.vtable.read = Action::Do(T::read);
self
}
pub const fn write(mut self) -> Self
where
T: io::Write,
{
self.vtable.write = Action::Do(T::write);
self
}
pub const fn seek(mut self) -> Self
where
T: io::Seek,
{
self.vtable.seek = Some(T::seek);
self
}
pub const fn strict(mut self) -> Self {
if matches!(self.vtable.read, Action::Ignore) {
self.vtable.read = Action::Unsupported
}
if matches!(self.vtable.write, Action::Ignore) {
self.vtable.write = Action::Unsupported
}
self
}
pub fn build(self, inner: T) -> IoCStream<T> {
self.build_with_mode(Mode::default(), inner)
}
pub fn build_with_mode(self, mode: Mode, inner: T) -> IoCStream<T> {
let Self { vtable } = self;
let cookie = Box::new(Cookie {
vtable,
inner,
drop_on_close: false,
});
let file = unsafe {
sys::fopencookie(
&*cookie as *const Cookie<T> as *const c_void as *mut c_void,
mode.as_cstr().as_ptr(),
sys::cookie_io_functions_t {
read: Some(Cookie::<T>::read),
write: Some(Cookie::<T>::write),
seek: Some(Cookie::<T>::seek),
close: Some(Cookie::<T>::close),
},
)
};
match NonNull::new(file.cast::<libc::FILE>()) {
Some(raw) => {
unsafe { libc::setbuf(raw.as_ptr(), ptr::null_mut()) }
IoCStream {
stream: unsafe { OwnedCStream::from_raw_c_stream(raw) },
cookie,
}
}
None => panic!(
"call to `fopencookie` failed, despite having a valid `mode`,\
perhaps an allocation failed?\
last os error: {}",
io::Error::last_os_error()
),
}
}
}
#[derive(Debug)]
pub struct IoCStream<T> {
stream: OwnedCStream,
cookie: Box<Cookie<T>>,
}
impl<T> IoCStream<T> {
pub fn reader(inner: T) -> Self
where
T: io::Read,
{
Builder::new().read().build_with_mode(Mode::Read, inner)
}
pub fn writer(inner: T) -> Self
where
T: io::Write,
{
Builder::new().write().build_with_mode(Mode::Write, inner)
}
pub fn get_ref(&self) -> &T {
&self.cookie.inner
}
pub fn get_mut(&mut self) -> &mut T {
&mut self.cookie.inner
}
pub fn into_inner(self) -> T {
self.cookie.inner
}
pub fn as_ptr(&self) -> *mut libc::FILE {
self.as_raw_c_stream().as_ptr()
}
pub unsafe fn into_owned_c_stream_unchecked(self) -> OwnedCStream {
let Self { stream, mut cookie } = self;
cookie.drop_on_close = true;
mem::forget(cookie);
stream
}
pub fn into_owned_c_stream(self) -> OwnedCStream
where
T: 'static,
{
unsafe { self.into_owned_c_stream_unchecked() }
}
}
impl<T> From<IoCStream<T>> for OwnedCStream
where
T: 'static,
{
fn from(value: IoCStream<T>) -> Self {
value.into_owned_c_stream()
}
}
impl<T> AsCStream for IoCStream<T> {
fn as_c_stream(&self) -> BorrowedCStream<'_> {
self.stream.as_c_stream()
}
}
impl<T> AsRawCStream for IoCStream<T> {
fn as_raw_c_stream(&self) -> cstream::RawCStream {
self.stream.as_raw_c_stream()
}
}
impl<T> IntoRawCStream for IoCStream<T>
where
T: 'static,
{
fn into_raw_c_stream(self) -> cstream::RawCStream {
self.into_owned_c_stream().into_raw_c_stream()
}
}
impl<T: io::Write> io::Write for IoCStream<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.cookie.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.cookie.inner.flush()
}
}
impl<T: io::Read> io::Read for IoCStream<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.cookie.inner.read(buf)
}
}
impl<T: io::Seek> io::Seek for IoCStream<T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.cookie.inner.seek(pos)
}
}
unsafe impl<T: Send> Send for IoCStream<T> {}
unsafe impl<T: Sync> Sync for IoCStream<T> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Cookie<T> {
vtable: VTable<T>,
drop_on_close: bool,
inner: T,
}
#[cfg(not(doctest))]
impl<T> Cookie<T> {
unsafe extern "C" fn read(cookie: *mut c_void, buf: *mut c_char, len: size_t) -> c_long {
let cookie = &mut *cookie.cast::<Cookie<T>>();
match cookie.vtable.read {
Action::Do(f) => f(
&mut cookie.inner,
slice::from_raw_parts_mut(buf.cast::<u8>(), len),
)
.map_err(setting_errno)
.and_then(|n| n.try_into().map_err(setting_errno))
.unwrap_or(-1),
Action::Unsupported => {
set_errno(libc::ENOTSUP);
-1
}
Action::Ignore => 0,
}
}
unsafe extern "C" fn write(cookie: *mut c_void, buf: *const c_char, len: size_t) -> c_long {
let cookie = &mut *cookie.cast::<Cookie<T>>();
match cookie.vtable.write {
Action::Do(f) => f(
&mut cookie.inner,
slice::from_raw_parts(buf.cast::<u8>(), len),
)
.map_err(setting_errno)
.and_then(|n| n.try_into().map_err(setting_errno))
.unwrap_or(0),
Action::Unsupported => {
set_errno(libc::ENOTSUP);
0
}
Action::Ignore => len.try_into().unwrap_or(c_long::MAX),
}
}
#[allow(unused)] unsafe extern "C" fn close(cookie: *mut c_void) -> c_int {
let cookie = &mut *cookie.cast::<Cookie<T>>();
let ret = match cookie.vtable.flush {
Some(f) => match f(&mut cookie.inner).map_err(setting_errno) {
Ok(()) => 0,
Err(()) => libc::EOF,
},
None => 0,
};
match cookie.drop_on_close {
true => drop(Box::from_raw(cookie)),
false => {}
}
ret
}
unsafe extern "C" fn seek(cookie: *mut c_void, offset: *mut off_t, whence: c_int) -> c_int {
let cookie = &mut *cookie.cast::<Cookie<T>>();
let requested_offset = *offset;
let pos = match whence {
libc::SEEK_SET => match requested_offset.try_into().map_err(setting_errno) {
Ok(it) => SeekFrom::Start(it),
Err(()) => return -1,
},
libc::SEEK_CUR => SeekFrom::Current(requested_offset),
libc::SEEK_END => SeekFrom::End(requested_offset),
_ => {
set_errno(libc::EINVAL);
return -1;
}
};
match cookie.vtable.seek {
Some(f) => match f(&mut cookie.inner, pos).map_err(setting_errno) {
Ok(n) => match n.try_into().map_err(setting_errno) {
Ok(new_offset) => {
*offset = new_offset;
0
}
Err(()) => -1,
},
Err(()) => -1,
},
None => {
set_errno(libc::ENOTSUP);
-1
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Mode {
Read,
#[default]
ReadPlus,
Write,
WritePlus,
Append,
AppendPlus,
}
impl Mode {
const ALL: &'static [Self] = &[
Mode::Read,
Mode::ReadPlus,
Mode::Write,
Mode::WritePlus,
Mode::Append,
Mode::AppendPlus,
];
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Mode {
type Err = UnrecognisedMode;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str(s)
}
}
impl Mode {
pub const fn as_cstr(&self) -> &'static CStr {
match self {
Mode::Read => c"r",
Mode::ReadPlus => c"r+",
Mode::Write => c"w",
Mode::WritePlus => c"w+",
Mode::Append => c"a",
Mode::AppendPlus => c"a+",
}
}
pub const fn as_str(&self) -> &'static str {
match self.as_cstr().to_str() {
Ok(it) => it,
Err(_) => unreachable!(),
}
}
pub const fn from_str(s: &str) -> Result<Self, UnrecognisedMode> {
let mut ix = Self::ALL.len();
while let Some(nix) = ix.checked_sub(1) {
ix = nix;
let candidate = Self::ALL[ix];
if slice_eq(s.as_bytes(), candidate.as_str().as_bytes()) {
return Ok(candidate);
}
}
Err(UnrecognisedMode)
}
}
const fn slice_eq(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut ix = left.len();
while let Some(nix) = ix.checked_sub(1) {
ix = nix;
if left[ix] != right[ix] {
return false;
}
}
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UnrecognisedMode;
impl fmt::Display for UnrecognisedMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unrecognised mode (expected one of")?;
for it in Mode::ALL {
f.write_char(' ')?;
f.write_str(it.as_str())?;
}
f.write_char(')')
}
}
impl std::error::Error for UnrecognisedMode {}
trait SetErrno {
fn errno(self) -> Option<c_int>;
}
impl SetErrno for io::Error {
fn errno(self) -> Option<c_int> {
self.raw_os_error()
}
}
impl SetErrno for TryFromIntError {
fn errno(self) -> Option<c_int> {
Some(libc::EOVERFLOW)
}
}
fn setting_errno(e: impl SetErrno) {
if let Some(errno) = e.errno() {
set_errno(errno)
}
}
fn set_errno(to: c_int) {
let dst = unsafe { &mut *libc::__errno_location() };
*dst = to;
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_TEXT: &str = "hello, world!";
#[test]
fn borrowed() {
let mut v = vec![];
let stream = Builder::new().write().build(&mut v);
assert_eq!(
cstream::write(TEST_TEXT.as_bytes(), stream),
TEST_TEXT.len()
);
assert_eq!(v, TEST_TEXT.as_bytes());
}
#[test]
fn trait_object() {
let mut v = vec![];
let stream = Builder::<Box<dyn io::Write>>::new()
.write()
.build(Box::new(&mut v));
assert_eq!(
cstream::write(TEST_TEXT.as_bytes(), stream),
TEST_TEXT.len()
);
assert_eq!(v, TEST_TEXT.as_bytes());
}
#[test]
fn streams_have_no_fileno() {
let stream = IoCStream::writer(io::empty());
assert_eq!(cstream::fileno(&stream), None);
}
#[test]
fn seek_with_no_seek() {
unsafe {
let handle = sys::fopencookie(
ptr::null_mut(),
Mode::Read.as_cstr().as_ptr(),
sys::cookie_io_functions_t {
read: None,
write: None,
seek: None,
close: None,
},
)
.cast::<libc::FILE>();
assert!(!handle.is_null());
let ret = libc::fseek(handle, 0, libc::SEEK_SET);
assert_eq!(ret, -1);
};
}
#[test]
fn fopencookie_never_fails() {
for mode in Mode::ALL {
for read in [None, Some(noop_err::read as _)] {
for write in [None, Some(noop_err::write as _)] {
for seek in [None, Some(noop_err::seek as _)] {
for close in [None, Some(noop_err::close as _)] {
let ret = unsafe {
sys::fopencookie(
ptr::null_mut(),
mode.as_cstr().as_ptr(),
sys::cookie_io_functions_t {
read,
write,
seek,
close,
},
)
};
assert_ne!(ret, ptr::null_mut())
}
}
}
}
}
}
mod noop_err {
use libc::{c_char, c_int, c_long, c_void, off_t, size_t};
pub unsafe extern "C" fn read(
_cookie: *mut c_void,
_buf: *mut c_char,
_len: size_t,
) -> c_long {
-1
}
pub unsafe extern "C" fn write(
_cookie: *mut c_void,
_buf: *const c_char,
_len: size_t,
) -> c_long {
0
}
pub unsafe extern "C" fn close(_cookie: *mut c_void) -> c_int {
libc::EOF
}
pub unsafe extern "C" fn seek(
_cookie: *mut c_void,
_offset: *mut off_t,
_whence: c_int,
) -> c_int {
-1
}
}
}