use crate::data::{self, Value};
use crate::file::{FileError, PageReader};
use crate::format::{catalog_flags, ObjectType, CATALOG_PAGE};
use crate::table;
#[derive(Debug, Clone)]
pub struct CatalogEntry {
pub name: String,
pub object_type: ObjectType,
pub table_page: u32,
pub flags: u32,
}
pub fn read_catalog(reader: &mut PageReader) -> Result<Vec<CatalogEntry>, FileError> {
let tdef = table::read_table_def(reader, "MSysObjects", CATALOG_PAGE)?;
let result = data::read_table_rows(reader, &tdef)?;
result.warn_skipped("MSysObjects");
let (mut id_idx, mut name_idx, mut type_idx, mut flags_idx) = (None, None, None, None);
for (i, col) in tdef.columns.iter().enumerate() {
match col.name.as_str() {
"Id" => id_idx = Some(i),
"Name" => name_idx = Some(i),
"Type" => type_idx = Some(i),
"Flags" => flags_idx = Some(i),
_ => {}
}
}
let id_idx = id_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysObjects missing Id column",
})?;
let name_idx = name_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysObjects missing Name column",
})?;
let type_idx = type_idx.ok_or(FileError::InvalidTableDef {
reason: "MSysObjects missing Type column",
})?;
let mut entries = Vec::new();
for row in &result.rows {
let name = match row.get(name_idx) {
Some(Value::Text(s)) if !s.is_empty() => s.clone(),
_ => continue,
};
let object_type = match row.get(type_idx) {
Some(Value::Int(v)) => match ObjectType::try_from(*v as i32) {
Ok(ot) => ot,
Err(_) => continue,
},
_ => continue,
};
let table_page = match row.get(id_idx) {
Some(Value::Long(id)) => (*id & 0x00FF_FFFF) as u32,
_ => continue,
};
let flags = match flags_idx.and_then(|i| row.get(i)) {
Some(Value::Long(f)) => *f as u32,
_ => 0,
};
entries.push(CatalogEntry {
name,
object_type,
table_page,
flags,
});
}
Ok(entries)
}
pub fn table_names(reader: &mut PageReader) -> Result<Vec<String>, FileError> {
let catalog = read_catalog(reader)?;
let names = catalog
.into_iter()
.filter(|e| {
e.object_type == ObjectType::Table
&& (e.flags & (catalog_flags::SYSTEM | catalog_flags::HIDDEN)) == 0
})
.map(|e| e.name)
.collect();
Ok(names)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::file::PageReader;
fn test_data_path(relative: &str) -> Option<std::path::PathBuf> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::PathBuf::from(manifest_dir)
.join("../../testdata")
.join(relative);
if path.exists() {
Some(path)
} else {
None
}
}
macro_rules! skip_if_missing {
($path:expr) => {
match test_data_path($path) {
Some(p) => p,
None => {
eprintln!("SKIP: test data not found: {}", $path);
return;
}
}
};
}
fn assert_catalog(path: &std::path::Path) {
let mut reader = PageReader::open(path).unwrap();
let catalog = read_catalog(&mut reader).unwrap();
assert!(!catalog.is_empty(), "catalog should not be empty");
for entry in &catalog {
assert!(!entry.name.is_empty(), "entry name should not be empty");
}
let msysobjects = catalog
.iter()
.find(|e| e.name == "MSysObjects")
.expect("MSysObjects should be in the catalog");
assert_eq!(msysobjects.object_type, ObjectType::Table);
assert_ne!(
msysobjects.flags & catalog_flags::SYSTEM,
0,
"MSysObjects should have the system flag set"
);
}
#[test]
fn jet3_read_catalog() {
let path = skip_if_missing!("V1997/testV1997.mdb");
assert_catalog(&path);
}
#[test]
fn jet4_read_catalog() {
let path = skip_if_missing!("V2003/testV2003.mdb");
assert_catalog(&path);
}
#[test]
fn ace12_read_catalog() {
let path = skip_if_missing!("V2007/testV2007.accdb");
assert_catalog(&path);
}
#[test]
fn ace14_read_catalog() {
let path = skip_if_missing!("V2010/testV2010.accdb");
assert_catalog(&path);
}
#[test]
fn ace17_read_catalog() {
let path = skip_if_missing!("V2019/extDateTestV2019.accdb");
assert_catalog(&path);
}
fn assert_table_names(path: &std::path::Path) {
let mut reader = PageReader::open(path).unwrap();
let names = table_names(&mut reader).unwrap();
assert!(!names.is_empty(), "should have at least one user table");
for name in &names {
assert!(
!name.starts_with("MSys"),
"system table {name} should not appear in table_names"
);
}
}
#[test]
fn jet3_table_names() {
let path = skip_if_missing!("V1997/testV1997.mdb");
assert_table_names(&path);
}
#[test]
fn jet4_table_names() {
let path = skip_if_missing!("V2003/testV2003.mdb");
assert_table_names(&path);
}
#[test]
fn ace12_table_names() {
let path = skip_if_missing!("V2007/testV2007.accdb");
assert_table_names(&path);
}
#[test]
fn ace14_table_names() {
let path = skip_if_missing!("V2010/testV2010.accdb");
assert_table_names(&path);
}
#[test]
fn ace17_table_names() {
let path = skip_if_missing!("V2019/extDateTestV2019.accdb");
assert_table_names(&path);
}
fn filter_user_tables(catalog: Vec<CatalogEntry>) -> Vec<String> {
catalog
.into_iter()
.filter(|e| {
e.object_type == ObjectType::Table
&& (e.flags & (catalog_flags::SYSTEM | catalog_flags::HIDDEN)) == 0
})
.map(|e| e.name)
.collect()
}
fn entry(name: &str, object_type: ObjectType, flags: u32) -> CatalogEntry {
CatalogEntry {
name: name.to_string(),
object_type,
table_page: 100,
flags,
}
}
#[test]
fn filter_excludes_system_flag() {
let catalog = vec![
entry("MSysObjects", ObjectType::Table, catalog_flags::SYSTEM),
entry("Users", ObjectType::Table, 0),
];
let names = filter_user_tables(catalog);
assert_eq!(names, vec!["Users"]);
}
#[test]
fn filter_excludes_hidden_flag() {
let catalog = vec![
entry(
"MSysNavPaneGroups",
ObjectType::Table,
catalog_flags::HIDDEN,
),
entry("Orders", ObjectType::Table, 0),
];
let names = filter_user_tables(catalog);
assert_eq!(names, vec!["Orders"]);
}
#[test]
fn filter_excludes_system_and_hidden() {
let catalog = vec![entry(
"Internal",
ObjectType::Table,
catalog_flags::SYSTEM | catalog_flags::HIDDEN,
)];
let names = filter_user_tables(catalog);
assert!(names.is_empty());
}
#[test]
fn filter_includes_normal_table() {
let catalog = vec![entry("Products", ObjectType::Table, 0)];
let names = filter_user_tables(catalog);
assert_eq!(names, vec!["Products"]);
}
#[test]
fn filter_excludes_non_table_types() {
let catalog = vec![
entry("MyQuery", ObjectType::Query, 0),
entry("MyForm", ObjectType::Form, 0),
entry("MyMacro", ObjectType::Macro, 0),
entry("MyReport", ObjectType::Report, 0),
entry("Users", ObjectType::Table, 0),
];
let names = filter_user_tables(catalog);
assert_eq!(names, vec!["Users"]);
}
#[test]
fn filter_empty_catalog() {
let names = filter_user_tables(vec![]);
assert!(names.is_empty());
}
#[test]
fn filter_mixed_flags_and_types() {
let catalog = vec![
entry("MSysObjects", ObjectType::Table, catalog_flags::SYSTEM),
entry("MSysACEs", ObjectType::Table, catalog_flags::SYSTEM),
entry(
"MSysNavPaneGroups",
ObjectType::Table,
catalog_flags::HIDDEN | 0x08,
),
entry("SavedQuery", ObjectType::Query, 0),
entry("Employees", ObjectType::Table, 0),
entry("Departments", ObjectType::Table, 0),
];
let names = filter_user_tables(catalog);
assert_eq!(names, vec!["Employees", "Departments"]);
}
#[test]
fn japanese_table_name() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let catalog = read_catalog(&mut reader).unwrap();
let jp_table = catalog.iter().find(|e| e.name == "jp_テーブル2");
assert!(
jp_table.is_some(),
"catalog should contain jp_テーブル2, found: {:?}",
catalog.iter().map(|e| &e.name).collect::<Vec<_>>()
);
assert_eq!(jp_table.unwrap().object_type, ObjectType::Table);
}
}