Live shapes: a polygon read from a file has 3 to 1000 sides

Every command keeps a live polygon's side count in 3..=1000
(shape.polygon, object.setLiveShape, the Polygon tool), but reading a
document took `sides` as any u32. Selecting such a polygon builds its
Live Corners: LiveShape::polygon_outline makes one point per side and
set_corners resizes the corner radii and kinds to the side count. A
document a few bytes long ("sides": 4000000000) makes the app allocate
tens of gigabytes and abort or stall, and the unsaved work in other
documents is lost with it. Transforming such a polygon with cut
corners regenerates the same outline.

`sides` is now clamped to 3..=1000 when it is deserialized, so a
polygon from any document that is read (opened, placed, packaged or
taken from a library) keeps the limit the commands keep, and the code
that uses the count needs no check of its own.

The test reads polygons with 0, 5, 1000 and 4,000,000,000 sides,
checks they come out with 3, 5, 1000 and 1000, and that the clamped
one's outline has 1000 anchors.

Reported-by: BS.ehlas.cz
This commit is contained in:
Martin Vlach
2026-10-20 05:28:32 +07:00
committed by bflatastic
parent 6bed87b5fb
commit 38e73060a7
+26
View File
@@ -102,6 +102,8 @@ pub enum LiveShape {
},
Polygon {
radius: f64,
/// 3–1000, as [`LiveShape::set_sides`] gives it; a file's count is clamped when read.
#[serde(deserialize_with = "de_sides")]
sides: u32,
xf: Affine,
/// Corner radii in document units, by vertex from the first one clockwise (none: sharp).
@@ -326,6 +328,12 @@ fn all_round(kinds: &[CornerKind]) -> bool {
kinds.iter().all(|k| *k == CornerKind::Round)
}
/// A polygon's side count read from a file, clamped as the commands clamp it: the outline and its
/// corner lists hold one entry per side, so a huge count would allocate gigabytes.
fn de_sides<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
u32::deserialize(d).map(|n| n.clamp(3, 1000))
}
/// Whether `a` only moves (no rotation, scale, shear or reflection).
fn moves_only(a: Affine) -> bool {
let [a0, a1, a2, a3, _, _] = a.as_coeffs();
@@ -2190,6 +2198,24 @@ mod tests {
assert_eq!(path.label(), "Path");
}
/// A polygon read from a file has 3 to 1000 sides, as the commands give it: selecting one
/// with four billion made its outline and corner lists allocate tens of gigabytes.
#[test]
fn a_polygon_from_a_file_has_3_to_1000_sides() {
let read = |sides: u32| {
let json = serde_json::json!({"shape": "polygon", "radius": 10.0, "sides": sides, "xf": [1, 0, 0, 1, 0, 0]});
serde_json::from_value::<LiveShape>(json).unwrap()
};
let sides_of = |l: &LiveShape| match l {
LiveShape::Polygon { sides, .. } => *sides,
_ => panic!("not a polygon: {l:?}"),
};
let huge = read(4_000_000_000);
assert_eq!([0, 5, 1000].map(|n| sides_of(&read(n))), [3, 5, 1000]);
assert_eq!(sides_of(&huge), 1000);
assert_eq!(huge.polygon_outline().anchor_count(), 1000);
}
#[test]
fn a_polygon_reports_and_takes_its_radius_angle_and_equal_sides() {
let close = |a: f64, b: f64| (a - b).abs() < 1e-9;