use std::path::{Path, PathBuf};
use memmap2::Mmap;
use crate::{catalog::CatalogEntry, record::ColumnDef, EseError, EseHeader, EsePage};
#[derive(Debug)]
pub struct TableCursor<'db> {
db: &'db EseDatabase,
leaf_pages: Vec<u32>,
page_idx: usize,
tag_idx: usize, }
impl Iterator for TableCursor<'_> {
type Item = Result<(u32, usize, Vec<u8>), EseError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let &page_num = self.leaf_pages.get(self.page_idx)?;
let page = match self.db.read_page(page_num) {
Ok(p) => p,
Err(e) => {
self.page_idx += 1;
self.tag_idx = 1;
return Some(Err(e));
}
};
let tags = match page.tags() {
Ok(t) => t,
Err(e) => {
self.page_idx += 1;
self.tag_idx = 1;
return Some(Err(e));
}
};
if self.tag_idx >= tags.len() {
self.page_idx += 1;
self.tag_idx = 1;
continue;
}
let tag_idx = self.tag_idx;
self.tag_idx += 1;
let (tag_off, tag_sz) = tags[tag_idx];
if tag_sz == 0 {
continue; }
let start = EsePage::HEADER_SIZE.saturating_add(usize::from(tag_off));
let end = start.saturating_add(usize::from(tag_sz));
if end > page.data.len() {
return Some(Err(EseError::Corrupt {
page: page_num,
detail: format!(
"tag {tag_idx} out of bounds (offset={tag_off}, size={tag_sz}, \
page_len={})",
page.data.len()
),
}));
}
return Some(Ok((page_num, tag_idx, page.data[start..end].to_vec())));
}
}
}
pub struct EseDatabase {
path: PathBuf,
pub header: EseHeader,
mmap: Mmap,
}
impl std::fmt::Debug for EseDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EseDatabase")
.field("path", &self.path)
.field("header", &self.header)
.field("mmap_len", &self.mmap.len())
.finish()
}
}
impl EseDatabase {
pub fn open(path: &Path) -> Result<Self, EseError> {
let file = std::fs::File::open(path)?;
let mmap = unsafe { Mmap::map(&file) }?;
let header = EseHeader::from_bytes(&mmap)?;
Ok(Self {
path: path.to_owned(),
header,
mmap,
})
}
pub fn raw_page_slice(&self, page_number: u32) -> Result<&[u8], EseError> {
let page_size = self.header.page_size as usize;
let start = usize::try_from(page_number)
.unwrap_or(usize::MAX)
.saturating_mul(page_size);
let end = start.saturating_add(page_size);
if end > self.mmap.len() {
return Err(EseError::Corrupt {
page: page_number,
detail: format!(
"page beyond file end: need offset {end}, file is {} bytes",
self.mmap.len()
),
});
}
Ok(&self.mmap[start..end])
}
pub fn read_page(&self, page_number: u32) -> Result<EsePage, EseError> {
Ok(EsePage {
page_number,
data: self.raw_page_slice(page_number)?.to_vec(),
})
}
pub fn page_count(&self) -> u64 {
u64::try_from(self.mmap.len()).unwrap_or(0) / u64::from(self.header.page_size)
}
pub fn catalog_entries(&self) -> Result<Vec<CatalogEntry>, EseError> {
const CATALOG_ROOT: u32 = 5; let leaf_pages = self.walk_leaf_pages(CATALOG_ROOT)?;
let mut by_name: std::collections::HashMap<String, CatalogEntry> = Default::default();
for page_num in leaf_pages {
let page = self.read_page(page_num)?;
let real_entries = CatalogEntry::scan_catalog_page_data(page.raw_data_area()?);
if !real_entries.is_empty() {
for entry in real_entries {
by_name.insert(entry.object_name.clone(), entry);
}
} else {
let tags = page.tags()?;
for i in 1..tags.len() {
let data = page.record_data(i)?;
if let Some(entry) = CatalogEntry::parse_real_catalog_record(data) {
by_name.insert(entry.object_name.clone(), entry);
} else if let Ok(entry) = CatalogEntry::from_bytes(data) {
by_name.insert(entry.object_name.clone(), entry);
}
}
}
}
Ok(by_name.into_values().collect())
}
pub fn walk_leaf_pages(&self, root_page: u32) -> Result<Vec<u32>, EseError> {
let page = self.read_page(root_page)?;
let hdr = page.parse_header()?;
if hdr.page_flags & crate::PAGE_FLAG_LEAF != 0 {
return Ok(vec![root_page]);
}
let tag_count = hdr.available_page_tag_count as usize;
let mut leaves = Vec::new();
for i in 1..tag_count {
let data = page.record_data(i)?;
let Some(child_bytes) = data.get(data.len().saturating_sub(4)..) else {
continue; };
if child_bytes.len() < 4 {
continue;
}
let child_ese = u32::from_le_bytes([
child_bytes[0],
child_bytes[1],
child_bytes[2],
child_bytes[3],
]);
let child_page = child_ese + 1; let mut child_leaves = self.walk_leaf_pages(child_page)?;
leaves.append(&mut child_leaves);
}
Ok(leaves)
}
pub fn find_table_page(&self, name: &str) -> Result<u32, EseError> {
let entries = self.catalog_entries()?;
entries
.iter()
.find(|e| e.object_name == name)
.map(|e| e.table_page)
.ok_or_else(|| EseError::TableNotFound {
name: name.to_owned(),
})
}
pub fn table_records_from_root(&self, root_page: u32) -> Result<TableCursor<'_>, EseError> {
let leaf_pages = self.walk_leaf_pages(root_page)?;
Ok(TableCursor {
db: self,
leaf_pages,
page_idx: 0,
tag_idx: 1,
})
}
pub fn table_columns(&self, table_name: &str) -> Result<Vec<ColumnDef>, EseError> {
let entries = self.catalog_entries()?;
let table = entries
.iter()
.find(|e| e.object_type == 1 && e.object_name == table_name)
.ok_or_else(|| EseError::TableNotFound { name: table_name.to_owned() })?;
let table_obj_id = table.object_id;
let mut cols: Vec<ColumnDef> = entries
.iter()
.filter(|e| e.object_type == 2 && e.parent_object_id == table_obj_id)
.map(|e| ColumnDef {
column_id: e.object_id,
name: e.object_name.clone(),
coltyp: e.table_page as u8,
})
.collect();
cols.sort_by_key(|c| c.column_id);
Ok(cols)
}
pub fn table_records(&self, table_name: &str) -> Result<TableCursor<'_>, EseError> {
let root_page = self.find_table_page(table_name)?;
self.table_records_from_root(root_page)
}
}