Skip to content

Commit 49c3f4c

Browse files
authored
Merge pull request #1491 from elektrischermoench/feat/handler-lzfse
feat(handler): add LZFSE decompression handler
2 parents 272a887 + e2cfb6b commit 49c3f4c

16 files changed

Lines changed: 255 additions & 0 deletions

File tree

docs/handlers.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
| [`LZ4`](#lz4) | COMPRESSION | :octicons-check-16: |
4444
| [`LZ4 (LEGACY)`](#lz4-legacy) | COMPRESSION | :octicons-check-16: |
4545
| [`LZ4 (SKIPPABLE)`](#lz4-skippable) | COMPRESSION | :octicons-check-16: |
46+
| [`LZFSE`](#lzfse) | COMPRESSION | :octicons-check-16: |
4647
| [`LZH`](#lzh) | COMPRESSION | :octicons-check-16: |
4748
| [`LZIP`](#lzip) | COMPRESSION | :octicons-check-16: |
4849
| [`LZMA`](#lzma) | COMPRESSION | :octicons-check-16: |
@@ -800,6 +801,22 @@
800801

801802
- [LZ4 Frame Format Documentation](https://github.com/lz4/lz4/blob/dev/doc/lz4_Frame_format.md){ target="_blank" }
802803
- [LZ4 Wikipedia](https://en.wikipedia.org/wiki/LZ4_(compression_algorithm)){ target="_blank" }
804+
## LZFSE
805+
806+
!!! success "Fully supported"
807+
808+
=== "Description"
809+
810+
LZFSE is a lossless compression algorithm developed by Apple and open-sourced in 2016. It combines Lempel-Ziv back-references with Finite State Entropy coding and is the default compression format used in iOS and macOS firmware images.
811+
812+
---
813+
814+
- **Handler type:** Compression
815+
- **Vendor:** Apple
816+
817+
=== "References"
818+
819+
- [lzfse - Apple open-source LZFSE library](https://github.com/lzfse/lzfse){ target="_blank" }
803820
## LZH
804821

805822
!!! success "Fully supported"

package.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ python3.pkgs.buildPythonApplication {
8484
lark
8585
lief.py
8686
lzallright
87+
lzfse
8788
python3.pkgs.lz4 # shadowed by pkgs.lz4
8889
plotext
8990
pluggy

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"lief>=0.16.1",
1616
"lz4>=4.3.2,!=4.4.3", # 4.4.3 doesn't have aarch64 wheels https://github.com/python-lz4/python-lz4/pull/298
1717
"lzallright>=0.2.6",
18+
"lzfse>=0.4.2",
1819
"plotext>=4.2.0,<6.0",
1920
"pluggy>=1.3.0",
2021
"pydantic>=2.0",

python/unblob/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
compress,
3535
gzip,
3636
lz4,
37+
lzfse,
3738
lzh,
3839
lzip,
3940
lzma,
@@ -136,6 +137,7 @@
136137
stuffit.StuffIt5Handler,
137138
bzip2.BZip2Handler,
138139
compress.UnixCompressHandler,
140+
lzfse.LZFSEHandler,
139141
gzip.GZIPHandler,
140142
lzh.LZHHandler,
141143
lzip.LZipHandler,
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import io
2+
from enum import Enum
3+
from pathlib import Path
4+
5+
import lzfse
6+
7+
from unblob.file_utils import (
8+
Endian,
9+
File,
10+
FileSystem,
11+
InvalidInputFormat,
12+
StructParser,
13+
)
14+
from unblob.models import (
15+
Extractor,
16+
ExtractResult,
17+
Handler,
18+
HandlerDoc,
19+
HandlerType,
20+
HexString,
21+
Reference,
22+
ValidChunk,
23+
)
24+
25+
26+
class LZFSEMagic(bytes, Enum):
27+
END = b"bvx$" # end-of-stream block
28+
UNCOMPRESSED = b"bvx-" # raw block
29+
LZVN = b"bvxn" # LZVN-compressed block
30+
LZFSE_V1 = b"bvx1" # LZFSE v1 block (legacy)
31+
LZFSE_V2 = b"bvx2" # LZFSE v2 block
32+
33+
34+
# sizeof(lzfse_compressed_block_header_v1), including struct alignment padding
35+
_V1_HEADER_SIZE = 772
36+
37+
# length of every LZFSE block magic
38+
MAGIC_LEN = 4
39+
40+
# a stream decoding to zero bytes: an empty raw block followed by the end block,
41+
# as emitted by lzfse.compress(b"")
42+
_EMPTY_STREAM = LZFSEMagic.UNCOMPRESSED + b"\x00\x00\x00\x00" + LZFSEMagic.END
43+
44+
C_DEFINITIONS = r"""
45+
typedef struct lzfse_uncompressed {
46+
char magic[4];
47+
uint32 n_raw_bytes;
48+
} lzfse_uncompressed_t;
49+
50+
typedef struct lzvn_compressed {
51+
char magic[4];
52+
uint32 n_raw_bytes;
53+
uint32 n_payload_bytes;
54+
} lzvn_compressed_t;
55+
56+
typedef struct lzfse_v1 {
57+
char magic[4];
58+
uint32 n_raw_bytes;
59+
uint32 n_payload_bytes;
60+
uint32 n_literals;
61+
uint32 n_matches;
62+
uint32 n_literal_payload_bytes;
63+
uint32 n_lmd_payload_bytes;
64+
} lzfse_v1_t;
65+
66+
typedef struct lzfse_v2 {
67+
char magic[4];
68+
uint32 n_raw_bytes;
69+
uint64 packed_fields_0;
70+
uint64 packed_fields_1;
71+
uint64 packed_fields_2;
72+
} lzfse_v2_t;
73+
"""
74+
75+
_parser = StructParser(C_DEFINITIONS)
76+
77+
78+
class LZFSEExtractor(Extractor):
79+
def extract(self, inpath: Path, outdir: Path) -> ExtractResult | None:
80+
fs = FileSystem(outdir)
81+
with File.from_path(inpath) as file:
82+
content = file.read()
83+
# lzfse.decompress raises on a stream that decodes to zero bytes,
84+
# so write the empty output ourselves.
85+
decompressed = (
86+
b"" if content == _EMPTY_STREAM else lzfse.decompress(content)
87+
)
88+
fs.write_bytes(Path(f"{inpath.stem}.bin"), decompressed)
89+
return ExtractResult(reports=fs.problems)
90+
91+
92+
class LZFSEHandler(Handler):
93+
NAME = "lzfse"
94+
95+
PATTERNS = [
96+
HexString("62 76 78 2D"), # "bvx-" uncompressed block
97+
HexString("62 76 78 31"), # "bvx1" LZFSE v1 compressed block (legacy)
98+
HexString("62 76 78 6E"), # "bvxn" LZVN compressed block
99+
HexString("62 76 78 32"), # "bvx2" LZFSE v2 compressed block
100+
]
101+
102+
EXTRACTOR = LZFSEExtractor()
103+
104+
DOC = HandlerDoc(
105+
name="LZFSE",
106+
description="LZFSE is a lossless compression algorithm developed by Apple and open-sourced in 2016. It combines Lempel-Ziv back-references with Finite State Entropy coding and is the default compression format used in iOS and macOS firmware images.",
107+
handler_type=HandlerType.COMPRESSION,
108+
vendor="Apple",
109+
references=[
110+
Reference(
111+
title="lzfse - Apple open-source LZFSE library",
112+
url="https://github.com/lzfse/lzfse",
113+
),
114+
],
115+
limitations=[],
116+
)
117+
118+
def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
119+
# An LZFSE stream is a sequence of blocks terminated by an end-of-stream
120+
# block. Walk the blocks using each header's declared size instead of
121+
# scanning for "bvx$", which could otherwise be matched inside payload data.
122+
offset = start_offset
123+
magic = file[offset : offset + MAGIC_LEN]
124+
while magic != LZFSEMagic.END:
125+
if len(magic) < MAGIC_LEN:
126+
raise InvalidInputFormat("Truncated LZFSE stream: no end block")
127+
block_size = self._block_size(file, offset, magic)
128+
if block_size <= 0:
129+
raise InvalidInputFormat("Invalid LZFSE block size")
130+
offset += block_size
131+
magic = file[offset : offset + MAGIC_LEN]
132+
133+
return ValidChunk(start_offset=start_offset, end_offset=offset + MAGIC_LEN)
134+
135+
@staticmethod
136+
def _block_size(file: File, offset: int, magic: bytes) -> int:
137+
"""Size in bytes of the LZFSE block at offset, including its header."""
138+
file.seek(offset, io.SEEK_SET)
139+
match magic:
140+
case LZFSEMagic.UNCOMPRESSED:
141+
header = _parser.parse("lzfse_uncompressed_t", file, Endian.LITTLE)
142+
size = 8 + header.n_raw_bytes
143+
case LZFSEMagic.LZVN:
144+
header = _parser.parse("lzvn_compressed_t", file, Endian.LITTLE)
145+
size = 12 + header.n_payload_bytes
146+
case LZFSEMagic.LZFSE_V1:
147+
header = _parser.parse("lzfse_v1_t", file, Endian.LITTLE)
148+
size = (
149+
_V1_HEADER_SIZE
150+
+ header.n_literal_payload_bytes
151+
+ header.n_lmd_payload_bytes
152+
)
153+
case LZFSEMagic.LZFSE_V2:
154+
header = _parser.parse("lzfse_v2_t", file, Endian.LITTLE)
155+
# v2 packs the sizes into bit-fields across three uint64s.
156+
n_literal = (header.packed_fields_0 >> 20) & 0xFFFFF
157+
n_lmd = (header.packed_fields_1 >> 40) & 0xFFFFF
158+
header_size = header.packed_fields_2 & 0xFFFFFFFF
159+
size = header_size + n_literal + n_lmd
160+
case _:
161+
raise InvalidInputFormat(f"Unknown LZFSE block magic: {magic!r}")
162+
return size
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:cf0a19bc8c7a1ccba4e1e983ce0ba2c22788ead0796cd8288587de8e4094fc82
3+
size 38
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:357bbbd3de75f5b4772fb4d2be5de0611d1902b20a31ff38778ac93c0dc2940d
3+
size 816
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:3177574f01fa30b42eb8b316949854c0ede3c1a8c3a724e6bffcd3774591c43c
3+
size 191
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:c33c8622ba697ef366f29bf58e76f22ff557f4cef7e875289e11cb3caef8c046
3+
size 149
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:9502b7226136d6e97cbe78c04ac9ea572595fc7b25ae1394759d45a1796b86ff
3+
size 12

0 commit comments

Comments
 (0)