open-standards/wai/wai-vector/src/wvg1.rs
Transaction Science 2f147d2a30
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
wai-vector: WOS1 and WVG1 with the refusal order of §2.7, the clock, tracks, the interval check and the cost (D7-2)
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]>
2026-10-11 03:59:47 -04:00

1392 lines
46 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `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 })
}