open-standards/wai/wai-vector/src/form.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

486 lines
17 KiB
Rust

//! The JSON authoring form (§2.12, informative): one member per field of `WVG1`,
//! which makes a vector reviewable. The form is never a payload, and a sink never
//! reads it.
//!
//! [`to_json`] writes every field as carried, zero fields included (named
//! `zero@<offset>`), so any payload whose length is exact for its header converts to
//! the form and back to the same bytes. A payload that cannot be split by its header
//! is written as `{"raw": "<hex>"}`. [`to_bytes`] reads either form.
//!
//! Layout: `header` is an object; `sizes`, `rates`, `sets`, `tracks`, `paths`,
//! `texts` and `items` are arrays of objects; `tables` (17 entries), `paints`
//! (`[r, g, b, a]`), `keys` (`[t, value, interp, zero]`), `path_points`
//! (`[x, y, tag]`), `glyphs` (`[gid, zero, x, y]`) and `clusters`
//! (`[glyph_count, zero, byte_first, byte_len]`) are arrays of arrays; `path_ends` is
//! an array of integers; `strings` is a string, or `{"hex": …}` when it is not UTF-8;
//! `inline` is hexadecimal. A `note` member is ignored.
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use crate::json::{self, Value, hex, push_int, push_str, unhex};
use crate::wos1::Point;
use crate::wvg1::*;
fn line(out: &mut String, key: &str, v: &str, last: bool) {
push_str(out, key);
out.push(':');
out.push_str(v);
if !last {
out.push(',');
}
out.push('\n');
}
fn arr_lines(out: &mut String, key: &str, rows: Vec<String>, last: bool) {
push_str(out, key);
out.push_str(":[");
if !rows.is_empty() {
out.push('\n');
}
let n = rows.len();
for (i, r) in rows.into_iter().enumerate() {
out.push_str(&r);
if i + 1 < n {
out.push(',');
}
out.push('\n');
}
out.push(']');
if !last {
out.push(',');
}
out.push('\n');
}
fn ints(v: &[i128]) -> String {
let mut s = String::from("[");
for (i, x) in v.iter().enumerate() {
if i > 0 {
s.push(',');
}
push_int(&mut s, *x);
}
s.push(']');
s
}
fn bytes_arr(b: &[u8]) -> String {
ints(&b.iter().map(|&x| i128::from(x)).collect::<Vec<_>>())
}
fn o(members: &[(&str, String)]) -> String {
let mut s = String::from("{");
for (i, (k, v)) in members.iter().enumerate() {
if i > 0 {
s.push(',');
}
push_str(&mut s, k);
s.push(':');
s.push_str(v);
}
s.push('}');
s
}
fn n(v: impl Into<i128>) -> String {
let mut s = String::new();
push_int(&mut s, v.into());
s
}
/// The form of a payload, with an optional note.
pub fn to_json(bytes: &[u8], note: &str) -> String {
let mut out = String::from("{\n");
if !note.is_empty() {
let mut s = String::new();
push_str(&mut s, note);
line(&mut out, "note", &s, false);
}
let splittable = bytes.len() >= 80 && Header::read(bytes).exact_len() == bytes.len() as u64;
if !splittable {
let mut s = String::new();
push_str(&mut s, &hex(bytes));
line(&mut out, "raw", &s, true);
out.push_str("}\n");
return out;
}
let d = read_tables(bytes, Header::read(bytes));
let h = &d.header;
let magic = if h.magic.iter().all(|c| c.is_ascii_graphic()) {
let mut s = String::new();
push_str(&mut s, core::str::from_utf8(&h.magic).unwrap_or(""));
s
} else {
bytes_arr(&h.magic)
};
let header = o(&[
("magic", magic),
("format", n(h.format)),
("tb", n(h.tb)),
("t0", n(h.t0)),
("dur", n(h.dur)),
("design_w", n(h.design_w)),
("design_h", n(h.design_h)),
("n_sizes", n(h.n_sizes)),
("n_rates", n(h.n_rates)),
("n_sets", n(h.n_sets)),
("n_tables", n(h.n_tables)),
("n_paints", n(h.n_paints)),
("n_texts", n(h.n_texts)),
("n_tracks", n(h.n_tracks)),
("n_paths", n(h.n_paths)),
("n_items", n(h.n_items)),
("n_keys", n(h.n_keys)),
("n_glyphs", n(h.n_glyphs)),
("n_clusters", n(h.n_clusters)),
("path_contours", n(h.path_contours)),
("path_points", n(h.path_points)),
("strings_len", n(h.strings_len)),
("inline_len", n(h.inline_len)),
("held_len", n(h.held_len)),
("zero@72", bytes_arr(&h.zero)),
]);
line(&mut out, "header", &header, false);
arr_lines(&mut out, "sizes", d.sizes.iter().map(|s| o(&[("w", n(s.w)), ("h", n(s.h)), ("cost", n(s.cost))])).collect(), false);
arr_lines(&mut out, "rates", d.rates.iter().map(|r| o(&[("num", n(r.num)), ("den", n(r.den))])).collect(), false);
arr_lines(
&mut out,
"sets",
d.sets
.iter()
.map(|s| {
let mut h = String::new();
push_str(&mut h, &hex(&s.sha256));
o(&[("carriage", n(s.carriage)), ("zero@1", bytes_arr(&s.zero)), ("length", n(s.length)), ("sha256", h)])
})
.collect(),
false,
);
arr_lines(&mut out, "tables", d.tables.iter().map(|t| ints(&t.e.map(i128::from))).collect(), false);
arr_lines(&mut out, "paints", d.paints.iter().map(|p| bytes_arr(&p.rgba)).collect(), false);
arr_lines(
&mut out,
"tracks",
d.tracks.iter().map(|t| o(&[("kind", n(t.kind)), ("zero@1", n(t.zero1)), ("zero@2", n(t.zero2)), ("n_keys", n(t.n_keys))])).collect(),
false,
);
arr_lines(
&mut out,
"keys",
d.keys.iter().map(|k| format!("[{},{},{},{}]", k.t, k.value, k.interp, bytes_arr(&k.zero))).collect(),
false,
);
arr_lines(
&mut out,
"paths",
d.paths
.iter()
.map(|p| {
o(&[
("n_contours", n(p.n_contours)),
("n_points", n(p.n_points)),
("zero@4", n(p.zero)),
("xmin", n(p.bbox[0])),
("ymin", n(p.bbox[1])),
("xmax", n(p.bbox[2])),
("ymax", n(p.bbox[3])),
])
})
.collect(),
false,
);
line(&mut out, "path_ends", &ints(&d.path_ends.iter().map(|&e| i128::from(e)).collect::<Vec<_>>()), false);
arr_lines(&mut out, "path_points", d.path_points.iter().map(|p| format!("[{},{},{}]", p.x, p.y, p.tag)).collect(), false);
arr_lines(
&mut out,
"texts",
d.texts
.iter()
.map(|t| {
o(&[
("purpose", n(t.purpose)),
("dir", n(t.dir)),
("lang_len", n(t.lang_len)),
("mode", n(t.mode)),
("group", n(t.group)),
("ruby", n(t.ruby)),
("ruby_start", n(t.ruby_start)),
("ruby_len", n(t.ruby_len)),
("str_len", n(t.str_len)),
])
})
.collect(),
false,
);
arr_lines(
&mut out,
"items",
d.items
.iter()
.map(|it| {
o(&[
("kind", n(it.kind)),
("rule", n(it.rule)),
("zero@2", n(it.zero2)),
("ref", n(it.reference)),
("paint", n(it.paint)),
("text", n(it.text)),
("zero@10", n(it.zero10)),
("n_glyphs", n(it.n_glyphs)),
("n_clusters", n(it.n_clusters)),
("t_on", n(it.t_on)),
("t_off", n(it.t_off)),
("unit_scale", n(it.unit_scale)),
("m00", n(it.slots[0])),
("m01", n(it.slots[1])),
("m10", n(it.slots[2])),
("m11", n(it.slots[3])),
("tx", n(it.slots[4])),
("ty", n(it.slots[5])),
("op", n(it.slots[6])),
("zero@42", bytes_arr(&it.zero42)),
])
})
.collect(),
false,
);
arr_lines(&mut out, "glyphs", d.glyphs.iter().map(|g| format!("[{},{},{},{}]", g.gid, g.zero, g.x, g.y)).collect(), false);
arr_lines(
&mut out,
"clusters",
d.clusters.iter().map(|c| format!("[{},{},{},{}]", c.glyph_count, c.zero, c.byte_first, c.byte_len)).collect(),
false,
);
let strings = match core::str::from_utf8(&d.strings) {
Ok(s) => {
let mut t = String::new();
push_str(&mut t, s);
t
}
Err(_) => {
let mut t = String::from("{\"hex\":");
push_str(&mut t, &hex(&d.strings));
t.push('}');
t
}
};
line(&mut out, "strings", &strings, false);
let mut inl = String::new();
push_str(&mut inl, &hex(&d.inline));
line(&mut out, "inline", &inl, true);
out.push_str("}\n");
out
}
type R<T> = Result<T, String>;
fn member<'a>(v: &'a Value, k: &str) -> R<&'a Value> {
v.get(k).ok_or_else(|| format!("missing member {k}"))
}
fn int_in(v: &Value, lo: i128, hi: i128, what: &str) -> R<i128> {
match v.as_int() {
Some(x) if (lo..=hi).contains(&x) => Ok(x),
_ => Err(format!("{what}: not an integer in {lo}..={hi}")),
}
}
fn uf(v: &Value, k: &str, bits: u32) -> R<i128> {
int_in(member(v, k)?, 0, (1i128 << bits) - 1, k)
}
fn sf(v: &Value, k: &str, bits: u32) -> R<i128> {
int_in(member(v, k)?, -(1i128 << (bits - 1)), (1i128 << (bits - 1)) - 1, k)
}
fn arr<'a>(v: &'a Value, k: &str) -> R<&'a [Value]> {
member(v, k)?.as_arr().ok_or_else(|| format!("{k}: not an array"))
}
fn byte_list<const N: usize>(v: &Value, what: &str) -> R<[u8; N]> {
let a = v.as_arr().ok_or_else(|| format!("{what}: not an array"))?;
if a.len() != N {
return Err(format!("{what}: {N} bytes"));
}
let mut out = [0u8; N];
for (o, x) in out.iter_mut().zip(a) {
*o = int_in(x, 0, 255, what)? as u8;
}
Ok(out)
}
fn tuple<'a>(v: &'a Value, len: usize, what: &str) -> R<&'a [Value]> {
match v.as_arr() {
Some(a) if a.len() == len => Ok(a),
_ => Err(format!("{what}: an array of {len}")),
}
}
/// The payload bytes of a form.
pub fn to_bytes(text: &str) -> R<Vec<u8>> {
let v = json::parse(text).map_err(|e| format!("not JSON at byte {}", e.at))?;
if let Some(raw) = v.get("raw") {
return raw.as_str().and_then(unhex).ok_or_else(|| String::from("raw: not hexadecimal"));
}
let hv = member(&v, "header")?;
let magic = match member(hv, "magic")? {
Value::Str(s) if s.len() == 4 => {
let b = s.as_bytes();
[b[0], b[1], b[2], b[3]]
}
m => byte_list::<4>(m, "magic")?,
};
let h = Header {
magic,
format: uf(hv, "format", 32)? as u32,
tb: uf(hv, "tb", 32)? as u32,
t0: uf(hv, "t0", 32)? as u32,
dur: uf(hv, "dur", 32)? as u32,
design_w: uf(hv, "design_w", 16)? as u16,
design_h: uf(hv, "design_h", 16)? as u16,
n_sizes: uf(hv, "n_sizes", 8)? as u8,
n_rates: uf(hv, "n_rates", 8)? as u8,
n_sets: uf(hv, "n_sets", 8)? as u8,
n_tables: uf(hv, "n_tables", 8)? as u8,
n_paints: uf(hv, "n_paints", 16)? as u16,
n_texts: uf(hv, "n_texts", 16)? as u16,
n_tracks: uf(hv, "n_tracks", 16)? as u16,
n_paths: uf(hv, "n_paths", 16)? as u16,
n_items: uf(hv, "n_items", 32)? as u32,
n_keys: uf(hv, "n_keys", 32)? as u32,
n_glyphs: uf(hv, "n_glyphs", 32)? as u32,
n_clusters: uf(hv, "n_clusters", 32)? as u32,
path_contours: uf(hv, "path_contours", 32)? as u32,
path_points: uf(hv, "path_points", 32)? as u32,
strings_len: uf(hv, "strings_len", 32)? as u32,
inline_len: uf(hv, "inline_len", 32)? as u32,
held_len: uf(hv, "held_len", 32)? as u32,
zero: byte_list::<8>(member(hv, "zero@72")?, "zero@72")?,
};
let mut d = Doc { header: h, ..Doc::default() };
for s in arr(&v, "sizes")? {
d.sizes.push(Size { w: uf(s, "w", 16)? as u16, h: uf(s, "h", 16)? as u16, cost: uf(s, "cost", 32)? as u32 });
}
for r in arr(&v, "rates")? {
d.rates.push(Rate { num: uf(r, "num", 32)? as u32, den: uf(r, "den", 32)? as u32 });
}
for s in arr(&v, "sets")? {
let sha = member(s, "sha256")?.as_str().and_then(unhex).filter(|b| b.len() == 32).ok_or("sha256: 64 hex digits")?;
let mut sha256 = [0u8; 32];
sha256.copy_from_slice(&sha);
d.sets.push(SetRec {
carriage: uf(s, "carriage", 8)? as u8,
zero: byte_list::<3>(member(s, "zero@1")?, "zero@1")?,
length: uf(s, "length", 32)? as u32,
sha256,
});
}
for t in arr(&v, "tables")? {
let a = tuple(t, 17, "table")?;
let mut e = [0u32; 17];
for (x, y) in e.iter_mut().zip(a) {
*x = int_in(y, 0, u32::MAX.into(), "E")? as u32;
}
d.tables.push(Table { e });
}
for p in arr(&v, "paints")? {
d.paints.push(Paint { rgba: byte_list::<4>(p, "paint")? });
}
for t in arr(&v, "tracks")? {
d.tracks.push(Track {
kind: uf(t, "kind", 8)? as u8,
zero1: uf(t, "zero@1", 8)? as u8,
zero2: uf(t, "zero@2", 16)? as u16,
n_keys: uf(t, "n_keys", 32)? as u32,
});
}
for k in arr(&v, "keys")? {
let a = tuple(k, 4, "key")?;
d.keys.push(Key {
t: int_in(&a[0], 0, u32::MAX.into(), "t")? as u32,
value: int_in(&a[1], i32::MIN.into(), i32::MAX.into(), "value")? as i32,
interp: int_in(&a[2], 0, 255, "interp")? as u8,
zero: byte_list::<3>(&a[3], "key zero")?,
});
}
for p in arr(&v, "paths")? {
d.paths.push(Path {
n_contours: uf(p, "n_contours", 16)? as u16,
n_points: uf(p, "n_points", 16)? as u16,
zero: uf(p, "zero@4", 32)? as u32,
bbox: [sf(p, "xmin", 16)? as i16, sf(p, "ymin", 16)? as i16, sf(p, "xmax", 16)? as i16, sf(p, "ymax", 16)? as i16],
});
}
for e in arr(&v, "path_ends")? {
d.path_ends.push(int_in(e, 0, 65_535, "path end")? as u16);
}
for p in arr(&v, "path_points")? {
let a = tuple(p, 3, "point")?;
d.path_points.push(Point {
x: int_in(&a[0], -32_768, 32_767, "x")? as i16,
y: int_in(&a[1], -32_768, 32_767, "y")? as i16,
tag: int_in(&a[2], 0, 255, "tag")? as u8,
});
}
for t in arr(&v, "texts")? {
d.texts.push(Text {
purpose: uf(t, "purpose", 8)? as u8,
dir: uf(t, "dir", 8)? as u8,
lang_len: uf(t, "lang_len", 8)? as u8,
mode: uf(t, "mode", 8)? as u8,
group: uf(t, "group", 16)? as u16,
ruby: uf(t, "ruby", 16)? as u16,
ruby_start: uf(t, "ruby_start", 32)? as u32,
ruby_len: uf(t, "ruby_len", 32)? as u32,
str_len: uf(t, "str_len", 32)? as u32,
});
}
for it in arr(&v, "items")? {
let mut slots = [0u16; 7];
for (s, k) in slots.iter_mut().zip(["m00", "m01", "m10", "m11", "tx", "ty", "op"]) {
*s = uf(it, k, 16)? as u16;
}
d.items.push(Item {
kind: uf(it, "kind", 8)? as u8,
rule: uf(it, "rule", 8)? as u8,
zero2: uf(it, "zero@2", 16)? as u16,
reference: uf(it, "ref", 16)? as u16,
paint: uf(it, "paint", 16)? as u16,
text: uf(it, "text", 16)? as u16,
zero10: uf(it, "zero@10", 16)? as u16,
n_glyphs: uf(it, "n_glyphs", 16)? as u16,
n_clusters: uf(it, "n_clusters", 16)? as u16,
t_on: uf(it, "t_on", 32)? as u32,
t_off: uf(it, "t_off", 32)? as u32,
unit_scale: uf(it, "unit_scale", 32)? as u32,
slots,
zero42: byte_list::<6>(member(it, "zero@42")?, "zero@42")?,
});
}
for g in arr(&v, "glyphs")? {
let a = tuple(g, 4, "glyph")?;
d.glyphs.push(GlyphRec {
gid: int_in(&a[0], 0, 65_535, "gid")? as u16,
zero: int_in(&a[1], 0, 65_535, "zero")? as u16,
x: int_in(&a[2], i32::MIN.into(), i32::MAX.into(), "x")? as i32,
y: int_in(&a[3], i32::MIN.into(), i32::MAX.into(), "y")? as i32,
});
}
for c in arr(&v, "clusters")? {
let a = tuple(c, 4, "cluster")?;
d.clusters.push(Cluster {
glyph_count: int_in(&a[0], 0, 65_535, "glyph_count")? as u16,
zero: int_in(&a[1], 0, 65_535, "zero")? as u16,
byte_first: int_in(&a[2], 0, u32::MAX.into(), "byte_first")? as u32,
byte_len: int_in(&a[3], 0, u32::MAX.into(), "byte_len")? as u32,
});
}
d.strings = match member(&v, "strings")? {
Value::Str(s) => s.as_bytes().to_vec(),
x => member(x, "hex")?.as_str().and_then(unhex).ok_or("strings: hex")?,
};
d.inline = member(&v, "inline")?.as_str().and_then(unhex).ok_or("inline: hex")?;
Ok(d.write())
}