Skip to content

Commit 183e29d

Browse files
authored
Merge branch '3.14' into backport-47c249a-3.14
2 parents 3f7afe1 + 583d7b6 commit 183e29d

25 files changed

Lines changed: 257 additions & 43 deletions

‎Lib/codecs.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,10 @@ def reset(self):
400400
pass
401401

402402
def seek(self, offset, whence=0):
403-
self.stream.seek(offset, whence)
403+
# reset() may write to the stream: call it at the old position.
404404
if whence == 0 and offset == 0:
405405
self.reset()
406+
self.stream.seek(offset, whence)
406407

407408
def __getattr__(self, name,
408409
getattr=getattr):
@@ -739,10 +740,11 @@ def reset(self):
739740
self.writer.reset()
740741

741742
def seek(self, offset, whence=0):
742-
self.stream.seek(offset, whence)
743-
self.reader.reset()
743+
# writer.reset() may write to the stream: call it at the old position.
744744
if whence == 0 and offset == 0:
745745
self.writer.reset()
746+
self.stream.seek(offset, whence)
747+
self.reader.reset()
746748

747749
def __getattr__(self, name,
748750
getattr=getattr):
@@ -862,8 +864,8 @@ def reset(self):
862864
def seek(self, offset, whence=0):
863865
# Seeks must be propagated to both the readers and writers
864866
# as they might need to reset their internal buffers.
865-
self.reader.seek(offset, whence)
866867
self.writer.seek(offset, whence)
868+
self.reader.seek(offset, whence)
867869

868870
def __getattr__(self, name,
869871
getattr=getattr):

‎Lib/gzip.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ def main():
732732
compresslevel=compresslevel)
733733
else:
734734
f = builtins.open(arg, "rb")
735-
g = open(arg + ".gz", "wb")
735+
g = open(arg + ".gz", "wb", compresslevel=compresslevel)
736736
while True:
737737
chunk = f.read(READ_BUFFER_SIZE)
738738
if not chunk:

‎Lib/multiprocessing/shared_memory.py‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,14 +305,21 @@ def _extract_recreation_code(value):
305305
else:
306306
return 3 # NoneType
307307

308+
@staticmethod
309+
def _encode_value(value):
310+
if not isinstance(value, str):
311+
return value
312+
else:
313+
return value.encode(_encoding)
314+
308315
def __init__(self, sequence=None, *, name=None):
309316
if name is None or sequence is not None:
310317
sequence = sequence or ()
311318
_formats = [
312319
self._types_mapping[type(item)]
313320
if not isinstance(item, (str, bytes))
314321
else self._types_mapping[type(item)] % (
315-
self._alignment * (len(item) // self._alignment + 1),
322+
self._alignment * ((len(self._encode_value(item)) - 1) // self._alignment + 1),
316323
)
317324
for item in sequence
318325
]
@@ -353,7 +360,7 @@ def __init__(self, sequence=None, *, name=None):
353360
"".join(_formats),
354361
self.shm.buf,
355362
self._offset_data_start,
356-
*(v.encode(_enc) if isinstance(v, str) else v for v in sequence)
363+
*(self._encode_value(v) for v in sequence)
357364
)
358365
struct.pack_into(
359366
self._format_packing_metainfo,
@@ -463,8 +470,7 @@ def __setitem__(self, position, value):
463470
else:
464471
allocated_length = self._allocated_offsets[position + 1] - item_offset
465472

466-
encoded_value = (value.encode(_encoding)
467-
if isinstance(value, str) else value)
473+
encoded_value = self._encode_value(value)
468474
if len(encoded_value) > allocated_length:
469475
raise ValueError("bytes/str item exceeds available storage")
470476
if current_format[-1] == "s":

‎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():

‎Lib/test/_test_multiprocessing.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4688,7 +4688,7 @@ def test_shared_memory_SharedMemoryManager_basics(self):
46884688

46894689
def test_shared_memory_ShareableList_basics(self):
46904690
sl = shared_memory.ShareableList(
4691-
['howdy', b'HoWdY', -273.154, 100, None, True, 42]
4691+
['howdy', b'HoWdY', -273.154, 100, None, True, 42, '💥 💥']
46924692
)
46934693
self.addCleanup(sl.shm.unlink)
46944694

@@ -4698,22 +4698,22 @@ def test_shared_memory_ShareableList_basics(self):
46984698

46994699
# Index Out of Range (get)
47004700
with self.assertRaises(IndexError):
4701-
sl[7]
4701+
sl[8]
47024702

47034703
# Index Out of Range (set)
47044704
with self.assertRaises(IndexError):
4705-
sl[7] = 2
4705+
sl[8] = 2
47064706

47074707
# Assign value without format change (str -> str)
47084708
current_format = sl._get_packing_format(0)
47094709
sl[0] = 'howdy'
47104710
self.assertEqual(current_format, sl._get_packing_format(0))
47114711

47124712
# Verify attributes are readable.
4713-
self.assertEqual(sl.format, '8s8sdqxxxxxx?xxxxxxxx?q')
4713+
self.assertEqual(sl.format, '8s8sdqxxxxxx?xxxxxxxx?q16s')
47144714

47154715
# Exercise len().
4716-
self.assertEqual(len(sl), 7)
4716+
self.assertEqual(len(sl), 8)
47174717

47184718
# Exercise index().
47194719
with warnings.catch_warnings():
@@ -4725,30 +4725,31 @@ def test_shared_memory_ShareableList_basics(self):
47254725

47264726
# Exercise retrieving individual values.
47274727
self.assertEqual(sl[0], 'howdy')
4728-
self.assertEqual(sl[-2], True)
4728+
self.assertEqual(sl[-3], True)
4729+
self.assertEqual(sl[-1], '💥 💥')
47294730

47304731
# Exercise iterability.
47314732
self.assertEqual(
47324733
tuple(sl),
4733-
('howdy', b'HoWdY', -273.154, 100, None, True, 42)
4734+
('howdy', b'HoWdY', -273.154, 100, None, True, 42, '💥 💥')
47344735
)
47354736

47364737
# Exercise modifying individual values.
47374738
sl[3] = 42
47384739
self.assertEqual(sl[3], 42)
47394740
sl[4] = 'some' # Change type at a given position.
47404741
self.assertEqual(sl[4], 'some')
4741-
self.assertEqual(sl.format, '8s8sdq8sxxxxxxx?q')
4742+
self.assertEqual(sl.format, '8s8sdq8sxxxxxxx?q16s')
47424743
with self.assertRaisesRegex(ValueError,
47434744
"exceeds available storage"):
47444745
sl[4] = 'far too many'
47454746
self.assertEqual(sl[4], 'some')
4746-
sl[0] = 'encodés' # Exactly 8 bytes of UTF-8 data
4747-
self.assertEqual(sl[0], 'encodés')
4747+
sl[0] = 'éncodé' # Exactly 8 bytes of UTF-8 data
4748+
self.assertEqual(sl[0], 'éncodé')
47484749
self.assertEqual(sl[1], b'HoWdY') # no spillage
47494750
with self.assertRaisesRegex(ValueError,
47504751
"exceeds available storage"):
4751-
sl[0] = 'encodées' # Exactly 9 bytes of UTF-8 data
4752+
sl[0] = 'éncodés' # Exactly 9 bytes of UTF-8 data
47524753
self.assertEqual(sl[1], b'HoWdY')
47534754
with self.assertRaisesRegex(ValueError,
47544755
"exceeds available storage"):

‎Lib/test/test_codecencodings_cn.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,15 @@ class Test_HZ(multibytecodec_support.TestBase, unittest.TestCase):
9292
(b'~{Dc~\n:C~}', 'strict', None), # ~\n only in ASCII mode
9393
)
9494

95+
def test_escape_at_buffer_boundary(self):
96+
# '~~' must not be cut in two when the output buffer is enlarged.
97+
for pairs in range(40):
98+
for letters in range(3):
99+
text = 'a\u4e2d' * pairs + 'x' * letters + '~{'
100+
with self.subTest(pairs=pairs, letters=letters):
101+
encoded = text.encode('hz')
102+
self.assertEndsWith(encoded, b'~~{')
103+
self.assertEqual(encoded.decode('hz'), text)
104+
95105
if __name__ == "__main__":
96106
unittest.main()

‎Lib/test/test_gzip.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1189,7 +1189,8 @@ def test_compress_infile_outfile_default(self):
11891189

11901190
@create_and_remove_directory(TEMPDIR)
11911191
def test_compress_infile_outfile(self):
1192-
for compress_level in ('--fast', '--best'):
1192+
# The XFL header byte records the compression level used.
1193+
for compress_level, xfl in (('--fast', 4), ('--best', 2)):
11931194
with self.subTest(compress_level=compress_level):
11941195
local_testgzip = os.path.join(TEMPDIR, 'testgzip')
11951196
gzipname = local_testgzip + '.gz'
@@ -1203,6 +1204,8 @@ def test_compress_infile_outfile(self):
12031204
self.assertTrue(os.path.exists(gzipname))
12041205
self.assertEqual(out, b'')
12051206
self.assertEqual(err, b'')
1207+
with open(gzipname, 'rb') as fp:
1208+
self.assertEqual(fp.read()[8], xfl)
12061209
os.remove(gzipname)
12071210
self.assertFalse(os.path.exists(gzipname))
12081211

‎Lib/test/test_multibytecodec.py‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def test_nullcoding(self):
3939
self.assertEqual(str(b'', enc), '')
4040
self.assertEqual(''.encode(enc), b'')
4141

42+
def test_hz_tilde_at_buffer_boundary(self):
43+
text = 'a' + '中a' * 6 + '~'
44+
self.assertEqual(text.encode('hz').decode('hz'), text)
45+
4246
def test_str_decode(self):
4347
for enc in ALL_CJKENCODINGS:
4448
self.assertEqual('abcd'.encode(enc), b'abcd')
@@ -91,6 +95,12 @@ def test_decode_unicode(self):
9195

9296
class Test_IncrementalEncoder(unittest.TestCase):
9397

98+
def test_iso2022_kr_reset(self):
99+
encoder = codecs.getincrementalencoder('iso2022_kr')()
100+
encoder.encode('가')
101+
encoder.reset()
102+
self.assertEqual(encoder.encode('가', final=True), '가'.encode('iso2022_kr'))
103+
94104
def test_stateless(self):
95105
# cp949 encoder isn't stateful at all.
96106
encoder = codecs.getincrementalencoder('cp949')()
@@ -242,6 +252,19 @@ def test_subinterp(self):
242252

243253
class Test_IncrementalDecoder(unittest.TestCase):
244254

255+
def test_iso2022_split_escape(self):
256+
raw = b'\x1b$0000000B'
257+
decoder = codecs.getincrementaldecoder('iso2022_jp')('replace')
258+
decoded = decoder.decode(raw[:-1]) + decoder.decode(raw[-1:], final=True)
259+
self.assertEqual(decoded, raw.decode('iso2022_jp', 'replace'))
260+
261+
def test_iso2022_reset(self):
262+
decoder = codecs.getincrementaldecoder('iso2022_jp')()
263+
decoder.decode(b'\x1b%')
264+
decoder.reset()
265+
with self.assertRaises(UnicodeDecodeError):
266+
decoder.decode(b'\xff', final=True)
267+
245268
def test_dbcs(self):
246269
# cp949 decoder is simple with only 1 or 2 bytes sequences.
247270
decoder = codecs.getincrementaldecoder('cp949')()
@@ -325,7 +348,7 @@ def test_setstate_validates_input(self):
325348
self.assertRaises(TypeError, decoder.setstate, 123)
326349
self.assertRaises(TypeError, decoder.setstate, ("invalid", 0))
327350
self.assertRaises(TypeError, decoder.setstate, (b"1234", "invalid"))
328-
self.assertRaises(UnicodeDecodeError, decoder.setstate, (b"123456789", 0))
351+
self.assertRaises(UnicodeDecodeError, decoder.setstate, (b"0" * 17, 0))
329352

330353
def test_setstate_invalid_designation(self):
331354
# gh-153603: an unknown charset designation in the state must not crash
@@ -438,6 +461,42 @@ def test_incrementalencoder_final(self):
438461
self.assertEqual(output, self.expected_reset)
439462
self.assertEqual(encoder.encode('', final=True), b'')
440463

464+
def test_streamwriter_reset(self):
465+
stream = io.BytesIO()
466+
writer = codecs.getwriter(self.encoding)(stream)
467+
writer.reset()
468+
self.assertEqual(stream.getvalue(), b'')
469+
writer.write(self.text)
470+
self.assertEqual(stream.getvalue(), self.expected)
471+
writer.reset()
472+
self.assertEqual(stream.getvalue(), self.expected_reset)
473+
writer.reset()
474+
self.assertEqual(stream.getvalue(), self.expected_reset)
475+
writer.write(self.text)
476+
writer.reset()
477+
self.assertEqual(stream.getvalue(), self.expected_reset * 2)
478+
479+
def test_streamwriter_seek0(self):
480+
# The reset sequence is written before the stream is rewound.
481+
info = codecs.lookup(self.encoding)
482+
for wrap in (
483+
info.streamwriter,
484+
lambda stream: codecs.StreamReaderWriter(
485+
stream, info.streamreader, info.streamwriter),
486+
lambda stream: codecs.EncodedFile(stream, 'utf-8', self.encoding),
487+
):
488+
stream = io.BytesIO()
489+
writer = wrap(stream)
490+
text = self.text
491+
if isinstance(writer, codecs.StreamRecoder):
492+
text = text.encode()
493+
writer.write(text)
494+
writer.seek(0)
495+
self.assertEqual(stream.tell(), 0)
496+
self.assertEqual(stream.getvalue(), self.expected_reset)
497+
writer.write(text)
498+
self.assertEqual(stream.getvalue(), self.expected_reset)
499+
441500
def test_incrementalencoder_error(self):
442501
# The output of a failed encode() is lost, so the shift state
443502
# must not be changed.

‎Lib/test/test_tarfile.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4526,6 +4526,33 @@ def test_sneaky_hardlink_fallback_deep(self):
45264526
self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape'))
45274527
self.expect_file("s", symlink_to=os.path.join('..', 'escape'))
45284528

4529+
@symlink_test
4530+
def test_sneaky_hardlink_fallback_attrs(self):
4531+
# Test that when a hardlink is extracted as a copy of a symlink
4532+
# member, the hardlink's attributes are not applied to the file
4533+
# the symlink points to.
4534+
with ArchiveMaker() as arc:
4535+
arc.add("a/s", symlink_to=os.path.join("..", "flag"))
4536+
arc.add("s", hardlink_to=os.path.join("a", "s"),
4537+
mode='-rwxr-xr-x', mtime=86400, uid=1337, gid=1337)
4538+
4539+
for filter in 'tar', 'fully_trusted':
4540+
with (
4541+
self.subTest(filter),
4542+
unittest.mock.patch("os.chown", create=True) as mock_chown,
4543+
self.check_context(arc.open(), filter) as cc,
4544+
):
4545+
if not os_helper.can_symlink():
4546+
self.expect_file("a/")
4547+
else:
4548+
self.expect_file("a/s", symlink_to=os.path.join('..', 'flag'))
4549+
self.expect_file("s", symlink_to=os.path.join('..', 'flag'))
4550+
st = (cc.outerdir / 'flag').stat()
4551+
self.assertNotEqual(st.st_mtime, 86400)
4552+
if sys.platform != "win32":
4553+
self.assertNotEqual(st.st_mode & 0o777, 0o755)
4554+
mock_chown.assert_not_called()
4555+
45294556
@symlink_test
45304557
@os_helper.skip_unless_hardlink
45314558
def test_sneaky_hardlink_relocation(self):

‎Lib/test/test_zipfile/test_core.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,6 +2132,20 @@ def test_is_zip_valid_file(self):
21322132
self.assertTrue(zipfile.is_zipfile(fp))
21332133
self.assertEqual(fp.tell(), mid)
21342134

2135+
def test_is_zip_bad_central_directory_offset(self):
2136+
"""Check that is_zipfile() rejects a central directory before the file."""
2137+
fp = io.BytesIO()
2138+
with zipfile.ZipFile(fp, mode="w") as zipf:
2139+
zipf.writestr("foo.txt", b"O, for a Muse of Fire!")
2140+
data = bytearray(fp.getvalue())
2141+
endrec = len(data) - zipfile.sizeEndCentDir
2142+
struct.pack_into("<L", data, endrec + 12, len(data))
2143+
fp = io.BytesIO(data)
2144+
pos = fp.seek(5)
2145+
self.assertFalse(zipfile.is_zipfile(fp))
2146+
self.assertEqual(fp.tell(), pos)
2147+
self.assertRaises(zipfile.BadZipFile, zipfile.ZipFile, io.BytesIO(data))
2148+
21352149
def test_non_existent_file_raises_OSError(self):
21362150
# make sure we don't raise an AttributeError when a partially-constructed
21372151
# ZipFile instance is finalized; this tests for regression on SF tracker

0 commit comments

Comments
 (0)