cloneable-file 0.1.1

Cloneable file descriptor
Documentation
//! Cloneable file descriptor.
//!
//! This allows a file descriptor to be cloned, ordered, compared, hashed etc.
//!
//! # Usage
//!
//! ```rust
//! # use cloneable_file::CloneableFile;
//! # use std::{fs, io};
//! #[derive(Clone)]
//! struct Event {
//!   file: CloneableFile,
//! }
//!
//! fn process(vec: Vec<Event>) {}
//!
//! # fn main() -> io::Result<()> {
//! let mut vec = Vec::new();
//! let file = CloneableFile::create("foo.txt")?;
//! vec.push(Event { file: file });
//! process(vec.clone());
//! # fs::remove_file("foo.txt")?;
//! # Ok(())
//! # }
//! ```
mod handle;
mod lock;

use std::{
    cmp::Ordering,
    fs::{File, Metadata},
    hash::{Hash, Hasher},
    io::{self, Read, Seek, Write},
    path::Path,
};

use handle::PortableFileHandle;
use lock::Lock;

type Arc<T> = std::sync::Arc<T>;

/// Wrapper struct to avoid allocating two [`Arc`] instances in
/// [`CloneableFile`].
#[derive(Debug)]
struct FileImpl {
    file: File,
    state_lock: Lock<()>,
}

/// An object providing access to an open file on the filesystem.
///
/// Compared to [`std::fs::File`] this object also implements [`Clone`],
/// [`PartialEq`], [`Eq`], [`PartialOrd`] and [`Ord`] traits which makes it
/// useful in certain contexts.
///
/// # Note
///
/// Cloning the file works by increasing the reference count on the internal
/// [`std::fs::File`] instance, and comparison uses raw file descriptor handles.
#[derive(Debug, Clone)]
pub struct CloneableFile {
    inner: Arc<FileImpl>,
}

impl CloneableFile {
    /// Opens a file in write-only mode.
    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<CloneableFile> {
        File::open(path).map(CloneableFile::from)
    }

    /// Calls a closure with a file descriptor and with a lock acquired.
    fn with_lock<T>(&self, func: impl FnOnce(&File) -> T) -> T {
        lock::acquire(&self.inner.state_lock, |_| func(&self.inner.file))
    }

    /// Opens a file in write-only mode.
    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<CloneableFile> {
        File::create(path).map(CloneableFile::from)
    }

    /// Queries metadata about the underlying file.
    pub fn metadata(&self) -> io::Result<Metadata> {
        self.with_lock(|f| f.metadata())
    }

    /// Truncates or extends the underlying file, updating the size of this file
    /// to become size.
    pub fn set_len(&self, size: u64) -> io::Result<()> {
        self.with_lock(|f| f.set_len(size))
    }

    /// Creates a new [`CloneableFile`] instance that shares the same underlying
    /// file handle as the existing [`CloneableFile`] instance. Reads, writes,
    /// and seeks will affect both [`CloneableFile`] instances simultaneously.
    pub fn try_clone(&self) -> io::Result<CloneableFile> {
        self.with_lock(|f| f.try_clone().map(CloneableFile::from))
    }

    fn portable_fd(&self) -> PortableFileHandle {
        handle::portable_file_handle(&self.inner.file)
    }
}

impl From<File> for CloneableFile {
    fn from(file: File) -> Self {
        CloneableFile {
            inner: Arc::new(FileImpl {
                file,
                state_lock: Lock::new(()),
            }),
        }
    }
}

impl Read for CloneableFile {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.with_lock(|mut f| f.read(buf))
    }
}

impl Read for &CloneableFile {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.with_lock(|mut f| f.read(buf))
    }
}

impl Write for CloneableFile {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.with_lock(|mut f| f.write(buf))
    }

    fn flush(&mut self) -> io::Result<()> {
        self.with_lock(|mut f| f.flush())
    }
}

impl Write for &CloneableFile {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.with_lock(|mut f| f.write(buf))
    }

    fn flush(&mut self) -> io::Result<()> {
        self.with_lock(|mut f| f.flush())
    }
}

impl Seek for CloneableFile {
    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
        self.with_lock(|mut f| f.seek(pos))
    }
}

impl Seek for &CloneableFile {
    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
        self.with_lock(|mut f| f.seek(pos))
    }
}

impl PartialEq for CloneableFile {
    fn eq(&self, other: &Self) -> bool {
        self.portable_fd().eq(&other.portable_fd())
    }
}

impl Eq for CloneableFile {}

impl Hash for CloneableFile {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.portable_fd().hash(state)
    }
}

impl PartialOrd for CloneableFile {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.portable_fd().partial_cmp(&other.portable_fd())
    }
}

impl Ord for CloneableFile {
    fn cmp(&self, other: &Self) -> Ordering {
        self.portable_fd().cmp(&other.portable_fd())
    }
}

#[cfg(test)]
mod tests {
    use std::{
        cmp::Ordering,
        collections::{BTreeMap, HashMap},
    };

    use super::*;

    fn is_read_write_seek<T: io::Read + io::Write + io::Seek>(_: T) {}
    fn is_send_sync<T: Send + Sync>(_: T) {}

    fn cloneable_tempfile() -> io::Result<CloneableFile> {
        tempfile::tempfile().map(CloneableFile::from)
    }

    #[test]
    fn cloneable_file_is_usable() -> io::Result<()> {
        let f = cloneable_tempfile()?;
        let f2 = f.clone();
        is_read_write_seek(f);
        is_read_write_seek(&f2);
        is_send_sync(f2);
        is_send_sync(tempfile::tempfile()?);
        Ok(())
    }

    #[test]
    fn equality() -> io::Result<()> {
        let f1 = cloneable_tempfile()?;
        let f2 = cloneable_tempfile()?;
        assert_ne!(&f1, &f2);
        assert_eq!(&f2, &f2);
        assert_eq!(&f1, &f1);
        Ok(())
    }

    #[test]
    fn ordering() -> io::Result<()> {
        let f1 = cloneable_tempfile()?;
        let f2 = cloneable_tempfile()?;
        assert!(matches!(f1.cmp(&f2), Ordering::Greater | Ordering::Less));
        Ok(())
    }

    #[test]
    fn do_ops_with_lock_acquired() -> io::Result<()> {
        let f = cloneable_tempfile()?;
        f.with_lock(|mut f| {
            f.write_all(b"Hello,")?;
            f.write_all(b"qq world!\n")?;
            f.sync_all()?;
            Ok(())
        })
    }

    #[test]
    fn can_use_with_btreemap() -> io::Result<()> {
        let mut map = BTreeMap::new();
        let f1 = cloneable_tempfile()?;
        let f2 = cloneable_tempfile()?;
        map.insert(f1.clone(), 1);
        map.insert(f2, 2);
        assert_eq!(map.len(), 2);
        assert_eq!(map[&f1], 1);
        Ok(())
    }

    #[test]
    fn can_use_with_hashmap() -> io::Result<()> {
        let mut map = HashMap::new();
        let f1 = cloneable_tempfile()?;
        let f2 = cloneable_tempfile()?;
        map.insert(f1.clone(), 1);
        map.insert(f2, 2);
        assert_eq!(map.len(), 2);
        assert_eq!(map[&f1], 1);
        Ok(())
    }
}