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
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Multi-part parsing for file uploads.
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
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
24__all__ = ("MultiPartParser", "MultiPartParserError", "InputStreamExhausted")
27class MultiPartParserError(Exception):
28 pass
31class InputStreamExhausted(Exception):
32 """
33 No more reads are allowed from this device.
34 """
36 pass
39RAW = "raw"
40FILE = "file"
41FIELD = "field"
44class MultiPartParser:
45 """
46 A rfc2388 multipart/form-data parser.
48 ``MultiValueDict.parse()`` reads the input stream in ``chunk_size`` chunks
49 and returns a tuple of ``(MultiValueDict(POST), MultiValueDict(FILES))``.
50 """
52 def __init__(self, META, input_data, upload_handlers, encoding=None):
53 """
54 Initialize the MultiPartParser object.
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)
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 )
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
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)
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
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)
106 self._meta = META
107 self._encoding = encoding or settings.DEFAULT_CHARSET
108 self._content_length = content_length
109 self._upload_handlers = upload_handlers
111 def parse(self):
112 """
113 Parse the POST data and break it into a FILES MultiValueDict and a POST
114 MultiValueDict.
116 Return a tuple containing the POST and FILES dictionary, respectively.
117 """
118 from django.http import QueryDict
120 encoding = self._encoding
121 handlers = self._upload_handlers
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()
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]
142 # Create the data structures to be used later.
143 self._post = QueryDict(mutable=True)
144 self._files = MultiValueDict()
146 # Instantiate the parser and stream:
147 stream = LazyStream(ChunkIter(self._input_data, self._chunk_size))
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)
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
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
172 try:
173 disposition = meta_data["content-disposition"][1]
174 field_name = disposition["name"].strip()
175 except (KeyError, IndexError, AttributeError):
176 continue
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")
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 )
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 )
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)
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 )
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
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")
243 try:
244 content_length = int(meta_data.get("content-length")[0])
245 except (IndexError, TypeError, ValueError):
246 content_length = None
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
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.
270 stripped_chunk = b"".join(chunk.split())
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
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
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
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)
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
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
339 def sanitize_file_name(self, file_name):
340 """
341 Sanitize the filename of an upload.
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.
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]
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
360 IE_sanitize = sanitize_file_name
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()
372class LazyStream:
373 """
374 The LazyStream wrapper allows one to get and "unget" bytes from a stream.
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 """
381 def __init__(self, producer, length=None):
382 """
383 Every LazyStream must have a producer when instantiated.
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 = []
396 def tell(self):
397 return self.position
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
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"
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
423 return b"".join(parts())
425 def __next__(self):
426 """
427 Used when the exact number of bytes to read is unimportant.
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
441 def close(self):
442 """
443 Used to invalidate/disable this lazy stream.
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 = []
450 def __iter__(self):
451 return self
453 def unget(self, bytes):
454 """
455 Place bytes back onto the front of the lazy stream.
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
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 )
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 )
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 """
497 def __init__(self, flo, chunk_size=64 * 1024):
498 self.flo = flo
499 self.chunk_size = chunk_size
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()
511 def __iter__(self):
512 return self
515class InterBoundaryIter:
516 """
517 A Producer that will iterate over boundaries.
518 """
520 def __init__(self, stream, boundary):
521 self._stream = stream
522 self._boundary = boundary
524 def __iter__(self):
525 return self
527 def __next__(self):
528 try:
529 return LazyStream(BoundaryIter(self._stream, self._boundary))
530 except InputStreamExhausted:
531 raise StopIteration()
534class BoundaryIter:
535 """
536 A Producer that is sensitive to boundaries.
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.
542 The future calls to next() after locating the boundary will raise a
543 StopIteration exception.
544 """
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
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)
561 def __iter__(self):
562 return self
564 def __next__(self):
565 if self._done:
566 raise StopIteration()
568 stream = self._stream
569 rollback = self._rollback
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
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()
586 chunk = b"".join(chunks)
587 boundary = self._find_boundary(chunk)
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]
605 def _find_boundary(self, data):
606 """
607 Find a multipart boundary in data.
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
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.
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)
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")
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)
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)
667 header = chunk[:header_end]
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 :])
673 TYPE = RAW
674 outdict = {}
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
685 if name == "content-disposition":
686 TYPE = FIELD
687 if params.get("filename"):
688 TYPE = FILE
690 outdict[name] = value, params
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)
695 return (TYPE, outdict, stream)
698class Parser:
699 def __init__(self, stream, boundary):
700 self._stream = stream
701 self._separator = b"--" + boundary
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)
710def parse_header(line):
711 """
712 Parse the header into a key-value.
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
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