1use std::fs;
16use std::io::{Read, Write};
17use std::path::Path;
18use std::process;
19use tracing::warn;
20
21pub const MAX_FILE_SIZE: u64 = 10_000_000;
24
25pub fn atomic_write(path: &str, content: &str) -> Result<(), String> {
29 let tmp = format!("{}.tmp.{}", path, process::id());
30 let result = (|| -> Result<(), String> {
32 let mut f = fs::File::create(&tmp).map_err(|e| format!("atomic_write: create {}: {}", tmp, e))?;
33 f.write_all(content.as_bytes()).map_err(|e| format!("atomic_write: write {}: {}", tmp, e))?;
34 f.sync_all().map_err(|e| format!("atomic_write: sync {}: {}", tmp, e))?;
36 drop(f);
37 fs::rename(&tmp, path).map_err(|e| format!("atomic_write: rename {} -> {}: {}", tmp, path, e))?;
38 Ok(())
39 })();
40 if result.is_err() {
41 let _ = fs::remove_file(&tmp);
42 }
43 result
44}
45
46pub fn safe_read(path: &str) -> Result<String, String> {
49 let p = Path::new(path);
50 match p.metadata() {
51 Ok(meta) => {
52 if meta.len() > MAX_FILE_SIZE {
53 return Err(format!(
54 "safe_read: {} is {} bytes, exceeds max {} bytes",
55 path, meta.len(), MAX_FILE_SIZE
56 ));
57 }
58 }
59 Err(e) => {
60 if e.kind() == std::io::ErrorKind::NotFound {
61 return Err(format!("safe_read: {} does not exist", path));
62 }
63 return Err(format!("safe_read: {}: {}", path, e));
64 }
65 }
66 fs::read_to_string(path).map_err(|e| format!("safe_read: {}: {}", path, e))
67}
68
69pub fn safe_read_stdin() -> Result<String, String> {
72 let mut buf = Vec::with_capacity(4096);
73 let mut handle = std::io::stdin().take(MAX_FILE_SIZE + 1);
74 if let Err(e) = handle.read_to_end(&mut buf) {
75 return Err(format!("safe_read_stdin: read failed: {}", e));
76 }
77 if buf.len() as u64 > MAX_FILE_SIZE {
78 return Err(format!(
79 "safe_read_stdin: input exceeds {} bytes",
80 MAX_FILE_SIZE
81 ));
82 }
83 String::from_utf8(buf).map_err(|e| format!("safe_read_stdin: invalid UTF-8: {}", e))
84}
85
86pub fn safe_open_db(path: &str) -> Result<rusqlite::Connection, String> {
89 let db = rusqlite::Connection::open(path)
90 .map_err(|e| format!("safe_open_db: open {}: {}", path, e))?;
91 if let Err(e) = db.execute_batch(
92 "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
93 ) {
94 warn!("safe_open_db: PRAGMA set failed: {}", e);
95 }
96 Ok(db)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 #[allow(unused_imports)]
105 fn test_atomic_write_creates_file() {
106 let dir = std::env::temp_dir().join("reliary_fs_safe_atomic");
107 let _ = fs::create_dir_all(&dir);
108 let path = dir.join("test.txt");
109 atomic_write(path.to_str().unwrap(), "hello").unwrap();
110 let content = fs::read_to_string(&path).unwrap();
111 assert_eq!(content, "hello");
112 let _ = fs::remove_dir_all(&dir);
113 }
114
115 #[test]
116 fn test_atomic_write_cleans_tmp_on_rename_failure() {
117 let bad_path = "/nonexistent/directory/test.txt";
119 let result = atomic_write(bad_path, "content");
120 assert!(result.is_err(), "should fail when parent dir doesn't exist");
121 }
123
124 #[test]
125 fn test_safe_read_size_cap() {
126 let dir = std::env::temp_dir().join("reliary_fs_safe_read");
127 let _ = fs::create_dir_all(&dir);
128 let path = dir.join("small.txt");
129 fs::write(&path, "small content").unwrap();
130 let result = safe_read(path.to_str().unwrap());
131 assert!(result.is_ok());
132 assert_eq!(result.unwrap(), "small content");
133 let _ = fs::remove_dir_all(&dir);
134 }
135
136 #[test]
137 fn test_safe_read_missing_file() {
138 let result = safe_read("/nonexistent/path/file.txt");
139 assert!(result.is_err());
140 }
141}