Skip to content
Open
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
32 changes: 32 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5754,6 +5754,38 @@ def test_read_with_unsuitable_metadata_encoding(self):
with self.assertRaises(UnicodeDecodeError):
zipfile.ZipFile(TESTFN, "r", metadata_encoding='utf-8')

def test_read_invalid_utf8_filename(self):
# A file name marked as UTF-8 (general purpose bit 11) but holding
# bytes that are not valid UTF-8 is a corrupt archive, not a decoding
# error for the caller to handle.
def build(name, flag):
lfh = struct.pack(zipfile.structFileHeader,
zipfile.stringFileHeader,
20, 0, flag, 0, 0, 0, 0, 0, 0,
len(name), 0) + name
cd = struct.pack(zipfile.structCentralDir,
zipfile.stringCentralDir,
20, 0, 20, 0, flag, 0, 0, 0, 0, 0, 0,
len(name), 0, 0, 0, 0, 0, 0) + name
eocd = struct.pack(zipfile.structEndArchive,
zipfile.stringEndArchive,
0, 0, 1, 1, len(cd), len(lfh), 0)
return lfh + cd + eocd

# Central directory path (ZipFile()).
data = build(b'\xff\xfe', zipfile._MASK_UTF_FILENAME)
with self.assertRaisesRegex(zipfile.BadZipFile, 'is marked as UTF-8'):
zipfile.ZipFile(io.BytesIO(data))

# Local file header path (open()); the central directory name is
# valid UTF-8 so it is only the header decode that fails. Keep the
# replacement the same length so header offsets stay valid.
good = build(b'good', zipfile._MASK_UTF_FILENAME)
corrupt = good.replace(b'good', b'\xff\xfe\xff\xfe', 1)
with zipfile.ZipFile(io.BytesIO(corrupt)) as zipfp:
with self.assertRaisesRegex(zipfile.BadZipFile, 'is marked as UTF-8'):
zipfp.read('good')

def test_read_after_append(self):
newname = '\u56db' # Han 'four'
newname2 = 'fünf' # representable in cp437, but still stored as UTF-8
Expand Down
14 changes: 12 additions & 2 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2050,7 +2050,12 @@ def _RealGetContents(self):
flags = centdir[_CD_FLAG_BITS]
if flags & _MASK_UTF_FILENAME:
# UTF-8 file names extension
filename = filename.decode('utf-8')
try:
filename = filename.decode('utf-8')
except UnicodeDecodeError as exc:
raise BadZipFile(
f"File name {filename!r} is marked as UTF-8 "
f"but is not valid UTF-8") from exc
else:
# Historical ZIP filename encoding
filename = filename.decode(self.metadata_encoding or 'cp437')
Expand Down Expand Up @@ -2231,7 +2236,12 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False):

if fheader[_FH_GENERAL_PURPOSE_FLAG_BITS] & _MASK_UTF_FILENAME:
# UTF-8 filename
fname_str = fname.decode("utf-8")
try:
fname_str = fname.decode("utf-8")
except UnicodeDecodeError as exc:
raise BadZipFile(
f"File name {fname!r} is marked as UTF-8 "
f"but is not valid UTF-8") from exc
else:
fname_str = fname.decode(self.metadata_encoding or "cp437")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Raise :exc:`zipfile.BadZipFile` instead of :exc:`UnicodeDecodeError` when
opening a :class:`zipfile.ZipFile` whose member name is flagged as UTF-8 but
contains bytes that are not valid UTF-8.
Loading