Repository: rasterio/rasterio

Issue:
InMemoryRaster should validate shape of input image
Right now, we aren't validating `shape` of incoming `image` used to create `InMemoryRaster`, which leads to trickier exceptions like:

```
rasterio/_features.pyx:303: in rasterio._features._rasterize
    with InMemoryRaster(image=image, transform=transform) as mem:
rasterio/_io.pyx:1629: in rasterio._io.InMemoryRaster.__cinit__
    self._hds = exc_wrap_pointer(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   raise exc
E   rasterio._err.CPLE_AppDefinedError: Attempt to create 0x0 dataset is illegal,sizes must be larger than zero.

rasterio/_err.pyx:188: CPLE_AppDefinedError
```

We may want to also do a bit more validation in the functions that call it, like `_rasterize` in this case.

We should probably return `ValueError` in these cases.

This specifically came up when trying to create a mask of geometries that do not overlap a raster.  We'll prevent that specific case by better validation in the calling functions.




Discussion:

Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -280,6 +280,9 @@ def format_cast_error(param, dtype):
     else:
         raise ValueError('Either an out_shape or image must be provided')
 
+    if min(out.shape) == 0:
+        raise ValueError("width and height must be > 0")
+
     transform = guard_transform(transform)
     _rasterize(valid_shapes, out, transform.to_gdal(), all_touched)
     return out


Test patch:
diff --git a/tests/test_features.py b/tests/test_features.py
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -84,6 +84,45 @@ def test_geometry_mask_invert(basic_geometry, basic_image_2x2):
         )
     )
 
+def test_geometry_invalid_geom():
+    """An invalid geometry should fail"""
+
+    invalid_geoms = [
+        {'type': 'Invalid'},  # wrong type
+        {'type': 'Point'},  # missing coordinates
+        {'type': 'Point', 'coordinates': []}  # empty coordinates
+    ]
+
+    for geom in invalid_geoms:
+        with pytest.raises(ValueError) as exc_info:
+            geometry_mask(
+                [geom],
+                out_shape=DEFAULT_SHAPE,
+                transform=Affine.identity())
+
+        assert 'Invalid geometry' in exc_info.value.args[0]
+
+
+def test_geometry_mask_invalid_shape(basic_geometry):
+    """A width==0 or height==0 should fail with ValueError"""
+
+    for shape in [(0, 0), (1, 0), (0, 1)]:
+        with pytest.raises(ValueError) as exc_info:
+            geometry_mask(
+                [basic_geometry],
+                out_shape=shape,
+                transform=Affine.identity())
+
+        assert 'must be > 0' in exc_info.value.args[0]
+
+
+def test_geometry_mask_no_transform(basic_geometry):
+    with pytest.raises(TypeError) as exc_info:
+        geometry_mask(
+            [basic_geometry],
+            out_shape=DEFAULT_SHAPE,
+            transform=None)
+
 
 def test_geometry_window(basic_image_file, basic_geometry):
     with rasterio.open(basic_image_file) as src:
@@ -160,8 +199,7 @@ def test_geometry_no_overlap(path_rgb_byte_tif, basic_geometry):
     
     with rasterio.open(path_rgb_byte_tif) as src:
         with pytest.raises(WindowError):
-            window = geometry_window(src, [basic_geometry], north_up=False)
-
+            geometry_window(src, [basic_geometry], north_up=False)
 
 
 def test_is_valid_geom_point(geojson_point):
@@ -442,6 +480,11 @@ def test_rasterize_invalid_out_shape(basic_geometry):
         rasterize([basic_geometry], out_shape=(10,))
     assert 'Invalid out_shape' in str(ex.value)
 
+    for shape in [(0, 0), (1, 0), (0, 1)]:
+        with pytest.raises(ValueError) as ex:
+            rasterize([basic_geometry], out_shape=shape)
+        assert 'must be > 0' in str(ex.value)
+
 
 def test_rasterize_default_value(basic_geometry, basic_image_2x2):
     """All shapes should rasterize to the default value."""