#[cfg(not(feature = "std"))]
use crate::io::Write;
use crate::path::Path;
use crate::{
Slice,
fs::{Fs, FsFile, SyncMode},
};
#[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 CURRENT_VERSION_FILE: &str = "current";
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())
}
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)
}
}
#[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> {
#[expect(unsafe_code, reason = "see safety")]
let mut builder = unsafe { Slice::builder_unzeroed(size) };
let bytes_read = file.read_at(&mut builder, offset)?;
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
),
));
}
Ok(builder.freeze().into())
}
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),
) {
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);
let _ = fs.remove_file(&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) {
let _ = fs.remove_file(&tmp_path);
return Err(e);
}
fsync_directory(folder, fs, mode)?;
Ok(())
}
pub fn fsync_directory(path: &Path, fs: &dyn Fs, mode: SyncMode) -> crate::io::Result<()> {
fs.sync_directory_with(path, mode)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::useless_vec,
reason = "test code"
)]
mod tests;