Skip to content

Commit 75d82ce

Browse files
committed
gh-155286: Write a valid empty zstd archive
1 parent 7c653e2 commit 75d82ce

3 files changed

Lines changed: 12 additions & 4 deletions

File tree

Lib/compression/zstd/_zstdfile.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self, file, /, mode='r', *,
5454
self._close_fp = False
5555
self._mode = _MODE_CLOSED
5656
self._buffer = None
57+
self._write_started = False
5758

5859
if not isinstance(mode, str):
5960
raise ValueError('mode must be a str')
@@ -68,6 +69,9 @@ def __init__(self, file, /, mode='r', *,
6869
if level is not None and not isinstance(level, int):
6970
raise TypeError('level must be int or None')
7071
self._mode = _MODE_WRITE
72+
# Do not add an empty frame when closing an existing archive in
73+
# append mode without writing anything.
74+
self._write_started = mode == 'a'
7175
self._compressor = ZstdCompressor(level=level, options=options,
7276
zstd_dict=zstd_dict)
7377
self._pos = 0
@@ -131,6 +135,7 @@ def write(self, data, /):
131135
length = _nbytes(data)
132136

133137
compressed = self._compressor.compress(data)
138+
self._write_started = True
134139
self._fp.write(compressed)
135140
self._pos += length
136141
return length
@@ -153,10 +158,11 @@ def flush(self, mode=FLUSH_BLOCK):
153158
raise ValueError('Invalid mode argument, expected either '
154159
'ZstdFile.FLUSH_FRAME or '
155160
'ZstdFile.FLUSH_BLOCK')
156-
if self._compressor.last_mode == mode:
161+
if self._compressor.last_mode == mode and self._write_started:
157162
return
158163
# Flush zstd block/frame, and write.
159164
data = self._compressor.flush(mode)
165+
self._write_started = True
160166
self._fp.write(data)
161167
if hasattr(self._fp, 'flush'):
162168
self._fp.flush()

Lib/test/test_zstd.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,16 +2120,16 @@ def test_write_empty_frame(self):
21202120
self.assertNotEqual(c.flush(c.FLUSH_FRAME), b'')
21212121
self.assertNotEqual(c.flush(c.FLUSH_FRAME), b'')
21222122

2123-
# don't generate empty content frame
2123+
# generate an empty content frame when the file is closed
21242124
bo = io.BytesIO()
21252125
with ZstdFile(bo, 'w') as f:
21262126
pass
2127-
self.assertEqual(bo.getvalue(), b'')
2127+
self.assertEqual(decompress(bo.getvalue()), b'')
21282128

21292129
bo = io.BytesIO()
21302130
with ZstdFile(bo, 'w') as f:
21312131
f.flush(f.FLUSH_FRAME)
2132-
self.assertEqual(bo.getvalue(), b'')
2132+
self.assertEqual(decompress(bo.getvalue()), b'')
21332133

21342134
# if .write(b''), generate empty content frame
21352135
bo = io.BytesIO()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :class:`~compression.zstd.ZstdFile` creating an invalid zero-byte archive
2+
when an output file is closed without any writes.

0 commit comments

Comments
 (0)