- 1
//! Filesystem writes that either land completely or not at all. - 2
//! - 3
//! Every install and update writes through here. A partial write to an - 4
//! executable or a manifest leaves an install that reports itself healthy - 5
//! while being unrunnable, which is the failure mode hardest to diagnose - 6
//! after the fact. - 7
- 8
use std::path::Path; - 9
- 10
/// Write bytes to `path` via a temporary file in the same directory, then - 11
/// rename. The rename is atomic within a filesystem, so a reader sees - 12
/// either the old contents or the new ones. - 13
pub fn write(path: &Path, bytes: &[u8]) -> Result<(), String> { - 14
let parent = path - 15
.parent() - 16
.ok_or_else(|| format!("{} has no parent directory", path.display()))?; - 17
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?; - 18
// Unique per process so two concurrent writers cannot share a temp - 19
// file and interleave their bytes. - 20
let tmp = parent.join(format!( - 21
".{}.{}.tmp", - 22
path.file_name() - 23
.and_then(|n| n.to_str()) - 24
.unwrap_or("vak-write"), - 25
std::process::id() - 26
)); - 27
let result = (|| { - 28
std::fs::write(&tmp, bytes).map_err(|e| format!("write {}: {e}", tmp.display()))?; - 29
std::fs::rename(&tmp, path).map_err(|e| format!("rename into {}: {e}", path.display())) - 30
})(); - 31
if result.is_err() { - 32
let _ = std::fs::remove_file(&tmp); - 33
} - 34
result - 35
} - 36
- 37
/// Write bytes and mark the result executable. - 38
pub fn write_executable(path: &Path, bytes: &[u8]) -> Result<(), String> { - 39
write(path, bytes)?; - 40
set_executable(path) - 41
} - 42
- 43
pub fn set_executable(path: &Path) -> Result<(), String> { - 44
#[cfg(unix)] - 45
{ - 46
use std::os::unix::fs::PermissionsExt as _; - 47
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)) - 48
.map_err(|e| format!("chmod {}: {e}", path.display()))?; - 49
} - 50
#[cfg(not(unix))] - 51
let _ = path; - 52
Ok(()) - 53
} - 54
- 55
/// Recursively copy a directory tree, each file written atomically. - 56
pub fn copy_dir(src: &Path, dst: &Path) -> Result<(), String> { - 57
std::fs::create_dir_all(dst).map_err(|e| format!("mkdir {}: {e}", dst.display()))?; - 58
for entry in std::fs::read_dir(src).map_err(|e| format!("read dir {}: {e}", src.display()))? { - 59
let entry = entry.map_err(|e| format!("dir entry under {}: {e}", src.display()))?; - 60
let ty = entry - 61
.file_type() - 62
.map_err(|e| format!("file type {}: {e}", entry.path().display()))?; - 63
let target = dst.join(entry.file_name()); - 64
if ty.is_dir() { - 65
copy_dir(&entry.path(), &target)?; - 66
} else { - 67
let bytes = std::fs::read(entry.path()) - 68
.map_err(|e| format!("read {}: {e}", entry.path().display()))?; - 69
write(&target, &bytes)?; - 70
} - 71
} - 72
Ok(()) - 73
} - 74
- 75
#[cfg(test)] - 76
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 77
mod tests { - 78
use super::*; - 79
- 80
#[test] - 81
fn write_replaces_content_and_leaves_no_temp_behind() { - 82
let dir = tempfile::tempdir().unwrap(); - 83
let f = dir.path().join("f.json"); - 84
write(&f, b"one").unwrap(); - 85
write(&f, b"two").unwrap(); - 86
assert_eq!(std::fs::read_to_string(&f).unwrap(), "two"); - 87
let leftovers: Vec<_> = std::fs::read_dir(dir.path()) - 88
.unwrap() - 89
.filter_map(Result::ok) - 90
.filter(|e| e.file_name().to_string_lossy().ends_with(".tmp")) - 91
.collect(); - 92
assert!(leftovers.is_empty(), "temp files must not survive a write"); - 93
} - 94
- 95
#[test] - 96
fn executable_bit_is_set_on_a_written_binary() { - 97
let dir = tempfile::tempdir().unwrap(); - 98
let dst = dir.path().join("nested").join("dst"); - 99
write_executable(&dst, b"#!/bin/sh\n").unwrap(); - 100
#[cfg(unix)] - 101
{ - 102
use std::os::unix::fs::PermissionsExt as _; - 103
let mode = std::fs::metadata(&dst).unwrap().permissions().mode(); - 104
assert_eq!(mode & 0o111, 0o111); - 105
} - 106
} - 107
- 108
#[test] - 109
fn copy_dir_reproduces_a_nested_tree() { - 110
let dir = tempfile::tempdir().unwrap(); - 111
let src = dir.path().join("src"); - 112
std::fs::create_dir_all(src.join("a/b")).unwrap(); - 113
std::fs::write(src.join("top.txt"), b"top").unwrap(); - 114
std::fs::write(src.join("a/b/deep.txt"), b"deep").unwrap(); - 115
let dst = dir.path().join("dst"); - 116
copy_dir(&src, &dst).unwrap(); - 117
assert_eq!(std::fs::read_to_string(dst.join("top.txt")).unwrap(), "top"); - 118
assert_eq!( - 119
std::fs::read_to_string(dst.join("a/b/deep.txt")).unwrap(), - 120
"deep" - 121
); - 122
} - 123
} - 124
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.