use crate::table::TableDef;
use crate::util::{u16le, u32le};
const FKP_SIZE: usize = 512;
const MAX_FKP_ENTRIES: usize = 1 << 20;
#[cfg(test)]
thread_local! {
static TEST_MAX_FKP: std::cell::Cell<usize> = const { std::cell::Cell::new(MAX_FKP_ENTRIES) };
}
#[cfg(test)]
fn set_test_max_fkp(n: usize) {
TEST_MAX_FKP.with(|c| c.set(n));
}
fn max_fkp_entries() -> usize {
#[cfg(test)]
{
TEST_MAX_FKP.with(|c| c.get())
}
#[cfg(not(test))]
{
MAX_FKP_ENTRIES
}
}
const SPRM_P_ISTD: u16 = 0x4600; const SPRM_P_JC: u16 = 0x2403; const SPRM_P_FIN_TABLE: u16 = 0x2416;
const SPRM_P_FTTP: u16 = 0x2417;
const SPRM_P_OUT_LVL: u16 = 0x2640; const SPRM_P_ILVL: u16 = 0x260A;
const SPRM_T_TABLE_HEADER: u16 = 0x3404; const SPRM_P_ILFO: u16 = 0x460B;
const SPRM_T_DEF_TABLE: u16 = 0xD608;
#[derive(Debug, Clone, Default)]
struct PapEntry {
fc_lim: u32,
in_table: bool,
ttp: bool,
ilfo: u16,
ilvl: u8,
istd: u16,
outlvl: Option<u8>,
jc: u8,
table_header: bool,
table_def: Option<TableDef>,
}
#[derive(Debug, Clone, Copy, Default)]
struct Pap {
in_table: bool,
ttp: bool,
ilfo: u16,
ilvl: u8,
istd: u16,
outlvl: Option<u8>,
jc: u8,
table_header: bool,
}
#[derive(Debug, Default)]
pub(crate) struct PapxTable {
entries: Vec<PapEntry>,
}
impl PapxTable {
fn entry_at(&self, fc: u32) -> Option<&PapEntry> {
let i = self.entries.partition_point(|e| e.fc_lim <= fc);
self.entries.get(i)
}
pub(crate) fn at(&self, fc: u32) -> (bool, bool) {
self.entry_at(fc)
.map(|e| (e.in_table, e.ttp))
.unwrap_or((false, false))
}
pub(crate) fn list_at(&self, fc: u32) -> (u16, u8) {
self.entry_at(fc)
.map(|e| (e.ilfo, e.ilvl))
.unwrap_or((0, 0))
}
pub(crate) fn style_at(&self, fc: u32) -> (u16, Option<u8>, u8) {
self.entry_at(fc)
.map(|e| (e.istd, e.outlvl, e.jc))
.unwrap_or((0, None, 0))
}
pub(crate) fn table_def_at(&self, fc: u32) -> Option<&TableDef> {
self.entry_at(fc).and_then(|e| e.table_def.as_ref())
}
pub(crate) fn table_header_at(&self, fc: u32) -> bool {
self.entry_at(fc).map(|e| e.table_header).unwrap_or(false)
}
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub(crate) fn parse(word: &[u8], table: &[u8], fc_plcf: usize, lcb_plcf: usize) -> PapxTable {
let mut entries = Vec::new();
if lcb_plcf < 4 {
return PapxTable { entries };
}
let Some(plc) = table.get(fc_plcf..fc_plcf.saturating_add(lcb_plcf)) else {
return PapxTable { entries };
};
let cap = max_fkp_entries();
let n = ((plc.len().saturating_sub(4)) / 8).min(cap);
let pn_base = 4 * (n + 1);
for i in 0..n {
if entries.len() >= cap {
break;
}
let Some(pn_raw) = u32le(plc, pn_base + i * 4) else {
break;
};
let page = (pn_raw & 0x003F_FFFF) as usize; let off = page.saturating_mul(FKP_SIZE);
parse_fkp(word, off, &mut entries);
}
entries.sort_by_key(|e| e.fc_lim);
PapxTable { entries }
}
fn parse_fkp(word: &[u8], page_off: usize, out: &mut Vec<PapEntry>) {
let Some(page) = word.get(page_off..page_off + FKP_SIZE) else {
return;
};
let crun = page[FKP_SIZE - 1] as usize;
if crun == 0 || 4 * (crun + 1) + 13 * crun >= FKP_SIZE {
return;
}
for i in 0..crun {
let fc_lim = match u32le(page, 4 * (i + 1)) {
Some(v) => v,
None => break,
};
let bx_off = 4 * (crun + 1) + i * 13;
let b_offset = page.get(bx_off).copied().unwrap_or(0) as usize;
let (pap, table_def) = if b_offset == 0 {
(Pap::default(), None)
} else {
parse_papx(page, b_offset * 2)
};
out.push(PapEntry {
fc_lim,
in_table: pap.in_table,
ttp: pap.ttp,
ilfo: pap.ilfo,
ilvl: pap.ilvl,
istd: pap.istd,
outlvl: pap.outlvl,
jc: pap.jc,
table_header: pap.table_header,
table_def,
});
}
}
fn parse_papx(page: &[u8], off: usize) -> (Pap, Option<TableDef>) {
let Some(&cb) = page.get(off) else {
return (Pap::default(), None);
};
let (data_off, data_len) = if cb != 0 {
(off + 1, (cb as usize) * 2 - 1)
} else {
let cb2 = page.get(off + 1).copied().unwrap_or(0) as usize;
(off + 2, cb2 * 2)
};
if data_len < 2 {
return (Pap::default(), None);
}
let istd = u16le(page, data_off).unwrap_or(0);
match page.get(data_off + 2..data_off + data_len) {
Some(gp) => scan_grpprl(gp, istd),
None => (
Pap {
istd,
..Pap::default()
},
None,
),
}
}
fn scan_grpprl(gp: &[u8], istd: u16) -> (Pap, Option<TableDef>) {
let mut pap = Pap {
istd,
..Pap::default()
};
let mut table_def = None;
let mut pos = 0;
while pos + 2 <= gp.len() {
let Some(sprm) = u16le(gp, pos) else { break };
let op = pos + 2;
let Some(len) = operand_len(sprm, gp, op) else {
break;
};
match sprm {
SPRM_P_ISTD => pap.istd = u16le(gp, op).unwrap_or(istd),
SPRM_P_JC => pap.jc = gp.get(op).copied().unwrap_or(0),
SPRM_P_FIN_TABLE => pap.in_table = gp.get(op).copied().unwrap_or(0) != 0,
SPRM_P_FTTP => pap.ttp = gp.get(op).copied().unwrap_or(0) != 0,
SPRM_P_OUT_LVL => pap.outlvl = Some(gp.get(op).copied().unwrap_or(9)),
SPRM_T_TABLE_HEADER => pap.table_header = gp.get(op).copied().unwrap_or(0) != 0,
SPRM_P_ILVL => pap.ilvl = gp.get(op).copied().unwrap_or(0),
SPRM_P_ILFO => pap.ilfo = u16le(gp, op).unwrap_or(0),
SPRM_T_DEF_TABLE => {
if let Some(operand) = gp.get(op..op + len) {
table_def = TableDef::parse(operand);
}
}
_ => {}
}
pos = op + len;
}
(pap, table_def)
}
fn operand_len(sprm: u16, data: &[u8], op: usize) -> Option<usize> {
match (sprm >> 13) & 0x7 {
0 | 1 => Some(1),
2 | 4 | 5 => Some(2),
3 => Some(4),
7 => Some(3),
6 => {
if sprm == SPRM_T_DEF_TABLE {
let cb = u16le(data, op)? as usize;
(cb != 0).then_some(1 + cb)
} else {
Some(1 + *data.get(op)? as usize)
}
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scans_table_flags() {
let (p, _) = scan_grpprl(&[0x16, 0x24, 0x01, 0x17, 0x24, 0x01], 0);
assert!(p.in_table && p.ttp);
let (p2, _) = scan_grpprl(&[0x00, 0x44, 0xAA, 0xBB, 0x16, 0x24, 0x01], 0);
assert!(p2.in_table && !p2.ttp);
}
#[test]
fn scans_list_props() {
let (p, _) = scan_grpprl(&[0x0A, 0x26, 0x02, 0x0B, 0x46, 0x05, 0x00], 0);
assert_eq!((p.ilfo, p.ilvl), (5, 2));
}
#[test]
fn scans_style_outline_align() {
let (p, _) = scan_grpprl(&[0x03, 0x24, 0x01, 0x40, 0x26, 0x02], 7);
assert_eq!(p.istd, 7);
assert_eq!(p.jc, 1);
assert_eq!(p.outlvl, Some(2));
let (p2, _) = scan_grpprl(&[0x00, 0x46, 0x05, 0x00], 7);
assert_eq!(p2.istd, 5);
}
#[test]
fn parses_tdeftable_then_reads_flags() {
let mut gp = vec![0x08, 0xD6, 0x1A, 0x00]; gp.push(1); gp.extend_from_slice(&0i16.to_le_bytes()); gp.extend_from_slice(&100i16.to_le_bytes()); gp.extend_from_slice(&[0u8; 20]); gp.extend_from_slice(&[0x16, 0x24, 0x01]); gp.extend_from_slice(&[0x17, 0x24, 0x01]); let (p, def) = scan_grpprl(&gp, 0);
assert!(p.in_table && p.ttp);
assert_eq!(def.unwrap().rgdxa, vec![0, 100]);
}
#[test]
fn lookup_by_fc() {
let mk = |fc_lim, in_table, ttp| PapEntry {
fc_lim,
in_table,
ttp,
ilfo: 0,
ilvl: 0,
istd: 0,
outlvl: None,
jc: 0,
table_header: false,
table_def: None,
};
let t = PapxTable {
entries: vec![
mk(100, false, false),
mk(200, true, false),
mk(300, true, true),
],
};
assert_eq!(t.at(50), (false, false)); assert_eq!(t.at(150), (true, false)); assert_eq!(t.at(250), (true, true)); assert_eq!(t.at(999), (false, false)); }
#[test]
fn bin_table_entry_count_is_capped() {
let mut word = vec![0u8; FKP_SIZE];
word[FKP_SIZE - 1] = 29;
let n_pages = 10usize;
let plc = vec![0u8; 4 * (n_pages + 1) + 4 * n_pages];
set_test_max_fkp(64);
let t = parse(&word, &plc, 0, plc.len());
set_test_max_fkp(MAX_FKP_ENTRIES);
assert!(
t.entries.len() >= 64 && t.entries.len() < n_pages * 29,
"entry cap did not bound the repeated-page bin table: {}",
t.entries.len()
);
}
}