coven_foundation/files/
file_reader.rs1use super::{FileError, ObservationError};
4use std::{
5 fs::{File, Metadata},
6 io,
7 path::{Path, PathBuf},
8 time::SystemTime,
9};
10
11pub struct FileReader {
14 file: File,
15 path: PathBuf,
16 size: u64,
17}
18
19impl FileReader {
20 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 pub fn size(&self) -> u64 {
44 self.size
45 }
46
47 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
100fn 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;