use crate::encryption::EncryptionProvider;
use crate::fs::{Fs, FsOpenOptions, SyncMode};
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
const HEADER_LEN: usize = 8 + 4;
const CHECKSUM_LEN: usize = 16;
#[must_use]
pub fn sidecar_path(table_path: &Path) -> PathBuf {
let mut name = table_path.as_os_str().to_os_string();
name.push(".restrict-bound");
PathBuf::from(name)
}
#[must_use]
pub fn table_id_from_sidecar_name(file_name: &str) -> Option<u64> {
let stem = file_name.strip_suffix(".tmp").unwrap_or(file_name);
stem.strip_suffix(".restrict-bound")?.parse().ok()
}
#[must_use]
fn sidecar_tmp_path(table_path: &Path) -> PathBuf {
let mut name = sidecar_path(table_path).into_os_string();
name.push(".tmp");
PathBuf::from(name)
}
fn serialize(table_id: u64, bound: &[u8]) -> Vec<u8> {
#[expect(
clippy::expect_used,
reason = "user keys are length-capped at u16::MAX by the writer"
)]
let bound_len = u32::try_from(bound.len()).expect("restriction bound length exceeds u32");
let mut out = Vec::with_capacity(HEADER_LEN + bound.len() + CHECKSUM_LEN);
out.extend_from_slice(&table_id.to_le_bytes());
out.extend_from_slice(&bound_len.to_le_bytes());
out.extend_from_slice(bound);
let checksum = crate::hash::hash128(&out);
out.extend_from_slice(&checksum.to_le_bytes());
out
}
fn deserialize(plain: &[u8]) -> Option<(u64, Vec<u8>)> {
if plain.len() < HEADER_LEN + CHECKSUM_LEN {
return None;
}
let (body, checksum_bytes) = plain.split_at(plain.len() - CHECKSUM_LEN);
let stored = u128::from_le_bytes(checksum_bytes.try_into().ok()?);
if crate::hash::hash128(body) != stored {
return None;
}
let table_id = u64::from_le_bytes(body.get(0..8)?.try_into().ok()?);
let bound_len = u32::from_le_bytes(body.get(8..12)?.try_into().ok()?) as usize;
let bound = body.get(HEADER_LEN..)?;
if bound.len() != bound_len {
return None;
}
Some((table_id, bound.to_vec()))
}
pub fn write(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
bound: &[u8],
sync_mode: SyncMode,
) -> crate::Result<()> {
let content = encode(encryption, table_id, bound)?;
publish_raw(fs, table_path, &content, sync_mode)
}
fn encode(
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
bound: &[u8],
) -> crate::Result<Vec<u8>> {
if bound.len() > u16::MAX as usize {
return Err(crate::Error::InvalidHeader(
"restriction bound exceeds the maximum key length",
));
}
let plain = serialize(table_id, bound);
match encryption {
Some(enc) => Ok(enc.encrypt(&plain)?),
None => Ok(plain),
}
}
pub(crate) fn publish_raw(
fs: &dyn Fs,
table_path: &Path,
content: &[u8],
sync_mode: SyncMode,
) -> crate::Result<()> {
publish_many(&[(fs, table_path, content)], sync_mode)
.pop()
.unwrap_or(Ok(()))
}
pub(crate) struct SidecarWrite<'a> {
pub fs: &'a dyn Fs,
pub table_path: &'a Path,
pub encryption: Option<&'a dyn EncryptionProvider>,
pub table_id: u64,
pub bound: &'a [u8],
}
pub(crate) fn write_many(
writes: &[SidecarWrite<'_>],
sync_mode: SyncMode,
) -> Vec<crate::Result<()>> {
let encoded: Vec<crate::Result<Vec<u8>>> = writes
.iter()
.map(|w| encode(w.encryption, w.table_id, w.bound))
.collect();
let published = {
let items: Vec<(&dyn Fs, &Path, &[u8])> = writes
.iter()
.zip(&encoded)
.filter_map(|(w, content)| {
content
.as_ref()
.ok()
.map(|content| (w.fs, w.table_path, content.as_slice()))
})
.collect();
publish_many(&items, sync_mode)
};
let mut published = published.into_iter();
encoded
.into_iter()
.map(|content| match content {
Err(e) => Err(e),
Ok(_) => published.next().unwrap_or(Ok(())),
})
.collect()
}
struct FolderSync<'a> {
fs: &'a dyn Fs,
folder: &'a Path,
failure: Option<(crate::io::ErrorKind, alloc::string::String)>,
}
fn publish_many(items: &[(&dyn Fs, &Path, &[u8])], sync_mode: SyncMode) -> Vec<crate::Result<()>> {
let mut results: Vec<crate::Result<()>> = items
.iter()
.map(|&(fs, table_path, content)| {
let mut file = fs.open(
&sidecar_tmp_path(table_path),
&FsOpenOptions::new().write(true).create(true).truncate(true),
)?;
file.write_all(content)?;
file.flush()?;
crate::fs::FsFile::sync_all_with(&*file, sync_mode)?;
Ok(())
})
.collect();
for (result, &(fs, table_path, _)) in results.iter_mut().zip(items) {
if result.is_ok() {
*result = fs
.rename(&sidecar_tmp_path(table_path), &sidecar_path(table_path))
.map_err(crate::Error::from);
}
}
let mut synced: Vec<FolderSync<'_>> = Vec::new();
for (result, &(fs, table_path, _)) in results.iter_mut().zip(items) {
let Some(parent) = table_path.parent().filter(|_| result.is_ok()) else {
continue;
};
let failure = if let Some(seen) = synced
.iter()
.find(|seen| core::ptr::addr_eq(seen.fs, fs) && seen.folder == parent)
{
seen.failure.clone()
} else {
let failure = fs
.sync_directory_with(parent, sync_mode)
.err()
.map(|e| (e.kind(), alloc::format!("{e}")));
synced.push(FolderSync {
fs,
folder: parent,
failure: failure.clone(),
});
failure
};
if let Some((kind, message)) = failure {
*result = Err(crate::Error::from(crate::io::Error::new(kind, message)));
}
}
for (result, &(fs, table_path, _)) in results.iter().zip(items) {
if result.is_err() {
let _ = fs.remove_file(&sidecar_tmp_path(table_path));
}
}
results
}
pub(crate) fn max_encoded_len(encryption: Option<&dyn EncryptionProvider>) -> u64 {
HEADER_LEN as u64
+ u64::from(u16::MAX)
+ CHECKSUM_LEN as u64
+ u64::from(encryption.map_or(0, EncryptionProvider::max_overhead))
}
pub enum SidecarRead {
Present(u64, Vec<u8>),
Missing,
Corrupt,
}
pub fn read(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
) -> crate::Result<SidecarRead> {
let path = sidecar_path(table_path);
let mut file = match fs.open(&path, &FsOpenOptions::new().read(true)) {
Ok(file) => file,
Err(e) if e.kind() == crate::io::ErrorKind::NotFound => return Ok(SidecarRead::Missing),
Err(e) => return Err(crate::Error::from(e)),
};
let max_len = max_encoded_len(encryption);
if file.metadata()?.len > max_len {
return Ok(SidecarRead::Corrupt);
}
let mut content = Vec::new();
std::io::Read::read_to_end(
&mut std::io::Read::take(&mut file, max_len.saturating_add(1)),
&mut content,
)
.map_err(crate::Error::from)?;
if content.len() as u64 > max_len {
return Ok(SidecarRead::Corrupt);
}
let plain = match encryption {
Some(enc) => match enc.decrypt(&content) {
Ok(plain) => plain,
Err(_) => return Ok(SidecarRead::Corrupt),
},
None => content,
};
match deserialize(&plain) {
Some((table_id, bound)) => Ok(SidecarRead::Present(table_id, bound)),
None => Ok(SidecarRead::Corrupt),
}
}
pub fn remove(fs: &dyn Fs, table_path: &Path, sync_mode: SyncMode) {
let path = sidecar_path(table_path);
if fs.remove_file(&path).is_ok()
&& let Some(parent) = path.parent()
{
let _ = fs.sync_directory_with(parent, sync_mode);
}
}
pub fn exists(fs: &dyn Fs, table_path: &Path) -> crate::io::Result<bool> {
fs.exists(&sidecar_path(table_path))
}
#[cfg(test)]
mod tests;