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.is_file() {
57 return Err(format!("safe_read: {} is not a regular file", path));
58 }
59 if meta.len() > MAX_FILE_SIZE {
60 return Err(format!(
61 "safe_read: {} is {} bytes, exceeds max {} bytes",
62 path, meta.len(), MAX_FILE_SIZE
63 ));
64 }
65 }
66 Err(e) => {
67 if e.kind() == std::io::ErrorKind::NotFound {
68 return Err(format!("safe_read: {} does not exist", path));
69 }
70 return Err(format!("safe_read: {}: {}", path, e));
71 }
72 }
73 fs::read_to_string(path).map_err(|e| format!("safe_read: {}: {}", path, e))
74}
75
76pub fn safe_read_stdin() -> Result<String, String> {
79 let mut buf = Vec::with_capacity(4096);
80 let mut handle = std::io::stdin().take(MAX_FILE_SIZE + 1);
81 if let Err(e) = handle.read_to_end(&mut buf) {
82 return Err(format!("safe_read_stdin: read failed: {}", e));
83 }
84 if buf.len() as u64 > MAX_FILE_SIZE {
85 return Err(format!(
86 "safe_read_stdin: input exceeds {} bytes",
87 MAX_FILE_SIZE
88 ));
89 }
90 String::from_utf8(buf).map_err(|e| format!("safe_read_stdin: invalid UTF-8: {}", e))
91}
92
93pub fn safe_open_db(path: &str) -> Result<rusqlite::Connection, String> {
96 let db = rusqlite::Connection::open(path)
97 .map_err(|e| format!("safe_open_db: open {}: {}", path, e))?;
98 if let Err(e) = db.execute_batch(
99 "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
100 ) {
101 warn!("safe_open_db: PRAGMA set failed: {}", e);
102 }
103 Ok(db)
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 #[allow(unused_imports)]
112 fn test_atomic_write_creates_file() {
113 let dir = std::env::temp_dir().join("reliary_fs_safe_atomic");
114 let _ = fs::create_dir_all(&dir);
115 let path = dir.join("test.txt");
116 atomic_write(path.to_str().unwrap(), "hello").unwrap();
117 let content = fs::read_to_string(&path).unwrap();
118 assert_eq!(content, "hello");
119 let _ = fs::remove_dir_all(&dir);
120 }
121
122 #[test]
123 fn test_atomic_write_cleans_tmp_on_rename_failure() {
124 let bad_path = "/nonexistent/directory/test.txt";
126 let result = atomic_write(bad_path, "content");
127 assert!(result.is_err(), "should fail when parent dir doesn't exist");
128 }
130
131 #[test]
132 fn test_safe_read_size_cap() {
133 let dir = std::env::temp_dir().join("reliary_fs_safe_read");
134 let _ = fs::create_dir_all(&dir);
135 let path = dir.join("small.txt");
136 fs::write(&path, "small content").unwrap();
137 let result = safe_read(path.to_str().unwrap());
138 assert!(result.is_ok());
139 assert_eq!(result.unwrap(), "small content");
140 let _ = fs::remove_dir_all(&dir);
141 }
142
143 #[test]
144 fn test_safe_read_missing_file() {
145 let result = safe_read("/nonexistent/path/file.txt");
146 assert!(result.is_err());
147 }
148}