Repository: pallets/werkzeug

Issue:
parse_options_header fails to parse quoted values
``` python
header = '''multipart/form-data; boundary=----WebKitFormBoundaryUv6L4qL9bQlBDkzK
form-data; name="αυτό είναι, ένα αρχείο με Ϋ διαλυτικά ;.rtf"; filename="αυτό είναι, ένα αρχείο με Ϋ διαλυτικά ;.rtf"
application/msword''' 
print parse_options_header(header)[1]["filename"]
```

Given the above code the output should be :

``` text
αυτό είναι, ένα αρχείο με Ϋ διαλυτικά ;
```

without any quotes, and with a final semicolon. Instead it is:

``` text
"αυτό είναι, ένα αρχείο με Ϋ διαλυτικά
```



Discussion:
I just found out that the problem is because of the semicolon which is part of the filename. Is this an http limitation?

@sstergou No, this is not a limitation of HTTP. It is definetly a bug in Werkzeug. We already had a similar issue with #271, and apparently 537756fffc78887ad25e101a51e4aa19d1c81105 did not do enough to get this part of Werkzeug fixed.

Actually this now seems like improper usage of parse_options_header. You might want to take a look at `werkzeug.formparser` instead.

I traced the problem to that function. I think that werkzeug uses it internally in order to parse filenames in uploaded files.

Please show what you originally did in your own code that triggered the bug.

``` python
    def on_upload_file(self, request, resource=""):
        try:
            dirpath = self.get_path(resource)
            self._assert.path_exists(dirpath)
            items = request.files.items()
            if len(items) < 1:
                return Response("Request Did Not Contain Files", status=400)
            data, errors = [], []
            for name, fs in items:
                try:
                    name = secure_filename(name.decode('utf-8', errors="replace"))
                    filepath = j(dirpath, name)
                    self._assert.is_in(filepath)
                    if os.path.isfile(filepath):
                        if "overwrite" not in request.args:
                            filepath = get_valid_name(filepath)
                    fs.save(filepath)
                    fs.close()
                    data.append(self.metadata(filepath))
                except WillowError as e:
                    errors.append(e.message)
                    continue
                except Exception as e:
                    msg = "Unable to Upload File %s" % name
                    logger.exception(msg)
                    errors.append(msg)
                    continue
            return JSONResponse(data, errors)
        except WillowError as e:
            return e.response()
        except Exception as e:
            msg = "Unable to Access Resource %s" % resource
            logger.exception(msg)
            return InternalServerError(msg)
```

where secure_filename is not the werkzeug's function. The application that I'm building requires that filenames stay untouched even after the upload. Here is my function that simply replaces the path separator.

``` python
def secure_filename(filename):
    for sep in os.path.sep, os.path.altsep:
        if sep:
            filename = filename.replace(sep, '_')
    return filename
```

j is simply an alias, get_valid_name calculates a name that does not already exist

``` python
j = os.path.join

def get_valid_name(path):
    i = 0
    name, ext = os.path.splitext(path)
    newpath = path
    while os.path.exists(newpath):
        i += 1
        newpath = name + " (%s)" % i + ext
    return newpath

```

The code ends with a filename exception on  `fs.save(filepath)`.


Resolving patch:
diff --git a/werkzeug/http.py b/werkzeug/http.py
--- a/werkzeug/http.py
+++ b/werkzeug/http.py
@@ -61,9 +61,10 @@
 _unsafe_header_chars = set('()<>@,;:\"/[]?={} \t')
 _quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
 _option_header_piece_re = re.compile(
-    r';\s*(%s|[^\s;=]+)\s*(?:=\s*(%s|[^;]+))?\s*' %
+    r';\s*(%s|[^\s;,=]+)\s*(?:=\s*(%s|[^;,]+)?)?\s*' %
     (_quoted_string_re, _quoted_string_re)
 )
+_option_header_start_mime_type = re.compile(r',\s*([^;,\s]+)([;,]\s*.+)?')
 
 _entity_headers = frozenset([
     'allow', 'content-encoding', 'content-language', 'content-length',
@@ -317,7 +318,7 @@ def parse_dict_header(value, cls=dict):
     return result
 
 
-def parse_options_header(value):
+def parse_options_header(value, multiple=False):
     """Parse a ``Content-Type`` like header into a tuple with the content
     type and the options:
 
@@ -331,23 +332,43 @@ def parse_options_header(value):
     .. versionadded:: 0.5
 
     :param value: the header to parse.
-    :return: (str, options)
+    :param multiple: Whether try to parse and return multiple MIME types
+    :return: (mimetype, options) or (mimetype, options, mimetype, options, …)
+             if multiple=True
     """
-    def _tokenize(string):
-        for match in _option_header_piece_re.finditer(string):
-            key, value = match.groups()
-            key = unquote_header_value(key)
-            if value is not None:
-                value = unquote_header_value(value, key == 'filename')
-            yield key, value
 
     if not value:
         return '', {}
 
-    parts = _tokenize(';' + value)
-    name = next(parts)[0]
-    extra = dict(parts)
-    return name, extra
+    result = []
+
+    value = "," + value.replace("\n", ",")
+    while value:
+        match = _option_header_start_mime_type.match(value)
+        if not match:
+            break
+        result.append(match.group(1))  # mimetype
+        options = {}
+        # Parse options
+        rest = match.group(2)
+        while rest:
+            optmatch = _option_header_piece_re.match(rest)
+            if not optmatch:
+                break
+            option, option_value = optmatch.groups()
+            option = unquote_header_value(option)
+            if option_value is not None:
+                option_value = unquote_header_value(
+                    option_value,
+                    option == 'filename')
+            options[option] = option_value
+            rest = rest[optmatch.end():]
+        result.append(options)
+        if multiple is False:
+            return tuple(result)
+        value = rest
+
+    return tuple(result)
 
 
 def parse_accept_header(value, cls=None):


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -232,6 +232,10 @@ def test_remove_hop_by_hop_headers(self):
         assert headers2 == datastructures.Headers([('Foo', 'bar')])
 
     def test_parse_options_header(self):
+        assert http.parse_options_header(None) == \
+            ('', {})
+        assert http.parse_options_header("") == \
+            ('', {})
         assert http.parse_options_header(r'something; foo="other\"thing"') == \
             ('something', {'foo': 'other"thing'})
         assert http.parse_options_header(r'something; foo="other\"thing"; meh=42') == \
@@ -242,6 +246,22 @@ def test_parse_options_header(self):
             ('something', {'foo': 'other;thing', 'meh': '42', 'bleh': None})
         assert http.parse_options_header('something; foo="otherthing"; meh=; bleh') == \
             ('something', {'foo': 'otherthing', 'meh': None, 'bleh': None})
+        # Issue #404
+        assert http.parse_options_header('multipart/form-data; name="foo bar"; filename="bar foo"') == \
+            ('multipart/form-data', {'name': 'foo bar', 'filename': 'bar foo'})
+        # Examples from RFC
+        assert http.parse_options_header('audio/*; q=0.2, audio/basic') == \
+            ('audio/*', {'q': '0.2'})
+        assert http.parse_options_header('audio/*; q=0.2, audio/basic', multiple=True) == \
+            ('audio/*', {'q': '0.2'}, "audio/basic", {})
+        assert http.parse_options_header(
+            'text/plain; q=0.5, text/html\n        '
+            'text/x-dvi; q=0.8, text/x-c',
+            multiple=True) == \
+            ('text/plain', {'q': '0.5'}, "text/html", {},
+             "text/x-dvi", {'q': '0.8'}, "text/x-c", {})
+        assert http.parse_options_header('text/plain; q=0.5, text/html\n        text/x-dvi; q=0.8, text/x-c') == \
+            ('text/plain', {'q': '0.5'})
 
     def test_dump_options_header(self):
         assert http.dump_options_header('foo', {'bar': 42}) == \