From e511576f664f50110789df113a26fa1391f57b1e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 9 Apr 2026 08:58:00 +0000 Subject: [PATCH] :bug: 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 --- common/src/app/common/types/path/impl.cljc | 112 ++++++++++-------- .../common_tests/types/path_data_test.cljc | 58 +++++++++ 2 files changed, 118 insertions(+), 52 deletions(-) diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index 2db3fcb2e9..3eafdee042 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -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) diff --git a/common/test/common_tests/types/path_data_test.cljc b/common/test/common_tests/types/path_data_test.cljc index 252334b459..e4d2881b18 100644 --- a/common/test/common_tests/types/path_data_test.cljc +++ b/common/test/common_tests/types/path_data_test.cljc @@ -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")))))