Gepubliceerde bron
Download bron ZIP
CH-J Tools
Blader door mappen en bestanden voor een specifieke toepassing release.
CH-J / Server Manager
1704 bestanden
0.1.2-beta
Toegevoegd: · Gebouwd:
1
//! Atomic private file writes and bounded download-cache helpers2
//! (port of `storage/atomicFile.js` and `storage/downloadCache.js`).3
//!4
//! Improvement over 0.1.0: the temporary file is fsynced before the rename and the5
//! parent directory is fsynced afterwards on POSIX, so a crash cannot leave a6
//! truncated JSON file behind.8
use crate::core::error::{CoreError, CoreResult};9
use rand::RngCore;10
use serde::Serialize;11
use std::fs::{self, File, OpenOptions};12
use std::io::Write;13
use std::path::{Path, PathBuf};15
fn random_suffix() -> String {16
let mut bytes = [0u8; 6];17
rand::thread_rng().fill_bytes(&mut bytes);18
hex::encode(bytes)19
}21
#[cfg(unix)]22
fn open_private(path: &Path, mode: u32) -> std::io::Result<File> {23
use std::os::unix::fs::OpenOptionsExt;24
OpenOptions::new().write(true).create_new(true).mode(mode).open(path)25
}27
#[cfg(not(unix))]28
fn open_private(path: &Path, _mode: u32) -> std::io::Result<File> {29
OpenOptions::new().write(true).create_new(true).open(path)30
}32
pub fn ensure_private_dir(path: &Path) -> CoreResult<()> {33
fs::create_dir_all(path)?;34
#[cfg(unix)]35
{36
use std::os::unix::fs::PermissionsExt;37
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o700));38
}39
Ok(())40
}42
pub fn atomic_write_file(path: &Path, content: &[u8], mode: u32) -> CoreResult<()> {43
let parent = path.parent().ok_or_else(|| CoreError::new("EIO", "Target has no parent directory."))?;44
fs::create_dir_all(parent)?;45
let file_name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();46
let temp = parent.join(format!("{file_name}.tmp-{}", random_suffix()));47
let result = (|| -> CoreResult<()> {48
let mut file = open_private(&temp, mode)?;49
file.write_all(content)?;50
file.sync_all()?;51
drop(file);52
// `rename` replaces atomically on POSIX and uses MoveFileEx(REPLACE_EXISTING) on Windows.53
fs::rename(&temp, path)?;54
#[cfg(unix)]55
if let Ok(dir) = File::open(parent) {56
let _ = dir.sync_all();57
}58
Ok(())59
})();60
if result.is_err() {61
let _ = fs::remove_file(&temp);62
}63
result64
}66
pub fn atomic_write_json<T: Serialize + ?Sized>(path: &Path, value: &T) -> CoreResult<()> {67
let mut text = serde_json::to_string_pretty(value)?;68
text.push('\n');69
atomic_write_file(path, text.as_bytes(), 0o600)70
}72
pub fn read_json(path: &Path) -> CoreResult<serde_json::Value> {73
let text = fs::read_to_string(path)?;74
Ok(serde_json::from_str(&text)?)75
}77
fn resolved_cache_root(root: &Path) -> CoreResult<PathBuf> {78
if root.as_os_str().is_empty() {79
return Err(CoreError::plain("Download cache root is required."));80
}81
let resolved = std::path::absolute(root)?;82
if resolved.parent().is_none() {83
return Err(CoreError::plain("Refusing to use a filesystem root as a download cache."));84
}85
Ok(resolved)86
}88
pub fn clean_download_cache(root: &Path) -> CoreResult<(bool, PathBuf)> {89
let root = resolved_cache_root(root)?;90
let existed = root.exists();91
if existed {92
fs::remove_dir_all(&root)?;93
}94
ensure_private_dir(&root)?;95
Ok((existed, root))96
}98
/// True when `target` is strictly inside `root` (lexically and after symlink resolution).99
pub fn is_strictly_inside(root: &Path, target: &Path) -> bool {100
let (Ok(root), Ok(target)) = (std::path::absolute(root), std::path::absolute(target)) else { return false };101
if target == root || !target.starts_with(&root) {102
return false;103
}104
match (fs::canonicalize(&root), fs::canonicalize(&target)) {105
(Ok(real_root), Ok(real_target)) => real_target != real_root && real_target.starts_with(&real_root),106
(Ok(_), Err(_)) => true, // target does not exist yet; lexical check already passed107
_ => false,108
}109
}111
pub fn remove_cached_file(root: &Path, file: &Path) -> CoreResult<bool> {112
let root = resolved_cache_root(root)?;113
let target = std::path::absolute(file)?;114
if !is_strictly_inside(&root, &target) {115
return Err(CoreError::plain("Downloaded package path is outside its cache."));116
}117
let Ok(meta) = fs::symlink_metadata(&target) else { return Ok(false) };118
if !meta.is_file() {119
return Err(CoreError::plain("Downloaded package is not a regular file."));120
}121
fs::remove_file(&target)?;122
let mut current = target.parent().map(Path::to_path_buf);123
while let Some(dir) = current {124
if dir == root || fs::remove_dir(&dir).is_err() {125
break;126
}127
current = dir.parent().map(Path::to_path_buf);128
}129
Ok(true)130
}132
#[cfg(test)]133
mod tests {134
use super::*;136
#[test]137
fn atomic_write_replaces_and_is_private() {138
let dir = tempfile::tempdir().unwrap();139
let path = dir.path().join("a").join("config.json");140
atomic_write_json(&path, &serde_json::json!({"a": 1})).unwrap();141
atomic_write_json(&path, &serde_json::json!({"a": 2})).unwrap();142
assert_eq!(read_json(&path).unwrap()["a"], 2);143
#[cfg(unix)]144
{145
use std::os::unix::fs::PermissionsExt;146
assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);147
}148
let leftovers: Vec<_> = fs::read_dir(path.parent().unwrap()).unwrap().collect();149
assert_eq!(leftovers.len(), 1);150
}152
#[test]153
fn cache_removal_stays_inside_root() {154
let dir = tempfile::tempdir().unwrap();155
let root = dir.path().join("cache");156
clean_download_cache(&root).unwrap();157
let outside = dir.path().join("outside.bin");158
fs::write(&outside, b"x").unwrap();159
assert!(remove_cached_file(&root, &outside).is_err());160
let inside = root.join("x").join("pkg.bin");161
fs::create_dir_all(inside.parent().unwrap()).unwrap();162
fs::write(&inside, b"x").unwrap();163
assert!(remove_cached_file(&root, &inside).unwrap());164
assert!(!root.join("x").exists());165
assert!(root.exists());166
}167
}SHA-256
d48b91cdf92be7c5f35ff7959990a83072be3462c970fde1563c7fed8741cc16CH-J Proprietary Software License 1.14 · Bron wordt geleverd onder de CH-J Proprietary Software License 1.14. De beschikbaarheid verandert de licentievoorwaarden niet of verleent geen extra rechten.
Archief SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f