Skip to main content

coven_foundation/files/
file_reader.rs

1//! Open read-only handles for positioned reads.
2
3use super::{FileError, ObservationError};
4use std::{
5    fs::{File, Metadata},
6    io,
7    path::{Path, PathBuf},
8    time::SystemTime,
9};
10
11/// An open read-only file. Renaming or unlinking its path never redirects reads.
12/// Reads return what's on disk, as the platform's own positioned reads do.
13pub struct FileReader {
14    file: File,
15    path: PathBuf,
16    size: u64,
17}
18
19impl FileReader {
20    /// Open a user original and check its size and modification time,
21    /// once: later reads return what's on disk.
22    pub(super) fn open_original(
23        path: &Path,
24        size: u64,
25        modified: SystemTime,
26    ) -> Result<Self, ObservationError> {
27        let (file, metadata) = open_file(path)?;
28        let opened = metadata
29            .modified()
30            .map_err(|source| FileError::at("read modification time", path, source))?;
31        if metadata.len() != size || opened != modified {
32            return Err(ObservationError::Changed(path.to_owned()));
33        }
34        Ok(Self { file, path: path.to_owned(), size })
35    }
36
37    pub(crate) fn open(path: &Path) -> Result<Self, ObservationError> {
38        let (file, metadata) = open_file(path)?;
39        Ok(Self { file, path: path.to_owned(), size: metadata.len() })
40    }
41
42    /// The size captured on this open handle.
43    pub fn size(&self) -> u64 {
44        self.size
45    }
46
47    /// Read exactly the requested bytes using the open file's positioned-read API.
48    /// A range beyond the captured size fails instead of returning a short read,
49    /// and a file found shorter than that range fails as changed.
50    pub fn read_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, ObservationError> {
51        #[cfg(any(test, feature = "test-utils"))]
52        crate::test_work::file_range_read();
53        if offset.checked_add(len as u64).is_none_or(|end| end > self.size()) {
54            return Err(FileError::at(
55                "read file range",
56                &self.path,
57                io::Error::new(io::ErrorKind::InvalidInput, "range exceeds file size"),
58            )
59            .into());
60        }
61        let mut bytes = Vec::new();
62        bytes.try_reserve_exact(len).map_err(|source| {
63            FileError::at(
64                "allocate file range",
65                &self.path,
66                io::Error::new(io::ErrorKind::OutOfMemory, source),
67            )
68        })?;
69        bytes.resize(len, 0);
70        self.read_exact_at(offset, &mut bytes)?;
71        Ok(bytes)
72    }
73
74    fn read_exact_at(&self, mut offset: u64, mut bytes: &mut [u8]) -> Result<(), ObservationError> {
75        while !bytes.is_empty() {
76            #[cfg(unix)]
77            let count = {
78                use std::os::unix::fs::FileExt;
79                self.file.read_at(bytes, offset)
80            };
81            #[cfg(windows)]
82            let count = {
83                use std::os::windows::fs::FileExt;
84                self.file.seek_read(bytes, offset)
85            };
86            match count {
87                Ok(0) => return Err(ObservationError::Changed(self.path.clone())),
88                Ok(count) => {
89                    bytes = &mut bytes[count..];
90                    offset += count as u64;
91                }
92                Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
93                Err(error) => return Err(ObservationError::at("read file", &self.path, error)),
94            }
95        }
96        Ok(())
97    }
98}
99
100/// Open a regular file for positioned reads, with its metadata.
101fn open_file(path: &Path) -> Result<(File, Metadata), ObservationError> {
102    #[cfg(any(test, feature = "test-utils"))]
103    super::reached::reached(path);
104    let file = File::open(path).map_err(|error| ObservationError::at("read file", path, error))?;
105    let metadata =
106        file.metadata().map_err(|error| ObservationError::at("read file", path, error))?;
107    if !metadata.is_file() {
108        return Err(FileError::at(
109            "open file for ranges",
110            path,
111            io::Error::new(io::ErrorKind::InvalidInput, "a regular file is required"),
112        )
113        .into());
114    }
115    Ok((file, metadata))
116}
117
118#[cfg(test)]
119#[path = "file_reader_tests.rs"]
120mod tests;