use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::io::Write as _;
use std::path::{Path, PathBuf};
use secrecy::{ExposeSecret, SecretSlice};
use super::{PARTIAL, STORED, SpoolError};
pub(super) fn scan(
directory: &Path,
skip: Option<&Path>,
) -> Result<BTreeMap<u64, u64>, SpoolError> {
let mut sizes = BTreeMap::new();
for entry in fs::read_dir(directory).map_err(io("read", directory))? {
let entry = entry.map_err(io("read", directory))?;
let path = entry.path();
if Some(path.as_path()) == skip {
continue;
}
let extension = path.extension().and_then(OsStr::to_str);
let sequence = path
.file_stem()
.and_then(OsStr::to_str)
.and_then(|it| it.parse::<u64>().ok());
match (extension, sequence) {
(Some(STORED), Some(sequence)) => {
let size = entry.metadata().map_err(io("read", &path))?.len();
sizes.insert(sequence, size);
}
(Some(PARTIAL), Some(_)) => {
fs::remove_file(&path).map_err(io("removed", &path))?;
}
_ => {
return Err(SpoolError::Foreign {
directory: directory.to_owned(),
path,
});
}
}
}
Ok(sizes)
}
pub(super) fn first_foreign(
directory: &Path,
skip: Option<&Path>,
) -> Result<Option<PathBuf>, SpoolError> {
for entry in fs::read_dir(directory).map_err(io("read", directory))? {
let path = entry.map_err(io("read", directory))?.path();
if Some(path.as_path()) == skip {
continue;
}
let extension = path.extension().and_then(OsStr::to_str);
let numbered = path
.file_stem()
.and_then(OsStr::to_str)
.is_some_and(|stem| stem.parse::<u64>().is_ok());
if !(numbered && matches!(extension, Some(STORED | PARTIAL))) {
return Ok(Some(path));
}
}
Ok(None)
}
#[cfg(unix)]
pub(super) fn writable_by_mode(path: &Path) -> Result<bool, SpoolError> {
use std::os::unix::fs::PermissionsExt as _;
let mode = fs::metadata(path)
.map_err(io("read", path))?
.permissions()
.mode();
Ok([0o300, 0o030, 0o003]
.into_iter()
.any(|bits| mode & bits == bits))
}
#[cfg(not(unix))]
pub(super) fn writable_by_mode(path: &Path) -> Result<bool, SpoolError> {
Ok(!fs::metadata(path)
.map_err(io("read", path))?
.permissions()
.readonly())
}
pub(super) fn probe(directory: &Path) -> Result<(), SpoolError> {
let path = file(directory, u64::MAX, PARTIAL);
let handle = private_file(&path)?;
drop(handle);
fs::remove_file(&path).map_err(io("removed", &path))
}
pub(super) fn file(directory: &Path, sequence: u64, extension: &str) -> PathBuf {
directory.join(format!("{sequence:020}.{extension}"))
}
pub(super) fn write_durably(
directory: &Path,
sequence: u64,
message: &SecretSlice<u8>,
) -> Result<(), SpoolError> {
let partial = file(directory, sequence, PARTIAL);
let stored = file(directory, sequence, STORED);
let mut handle = private_file(&partial)?;
handle
.write_all(message.expose_secret())
.map_err(io("written", &partial))?;
handle.sync_all().map_err(io("written", &partial))?;
drop(handle);
fs::rename(&partial, &stored).map_err(io("written", &stored))?;
sync_directory(directory)
}
#[cfg(unix)]
fn private_file(path: &Path) -> Result<File, SpoolError> {
use std::os::unix::fs::OpenOptionsExt as _;
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
.map_err(io("created", path))
}
#[cfg(not(unix))]
fn private_file(path: &Path) -> Result<File, SpoolError> {
OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(io("created", path))
}
#[cfg(unix)]
pub(super) fn sync_directory(directory: &Path) -> Result<(), SpoolError> {
File::open(directory)
.and_then(|handle| handle.sync_all())
.map_err(io("written", directory))
}
#[cfg(not(unix))]
pub(super) fn sync_directory(_directory: &Path) -> Result<(), SpoolError> {
Ok(())
}
#[cfg(unix)]
pub(super) fn create_private(directory: &Path) -> Result<(), SpoolError> {
use std::os::unix::fs::DirBuilderExt as _;
if directory.is_dir() {
return Ok(());
}
fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(directory)
.map_err(io("created", directory))
}
#[cfg(not(unix))]
pub(super) fn create_private(directory: &Path) -> Result<(), SpoolError> {
fs::create_dir_all(directory).map_err(io("created", directory))
}
#[cfg(unix)]
pub(super) fn check_private(directory: &Path) -> Result<(), SpoolError> {
use std::os::unix::fs::PermissionsExt as _;
let mode = fs::metadata(directory)
.map_err(io("read", directory))?
.permissions()
.mode();
let shared = mode & 0o077;
if shared == 0 {
Ok(())
} else {
Err(SpoolError::Exposed(directory.to_owned()))
}
}
#[cfg(not(unix))]
pub(super) fn check_private(_directory: &Path) -> Result<(), SpoolError> {
Ok(())
}
pub(super) fn io<'a>(
action: &'static str,
path: &'a Path,
) -> impl FnOnce(std::io::Error) -> SpoolError + 'a {
move |source| SpoolError::Io {
action,
path: path.to_owned(),
source,
}
}