use std::path::Path;
use crate::{Error, Result};
pub(crate) const META_MAGIC: [u8; 16] = *b"EMDB-META\0\0\0\0\0\0\0";
pub(crate) const META_FORMAT_VERSION: u32 = 1;
pub(crate) const META_SALT_LEN: usize = 16;
pub(crate) const META_VERIFY_LEN: usize = 60;
pub(crate) const META_BODY_LEN: usize = 112;
pub(crate) const FLAG_ENCRYPTED: u32 = 1 << 0;
pub(crate) const FLAG_CIPHER_CHACHA20: u32 = 1 << 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MetaHeader {
pub(crate) flags: u32,
pub(crate) created_at_ms: u64,
pub(crate) encryption_salt: [u8; META_SALT_LEN],
pub(crate) encryption_verify: [u8; META_VERIFY_LEN],
}
impl MetaHeader {
pub(crate) fn fresh(flags: u32) -> Self {
Self {
flags,
created_at_ms: now_unix_millis(),
encryption_salt: [0_u8; META_SALT_LEN],
encryption_verify: [0_u8; META_VERIFY_LEN],
}
}
pub(crate) fn encode(&self) -> [u8; META_BODY_LEN] {
let mut buf = [0_u8; META_BODY_LEN];
buf[0..16].copy_from_slice(&META_MAGIC);
buf[16..20].copy_from_slice(&META_FORMAT_VERSION.to_le_bytes());
buf[20..24].copy_from_slice(&self.flags.to_le_bytes());
buf[24..32].copy_from_slice(&self.created_at_ms.to_le_bytes());
buf[32..48].copy_from_slice(&self.encryption_salt);
buf[48..108].copy_from_slice(&self.encryption_verify);
let crc = crc32fast::hash(&buf[..108]);
buf[108..112].copy_from_slice(&crc.to_le_bytes());
buf
}
pub(crate) fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < META_BODY_LEN {
return Err(Error::Corrupted {
offset: 0,
reason: "meta sidecar shorter than the v1 body",
});
}
if buf[..16] != META_MAGIC {
return Err(Error::MagicMismatch);
}
let version = u32::from_le_bytes(read_4(buf, 16));
if version != META_FORMAT_VERSION {
return Err(Error::VersionMismatch {
found: version,
expected: META_FORMAT_VERSION,
});
}
let stored_crc = u32::from_le_bytes(read_4(buf, 108));
let actual_crc = crc32fast::hash(&buf[..108]);
if stored_crc != actual_crc {
return Err(Error::Corrupted {
offset: 108,
reason: "meta sidecar CRC mismatch",
});
}
let flags = u32::from_le_bytes(read_4(buf, 20));
let created_at_ms = u64::from_le_bytes(read_8(buf, 24));
let mut encryption_salt = [0_u8; META_SALT_LEN];
encryption_salt.copy_from_slice(&buf[32..48]);
let mut encryption_verify = [0_u8; META_VERIFY_LEN];
encryption_verify.copy_from_slice(&buf[48..108]);
Ok(Self {
flags,
created_at_ms,
encryption_salt,
encryption_verify,
})
}
}
pub(crate) fn meta_path_for(db_path: &Path) -> std::path::PathBuf {
let mut p = db_path.as_os_str().to_owned();
p.push(".meta");
std::path::PathBuf::from(p)
}
pub(crate) fn read(db_path: &Path) -> Result<Option<MetaHeader>> {
let path = meta_path_for(db_path);
match std::fs::read(&path) {
Ok(bytes) => Ok(Some(MetaHeader::decode(&bytes)?)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(Error::from(err)),
}
}
pub(crate) fn write(db_path: &Path, header: &MetaHeader) -> Result<()> {
let fs = fsys::builder()
.tune_for(fsys::Workload::Database)
.build()
.map_err(|err| Error::Io(std::io::Error::other(format!("fsys init: {err}"))))?;
write_with(&fs, db_path, header)
}
pub(crate) fn write_with(fs: &fsys::Handle, db_path: &Path, header: &MetaHeader) -> Result<()> {
let path = meta_path_for(db_path);
let body = header.encode();
fs.write(&path, &body)
.map_err(|err| Error::Io(std::io::Error::other(format!("fsys write meta: {err}"))))?;
Ok(())
}
#[inline]
fn read_4(buf: &[u8], offset: usize) -> [u8; 4] {
let mut out = [0_u8; 4];
out.copy_from_slice(&buf[offset..offset + 4]);
out
}
#[inline]
fn read_8(buf: &[u8], offset: usize) -> [u8; 8] {
let mut out = [0_u8; 8];
out.copy_from_slice(&buf[offset..offset + 8]);
out
}
fn now_unix_millis() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis().min(u64::MAX as u128) as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_default_header() {
let h = MetaHeader::fresh(0);
let buf = h.encode();
let decoded = MetaHeader::decode(&buf).expect("decode");
assert_eq!(h, decoded);
}
#[test]
fn round_trip_with_encryption_payload() {
let mut salt = [0_u8; META_SALT_LEN];
for (i, b) in salt.iter_mut().enumerate() {
*b = i as u8;
}
let mut verify = [0_u8; META_VERIFY_LEN];
for (i, b) in verify.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
let h = MetaHeader {
flags: FLAG_ENCRYPTED | FLAG_CIPHER_CHACHA20,
created_at_ms: 1_700_000_000_123,
encryption_salt: salt,
encryption_verify: verify,
};
let buf = h.encode();
assert_eq!(buf.len(), META_BODY_LEN);
let decoded = MetaHeader::decode(&buf).expect("decode");
assert_eq!(h, decoded);
}
#[test]
fn rejects_bad_magic() {
let mut buf = MetaHeader::fresh(0).encode();
buf[0] ^= 0x01;
assert!(matches!(
MetaHeader::decode(&buf),
Err(Error::MagicMismatch)
));
}
#[test]
fn rejects_bad_version() {
let mut buf = MetaHeader::fresh(0).encode();
buf[16] = 99;
assert!(matches!(
MetaHeader::decode(&buf),
Err(Error::VersionMismatch { .. })
));
}
#[test]
fn rejects_bad_crc() {
let mut buf = MetaHeader::fresh(0).encode();
buf[24] ^= 0x01; assert!(matches!(
MetaHeader::decode(&buf),
Err(Error::Corrupted { .. })
));
}
}