vectorcraft/crates/affinity/tests/outline_limits.rs

205 lines
8.5 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.

//! Original synthetic graph tests for bounded `.af` outline expansion. These model the reader's
//! accepted schema, without using Affinity software, third-party parser code or external assets.
#![cfg(feature = "synth")]
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use vectorcraft_affinity::{
Document, Limits,
model::{Kind, Path, Point},
synth::{self, F, Method, tag},
};
fn compound(id: u32, children: Vec<F>) -> F {
F::Def(id, vec![tag(b"Comp")], vec![(tag(b"Chld"), F::Shared(children))])
}
fn shape(id: u32, class: &[u8; 4], parameters: Vec<(vectorcraft_affinity::stream::Tag, F)>) -> F {
F::Def(
id,
vec![tag(b"ShpN")],
vec![(tag(b"ShpB"), F::F64s(vec![0.0, 0.0, 10.0, 20.0])), (tag(b"Shpe"), F::Def(id + 10_000, vec![tag(class)], parameters))],
)
}
fn read(definitions: Vec<F>, top: u32) -> Document {
let data = synth::stream(&[
(tag(b"Defs"), F::Shared(definitions)),
(
tag(b"DocR"),
F::Def(
1,
vec![tag(b"DocN")],
vec![(
tag(b"Chld"),
F::Shared(vec![F::Def(
2,
vec![tag(b"Sprd")],
vec![(tag(b"SprB"), F::F64s(vec![0.0, 0.0, 400.0, 300.0])), (tag(b"Chld"), F::Shared(vec![F::Ref(top)]))],
)]),
)],
),
),
]);
let bytes = synth::container(&[("doc.dat", &data, Method::Zlib)], None);
vectorcraft_affinity::read(&bytes, Limits::default()).unwrap()
}
fn path(document: &Document) -> &Path {
match &document.spreads[0].nodes[0].kind {
Kind::Shape { path, .. } => path,
other => panic!("expected shape, got {other:?}"),
}
}
fn rejected(document: &Document, warning: &str) {
assert!(matches!(document.spreads[0].nodes[0].kind, Kind::Unsupported));
assert!(document.warnings.iter().any(|value| value.contains(warning)), "{:?}", document.warnings);
}
#[test]
fn self_cycle_and_mutual_cycle_discard_the_entire_compound() {
// A valid first operand ensures a malformed later operand cannot leave a plausible partial path.
let rectangle = shape(100, b"ShNR", vec![]);
let self_cycle = compound(101, vec![F::Ref(100), F::Ref(101)]);
rejected(&read(vec![rectangle.clone(), self_cycle], 101), "compound outline that contains itself");
let mutual_cycle = compound(101, vec![F::Ref(100), compound(102, vec![F::Ref(101)])]);
rejected(&read(vec![rectangle, mutual_cycle], 101), "compound outline that contains itself");
}
#[test]
fn deep_reference_chain_is_bounded_independently_of_stream_nesting() {
let mut definitions = vec![shape(100, b"ShNR", vec![])];
for id in 101..=250 {
definitions.push(compound(id, vec![F::Ref(id - 1)]));
}
rejected(&read(definitions, 250), "compound outlines nested deeper than 128 levels");
}
#[test]
fn outline_at_the_depth_limit_is_still_imported() {
let mut definitions = vec![shape(100, b"ShNR", vec![])];
for id in 101..=227 {
definitions.push(compound(id, vec![F::Ref(id - 1)]));
}
let document = read(definitions, 227);
assert!(document.warnings.is_empty(), "{:?}", document.warnings);
assert_eq!(path(&document).subpaths.len(), 1);
assert_eq!(path(&document).subpaths[0].segments.len(), 3);
}
#[test]
fn excessive_declared_curve_count_is_rejected_before_expansion() {
let curve = F::Def(
100,
vec![tag(b"PCrv")],
vec![(tag(b"Crvs"), F::Def(101, vec![tag(b"PCvD")], vec![(tag(b"Data"), F::Pos(vec![F::U8(0), F::U32(u32::MAX)]))]))],
);
rejected(&read(vec![curve], 100), "compound outlines exceeding the geometry work limit");
}
#[test]
fn empty_shared_fanout_cannot_bypass_the_operand_visit_limit() {
// Only 21 on-disk objects, but an unbounded traversal would visit over two million operands.
let mut definitions = vec![compound(100, vec![])];
for id in 101..=120 {
definitions.push(compound(id, vec![F::Ref(id - 1), F::Ref(id - 1)]));
}
rejected(&read(definitions, 120), "compound outlines exceeding the operand visit limit");
}
#[test]
fn shared_large_operands_are_charged_for_cumulative_geometry_work() {
// A small reference graph expands a 20,000-segment star. Count work across every reuse and
// transform, rather than permitting each individual operand's allocation limit repeatedly.
let mut definitions = vec![shape(100, b"ShSt", vec![(tag(b"Pnts"), F::I32(10_000))])];
for id in 101..=112 {
definitions.push(compound(id, vec![F::Ref(id - 1), F::Ref(id - 1)]));
}
rejected(&read(definitions, 112), "compound outlines exceeding the geometry work limit");
}
#[test]
fn ordinary_nested_compound_preserves_repeated_operands_and_transforms() {
let mut rectangle = shape(100, b"ShNR", vec![]);
if let F::Def(_, _, fields) = &mut rectangle {
fields.push((tag(b"Xfrm"), F::F64s(vec![1.0, 0.0, 3.0, 0.0, 1.0, 7.0])));
}
let inner = compound(101, vec![F::Ref(100), F::Ref(100)]);
let mut outer = compound(102, vec![F::Ref(101)]);
if let F::Def(_, _, fields) = &mut outer {
fields.push((tag(b"Xfrm"), F::F64s(vec![1.0, 0.0, 100.0, 0.0, 1.0, 200.0])));
}
let document = read(vec![rectangle, inner, outer], 102);
assert!(document.warnings.is_empty(), "{:?}", document.warnings);
let path = path(&document);
assert_eq!(path.subpaths.len(), 2);
assert_eq!(path.subpaths[0], path.subpaths[1]);
assert_eq!(path.subpaths[0].start, Point { x: 103.0, y: 207.0 });
assert_eq!(path.subpaths[0].segments.last().unwrap()[2], Point { x: 103.0, y: 227.0 });
}
#[test]
fn overflowing_compound_transform_is_reported_before_processing_geometry() {
let mut child = shape(100, b"ShNR", vec![]);
if let F::Def(_, _, fields) = &mut child {
fields.push((tag(b"Xfrm"), F::F64s(vec![1.0e308, 0.0, 0.0, 0.0, 1.0, 0.0])));
}
let mut outer = compound(101, vec![F::Ref(100)]);
if let F::Def(_, _, fields) = &mut outer {
fields.push((tag(b"Xfrm"), F::F64s(vec![1.0e308, 0.0, 0.0, 0.0, 1.0, 0.0])));
}
rejected(&read(vec![child, outer], 101), "compound operand with an invalid transform");
}
#[test]
fn pie_angles_with_huge_finite_values_do_not_loop_or_emit_nonfinite_points() {
let document = read(vec![shape(100, b"ShPi", vec![(tag(b"AngS"), F::F64(1.0e308)), (tag(b"AngE"), F::F64(-1.0e308))])], 100);
for subpath in &path(&document).subpaths {
assert!(subpath.start.x.is_finite() && subpath.start.y.is_finite());
assert!(subpath.segments.len() <= 20);
assert!(subpath.segments.iter().flatten().all(|point| point.x.is_finite() && point.y.is_finite()));
}
}
#[test]
fn wrapped_pie_angles_keep_the_same_geometry_as_the_positive_span() {
let make = |end| read(vec![shape(100, b"ShPi", vec![(tag(b"AngS"), F::F64(0.0)), (tag(b"AngE"), F::F64(end))])], 100);
assert_eq!(path(&make(-std::f64::consts::FRAC_PI_2)), path(&make(3.0 * std::f64::consts::FRAC_PI_2)));
}
/// `CTyp`: an array of corner-type enums (top left, top right, bottom right, bottom left).
fn corner_types(ids: [u16; 4]) -> F {
let mut raw = vec![0xaa];
raw.extend(tag(b"CTyp").0.to_le_bytes());
raw.extend(4u32.to_le_bytes());
raw.extend(0u16.to_le_bytes());
raw.extend(ids.iter().flat_map(|id| id.to_le_bytes()));
F::Raw(raw)
}
#[test]
fn unlocked_rectangle_corners_keep_their_own_radius_and_type() {
let p = |x: f64, y: f64| Point { x, y };
// 10×20 box: relative radii scale by 10. Round 5, round 1, straight 3, none.
let corners = |lock: Option<bool>| {
let mut fields = vec![(tag(b"ShCR"), F::F64s(vec![0.5, 0.1, 0.3, 0.0])), (tag(b"CTyp"), corner_types([0, 0, 1, 4]))];
fields.extend(lock.map(|l| (tag(b"Lock"), F::Bool(l))));
read(vec![shape(100, b"ShNR", fields)], 100)
};
let unlocked = corners(Some(false));
assert!(unlocked.warnings.is_empty(), "{:?}", unlocked.warnings);
let sub = &path(&unlocked).subpaths[0];
assert_eq!(sub.start, p(5.0, 0.0));
let ends: Vec<Point> = sub.segments.iter().map(|s| s[2]).collect();
assert_eq!(ends, [p(9.0, 0.0), p(10.0, 1.0), p(10.0, 17.0), p(7.0, 20.0), p(0.0, 20.0), p(0.0, 5.0), p(5.0, 0.0)]);
assert_eq!(sub.segments[3], [p(7.0, 20.0); 3], "the straight corner is a line");
assert_ne!(sub.segments[1][0], p(9.0, 0.0), "the round corner is a curve");
// Locked (the default): every corner follows the top left one.
let locked = corners(None);
let sub = &path(&locked).subpaths[0];
assert_eq!(sub.segments.len(), 8);
assert_eq!(sub.segments[3][2], p(5.0, 20.0));
}