diff --git a/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java b/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java index 9685c5869..ad0e07744 100644 --- a/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java +++ b/library/src/main/java/com/google/maps/android/data/geojson/GeoJsonParser.java @@ -177,7 +177,18 @@ private static LatLngBounds parseBoundingBox(JSONArray coordinates) throws JSONE * @param geoJsonGeometry geometry object to parse * @return Geometry object */ + /** Maximum nesting depth for GeometryCollection to prevent stack overflow. */ + private static final int MAX_GEOMETRY_DEPTH = 20; + public static Geometry parseGeometry(JSONObject geoJsonGeometry) { + return parseGeometry(geoJsonGeometry, MAX_GEOMETRY_DEPTH); + } + + public static Geometry parseGeometry(JSONObject geoJsonGeometry, int maxDepth) { + if (maxDepth < 0) { + Log.w(LOG_TAG, "GeoJSON geometry nesting depth limit exhausted, ignoring."); + return null; + } try { String geometryType = geoJsonGeometry.getString("type"); JSONArray geometryArray; @@ -190,7 +201,7 @@ public static Geometry parseGeometry(JSONObject geoJsonGeometry) { // No geometries or coordinates array return null; } - return createGeometry(geometryType, geometryArray); + return createGeometry(geometryType, geometryArray, maxDepth); } catch (JSONException e) { return null; } @@ -239,7 +250,7 @@ private static HashMap parseProperties(JSONObject properties) * @return Geometry object * @throws JSONException if the coordinates or geometries could be parsed */ - private static Geometry createGeometry(String geometryType, JSONArray geometryArray) + private static Geometry createGeometry(String geometryType, JSONArray geometryArray, int maxDepth) throws JSONException { switch (geometryType) { case POINT: @@ -255,7 +266,7 @@ private static Geometry createGeometry(String geometryType, JSONArray geometryAr case MULTIPOLYGON: return createMultiPolygon(geometryArray); case GEOMETRY_COLLECTION: - return createGeometryCollection(geometryArray); + return createGeometryCollection(geometryArray, maxDepth - 1); } return null; } @@ -360,13 +371,13 @@ private static GeoJsonMultiPolygon createMultiPolygon(JSONArray coordinates) * @return GeoJsonGeometryCollection object * @throws JSONException if geometries cannot be parsed */ - private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries) + private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries, int maxDepth) throws JSONException { ArrayList geometryCollectionElements = new ArrayList<>(); for (int i = 0; i < geometries.length(); i++) { JSONObject geometryElement = geometries.getJSONObject(i); - Geometry geometry = parseGeometry(geometryElement); + Geometry geometry = parseGeometry(geometryElement, maxDepth); if (geometry != null) { // Do not add geometries that could not be parsed geometryCollectionElements.add(geometry); diff --git a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java index a4ada21e3..3529beebe 100644 --- a/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java +++ b/library/src/test/java/com/google/maps/android/data/geojson/GeoJsonParserTest.java @@ -386,4 +386,65 @@ public void testInvalidGeometry() throws Exception { parser = new GeoJsonParser(invalidGeometryInvalidCoordinatesString()); assertEquals(0, parser.getFeatures().size()); } + + // --------------------------------------------------------------- + // Tests for fix: StackOverflowError on deeply nested GeometryCollection + // PR #1699 + // --------------------------------------------------------------- + + private static JSONObject buildNestedGeometryCollection(int depth) throws Exception { + JSONObject innermost = new JSONObject(); + innermost.put("type", "Point"); + innermost.put("coordinates", new org.json.JSONArray("[0, 0]")); + + JSONObject current = innermost; + for (int i = 0; i < depth; i++) { + JSONObject wrapper = new JSONObject(); + wrapper.put("type", "GeometryCollection"); + wrapper.put("geometries", new org.json.JSONArray().put(current)); + current = wrapper; + } + return current; + } + + @Test + public void testDeeplyNestedGeometryCollection_doesNotThrowStackOverflow() throws Exception { + JSONObject deeplyNested = buildNestedGeometryCollection(2000); + try { + GeoJsonParser.parseGeometry(deeplyNested); + } catch (StackOverflowError e) { + throw new AssertionError( + "StackOverflowError thrown for deeply nested GeometryCollection — fix did not work!", e); + } + } + + + @Test + public void testGeometryBeyondMaxDepth_returnsNull() throws Exception { + JSONObject point = new JSONObject(); + point.put("type", "Point"); + point.put("coordinates", new org.json.JSONArray("[0, 0]")); + Geometry result = GeoJsonParser.parseGeometry(point, -1); + assertNull("Geometry exceeding max depth (countdown < 0) should be null", result); + } + + @Test + public void testCustomMaxDepth_respectsLimit() throws Exception { + JSONObject point = new JSONObject(); + point.put("type", "Point"); + point.put("coordinates", new org.json.JSONArray("[0, 0]")); + Geometry valid = GeoJsonParser.parseGeometry(point, 0); + assertNotNull("Geometry at maxDepth 0 should not be null", valid); + Geometry invalid = GeoJsonParser.parseGeometry(point, -1); + assertNull("Geometry at maxDepth -1 should be null", invalid); + } + + @Test + public void testShallowNestedGeometryCollection_parsedCorrectly() throws Exception { + JSONObject shallow = buildNestedGeometryCollection(3); + Geometry result = GeoJsonParser.parseGeometry(shallow); + assertNotNull("GeometryCollection with normal nesting should not be null", result); + assertTrue("Geometry type must be GeoJsonGeometryCollection", + result instanceof GeoJsonGeometryCollection); + } }