Skip to content

Commit 9b2c5cb

Browse files
committed
Make CompressedStream more generic
1 parent d0e49b1 commit 9b2c5cb

2 files changed

Lines changed: 26 additions & 28 deletions

File tree

dissect/util/stream.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -651,14 +651,21 @@ def readall(self) -> bytes:
651651
class CompressedStream(AlignedStream):
652652
"""Create a stream from a file-like object which is compresssed. Decompressing it with the supplied decompressor.
653653
654+
This is useful for compressed file formats that store the compressed data in chunks like:
655+
* Windows Imaging (WIM) archives (LZXPRESS + Huffman, LZX)
656+
* Windows Overlay Filter (WOF) compressed files (LZXPRESS + Huffman, LZNT1, LZX) .
657+
* Hiberfil.sys files (LZXPRESS).
658+
654659
Args:
655660
fh: The source file-like object.
656661
offset: The offset in the source file-like object to start decompressing from.
657662
compressed_size: The size of the compressed data.
658663
original_size: The size of the decompressed data.
659664
decompressor: A function that decompresses a chunk of data.
660665
chunk_size: The size of each chunk in bytes. Default is 32 KiB.
666+
chunks: A tuple of offsets to each chunk of compressed data.
661667
"""
668+
662669
def __init__(
663670
self,
664671
fh: BinaryIO,
@@ -667,44 +674,33 @@ def __init__(
667674
original_size: int,
668675
decompressor: Callable[[bytes], bytes],
669676
chunk_size: int = 32 * 1024,
677+
chunks: tuple[int, ...] | None = None,
670678
):
671679
self.fh = fh
672680
self.offset = offset
673681
self.compressed_size = compressed_size
674682
self.original_size = original_size
675683
self.decompressor = decompressor
676684
self.chunk_size = chunk_size
677-
678-
# Read the chunk table in advance
679-
if chunk_size:
680-
fh.seek(self.offset)
681-
num_chunks = (original_size + self.chunk_size - 1) // self.chunk_size - 1
682-
if num_chunks == 0:
683-
self._chunks = (0,)
684-
else:
685-
entry_size = "Q" if original_size > 0xFFFFFFFF else "I"
686-
pattern = f"<{num_chunks}{entry_size}"
687-
self._chunks = (0, *struct.unpack(pattern, fh.read(struct.calcsize(pattern))))
688-
689-
self._data_offset = fh.tell()
685+
self.chunks = chunks or (offset,)
690686

691687
self._read_chunk = lru_cache(32)(self._read_chunk)
692688
super().__init__(self.original_size)
693689

694690
def _read(self, offset: int, length: int) -> bytes:
695691
result = []
696692

697-
num_chunks = len(self._chunks)
693+
num_chunks = len(self.chunks)
698694
chunk, offset_in_chunk = divmod(offset, self.chunk_size)
699695

700696
while length:
701697
if chunk >= num_chunks:
702698
# We somehow requested more data than we have runs for
703699
break
704700

705-
chunk_offset = self._chunks[chunk]
701+
chunk_offset = self.chunks[chunk]
706702
if chunk < num_chunks - 1:
707-
next_chunk_offset = self._chunks[chunk + 1]
703+
next_chunk_offset = self.chunks[chunk + 1]
708704
chunk_remaining = self.chunk_size - offset_in_chunk
709705
else:
710706
next_chunk_offset = self.compressed_size
@@ -722,6 +718,6 @@ def _read(self, offset: int, length: int) -> bytes:
722718
return b"".join(result)
723719

724720
def _read_chunk(self, offset: int, size: int) -> bytes:
725-
self.fh.seek(self._data_offset + offset)
721+
self.fh.seek(offset)
726722
buf = self.fh.read(size)
727723
return self.decompressor(buf)

tests/test_stream.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -220,24 +220,26 @@ def test_zlib_stream() -> None:
220220
assert fh.read() == data
221221

222222

223-
def test_compressed_stream() -> None:
224-
data = io.BytesIO(bytes.fromhex(
225-
"4034030000000000000000000000000000000000000000000000000000000000"
226-
"0000000000000000000000000000000000000000000000000000000000000000"
227-
"0000000000000000000000000000000000000000000000000000000000000000"
228-
"0000000000000000000000000000000000000000000000000000000000000000"
229-
"0300000000000010000000000000000000000000000000000000000000000000"
230-
"0000000000000000000000000000000000000000000000000000000000000000"
231-
"0000000000000000000000000000000000000000000000000000000000000000"
232-
"0000000000000000000000000000000000000000000000000000000000000000"
233-
"a2e700b0fffc0ffffc07fffc030000fffc03"
223+
def test_compressed_stream_lzxpress_huffman() -> None:
224+
data = io.BytesIO(
225+
bytes.fromhex(
226+
"4034030000000000000000000000000000000000000000000000000000000000"
227+
"0000000000000000000000000000000000000000000000000000000000000000"
228+
"0000000000000000000000000000000000000000000000000000000000000000"
229+
"0000000000000000000000000000000000000000000000000000000000000000"
230+
"0300000000000010000000000000000000000000000000000000000000000000"
231+
"0000000000000000000000000000000000000000000000000000000000000000"
232+
"0000000000000000000000000000000000000000000000000000000000000000"
233+
"0000000000000000000000000000000000000000000000000000000000000000"
234+
"a2e700b0fffc0ffffc07fffc030000fffc03"
234235
)
235236
)
236237

237238
size = 8192
238239
compressed_size = 274
239240

240241
fh = stream.CompressedStream(data, 0, compressed_size, size, lzxpress_huffman.decompress)
242+
assert fh.chunks == (0,)
241243

242244
fh.seek(0)
243245
assert fh.read(4096) == b"\x01" * 4096

0 commit comments

Comments
 (0)