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<()> {
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);
let content = match encryption {
Some(enc) => enc.encrypt(&plain)?,
None => plain,
};
publish_raw(fs, table_path, &content, sync_mode)
}
pub(crate) fn publish_raw(
fs: &dyn Fs,
table_path: &Path,
content: &[u8],
sync_mode: SyncMode,
) -> crate::Result<()> {
let path = sidecar_path(table_path);
let tmp = sidecar_tmp_path(table_path);
let publish = (|| -> crate::Result<()> {
let mut file = fs.open(
&tmp,
&FsOpenOptions::new().write(true).create(true).truncate(true),
)?;
file.write_all(content)?;
file.flush()?;
crate::fs::FsFile::sync_all_with(&*file, sync_mode)?;
drop(file);
fs.rename(&tmp, &path)?;
if let Some(parent) = path.parent() {
fs.sync_directory_with(parent, sync_mode)?;
}
Ok(())
})();
if publish.is_err() {
let _ = fs.remove_file(&tmp);
}
publish
}
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;