Skip to main content

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}