🐛 Normalize PathData coordinates to safe integer bounds on read

Add normalize-coord helper function that clamps coordinate values to
max-safe-int and min-safe-int bounds when reading segments from PathData
binary buffer. Applies normalization to read-segment, impl-walk,
impl-reduce, and impl-lookup functions to ensure coordinates remain
within safe bounds.

Add corresponding test to verify out-of-bounds coordinates are properly
clamped when reading PathData.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh 2026-04-09 08:58:00 +00:00 committed by Belén Albeza
parent a403175d5c
commit e511576f66
2 changed files with 118 additions and 52 deletions

View File

@ -30,6 +30,18 @@
#?(:clj (set! *warn-on-reflection* true))
(def ^:const SEGMENT-U8-SIZE 28)
(defn- normalize-coord
"Normalize a coordinate value to be within safe integer bounds.
Clamps values greater than max-safe-int to max-safe-int,
and values less than min-safe-int to min-safe-int.
Always returns a double."
[v]
(cond
(> v sm/max-safe-int) (double sm/max-safe-int)
(< v sm/min-safe-int) (double sm/min-safe-int)
:else (double v)))
(def ^:const SEGMENT-U32-SIZE (/ SEGMENT-U8-SIZE 4))
(defprotocol IPathData
@ -121,12 +133,12 @@
(if (< index size)
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)
c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))
c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))
type (case type
1 :move-to
2 :line-to
@ -148,12 +160,12 @@
(if (< index size)
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)
c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))
c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))
type (case type
1 :move-to
2 :line-to
@ -172,12 +184,12 @@
[buffer index f]
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)
c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))
c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))
type (case type
1 :move-to
2 :line-to
@ -252,31 +264,31 @@
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)]
(case (long type)
1 (let [x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))]
1 (let [x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))]
{:command :move-to
:params {:x (double x)
:y (double y)}})
:params {:x x
:y y}})
2 (let [x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))]
2 (let [x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))]
{:command :line-to
:params {:x (double x)
:y (double y)}})
:params {:x x
:y y}})
3 (let [c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))]
3 (let [c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))]
{:command :curve-to
:params {:x (double x)
:y (double y)
:c1x (double c1x)
:c1y (double c1y)
:c2x (double c2x)
:c2y (double c2y)}})
:params {:x x
:y y
:c1x c1x
:c1y c1y
:c2x c2x
:c2y c2y}})
4 {:command :close-path
:params {}}
@ -666,8 +678,6 @@
(defn from-plain
"Create a PathData instance from plain data structures"
[segments]
(assert (check-plain-content segments))
(let [total (count segments)
buffer (buf/allocate (* total SEGMENT-U8-SIZE))]
(loop [index 0]
@ -677,30 +687,28 @@
(case (get segment :command)
:move-to
(let [params (get segment :params)
x (float (get params :x))
y (float (get params :y))]
x (normalize-coord (get params :x))
y (normalize-coord (get params :y))]
(buf/write-short buffer offset 1)
(buf/write-float buffer (+ offset 20) x)
(buf/write-float buffer (+ offset 24) y))
:line-to
(let [params (get segment :params)
x (float (get params :x))
y (float (get params :y))]
x (normalize-coord (get params :x))
y (normalize-coord (get params :y))]
(buf/write-short buffer offset 2)
(buf/write-float buffer (+ offset 20) x)
(buf/write-float buffer (+ offset 24) y))
:curve-to
(let [params (get segment :params)
x (float (get params :x))
y (float (get params :y))
c1x (float (get params :c1x x))
c1y (float (get params :c1y y))
c2x (float (get params :c2x x))
c2y (float (get params :c2y y))]
x (normalize-coord (get params :x))
y (normalize-coord (get params :y))
c1x (normalize-coord (get params :c1x x))
c1y (normalize-coord (get params :c1y y))
c2x (normalize-coord (get params :c2x x))
c2y (normalize-coord (get params :c2y y))]
(buf/write-short buffer offset 3)
(buf/write-float buffer (+ offset 4) c1x)
(buf/write-float buffer (+ offset 8) c1y)

View File

@ -13,6 +13,7 @@
[app.common.geom.rect :as grc]
[app.common.math :as mth]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.transit :as trans]
[app.common.types.path :as path]
[app.common.types.path.bool :as path.bool]
@ -1418,3 +1419,60 @@
;; Verify first and last entries specifically
(t/is (= :move-to (first seq-types)))
(t/is (= :close-path (last seq-types))))))
(t/deftest path-data-read-normalizes-out-of-bounds-coordinates
(let [max-safe (double sm/max-safe-int)
min-safe (double sm/min-safe-int)
;; Create content with values exceeding safe bounds
content-with-out-of-bounds
[{:command :move-to :params {:x (+ max-safe 1000.0) :y (- min-safe 1000.0)}}
{:command :line-to :params {:x (- min-safe 500.0) :y (+ max-safe 500.0)}}
{:command :curve-to :params
{:c1x (+ max-safe 200.0) :c1y (- min-safe 200.0)
:c2x (+ max-safe 300.0) :c2y (- min-safe 300.0)
:x (+ max-safe 400.0) :y (- min-safe 400.0)}}
{:command :close-path :params {}}]
;; Create PathData from the content
pdata (path/content content-with-out-of-bounds)
;; Read it back
result (vec pdata)]
(t/testing "Coordinates exceeding max-safe-int are clamped to max-safe-int"
(let [move-to (first result)
line-to (second result)]
(t/is (= max-safe (:x (:params move-to))) "x in move-to should be clamped to max-safe-int")
(t/is (= min-safe (:y (:params move-to))) "y in move-to should be clamped to min-safe-int")
(t/is (= min-safe (:x (:params line-to))) "x in line-to should be clamped to min-safe-int")
(t/is (= max-safe (:y (:params line-to))) "y in line-to should be clamped to max-safe-int")))
(t/testing "Curve-to coordinates are clamped"
(let [curve-to (nth result 2)]
(t/is (= max-safe (:c1x (:params curve-to))) "c1x should be clamped")
(t/is (= min-safe (:c1y (:params curve-to))) "c1y should be clamped")
(t/is (= max-safe (:c2x (:params curve-to))) "c2x should be clamped")
(t/is (= min-safe (:c2y (:params curve-to))) "c2y should be clamped")
(t/is (= max-safe (:x (:params curve-to))) "x should be clamped")
(t/is (= min-safe (:y (:params curve-to))) "y should be clamped")))
(t/testing "-lookup normalizes coordinates"
(let [move-to (path.impl/-lookup pdata 0 (fn [_ _ _ _ _ x y] {:x x :y y}))]
(t/is (= max-safe (:x move-to)) "lookup x should be clamped")
(t/is (= min-safe (:y move-to)) "lookup y should be clamped")))
(t/testing "-walk normalizes coordinates"
(let [coords (path.impl/-walk pdata
(fn [_ _ _ _ _ x y]
(when (and x y) {:x x :y y}))
[])]
(t/is (= max-safe (:x (first coords))) "walk first x should be clamped")
(t/is (= min-safe (:y (first coords))) "walk first y should be clamped")))
(t/testing "-reduce normalizes coordinates"
(let [[move-res] (path.impl/-reduce pdata
(fn [acc _ _ _ _ _ _ x y]
(if (and x y) (conj acc {:x x :y y}) acc))
[])]
(t/is (= max-safe (:x move-res)) "reduce first x should be clamped")
(t/is (= min-safe (:y move-res)) "reduce first y should be clamped")))))