1pub 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
25pub 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 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 buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
60 buf[236..240].copy_from_slice(&(u32::try_from(page_size).unwrap()).to_le_bytes());
62 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 #[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 #[test]
96 fn database_read_page_1_returns_correct_data() {
97 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 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 #[test]
127 fn database_page_count_header_only() {
128 let tmp = make_test_ese_file(0); 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); 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 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 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 let mut buf = vec![0u8; 4096];
189 buf[4..8].copy_from_slice(&0x89AB_CDEF_u32.to_le_bytes());
191 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}