|
@@ -56,22 +56,18 @@ cdef class CompressorBase:
|
|
|
also handles compression format auto detection and
|
|
|
adding/stripping the ID header (which enable auto detection).
|
|
|
"""
|
|
|
- ID = b'\xFF' # reserved and not used
|
|
|
- # overwrite with a unique 1-byte bytestring in child classes
|
|
|
+ ID = 0xFF # reserved and not used
|
|
|
+ # overwrite with a unique 1-byte bytestring in child classes
|
|
|
name = 'baseclass'
|
|
|
|
|
|
@classmethod
|
|
|
def detect(cls, data):
|
|
|
- return data.startswith(cls.ID)
|
|
|
+ return data and data[0] == cls.ID
|
|
|
|
|
|
- def __init__(self, level=255, **kwargs):
|
|
|
+ def __init__(self, level=255, legacy_mode=False, **kwargs):
|
|
|
assert 0 <= level <= 255
|
|
|
self.level = level
|
|
|
- if self.ID is not None:
|
|
|
- self.id_level = self.ID + bytes((level, )) # level 255 means "unknown level"
|
|
|
- assert len(self.id_level) == 2
|
|
|
- else:
|
|
|
- self.id_level = None
|
|
|
+ self.legacy_mode = legacy_mode # True: support prefixed ctype/clevel bytes
|
|
|
|
|
|
def decide(self, data):
|
|
|
"""
|
|
@@ -86,24 +82,48 @@ cdef class CompressorBase:
|
|
|
"""
|
|
|
return self
|
|
|
|
|
|
- def compress(self, data):
|
|
|
+ def compress(self, meta, data):
|
|
|
"""
|
|
|
- Compress *data* (bytes) and return bytes result. Prepend the ID bytes of this compressor,
|
|
|
- which is needed so that the correct decompressor can be used for decompression.
|
|
|
+ Compress *data* (bytes) and return compression metadata and compressed bytes.
|
|
|
"""
|
|
|
- # add id_level bytes
|
|
|
- return self.id_level + data
|
|
|
+ if self.legacy_mode:
|
|
|
+ return None, bytes((self.ID, self.level)) + data
|
|
|
+ else:
|
|
|
+ meta["ctype"] = self.ID
|
|
|
+ meta["clevel"] = self.level
|
|
|
+ meta["csize"] = len(data)
|
|
|
+ return meta, data
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
+ def decompress(self, meta, data):
|
|
|
"""
|
|
|
Decompress *data* (preferably a memoryview, bytes also acceptable) and return bytes result.
|
|
|
- The leading Compressor ID bytes need to be present.
|
|
|
+
|
|
|
+ Legacy mode: The leading Compressor ID bytes need to be present.
|
|
|
|
|
|
Only handles input generated by _this_ Compressor - for a general purpose
|
|
|
decompression method see *Compressor.decompress*.
|
|
|
"""
|
|
|
- # strip id_level bytes
|
|
|
- return data[2:]
|
|
|
+ if self.legacy_mode:
|
|
|
+ assert meta is None
|
|
|
+ meta = {}
|
|
|
+ meta["ctype"] = data[0]
|
|
|
+ meta["clevel"] = data[1]
|
|
|
+ meta["csize"] = len(data)
|
|
|
+ return meta, data[2:]
|
|
|
+ else:
|
|
|
+ assert isinstance(meta, dict)
|
|
|
+ assert "ctype" in meta
|
|
|
+ assert "clevel" in meta
|
|
|
+ return meta, data
|
|
|
+
|
|
|
+ def check_fix_size(self, meta, data):
|
|
|
+ if "size" in meta:
|
|
|
+ assert meta["size"] == len(data)
|
|
|
+ elif self.legacy_mode:
|
|
|
+ meta["size"] = len(data)
|
|
|
+ else:
|
|
|
+ pass # raise ValueError("size not present and not in legacy mode")
|
|
|
+
|
|
|
|
|
|
cdef class DecidingCompressor(CompressorBase):
|
|
|
"""
|
|
@@ -112,12 +132,12 @@ cdef class DecidingCompressor(CompressorBase):
|
|
|
"""
|
|
|
name = 'decidebaseclass'
|
|
|
|
|
|
- def __init__(self, level=255, **kwargs):
|
|
|
- super().__init__(level=level, **kwargs)
|
|
|
+ def __init__(self, level=255, legacy_mode=False, **kwargs):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)
|
|
|
|
|
|
- def _decide(self, data):
|
|
|
+ def _decide(self, meta, data):
|
|
|
"""
|
|
|
- Decides what to do with *data*. Returns (compressor, compressed_data).
|
|
|
+ Decides what to do with *data*. Returns (compressor, meta, compressed_data).
|
|
|
|
|
|
*compressed_data* can be the result of *data* being processed by *compressor*,
|
|
|
if that is generated as a side-effect of the decision process, or None otherwise.
|
|
@@ -127,47 +147,50 @@ cdef class DecidingCompressor(CompressorBase):
|
|
|
"""
|
|
|
raise NotImplementedError
|
|
|
|
|
|
- def decide(self, data):
|
|
|
- return self._decide(data)[0]
|
|
|
+ def decide(self, meta, data):
|
|
|
+ return self._decide(meta, data)[0]
|
|
|
|
|
|
- def decide_compress(self, data):
|
|
|
+ def decide_compress(self, meta, data):
|
|
|
"""
|
|
|
Decides what to do with *data* and handle accordingly. Returns (compressor, compressed_data).
|
|
|
|
|
|
*compressed_data* is the result of *data* being processed by *compressor*.
|
|
|
"""
|
|
|
- compressor, compressed_data = self._decide(data)
|
|
|
+ compressor, (meta, compressed_data) = self._decide(meta, data)
|
|
|
|
|
|
if compressed_data is None:
|
|
|
- compressed_data = compressor.compress(data)
|
|
|
+ meta, compressed_data = compressor.compress(meta, data)
|
|
|
|
|
|
if compressor is self:
|
|
|
# call super class to add ID bytes
|
|
|
- return self, super().compress(compressed_data)
|
|
|
+ return self, super().compress(meta, compressed_data)
|
|
|
|
|
|
- return compressor, compressed_data
|
|
|
+ return compressor, (meta, compressed_data)
|
|
|
|
|
|
- def compress(self, data):
|
|
|
- return self.decide_compress(data)[1]
|
|
|
+ def compress(self, meta, data):
|
|
|
+ meta["size"] = len(data)
|
|
|
+ return self.decide_compress(meta, data)[1]
|
|
|
|
|
|
class CNONE(CompressorBase):
|
|
|
"""
|
|
|
none - no compression, just pass through data
|
|
|
"""
|
|
|
- ID = b'\x00'
|
|
|
+ ID = 0x00
|
|
|
name = 'none'
|
|
|
|
|
|
- def __init__(self, level=255, **kwargs):
|
|
|
- super().__init__(level=level, **kwargs) # no defined levels for CNONE, so just say "unknown"
|
|
|
+ def __init__(self, level=255, legacy_mode=False, **kwargs):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode, **kwargs) # no defined levels for CNONE, so just say "unknown"
|
|
|
|
|
|
- def compress(self, data):
|
|
|
- return super().compress(data)
|
|
|
+ def compress(self, meta, data):
|
|
|
+ meta["size"] = len(data)
|
|
|
+ return super().compress(meta, data)
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
- data = super().decompress(data)
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ meta, data = super().decompress(meta, data)
|
|
|
if not isinstance(data, bytes):
|
|
|
data = bytes(data)
|
|
|
- return data
|
|
|
+ self.check_fix_size(meta, data)
|
|
|
+ return meta, data
|
|
|
|
|
|
|
|
|
class LZ4(DecidingCompressor):
|
|
@@ -179,13 +202,13 @@ class LZ4(DecidingCompressor):
|
|
|
- wrapper releases CPython's GIL to support multithreaded code
|
|
|
- uses safe lz4 methods that never go beyond the end of the output buffer
|
|
|
"""
|
|
|
- ID = b'\x01'
|
|
|
+ ID = 0x01
|
|
|
name = 'lz4'
|
|
|
|
|
|
- def __init__(self, level=255, **kwargs):
|
|
|
- super().__init__(level=level, **kwargs) # no defined levels for LZ4, so just say "unknown"
|
|
|
+ def __init__(self, level=255, legacy_mode=False, **kwargs):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode, **kwargs) # no defined levels for LZ4, so just say "unknown"
|
|
|
|
|
|
- def _decide(self, idata):
|
|
|
+ def _decide(self, meta, idata):
|
|
|
"""
|
|
|
Decides what to do with *data*. Returns (compressor, lz4_data).
|
|
|
|
|
@@ -206,12 +229,12 @@ class LZ4(DecidingCompressor):
|
|
|
raise Exception('lz4 compress failed')
|
|
|
# only compress if the result actually is smaller
|
|
|
if osize < isize:
|
|
|
- return self, dest[:osize]
|
|
|
+ return self, (meta, dest[:osize])
|
|
|
else:
|
|
|
- return NONE_COMPRESSOR, None
|
|
|
+ return NONE_COMPRESSOR, (meta, None)
|
|
|
|
|
|
- def decompress(self, idata):
|
|
|
- idata = super().decompress(idata)
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ meta, idata = super().decompress(meta, data)
|
|
|
if not isinstance(idata, bytes):
|
|
|
idata = bytes(idata) # code below does not work with memoryview
|
|
|
cdef int isize = len(idata)
|
|
@@ -237,23 +260,25 @@ class LZ4(DecidingCompressor):
|
|
|
raise DecompressionError('lz4 decompress failed')
|
|
|
# likely the buffer was too small, get a bigger one:
|
|
|
osize = int(1.5 * osize)
|
|
|
- return dest[:rsize]
|
|
|
+ data = dest[:rsize]
|
|
|
+ self.check_fix_size(meta, data)
|
|
|
+ return meta, data
|
|
|
|
|
|
|
|
|
class LZMA(DecidingCompressor):
|
|
|
"""
|
|
|
lzma compression / decompression
|
|
|
"""
|
|
|
- ID = b'\x02'
|
|
|
+ ID = 0x02
|
|
|
name = 'lzma'
|
|
|
|
|
|
- def __init__(self, level=6, **kwargs):
|
|
|
- super().__init__(level=level, **kwargs)
|
|
|
+ def __init__(self, level=6, legacy_mode=False, **kwargs):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)
|
|
|
self.level = level
|
|
|
if lzma is None:
|
|
|
raise ValueError('No lzma support found.')
|
|
|
|
|
|
- def _decide(self, data):
|
|
|
+ def _decide(self, meta, data):
|
|
|
"""
|
|
|
Decides what to do with *data*. Returns (compressor, lzma_data).
|
|
|
|
|
@@ -262,14 +287,16 @@ class LZMA(DecidingCompressor):
|
|
|
# we do not need integrity checks in lzma, we do that already
|
|
|
lzma_data = lzma.compress(data, preset=self.level, check=lzma.CHECK_NONE)
|
|
|
if len(lzma_data) < len(data):
|
|
|
- return self, lzma_data
|
|
|
+ return self, (meta, lzma_data)
|
|
|
else:
|
|
|
- return NONE_COMPRESSOR, None
|
|
|
+ return NONE_COMPRESSOR, (meta, None)
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
- data = super().decompress(data)
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ meta, data = super().decompress(meta, data)
|
|
|
try:
|
|
|
- return lzma.decompress(data)
|
|
|
+ data = lzma.decompress(data)
|
|
|
+ self.check_fix_size(meta, data)
|
|
|
+ return meta, data
|
|
|
except lzma.LZMAError as e:
|
|
|
raise DecompressionError(str(e)) from None
|
|
|
|
|
@@ -279,14 +306,14 @@ class ZSTD(DecidingCompressor):
|
|
|
# This is a NOT THREAD SAFE implementation.
|
|
|
# Only ONE python context must be created at a time.
|
|
|
# It should work flawlessly as long as borg will call ONLY ONE compression job at time.
|
|
|
- ID = b'\x03'
|
|
|
+ ID = 0x03
|
|
|
name = 'zstd'
|
|
|
|
|
|
- def __init__(self, level=3, **kwargs):
|
|
|
- super().__init__(level=level, **kwargs)
|
|
|
+ def __init__(self, level=3, legacy_mode=False, **kwargs):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)
|
|
|
self.level = level
|
|
|
|
|
|
- def _decide(self, idata):
|
|
|
+ def _decide(self, meta, idata):
|
|
|
"""
|
|
|
Decides what to do with *data*. Returns (compressor, zstd_data).
|
|
|
|
|
@@ -308,12 +335,12 @@ class ZSTD(DecidingCompressor):
|
|
|
raise Exception('zstd compress failed: %s' % ZSTD_getErrorName(osize))
|
|
|
# only compress if the result actually is smaller
|
|
|
if osize < isize:
|
|
|
- return self, dest[:osize]
|
|
|
+ return self, (meta, dest[:osize])
|
|
|
else:
|
|
|
- return NONE_COMPRESSOR, None
|
|
|
+ return NONE_COMPRESSOR, (meta, None)
|
|
|
|
|
|
- def decompress(self, idata):
|
|
|
- idata = super().decompress(idata)
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ meta, idata = super().decompress(meta, data)
|
|
|
if not isinstance(idata, bytes):
|
|
|
idata = bytes(idata) # code below does not work with memoryview
|
|
|
cdef int isize = len(idata)
|
|
@@ -337,21 +364,23 @@ class ZSTD(DecidingCompressor):
|
|
|
raise DecompressionError('zstd decompress failed: %s' % ZSTD_getErrorName(rsize))
|
|
|
if rsize != osize:
|
|
|
raise DecompressionError('zstd decompress failed: size mismatch')
|
|
|
- return dest[:osize]
|
|
|
+ data = dest[:osize]
|
|
|
+ self.check_fix_size(meta, data)
|
|
|
+ return meta, data
|
|
|
|
|
|
|
|
|
class ZLIB(DecidingCompressor):
|
|
|
"""
|
|
|
zlib compression / decompression (python stdlib)
|
|
|
"""
|
|
|
- ID = b'\x05'
|
|
|
+ ID = 0x05
|
|
|
name = 'zlib'
|
|
|
|
|
|
- def __init__(self, level=6, **kwargs):
|
|
|
- super().__init__(level=level, **kwargs)
|
|
|
+ def __init__(self, level=6, legacy_mode=False, **kwargs):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode, **kwargs)
|
|
|
self.level = level
|
|
|
|
|
|
- def _decide(self, data):
|
|
|
+ def _decide(self, meta, data):
|
|
|
"""
|
|
|
Decides what to do with *data*. Returns (compressor, zlib_data).
|
|
|
|
|
@@ -359,14 +388,16 @@ class ZLIB(DecidingCompressor):
|
|
|
"""
|
|
|
zlib_data = zlib.compress(data, self.level)
|
|
|
if len(zlib_data) < len(data):
|
|
|
- return self, zlib_data
|
|
|
+ return self, (meta, zlib_data)
|
|
|
else:
|
|
|
- return NONE_COMPRESSOR, None
|
|
|
+ return NONE_COMPRESSOR, (meta, None)
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
- data = super().decompress(data)
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ meta, data = super().decompress(meta, data)
|
|
|
try:
|
|
|
- return zlib.decompress(data)
|
|
|
+ data = zlib.decompress(data)
|
|
|
+ self.check_fix_size(meta, data)
|
|
|
+ return meta, data
|
|
|
except zlib.error as e:
|
|
|
raise DecompressionError(str(e)) from None
|
|
|
|
|
@@ -382,7 +413,7 @@ class ZLIB_legacy(CompressorBase):
|
|
|
Newer borg uses the ZLIB class that has separate ID bytes (as all the other
|
|
|
compressors) and does not need this hack.
|
|
|
"""
|
|
|
- ID = b'\x08' # not used here, see detect()
|
|
|
+ ID = 0x08 # not used here, see detect()
|
|
|
# avoid all 0x.8 IDs elsewhere!
|
|
|
name = 'zlib_legacy'
|
|
|
|
|
@@ -398,14 +429,14 @@ class ZLIB_legacy(CompressorBase):
|
|
|
super().__init__(level=level, **kwargs)
|
|
|
self.level = level
|
|
|
|
|
|
- def compress(self, data):
|
|
|
+ def compress(self, meta, data):
|
|
|
# note: for compatibility no super call, do not add ID bytes
|
|
|
- return zlib.compress(data, self.level)
|
|
|
+ return None, zlib.compress(data, self.level)
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
+ def decompress(self, meta, data):
|
|
|
# note: for compatibility no super call, do not strip ID bytes
|
|
|
try:
|
|
|
- return zlib.decompress(data)
|
|
|
+ return meta, zlib.decompress(data)
|
|
|
except zlib.error as e:
|
|
|
raise DecompressionError(str(e)) from None
|
|
|
|
|
@@ -425,7 +456,7 @@ class Auto(CompressorBase):
|
|
|
super().__init__()
|
|
|
self.compressor = compressor
|
|
|
|
|
|
- def _decide(self, data):
|
|
|
+ def _decide(self, meta, data):
|
|
|
"""
|
|
|
Decides what to do with *data*. Returns (compressor, compressed_data).
|
|
|
|
|
@@ -448,33 +479,33 @@ class Auto(CompressorBase):
|
|
|
Note: While it makes no sense, the expensive compressor may well be set
|
|
|
to the LZ4 compressor.
|
|
|
"""
|
|
|
- compressor, compressed_data = LZ4_COMPRESSOR.decide_compress(data)
|
|
|
+ compressor, (meta, compressed_data) = LZ4_COMPRESSOR.decide_compress(meta, data)
|
|
|
# compressed_data includes the compression type header, while data does not yet
|
|
|
ratio = len(compressed_data) / (len(data) + 2)
|
|
|
if ratio < 0.97:
|
|
|
- return self.compressor, compressed_data
|
|
|
+ return self.compressor, (meta, compressed_data)
|
|
|
else:
|
|
|
- return compressor, compressed_data
|
|
|
+ return compressor, (meta, compressed_data)
|
|
|
|
|
|
- def decide(self, data):
|
|
|
- return self._decide(data)[0]
|
|
|
+ def decide(self, meta, data):
|
|
|
+ return self._decide(meta, data)[0]
|
|
|
|
|
|
- def compress(self, data):
|
|
|
- compressor, cheap_compressed_data = self._decide(data)
|
|
|
+ def compress(self, meta, data):
|
|
|
+ compressor, (cheap_meta, cheap_compressed_data) = self._decide(dict(meta), data)
|
|
|
if compressor in (LZ4_COMPRESSOR, NONE_COMPRESSOR):
|
|
|
# we know that trying to compress with expensive compressor is likely pointless,
|
|
|
# so we fallback to return the cheap compressed data.
|
|
|
- return cheap_compressed_data
|
|
|
+ return cheap_meta, cheap_compressed_data
|
|
|
# if we get here, the decider decided to try the expensive compressor.
|
|
|
# we also know that the compressed data returned by the decider is lz4 compressed.
|
|
|
- expensive_compressed_data = compressor.compress(data)
|
|
|
+ expensive_meta, expensive_compressed_data = compressor.compress(dict(meta), data)
|
|
|
ratio = len(expensive_compressed_data) / len(cheap_compressed_data)
|
|
|
if ratio < 0.99:
|
|
|
# the expensive compressor managed to squeeze the data significantly better than lz4.
|
|
|
- return expensive_compressed_data
|
|
|
+ return expensive_meta, expensive_compressed_data
|
|
|
else:
|
|
|
# otherwise let's just store the lz4 data, which decompresses extremely fast.
|
|
|
- return cheap_compressed_data
|
|
|
+ return cheap_meta, cheap_compressed_data
|
|
|
|
|
|
def decompress(self, data):
|
|
|
raise NotImplementedError
|
|
@@ -487,14 +518,14 @@ class ObfuscateSize(CompressorBase):
|
|
|
"""
|
|
|
Meta-Compressor that obfuscates the compressed data size.
|
|
|
"""
|
|
|
- ID = b'\x04'
|
|
|
+ ID = 0x04
|
|
|
name = 'obfuscate'
|
|
|
|
|
|
header_fmt = Struct('<I')
|
|
|
header_len = len(header_fmt.pack(0))
|
|
|
|
|
|
- def __init__(self, level=None, compressor=None):
|
|
|
- super().__init__(level=level) # data will be encrypted, so we can tell the level
|
|
|
+ def __init__(self, level=None, compressor=None, legacy_mode=False):
|
|
|
+ super().__init__(level=level, legacy_mode=legacy_mode) # data will be encrypted, so we can tell the level
|
|
|
self.compressor = compressor
|
|
|
if level is None:
|
|
|
pass # decompression
|
|
@@ -524,25 +555,30 @@ class ObfuscateSize(CompressorBase):
|
|
|
def _random_padding_obfuscate(self, compr_size):
|
|
|
return int(self.max_padding_size * random.random())
|
|
|
|
|
|
- def compress(self, data):
|
|
|
- compressed_data = self.compressor.compress(data) # compress data
|
|
|
+ def compress(self, meta, data):
|
|
|
+ assert not self.legacy_mode # we never call this in legacy mode
|
|
|
+ meta = dict(meta) # make a copy, do not modify caller's dict
|
|
|
+ meta, compressed_data = self.compressor.compress(meta, data) # compress data
|
|
|
compr_size = len(compressed_data)
|
|
|
- header = self.header_fmt.pack(compr_size)
|
|
|
+ assert "csize" in meta, repr(meta)
|
|
|
+ meta["psize"] = meta["csize"] # psize (payload size) is the csize (compressed size) of the inner compressor
|
|
|
addtl_size = self._obfuscate(compr_size)
|
|
|
addtl_size = max(0, addtl_size) # we can only make it longer, not shorter!
|
|
|
addtl_size = min(MAX_DATA_SIZE - 1024 - compr_size, addtl_size) # stay away from MAX_DATA_SIZE
|
|
|
trailer = bytes(addtl_size)
|
|
|
- obfuscated_data = b''.join([header, compressed_data, trailer])
|
|
|
- return super().compress(obfuscated_data) # add ID header
|
|
|
+ obfuscated_data = compressed_data + trailer
|
|
|
+ meta["csize"] = len(obfuscated_data) # csize is the overall output size of this "obfuscation compressor"
|
|
|
+ return meta, obfuscated_data # for borg2 it is enough that we have the payload size in meta["psize"]
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
- obfuscated_data = super().decompress(data) # remove obfuscator ID header
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ assert self.legacy_mode # borg2 never dispatches to this, only used for legacy mode
|
|
|
+ meta, obfuscated_data = super().decompress(meta, data) # remove obfuscator ID header
|
|
|
compr_size = self.header_fmt.unpack(obfuscated_data[0:self.header_len])[0]
|
|
|
compressed_data = obfuscated_data[self.header_len:self.header_len+compr_size]
|
|
|
if self.compressor is None:
|
|
|
compressor_cls = Compressor.detect(compressed_data)[0]
|
|
|
self.compressor = compressor_cls()
|
|
|
- return self.compressor.decompress(compressed_data) # decompress data
|
|
|
+ return self.compressor.decompress(meta, compressed_data) # decompress data
|
|
|
|
|
|
|
|
|
# Maps valid compressor names to their class
|
|
@@ -576,12 +612,18 @@ class Compressor:
|
|
|
self.params = kwargs
|
|
|
self.compressor = get_compressor(name, **self.params)
|
|
|
|
|
|
- def compress(self, data):
|
|
|
- return self.compressor.compress(data)
|
|
|
+ def compress(self, meta, data):
|
|
|
+ return self.compressor.compress(meta, data)
|
|
|
|
|
|
- def decompress(self, data):
|
|
|
- compressor_cls = self.detect(data)[0]
|
|
|
- return compressor_cls(**self.params).decompress(data)
|
|
|
+ def decompress(self, meta, data):
|
|
|
+ if self.compressor.legacy_mode:
|
|
|
+ hdr = data[:2]
|
|
|
+ else:
|
|
|
+ ctype = meta["ctype"]
|
|
|
+ clevel = meta["clevel"]
|
|
|
+ hdr = bytes((ctype, clevel))
|
|
|
+ compressor_cls = self.detect(hdr)[0]
|
|
|
+ return compressor_cls(**self.params).decompress(meta, data)
|
|
|
|
|
|
@staticmethod
|
|
|
def detect(data):
|