Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/http/multipartparser.py: 79%

385 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Multi-part parsing for file uploads. 

3 

4Exposes one class, ``MultiPartParser``, which feeds chunks of uploaded data to 

5file upload handlers for processing. 

6""" 

7import base64 

8import binascii 

9import cgi 

10import collections 

11import html 

12from urllib.parse import unquote 

13 

14from django.conf import settings 

15from django.core.exceptions import ( 

16 RequestDataTooBig, 

17 SuspiciousMultipartForm, 

18 TooManyFieldsSent, 

19) 

20from django.core.files.uploadhandler import SkipFile, StopFutureHandlers, StopUpload 

21from django.utils.datastructures import MultiValueDict 

22from django.utils.encoding import force_str 

23 

24__all__ = ("MultiPartParser", "MultiPartParserError", "InputStreamExhausted") 

25 

26 

27class MultiPartParserError(Exception): 

28 pass 

29 

30 

31class InputStreamExhausted(Exception): 

32 """ 

33 No more reads are allowed from this device. 

34 """ 

35 

36 pass 

37 

38 

39RAW = "raw" 

40FILE = "file" 

41FIELD = "field" 

42 

43 

44class MultiPartParser: 

45 """ 

46 A rfc2388 multipart/form-data parser. 

47 

48 ``MultiValueDict.parse()`` reads the input stream in ``chunk_size`` chunks 

49 and returns a tuple of ``(MultiValueDict(POST), MultiValueDict(FILES))``. 

50 """ 

51 

52 def __init__(self, META, input_data, upload_handlers, encoding=None): 

53 """ 

54 Initialize the MultiPartParser object. 

55 

56 :META: 

57 The standard ``META`` dictionary in Django request objects. 

58 :input_data: 

59 The raw post data, as a file-like object. 

60 :upload_handlers: 

61 A list of UploadHandler instances that perform operations on the 

62 uploaded data. 

63 :encoding: 

64 The encoding with which to treat the incoming data. 

65 """ 

66 # Content-Type should contain multipart and the boundary information. 

67 content_type = META.get("CONTENT_TYPE", "") 

68 if not content_type.startswith("multipart/"): 68 ↛ 69line 68 didn't jump to line 69, because the condition on line 68 was never true

69 raise MultiPartParserError("Invalid Content-Type: %s" % content_type) 

70 

71 # Parse the header to get the boundary to split the parts. 

72 try: 

73 ctypes, opts = parse_header(content_type.encode("ascii")) 

74 except UnicodeEncodeError: 

75 raise MultiPartParserError( 

76 "Invalid non-ASCII Content-Type in multipart: %s" 

77 % force_str(content_type) 

78 ) 

79 boundary = opts.get("boundary") 

80 if not boundary or not cgi.valid_boundary(boundary): 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true

81 raise MultiPartParserError( 

82 "Invalid boundary in multipart: %s" % force_str(boundary) 

83 ) 

84 

85 # Content-Length should contain the length of the body we are about 

86 # to receive. 

87 try: 

88 content_length = int(META.get("CONTENT_LENGTH", 0)) 

89 except (ValueError, TypeError): 

90 content_length = 0 

91 

92 if content_length < 0: 92 ↛ 94line 92 didn't jump to line 94, because the condition on line 92 was never true

93 # This means we shouldn't continue...raise an error. 

94 raise MultiPartParserError("Invalid content length: %r" % content_length) 

95 

96 if isinstance(boundary, str): 96 ↛ 97line 96 didn't jump to line 97, because the condition on line 96 was never true

97 boundary = boundary.encode("ascii") 

98 self._boundary = boundary 

99 self._input_data = input_data 

100 

101 # For compatibility with low-level network APIs (with 32-bit integers), 

102 # the chunk size should be < 2^31, but still divisible by 4. 

103 possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size] 

104 self._chunk_size = min([2**31 - 4] + possible_sizes) 

105 

106 self._meta = META 

107 self._encoding = encoding or settings.DEFAULT_CHARSET 

108 self._content_length = content_length 

109 self._upload_handlers = upload_handlers 

110 

111 def parse(self): 

112 """ 

113 Parse the POST data and break it into a FILES MultiValueDict and a POST 

114 MultiValueDict. 

115 

116 Return a tuple containing the POST and FILES dictionary, respectively. 

117 """ 

118 from django.http import QueryDict 

119 

120 encoding = self._encoding 

121 handlers = self._upload_handlers 

122 

123 # HTTP spec says that Content-Length >= 0 is valid 

124 # handling content-length == 0 before continuing 

125 if self._content_length == 0: 125 ↛ 126line 125 didn't jump to line 126, because the condition on line 125 was never true

126 return QueryDict(encoding=self._encoding), MultiValueDict() 

127 

128 # See if any of the handlers take care of the parsing. 

129 # This allows overriding everything if need be. 

130 for handler in handlers: 

131 result = handler.handle_raw_input( 

132 self._input_data, 

133 self._meta, 

134 self._content_length, 

135 self._boundary, 

136 encoding, 

137 ) 

138 # Check to see if it was handled 

139 if result is not None: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true

140 return result[0], result[1] 

141 

142 # Create the data structures to be used later. 

143 self._post = QueryDict(mutable=True) 

144 self._files = MultiValueDict() 

145 

146 # Instantiate the parser and stream: 

147 stream = LazyStream(ChunkIter(self._input_data, self._chunk_size)) 

148 

149 # Whether or not to signal a file-completion at the beginning of the loop. 

150 old_field_name = None 

151 counters = [0] * len(handlers) 

152 

153 # Number of bytes that have been read. 

154 num_bytes_read = 0 

155 # To count the number of keys in the request. 

156 num_post_keys = 0 

157 # To limit the amount of data read from the request. 

158 read_size = None 

159 # Whether a file upload is finished. 

160 uploaded_file = True 

161 

162 try: 

163 for item_type, meta_data, field_stream in Parser(stream, self._boundary): 

164 if old_field_name: 

165 # We run this at the beginning of the next loop 

166 # since we cannot be sure a file is complete until 

167 # we hit the next boundary/part of the multipart content. 

168 self.handle_file_complete(old_field_name, counters) 

169 old_field_name = None 

170 uploaded_file = True 

171 

172 try: 

173 disposition = meta_data["content-disposition"][1] 

174 field_name = disposition["name"].strip() 

175 except (KeyError, IndexError, AttributeError): 

176 continue 

177 

178 transfer_encoding = meta_data.get("content-transfer-encoding") 

179 if transfer_encoding is not None: 179 ↛ 180line 179 didn't jump to line 180, because the condition on line 179 was never true

180 transfer_encoding = transfer_encoding[0].strip() 

181 field_name = force_str(field_name, encoding, errors="replace") 

182 

183 if item_type == FIELD: 

184 # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FIELDS. 

185 num_post_keys += 1 

186 if ( 186 ↛ 190line 186 didn't jump to line 190

187 settings.DATA_UPLOAD_MAX_NUMBER_FIELDS is not None 

188 and settings.DATA_UPLOAD_MAX_NUMBER_FIELDS < num_post_keys 

189 ): 

190 raise TooManyFieldsSent( 

191 "The number of GET/POST parameters exceeded " 

192 "settings.DATA_UPLOAD_MAX_NUMBER_FIELDS." 

193 ) 

194 

195 # Avoid reading more than DATA_UPLOAD_MAX_MEMORY_SIZE. 

196 if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None: 196 ↛ 202line 196 didn't jump to line 202, because the condition on line 196 was never false

197 read_size = ( 

198 settings.DATA_UPLOAD_MAX_MEMORY_SIZE - num_bytes_read 

199 ) 

200 

201 # This is a post field, we can just set it in the post 

202 if transfer_encoding == "base64": 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true

203 raw_data = field_stream.read(size=read_size) 

204 num_bytes_read += len(raw_data) 

205 try: 

206 data = base64.b64decode(raw_data) 

207 except binascii.Error: 

208 data = raw_data 

209 else: 

210 data = field_stream.read(size=read_size) 

211 num_bytes_read += len(data) 

212 

213 # Add two here to make the check consistent with the 

214 # x-www-form-urlencoded check that includes '&='. 

215 num_bytes_read += len(field_name) + 2 

216 if ( 216 ↛ 220line 216 didn't jump to line 220

217 settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None 

218 and num_bytes_read > settings.DATA_UPLOAD_MAX_MEMORY_SIZE 

219 ): 

220 raise RequestDataTooBig( 

221 "Request body exceeded " 

222 "settings.DATA_UPLOAD_MAX_MEMORY_SIZE." 

223 ) 

224 

225 self._post.appendlist( 

226 field_name, force_str(data, encoding, errors="replace") 

227 ) 

228 elif item_type == FILE: 228 ↛ 307line 228 didn't jump to line 307, because the condition on line 228 was never false

229 # This is a file, use the handler... 

230 file_name = disposition.get("filename") 

231 if file_name: 231 ↛ 234line 231 didn't jump to line 234, because the condition on line 231 was never false

232 file_name = force_str(file_name, encoding, errors="replace") 

233 file_name = self.sanitize_file_name(file_name) 

234 if not file_name: 234 ↛ 235line 234 didn't jump to line 235, because the condition on line 234 was never true

235 continue 

236 

237 content_type, content_type_extra = meta_data.get( 

238 "content-type", ("", {}) 

239 ) 

240 content_type = content_type.strip() 

241 charset = content_type_extra.get("charset") 

242 

243 try: 

244 content_length = int(meta_data.get("content-length")[0]) 

245 except (IndexError, TypeError, ValueError): 

246 content_length = None 

247 

248 counters = [0] * len(handlers) 

249 uploaded_file = False 

250 try: 

251 for handler in handlers: 251 ↛ 264line 251 didn't jump to line 264, because the loop on line 251 didn't complete

252 try: 

253 handler.new_file( 

254 field_name, 

255 file_name, 

256 content_type, 

257 content_length, 

258 charset, 

259 content_type_extra, 

260 ) 

261 except StopFutureHandlers: 

262 break 

263 

264 for chunk in field_stream: 

265 if transfer_encoding == "base64": 265 ↛ 270line 265 didn't jump to line 270, because the condition on line 265 was never true

266 # We only special-case base64 transfer encoding 

267 # We should always decode base64 chunks by 

268 # multiple of 4, ignoring whitespace. 

269 

270 stripped_chunk = b"".join(chunk.split()) 

271 

272 remaining = len(stripped_chunk) % 4 

273 while remaining != 0: 

274 over_chunk = field_stream.read(4 - remaining) 

275 if not over_chunk: 

276 break 

277 stripped_chunk += b"".join(over_chunk.split()) 

278 remaining = len(stripped_chunk) % 4 

279 

280 try: 

281 chunk = base64.b64decode(stripped_chunk) 

282 except Exception as exc: 

283 # Since this is only a chunk, any error is 

284 # an unfixable error. 

285 raise MultiPartParserError( 

286 "Could not decode base64 data." 

287 ) from exc 

288 

289 for i, handler in enumerate(handlers): 289 ↛ 264line 289 didn't jump to line 264, because the loop on line 289 didn't complete

290 chunk_length = len(chunk) 

291 chunk = handler.receive_data_chunk(chunk, counters[i]) 

292 counters[i] += chunk_length 

293 if chunk is None: 293 ↛ 289line 293 didn't jump to line 289, because the condition on line 293 was never false

294 # Don't continue if the chunk received by 

295 # the handler is None. 

296 break 

297 

298 except SkipFile: 

299 self._close_files() 

300 # Just use up the rest of this file... 

301 exhaust(field_stream) 

302 else: 

303 # Handle file upload completions on next iteration. 

304 old_field_name = field_name 

305 else: 

306 # If this is neither a FIELD or a FILE, just exhaust the stream. 

307 exhaust(stream) 

308 except StopUpload as e: 

309 self._close_files() 

310 if not e.connection_reset: 

311 exhaust(self._input_data) 

312 else: 

313 if not uploaded_file: 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true

314 for handler in handlers: 

315 handler.upload_interrupted() 

316 # Make sure that the request data is all fed 

317 exhaust(self._input_data) 

318 

319 # Signal that the upload has completed. 

320 # any() shortcircuits if a handler's upload_complete() returns a value. 

321 any(handler.upload_complete() for handler in handlers) 

322 self._post._mutable = False 

323 return self._post, self._files 

324 

325 def handle_file_complete(self, old_field_name, counters): 

326 """ 

327 Handle all the signaling that takes place when a file is complete. 

328 """ 

329 for i, handler in enumerate(self._upload_handlers): 329 ↛ exitline 329 didn't return from function 'handle_file_complete', because the loop on line 329 didn't complete

330 file_obj = handler.file_complete(counters[i]) 

331 if file_obj: 331 ↛ 329line 331 didn't jump to line 329, because the condition on line 331 was never false

332 # If it returns a file object, then set the files dict. 

333 self._files.appendlist( 

334 force_str(old_field_name, self._encoding, errors="replace"), 

335 file_obj, 

336 ) 

337 break 

338 

339 def sanitize_file_name(self, file_name): 

340 """ 

341 Sanitize the filename of an upload. 

342 

343 Remove all possible path separators, even though that might remove more 

344 than actually required by the target system. Filenames that could 

345 potentially cause problems (current/parent dir) are also discarded. 

346 

347 It should be noted that this function could still return a "filepath" 

348 like "C:some_file.txt" which is handled later on by the storage layer. 

349 So while this function does sanitize filenames to some extent, the 

350 resulting filename should still be considered as untrusted user input. 

351 """ 

352 file_name = html.unescape(file_name) 

353 file_name = file_name.rsplit("/")[-1] 

354 file_name = file_name.rsplit("\\")[-1] 

355 

356 if file_name in {"", ".", ".."}: 356 ↛ 357line 356 didn't jump to line 357, because the condition on line 356 was never true

357 return None 

358 return file_name 

359 

360 IE_sanitize = sanitize_file_name 

361 

362 def _close_files(self): 

363 # Free up all file handles. 

364 # FIXME: this currently assumes that upload handlers store the file as 'file' 

365 # We should document that... 

366 # (Maybe add handler.free_file to complement new_file) 

367 for handler in self._upload_handlers: 

368 if hasattr(handler, "file"): 

369 handler.file.close() 

370 

371 

372class LazyStream: 

373 """ 

374 The LazyStream wrapper allows one to get and "unget" bytes from a stream. 

375 

376 Given a producer object (an iterator that yields bytestrings), the 

377 LazyStream object will support iteration, reading, and keeping a "look-back" 

378 variable in case you need to "unget" some bytes. 

379 """ 

380 

381 def __init__(self, producer, length=None): 

382 """ 

383 Every LazyStream must have a producer when instantiated. 

384 

385 A producer is an iterable that returns a string each time it 

386 is called. 

387 """ 

388 self._producer = producer 

389 self._empty = False 

390 self._leftover = b"" 

391 self.length = length 

392 self.position = 0 

393 self._remaining = length 

394 self._unget_history = [] 

395 

396 def tell(self): 

397 return self.position 

398 

399 def read(self, size=None): 

400 def parts(): 

401 remaining = self._remaining if size is None else size 

402 # do the whole thing in one shot if no limit was provided. 

403 if remaining is None: 403 ↛ 404line 403 didn't jump to line 404, because the condition on line 403 was never true

404 yield b"".join(self) 

405 return 

406 

407 # otherwise do some bookkeeping to return exactly enough 

408 # of the stream and stashing any extra content we get from 

409 # the producer 

410 while remaining != 0: 

411 assert remaining > 0, "remaining bytes to read should never go negative" 

412 

413 try: 

414 chunk = next(self) 

415 except StopIteration: 

416 return 

417 else: 

418 emitting = chunk[:remaining] 

419 self.unget(chunk[remaining:]) 

420 remaining -= len(emitting) 

421 yield emitting 

422 

423 return b"".join(parts()) 

424 

425 def __next__(self): 

426 """ 

427 Used when the exact number of bytes to read is unimportant. 

428 

429 Return whatever chunk is conveniently returned from the iterator. 

430 Useful to avoid unnecessary bookkeeping if performance is an issue. 

431 """ 

432 if self._leftover: 

433 output = self._leftover 

434 self._leftover = b"" 

435 else: 

436 output = next(self._producer) 

437 self._unget_history = [] 

438 self.position += len(output) 

439 return output 

440 

441 def close(self): 

442 """ 

443 Used to invalidate/disable this lazy stream. 

444 

445 Replace the producer with an empty list. Any leftover bytes that have 

446 already been read will still be reported upon read() and/or next(). 

447 """ 

448 self._producer = [] 

449 

450 def __iter__(self): 

451 return self 

452 

453 def unget(self, bytes): 

454 """ 

455 Place bytes back onto the front of the lazy stream. 

456 

457 Future calls to read() will return those bytes first. The 

458 stream position and thus tell() will be rewound. 

459 """ 

460 if not bytes: 

461 return 

462 self._update_unget_history(len(bytes)) 

463 self.position -= len(bytes) 

464 self._leftover = bytes + self._leftover 

465 

466 def _update_unget_history(self, num_bytes): 

467 """ 

468 Update the unget history as a sanity check to see if we've pushed 

469 back the same number of bytes in one chunk. If we keep ungetting the 

470 same number of bytes many times (here, 50), we're mostly likely in an 

471 infinite loop of some sort. This is usually caused by a 

472 maliciously-malformed MIME request. 

473 """ 

474 self._unget_history = [num_bytes] + self._unget_history[:49] 

475 number_equal = len( 

476 [ 

477 current_number 

478 for current_number in self._unget_history 

479 if current_number == num_bytes 

480 ] 

481 ) 

482 

483 if number_equal > 40: 483 ↛ 484line 483 didn't jump to line 484, because the condition on line 483 was never true

484 raise SuspiciousMultipartForm( 

485 "The multipart parser got stuck, which shouldn't happen with" 

486 " normal uploaded files. Check for malicious upload activity;" 

487 " if there is none, report this to the Django developers." 

488 ) 

489 

490 

491class ChunkIter: 

492 """ 

493 An iterable that will yield chunks of data. Given a file-like object as the 

494 constructor, yield chunks of read operations from that object. 

495 """ 

496 

497 def __init__(self, flo, chunk_size=64 * 1024): 

498 self.flo = flo 

499 self.chunk_size = chunk_size 

500 

501 def __next__(self): 

502 try: 

503 data = self.flo.read(self.chunk_size) 

504 except InputStreamExhausted: 

505 raise StopIteration() 

506 if data: 

507 return data 

508 else: 

509 raise StopIteration() 

510 

511 def __iter__(self): 

512 return self 

513 

514 

515class InterBoundaryIter: 

516 """ 

517 A Producer that will iterate over boundaries. 

518 """ 

519 

520 def __init__(self, stream, boundary): 

521 self._stream = stream 

522 self._boundary = boundary 

523 

524 def __iter__(self): 

525 return self 

526 

527 def __next__(self): 

528 try: 

529 return LazyStream(BoundaryIter(self._stream, self._boundary)) 

530 except InputStreamExhausted: 

531 raise StopIteration() 

532 

533 

534class BoundaryIter: 

535 """ 

536 A Producer that is sensitive to boundaries. 

537 

538 Will happily yield bytes until a boundary is found. Will yield the bytes 

539 before the boundary, throw away the boundary bytes themselves, and push the 

540 post-boundary bytes back on the stream. 

541 

542 The future calls to next() after locating the boundary will raise a 

543 StopIteration exception. 

544 """ 

545 

546 def __init__(self, stream, boundary): 

547 self._stream = stream 

548 self._boundary = boundary 

549 self._done = False 

550 # rollback an additional six bytes because the format is like 

551 # this: CRLF<boundary>[--CRLF] 

552 self._rollback = len(boundary) + 6 

553 

554 # Try to use mx fast string search if available. Otherwise 

555 # use Python find. Wrap the latter for consistency. 

556 unused_char = self._stream.read(1) 

557 if not unused_char: 

558 raise InputStreamExhausted() 

559 self._stream.unget(unused_char) 

560 

561 def __iter__(self): 

562 return self 

563 

564 def __next__(self): 

565 if self._done: 

566 raise StopIteration() 

567 

568 stream = self._stream 

569 rollback = self._rollback 

570 

571 bytes_read = 0 

572 chunks = [] 

573 for bytes in stream: 

574 bytes_read += len(bytes) 

575 chunks.append(bytes) 

576 if bytes_read > rollback: 

577 break 

578 if not bytes: 578 ↛ 579line 578 didn't jump to line 579, because the condition on line 578 was never true

579 break 

580 else: 

581 self._done = True 

582 

583 if not chunks: 583 ↛ 584line 583 didn't jump to line 584, because the condition on line 583 was never true

584 raise StopIteration() 

585 

586 chunk = b"".join(chunks) 

587 boundary = self._find_boundary(chunk) 

588 

589 if boundary: 

590 end, next = boundary 

591 stream.unget(chunk[next:]) 

592 self._done = True 

593 return chunk[:end] 

594 else: 

595 # make sure we don't treat a partial boundary (and 

596 # its separators) as data 

597 if not chunk[:-rollback]: # and len(chunk) >= (len(self._boundary) + 6): 597 ↛ 602line 597 didn't jump to line 602, because the condition on line 597 was never false

598 # There's nothing left, we should just return and mark as done. 

599 self._done = True 

600 return chunk 

601 else: 

602 stream.unget(chunk[-rollback:]) 

603 return chunk[:-rollback] 

604 

605 def _find_boundary(self, data): 

606 """ 

607 Find a multipart boundary in data. 

608 

609 Should no boundary exist in the data, return None. Otherwise, return 

610 a tuple containing the indices of the following: 

611 * the end of current encapsulation 

612 * the start of the next encapsulation 

613 """ 

614 index = data.find(self._boundary) 

615 if index < 0: 

616 return None 

617 else: 

618 end = index 

619 next = index + len(self._boundary) 

620 # backup over CRLF 

621 last = max(0, end - 1) 

622 if data[last : last + 1] == b"\n": 

623 end -= 1 

624 last = max(0, end - 1) 

625 if data[last : last + 1] == b"\r": 

626 end -= 1 

627 return end, next 

628 

629 

630def exhaust(stream_or_iterable): 

631 """Exhaust an iterator or stream.""" 

632 try: 

633 iterator = iter(stream_or_iterable) 

634 except TypeError: 

635 iterator = ChunkIter(stream_or_iterable, 16384) 

636 collections.deque(iterator, maxlen=0) # consume iterator quickly. 

637 

638 

639def parse_boundary_stream(stream, max_header_size): 

640 """ 

641 Parse one and exactly one stream that encapsulates a boundary. 

642 """ 

643 # Stream at beginning of header, look for end of header 

644 # and parse it if found. The header must fit within one 

645 # chunk. 

646 chunk = stream.read(max_header_size) 

647 

648 # 'find' returns the top of these four bytes, so we'll 

649 # need to munch them later to prevent them from polluting 

650 # the payload. 

651 header_end = chunk.find(b"\r\n\r\n") 

652 

653 def _parse_header(line): 

654 main_value_pair, params = parse_header(line) 

655 try: 

656 name, value = main_value_pair.split(":", 1) 

657 except ValueError: 

658 raise ValueError("Invalid header: %r" % line) 

659 return name, (value, params) 

660 

661 if header_end == -1: 

662 # we find no header, so we just mark this fact and pass on 

663 # the stream verbatim 

664 stream.unget(chunk) 

665 return (RAW, {}, stream) 

666 

667 header = chunk[:header_end] 

668 

669 # here we place any excess chunk back onto the stream, as 

670 # well as throwing away the CRLFCRLF bytes from above. 

671 stream.unget(chunk[header_end + 4 :]) 

672 

673 TYPE = RAW 

674 outdict = {} 

675 

676 # Eliminate blank lines 

677 for line in header.split(b"\r\n"): 

678 # This terminology ("main value" and "dictionary of 

679 # parameters") is from the Python docs. 

680 try: 

681 name, (value, params) = _parse_header(line) 

682 except ValueError: 

683 continue 

684 

685 if name == "content-disposition": 

686 TYPE = FIELD 

687 if params.get("filename"): 

688 TYPE = FILE 

689 

690 outdict[name] = value, params 

691 

692 if TYPE == RAW: 692 ↛ 693line 692 didn't jump to line 693, because the condition on line 692 was never true

693 stream.unget(chunk) 

694 

695 return (TYPE, outdict, stream) 

696 

697 

698class Parser: 

699 def __init__(self, stream, boundary): 

700 self._stream = stream 

701 self._separator = b"--" + boundary 

702 

703 def __iter__(self): 

704 boundarystream = InterBoundaryIter(self._stream, self._separator) 

705 for sub_stream in boundarystream: 

706 # Iterate over each part 

707 yield parse_boundary_stream(sub_stream, 1024) 

708 

709 

710def parse_header(line): 

711 """ 

712 Parse the header into a key-value. 

713 

714 Input (line): bytes, output: str for key/name, bytes for values which 

715 will be decoded later. 

716 """ 

717 plist = _parse_header_params(b";" + line) 

718 key = plist.pop(0).lower().decode("ascii") 

719 pdict = {} 

720 for p in plist: 

721 i = p.find(b"=") 

722 if i >= 0: 722 ↛ 720line 722 didn't jump to line 720, because the condition on line 722 was never false

723 has_encoding = False 

724 name = p[:i].strip().lower().decode("ascii") 

725 if name.endswith("*"): 725 ↛ 728line 725 didn't jump to line 728, because the condition on line 725 was never true

726 # Lang/encoding embedded in the value (like "filename*=UTF-8''file.ext") 

727 # http://tools.ietf.org/html/rfc2231#section-4 

728 name = name[:-1] 

729 if p.count(b"'") == 2: 

730 has_encoding = True 

731 value = p[i + 1 :].strip() 

732 if len(value) >= 2 and value[:1] == value[-1:] == b'"': 

733 value = value[1:-1] 

734 value = value.replace(b"\\\\", b"\\").replace(b'\\"', b'"') 

735 if has_encoding: 735 ↛ 736line 735 didn't jump to line 736, because the condition on line 735 was never true

736 encoding, lang, value = value.split(b"'") 

737 value = unquote(value.decode(), encoding=encoding.decode()) 

738 pdict[name] = value 

739 return key, pdict 

740 

741 

742def _parse_header_params(s): 

743 plist = [] 

744 while s[:1] == b";": 

745 s = s[1:] 

746 end = s.find(b";") 

747 while end > 0 and s.count(b'"', 0, end) % 2: 747 ↛ 748line 747 didn't jump to line 748, because the condition on line 747 was never true

748 end = s.find(b";", end + 1) 

749 if end < 0: 

750 end = len(s) 

751 f = s[:end] 

752 plist.append(f.strip()) 

753 s = s[end:] 

754 return plist