From git at osgeo.org Fri May 1 05:46:24 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 1 May 2026 05:46:24 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 5ae8f7c6499b41e650e389a1c9eb2a81025b252c Message-ID: <20260501124624.C8D0E16F952@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 5ae8f7c6499b41e650e389a1c9eb2a81025b252c (commit) from 789196b9e19eed3299f47ea5437ac089ac841e7d (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 5ae8f7c6499b41e650e389a1c9eb2a81025b252c Author: Daniel Baston Date: Fri May 1 08:45:59 2026 -0400 CircularArc: Handle self-assignment (#1431) Resolves https://github.com/libgeos/geos/issues/1430 diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp index 1bca8155a..7dfcbbf34 100644 --- a/src/geom/CircularArc.cpp +++ b/src/geom/CircularArc.cpp @@ -142,6 +142,10 @@ CircularArc::CircularArc(CircularArc&& other) noexcept { CircularArc& CircularArc::operator=(const CircularArc& other) { + if (this == &other) { + return *this; + } + if (m_own_coordinates) { delete m_seq; } diff --git a/tests/unit/geom/CircularArcTest.cpp b/tests/unit/geom/CircularArcTest.cpp index 67df5734a..47078db1f 100644 --- a/tests/unit/geom/CircularArcTest.cpp +++ b/tests/unit/geom/CircularArcTest.cpp @@ -148,4 +148,32 @@ void object::test<6>() { ensure_equals(c1, c2); } +template<> +template<> +void object::test<7>() { + set_test_name("self-assignment"); + + CoordinateXY p0(0.0, 0.0); + CoordinateXY p1(1.0, 1.0); + CoordinateXY p2(2.0, 0.0); + + CircularArc arc = CircularArc::create(p0, p1, p2); + + const auto radius = arc.getRadius(); + const auto center = arc.getCenter(); + const auto orientation = arc.getOrientation(); + + CircularArc* arcPtr = &arc; + *arcPtr = arc; + + ensure_equals(arc.p0(), p0); + ensure_equals(arc.p1(), p1); + ensure_equals(arc.p2(), p2); + + ensure_equals(arc.getRadius(), radius); + ensure_equals(arc.getCenter(), center); + ensure_equals(arc.getOrientation(), orientation); +} + + } ----------------------------------------------------------------------- Summary of changes: src/geom/CircularArc.cpp | 4 ++++ tests/unit/geom/CircularArcTest.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Fri May 1 11:59:31 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 1 May 2026 11:59:31 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/all-actions-6c4df93294 created. 446752af7b7a34f4c484b7bf7f991ff8913da14e Message-ID: <20260501185932.D8405182703@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, dependabot/github_actions/all-actions-6c4df93294 has been created at 446752af7b7a34f4c484b7bf7f991ff8913da14e (commit) - Log ----------------------------------------------------------------- commit 446752af7b7a34f4c484b7bf7f991ff8913da14e Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri May 1 18:59:06 2026 +0000 Bump the all-actions group with 2 updates Bumps the all-actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `github/codeql-action` from 4 to 4.35.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4...v4.35.2) Updates `softprops/action-gh-release` from 2 to 3 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-actions ... Signed-off-by: dependabot[bot] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 46eb081ac..ae1720446 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init at v4 + uses: github/codeql-action/init at v4.35.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,6 +63,6 @@ jobs: make -j$(nproc) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze at v4 + uses: github/codeql-action/analyze at v4.35.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2edbafad7..db43f25c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: - name: Create Release if: ${{ steps.tag.outputs.tag == steps.version.outputs.geosversion }} id: create_release - uses: softprops/action-gh-release at v2 + uses: softprops/action-gh-release at v3 with: body_path: ${{ steps.notes.outputs.geosnotes }} name: Release ${{ steps.version.outputs.geosversion }} ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Mon May 4 05:43:29 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 4 May 2026 05:43:29 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 7ce71942d882ca8ed301181d03d4b31177c340d0 Message-ID: <20260504124332.E72E818ADDD@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 7ce71942d882ca8ed301181d03d4b31177c340d0 (commit) via b06377c2680f9ab4cef6f9d4bcaf0301e9e1bf59 (commit) from 5ae8f7c6499b41e650e389a1c9eb2a81025b252c (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 7ce71942d882ca8ed301181d03d4b31177c340d0 Author: Daniel Baston Date: Fri May 1 16:41:22 2026 -0400 DistanceToPoint: Set dllexport diff --git a/include/geos/algorithm/distance/DistanceToPoint.h b/include/geos/algorithm/distance/DistanceToPoint.h index 108b85506..10f280ade 100644 --- a/include/geos/algorithm/distance/DistanceToPoint.h +++ b/include/geos/algorithm/distance/DistanceToPoint.h @@ -18,6 +18,7 @@ #pragma once +#include #include // for composition namespace geos { @@ -43,7 +44,7 @@ namespace distance { // geos::algorithm::distance * * Also computes two points which are separated by the distance. */ -class DistanceToPoint { +class GEOS_DLL DistanceToPoint { public: DistanceToPoint() {} commit b06377c2680f9ab4cef6f9d4bcaf0301e9e1bf59 Author: Daniel Baston Date: Fri May 1 16:32:44 2026 -0400 Avoid -Warray-bounds warning on gcc 16 diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index 5568b7731..a1be8e65b 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1885,6 +1885,10 @@ extern "C" { GEOSGridIntersectionFractions_r(GEOSContextHandle_t extHandle, const Geometry* g, double xmin, double ymin, double xmax, double ymax, unsigned nx, unsigned ny, float* buf) { +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#endif return execute(extHandle, 0, [&]() { Envelope env(xmin, xmax, ymin, ymax); double dx = env.getWidth() / static_cast(nx); @@ -1900,6 +1904,9 @@ extern "C" { return 1; }); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif } Geometry* ----------------------------------------------------------------------- Summary of changes: capi/geos_ts_c.cpp | 7 +++++++ include/geos/algorithm/distance/DistanceToPoint.h | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) hooks/post-receive -- GEOS From git at osgeo.org Mon May 4 12:11:41 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 4 May 2026 12:11:41 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 0028dd486e18abb779d3c1d1dfeb1a3f34e4af5c Message-ID: <20260504191146.B314418C7C3@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 0028dd486e18abb779d3c1d1dfeb1a3f34e4af5c (commit) from 7ce71942d882ca8ed301181d03d4b31177c340d0 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 0028dd486e18abb779d3c1d1dfeb1a3f34e4af5c Author: Daniel Baston Date: Mon May 4 15:11:19 2026 -0400 GeometrySplitter: Support splitting CompoundCurve and CircularString at point (#1433) diff --git a/include/geos/algorithm/CircularArcs.h b/include/geos/algorithm/CircularArcs.h index d8793d561..a32a197cb 100644 --- a/include/geos/algorithm/CircularArcs.h +++ b/include/geos/algorithm/CircularArcs.h @@ -16,11 +16,17 @@ #include #include -#include #include #include +namespace geos { +namespace geom { +class CoordinateSequence; +class Envelope; +} +} + namespace geos { namespace algorithm { @@ -46,6 +52,19 @@ public: /// Return the point defined by a circle center, radius, and angle static geom::CoordinateXY createPoint(const geom::CoordinateXY& center, double radius, double theta); + /** Interpolate Z/M values for a point added to an arc. + * + * @param seq CoordinateSequence containing the arc points + * @param i0 Index of the first point in the arc + * @param center The center point of the circle defining the arc + * @param isCCW Whether the arc is counterclockwise + * @param pt The point to interpolate Z/M values for + * @param z The interpolated Z value + * @param m The interpolated M value + */ + static void interpolateZM(const geom::CoordinateSequence &seq, size_t i0, const geom::CoordinateXY ¢er, bool isCCW, geom::CoordinateXY &pt, double + &z, double &m); + /** Determines whether and where a circle intersects a line segment. * * @param center The center point of the circle diff --git a/include/geos/geom/SimpleCurve.h b/include/geos/geom/SimpleCurve.h index da24b0125..777ff27a7 100644 --- a/include/geos/geom/SimpleCurve.h +++ b/include/geos/geom/SimpleCurve.h @@ -101,6 +101,8 @@ public: bool isEmpty() const override; + std::unique_ptr clone() const; + std::unique_ptr reverse() const; protected: diff --git a/include/geos/operation/split/GeometrySplitter.h b/include/geos/operation/split/GeometrySplitter.h index 7511940a8..08ee641c9 100644 --- a/include/geos/operation/split/GeometrySplitter.h +++ b/include/geos/operation/split/GeometrySplitter.h @@ -18,8 +18,8 @@ #include namespace geos::geom { +class Curve; class Geometry; -class LineString; class Point; } @@ -51,12 +51,14 @@ private: splitPolygonalWithEdge(const geom::Geometry& geom, const geom::Geometry& edge); static std::unique_ptr - splitLineWithPoint(const geom::LineString& g, const geom::Point& point); + splitCurveWithPoint(const geom::Curve& g, const geom::Point& point); static std::unique_ptr splitAtPoints(const geom::Geometry& geom, const geom::Geometry& splitPoints); class SplitWithPointTransformer; + + static constexpr double POINT_TO_LINE_TOLERANCE = 1e-10; }; } diff --git a/include/geos/operation/split/SplitGeometryAtVertex.h b/include/geos/operation/split/SplitGeometryAtVertex.h deleted file mode 100644 index 37c331cb9..000000000 --- a/include/geos/operation/split/SplitGeometryAtVertex.h +++ /dev/null @@ -1,48 +0,0 @@ -/********************************************************************** -* - * GEOS - Geometry Engine Open Source - * http://geos.osgeo.org - * - * Copyright (C) 2026 ISciences, LLC - * - * This is free software; you can redistribute and/or modify it under - * the terms of the GNU Lesser General Public Licence as published - * by the Free Software Foundation. - * See the COPYING file for more information. - * - **********************************************************************/ - -#pragma once - -#include -#include -#include - -// Forward declarations -namespace geos::geom { -class CircularString; -class CoordinateXY; -class LineString; -class SimpleCurve; -} - -namespace geos::operation::split { - -class GEOS_DLL SplitGeometryAtVertex { - -public: - static std::pair, std::unique_ptr> - splitSimpleCurveAtVertex(const geom::SimpleCurve& sc, std::size_t i); - - static std::pair, std::unique_ptr> - splitLineStringAtVertex(const geom::LineString& ls, std::size_t i); - - static std::pair, std::unique_ptr> - splitCircularStringAtVertex(const geom::CircularString& cs, std::size_t i); - - static std::pair, std::unique_ptr> - splitLineStringAtPoint(const geom::LineString& ls, std::size_t i, const geom::CoordinateXY& pt); - -}; - -} diff --git a/include/geos/operation/split/SplitLinealAtPoint.h b/include/geos/operation/split/SplitLinealAtPoint.h new file mode 100644 index 000000000..a5e1be445 --- /dev/null +++ b/include/geos/operation/split/SplitLinealAtPoint.h @@ -0,0 +1,90 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +// Forward declarations +namespace geos::geom { +class CircularString; +class CompoundCurve; +class CoordinateXY; +class Curve; +class LineString; +class SimpleCurve; +} + +namespace geos::operation::split { + +class GEOS_DLL SplitLinealAtPoint { + +public: + /** Split a curve into two sections at a specified vertex. + * + * If the vertex is the first or last vertex of the geometry, one of the returned + * geometries will be empty. + * + * If the input geometry is a CircularString, the vertex must be an endpoint of an arc. + * + * @param sc the curve to split + * @param i the index of the vertex to split at. + * @return a pair of split geometries. + */ + static std::pair, std::unique_ptr> + splitSimpleCurveAtVertex(const geom::SimpleCurve& sc, std::size_t i); + + /** Split a SimpleCurve into two sections at an arbitrary point. + * + * @param sc the curve to split + * @param i the index of the vertex of the section (segment or arc) that contains pt + * @param pt the splitting point + * @return a pair of split geometries. + */ + static std::pair, std::unique_ptr> + splitSimpleCurveAtPoint(const geom::SimpleCurve &sc, std::size_t i, const geom::CoordinateXY &pt); + + /** Split a CompoundCurve into two sections at an arbitrary point. + * + * @param sc the curve to split + * @param i the component that contains the splitting vertex + * @param j the index of the vertex of the section (segment or arc) that contains pt + * @param pt the splitting point + * @return a pair of split geometries. + */ + static std::pair, std::unique_ptr> + splitCompoundCurveAtPoint(const geom::CompoundCurve &sc, std::size_t i, std::size_t j, const geom::CoordinateXY &pt); + + /** Split a CircularString into two sections at an arbitrary point. */ + static std::pair, std::unique_ptr> + splitCircularStringAtPoint(const geom::CircularString &ls, std::size_t i, const geom::CoordinateXY &pt); + + /** Split CircularString into two sections at vertex i. + * The vertex must be an endpoint of an arc. */ + static std::pair, std::unique_ptr> + splitCircularStringAtVertex(const geom::CircularString &cs, std::size_t i); + + /** Split a LineString into two sections at an arbitrary point. */ + static std::pair, std::unique_ptr> + splitLineStringAtPoint(const geom::LineString &ls, std::size_t i, const geom::CoordinateXY &pt); + + /** Split LineString into two sections at vertex i */ + static std::pair, std::unique_ptr> + splitLineStringAtVertex(const geom::LineString &ls, std::size_t i); + +}; + +} diff --git a/include/geos/util/Assert.h b/include/geos/util/Assert.h index 275de021e..06acf1bb5 100644 --- a/include/geos/util/Assert.h +++ b/include/geos/util/Assert.h @@ -39,6 +39,13 @@ public: isTrue(assertion, std::string()); } + template + static void + isNotNull(const T& ptr, const std::string& message) + { + isTrue(ptr != nullptr, message); + } + static void equals(const geom::CoordinateXY& expectedValue, const geom::CoordinateXY& actualValue, diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index 31a6eea7c..b5e6adcc3 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -25,109 +25,6 @@ using geos::geom::CircularArc; namespace geos::algorithm { -static double -interpolateValue(double a1, double a2, double frac) -{ - frac = std::clamp(frac, 0.0, 1.0); - if (std::isnan(a1)) { - return a2; - } - if (std::isnan(a2)) { - return a1; - } - return a1 + frac * (a2 - a1); -} - -static void -interpolateZM(const CircularArc& arc, const CoordinateXY& pt, double& z, double& m) -{ - using geom::Ordinate; - - const CoordinateSequence& seq = *arc.getCoordinateSequence(); - std::size_t i0 = arc.getCoordinatePosition(); - - // Read Z, M from control point - double z1, m1; - seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) { - z1 = arcPt.template get(); - m1 = arcPt.template get(); - }); - // Test point = control point? - // Take Z, M from the control point - if (arc.p1().equals2D(pt)) { - z = z1; - m = m1; - return; - } - - // Read Z, M from start point - double z0, m0; - seq.applyAt(i0, [&z0, &m0](const auto& arcPt) { - z0 = arcPt.template get(); - m0 = arcPt.template get(); - }); - // Test point = start point? - // Take Z, M from the start point - if (arc.p0().equals2D(pt)) { - z = z0; - m = m0; - return; - } - - // Read Z, M from end point - double z2, m2; - seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) { - z2 = arcPt.template get(); - m2 = arcPt.template get(); - }); - // Test point = end point? - // Take Z, M from the end point - if (arc.p2().equals2D(pt)) { - z = z2; - m = m2; - return; - } - - double theta0 = arc.theta0(); - const double theta1 = arc.theta1(); - double theta2 = arc.theta2(); - const double theta = CircularArcs::getAngle(pt, arc.getCenter()); - - if (!arc.isCCW()) { - std::swap(theta0, theta2); - std::swap(z0, z2); - std::swap(m0, m2); - } - - if (std::isnan(z1)) { - // Interpolate between p0 / p2 - const double frac = Angle::fractionCCW(theta, theta0, theta2); - z = interpolateValue(z0, z2, frac); - } else if (Angle::isWithinCCW(theta, theta0, theta1)) { - // Interpolate between p0 / p1 - const double frac = Angle::fractionCCW(theta, theta0, theta1); - z = interpolateValue(z0, z1, frac); - } else { - // Interpolate between p1 / p2 - const double frac = Angle::fractionCCW(theta, theta1, theta2); - z = interpolateValue(z1, z2, frac); - } - - if (std::isnan(m1)) { - // Interpolate between p0 / p2 - const double frac = Angle::fractionCCW(theta, theta0, theta2); - m = interpolateValue(m0, m2, frac); - } else if (Angle::isWithinCCW(theta, theta0, theta1)) { - // Interpolate between p0 / p1 - const double frac = Angle::fractionCCW(theta, theta0, theta1); - m = interpolateValue(m0, m1, frac); - } else { - // Interpolate between p1 / p2 - const double frac = Angle::fractionCCW(theta, theta1, theta2); - m = interpolateValue(m1, m2, frac); - } - -} // Interpolate the Z/M values of a point lying on the provided line segment static void @@ -155,8 +52,8 @@ interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::Coordinate double z0, m0; double z1, m1; - interpolateZM(arc0, pt, z0, m0); - interpolateZM(arc1, pt, z1, m1); + CircularArcs::interpolateZM(*arc0.getCoordinateSequence(), arc0.getCoordinatePosition(), arc0.getCenter(), arc0.isCCW(), pt, z0, m0); + CircularArcs::interpolateZM(*arc1.getCoordinateSequence(), arc1.getCoordinatePosition(), arc1.getCenter(), arc1.isCCW(), pt, z1, m1); if (std::isnan(pt.z)) { pt.z = Interpolate::getOrAverage(z0, z1); @@ -179,7 +76,7 @@ interpolateZM(const CircularArc& arc0, double z0, m0; double z1, m1; - interpolateZM(arc0, pt, z0, m0); + CircularArcs::interpolateZM(*arc0.getCoordinateSequence(), arc0.getCoordinatePosition(), arc0.getCenter(), arc0.isCCW(), pt, z0, m0); interpolateSegmentZM(seq, ind0, ind1, pt, z1, m1); if (std::isnan(pt.z)) { @@ -191,7 +88,6 @@ interpolateZM(const CircularArc& arc0, } - bool CircularArcIntersector::hasIntersection(const geom::CoordinateXY &p) const { switch (nPt) { diff --git a/src/algorithm/CircularArcs.cpp b/src/algorithm/CircularArcs.cpp index ee28c96c4..eeb779e2b 100644 --- a/src/algorithm/CircularArcs.cpp +++ b/src/algorithm/CircularArcs.cpp @@ -20,6 +20,7 @@ #include #include +using geos::geom::CoordinateSequence; using geos::geom::CoordinateXY; using geos::geom::Envelope; using geos::geom::LineSegment; @@ -75,6 +76,112 @@ CircularArcs::createPoint(const CoordinateXY& center, double radius, double thet return { center.x + radius* std::cos(theta), center.y + radius* std::sin(theta) }; } +static double +interpolateValue(double a1, double a2, double frac) +{ + frac = std::clamp(frac, 0.0, 1.0); + if (std::isnan(a1)) { + return a2; + } + if (std::isnan(a2)) { + return a1; + } + return a1 + frac * (a2 - a1); +} + +void +CircularArcs::interpolateZM(const CoordinateSequence& seq, std::size_t i0, + const CoordinateXY ¢er, bool isCCW, + CoordinateXY &pt, double &z, double &m) +{ + using geom::Ordinate; + + const CoordinateXY& p0 = seq.getAt(i0); + const CoordinateXY& p1 = seq.getAt(i0 + 1); + const CoordinateXY& p2 = seq.getAt(i0 + 2); + + // Read Z, M from control point + double z1, m1; + seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) { + z1 = arcPt.template get(); + m1 = arcPt.template get(); + }); + // Test point = control point? + // Take Z, M from the control point + if (p1.equals2D(pt)) { + z = z1; + m = m1; + return; + } + + // Read Z, M from start point + double z0, m0; + seq.applyAt(i0, [&z0, &m0](const auto& arcPt) { + z0 = arcPt.template get(); + m0 = arcPt.template get(); + }); + // Test point = start point? + // Take Z, M from the start point + if (p0.equals2D(pt)) { + z = z0; + m = m0; + return; + } + + // Read Z, M from end point + double z2, m2; + seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) { + z2 = arcPt.template get(); + m2 = arcPt.template get(); + }); + // Test point = end point? + // Take Z, M from the end point + if (p2.equals2D(pt)) { + z = z2; + m = m2; + return; + } + + double theta0 = getAngle(p0, center); + const double theta1 = getAngle(p1, center); + double theta2 = getAngle(p2, center); + const double theta = getAngle(pt, center); + + if (!isCCW) { + std::swap(theta0, theta2); + std::swap(z0, z2); + std::swap(m0, m2); + } + + if (std::isnan(z1)) { + // Interpolate between p0 / p2 + const double frac = Angle::fractionCCW(theta, theta0, theta2); + z = interpolateValue(z0, z2, frac); + } else if (Angle::isWithinCCW(theta, theta0, theta1)) { + // Interpolate between p0 / p1 + const double frac = Angle::fractionCCW(theta, theta0, theta1); + z = interpolateValue(z0, z1, frac); + } else { + // Interpolate between p1 / p2 + const double frac = Angle::fractionCCW(theta, theta1, theta2); + z = interpolateValue(z1, z2, frac); + } + + if (std::isnan(m1)) { + // Interpolate between p0 / p2 + const double frac = Angle::fractionCCW(theta, theta0, theta2); + m = interpolateValue(m0, m2, frac); + } else if (Angle::isWithinCCW(theta, theta0, theta1)) { + // Interpolate between p0 / p1 + const double frac = Angle::fractionCCW(theta, theta0, theta1); + m = interpolateValue(m0, m1, frac); + } else { + // Interpolate between p1 / p2 + const double frac = Angle::fractionCCW(theta, theta1, theta2); + m = interpolateValue(m1, m2, frac); + } +} + template CoordinateXY getCenterImpl(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2) { diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp index 7dfcbbf34..dd5a56d85 100644 --- a/src/geom/CircularArc.cpp +++ b/src/geom/CircularArc.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp index 70916aa9c..26f8d450f 100644 --- a/src/geom/CompoundCurve.cpp +++ b/src/geom/CompoundCurve.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include namespace geos { @@ -379,7 +379,7 @@ CompoundCurve::normalizeClosed() } else if (minInd == curves[minCurve]->getNumPoints() - 1) { finalCurve = std::move(curves[minCurve]); } else { - auto split = operation::split::SplitGeometryAtVertex::splitSimpleCurveAtVertex(*curves[minCurve], minInd); + auto split = operation::split::SplitLinealAtPoint::splitSimpleCurveAtVertex(*curves[minCurve], minInd); newCurves.push_back(std::move(split.second)); finalCurve = std::move(split.first); } diff --git a/src/geom/SimpleCurve.cpp b/src/geom/SimpleCurve.cpp index 40fbf460b..14ff8e39a 100644 --- a/src/geom/SimpleCurve.cpp +++ b/src/geom/SimpleCurve.cpp @@ -330,6 +330,12 @@ SimpleCurve::isEmpty() const return points->isEmpty(); } +std::unique_ptr +SimpleCurve::clone() const +{ + return std::unique_ptr(static_cast(cloneImpl())); +} + std::unique_ptr SimpleCurve::reverse() const { diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index 488293ac3..d16acafd5 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -15,6 +15,8 @@ #include #include +#include + using geos::geom::CoordinateXYZM; using geos::geom::CircularArc; diff --git a/src/operation/split/GeometrySplitter.cpp b/src/operation/split/GeometrySplitter.cpp index 41cac82ce..a795d9f4f 100644 --- a/src/operation/split/GeometrySplitter.cpp +++ b/src/operation/split/GeometrySplitter.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -31,11 +32,13 @@ #include #include #include -#include +#include #include +#include using geos::geom::CircularString; using geos::geom::CoordinateXY; +using geos::geom::CompoundCurve; using geos::geom::Curve; using geos::geom::CurvePolygon; using geos::geom::Geometry; @@ -45,6 +48,7 @@ using geos::geom::LineString; using geos::geom::MultiLineString; using geos::geom::MultiPoint; using geos::geom::Polygon; +using geos::geom::SimpleCurve; using geos::geom::prep::PreparedGeometryFactory; using geos::geom::util::GeometryCombiner; using geos::geom::util::GeometryTransformer; @@ -59,20 +63,27 @@ using geos::shape::random::RandomPointsBuilder; namespace geos::operation::split { -class GeometrySplitter::SplitWithPointTransformer : public geom::util::GeometryTransformer { +class GeometrySplitter::SplitWithPointTransformer : public GeometryTransformer { public: - SplitWithPointTransformer(const Point& pt) : m_pt(pt) {} + explicit SplitWithPointTransformer(const Point& pt) : m_pt(pt) {} + +protected: + std::unique_ptr + transformCircularString(const CircularString* geom, const Geometry* /*parent*/) override + { + return splitCurveWithPoint(*geom, m_pt); + } std::unique_ptr - transformCircularString(const CircularString*, const Geometry* /*parent*/) override + transformCompoundCurve(const CompoundCurve* geom, const Geometry* /*parent*/) override { - throw geos::util::UnsupportedOperationException("Splitting a CircularString with a point is not supported."); + return splitCurveWithPoint(*geom, m_pt); } std::unique_ptr transformLineString(const LineString* geom, const Geometry* /*parent*/) override { - return GeometrySplitter::splitLineWithPoint(*geom, m_pt); + return splitCurveWithPoint(*geom, m_pt); } std::unique_ptr @@ -210,11 +221,10 @@ GeometrySplitter::split(const Geometry &geom, const Geometry &splitGeom) return splitLinealWithEdge(*toSplit, splitGeom); } -std::unique_ptr -GeometrySplitter::splitLineWithPoint(const geom::LineString& g, const Point& point) -{ - constexpr double tolerance = 1e-10; +std::unique_ptr +GeometrySplitter::splitCurveWithPoint(const Curve& g, const Point& point) +{ if (g.isEmpty()) { std::vector> geoms; geoms.push_back(g.clone()); @@ -223,7 +233,7 @@ GeometrySplitter::splitLineWithPoint(const geom::LineString& g, const Point& poi DistanceOp distance(g, point); - if (distance.distance() > tolerance) { + if (distance.distance() > POINT_TO_LINE_TOLERANCE) { std::vector> geoms; geoms.push_back(g.clone()); return g.getFactory()->createGeometryCollection(std::move(geoms)); @@ -231,22 +241,26 @@ GeometrySplitter::splitLineWithPoint(const geom::LineString& g, const Point& poi const auto& nearestLoc = distance.nearestLocations()[0]; - const auto* seq = detail::down_cast(nearestLoc.getGeometryComponent())->getCoordinatesRO(); + std::pair, std::unique_ptr> split; + if (g.getGeometryTypeId() == geom::GEOS_COMPOUNDCURVE) { + const CompoundCurve& cc = static_cast(g); - const CoordinateXY& p0 = seq->getAt(nearestLoc.getSegmentIndex()); - const CoordinateXY& p1 = seq->getAt(nearestLoc.getSegmentIndex() + 1); + for (std::size_t i = 0 ; i < cc.getNumCurves(); i++) { + const auto* sc = cc.getCurveN(i); - - std::pair, std::unique_ptr> split; - if (nearestLoc.getCoordinate().equals2D(p0)) { - // no need to add a new point - split = SplitGeometryAtVertex::splitLineStringAtVertex(static_cast(g), nearestLoc.getSegmentIndex()); - } else if (nearestLoc.getCoordinate().equals2D(p1)) { - split = SplitGeometryAtVertex::splitLineStringAtVertex(static_cast(g), nearestLoc.getSegmentIndex() + 1); + if (sc == nearestLoc.getGeometryComponent()) { + split = SplitLinealAtPoint::splitCompoundCurveAtPoint(cc, i, nearestLoc.getSegmentIndex(), nearestLoc.getCoordinate()); + break; + } + } } else { - split = SplitGeometryAtVertex::splitLineStringAtPoint(static_cast(g), nearestLoc.getSegmentIndex(), nearestLoc.getCoordinate()); + const SimpleCurve& sc = static_cast(g); + split = SplitLinealAtPoint::splitSimpleCurveAtPoint(sc, nearestLoc.getSegmentIndex(), nearestLoc.getCoordinate()); } + util::Assert::isNotNull(split.first, "Split result first curve should not be null"); + util::Assert::isNotNull(split.second, "Split result second curve should not be null"); + std::vector> geoms; if (!split.first->isEmpty()) { geoms.push_back(std::move(split.first)); diff --git a/src/operation/split/SplitGeometryAtVertex.cpp b/src/operation/split/SplitGeometryAtVertex.cpp deleted file mode 100644 index 512e6f009..000000000 --- a/src/operation/split/SplitGeometryAtVertex.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/********************************************************************** -* - * GEOS - Geometry Engine Open Source - * http://geos.osgeo.org - * - * Copyright (C) 2026 ISciences, LLC - * - * This is free software; you can redistribute and/or modify it under - * the terms of the GNU Lesser General Public Licence as published - * by the Free Software Foundation. - * See the COPYING file for more information. - * - **********************************************************************/ - -#include - -#include -#include -#include -#include -#include - -using geos::geom::CoordinateSequence; -using geos::geom::CoordinateXY; -using geos::geom::CoordinateXYZM; -using geos::geom::CircularString; -using geos::geom::LineString; -using geos::geom::SimpleCurve; -using geos::geom::GeometryTypeId; - -namespace geos::operation::split { - -std::pair, std::unique_ptr> -SplitGeometryAtVertex::splitLineStringAtPoint(const LineString& ls, std::size_t i, const CoordinateXY& pt) -{ - const auto& gf = *ls.getFactory(); - const CoordinateSequence& pts = *ls.getCoordinatesRO(); - - if (i + 1 >= pts.size()) { - throw util::IllegalArgumentException("Cannot split LineString at point beyond end"); - } - - auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); - auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); - - CoordinateXYZM ptZM(pt); - if (pts.hasZ() || pts.hasM()) { - CoordinateXYZM prev; - CoordinateXYZM next; - pts.getAt(i, prev); - pts.getAt(i + 1, next); - - ptZM.z = algorithm::Interpolate::zGetOrInterpolate(pt, prev, next); - ptZM.m = algorithm::Interpolate::mGetOrInterpolate(pt, prev, next); - } - - pts1->add(pts, 0, i); - pts1->add(ptZM); - - if (i < pts.size() - 1) { - pts2->add(ptZM); - pts2->add(pts, i + 1, pts.size() - 1); - } - - return { gf.createLineString(pts1), gf.createLineString(pts2) }; -} - -std::pair, std::unique_ptr> -SplitGeometryAtVertex::splitLineStringAtVertex(const LineString& ls, std::size_t i) -{ - const auto& gf = *ls.getFactory(); - const CoordinateSequence& pts = *ls.getCoordinatesRO(); - - auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); - auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); - - if (i > 0) { - pts1->add(pts, 0, i); - } - if (i < pts.size() - 1) { - pts2->add(pts, i, pts.size() - 1); - } - - return { gf.createLineString(pts1), gf.createLineString(pts2) }; -} - -std::pair, std::unique_ptr> - SplitGeometryAtVertex::splitCircularStringAtVertex(const CircularString& cs, std::size_t i) -{ - const auto& gf = *cs.getFactory(); - const CoordinateSequence& pts = *cs.getCoordinatesRO(); - - if (i % 2) { - throw util::IllegalArgumentException("Cannot split CircularString at arc control point"); - } - - auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); - auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); - - if (i > 0) { - pts1->add(pts, 0, i); - } - if (i < pts.size() - 1) { - pts2->add(pts, i, pts.size() - 1); - } - - return { gf.createCircularString(pts1), gf.createCircularString(pts2) }; -} - -std::pair, std::unique_ptr> - SplitGeometryAtVertex::splitSimpleCurveAtVertex(const SimpleCurve& sc, std::size_t i) -{ - if (sc.getGeometryTypeId() == GeometryTypeId::GEOS_CIRCULARSTRING) { - return splitCircularStringAtVertex(static_cast(sc), i); - } - if (sc.getGeometryTypeId() == GeometryTypeId::GEOS_LINESTRING || sc.getGeometryTypeId() == GeometryTypeId::GEOS_LINEARRING) { - return splitLineStringAtVertex(static_cast(sc), i); - } - - throw util::UnsupportedOperationException("Unhandled type in SplitGeometryAtVertex::splitAtVertex"); -} - -} \ No newline at end of file diff --git a/src/operation/split/SplitLinealAtPoint.cpp b/src/operation/split/SplitLinealAtPoint.cpp new file mode 100644 index 000000000..a3e230af7 --- /dev/null +++ b/src/operation/split/SplitLinealAtPoint.cpp @@ -0,0 +1,252 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +using geos::algorithm::CircularArcs; +using geos::geom::CircularArc; +using geos::geom::CircularString; +using geos::geom::CompoundCurve; +using geos::geom::CoordinateSequence; +using geos::geom::CoordinateXY; +using geos::geom::CoordinateXYZM; +using geos::geom::Curve; +using geos::geom::Geometry; +using geos::geom::GeometryTypeId; +using geos::geom::LineString; +using geos::geom::SimpleCurve; + +namespace geos::operation::split { + +std::pair, std::unique_ptr> +SplitLinealAtPoint::splitLineStringAtPoint(const LineString& ls, std::size_t i, const CoordinateXY& pt) +{ + const auto& gf = *ls.getFactory(); + const CoordinateSequence& pts = *ls.getCoordinatesRO(); + + if (pt.equals2D(pts.getAt(i))) { + return splitLineStringAtVertex(ls, i); + } + + if (i + 1 >= pts.size()) { + throw util::IllegalArgumentException("Cannot split LineString at point beyond end"); + } + + if (pt.equals2D(pts.getAt(i + 1))) { + return splitLineStringAtVertex(ls, i + 1); + } + + auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); + auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); + + CoordinateXYZM ptZM(pt); + if (pts.hasZ() || pts.hasM()) { + CoordinateXYZM prev; + CoordinateXYZM next; + pts.getAt(i, prev); + pts.getAt(i + 1, next); + + ptZM.z = algorithm::Interpolate::zGetOrInterpolate(pt, prev, next); + ptZM.m = algorithm::Interpolate::mGetOrInterpolate(pt, prev, next); + } + + pts1->add(pts, 0, i); + pts1->add(ptZM); + + if (i < pts.size() - 1) { + pts2->add(ptZM); + pts2->add(pts, i + 1, pts.size() - 1); + } + + return { gf.createLineString(pts1), gf.createLineString(pts2) }; +} + +std::pair, std::unique_ptr> +SplitLinealAtPoint::splitCircularStringAtPoint(const CircularString& cs, std::size_t i, const CoordinateXY& pt) +{ + const auto& gf = *cs.getFactory(); + const CoordinateSequence& pts = *cs.getCoordinatesRO(); + + if (i % 2) { + throw util::IllegalArgumentException("Section index must be the start of an arc"); + } + + if (pt.equals2D(pts.getAt(i))) { + return splitCircularStringAtVertex(cs, i); + } + + if (i + 2 >= pts.size()) { + throw util::IllegalArgumentException("Cannot split CircularString at arc beyond end"); + } + + const CircularArc arc(pts, i); + + if (pt.equals2D(arc.p2())) { + return splitCircularStringAtVertex(cs, i + 2); + } + + auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); + auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); + + CoordinateXYZM splitPointZM(pt); + if (pts.hasZ() || pts.hasM()) { + CircularArcs::interpolateZM(pts, i, arc.getCenter(), arc.isCCW(), splitPointZM, splitPointZM.z, splitPointZM.m); + } + + CoordinateXYZM sec0Midpoint(CircularArcs::getMidpoint(pts.getAt(i), splitPointZM, arc.getCenter(), arc.getRadius(), arc.isCCW())); + if (pts.hasZ() || pts.hasM()) { + CircularArcs::interpolateZM(pts, i, arc.getCenter(), arc.isCCW(), sec0Midpoint, sec0Midpoint.z, sec0Midpoint.m); + } + pts1->add(pts, 0, i); + pts1->add(sec0Midpoint); + pts1->add(splitPointZM); + + CoordinateXYZM sec1Midpoint(CircularArcs::getMidpoint(splitPointZM, pts.getAt(i + 2), arc.getCenter(), arc.getRadius(), arc.isCCW())); + if (pts.hasZ() || pts.hasM()) { + CircularArcs::interpolateZM(pts, i, arc.getCenter(), arc.isCCW(), sec1Midpoint, sec1Midpoint.z, sec1Midpoint.m); + } + pts2->add(splitPointZM); + pts2->add(sec1Midpoint); + pts2->add(pts, i + 2, pts.size() - 1); + + return { gf.createCircularString(pts1), gf.createCircularString(pts2) }; +} + +std::pair, std::unique_ptr> +SplitLinealAtPoint::splitLineStringAtVertex(const LineString& ls, std::size_t i) +{ + const auto& gf = *ls.getFactory(); + const CoordinateSequence& pts = *ls.getCoordinatesRO(); + + auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); + auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); + + if (i > 0) { + pts1->add(pts, 0, i); + } + if (i < pts.size() - 1) { + pts2->add(pts, i, pts.size() - 1); + } + + return { gf.createLineString(pts1), gf.createLineString(pts2) }; +} + +std::pair, std::unique_ptr> + SplitLinealAtPoint::splitCircularStringAtVertex(const CircularString& cs, std::size_t i) +{ + const auto& gf = *cs.getFactory(); + const CoordinateSequence& pts = *cs.getCoordinatesRO(); + + if (i % 2) { + throw util::IllegalArgumentException("Cannot split CircularString at arc control point"); + } + + auto pts1 = std::make_shared(0, pts.hasZ(), pts.hasM()); + auto pts2 = std::make_shared(0, pts.hasZ(), pts.hasM()); + + if (i > 0) { + pts1->add(pts, 0, i); + } + if (i < pts.size() - 1) { + pts2->add(pts, i, pts.size() - 1); + } + + return { gf.createCircularString(pts1), gf.createCircularString(pts2) }; +} + +std::pair, std::unique_ptr> + SplitLinealAtPoint::splitSimpleCurveAtVertex(const SimpleCurve& sc, std::size_t i) +{ + if (sc.getGeometryTypeId() == GeometryTypeId::GEOS_CIRCULARSTRING) { + return splitCircularStringAtVertex(static_cast(sc), i); + } + if (sc.getGeometryTypeId() == GeometryTypeId::GEOS_LINESTRING || sc.getGeometryTypeId() == GeometryTypeId::GEOS_LINEARRING) { + return splitLineStringAtVertex(static_cast(sc), i); + } + + throw util::UnsupportedOperationException("Unhandled type in SplitLinealAtPoint::splitAtVertex"); +} + +std::pair, std::unique_ptr> +SplitLinealAtPoint::splitSimpleCurveAtPoint(const SimpleCurve& sc, std::size_t i, const CoordinateXY& pt) +{ + if (sc.getGeometryTypeId() == GeometryTypeId::GEOS_CIRCULARSTRING) { + return splitCircularStringAtPoint(static_cast(sc), i, pt); + } + if (sc.getGeometryTypeId() == GeometryTypeId::GEOS_LINESTRING || sc.getGeometryTypeId() == GeometryTypeId::GEOS_LINEARRING) { + return splitLineStringAtPoint(static_cast(sc), i, pt); + } + + throw util::UnsupportedOperationException("Unhandled type in SplitLinealAtPoint::splitAtVertex"); +} + +template +void removeEmptyGeometries(T& geoms) { + geoms.erase(std::remove_if(geoms.begin(), geoms.end(), [](const auto& curve) { return curve->isEmpty(); }), geoms.end()); +} + +static std::unique_ptr +makeCurve(const geom::GeometryFactory& gfact, std::vector> curves) +{ + if (curves.size() == 1) { + return std::move(curves[0]); + } + + removeEmptyGeometries(curves); + + if (curves.size() == 1) { + return std::move(curves[0]); + } + + return gfact.createCompoundCurve(std::move(curves)); +} + +std::pair, std::unique_ptr> +SplitLinealAtPoint::splitCompoundCurveAtPoint(const CompoundCurve& cc, std::size_t component, std::size_t section, const CoordinateXY& pt) +{ + const auto& gfact = *cc.getFactory(); + + std::vector> firstCurves; + std::vector> secondCurves; + + for (std::size_t i = 0; i < cc.getNumCurves(); i++) { + const auto* sc = cc.getCurveN(i); + + if (i < component) { + firstCurves.push_back(sc->clone()); + } else if (i == component) { + auto split = splitSimpleCurveAtPoint(*sc, section, pt); + firstCurves.push_back(std::move(split.first)); + secondCurves.push_back(std::move(split.second)); + } else { + secondCurves.push_back(sc->clone()); + } + } + + std::pair, std::unique_ptr> result; + result.first = makeCurve(gfact, std::move(firstCurves)); + result.second = makeCurve(gfact, std::move(secondCurves)); + + return result; +} + +} \ No newline at end of file diff --git a/tests/unit/operation/split/GeometrySplitterTest.cpp b/tests/unit/operation/split/GeometrySplitterTest.cpp index 15a53d964..988644c10 100644 --- a/tests/unit/operation/split/GeometrySplitterTest.cpp +++ b/tests/unit/operation/split/GeometrySplitterTest.cpp @@ -606,13 +606,11 @@ void object::test<49>() ); } -#if 0 template<> template<> void object::test<50>() { set_test_name("split CircularString with point"); - // not implemented yet: need curve support in DistanceOp testSplit("CIRCULARSTRING (-5 0, -4 3, 4 3)", "POINT (0 5)", @@ -624,18 +622,27 @@ template<> void object::test<51>() { set_test_name("split CompoundCurve with point on curve"); - // not implemented yet: need curve support in DistanceOp testSplit("COMPOUNDCURVE(CIRCULARSTRING (-5 0, -4 3, 4 3), (4 3, 0 0))", "POINT (0 5)", - "GEOMETRYCOLLECTION (CIRCULARSTRING (-5 0, -3.5355339059327373 3.5355339059327378, 0 5), CIRCULARSTRING (0 5, 2.2360679774997902 4.47213595499958, 4 3))"); + "GEOMETRYCOLLECTION (CIRCULARSTRING (-5 0, -3.5355339059327373 3.5355339059327378, 0 5), COMPOUNDCURVE(CIRCULARSTRING (0 5, 2.2360679774997902 4.47213595499958, 4 3), (4 3, 0 0)))"); +} + +template<> +template<> +void object::test<52>() +{ + set_test_name("split CompoundCurve with point on line"); + + testSplit("COMPOUNDCURVE(CIRCULARSTRING (-5 0, -4 3, 4 3), (4 3, 0 0))", + "POINT (2 1.5)", + "GEOMETRYCOLLECTION (COMPOUNDCURVE( CIRCULARSTRING (-5 0, -4 3, 4 3), (4 3, 2 1.5)), LINESTRING (2 1.5, 0 0))"); } -#endif #if 0 template<> template<> -void object::test<52>() +void object::test<53>() { set_test_name("nearly-collapsed Polygon (QGIS test #1)"); // void TestQgsGeometry::splitGeometry() @@ -651,7 +658,7 @@ void object::test<52>() template<> template<> -void object::test<53>() +void object::test<54>() { set_test_name("Z values of split edge are not used in interpolation; QGIS test #2"); // See https://github.com/qgis/QGIS/issues/33489 @@ -663,7 +670,7 @@ void object::test<53>() template<> template<> -void object::test<54>() +void object::test<55>() { set_test_name("split CompoundCurve at an existing vertex; QGIS test #3"); @@ -674,7 +681,7 @@ void object::test<54>() template<> template<> -void object::test<55>() +void object::test<56>() { set_test_name("Split self-intersecting LineString at points; adaptation of QGIS test #4"); @@ -687,7 +694,7 @@ void object::test<55>() #if 0 template<> template<> -void object::test<56>() +void object::test<57>() { set_test_name("do not split on self-intersections; QGIS test #4"); @@ -705,7 +712,7 @@ void object::test<56>() template<> template<> -void object::test<57>() +void object::test<58>() { set_test_name("do not split on self-intersections; QGIS test #5"); @@ -717,7 +724,7 @@ void object::test<57>() template<> template<> -void object::test<58>() +void object::test<59>() { set_test_name("split LineString Z on existing vertex; QGIS test #6"); @@ -729,7 +736,7 @@ void object::test<58>() template<> template<> -void object::test<59>() +void object::test<60>() { // Should not crash - https://github.com/qgis/QGIS/issues/50948 testSplit("LINESTRING ( -63294.10966012725839391 -79156.27234554117603693, -63290.25259721937618451 -79162.78533450335089583, -63290.25259721936890855 -79162.78533450335089583)", @@ -739,7 +746,7 @@ void object::test<59>() template<> template<> -void object::test<60>() +void object::test<61>() { // Should not split the first part - https://github.com/qgis/QGIS/issues/54155 testSplit("MULTILINESTRING((0 1, 1 0), (0 2, 2 0))", @@ -749,7 +756,7 @@ void object::test<60>() template<> template<> -void object::test<61>() +void object::test<62>() { set_test_name("cannot split Polygon with point"); @@ -761,7 +768,7 @@ void object::test<61>() template<> template<> -void object::test<62>() +void object::test<63>() { set_test_name("cannot split CurvePolygon with point"); @@ -771,4 +778,37 @@ void object::test<62>() ensure_THROW(GeometrySplitter::split(*geom, *splitGeom), geos::util::IllegalArgumentException); } +template<> +template<> +void object::test<64>() +{ + set_test_name("split LineString with disjoint point"); + + testSplit("LINESTRING (0 0, 10 0)", + "POINT (5 1)", + "GEOMETRYCOLLECTION (LINESTRING (0 0, 10 0))"); +} + +template<> +template<> +void object::test<65>() +{ + set_test_name("split LineString with empty point"); + + testSplit("LINESTRING (0 0, 10 0)", + "POINT EMPTY", + "GEOMETRYCOLLECTION (LINESTRING (0 0, 10 0))"); +} + +template<> +template<> +void object::test<66>() +{ + set_test_name("split LineString with invalid point"); + + testSplit("LINESTRING (0 0, 10 0)", + "POINT (6 NaN)", + "GEOMETRYCOLLECTION (LINESTRING (0 0, 10 0))"); +} + } diff --git a/tests/unit/operation/split/SplitGeometryAtVertexTest.cpp b/tests/unit/operation/split/SplitGeometryAtVertexTest.cpp deleted file mode 100644 index 59af695d2..000000000 --- a/tests/unit/operation/split/SplitGeometryAtVertexTest.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -using geos::geom::CoordinateXY; -using geos::geom::CircularString; -using geos::geom::LineString; -using geos::operation::split::SplitGeometryAtVertex; - -namespace tut { - -struct test_splitgeometryatvertex_data { - const geos::io::WKTReader reader_; -}; - -typedef test_group group; -typedef group::object object; - -group test_splitgeometryatvertextest_group("geos::operation::split::SplitGeometryAtVertex"); - -template<> -template<> -void object::test<1>() -{ - set_test_name("Split LineString ZM at vertex"); - - auto input = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 2 4 5, 6 1 5 6)"); - - { - auto splitAtStart = SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, 0); - - ensure(splitAtStart.first->isEmpty()); - ensure(splitAtStart.first->hasZ()); - ensure(splitAtStart.first->hasM()); - ensure(splitAtStart.second->equalsIdentical(input.get())); - } - - { - auto splitAtEnd = SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, input->getNumPoints() - 1); - - ensure(splitAtEnd.first->equalsIdentical(input.get())); - ensure(splitAtEnd.second->isEmpty()); - ensure(splitAtEnd.second->hasZ()); - ensure(splitAtEnd.second->hasM()); - } - - { - auto splitInMiddle = SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, 2); - - auto expectedFirst = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 2 4 5)"); - auto expectedSecond = reader_.read("LINESTRING ZM (2 2 4 5, 6 1 5 6)"); - - ensure(splitInMiddle.first->equalsIdentical(expectedFirst.get())); - ensure(splitInMiddle.second->equalsIdentical(expectedSecond.get())); - } -} - -template<> -template<> -void object::test<2>() -{ - set_test_name("Split CircularString ZM at vertex"); - - auto input = reader_.read("CIRCULARSTRING ZM (-5 0 1 2, 0 5 2 3, 5 0 3 4, 10 -5 4 5, 15 0 5 6)"); - - { - auto splitAtStart = SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, 0); - - ensure(splitAtStart.first->isEmpty()); - ensure(splitAtStart.first->hasZ()); - ensure(splitAtStart.first->hasM()); - ensure(splitAtStart.second->equalsIdentical(input.get())); - } - - { - auto splitAtEnd = SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, input->getNumPoints() - 1); - - ensure(splitAtEnd.first->equalsIdentical(input.get())); - ensure(splitAtEnd.second->isEmpty()); - ensure(splitAtEnd.second->hasZ()); - ensure(splitAtEnd.second->hasM()); - } - - { - auto splitInMiddle = SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, 2); - - auto expectedFirst = reader_.read("CIRCULARSTRING ZM (-5 0 1 2, 0 5 2 3, 5 0 3 4)"); - auto expectedSecond = reader_.read("CIRCULARSTRING ZM (5 0 3 4, 10 -5 4 5, 15 0 5 6)"); - - ensure(splitInMiddle.first->equalsIdentical(expectedFirst.get())); - ensure(splitInMiddle.second->equalsIdentical(expectedSecond.get())); - } - - ensure_THROW(SplitGeometryAtVertex::splitSimpleCurveAtVertex(*input, 1), geos::util::IllegalArgumentException); -} - -template<> -template<> -void object::test<3>() -{ - set_test_name("Split LineString ZM at new point"); - - auto input = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 2 4 5, 6 1 5 6)"); - - CoordinateXY pt{2, 3}; - - ensure_THROW(SplitGeometryAtVertex::splitLineStringAtPoint(*input, 3, pt), geos::util::IllegalArgumentException); - - // Split first segment - { - auto [first, second] = SplitGeometryAtVertex::splitLineStringAtPoint(*input, 0, pt); - - auto expectedFirst = reader_.read("LINESTRING ZM (0 3 2 3, 2 3 2.282842712474619 3.282842712474619)"); - auto expectedSecond = reader_.read("LINESTRING ZM (2 3 2.282842712474619 3.282842712474619, 5 8 3 4, 2 2 4 5, 6 1 5 6)"); - - ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); - ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); - } - - // Split second segment - { - auto [first, second] = SplitGeometryAtVertex::splitLineStringAtPoint(*input, 1, pt); - - auto expectedFirst = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 3 3.8692269873603533 4.869226987360353)"); - auto expectedSecond = reader_.read("LINESTRING ZM (2 3 3.8692269873603533 4.869226987360353, 2 2 4 5, 6 1 5 6)"); - - ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); - ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); - } - - -} - -} diff --git a/tests/unit/operation/split/SplitLinealAtPointTest.cpp b/tests/unit/operation/split/SplitLinealAtPointTest.cpp new file mode 100644 index 000000000..8bf29cff7 --- /dev/null +++ b/tests/unit/operation/split/SplitLinealAtPointTest.cpp @@ -0,0 +1,243 @@ +#include +#include +#include + +#include +#include +#include +#include + +using geos::geom::CoordinateXY; +using geos::geom::CircularString; +using geos::geom::LineString; +using geos::operation::split::SplitLinealAtPoint; + +namespace tut { + +struct test_splitlinealatpoint_data { + const geos::io::WKTReader reader_; +}; + +typedef test_group group; +typedef group::object object; + +group test_splitlinealatpointtest_group("geos::operation::split::SplitLinealAtPoint"); + +template<> +template<> +void object::test<1>() +{ + set_test_name("Split LineString ZM at vertex"); + + auto input = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 2 4 5, 6 1 5 6)"); + + { + auto splitAtStart = SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, 0); + + ensure(splitAtStart.first->isEmpty()); + ensure(splitAtStart.first->hasZ()); + ensure(splitAtStart.first->hasM()); + ensure(splitAtStart.second->equalsIdentical(input.get())); + } + + { + auto splitAtEnd = SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, input->getNumPoints() - 1); + + ensure(splitAtEnd.first->equalsIdentical(input.get())); + ensure(splitAtEnd.second->isEmpty()); + ensure(splitAtEnd.second->hasZ()); + ensure(splitAtEnd.second->hasM()); + } + + { + auto splitInMiddle = SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, 2); + + auto expectedFirst = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 2 4 5)"); + auto expectedSecond = reader_.read("LINESTRING ZM (2 2 4 5, 6 1 5 6)"); + + ensure(splitInMiddle.first->equalsIdentical(expectedFirst.get())); + ensure(splitInMiddle.second->equalsIdentical(expectedSecond.get())); + } +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("Split CircularString ZM at vertex"); + + auto input = reader_.read("CIRCULARSTRING ZM (-5 0 1 2, 0 5 2 3, 5 0 3 4, 10 -5 4 5, 15 0 5 6)"); + + { + auto splitAtStart = SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, 0); + + ensure(splitAtStart.first->isEmpty()); + ensure(splitAtStart.first->hasZ()); + ensure(splitAtStart.first->hasM()); + ensure(splitAtStart.second->equalsIdentical(input.get())); + } + + { + auto splitAtEnd = SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, input->getNumPoints() - 1); + + ensure(splitAtEnd.first->equalsIdentical(input.get())); + ensure(splitAtEnd.second->isEmpty()); + ensure(splitAtEnd.second->hasZ()); + ensure(splitAtEnd.second->hasM()); + } + + { + auto splitInMiddle = SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, 2); + + auto expectedFirst = reader_.read("CIRCULARSTRING ZM (-5 0 1 2, 0 5 2 3, 5 0 3 4)"); + auto expectedSecond = reader_.read("CIRCULARSTRING ZM (5 0 3 4, 10 -5 4 5, 15 0 5 6)"); + + ensure(splitInMiddle.first->equalsIdentical(expectedFirst.get())); + ensure(splitInMiddle.second->equalsIdentical(expectedSecond.get())); + } + + ensure_THROW(SplitLinealAtPoint::splitSimpleCurveAtVertex(*input, 1), geos::util::IllegalArgumentException); +} + +template<> +template<> +void object::test<3>() +{ + set_test_name("Split LineString ZM at new point"); + + auto input = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 2 4 5, 6 1 5 6)"); + + CoordinateXY pt{2, 3}; + + ensure_THROW(SplitLinealAtPoint::splitLineStringAtPoint(*input, 3, pt), geos::util::IllegalArgumentException); + + // Split first segment + { + auto [first, second] = SplitLinealAtPoint::splitLineStringAtPoint(*input, 0, pt); + + auto expectedFirst = reader_.read("LINESTRING ZM (0 3 2 3, 2 3 2.282842712474619 3.282842712474619)"); + auto expectedSecond = reader_.read("LINESTRING ZM (2 3 2.282842712474619 3.282842712474619, 5 8 3 4, 2 2 4 5, 6 1 5 6)"); + + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); + } + + // Split second segment + { + auto [first, second] = SplitLinealAtPoint::splitLineStringAtPoint(*input, 1, pt); + + auto expectedFirst = reader_.read("LINESTRING ZM (0 3 2 3, 5 8 3 4, 2 3 3.8692269873603533 4.869226987360353)"); + auto expectedSecond = reader_.read("LINESTRING ZM (2 3 3.8692269873603533 4.869226987360353, 2 2 4 5, 6 1 5 6)"); + + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); + } + +} + +template<> +template<> +void object::test<4>() +{ + set_test_name("Split CompoundCurve ZM at existing vertices"); + + auto input = reader_.read("COMPOUNDCURVE ZM(CIRCULARSTRING ZM(2 8 1 2, 4 7 3 4, 1 9 5 6, 3 15 7 8, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16), CIRCULARSTRING (4 12 15 16, 4 10 17 18, 2 8 19 20))"); + + // Split at first point + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 0, 0, CoordinateXY{2, 8}); + + ensure(first->isEmpty()); + ensure(second->equalsIdentical(input.get())); + } + + // Split at intermediate point of first curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 0, 2, CoordinateXY{1, 9}); + + auto expectedFirst = reader_.read("CIRCULARSTRING ZM (2 8 1 2, 4 7 3 4, 1 9 5 6)"); + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + + auto expectedSecond = reader_.read("COMPOUNDCURVE ZM (CIRCULARSTRING ZM(1 9 5 6, 3 15 7 8, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16), CIRCULARSTRING (4 12 15 16, 4 10 17 18, 2 8 19 20))"); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); + } + + // Split at last point of first curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 0, 2, CoordinateXY{8, 16}); + + ensure(first->equalsIdentical(input->getCurveN(0))); + + auto expectedSecond = reader_.read("COMPOUNDCURVE ZM ((8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16), CIRCULARSTRING (4 12 15 16, 4 10 17 18, 2 8 19 20))"); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); + } + + // Split at first point of second curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 1, 0, CoordinateXY{8, 16}); + + ensure(first->equalsIdentical(input->getCurveN(0))); + + auto expectedSecond = reader_.read("COMPOUNDCURVE ZM ((8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16), CIRCULARSTRING (4 12 15 16, 4 10 17 18, 2 8 19 20))"); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); + } + + // Split at intermate point of second curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 1, 1, CoordinateXY{6, 14}); + + auto expectedFirst = reader_.read("COMPOUNDCURVE ZM (CIRCULARSTRING ZM(2 8 1 2, 4 7 3 4, 1 9 5 6, 3 15 7 8, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14))"); + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + + auto expectedSecond = reader_.read("COMPOUNDCURVE ((6 14 13 14, 4 12 15 16), CIRCULARSTRING (4 12 15 16, 4 10 17 18, 2 8 19 20))"); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); + } + + // Split at last point of second curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 1, 3, CoordinateXY{4, 12}); + + auto expectedFirst = reader_.read("COMPOUNDCURVE (CIRCULARSTRING(2 8 1 2, 4 7 3 4, 1 9 5 6, 3 15 7 8, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16))"); + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + + ensure(second->equalsIdentical(input->getCurveN(2))); + } + + // Split at first point of third curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 2, 0, CoordinateXY{4, 12}); + + auto expectedFirst = reader_.read("COMPOUNDCURVE (CIRCULARSTRING(2 8 1 2, 4 7 3 4, 1 9 5 6, 3 15 7 8, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16))"); + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + + ensure(second->equalsIdentical(input->getCurveN(2))); + } + + // Split at last point of third curve + { + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 2, 2, CoordinateXY{2, 8}); + + ensure(first->equalsIdentical(input.get())); + ensure(second->isEmpty()); + } + +} + +template<> +template<> +void object::test<5>() +{ + set_test_name("Split CompoundCurve ZM at new point"); + + auto input = reader_.read("COMPOUNDCURVE ZM(CIRCULARSTRING ZM(2 8 1 2, 4 7 3 4, 1 9 5 6, 3 15 7 8, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16), CIRCULARSTRING (4 12 15 16, 4 10 17 18, 2 8 19 20))"); + + auto [first, second] = SplitLinealAtPoint::splitCompoundCurveAtPoint(*input, 0, 2, CoordinateXY{5, 16}); + + auto expectedFirst = reader_.read("CIRCULARSTRING ZM (2 8 1 2, 4 7 3 4, 1 9 5 6, 1.5502525316941682 13.328427124746192 6.33570524800221 7.33570524800221, 5 16 7.851489605544531 8.851489605544531)"); + ensure_equals_exact_geometry_xyzm(first.get(), expectedFirst.get(), 0.0); + + auto expectedSecond = reader_.read("COMPOUNDCURVE ZM (CIRCULARSTRING ZM (5 16 7.851489605544531 8.851489605544531, 6.5 16.20087712549569 8.425744802772266 9.425744802772266, 8 16 9 10), (8 16 9 10, 8 10 11 12, 6 14 13 14, 4 12 15 16), CIRCULARSTRING ZM (4 12 15 16, 4 10 17 18, 2 8 19 20)) "); + ensure_equals_exact_geometry_xyzm(second.get(), expectedSecond.get(), 0.0); +} + +} ----------------------------------------------------------------------- Summary of changes: include/geos/algorithm/CircularArcs.h | 21 +- include/geos/geom/SimpleCurve.h | 2 + include/geos/operation/split/GeometrySplitter.h | 6 +- .../geos/operation/split/SplitGeometryAtVertex.h | 48 ---- include/geos/operation/split/SplitLinealAtPoint.h | 90 ++++++++ include/geos/util/Assert.h | 7 + src/algorithm/CircularArcIntersector.cpp | 110 +-------- src/algorithm/CircularArcs.cpp | 107 +++++++++ src/geom/CircularArc.cpp | 1 + src/geom/CompoundCurve.cpp | 4 +- src/geom/SimpleCurve.cpp | 6 + src/noding/NodableArcString.cpp | 2 + src/operation/split/GeometrySplitter.cpp | 56 +++-- src/operation/split/SplitGeometryAtVertex.cpp | 123 ---------- src/operation/split/SplitLinealAtPoint.cpp | 252 +++++++++++++++++++++ .../unit/operation/split/GeometrySplitterTest.cpp | 72 ++++-- .../operation/split/SplitGeometryAtVertexTest.cpp | 139 ------------ .../operation/split/SplitLinealAtPointTest.cpp | 243 ++++++++++++++++++++ 18 files changed, 830 insertions(+), 459 deletions(-) delete mode 100644 include/geos/operation/split/SplitGeometryAtVertex.h create mode 100644 include/geos/operation/split/SplitLinealAtPoint.h delete mode 100644 src/operation/split/SplitGeometryAtVertex.cpp create mode 100644 src/operation/split/SplitLinealAtPoint.cpp delete mode 100644 tests/unit/operation/split/SplitGeometryAtVertexTest.cpp create mode 100644 tests/unit/operation/split/SplitLinealAtPointTest.cpp hooks/post-receive -- GEOS From git at osgeo.org Mon May 4 14:32:21 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 4 May 2026 14:32:21 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. cf859843556dd76e9597c588543915b7aa7d7bc4 Message-ID: <20260504213222.2BA7718D483@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via cf859843556dd76e9597c588543915b7aa7d7bc4 (commit) from 0028dd486e18abb779d3c1d1dfeb1a3f34e4af5c (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit cf859843556dd76e9597c588543915b7aa7d7bc4 Author: MUGUNDAN Date: Mon May 4 17:31:58 2026 -0400 Fix GEOS benchmarking on Windows (#1429) * Fix GEOS benchmarking on Windows * Add instructions for benchmarking Geos library diff --git a/benchmarks/README.md b/benchmarks/README.md index 65eb4b4f9..760815be6 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,10 +1,64 @@ -## Benchmarks +## Building -A variety of programs to execute various kinds of tests, -including benchmarks, stability and robustness tests. +Building the benchmark tests must be enabled using: -### Building +```bash +cmake -DBUILD_BENCHMARKS=ON .. +``` -Building the benchmark tests must be enabled using +Geos uses Google Benchmarks for validating performance of Geos library. It can be built using the following ways: - cmake -DBUILD_BENCHMARKS=ON .. +### Linux + +Install via package manager: + +```bash +# Debian/Ubuntu +sudo apt install libbenchmark-dev + +# Fedora +sudo dnf install google-benchmark-devel +``` + +Then build: + +```bash +mkdir build && cd build +cmake -DBUILD_BENCHMARKS=ON .. +make -j$(nproc) +``` + +### Windows (MSYS2) + +Install Google Benchmark via pacman: + +```bash +# MINGW64 +pacman -S mingw-w64-x86_64-benchmark + +# UCRT64 +pacman -S mingw-w64-ucrt-x86_64-benchmark + +# CLANG64 +pacman -S mingw-w64-clang-x86_64-benchmark + +# CLANGARM64 +pacman -S mingw-w64-clang-aarch64-benchmark +``` + +Then build: + +```bash +mkdir build && cd build +cmake -G "MinGW Makefiles" -DBUILD_BENCHMARKS=ON .. +mingw32-make -j$(nproc) +``` + +### Windows (MSVC / vcpkg) + +```bash +vcpkg install benchmark:x64-windows +mkdir build && cd build +cmake -DBUILD_BENCHMARKS=ON -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake .. +cmake --build . --config Release +``` diff --git a/benchmarks/algorithm/CMakeLists.txt b/benchmarks/algorithm/CMakeLists.txt index 3b8f867a5..6e8382307 100644 --- a/benchmarks/algorithm/CMakeLists.txt +++ b/benchmarks/algorithm/CMakeLists.txt @@ -29,7 +29,7 @@ if (benchmark_FOUND) $ $) target_link_libraries(perf_orientation PRIVATE - benchmark::benchmark geos_cxx_flags) + benchmark::benchmark geos) add_executable(perf_line_intersector LineIntersectorPerfTest.cpp) target_include_directories(perf_line_intersector PUBLIC diff --git a/benchmarks/algorithm/OrientationIndexStressTest.cpp b/benchmarks/algorithm/OrientationIndexStressTest.cpp index 24e19677f..c4c6f7246 100644 --- a/benchmarks/algorithm/OrientationIndexStressTest.cpp +++ b/benchmarks/algorithm/OrientationIndexStressTest.cpp @@ -43,6 +43,11 @@ Two kinds of test generators are provided: #include +// MSVC does not provide POSIX random(), Use rand() for compatibility +#ifdef _MSC_VER +#define random rand +#endif + using namespace geos::algorithm; using namespace geos::geom; using namespace geos::io; diff --git a/include/geos/index/intervalrtree/IntervalRTreeLeafNode.h b/include/geos/index/intervalrtree/IntervalRTreeLeafNode.h index b0c3692a9..5dee32cc6 100644 --- a/include/geos/index/intervalrtree/IntervalRTreeLeafNode.h +++ b/include/geos/index/intervalrtree/IntervalRTreeLeafNode.h @@ -15,6 +15,7 @@ #pragma once +#include #include // inherited @@ -30,7 +31,7 @@ namespace geos { namespace index { namespace intervalrtree { -class IntervalRTreeLeafNode : public IntervalRTreeNode { +class GEOS_DLL IntervalRTreeLeafNode : public IntervalRTreeNode { private: /// externally owned void* item; diff --git a/include/geos/index/intervalrtree/SortedPackedIntervalRTree.h b/include/geos/index/intervalrtree/SortedPackedIntervalRTree.h index ec472613e..b97623023 100644 --- a/include/geos/index/intervalrtree/SortedPackedIntervalRTree.h +++ b/include/geos/index/intervalrtree/SortedPackedIntervalRTree.h @@ -15,6 +15,7 @@ #pragma once +#include #include #include #include @@ -48,7 +49,7 @@ namespace intervalrtree { * @author Martin Davis * */ -class SortedPackedIntervalRTree { +class GEOS_DLL SortedPackedIntervalRTree { private: std::vector leaves; std::vector branches; ----------------------------------------------------------------------- Summary of changes: benchmarks/README.md | 66 ++++++++++++++++++++-- benchmarks/algorithm/CMakeLists.txt | 2 +- .../algorithm/OrientationIndexStressTest.cpp | 5 ++ .../index/intervalrtree/IntervalRTreeLeafNode.h | 3 +- .../intervalrtree/SortedPackedIntervalRTree.h | 3 +- 5 files changed, 70 insertions(+), 9 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Wed May 13 12:12:56 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 13 May 2026 12:12:56 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 8b8b3da7a3d9fb8953ff60bc49aa0320d51ae45c Message-ID: <20260513191256.ED4671B1070@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 8b8b3da7a3d9fb8953ff60bc49aa0320d51ae45c (commit) from cf859843556dd76e9597c588543915b7aa7d7bc4 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 8b8b3da7a3d9fb8953ff60bc49aa0320d51ae45c Author: Paul Ramsey Date: Wed May 13 12:11:50 2026 -0700 Limit recursion depth in WKT/WKB/GeoJSON readers Prevents stack overflow on malformed input. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index a1be8e65b..9488b4be9 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1147,6 +1147,7 @@ extern "C" { return execute(extHandle, [&]() { auto ids = clusters->getClusterIds(GEOS_CLUSTER_NONE); std::size_t* ids_buf = (size_t*) malloc(ids.size() * sizeof(std::size_t)); + if (!ids_buf) return ids_buf; std::copy(ids.begin(), ids.end(), ids_buf); return ids_buf; }); diff --git a/include/geos/io/GeoJSONReader.h b/include/geos/io/GeoJSONReader.h index 41834f1dd..9d9d09d20 100644 --- a/include/geos/io/GeoJSONReader.h +++ b/include/geos/io/GeoJSONReader.h @@ -75,6 +75,7 @@ public: private: const geom::GeometryFactory& geometryFactory; + mutable int parseDepth_ = 0; std::unique_ptr readFeatureForGeometry(const geos_nlohmann::json& j) const; diff --git a/include/geos/io/WKBReader.h b/include/geos/io/WKBReader.h index 58eeb8f74..3b97cb498 100644 --- a/include/geos/io/WKBReader.h +++ b/include/geos/io/WKBReader.h @@ -156,6 +156,8 @@ private: std::array ordValues; + int parseDepth_ = 0; + std::unique_ptr readGeometry(); std::unique_ptr readPoint(); diff --git a/include/geos/io/WKTReader.h b/include/geos/io/WKTReader.h index 9fc86c978..bd4bc56cd 100644 --- a/include/geos/io/WKTReader.h +++ b/include/geos/io/WKTReader.h @@ -149,6 +149,7 @@ private: const geom::GeometryFactory* geometryFactory; const geom::PrecisionModel* precisionModel; bool fixStructure; + mutable int parseDepth_ = 0; void getPreciseCoordinate(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, geom::CoordinateXYZM&) const; diff --git a/src/io/GeoJSONReader.cpp b/src/io/GeoJSONReader.cpp index 0f0817e32..1ab54dd9e 100644 --- a/src/io/GeoJSONReader.cpp +++ b/src/io/GeoJSONReader.cpp @@ -40,12 +40,15 @@ using json = geos_nlohmann::json; namespace geos { namespace io { // geos.io +static constexpr int MAX_PARSE_DEPTH = 100; + GeoJSONReader::GeoJSONReader(): GeoJSONReader(*(GeometryFactory::getDefaultInstance())) {} GeoJSONReader::GeoJSONReader(const geom::GeometryFactory& gf) : geometryFactory(gf) {} std::unique_ptr GeoJSONReader::read(const std::string& geoJsonText) const { + parseDepth_ = 0; try { const json& j = json::parse(geoJsonText); const std::string& type = j.at("type"); @@ -66,6 +69,7 @@ std::unique_ptr GeoJSONReader::read(const std::string& geoJsonTe GeoJSONFeatureCollection GeoJSONReader::readFeatures(const std::string& geoJsonText) const { + parseDepth_ = 0; try { const json& j = json::parse(geoJsonText); const std::string& type = j.at("type"); @@ -121,6 +125,12 @@ std::map GeoJSONReader::readProperties( GeoJSONValue GeoJSONReader::readProperty( const geos_nlohmann::json& value) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input property exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + if (value.is_string()) { return GeoJSONValue { value.get() }; } @@ -179,6 +189,12 @@ GeoJSONFeatureCollection GeoJSONReader::readFeatureCollection( std::unique_ptr GeoJSONReader::readGeometry( const geos_nlohmann::json& j) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + const std::string& type = j.at("type"); if (type == "Point") { return readPoint(j); diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index 65ba6c4c1..d5683f2dc 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -252,13 +252,22 @@ WKBReader::read(std::istream& is) std::unique_ptr WKBReader::read(const unsigned char* buf, size_t size) { + parseDepth_ = 0; dis = ByteOrderDataInStream(buf, size); // will default to machine endian return readGeometry(); } +static constexpr int MAX_PARSE_DEPTH = 100; + std::unique_ptr WKBReader::readGeometry() { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + // determine byte order unsigned char byteOrder = dis.readByte(); diff --git a/src/io/WKTReader.cpp b/src/io/WKTReader.cpp index 3c1d6fa69..a49b2cf98 100644 --- a/src/io/WKTReader.cpp +++ b/src/io/WKTReader.cpp @@ -49,9 +49,12 @@ using namespace geos::geom; namespace geos { namespace io { // geos.io +static constexpr int MAX_PARSE_DEPTH = 100; + std::unique_ptr WKTReader::read(const std::string& wellKnownText) const { + parseDepth_ = 0; CLocalizer clocale; StringTokenizer tokenizer(wellKnownText); OrdinateSet ordinateFlags = OrdinateSet::createXY(); @@ -302,6 +305,12 @@ WKTReader::getNextWord(StringTokenizer* tokenizer) std::unique_ptr WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, const GeometryTypeId* emptyType) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + std::string type = getNextWord(tokenizer); std::unique_ptr geom; diff --git a/tests/unit/io/GeoJSONReaderTest.cpp b/tests/unit/io/GeoJSONReaderTest.cpp index 2d474d3d7..c419eaf55 100644 --- a/tests/unit/io/GeoJSONReaderTest.cpp +++ b/tests/unit/io/GeoJSONReaderTest.cpp @@ -619,4 +619,21 @@ void object::test<40> ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); } +// Deeply nested GeometryCollection should throw ParseException (not stack overflow) +template<> +template<> +void object::test<41> +() +{ + std::string geojson; + for (int i = 0; i < 200; i++) geojson += "{\"type\":\"GeometryCollection\",\"geometries\":["; + geojson += "{\"type\":\"Point\",\"coordinates\":[0,0]}"; + for (int i = 0; i < 200; i++) geojson += "]}"; + + try { + geojsonreader.read(geojson); + fail("Expected ParseException for deeply nested GeoJSON"); + } catch (const geos::io::ParseException&) {} +} + } diff --git a/tests/unit/io/WKBReaderTest.cpp b/tests/unit/io/WKBReaderTest.cpp index 6628351a8..ebdba7a40 100644 --- a/tests/unit/io/WKBReaderTest.cpp +++ b/tests/unit/io/WKBReaderTest.cpp @@ -7,11 +7,13 @@ // tut #include +#include // geos #include #include #include #include +#include #include #include #include @@ -838,5 +840,29 @@ void object::test<36> "ParseException: Expected SimpleCurve but got Point"); } + +template<> +template<> +void object::test<37> +() +{ + set_test_name("ParseException on deeply nested WKB collection avoid stack overflow"); + + // Each GeometryCollection header (NDR): byteOrder=01, type=07000000, numGeoms=01000000 + const std::vector header = {0x01, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; + // Innermost empty GeometryCollection: byteOrder=01, type=07000000, numGeoms=00000000 + const std::vector inner = {0x01, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::vector buf; + for (int i = 0; i < 200; i++) buf.insert(buf.end(), header.begin(), header.end()); + buf.insert(buf.end(), inner.begin(), inner.end()); + + ensure_THROW(wkbreader.read(buf.data(), buf.size()), geos::io::ParseException); + + // try { + // wkbreader.read(buf.data(), buf.size()); + // fail("Expected ParseException for deeply nested WKB"); + // } catch (const geos::util::GEOSException&) {} +} + } // namespace tut diff --git a/tests/unit/io/WKTReaderTest.cpp b/tests/unit/io/WKTReaderTest.cpp index af9621916..2a28e805b 100644 --- a/tests/unit/io/WKTReaderTest.cpp +++ b/tests/unit/io/WKTReaderTest.cpp @@ -3,6 +3,7 @@ // tut #include +#include // geos #include #include @@ -162,17 +163,7 @@ template<> void object::test<6> () { - try { - wktreader.read("POLYGON( EMPTY, (1 1,2 2,1 2,1 1))"); - fail("Did not get expected exception"); - } - catch(const geos::util::IllegalArgumentException& ex) { - ensure("Got expected exception", true); - (void)(ex.what()); - } - catch(...) { - fail("Got unexpected exception"); - } + ensure_THROW(wktreader.read("POLYGON( EMPTY, (1 1,2 2,1 2,1 1))"), geos::util::IllegalArgumentException); } // POINT(0 0) http://trac.osgeo.org/geos/ticket/610 @@ -499,4 +490,17 @@ void object::test<25> ensure_parseexception("POLYGON Z M EMPTY"); } +template<> +template<> +void object::test<26> +() +{ + set_test_name("ParseException on deeply nested WKT collection avoid stack overflow"); + std::string wkt; + for (int i = 0; i < 200; i++) wkt += "GEOMETRYCOLLECTION("; + wkt += "POINT(0 0)"; + for (int i = 0; i < 200; i++) wkt += ")"; + ensure_parseexception(wkt); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: capi/geos_ts_c.cpp | 1 + include/geos/io/GeoJSONReader.h | 1 + include/geos/io/WKBReader.h | 2 ++ include/geos/io/WKTReader.h | 1 + src/io/GeoJSONReader.cpp | 16 ++++++++++++++++ src/io/WKBReader.cpp | 9 +++++++++ src/io/WKTReader.cpp | 9 +++++++++ tests/unit/io/GeoJSONReaderTest.cpp | 17 +++++++++++++++++ tests/unit/io/WKBReaderTest.cpp | 26 ++++++++++++++++++++++++++ tests/unit/io/WKTReaderTest.cpp | 26 +++++++++++++++----------- 10 files changed, 97 insertions(+), 11 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Wed May 13 14:37:17 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 13 May 2026 14:37:17 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch 3.13 updated. c3a9f82726a5c90663f79272152194f1d6a43105 Message-ID: <20260513213717.925C71B5255@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, 3.13 has been updated via c3a9f82726a5c90663f79272152194f1d6a43105 (commit) via f8910e84a0f3580f0847373cd77205703bf18c78 (commit) from 1d2f7a27e98237776e365c25e4ab7b2a34370ff9 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit c3a9f82726a5c90663f79272152194f1d6a43105 Author: Paul Ramsey Date: Wed May 13 13:46:33 2026 -0700 Fix CI issue for mingw64 diff --git a/include/geos/algorithm/distance/DistanceToPoint.h b/include/geos/algorithm/distance/DistanceToPoint.h index 108b85506..f1889397b 100644 --- a/include/geos/algorithm/distance/DistanceToPoint.h +++ b/include/geos/algorithm/distance/DistanceToPoint.h @@ -19,6 +19,7 @@ #pragma once #include // for composition +#include namespace geos { namespace algorithm { @@ -43,7 +44,7 @@ namespace distance { // geos::algorithm::distance * * Also computes two points which are separated by the distance. */ -class DistanceToPoint { +class GEOS_DLL DistanceToPoint { public: DistanceToPoint() {} commit f8910e84a0f3580f0847373cd77205703bf18c78 Author: Paul Ramsey Date: Wed May 13 12:11:50 2026 -0700 Limit recursion depth in WKT/WKB/GeoJSON readers Prevents stack overflow on malformed input. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index ef238229a..f76dc25ee 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -948,7 +948,6 @@ extern "C" { }); } - Geometry* GEOSGeomFromWKT_r(GEOSContextHandle_t extHandle, const char* wkt) { diff --git a/include/geos/io/GeoJSONReader.h b/include/geos/io/GeoJSONReader.h index 41834f1dd..9d9d09d20 100644 --- a/include/geos/io/GeoJSONReader.h +++ b/include/geos/io/GeoJSONReader.h @@ -75,6 +75,7 @@ public: private: const geom::GeometryFactory& geometryFactory; + mutable int parseDepth_ = 0; std::unique_ptr readFeatureForGeometry(const geos_nlohmann::json& j) const; diff --git a/include/geos/io/WKBReader.h b/include/geos/io/WKBReader.h index 9d410c250..2b8b5beae 100644 --- a/include/geos/io/WKBReader.h +++ b/include/geos/io/WKBReader.h @@ -146,6 +146,8 @@ private: std::array ordValues; + int parseDepth_ = 0; + std::unique_ptr readGeometry(); std::unique_ptr readPoint(); diff --git a/include/geos/io/WKTReader.h b/include/geos/io/WKTReader.h index 7294d64f5..46529c3c8 100644 --- a/include/geos/io/WKTReader.h +++ b/include/geos/io/WKTReader.h @@ -148,6 +148,7 @@ private: const geom::GeometryFactory* geometryFactory; const geom::PrecisionModel* precisionModel; bool fixStructure; + mutable int parseDepth_ = 0; void getPreciseCoordinate(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, geom::CoordinateXYZM&) const; diff --git a/src/io/GeoJSONReader.cpp b/src/io/GeoJSONReader.cpp index 0f0817e32..1ab54dd9e 100644 --- a/src/io/GeoJSONReader.cpp +++ b/src/io/GeoJSONReader.cpp @@ -40,12 +40,15 @@ using json = geos_nlohmann::json; namespace geos { namespace io { // geos.io +static constexpr int MAX_PARSE_DEPTH = 100; + GeoJSONReader::GeoJSONReader(): GeoJSONReader(*(GeometryFactory::getDefaultInstance())) {} GeoJSONReader::GeoJSONReader(const geom::GeometryFactory& gf) : geometryFactory(gf) {} std::unique_ptr GeoJSONReader::read(const std::string& geoJsonText) const { + parseDepth_ = 0; try { const json& j = json::parse(geoJsonText); const std::string& type = j.at("type"); @@ -66,6 +69,7 @@ std::unique_ptr GeoJSONReader::read(const std::string& geoJsonTe GeoJSONFeatureCollection GeoJSONReader::readFeatures(const std::string& geoJsonText) const { + parseDepth_ = 0; try { const json& j = json::parse(geoJsonText); const std::string& type = j.at("type"); @@ -121,6 +125,12 @@ std::map GeoJSONReader::readProperties( GeoJSONValue GeoJSONReader::readProperty( const geos_nlohmann::json& value) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input property exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + if (value.is_string()) { return GeoJSONValue { value.get() }; } @@ -179,6 +189,12 @@ GeoJSONFeatureCollection GeoJSONReader::readFeatureCollection( std::unique_ptr GeoJSONReader::readGeometry( const geos_nlohmann::json& j) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + const std::string& type = j.at("type"); if (type == "Point") { return readPoint(j); diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index 5410ab0c7..d81a88ddc 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -245,13 +245,22 @@ WKBReader::read(std::istream& is) std::unique_ptr WKBReader::read(const unsigned char* buf, size_t size) { + parseDepth_ = 0; dis = ByteOrderDataInStream(buf, size); // will default to machine endian return readGeometry(); } +static constexpr int MAX_PARSE_DEPTH = 100; + std::unique_ptr WKBReader::readGeometry() { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + // determine byte order unsigned char byteOrder = dis.readByte(); diff --git a/src/io/WKTReader.cpp b/src/io/WKTReader.cpp index fc850d220..8b66e021c 100644 --- a/src/io/WKTReader.cpp +++ b/src/io/WKTReader.cpp @@ -49,9 +49,12 @@ using namespace geos::geom; namespace geos { namespace io { // geos.io +static constexpr int MAX_PARSE_DEPTH = 100; + std::unique_ptr WKTReader::read(const std::string& wellKnownText) const { + parseDepth_ = 0; CLocalizer clocale; StringTokenizer tokenizer(wellKnownText); OrdinateSet ordinateFlags = OrdinateSet::createXY(); @@ -267,6 +270,12 @@ WKTReader::getNextWord(StringTokenizer* tokenizer) std::unique_ptr WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, const GeometryTypeId* emptyType) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + std::string type = getNextWord(tokenizer); std::unique_ptr geom; diff --git a/tests/unit/io/GeoJSONReaderTest.cpp b/tests/unit/io/GeoJSONReaderTest.cpp index c5ff39db8..bafb21ffc 100644 --- a/tests/unit/io/GeoJSONReaderTest.cpp +++ b/tests/unit/io/GeoJSONReaderTest.cpp @@ -620,4 +620,21 @@ void object::test<40> ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); } +// Deeply nested GeometryCollection should throw ParseException (not stack overflow) +template<> +template<> +void object::test<41> +() +{ + std::string geojson; + for (int i = 0; i < 200; i++) geojson += "{\"type\":\"GeometryCollection\",\"geometries\":["; + geojson += "{\"type\":\"Point\",\"coordinates\":[0,0]}"; + for (int i = 0; i < 200; i++) geojson += "]}"; + + try { + geojsonreader.read(geojson); + fail("Expected ParseException for deeply nested GeoJSON"); + } catch (const geos::io::ParseException&) {} +} + } diff --git a/tests/unit/io/WKBReaderTest.cpp b/tests/unit/io/WKBReaderTest.cpp index 6628351a8..0b6b540b6 100644 --- a/tests/unit/io/WKBReaderTest.cpp +++ b/tests/unit/io/WKBReaderTest.cpp @@ -7,11 +7,13 @@ // tut #include +#include // geos #include #include #include #include +#include #include #include #include @@ -838,5 +840,24 @@ void object::test<36> "ParseException: Expected SimpleCurve but got Point"); } + +template<> +template<> +void object::test<37> +() +{ + set_test_name("ParseException on deeply nested WKB collection avoid stack overflow"); + + // Each GeometryCollection header (NDR): byteOrder=01, type=07000000, numGeoms=01000000 + const std::vector header = {0x01, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; + // Innermost empty GeometryCollection: byteOrder=01, type=07000000, numGeoms=00000000 + const std::vector inner = {0x01, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::vector buf; + for (int i = 0; i < 200; i++) buf.insert(buf.end(), header.begin(), header.end()); + buf.insert(buf.end(), inner.begin(), inner.end()); + + ensure_THROW(wkbreader.read(buf.data(), buf.size()), geos::io::ParseException); +} + } // namespace tut diff --git a/tests/unit/io/WKTReaderTest.cpp b/tests/unit/io/WKTReaderTest.cpp index 59e5f1b25..5da2f3806 100644 --- a/tests/unit/io/WKTReaderTest.cpp +++ b/tests/unit/io/WKTReaderTest.cpp @@ -3,6 +3,7 @@ // tut #include +#include // geos #include #include @@ -162,17 +163,7 @@ template<> void object::test<6> () { - try { - wktreader.read("POLYGON( EMPTY, (1 1,2 2,1 2,1 1))"); - fail("Did not get expected exception"); - } - catch(const geos::util::IllegalArgumentException& ex) { - ensure("Got expected exception", true); - (void)(ex.what()); - } - catch(...) { - fail("Got unexpected exception"); - } + ensure_THROW(wktreader.read("POLYGON( EMPTY, (1 1,2 2,1 2,1 1))"), geos::util::IllegalArgumentException); } // POINT(0 0) http://trac.osgeo.org/geos/ticket/610 @@ -473,4 +464,17 @@ void object::test<24> ensure_equals(geom->getNumGeometries(), 3u); } +template<> +template<> +void object::test<26> +() +{ + set_test_name("ParseException on deeply nested WKT collection avoid stack overflow"); + std::string wkt; + for (int i = 0; i < 200; i++) wkt += "GEOMETRYCOLLECTION("; + wkt += "POINT(0 0)"; + for (int i = 0; i < 200; i++) wkt += ")"; + ensure_parseexception(wkt); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: capi/geos_ts_c.cpp | 1 - include/geos/algorithm/distance/DistanceToPoint.h | 3 ++- include/geos/io/GeoJSONReader.h | 1 + include/geos/io/WKBReader.h | 2 ++ include/geos/io/WKTReader.h | 1 + src/io/GeoJSONReader.cpp | 16 ++++++++++++++ src/io/WKBReader.cpp | 9 ++++++++ src/io/WKTReader.cpp | 9 ++++++++ tests/unit/io/GeoJSONReaderTest.cpp | 17 +++++++++++++++ tests/unit/io/WKBReaderTest.cpp | 21 ++++++++++++++++++ tests/unit/io/WKTReaderTest.cpp | 26 +++++++++++++---------- 11 files changed, 93 insertions(+), 13 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Wed May 13 14:37:24 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 13 May 2026 14:37:24 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch 3.14 updated. 47d87bfb2bcf6c427dfe2aa7425ee3f9fc2841c4 Message-ID: <20260513213724.D5D1A1B4F5D@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, 3.14 has been updated via 47d87bfb2bcf6c427dfe2aa7425ee3f9fc2841c4 (commit) via e13c09ac45200f19a02b6e2262d6750b7cbd39a2 (commit) via 0f7182f24feff3e02440000a0f78cbbe41da1b98 (commit) via bf706b77672c87a243d53fc7cb8133564336c0d5 (commit) from 03d50b54fd00017c74d34ba44a4378440a185061 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 47d87bfb2bcf6c427dfe2aa7425ee3f9fc2841c4 Author: Paul Ramsey Date: Wed May 13 13:57:19 2026 -0700 MinGW64 CI build fix diff --git a/include/geos/algorithm/distance/DistanceToPoint.h b/include/geos/algorithm/distance/DistanceToPoint.h index 108b85506..f1889397b 100644 --- a/include/geos/algorithm/distance/DistanceToPoint.h +++ b/include/geos/algorithm/distance/DistanceToPoint.h @@ -19,6 +19,7 @@ #pragma once #include // for composition +#include namespace geos { namespace algorithm { @@ -43,7 +44,7 @@ namespace distance { // geos::algorithm::distance * * Also computes two points which are separated by the distance. */ -class DistanceToPoint { +class GEOS_DLL DistanceToPoint { public: DistanceToPoint() {} commit e13c09ac45200f19a02b6e2262d6750b7cbd39a2 Author: Paul Ramsey Date: Wed May 13 13:46:19 2026 -0700 Fix CI issue for mingw64 diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index 8802cff8f..78724b430 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1824,6 +1824,10 @@ extern "C" { GEOSGridIntersectionFractions_r(GEOSContextHandle_t extHandle, const Geometry* g, double xmin, double ymin, double xmax, double ymax, unsigned nx, unsigned ny, float* buf) { +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" +#endif return execute(extHandle, 0, [&]() { Envelope env(xmin, xmax, ymin, ymax); double dx = env.getWidth() / static_cast(nx); @@ -1839,6 +1843,9 @@ extern "C" { return 1; }); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif } Geometry* commit 0f7182f24feff3e02440000a0f78cbbe41da1b98 Author: Paul Ramsey Date: Wed May 13 12:13:30 2026 -0700 News entry for GH-1437 diff --git a/NEWS.md b/NEWS.md index 08b2802a4..7e7ea284e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,7 @@ - Fix some cases of dropped M values in overlay (GH-1364/GH-1388, Dan Baston) - GEOSClusterDBSCAN fix unassigned clusters with minPoints <= 1 (GH-1386, Dan Baston) - Fix crash in GEOSConvexHull (GH-1358, Dan Baston) + - Guard against stack overflow in inputs (GH-1437, Paul Ramsey) ## Changes in 3.14.1 2025-10-27 commit bf706b77672c87a243d53fc7cb8133564336c0d5 Author: Paul Ramsey Date: Wed May 13 12:11:50 2026 -0700 Limit recursion depth in WKT/WKB/GeoJSON readers Prevents stack overflow on malformed input. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index f4690f04f..8802cff8f 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1109,6 +1109,7 @@ extern "C" { return execute(extHandle, [&]() { auto ids = clusters->getClusterIds(GEOS_CLUSTER_NONE); std::size_t* ids_buf = (size_t*) malloc(ids.size() * sizeof(std::size_t)); + if (!ids_buf) return ids_buf; std::copy(ids.begin(), ids.end(), ids_buf); return ids_buf; }); diff --git a/include/geos/io/GeoJSONReader.h b/include/geos/io/GeoJSONReader.h index 41834f1dd..9d9d09d20 100644 --- a/include/geos/io/GeoJSONReader.h +++ b/include/geos/io/GeoJSONReader.h @@ -75,6 +75,7 @@ public: private: const geom::GeometryFactory& geometryFactory; + mutable int parseDepth_ = 0; std::unique_ptr readFeatureForGeometry(const geos_nlohmann::json& j) const; diff --git a/include/geos/io/WKBReader.h b/include/geos/io/WKBReader.h index 9d410c250..2b8b5beae 100644 --- a/include/geos/io/WKBReader.h +++ b/include/geos/io/WKBReader.h @@ -146,6 +146,8 @@ private: std::array ordValues; + int parseDepth_ = 0; + std::unique_ptr readGeometry(); std::unique_ptr readPoint(); diff --git a/include/geos/io/WKTReader.h b/include/geos/io/WKTReader.h index 7294d64f5..46529c3c8 100644 --- a/include/geos/io/WKTReader.h +++ b/include/geos/io/WKTReader.h @@ -148,6 +148,7 @@ private: const geom::GeometryFactory* geometryFactory; const geom::PrecisionModel* precisionModel; bool fixStructure; + mutable int parseDepth_ = 0; void getPreciseCoordinate(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, geom::CoordinateXYZM&) const; diff --git a/src/io/GeoJSONReader.cpp b/src/io/GeoJSONReader.cpp index 0f0817e32..1ab54dd9e 100644 --- a/src/io/GeoJSONReader.cpp +++ b/src/io/GeoJSONReader.cpp @@ -40,12 +40,15 @@ using json = geos_nlohmann::json; namespace geos { namespace io { // geos.io +static constexpr int MAX_PARSE_DEPTH = 100; + GeoJSONReader::GeoJSONReader(): GeoJSONReader(*(GeometryFactory::getDefaultInstance())) {} GeoJSONReader::GeoJSONReader(const geom::GeometryFactory& gf) : geometryFactory(gf) {} std::unique_ptr GeoJSONReader::read(const std::string& geoJsonText) const { + parseDepth_ = 0; try { const json& j = json::parse(geoJsonText); const std::string& type = j.at("type"); @@ -66,6 +69,7 @@ std::unique_ptr GeoJSONReader::read(const std::string& geoJsonTe GeoJSONFeatureCollection GeoJSONReader::readFeatures(const std::string& geoJsonText) const { + parseDepth_ = 0; try { const json& j = json::parse(geoJsonText); const std::string& type = j.at("type"); @@ -121,6 +125,12 @@ std::map GeoJSONReader::readProperties( GeoJSONValue GeoJSONReader::readProperty( const geos_nlohmann::json& value) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input property exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + if (value.is_string()) { return GeoJSONValue { value.get() }; } @@ -179,6 +189,12 @@ GeoJSONFeatureCollection GeoJSONReader::readFeatureCollection( std::unique_ptr GeoJSONReader::readGeometry( const geos_nlohmann::json& j) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + const std::string& type = j.at("type"); if (type == "Point") { return readPoint(j); diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index 5410ab0c7..d81a88ddc 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -245,13 +245,22 @@ WKBReader::read(std::istream& is) std::unique_ptr WKBReader::read(const unsigned char* buf, size_t size) { + parseDepth_ = 0; dis = ByteOrderDataInStream(buf, size); // will default to machine endian return readGeometry(); } +static constexpr int MAX_PARSE_DEPTH = 100; + std::unique_ptr WKBReader::readGeometry() { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + // determine byte order unsigned char byteOrder = dis.readByte(); diff --git a/src/io/WKTReader.cpp b/src/io/WKTReader.cpp index 2b4400a6b..5fec339bb 100644 --- a/src/io/WKTReader.cpp +++ b/src/io/WKTReader.cpp @@ -49,9 +49,12 @@ using namespace geos::geom; namespace geos { namespace io { // geos.io +static constexpr int MAX_PARSE_DEPTH = 100; + std::unique_ptr WKTReader::read(const std::string& wellKnownText) const { + parseDepth_ = 0; CLocalizer clocale; StringTokenizer tokenizer(wellKnownText); OrdinateSet ordinateFlags = OrdinateSet::createXY(); @@ -285,6 +288,12 @@ WKTReader::getNextWord(StringTokenizer* tokenizer) std::unique_ptr WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, const GeometryTypeId* emptyType) const { + if (parseDepth_ >= MAX_PARSE_DEPTH) { + throw ParseException("Input geometry exceeds nesting depth limit"); + } + ++parseDepth_; + struct DepthGuard { int& d; ~DepthGuard() { --d; } } guard{parseDepth_}; + std::string type = getNextWord(tokenizer); std::unique_ptr geom; diff --git a/tests/unit/io/GeoJSONReaderTest.cpp b/tests/unit/io/GeoJSONReaderTest.cpp index 2d474d3d7..c419eaf55 100644 --- a/tests/unit/io/GeoJSONReaderTest.cpp +++ b/tests/unit/io/GeoJSONReaderTest.cpp @@ -619,4 +619,21 @@ void object::test<40> ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); } +// Deeply nested GeometryCollection should throw ParseException (not stack overflow) +template<> +template<> +void object::test<41> +() +{ + std::string geojson; + for (int i = 0; i < 200; i++) geojson += "{\"type\":\"GeometryCollection\",\"geometries\":["; + geojson += "{\"type\":\"Point\",\"coordinates\":[0,0]}"; + for (int i = 0; i < 200; i++) geojson += "]}"; + + try { + geojsonreader.read(geojson); + fail("Expected ParseException for deeply nested GeoJSON"); + } catch (const geos::io::ParseException&) {} +} + } diff --git a/tests/unit/io/WKBReaderTest.cpp b/tests/unit/io/WKBReaderTest.cpp index 6628351a8..ebdba7a40 100644 --- a/tests/unit/io/WKBReaderTest.cpp +++ b/tests/unit/io/WKBReaderTest.cpp @@ -7,11 +7,13 @@ // tut #include +#include // geos #include #include #include #include +#include #include #include #include @@ -838,5 +840,29 @@ void object::test<36> "ParseException: Expected SimpleCurve but got Point"); } + +template<> +template<> +void object::test<37> +() +{ + set_test_name("ParseException on deeply nested WKB collection avoid stack overflow"); + + // Each GeometryCollection header (NDR): byteOrder=01, type=07000000, numGeoms=01000000 + const std::vector header = {0x01, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; + // Innermost empty GeometryCollection: byteOrder=01, type=07000000, numGeoms=00000000 + const std::vector inner = {0x01, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::vector buf; + for (int i = 0; i < 200; i++) buf.insert(buf.end(), header.begin(), header.end()); + buf.insert(buf.end(), inner.begin(), inner.end()); + + ensure_THROW(wkbreader.read(buf.data(), buf.size()), geos::io::ParseException); + + // try { + // wkbreader.read(buf.data(), buf.size()); + // fail("Expected ParseException for deeply nested WKB"); + // } catch (const geos::util::GEOSException&) {} +} + } // namespace tut diff --git a/tests/unit/io/WKTReaderTest.cpp b/tests/unit/io/WKTReaderTest.cpp index af9621916..2a28e805b 100644 --- a/tests/unit/io/WKTReaderTest.cpp +++ b/tests/unit/io/WKTReaderTest.cpp @@ -3,6 +3,7 @@ // tut #include +#include // geos #include #include @@ -162,17 +163,7 @@ template<> void object::test<6> () { - try { - wktreader.read("POLYGON( EMPTY, (1 1,2 2,1 2,1 1))"); - fail("Did not get expected exception"); - } - catch(const geos::util::IllegalArgumentException& ex) { - ensure("Got expected exception", true); - (void)(ex.what()); - } - catch(...) { - fail("Got unexpected exception"); - } + ensure_THROW(wktreader.read("POLYGON( EMPTY, (1 1,2 2,1 2,1 1))"), geos::util::IllegalArgumentException); } // POINT(0 0) http://trac.osgeo.org/geos/ticket/610 @@ -499,4 +490,17 @@ void object::test<25> ensure_parseexception("POLYGON Z M EMPTY"); } +template<> +template<> +void object::test<26> +() +{ + set_test_name("ParseException on deeply nested WKT collection avoid stack overflow"); + std::string wkt; + for (int i = 0; i < 200; i++) wkt += "GEOMETRYCOLLECTION("; + wkt += "POINT(0 0)"; + for (int i = 0; i < 200; i++) wkt += ")"; + ensure_parseexception(wkt); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + capi/geos_ts_c.cpp | 8 +++++++ include/geos/algorithm/distance/DistanceToPoint.h | 3 ++- include/geos/io/GeoJSONReader.h | 1 + include/geos/io/WKBReader.h | 2 ++ include/geos/io/WKTReader.h | 1 + src/io/GeoJSONReader.cpp | 16 ++++++++++++++ src/io/WKBReader.cpp | 9 ++++++++ src/io/WKTReader.cpp | 9 ++++++++ tests/unit/io/GeoJSONReaderTest.cpp | 17 +++++++++++++++ tests/unit/io/WKBReaderTest.cpp | 26 +++++++++++++++++++++++ tests/unit/io/WKTReaderTest.cpp | 26 +++++++++++++---------- 12 files changed, 107 insertions(+), 12 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Wed May 13 15:25:01 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 13 May 2026 15:25:01 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch 3.14 updated. a7a985eca203a98c19b36641ac29bdbac1c9db31 Message-ID: <20260513222501.67E681B51F1@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, 3.14 has been updated via a7a985eca203a98c19b36641ac29bdbac1c9db31 (commit) via b008825ac9de9001919a189e1a2d9a6ec6dfa768 (commit) from 47d87bfb2bcf6c427dfe2aa7425ee3f9fc2841c4 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit a7a985eca203a98c19b36641ac29bdbac1c9db31 Author: Paul Ramsey Date: Wed May 13 15:24:21 2026 -0700 News item for WKB overflow risk diff --git a/NEWS.md b/NEWS.md index 7e7ea284e..a60c7ad58 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ - GEOSClusterDBSCAN fix unassigned clusters with minPoints <= 1 (GH-1386, Dan Baston) - Fix crash in GEOSConvexHull (GH-1358, Dan Baston) - Guard against stack overflow in inputs (GH-1437, Paul Ramsey) + - Avoid overflow risk in WKB reader (Paul Ramsey) ## Changes in 3.14.1 2025-10-27 commit b008825ac9de9001919a189e1a2d9a6ec6dfa768 Author: Paul Ramsey Date: Wed May 13 22:00:16 2026 +0000 WKBReader: use division in minMemSize() to avoid overflow risk diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index d81a88ddc..442d28506 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -187,7 +187,6 @@ WKBReader::readHEX(std::istream& is) void WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const { - uint64_t minSize = 0; constexpr uint64_t minCoordSize = 2 * sizeof(double); constexpr uint64_t minPtSize = (1+4) + minCoordSize; constexpr uint64_t minLineSize = (1+4+4); // empty line @@ -195,35 +194,36 @@ WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const constexpr uint64_t minPolySize = (1+4+4); // empty polygon constexpr uint64_t minGeomSize = minLineSize; + uint64_t perElement = 0; switch(geomType) { case GEOS_LINESTRING: case GEOS_LINEARRING: case GEOS_CIRCULARSTRING: case GEOS_COMPOUNDCURVE: case GEOS_POINT: - minSize = size * minCoordSize; + perElement = minCoordSize; break; case GEOS_POLYGON: case GEOS_CURVEPOLYGON: - minSize = size * minRingSize; + perElement = minRingSize; break; case GEOS_MULTIPOINT: - minSize = size * minPtSize; + perElement = minPtSize; break; case GEOS_MULTILINESTRING: case GEOS_MULTICURVE: - minSize = size * minLineSize; + perElement = minLineSize; break; case GEOS_MULTIPOLYGON: case GEOS_MULTISURFACE: - minSize = size * minPolySize; + perElement = minPolySize; break; case GEOS_GEOMETRYCOLLECTION: - minSize = size * minGeomSize; + perElement = minGeomSize; break; } - if (dis.size() < minSize) { + if (perElement > 0 && size > dis.size() / perElement) { throw ParseException("Input buffer is smaller than requested object size"); } } ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + src/io/WKBReader.cpp | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Wed May 13 15:25:32 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 13 May 2026 15:25:32 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch 3.13 updated. b61d1f844667f7649bfa696b5319df7e03df9237 Message-ID: <20260513222532.656E61B51F9@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, 3.13 has been updated via b61d1f844667f7649bfa696b5319df7e03df9237 (commit) from c3a9f82726a5c90663f79272152194f1d6a43105 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit b61d1f844667f7649bfa696b5319df7e03df9237 Author: Paul Ramsey Date: Wed May 13 15:25:05 2026 -0700 News for GH-1437 diff --git a/NEWS.md b/NEWS.md index cfb667dee..4a365a95a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ - Fix incorrect envelope calculation for arcs (GH-1314, Dan Baston) - Quiet FP_DIVBYZERO exception from CGAlgorithmsDD::intersection (GH-1235, Paul Ramsey) - Avoid crash on buffer of geometry with only invalid coordinates (GH-1335, Dan Baston) + - Guard against stack overflow in inputs (GH-1437, Paul Ramsey) ## Changes in 3.13.1 2025-03-03 ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + 1 file changed, 1 insertion(+) hooks/post-receive -- GEOS From git at osgeo.org Wed May 13 15:26:34 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 13 May 2026 15:26:34 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch 3.13 updated. 98da0fad0b9abff91349cf71d19196c5d48e258a Message-ID: <20260513222634.79E4A1B51FD@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, 3.13 has been updated via 98da0fad0b9abff91349cf71d19196c5d48e258a (commit) via 663e9b48b606b5e9d274fdbb41e4be2cbcc1912e (commit) from b61d1f844667f7649bfa696b5319df7e03df9237 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 98da0fad0b9abff91349cf71d19196c5d48e258a Author: Paul Ramsey Date: Wed May 13 15:26:02 2026 -0700 News entry for WKB overflow diff --git a/NEWS.md b/NEWS.md index 4a365a95a..be94fcc4d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,7 @@ - Quiet FP_DIVBYZERO exception from CGAlgorithmsDD::intersection (GH-1235, Paul Ramsey) - Avoid crash on buffer of geometry with only invalid coordinates (GH-1335, Dan Baston) - Guard against stack overflow in inputs (GH-1437, Paul Ramsey) + - Avoid overflow risk in WKB reader (Paul Ramsey) ## Changes in 3.13.1 2025-03-03 commit 663e9b48b606b5e9d274fdbb41e4be2cbcc1912e Author: Paul Ramsey Date: Wed May 13 22:00:16 2026 +0000 WKBReader: use division in minMemSize() to avoid overflow risk diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index d81a88ddc..442d28506 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -187,7 +187,6 @@ WKBReader::readHEX(std::istream& is) void WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const { - uint64_t minSize = 0; constexpr uint64_t minCoordSize = 2 * sizeof(double); constexpr uint64_t minPtSize = (1+4) + minCoordSize; constexpr uint64_t minLineSize = (1+4+4); // empty line @@ -195,35 +194,36 @@ WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const constexpr uint64_t minPolySize = (1+4+4); // empty polygon constexpr uint64_t minGeomSize = minLineSize; + uint64_t perElement = 0; switch(geomType) { case GEOS_LINESTRING: case GEOS_LINEARRING: case GEOS_CIRCULARSTRING: case GEOS_COMPOUNDCURVE: case GEOS_POINT: - minSize = size * minCoordSize; + perElement = minCoordSize; break; case GEOS_POLYGON: case GEOS_CURVEPOLYGON: - minSize = size * minRingSize; + perElement = minRingSize; break; case GEOS_MULTIPOINT: - minSize = size * minPtSize; + perElement = minPtSize; break; case GEOS_MULTILINESTRING: case GEOS_MULTICURVE: - minSize = size * minLineSize; + perElement = minLineSize; break; case GEOS_MULTIPOLYGON: case GEOS_MULTISURFACE: - minSize = size * minPolySize; + perElement = minPolySize; break; case GEOS_GEOMETRYCOLLECTION: - minSize = size * minGeomSize; + perElement = minGeomSize; break; } - if (dis.size() < minSize) { + if (perElement > 0 && size > dis.size() / perElement) { throw ParseException("Input buffer is smaller than requested object size"); } } ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + src/io/WKBReader.cpp | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Fri May 15 11:49:27 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 15 May 2026 11:49:27 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 87af72224bf9f066353d2215e2e3dea50fba8cb1 Message-ID: <20260515184931.9EF8231C0C@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 87af72224bf9f066353d2215e2e3dea50fba8cb1 (commit) via dba47b90ed061f7d2d9584909f6e54f5dbdede26 (commit) from 8b8b3da7a3d9fb8953ff60bc49aa0320d51ae45c (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 87af72224bf9f066353d2215e2e3dea50fba8cb1 Author: Paul Ramsey Date: Wed May 13 22:00:52 2026 +0000 Test for use division in minMemSize() diff --git a/tests/unit/io/WKBReaderTest.cpp b/tests/unit/io/WKBReaderTest.cpp index ebdba7a40..b4024dee1 100644 --- a/tests/unit/io/WKBReaderTest.cpp +++ b/tests/unit/io/WKBReaderTest.cpp @@ -857,11 +857,27 @@ void object::test<37> buf.insert(buf.end(), inner.begin(), inner.end()); ensure_THROW(wkbreader.read(buf.data(), buf.size()), geos::io::ParseException); +} - // try { - // wkbreader.read(buf.data(), buf.size()); - // fail("Expected ParseException for deeply nested WKB"); - // } catch (const geos::util::GEOSException&) {} +// Claimed element count larger than buffer can hold should throw ParseException +template<> +template<> +void object::test<38> +() +{ + set_test_name("ParseException when WKB element count exceeds buffer capacity"); + + // NDR GeometryCollection with numGeoms = 0x00FFFFFF in a 20-byte buffer + // Header: byteOrder=01, type=07000000, numGeoms=FFFFFF00 (little-endian 0x00FFFFFF) + std::vector buf = { + 0x01, // NDR byte order + 0x07, 0x00, 0x00, 0x00, // type: GeometryCollection + 0xFF, 0xFF, 0xFF, 0x00, // numGeoms: 0x00FFFFFF (little-endian) + 0x00, 0x00, 0x00, 0x00, 0x00, // padding to make buffer 20 bytes + 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + ensure_THROW(wkbreader.read(buf.data(), buf.size()), geos::io::ParseException); } } // namespace tut commit dba47b90ed061f7d2d9584909f6e54f5dbdede26 Author: Paul Ramsey Date: Wed May 13 22:00:16 2026 +0000 WKBReader: use division in minMemSize() to avoid overflow risk diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index d5683f2dc..b366d91b9 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -194,7 +194,6 @@ WKBReader::readHEX(const std::string& hex) void WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const { - uint64_t minSize = 0; constexpr uint64_t minCoordSize = 2 * sizeof(double); constexpr uint64_t minPtSize = (1+4) + minCoordSize; constexpr uint64_t minLineSize = (1+4+4); // empty line @@ -202,35 +201,36 @@ WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const constexpr uint64_t minPolySize = (1+4+4); // empty polygon constexpr uint64_t minGeomSize = minLineSize; + uint64_t perElement = 0; switch(geomType) { case GEOS_LINESTRING: case GEOS_LINEARRING: case GEOS_CIRCULARSTRING: case GEOS_COMPOUNDCURVE: case GEOS_POINT: - minSize = size * minCoordSize; + perElement = minCoordSize; break; case GEOS_POLYGON: case GEOS_CURVEPOLYGON: - minSize = size * minRingSize; + perElement = minRingSize; break; case GEOS_MULTIPOINT: - minSize = size * minPtSize; + perElement = minPtSize; break; case GEOS_MULTILINESTRING: case GEOS_MULTICURVE: - minSize = size * minLineSize; + perElement = minLineSize; break; case GEOS_MULTIPOLYGON: case GEOS_MULTISURFACE: - minSize = size * minPolySize; + perElement = minPolySize; break; case GEOS_GEOMETRYCOLLECTION: - minSize = size * minGeomSize; + perElement = minGeomSize; break; } - if (dis.size() < minSize) { + if (perElement > 0 && size > dis.size() / perElement) { throw ParseException("Input buffer is smaller than requested object size"); } } ----------------------------------------------------------------------- Summary of changes: src/io/WKBReader.cpp | 16 ++++++++-------- tests/unit/io/WKBReaderTest.cpp | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) hooks/post-receive -- GEOS