#[cfg(unix)]
use std::{
fs::{self, File, OpenOptions},
io::Write as _,
os::unix::fs::OpenOptionsExt as _,
sync::atomic::{AtomicU64, Ordering},
};
use std::{
io,
path::{Path, PathBuf},
};
#[cfg(all(test, unix))]
mod tests;
#[cfg(unix)]
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, thiserror::Error)]
pub enum PrivateBytesError {
#[error("private scratch creation failed")]
BeforeCreation(#[source] io::Error),
#[error("private scratch write{} failed", if .cleanup.is_some() { " and cleanup" } else { "" })]
Write {
path: PathBuf,
source: io::Error,
cleanup: Option<io::Error>,
},
}
pub fn create_private_bytes(
parent: &Path,
prefix: &str,
bytes: &[u8],
attempts: usize,
) -> Result<PathBuf, PrivateBytesError> {
#[cfg(unix)]
{
create_with_sequence(parent, prefix, bytes, attempts, || {
SEQUENCE.fetch_add(1, Ordering::Relaxed)
})
}
#[cfg(not(unix))]
{
let _ = (parent, prefix, bytes, attempts);
Err(PrivateBytesError::BeforeCreation(io::Error::new(
io::ErrorKind::Unsupported,
"private scratch files require Unix permissions",
)))
}
}
#[cfg(unix)]
fn create_with_sequence(
parent: &Path,
prefix: &str,
bytes: &[u8],
attempts: usize,
mut sequence: impl FnMut() -> u64,
) -> Result<PathBuf, PrivateBytesError> {
use std::path::Component;
let mut components = Path::new(prefix).components();
if attempts == 0
|| prefix.contains(['\0', '\r', '\n'])
|| !matches!(
(components.next(), components.next()),
(Some(Component::Normal(name)), None) if name == std::ffi::OsStr::new(prefix)
)
{
return Err(PrivateBytesError::BeforeCreation(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid private scratch request",
)));
}
let parent = parent
.canonicalize()
.map_err(PrivateBytesError::BeforeCreation)?;
for _ in 0..attempts {
let path = parent.join(format!("{prefix}-{}-{}", std::process::id(), sequence()));
match OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&path)
{
Ok(file) => {
return finish_candidate(
file,
path,
|file| file.write_all(bytes),
|path| fs::remove_file(path),
);
}
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
Err(source) => return Err(PrivateBytesError::BeforeCreation(source)),
}
}
Err(PrivateBytesError::BeforeCreation(io::Error::new(
io::ErrorKind::AlreadyExists,
"private scratch creation attempts exhausted",
)))
}
#[cfg(unix)]
fn finish_candidate(
mut file: File,
path: PathBuf,
write: impl FnOnce(&mut File) -> io::Result<()>,
remove: impl FnOnce(&Path) -> io::Result<()>,
) -> Result<PathBuf, PrivateBytesError> {
let result = write(&mut file);
drop(file);
match result {
Ok(()) => Ok(path),
Err(source) => {
let cleanup = remove(&path).err();
Err(PrivateBytesError::Write {
path,
source,
cleanup,
})
}
}
}