use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
pub const TMP_SUFFIX: &str = ".tmp";
pub const PROGRESS_SUFFIX: &str = ".progress";
static TMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn io_err(path: &Path, op: &'static str, e: &io::Error) -> Error {
Error::Io(io::Error::new(e.kind(), format!("{op} failed on {}: {e}", path.display())))
}
fn io_msg(msg: String) -> Error {
Error::Io(io::Error::other(msg))
}
pub fn sync_dir(dir: &Path) -> Result<()> {
#[cfg(windows)]
{
let _ = dir; Ok(())
}
#[cfg(not(windows))]
{
let f = File::open(dir).map_err(|e| io_err(dir, "open dir", &e))?;
f.sync_all().map_err(|e| io_err(dir, "fsync dir", &e))?;
Ok(())
}
}
fn parent_of(path: &Path) -> PathBuf {
path.parent()
.filter(|p| !p.as_os_str().is_empty())
.map_or_else(|| PathBuf::from("."), Path::to_path_buf)
}
pub fn write_atomic(target: &Path, data: &[u8]) -> Result<()> {
let mut w = AtomicWriter::create(target)?;
w.write_all(data)?;
w.commit()
}
pub struct AtomicWriter {
target: PathBuf,
tmp: PathBuf,
file: Option<File>,
written: u64,
}
impl std::fmt::Debug for AtomicWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AtomicWriter")
.field("target", &self.target)
.field("written", &self.written)
.field("open", &self.file.is_some())
.finish()
}
}
impl AtomicWriter {
pub fn create(target: &Path) -> Result<Self> {
let dir = parent_of(target);
if !dir.exists() {
fs::create_dir_all(&dir).map_err(|e| io_err(&dir, "create_dir_all", &e))?;
}
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let seq = TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let tmp_name = format!(
"{}.{}.{:x}-{}{}",
target.file_name().and_then(|s| s.to_str()).unwrap_or("file"),
std::process::id(),
nanos,
seq,
TMP_SUFFIX
);
let tmp = dir.join(tmp_name);
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp)
.map_err(|e| io_err(&tmp, "create temp file", &e))?;
Ok(Self { target: target.to_path_buf(), tmp, file: Option::from(file), written: 0 })
}
pub fn write_all(&mut self, buf: &[u8]) -> Result<()> {
let f = self.file.as_mut().ok_or_else(|| {
io_msg("writer already committed or aborted".to_owned())
})?;
f.write_all(buf).map_err(|e| io_err(&self.tmp, "write", &e))?;
self.written = self.written.saturating_add(buf.len() as u64);
Ok(())
}
#[must_use]
pub const fn written(&self) -> u64 {
self.written
}
#[must_use]
pub fn target(&self) -> &Path {
&self.target
}
pub fn commit(mut self) -> Result<()> {
let Some(file) = self.file.take() else {
return Err(io_msg("writer already committed or aborted".to_owned()));
};
if let Err(e) = file.sync_all() {
let err = io_err(&self.tmp, "fsync", &e);
drop(file);
let _ = fs::remove_file(&self.tmp);
return Err(err);
}
drop(file);
let dir = parent_of(&self.target);
if let Err(e) = sync_dir(&dir) {
let _ = fs::remove_file(&self.tmp);
return Err(e);
}
if let Err(e) = fs::rename(&self.tmp, &self.target) {
let err = io_err(&self.tmp, "rename to target", &e);
let _ = fs::remove_file(&self.tmp);
return Err(err);
}
sync_dir(&dir)
}
pub fn abort(mut self) {
self.cleanup();
}
fn cleanup(&mut self) {
drop(self.file.take());
let _ = fs::remove_file(&self.tmp);
}
}
impl std::io::Write for AtomicWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let f = self.file.as_mut().ok_or_else(|| {
std::io::Error::other("writer already committed or aborted")
})?;
let n = f.write(buf)?;
self.written = self.written.saturating_add(n as u64);
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.as_mut().map_or(Ok(()), std::io::Write::flush)
}
}
impl std::io::Seek for AtomicWriter {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
let f = self.file.as_mut().ok_or_else(|| {
std::io::Error::other("writer already committed or aborted")
})?;
f.seek(pos)
}
}
impl Drop for AtomicWriter {
fn drop(&mut self) {
if self.file.is_some() {
self.cleanup();
}
}
}
#[derive(Debug)]
pub struct TempPlaintext {
path: PathBuf,
shred_on_drop: bool,
}
impl TempPlaintext {
pub fn create(dir: &Path, filename: &str, plaintext: &[u8]) -> Result<Self> {
fs::create_dir_all(dir).map_err(|e| io_err(dir, "create_dir_all", &e))?;
mark_dir_no_index(dir)?;
let path = dir.join(filename);
write_atomic(&path, plaintext)?;
restrict_permissions(&path)?;
Ok(Self { path, shred_on_drop: true })
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub const fn set_shred_on_drop(&mut self, shred: bool) {
self.shred_on_drop = shred;
}
pub fn shred(&self) -> Result<()> {
shred_file(&self.path, self.shred_on_drop)
}
}
impl Drop for TempPlaintext {
fn drop(&mut self) {
let _ = shred_file(&self.path, self.shred_on_drop);
}
}
fn shred_file(path: &Path, overwrite: bool) -> Result<()> {
if !path.exists() {
return Ok(());
}
if overwrite
&& let Ok(meta) = fs::metadata(path)
{
let len = meta.len();
if len > 0
&& let Ok(mut f) = OpenOptions::new().write(true).open(path)
{
const BUF: usize = 64 * 1024;
let mut buf = [0u8; BUF];
crate::util::fill_random(&mut buf);
let mut left = len;
while left > 0 {
let n = usize::try_from(left.min(BUF as u64)).unwrap_or(BUF);
if f.write_all(buf.get(..n).unwrap_or(&buf)).is_err() {
break;
}
left = left.saturating_sub(n as u64);
}
let _ = f.sync_all();
}
}
fs::remove_file(path).map_err(|e| io_err(path, "remove temp plaintext", &e))
}
pub fn mark_dir_no_index(dir: &Path) -> Result<()> {
if !dir.is_dir() {
return Ok(());
}
#[cfg(target_os = "macos")]
{
let marker = dir.join(".metadata_never_index");
if !marker.exists() {
fs::write(&marker, b"").map_err(|e| io_err(&marker, "write marker", &e))?;
}
}
#[cfg(windows)]
{
let ini = dir.join("desktop.ini");
if !ini.exists() {
let content = "[.ShellClassInfo]\r\nNoThumbnailCache=1\r\n";
fs::write(&ini, content).map_err(|e| io_err(&ini, "write desktop.ini", &e))?;
}
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let marker = dir.join(".nomedia");
if !marker.exists() {
fs::write(&marker, b"").map_err(|e| io_err(&marker, "write marker", &e))?;
}
}
Ok(())
}
pub fn restrict_permissions(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let perm = fs::Permissions::from_mode(0o600);
fs::set_permissions(path, perm).map_err(|e| io_err(path, "set_permissions", &e))?;
}
#[cfg(not(unix))]
{
let _ = path;
}
Ok(())
}
pub fn cleanup_stale(dir: &Path) -> Result<usize> {
if !dir.is_dir() {
return Ok(0);
}
let mut n: usize = 0;
let rd = fs::read_dir(dir).map_err(|e| io_err(dir, "read_dir", &e))?;
for entry in rd.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
if (name.ends_with(TMP_SUFFIX) || name.ends_with(PROGRESS_SUFFIX))
&& fs::remove_file(&path).is_ok()
{
n = n.saturating_add(1);
}
}
Ok(n)
}