#[cfg(not(feature = "std"))]
use crate::io::Write;
use crate::path::Path;
use crate::{
Slice,
fs::{Fs, FsFile, ReadBuf, SyncMode},
slice::UnfilledSlice,
};
#[cfg(feature = "std")]
use std::io::Write;
pub const MAGIC_BYTES: [u8; 4] = [b'L', b'S', b'M', 4];
pub const TABLES_FOLDER: &str = "tables";
pub const BLOBS_FOLDER: &str = "blobs";
pub const DICTS_FOLDER: &str = "dicts";
pub const RESTORE_MARKER_FILE: &str = "restoring";
pub const REPAIR_TMP_SUFFIX: &str = ".repair-tmp";
#[must_use]
pub fn table_id_from_repair_tmp_name(file_name: &str) -> Option<crate::TableId> {
file_name
.strip_suffix(REPAIR_TMP_SUFFIX)
.and_then(|id| id.parse::<crate::TableId>().ok())
}
#[must_use]
pub fn canonical_id(digits: &str) -> Option<u64> {
let canonical = !digits.is_empty()
&& digits.bytes().all(|b| b.is_ascii_digit())
&& (digits == "0" || !digits.starts_with('0'));
if canonical { digits.parse().ok() } else { None }
}
pub const BLOB_SALVAGE_TMP_SUFFIX: &str = ".salvage-tmp";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlobDirEntry {
Blob(crate::vlog::BlobFileId),
SalvageTmp(crate::vlog::BlobFileId),
Foreign,
}
impl BlobDirEntry {
#[must_use]
pub fn classify(file_name: &str) -> Self {
let owned_id = |rest: &str, make: fn(crate::vlog::BlobFileId) -> Self| {
rest.parse::<crate::vlog::BlobFileId>()
.map_or(Self::Foreign, make)
};
if let Some(rest) = file_name.strip_suffix(BLOB_SALVAGE_TMP_SUFFIX) {
return owned_id(rest, Self::SalvageTmp);
}
owned_id(file_name, Self::Blob)
}
}
pub type DictId = u32;
pub const DICT_TMP_SUFFIX: &str = ".tmp";
pub const DICT_DAMAGED_SUFFIX: &str = ".damaged";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DictDirEntry {
Dict(DictId),
Tmp(DictId),
Foreign,
}
impl DictDirEntry {
#[must_use]
pub fn classify(file_name: &str) -> Self {
let owned_id = |rest: &str, make: fn(DictId) -> Self| {
canonical_id(rest)
.and_then(|id| DictId::try_from(id).ok())
.map_or(Self::Foreign, make)
};
if let Some(rest) = file_name.strip_suffix(DICT_TMP_SUFFIX) {
return owned_id(rest, Self::Tmp);
}
owned_id(file_name, Self::Dict)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TableDirEntry {
Table(crate::TableId),
HealAttest(crate::TableId),
HealAttestTmp(crate::TableId),
HealTmp(crate::TableId),
RestrictBound(crate::TableId),
RestrictBoundTmp(crate::TableId),
RepairTmp(crate::TableId),
RepairTmpCompanion(crate::TableId),
Foreign,
}
impl TableDirEntry {
#[must_use]
pub fn classify(file_name: &str) -> Self {
let owned_id = |rest: &str, make: fn(crate::TableId) -> Self| {
rest.parse::<crate::TableId>().map_or(Self::Foreign, make)
};
if let Some(rest) = file_name.strip_suffix(".heal-attest.tmp") {
return owned_id(rest, Self::HealAttestTmp);
}
if let Some(rest) = file_name.strip_suffix(".heal-attest") {
return owned_id(rest, Self::HealAttest);
}
if let Some((id, seq)) = file_name.split_once(".healtmp-") {
if seq.parse::<u64>().is_ok() {
return owned_id(id, Self::HealTmp);
}
return Self::Foreign;
}
if let Some(rest) = file_name.strip_suffix(".restrict-bound.tmp") {
if let Some(temp_owner) = rest.strip_suffix(REPAIR_TMP_SUFFIX) {
return owned_id(temp_owner, Self::RepairTmpCompanion);
}
return owned_id(rest, Self::RestrictBoundTmp);
}
if let Some(rest) = file_name.strip_suffix(".restrict-bound") {
if let Some(temp_owner) = rest.strip_suffix(REPAIR_TMP_SUFFIX) {
return owned_id(temp_owner, Self::RepairTmpCompanion);
}
return owned_id(rest, Self::RestrictBound);
}
if let Some(rest) = file_name.strip_suffix(REPAIR_TMP_SUFFIX) {
return owned_id(rest, Self::RepairTmp);
}
file_name
.parse::<crate::TableId>()
.map_or(Self::Foreign, Self::Table)
}
}
pub(crate) fn on_disk_bytes(fs: &dyn Fs, path: &Path) -> crate::io::Result<u64> {
let len = fs.metadata(path)?.len;
Ok(match fs.allocated_size(path)? {
Some(allocated) => allocated.min(len),
None => len,
})
}
pub(crate) fn checksum_from_with_overrides(
fs: &dyn Fs,
path: &Path,
start: u64,
overrides: &[(u64, alloc::vec::Vec<u8>)],
) -> crate::Result<u128> {
#[cfg(not(feature = "std"))]
use crate::io::{Read, Seek, SeekFrom};
#[cfg(feature = "std")]
use std::io::{Read, Seek, SeekFrom};
let mut file = fs.open(path, &crate::fs::FsOpenOptions::new().read(true))?;
if start != 0 {
file.seek(SeekFrom::Start(start))?;
}
let mut hasher = xxhash_rust::xxh3::Xxh3Default::new();
let mut buf = alloc::vec![0u8; 256 * 1024];
let mut chunk_start = start;
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break; }
let chunk_end = chunk_start + n as u64;
let Some(chunk) = buf.get_mut(..n) else { break };
for (off, bytes) in overrides {
let ov_end = *off + bytes.len() as u64;
let lo = (*off).max(chunk_start);
let hi = ov_end.min(chunk_end);
if lo >= hi {
continue;
}
let (Ok(dst_lo), Ok(dst_hi), Ok(src_lo), Ok(src_hi)) = (
usize::try_from(lo - chunk_start),
usize::try_from(hi - chunk_start),
usize::try_from(lo - *off),
usize::try_from(hi - *off),
) else {
continue;
};
if let (Some(dst), Some(src)) =
(chunk.get_mut(dst_lo..dst_hi), bytes.get(src_lo..src_hi))
{
dst.copy_from_slice(src);
}
}
hasher.update(&*chunk);
chunk_start = chunk_end;
}
Ok(hasher.digest128())
}
pub fn read_exact(file: &dyn FsFile, offset: u64, size: usize) -> crate::io::Result<Slice> {
read_exact_paced(file, offset, size, None)
}
pub(crate) fn read_exact_paced(
file: &dyn FsFile,
offset: u64,
size: usize,
pace: Option<&dyn crate::table::util::ReadPacer>,
) -> crate::io::Result<Slice> {
let mut slice = UnfilledSlice::new(size);
let mut target = slice.target();
read_at_paced(file, &mut target, offset, pace)?;
let bytes_read = target.filled();
if bytes_read != size {
return Err(crate::io::Error::new(
crate::io::ErrorKind::UnexpectedEof,
format!(
"read_exact({bytes_read}) at {offset} did not read enough bytes {size}; file has length {}",
file.metadata()?.len
),
));
}
#[expect(unsafe_code, reason = "see safety")]
Ok(unsafe { slice.assume_filled() })
}
pub(crate) fn read_at_paced(
file: &dyn FsFile,
buf: &mut ReadBuf<'_>,
offset: u64,
pace: Option<&dyn crate::table::util::ReadPacer>,
) -> crate::io::Result<()> {
let Some(pace) = pace else {
return file.read_at_buf(buf, offset);
};
if !pace.active() {
file.read_at_buf(buf, offset)?;
pace.read_bytes(buf.filled() as u64);
return Ok(());
}
let size = buf.capacity();
while buf.filled() < size {
let read = buf.filled();
let len = (size - read).min(crate::table::util::PACE_PORTION);
let at = offset.checked_add(read as u64).ok_or_else(|| {
crate::io::Error::new(
crate::io::ErrorKind::InvalidInput,
"read_at_paced: offset past the largest file position",
)
})?;
pace.pace(at, len as u64);
let mut portion = buf.portion(read, len);
#[cfg(feature = "std")]
let started = std::time::Instant::now();
let result = file.read_at_buf(&mut portion, at);
#[cfg(feature = "std")]
pace.read_took(started.elapsed());
result?;
let got = portion.filled();
pace.read_bytes(got as u64);
buf.extend_from_portion(got);
if got < len {
break;
}
}
Ok(())
}
pub fn read_exact_many(
file: &dyn FsFile,
regions: &[(u64, usize)],
) -> crate::io::Result<alloc::vec::Vec<Slice>> {
let mut slices: alloc::vec::Vec<_> = regions
.iter()
.map(|&(_, size)| UnfilledSlice::new(size))
.collect();
let mut targets: alloc::vec::Vec<(u64, ReadBuf<'_>)> = regions
.iter()
.zip(slices.iter_mut())
.map(|(&(offset, _), slice)| (offset, slice.target()))
.collect();
file.read_many_buf(&mut targets)?;
if targets
.iter()
.any(|(_, target)| target.filled() != target.capacity())
{
return Err(crate::io::Error::new(
crate::io::ErrorKind::UnexpectedEof,
"read_exact_many: a region came back short",
));
}
drop(targets);
Ok(slices
.into_iter()
.map(|slice| {
#[expect(unsafe_code, reason = "see safety")]
unsafe {
slice.assume_filled()
}
})
.collect())
}
pub fn rewrite_atomic(
path: &Path,
content: &[u8],
fs: &dyn Fs,
mode: SyncMode,
) -> crate::io::Result<()> {
use crate::fs::FsOpenOptions;
use core::sync::atomic::Ordering;
use portable_atomic::AtomicU64;
static TEMP_SEQ: AtomicU64 = AtomicU64::new(0);
#[expect(
clippy::expect_used,
reason = "every file should have a parent directory"
)]
let folder = path.parent().expect("should have a parent");
#[cfg(feature = "std")]
let pid = std::process::id();
#[cfg(not(feature = "std"))]
let pid = 0u32;
let tmp_path = loop {
let seq = TEMP_SEQ.fetch_add(1, Ordering::Relaxed);
let candidate = folder.join(format!(".tmp_{pid}_{seq}"));
match fs.open(
&candidate,
&FsOpenOptions::new()
.write(true)
.create_new(true)
.write_origin(crate::fs::WriteOrigin::Metadata),
) {
Ok(mut file) => {
let write_result = file
.write_all(content)
.map_err(crate::io::Error::from)
.and_then(|()| file.flush().map_err(crate::io::Error::from))
.and_then(|()| FsFile::sync_all_with(&*file, mode));
if let Err(e) = write_result {
drop(file);
remove_temp_or_log(fs, &candidate);
return Err(e);
}
break candidate;
}
Err(e) if e.kind() == crate::io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(e),
}
};
if let Err(e) = fs.rename(&tmp_path, path) {
remove_temp_or_log(fs, &tmp_path);
return Err(e);
}
fsync_directory(folder, fs, mode)?;
Ok(())
}
fn remove_temp_or_log(fs: &dyn Fs, tmp: &Path) {
if let Err(e) = fs.remove_file(tmp) {
log::warn!("could not remove temp file {}: {e}", tmp.display());
}
}
pub fn fsync_directory(path: &Path, fs: &dyn Fs, mode: SyncMode) -> crate::io::Result<()> {
fs.sync_directory_with(path, mode)
}
#[must_use]
pub fn folder_sync_mode(fs: &dyn Fs, folder: &Path, sync_mode: SyncMode) -> SyncMode {
if sync_mode == SyncMode::Barrier
&& folder.parent().is_none_or(|above| {
let here = fs.volume_id(folder);
here.is_none() || here != fs.volume_id(above)
})
{
SyncMode::Full
} else {
sync_mode
}
}
#[must_use]
pub fn entry_directory(path: &Path) -> &Path {
path.parent()
.filter(|parent| *parent != Path::new(""))
.unwrap_or_else(|| Path::new("."))
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::useless_vec,
reason = "test code"
)]
mod tests;