Skip to content

Commit a16be67

Browse files
authored
Merge branch '3.14' into backport-1259f16-3.14
2 parents 0ba3386 + d67f2a9 commit a16be67

27 files changed

Lines changed: 607 additions & 86 deletions

‎Doc/library/numbers.rst‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ them to the same values. This may be subtle if there are two different
9393
extensions of the real numbers. See also :ref:`numeric-hash`.
9494

9595

96-
Adding More Numeric ABCs
96+
Adding more numeric ABCs
9797
~~~~~~~~~~~~~~~~~~~~~~~~
9898

9999
There are, of course, more possible ABCs for numbers, and this would
@@ -143,11 +143,12 @@ defined as::
143143

144144

145145
There are 5 different cases for a mixed-type operation on subclasses
146-
of :class:`Complex`. I'll refer to all of the above code that doesn't
147-
refer to ``MyIntegral`` and ``OtherTypeIKnowAbout`` as
148-
"boilerplate". ``a`` will be an instance of ``A``, which is a subtype
149-
of :class:`Complex` (``a : A <: Complex``), and ``b : B <:
150-
Complex``. I'll consider ``a + b``:
146+
of :class:`Complex`. All of the above code that doesn't refer to
147+
``MyIntegral`` and ``OtherTypeIKnowAbout`` is referred to as "boilerplate".
148+
149+
In this description, ``a`` is an instance of ``A`` and ``b`` is an
150+
instance of ``B``, where ``A`` and ``B`` are both subtypes of :class:`Complex`.
151+
Consider ``a + b``:
151152

152153
1. If ``A`` defines an :meth:`~object.__add__` which accepts ``b``, all is
153154
well.
@@ -162,13 +163,14 @@ Complex``. I'll consider ``a + b``:
162163
4. If it falls back to the boilerplate, there are no more possible
163164
methods to try, so this is where the default implementation
164165
should live.
165-
5. If ``B <: A``, Python tries ``B.__radd__`` before
166+
5. If ``B`` is a subtype of ``A``, Python tries ``B.__radd__`` before
166167
``A.__add__``. This is ok, because it was implemented with
167168
knowledge of ``A``, so it can handle those instances before
168169
delegating to :class:`Complex`.
169170

170-
If ``A <: Complex`` and ``B <: Real`` without sharing any other knowledge,
171-
then the appropriate shared operation is the one involving the built
171+
If ``A`` is a subtype of :class:`Complex` and ``B`` is a subtype of
172+
:class:`Real` without sharing any other knowledge, then the appropriate
173+
shared operation is the one involving the built
172174
in :class:`complex`, and both :meth:`~object.__radd__` s land there, so ``a+b
173175
== b+a``.
174176

‎Lib/bz2.py‎

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
# Value 2 no longer used
2323
_MODE_WRITE = 3
2424

25+
# Every bzip2 stream starts with one of these, "BZh" and the block size.
26+
_STREAM_MAGIC = (b'BZh1', b'BZh2', b'BZh3', b'BZh4', b'BZh5', b'BZh6', b'BZh7', b'BZh8', b'BZh9')
27+
2528

2629
class BZ2File(_streams.BaseStream):
2730

@@ -89,7 +92,8 @@ def __init__(self, filename, mode="r", *, compresslevel=9):
8992

9093
if self._mode == _MODE_READ:
9194
raw = _streams.DecompressReader(self._fp,
92-
BZ2Decompressor, trailing_error=OSError)
95+
BZ2Decompressor, trailing_error=OSError,
96+
stream_magic=_STREAM_MAGIC)
9397
self._buffer = io.BufferedReader(raw)
9498
else:
9599
self._pos = 0
@@ -334,19 +338,8 @@ def decompress(data):
334338
335339
For incremental decompression, use a BZ2Decompressor object instead.
336340
"""
337-
results = []
338-
while data:
339-
decomp = BZ2Decompressor()
340-
try:
341-
res = decomp.decompress(data)
342-
except OSError:
343-
if results:
344-
break # Leftover data is not a valid bzip2 stream; ignore it.
345-
else:
346-
raise # Error on the first iteration; bail out.
347-
results.append(res)
348-
if not decomp.eof:
349-
raise ValueError("Compressed data ended before the "
350-
"end-of-stream marker was reached")
351-
data = decomp.unused_data
352-
return b"".join(results)
341+
if not data:
342+
return b""
343+
return _streams._decompress_all(data, BZ2Decompressor, OSError,
344+
stream_magic=_STREAM_MAGIC,
345+
truncated_error=ValueError)

‎Lib/compression/_common/_streams.py‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class DecompressReader(io.RawIOBase):
3636
def readable(self):
3737
return True
3838

39-
def __init__(self, fp, decomp_factory, trailing_error=(), **decomp_args):
39+
def __init__(self, fp, decomp_factory, trailing_error=(), *,
40+
stream_magic=(), **decomp_args):
4041
self._fp = fp
4142
self._eof = False
4243
self._pos = 0 # Current offset in decompressed stream
@@ -56,6 +57,11 @@ def __init__(self, fp, decomp_factory, trailing_error=(), **decomp_args):
5657
# trailing data to ignore
5758
self._trailing_error = trailing_error
5859

60+
# Prefix (or tuple of prefixes) that every compressed stream starts
61+
# with, if the format has one. Data starting with it is a damaged
62+
# stream rather than trailing data, so its error is not ignored.
63+
self._stream_magic = stream_magic
64+
5965
def close(self):
6066
self._decompressor = None
6167
return super().close()
@@ -90,6 +96,9 @@ def read(self, size=-1):
9096
try:
9197
data = self._decompressor.decompress(rawblock, size)
9298
except self._trailing_error:
99+
if (self._stream_magic
100+
and rawblock.startswith(self._stream_magic)):
101+
raise # Damaged stream, not trailing data.
93102
# Trailing data isn't a valid compressed stream; ignore it.
94103
break
95104
else:
@@ -160,3 +169,60 @@ def seek(self, offset, whence=io.SEEK_SET):
160169
def tell(self):
161170
"""Return the current file position."""
162171
return self._pos
172+
173+
174+
def _decompress_all(data, decomp_factory, trailing_error=(), *,
175+
stream_magic=(), truncated_error, **decomp_args):
176+
"""Decompress all of the concatenated streams in *data*.
177+
178+
Each stream is decompressed by a new decompressor, made by calling
179+
*decomp_factory* with *decomp_args*. As in DecompressReader, an error
180+
of class *trailing_error* in a stream after the first one ends the
181+
data, unless that stream starts with *stream_magic*. A stream without
182+
an end-of-stream marker raises *truncated_error*.
183+
"""
184+
results = []
185+
# The views are released on exit, so that the caller can resize a
186+
# bytearray argument again, also while handling an exception.
187+
with memoryview(data) as view, view.cast("B") as data:
188+
pos = 0
189+
# There is usually a single stream: the first chunk is all of the
190+
# data, to decompress it in one go.
191+
chunk_size = len(data)
192+
while True:
193+
decomp = decomp_factory(**decomp_args)
194+
start = pos
195+
res = []
196+
try:
197+
# Feed the stream in chunks of increasing size, so that what
198+
# is read past its end (and copied into unused_data) is
199+
# bounded by the size of the stream, not by the size of all
200+
# the streams that follow it.
201+
while not decomp.eof and pos < len(data):
202+
with data[pos:pos + chunk_size] as chunk:
203+
res.append(decomp.decompress(chunk))
204+
pos += len(chunk)
205+
chunk_size *= 2
206+
except trailing_error:
207+
if not results:
208+
raise # Error in the first stream; bail out.
209+
if stream_magic:
210+
if isinstance(stream_magic, bytes):
211+
stream_magic = (stream_magic,)
212+
size = max(map(len, stream_magic))
213+
with data[start:start + size] as head:
214+
if head.tobytes().startswith(stream_magic):
215+
raise # Damaged stream, not trailing data.
216+
# Trailing data isn't a valid compressed stream; ignore it.
217+
break
218+
results.extend(res)
219+
if not decomp.eof:
220+
raise truncated_error("Compressed data ended before the "
221+
"end-of-stream marker was reached")
222+
pos -= len(decomp.unused_data)
223+
if pos == len(data):
224+
break
225+
# Later streams start small: a larger first chunk reads further
226+
# past the end of a short stream, a smaller one needs more calls.
227+
chunk_size = 1024
228+
return b"".join(results)

‎Lib/compression/zstd/__init__.py‎

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import enum
3131
from _zstd import (ZstdCompressor, ZstdDecompressor, ZstdDict, ZstdError,
3232
get_frame_size, zstd_version)
33+
from compression._common import _streams
3334
from compression.zstd._zstdfile import ZstdFile, open, _nbytes
3435

3536
# zstd_version_number is (MAJOR * 100 * 100 + MINOR * 100 + RELEASE)
@@ -161,17 +162,9 @@ def decompress(data, zstd_dict=None, options=None):
161162
162163
For incremental decompression, use a ZstdDecompressor instead.
163164
"""
164-
results = []
165-
while True:
166-
decomp = ZstdDecompressor(options=options, zstd_dict=zstd_dict)
167-
results.append(decomp.decompress(data))
168-
if not decomp.eof:
169-
raise ZstdError('Compressed data ended before the '
170-
'end-of-stream marker was reached')
171-
data = decomp.unused_data
172-
if not data:
173-
break
174-
return b''.join(results)
165+
return _streams._decompress_all(data, ZstdDecompressor,
166+
truncated_error=ZstdError,
167+
options=options, zstd_dict=zstd_dict)
175168

176169

177170
class CompressionParameter(enum.IntEnum):

‎Lib/gzip.py‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -650,24 +650,47 @@ def decompress(data):
650650
Return the decompressed string.
651651
"""
652652
decompressed_members = []
653+
fp = io.BytesIO(data)
654+
# There is usually a single member: the first chunk is all of the data,
655+
# to decompress it in one go.
656+
chunk_size = len(data)
653657
while True:
654-
fp = io.BytesIO(data)
655658
if _read_gzip_header(fp) is None:
656659
return b"".join(decompressed_members)
657660
# Use a zlib raw deflate compressor
658661
do = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
659-
# Read all the data except the header
660-
decompressed = do.decompress(data[fp.tell():])
661-
if not do.eof or len(do.unused_data) < 8:
662+
crc = zlib.crc32(b"")
663+
length = 0
664+
# Feed the member in chunks of increasing size, so that what is read
665+
# past its end (and copied into unused_data) is bounded by the size
666+
# of the member, not by the size of all the members that follow it.
667+
while not do.eof and (chunk := fp.read(chunk_size)):
668+
decompressed = do.decompress(chunk)
669+
crc = zlib.crc32(decompressed, crc)
670+
length += len(decompressed)
671+
decompressed_members.append(decompressed)
672+
chunk_size *= 2
673+
# Later members start small: a larger first chunk reads further past
674+
# the end of a short member, a smaller one needs more calls.
675+
chunk_size = 1024
676+
# Rewind to the end of the deflate stream and read the trailer
677+
fp.seek(-len(do.unused_data), io.SEEK_CUR)
678+
trailer = fp.read(8)
679+
if not do.eof or len(trailer) < 8:
662680
raise EOFError("Compressed file ended before the end-of-stream "
663681
"marker was reached")
664-
crc, length = struct.unpack("<II", do.unused_data[:8])
665-
if crc != zlib.crc32(decompressed):
682+
crc32, isize = struct.unpack("<II", trailer)
683+
if crc32 != crc:
666684
raise BadGzipFile("CRC check failed")
667-
if length != (len(decompressed) & 0xffffffff):
685+
if isize != (length & 0xffffffff):
668686
raise BadGzipFile("Incorrect length of data produced")
669-
decompressed_members.append(decompressed)
670-
data = do.unused_data[8:].lstrip(b"\x00")
687+
# Gzip files can be padded with zeroes and still have archives.
688+
# Consume all zero bytes and set the file position to the first
689+
# non-zero byte.
690+
while chunk := fp.read(chunk_size):
691+
if chunk := chunk.lstrip(b"\x00"):
692+
fp.seek(-len(chunk), io.SEEK_CUR)
693+
break
671694

672695

673696
def main():

‎Lib/lzma.py‎

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@
3535
_MODE_WRITE = 3
3636

3737

38+
def _stream_magic(format):
39+
# Every stream of an .xz file starts with these bytes. The other
40+
# formats, including an .lzma stream read with FORMAT_AUTO, have
41+
# nothing that tells a damaged stream from trailing data.
42+
return b"\xfd7zXZ\x00" if format in (FORMAT_AUTO, FORMAT_XZ) else b""
43+
44+
3845
class LZMAFile(_streams.BaseStream):
3946

4047
"""A file object providing transparent LZMA (de)compression.
@@ -128,7 +135,8 @@ def __init__(self, filename=None, mode="r", *,
128135

129136
if self._mode == _MODE_READ:
130137
raw = _streams.DecompressReader(self._fp, LZMADecompressor,
131-
trailing_error=LZMAError, format=format, filters=filters)
138+
trailing_error=LZMAError, stream_magic=_stream_magic(format),
139+
format=format, filters=filters)
132140
self._buffer = io.BufferedReader(raw)
133141

134142
def close(self):
@@ -344,21 +352,8 @@ def decompress(data, format=FORMAT_AUTO, memlimit=None, filters=None):
344352
345353
For incremental decompression, use an LZMADecompressor instead.
346354
"""
347-
results = []
348-
while True:
349-
decomp = LZMADecompressor(format, memlimit, filters)
350-
try:
351-
res = decomp.decompress(data)
352-
except LZMAError:
353-
if results:
354-
break # Leftover data is not a valid LZMA/XZ stream; ignore it.
355-
else:
356-
raise # Error on the first iteration; bail out.
357-
results.append(res)
358-
if not decomp.eof:
359-
raise LZMAError("Compressed data ended before the "
360-
"end-of-stream marker was reached")
361-
data = decomp.unused_data
362-
if not data:
363-
break
364-
return b"".join(results)
355+
return _streams._decompress_all(data, LZMADecompressor, LZMAError,
356+
stream_magic=_stream_magic(format),
357+
truncated_error=LZMAError,
358+
format=format, memlimit=memlimit,
359+
filters=filters)

‎Lib/tarfile.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2707,6 +2707,13 @@ def _extract_member(self, tarinfo, targetpath, set_attrs=True,
27072707
else:
27082708
self.makefile(tarinfo, targetpath)
27092709

2710+
if tarinfo.islnk() and os.path.islink(targetpath):
2711+
# makelink_with_filter() fell back to extracting a copy of a
2712+
# symlink member, which already has its own attributes set.
2713+
# Setting the hard link's attributes here would follow the
2714+
# symlink, possibly to a file outside the destination directory.
2715+
set_attrs = False
2716+
27102717
if set_attrs:
27112718
self.chown(tarinfo, targetpath, numeric_owner)
27122719
if not tarinfo.issym():

0 commit comments

Comments
 (0)