use crate::parser::{read_u16, read_u32};
use crate::Error;
pub const GASP_VERSION_0: u16 = 0;
pub const GASP_VERSION_1: u16 = 1;
pub const GASP_PPEM_SENTINEL: u16 = 0xFFFF;
pub const GASP_GRIDFIT: u16 = 0x0001;
pub const GASP_DOGRAY: u16 = 0x0002;
pub const GASP_SYMMETRIC_GRIDFIT: u16 = 0x0004;
pub const GASP_SYMMETRIC_SMOOTHING: u16 = 0x0008;
pub const GASP_RESERVED_MASK: u16 = 0xFFF0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GaspRange {
pub range_max_ppem: u16,
pub flags: u16,
}
impl GaspRange {
pub fn has_flag(&self, flag: u16) -> bool {
self.flags & flag != 0
}
pub fn gridfit(&self) -> bool {
self.has_flag(GASP_GRIDFIT)
}
pub fn dogray(&self) -> bool {
self.has_flag(GASP_DOGRAY)
}
pub fn symmetric_gridfit(&self) -> bool {
self.has_flag(GASP_SYMMETRIC_GRIDFIT)
}
pub fn symmetric_smoothing(&self) -> bool {
self.has_flag(GASP_SYMMETRIC_SMOOTHING)
}
pub fn reserved_bits(&self) -> u16 {
self.flags & GASP_RESERVED_MASK
}
}
#[derive(Debug, Clone)]
pub struct GaspTable {
version: u16,
ranges: Vec<GaspRange>,
}
impl GaspTable {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 4 {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
match version {
GASP_VERSION_0 | GASP_VERSION_1 => {}
_ => return Err(Error::BadStructure("gasp: unrecognised version")),
}
let num_ranges = read_u16(bytes, 2)? as usize;
let body_len = num_ranges
.checked_mul(4)
.ok_or(Error::BadStructure("gasp: numRanges overflow"))?;
let total = body_len
.checked_add(4)
.ok_or(Error::BadStructure("gasp: numRanges overflow"))?;
if bytes.len() < total {
return Err(Error::UnexpectedEof);
}
let mut ranges = Vec::with_capacity(num_ranges);
let mut prev_max: Option<u16> = None;
for i in 0..num_ranges {
let off = 4 + i * 4;
let range_max_ppem = read_u16(bytes, off)?;
let flags = read_u16(bytes, off + 2)?;
if let Some(prev) = prev_max {
if range_max_ppem <= prev {
return Err(Error::BadStructure(
"gasp: gaspRange not sorted by increasing rangeMaxPPEM",
));
}
}
prev_max = Some(range_max_ppem);
ranges.push(GaspRange {
range_max_ppem,
flags,
});
}
Ok(Self { version, ranges })
}
pub fn version_raw(&self) -> u16 {
self.version
}
pub fn is_version_1(&self) -> bool {
self.version == GASP_VERSION_1
}
pub fn ranges(&self) -> &[GaspRange] {
&self.ranges
}
pub fn covers_all_sizes(&self) -> bool {
matches!(self.ranges.as_slice(),
[r] if r.range_max_ppem == GASP_PPEM_SENTINEL)
}
pub fn behavior_for_ppem(&self, ppem: u16) -> Option<&GaspRange> {
self.ranges.iter().find(|r| r.range_max_ppem >= ppem)
}
}
pub const GASP_TABLE_TAG: u32 = 0x6761_7370;
#[allow(dead_code)] fn gasp_tag_fixed() -> u32 {
let raw = b"gasp";
read_u32(raw, 0).expect("4-byte literal")
}
#[cfg(test)]
mod tests {
use super::*;
fn make_gasp(version: u16, records: &[(u16, u16)]) -> Vec<u8> {
let mut b = Vec::with_capacity(4 + records.len() * 4);
b.extend_from_slice(&version.to_be_bytes());
b.extend_from_slice(&(records.len() as u16).to_be_bytes());
for (ppem, flags) in records {
b.extend_from_slice(&ppem.to_be_bytes());
b.extend_from_slice(&flags.to_be_bytes());
}
b
}
#[test]
fn parses_spec_sample_v1_four_ranges() {
let bytes = make_gasp(
GASP_VERSION_1,
&[(8, 0x000A), (16, 0x0005), (19, 0x0007), (0xFFFF, 0x000F)],
);
let g = GaspTable::parse(&bytes).unwrap();
assert!(g.is_version_1());
assert_eq!(g.ranges().len(), 4);
let r = g.behavior_for_ppem(8).unwrap();
assert!(r.dogray());
assert!(r.symmetric_smoothing());
assert!(!r.gridfit());
let r = g.behavior_for_ppem(12).unwrap();
assert!(r.gridfit());
assert!(r.symmetric_gridfit());
assert!(!r.dogray());
let r = g.behavior_for_ppem(19).unwrap();
assert!(r.gridfit());
assert!(r.dogray());
assert!(r.symmetric_gridfit());
let r = g.behavior_for_ppem(20).unwrap();
assert_eq!(r.range_max_ppem, GASP_PPEM_SENTINEL);
assert!(r.gridfit() && r.dogray() && r.symmetric_gridfit() && r.symmetric_smoothing());
assert!(!g.covers_all_sizes());
}
#[test]
fn single_sentinel_covers_all_sizes() {
let bytes = make_gasp(GASP_VERSION_0, &[(0xFFFF, GASP_GRIDFIT | GASP_DOGRAY)]);
let g = GaspTable::parse(&bytes).unwrap();
assert!(g.covers_all_sizes());
assert_eq!(g.version_raw(), GASP_VERSION_0);
assert!(!g.is_version_1());
for &ppem in &[1u16, 12, 96, 0xFFFE] {
let r = g.behavior_for_ppem(ppem).unwrap();
assert!(r.gridfit());
assert!(r.dogray());
assert!(!r.symmetric_gridfit());
assert!(!r.symmetric_smoothing());
}
}
#[test]
fn rejects_unsorted_records() {
let bytes = make_gasp(GASP_VERSION_0, &[(8, 0x0001), (6, 0x0002)]);
let err = GaspTable::parse(&bytes).unwrap_err();
match err {
Error::BadStructure(s) => assert!(s.contains("sorted")),
other => panic!("expected BadStructure, got {other:?}"),
}
}
#[test]
fn rejects_duplicate_rangemaxppem() {
let bytes = make_gasp(GASP_VERSION_1, &[(16, 0x0001), (16, 0x0002)]);
assert!(GaspTable::parse(&bytes).is_err());
}
#[test]
fn rejects_unrecognised_version() {
let bytes = make_gasp(2, &[(0xFFFF, 0x0003)]);
assert!(GaspTable::parse(&bytes).is_err());
}
#[test]
fn rejects_short_header() {
let bytes = vec![0u8, 0, 0];
assert!(matches!(
GaspTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn rejects_truncated_records() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&GASP_VERSION_0.to_be_bytes());
bytes.extend_from_slice(&2u16.to_be_bytes());
bytes.extend_from_slice(&8u16.to_be_bytes());
bytes.extend_from_slice(&0x0001u16.to_be_bytes());
assert!(matches!(
GaspTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
#[test]
fn reserved_bits_preserved_but_dont_block_parse() {
let bytes = make_gasp(GASP_VERSION_1, &[(0xFFFF, 0x8010 | GASP_GRIDFIT)]);
let g = GaspTable::parse(&bytes).unwrap();
let r = &g.ranges()[0];
assert!(r.gridfit());
assert_eq!(r.reserved_bits(), 0x8010);
}
#[test]
fn behavior_for_ppem_falls_off_end() {
let bytes = make_gasp(GASP_VERSION_0, &[(8, 0x0002), (16, 0x0003)]);
let g = GaspTable::parse(&bytes).unwrap();
assert!(g.behavior_for_ppem(17).is_none());
}
#[test]
fn gasp_tag_is_four_byte_ascii_literal() {
assert_eq!(GASP_TABLE_TAG, gasp_tag_fixed());
}
}