use std::{
fs::{self, File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
path::Path,
sync::atomic::{AtomicU64, Ordering},
};
use crate::runtime::error::RuntimeError;
use super::store_error;
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(1);
pub(super) fn atomic_replace(path: &Path, contents: &[u8]) -> Result<(), RuntimeError> {
let parent = parent_dir(path)?;
fs::create_dir_all(parent)
.map_err(|error| store_error(&format!("create '{}'", parent.display()), error))?;
let temp_path = parent.join(format!(
".{}.tmp-{}-{}",
file_name(path)?,
std::process::id(),
NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed),
));
let write = (|| -> std::io::Result<()> {
let mut file = File::create(&temp_path)?;
file.write_all(contents)?;
file.sync_all()
})();
if let Err(error) = write {
let _ = fs::remove_file(&temp_path);
return Err(store_error(
&format!("write '{}'", temp_path.display()),
error,
));
}
if let Err(error) = fs::rename(&temp_path, path) {
let _ = fs::remove_file(&temp_path);
return Err(store_error(
&format!("rename into '{}'", path.display()),
error,
));
}
fsync_dir(parent)
}
pub(super) fn append_lines(path: &Path, lines: &[String]) -> Result<(), RuntimeError> {
if lines.is_empty() {
return Ok(());
}
let parent = parent_dir(path)?;
fs::create_dir_all(parent)
.map_err(|error| store_error(&format!("create '{}'", parent.display()), error))?;
let existed = path.exists();
let mut buffer = String::new();
for line in lines {
buffer.push_str(line);
buffer.push('\n');
}
let append = (|| -> std::io::Result<()> {
if existed {
let mut repair = OpenOptions::new().read(true).write(true).open(path)?;
drop_truncated_tail(&mut repair)?;
}
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
file.write_all(buffer.as_bytes())?;
file.sync_all()
})();
append.map_err(|error| store_error(&format!("append to '{}'", path.display()), error))?;
if !existed {
fsync_dir(parent)?;
}
Ok(())
}
fn drop_truncated_tail(file: &mut File) -> std::io::Result<()> {
let len = file.metadata()?.len();
if len == 0 {
return Ok(());
}
const CHUNK: u64 = 4096;
let mut end = len;
loop {
let start = end.saturating_sub(CHUNK);
let mut chunk = vec![0u8; (end - start) as usize];
file.seek(SeekFrom::Start(start))?;
file.read_exact(&mut chunk)?;
if end == len && chunk.last() == Some(&b'\n') {
return Ok(());
}
if let Some(offset) = chunk.iter().rposition(|byte| *byte == b'\n') {
file.set_len(start + offset as u64 + 1)?;
return Ok(());
}
if start == 0 {
file.set_len(0)?;
return Ok(());
}
end = start;
}
}
pub(super) fn read_optional(path: &Path) -> Result<Option<String>, RuntimeError> {
match fs::read_to_string(path) {
Ok(contents) => Ok(Some(contents)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(store_error(&format!("read '{}'", path.display()), error)),
}
}
pub(super) fn fsync_dir(dir: &Path) -> Result<(), RuntimeError> {
#[cfg(unix)]
{
let handle = File::open(dir)
.map_err(|error| store_error(&format!("open directory '{}'", dir.display()), error))?;
handle
.sync_all()
.map_err(|error| store_error(&format!("sync directory '{}'", dir.display()), error))?;
}
#[cfg(not(unix))]
{
let _ = dir;
}
Ok(())
}
pub(super) fn encode_component(id: &str) -> String {
if is_plain_component(id) {
return id.to_string();
}
let mut encoded = String::with_capacity(2 + id.len() * 2);
encoded.push_str("x-");
for byte in id.as_bytes() {
use std::fmt::Write as _;
let _ = write!(&mut encoded, "{byte:02x}");
}
encoded
}
fn is_plain_component(id: &str) -> bool {
if id.is_empty() || id.starts_with('.') || id.ends_with('.') || id.starts_with("x-") {
return false;
}
let tame = id
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '-' | '_' | '.'));
if !tame {
return false;
}
let stem = id.split('.').next().unwrap_or(id);
!is_windows_reserved_device(stem)
}
fn is_windows_reserved_device(stem: &str) -> bool {
matches!(stem, "con" | "prn" | "aux" | "nul")
|| (stem.len() == 4
&& (stem.starts_with("com") || stem.starts_with("lpt"))
&& matches!(stem.as_bytes()[3], b'1'..=b'9'))
}
fn parent_dir(path: &Path) -> Result<&Path, RuntimeError> {
path.parent().ok_or_else(|| {
RuntimeError::Store(format!("path '{}' has no parent directory", path.display()))
})
}
fn file_name(path: &Path) -> Result<&str, RuntimeError> {
path.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
RuntimeError::Store(format!("path '{}' has no usable file name", path.display()))
})
}