use std::fs;
use std::io::{Read, Write};
use std::path::Path;
use std::process;
use tracing::warn;
pub const MAX_FILE_SIZE: u64 = 10_000_000;
pub fn atomic_write(path: &str, content: &str) -> Result<(), String> {
let tmp = format!("{}.tmp.{}", path, process::id());
let result = (|| -> Result<(), String> {
let mut f = fs::File::create(&tmp).map_err(|e| format!("atomic_write: create {}: {}", tmp, e))?;
f.write_all(content.as_bytes()).map_err(|e| format!("atomic_write: write {}: {}", tmp, e))?;
f.sync_all().map_err(|e| format!("atomic_write: sync {}: {}", tmp, e))?;
drop(f);
fs::rename(&tmp, path).map_err(|e| format!("atomic_write: rename {} -> {}: {}", tmp, path, e))?;
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&tmp);
}
result
}
pub fn safe_read(path: &str) -> Result<String, String> {
let p = Path::new(path);
match p.metadata() {
Ok(meta) => {
if meta.len() > MAX_FILE_SIZE {
return Err(format!(
"safe_read: {} is {} bytes, exceeds max {} bytes",
path, meta.len(), MAX_FILE_SIZE
));
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Err(format!("safe_read: {} does not exist", path));
}
return Err(format!("safe_read: {}: {}", path, e));
}
}
fs::read_to_string(path).map_err(|e| format!("safe_read: {}: {}", path, e))
}
pub fn safe_read_stdin() -> Result<String, String> {
let mut buf = Vec::with_capacity(4096);
let mut handle = std::io::stdin().take(MAX_FILE_SIZE + 1);
if let Err(e) = handle.read_to_end(&mut buf) {
return Err(format!("safe_read_stdin: read failed: {}", e));
}
if buf.len() as u64 > MAX_FILE_SIZE {
return Err(format!(
"safe_read_stdin: input exceeds {} bytes",
MAX_FILE_SIZE
));
}
String::from_utf8(buf).map_err(|e| format!("safe_read_stdin: invalid UTF-8: {}", e))
}
pub fn safe_open_db(path: &str) -> Result<rusqlite::Connection, String> {
let db = rusqlite::Connection::open(path)
.map_err(|e| format!("safe_open_db: open {}: {}", path, e))?;
if let Err(e) = db.execute_batch(
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
) {
warn!("safe_open_db: PRAGMA set failed: {}", e);
}
Ok(db)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(unused_imports)]
fn test_atomic_write_creates_file() {
let dir = std::env::temp_dir().join("reliary_fs_safe_atomic");
let _ = fs::create_dir_all(&dir);
let path = dir.join("test.txt");
atomic_write(path.to_str().unwrap(), "hello").unwrap();
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, "hello");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_atomic_write_cleans_tmp_on_rename_failure() {
let bad_path = "/nonexistent/directory/test.txt";
let result = atomic_write(bad_path, "content");
assert!(result.is_err(), "should fail when parent dir doesn't exist");
}
#[test]
fn test_safe_read_size_cap() {
let dir = std::env::temp_dir().join("reliary_fs_safe_read");
let _ = fs::create_dir_all(&dir);
let path = dir.join("small.txt");
fs::write(&path, "small content").unwrap();
let result = safe_read(path.to_str().unwrap());
assert!(result.is_ok());
assert_eq!(result.unwrap(), "small content");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_safe_read_missing_file() {
let result = safe_read("/nonexistent/path/file.txt");
assert!(result.is_err());
}
}