Skip to main content

ese_core/
page.rs

1//! ESE database page types.
2
3use crate::EseError;
4
5/// Default ESE page size in bytes (Vista+).
6pub const PAGE_SIZE: usize = 4096;
7
8/// Page flag: root page.
9pub const PAGE_FLAG_ROOT: u32 = 0x0001;
10/// Page flag: leaf page.
11pub const PAGE_FLAG_LEAF: u32 = 0x0002;
12/// Page flag: parent (internal B-tree node).
13pub const PAGE_FLAG_PARENT: u32 = 0x0004;
14/// Page flag: empty page.
15pub const PAGE_FLAG_EMPTY: u32 = 0x0008;
16/// Page flag: space tree page (OWN_EXT / AVAIL_EXT).
17pub const PAGE_FLAG_SPACE_TREE: u32 = 0x0020;
18/// Page flag: long-value page (stores overflow data for LongBinary/LongText columns).
19pub const PAGE_FLAG_LONG_VALUE: u32 = 0x0400;
20
21/// Parsed ESE page header fields (Vista+ "new" 40-byte format).
22#[derive(Debug, Clone)]
23pub struct EsePageHeader {
24    /// Previous page number; `None` when `0xFFFF_FFFF`.
25    pub prev_page: Option<u32>,
26    /// Next page number; `None` when `0xFFFF_FFFF`.
27    pub next_page: Option<u32>,
28    /// Father data page object ID.
29    pub fdp_object_id: u32,
30    /// Available data size.
31    pub available_data_size: u16,
32    /// Available uncommitted data.
33    pub available_uncommitted: u16,
34    /// Available data offset.
35    pub available_data_offset: u16,
36    /// Number of page tags (includes tag 0).
37    pub available_page_tag_count: u16,
38    /// Page flags.
39    pub page_flags: u32,
40}
41
42/// A single ESE database page.
43#[derive(Debug, Clone)]
44pub struct EsePage {
45    /// Page number (1-based).
46    pub page_number: u32,
47    /// Raw page data. Parse flags via [`EsePage::parse_header`].
48    pub data: Vec<u8>,
49}
50
51impl EsePage {
52    /// Minimum size of the Vista+ page header.
53    pub const HEADER_SIZE: usize = 40;
54
55    /// Parse the Vista+ 40-byte page header from `self.data`.
56    ///
57    /// Layout (all little-endian):
58    /// - 0x00 (4): XOR checksum
59    /// - 0x04 (4): ECC checksum (Vista+ addition)
60    /// - 0x08 (8): database time (JET_DBTIME)
61    /// - 0x10 (4): previous page number
62    /// - 0x14 (4): next page number
63    /// - 0x18 (4): FDP object ID
64    /// - 0x1C (2): available data size
65    /// - 0x1E (2): available uncommitted data
66    /// - 0x20 (2): available data offset
67    /// - 0x22 (2): available page tag count
68    /// - 0x24 (4): page flags
69    ///
70    /// # Errors
71    ///
72    /// Returns [`EseError::Corrupt`] if `self.data` is shorter than 40 bytes.
73    pub fn parse_header(&self) -> Result<EsePageHeader, EseError> {
74        let d = &self.data;
75        if d.len() < Self::HEADER_SIZE {
76            return Err(EseError::Corrupt {
77                page: self.page_number,
78                detail: format!("page header too short: {} < {}", d.len(), Self::HEADER_SIZE),
79            });
80        }
81
82        let prev_raw = u32::from_le_bytes([d[0x10], d[0x11], d[0x12], d[0x13]]);
83        let next_raw = u32::from_le_bytes([d[0x14], d[0x15], d[0x16], d[0x17]]);
84        let fdp_object_id = u32::from_le_bytes([d[0x18], d[0x19], d[0x1A], d[0x1B]]);
85        let available_data_size = u16::from_le_bytes([d[0x1C], d[0x1D]]);
86        let available_uncommitted = u16::from_le_bytes([d[0x1E], d[0x1F]]);
87        let available_data_offset = u16::from_le_bytes([d[0x20], d[0x21]]);
88        let available_page_tag_count = u16::from_le_bytes([d[0x22], d[0x23]]);
89        let page_flags = u32::from_le_bytes([d[0x24], d[0x25], d[0x26], d[0x27]]);
90
91        Ok(EsePageHeader {
92            prev_page: if prev_raw == 0xFFFF_FFFF {
93                None
94            } else {
95                Some(prev_raw)
96            },
97            next_page: if next_raw == 0xFFFF_FFFF {
98                None
99            } else {
100                Some(next_raw)
101            },
102            fdp_object_id,
103            available_data_size,
104            available_uncommitted,
105            available_data_offset,
106            available_page_tag_count,
107            page_flags,
108        })
109    }
110
111    /// Return the tag entries from the end of the page.
112    ///
113    /// Tags are stored at the END of the page, growing downward.
114    /// Each tag is 4 bytes: bits 0-14 = value offset, bit 15 = flag,
115    /// bits 16-30 = value size, bit 31 = flag.
116    ///
117    /// Returns `Vec<(offset, size)>` for each tag.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`EseError::TagArrayOverflow`] if the page is too short to hold the
122    /// declared tag count.
123    pub fn tags(&self) -> Result<Vec<(u16, u16)>, EseError> {
124        let hdr = self.parse_header()?;
125        let count = hdr.available_page_tag_count as usize;
126        let page_size = self.data.len();
127        if page_size < count * 4 {
128            return Err(EseError::TagArrayOverflow {
129                page: self.page_number,
130            });
131        }
132        let mut tags = Vec::with_capacity(count);
133        for i in 0..count {
134            let tag_offset = page_size - (i + 1) * 4;
135            let raw = u32::from_le_bytes([
136                self.data[tag_offset],
137                self.data[tag_offset + 1],
138                self.data[tag_offset + 2],
139                self.data[tag_offset + 3],
140            ]);
141            // ESE TAG struct layout (MS-ESEDB §2.2.7.1):
142            //   cb_ (SIZE)   = low  16 bits (bits  0-12 are value, bits 13-15 are flags)
143            //   ib_ (OFFSET) = high 16 bits (bits 16-28 are value, bits 29-31 are flags)
144            // Mask 0x1FFF strips the flag bits from each 13-bit field.
145            let value_size = (raw & 0x1FFF) as u16;           // cb_ = SIZE
146            let value_offset = ((raw >> 16) & 0x1FFF) as u16; // ib_ = OFFSET
147            // Guard: absolute position = HEADER_SIZE + relative_offset + size must not exceed page.
148            let end = Self::HEADER_SIZE + usize::from(value_offset) + usize::from(value_size);
149            if end > self.data.len() {
150                return Err(EseError::RecordTooShort {
151                    page: self.page_number,
152                    tag: i,
153                    got: self.data.len(),
154                    need: end,
155                });
156            }
157            tags.push((value_offset, value_size));
158        }
159        Ok(tags)
160    }
161
162    /// Return the raw data area of the page: bytes from the end of the 40-byte
163    /// page header to the start of the tag array at the page end.
164    ///
165    /// This span contains ALL record bytes laid out sequentially, including bytes
166    /// that may fall before the smallest tag offset (common in ESE's cumulative
167    /// key-prefix-compression format).  Scanning this area directly (rather than
168    /// individual tags) is the correct way to read real ESE catalog pages.
169    ///
170    /// # Errors
171    ///
172    /// Returns [`EseError::Corrupt`] if the header cannot be parsed.
173    pub fn raw_data_area(&self) -> Result<&[u8], EseError> {
174        let hdr = self.parse_header()?;
175        let tag_count = hdr.available_page_tag_count as usize;
176        let tag_array_bytes = tag_count.saturating_mul(4);
177        let tag_array_start = self.data.len().saturating_sub(tag_array_bytes);
178        let start = Self::HEADER_SIZE.min(tag_array_start);
179        Ok(&self.data[start..tag_array_start])
180    }
181
182    /// Return the raw record data slice for tag at `index`.
183    ///
184    /// Tag 0 is the page header tag. Data records start at tag 1.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`EseError::Corrupt`] if `index` is beyond the tag count, or
189    /// [`EseError::RecordTooShort`] if the tag's data range is out of bounds.
190    pub fn record_data(&self, index: usize) -> Result<&[u8], EseError> {
191        let tags = self.tags()?;
192        if index >= tags.len() {
193            return Err(EseError::Corrupt {
194                page: self.page_number,
195                detail: format!("tag index {index} out of range (tag count: {})", tags.len()),
196            });
197        }
198        let (offset, size) = tags[index];
199        // Tag offsets are relative to the end of the 40-byte Vista+ page header.
200        let start = Self::HEADER_SIZE + offset as usize;
201        let end = start + size as usize;
202        if end > self.data.len() {
203            return Err(EseError::RecordTooShort {
204                page: self.page_number,
205                tag: index,
206                got: self.data.len(),
207                need: end,
208            });
209        }
210        Ok(&self.data[start..end])
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    // ── ese-page-header story ────────────────────────────────────────────────
219
220    #[test]
221    fn parse_header_page_flags_leaf() {
222        let mut data = vec![0u8; 4096];
223        // page_flags at offset 0x24 (Vista+)
224        data[0x24..0x28].copy_from_slice(&PAGE_FLAG_LEAF.to_le_bytes());
225        let page = EsePage {
226            page_number: 1,
227            data,
228        };
229        let hdr = page.parse_header().expect("parse header");
230        assert_eq!(hdr.page_flags, PAGE_FLAG_LEAF);
231        assert!(hdr.page_flags & PAGE_FLAG_LEAF != 0);
232    }
233
234    #[test]
235    fn page_flag_constants_match_ese_spec() {
236        assert_eq!(PAGE_FLAG_ROOT, 0x0001);
237        assert_eq!(PAGE_FLAG_LEAF, 0x0002);
238        assert_eq!(PAGE_FLAG_PARENT, 0x0004);
239        assert_eq!(PAGE_FLAG_EMPTY, 0x0008);
240        assert_eq!(PAGE_FLAG_SPACE_TREE, 0x0020);
241        assert_eq!(PAGE_FLAG_LONG_VALUE, 0x0400);
242    }
243
244    #[test]
245    fn parse_header_prev_next_links() {
246        let mut data = vec![0u8; 4096];
247        data[0x10..0x14].copy_from_slice(&5u32.to_le_bytes()); // prev_page = 5 (Vista+: 0x10)
248        data[0x14..0x18].copy_from_slice(&7u32.to_le_bytes()); // next_page = 7 (Vista+: 0x14)
249        let page = EsePage {
250            page_number: 3,
251            data,
252        };
253        let hdr = page.parse_header().expect("parse header");
254        assert_eq!(hdr.prev_page, Some(5));
255        assert_eq!(hdr.next_page, Some(7));
256    }
257
258    #[test]
259    fn parse_header_none_when_0xffffffff() {
260        let mut data = vec![0u8; 4096];
261        data[0x10..0x14].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); // prev (Vista+: 0x10)
262        data[0x14..0x18].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); // next (Vista+: 0x14)
263        let page = EsePage {
264            page_number: 1,
265            data,
266        };
267        let hdr = page.parse_header().expect("parse header");
268        assert_eq!(hdr.prev_page, None);
269        assert_eq!(hdr.next_page, None);
270    }
271
272    #[test]
273    fn parse_header_short_buffer_returns_err() {
274        let data = vec![0u8; 20]; // shorter than 40 bytes
275        let page = EsePage {
276            page_number: 1,
277            data,
278        };
279        let result = page.parse_header();
280        assert!(result.is_err(), "short buffer must return Err");
281    }
282
283    // ── Vista+ offset regression tests (real SRUDB.dat ground-truth) ─────────
284
285    #[test]
286    fn parse_header_tag_count_at_vista_offset_0x22() {
287        // Vista+ page header: available_page_tag_count is at 0x22 (not 0x1E).
288        // Writing 5 at 0x22 must be read back as 5; writing at 0x1E must NOT.
289        let mut data = vec![0u8; 4096];
290        data[0x22..0x24].copy_from_slice(&5u16.to_le_bytes());
291        let page = EsePage { page_number: 1, data };
292        let hdr = page.parse_header().expect("parse header");
293        assert_eq!(hdr.available_page_tag_count, 5,
294            "tag count must be read from offset 0x22 per the Vista+ ESE format");
295    }
296
297    #[test]
298    fn parse_header_flags_at_vista_offset_0x24() {
299        // Vista+ page header: page_flags is at 0x24 (not 0x20).
300        let mut data = vec![0u8; 4096];
301        data[0x24..0x28].copy_from_slice(&PAGE_FLAG_LEAF.to_le_bytes());
302        let page = EsePage { page_number: 1, data };
303        let hdr = page.parse_header().expect("parse header");
304        assert!(hdr.page_flags & PAGE_FLAG_LEAF != 0,
305            "LEAF flag must be read from offset 0x24 per the Vista+ ESE format");
306    }
307
308    #[test]
309    fn parse_header_prev_next_at_vista_offsets_0x10_0x14() {
310        // Vista+ page header: prev_page at 0x10, next_page at 0x14.
311        let mut data = vec![0u8; 4096];
312        data[0x10..0x14].copy_from_slice(&5u32.to_le_bytes());
313        data[0x14..0x18].copy_from_slice(&9u32.to_le_bytes());
314        let page = EsePage { page_number: 1, data };
315        let hdr = page.parse_header().expect("parse header");
316        assert_eq!(hdr.prev_page, Some(5));
317        assert_eq!(hdr.next_page, Some(9));
318    }
319
320    // ── ese-page-tags story ──────────────────────────────────────────────────
321
322    /// Build a synthetic page with embedded data records.
323    ///
324    /// Layout:
325    /// - Header (40 bytes) at absolute bytes 0-39
326    /// - Records packed at absolute bytes 40+; tags store RELATIVE offsets (from header end)
327    /// - Tag array at page end: tag[0] covers header, tags[1..] cover records
328    fn make_page_with_records(page_size: usize, records: &[&[u8]]) -> Vec<u8> {
329        const HEADER_SIZE: usize = 40;
330        let mut d = vec![0u8; page_size];
331        let tag_count = u16::try_from(1 + records.len()).unwrap_or(u16::MAX);
332        // Vista+ header: tag_count at 0x22, PAGE_FLAG_LEAF at 0x24
333        d[0x22..0x24].copy_from_slice(&tag_count.to_le_bytes());
334        d[0x24..0x28].copy_from_slice(&PAGE_FLAG_LEAF.to_le_bytes());
335
336        // Tag 0: relative offset=0, size=40 (covers the page header)
337        write_tag(&mut d, page_size, 0, 0, 40);
338
339        // Data records; tags store RELATIVE offsets (physical = HEADER_SIZE + relative)
340        let mut cur_relative: u16 = 0;
341        for (i, rec) in records.iter().enumerate() {
342            let tag_idx = i + 1;
343            let rec_size = u16::try_from(rec.len()).unwrap_or(u16::MAX);
344            let absolute_start = HEADER_SIZE + usize::from(cur_relative);
345            d[absolute_start..absolute_start + rec.len()].copy_from_slice(rec);
346            write_tag(&mut d, page_size, tag_idx, cur_relative, rec_size);
347            cur_relative += rec_size;
348        }
349        d
350    }
351
352    fn write_tag(page: &mut [u8], page_size: usize, tag_idx: usize, offset: u16, size: u16) {
353        // Real ESE format: cb_ (size) in LOW 13 bits, ib_ (offset) in HIGH 13 bits.
354        let raw: u32 = (u32::from(size) & 0x1FFF) | ((u32::from(offset) & 0x1FFF) << 16);
355        let pos = page_size - (tag_idx + 1) * 4;
356        page[pos..pos + 4].copy_from_slice(&raw.to_le_bytes());
357    }
358
359    #[test]
360    fn tags_count_matches_header() {
361        // 2 data records => tag_count = 3 (tag0 + 2 data tags)
362        let data = make_page_with_records(4096, &[b"hello", b"world"]);
363        let page = EsePage {
364            page_number: 1,
365            data,
366        };
367        let tags = page.tags().expect("tags");
368        assert_eq!(tags.len(), 3);
369    }
370
371    #[test]
372    fn tags_first_record_offset_and_size() {
373        let data = make_page_with_records(4096, &[b"AAAA", b"BBBB"]);
374        let page = EsePage {
375            page_number: 1,
376            data,
377        };
378        let tags = page.tags().expect("tags");
379        // tag 1 is first data record; relative offset=0 (absolute=40), size=4
380        assert_eq!(tags[1], (0, 4));
381    }
382
383    #[test]
384    fn record_data_first_matches() {
385        let data = make_page_with_records(4096, &[b"AAAA", b"BBBB"]);
386        let page = EsePage {
387            page_number: 1,
388            data,
389        };
390        let rec = page.record_data(1).expect("record 1");
391        assert_eq!(rec, b"AAAA");
392    }
393
394    #[test]
395    fn record_data_second_matches() {
396        let data = make_page_with_records(4096, &[b"AAAA", b"BBBB"]);
397        let page = EsePage {
398            page_number: 1,
399            data,
400        };
401        let rec = page.record_data(2).expect("record 2");
402        assert_eq!(rec, b"BBBB");
403    }
404
405    #[test]
406    fn record_data_beyond_count_returns_err() {
407        let data = make_page_with_records(4096, &[b"X", b"Y"]);
408        let page = EsePage {
409            page_number: 1,
410            data,
411        };
412        let result = page.record_data(5);
413        assert!(result.is_err(), "index beyond tag count must return Err");
414    }
415
416    // ── real ESE tag field order (RED — cb_ in LOW bits, ib_ in HIGH bits) ──
417
418    /// ESE TAG struct: low 13 bits = cb_ (SIZE), high 13 bits = ib_ (OFFSET).
419    /// These two tests pin that contract against a page built with REAL ESE encoding.
420    /// They will FAIL until `tags()` is fixed to read size from LOW and offset from HIGH.
421    #[test]
422    fn tags_real_ese_format_size_in_low_bits_offset_in_high_bits() {
423        // raw = 4u32 means size=4 in LOW, offset=0 in HIGH (real ESE format).
424        // Buggy code reads: offset=4 (LOW), size=0 (HIGH) → returns empty.
425        // Fixed code reads: offset=0 (HIGH), size=4 (LOW) → returns sentinel.
426        let mut data = vec![0u8; 4096];
427        let sentinel = [0xDE, 0xAD, 0xBE, 0xEFu8];
428        data[40..44].copy_from_slice(&sentinel); // HEADER_SIZE=40, offset=0
429
430        data[0x22..0x24].copy_from_slice(&2u16.to_le_bytes()); // tag_count=2
431        data[0x24..0x28].copy_from_slice(&PAGE_FLAG_LEAF.to_le_bytes());
432        data[0x10..0x14].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
433        data[0x14..0x18].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
434        // Tag 0: real ESE — size=40 in LOW, offset=0 in HIGH → 40u32
435        data[4096 - 4..4096].copy_from_slice(&40u32.to_le_bytes());
436        // Tag 1: real ESE — size=4 in LOW, offset=0 in HIGH → 4u32
437        data[4096 - 8..4096 - 4].copy_from_slice(&4u32.to_le_bytes());
438
439        let page = EsePage { page_number: 1, data };
440        let (tag_offset, tag_size) = page.tags().expect("tags")[1];
441        assert_eq!(tag_size, 4, "cb_ (size) must be read from LOW 13 bits");
442        assert_eq!(tag_offset, 0, "ib_ (offset) must be read from HIGH 13 bits");
443        assert_eq!(
444            page.record_data(1).expect("record_data"),
445            &sentinel,
446            "real ESE tag: size in LOW bits, offset in HIGH bits"
447        );
448    }
449
450    #[test]
451    fn record_data_real_ese_nonsequential_offset() {
452        // Record 1 at offset=0, record 2 at offset=8 (non-sequential — 4-byte gap).
453        // Direct access (offset+size per tag) must return correct bytes for both.
454        let mut data = vec![0u8; 4096];
455        let rec1 = [0xAAu8; 4];
456        let rec2 = [0xBBu8; 4];
457        data[40..44].copy_from_slice(&rec1); // HEADER_SIZE + offset=0
458        data[48..52].copy_from_slice(&rec2); // HEADER_SIZE + offset=8
459
460        data[0x22..0x24].copy_from_slice(&3u16.to_le_bytes()); // tag_count=3
461        data[0x24..0x28].copy_from_slice(&PAGE_FLAG_LEAF.to_le_bytes());
462        data[0x10..0x14].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
463        data[0x14..0x18].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
464        // Tag 0: size=40, offset=0 → 40u32
465        data[4096 - 4..4096].copy_from_slice(&40u32.to_le_bytes());
466        // Tag 1: size=4, offset=0 → 4u32
467        data[4096 - 8..4096 - 4].copy_from_slice(&4u32.to_le_bytes());
468        // Tag 2: size=4, offset=8 → (8u32 << 16) | 4u32
469        data[4096 - 12..4096 - 8].copy_from_slice(&((8u32 << 16) | 4u32).to_le_bytes());
470
471        let page = EsePage { page_number: 1, data };
472        assert_eq!(
473            page.record_data(1).expect("rec1"),
474            &rec1,
475            "record 1 at offset=0"
476        );
477        assert_eq!(
478            page.record_data(2).expect("rec2"),
479            &rec2,
480            "record 2 at non-sequential offset=8"
481        );
482    }
483
484    // ── tag offset relative-to-header story ──────────────────────────────────
485
486    #[test]
487    fn record_data_treats_tag_offset_as_relative_to_header_end() {
488        // Per MS-ESEDB spec and libesedb/impacket: tag offsets are relative to
489        // the END of the page header (byte 40 for Vista+ pages), not absolute
490        // from page start.
491        //
492        // This test places sentinel bytes at absolute byte 42 (= HEADER_SIZE + 2)
493        // and gives tag 1 a relative offset of 2. Correct behaviour: record_data(1)
494        // returns the sentinel bytes. Wrong (absolute) behaviour: returns zeros
495        // from within the 40-byte header at byte 2.
496        let mut data = vec![0u8; 4096];
497        let sentinel = [0xCA, 0xFE, 0xBA, 0xBEu8];
498        // Place sentinel at absolute byte 42 = HEADER_SIZE(40) + relative_offset(2)
499        data[42..46].copy_from_slice(&sentinel);
500
501        let tag_count: u16 = 2;
502        data[0x22..0x24].copy_from_slice(&tag_count.to_le_bytes());
503        data[0x24..0x28].copy_from_slice(&PAGE_FLAG_LEAF.to_le_bytes());
504        data[0x10..0x14].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
505        data[0x14..0x18].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
506
507        // Real ESE format: cb_ (size) in LOW bits, ib_ (offset) in HIGH bits.
508        // Tag 0: size=40, offset=0 → raw = 40u32
509        let tag0: u32 = 40u32;
510        data[4096 - 4..4096].copy_from_slice(&tag0.to_le_bytes());
511
512        // Tag 1: size=4, offset=2 → absolute bytes [42..46]
513        let tag1: u32 = 4u32 | (2u32 << 16);
514        data[4096 - 8..4096 - 4].copy_from_slice(&tag1.to_le_bytes());
515
516        let page = EsePage { page_number: 1, data };
517        let rec = page.record_data(1).expect("record_data(1)");
518        assert_eq!(
519            rec,
520            &sentinel,
521            "record_data must add HEADER_SIZE(40) to tag offset; \
522             relative offset 2 must read from absolute byte 42, not byte 2"
523        );
524    }
525}