Repository: shapely/shapely (language: python)

Issue:
Protect against Abort for wkt output with very large floats
Closes #1903 


Discussion / hints:
Performance doesn't seem to suffer significantly:

this PR: 
```
[62.50%] ··· benchmarks.IOSuite.time_read_from_wkb                               144±2ms
[75.00%] ··· benchmarks.IOSuite.time_read_from_wkt                               936±2ms
[87.50%] ··· benchmarks.IOSuite.time_write_to_wkb                               88.5±2ms
[100.00%] ··· benchmarks.IOSuite.time_write_to_wkt                              449±0.8ms
```

vs. main:

```
[12.50%] ··· Running (benchmarks.IOSuite.time_read_from_wkb--)....
[62.50%] ··· benchmarks.IOSuite.time_read_from_wkb                             143±0.9ms
[75.00%] ··· benchmarks.IOSuite.time_read_from_wkt                               910±1ms
[87.50%] ··· benchmarks.IOSuite.time_write_to_wkb                               87.5±1ms
[100.00%] ··· benchmarks.IOSuite.time_write_to_wkt                              445±0.9ms
```
BTW, I just confirmed that the same issue is there for the Z coordinate, and we dont have a GetZMax GEOS function...
Seems we would need a lower threshold for Windows ..
> Do we want to include it for 2.0.2? 

Don’t think we should rush this for 2.0.2. It’s been there probably for a long time and it only just came up. I also think we should fix this for the Z coordinate.

> Seems we would need a lower threshold for Windows ..

I guess we need to approach the 128-size buffer mentioned in the GEOS issue theoretically.
The additional check on the Z coordinate does give ~ 7% performance regression:

```
· Discovering benchmarks
· Running 4 total benchmarks (1 commits * 1 environments * 4 benchmarks)
[ 0.00%] ·· Benchmarking existing-py_home_casper_code_shapely_.venv_bin_python
[12.50%] ··· Running (benchmarks.IOSuite.time_read_from_wkb--)....
[62.50%] ··· benchmarks.IOSuite.time_read_from_wkb                                                                                                              144±0.8ms
[75.00%] ··· benchmarks.IOSuite.time_read_from_wkt                                                                                                                912±3ms
[87.50%] ··· benchmarks.IOSuite.time_write_to_wkb                                                                                                                87.8±1ms
[100.00%] ··· benchmarks.IOSuite.time_write_to_wkt                                                                                                               475±0.9ms
```
> The additional check on the Z coordinate does give ~ 7% performance regression:

Should there somewhere be a shortcut for not looping over the whole coordseq if the geometry actually has no z coordinates? (in that case GEOSCoordSeq_getZ_r just always gives NaNs?)
> > The additional check on the Z coordinate does give ~ 7% performance regression:
> 
> Should there somewhere be a shortcut for not looping over the whole coordseq if the geometry actually has no z coordinates? (in that case GEOSCoordSeq_getZ_r just always gives NaNs?)

The reason I do this for all geometries, is that `GetCoordinateDimension` will return `2` if the first Z coordinate is a NaN - but then other coordinates could still contain out of bound coordinates resulting in a crash.
But we're lucky: GEOS 3.11 *also* does the same [link](https://github.com/libgeos/geos/blob/3.11/src/io/WKTWriter.cpp#L193) to decide whether to write Z coordinates in WKT.

GEOS 3.12 does `HasZ` when *not* removing empty dimensions (https://github.com/libgeos/geos/blob/35c10bfbc79a1af42dc427072a4a3f663288db33/src/io/WKTWriter.cpp#L217C39-L217C43) which is the default, which boils down to the same behaviour as GEOS 3.11.

So I'll add the `HasZ` check and some tests to verify
Performance regression is down to an acceptable levels again:

```
[62.50%] ··· benchmarks.IOSuite.time_read_from_wkb             144±0.7ms
[75.00%] ··· benchmarks.IOSuite.time_read_from_wkt               912±3ms
[87.50%] ··· benchmarks.IOSuite.time_write_to_wkb               88.2±1ms
[100.00%] ··· benchmarks.IOSuite.time_write_to_wkt              452±0.6ms
```

Fix patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index e149aa604..cb4a4206c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -6,7 +6,7 @@ Changes
 
 Bug fixes:
 
-- Prevent crash when serializing a number > 1e100 to WKT. (#1907)
+- Prevent crash when serializing a number > 1e100 to WKT with GEOS < 3.13. (#1907)
 
 Improvements:
 
diff --git a/src/geos.c b/src/geos.c
index 488154e71..c7db029f9 100644
--- a/src/geos.c
+++ b/src/geos.c
@@ -402,6 +402,7 @@ char get_zmax_collection(GEOSContextHandle_t ctx, const GEOSGeometry* geom,
   return 1;
 }
 
+#if !GEOS_SINCE_3_13_0
 char check_to_wkt_trim_compatible(GEOSContextHandle_t ctx, const GEOSGeometry* geom,
                                   int dimension) {
   char is_empty;
@@ -431,6 +432,7 @@ char check_to_wkt_trim_compatible(GEOSContextHandle_t ctx, const GEOSGeometry* g
 
   return PGERR_SUCCESS;
 }
+#endif  // !GEOS_SINCE_3_13_0
 
 #if GEOS_SINCE_3_9_0
 
diff --git a/src/geos.h b/src/geos.h
index fec584ac7..26367a1c8 100644
--- a/src/geos.h
+++ b/src/geos.h
@@ -91,7 +91,7 @@ enum ShapelyErrorCode {
                       "WKT output of multipoints with an empty point is unsupported on " \
                       "this version of GEOS.");                                          \
       break;                                                                             \
-    case PGERR_COORD_OUT_OF_BOUNDS:                                                      \
+    case PGERR_COORD_OUT_OF_BOUNDS:  /* applies to GEOS <3.13.0 with trim enabled  */    \
       PyErr_SetString(PyExc_ValueError,                                                  \
                       "WKT output of coordinates greater than 1E+100 is unsupported on " \
                       "this version of GEOS.");                                          \
@@ -163,6 +163,7 @@ enum ShapelyErrorCode {
 #define GEOS_SINCE_3_10_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 10))
 #define GEOS_SINCE_3_11_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 11))
 #define GEOS_SINCE_3_12_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 12))
+#define GEOS_SINCE_3_13_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 13))
 
 extern void* geos_context[1];
 extern PyObject* geos_exception[1];
@@ -173,7 +174,9 @@ extern char has_point_empty(GEOSContextHandle_t ctx, GEOSGeometry* geom);
 extern GEOSGeometry* point_empty_to_nan_all_geoms(GEOSContextHandle_t ctx,
                                                   GEOSGeometry* geom);
 extern char check_to_wkt_compatible(GEOSContextHandle_t ctx, GEOSGeometry* geom);
+#if !GEOS_SINCE_3_13_0
 extern char check_to_wkt_trim_compatible(GEOSContextHandle_t ctx, const GEOSGeometry* geom, int dimension);
+#endif  // !GEOS_SINCE_3_13_0
 #if GEOS_SINCE_3_9_0
 extern char wkt_empty_3d_geometry(GEOSContextHandle_t ctx, GEOSGeometry* geom,
                                   char** wkt);
diff --git a/src/pygeom.c b/src/pygeom.c
index 2c058c0d0..2af80e20a 100644
--- a/src/pygeom.c
+++ b/src/pygeom.c
@@ -81,12 +81,14 @@ static PyObject* GeometryObject_ToWKT(GeometryObject* obj) {
   }
 
   GEOS_INIT;
+#if !GEOS_SINCE_3_13_0
   if (trim) {
     errstate = check_to_wkt_trim_compatible(ctx, geom, dimension);
   }
   if (errstate != PGERR_SUCCESS) {
     goto finish;
   }
+#endif  // !GEOS_SINCE_3_13_0
 
 #if GEOS_SINCE_3_9_0
   errstate = wkt_empty_3d_geometry(ctx, geom, &wkt);
diff --git a/src/ufuncs.c b/src/ufuncs.c
index e0aa6272b..ed83e9619 100644
--- a/src/ufuncs.c
+++ b/src/ufuncs.c
@@ -3275,12 +3275,14 @@ static void to_wkt_func(char** args, const npy_intp* dimensions, const npy_intp*
       Py_INCREF(Py_None);
       *out = Py_None;
     } else {
+#if !GEOS_SINCE_3_13_0
       if (trim) {
         errstate = check_to_wkt_trim_compatible(ctx, in1, dimension);
         if (errstate != PGERR_SUCCESS) {
           goto finish;
         }
       }
+#endif  // !GEOS_SINCE_3_13_0
 #if GEOS_SINCE_3_9_0
       errstate = wkt_empty_3d_geometry(ctx, in1, &wkt);
       if (errstate != PGERR_SUCCESS) {


Test patch:
diff --git a/shapely/tests/geometry/test_format.py b/shapely/tests/geometry/test_format.py
index 689f2e0ba..0bbbe1125 100644
--- a/shapely/tests/geometry/test_format.py
+++ b/shapely/tests/geometry/test_format.py
@@ -93,7 +93,12 @@ def test_format_polygon():
     )
 
     # 'g' format varies depending on GEOS version
-    if geos_version < (3, 10, 0):
+    if geos_version >= (3, 13, 0):
+        expected_2G = (
+            "POLYGON ((10 0, 7.07 -7.07, 6.12E-16 -10, -7.07 -7.07, "
+            "-10 -1.22E-15, -7.07 7.07, -1.84E-15 10, 7.07 7.07, 10 0))"
+        )
+    elif geos_version < (3, 10, 0):
         expected_2G = (
             "POLYGON ((10 0, 7.1 -7.1, 1.6E-14 -10, -7.1 -7.1, "
             "-10 -3.2E-14, -7.1 7.1, -4.6E-14 10, 7.1 7.1, 10 0))"
diff --git a/shapely/tests/test_io.py b/shapely/tests/test_io.py
index f49d20572..21a750bd8 100644
--- a/shapely/tests/test_io.py
+++ b/shapely/tests/test_io.py
@@ -396,11 +396,17 @@ def test_to_wkt_large_float_ok(geom):
 
 
 @pytest.mark.parametrize("geom", [Point(1e101, 0), Point(0, 1e101)])
-def test_to_wkt_large_float_err(geom):
-    # https://github.com/shapely/shapely/issues/1903
-    with pytest.raises(ValueError, match="WKT output of coordinates greater than.*"):
-        shapely.to_wkt(geom)
-    assert "Exception in WKT writer" in repr(geom)
+def test_to_wkt_large_float(geom):
+    if shapely.geos_version >= (3, 13, 0):
+        # round-trip WKT
+        assert geom.equals(shapely.from_wkt(shapely.to_wkt(geom)))
+    else:
+        # https://github.com/shapely/shapely/issues/1903
+        with pytest.raises(
+            ValueError, match="WKT output of coordinates greater than.*"
+        ):
+            shapely.to_wkt(geom)
+        assert "Exception in WKT writer" in repr(geom)
 
 
 @pytest.mark.parametrize(