#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use crate::unified_api::error::{CoreError, Result};
use std::io::Write;
use std::path::Path;
pub struct AtomicWrite<'a> {
bytes: &'a [u8],
unix_mode: Option<u32>,
harden_windows_acl: bool,
overwrite_existing: bool,
}
impl<'a> AtomicWrite<'a> {
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
Self { bytes, unix_mode: None, harden_windows_acl: false, overwrite_existing: false }
}
pub fn write_secret(bytes: &[u8], path: &Path) -> Result<()> {
AtomicWrite::new(bytes).secret_mode().write(path)
}
pub fn write_overwrite(bytes: &[u8], path: &Path) -> Result<()> {
AtomicWrite::new(bytes).overwrite_existing(true).write(path)
}
#[must_use]
pub fn secret_mode(mut self) -> Self {
self.unix_mode = Some(0o600);
self.harden_windows_acl = true;
self
}
#[must_use]
pub fn unix_mode(mut self, mode: u32) -> Self {
self.unix_mode = Some(mode);
self
}
#[must_use]
pub fn overwrite_existing(mut self, allow: bool) -> Self {
self.overwrite_existing = allow;
self
}
pub fn write(self, path: &Path) -> Result<()> {
let parent =
path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or_else(|| Path::new("."));
let mut tmp = tempfile::NamedTempFile::new_in(parent).map_err(|e| {
CoreError::ConfigurationError(format!(
"failed to create tempfile in {}: {e}",
parent.display()
))
})?;
#[cfg(unix)]
if let Some(mode) = self.unix_mode {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(mode);
tmp.as_file()
.set_permissions(perms)
.map_err(|e| CoreError::Internal(format!("chmod tempfile: {e}")))?;
}
tmp.write_all(self.bytes)
.map_err(|e| CoreError::Internal(format!("write tempfile: {e}")))?;
tmp.as_file()
.sync_all()
.map_err(|e| CoreError::Internal(format!("fsync tempfile: {e}")))?;
if self.harden_windows_acl
&& let Err(e) = crate::unified_api::set_local_admin_dacl(tmp.path())
{
use std::io::{Seek, SeekFrom, Write};
const ZERO_CHUNK: [u8; 4096] = [0u8; 4096];
let f = tmp.as_file_mut();
if let Err(seek_err) = f.seek(SeekFrom::Start(0)) {
return Err(CoreError::Internal(format!(
"windows DACL hardening on tempfile in {} failed: {e}; \
plaintext-scrub aborted because seek-to-start failed: {seek_err}",
parent.display()
)));
}
let mut remaining = self.bytes.len();
while remaining > 0 {
let n = remaining.min(ZERO_CHUNK.len());
#[expect(
clippy::indexing_slicing,
reason = "indexing into a slice whose length is known at this site"
)]
let chunk = &ZERO_CHUNK[..n];
if f.write_all(chunk).is_err() {
break;
}
remaining = remaining.saturating_sub(n);
}
let _ = f.sync_all();
let _ = f.set_len(0);
return Err(CoreError::Internal(format!(
"windows DACL hardening on tempfile in {} failed: {e}",
parent.display()
)));
}
if self.overwrite_existing {
tmp.persist(path).map_err(|e| {
CoreError::Internal(format!("atomic rename to {}: {e}", path.display()))
})?;
} else {
tmp.persist_noclobber(path).map_err(|e| {
if e.error.kind() == std::io::ErrorKind::AlreadyExists {
CoreError::ConfigurationError(format!(
"Refusing to overwrite existing file: {}. \
Pass --force (or pre-delete the file) if this is intentional.",
path.display()
))
} else {
CoreError::Internal(format!("atomic rename to {}: {e}", path.display()))
}
})?;
}
#[cfg(unix)]
{
let dir = std::fs::File::open(parent).map_err(|e| {
CoreError::Internal(format!(
"atomic_write parent fsync: open({}) failed: {e}",
parent.display()
))
})?;
if let Err(e) = dir.sync_all() {
let acceptable = matches!(
e.kind(),
std::io::ErrorKind::Unsupported | std::io::ErrorKind::InvalidInput
);
if !acceptable {
return Err(CoreError::Internal(format!(
"atomic_write parent fsync({}) failed: {e}",
parent.display()
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test scaffolding: source-shape regression assertions")]
mod regression_tests {
const SRC: &str = include_str!("atomic_write.rs");
#[test]
fn test_dacl_failure_scrub_seeks_before_writing_zeros() {
let block_anchor = "Best-effort secret-bytes scrub before `tmp` drops and";
let block_start = SRC.find(block_anchor).expect(
"scrub-block anchor comment moved or removed — update this test \
to anchor on the new comment OR review the change for the \
ordering invariant before adjusting the anchor.",
);
let block = &SRC[block_start..];
let seek_pos = block
.find("f.seek(SeekFrom::Start(0))")
.expect("seek-to-start call missing from scrub block");
let write_pos =
block.find("f.write_all(chunk)").expect("zero-write loop missing from scrub block");
assert!(
seek_pos < write_pos,
"DACL-scrub ordering regressed: seek(SeekFrom::Start(0)) MUST appear \
before write_all(chunk) so plaintext is overwritten, not appended. \
seek_pos={seek_pos}, write_pos={write_pos}"
);
}
}