pub mod catalog;
pub mod database;
pub mod error;
pub mod header;
pub mod page;
pub mod record;
pub use catalog::CatalogEntry;
pub use database::{EseDatabase, TableCursor};
pub use error::EseError;
pub use header::{
EseHeader, DB_STATE_CLEAN_SHUTDOWN, DB_STATE_DIRTY_SHUTDOWN, DB_STATE_JUST_CREATED,
};
pub use page::{
EsePage, EsePageHeader, PAGE_FLAG_EMPTY, PAGE_FLAG_LEAF, PAGE_FLAG_LONG_VALUE,
PAGE_FLAG_PARENT, PAGE_FLAG_ROOT, PAGE_FLAG_SPACE_TREE, PAGE_SIZE,
};
pub use record::{coltyp, decode_record, fixed_col_size, verify_page_checksum, ChecksumResult, ColumnDef, EseValue};
pub fn open(path: &std::path::Path) -> Result<EseHeader, EseError> {
use std::io::Read as _;
let mut f = std::fs::File::open(path)?;
let mut buf = vec![0u8; EseHeader::SIZE];
let n = f.read(&mut buf)?;
if n < EseHeader::SIZE {
return Err(EseError::Corrupt {
page: 0,
detail: format!("file too short: need {} bytes, got {}", EseHeader::SIZE, n),
});
}
EseHeader::from_bytes(&buf)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use tempfile::NamedTempFile;
fn make_test_ese_file(extra_pages: usize) -> NamedTempFile {
let page_size: usize = 4096;
let mut buf = vec![0u8; page_size * (1 + extra_pages)];
buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
buf[236..240].copy_from_slice(&(u32::try_from(page_size).unwrap()).to_le_bytes());
for i in 0..extra_pages {
buf[page_size * (i + 1)] = u8::try_from(i + 1).unwrap_or(u8::MAX);
}
let mut tmp = NamedTempFile::new().expect("tempfile");
tmp.write_all(&buf).expect("write test ESE file");
tmp
}
#[test]
fn database_open_valid_ese_file_succeeds() {
let tmp = make_test_ese_file(0);
let db = EseDatabase::open(tmp.path()).expect("should open valid ESE file");
assert_eq!(db.header.page_size, 4096);
}
#[test]
fn database_open_nonexistent_returns_err() {
let result = EseDatabase::open(std::path::Path::new("/nonexistent/x.dat"));
assert!(result.is_err());
}
#[test]
fn database_open_empty_file_returns_err() {
let tmp = NamedTempFile::new().expect("tempfile");
let result = EseDatabase::open(tmp.path());
assert!(result.is_err());
}
#[test]
fn database_read_page_1_returns_correct_data() {
let tmp = make_test_ese_file(2);
let db = EseDatabase::open(tmp.path()).expect("open");
let page = db.read_page(1).expect("read page 1");
assert_eq!(page.page_number, 1);
assert_eq!(page.data.len(), 4096);
assert_eq!(page.data[0], 1u8, "first byte of page 1 should be 1");
}
#[test]
fn database_read_page_2_returns_correct_data() {
let tmp = make_test_ese_file(2);
let db = EseDatabase::open(tmp.path()).expect("open");
let page = db.read_page(2).expect("read page 2");
assert_eq!(page.page_number, 2);
assert_eq!(page.data[0], 2u8, "first byte of page 2 should be 2");
}
#[test]
fn database_read_page_beyond_eof_returns_err() {
let tmp = make_test_ese_file(1);
let db = EseDatabase::open(tmp.path()).expect("open");
let result = db.read_page(5);
assert!(result.is_err(), "page beyond EOF must return Err");
}
#[test]
fn database_page_count_header_only() {
let tmp = make_test_ese_file(0); let db = EseDatabase::open(tmp.path()).expect("open");
assert_eq!(db.page_count(), 1u64);
}
#[test]
fn database_page_count_with_data_pages() {
let tmp = make_test_ese_file(3); let db = EseDatabase::open(tmp.path()).expect("open");
assert_eq!(db.page_count(), 4u64);
}
#[test]
fn open_nonexistent_returns_err() {
let result = open(std::path::Path::new("/nonexistent/SRUDB.dat"));
assert!(result.is_err(), "opening nonexistent file must return Err");
}
#[test]
fn open_empty_file_returns_err() {
let tmp = NamedTempFile::new().expect("tempfile");
let result = open(tmp.path());
assert!(result.is_err(), "empty file must return Err (too short)");
}
#[test]
fn ese_header_parses_db_time() {
let mut buf = vec![0u8; 4096];
buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
buf[0x10..0x18].copy_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());
let header = EseHeader::from_bytes(&buf).expect("valid header");
assert_eq!(header.db_time, 0x0102_0304_0506_0708_u64);
}
#[test]
fn ese_header_parses_db_state_dirty() {
let mut buf = vec![0u8; 4096];
buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
buf[0x28..0x2C].copy_from_slice(&DB_STATE_DIRTY_SHUTDOWN.to_le_bytes());
let header = EseHeader::from_bytes(&buf).expect("valid header");
assert_eq!(header.db_state, DB_STATE_DIRTY_SHUTDOWN);
}
#[test]
fn ese_header_parses_db_state_clean() {
let mut buf = vec![0u8; 4096];
buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
buf[0x28..0x2C].copy_from_slice(&DB_STATE_CLEAN_SHUTDOWN.to_le_bytes());
let header = EseHeader::from_bytes(&buf).expect("valid header");
assert_eq!(header.db_state, DB_STATE_CLEAN_SHUTDOWN);
}
#[test]
fn ese_header_has_page_size_field() {
let mut buf = vec![0u8; 4096];
buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
buf[236..240].copy_from_slice(&4096_u32.to_le_bytes());
let header = EseHeader::from_bytes(&buf).expect("valid header");
assert_eq!(header.page_size, 4096);
}
#[test]
fn ese_page_has_page_number() {
let page = EsePage {
page_number: 42,
data: vec![],
};
assert_eq!(page.page_number, 42);
}
#[test]
fn ese_error_implements_display() {
let err = EseError::Corrupt {
page: 0,
detail: "file too short: need 4096 bytes, got 0".into(),
};
let msg = format!("{err}");
assert!(msg.contains("too short"));
}
}