Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions mimeparse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def quality(mime_type: str, ranges: str) -> float:
text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
0.7
"""
parsed_ranges = [parse_media_range(r) for r in ranges.split(',')]
parsed_ranges = [parse_media_range(r) for r in _split_media_ranges(ranges)]

return quality_parsed(mime_type, parsed_ranges)

Expand All @@ -194,7 +194,7 @@ def best_match(supported: Iterable[str], header: str) -> str:
'text/*;q=0.5,*/*; q=0.1')
'text/xml'
"""
split_header = _filter_blank(header.split(','))
split_header = _filter_blank(_split_media_ranges(header))
parsed_header = [parse_media_range(r) for r in split_header]
weighted_matches = []
pos = 0
Expand All @@ -210,6 +210,28 @@ def best_match(supported: Iterable[str], header: str) -> str:
return weighted_matches[-1][0][0] and weighted_matches[-1][2] or ''


def _split_media_ranges(header: str) -> Generator[str, None, None]:
"""Split an Accept list without splitting quoted parameter values."""
if '"' not in header:
yield from header.split(',')
return

start = 0
quoted = False
escaped = False
for index, char in enumerate(header):
if escaped:
escaped = False
elif quoted and char == '\\':
escaped = True
elif char == '"':
quoted = not quoted
elif char == ',' and not quoted:
yield header[start:index]
start = index + 1
yield header[start:]


def _filter_blank(i: Iterable[str]) -> Generator[str, None, None]:
"""Return all non-empty items in the list."""
for s in i:
Expand Down
24 changes: 24 additions & 0 deletions mimeparse_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ def test_best_match(self):
for args, expected, description in self.test_data['best_match']:
self._test_best_match(args, expected, description)

def test_quoted_commas(self):
values = (
r'"a,b"',
r'"a\",b"',
r'"a\\,b"',
r'"a,b\\"',
r'"a;b,c"',
r'"a,\"b,c\""',
)
for value in values:
with self.subTest(value=value):
media_type = f'text/plain;note={value}'
header = f'{media_type},application/json;q=0.2'
self.assertEqual(mimeparse.quality(media_type, header), 1)
self.assertEqual(
mimeparse.best_match([media_type, 'application/json'], header),
media_type,
)

def test_quoted_commas_with_blank_ranges(self):
media_type = 'text/plain;note="a,b"'
header = f', {media_type}, , '
self.assertEqual(mimeparse.best_match([media_type], header), media_type)

def test_parse_mime_type(self):
for args, expected in self.test_data['parse_mime_type']:
self._test_parse_mime_type(args, expected)
Expand Down
30 changes: 30 additions & 0 deletions testdata.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@
],

"quality": [
[
[
"text/plain;profile=\"a,b\"",
"text/plain;profile=\"a,b\";q=0.8, application/json;q=0.2"
],
0.8
],
[
[
"application/json",
"text/plain;profile=\"a,b\";q=0.8, application/json;q=0.2"
],
0.2
],
[
[
"text/html;level=1",
Expand Down Expand Up @@ -66,6 +80,22 @@
],

"best_match": [
[
[
["text/plain;profile=\"a,b\"", "application/json"],
"text/plain;profile=\"a,b\";q=0.8, application/json;q=0.2"
],
"text/plain;profile=\"a,b\"",
"preserve a comma within a quoted parameter value"
],
[
[
["text/plain;profile=\"a,b\"", "application/json"],
"text/plain;profile=\"a,b\";q=0.2, application/json;q=0.8"
],
"application/json",
"compare quality factors after a quoted parameter containing a comma"
],
[
[
["application/json", "text/html"],
Expand Down