Repository: pallets/werkzeug

Issue:
consistency with `werkzeug.datastructures.Range` and `werkzeug.http.parse_range_header`?
As mentioned in [rfc 7233](https://tools.ietf.org/html/rfc7233)
>  A byte-range-spec is invalid if the last-byte-pos value is present
   and less than the first-byte-pos.
>
> DIGIT (decimal 0-9)
>
> ```
> byte-range-spec = first-byte-pos "-" [ last-byte-pos ]
> first-byte-pos  = 1*DIGIT
> last-byte-pos   = 1*DIGIT
> ```

```
In [101]: r = Range('bytes', [(0, 0)])

In [102]: r.to_header()
Out[102]: 'bytes=0--1'

In [103]: new_r = parse_range_header(r.to_header())

In [104]: print new_r
None
```

so, is `bytes=0--1` an invalid value?


Discussion:
Looks like it's invalid according to the spec you quoted. Similar to Python's `range`, the end value is not inclusive, maybe you wanted `(0, 1)`. `parse_range_header` returns `None` for invalid ranges, that doesn't mean you can't create a `Range` object that represents an invalid range. Why do you have that specific range? 
That specific range does not exists in real word.

I'm just curious about the parse result.
In my opinion, `Range('bytes', [(0, 0)])` should raise Error or return None but not return a `Range` object.

Resolving patch:
diff --git a/werkzeug/datastructures.py b/werkzeug/datastructures.py
--- a/werkzeug/datastructures.py
+++ b/werkzeug/datastructures.py
@@ -2296,11 +2296,15 @@ def __repr__(self):
 
 
 class Range(object):
-
-    """Represents a range header.  All the methods are only supporting bytes
-    as unit.  It does store multiple ranges but :meth:`range_for_length` will
+    """Represents a ``Range`` header. All methods only support only
+    bytes as the unit. Stores a list of ranges if given, but the methods
     only work if only one range is provided.
 
+    :raise ValueError: If the ranges provided are invalid.
+
+    .. versionchanged:: 0.15
+        The ranges passed in are validated.
+
     .. versionadded:: 0.7
     """
 
@@ -2311,6 +2315,10 @@ def __init__(self, units, ranges):
         #: The ranges are non-inclusive.
         self.ranges = ranges
 
+        for start, end in ranges:
+            if start is None or (end is not None and (start < 0 or start >= end)):
+                raise ValueError("{} is not a valid range.".format((start, end)))
+
     def range_for_length(self, length):
         """If the range is for bytes, the length is not None and there is
         exactly one range and it is satisfiable it returns a ``(start, stop)``


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -29,9 +29,10 @@
 from contextlib import contextmanager
 from copy import copy, deepcopy
 
-from werkzeug import datastructures
+from werkzeug import datastructures, http
 from werkzeug._compat import iterkeys, itervalues, iteritems, iterlists, \
     iterlistvalues, text_type, PY2
+from werkzeug.datastructures import Range
 from werkzeug.exceptions import BadRequestKeyError
 
 
@@ -1066,3 +1067,20 @@ def test_bytes_proper_sentinel(self):
         for idx, line in enumerate(binary_storage):
             assert idx < 2
         assert idx == 1
+
+
+@pytest.mark.parametrize(
+    "ranges", ([(0, 1), (-5, None)], [(5, None)])
+)
+def test_range_to_header(ranges):
+    header = Range("byes", ranges).to_header()
+    r = http.parse_range_header(header)
+    assert r.ranges == ranges
+
+
+@pytest.mark.parametrize(
+    "ranges", ([(0, 0)], [(None, 1)], [(1, 0)], [(0, 1), (-5, 10)])
+)
+def test_range_validates_ranges(ranges):
+    with pytest.raises(ValueError):
+        datastructures.Range("bytes", ranges)
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -551,16 +551,16 @@ def test_range_parsing(self):
         rv = http.parse_range_header('bytes=-')
         assert rv is None
 
-        rv = http.parse_range_header('bytes=bullshit')
+        rv = http.parse_range_header('bytes=bad')
         assert rv is None
 
-        rv = http.parse_range_header('bytes=bullshit-1')
+        rv = http.parse_range_header('bytes=bad-1')
         assert rv is None
 
-        rv = http.parse_range_header('bytes=-bullshit')
+        rv = http.parse_range_header('bytes=-bad')
         assert rv is None
 
-        rv = http.parse_range_header('bytes=52-99, bullshit')
+        rv = http.parse_range_header('bytes=52-99, bad')
         assert rv is None
 
     def test_content_range_parsing(self):