Skip to main content

harmos_runtime/filesystem/publication/
mod.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12#[cfg(unix)]
13mod unix;
14
15// The Windows Publication Primitives Are A Win32 FFI Boundary; Each Call Carries Its SAFETY Argument.
16#[cfg(windows)]
17#[allow(unsafe_code)]
18mod windows;
19
20/* === Re-Exports === */
21
22/* === Imports === */
23
24// Standard Library Imports
25use std::{
26    fs::{self, File, OpenOptions},
27    io::{self, Write},
28    path::{Path, PathBuf},
29};
30// Third Party Imports
31use tempfile::NamedTempFile;
32// Local Imports
33#[cfg(unix)]
34use unix as platform;
35
36#[cfg(windows)]
37use windows as platform;
38
39/* === Constants === */
40
41/* === Macros === */
42
43/* === Type Aliases === */
44
45/* === Structs === */
46
47struct Staging(PathBuf);
48
49/* === Enums === */
50
51#[derive(Debug, thiserror::Error)]
52pub enum ConfirmError<E> {
53    #[error(transparent)]
54    Filesystem(crate::filesystem::Error),
55
56    #[error(transparent)]
57    Checkpoint(E),
58}
59
60/* === Traits === */
61
62/* === Implementations === */
63
64impl Staging {
65    fn file(directory: &Path) -> io::Result<(Self, File)> {
66        let temporary = NamedTempFile::new_in(directory)?;
67        let (file, path) = temporary.keep().map_err(|error| error.error)?;
68        Ok((Self(path), file))
69    }
70}
71
72impl Drop for Staging {
73    fn drop(&mut self) {
74        let _ = fs::remove_file(&self.0);
75    }
76}
77
78/* === Public Functions === */
79
80pub fn create_dir_all(path: &Path) -> crate::filesystem::Result<()> {
81    platform::create_dir_all(path).map_err(|error| crate::filesystem::Error::publication("create directory", path, error))
82}
83
84pub fn move_directory(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
85    let metadata =
86        fs::symlink_metadata(source).map_err(|error| crate::filesystem::Error::publication("inspect directory", source, error))?;
87    if !metadata.is_dir() || metadata.file_type().is_symlink() {
88        return Err(crate::filesystem::Error::publication(
89            "move directory",
90            source,
91            io::Error::other("source must be a regular directory"),
92        ));
93    }
94    platform::move_directory(source, destination)
95        .map_err(|error| crate::filesystem::Error::publication("move directory", destination, error))
96}
97
98pub fn install(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
99    sync_file(source).map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
100    platform::install(source, destination).map_err(|error| crate::filesystem::Error::publication("install file", destination, error))
101}
102
103/// Durably Installs The Staged File While Retaining Its Writable Handle.
104/// The Caller Owns The Source Path And Keeps It Bound To This File.
105pub fn install_opened(file: &File, source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
106    file.sync_all().map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
107    platform::install_opened(file, source, destination)
108        .map_err(|error| crate::filesystem::Error::publication("install file", destination, error))
109}
110
111pub fn replace(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
112    sync_file(source).map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
113    platform::replace(source, destination).map_err(|error| crate::filesystem::Error::publication("replace file", destination, error))
114}
115
116pub fn remove(path: &Path) -> crate::filesystem::Result<()> {
117    match fs::symlink_metadata(parent(path)) {
118        Ok(metadata) if !metadata.is_dir() || metadata.file_type().is_symlink() => {
119            return Err(crate::filesystem::Error::publication(
120                "remove file",
121                path,
122                io::Error::new(io::ErrorKind::InvalidInput, "parent must be a regular directory"),
123            ));
124        }
125        Ok(_) => (),
126        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
127        Err(error) => {
128            return Err(crate::filesystem::Error::publication("inspect parent", path, error));
129        }
130    }
131    match fs::symlink_metadata(path) {
132        Ok(metadata) if !metadata.is_file() || metadata.file_type().is_symlink() => {
133            return Err(crate::filesystem::Error::publication(
134                "remove file",
135                path,
136                io::Error::new(io::ErrorKind::InvalidInput, "source must be a regular file"),
137            ));
138        }
139        Ok(_) => (),
140        Err(error) if error.kind() == io::ErrorKind::NotFound => (),
141        Err(error) => {
142            return Err(crate::filesystem::Error::publication("inspect file", path, error));
143        }
144    }
145    platform::remove(path).map_err(|error| crate::filesystem::Error::publication("remove file", path, error))
146}
147
148pub fn confirm(path: &Path) -> crate::filesystem::Result<()> {
149    match confirm_with_checkpoint(path, || Ok::<(), std::convert::Infallible>(())) {
150        Ok(()) => Ok(()),
151        Err(ConfirmError::Filesystem(error)) => Err(error),
152        Err(ConfirmError::Checkpoint(error)) => match error {},
153    }
154}
155
156pub fn confirm_with_checkpoint<E>(path: &Path, mut checkpoint: impl FnMut() -> Result<(), E>) -> Result<(), ConfirmError<E>> {
157    platform::confirm_with_checkpoint(path, &mut checkpoint)
158}
159
160#[tracing::instrument(level = "debug", skip_all)]
161pub fn write(path: &Path, bytes: &[u8]) -> crate::filesystem::Result<()> {
162    write_document(path, bytes).map_err(|error| crate::filesystem::Error::publication("write document", path, error))
163}
164
165/* === Private Functions === */
166
167fn confirm_error<E>(path: &Path, error: io::Error) -> ConfirmError<E> {
168    let error = crate::filesystem::Error::publication("confirm file durability", path, error);
169    ConfirmError::Filesystem(error)
170}
171
172fn write_document(path: &Path, bytes: &[u8]) -> io::Result<()> {
173    create_dir_all(parent(path))?;
174    let (staging, mut file) = Staging::file(parent(path))?;
175    file.write_all(bytes)?;
176    file.sync_all()?;
177    drop(file);
178    replace(&staging.0, path).map_err(Into::into)
179}
180
181fn sync_file(path: &Path) -> io::Result<()> {
182    // FlushFileBuffers requires GENERIC_WRITE. Read-only File::open is not a
183    // portable way to confirm file durability, even if no bytes are changed.
184    OpenOptions::new().read(true).write(true).open(path)?.sync_all()
185}
186
187fn parent(path: &Path) -> &Path {
188    path.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or(Path::new("."))
189}
190
191/* === Tests === */
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::io::{Read, Seek, SeekFrom};
197
198    #[test]
199    fn opened_install_preserves_identity_and_refuses_replacement() {
200        let root = tempfile::tempdir().unwrap();
201        let source = root.path().join("source");
202        let destination = root.path().join("destination");
203        let mut file = OpenOptions::new().create_new(true).read(true).write(true).open(&source).unwrap();
204        file.write_all(b"evidence").unwrap();
205        install_opened(&file, &source, &destination).unwrap();
206        file.seek(SeekFrom::Start(0)).unwrap();
207        let mut bytes = Vec::new();
208        file.read_to_end(&mut bytes).unwrap();
209        assert_eq!(bytes, b"evidence");
210        assert_eq!(fs::read(&destination).unwrap(), bytes);
211        let other = root.path().join("other");
212        let mut replacement = File::create(&other).unwrap();
213        replacement.write_all(b"different").unwrap();
214        assert!(install_opened(&replacement, &other, &destination).is_err());
215        assert_eq!(fs::read(&destination).unwrap(), b"evidence");
216        assert_eq!(fs::read(&other).unwrap(), b"different");
217    }
218}