use std::fmt;
use std::fs;
use std::io::Write;
use std::path::Path;
use crate::tree::Hash;
use crate::tree::node::HASH_SIZE;
const U64_LEN: usize = 8;
#[derive(Debug)]
pub enum CodecError {
Corrupt(String),
Io(std::io::Error),
}
impl fmt::Display for CodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Corrupt(reason) => write!(f, "branch metadata corrupted: {reason}"),
Self::Io(error) => write!(f, "branch metadata I/O error: {error}"),
}
}
}
impl std::error::Error for CodecError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Corrupt(_) => None,
}
}
}
pub fn push_u64(buffer: &mut Vec<u8>, value: u64) {
buffer.extend_from_slice(&value.to_le_bytes());
}
pub fn push_bytes(buffer: &mut Vec<u8>, bytes: &[u8]) {
push_u64(buffer, bytes.len() as u64);
buffer.extend_from_slice(bytes);
}
pub struct Reader<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Reader<'a> {
pub const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
pub fn expect_magic(&mut self, magic: [u8; 4]) -> Result<(), CodecError> {
let found = self.read_exact(magic.len())?;
if found == magic.as_slice() {
Ok(())
} else {
Err(CodecError::Corrupt("unrecognised file header".into()))
}
}
pub fn read_u64(&mut self) -> Result<u64, CodecError> {
let bytes = self.read_exact(U64_LEN)?;
let array: [u8; U64_LEN] = bytes
.try_into()
.map_err(|_error| CodecError::Corrupt("truncated integer".into()))?;
Ok(u64::from_le_bytes(array))
}
pub fn read_usize(&mut self) -> Result<usize, CodecError> {
let value = self.read_u64()?;
usize::try_from(value)
.map_err(|_error| CodecError::Corrupt("length exceeds platform width".into()))
}
pub const fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.offset)
}
pub fn read_bytes(&mut self) -> Result<Vec<u8>, CodecError> {
let len = self.read_usize()?;
Ok(self.read_exact(len)?.to_vec())
}
pub fn read_hash(&mut self) -> Result<Hash, CodecError> {
let bytes = self.read_exact(HASH_SIZE)?;
let array: [u8; HASH_SIZE] = bytes
.try_into()
.map_err(|_error| CodecError::Corrupt("truncated hash".into()))?;
Ok(Hash::from_bytes(array))
}
pub fn finish(&self) -> Result<(), CodecError> {
if self.offset == self.bytes.len() {
Ok(())
} else {
Err(CodecError::Corrupt("trailing bytes after entries".into()))
}
}
fn read_exact(&mut self, len: usize) -> Result<&'a [u8], CodecError> {
let end = self
.offset
.checked_add(len)
.ok_or_else(|| CodecError::Corrupt("length overflow".into()))?;
let slice = self
.bytes
.get(self.offset..end)
.ok_or_else(|| CodecError::Corrupt("unexpected end of file".into()))?;
self.offset = end;
Ok(slice)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoclobberOutcome {
Installed,
TargetExists,
}
#[derive(Debug)]
pub enum InstallError {
NotInstalled(CodecError),
InstalledUnfenced(CodecError),
}
impl fmt::Display for InstallError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotInstalled(error) => {
write!(f, "atomic install failed before the rename: {error}")
}
Self::InstalledUnfenced(error) => write!(
f,
"atomic install renamed the target but the directory-entry fsync failed: {error}"
),
}
}
}
impl std::error::Error for InstallError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::NotInstalled(error) | Self::InstalledUnfenced(error) => Some(error),
}
}
}
impl From<InstallError> for CodecError {
fn from(error: InstallError) -> Self {
match error {
InstallError::NotInstalled(inner) | InstallError::InstalledUnfenced(inner) => inner,
}
}
}
pub fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), InstallError> {
write_atomic_unfenced(path, bytes).map_err(InstallError::NotInstalled)?;
let parent = path.parent().ok_or_else(|| {
InstallError::InstalledUnfenced(CodecError::Corrupt(
"metadata path has no parent directory".into(),
))
})?;
sync_parent_dir(parent).map_err(InstallError::InstalledUnfenced)
}
pub(crate) fn write_atomic_unfenced(path: &Path, bytes: &[u8]) -> Result<(), CodecError> {
let (temp_file, _parent) = synced_temp_file(path, bytes)?;
temp_file
.persist(path)
.map(drop)
.map_err(|error| CodecError::Io(error.error))
}
pub fn write_atomic_noclobber(path: &Path, bytes: &[u8]) -> Result<NoclobberOutcome, InstallError> {
let outcome =
write_atomic_noclobber_unfenced(path, bytes).map_err(InstallError::NotInstalled)?;
if outcome == NoclobberOutcome::Installed {
let parent = path.parent().ok_or_else(|| {
InstallError::InstalledUnfenced(CodecError::Corrupt(
"metadata path has no parent directory".into(),
))
})?;
sync_parent_dir(parent).map_err(InstallError::InstalledUnfenced)?;
}
Ok(outcome)
}
pub(crate) fn write_atomic_noclobber_unfenced(
path: &Path,
bytes: &[u8],
) -> Result<NoclobberOutcome, CodecError> {
let (temp_file, _parent) = synced_temp_file(path, bytes)?;
match temp_file.persist_noclobber(path) {
Ok(_persisted) => Ok(NoclobberOutcome::Installed),
Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => {
Ok(NoclobberOutcome::TargetExists)
}
Err(error) => Err(CodecError::Io(error.error)),
}
}
fn synced_temp_file<'path>(
path: &'path Path,
bytes: &[u8],
) -> Result<(tempfile::NamedTempFile, &'path Path), CodecError> {
let parent = path
.parent()
.ok_or_else(|| CodecError::Corrupt("metadata path has no parent directory".into()))?;
fs::create_dir_all(parent).map_err(CodecError::Io)?;
let mut temp_file = tempfile::Builder::new()
.prefix(".branch-")
.suffix(".tmp")
.tempfile_in(parent)
.map_err(CodecError::Io)?;
temp_file.write_all(bytes).map_err(CodecError::Io)?;
temp_file.as_file_mut().sync_all().map_err(CodecError::Io)?;
Ok((temp_file, parent))
}
#[cfg(unix)]
pub(crate) fn sync_parent_dir(parent: &Path) -> Result<(), CodecError> {
#[cfg(test)]
if take_injected_parent_dir_sync_failure() {
return Err(CodecError::Io(std::io::Error::other(
"injected parent-directory fsync failure",
)));
}
fs::File::open(parent)
.and_then(|dir| dir.sync_all())
.map_err(CodecError::Io)
}
#[cfg(test)]
thread_local! {
static FAIL_NEXT_PARENT_DIR_SYNC: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) fn fail_next_parent_dir_sync() {
FAIL_NEXT_PARENT_DIR_SYNC.with(|flag| flag.set(true));
}
#[cfg(test)]
fn take_injected_parent_dir_sync_failure() -> bool {
FAIL_NEXT_PARENT_DIR_SYNC.with(std::cell::Cell::take)
}
#[cfg(not(unix))]
pub(crate) fn sync_parent_dir(_parent: &Path) -> Result<(), CodecError> {
Ok(())
}