205 lines
8.5 KiB
Rust
205 lines
8.5 KiB
Rust
//! 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));
|
||
}
|