use std::{
ffi::OsString,
fs::{File, OpenOptions},
io::{self, Read, Write},
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
};
#[cfg(windows)]
#[allow(unsafe_code)]
mod windows;
#[cfg(windows)]
use windows::{open_record, replace};
static NEXT: AtomicU64 = AtomicU64::new(0);
struct Temporary(PathBuf);
impl Drop for Temporary {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RecordOptions {
durable: bool,
#[cfg(unix)]
mode: u32,
read_limit: u64,
}
impl Default for RecordOptions {
fn default() -> Self {
Self::new()
}
}
impl RecordOptions {
pub const fn new() -> Self {
Self {
durable: false,
#[cfg(unix)]
mode: 0o600,
read_limit: 64 * 1024,
}
}
pub const fn durable(mut self, durable: bool) -> Self {
self.durable = durable;
self
}
#[cfg(unix)]
pub const fn mode(mut self, mode: u32) -> Self {
self.mode = mode;
self
}
pub const fn read_limit(mut self, bytes: u64) -> Self {
self.read_limit = bytes;
self
}
}
pub fn publish_record(path: &Path, contents: &[u8]) -> io::Result<()> {
publish_record_with(path, contents, RecordOptions::new())
}
pub fn publish_record_with(path: &Path, contents: &[u8], options: RecordOptions) -> io::Result<()> {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let filename = path.file_name().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"discovery path needs a filename",
)
})?;
let (temporary, mut file): (Temporary, File) = loop {
let mut name = OsString::from(".");
name.push(filename);
name.push(format!(
".{}.{}.tmp",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
let temporary = parent.join(name);
let mut open = OpenOptions::new();
open.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
open.mode(0o600);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
open.access_mode(windows::ACCESS);
}
match open.open(&temporary) {
Ok(file) => break (Temporary(temporary), file),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
};
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(std::fs::Permissions::from_mode(options.mode))?;
}
file.write_all(contents)?;
file.sync_all()?;
replace(file, &temporary.0, path)?;
if options.durable {
sync_renamed(path, parent)?;
}
Ok(())
}
#[cfg(not(windows))]
fn replace(file: File, from: &Path, to: &Path) -> io::Result<()> {
drop(file);
std::fs::rename(from, to)
}
#[cfg(not(windows))]
fn open_record(open: &OpenOptions, path: &Path) -> io::Result<File> {
open.open(path)
}
fn sync_renamed(to: &Path, parent: &Path) -> io::Result<()> {
let synced = if cfg!(windows) {
OpenOptions::new().write(true).open(to)?
} else {
File::open(parent)?
};
synced.sync_all()
}
fn read_regular(path: &Path, limit: u64) -> io::Result<Option<Vec<u8>>> {
let not_regular = || io::Error::new(io::ErrorKind::InvalidData, "record is not a regular file");
let mut open = OpenOptions::new();
open.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
open.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
open.custom_flags(0x0220_0000);
}
let file = match open_record(&open, path) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
#[cfg(unix)]
Err(error) if error.raw_os_error() == Some(libc::ELOOP) => return Err(not_regular()),
Err(error) => return Err(error),
};
if !file.metadata()?.is_file() {
return Err(not_regular());
}
let mut contents = Vec::new();
file.take(limit.saturating_add(1))
.read_to_end(&mut contents)?;
if contents.len() as u64 > limit {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"record is longer than its read limit",
));
}
Ok(Some(contents))
}
#[cfg(feature = "local")]
pub fn ensure_private_dir(dir: &Path) -> io::Result<()> {
std::fs::create_dir_all(dir)?;
let metadata = std::fs::symlink_metadata(dir)?;
if !metadata.is_dir() {
return Err(io::ErrorKind::PermissionDenied.into());
}
#[cfg(unix)]
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
if metadata.uid() != crate::local::unix::own_uid() {
return Err(io::ErrorKind::PermissionDenied.into());
}
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct RecordSlot(PathBuf, RecordOptions);
impl RecordSlot {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self(path.into(), RecordOptions::new())
}
pub fn options(mut self, options: RecordOptions) -> Self {
self.1 = options;
self
}
fn lock(&self) -> io::Result<crate::ProcessLock> {
let mut lock = self.0.as_os_str().to_owned();
lock.push(".record.lock");
crate::ProcessLock::acquire_until(
&lock,
std::time::Instant::now() + std::time::Duration::from_secs(2),
)?
.ok_or_else(|| io::ErrorKind::TimedOut.into())
}
pub fn replace(&self, contents: &[u8]) -> io::Result<()> {
let _lock = self.lock()?;
publish_record_with(&self.0, contents, self.1)
}
pub fn repair(&self, contents: &[u8]) -> io::Result<bool> {
let _lock = self.lock()?;
if self.matches(contents)? {
return Ok(false);
}
publish_record_with(&self.0, contents, self.1)?;
Ok(true)
}
pub fn replace_if(
&self,
contents: &[u8],
replace: impl FnOnce(Option<&[u8]>) -> bool,
) -> io::Result<bool> {
let _lock = self.lock()?;
let current = read_regular(&self.0, self.1.read_limit)?;
if !replace(current.as_deref()) {
return Ok(false);
}
publish_record_with(&self.0, contents, self.1)?;
Ok(true)
}
pub fn remove_if_matches(&self, contents: &[u8]) -> io::Result<bool> {
let _lock = self.lock()?;
if !self.matches(contents)? {
return Ok(false);
}
std::fs::remove_file(&self.0)?;
Ok(true)
}
pub fn remove_if(&self, remove: impl FnOnce(&[u8]) -> bool) -> io::Result<bool> {
let _lock = self.lock()?;
let Some(current) = read_regular(&self.0, self.1.read_limit)? else {
return Ok(false);
};
if !remove(¤t) {
return Ok(false);
}
std::fs::remove_file(&self.0)?;
Ok(true)
}
pub fn read(&self) -> io::Result<Option<Vec<u8>>> {
read_regular(&self.0, self.1.read_limit)
}
fn matches(&self, contents: &[u8]) -> io::Result<bool> {
let file = match open_record(OpenOptions::new().read(true), &self.0) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error),
};
let mut current = Vec::new();
file.take(contents.len().saturating_add(1) as u64)
.read_to_end(&mut current)?;
Ok(current == contents)
}
}