Some checks failed
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m13s
standards workspaces build + test / aac-rs (pull_request) Successful in 1m58s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 50s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 34s
standards workspaces build + test / map-rs (pull_request) Successful in 31s
standards workspaces build + test / mesh-rs (pull_request) Successful in 31s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no comparison (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no company or product (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / eoc-rs (pull_request) Has been cancelled
standards workspaces build + test / jcp-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / proof-rs (pull_request) Has been cancelled
standards workspaces build + test / sandbox-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI section references / cited sections exist (pull_request) Has been cancelled
wai-vector vector graphics / linux-aarch64 (pull_request) Has been cancelled
wai-vector vector graphics / linux-x86_64 (pull_request) Has been cancelled
wai-vector vector graphics / macos-arm64 (pull_request) Has been cancelled
wai-vector vector graphics / wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
wai-vector vector graphics / wasm32 under Node (pull_request) Has been cancelled
wai-vector vector graphics / fuzz, peak allocation, comparison count (pull_request) Has been cancelled
workflow hygiene / every workflow supersedes its own queue (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
The reference crate of wai.graphics.vector (no_std + alloc, no dependencies; SHA-256 is wai-ops's, by #[path]): - WOS1 parsed with §2.1's load checks in order, and written; - WVG1 split (steps 1-3, nothing allocated before the exact length), the record pass (step 4), the cross-record checks (step 5(a)-(h)), the sets by digest with at most length + 1 bytes read (step 6), the glyphs (7), the interval check (8) and the per-frame cost (9); - the clock with the frame range by division, track evaluation, and placement; - the JSON authoring form, one member per field, packing back to the same bytes; - the CLI: check, cost, pack, unpack, vectors gen and verify; - a wasm32 module and verify.mjs for Node. The corpus wai/vector-conformance/ (arith, canonical, refusals, geometry, cost) with its README's provenance line and a sha256.json sidecar naming the div255 table; wai-vector.yml with native, wasip2, node and fuzz jobs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1392 lines
46 KiB
Rust
1392 lines
46 KiB
Rust
//! `WVG1` (§2.2–§2.5, §2.7): the header and tables, parsed with the refusal order of
|
||
//! §2.7, and the canonical write.
|
||
//!
|
||
//! - [`split`] reads the tables once the exact length holds (steps 1–3). Nothing is
|
||
//! allocated before that, so a header's counts cannot reserve memory the bytes do not
|
||
//! back.
|
||
//! - [`parse`] adds the record pass (step 4) and the cross-record checks (step 5).
|
||
//! - [`load`] adds the sets (step 6), the glyph check (step 7), the interval check
|
||
//! (step 8) and the cost (step 9): a payload [`load`] returns renders at every
|
||
//! listed size, rate and frame.
|
||
//!
|
||
//! [`Doc`] holds every field as carried, zero fields included, so [`Doc::write`] of a
|
||
//! parsed payload gives back its bytes: the verifier's round trip (§2.5).
|
||
|
||
use alloc::vec;
|
||
use alloc::vec::Vec;
|
||
use core::cmp::Ordering;
|
||
|
||
use crate::arith::gcd;
|
||
use crate::bounds::*;
|
||
use crate::refuse::{Code, Refusal, refuse};
|
||
use crate::wos1::{self, Point, Wos1, rd16, rd32};
|
||
|
||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||
pub struct Header {
|
||
pub magic: [u8; 4],
|
||
pub format: u32,
|
||
pub tb: u32,
|
||
pub t0: u32,
|
||
pub dur: u32,
|
||
pub design_w: u16,
|
||
pub design_h: u16,
|
||
pub n_sizes: u8,
|
||
pub n_rates: u8,
|
||
pub n_sets: u8,
|
||
pub n_tables: u8,
|
||
pub n_paints: u16,
|
||
pub n_texts: u16,
|
||
pub n_tracks: u16,
|
||
pub n_paths: u16,
|
||
pub n_items: u32,
|
||
pub n_keys: u32,
|
||
pub n_glyphs: u32,
|
||
pub n_clusters: u32,
|
||
pub path_contours: u32,
|
||
pub path_points: u32,
|
||
pub strings_len: u32,
|
||
pub inline_len: u32,
|
||
pub held_len: u32,
|
||
pub zero: [u8; 8],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Size {
|
||
pub w: u16,
|
||
pub h: u16,
|
||
pub cost: u32,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Rate {
|
||
pub num: u32,
|
||
pub den: u32,
|
||
}
|
||
|
||
impl Rate {
|
||
/// Ticks per frame, `P = den·tb/num` (§3.1); an integer once the record pass holds.
|
||
pub fn ticks_per_frame(&self, tb: u32) -> u64 {
|
||
u64::from(self.den) * u64::from(tb) / u64::from(self.num)
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct SetRec {
|
||
/// 0 held, 1 inline.
|
||
pub carriage: u8,
|
||
pub zero: [u8; 3],
|
||
pub length: u32,
|
||
pub sha256: [u8; 32],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||
pub struct Table {
|
||
pub e: [u32; 17],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||
pub struct Paint {
|
||
pub rgba: [u8; 4],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Track {
|
||
/// 1 opacity, 2 translation, 3 matrix entry.
|
||
pub kind: u8,
|
||
pub zero1: u8,
|
||
pub zero2: u16,
|
||
pub n_keys: u32,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Key {
|
||
pub t: u32,
|
||
pub value: i32,
|
||
/// 0 hold, 1 linear, `2 + j` easing table `j`.
|
||
pub interp: u8,
|
||
pub zero: [u8; 3],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Path {
|
||
pub n_contours: u16,
|
||
pub n_points: u16,
|
||
pub zero: u32,
|
||
/// `xmin, ymin, xmax, ymax`.
|
||
pub bbox: [i16; 4],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Text {
|
||
pub purpose: u8,
|
||
pub dir: u8,
|
||
pub lang_len: u8,
|
||
pub mode: u8,
|
||
pub group: u16,
|
||
pub ruby: u16,
|
||
pub ruby_start: u32,
|
||
pub ruby_len: u32,
|
||
pub str_len: u32,
|
||
}
|
||
|
||
pub const KIND_BEGIN: u8 = 0;
|
||
pub const KIND_END: u8 = 1;
|
||
pub const KIND_PATH: u8 = 2;
|
||
pub const KIND_RUN: u8 = 3;
|
||
|
||
/// The track slots of an item, in their order.
|
||
pub const M00: usize = 0;
|
||
pub const M01: usize = 1;
|
||
pub const M10: usize = 2;
|
||
pub const M11: usize = 3;
|
||
pub const TX: usize = 4;
|
||
pub const TY: usize = 5;
|
||
pub const OP: usize = 6;
|
||
/// The track kind each slot requires (§2.4).
|
||
pub const SLOT_KIND: [u8; 7] = [3, 3, 3, 3, 2, 2, 1];
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Item {
|
||
pub kind: u8,
|
||
pub rule: u8,
|
||
pub zero2: u16,
|
||
pub reference: u16,
|
||
pub paint: u16,
|
||
pub text: u16,
|
||
pub zero10: u16,
|
||
pub n_glyphs: u16,
|
||
pub n_clusters: u16,
|
||
pub t_on: u32,
|
||
pub t_off: u32,
|
||
pub unit_scale: u32,
|
||
pub slots: [u16; 7],
|
||
pub zero42: [u8; 6],
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct GlyphRec {
|
||
pub gid: u16,
|
||
pub zero: u16,
|
||
pub x: i32,
|
||
pub y: i32,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Cluster {
|
||
pub glyph_count: u16,
|
||
pub zero: u16,
|
||
pub byte_first: u32,
|
||
pub byte_len: u32,
|
||
}
|
||
|
||
/// Where each owner's records start in their tables: prefix sums of the owners'
|
||
/// counts, valid once the sum rules (step 5(a)) hold.
|
||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||
pub struct Ix {
|
||
pub track_key0: Vec<u32>,
|
||
pub path_end0: Vec<u32>,
|
||
pub path_pt0: Vec<u32>,
|
||
/// The offset of each block's language tag in `STRINGS`; its string follows it.
|
||
pub text_off: Vec<u32>,
|
||
/// For each item, its first glyph record and first cluster (runs only).
|
||
pub item_glyph0: Vec<u32>,
|
||
pub item_cluster0: Vec<u32>,
|
||
}
|
||
|
||
/// A `WVG1` payload, every field as carried.
|
||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||
pub struct Doc {
|
||
pub header: Header,
|
||
pub sizes: Vec<Size>,
|
||
pub rates: Vec<Rate>,
|
||
pub sets: Vec<SetRec>,
|
||
pub tables: Vec<Table>,
|
||
pub paints: Vec<Paint>,
|
||
pub tracks: Vec<Track>,
|
||
pub keys: Vec<Key>,
|
||
pub paths: Vec<Path>,
|
||
pub path_ends: Vec<u16>,
|
||
pub path_points: Vec<Point>,
|
||
pub texts: Vec<Text>,
|
||
pub items: Vec<Item>,
|
||
pub glyphs: Vec<GlyphRec>,
|
||
pub clusters: Vec<Cluster>,
|
||
pub strings: Vec<u8>,
|
||
pub inline: Vec<u8>,
|
||
pub ix: Ix,
|
||
}
|
||
|
||
impl Header {
|
||
/// The exact length the counts give (§2.2).
|
||
pub fn exact_len(&self) -> u64 {
|
||
let h = self;
|
||
WVG1_HEADER as u64
|
||
+ REC_SIZE * u64::from(h.n_sizes)
|
||
+ REC_RATE * u64::from(h.n_rates)
|
||
+ REC_SET * u64::from(h.n_sets)
|
||
+ REC_TABLE * u64::from(h.n_tables)
|
||
+ REC_PAINT * u64::from(h.n_paints)
|
||
+ REC_TRACK * u64::from(h.n_tracks)
|
||
+ REC_KEY * u64::from(h.n_keys)
|
||
+ REC_PATH * u64::from(h.n_paths)
|
||
+ REC_PATH_END * u64::from(h.path_contours)
|
||
+ REC_PATH_POINT * u64::from(h.path_points)
|
||
+ REC_TEXT * u64::from(h.n_texts)
|
||
+ REC_ITEM * u64::from(h.n_items)
|
||
+ REC_GLYPH * u64::from(h.n_glyphs)
|
||
+ REC_CLUSTER * u64::from(h.n_clusters)
|
||
+ u64::from(h.strings_len)
|
||
+ u64::from(h.inline_len)
|
||
}
|
||
|
||
/// Read the 80-byte header. `b` holds at least 80 bytes.
|
||
pub fn read(b: &[u8]) -> Header {
|
||
let mut zero = [0u8; 8];
|
||
zero.copy_from_slice(&b[72..80]);
|
||
Header {
|
||
magic: [b[0], b[1], b[2], b[3]],
|
||
format: rd32(b, 4),
|
||
tb: rd32(b, 8),
|
||
t0: rd32(b, 12),
|
||
dur: rd32(b, 16),
|
||
design_w: rd16(b, 20),
|
||
design_h: rd16(b, 22),
|
||
n_sizes: b[24],
|
||
n_rates: b[25],
|
||
n_sets: b[26],
|
||
n_tables: b[27],
|
||
n_paints: rd16(b, 28),
|
||
n_texts: rd16(b, 30),
|
||
n_tracks: rd16(b, 32),
|
||
n_paths: rd16(b, 34),
|
||
n_items: rd32(b, 36),
|
||
n_keys: rd32(b, 40),
|
||
n_glyphs: rd32(b, 44),
|
||
n_clusters: rd32(b, 48),
|
||
path_contours: rd32(b, 52),
|
||
path_points: rd32(b, 56),
|
||
strings_len: rd32(b, 60),
|
||
inline_len: rd32(b, 64),
|
||
held_len: rd32(b, 68),
|
||
zero,
|
||
}
|
||
}
|
||
|
||
pub fn write(&self, o: &mut Vec<u8>) {
|
||
let h = self;
|
||
o.extend_from_slice(&h.magic);
|
||
for v in [h.format, h.tb, h.t0, h.dur] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
o.extend_from_slice(&h.design_w.to_le_bytes());
|
||
o.extend_from_slice(&h.design_h.to_le_bytes());
|
||
o.extend_from_slice(&[h.n_sizes, h.n_rates, h.n_sets, h.n_tables]);
|
||
for v in [h.n_paints, h.n_texts, h.n_tracks, h.n_paths] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
for v in [h.n_items, h.n_keys, h.n_glyphs, h.n_clusters, h.path_contours, h.path_points, h.strings_len, h.inline_len, h.held_len] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
o.extend_from_slice(&h.zero);
|
||
}
|
||
}
|
||
|
||
const PM: Code = Code::PayloadMalformed;
|
||
const FR: Code = Code::FieldRange;
|
||
|
||
/// Steps 1–3 of §2.7, then the tables read in file order. No record is validated.
|
||
pub fn split(b: &[u8]) -> Result<Doc, Refusal> {
|
||
// Step 1. A payload too short to hold its header has no readable magic.
|
||
if b.len() < WVG1_HEADER {
|
||
return refuse(PM, "1", "the header is incomplete", 0);
|
||
}
|
||
let h = Header::read(b);
|
||
if &h.magic != b"WVG1" {
|
||
return refuse(PM, "1", "magic", 0);
|
||
}
|
||
if h.format != 1 {
|
||
return refuse(PM, "1", "format", 0);
|
||
}
|
||
if h.zero != [0; 8] {
|
||
return refuse(PM, "1", "zero", 0);
|
||
}
|
||
// Step 2, in field order.
|
||
let caps: [(&'static str, u64, u64); 15] = [
|
||
("n_sizes", u64::from(h.n_sizes), SIZES_MAX),
|
||
("n_rates", u64::from(h.n_rates), RATES_MAX),
|
||
("n_sets", u64::from(h.n_sets), SETS_MAX),
|
||
("n_tables", u64::from(h.n_tables), TABLES_MAX),
|
||
("n_paints", u64::from(h.n_paints), PAINTS_MAX),
|
||
("n_paths", u64::from(h.n_paths), PATHS_MAX),
|
||
("n_items", u64::from(h.n_items), ITEMS_MAX),
|
||
("n_keys", u64::from(h.n_keys), KEYS_MAX),
|
||
("n_glyphs", u64::from(h.n_glyphs), GLYPHS_MAX),
|
||
("n_clusters", u64::from(h.n_clusters), CLUSTERS_MAX),
|
||
("path_contours", u64::from(h.path_contours), PATH_CONTOURS_MAX),
|
||
("path_points", u64::from(h.path_points), PATH_POINTS_MAX),
|
||
("strings_len", u64::from(h.strings_len), STRINGS_MAX),
|
||
("inline_len", u64::from(h.inline_len), INLINE_MAX),
|
||
("held_len", u64::from(h.held_len), HELD_MAX),
|
||
];
|
||
for (name, v, cap) in caps {
|
||
if v > cap {
|
||
return refuse(Code::TooLarge, "2", name, 0);
|
||
}
|
||
}
|
||
if u64::from(h.inline_len) + u64::from(h.held_len) > SETS_BYTES_MAX {
|
||
return refuse(Code::TooLarge, "2", "inline_len + held_len", 0);
|
||
}
|
||
let exact = h.exact_len();
|
||
if exact > PAYLOAD_MAX {
|
||
return refuse(Code::TooLarge, "2", "exact length", 0);
|
||
}
|
||
// Step 3.
|
||
if exact != b.len() as u64 {
|
||
return refuse(PM, "3", "exact length", 0);
|
||
}
|
||
Ok(read_tables(b, h))
|
||
}
|
||
|
||
/// The tables of a payload whose length is exact for its header. Every count is
|
||
/// backed by bytes, so the reservations below are bounded by the input.
|
||
pub fn read_tables(b: &[u8], h: Header) -> Doc {
|
||
let mut o = WVG1_HEADER;
|
||
let mut take = |n: usize| {
|
||
let s = o;
|
||
o += n;
|
||
s
|
||
};
|
||
let mut d = Doc::default();
|
||
for _ in 0..h.n_sizes {
|
||
let p = take(8);
|
||
d.sizes.push(Size { w: rd16(b, p), h: rd16(b, p + 2), cost: rd32(b, p + 4) });
|
||
}
|
||
for _ in 0..h.n_rates {
|
||
let p = take(8);
|
||
d.rates.push(Rate { num: rd32(b, p), den: rd32(b, p + 4) });
|
||
}
|
||
for _ in 0..h.n_sets {
|
||
let p = take(40);
|
||
let mut sha = [0u8; 32];
|
||
sha.copy_from_slice(&b[p + 8..p + 40]);
|
||
d.sets.push(SetRec { carriage: b[p], zero: [b[p + 1], b[p + 2], b[p + 3]], length: rd32(b, p + 4), sha256: sha });
|
||
}
|
||
for _ in 0..h.n_tables {
|
||
let p = take(68);
|
||
let mut e = [0u32; 17];
|
||
for (i, v) in e.iter_mut().enumerate() {
|
||
*v = rd32(b, p + 4 * i);
|
||
}
|
||
d.tables.push(Table { e });
|
||
}
|
||
d.paints = Vec::with_capacity(h.n_paints as usize);
|
||
for _ in 0..h.n_paints {
|
||
let p = take(4);
|
||
d.paints.push(Paint { rgba: [b[p], b[p + 1], b[p + 2], b[p + 3]] });
|
||
}
|
||
d.tracks = Vec::with_capacity(h.n_tracks as usize);
|
||
for _ in 0..h.n_tracks {
|
||
let p = take(8);
|
||
d.tracks.push(Track { kind: b[p], zero1: b[p + 1], zero2: rd16(b, p + 2), n_keys: rd32(b, p + 4) });
|
||
}
|
||
d.keys = Vec::with_capacity(h.n_keys as usize);
|
||
for _ in 0..h.n_keys {
|
||
let p = take(12);
|
||
d.keys.push(Key { t: rd32(b, p), value: rd32(b, p + 4) as i32, interp: b[p + 8], zero: [b[p + 9], b[p + 10], b[p + 11]] });
|
||
}
|
||
d.paths = Vec::with_capacity(h.n_paths as usize);
|
||
for _ in 0..h.n_paths {
|
||
let p = take(16);
|
||
d.paths.push(Path {
|
||
n_contours: rd16(b, p),
|
||
n_points: rd16(b, p + 2),
|
||
zero: rd32(b, p + 4),
|
||
bbox: [rd16(b, p + 8) as i16, rd16(b, p + 10) as i16, rd16(b, p + 12) as i16, rd16(b, p + 14) as i16],
|
||
});
|
||
}
|
||
d.path_ends = Vec::with_capacity(h.path_contours as usize);
|
||
for _ in 0..h.path_contours {
|
||
let p = take(2);
|
||
d.path_ends.push(rd16(b, p));
|
||
}
|
||
d.path_points = Vec::with_capacity(h.path_points as usize);
|
||
for _ in 0..h.path_points {
|
||
let p = take(5);
|
||
d.path_points.push(Point { x: rd16(b, p) as i16, y: rd16(b, p + 2) as i16, tag: b[p + 4] });
|
||
}
|
||
d.texts = Vec::with_capacity(h.n_texts as usize);
|
||
for _ in 0..h.n_texts {
|
||
let p = take(20);
|
||
d.texts.push(Text {
|
||
purpose: b[p],
|
||
dir: b[p + 1],
|
||
lang_len: b[p + 2],
|
||
mode: b[p + 3],
|
||
group: rd16(b, p + 4),
|
||
ruby: rd16(b, p + 6),
|
||
ruby_start: rd32(b, p + 8),
|
||
ruby_len: rd32(b, p + 12),
|
||
str_len: rd32(b, p + 16),
|
||
});
|
||
}
|
||
d.items = Vec::with_capacity(h.n_items as usize);
|
||
for _ in 0..h.n_items {
|
||
let p = take(48);
|
||
let mut slots = [0u16; 7];
|
||
for (i, s) in slots.iter_mut().enumerate() {
|
||
*s = rd16(b, p + 28 + 2 * i);
|
||
}
|
||
let mut z = [0u8; 6];
|
||
z.copy_from_slice(&b[p + 42..p + 48]);
|
||
d.items.push(Item {
|
||
kind: b[p],
|
||
rule: b[p + 1],
|
||
zero2: rd16(b, p + 2),
|
||
reference: rd16(b, p + 4),
|
||
paint: rd16(b, p + 6),
|
||
text: rd16(b, p + 8),
|
||
zero10: rd16(b, p + 10),
|
||
n_glyphs: rd16(b, p + 12),
|
||
n_clusters: rd16(b, p + 14),
|
||
t_on: rd32(b, p + 16),
|
||
t_off: rd32(b, p + 20),
|
||
unit_scale: rd32(b, p + 24),
|
||
slots,
|
||
zero42: z,
|
||
});
|
||
}
|
||
d.glyphs = Vec::with_capacity(h.n_glyphs as usize);
|
||
for _ in 0..h.n_glyphs {
|
||
let p = take(12);
|
||
d.glyphs.push(GlyphRec { gid: rd16(b, p), zero: rd16(b, p + 2), x: rd32(b, p + 4) as i32, y: rd32(b, p + 8) as i32 });
|
||
}
|
||
d.clusters = Vec::with_capacity(h.n_clusters as usize);
|
||
for _ in 0..h.n_clusters {
|
||
let p = take(12);
|
||
d.clusters.push(Cluster { glyph_count: rd16(b, p), zero: rd16(b, p + 2), byte_first: rd32(b, p + 4), byte_len: rd32(b, p + 8) });
|
||
}
|
||
let p = take(h.strings_len as usize);
|
||
d.strings = b[p..p + h.strings_len as usize].to_vec();
|
||
let p = take(h.inline_len as usize);
|
||
d.inline = b[p..p + h.inline_len as usize].to_vec();
|
||
d.header = h;
|
||
d
|
||
}
|
||
|
||
/// Steps 1–5 of §2.7.
|
||
pub fn parse(b: &[u8]) -> Result<Doc, Refusal> {
|
||
let mut d = split(b)?;
|
||
record_pass(&d)?;
|
||
sums(&d)?;
|
||
d.index();
|
||
cross(&d)?;
|
||
Ok(d)
|
||
}
|
||
|
||
/// Step 4: the record pass, in file order.
|
||
pub fn record_pass(d: &Doc) -> Result<(), Refusal> {
|
||
let h = &d.header;
|
||
let side = |v: u16| (SIDE_MIN..=SIDE_MAX).contains(&u32::from(v));
|
||
// The header's remaining fields, in field order.
|
||
if !(1..=TB_MAX).contains(&h.tb) {
|
||
return refuse(FR, "4", "tb", 0);
|
||
}
|
||
if u64::from(h.t0) >= TICK_LIMIT {
|
||
return refuse(FR, "4", "t0", 0);
|
||
}
|
||
if h.dur < 1 || u64::from(h.dur) > TICK_LIMIT - u64::from(h.t0) {
|
||
return refuse(FR, "4", "dur", 0);
|
||
}
|
||
if !side(h.design_w) {
|
||
return refuse(FR, "4", "design_w", 0);
|
||
}
|
||
if !side(h.design_h) {
|
||
return refuse(FR, "4", "design_h", 0);
|
||
}
|
||
if h.n_sizes == 0 {
|
||
return refuse(FR, "4", "n_sizes", 0);
|
||
}
|
||
if h.n_rates == 0 {
|
||
return refuse(FR, "4", "n_rates", 0);
|
||
}
|
||
if h.n_items == 0 {
|
||
return refuse(FR, "4", "n_items", 0);
|
||
}
|
||
let (dw, dh) = (u64::from(h.design_w), u64::from(h.design_h));
|
||
for (i, s) in d.sizes.iter().enumerate() {
|
||
let (w, hh) = (u64::from(s.w), u64::from(s.h));
|
||
if !side(s.w) || 8 * w < dw || w > 4 * dw {
|
||
return refuse(FR, "4", "SIZE.w", i as u64);
|
||
}
|
||
if !side(s.h) || w * dh != hh * dw {
|
||
return refuse(FR, "4", "SIZE.h", i as u64);
|
||
}
|
||
}
|
||
for (i, r) in d.rates.iter().enumerate() {
|
||
if !(1..=RATE_TERM_MAX).contains(&r.num) {
|
||
return refuse(FR, "4", "RATE.num", i as u64);
|
||
}
|
||
let (num, den) = (u64::from(r.num), u64::from(r.den));
|
||
if !(1..=RATE_TERM_MAX).contains(&r.den) || gcd(num, den) != 1 || num > RATE_MAX_FPS * den || (den * u64::from(h.tb)) % num != 0 {
|
||
return refuse(FR, "4", "RATE.den", i as u64);
|
||
}
|
||
}
|
||
for (i, s) in d.sets.iter().enumerate() {
|
||
if s.carriage > 1 {
|
||
return refuse(FR, "4", "SET.carriage", i as u64);
|
||
}
|
||
if s.zero != [0; 3] {
|
||
return refuse(PM, "4", "SET.zero", i as u64);
|
||
}
|
||
if !(SET_LEN_MIN..=SET_LEN_MAX).contains(&s.length) {
|
||
return refuse(FR, "4", "SET.length", i as u64);
|
||
}
|
||
}
|
||
for (i, t) in d.tables.iter().enumerate() {
|
||
for (j, &e) in t.e.iter().enumerate() {
|
||
let ok = match j {
|
||
0 => e == 0,
|
||
16 => e == Q16_ONE,
|
||
_ => e <= Q16_ONE,
|
||
};
|
||
if !ok {
|
||
return refuse(FR, "4", "TABLE.E", i as u64);
|
||
}
|
||
}
|
||
}
|
||
for (i, p) in d.paints.iter().enumerate() {
|
||
let [r, g, b, a] = p.rgba;
|
||
if r > a || g > a || b > a {
|
||
return refuse(FR, "4", "PAINT: r, g, b ≤ a", i as u64);
|
||
}
|
||
}
|
||
for (i, t) in d.tracks.iter().enumerate() {
|
||
if !(1..=3).contains(&t.kind) {
|
||
return refuse(FR, "4", "TRACK.kind", i as u64);
|
||
}
|
||
if t.zero1 != 0 || t.zero2 != 0 {
|
||
return refuse(PM, "4", "TRACK.zero", i as u64);
|
||
}
|
||
if !(1..=TRACK_KEYS_MAX).contains(&t.n_keys) {
|
||
return refuse(FR, "4", "TRACK.n_keys", i as u64);
|
||
}
|
||
}
|
||
for (i, k) in d.keys.iter().enumerate() {
|
||
if u64::from(k.t) >= TICK_LIMIT {
|
||
return refuse(FR, "4", "KEY.t", i as u64);
|
||
}
|
||
if u32::from(k.interp) >= 2 + u32::from(h.n_tables) {
|
||
return refuse(FR, "4", "KEY.interp", i as u64);
|
||
}
|
||
if k.zero != [0; 3] {
|
||
return refuse(PM, "4", "KEY.zero", i as u64);
|
||
}
|
||
}
|
||
for (i, p) in d.paths.iter().enumerate() {
|
||
if p.n_contours < 1 {
|
||
return refuse(FR, "4", "PATH.n_contours", i as u64);
|
||
}
|
||
if u64::from(p.n_points) > POINTS_MAX {
|
||
return refuse(Code::PointsLimit, "4", "PATH.n_points", i as u64);
|
||
}
|
||
if p.n_points < 1 || p.n_contours > p.n_points {
|
||
return refuse(FR, "4", "PATH.n_points", i as u64);
|
||
}
|
||
if p.zero != 0 {
|
||
return refuse(PM, "4", "PATH.zero", i as u64);
|
||
}
|
||
}
|
||
if let Some(i) = d.path_points.iter().position(|p| p.tag > 2) {
|
||
return refuse(FR, "4", "PATH_POINT.tag", i as u64);
|
||
}
|
||
for (i, t) in d.texts.iter().enumerate() {
|
||
let i = i as u64;
|
||
if t.purpose == 0 {
|
||
return refuse(FR, "4", "TEXT.purpose", i);
|
||
}
|
||
if t.dir > 1 {
|
||
return refuse(FR, "4", "TEXT.dir", i);
|
||
}
|
||
if !(1..=LANG_MAX).contains(&t.lang_len) {
|
||
return refuse(FR, "4", "TEXT.lang_len", i);
|
||
}
|
||
if t.mode > 2 {
|
||
return refuse(FR, "4", "TEXT.mode", i);
|
||
}
|
||
if t.ruby > h.n_texts {
|
||
return refuse(FR, "4", "TEXT.ruby", i);
|
||
}
|
||
if t.ruby == 0 && t.ruby_start != 0 {
|
||
return refuse(FR, "4", "TEXT.ruby_start", i);
|
||
}
|
||
if (t.ruby == 0) != (t.ruby_len == 0) {
|
||
return refuse(FR, "4", "TEXT.ruby_len", i);
|
||
}
|
||
if !(1..=STR_MAX).contains(&t.str_len) {
|
||
return refuse(FR, "4", "TEXT.str_len", i);
|
||
}
|
||
}
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
item_fields(h, it, i as u64)?;
|
||
}
|
||
for (i, g) in d.glyphs.iter().enumerate() {
|
||
if g.zero != 0 {
|
||
return refuse(PM, "4", "GLYPH.zero", i as u64);
|
||
}
|
||
if !(-GLYPH_POS_LIMIT..GLYPH_POS_LIMIT).contains(&g.x) {
|
||
return refuse(FR, "4", "GLYPH.x", i as u64);
|
||
}
|
||
if !(-GLYPH_POS_LIMIT..GLYPH_POS_LIMIT).contains(&g.y) {
|
||
return refuse(FR, "4", "GLYPH.y", i as u64);
|
||
}
|
||
}
|
||
if let Some(i) = d.clusters.iter().position(|c| c.zero != 0) {
|
||
return refuse(PM, "4", "CLUSTER.zero", i as u64);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// An `ITEM`'s fields in offset order (§2.4): each field's own range, then any rule
|
||
/// that relates it to an earlier field of the record.
|
||
fn item_fields(h: &Header, it: &Item, i: u64) -> Result<(), Refusal> {
|
||
let k = it.kind;
|
||
if k > KIND_RUN {
|
||
return refuse(FR, "4", "ITEM.kind", i);
|
||
}
|
||
let ok = match k {
|
||
KIND_BEGIN | KIND_PATH => it.rule <= 1,
|
||
_ => it.rule == 0,
|
||
};
|
||
if !ok {
|
||
return refuse(FR, "4", "ITEM.rule", i);
|
||
}
|
||
if it.zero2 != 0 {
|
||
return refuse(PM, "4", "ITEM.zero@2", i);
|
||
}
|
||
let r = it.reference;
|
||
let ok = match k {
|
||
KIND_BEGIN => r == NONE || r < h.n_paths,
|
||
KIND_END => r == 0,
|
||
KIND_PATH => r < h.n_paths,
|
||
_ => r < u16::from(h.n_sets),
|
||
};
|
||
if !ok || (k == KIND_BEGIN && r == NONE && it.rule != 0) {
|
||
return refuse(FR, "4", "ITEM.ref", i);
|
||
}
|
||
let ok = match k {
|
||
KIND_BEGIN => it.paint == NONE,
|
||
KIND_END => it.paint == 0,
|
||
_ => it.paint < h.n_paints,
|
||
};
|
||
if !ok {
|
||
return refuse(FR, "4", "ITEM.paint", i);
|
||
}
|
||
let ok = match k {
|
||
KIND_BEGIN => it.text == NONE,
|
||
KIND_END => it.text == 0,
|
||
KIND_PATH => it.text == NONE || it.text < h.n_texts,
|
||
_ => it.text < h.n_texts,
|
||
};
|
||
if !ok {
|
||
return refuse(FR, "4", "ITEM.text", i);
|
||
}
|
||
if it.zero10 != 0 {
|
||
return refuse(PM, "4", "ITEM.zero@10", i);
|
||
}
|
||
let run = k == KIND_RUN;
|
||
if (run && !(1..=RUN_GLYPHS_MAX).contains(&it.n_glyphs)) || (!run && it.n_glyphs != 0) {
|
||
return refuse(FR, "4", "ITEM.n_glyphs", i);
|
||
}
|
||
if (run && !(1..=RUN_CLUSTERS_MAX).contains(&it.n_clusters)) || (!run && it.n_clusters != 0) {
|
||
return refuse(FR, "4", "ITEM.n_clusters", i);
|
||
}
|
||
let end = k == KIND_END;
|
||
if (end && it.t_on != 0) || (!end && u64::from(it.t_on) >= TICK_LIMIT) {
|
||
return refuse(FR, "4", "ITEM.t_on", i);
|
||
}
|
||
if (end && it.t_off != 0) || (!end && (u64::from(it.t_off) > TICK_LIMIT || it.t_on >= it.t_off)) {
|
||
return refuse(FR, "4", "ITEM.t_off", i);
|
||
}
|
||
let us = it.unit_scale;
|
||
let ok = match k {
|
||
KIND_BEGIN => us <= UNIT_SCALE_MAX && (r == NONE) == (us == 0),
|
||
KIND_END => us == 0,
|
||
_ => (1..=UNIT_SCALE_MAX).contains(&us),
|
||
};
|
||
if !ok {
|
||
return refuse(FR, "4", "ITEM.unit_scale", i);
|
||
}
|
||
for (s, &v) in it.slots.iter().enumerate() {
|
||
let ok = match k {
|
||
KIND_END => v == 0,
|
||
KIND_BEGIN if s < TX => v == NONE,
|
||
_ => v == NONE || v < h.n_tracks,
|
||
};
|
||
if !ok {
|
||
return refuse(FR, "4", "ITEM.slot", i);
|
||
}
|
||
}
|
||
if it.zero42 != [0; 6] {
|
||
return refuse(PM, "4", "ITEM.zero@42", i);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Step 5(a): the sum rules.
|
||
pub fn sums(d: &Doc) -> Result<(), Refusal> {
|
||
let h = &d.header;
|
||
let by = |c: u8| d.sets.iter().filter(|s| s.carriage == c).map(|s| u64::from(s.length)).sum::<u64>();
|
||
if by(1) != u64::from(h.inline_len) {
|
||
return refuse(PM, "5a", "SET lengths, inline", 0);
|
||
}
|
||
if by(0) != u64::from(h.held_len) {
|
||
return refuse(PM, "5a", "SET lengths, held", 0);
|
||
}
|
||
if d.tracks.iter().map(|t| u64::from(t.n_keys)).sum::<u64>() != u64::from(h.n_keys) {
|
||
return refuse(PM, "5a", "TRACK n_keys", 0);
|
||
}
|
||
if d.paths.iter().map(|p| u64::from(p.n_contours)).sum::<u64>() != u64::from(h.path_contours) {
|
||
return refuse(PM, "5a", "PATH n_contours", 0);
|
||
}
|
||
if d.paths.iter().map(|p| u64::from(p.n_points)).sum::<u64>() != u64::from(h.path_points) {
|
||
return refuse(PM, "5a", "PATH n_points", 0);
|
||
}
|
||
if d.texts.iter().map(|t| u64::from(t.lang_len) + u64::from(t.str_len)).sum::<u64>() != u64::from(h.strings_len) {
|
||
return refuse(PM, "5a", "TEXT lang_len + str_len", 0);
|
||
}
|
||
let runs = || d.items.iter().filter(|it| it.kind == KIND_RUN);
|
||
if runs().map(|it| u64::from(it.n_glyphs)).sum::<u64>() != u64::from(h.n_glyphs) {
|
||
return refuse(PM, "5a", "run n_glyphs", 0);
|
||
}
|
||
if runs().map(|it| u64::from(it.n_clusters)).sum::<u64>() != u64::from(h.n_clusters) {
|
||
return refuse(PM, "5a", "run n_clusters", 0);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
impl Doc {
|
||
/// Fill [`Ix`] from the owners' counts.
|
||
pub fn index(&mut self) {
|
||
let mut ix = Ix::default();
|
||
let mut a = 0u32;
|
||
for t in &self.tracks {
|
||
ix.track_key0.push(a);
|
||
a += t.n_keys;
|
||
}
|
||
let (mut e, mut p) = (0u32, 0u32);
|
||
for q in &self.paths {
|
||
ix.path_end0.push(e);
|
||
ix.path_pt0.push(p);
|
||
e += u32::from(q.n_contours);
|
||
p += u32::from(q.n_points);
|
||
}
|
||
let mut s = 0u32;
|
||
for t in &self.texts {
|
||
ix.text_off.push(s);
|
||
s += u32::from(t.lang_len) + t.str_len;
|
||
}
|
||
let (mut g, mut c) = (0u32, 0u32);
|
||
for it in &self.items {
|
||
ix.item_glyph0.push(g);
|
||
ix.item_cluster0.push(c);
|
||
if it.kind == KIND_RUN {
|
||
g += u32::from(it.n_glyphs);
|
||
c += u32::from(it.n_clusters);
|
||
}
|
||
}
|
||
self.ix = ix;
|
||
}
|
||
|
||
pub fn track_keys(&self, t: usize) -> &[Key] {
|
||
&self.keys[self.ix.track_key0[t] as usize..][..self.tracks[t].n_keys as usize]
|
||
}
|
||
pub fn path_ends_of(&self, p: usize) -> &[u16] {
|
||
&self.path_ends[self.ix.path_end0[p] as usize..][..self.paths[p].n_contours as usize]
|
||
}
|
||
pub fn path_points_of(&self, p: usize) -> &[Point] {
|
||
&self.path_points[self.ix.path_pt0[p] as usize..][..self.paths[p].n_points as usize]
|
||
}
|
||
pub fn lang(&self, t: usize) -> &[u8] {
|
||
&self.strings[self.ix.text_off[t] as usize..][..self.texts[t].lang_len as usize]
|
||
}
|
||
pub fn string(&self, t: usize) -> &[u8] {
|
||
let o = self.ix.text_off[t] as usize + self.texts[t].lang_len as usize;
|
||
&self.strings[o..o + self.texts[t].str_len as usize]
|
||
}
|
||
pub fn run_glyphs(&self, i: usize) -> &[GlyphRec] {
|
||
&self.glyphs[self.ix.item_glyph0[i] as usize..][..self.items[i].n_glyphs as usize]
|
||
}
|
||
pub fn run_clusters(&self, i: usize) -> &[Cluster] {
|
||
&self.clusters[self.ix.item_cluster0[i] as usize..][..self.items[i].n_clusters as usize]
|
||
}
|
||
|
||
/// Set the header's counts from the tables' lengths, so the length is exact.
|
||
pub fn sync_counts(&mut self) {
|
||
let h = &mut self.header;
|
||
h.n_sizes = self.sizes.len() as u8;
|
||
h.n_rates = self.rates.len() as u8;
|
||
h.n_sets = self.sets.len() as u8;
|
||
h.n_tables = self.tables.len() as u8;
|
||
h.n_paints = self.paints.len() as u16;
|
||
h.n_texts = self.texts.len() as u16;
|
||
h.n_tracks = self.tracks.len() as u16;
|
||
h.n_paths = self.paths.len() as u16;
|
||
h.n_items = self.items.len() as u32;
|
||
h.n_keys = self.keys.len() as u32;
|
||
h.n_glyphs = self.glyphs.len() as u32;
|
||
h.n_clusters = self.clusters.len() as u32;
|
||
h.path_contours = self.path_ends.len() as u32;
|
||
h.path_points = self.path_points.len() as u32;
|
||
h.strings_len = self.strings.len() as u32;
|
||
h.inline_len = self.inline.len() as u32;
|
||
}
|
||
|
||
/// The canonical bytes: the header as held, then every table in file order.
|
||
pub fn write(&self) -> Vec<u8> {
|
||
let mut o = Vec::with_capacity(self.header.exact_len().min(PAYLOAD_MAX) as usize);
|
||
self.header.write(&mut o);
|
||
for s in &self.sizes {
|
||
o.extend_from_slice(&s.w.to_le_bytes());
|
||
o.extend_from_slice(&s.h.to_le_bytes());
|
||
o.extend_from_slice(&s.cost.to_le_bytes());
|
||
}
|
||
for r in &self.rates {
|
||
o.extend_from_slice(&r.num.to_le_bytes());
|
||
o.extend_from_slice(&r.den.to_le_bytes());
|
||
}
|
||
for s in &self.sets {
|
||
o.push(s.carriage);
|
||
o.extend_from_slice(&s.zero);
|
||
o.extend_from_slice(&s.length.to_le_bytes());
|
||
o.extend_from_slice(&s.sha256);
|
||
}
|
||
for t in &self.tables {
|
||
for e in t.e {
|
||
o.extend_from_slice(&e.to_le_bytes());
|
||
}
|
||
}
|
||
for p in &self.paints {
|
||
o.extend_from_slice(&p.rgba);
|
||
}
|
||
for t in &self.tracks {
|
||
o.push(t.kind);
|
||
o.push(t.zero1);
|
||
o.extend_from_slice(&t.zero2.to_le_bytes());
|
||
o.extend_from_slice(&t.n_keys.to_le_bytes());
|
||
}
|
||
for k in &self.keys {
|
||
o.extend_from_slice(&k.t.to_le_bytes());
|
||
o.extend_from_slice(&k.value.to_le_bytes());
|
||
o.push(k.interp);
|
||
o.extend_from_slice(&k.zero);
|
||
}
|
||
for p in &self.paths {
|
||
o.extend_from_slice(&p.n_contours.to_le_bytes());
|
||
o.extend_from_slice(&p.n_points.to_le_bytes());
|
||
o.extend_from_slice(&p.zero.to_le_bytes());
|
||
for v in p.bbox {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
}
|
||
for e in &self.path_ends {
|
||
o.extend_from_slice(&e.to_le_bytes());
|
||
}
|
||
for p in &self.path_points {
|
||
o.extend_from_slice(&p.x.to_le_bytes());
|
||
o.extend_from_slice(&p.y.to_le_bytes());
|
||
o.push(p.tag);
|
||
}
|
||
for t in &self.texts {
|
||
o.extend_from_slice(&[t.purpose, t.dir, t.lang_len, t.mode]);
|
||
o.extend_from_slice(&t.group.to_le_bytes());
|
||
o.extend_from_slice(&t.ruby.to_le_bytes());
|
||
for v in [t.ruby_start, t.ruby_len, t.str_len] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
}
|
||
for it in &self.items {
|
||
o.push(it.kind);
|
||
o.push(it.rule);
|
||
for v in [it.zero2, it.reference, it.paint, it.text, it.zero10, it.n_glyphs, it.n_clusters] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
for v in [it.t_on, it.t_off, it.unit_scale] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
for v in it.slots {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
o.extend_from_slice(&it.zero42);
|
||
}
|
||
for g in &self.glyphs {
|
||
o.extend_from_slice(&g.gid.to_le_bytes());
|
||
o.extend_from_slice(&g.zero.to_le_bytes());
|
||
o.extend_from_slice(&g.x.to_le_bytes());
|
||
o.extend_from_slice(&g.y.to_le_bytes());
|
||
}
|
||
for c in &self.clusters {
|
||
o.extend_from_slice(&c.glyph_count.to_le_bytes());
|
||
o.extend_from_slice(&c.zero.to_le_bytes());
|
||
o.extend_from_slice(&c.byte_first.to_le_bytes());
|
||
o.extend_from_slice(&c.byte_len.to_le_bytes());
|
||
}
|
||
o.extend_from_slice(&self.strings);
|
||
o.extend_from_slice(&self.inline);
|
||
o
|
||
}
|
||
|
||
/// The `TEXTS` table's bytes as carried.
|
||
pub fn texts_bytes(&self) -> Vec<u8> {
|
||
let mut o = Vec::with_capacity(20 * self.texts.len());
|
||
for t in &self.texts {
|
||
o.extend_from_slice(&[t.purpose, t.dir, t.lang_len, t.mode]);
|
||
o.extend_from_slice(&t.group.to_le_bytes());
|
||
o.extend_from_slice(&t.ruby.to_le_bytes());
|
||
for v in [t.ruby_start, t.ruby_len, t.str_len] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
}
|
||
o
|
||
}
|
||
}
|
||
|
||
/// Step 5 after the sum rules: (b) ordering, (c) typing, (d) groups, (e) paths, (f)
|
||
/// strings, (g) the canonical rules, (h) decorative path alternatives.
|
||
pub fn cross(d: &Doc) -> Result<(), Refusal> {
|
||
// (b)
|
||
for i in 1..d.sizes.len() {
|
||
if d.sizes[i].w <= d.sizes[i - 1].w {
|
||
return refuse(PM, "5b", "SIZEs ascend by w", i as u64);
|
||
}
|
||
}
|
||
for i in 1..d.rates.len() {
|
||
let (a, b) = (d.rates[i - 1], d.rates[i]);
|
||
if u64::from(a.num) * u64::from(b.den) >= u64::from(b.num) * u64::from(a.den) {
|
||
return refuse(PM, "5b", "RATEs ascend by num/den", i as u64);
|
||
}
|
||
}
|
||
for t in 0..d.tracks.len() {
|
||
let keys = d.track_keys(t);
|
||
if keys.windows(2).any(|w| w[1].t <= w[0].t) {
|
||
return refuse(PM, "5b", "keys ascend in t", t as u64);
|
||
}
|
||
if keys.last().is_some_and(|k| k.interp != 0) {
|
||
return refuse(PM, "5b", "the last key's interp is 0", t as u64);
|
||
}
|
||
}
|
||
// (c) typing, field_range: KEYS in order, then ITEMS in order.
|
||
for t in 0..d.tracks.len() {
|
||
let (lo, hi) = match d.tracks[t].kind {
|
||
1 => (0, OPACITY_MAX),
|
||
2 => (-TRANSLATION_MAX, TRANSLATION_MAX),
|
||
_ => (-MATRIX_MAX, MATRIX_MAX),
|
||
};
|
||
if let Some(j) = d.track_keys(t).iter().position(|k| !(lo..=hi).contains(&k.value)) {
|
||
return refuse(FR, "5c", "KEY.value for its track's kind", (d.ix.track_key0[t] as usize + j) as u64);
|
||
}
|
||
}
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
if it.kind == KIND_END {
|
||
continue;
|
||
}
|
||
for (s, &v) in it.slots.iter().enumerate() {
|
||
if v != NONE && d.tracks[v as usize].kind != SLOT_KIND[s] {
|
||
return refuse(FR, "5c", "a slot names a track of its kind", i as u64);
|
||
}
|
||
}
|
||
}
|
||
// (d)
|
||
let mut depth = 0u32;
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
match it.kind {
|
||
KIND_BEGIN => {
|
||
depth += 1;
|
||
if depth > GROUP_DEPTH_MAX {
|
||
return refuse(PM, "5d", "group depth", i as u64);
|
||
}
|
||
}
|
||
KIND_END => {
|
||
if depth == 0 {
|
||
return refuse(PM, "5d", "a group end with no group open", i as u64);
|
||
}
|
||
depth -= 1;
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
if depth != 0 {
|
||
return refuse(PM, "5d", "a group open after the last item", 0);
|
||
}
|
||
// (e)
|
||
for p in 0..d.paths.len() {
|
||
let (ends, pts) = (d.path_ends_of(p), d.path_points_of(p));
|
||
if !wos1::ends_ok(ends, d.paths[p].n_points) {
|
||
return refuse(PM, "5e", "PATH contour ends", p as u64);
|
||
}
|
||
if !wos1::outline_grammar_ok(pts, ends) {
|
||
return refuse(PM, "5e", "PATH contour grammar", p as u64);
|
||
}
|
||
if wos1::exact_box(pts) != d.paths[p].bbox {
|
||
return refuse(PM, "5e", "PATH box", p as u64);
|
||
}
|
||
}
|
||
// (f)
|
||
for t in 0..d.texts.len() {
|
||
if !lang_tag_ok(d.lang(t)) {
|
||
return refuse(PM, "5f", "language tag", t as u64);
|
||
}
|
||
if !string_ok(d.string(t)) {
|
||
return refuse(PM, "5f", "string", t as u64);
|
||
}
|
||
}
|
||
// (g)
|
||
first_reference(d)?;
|
||
no_duplicates(d)?;
|
||
text_blocks(d)?;
|
||
clusters(d)?;
|
||
// (h)
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
if it.kind == KIND_PATH && it.text != NONE && d.texts[it.text as usize].purpose == DECORATIVE {
|
||
return refuse(PM, "5h", "a path's alternative is decorative", i as u64);
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// §2.5 rule 6: ASCII letters, digits and hyphens, with no leading, trailing or
|
||
/// doubled hyphen.
|
||
pub fn lang_tag_ok(t: &[u8]) -> bool {
|
||
!t.is_empty()
|
||
&& t.iter().all(|c| c.is_ascii_alphanumeric() || *c == b'-')
|
||
&& t[0] != b'-'
|
||
&& t[t.len() - 1] != b'-'
|
||
&& !t.windows(2).any(|w| w == b"--")
|
||
}
|
||
|
||
/// §2.5 rule 6: valid UTF-8, no C0 control but U+000A, no U+007F, no C1 control.
|
||
pub fn string_ok(s: &[u8]) -> bool {
|
||
match core::str::from_utf8(s) {
|
||
Ok(s) => s.chars().all(|c| {
|
||
let v = c as u32;
|
||
!(v < 0x20 && v != 0x0A || v == 0x7F || (0x80..=0x9F).contains(&v))
|
||
}),
|
||
Err(_) => false,
|
||
}
|
||
}
|
||
|
||
/// A code-point boundary of a valid UTF-8 string: its end, or a byte that does not
|
||
/// continue a sequence.
|
||
fn boundary(s: &[u8], i: u64) -> bool {
|
||
i == s.len() as u64 || (i < s.len() as u64 && s[i as usize] & 0xC0 != 0x80)
|
||
}
|
||
|
||
/// First references in order 0, 1, 2, …, and every entry referenced.
|
||
struct FirstRef {
|
||
seen: Vec<bool>,
|
||
next: usize,
|
||
}
|
||
|
||
impl FirstRef {
|
||
fn new(n: usize) -> FirstRef {
|
||
FirstRef { seen: vec![false; n], next: 0 }
|
||
}
|
||
fn see(&mut self, v: usize) -> bool {
|
||
if !self.seen[v] {
|
||
if v != self.next {
|
||
return false;
|
||
}
|
||
self.seen[v] = true;
|
||
self.next += 1;
|
||
}
|
||
true
|
||
}
|
||
fn complete(&self) -> bool {
|
||
self.next == self.seen.len()
|
||
}
|
||
}
|
||
|
||
/// §2.5 rule 2.
|
||
fn first_reference(d: &Doc) -> Result<(), Refusal> {
|
||
let (mut paths, mut sets) = (FirstRef::new(d.paths.len()), FirstRef::new(d.sets.len()));
|
||
let (mut paints, mut tracks) = (FirstRef::new(d.paints.len()), FirstRef::new(d.tracks.len()));
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
if it.kind == KIND_END {
|
||
continue;
|
||
}
|
||
let ok = match it.kind {
|
||
KIND_RUN => sets.see(it.reference as usize),
|
||
_ if it.reference == NONE => true,
|
||
_ => paths.see(it.reference as usize),
|
||
} && (it.paint == NONE || paints.see(it.paint as usize))
|
||
&& it.slots.iter().all(|&s| s == NONE || tracks.see(s as usize));
|
||
if !ok {
|
||
return refuse(PM, "5g", "rule 2: first reference", i as u64);
|
||
}
|
||
}
|
||
if !(paths.complete() && sets.complete() && paints.complete() && tracks.complete()) {
|
||
return refuse(PM, "5g", "rule 2: every entry referenced", 0);
|
||
}
|
||
let mut tables = FirstRef::new(d.tables.len());
|
||
for t in 0..d.tracks.len() {
|
||
for k in d.track_keys(t) {
|
||
if k.interp >= 2 && !tables.see(k.interp as usize - 2) {
|
||
return refuse(PM, "5g", "rule 2: table first reference", t as u64);
|
||
}
|
||
}
|
||
}
|
||
if !tables.complete() {
|
||
return refuse(PM, "5g", "rule 2: every table referenced", 0);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Whether two entries of a pool of `n` compare equal, found by sorting the pool's
|
||
/// indices with `cmp`: at most about `n·log2 n` comparisons, never every pair
|
||
/// (§2.5 rule 3).
|
||
pub fn has_duplicate(n: usize, mut cmp: impl FnMut(usize, usize) -> Ordering) -> bool {
|
||
if n < 2 {
|
||
return false;
|
||
}
|
||
let mut idx: Vec<u32> = (0..n as u32).collect();
|
||
idx.sort_unstable_by(|&a, &b| cmp(a as usize, b as usize));
|
||
idx.windows(2).any(|w| cmp(w[0] as usize, w[1] as usize) == Ordering::Equal)
|
||
}
|
||
|
||
/// Tracks compare by kind, then by their key sequences.
|
||
pub fn cmp_tracks(d: &Doc, a: usize, b: usize) -> Ordering {
|
||
let key = |k: &Key| (k.t, k.value, k.interp);
|
||
d.tracks[a].kind.cmp(&d.tracks[b].kind).then_with(|| d.track_keys(a).iter().map(key).cmp(d.track_keys(b).iter().map(key)))
|
||
}
|
||
|
||
/// Paths compare by record, then contour ends, then points.
|
||
pub fn cmp_paths(d: &Doc, a: usize, b: usize) -> Ordering {
|
||
let rec = |p: &Path| (p.n_contours, p.n_points, p.zero, p.bbox);
|
||
rec(&d.paths[a])
|
||
.cmp(&rec(&d.paths[b]))
|
||
.then_with(|| d.path_ends_of(a).cmp(d.path_ends_of(b)))
|
||
.then_with(|| d.path_points_of(a).cmp(d.path_points_of(b)))
|
||
}
|
||
|
||
/// §2.5 rule 3.
|
||
fn no_duplicates(d: &Doc) -> Result<(), Refusal> {
|
||
if has_duplicate(d.sets.len(), |a, b| d.sets[a].sha256.cmp(&d.sets[b].sha256)) {
|
||
return refuse(PM, "5g", "rule 3: duplicate set", 0);
|
||
}
|
||
if has_duplicate(d.paints.len(), |a, b| d.paints[a].cmp(&d.paints[b])) {
|
||
return refuse(PM, "5g", "rule 3: duplicate paint", 0);
|
||
}
|
||
if has_duplicate(d.tracks.len(), |a, b| cmp_tracks(d, a, b)) {
|
||
return refuse(PM, "5g", "rule 3: duplicate track", 0);
|
||
}
|
||
if has_duplicate(d.paths.len(), |a, b| cmp_paths(d, a, b)) {
|
||
return refuse(PM, "5g", "rule 3: duplicate path", 0);
|
||
}
|
||
if has_duplicate(d.tables.len(), |a, b| d.tables[a].cmp(&d.tables[b])) {
|
||
return refuse(PM, "5g", "rule 3: duplicate table", 0);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// §2.5 rule 4.
|
||
fn text_blocks(d: &Doc) -> Result<(), Refusal> {
|
||
let mut named = vec![false; d.texts.len()];
|
||
for it in &d.items {
|
||
if (it.kind == KIND_RUN || it.kind == KIND_PATH) && it.text != NONE {
|
||
named[it.text as usize] = true;
|
||
}
|
||
}
|
||
if let Some(t) = named.iter().position(|n| !n) {
|
||
return refuse(PM, "5g", "rule 4: a block nothing draws", t as u64);
|
||
}
|
||
let mut max_group = 0u16;
|
||
for (i, t) in d.texts.iter().enumerate() {
|
||
let g = t.group;
|
||
if g != 0 {
|
||
if g > max_group.saturating_add(1) {
|
||
return refuse(PM, "5g", "rule 4: group numbered out of first use", i as u64);
|
||
}
|
||
if g <= max_group && d.texts[i - 1].group != g {
|
||
return refuse(PM, "5g", "rule 4: a group's blocks are not contiguous", i as u64);
|
||
}
|
||
max_group = max_group.max(g);
|
||
}
|
||
if t.ruby != 0 {
|
||
let base = t.ruby as usize - 1;
|
||
if base >= i {
|
||
return refuse(PM, "5g", "rule 4: ruby names a later block", i as u64);
|
||
}
|
||
let s = d.string(base);
|
||
let (a, n) = (u64::from(t.ruby_start), u64::from(t.ruby_len));
|
||
if a + n > s.len() as u64 || !boundary(s, a) || !boundary(s, a + n) {
|
||
return refuse(PM, "5g", "rule 4: ruby range", i as u64);
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// §2.5 rule 5.
|
||
fn clusters(d: &Doc) -> Result<(), Refusal> {
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
if it.kind != KIND_RUN {
|
||
continue;
|
||
}
|
||
let cs = d.run_clusters(i);
|
||
if cs.iter().map(|c| u64::from(c.glyph_count)).sum::<u64>() != u64::from(it.n_glyphs) {
|
||
return refuse(PM, "5g", "rule 5: glyph counts sum to n_glyphs", i as u64);
|
||
}
|
||
let s = d.string(it.text as usize);
|
||
for c in cs {
|
||
let (a, n) = (u64::from(c.byte_first), u64::from(c.byte_len));
|
||
if a + n > s.len() as u64 || !boundary(s, a) || !boundary(s, a + n) {
|
||
return refuse(PM, "5g", "rule 5: cluster range", i as u64);
|
||
}
|
||
if c.glyph_count == 0 && c.byte_len == 0 {
|
||
return refuse(PM, "5g", "rule 5: an empty cluster", i as u64);
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// How a sink obtains held sets (§2.7 step 6): by digest, as a stream it reads from.
|
||
pub trait Resolver {
|
||
/// Begin reading the held set named by `sha256`; false if the sink cannot obtain it.
|
||
fn open(&mut self, sha256: &[u8; 32]) -> bool;
|
||
/// Read the next bytes of the set last opened into `buf`; 0 at its end.
|
||
fn read(&mut self, buf: &mut [u8]) -> usize;
|
||
}
|
||
|
||
/// A resolver that holds no set.
|
||
pub struct NoSets;
|
||
|
||
impl Resolver for NoSets {
|
||
fn open(&mut self, _: &[u8; 32]) -> bool {
|
||
false
|
||
}
|
||
fn read(&mut self, _: &mut [u8]) -> usize {
|
||
0
|
||
}
|
||
}
|
||
|
||
/// A resolver over sets held in memory, found by their SHA-256.
|
||
#[derive(Default)]
|
||
pub struct SetStore {
|
||
sets: Vec<([u8; 32], Vec<u8>)>,
|
||
open: Option<(usize, usize)>,
|
||
}
|
||
|
||
impl SetStore {
|
||
pub fn new() -> SetStore {
|
||
SetStore::default()
|
||
}
|
||
/// Hold `bytes` under their SHA-256.
|
||
pub fn add(&mut self, bytes: Vec<u8>) {
|
||
let h = crate::sha256::sha256(&bytes);
|
||
if !self.sets.iter().any(|(k, _)| *k == h) {
|
||
self.sets.push((h, bytes));
|
||
}
|
||
}
|
||
/// Hold `bytes` under a digest of the caller's choosing (for refusal vectors).
|
||
pub fn add_as(&mut self, sha256: [u8; 32], bytes: Vec<u8>) {
|
||
self.sets.push((sha256, bytes));
|
||
}
|
||
pub fn len(&self) -> usize {
|
||
self.sets.len()
|
||
}
|
||
pub fn is_empty(&self) -> bool {
|
||
self.sets.is_empty()
|
||
}
|
||
}
|
||
|
||
impl Resolver for SetStore {
|
||
fn open(&mut self, sha256: &[u8; 32]) -> bool {
|
||
self.open = self.sets.iter().position(|(k, _)| k == sha256).map(|i| (i, 0));
|
||
self.open.is_some()
|
||
}
|
||
fn read(&mut self, buf: &mut [u8]) -> usize {
|
||
let Some((i, at)) = self.open else { return 0 };
|
||
let src = &self.sets[i].1[at..];
|
||
let n = src.len().min(buf.len());
|
||
buf[..n].copy_from_slice(&src[..n]);
|
||
self.open = Some((i, at + n));
|
||
n
|
||
}
|
||
}
|
||
|
||
/// Read at most `limit` bytes of the set the resolver has open.
|
||
pub fn read_held(r: &mut dyn Resolver, limit: usize) -> Vec<u8> {
|
||
const CHUNK: usize = 1 << 16;
|
||
let mut data: Vec<u8> = Vec::new();
|
||
while data.len() < limit {
|
||
let start = data.len();
|
||
let n = (limit - start).min(CHUNK);
|
||
data.resize(start + n, 0);
|
||
let got = r.read(&mut data[start..start + n]);
|
||
data.truncate(start + got.min(n));
|
||
if got == 0 {
|
||
break;
|
||
}
|
||
}
|
||
data
|
||
}
|
||
|
||
/// A payload that passed every check of §2.7, with its sets and each listed size's
|
||
/// cost.
|
||
#[derive(Clone, Debug)]
|
||
pub struct Loaded {
|
||
pub doc: Doc,
|
||
pub sets: Vec<Wos1>,
|
||
pub costs: Vec<u64>,
|
||
}
|
||
|
||
/// Steps 6 and 7: the sets, then the glyphs.
|
||
pub fn load_sets(d: &Doc, r: &mut dyn Resolver) -> Result<Vec<Wos1>, Refusal> {
|
||
let mut out = Vec::with_capacity(d.sets.len());
|
||
let mut at = 0usize;
|
||
for (i, s) in d.sets.iter().enumerate() {
|
||
let i = i as u64;
|
||
let len = s.length as usize;
|
||
let bytes: Vec<u8> = if s.carriage == 1 {
|
||
let b = d.inline[at..at + len].to_vec();
|
||
at += len;
|
||
b
|
||
} else {
|
||
if !r.open(&s.sha256) {
|
||
return refuse(Code::OutlineNotHeld, "6", "held set not obtained", i);
|
||
}
|
||
read_held(r, len + 1)
|
||
};
|
||
if bytes.len() != len {
|
||
return refuse(Code::OutlineMismatch, "6", "length", i);
|
||
}
|
||
if crate::sha256::sha256(&bytes) != s.sha256 {
|
||
return refuse(Code::OutlineMismatch, "6", "sha256", i);
|
||
}
|
||
match wos1::parse(&bytes) {
|
||
Ok(w) => out.push(w),
|
||
Err(e) => return Err(Refusal { step: "6", index: i, ..e }),
|
||
}
|
||
}
|
||
for (i, it) in d.items.iter().enumerate() {
|
||
if it.kind != KIND_RUN {
|
||
continue;
|
||
}
|
||
let set = &out[it.reference as usize];
|
||
for (k, g) in d.run_glyphs(i).iter().enumerate() {
|
||
if set.glyph(g.gid).is_none() {
|
||
return refuse(Code::GlyphMissing, "7", "gid not in the run's set", (d.ix.item_glyph0[i] as usize + k) as u64);
|
||
}
|
||
}
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
/// Steps 1–9 of §2.7.
|
||
pub fn load(b: &[u8], r: &mut dyn Resolver) -> Result<Loaded, Refusal> {
|
||
let doc = parse(b)?;
|
||
let sets = load_sets(&doc, r)?;
|
||
crate::place::interval_check(&doc, &sets)?;
|
||
let costs = crate::place::check_costs(&doc, &sets)?;
|
||
Ok(Loaded { doc, sets, costs })
|
||
}
|