1use std::fs::{self, File, OpenOptions};
33use std::io::{self, Write};
34use std::path::{Path, PathBuf};
35
36use crate::error::{Error, Result};
37
38pub const TMP_SUFFIX: &str = ".tmp";
40
41pub const PROGRESS_SUFFIX: &str = ".progress";
43
44static TMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
46
47fn io_err(path: &Path, op: &'static str, e: &io::Error) -> Error {
54 Error::Io(io::Error::new(e.kind(), format!("{op} failed on {}: {e}", path.display())))
55}
56
57fn io_msg(msg: String) -> Error {
59 Error::Io(io::Error::other(msg))
60}
61
62pub fn sync_dir(dir: &Path) -> Result<()> {
74 #[cfg(windows)]
75 {
76 let _ = dir; Ok(())
78 }
79 #[cfg(not(windows))]
80 {
81 let f = File::open(dir).map_err(|e| io_err(dir, "open dir", &e))?;
82 f.sync_all().map_err(|e| io_err(dir, "fsync dir", &e))?;
83 Ok(())
84 }
85}
86
87fn parent_of(path: &Path) -> PathBuf {
89 path.parent()
90 .filter(|p| !p.as_os_str().is_empty())
91 .map_or_else(|| PathBuf::from("."), Path::to_path_buf)
92}
93
94pub fn write_atomic(target: &Path, data: &[u8]) -> Result<()> {
103 let mut w = AtomicWriter::create(target)?;
104 w.write_all(data)?;
105 w.commit()
106}
107
108pub struct AtomicWriter {
126 target: PathBuf,
128 tmp: PathBuf,
130 file: Option<File>,
132 written: u64,
134}
135
136impl std::fmt::Debug for AtomicWriter {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 f.debug_struct("AtomicWriter")
139 .field("target", &self.target)
140 .field("written", &self.written)
141 .field("open", &self.file.is_some())
142 .finish()
143 }
144}
145
146impl AtomicWriter {
147 pub fn create(target: &Path) -> Result<Self> {
163 let dir = parent_of(target);
164 if !dir.exists() {
165 fs::create_dir_all(&dir).map_err(|e| io_err(&dir, "create_dir_all", &e))?;
166 }
167
168 let nanos = std::time::SystemTime::now()
169 .duration_since(std::time::UNIX_EPOCH)
170 .map_or(0, |d| d.as_nanos());
171 let seq = TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
172 let tmp_name = format!(
173 "{}.{}.{:x}-{}{}",
174 target.file_name().and_then(|s| s.to_str()).unwrap_or("file"),
175 std::process::id(),
176 nanos,
177 seq,
178 TMP_SUFFIX
179 );
180 let tmp = dir.join(tmp_name);
181
182 let file = OpenOptions::new()
183 .write(true)
184 .create(true)
185 .truncate(true)
186 .open(&tmp)
187 .map_err(|e| io_err(&tmp, "create temp file", &e))?;
188
189 Ok(Self { target: target.to_path_buf(), tmp, file: Option::from(file), written: 0 })
190 }
191
192 pub fn write_all(&mut self, buf: &[u8]) -> Result<()> {
198 let f = self.file.as_mut().ok_or_else(|| {
199 io_msg("writer already committed or aborted".to_owned())
200 })?;
201 f.write_all(buf).map_err(|e| io_err(&self.tmp, "write", &e))?;
202 self.written = self.written.saturating_add(buf.len() as u64);
203 Ok(())
204 }
205
206 #[must_use]
208 pub const fn written(&self) -> u64 {
209 self.written
210 }
211
212 #[must_use]
214 pub fn target(&self) -> &Path {
215 &self.target
216 }
217
218 pub fn commit(mut self) -> Result<()> {
224 let Some(file) = self.file.take() else {
225 return Err(io_msg("writer already committed or aborted".to_owned()));
226 };
227
228 if let Err(e) = file.sync_all() {
230 let err = io_err(&self.tmp, "fsync", &e);
231 drop(file);
232 let _ = fs::remove_file(&self.tmp);
233 return Err(err);
234 }
235 drop(file); let dir = parent_of(&self.target);
238
239 if let Err(e) = sync_dir(&dir) {
241 let _ = fs::remove_file(&self.tmp);
242 return Err(e);
243 }
244
245 if let Err(e) = fs::rename(&self.tmp, &self.target) {
249 let err = io_err(&self.tmp, "rename to target", &e);
250 let _ = fs::remove_file(&self.tmp);
251 return Err(err);
252 }
253
254 sync_dir(&dir)
256 }
257
258 pub fn abort(mut self) {
260 self.cleanup();
261 }
262
263 fn cleanup(&mut self) {
265 drop(self.file.take());
266 let _ = fs::remove_file(&self.tmp);
267 }
268}
269
270impl std::io::Write for AtomicWriter {
272 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
273 let f = self.file.as_mut().ok_or_else(|| {
274 std::io::Error::other("writer already committed or aborted")
275 })?;
276 let n = f.write(buf)?;
277 self.written = self.written.saturating_add(n as u64);
278 Ok(n)
279 }
280
281 fn flush(&mut self) -> std::io::Result<()> {
282 self.file.as_mut().map_or(Ok(()), std::io::Write::flush)
283 }
284}
285
286impl std::io::Seek for AtomicWriter {
291 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
292 let f = self.file.as_mut().ok_or_else(|| {
293 std::io::Error::other("writer already committed or aborted")
294 })?;
295 f.seek(pos)
296 }
297}
298
299impl Drop for AtomicWriter {
300 fn drop(&mut self) {
302 if self.file.is_some() {
303 self.cleanup();
304 }
305 }
306}
307
308#[derive(Debug)]
331pub struct TempPlaintext {
332 path: PathBuf,
334 shred_on_drop: bool,
336}
337
338impl TempPlaintext {
339 pub fn create(dir: &Path, filename: &str, plaintext: &[u8]) -> Result<Self> {
350 fs::create_dir_all(dir).map_err(|e| io_err(dir, "create_dir_all", &e))?;
351 mark_dir_no_index(dir)?;
352
353 let path = dir.join(filename);
354 write_atomic(&path, plaintext)?;
356 restrict_permissions(&path)?;
357
358 Ok(Self { path, shred_on_drop: true })
359 }
360
361 #[must_use]
363 pub fn path(&self) -> &Path {
364 &self.path
365 }
366
367 pub const fn set_shred_on_drop(&mut self, shred: bool) {
369 self.shred_on_drop = shred;
370 }
371
372 pub fn shred(&self) -> Result<()> {
380 shred_file(&self.path, self.shred_on_drop)
381 }
382}
383
384impl Drop for TempPlaintext {
385 fn drop(&mut self) {
386 let _ = shred_file(&self.path, self.shred_on_drop);
388 }
389}
390
391fn shred_file(path: &Path, overwrite: bool) -> Result<()> {
393 if !path.exists() {
394 return Ok(());
395 }
396 if overwrite
397 && let Ok(meta) = fs::metadata(path)
398 {
399 let len = meta.len();
400 if len > 0
401 && let Ok(mut f) = OpenOptions::new().write(true).open(path)
402 {
403 const BUF: usize = 64 * 1024;
405 let mut buf = [0u8; BUF];
406 crate::util::fill_random(&mut buf);
407 let mut left = len;
408 while left > 0 {
409 let n = usize::try_from(left.min(BUF as u64)).unwrap_or(BUF);
410 if f.write_all(buf.get(..n).unwrap_or(&buf)).is_err() {
411 break;
412 }
413 left = left.saturating_sub(n as u64);
414 }
415 let _ = f.sync_all();
416 }
417 }
418 fs::remove_file(path).map_err(|e| io_err(path, "remove temp plaintext", &e))
419}
420
421pub fn mark_dir_no_index(dir: &Path) -> Result<()> {
435 if !dir.is_dir() {
436 return Ok(());
437 }
438
439 #[cfg(target_os = "macos")]
440 {
441 let marker = dir.join(".metadata_never_index");
442 if !marker.exists() {
443 fs::write(&marker, b"").map_err(|e| io_err(&marker, "write marker", &e))?;
444 }
445 }
446
447 #[cfg(windows)]
448 {
449 let ini = dir.join("desktop.ini");
450 if !ini.exists() {
451 let content = "[.ShellClassInfo]\r\nNoThumbnailCache=1\r\n";
453 fs::write(&ini, content).map_err(|e| io_err(&ini, "write desktop.ini", &e))?;
454 }
455 }
456
457 #[cfg(all(unix, not(target_os = "macos")))]
458 {
459 let marker = dir.join(".nomedia");
460 if !marker.exists() {
461 fs::write(&marker, b"").map_err(|e| io_err(&marker, "write marker", &e))?;
462 }
463 }
464
465 Ok(())
466}
467
468pub fn restrict_permissions(path: &Path) -> Result<()> {
477 #[cfg(unix)]
478 {
479 use std::os::unix::fs::PermissionsExt as _;
480 let perm = fs::Permissions::from_mode(0o600);
481 fs::set_permissions(path, perm).map_err(|e| io_err(path, "set_permissions", &e))?;
482 }
483 #[cfg(not(unix))]
484 {
485 let _ = path;
486 }
487 Ok(())
488}
489
490pub fn cleanup_stale(dir: &Path) -> Result<usize> {
499 if !dir.is_dir() {
500 return Ok(0);
501 }
502 let mut n: usize = 0;
503 let rd = fs::read_dir(dir).map_err(|e| io_err(dir, "read_dir", &e))?;
504 for entry in rd.flatten() {
505 let path = entry.path();
506 if !path.is_file() {
507 continue;
508 }
509 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
510 continue;
511 };
512 if (name.ends_with(TMP_SUFFIX) || name.ends_with(PROGRESS_SUFFIX))
513 && fs::remove_file(&path).is_ok()
514 {
515 n = n.saturating_add(1);
516 }
517 }
518 Ok(n)
519}