use std::collections::{HashMap, HashSet};
use opensqlany::{ApModel, PageStore, PageType};
use crate::bv_recovery::{deobfuscate_with_bv, oracle_bv_e_page, recover_bv_qb_data};
use crate::syscolumn::SysColumn;
use crate::systable::SysTableEntry;
pub const SYSOBJECT_NAME_OFFSET: usize = 24;
const NAME_LEN_MIN: usize = 3;
const NAME_LEN_MAX: usize = 48;
fn looks_like_identifier(s: &[u8]) -> bool {
if s.is_empty() {
return false;
}
if !(s[0].is_ascii_alphabetic() || s[0] == b'_') {
return false;
}
s.iter().all(|&b| b.is_ascii_alphanumeric() || b == b'_')
}
pub fn bridge_owners_to_tables(
store: &PageStore,
model: &ApModel,
columns: &[SysColumn],
tables: &[SysTableEntry],
) -> HashMap<u32, String> {
let owner_set: HashSet<u32> = columns.iter().map(|c| c.owner_object_id).collect();
let mut name_set: HashSet<&str> = HashSet::new();
for t in tables {
name_set.insert(t.name.as_str());
}
if owner_set.is_empty() || name_set.is_empty() {
return HashMap::new();
}
let mut votes: HashMap<u32, HashMap<String, u32>> = HashMap::new();
let n_pages = store.page_count();
for pn in 0..n_pages {
let Ok(page) = store.page(pn) else { continue };
if page.trailer().page_type() != PageType::Extent {
continue;
}
let raw = page.bytes();
let plain = if let Some(bv) = recover_bv_qb_data(pn, raw) {
deobfuscate_with_bv(raw, pn, bv)
} else {
let bv = oracle_bv_e_page(pn, raw);
let cand = deobfuscate_with_bv(raw, pn, bv);
if cand[0] == 0 {
cand
} else {
model.deobfuscate_with_store(raw, pn, store)
}
};
let mut i = SYSOBJECT_NAME_OFFSET;
while i + 1 < plain.len() {
let nlen = plain[i] as usize;
if (NAME_LEN_MIN..=NAME_LEN_MAX).contains(&nlen) && i + 1 + nlen <= plain.len() {
let s = &plain[i + 1..i + 1 + nlen];
if looks_like_identifier(s)
&& let Ok(name) = std::str::from_utf8(s)
&& name_set.contains(name)
{
let oid_pos = i - SYSOBJECT_NAME_OFFSET;
let oid = u32::from_le_bytes([
plain[oid_pos],
plain[oid_pos + 1],
plain[oid_pos + 2],
plain[oid_pos + 3],
]);
if owner_set.contains(&oid) {
*votes
.entry(oid)
.or_default()
.entry(name.to_string())
.or_insert(0) += 1;
}
}
}
i += 1;
}
}
let mut bridge: HashMap<u32, String> = HashMap::new();
for (oid, cands) in votes {
if let Some((name, _)) = cands.into_iter().max_by_key(|(_, c)| *c) {
bridge.insert(oid, name);
}
}
bridge
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifier_check_basic() {
assert!(looks_like_identifier(b"abmc_invoice_header"));
assert!(looks_like_identifier(b"_underscore_lead"));
assert!(!looks_like_identifier(b""));
assert!(!looks_like_identifier(b"1starts_digit"));
assert!(!looks_like_identifier(b"has space"));
assert!(!looks_like_identifier(b"has-dash"));
}
}