Skip to main content

ese_core/
lib.rs

1//! ESE (Extensible Storage Engine) database reader foundation.
2//!
3//! Provides low-level access to ESE/JET Blue database files such as
4//! `SRUDB.dat`, `WebCacheV01.dat`, and Active Directory's `ntds.dit`.
5
6pub mod catalog;
7pub mod database;
8pub mod error;
9pub mod header;
10pub mod page;
11pub mod record;
12
13pub use catalog::CatalogEntry;
14pub use database::{EseDatabase, TableCursor};
15pub use error::EseError;
16pub use header::{
17    EseHeader, DB_STATE_CLEAN_SHUTDOWN, DB_STATE_DIRTY_SHUTDOWN, DB_STATE_JUST_CREATED,
18};
19pub use page::{
20    EsePage, EsePageHeader, PAGE_FLAG_EMPTY, PAGE_FLAG_LEAF, PAGE_FLAG_LONG_VALUE,
21    PAGE_FLAG_PARENT, PAGE_FLAG_ROOT, PAGE_FLAG_SPACE_TREE, PAGE_SIZE,
22};
23pub use record::{coltyp, decode_record, fixed_col_size, verify_page_checksum, ChecksumResult, ColumnDef, EseValue};
24
25/// Open an ESE database file and return the parsed header.
26///
27/// # Errors
28///
29/// Returns [`EseError`] if the file cannot be read or is not a valid ESE database.
30pub fn open(path: &std::path::Path) -> Result<EseHeader, EseError> {
31    use std::io::Read as _;
32    let mut f = std::fs::File::open(path)?;
33    let mut buf = vec![0u8; EseHeader::SIZE];
34    let n = f.read(&mut buf)?;
35    if n < EseHeader::SIZE {
36        return Err(EseError::Corrupt {
37            page: 0,
38            detail: format!("file too short: need {} bytes, got {}", EseHeader::SIZE, n),
39        });
40    }
41    EseHeader::from_bytes(&buf)
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use std::io::Write as _;
48    use tempfile::NamedTempFile;
49
50    /// Build a minimal valid ESE file in a temp file.
51    ///
52    /// `extra_pages` controls how many data pages (beyond the header page)
53    /// are appended. Each data page's first byte is set to its 1-based page
54    /// number so tests can verify the correct page was returned.
55    fn make_test_ese_file(extra_pages: usize) -> NamedTempFile {
56        let page_size: usize = 4096;
57        let mut buf = vec![0u8; page_size * (1 + extra_pages)];
58        // Header page: ESE magic at offset 4
59        buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
60        // page_size field at offset 0xEC = 236
61        buf[236..240].copy_from_slice(&(u32::try_from(page_size).unwrap()).to_le_bytes());
62        // Each data page: first byte = 1-based page number for easy identification
63        for i in 0..extra_pages {
64            buf[page_size * (i + 1)] = u8::try_from(i + 1).unwrap_or(u8::MAX);
65        }
66        let mut tmp = NamedTempFile::new().expect("tempfile");
67        tmp.write_all(&buf).expect("write test ESE file");
68        tmp
69    }
70
71    // ── EseDatabase::open ────────────────────────────────────────────────────
72
73    #[test]
74    fn database_open_valid_ese_file_succeeds() {
75        let tmp = make_test_ese_file(0);
76        let db = EseDatabase::open(tmp.path()).expect("should open valid ESE file");
77        assert_eq!(db.header.page_size, 4096);
78    }
79
80    #[test]
81    fn database_open_nonexistent_returns_err() {
82        let result = EseDatabase::open(std::path::Path::new("/nonexistent/x.dat"));
83        assert!(result.is_err());
84    }
85
86    #[test]
87    fn database_open_empty_file_returns_err() {
88        let tmp = NamedTempFile::new().expect("tempfile");
89        let result = EseDatabase::open(tmp.path());
90        assert!(result.is_err());
91    }
92
93    // ── EseDatabase::read_page ───────────────────────────────────────────────
94
95    #[test]
96    fn database_read_page_1_returns_correct_data() {
97        // 2 extra data pages; page 1's first byte should be 1
98        let tmp = make_test_ese_file(2);
99        let db = EseDatabase::open(tmp.path()).expect("open");
100        let page = db.read_page(1).expect("read page 1");
101        assert_eq!(page.page_number, 1);
102        assert_eq!(page.data.len(), 4096);
103        assert_eq!(page.data[0], 1u8, "first byte of page 1 should be 1");
104    }
105
106    #[test]
107    fn database_read_page_2_returns_correct_data() {
108        let tmp = make_test_ese_file(2);
109        let db = EseDatabase::open(tmp.path()).expect("open");
110        let page = db.read_page(2).expect("read page 2");
111        assert_eq!(page.page_number, 2);
112        assert_eq!(page.data[0], 2u8, "first byte of page 2 should be 2");
113    }
114
115    #[test]
116    fn database_read_page_beyond_eof_returns_err() {
117        // Only 1 data page exists (page 1); reading page 5 should fail
118        let tmp = make_test_ese_file(1);
119        let db = EseDatabase::open(tmp.path()).expect("open");
120        let result = db.read_page(5);
121        assert!(result.is_err(), "page beyond EOF must return Err");
122    }
123
124    // ── EseDatabase::page_count ──────────────────────────────────────────────
125
126    #[test]
127    fn database_page_count_header_only() {
128        let tmp = make_test_ese_file(0); // header only = 1 page
129        let db = EseDatabase::open(tmp.path()).expect("open");
130        assert_eq!(db.page_count(), 1u64);
131    }
132
133    #[test]
134    fn database_page_count_with_data_pages() {
135        let tmp = make_test_ese_file(3); // header + 3 data pages = 4 total
136        let db = EseDatabase::open(tmp.path()).expect("open");
137        assert_eq!(db.page_count(), 4u64);
138    }
139
140    #[test]
141    fn open_nonexistent_returns_err() {
142        let result = open(std::path::Path::new("/nonexistent/SRUDB.dat"));
143        assert!(result.is_err(), "opening nonexistent file must return Err");
144    }
145
146    #[test]
147    fn open_empty_file_returns_err() {
148        let tmp = NamedTempFile::new().expect("tempfile");
149        let result = open(tmp.path());
150        assert!(result.is_err(), "empty file must return Err (too short)");
151    }
152
153    #[test]
154    fn ese_header_parses_db_time() {
155        let mut buf = vec![0u8; 4096];
156        buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
157        buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
158        // db_time at offset 0x10: set to a known value
159        buf[0x10..0x18].copy_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());
160        let header = EseHeader::from_bytes(&buf).expect("valid header");
161        assert_eq!(header.db_time, 0x0102_0304_0506_0708_u64);
162    }
163
164    #[test]
165    fn ese_header_parses_db_state_dirty() {
166        let mut buf = vec![0u8; 4096];
167        buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
168        buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
169        // db_state = DirtyShutdown (2) at offset 0x28
170        buf[0x28..0x2C].copy_from_slice(&DB_STATE_DIRTY_SHUTDOWN.to_le_bytes());
171        let header = EseHeader::from_bytes(&buf).expect("valid header");
172        assert_eq!(header.db_state, DB_STATE_DIRTY_SHUTDOWN);
173    }
174
175    #[test]
176    fn ese_header_parses_db_state_clean() {
177        let mut buf = vec![0u8; 4096];
178        buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
179        buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
180        buf[0x28..0x2C].copy_from_slice(&DB_STATE_CLEAN_SHUTDOWN.to_le_bytes());
181        let header = EseHeader::from_bytes(&buf).expect("valid header");
182        assert_eq!(header.db_state, DB_STATE_CLEAN_SHUTDOWN);
183    }
184
185    #[test]
186    fn ese_header_has_page_size_field() {
187        // Construct a minimal valid ESE header buffer
188        let mut buf = vec![0u8; 4096];
189        // Signature at offset 4
190        buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
191        // Page size at offset 236 (0xEC): set to 4096
192        buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
193        let header = EseHeader::from_bytes(&buf).expect("valid header");
194        assert_eq!(header.page_size, 4096);
195    }
196
197    #[test]
198    fn ese_page_has_page_number() {
199        let page = EsePage {
200            page_number: 42,
201            data: vec![],
202        };
203        assert_eq!(page.page_number, 42);
204    }
205
206    #[test]
207    fn ese_error_implements_display() {
208        let err = EseError::Corrupt {
209            page: 0,
210            detail: "file too short: need 4096 bytes, got 0".into(),
211        };
212        let msg = format!("{err}");
213        assert!(msg.contains("too short"));
214    }
215}