Skip to content

Commit b095981

Browse files
committed
feat(handler): add geom handler for uzip, lzma and zstd compression
Geom_uzip is a FreeBSD feature for creating compressed disk images (usually containing UFS). The compression is done in blocks, and the resulting .uzip file can be mounted via the GEOM framework on FreeBSD. The mkuzip header includes a table with block counts and sizes. The header declares the block size (size of decompressed blocks) and total number of blocks. Block size must be a multiple of 512 and defaults to 16384 in mkuzip. It has the following structure: > Magic, which is a shebang & compression identifier stored on 16 bytes. > Format, which is a shell command that provides some general information. > Block size, stored on 4 bytes. > Block count, stored on 4 bytes. > Table of content (TOC), which depends on the file lentgh. The TOC is a list of uint64_t offsets into the file for each block. To determine the length of a given block, read the next TOC entry and subtract the current offset from the next offset (this is why there is an extra TOC entry at the end). Each block is compressed using zlib. A standard zlib decompressor will decode them to a block of size block_size. Unblob parses the TOC to determine end & start offset of the compressed file. It detects the compression method (zlib, lzma or zstd). Finally the chunks are decompressed to revocer the inital file. Empty chunks are ignored, which is why the decompressed file with unlbob can be a little bit lighter than the original one. [Sources] https://github.com/mikeryan/unuzip https://www.baeldung.com/linux/filesystem-in-a-file https://docs.python.org/3/library/zlib.html https://github.com/freebsd/freebsd-src/blob/master/sys/geom/uzip/g_uzip.c https://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html https://www.mail-archive.com/[email protected]/msg34955.html
1 parent b5fb776 commit b095981

File tree

21 files changed

+234
-0
lines changed

21 files changed

+234
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"pyfatfs>=1.0.5",
2020
"pyperscan>=0.3.0",
2121
"python-magic>=0.4.27",
22+
"pyzstd",
2223
"rarfile>=4.1",
2324
"rich>=13.3.5",
2425
"structlog>=24.1.0",

python/unblob/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
lzip,
3232
lzma,
3333
lzo,
34+
uzip,
3435
xz,
3536
zlib,
3637
zstd,
@@ -116,6 +117,7 @@
116117
zlib.ZlibHandler,
117118
engenius.EngeniusHandler,
118119
ecc.AutelECCHandler,
120+
uzip.UZIPHandler,
119121
)
120122

121123
BUILTIN_DIR_HANDLERS: DirectoryHandlers = (
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import lzma
2+
import zlib
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
import pyzstd
7+
8+
from unblob.file_utils import (
9+
Endian,
10+
FileSystem,
11+
InvalidInputFormat,
12+
StructParser,
13+
iterate_file,
14+
)
15+
from unblob.models import (
16+
Extractor,
17+
ExtractResult,
18+
File,
19+
Regex,
20+
StructHandler,
21+
ValidChunk,
22+
)
23+
24+
C_DEFINITIONS = r"""
25+
typedef struct uzip_header{
26+
char magic[16]; /* 16 bytes */
27+
char format[112]; /* 112 bytes */
28+
uint32_t block_size;
29+
uint32_t block_count;
30+
uint64_t toc[block_count]; /* table of content */
31+
} uzip_header_t;
32+
"""
33+
34+
HEADER_STRUCT = "uzip_header_t"
35+
36+
ZLIB_COMPRESSION = b"#!/bin/sh\x0a#V2.0\x20"
37+
LZMA_COMPRESSION = b"#!/bin/sh\x0a#L3.0\x0a"
38+
ZSTD_COMPRESSION = b"#!/bin/sh\x0a#Z4.0\x20"
39+
40+
41+
class UZIPExtractor(Extractor):
42+
def extract(self, inpath: Path, outdir: Path):
43+
infile = File.from_path(inpath)
44+
parser = StructParser(C_DEFINITIONS)
45+
header = parser.parse(HEADER_STRUCT, infile, Endian.BIG)
46+
fs = FileSystem(outdir)
47+
outpath = Path(inpath.stem)
48+
49+
if header.magic == ZLIB_COMPRESSION:
50+
decompressor_cls = zlib.decompressobj
51+
elif header.magic == LZMA_COMPRESSION:
52+
decompressor_cls = lzma.LZMADecompressor
53+
elif header.magic == ZSTD_COMPRESSION:
54+
decompressor_cls = pyzstd.ZstdDecompressor
55+
else:
56+
raise InvalidInputFormat("unsupported compression format")
57+
58+
with fs.open(outpath, "wb+") as outfile:
59+
for current_offset, next_offset in zip(header.toc[:-1], header.toc[1:]):
60+
compressed_len = next_offset - current_offset
61+
if compressed_len == 0:
62+
continue
63+
decompressor = decompressor_cls()
64+
for chunk in iterate_file(infile, current_offset, compressed_len):
65+
outfile.write(decompressor.decompress(chunk))
66+
return ExtractResult(reports=fs.problems)
67+
68+
69+
class UZIPHandler(StructHandler):
70+
NAME = "uzip"
71+
PATTERNS = [
72+
Regex(r"#!/bin/sh\x0A#V2.0"),
73+
Regex(r"#!/bin/sh\x0A#L3.0"),
74+
Regex(r"#!/bin/sh\x0A#Z4.0"),
75+
]
76+
HEADER_STRUCT = HEADER_STRUCT
77+
C_DEFINITIONS = C_DEFINITIONS
78+
EXTRACTOR = UZIPExtractor()
79+
80+
def calculate_chunk(self, file: File, start_offset: int) -> Optional[ValidChunk]:
81+
header = self.parse_header(file, Endian.BIG)
82+
# take the last TOC block offset, end of file is that block offset,
83+
# starting from the start offset
84+
end_offset = start_offset + header.toc[-1]
85+
return ValidChunk(
86+
start_offset=start_offset,
87+
end_offset=end_offset,
88+
)
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:bc53a5de25e6f5326564264fee9e1210067311c237d4d7a8299ebf244652cf05
3+
size 59392
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:c8f164384ebee852bbdc6f5fabcf231fa5fc35d9f236c30e38b9746f871be122
3+
size 59316
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e04449191a0c3eab172e5819c5c1e9c10a9cd2e4ffca2abacf065ac1e3bd1328
3+
size 458752
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:f2c0d5456a983ecd12e314fcfa19879179fc8424343baeb1325457472ae85601
3+
size 76
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:1e04c83a5444127b9ec58c4b2d8fef904816cee4609d798589d6e8af6086a322
3+
size 59904
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:cc069dc850a564078858cc13dc743dc5772d1e28edb1ce8a714f5a8749b5d43d
3+
size 60032
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:2ba6c3e09fa4b144f8f9fc29721c71df0bee753507c7071bdb8132409ce182d4
3+
size 59397
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e04449191a0c3eab172e5819c5c1e9c10a9cd2e4ffca2abacf065ac1e3bd1328
3+
size 458752
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:e48783451cfffa909b2c92ddb2b4c06b836aaa56f16aaab96349e8e9074d45b8
3+
size 507
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:c33ff1723e6b94ae7e6a0ecad3c8a5fc43ab6f39468170f7467e11a8192f6164
3+
size 64
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:7ad2f37110df3519cd58ede90a97a481853f2c9da95db4513d523f94aab9ca8c
3+
size 571
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:2ba6c3e09fa4b144f8f9fc29721c71df0bee753507c7071bdb8132409ce182d4
3+
size 59397
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e04449191a0c3eab172e5819c5c1e9c10a9cd2e4ffca2abacf065ac1e3bd1328
3+
size 458752
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:0f8a24f2de9727324844a152716880a2cb29512d19918ade566811fa3a8ae8d1
3+
size 58368
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:204c31af96edd20c6e074f58663a78106a8671930c76826938dcce4b9553d00e
3+
size 58269
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e04449191a0c3eab172e5819c5c1e9c10a9cd2e4ffca2abacf065ac1e3bd1328
3+
size 458752
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:4b298058e1d5fd3f2fa20ead21773912a5dc38da3c0da0bbc7de1adfb6011f1c
3+
size 99

0 commit comments

Comments
 (0)