Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 60 additions & 36 deletions Lib/_pyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ def nreadahead():
res += b
if res.endswith(b"\n"):
break
return bytes(res)
return res.take_bytes()

def __iter__(self):
self._checkClosed()
Expand Down Expand Up @@ -624,15 +624,15 @@ def read(self, size=-1):
if n < 0 or n > len(b):
raise ValueError(f"readinto returned {n} outside buffer size {len(b)}")
del b[n:]
return bytes(b)
return b.take_bytes()

def readall(self):
"""Read until EOF, using multiple read() call."""
res = bytearray()
while data := self.read(DEFAULT_BUFFER_SIZE):
res += data
if res:
return bytes(res)
return res.take_bytes()
else:
# b'' or None
return data
Expand Down Expand Up @@ -882,16 +882,28 @@ class BytesIO(BufferedIOBase):
_buffer = None

def __init__(self, initial_bytes=None):
# Use to keep self._buffer and self._pos consistent.
self._lock = Lock()

buf = bytearray()
if initial_bytes is not None:
buf += initial_bytes
self._buffer = buf
self._pos = 0

with self._lock:
self._buffer = buf
self._pos = 0

def __getstate__(self):
if self.closed:
raise ValueError("__getstate__ on closed file")
return self.__dict__.copy()
with self._lock:
state = self.__dict__.copy()
del state['_lock']
return state

def __setstate__(self, state):
self.__dict__.update(state)
self._lock = Lock()

def getvalue(self):
"""Return the bytes value (contents) of the buffer
Expand Down Expand Up @@ -924,14 +936,16 @@ def read(self, size=-1):
raise TypeError(f"{size!r} is not an integer")
else:
size = size_index()
if size < 0:
size = len(self._buffer)
if len(self._buffer) <= self._pos:
return b""
newpos = min(len(self._buffer), self._pos + size)
b = self._buffer[self._pos : newpos]
self._pos = newpos
return bytes(b)

with self._lock:
if size < 0:
size = len(self._buffer)
if len(self._buffer) <= self._pos:
return b""
newpos = min(len(self._buffer), self._pos + size)
b = self._buffer[self._pos : newpos]
self._pos = newpos
return bytes(b)

def read1(self, size=-1):
"""This is the same as read.
Expand All @@ -949,13 +963,14 @@ def write(self, b):
if n == 0:
return 0

pos = self._pos
if pos > len(self._buffer):
# Pad buffer to pos with null bytes.
self._buffer.resize(pos)
self._buffer[pos:pos + n] = view
self._pos += n
return n
with self._lock:
pos = self._pos
if pos > len(self._buffer):
# Pad buffer to pos with null bytes.
self._buffer.resize(pos)
self._buffer[pos:pos + n] = view
self._pos += n
return n

def seek(self, pos, whence=0):
if self.closed:
Expand All @@ -971,9 +986,11 @@ def seek(self, pos, whence=0):
raise ValueError("negative seek position %r" % (pos,))
self._pos = pos
elif whence == 1:
self._pos = max(0, self._pos + pos)
with self._lock:
self._pos = max(0, self._pos + pos)
elif whence == 2:
self._pos = max(0, len(self._buffer) + pos)
with self._lock:
self._pos = max(0, len(self._buffer) + pos)
else:
raise ValueError("unsupported whence value")
return self._pos
Expand All @@ -986,18 +1003,20 @@ def tell(self):
def truncate(self, pos=None):
if self.closed:
raise ValueError("truncate on closed file")
if pos is None:
pos = self._pos
else:
try:
pos_index = pos.__index__
except AttributeError:
raise TypeError(f"{pos!r} is not an integer")

with self._lock:
if pos is None:
pos = self._pos
else:
pos = pos_index()
if pos < 0:
raise ValueError("negative truncate position %r" % (pos,))
del self._buffer[pos:]
try:
pos_index = pos.__index__
except AttributeError:
raise TypeError(f"{pos!r} is not an integer")
else:
pos = pos_index()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve truncate indices before locking BytesIO

When pos.__index__() calls back into the same buffer, for example by invoking b.read(1), truncate() already owns the non-reentrant _thread.lock and the callback waits on that same lock forever. Resolve the user-defined index before entering the critical section, as seek() does, and lock only the state access and mutation.

Useful? React with 👍 / 👎.

if pos < 0:
raise ValueError("negative truncate position %r" % (pos,))
del self._buffer[pos:]
return pos

def readable(self):
Expand Down Expand Up @@ -1486,6 +1505,7 @@ class FileIO(RawIOBase):
_writable = False
_appending = False
_seekable = None
_truncate = False
_closefd = True

def __init__(self, file, mode='r', closefd=True, opener=None):
Expand Down Expand Up @@ -1546,6 +1566,7 @@ def __init__(self, file, mode='r', closefd=True, opener=None):
flags = 0
elif 'w' in mode:
self._writable = True
self._truncate = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset truncation state when reinitializing FileIO

When the same pure-Python FileIO is first initialized with w+ and then reinitialized with f.__init__(..., 'r+'), this assignment leaves _truncate true because initialization never clears it. The second handle therefore reports wb+ rather than rb+, and consumers such as GzipFile can consequently infer writing and overwrite data; reset this mode-specific state before parsing every new mode.

Useful? React with 👍 / 👎.

flags = os.O_CREAT | os.O_TRUNC
elif 'a' in mode:
self._writable = True
Expand Down Expand Up @@ -1727,7 +1748,7 @@ def readall(self):
assert len(result) - bytes_read >= 1, \
"os.readinto buffer size 0 will result in erroneous EOF / returns 0"
result.resize(bytes_read)
return bytes(result)
return result.take_bytes()

def readinto(self, buffer):
"""Same as RawIOBase.readinto()."""
Expand Down Expand Up @@ -1871,7 +1892,10 @@ def mode(self):
return 'ab'
elif self._readable:
if self._writable:
return 'rb+'
if self._truncate:
return 'wb+'
else:
return 'rb+'
else:
return 'rb'
else:
Expand Down
93 changes: 64 additions & 29 deletions Lib/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
_WRITE_BUFFER_SIZE = 4 * io.DEFAULT_BUFFER_SIZE


def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_BEST,
def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_TRADEOFF,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document compression level 6 as the default

This changes the public default to _COMPRESS_LEVEL_TRADEOFF (6), but open() still says at line 42 that the default is 9, and the GzipFile constructor docstring repeats that claim. As a result, help(gzip.open) and help(gzip.GzipFile) promise maximum compression while calls actually use level 6; update both docstrings with the default change.

Useful? React with 👍 / 👎.

encoding=None, errors=None, newline=None):
"""Open a gzip-compressed file in binary or text mode.

Expand Down Expand Up @@ -158,7 +158,7 @@ class GzipFile(_streams.BaseStream):
myfileobj = None

def __init__(self, filename=None, mode=None,
compresslevel=_COMPRESS_LEVEL_BEST, fileobj=None, mtime=None):
compresslevel=_COMPRESS_LEVEL_TRADEOFF, fileobj=None, mtime=None):
"""Constructor for the GzipFile class.

At least one of fileobj and filename must be given a
Expand Down Expand Up @@ -189,9 +189,10 @@ def __init__(self, filename=None, mode=None,

The optional mtime argument is the timestamp requested by gzip.
The time is in Unix format, i.e., seconds since 00:00:00 UTC,
January 1, 1970. If mtime is omitted or None, the current time
is used. Use mtime = 0 to generate a compressed stream that does
not depend on creation time.
January 1, 1970. Set mtime to 0 to generate a compressed stream
that does not depend on creation time. If mtime is omitted or None,
the current time is used. If the resulting mtime is outside the
range 0 to 2**32-1, then the value 0 is used instead.

"""

Expand Down Expand Up @@ -270,8 +271,6 @@ def _init_write(self, filename):
self.name = filename
self.crc = zlib.crc32(b"")
self.size = 0
self.writebuf = []
self.bufsize = 0
self.offset = 0 # Current file offset for seek(), tell(), etc

def tell(self):
Expand Down Expand Up @@ -299,6 +298,8 @@ def _write_gzip_header(self, compresslevel):
mtime = self._write_mtime
if mtime is None:
mtime = time.time()
if not 0 <= mtime < 2**32:
mtime = 0
write32u(self.fileobj, int(mtime))
if compresslevel == _COMPRESS_LEVEL_BEST:
xfl = b'\002'
Expand Down Expand Up @@ -488,40 +489,73 @@ def _read_exact(fp, n):
return data


def _read_until_null(fp, crc=None):
'''Read until the first encountered null byte in fp.
If crc is not None, update and return the CRC.
'''
if crc is None:
while True:
s = fp.read(1)
if not s or s == b'\000':
break
else:
while True:
s = fp.read(1)
crc = zlib.crc32(s, crc)
if not s or s == b'\000':
break
return crc


def _read_gzip_header(fp):
'''Read a gzip header from `fp` and progress to the end of the header.

Returns last mtime if header was present or None otherwise.
'''
magic = fp.read(2)
if magic == b'':
if not magic:
return None

if magic != b'\037\213':
raise BadGzipFile('Not a gzipped file (%r)' % magic)

(method, flag, last_mtime) = struct.unpack("<BBIxx", _read_exact(fp, 8))
base_header = _read_exact(fp, 8)
(method, flag, last_mtime) = struct.unpack("<BBIxx", base_header)
if method != 8:
raise BadGzipFile('Unknown compression method')

# Most common cases are no flags (gzip.compress, zlib.compress) or only
# FNAME set (GzipFile, gzip command line application). Exit early
# in those cases.
if not flag:
return last_mtime
if flag == FNAME:
# Read and discard a null-terminated string containing the filename
_read_until_null(fp)
return last_mtime

# Processing for more complex flags. Save header parts for FHCRC checking.
if flag & FHCRC:
crc = zlib.crc32(magic + base_header)
else:
crc = None
if flag & FEXTRA:
# Read & discard the extra field, if present
extra_len, = struct.unpack("<H", _read_exact(fp, 2))
_read_exact(fp, extra_len)
extra_len_bytes = _read_exact(fp, 2)
extra_len, = struct.unpack("<H", extra_len_bytes)
extra = _read_exact(fp, extra_len)
if crc is not None:
crc = zlib.crc32(extra_len_bytes, crc)
crc = zlib.crc32(extra, crc)
if flag & FNAME:
# Read and discard a null-terminated string containing the filename
while True:
s = fp.read(1)
if not s or s==b'\000':
break
crc = _read_until_null(fp, crc)
if flag & FCOMMENT:
# Read and discard a null-terminated string containing a comment
while True:
s = fp.read(1)
if not s or s==b'\000':
break
if flag & FHCRC:
_read_exact(fp, 2) # Read & discard the 16-bit header CRC
crc = _read_until_null(fp, crc)
if crc is not None:
# Header CRC is the last 16 bits of a crc32.
header_crc, = struct.unpack("<H", _read_exact(fp, 2))
crc = crc & 0xFFFF
if header_crc != crc:
raise BadGzipFile(f"Corrupted gzip header. Checksums do not "
f"match: {crc:04x} != {header_crc:04x}")
return last_mtime


Expand Down Expand Up @@ -623,7 +657,7 @@ def _rewind(self):
self._new_member = True


def compress(data, compresslevel=_COMPRESS_LEVEL_BEST, *, mtime=0):
def compress(data, compresslevel=_COMPRESS_LEVEL_TRADEOFF, *, mtime=0):
"""Compress data in one shot and return the compressed string.

compresslevel sets the compression level in range of 0-9.
Expand All @@ -634,6 +668,8 @@ def compress(data, compresslevel=_COMPRESS_LEVEL_BEST, *, mtime=0):
gzip_data = zlib.compress(data, level=compresslevel, wbits=31)
if mtime is None:
mtime = time.time()
if not 0 <= mtime < 2**32:
mtime = 0
# Reuse gzip header created by zlib, replace mtime and OS byte for
# consistency.
header = struct.pack("<4sLBB", gzip_data, int(mtime), gzip_data[8], 255)
Expand Down Expand Up @@ -668,15 +704,14 @@ def decompress(data):
def main():
from argparse import ArgumentParser
parser = ArgumentParser(description=
"A simple command line interface for the gzip module: act like gzip, "
"A simple command line interface for the `gzip` module: act like `gzip`, "
"but do not delete the input file.",
color=True,
)
group = parser.add_mutually_exclusive_group()
group.add_argument('--fast', action='store_true', help='compress faster')
group.add_argument('--best', action='store_true', help='compress better')
group.add_argument("-d", "--decompress", action="store_true",
help="act like gunzip instead of gzip")
help="act like `gunzip` instead of `gzip`")

parser.add_argument("args", nargs="*", default=["-"], metavar='file')
args = parser.parse_args()
Expand Down
Loading
Loading