PDFR: share parsing while retaining audited EPS fixes

This commit is contained in:
skylar taylor-barrick committed 2026-10-13 14:32:36 -06:00
1 parent 549e9d8251
commit a954ba4fa4
15 files changed
+773 -182

No files matched your search

+6 -3
View File
@@ -1,4 +1,4 @@
//! Workspace automation: `cargo xtask <layers|assets|wasm|web|ico|ci>`.
//! Workspace automation: `cargo xtask <layers|assets|pdf-deps|wasm|web|ico|ci>`.
//!
//! - `layers`: enforces the dependency layering of `docs/architecture.md` §1 (downward-only edges,
//! listed same-layer edges, no UI/OS crates below L5).
@@ -9,12 +9,13 @@
//! - `assets`: every asset file (image, icon, font, LUT, audio, video…) has a complete
//! `<file>.attribution` sidecar and an entry in `ATTRIBUTION.md` (AGENTS.md §1).
//! - `ico <out.ico> <in.png>…`: pack PNGs into a Windows `.ico` (used by `packaging/icons.sh`).
//! - `ci`: fmt check, clippy -D warnings, tests, layers, assets, wasm.
//! - `ci`: fmt check, clippy -D warnings, tests, PDF dependency policy, layers, assets, wasm.
//! - `bench [--bin CLI] [--quick] [--compare OLD.jsonl] …`: render benchmark of generated scenarios
//! (memory, CPU, wall time) into `target/bench/results.jsonl` (`bench.rs`, docs/testing.md).
mod bench;
mod ico;
mod pdf_deps;
mod version;
use std::process::{Command, ExitCode};
@@ -410,6 +411,7 @@ fn ci() -> Result<(), String> {
run(Command::new(cargo).args(["fmt", "--check"]))?;
run(Command::new(cargo).args(["clippy", "--workspace", "--all-targets", "--release", "--", "-D", "warnings"]))?;
run(Command::new(cargo).args(["test", "--workspace", "--release"]))?;
pdf_deps::run(&root())?;
layers()?;
assets()?;
wasm()
@@ -421,6 +423,7 @@ fn main() -> ExitCode {
"layers" => layers(),
"wasm" => wasm(),
"assets" => assets(),
"pdf-deps" => pdf_deps::run(&root()),
"ci" => ci(),
"web" => web(&std::env::args().skip(2).collect::<Vec<_>>()),
"bench" => bench::run(&root(), &std::env::args().skip(2).collect::<Vec<_>>()),
@@ -432,7 +435,7 @@ fn main() -> ExitCode {
let rest: Vec<String> = std::env::args().skip(2).collect();
version::run(&root(), &rest.iter().map(String::as_str).collect::<Vec<_>>())
}
_ => Err("usage: cargo xtask <layers|assets|wasm|web [--dev] [--serve PORT]|ico OUT.ico IN.png…|version [set X.Y.Z]|ci|bench [--bin CLI] [--quick] [--project F --comp C] [--compare OLD.jsonl] (--help)>".into()),
_ => Err("usage: cargo xtask <layers|assets|pdf-deps|wasm|web [--dev] [--serve PORT]|ico OUT.ico IN.png…|version [set X.Y.Z]|ci|bench [--bin CLI] [--quick] [--project F --comp C] [--compare OLD.jsonl] (--help)>".into()),
};
match r {
Ok(()) => ExitCode::SUCCESS,
+117
View File
@@ -0,0 +1,117 @@
//! Pin the shared parser and reject interpreter features that would embed backend assets.
use std::path::Path;
use std::process::Command;
use serde_json::Value;
const REV: &str = "89dcbb3060377a26c99a6d80caa16084366704c4";
const PACKAGES: &[(&str, &str)] = &[("craft-pdf", "0.1.0"), ("hayro-syntax", "0.7.2")];
pub fn run(root: &Path) -> Result<(), String> {
let output = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
.current_dir(root)
.args(["metadata", "--locked", "--format-version", "1"])
.output()
.map_err(|e| format!("cargo metadata: {e}"))?;
if !output.status.success() {
return Err(format!("cargo metadata: {}", String::from_utf8_lossy(&output.stderr)));
}
let metadata: Value = serde_json::from_slice(&output.stdout).map_err(|e| format!("cargo metadata: {e}"))?;
validate(&metadata)?;
println!("PDF backend: pinned parser only; no Hayro interpreter, rasterizer or bundled CMaps");
Ok(())
}
fn validate(metadata: &Value) -> Result<(), String> {
let packages = metadata.get("packages").and_then(Value::as_array).ok_or("cargo metadata has no packages")?;
let nodes = metadata.pointer("/resolve/nodes").and_then(Value::as_array).ok_or("cargo metadata has no resolved nodes")?;
let source = format!("git+https://github.com/storytold/craft-libs?rev={REV}#{REV}");
for (name, version) in PACKAGES {
let matches: Vec<_> = packages.iter().filter(|p| p["name"] == *name).collect();
if matches.len() != 1 || matches[0]["version"] != *version || matches[0]["source"] != source {
return Err(format!("{name}: expected exactly {version} from {source}"));
}
}
for (name, allowed) in [("craft-pdf", "read"), ("hayro-syntax", "std")] {
let package = packages.iter().find(|p| p["name"] == name).ok_or_else(|| format!("missing {name}"))?;
let node = nodes.iter().find(|n| n["id"] == package["id"]).ok_or_else(|| format!("{name}: missing resolved node"))?;
let features = node["features"].as_array().ok_or_else(|| format!("{name}: missing resolved features"))?;
if features.len() != 1 || features.first().and_then(Value::as_str) != Some(allowed) {
return Err(format!("{name}: expected only the {allowed} feature"));
}
}
for package in packages {
let name = package["name"].as_str().unwrap_or("");
let active = nodes.iter().any(|node| node["id"] == package["id"]);
if active && matches!(name, "hayro" | "hayro-interpret" | "hayro-cmap" | "krilla") {
return Err(format!("{name}: EffectCraft must use craft-pdf with only the read feature"));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn metadata() -> Value {
let packages: Vec<_> = PACKAGES.iter().map(|(name, version)| {
json!({"id": name, "name": name, "version": version, "source": format!("git+https://github.com/storytold/craft-libs?rev={REV}#{REV}")})
}).collect();
json!({"packages": packages, "resolve": {"nodes": [
{"id": "hayro-syntax", "features": ["std"]}, {"id": "craft-pdf", "features": ["read"]}
]}})
}
#[test]
fn accepts_the_pinned_parser_only_graph() {
assert!(validate(&metadata()).is_ok());
}
#[test]
fn rejects_unpinned_duplicate_and_missing_parsers() {
let mut m = metadata();
m["packages"][0]["source"] = json!("registry");
assert!(validate(&m).is_err());
let mut m = metadata();
let duplicate = m["packages"][0].clone();
m["packages"].as_array_mut().unwrap().push(duplicate);
assert!(validate(&m).is_err());
m["packages"].as_array_mut().unwrap().retain(|p| p["name"] != "hayro-syntax");
assert!(validate(&m).is_err());
}
#[test]
fn rejects_shared_reader_source_drift() {
let mut m = metadata();
m["packages"][0]["source"] = Value::Null;
assert!(validate(&m).is_err());
}
#[test]
fn rejects_extra_features_and_missing_active_nodes() {
for (index, feature) in [(0, "images"), (1, "write"), (1, "raster")] {
let mut m = metadata();
m["resolve"]["nodes"][index]["features"].as_array_mut().unwrap().push(json!(feature));
assert!(validate(&m).is_err());
}
let mut m = metadata();
m["resolve"]["nodes"].as_array_mut().unwrap().pop();
assert!(validate(&m).is_err());
}
#[test]
fn rejects_backend_asset_features_and_missing_resolution() {
for name in ["hayro", "hayro-interpret", "hayro-cmap", "krilla"] {
let mut m = metadata();
m["packages"].as_array_mut().unwrap().push(json!({"id": name, "name": name}));
m["resolve"]["nodes"].as_array_mut().unwrap().push(json!({"id": name}));
assert!(validate(&m).is_err());
}
let mut m = metadata();
m["resolve"] = Value::Null;
assert!(validate(&m).is_err());
}
}