Skip to content

Commit

Permalink
fix: Allow empty segment names
Browse files Browse the repository at this point in the history
RFC requires the presence of a name option in the Content-Disposition header, but the value is allowed to be empty.

fixes #56
  • Loading branch information
defnull committed Sep 28, 2024
1 parent e80c30a commit 6756044
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 15 deletions.
19 changes: 11 additions & 8 deletions multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,8 @@ class MultipartSegment:

#: List of headers as name/value pairs with normalized (Title-Case) names.
headerlist: List[Tuple[str, str]]
#: The required 'name' option of the Content-Disposition header.
#: The 'name' option of the Content-Disposition header. Always a string,
#: but may be empty.
name: str
#: The optional 'filename' option of the Content-Disposition header.
filename: Optional[str]
Expand Down Expand Up @@ -449,7 +450,7 @@ def __init__(self, parser: PushMultipartParser):
self._size_limit = parser.max_segment_size

def _add_headerline(self, line: bytearray):
assert line and not self.name
assert line and self.name is None
parser = self._parser

if line[0] in b" \t": # Multi-line header value
Expand Down Expand Up @@ -477,15 +478,17 @@ def _add_headerline(self, line: bytearray):
self.headerlist.append((name.title(), value))

def _close_headers(self):
assert not self.name and not self.complete
assert self.name is None

cdisp = self.header("Content-Disposition")
if not cdisp:
raise self._fail("Missing Content-Disposition segment header")
cdisp, args = parse_options_header(cdisp)
if cdisp != "form-data" or "name" not in args:
raise self._fail("Invalid Content-Disposition segment header")
self.name = args.get("name")
if cdisp != "form-data":
raise self._fail("Invalid Content-Disposition segment header: Wrong type")
if "name" not in args and self._parser.strict:
raise self._fail("Invalid Content-Disposition segment header: Missing name option")
self.name = args.get("name", "")
self.filename = args.get("filename")

content_type = self.header("Content-Type", "application/octet-stream")
Expand All @@ -495,15 +498,15 @@ def _close_headers(self):
self._clen = int(self.header("Content-Length", -1))

def _update_size(self, bytecount: int):
assert self.name and not self.complete
assert self.name is not None and not self.complete
self.size += bytecount
if self._clen >= 0 and self.size > self._clen:
raise self._fail("Segment Content-Length exceeded")
if self.size > self._size_limit:
raise self._fail("Maximum segment size exceeded")

def _mark_complete(self):
assert self.name and not self.complete
assert self.name is not None and not self.complete
if self._clen >= 0 and self.size != self._clen:
raise self._fail("Segment size does not match Content-Length header")
self.complete = True
Expand Down
29 changes: 22 additions & 7 deletions test/test_push_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,30 @@ def test_header_bad_name(self):
b"--boundary\r\ninvalid\xc3\x28:value\r\n\r\ndata\r\n--boundary--"
)

def test_header_bad_disposition(self):
with self.assertMPE("Invalid Content-Disposition segment header"):
def test_header_wrong_segment_subtype(self):
with self.assertMPE("Invalid Content-Disposition segment header: Wrong type"):
self.parse(
b"--boundary\r\nContent-Disposition: mixed\r\n\r\ndata\r\n--boundary--"
)
self.reset()
with self.assertMPE("Invalid Content-Disposition segment header"):
self.parse(
b"--boundary\r\nContent-Disposition: form-data\r\n\r\ndata\r\n--boundary--"
)

def test_segment_empty_name(self):
self.parse(b"--boundary\r\n")
parts = self.parse(b"Content-Disposition: form-data; name\r\n\r\n")
self.assertEqual(parts[0].name, "")
self.parse(b"\r\n--boundary\r\n")
parts = self.parse(b"Content-Disposition: form-data; name=\r\n\r\n")
self.assertEqual(parts[0].name, "")
self.parse(b"\r\n--boundary\r\n")
parts = self.parse(b'Content-Disposition: form-data; name=""\r\n\r\n')
self.assertEqual(parts[0].name, "")

@assertStrict("Invalid Content-Disposition segment header: Missing name option")
def test_segment_missing_name(self, strict):
self.reset(strict=strict)
self.parse(b"--boundary\r\n")
parts = self.parse(b"Content-Disposition: form-data;\r\n\r\n")
print(parts)
self.assertEqual(parts[0].name, "")

def test_segment_count_limit(self):
self.reset(max_segment_count=1)
Expand Down Expand Up @@ -261,3 +275,4 @@ def test_segment_handle_access(self):
self.assertEqual(part.filename, "bar.txt")
with self.assertRaises(KeyError):
part["Missing"]

0 comments on commit 6756044

Please sign in to comment.