app_json_settings/core/
save.rs1use std::fs::{self, File, OpenOptions};
2use std::io::{self, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::Result;
8
9#[cfg(unix)]
10use std::os::unix::fs::OpenOptionsExt;
11#[cfg(windows)]
12use std::os::windows::ffi::OsStrExt;
13
14static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
15
16#[cfg(windows)]
17#[allow(non_snake_case)]
18#[link(name = "kernel32")]
19unsafe extern "system" {
20 fn MoveFileExW(existing_file_name: *const u16, new_file_name: *const u16, flags: u32) -> i32;
21}
22
23#[derive(Debug, Clone, Copy, Eq, PartialEq)]
25pub enum SaveMode {
26 Atomic,
31
32 Direct,
38}
39
40pub fn save_to_path(path: &Path, content: &str, mode: SaveMode) -> Result<()> {
41 if let Some(parent) = non_empty_parent(path) {
42 fs::create_dir_all(parent)?;
43 }
44
45 match mode {
46 SaveMode::Atomic => save_atomic(path, content),
47 SaveMode::Direct => save_direct(path, content),
48 }
49}
50
51fn save_direct(path: &Path, content: &str) -> Result<()> {
52 fs::write(path, content)?;
53 Ok(())
54}
55
56fn save_atomic(path: &Path, content: &str) -> Result<()> {
57 let (temp_path, mut temp_file) = create_temp_file(path)?;
58
59 let result = (|| -> io::Result<()> {
60 temp_file.write_all(content.as_bytes())?;
61 temp_file.sync_all()?;
62 drop(temp_file);
63
64 #[cfg(unix)]
65 apply_target_mode(&temp_path, path);
66
67 replace_file(&temp_path, path)?;
68 sync_parent_dir(path);
69 Ok(())
70 })();
71
72 if result.is_err() {
73 let _ = fs::remove_file(&temp_path);
74 }
75
76 result?;
77 Ok(())
78}
79
80#[cfg(unix)]
89fn apply_target_mode(temp_path: &Path, target_path: &Path) {
90 if let Ok(metadata) = fs::metadata(target_path) {
91 let _ = fs::set_permissions(temp_path, metadata.permissions());
92 }
93}
94
95fn create_temp_file(target: &Path) -> io::Result<(PathBuf, File)> {
96 let parent = non_empty_parent(target).unwrap_or_else(|| Path::new("."));
97 let target_name = target
98 .file_name()
99 .map(|name| name.to_string_lossy())
100 .unwrap_or_else(|| "settings.json".into());
101
102 for attempt in 0..1000_u16 {
103 let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
104 let nanos = SystemTime::now()
105 .duration_since(UNIX_EPOCH)
106 .map(|duration| duration.as_nanos())
107 .unwrap_or(0);
108 let temp_name = format!(
109 ".{target_name}.tmp.{}.{}.{}.{}",
110 std::process::id(),
111 nanos,
112 counter,
113 attempt
114 );
115 let temp_path = parent.join(temp_name);
116
117 let mut open_options = OpenOptions::new();
118 open_options.write(true).create_new(true);
119 #[cfg(unix)]
124 open_options.mode(0o600);
125
126 match open_options.open(&temp_path) {
127 Ok(file) => return Ok((temp_path, file)),
128 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
129 Err(error) => return Err(error),
130 }
131 }
132
133 Err(io::Error::new(
134 io::ErrorKind::AlreadyExists,
135 "could not create a unique temporary settings file",
136 ))
137}
138
139#[cfg(unix)]
140fn replace_file(temp_path: &Path, target_path: &Path) -> io::Result<()> {
141 fs::rename(temp_path, target_path)
142}
143
144#[cfg(windows)]
145fn replace_file(temp_path: &Path, target_path: &Path) -> io::Result<()> {
146 const MOVEFILE_REPLACE_EXISTING: u32 = 0x0000_0001;
147 const MOVEFILE_WRITE_THROUGH: u32 = 0x0000_0008;
148
149 let old_path = wide_null_terminated(temp_path);
150 let new_path = wide_null_terminated(target_path);
151
152 let ok = unsafe {
156 MoveFileExW(
157 old_path.as_ptr(),
158 new_path.as_ptr(),
159 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
160 )
161 };
162
163 if ok == 0 {
164 Err(io::Error::last_os_error())
165 } else {
166 Ok(())
167 }
168}
169
170#[cfg(windows)]
171fn wide_null_terminated(path: &Path) -> Vec<u16> {
172 path.as_os_str().encode_wide().chain([0]).collect()
173}
174
175#[cfg(not(any(unix, windows)))]
176fn replace_file(temp_path: &Path, target_path: &Path) -> io::Result<()> {
177 if target_path.exists() {
178 return Err(io::Error::new(
179 io::ErrorKind::Unsupported,
180 "atomic replacement is not implemented for this target; use SaveMode::Direct",
181 ));
182 }
183
184 fs::rename(temp_path, target_path)
185}
186
187fn non_empty_parent(path: &Path) -> Option<&Path> {
188 path.parent()
189 .filter(|parent| !parent.as_os_str().is_empty())
190}
191
192fn sync_parent_dir(path: &Path) {
193 if let Some(parent) = non_empty_parent(path) {
194 if let Ok(dir) = File::open(parent) {
195 let _ = dir.sync_all();
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests;