use crate::parser::{read_i16, read_u16, read_u8};
use crate::Error;
pub const VDMX_TABLE_TAG: u32 = 0x5644_4D58;
pub const VDMX_VERSION_0: u16 = 0;
pub const VDMX_VERSION_1: u16 = 1;
pub const VDMX_HEADER_LEN: usize = 6;
pub const VDMX_RATIO_RECORD_LEN: usize = 4;
pub const VDMX_OFFSET_LEN: usize = 2;
pub const VDMX_GROUP_HEADER_LEN: usize = 4;
pub const VDMX_VTABLE_RECORD_LEN: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RatioRange {
pub char_set: u8,
pub x_ratio: u8,
pub y_start_ratio: u8,
pub y_end_ratio: u8,
}
impl RatioRange {
pub fn is_sentinel(&self) -> bool {
self.x_ratio == 0 && self.y_start_ratio == 0 && self.y_end_ratio == 0
}
pub fn matches(&self, device_x_ratio: u8, device_y_ratio: u8) -> bool {
if self.is_sentinel() {
return true;
}
device_x_ratio == self.x_ratio
&& device_y_ratio >= self.y_start_ratio
&& device_y_ratio <= self.y_end_ratio
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VdmxVTableRecord {
pub y_pel_height: u16,
pub y_max: i16,
pub y_min: i16,
}
#[derive(Debug, Clone)]
pub struct VdmxGroup {
start_sz: u8,
end_sz: u8,
entries: Vec<VdmxVTableRecord>,
}
impl VdmxGroup {
pub fn start_sz(&self) -> u8 {
self.start_sz
}
pub fn end_sz(&self) -> u8 {
self.end_sz
}
pub fn num_entries(&self) -> u16 {
self.entries.len() as u16
}
pub fn entries(&self) -> &[VdmxVTableRecord] {
&self.entries
}
pub fn record_for_ppem(&self, ppem: u16) -> Option<&VdmxVTableRecord> {
match self.entries.binary_search_by_key(&ppem, |e| e.y_pel_height) {
Ok(i) => self.entries.get(i),
Err(_) => None,
}
}
pub fn y_extent_for_ppem(&self, ppem: u16) -> Option<(i16, i16)> {
self.record_for_ppem(ppem).map(|r| (r.y_max, r.y_min))
}
}
#[derive(Debug, Clone)]
pub struct VdmxTable {
version: u16,
num_recs: u16,
ratios: Vec<RatioRange>,
ratio_group_index: Vec<usize>,
groups: Vec<VdmxGroup>,
}
impl VdmxTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < VDMX_HEADER_LEN {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
if version != VDMX_VERSION_0 && version != VDMX_VERSION_1 {
return Err(Error::BadStructure("VDMX: unrecognised version"));
}
let num_recs = read_u16(bytes, 2)?;
let num_ratios = read_u16(bytes, 4)?;
if num_ratios == 0 {
return Err(Error::BadStructure("VDMX: numRatios must be at least 1"));
}
if num_recs == 0 {
return Err(Error::BadStructure(
"VDMX: numRecs must be at least 1 (§5.7.8)",
));
}
let ratio_array_off = VDMX_HEADER_LEN;
let ratio_array_bytes = (num_ratios as usize)
.checked_mul(VDMX_RATIO_RECORD_LEN)
.ok_or(Error::BadStructure("VDMX: numRatios overflow"))?;
let offset_array_off = ratio_array_off
.checked_add(ratio_array_bytes)
.ok_or(Error::BadStructure("VDMX: ratio array overflow"))?;
let offset_array_bytes = (num_ratios as usize)
.checked_mul(VDMX_OFFSET_LEN)
.ok_or(Error::BadStructure("VDMX: offset array overflow"))?;
let after_offsets = offset_array_off
.checked_add(offset_array_bytes)
.ok_or(Error::BadStructure("VDMX: offset array overflow"))?;
if bytes.len() < after_offsets {
return Err(Error::UnexpectedEof);
}
let mut ratios = Vec::with_capacity(num_ratios as usize);
for i in 0..(num_ratios as usize) {
let off = ratio_array_off + i * VDMX_RATIO_RECORD_LEN;
let r = RatioRange {
char_set: read_u8(bytes, off)?,
x_ratio: read_u8(bytes, off + 1)?,
y_start_ratio: read_u8(bytes, off + 2)?,
y_end_ratio: read_u8(bytes, off + 3)?,
};
if r.is_sentinel() && (i + 1) != (num_ratios as usize) {
return Err(Error::BadStructure(
"VDMX: sentinel ratio record must be last",
));
}
ratios.push(r);
}
let mut raw_offsets = Vec::with_capacity(num_ratios as usize);
for i in 0..(num_ratios as usize) {
let off = offset_array_off + i * VDMX_OFFSET_LEN;
raw_offsets.push(read_u16(bytes, off)? as usize);
}
let mut unique_offsets: Vec<usize> = Vec::new();
let mut ratio_group_index = Vec::with_capacity(num_ratios as usize);
for &off in &raw_offsets {
if off == 0 {
return Err(Error::BadStructure(
"VDMX: per-ratio offset must not be zero",
));
}
let idx = match unique_offsets.iter().position(|&o| o == off) {
Some(i) => i,
None => {
unique_offsets.push(off);
unique_offsets.len() - 1
}
};
ratio_group_index.push(idx);
}
let mut groups = Vec::with_capacity(unique_offsets.len());
for &off in &unique_offsets {
groups.push(Self::parse_group(bytes, off)?);
}
if (num_recs as usize) < unique_offsets.len() {
return Err(Error::BadStructure(
"VDMX: numRecs lower than number of distinct group offsets",
));
}
Ok(Self {
version,
num_recs,
ratios,
ratio_group_index,
groups,
})
}
fn parse_group(bytes: &[u8], off: usize) -> Result<VdmxGroup, Error> {
let end = off
.checked_add(VDMX_GROUP_HEADER_LEN)
.ok_or(Error::BadStructure("VDMX: group offset overflow"))?;
if bytes.len() < end {
return Err(Error::BadOffset);
}
let recs = read_u16(bytes, off)?;
let start_sz = read_u8(bytes, off + 2)?;
let end_sz = read_u8(bytes, off + 3)?;
let entries_off = end;
let entries_bytes = (recs as usize)
.checked_mul(VDMX_VTABLE_RECORD_LEN)
.ok_or(Error::BadStructure("VDMX: group recs overflow"))?;
let entries_end = entries_off
.checked_add(entries_bytes)
.ok_or(Error::BadStructure("VDMX: group recs overflow"))?;
if bytes.len() < entries_end {
return Err(Error::UnexpectedEof);
}
let mut entries = Vec::with_capacity(recs as usize);
let mut prev_ppem: Option<u16> = None;
for i in 0..(recs as usize) {
let rec_off = entries_off + i * VDMX_VTABLE_RECORD_LEN;
let y_pel_height = read_u16(bytes, rec_off)?;
let y_max = read_i16(bytes, rec_off + 2)?;
let y_min = read_i16(bytes, rec_off + 4)?;
if let Some(prev) = prev_ppem {
if y_pel_height <= prev {
return Err(Error::BadStructure(
"VDMX: vTable yPelHeight not strictly increasing",
));
}
}
prev_ppem = Some(y_pel_height);
entries.push(VdmxVTableRecord {
y_pel_height,
y_max,
y_min,
});
}
Ok(VdmxGroup {
start_sz,
end_sz,
entries,
})
}
pub fn version_raw(&self) -> u16 {
self.version
}
pub fn num_recs(&self) -> u16 {
self.num_recs
}
pub fn num_ratios(&self) -> u16 {
self.ratios.len() as u16
}
pub fn ratios(&self) -> &[RatioRange] {
&self.ratios
}
pub fn groups(&self) -> &[VdmxGroup] {
&self.groups
}
pub fn group_for_ratio_index(&self, ratio_index: usize) -> Option<&VdmxGroup> {
let gi = *self.ratio_group_index.get(ratio_index)?;
self.groups.get(gi)
}
pub fn group_for_device_ratio(
&self,
device_x_ratio: u8,
device_y_ratio: u8,
) -> Option<&VdmxGroup> {
for (i, r) in self.ratios.iter().enumerate() {
if r.matches(device_x_ratio, device_y_ratio) {
return self.group_for_ratio_index(i);
}
}
None
}
pub fn y_extent_for_device(
&self,
ppem: u16,
device_x_ratio: u8,
device_y_ratio: u8,
) -> Option<(i16, i16)> {
self.group_for_device_ratio(device_x_ratio, device_y_ratio)?
.y_extent_for_ppem(ppem)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_simple_vdmx(version: u16, entries: &[(u16, i16, i16)]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&version.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&[1, 1, 1, 1]);
let group_off = VDMX_HEADER_LEN + VDMX_RATIO_RECORD_LEN + VDMX_OFFSET_LEN;
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&(entries.len() as u16).to_be_bytes());
out.push(entries.first().map(|e| e.0 as u8).unwrap_or(0));
out.push(entries.last().map(|e| e.0 as u8).unwrap_or(0));
for &(ppem, ymax, ymin) in entries {
out.extend_from_slice(&ppem.to_be_bytes());
out.extend_from_slice(&ymax.to_be_bytes());
out.extend_from_slice(&ymin.to_be_bytes());
}
out
}
#[test]
fn parses_single_ratio_single_group() {
let bytes = make_simple_vdmx(VDMX_VERSION_1, &[(8, 7, -2), (12, 11, -3), (16, 14, -4)]);
let t = VdmxTable::parse(&bytes).expect("parse");
assert_eq!(t.version_raw(), VDMX_VERSION_1);
assert_eq!(t.num_recs(), 1);
assert_eq!(t.num_ratios(), 1);
let r = &t.ratios()[0];
assert_eq!(r.x_ratio, 1);
assert!(!r.is_sentinel());
assert!(r.matches(1, 1));
assert!(!r.matches(2, 1));
let g = t.group_for_ratio_index(0).expect("group");
assert_eq!(g.start_sz(), 8);
assert_eq!(g.end_sz(), 16);
assert_eq!(g.num_entries(), 3);
assert_eq!(g.entries()[1].y_pel_height, 12);
assert_eq!(g.y_extent_for_ppem(12), Some((11, -3)));
assert_eq!(g.y_extent_for_ppem(14), None); assert_eq!(t.y_extent_for_device(16, 1, 1), Some((14, -4)));
assert_eq!(t.y_extent_for_device(16, 2, 1), None); }
#[test]
fn rejects_short_header() {
let bytes = vec![0u8; 5];
assert!(matches!(
VdmxTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_unknown_version() {
let mut bytes = vec![0u8; VDMX_HEADER_LEN];
bytes[0..2].copy_from_slice(&2u16.to_be_bytes());
assert!(matches!(
VdmxTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_zero_num_ratios() {
let mut bytes = vec![0u8; VDMX_HEADER_LEN];
bytes[0..2].copy_from_slice(&VDMX_VERSION_1.to_be_bytes());
bytes[2..4].copy_from_slice(&1u16.to_be_bytes());
bytes[4..6].copy_from_slice(&0u16.to_be_bytes());
assert!(matches!(
VdmxTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_zero_num_recs() {
let mut bytes = vec![0u8; VDMX_HEADER_LEN];
bytes[0..2].copy_from_slice(&VDMX_VERSION_1.to_be_bytes());
bytes[2..4].copy_from_slice(&0u16.to_be_bytes());
bytes[4..6].copy_from_slice(&1u16.to_be_bytes());
assert!(matches!(
VdmxTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_truncated_offset_array() {
let mut bytes = vec![0u8; VDMX_HEADER_LEN + VDMX_RATIO_RECORD_LEN];
bytes[0..2].copy_from_slice(&VDMX_VERSION_1.to_be_bytes());
bytes[2..4].copy_from_slice(&1u16.to_be_bytes());
bytes[4..6].copy_from_slice(&1u16.to_be_bytes());
assert!(matches!(
VdmxTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_zero_offset() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&VDMX_VERSION_1.to_be_bytes());
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.extend_from_slice(&[1, 1, 1, 1]);
bytes.extend_from_slice(&0u16.to_be_bytes());
assert!(matches!(
VdmxTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_non_monotonic_vtable() {
let mut out = Vec::new();
out.extend_from_slice(&VDMX_VERSION_1.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&[1, 1, 1, 1]);
let group_off = VDMX_HEADER_LEN + VDMX_RATIO_RECORD_LEN + VDMX_OFFSET_LEN;
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes()); out.push(12);
out.push(12);
out.extend_from_slice(&12u16.to_be_bytes());
out.extend_from_slice(&5i16.to_be_bytes());
out.extend_from_slice(&(-1i16).to_be_bytes());
out.extend_from_slice(&12u16.to_be_bytes()); out.extend_from_slice(&6i16.to_be_bytes());
out.extend_from_slice(&(-2i16).to_be_bytes());
assert!(matches!(
VdmxTable::parse(&out),
Err(Error::BadStructure(_))
));
}
#[test]
fn sentinel_must_be_last() {
let mut out = Vec::new();
out.extend_from_slice(&VDMX_VERSION_1.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes());
out.extend_from_slice(&[0, 0, 0, 0]); out.extend_from_slice(&[1, 1, 1, 1]);
let group_off = VDMX_HEADER_LEN + 2 * VDMX_RATIO_RECORD_LEN + 2 * VDMX_OFFSET_LEN;
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.push(10);
out.push(10);
out.extend_from_slice(&10u16.to_be_bytes());
out.extend_from_slice(&5i16.to_be_bytes());
out.extend_from_slice(&(-1i16).to_be_bytes());
assert!(matches!(
VdmxTable::parse(&out),
Err(Error::BadStructure(_))
));
}
#[test]
fn sentinel_at_end_matches_all_ratios() {
let mut out = Vec::new();
out.extend_from_slice(&VDMX_VERSION_1.to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes());
out.extend_from_slice(&[1, 1, 1, 1]);
out.extend_from_slice(&[0, 0, 0, 0]);
let g0_off = VDMX_HEADER_LEN + 2 * VDMX_RATIO_RECORD_LEN + 2 * VDMX_OFFSET_LEN;
let g0_size = VDMX_GROUP_HEADER_LEN + VDMX_VTABLE_RECORD_LEN;
let g1_off = g0_off + g0_size;
out.extend_from_slice(&(g0_off as u16).to_be_bytes());
out.extend_from_slice(&(g1_off as u16).to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.push(10);
out.push(10);
out.extend_from_slice(&10u16.to_be_bytes());
out.extend_from_slice(&7i16.to_be_bytes());
out.extend_from_slice(&(-2i16).to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.push(12);
out.push(12);
out.extend_from_slice(&12u16.to_be_bytes());
out.extend_from_slice(&9i16.to_be_bytes());
out.extend_from_slice(&(-3i16).to_be_bytes());
let t = VdmxTable::parse(&out).expect("parse");
assert_eq!(t.num_ratios(), 2);
assert_eq!(t.groups().len(), 2);
assert_eq!(t.y_extent_for_device(10, 1, 1), Some((7, -2)));
assert_eq!(t.y_extent_for_device(12, 2, 3), Some((9, -3)));
assert_eq!(t.y_extent_for_device(10, 2, 3), None);
}
#[test]
fn ratios_can_share_one_group() {
let mut out = Vec::new();
out.extend_from_slice(&VDMX_VERSION_1.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes());
out.extend_from_slice(&[1, 1, 1, 1]);
out.extend_from_slice(&[1, 4, 3, 3]);
let group_off = VDMX_HEADER_LEN + 2 * VDMX_RATIO_RECORD_LEN + 2 * VDMX_OFFSET_LEN;
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.push(11);
out.push(11);
out.extend_from_slice(&11u16.to_be_bytes());
out.extend_from_slice(&8i16.to_be_bytes());
out.extend_from_slice(&(-2i16).to_be_bytes());
let t = VdmxTable::parse(&out).expect("parse");
assert_eq!(t.num_ratios(), 2);
assert_eq!(t.groups().len(), 1); let g0 = t.group_for_ratio_index(0).unwrap();
let g1 = t.group_for_ratio_index(1).unwrap();
assert_eq!(g0.entries().len(), g1.entries().len());
assert_eq!(g0.y_extent_for_ppem(11), Some((8, -2)));
assert_eq!(g1.y_extent_for_ppem(11), Some((8, -2)));
}
#[test]
fn supports_y_pel_height_above_255() {
let mut out = Vec::new();
out.extend_from_slice(&VDMX_VERSION_1.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&[1, 1, 1, 1]);
let group_off = VDMX_HEADER_LEN + VDMX_RATIO_RECORD_LEN + VDMX_OFFSET_LEN;
out.extend_from_slice(&(group_off as u16).to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes());
out.push(0); out.push(0);
out.extend_from_slice(&12u16.to_be_bytes());
out.extend_from_slice(&8i16.to_be_bytes());
out.extend_from_slice(&(-2i16).to_be_bytes());
out.extend_from_slice(&1024u16.to_be_bytes());
out.extend_from_slice(&800i16.to_be_bytes());
out.extend_from_slice(&(-200i16).to_be_bytes());
let t = VdmxTable::parse(&out).expect("parse");
let g = t.group_for_ratio_index(0).unwrap();
assert_eq!(g.entries().len(), 2);
assert_eq!(g.y_extent_for_ppem(12), Some((8, -2)));
assert_eq!(g.y_extent_for_ppem(1024), Some((800, -200)));
}
#[test]
fn ratio_matches_within_y_range() {
let r = RatioRange {
char_set: 1,
x_ratio: 2,
y_start_ratio: 1,
y_end_ratio: 3,
};
assert!(r.matches(2, 1));
assert!(r.matches(2, 2));
assert!(r.matches(2, 3));
assert!(!r.matches(2, 4));
assert!(!r.matches(3, 2));
assert!(!r.matches(0, 0)); }
#[test]
fn version_0_also_parses() {
let bytes = make_simple_vdmx(VDMX_VERSION_0, &[(10, 6, -2)]);
let t = VdmxTable::parse(&bytes).expect("parse");
assert_eq!(t.version_raw(), VDMX_VERSION_0);
assert_eq!(t.y_extent_for_device(10, 1, 1), Some((6, -2)));
}
}