use crate::parser::{read_u16, read_u32};
use crate::Error;
const CONDITION_FORMAT_AXIS_RANGE: u16 = 1;
fn f2dot14(raw: i16) -> f32 {
raw as f32 / 16384.0
}
#[derive(Debug, Clone)]
pub struct FeatureVariations<'a> {
table: &'a [u8],
fv_off: u32,
record_count: u32,
}
impl<'a> FeatureVariations<'a> {
pub fn parse(table: &'a [u8], fv_off: u32) -> Result<Option<Self>, Error> {
if fv_off == 0 {
return Ok(None);
}
let start = fv_off as usize;
let body = table.get(start..).ok_or(Error::BadOffset)?;
if body.len() < 8 {
return Err(Error::UnexpectedEof);
}
let major = read_u16(body, 0)?;
if major != 1 {
return Err(Error::BadStructure(
"FeatureVariations: unsupported major version",
));
}
let record_count = read_u32(body, 4)?;
let need = 8usize
.checked_add(
(record_count as usize)
.checked_mul(8)
.ok_or(Error::BadStructure(
"FeatureVariations: record count overflow",
))?,
)
.ok_or(Error::BadStructure(
"FeatureVariations: record array overflow",
))?;
if body.len() < need {
return Err(Error::UnexpectedEof);
}
Ok(Some(Self {
table,
fv_off,
record_count,
}))
}
pub fn record_count(&self) -> u32 {
self.record_count
}
pub fn active_substitution(
&self,
normalised_coords: &[f32],
) -> Option<FeatureTableSubstitution<'a>> {
let fv = self.table.get(self.fv_off as usize..)?;
for i in 0..self.record_count as usize {
let rec = 8 + i * 8;
let condition_set_off = read_u32(fv, rec).ok()?;
let subst_off = read_u32(fv, rec + 4).ok()?;
if !self.condition_set_matches(fv, condition_set_off, normalised_coords) {
continue;
}
if subst_off == 0 {
return None;
}
let subst_body = fv.get(subst_off as usize..)?;
match FeatureTableSubstitution::parse(self.table, self.fv_off + subst_off, subst_body) {
Ok(Some(s)) => return Some(s),
Ok(None) => continue,
Err(_) => return None,
}
}
None
}
fn condition_set_matches(&self, fv: &[u8], condition_set_off: u32, coords: &[f32]) -> bool {
if condition_set_off == 0 {
return true;
}
let cs = match fv.get(condition_set_off as usize..) {
Some(s) => s,
None => return false,
};
let count = match read_u16(cs, 0) {
Ok(v) => v as usize,
Err(_) => return false,
};
if count == 0 {
return true;
}
if cs.len() < 2 + count * 4 {
return false;
}
for i in 0..count {
let cond_off = match read_u32(cs, 2 + i * 4) {
Ok(v) => v as usize,
Err(_) => return false,
};
let cond = match cs.get(cond_off..) {
Some(s) => s,
None => return false,
};
if !condition_matches(cond, coords) {
return false;
}
}
true
}
}
fn condition_matches(cond: &[u8], coords: &[f32]) -> bool {
let format = match read_u16(cond, 0) {
Ok(v) => v,
Err(_) => return false,
};
if format != CONDITION_FORMAT_AXIS_RANGE {
return false;
}
let axis_index = match read_u16(cond, 2) {
Ok(v) => v as usize,
Err(_) => return false,
};
let min = match crate::parser::read_i16(cond, 4) {
Ok(v) => f2dot14(v),
Err(_) => return false,
};
let max = match crate::parser::read_i16(cond, 6) {
Ok(v) => f2dot14(v),
Err(_) => return false,
};
let value = match coords.get(axis_index) {
Some(v) => *v,
None => return false,
};
value >= min && value <= max
}
#[derive(Debug, Clone)]
pub struct FeatureTableSubstitution<'a> {
#[allow(dead_code)]
table: &'a [u8],
body: &'a [u8],
substitution_count: u16,
}
impl<'a> FeatureTableSubstitution<'a> {
fn parse(table: &'a [u8], _subst_off: u32, body: &'a [u8]) -> Result<Option<Self>, Error> {
if body.len() < 6 {
return Err(Error::UnexpectedEof);
}
let major = read_u16(body, 0)?;
if major != 1 {
return Ok(None);
}
let substitution_count = read_u16(body, 4)?;
if body.len() < 6 + substitution_count as usize * 6 {
return Err(Error::UnexpectedEof);
}
Ok(Some(Self {
table,
body,
substitution_count,
}))
}
pub fn substitution_count(&self) -> u16 {
self.substitution_count
}
pub fn lookup_indices_for_feature(&self, feature_index: u16) -> Option<Vec<u16>> {
for i in 0..self.substitution_count as usize {
let r = 6 + i * 6;
let fi = read_u16(self.body, r).ok()?;
if fi == feature_index {
let alt_off = read_u32(self.body, r + 2).ok()? as usize;
let alt = self.body.get(alt_off..)?;
if alt.len() < 4 {
return None;
}
let count = read_u16(alt, 2).ok()? as usize;
if alt.len() < 4 + count * 2 {
return None;
}
let mut idxs = Vec::with_capacity(count);
for k in 0..count {
idxs.push(read_u16(alt, 4 + k * 2).ok()?);
}
return Some(idxs);
}
if fi > feature_index {
return None;
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
const FV_BASE: u32 = 16;
fn build_fv(axis: u16, min: i16, max: i16, feat: u16, lookup: u16) -> Vec<u8> {
let mut b = vec![0u8; FV_BASE as usize];
b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u32.to_be_bytes());
let rec_pos = b.len();
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes());
let cs = b.len() as u32;
b.extend_from_slice(&1u16.to_be_bytes()); let cond_off_pos = b.len();
b.extend_from_slice(&0u32.to_be_bytes()); let cond_rel = (b.len() as u32) - cs;
b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&axis.to_be_bytes()); b.extend_from_slice(&min.to_be_bytes()); b.extend_from_slice(&max.to_be_bytes());
let ss = b.len() as u32;
b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&feat.to_be_bytes()); let alt_off_pos = b.len();
b.extend_from_slice(&0u32.to_be_bytes()); let alt_rel = (b.len() as u32) - ss;
b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&lookup.to_be_bytes());
b[rec_pos..rec_pos + 4].copy_from_slice(&(cs - FV_BASE).to_be_bytes());
b[rec_pos + 4..rec_pos + 8].copy_from_slice(&(ss - FV_BASE).to_be_bytes());
b[cond_off_pos..cond_off_pos + 4].copy_from_slice(&cond_rel.to_be_bytes());
b[alt_off_pos..alt_off_pos + 4].copy_from_slice(&alt_rel.to_be_bytes());
b
}
fn f2(v: f32) -> i16 {
(v * 16384.0).round() as i16
}
#[test]
fn parse_rejects_unsupported_major() {
let mut b = build_fv(0, f2(0.5), f2(1.0), 3, 7);
b[FV_BASE as usize] = 0;
b[FV_BASE as usize + 1] = 2; assert!(matches!(
FeatureVariations::parse(&b, FV_BASE),
Err(Error::BadStructure(_))
));
}
#[test]
fn zero_offset_is_no_table() {
assert!(FeatureVariations::parse(&[], 0).unwrap().is_none());
}
#[test]
fn axis_in_range_substitutes() {
let b = build_fv(0, f2(0.5), f2(1.0), 3, 7);
let fv = FeatureVariations::parse(&b, FV_BASE).unwrap().unwrap();
let sub = fv.active_substitution(&[0.75]).unwrap();
assert_eq!(sub.substitution_count(), 1);
assert_eq!(sub.lookup_indices_for_feature(3), Some(vec![7]));
assert_eq!(sub.lookup_indices_for_feature(2), None);
assert_eq!(sub.lookup_indices_for_feature(4), None);
}
#[test]
fn axis_out_of_range_no_substitution() {
let b = build_fv(0, f2(0.5), f2(1.0), 3, 7);
let fv = FeatureVariations::parse(&b, FV_BASE).unwrap().unwrap();
assert!(fv.active_substitution(&[0.25]).is_none());
assert!(fv.active_substitution(&[0.5]).is_some());
assert!(fv.active_substitution(&[1.0]).is_some());
}
#[test]
fn missing_axis_coordinate_fails_match() {
let b = build_fv(1, f2(-1.0), f2(1.0), 3, 7);
let fv = FeatureVariations::parse(&b, FV_BASE).unwrap().unwrap();
assert!(fv.active_substitution(&[0.0]).is_none());
}
#[test]
fn universal_condition_set_always_matches() {
let mut b = vec![0u8; FV_BASE as usize];
b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u32.to_be_bytes()); let rec = b.len();
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); let ss = b.len() as u32;
b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&9u16.to_be_bytes()); let alt_pos = b.len();
b.extend_from_slice(&0u32.to_be_bytes());
let alt_rel = (b.len() as u32) - ss;
b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&2u16.to_be_bytes()); b.extend_from_slice(&4u16.to_be_bytes());
b.extend_from_slice(&5u16.to_be_bytes());
b[rec + 4..rec + 8].copy_from_slice(&(ss - FV_BASE).to_be_bytes());
b[alt_pos..alt_pos + 4].copy_from_slice(&alt_rel.to_be_bytes());
let fv = FeatureVariations::parse(&b, FV_BASE).unwrap().unwrap();
let sub = fv.active_substitution(&[]).unwrap();
assert_eq!(sub.lookup_indices_for_feature(9), Some(vec![4, 5]));
}
#[test]
fn unsupported_subst_version_skips_record() {
let mut b = build_fv(0, f2(-1.0), f2(1.0), 3, 7);
let rec_subst = FV_BASE as usize + 12;
let ss_rel = u32::from_be_bytes([
b[rec_subst],
b[rec_subst + 1],
b[rec_subst + 2],
b[rec_subst + 3],
]) as usize;
let ss = FV_BASE as usize + ss_rel;
b[ss] = 0;
b[ss + 1] = 2; let fv = FeatureVariations::parse(&b, FV_BASE).unwrap().unwrap();
assert!(fv.active_substitution(&[0.0]).is_none());
}
}