ese_core/header.rs
1//! ESE database header (first 4096 bytes of the file).
2
3use crate::error::EseError;
4
5/// Database state constants (offset 0x28 in file header).
6pub const DB_STATE_JUST_CREATED: u32 = 1;
7/// Database closed uncleanly; a transaction log replay is required.
8pub const DB_STATE_DIRTY_SHUTDOWN: u32 = 2;
9/// Database closed cleanly.
10pub const DB_STATE_CLEAN_SHUTDOWN: u32 = 3;
11
12/// Parsed ESE database file header.
13#[derive(Debug, Clone)]
14pub struct EseHeader {
15 /// ESE magic: `0x89ABCDEF` (little-endian at offset 4).
16 pub signature: u32,
17 /// Database format version.
18 pub format_version: u32,
19 /// Page size in bytes (typically 4096 or 8192).
20 pub page_size: u32,
21 /// Database time field (offset `0x10`, 8 bytes LE).
22 ///
23 /// Stored as a `JET_LOGTIME`-derived u64. The low 32 bits are compared
24 /// against per-page `db_time` values to detect timestamp skew.
25 pub db_time: u64,
26 /// Database state (offset `0x28`).
27 ///
28 /// `2` = dirty shutdown (log replay needed), `3` = clean shutdown.
29 pub db_state: u32,
30}
31
32impl EseHeader {
33 /// Size of the ESE header block (first full page).
34 pub const SIZE: usize = 4096;
35
36 /// Parse an ESE header from the first 4096 bytes of a database file.
37 ///
38 /// ESE header layout (all little-endian):
39 /// - Offset 0x00 (4 bytes): checksum
40 /// - Offset 0x04 (4 bytes): signature (0x89ABCDEF)
41 /// - Offset 0x08 (4 bytes): format version
42 /// - Offset 0x0C (4 bytes): format revision
43 /// - Offset 0xEC (4 bytes): page size (0 means 4096)
44 pub fn from_bytes(data: &[u8]) -> Result<Self, EseError> {
45 if data.len() < Self::SIZE {
46 return Err(EseError::Corrupt {
47 page: 0,
48 detail: format!(
49 "file too short: need {} bytes, got {}",
50 Self::SIZE,
51 data.len()
52 ),
53 });
54 }
55 let sig = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
56 if sig != 0x89AB_CDEF {
57 return Err(EseError::InvalidMagic {
58 page: 0,
59 found: sig,
60 });
61 }
62 let format_version = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
63 // db_time at offset 0x10 (8 bytes LE)
64 let db_time = u64::from_le_bytes([
65 data[0x10], data[0x11], data[0x12], data[0x13], data[0x14], data[0x15], data[0x16],
66 data[0x17],
67 ]);
68 // db_state at offset 0x28 (4 bytes LE)
69 let db_state = u32::from_le_bytes([data[0x28], data[0x29], data[0x2A], data[0x2B]]);
70 // Page size is at offset 0xEC = 236.
71 // 0 is a special sentinel meaning "use the default 4096".
72 let raw_page_size = u32::from_le_bytes([data[236], data[237], data[238], data[239]]);
73 let page_size = if raw_page_size == 0 {
74 4096
75 } else {
76 raw_page_size
77 };
78 // Guard: only the four power-of-two sizes that ESE actually uses are valid.
79 // Any other value indicates a corrupt or crafted header.
80 const VALID_PAGE_SIZES: [u32; 4] = [4096, 8192, 16384, 32768];
81 if !VALID_PAGE_SIZES.contains(&page_size) {
82 return Err(EseError::Corrupt {
83 page: 0,
84 detail: format!(
85 "invalid page_size {page_size}: must be one of 4096, 8192, 16384, 32768"
86 ),
87 });
88 }
89 Ok(EseHeader {
90 signature: sig,
91 format_version,
92 page_size,
93 db_time,
94 db_state,
95 })
96 }
97}