Repository: rasterio/rasterio

Issue:
Rasterize segfaults when passing a shapely GeometryCollection
## Steps to reproduce the problem.

```python
import rasterio.features
import shapely.geometry

collection = shapely.geometry.GeometryCollection([])
rasterio.features.rasterize([(collection, 1)], out_shape=[1,1])
```

(same with non-empty / non-trivial collections)

## Operating system

Windows 10

## Rasterio version and provenance

> rasterio                  0.36.0                   py27_0    conda-forge



Discussion:
I have confirmed this is a bug, still present on rasterio 1.0a10.  (OSX).

Nice catch @dionhaefner
Also getting segfaults if we try to rasterize a GeoJSON `Feature` or `FeatureCollection`.

Apparently the checks we have in place are not sufficiently differentiating between acceptable inputs, and inputs that will cause segfaults when we turn around and call GDAL.
Question: is GeoJSON valid if there are no coordinates for a given geometry type?

E.g., an empty Point seems invalid to me, at least for purposes of how we are using it here.

Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -239,16 +239,15 @@ def format_cast_error(param, dtype):
             value = default_value
         geom = getattr(geom, '__geo_interface__', None) or geom
 
-        # not isinstance(geom, dict) or
-        if 'type' in geom or 'coordinates' in geom:
-            valid_shapes.append((geom, value))
-            shape_values.append(value)
-
-        else:
+        # geom must be a valid GeoJSON geometry type and non-empty
+        if not is_valid_geom(geom):
             raise ValueError(
                 'Invalid geometry object at index {0}'.format(index)
             )
 
+        valid_shapes.append((geom, value))
+        shape_values.append(value)
+
     if not valid_shapes:
         raise ValueError('No valid geometry objects found for rasterize')
 
@@ -368,4 +367,87 @@ def geometry_window(raster, shapes, pad_x=0, pad_y=0, north_up=True,
     # This will raise a WindowError if windows do not overlap
     window = window.intersection(raster_window)
 
-    return window
\ No newline at end of file
+    return window
+
+
+def is_valid_geom(geom):
+    """
+    Checks to see if geometry is a valid GeoJSON geometry type or
+    GeometryCollection.
+
+    Geometries must be non-empty, and have at least x, y coordinates.
+
+    Note: only the first coordinate is checked for validity.
+    
+    Parameters
+    ----------
+    geom: an object that implements the geo interface or GeoJSON-like object
+
+    Returns
+    -------
+    bool: True if object is a valid GeoJSON geometry type
+    """
+    
+    geom_types = {'Point', 'MultiPoint', 'LineString', 'MultiLineString',
+                  'Polygon', 'MultiPolygon'}
+
+    if 'type' not in geom:
+        return False
+
+    try:
+        geom_type = geom['type']
+        if geom_type not in geom_types.union({'GeometryCollection'}):
+            return False
+
+    except TypeError:
+        return False
+
+    if geom_type in geom_types:
+        if 'coordinates' not in geom:
+            return False
+
+        coords = geom['coordinates']
+
+        if geom_type == 'Point':
+            # Points must have at least x, y
+            return len(coords) >= 2
+
+        if geom_type == 'MultiPoint':
+            # Multi points must have at least one point with at least x, y
+            return len(coords) > 0 and len(coords[0]) >= 2
+
+        if geom_type == 'LineString':
+            # Lines must have at least 2 coordinates and at least x, y for
+            # a coordinate
+            return len(coords) >= 2 and len(coords[0]) >= 2
+
+        if geom_type == 'MultiLineString':
+            # Multi lines must have at least one LineString
+            return (len(coords) > 0 and len(coords[0]) >= 2 and
+                    len(coords[0][0]) >=2)
+
+        if geom_type == 'Polygon':
+            # Polygons must have at least 1 ring, with at least 4 coordinates,
+            # with at least x, y for a coordinate
+            return (len(coords) > 0 and len(coords[0]) >= 4 and
+                    len(coords[0][0]) >=2)
+
+        if geom_type == 'MultiPolygon':
+            # Muti polygons must have at least one Polygon
+            return (len(coords) > 0 and len(coords[0]) > 0 and
+                    len(coords[0][0]) >= 4 and len(coords[0][0][0]) >=2)
+
+    if geom_type == 'GeometryCollection':
+        if not 'geometries' in geom:
+            return False
+
+        if not len(geom['geometries']) > 0:
+            # While technically valid according to GeoJSON spec, an empty
+            # GeometryCollection will cause issues if used in rasterio
+            return False
+
+        for g in geom['geometries']:
+            if not is_valid_geom(g):
+                return False  # short-circuit and fail early
+
+    return True


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -66,6 +66,128 @@ def basic_geometry():
     }
 
 
+@pytest.fixture
+def geojson_point():
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style Point geometry object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return {
+        'type': 'Point',
+        'coordinates': (2, 2)
+    }
+
+
+@pytest.fixture
+def geojson_multipoint():
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style MultiPoint geometry object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return {
+        'type': 'MultiPoint',
+        'coordinates': ((2, 2), (4, 4))
+    }
+
+
+@pytest.fixture
+def geojson_line():
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style LineString geometry object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return {
+        'type': 'LineString',
+        'coordinates': ((2, 2), (4, 4))
+    }
+
+
+@pytest.fixture
+def geojson_multiline():
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style MultiLineString geometry object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return {
+        'type': 'MultiLineString',
+        'coordinates': (((2, 2), (4, 4)), ((0, 0), (4, 0)))
+    }
+
+
+@pytest.fixture
+def geojson_polygon(basic_geometry):
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style Polygon geometry object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return basic_geometry
+
+
+@pytest.fixture
+def geojson_multipolygon():
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style MultiPolygon geometry object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return {
+        'type': 'MultiPolygon',
+        'coordinates': (
+            (((2, 2), (2, 4), (4, 4), (4, 2), (2, 2)), ),
+            (((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)), )
+        )
+    }
+
+
+@pytest.fixture
+def geojson_geomcollection():
+    """
+    Returns
+    -------
+
+    dict: GeoJSON-style GeometryCollection object.
+        Coordinates are in grid coordinates (Affine.identity()).
+    """
+
+    return {
+        'type': 'GeometryCollection',
+        'geometries': (
+            {
+                'type': 'Polygon',
+                'coordinates': (((2, 2), (2, 4), (4, 4), (4, 2), (2, 2)), )
+            },
+            {
+                'type': 'Polygon',
+                'coordinates': (((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)), )
+            }
+        )
+    }
+
+
+
 @pytest.fixture
 def basic_feature(basic_geometry):
     """
diff --git a/tests/test_features.py b/tests/test_features.py
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -1,3 +1,4 @@
+from copy import deepcopy
 import logging
 import sys
 
@@ -8,7 +9,8 @@
 import rasterio
 from rasterio.errors import WindowError
 from rasterio.features import (
-    bounds, geometry_mask, geometry_window, rasterize, sieve, shapes)
+    bounds, geometry_mask, geometry_window, is_valid_geom, rasterize, sieve,
+    shapes)
 
 
 DEFAULT_SHAPE = (10, 10)
@@ -162,13 +164,233 @@ def test_geometry_no_overlap(path_rgb_byte_tif, basic_geometry):
 
 
 
-def test_rasterize(basic_geometry, basic_image_2x2):
-    """Rasterize operation should succeed for both an out_shape and out."""
+def test_is_valid_geom_point(geojson_point):
+    """Properly formed GeoJSON Point is valid"""
+    assert is_valid_geom(geojson_point)
+
+    # Empty coordinates are invalid
+    geojson_point['coordinates'] = []
+    assert not is_valid_geom(geojson_point)
+
+
+def test_is_valid_geom_multipoint(geojson_multipoint):
+    """Properly formed GeoJSON MultiPoint is valid"""
+    assert is_valid_geom(geojson_multipoint)
+
+    # Empty iterable is invalid
+    geom = deepcopy(geojson_multipoint)
+    geom['coordinates'] = []
+    assert not is_valid_geom(geom)
+
+    # Empty first coordinate is invalid
+    geom = deepcopy(geojson_multipoint)
+    geom['coordinates'] = [[]]
+
+
+def test_is_valid_geom_line(geojson_line):
+    """Properly formed GeoJSON LineString is valid"""
+
+    assert is_valid_geom(geojson_line)
+
+    # Empty iterable is invalid
+    geom = deepcopy(geojson_line)
+    geom['coordinates'] = []
+    assert not is_valid_geom(geom)
+
+    # Empty first coordinate is invalid
+    geom = deepcopy(geojson_line)
+    geom['coordinates'] = [[]]
+
+
+def test_is_valid_geom_multiline(geojson_line):
+    """Properly formed GeoJSON MultiLineString is valid"""
+
+    assert is_valid_geom(geojson_line)
+
+    # Empty iterables are invalid
+    geom = deepcopy(geojson_line)
+    geom['coordinates'] = []
+    assert not is_valid_geom(geom)
+
+    geom = deepcopy(geojson_line)
+    geom['coordinates'] = [[]]
+    assert not is_valid_geom(geom)
+
+    # Empty first coordinate is invalid
+    geom = deepcopy(geojson_line)
+    geom['coordinates'] = [[[]]]
+    assert not is_valid_geom(geom)
+
+
+def test_is_valid_geom_polygon(geojson_polygon):
+    """Properly formed GeoJSON Polygon is valid"""
+
+    assert is_valid_geom(geojson_polygon)
+
+    # Empty iterables are invalid
+    geom = deepcopy(geojson_polygon)
+    geom['coordinates'] = []
+    assert not is_valid_geom(geom)
+
+    geom = deepcopy(geojson_polygon)
+    geom['coordinates'] = [[]]
+    assert not is_valid_geom(geom)
+
+    # Empty first coordinate is invalid
+    geom = deepcopy(geojson_polygon)
+    geom['coordinates'] = [[[]]]
+    assert not is_valid_geom(geom)
+
+
+def test_is_valid_geom_multipolygon(geojson_multipolygon):
+    """Properly formed GeoJSON MultiPolygon is valid"""
+
+    assert is_valid_geom(geojson_multipolygon)
+
+    # Empty iterables are invalid
+    geom = deepcopy(geojson_multipolygon)
+    geom['coordinates'] = []
+    assert not is_valid_geom(geom)
+
+    geom = deepcopy(geojson_multipolygon)
+    geom['coordinates'] = [[]]
+    assert not is_valid_geom(geom)
+
+    geom = deepcopy(geojson_multipolygon)
+    geom['coordinates'] = [[[]]]
+    assert not is_valid_geom(geom)
+
+    # Empty first coordinate is invalid
+    geom = deepcopy(geojson_multipolygon)
+    geom['coordinates'] = [[[[]]]]
+    assert not is_valid_geom(geom)
+
+
+def test_is_valid_geom_geomcollection(geojson_geomcollection):
+    """Properly formed GeoJSON GeometryCollection is valid"""
+
+    assert is_valid_geom(geojson_geomcollection)
+
+    # Empty GeometryCollection is invalid
+    geom = deepcopy(geojson_geomcollection)
+    geom['geometries'] = []
+    assert not is_valid_geom(geom)
+
+
+def test_is_valid_geom_invalid_inputs():
+    """Improperly formed GeoJSON objects should fail"""
+
+    assert not is_valid_geom('type')
+    assert not is_valid_geom(['type'])
+    assert not is_valid_geom({'type': 'Invalid'})  # wrong type
+    assert not is_valid_geom({'type': 'Point'})  # Missing coordinates
+
+
+def test_rasterize_point(geojson_point):
+    expected = np.zeros(shape=DEFAULT_SHAPE, dtype='uint8')
+    expected[2, 2] = 1
+
     assert np.array_equal(
-        basic_image_2x2,
-        rasterize([basic_geometry], out_shape=DEFAULT_SHAPE)
+        rasterize([geojson_point], out_shape=DEFAULT_SHAPE),
+        expected
+    )
+
+
+def test_rasterize_multipoint(geojson_multipoint):
+    expected = np.zeros(shape=DEFAULT_SHAPE, dtype='uint8')
+    expected[2, 2] = 1
+    expected[4, 4] = 1
+
+    assert np.array_equal(
+        rasterize([geojson_multipoint], out_shape=DEFAULT_SHAPE),
+        expected
     )
 
+
+def test_rasterize_line(geojson_line):
+    expected = np.zeros(shape=DEFAULT_SHAPE, dtype='uint8')
+    expected[2, 2] = 1
+    expected[3, 3] = 1
+    expected[4, 4] = 1
+
+    assert np.array_equal(
+        rasterize([geojson_line], out_shape=DEFAULT_SHAPE),
+        expected
+    )
+
+
+def test_rasterize_multiline(geojson_multiline):
+    expected = np.zeros(shape=DEFAULT_SHAPE, dtype='uint8')
+    expected[2, 2] = 1
+    expected[3, 3] = 1
+    expected[4, 4] = 1
+    expected[0, 0:5] = 1
+
+    assert np.array_equal(
+        rasterize([geojson_multiline], out_shape=DEFAULT_SHAPE),
+        expected
+    )
+
+
+def test_rasterize_polygon(geojson_polygon, basic_image_2x2):
+    assert np.array_equal(
+        rasterize([geojson_polygon], out_shape=DEFAULT_SHAPE),
+        basic_image_2x2
+    )
+
+
+def test_rasterize_multipolygon(geojson_multipolygon):
+    expected = np.zeros(shape=DEFAULT_SHAPE, dtype='uint8')
+    expected[0:1, 0:1] = 1
+    expected[2:4, 2:4] = 1
+
+    assert np.array_equal(
+        rasterize([geojson_multipolygon], out_shape=DEFAULT_SHAPE),
+        expected
+    )
+
+
+def test_rasterize_geomcollection(geojson_geomcollection):
+    expected = np.zeros(shape=DEFAULT_SHAPE, dtype='uint8')
+    expected[0:1, 0:1] = 1
+    expected[2:4, 2:4] = 1
+
+    assert np.array_equal(
+        rasterize([geojson_geomcollection], out_shape=DEFAULT_SHAPE),
+        expected
+    )
+
+
+def test_rasterize_invalid_geom():
+    """Invalid GeoJSON should fail with exception"""
+
+    with pytest.raises(ValueError):
+        rasterize([{'type'}], out_shape=DEFAULT_SHAPE)
+
+    with pytest.raises(ValueError):
+        rasterize([{'type': 'Invalid'}], out_shape=DEFAULT_SHAPE)
+
+    with pytest.raises(ValueError):
+        rasterize([{'type': 'Point'}], out_shape=DEFAULT_SHAPE)
+
+    with pytest.raises(ValueError):
+        # Empty coordinates should fail
+        rasterize([{'type': 'Point', 'coordinates': []}],
+                  out_shape=DEFAULT_SHAPE)
+
+    with pytest.raises(ValueError):
+        # Empty GeometryCollection should fail
+        rasterize([{'type': 'GeometryCollection', 'geometries': []}],
+                  out_shape=DEFAULT_SHAPE)
+
+    with pytest.raises(ValueError):
+        # GeometryCollection with bad geometry should fail
+        rasterize([{'type': 'GeometryCollection', 'geometries': [
+            {'type': 'Invalid', 'coordinates': []}]}], out_shape=DEFAULT_SHAPE)
+
+
+def test_rasterize_out_image(basic_geometry, basic_image_2x2):
+    """Rasterize operation should succeed for an out image."""
     out = np.zeros(DEFAULT_SHAPE)
     rasterize([basic_geometry], out=out)
     assert np.array_equal(basic_image_2x2, out)
@@ -403,6 +625,18 @@ def test_rasterize_internal_driver_manager(basic_geometry):
     assert rasterize([basic_geometry], out_shape=DEFAULT_SHAPE).sum() == 4
 
 
+def test_rasterize_geo_interface(geojson_polygon):
+    """Objects that implement the geo interface should rasterize properly"""
+
+    class GeoObj:
+        @property
+        def __geo_interface__(self):
+            return geojson_polygon
+
+    assert rasterize([GeoObj()], out_shape=DEFAULT_SHAPE).sum() == 4
+
+
+
 def test_shapes(basic_image):
     """Test creation of shapes from pixel values."""
     results = list(shapes(basic_image))
diff --git a/tests/test_warp.py b/tests/test_warp.py
--- a/tests/test_warp.py
+++ b/tests/test_warp.py
@@ -831,7 +831,7 @@ def test_transform_geom_linestring_precision_iso(polygon_3373):
 def test_transform_geom_linestring_precision_z(polygon_3373):
     ring = polygon_3373['coordinates'][0]
     x, y = zip(*ring)
-    ring = zip(x, y, [0.0 for i in range(len(x))])
+    ring = list(zip(x, y, [0.0 for i in range(len(x))]))
     geom = {'type': 'LineString', 'coordinates': ring}
     result = transform_geom('EPSG:3373', 'EPSG:3373', geom, precision=1)
     assert int(result['coordinates'][0][0] * 10) == 7988423