parse-multipart-form-data 0.1.0a0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parse_multipart_form_data-0.1.0a0.dist-info/METADATA +142 -0
- parse_multipart_form_data-0.1.0a0.dist-info/RECORD +6 -0
- parse_multipart_form_data-0.1.0a0.dist-info/WHEEL +6 -0
- parse_multipart_form_data-0.1.0a0.dist-info/licenses/LICENSE +21 -0
- parse_multipart_form_data-0.1.0a0.dist-info/top_level.txt +1 -0
- parse_multipart_form_data.py +386 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parse-multipart-form-data
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A streaming parser for multipart/form-data request bodies.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/parse-multipart-form-data
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/parse-multipart-form-data/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 2
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: enum34; python_version < "3.4"
|
|
16
|
+
Requires-Dist: parse-http-header-line
|
|
17
|
+
Requires-Dist: put-back-iterator
|
|
18
|
+
Requires-Dist: typing; python_version < "3.5"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# parse-multipart-form-data
|
|
22
|
+
|
|
23
|
+
A small, streaming parser for HTTP `multipart/form-data` request bodies.
|
|
24
|
+
|
|
25
|
+
The parser consumes an iterable of byte chunks and yields a streaming event
|
|
26
|
+
sequence for each uploaded file. It does not depend on a web framework or read
|
|
27
|
+
from sockets itself, so callers control request-body I/O and where uploaded
|
|
28
|
+
content is stored.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install parse-multipart-form-data
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For local development from this checkout:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -e .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from parse_multipart_form_data import (
|
|
46
|
+
PartBegin,
|
|
47
|
+
PartData,
|
|
48
|
+
PartEnd,
|
|
49
|
+
parse_multipart_form_data,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
body_chunks = [
|
|
53
|
+
b"--BOUNDARY\r\n",
|
|
54
|
+
b'Content-Disposition: form-data; name="file"; filename="hello.txt"\r\n',
|
|
55
|
+
b"\r\n",
|
|
56
|
+
b"hello world\r\n",
|
|
57
|
+
b"--BOUNDARY--\r\n",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
output = None
|
|
61
|
+
for event in parse_multipart_form_data(
|
|
62
|
+
"multipart/form-data; boundary=BOUNDARY", body_chunks):
|
|
63
|
+
if isinstance(event, PartBegin):
|
|
64
|
+
output = open(event.filename, "wb")
|
|
65
|
+
elif isinstance(event, PartData):
|
|
66
|
+
output.write(event.data)
|
|
67
|
+
else: # PartEnd
|
|
68
|
+
output.close()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The event stream is:
|
|
72
|
+
|
|
73
|
+
- `PartBegin(filename)` at the beginning of a file part;
|
|
74
|
+
- `PartData(bytes_chunk)` for each file-content chunk; and
|
|
75
|
+
- `PartEnd(is_final)` after its delimiter has been consumed.
|
|
76
|
+
|
|
77
|
+
Only parts with a `filename` or `filename*` parameter produce events; ordinary
|
|
78
|
+
form fields are consumed and skipped. A valid RFC 6266 `filename*` (UTF-8 or
|
|
79
|
+
ISO-8859-1) takes precedence over `filename`.
|
|
80
|
+
|
|
81
|
+
## Delimiter compatibility
|
|
82
|
+
|
|
83
|
+
Only exact delimiter lines are supported:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
--BOUNDARY\r\n
|
|
87
|
+
--BOUNDARY--\r\n
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
MIME transport padding (spaces or tabs after a delimiter) is not supported.
|
|
91
|
+
Supporting arbitrary transport padding requires byte-by-byte input reading,
|
|
92
|
+
which is inefficient in Python and rare in real-world multipart form uploads.
|
|
93
|
+
|
|
94
|
+
Boundary values may be quoted or unquoted, including RFC 2046 boundary
|
|
95
|
+
characters such as `=`, `:`, `/`, `?`, `(`, `)`, and `,`. Preamble and
|
|
96
|
+
part-header lines are limited to 8192 bytes of content.
|
|
97
|
+
|
|
98
|
+
## Parser states
|
|
99
|
+
|
|
100
|
+
The parser has one `MultipartState` vocabulary. Its transitions are:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
SEEK_OPENING_BOUNDARY -> READ_HEADERS | DONE
|
|
104
|
+
READ_HEADERS -> MAYBE_BOUNDARY
|
|
105
|
+
READ_PART_BODY -> READ_PART_BODY | MAYBE_BOUNDARY
|
|
106
|
+
MAYBE_BOUNDARY -> MAYBE_BOUNDARY | READ_PART_BODY | READ_HEADERS | DONE
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Minimal wire-format machine
|
|
110
|
+
|
|
111
|
+
For the multipart example in the module documentation, the parser's essential
|
|
112
|
+
wire-level transitions are (`CRLF` denotes the literal `\r\n` byte pair):
|
|
113
|
+
|
|
114
|
+
```mermaid
|
|
115
|
+
stateDiagram-v2
|
|
116
|
+
[*] --> SEEK_OPENING_BOUNDARY
|
|
117
|
+
SEEK_OPENING_BOUNDARY --> READ_HEADERS: --BOUNDARY CRLF
|
|
118
|
+
SEEK_OPENING_BOUNDARY --> DONE: --BOUNDARY-- CRLF
|
|
119
|
+
READ_HEADERS --> READ_HEADERS: header CRLF
|
|
120
|
+
READ_HEADERS --> MAYBE_BOUNDARY: blank CRLF / empty part check
|
|
121
|
+
READ_PART_BODY --> READ_PART_BODY: not *.CRLF
|
|
122
|
+
READ_PART_BODY --> MAYBE_BOUNDARY: *.CRLF
|
|
123
|
+
MAYBE_BOUNDARY --> MAYBE_BOUNDARY: non-boundary line ending CRLF
|
|
124
|
+
MAYBE_BOUNDARY --> READ_PART_BODY: non-boundary prefix
|
|
125
|
+
MAYBE_BOUNDARY --> READ_HEADERS: --BOUNDARY CRLF
|
|
126
|
+
MAYBE_BOUNDARY --> DONE: --BOUNDARY-- CRLF
|
|
127
|
+
DONE --> [*]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The single parser generator holds its state across every event. A boundary
|
|
131
|
+
suffix is consumed before it emits `PartEnd`, so the next event always starts
|
|
132
|
+
a well-defined next transition.
|
|
133
|
+
|
|
134
|
+
## Testing
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python -m unittest discover -s tests
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
parse_multipart_form_data.py,sha256=7ahsPgdmTTewMd7TrPDXMBUj3IrPKDvULHZCOpQwv3g,13627
|
|
2
|
+
parse_multipart_form_data-0.1.0a0.dist-info/licenses/LICENSE,sha256=hvfX-ADssuMYgrXDUAjMMut4l8W3meA31ZAYfpdlJKY,1066
|
|
3
|
+
parse_multipart_form_data-0.1.0a0.dist-info/METADATA,sha256=X3m_2uPG6dMtM4aglmdgvPSElOp3JGPddXZxq1Pav3U,4303
|
|
4
|
+
parse_multipart_form_data-0.1.0a0.dist-info/WHEEL,sha256=4YBfCYNH4wlLpv3pzq1hbEuIlXA4WJabKLFurZ7eTL0,109
|
|
5
|
+
parse_multipart_form_data-0.1.0a0.dist-info/top_level.txt,sha256=ST9spZQ6x_5E_bOD1SjGsuSHFYd9w0mUrms0orcIYD4,26
|
|
6
|
+
parse_multipart_form_data-0.1.0a0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jifeng Wu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse_multipart_form_data
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Copyright (c) 2026 Jifeng Wu
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file in the project root
|
|
3
|
+
# for full license information.
|
|
4
|
+
"""Streaming parser for ``multipart/form-data`` request bodies.
|
|
5
|
+
|
|
6
|
+
The parser is one explicit generator state machine. It emits a flat event
|
|
7
|
+
stream for file-part starts, content chunks, and ends; it does not create a
|
|
8
|
+
separate part object or nested content iterator.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import unicode_literals
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Iterable, Iterator, List, Optional, Text, Tuple
|
|
13
|
+
from parse_http_header_line import (
|
|
14
|
+
MalformedHeaderLineError,
|
|
15
|
+
MalformedParameterListError,
|
|
16
|
+
parse_name_value,
|
|
17
|
+
parse_parameters,
|
|
18
|
+
)
|
|
19
|
+
from put_back_iterator import PutBackIterator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MultipartState(Enum):
|
|
23
|
+
"""The semantic states of ``parse_multipart_form_data``."""
|
|
24
|
+
|
|
25
|
+
SEEK_OPENING_BOUNDARY = 1
|
|
26
|
+
READ_HEADERS = 2
|
|
27
|
+
READ_PART_BODY = 3
|
|
28
|
+
MAYBE_BOUNDARY = 4
|
|
29
|
+
DONE = 5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Maximum content length of one preamble or part-header line. 8192 bytes is
|
|
33
|
+
# the de facto per-line limit used by Apache, nginx, Tomcat, and Jetty.
|
|
34
|
+
MAX_MULTIPART_HEADER_LINE = 8192
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MultipartEvent(object):
|
|
38
|
+
"""Base class for the events yielded by :func:`parse_multipart_form_data`."""
|
|
39
|
+
|
|
40
|
+
__slots__ = ()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PartBegin(MultipartEvent):
|
|
44
|
+
"""A file part began with the decoded *filename*."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("filename",)
|
|
47
|
+
|
|
48
|
+
def __init__(self, filename):
|
|
49
|
+
# type: (Text) -> None
|
|
50
|
+
self.filename = filename
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PartData(MultipartEvent):
|
|
54
|
+
"""A chunk of file-part content."""
|
|
55
|
+
|
|
56
|
+
__slots__ = ("data",)
|
|
57
|
+
|
|
58
|
+
def __init__(self, data):
|
|
59
|
+
# type: (bytes) -> None
|
|
60
|
+
self.data = data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PartEnd(MultipartEvent):
|
|
64
|
+
"""A file part ended; *is_final* marks the closing boundary."""
|
|
65
|
+
|
|
66
|
+
__slots__ = ("is_final",)
|
|
67
|
+
|
|
68
|
+
def __init__(self, is_final):
|
|
69
|
+
# type: (bool) -> None
|
|
70
|
+
self.is_final = is_final
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_crlf_chunk(stream):
|
|
74
|
+
# type: (PutBackIterator[bytes]) -> Tuple[Optional[bytes], bool]
|
|
75
|
+
"""Read one CRLF-terminated chunk of bytes from *stream*.
|
|
76
|
+
|
|
77
|
+
Returns ``(chunk, True)`` when *chunk* ends with CRLF. Returns
|
|
78
|
+
``(chunk, False)`` when *chunk* does not end with CRLF. A ``False``
|
|
79
|
+
result guarantees there is no CRLF straddling the chunk boundary: any
|
|
80
|
+
trailing CR has already been checked and is not followed by LF.
|
|
81
|
+
|
|
82
|
+
Returns ``(None, False)`` at end of stream.
|
|
83
|
+
|
|
84
|
+
The returned bytes are never modified: a terminating CRLF is included,
|
|
85
|
+
and any CR or CRLF in the middle of *chunk* is preserved verbatim.
|
|
86
|
+
"""
|
|
87
|
+
while stream.has_next():
|
|
88
|
+
chunk = next(stream)
|
|
89
|
+
if not chunk:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
crlf_index = chunk.find(b"\r\n")
|
|
93
|
+
if crlf_index != -1:
|
|
94
|
+
first = chunk[: crlf_index + 2]
|
|
95
|
+
rest = chunk[crlf_index + 2 :]
|
|
96
|
+
if rest:
|
|
97
|
+
stream.put_back(rest)
|
|
98
|
+
return first, True
|
|
99
|
+
|
|
100
|
+
if not chunk.endswith(b"\r"):
|
|
101
|
+
return chunk, False
|
|
102
|
+
|
|
103
|
+
# The chunk ends with CR; look ahead for the matching LF. Empty
|
|
104
|
+
# chunks are skipped so the CR pairs with the next real byte.
|
|
105
|
+
next_chunk = b""
|
|
106
|
+
while stream.has_next():
|
|
107
|
+
next_chunk = next(stream)
|
|
108
|
+
if next_chunk:
|
|
109
|
+
break
|
|
110
|
+
else:
|
|
111
|
+
return chunk, False
|
|
112
|
+
|
|
113
|
+
if next_chunk[:1] == b"\n":
|
|
114
|
+
if len(next_chunk) > 1:
|
|
115
|
+
stream.put_back(next_chunk[1:])
|
|
116
|
+
return chunk + b"\n", True
|
|
117
|
+
|
|
118
|
+
stream.put_back(next_chunk)
|
|
119
|
+
return chunk, False
|
|
120
|
+
|
|
121
|
+
return None, False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def read_crlf_line(stream, n=None):
|
|
125
|
+
# type: (PutBackIterator[bytes], Optional[int]) -> Tuple[Optional[bytes], bool]
|
|
126
|
+
"""Read through one CRLF or at most *n* bytes when *n* is provided.
|
|
127
|
+
|
|
128
|
+
Returns ``(line, True)`` when *line* ends in CRLF. Returns ``(line,
|
|
129
|
+
False)`` when the optional limit is reached first or when end of stream
|
|
130
|
+
follows a non-terminated line. Returns ``(None, False)`` only when the
|
|
131
|
+
stream is already exhausted. The returned line includes its CRLF when
|
|
132
|
+
present.
|
|
133
|
+
|
|
134
|
+
Like :func:`read_crlf_chunk`, this function never splits a CRLF across a
|
|
135
|
+
returned non-terminated prefix and the unread stream. If *n* would cut
|
|
136
|
+
between CR and LF, it returns the shorter prefix and leaves the whole CRLF
|
|
137
|
+
unread.
|
|
138
|
+
"""
|
|
139
|
+
fragments = [] # type: List[bytes]
|
|
140
|
+
remaining = n
|
|
141
|
+
while True:
|
|
142
|
+
if remaining == 0:
|
|
143
|
+
return b"".join(fragments), False
|
|
144
|
+
chunk, terminated = read_crlf_chunk(stream)
|
|
145
|
+
if chunk is None:
|
|
146
|
+
if fragments:
|
|
147
|
+
return b"".join(fragments), False
|
|
148
|
+
return None, False
|
|
149
|
+
if remaining is not None and len(chunk) > remaining:
|
|
150
|
+
length = remaining
|
|
151
|
+
if (
|
|
152
|
+
chunk[length - 1 : length] == b"\r"
|
|
153
|
+
and chunk[length : length + 1] == b"\n"
|
|
154
|
+
):
|
|
155
|
+
length -= 1
|
|
156
|
+
fragments.append(chunk[:length])
|
|
157
|
+
stream.put_back(chunk[length:])
|
|
158
|
+
return b"".join(fragments), False
|
|
159
|
+
fragments.append(chunk)
|
|
160
|
+
if remaining is not None:
|
|
161
|
+
remaining -= len(chunk)
|
|
162
|
+
if terminated:
|
|
163
|
+
return b"".join(fragments), True
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def boundary_from_content_type(content_type):
|
|
167
|
+
# type: (Text) -> Optional[Text]
|
|
168
|
+
"""Return the usable boundary from a ``multipart/form-data`` value."""
|
|
169
|
+
try:
|
|
170
|
+
type_text, parameters = parse_parameters(content_type)
|
|
171
|
+
except MalformedParameterListError:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
if type_text != "multipart/form-data":
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
for name, value in parameters:
|
|
178
|
+
if name == "boundary":
|
|
179
|
+
return value or None
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def filename_from_ext_value(value):
|
|
184
|
+
# type: (Text) -> Optional[Text]
|
|
185
|
+
"""Decode an RFC 6266 ``filename*`` ext-value to a filename.
|
|
186
|
+
|
|
187
|
+
The ext-value is ``charset ' language ' percent-encoded-bytes``. UTF-8
|
|
188
|
+
and ISO-8859-1/Latin-1 charsets are supported; any other charset, a
|
|
189
|
+
malformed value, or bytes that do not decode are rejected by returning
|
|
190
|
+
``None`` so the caller can fall back to a plain ``filename``.
|
|
191
|
+
"""
|
|
192
|
+
parts = value.split("'", 2)
|
|
193
|
+
if len(parts) != 3:
|
|
194
|
+
return None
|
|
195
|
+
charset, language, encoded = parts
|
|
196
|
+
if not charset or not encoded:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
charset_lower = charset.lower()
|
|
200
|
+
if charset_lower in ("utf-8", "utf8"):
|
|
201
|
+
codec = "utf-8"
|
|
202
|
+
elif charset_lower in ("iso-8859-1", "latin-1", "latin1"):
|
|
203
|
+
codec = "latin-1"
|
|
204
|
+
else:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
data = encoded.encode("latin-1")
|
|
208
|
+
result = bytearray()
|
|
209
|
+
index = 0
|
|
210
|
+
length = len(data)
|
|
211
|
+
while index < length:
|
|
212
|
+
if data[index : index + 1] == b"%" and index + 2 < length:
|
|
213
|
+
try:
|
|
214
|
+
result.append(int(data[index + 1 : index + 3], 16))
|
|
215
|
+
index += 3
|
|
216
|
+
continue
|
|
217
|
+
except ValueError:
|
|
218
|
+
pass
|
|
219
|
+
result.append(data[index])
|
|
220
|
+
index += 1
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
return bytes(result).decode(codec)
|
|
224
|
+
except UnicodeDecodeError:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def filename_from_header_line(header_line):
|
|
229
|
+
# type: (Text) -> Optional[Text]
|
|
230
|
+
"""Return a UTF-8 (or Latin-1 fallback) filename from one header line.
|
|
231
|
+
|
|
232
|
+
A valid ``filename*`` parameter takes precedence over ``filename``.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
name, value = parse_name_value(header_line)
|
|
236
|
+
except MalformedHeaderLineError:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
if name != "content-disposition":
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
disposition, parameters = parse_parameters(value)
|
|
244
|
+
except MalformedParameterListError:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
if disposition != "form-data":
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
filename = None
|
|
251
|
+
extended_filename = None
|
|
252
|
+
has_name = False
|
|
253
|
+
for parameter_name, parameter_value in parameters:
|
|
254
|
+
if parameter_name == "name":
|
|
255
|
+
has_name = True
|
|
256
|
+
elif parameter_name == "filename" and filename is None:
|
|
257
|
+
filename_bytes = parameter_value.encode("latin-1")
|
|
258
|
+
try:
|
|
259
|
+
filename = filename_bytes.decode("utf-8")
|
|
260
|
+
except UnicodeDecodeError:
|
|
261
|
+
filename = filename_bytes.decode("latin-1")
|
|
262
|
+
elif parameter_name == "filename*" and extended_filename is None:
|
|
263
|
+
extended_filename = filename_from_ext_value(parameter_value)
|
|
264
|
+
|
|
265
|
+
if has_name:
|
|
266
|
+
if extended_filename is not None:
|
|
267
|
+
return extended_filename
|
|
268
|
+
return filename
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def read_header_line(stream):
|
|
273
|
+
# type: (PutBackIterator[bytes]) -> Text
|
|
274
|
+
"""Read one CRLF-terminated part-header line as Latin-1 text.
|
|
275
|
+
|
|
276
|
+
The line's content is bounded by :data:`MAX_MULTIPART_HEADER_LINE` so a
|
|
277
|
+
malicious or corrupt body cannot force unbounded buffering.
|
|
278
|
+
"""
|
|
279
|
+
line, terminated = read_crlf_line(stream, MAX_MULTIPART_HEADER_LINE)
|
|
280
|
+
if line is None or not terminated:
|
|
281
|
+
raise ValueError("multipart header line is not terminated or is too long")
|
|
282
|
+
return line[:-2].decode("latin-1")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def parse_multipart_form_data(content_type, body_chunks):
|
|
286
|
+
# type: (Text, Iterable[bytes]) -> Iterator[MultipartEvent]
|
|
287
|
+
"""Yield file lifecycle events from one explicit multipart state machine.
|
|
288
|
+
|
|
289
|
+
Events are :class:`PartBegin`, :class:`PartData`, and :class:`PartEnd`.
|
|
290
|
+
Non-file fields are consumed without events.
|
|
291
|
+
"""
|
|
292
|
+
boundary_text = boundary_from_content_type(content_type)
|
|
293
|
+
if boundary_text is None:
|
|
294
|
+
raise ValueError("Invalid Content-Type for multipart/form-data")
|
|
295
|
+
|
|
296
|
+
stream = PutBackIterator(body_chunks) # type: PutBackIterator[bytes]
|
|
297
|
+
boundary = b"--" + boundary_text.encode("utf-8")
|
|
298
|
+
next_boundary = boundary + b"\r\n"
|
|
299
|
+
final_boundary = boundary + b"--\r\n"
|
|
300
|
+
state = MultipartState.SEEK_OPENING_BOUNDARY
|
|
301
|
+
filename = None
|
|
302
|
+
held_chunk = b""
|
|
303
|
+
|
|
304
|
+
while state is not MultipartState.DONE:
|
|
305
|
+
if state is MultipartState.SEEK_OPENING_BOUNDARY:
|
|
306
|
+
# The opening delimiter is the first complete delimiter line; all
|
|
307
|
+
# preceding CRLF lines are the optional MIME preamble.
|
|
308
|
+
while True:
|
|
309
|
+
line, terminated = read_crlf_line(stream, MAX_MULTIPART_HEADER_LINE)
|
|
310
|
+
if line is None:
|
|
311
|
+
raise ValueError("multipart body ended before its boundary")
|
|
312
|
+
if terminated and line == next_boundary:
|
|
313
|
+
state = MultipartState.READ_HEADERS
|
|
314
|
+
break
|
|
315
|
+
if terminated and line == final_boundary:
|
|
316
|
+
state = MultipartState.DONE
|
|
317
|
+
break
|
|
318
|
+
if not terminated:
|
|
319
|
+
raise ValueError(
|
|
320
|
+
"multipart body ended before its boundary or "
|
|
321
|
+
"preamble line is too long"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
elif state is MultipartState.READ_HEADERS:
|
|
325
|
+
filename = None
|
|
326
|
+
while True:
|
|
327
|
+
header_line = read_header_line(stream)
|
|
328
|
+
if not header_line:
|
|
329
|
+
if filename is not None:
|
|
330
|
+
yield PartBegin(filename)
|
|
331
|
+
held_chunk = b""
|
|
332
|
+
state = MultipartState.MAYBE_BOUNDARY
|
|
333
|
+
break
|
|
334
|
+
parsed_filename = filename_from_header_line(header_line)
|
|
335
|
+
if parsed_filename is not None:
|
|
336
|
+
filename = parsed_filename
|
|
337
|
+
|
|
338
|
+
elif state is MultipartState.MAYBE_BOUNDARY:
|
|
339
|
+
delimiter, terminated = read_crlf_line(stream, len(final_boundary))
|
|
340
|
+
is_final = terminated and delimiter == final_boundary
|
|
341
|
+
if terminated and delimiter == next_boundary:
|
|
342
|
+
if len(held_chunk) > 2 and filename is not None:
|
|
343
|
+
yield PartData(held_chunk[:-2])
|
|
344
|
+
held_chunk = b""
|
|
345
|
+
if filename is not None:
|
|
346
|
+
yield PartEnd(False)
|
|
347
|
+
state = MultipartState.READ_HEADERS
|
|
348
|
+
elif is_final:
|
|
349
|
+
if len(held_chunk) > 2 and filename is not None:
|
|
350
|
+
yield PartData(held_chunk[:-2])
|
|
351
|
+
held_chunk = b""
|
|
352
|
+
if filename is not None:
|
|
353
|
+
yield PartEnd(True)
|
|
354
|
+
state = MultipartState.DONE
|
|
355
|
+
else:
|
|
356
|
+
# The candidate was not a delimiter, so the held complete
|
|
357
|
+
# line is ordinary body content, including its CRLF.
|
|
358
|
+
if held_chunk and filename is not None:
|
|
359
|
+
yield PartData(held_chunk)
|
|
360
|
+
if terminated and delimiter:
|
|
361
|
+
# Hold this complete non-delimiter line while checking the
|
|
362
|
+
# bytes after its CRLF on the next MAYBE_BOUNDARY step.
|
|
363
|
+
held_chunk = delimiter
|
|
364
|
+
state = MultipartState.MAYBE_BOUNDARY
|
|
365
|
+
else:
|
|
366
|
+
if delimiter and filename is not None:
|
|
367
|
+
yield PartData(delimiter)
|
|
368
|
+
held_chunk = b""
|
|
369
|
+
state = MultipartState.READ_PART_BODY
|
|
370
|
+
|
|
371
|
+
elif state is MultipartState.READ_PART_BODY:
|
|
372
|
+
chunk, terminated = read_crlf_chunk(stream)
|
|
373
|
+
if chunk is None:
|
|
374
|
+
raise ValueError("multipart body ended before its boundary")
|
|
375
|
+
if terminated:
|
|
376
|
+
# Keep this complete line until MAYBE_BOUNDARY determines
|
|
377
|
+
# whether its trailing CRLF introduces a delimiter.
|
|
378
|
+
held_chunk = chunk
|
|
379
|
+
state = MultipartState.MAYBE_BOUNDARY
|
|
380
|
+
else:
|
|
381
|
+
if chunk and filename is not None:
|
|
382
|
+
yield PartData(chunk)
|
|
383
|
+
state = MultipartState.READ_PART_BODY
|
|
384
|
+
|
|
385
|
+
else:
|
|
386
|
+
raise ValueError("invalid multipart parser state")
|