diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 9db9697105d6b53..dc99d98cc83b7f8 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -172,11 +172,14 @@ ZipFile Objects The *compresslevel* parameter controls the compression level to use when writing files to the archive. - When using :const:`ZIP_STORED` or :const:`ZIP_LZMA` it has no effect. + When using :const:`ZIP_STORED` it has no effect. When using :const:`ZIP_DEFLATED` integers ``0`` through ``9`` are accepted (see :class:`zlib ` for more information). When using :const:`ZIP_BZIP2` integers ``1`` through ``9`` are accepted (see :class:`bz2 ` for more information). + When using :const:`ZIP_LZMA` integers ``0`` through ``9`` are accepted, + as are the values ``0 | lzma.PRESET_EXTREME`` through ``9 | lzma.PRESET_EXTREME`` + (see :class:`lzma ` for more information). The *strict_timestamps* argument, when set to ``False``, allows to zip files older than 1980-01-01 at the cost of setting the @@ -221,6 +224,9 @@ ZipFile Objects .. versionadded:: 3.8 The *strict_timestamps* keyword-only argument + .. versionchanged:: 3.8 + Add support for using LZMA presets with the *compresslevel* parameter. + .. method:: ZipFile.close() diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 0c8ffcdbf14afe2..dc525b3b350aac3 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -633,6 +633,20 @@ class LzmaTestsWithSourceFile(AbstractTestsWithSourceFile, unittest.TestCase): compression = zipfile.ZIP_LZMA + def test_write_preset(self): + # Initialize an LZMA compressor with a preset and get the generated + # property header. + props_header = zipfile.LZMACompressor(preset=1).compress(b'') + + # Write a ZIP archive with that LZMA compression preset and ensure + # that the property header above is written to that archive. + with io.BytesIO() as fp: + kwargs = {'compression': self.compression, 'compresslevel': 1} + with zipfile.ZipFile(fp, 'w', **kwargs) as zipfp: + zipfp.writestr('dummy_file', b'dummy_contents') + written_bytes = fp.getvalue() + self.assertIn(props_header, written_bytes) + class AbstractTestZip64InSmallFiles: # These tests test the ZIP64 functionality without using large files, diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 3c1f1235034a9e2..6fbf101c060ce50 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -596,15 +596,69 @@ def decrypter(data): class LZMACompressor: - - def __init__(self): + # LZMA uses "filter chains" to control compression settings (e.g., how + # much memory and CPU to use) in details. liblzma defines certain filter + # chain configurations as "preset levels," akin to the compression levels + # in zlib or bzip2. + _PRESET_OPTIONS_MAP = { + # Levels 0 through 9, without the "extreme" setting + 0x00000000: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 18, + 'mode': 1, 'mf': 3, 'nice_len': 128, 'depth': 4}, + 0x00000001: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 20, + 'mode': 1, 'mf': 4, 'nice_len': 128, 'depth': 8}, + 0x00000002: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 21, + 'mode': 1, 'mf': 4, 'nice_len': 273, 'depth': 24}, + 0x00000003: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 22, + 'mode': 1, 'mf': 4, 'nice_len': 273, 'depth': 48}, + 0x00000004: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 22, + 'mode': 2, 'mf': 20, 'nice_len': 16, 'depth': 0}, + 0x00000005: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 23, + 'mode': 2, 'mf': 20, 'nice_len': 32, 'depth': 0}, + 0x00000006: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 23, + 'mode': 2, 'mf': 20, 'nice_len': 64, 'depth': 0}, + 0x00000007: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 24, + 'mode': 2, 'mf': 20, 'nice_len': 64, 'depth': 0}, + 0x00000008: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 25, + 'mode': 2, 'mf': 20, 'nice_len': 64, 'depth': 0}, + 0x00000009: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 26, + 'mode': 2, 'mf': 20, 'nice_len': 64, 'depth': 0}, + # Levels 0 through 9, OR-ed with the "extreme" setting (PRESET_EXTREME) + 0x80000000: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 18, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000001: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 20, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000002: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 21, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000003: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 22, + 'mode': 2, 'mf': 20, 'nice_len': 192, 'depth': 0}, + 0x80000004: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 22, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000005: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 23, + 'mode': 2, 'mf': 20, 'nice_len': 192, 'depth': 0}, + 0x80000006: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 23, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000007: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 24, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000008: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 25, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + 0x80000009: {'lc': 3, 'lp': 0, 'pb': 2, 'dict_size': 1 << 26, + 'mode': 2, 'mf': 20, 'nice_len': 273, 'depth': 512}, + } + + def __init__(self, preset=None): self._comp = None + if (preset is not None) and (preset not in self._PRESET_OPTIONS_MAP): + raise ValueError(f'invalid preset {preset}') + self._preset = preset def _init(self): - props = lzma._encode_filter_properties({'id': lzma.FILTER_LZMA1}) - self._comp = lzma.LZMACompressor(lzma.FORMAT_RAW, filters=[ - lzma._decode_filter_properties(lzma.FILTER_LZMA1, props) - ]) + # Translate the encoder settings into the value to be written to + # the LZMA Properties Header in the ZIP file. + opts = {'id': lzma.FILTER_LZMA1} + if self._preset is not None: + opts.update(self._PRESET_OPTIONS_MAP[self._preset]) + props = lzma._encode_filter_properties(opts) + self._comp = lzma.LZMACompressor(lzma.FORMAT_RAW, filters=[opts]) return struct.pack('