cstream/
lib.rs

1//! Utilities for [`libc::FILE`] streams 'c streams'
2//!
3//! # Features
4//! - Owned and borrowed c streams.
5//!   Mirroring [`std::os::fd`](https://doc.rust-lang.org/std/os/fd/index.html)'s API.
6//! - Rusty wrappers around [`libc`]'s stream-oriented functions.
7//!   [`libc::fgets`] becomes [`gets`], etc.
8//! - An [`Io`] wrapper, which implements [`io`](std::io) traits.
9
10#![cfg_attr(not(feature = "std"), no_std)]
11#![cfg_attr(do_doc_cfg, feature(doc_cfg))]
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16use core::{
17    ffi::CStr,
18    fmt,
19    marker::PhantomData,
20    mem,
21    ptr::{self, NonNull},
22};
23use libc::{c_char, c_int, c_void};
24
25#[cfg(feature = "std")]
26mod io;
27#[cfg_attr(do_doc_cfg, doc(cfg(feature = "std")))]
28#[cfg(feature = "std")]
29pub use io::Io;
30
31/// A [`libc::FILE`] stream.
32pub type RawCStream = NonNull<libc::FILE>;
33
34/// An owned [`RawCStream`].
35/// This [`libc::fclose`]s the stream on drop.
36/// It is guaranteed that nobody else will close the stream.
37///
38/// This uses `#[repr(transparent)]` and has the representation of a host stream,
39/// so it can be used in FFI in places where a stream is passed as a consumed argument
40/// or returned as an owned value, and it never has the value `NULL`.
41#[derive(Debug)]
42#[repr(transparent)]
43pub struct OwnedCStream {
44    raw: RawCStream,
45}
46
47impl Drop for OwnedCStream {
48    fn drop(&mut self) {
49        unsafe {
50            // Errors are ignored
51            let _ = libc::fclose(self.raw.as_ptr());
52        }
53    }
54}
55
56impl AsCStream for OwnedCStream {
57    fn as_c_stream(&self) -> BorrowedCStream<'_> {
58        unsafe { BorrowedCStream::borrow_raw(self.raw) }
59    }
60}
61
62impl AsRawCStream for OwnedCStream {
63    fn as_raw_c_stream(&self) -> RawCStream {
64        self.raw
65    }
66}
67
68impl FromRawCStream for OwnedCStream {
69    unsafe fn from_raw_c_stream(raw: RawCStream) -> Self {
70        Self { raw }
71    }
72}
73
74impl IntoRawCStream for OwnedCStream {
75    fn into_raw_c_stream(self) -> RawCStream {
76        let raw = self.raw;
77        mem::forget(self);
78        raw
79    }
80}
81
82/// An [`OwnedCStream`] with a custom buffer.
83#[cfg(feature = "alloc")]
84#[cfg_attr(do_doc_cfg, doc(cfg(feature = "alloc")))]
85#[derive(Debug)]
86pub struct BufferedCStream {
87    stream: OwnedCStream,
88    // this must be last so that it's dropped after flushing
89    #[allow(unused)]
90    buffer: alloc::boxed::Box<[u8]>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub enum BufferMode {
95    Block,
96    Line,
97}
98
99#[cfg(feature = "alloc")]
100impl BufferedCStream {
101    pub fn new(stream: OwnedCStream, size: usize, mode: BufferMode) -> Option<Self> {
102        let mut buffer = alloc::vec![0; size].into_boxed_slice();
103        match unsafe {
104            libc::setvbuf(
105                ptr(&stream),
106                buffer.as_mut_ptr().cast::<c_char>(),
107                match mode {
108                    BufferMode::Block => libc::_IOFBF,
109                    BufferMode::Line => libc::_IOLBF,
110                },
111                buffer.len(),
112            )
113        } {
114            0 => Some(Self { stream, buffer }),
115            _ => None,
116        }
117    }
118}
119
120#[cfg(feature = "alloc")]
121#[cfg_attr(do_doc_cfg, doc(cfg(feature = "alloc")))]
122impl AsCStream for BufferedCStream {
123    fn as_c_stream(&self) -> BorrowedCStream<'_> {
124        self.stream.as_c_stream()
125    }
126}
127
128#[cfg(feature = "alloc")]
129#[cfg_attr(do_doc_cfg, doc(cfg(feature = "alloc")))]
130impl AsRawCStream for BufferedCStream {
131    fn as_raw_c_stream(&self) -> RawCStream {
132        self.stream.as_raw_c_stream()
133    }
134}
135
136/// A borrowed [`RawCStream`].
137///
138/// This has a lifetime parameter to tie it to the lifetime of something that owns the stream.
139/// For the duration of that lifetime, it is guaranteed that nobody will close the stream.
140///
141/// This uses `#[repr(transparent)]` and has the representation of a host stream,
142/// so it can be used in FFI in places where a file descriptor is passed as an argument,
143/// it is not captured or consumed, and it never has the value `NULL`.
144///
145/// This type’s [`Clone`] implementation returns another [`BorrowedCStream`] rather than an [`OwnedCStream`].
146/// It just makes a trivial copy of the raw file descriptor,
147/// which is then borrowed under the same lifetime.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
149#[repr(transparent)]
150pub struct BorrowedCStream<'a> {
151    raw: RawCStream,
152    lifetime: PhantomData<&'a OwnedCStream>,
153}
154
155impl<'a> BorrowedCStream<'a> {
156    /// Return a [`BorrowedCStream`] holding the given stream.
157    ///
158    /// # Safety
159    /// The resource pointed to by `raw` must remain open for the duration of the returned [`BorrowedCStream`].
160    pub const unsafe fn borrow_raw(raw: RawCStream) -> Self {
161        Self {
162            raw,
163            lifetime: PhantomData,
164        }
165    }
166}
167
168impl AsCStream for BorrowedCStream<'_> {
169    fn as_c_stream(&self) -> BorrowedCStream<'_> {
170        *self
171    }
172}
173
174impl AsRawCStream for BorrowedCStream<'_> {
175    fn as_raw_c_stream(&self) -> RawCStream {
176        self.raw
177    }
178}
179
180macro_rules! pointer_impls {
181    ($trait:ident, $method:ident, $return:ty) => {
182        impl<T: $trait + ?Sized> $trait for &T {
183            fn $method(&self) -> $return {
184                T::$method(self)
185            }
186        }
187        impl<T: $trait + ?Sized> $trait for &mut T {
188            fn $method(&self) -> $return {
189                T::$method(self)
190            }
191        }
192        #[cfg(feature = "alloc")]
193        #[cfg_attr(do_doc_cfg, doc(cfg(feature = "alloc")))]
194        impl<T: $trait + ?Sized> $trait for alloc::boxed::Box<T> {
195            fn $method(&self) -> $return {
196                T::$method(self)
197            }
198        }
199        #[cfg(feature = "alloc")]
200        #[cfg_attr(do_doc_cfg, doc(cfg(feature = "alloc")))]
201        impl<T: $trait + ?Sized> $trait for alloc::rc::Rc<T> {
202            fn $method(&self) -> $return {
203                T::$method(self)
204            }
205        }
206        #[cfg(feature = "alloc")]
207        #[cfg_attr(do_doc_cfg, doc(cfg(feature = "alloc")))]
208        impl<T: $trait + ?Sized> $trait for alloc::sync::Arc<T> {
209            fn $method(&self) -> $return {
210                T::$method(self)
211            }
212        }
213    };
214}
215
216/// A trait to borrow a stream from an underlying object.
217pub trait AsCStream {
218    /// Borrows the stream.
219    fn as_c_stream(&self) -> BorrowedCStream<'_>;
220}
221pointer_impls!(AsCStream, as_c_stream, BorrowedCStream<'_>);
222
223/// A trait to extract the raw stream from an underlying object.
224pub trait AsRawCStream {
225    /// Extracts the raw stream.
226    ///
227    /// This function is typically used to borrow an owned stream.
228    /// When used in this way,
229    /// this method does not pass ownership of the raw stream to the caller,
230    /// and the stream is only guaranteed to be valid while the original object has not yet been destroyed.
231    ///
232    /// However, borrowing is not strictly required.
233    /// See [`AsCStream::as_c_stream`] for an API which strictly borrows a stream.
234    fn as_raw_c_stream(&self) -> RawCStream;
235}
236pointer_impls!(AsRawCStream, as_raw_c_stream, RawCStream);
237
238/// A trait to express the ability to construct an object from a raw stream.
239pub trait FromRawCStream {
240    /// Constructs a new instance of `Self` from the given raw stream.
241    ///
242    /// This function is typically used to consume ownership of the specified stream.
243    /// When used in this way,
244    /// the returned object will take responsibility for closing it when the object goes out of scope.
245    ///
246    /// # Safety
247    /// - The stream passed in must be an [owned stream](https://doc.rust-lang.org/std/io/index.html#io-safety); in particular, it must be open.
248    unsafe fn from_raw_c_stream(raw: RawCStream) -> Self;
249}
250impl FromRawCStream for RawCStream {
251    unsafe fn from_raw_c_stream(raw: RawCStream) -> Self {
252        raw
253    }
254}
255
256/// A trait to express the ability to consume an object and acquire ownership of its raw stream.
257pub trait IntoRawCStream {
258    /// Consumes this object, returning the raw underlying stream.
259    ///
260    /// This function is typically used to transfer ownership of the underlying stream to the caller.
261    /// When used in this way,
262    /// callers are then the unique owners of the file descriptor and must close it once it’s no longer needed.
263    fn into_raw_c_stream(self) -> RawCStream;
264}
265
266impl IntoRawCStream for RawCStream {
267    fn into_raw_c_stream(self) -> RawCStream {
268        self
269    }
270}
271
272//////////////////////
273// Operations on files
274//////////////////////
275
276pub fn tmpfile() -> Option<OwnedCStream> {
277    let raw = NonNull::new(unsafe { libc::tmpfile() })?;
278    Some(unsafe { OwnedCStream::from_raw_c_stream(raw) })
279}
280
281//////////////
282// File access
283//////////////
284
285/// Take `&T` to ensure that `stream` is alive for the outer scope
286fn ptr<T: AsCStream>(stream: &T) -> *mut libc::FILE {
287    stream.as_c_stream().as_raw_c_stream().as_ptr()
288}
289
290/// Wrapper around [`libc::fflush`].
291#[allow(clippy::result_unit_err)]
292pub fn flush<T: AsCStream>(stream: T) -> Result<(), ()> {
293    let ret = unsafe { libc::fflush(ptr(&stream)) };
294    match ret {
295        0 => Ok(()),
296        libc::EOF => Err(()),
297        _undocumented => Err(()),
298    }
299}
300
301/// Wrapper around [`libc::fopen`].
302pub fn open(filename: &CStr, mode: &CStr) -> Option<OwnedCStream> {
303    let raw = NonNull::new(unsafe { libc::fopen(filename.as_ptr(), mode.as_ptr()) })?;
304    Some(unsafe { OwnedCStream::from_raw_c_stream(raw) })
305}
306
307/// Wrapper around [`libc::freopen`].
308pub fn reopen(filename: Option<&CStr>, mode: &CStr, stream: OwnedCStream) -> Option<OwnedCStream> {
309    let raw = NonNull::new(unsafe {
310        libc::freopen(
311            filename.map(CStr::as_ptr).unwrap_or_else(ptr::null),
312            mode.as_ptr(),
313            stream.into_raw_c_stream().as_ptr(),
314        )
315    })?;
316    Some(unsafe { OwnedCStream::from_raw_c_stream(raw) })
317}
318
319/// Wrapper around [`libc::fileno`].
320pub fn fileno<T: AsCStream>(stream: T) -> Option<c_int> {
321    match unsafe { libc::fileno(ptr(&stream)) } {
322        -1 => None,
323        other => Some(other),
324    }
325}
326
327/////////////////////////
328// Character input/output
329/////////////////////////
330
331/// Wrapper around [`libc::fgetc`].
332pub fn getc<T: AsCStream>(stream: T) -> Option<u8> {
333    match unsafe { libc::fgetc(ptr(&stream)) } {
334        libc::EOF => None,
335        it => Some(it as u8),
336    }
337}
338
339/// Wrapper around [`libc::ungetc`].
340#[allow(clippy::result_unit_err)]
341pub fn ungetc<T: AsCStream>(char: u8, stream: T) -> Result<(), ()> {
342    match unsafe { libc::ungetc(char as c_int, ptr(&stream)) } {
343        libc::EOF => Err(()),
344        _ => Ok(()),
345    }
346}
347
348/// Wrapper around [`libc::fgets`].
349pub fn gets<T: AsCStream>(buf: &mut [u8], stream: T) -> Option<&CStr> {
350    match unsafe {
351        libc::fgets(
352            buf.as_mut_ptr().cast::<c_char>(),
353            buf.len().try_into().ok()?,
354            ptr(&stream),
355        )
356    }
357    .is_null()
358    {
359        true => None,
360        false => CStr::from_bytes_until_nul(buf).ok(),
361    }
362}
363
364/// Wrapper around [`libc::fputc`].
365#[allow(clippy::result_unit_err, clippy::if_same_then_else)]
366pub fn putc<T: AsCStream>(char: u8, stream: T) -> Result<(), ()> {
367    let ret = unsafe { libc::fputc(char as c_int, ptr(&stream)) };
368    if ret == libc::EOF {
369        Err(())
370    } else {
371        Ok(())
372    }
373}
374
375/// Wrapper around [`libc::fputs`].
376#[allow(clippy::result_unit_err)]
377pub fn puts<T: AsCStream>(s: &CStr, stream: T) -> Result<(), ()> {
378    let ret = unsafe { libc::fputs(s.as_ptr(), ptr(&stream)) };
379    if ret == libc::EOF {
380        Err(())
381    } else if ret > 0 {
382        Ok(())
383    } else {
384        Err(()) // undocumented
385    }
386}
387
388//////////////////////
389// Direct input/output
390//////////////////////
391
392/// Wrapper around [`libc::fread`].
393pub fn read<T: AsCStream>(buf: &mut [u8], stream: T) -> usize {
394    unsafe {
395        libc::fread(
396            buf.as_mut_ptr().cast::<c_void>(),
397            1,
398            buf.len(),
399            ptr(&stream),
400        )
401    }
402}
403
404/// Wrapper around [`libc::fwrite`].
405pub fn write<T: AsCStream>(buf: &[u8], stream: T) -> usize {
406    unsafe { libc::fwrite(buf.as_ptr().cast::<c_void>(), 1, buf.len(), ptr(&stream)) }
407}
408///////////////////
409// File positioning
410///////////////////
411
412/// Wrapper around [`libc::fseek`].
413#[cfg(feature = "std")]
414#[cfg_attr(do_doc_cfg, doc(cfg(feature = "std")))]
415#[allow(clippy::result_unit_err)]
416pub fn seek<T: AsCStream>(stream: T, pos: std::io::SeekFrom) -> Result<(), ()> {
417    let (offset, whence) = match pos {
418        std::io::SeekFrom::Start(it) => (it.try_into().map_err(|_| ())?, libc::SEEK_SET),
419        std::io::SeekFrom::End(it) => (it, libc::SEEK_END),
420        std::io::SeekFrom::Current(it) => (it, libc::SEEK_CUR),
421    };
422    match unsafe { libc::fseek(ptr(&stream), offset, whence) } {
423        0 => Ok(()),
424        -1 => Err(()),
425        _undocumented => Err(()),
426    }
427}
428
429/// Wrapper around [`libc::ftell`].
430pub fn tell<T: AsCStream>(stream: T) -> Option<u64> {
431    unsafe { libc::ftell(ptr(&stream)) }.try_into().ok()
432}
433
434/// Wrapper around [`libc::rewind`].
435pub fn rewind<T: AsCStream>(stream: T) {
436    unsafe { libc::rewind(ptr(&stream)) }
437}
438
439/////////////////
440// Error-handling
441/////////////////
442
443/// Wrapper around [`libc::feof`].
444pub fn eof<T: AsCStream>(stream: T) -> bool {
445    unsafe { libc::feof(ptr(&stream)) != 0 }
446}
447
448/// Wrapper around [`libc::clearerr`].
449pub fn clear_errors<T: AsCStream>(stream: T) {
450    unsafe { libc::clearerr(ptr(&stream)) }
451}
452
453/// A capture of the error indicator on a [`RawCStream`] through [`libc::ferror`].
454#[derive(Debug, Clone)]
455pub struct FError(pub i32);
456
457impl FError {
458    pub fn of<T: AsCStream>(stream: T) -> Self {
459        Self(unsafe { libc::ferror(ptr(&stream)) })
460    }
461}
462
463impl fmt::Display for FError {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        f.write_fmt(format_args!("error indicator {} was set on stream", self.0))
466    }
467}
468
469#[cfg(feature = "std")]
470impl std::error::Error for FError {}
471
472#[cfg(all(test, feature = "std"))]
473fn read_to_string(mut f: impl std::io::Read) -> String {
474    let mut s = String::new();
475    f.read_to_string(&mut s).unwrap();
476    s
477}
478
479#[cfg(all(test, feature = "std"))]
480mod tests {
481    use super::*;
482    use std::{ffi::CString, fs, os::unix::ffi::OsStrExt as _};
483    use tempfile::NamedTempFile;
484
485    #[test]
486    fn test() {
487        let named = NamedTempFile::new().unwrap();
488        let path = CString::new(named.path().as_os_str().as_bytes()).unwrap();
489        let stream = open(&path, c"rw+").unwrap();
490        assert_eq!(write(b"hello, world!", stream), 13);
491        assert_eq!(fs::read_to_string(named.path()).unwrap(), "hello, world!");
492    }
493}