Skip to main content

coven_foundation/files/
observed_file.rs

1//! Observe a user's original without acquiring any ability to modify it.
2
3use super::{FileError, FileReader};
4use std::{
5    fs::Metadata,
6    io,
7    path::{Path, PathBuf},
8    time::SystemTime,
9};
10use tokio::io::AsyncReadExt;
11
12/// Facts captured while reading one original; validation never rereads its bytes.
13#[derive(Debug)]
14pub struct ObservedFile {
15    path: PathBuf,
16    metadata: Metadata,
17    modified_at: SystemTime,
18}
19
20/// A missing or changed original is distinct from an operating-system failure.
21#[derive(Debug, thiserror::Error)]
22pub enum ObservationError {
23    /// The original no longer exists.
24    #[error("file is missing: {}", .0.display())]
25    Missing(PathBuf),
26    /// The file changed during or after the read.
27    #[error("file changed: {}", .0.display())]
28    Changed(PathBuf),
29    /// The filesystem refused an operation.
30    #[error(transparent)]
31    File(#[from] FileError),
32}
33
34impl ObservationError {
35    pub(super) fn at(operation: &'static str, path: &Path, error: io::Error) -> Self {
36        if error.kind() == io::ErrorKind::NotFound {
37            Self::Missing(path.to_owned())
38        } else {
39            FileError::at(operation, path, error).into()
40        }
41    }
42}
43
44impl ObservedFile {
45    /// The original's path.
46    pub fn path(&self) -> &Path {
47        &self.path
48    }
49    /// Its size at the time of the read.
50    pub fn size(&self) -> u64 {
51        self.metadata.len()
52    }
53    /// Its modification time at the time of the read.
54    pub fn modified_at(&self) -> SystemTime {
55        self.modified_at
56    }
57
58    /// Refuse a missing, replaced or modified original without reading its bytes.
59    pub fn validate(&self) -> Result<(), ObservationError> {
60        let metadata = std::fs::metadata(&self.path)
61            .map_err(|e| ObservationError::at("read original", &self.path, e))?;
62        self.check(&metadata)
63    }
64
65    fn check(&self, metadata: &Metadata) -> Result<(), ObservationError> {
66        let modified = metadata
67            .modified()
68            .map_err(|e| FileError::at("read modification time", &self.path, e))?;
69        let mut same =
70            metadata.is_file() && metadata.len() == self.size() && modified == self.modified_at;
71        #[cfg(unix)]
72        {
73            use std::os::unix::fs::MetadataExt;
74            same &= metadata.dev() == self.metadata.dev()
75                && metadata.ino() == self.metadata.ino()
76                && metadata.ctime() == self.metadata.ctime()
77                && metadata.ctime_nsec() == self.metadata.ctime_nsec();
78        }
79        #[cfg(windows)]
80        {
81            use std::os::windows::fs::MetadataExt;
82            same &= metadata.creation_time() == self.metadata.creation_time();
83        }
84        if same {
85            Ok(())
86        } else {
87            Err(ObservationError::Changed(self.path.clone()))
88        }
89    }
90}
91
92/// A user's original: a file outside coven's directories, at the path the
93/// app named for it, which coven reads and never changes (ยง17.3). It is made
94/// where the app's call hands the path in, or where coven reads back the path
95/// it recorded.
96#[derive(Debug)]
97pub struct UserOriginal {
98    path: PathBuf,
99}
100
101impl UserOriginal {
102    /// The original at `path`.
103    pub fn at(path: PathBuf) -> Self {
104        Self { path }
105    }
106
107    /// Its path.
108    pub fn path(&self) -> &Path {
109        &self.path
110    }
111
112    /// Read it once in bounded chunks, checking its handle and path before
113    /// returning. The consumer can hash the bytes and report progress.
114    pub async fn observe(
115        &self,
116        consume: impl FnMut(&[u8]),
117    ) -> Result<ObservedFile, ObservationError> {
118        observe_file(&self.path, consume).await
119    }
120
121    /// Open it for positioned reads and check its size and modification
122    /// time, once: later reads return what's on disk.
123    pub fn open_reader(
124        &self,
125        size: u64,
126        modified: SystemTime,
127    ) -> Result<FileReader, ObservationError> {
128        FileReader::open_original(&self.path, size, modified)
129    }
130}
131
132/// Observe an original's path, size and modification time without reading
133/// its bytes: it must be a regular file, and its path must name the same file
134/// after its handle's facts are taken.
135pub(super) async fn observe_metadata(path: &Path) -> Result<ObservedFile, ObservationError> {
136    let file = tokio::fs::File::open(path)
137        .await
138        .map_err(|e| ObservationError::at("read original", path, e))?;
139    let observed = observed(path, &file).await?;
140    observed.validate()?;
141    Ok(observed)
142}
143
144/// The facts of an opened original, refusing anything but a regular file.
145/// Its path is any the operating system opens, valid Unicode or not:
146/// coven records its bytes (`path_bytes`).
147async fn observed(path: &Path, file: &tokio::fs::File) -> Result<ObservedFile, ObservationError> {
148    let metadata =
149        file.metadata().await.map_err(|e| ObservationError::at("read original", path, e))?;
150    if !metadata.is_file() {
151        return Err(FileError::at(
152            "read original",
153            path,
154            io::Error::new(io::ErrorKind::InvalidInput, "a regular file is required"),
155        )
156        .into());
157    }
158    let modified_at =
159        metadata.modified().map_err(|e| FileError::at("read modification time", path, e))?;
160    Ok(ObservedFile { path: path.to_owned(), metadata, modified_at })
161}
162
163async fn observe_file(
164    path: &Path,
165    mut consume: impl FnMut(&[u8]),
166) -> Result<ObservedFile, ObservationError> {
167    let mut file = tokio::fs::File::open(path)
168        .await
169        .map_err(|e| ObservationError::at("read original", path, e))?;
170    let observed = observed(path, &file).await?;
171    let mut buffer = vec![0; 64 * 1024];
172    let mut size = 0u64;
173    loop {
174        let read = file
175            .read(&mut buffer)
176            .await
177            .map_err(|e| ObservationError::at("read original", path, e))?;
178        if read == 0 {
179            break;
180        }
181        size += read as u64;
182        consume(&buffer[..read]);
183    }
184    if size != observed.size() {
185        return Err(ObservationError::Changed(path.to_owned()));
186    }
187    observed.check(
188        &file.metadata().await.map_err(|e| ObservationError::at("read original", path, e))?,
189    )?;
190    observed.validate()?;
191    Ok(observed)
192}