use crate::parser::read_u16;
use crate::Error;
pub const JSTF_TABLE_TAG: [u8; 4] = *b"JSTF";
#[derive(Debug, Clone)]
pub struct JstfTable<'a> {
data: &'a [u8],
scripts: Vec<([u8; 4], usize)>,
}
impl<'a> JstfTable<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
let major = read_u16(data, 0)?;
if major != 1 {
return Err(Error::BadStructure("JSTF major version not 1"));
}
let count = read_u16(data, 4)? as usize;
let mut scripts = Vec::with_capacity(count);
for i in 0..count {
let rec = 6 + i * 6; let tag = [
*data.get(rec).ok_or(Error::UnexpectedEof)?,
*data.get(rec + 1).ok_or(Error::UnexpectedEof)?,
*data.get(rec + 2).ok_or(Error::UnexpectedEof)?,
*data.get(rec + 3).ok_or(Error::UnexpectedEof)?,
];
let off = read_u16(data, rec + 4)? as usize;
if off == 0 || off >= data.len() {
return Err(Error::BadStructure("JSTF script offset OOB"));
}
scripts.push((tag, off));
}
Ok(Self { data, scripts })
}
pub fn script_tags(&self) -> impl Iterator<Item = [u8; 4]> + '_ {
self.scripts.iter().map(|(t, _)| *t)
}
pub fn script_count(&self) -> usize {
self.scripts.len()
}
pub fn script(&self, tag: &[u8; 4]) -> Option<JstfScript<'a>> {
let (_, off) = self.scripts.iter().find(|(t, _)| t == tag)?;
Some(JstfScript {
data: self.data,
base: *off,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct JstfScript<'a> {
data: &'a [u8],
base: usize,
}
impl<'a> JstfScript<'a> {
pub fn extender_glyphs(&self) -> Vec<u16> {
let mut out = Vec::new();
let Ok(off) = read_u16(self.data, self.base) else {
return out;
};
if off == 0 {
return out;
}
let at = self.base + off as usize;
let Ok(count) = read_u16(self.data, at) else {
return out;
};
for i in 0..count as usize {
match read_u16(self.data, at + 2 + i * 2) {
Ok(g) => out.push(g),
Err(_) => break,
}
}
out
}
pub fn default_lang_sys(&self) -> Option<JstfLangSys<'a>> {
let off = read_u16(self.data, self.base + 2).ok()? as usize;
if off == 0 {
return None;
}
Some(JstfLangSys {
data: self.data,
base: self.base + off,
})
}
pub fn lang_sys_count(&self) -> usize {
read_u16(self.data, self.base + 4)
.map(|n| n as usize)
.unwrap_or(0)
}
pub fn lang_sys_tags(&self) -> Vec<[u8; 4]> {
let n = self.lang_sys_count();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let rec = self.base + 6 + i * 6;
if let Some(s) = self.data.get(rec..rec + 4) {
out.push([s[0], s[1], s[2], s[3]]);
}
}
out
}
pub fn lang_sys(&self, tag: &[u8; 4]) -> Option<JstfLangSys<'a>> {
let n = self.lang_sys_count();
for i in 0..n {
let rec = self.base + 6 + i * 6;
let t = self.data.get(rec..rec + 4)?;
if t == tag {
let off = read_u16(self.data, rec + 4).ok()? as usize;
if off == 0 {
return None;
}
return Some(JstfLangSys {
data: self.data,
base: self.base + off,
});
}
}
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct JstfLangSys<'a> {
data: &'a [u8],
base: usize,
}
impl<'a> JstfLangSys<'a> {
pub fn priority_count(&self) -> usize {
read_u16(self.data, self.base)
.map(|n| n as usize)
.unwrap_or(0)
}
pub fn priority(&self, index: usize) -> Option<JstfPriority<'a>> {
if index >= self.priority_count() {
return None;
}
let off = read_u16(self.data, self.base + 2 + index * 2).ok()? as usize;
if off == 0 {
return None;
}
Some(JstfPriority {
data: self.data,
base: self.base + off,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JstfMod {
ShrinkageEnableGsub = 0,
ShrinkageDisableGsub = 1,
ShrinkageEnableGpos = 2,
ShrinkageDisableGpos = 3,
ShrinkageJstfMax = 4,
ExtensionEnableGsub = 5,
ExtensionDisableGsub = 6,
ExtensionEnableGpos = 7,
ExtensionDisableGpos = 8,
ExtensionJstfMax = 9,
}
#[derive(Debug, Clone, Copy)]
pub struct JstfPriority<'a> {
data: &'a [u8],
base: usize,
}
impl<'a> JstfPriority<'a> {
pub fn mod_list(&self, slot: JstfMod) -> Vec<u16> {
let mut out = Vec::new();
if matches!(slot, JstfMod::ShrinkageJstfMax | JstfMod::ExtensionJstfMax) {
return out;
}
let Ok(off) = read_u16(self.data, self.base + slot as usize * 2) else {
return out;
};
if off == 0 {
return out;
}
let at = self.base + off as usize;
let Ok(count) = read_u16(self.data, at) else {
return out;
};
for i in 0..count as usize {
match read_u16(self.data, at + 2 + i * 2) {
Ok(idx) => out.push(idx),
Err(_) => break,
}
}
out
}
pub fn jstf_max_lookup_count(&self, slot: JstfMod) -> Option<usize> {
if !matches!(slot, JstfMod::ShrinkageJstfMax | JstfMod::ExtensionJstfMax) {
return None;
}
let off = read_u16(self.data, self.base + slot as usize * 2).ok()? as usize;
if off == 0 {
return None;
}
Some(read_u16(self.data, self.base + off).ok()? as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn be(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
fn build_jstf() -> Vec<u8> {
let mut d = Vec::new();
d.extend_from_slice(&be(1)); d.extend_from_slice(&be(0)); d.extend_from_slice(&be(1)); d.extend_from_slice(b"DFLT"); let script_off_pos = d.len();
d.extend_from_slice(&be(0));
let script_base = d.len();
let ext_off_pos = d.len();
d.extend_from_slice(&be(0)); let deflang_off_pos = d.len();
d.extend_from_slice(&be(0)); d.extend_from_slice(&be(0));
let ext_at = d.len();
d.extend_from_slice(&be(2)); d.extend_from_slice(&be(33));
d.extend_from_slice(&be(34));
d[ext_off_pos..ext_off_pos + 2].copy_from_slice(&be((ext_at - script_base) as u16));
let lang_at = d.len();
d.extend_from_slice(&be(1)); let prio_off_pos = d.len();
d.extend_from_slice(&be(0)); d[deflang_off_pos..deflang_off_pos + 2]
.copy_from_slice(&be((lang_at - script_base) as u16));
let prio_at = d.len();
let modlist_off_pos = d.len();
d.extend_from_slice(&be(0)); for _ in 1..10 {
d.extend_from_slice(&be(0));
}
d[prio_off_pos..prio_off_pos + 2].copy_from_slice(&be((prio_at - lang_at) as u16));
let modlist_at = d.len();
d.extend_from_slice(&be(1)); d.extend_from_slice(&be(7)); d[modlist_off_pos..modlist_off_pos + 2].copy_from_slice(&be((modlist_at - prio_at) as u16));
d[script_off_pos..script_off_pos + 2].copy_from_slice(&be(script_base as u16));
d
}
#[test]
fn jstf_navigation() {
let data = build_jstf();
let t = JstfTable::parse(&data).expect("parse");
assert_eq!(t.script_count(), 1);
assert_eq!(t.script_tags().collect::<Vec<_>>(), vec![*b"DFLT"]);
let script = t.script(b"DFLT").expect("script");
assert_eq!(script.extender_glyphs(), vec![33, 34]);
assert_eq!(script.lang_sys_count(), 0);
let lang = script.default_lang_sys().expect("default langsys");
assert_eq!(lang.priority_count(), 1);
let prio = lang.priority(0).expect("priority 0");
assert_eq!(prio.mod_list(JstfMod::ShrinkageEnableGsub), vec![7]);
assert!(prio.mod_list(JstfMod::ExtensionEnableGpos).is_empty());
assert!(prio
.jstf_max_lookup_count(JstfMod::ShrinkageJstfMax)
.is_none());
assert!(lang.priority(1).is_none());
}
#[test]
fn rejects_wrong_version() {
let mut data = vec![0u8; 6];
data[1] = 3; assert!(JstfTable::parse(&data).is_err());
}
#[test]
fn unknown_script_is_none() {
let data = build_jstf();
let t = JstfTable::parse(&data).expect("parse");
assert!(t.script(b"arab").is_none());
}
}