1use std::io;
13use std::path::Path;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct FileMode(pub u32);
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct DirMode(pub u32);
29
30pub async fn write_atomic_restricted(
40 path: impl AsRef<Path>,
41 contents: impl AsRef<[u8]>,
42 file: FileMode,
43 dir: DirMode,
44) -> io::Result<()> {
45 let path = path.as_ref().to_owned();
46 let contents = contents.as_ref().to_vec();
47
48 let parent = path
49 .parent()
50 .ok_or_else(|| {
51 io::Error::new(
52 io::ErrorKind::InvalidInput,
53 format!("{} has no parent directory", path.display()),
54 )
55 })?
56 .to_owned();
57
58 create_dir_with_mode(&parent, dir.0).await?;
59
60 let file_name = path
61 .file_name()
62 .ok_or_else(|| {
63 io::Error::new(
64 io::ErrorKind::InvalidInput,
65 format!("{} has no file name", path.display()),
66 )
67 })?
68 .to_os_string();
69 let mut tmp_name = file_name;
70 tmp_name.push(format!(".tmp.{}", std::process::id()));
71 let tmp_path = parent.join(&tmp_name);
72
73 write_file_with_mode(&tmp_path, &contents, file.0).await?;
74
75 let rename_result = atomic_rename_over(&tmp_path, &path).await;
76 if rename_result.is_err() {
77 let _ = tokio::fs::remove_file(&tmp_path).await;
78 }
79 rename_result
80}
81
82pub async fn atomic_rename_over(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
94 #[cfg(unix)]
95 {
96 tokio::fs::rename(from.as_ref(), to.as_ref()).await
97 }
98 #[cfg(windows)]
99 {
100 fn atomic_rename_over_impl(from: &Path, to: &Path) -> io::Result<()> {
101 use windows::Win32::Storage::FileSystem::{
102 MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW,
103 REPLACEFILE_IGNORE_MERGE_ERRORS, ReplaceFileW,
104 };
105 use windows::core::HSTRING;
106
107 let from_w = HSTRING::from(from.as_os_str());
108 let to_w = HSTRING::from(to.as_os_str());
109
110 if to.exists() {
111 let result = unsafe {
112 ReplaceFileW(
113 &to_w,
114 &from_w,
115 windows::core::PCWSTR::null(),
116 REPLACEFILE_IGNORE_MERGE_ERRORS,
117 None,
118 None,
119 )
120 };
121 return result.map_err(|e| io::Error::new(io::ErrorKind::Other, e));
122 }
123
124 let result = unsafe {
125 MoveFileExW(
126 &from_w,
127 &to_w,
128 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
129 )
130 };
131 result.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
132 }
133
134 let from = from.as_ref().to_owned();
135 let to = to.as_ref().to_owned();
136 tokio::task::spawn_blocking(move || atomic_rename_over_impl(&from, &to))
137 .await
138 .map_err(io::Error::other)?
139 }
140}
141
142pub async fn remove_file_if_exists(path: impl AsRef<Path>) -> io::Result<()> {
149 match tokio::fs::remove_file(path).await {
150 Ok(()) => Ok(()),
151 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
152 Err(e) => Err(e),
153 }
154}
155
156#[cfg(unix)]
157async fn create_dir_with_mode(dir: &Path, mode: u32) -> io::Result<()> {
158 #[cfg(unix)]
159 {
160 use std::os::unix::fs::PermissionsExt;
161 match tokio::fs::metadata(dir).await {
162 Ok(meta) => {
163 let current = meta.permissions().mode() & 0o777;
164 if current != mode {
165 tokio::fs::set_permissions(dir, std::fs::Permissions::from_mode(mode)).await?;
166 }
167 }
168 Err(e) if e.kind() == io::ErrorKind::NotFound => {
169 let mut builder = tokio::fs::DirBuilder::new();
170 builder.recursive(true).mode(mode);
171 builder.create(dir).await?;
172 }
173 Err(e) => return Err(e),
174 }
175 }
176
177 #[cfg(windows)]
178 {
179 tokio::fs::create_dir_all(dir).await
180 }
181 Ok(())
182}
183
184async fn write_file_with_mode(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> {
185 #[cfg(unix)]
186 {
187 use tokio::io::AsyncWriteExt;
188 let mut opts = tokio::fs::OpenOptions::new();
189 opts.write(true).create(true).truncate(true).mode(mode);
190 let mut f = opts.open(path).await?;
191 f.write_all(contents).await?;
192 f.sync_all().await?;
193 }
194
195 #[cfg(windows)]
196 {
197 tokio::fs::write(path, contents).await
198 }
199
200 Ok(())
201}
202
203pub mod blocking {
207 use super::{DirMode, FileMode};
208 use std::io;
209 use std::path::Path;
210
211 fn block_on<F: std::future::Future<Output = io::Result<()>>>(f: F) -> io::Result<()> {
212 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f))
213 }
214
215 pub fn write_atomic_restricted(
226 path: impl AsRef<Path>,
227 contents: impl AsRef<[u8]>,
228 file: FileMode,
229 dir: DirMode,
230 ) -> io::Result<()> {
231 block_on(super::write_atomic_restricted(path, contents, file, dir))
232 }
233
234 pub fn remove_if_exists(path: impl AsRef<Path>) -> io::Result<()> {
241 block_on(super::remove_file_if_exists(path))
242 }
243}
244
245#[cfg(all(test, unix))]
246#[allow(clippy::unwrap_used)]
247mod tests {
248 use super::*;
249 use std::os::unix::fs::PermissionsExt;
250
251 #[tokio::test]
253 async fn writes_file_0600_in_dir_0700() {
254 let tmp = tempfile::tempdir().unwrap();
255 let dir = tmp.path().join("hm");
256 let file = dir.join("credentials.toml");
257
258 write_atomic_restricted(
259 &file,
260 b"token = \"hunter2\"\n",
261 FileMode(0o600),
262 DirMode(0o700),
263 )
264 .await
265 .unwrap();
266
267 let fmode = std::fs::metadata(&file).unwrap().permissions().mode() & 0o777;
268 assert_eq!(fmode, 0o600, "file mode must be 0o600, got {fmode:o}");
269 let dmode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
270 assert_eq!(dmode, 0o700, "dir mode must be 0o700, got {dmode:o}");
271 }
272
273 #[tokio::test]
276 async fn rewrite_preserves_0600() {
277 let tmp = tempfile::tempdir().unwrap();
278 let file = tmp.path().join("credentials.toml");
279 write_atomic_restricted(&file, b"a", FileMode(0o600), DirMode(0o700))
280 .await
281 .unwrap();
282 write_atomic_restricted(&file, b"bb", FileMode(0o600), DirMode(0o700))
283 .await
284 .unwrap();
285 let fmode = std::fs::metadata(&file).unwrap().permissions().mode() & 0o777;
286 assert_eq!(fmode, 0o600, "file mode must stay 0o600, got {fmode:o}");
287 }
288}