use crate::Checksum;
use crate::encryption::EncryptionProvider;
use crate::fs::{Fs, FsOpenOptions};
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
const KIND_COMPLETED: u8 = 0;
#[cfg(test)]
const KIND_IN_PROGRESS: u8 = 1;
const ATTEST_LEN: usize = 1 + 8 + 16 + 16;
fn attest_path(table_path: &Path) -> PathBuf {
let mut name = table_path.as_os_str().to_os_string();
name.push(".heal-attest");
PathBuf::from(name)
}
fn attest_tmp_path(table_path: &Path) -> PathBuf {
let mut name = attest_path(table_path).into_os_string();
name.push(".tmp");
PathBuf::from(name)
}
fn serialize(kind: u8, table_id: u64, pre: u128, post: u128) -> Vec<u8> {
let mut out = Vec::with_capacity(ATTEST_LEN);
out.push(kind);
out.extend_from_slice(&table_id.to_le_bytes());
out.extend_from_slice(&pre.to_le_bytes());
out.extend_from_slice(&post.to_le_bytes());
out
}
fn deserialize(plain: &[u8]) -> Option<(u8, u64, u128, u128)> {
let kind = *plain.first()?;
let id = u64::from_le_bytes(plain.get(1..9)?.try_into().ok()?);
let pre = u128::from_le_bytes(plain.get(9..25)?.try_into().ok()?);
let post = u128::from_le_bytes(plain.get(25..41)?.try_into().ok()?);
Some((kind, id, pre, post))
}
fn write_sidecar(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
plain: &[u8],
) -> crate::Result<()> {
let content = match encryption {
Some(enc) => enc.encrypt(plain)?,
None => plain.to_vec(),
};
let path = attest_path(table_path);
let tmp = attest_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()?;
file.sync_all()?;
drop(file);
fs.rename(&tmp, &path)?;
if let Some(parent) = path.parent() {
fs.sync_directory(parent)?;
}
Ok(())
})();
if publish.is_err() {
let _ = fs.remove_file(&tmp);
}
publish
}
enum SidecarRead {
Present(u8, u64, u128, u128),
Missing,
Inconclusive,
}
fn read_sidecar(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
) -> SidecarRead {
let path = attest_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 SidecarRead::Missing,
Err(_) => return SidecarRead::Inconclusive,
};
let max_len =
ATTEST_LEN as u64 + u64::from(encryption.map_or(0, EncryptionProvider::max_overhead));
match file.metadata() {
Ok(meta) if meta.len > max_len => return SidecarRead::Inconclusive,
Ok(_) => {}
Err(_) => return SidecarRead::Inconclusive,
}
let mut content = Vec::new();
if std::io::Read::read_to_end(
&mut std::io::Read::take(&mut file, max_len.saturating_add(1)),
&mut content,
)
.is_err()
{
return SidecarRead::Inconclusive;
}
if content.len() as u64 > max_len {
return SidecarRead::Inconclusive;
}
let plain = match encryption {
Some(enc) => match enc.decrypt(&content) {
Ok(plain) => plain,
Err(_) => return SidecarRead::Inconclusive,
},
None => content,
};
match deserialize(&plain) {
Some((kind, id, pre, post)) => SidecarRead::Present(kind, id, pre, post),
None => SidecarRead::Inconclusive,
}
}
pub enum AttestResult {
Attests,
Absent,
Inconclusive,
}
#[cfg(test)]
pub fn write_in_progress(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
pre: Checksum,
) -> crate::Result<()> {
let plain = serialize(KIND_IN_PROGRESS, table_id, pre.into_u128(), 0);
write_sidecar(fs, table_path, encryption, &plain)
}
#[cfg(test)]
pub(super) fn attests_in_progress(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
manifest: Checksum,
) -> bool {
match read_sidecar(fs, table_path, encryption) {
SidecarRead::Present(kind, id, pre, _post) => {
kind == KIND_IN_PROGRESS && id == table_id && pre == manifest.into_u128()
}
SidecarRead::Missing | SidecarRead::Inconclusive => false,
}
}
pub fn write(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
pre: Checksum,
post: Checksum,
) -> crate::Result<()> {
let plain = serialize(KIND_COMPLETED, table_id, pre.into_u128(), post.into_u128());
write_sidecar(fs, table_path, encryption, &plain)
}
pub(super) fn attests(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
current: Checksum,
manifest: Checksum,
) -> AttestResult {
match read_sidecar(fs, table_path, encryption) {
SidecarRead::Present(kind, id, pre, post) => {
if kind == KIND_COMPLETED
&& id == table_id
&& post == current.into_u128()
&& pre == manifest.into_u128()
{
AttestResult::Attests
} else {
AttestResult::Absent
}
}
SidecarRead::Missing => AttestResult::Absent,
SidecarRead::Inconclusive => AttestResult::Inconclusive,
}
}
#[cfg(any(feature = "page_ecc", test))]
pub fn attests_post(
fs: &dyn Fs,
table_path: &Path,
encryption: Option<&dyn EncryptionProvider>,
table_id: u64,
post: Checksum,
) -> AttestResult {
match read_sidecar(fs, table_path, encryption) {
SidecarRead::Present(kind, id, _pre, marker_post)
if kind == KIND_COMPLETED && id == table_id && marker_post == post.into_u128() =>
{
AttestResult::Attests
}
SidecarRead::Present(..) | SidecarRead::Missing => AttestResult::Absent,
SidecarRead::Inconclusive => AttestResult::Inconclusive,
}
}
pub fn remove(fs: &dyn Fs, table_path: &Path) {
let path = attest_path(table_path);
if fs.remove_file(&path).is_ok()
&& let Some(parent) = path.parent()
{
let _ = fs.sync_directory(parent);
}
}
pub fn exists(fs: &dyn Fs, table_path: &Path) -> crate::io::Result<bool> {
fs.exists(&attest_path(table_path))
}
#[cfg(test)]
mod tests;