cloneable_file/
lib.rs

1//! Cloneable file descriptor.
2//!
3//! This allows a file descriptor to be cloned, ordered, compared, hashed etc.
4//!
5//! # Usage
6//!
7//! ```rust
8//! # use cloneable_file::CloneableFile;
9//! # use std::{fs, io};
10//! #[derive(Clone)]
11//! struct Event {
12//!   file: CloneableFile,
13//! }
14//!
15//! fn process(vec: Vec<Event>) {}
16//!
17//! # fn main() -> io::Result<()> {
18//! let mut vec = Vec::new();
19//! let file = CloneableFile::create("foo.txt")?;
20//! vec.push(Event { file: file });
21//! process(vec.clone());
22//! # fs::remove_file("foo.txt")?;
23//! # Ok(())
24//! # }
25//! ```
26mod handle;
27mod lock;
28
29use std::{
30    cmp::Ordering,
31    fs::{File, Metadata},
32    hash::{Hash, Hasher},
33    io::{self, Read, Seek, Write},
34    path::Path,
35};
36
37use handle::PortableFileHandle;
38use lock::Lock;
39
40type Arc<T> = std::sync::Arc<T>;
41
42/// Wrapper struct to avoid allocating two [`Arc`] instances in
43/// [`CloneableFile`].
44#[derive(Debug)]
45struct FileImpl {
46    file: File,
47    state_lock: Lock<()>,
48}
49
50/// An object providing access to an open file on the filesystem.
51///
52/// Compared to [`std::fs::File`] this object also implements [`Clone`],
53/// [`PartialEq`], [`Eq`], [`PartialOrd`] and [`Ord`] traits which makes it
54/// useful in certain contexts.
55///
56/// # Note
57///
58/// Cloning the file works by increasing the reference count on the internal
59/// [`std::fs::File`] instance, and comparison uses raw file descriptor handles.
60#[derive(Debug, Clone)]
61pub struct CloneableFile {
62    inner: Arc<FileImpl>,
63}
64
65impl CloneableFile {
66    /// Opens a file in write-only mode.
67    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<CloneableFile> {
68        File::open(path).map(CloneableFile::from)
69    }
70
71    /// Calls a closure with a file descriptor and with a lock acquired.
72    fn with_lock<T>(&self, func: impl FnOnce(&File) -> T) -> T {
73        lock::acquire(&self.inner.state_lock, |_| func(&self.inner.file))
74    }
75
76    /// Opens a file in write-only mode.
77    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<CloneableFile> {
78        File::create(path).map(CloneableFile::from)
79    }
80
81    /// Queries metadata about the underlying file.
82    pub fn metadata(&self) -> io::Result<Metadata> {
83        self.with_lock(|f| f.metadata())
84    }
85
86    /// Truncates or extends the underlying file, updating the size of this file
87    /// to become size.
88    pub fn set_len(&self, size: u64) -> io::Result<()> {
89        self.with_lock(|f| f.set_len(size))
90    }
91
92    /// Creates a new [`CloneableFile`] instance that shares the same underlying
93    /// file handle as the existing [`CloneableFile`] instance. Reads, writes,
94    /// and seeks will affect both [`CloneableFile`] instances simultaneously.
95    pub fn try_clone(&self) -> io::Result<CloneableFile> {
96        self.with_lock(|f| f.try_clone().map(CloneableFile::from))
97    }
98
99    fn portable_fd(&self) -> PortableFileHandle {
100        handle::portable_file_handle(&self.inner.file)
101    }
102}
103
104impl From<File> for CloneableFile {
105    fn from(file: File) -> Self {
106        CloneableFile {
107            inner: Arc::new(FileImpl {
108                file,
109                state_lock: Lock::new(()),
110            }),
111        }
112    }
113}
114
115impl Read for CloneableFile {
116    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
117        self.with_lock(|mut f| f.read(buf))
118    }
119}
120
121impl Read for &CloneableFile {
122    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
123        self.with_lock(|mut f| f.read(buf))
124    }
125}
126
127impl Write for CloneableFile {
128    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
129        self.with_lock(|mut f| f.write(buf))
130    }
131
132    fn flush(&mut self) -> io::Result<()> {
133        self.with_lock(|mut f| f.flush())
134    }
135}
136
137impl Write for &CloneableFile {
138    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
139        self.with_lock(|mut f| f.write(buf))
140    }
141
142    fn flush(&mut self) -> io::Result<()> {
143        self.with_lock(|mut f| f.flush())
144    }
145}
146
147impl Seek for CloneableFile {
148    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
149        self.with_lock(|mut f| f.seek(pos))
150    }
151}
152
153impl Seek for &CloneableFile {
154    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
155        self.with_lock(|mut f| f.seek(pos))
156    }
157}
158
159impl PartialEq for CloneableFile {
160    fn eq(&self, other: &Self) -> bool {
161        self.portable_fd().eq(&other.portable_fd())
162    }
163}
164
165impl Eq for CloneableFile {}
166
167impl Hash for CloneableFile {
168    fn hash<H: Hasher>(&self, state: &mut H) {
169        self.portable_fd().hash(state)
170    }
171}
172
173impl PartialOrd for CloneableFile {
174    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
175        self.portable_fd().partial_cmp(&other.portable_fd())
176    }
177}
178
179impl Ord for CloneableFile {
180    fn cmp(&self, other: &Self) -> Ordering {
181        self.portable_fd().cmp(&other.portable_fd())
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use std::{
188        cmp::Ordering,
189        collections::{BTreeMap, HashMap},
190    };
191
192    use super::*;
193
194    fn is_read_write_seek<T: io::Read + io::Write + io::Seek>(_: T) {}
195    fn is_send_sync<T: Send + Sync>(_: T) {}
196
197    fn cloneable_tempfile() -> io::Result<CloneableFile> {
198        tempfile::tempfile().map(CloneableFile::from)
199    }
200
201    #[test]
202    fn cloneable_file_is_usable() -> io::Result<()> {
203        let f = cloneable_tempfile()?;
204        let f2 = f.clone();
205        is_read_write_seek(f);
206        is_read_write_seek(&f2);
207        is_send_sync(f2);
208        is_send_sync(tempfile::tempfile()?);
209        Ok(())
210    }
211
212    #[test]
213    fn equality() -> io::Result<()> {
214        let f1 = cloneable_tempfile()?;
215        let f2 = cloneable_tempfile()?;
216        assert_ne!(&f1, &f2);
217        assert_eq!(&f2, &f2);
218        assert_eq!(&f1, &f1);
219        Ok(())
220    }
221
222    #[test]
223    fn ordering() -> io::Result<()> {
224        let f1 = cloneable_tempfile()?;
225        let f2 = cloneable_tempfile()?;
226        assert!(matches!(f1.cmp(&f2), Ordering::Greater | Ordering::Less));
227        Ok(())
228    }
229
230    #[test]
231    fn do_ops_with_lock_acquired() -> io::Result<()> {
232        let f = cloneable_tempfile()?;
233        f.with_lock(|mut f| {
234            f.write_all(b"Hello,")?;
235            f.write_all(b"qq world!\n")?;
236            f.sync_all()?;
237            Ok(())
238        })
239    }
240
241    #[test]
242    fn can_use_with_btreemap() -> io::Result<()> {
243        let mut map = BTreeMap::new();
244        let f1 = cloneable_tempfile()?;
245        let f2 = cloneable_tempfile()?;
246        map.insert(f1.clone(), 1);
247        map.insert(f2, 2);
248        assert_eq!(map.len(), 2);
249        assert_eq!(map[&f1], 1);
250        Ok(())
251    }
252
253    #[test]
254    fn can_use_with_hashmap() -> io::Result<()> {
255        let mut map = HashMap::new();
256        let f1 = cloneable_tempfile()?;
257        let f2 = cloneable_tempfile()?;
258        map.insert(f1.clone(), 1);
259        map.insert(f2, 2);
260        assert_eq!(map.len(), 2);
261        assert_eq!(map[&f1], 1);
262        Ok(())
263    }
264}