use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
pub const ESE_FILE_SIGNATURE: u32 = 0x89AB_CDEF;
pub mod file_type {
pub const DATABASE: u32 = 0;
pub const STREAMING_FILE: u32 = 1;
}
pub mod page_flag {
pub const ROOT: u32 = 0x0000_0001;
pub const LEAF: u32 = 0x0000_0002;
pub const PARENT: u32 = 0x0000_0004;
pub const EMPTY: u32 = 0x0000_0008;
pub const SPACE_TREE: u32 = 0x0000_0020;
pub const INDEX: u32 = 0x0000_0040;
pub const LONG_VALUE: u32 = 0x0000_0080;
pub const NEW_RECORD_FORMAT: u32 = 0x0000_2000;
pub const SCRUBBED: u32 = 0x0000_4000;
pub const NEW_CHECKSUM_FORMAT: u32 = 0x0000_8000;
}
pub struct EseFile {
file: File,
format_version: u32,
format_revision: u32,
page_size: u32,
file_type: u32,
previous_incremental_backup_log_gen: u32,
previous_full_backup_log_gen: u32,
current_full_backup_log_gen: u32,
}
impl EseFile {
pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
let mut file = File::open(path).map_err(Error::Io)?;
let mut hdr = [0u8; 668];
file.read_exact(&mut hdr).map_err(Error::Io)?;
let parsed = Self::parse_header_fields(&hdr)?;
Ok(EseFile {
file,
format_version: parsed.format_version,
format_revision: parsed.format_revision,
page_size: parsed.page_size,
file_type: parsed.file_type,
previous_incremental_backup_log_gen: parsed.previous_incremental_backup_log_gen,
previous_full_backup_log_gen: parsed.previous_full_backup_log_gen,
current_full_backup_log_gen: parsed.current_full_backup_log_gen,
})
}
pub fn parse_header(hdr: &[u8]) -> Result<HeaderFields, Error> {
Self::parse_header_fields(hdr)
}
fn parse_header_fields(hdr: &[u8]) -> Result<HeaderFields, Error> {
if hdr.len() < 668 {
return Err(Error::Format(format!(
"header too short: {} bytes (want 668)",
hdr.len()
)));
}
let magic = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
if magic != ESE_FILE_SIGNATURE {
return Err(Error::Format(format!(
"bad ESE magic: got {magic:#010x}, want {ESE_FILE_SIGNATURE:#010x}"
)));
}
let format_version = u32::from_le_bytes([hdr[8], hdr[9], hdr[10], hdr[11]]);
let file_type = u32::from_le_bytes([hdr[12], hdr[13], hdr[14], hdr[15]]);
let page_size = u32::from_le_bytes([hdr[236], hdr[237], hdr[238], hdr[239]]);
if ![2048, 4096, 8192, 16384, 32768].contains(&page_size) {
return Err(Error::Format(format!("odd page size: {page_size}")));
}
let format_revision = u32::from_le_bytes([hdr[240], hdr[241], hdr[242], hdr[243]]);
let previous_full_backup_log_gen =
u32::from_le_bytes([hdr[256], hdr[257], hdr[258], hdr[259]]);
let previous_incremental_backup_log_gen =
u32::from_le_bytes([hdr[268], hdr[269], hdr[270], hdr[271]]);
let current_full_backup_log_gen =
u32::from_le_bytes([hdr[280], hdr[281], hdr[282], hdr[283]]);
Ok(HeaderFields {
format_version,
format_revision,
page_size,
file_type,
previous_full_backup_log_gen,
previous_incremental_backup_log_gen,
current_full_backup_log_gen,
})
}
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn format_revision(&self) -> u32 {
self.format_revision
}
pub fn page_size(&self) -> u32 {
self.page_size
}
pub fn file_type(&self) -> u32 {
self.file_type
}
pub fn previous_full_backup_log_gen(&self) -> u32 {
self.previous_full_backup_log_gen
}
pub fn previous_incremental_backup_log_gen(&self) -> u32 {
self.previous_incremental_backup_log_gen
}
pub fn current_full_backup_log_gen(&self) -> u32 {
self.current_full_backup_log_gen
}
pub fn page(&mut self, page_number: u32) -> Result<RawPage, Error> {
let offset = page_number as u64 * self.page_size as u64;
self.file.seek(SeekFrom::Start(offset)).map_err(Error::Io)?;
let mut buf = vec![0u8; self.page_size as usize];
self.file.read_exact(&mut buf).map_err(Error::Io)?;
RawPage::parse(page_number, &buf, self.page_size)
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct HeaderFields {
pub format_version: u32,
pub format_revision: u32,
pub page_size: u32,
pub file_type: u32,
pub previous_full_backup_log_gen: u32,
pub previous_incremental_backup_log_gen: u32,
pub current_full_backup_log_gen: u32,
}
#[derive(Clone)]
#[non_exhaustive]
pub struct RawPage {
pub page_number: u32,
pub page_flags: u32,
pub previous_page: u32,
pub next_page: u32,
pub father_data_page: u32,
pub object_id: u32,
pub available_data_size: u16,
pub available_page_tag: u16,
pub bytes: Vec<u8>,
}
impl std::fmt::Debug for RawPage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawPage")
.field("page_number", &self.page_number)
.field("page_flags", &format_args!("{:#010x}", self.page_flags))
.field("previous_page", &self.previous_page)
.field("next_page", &self.next_page)
.field("father_data_page", &self.father_data_page)
.field("object_id", &self.object_id)
.field("available_data_size", &self.available_data_size)
.field("available_page_tag", &self.available_page_tag)
.field("bytes_len", &self.bytes.len())
.finish()
}
}
impl RawPage {
pub fn parse(page_number: u32, bytes: &[u8], page_size: u32) -> Result<Self, Error> {
if bytes.len() as u32 != page_size {
return Err(Error::Format(format!(
"page {page_number}: got {} bytes, expected {page_size}",
bytes.len()
)));
}
if page_size < 48 {
return Err(Error::Format(format!("page size too small: {page_size}")));
}
let previous_page = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
let next_page = u32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]);
let father_data_page = u32::from_le_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]);
let available_data_size = u16::from_le_bytes([bytes[32], bytes[33]]);
let available_page_tag = u16::from_le_bytes([bytes[38], bytes[39]]);
let page_flags = u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]);
let object_id = u32::from_le_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]);
Ok(RawPage {
page_number,
page_flags,
previous_page,
next_page,
father_data_page,
object_id,
available_data_size,
available_page_tag,
bytes: bytes.to_vec(),
})
}
pub fn has_flag(&self, flag: u32) -> bool {
(self.page_flags & flag) == flag
}
pub fn is_leaf(&self) -> bool {
self.has_flag(page_flag::LEAF)
}
pub fn is_root(&self) -> bool {
self.has_flag(page_flag::ROOT)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Io(std::io::Error),
Format(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(e) => write!(f, "io: {e}"),
Error::Format(s) => write!(f, "format: {s}"),
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_header(page_size: u32) -> Vec<u8> {
let mut hdr = vec![0u8; 668];
hdr[4..8].copy_from_slice(&ESE_FILE_SIGNATURE.to_le_bytes());
hdr[8..12].copy_from_slice(&620u32.to_le_bytes()); hdr[12..16].copy_from_slice(&file_type::DATABASE.to_le_bytes());
hdr[236..240].copy_from_slice(&page_size.to_le_bytes());
hdr[240..244].copy_from_slice(&17u32.to_le_bytes()); hdr
}
#[test]
fn header_accepts_valid_synthetic() {
let hdr = synthetic_header(8192);
let h = EseFile::parse_header(&hdr).unwrap();
assert_eq!(h.page_size, 8192);
assert_eq!(h.format_version, 620);
assert_eq!(h.format_revision, 17);
assert_eq!(h.file_type, file_type::DATABASE);
}
#[test]
fn header_rejects_short() {
assert!(matches!(
EseFile::parse_header(&[0u8; 300]).unwrap_err(),
Error::Format(_)
));
}
#[test]
fn header_rejects_bad_magic() {
let mut hdr = synthetic_header(8192);
hdr[4..8].copy_from_slice(&0xdeadbeefu32.to_le_bytes());
assert!(matches!(
EseFile::parse_header(&hdr).unwrap_err(),
Error::Format(_)
));
}
#[test]
fn header_rejects_odd_page_size() {
let hdr = synthetic_header(1234);
assert!(matches!(
EseFile::parse_header(&hdr).unwrap_err(),
Error::Format(_)
));
}
#[test]
fn page_flags_have_expected_bits() {
assert_eq!(page_flag::ROOT, 0x1);
assert_eq!(page_flag::LEAF, 0x2);
assert_eq!(page_flag::PARENT, 0x4);
assert_eq!(page_flag::LONG_VALUE, 0x80);
}
#[test]
fn raw_page_parse_reads_header_fields() {
let mut bytes = vec![0u8; 4096];
bytes[20..24].copy_from_slice(&100u32.to_le_bytes()); bytes[24..28].copy_from_slice(&102u32.to_le_bytes()); bytes[28..32].copy_from_slice(&2u32.to_le_bytes()); bytes[32..34].copy_from_slice(&512u16.to_le_bytes()); bytes[38..40].copy_from_slice(&5u16.to_le_bytes()); bytes[40..44]
.copy_from_slice(&(page_flag::LEAF | page_flag::NEW_RECORD_FORMAT).to_le_bytes());
bytes[44..48].copy_from_slice(&42u32.to_le_bytes()); let page = RawPage::parse(101, &bytes, 4096).unwrap();
assert_eq!(page.page_number, 101);
assert_eq!(page.previous_page, 100);
assert_eq!(page.next_page, 102);
assert_eq!(page.available_data_size, 512);
assert_eq!(page.available_page_tag, 5);
assert_eq!(page.object_id, 42);
assert!(page.is_leaf());
assert!(!page.is_root());
assert!(page.has_flag(page_flag::NEW_RECORD_FORMAT));
}
#[test]
fn raw_page_rejects_size_mismatch() {
let bytes = vec![0u8; 100];
assert!(matches!(
RawPage::parse(1, &bytes, 4096).unwrap_err(),
Error::Format(_)
));
}
}