Skip to content

Commit 43b6fcf

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 1d06159 commit 43b6fcf

File tree

22 files changed

+281
-0
lines changed

22 files changed

+281
-0
lines changed

overlay.nix

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ final: prev:
2929
];
3030
};
3131

32+
dependencies = (super.dependencies or [ ]) ++ [ final.python3.pkgs.pyzstd ];
33+
3234
# remove this when packaging changes are upstreamed
3335
cargoDeps = final.rustPlatform.importCargoLock {
3436
lockFile = ./Cargo.lock;

pyproject.toml

+1
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

+2
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 = (
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import lzma
2+
import re
3+
import zlib
4+
from pathlib import Path
5+
from typing import Callable, Optional
6+
7+
import pyzstd
8+
9+
from unblob.file_utils import (
10+
Endian,
11+
FileSystem,
12+
InvalidInputFormat,
13+
StructParser,
14+
iterate_file,
15+
)
16+
from unblob.models import (
17+
Extractor,
18+
ExtractResult,
19+
File,
20+
Regex,
21+
StructHandler,
22+
ValidChunk,
23+
)
24+
25+
# [Ref] https://github.com/freebsd/freebsd-src/tree/master/sys/geom/uzip
26+
C_DEFINITIONS = r"""
27+
typedef struct uzip_header{
28+
char magic[16];
29+
char format[112];
30+
uint32_t block_size;
31+
uint32_t block_count;
32+
uint64_t toc[block_count];
33+
} uzip_header_t;
34+
"""
35+
36+
HEADER_STRUCT = "uzip_header_t"
37+
38+
ZLIB_COMPRESSION = "#!/bin/sh\x0a#V2.0\x20"
39+
LZMA_COMPRESSION = "#!/bin/sh\x0a#L3.0\x0a"
40+
ZSTD_COMPRESSION = "#!/bin/sh\x0a#Z4.0\x20"
41+
42+
43+
class Decompressor:
44+
DECOMPRESSOR: Callable
45+
46+
def __init__(self):
47+
self._decompressor = self.DECOMPRESSOR()
48+
49+
def decompress(self, data: bytes) -> bytes:
50+
return self._decompressor.decompress(data)
51+
52+
def flush(self) -> bytes:
53+
return b""
54+
55+
56+
class LZMADecompressor(Decompressor):
57+
DECOMPRESSOR = lzma.LZMADecompressor
58+
59+
60+
class ZLIBDecompressor(Decompressor):
61+
DECOMPRESSOR = zlib.decompressobj
62+
63+
def flush(self) -> bytes:
64+
return self._decompressor.flush()
65+
66+
67+
class ZSTDDecompressor(Decompressor):
68+
DECOMPRESSOR = pyzstd.EndlessZstdDecompressor
69+
70+
71+
DECOMPRESS_METHOD: dict[bytes, type[Decompressor]] = {
72+
ZLIB_COMPRESSION.encode(): ZLIBDecompressor,
73+
LZMA_COMPRESSION.encode(): LZMADecompressor,
74+
ZSTD_COMPRESSION.encode(): ZSTDDecompressor,
75+
}
76+
77+
78+
class UZIPExtractor(Extractor):
79+
def extract(self, inpath: Path, outdir: Path):
80+
infile = File.from_path(inpath)
81+
parser = StructParser(C_DEFINITIONS)
82+
header = parser.parse(HEADER_STRUCT, infile, Endian.BIG)
83+
fs = FileSystem(outdir)
84+
outpath = Path(inpath.stem)
85+
86+
try:
87+
decompressor_cls = DECOMPRESS_METHOD[header.magic]
88+
except LookupError:
89+
raise InvalidInputFormat("unsupported compression format") from None
90+
91+
with fs.open(outpath, "wb+") as outfile:
92+
for current_offset, next_offset in zip(header.toc[:-1], header.toc[1:]):
93+
compressed_len = next_offset - current_offset
94+
if compressed_len == 0:
95+
continue
96+
decompressor = decompressor_cls()
97+
for chunk in iterate_file(infile, current_offset, compressed_len):
98+
outfile.write(decompressor.decompress(chunk))
99+
outfile.write(decompressor.flush())
100+
return ExtractResult(reports=fs.problems)
101+
102+
103+
class UZIPHandler(StructHandler):
104+
NAME = "uzip"
105+
PATTERNS = [
106+
Regex(re.escape(ZLIB_COMPRESSION)),
107+
Regex(re.escape(LZMA_COMPRESSION)),
108+
Regex(re.escape(ZSTD_COMPRESSION)),
109+
]
110+
HEADER_STRUCT = HEADER_STRUCT
111+
C_DEFINITIONS = C_DEFINITIONS
112+
EXTRACTOR = UZIPExtractor()
113+
114+
def is_valid_header(self, header) -> bool:
115+
return (
116+
header.block_count > 0
117+
and header.block_size > 0
118+
and header.block_size % 512 == 0
119+
)
120+
121+
def calculate_chunk(self, file: File, start_offset: int) -> Optional[ValidChunk]:
122+
header = self.parse_header(file, Endian.BIG)
123+
124+
if not self.is_valid_header(header):
125+
raise InvalidInputFormat("Invalid uzip header.")
126+
127+
# take the last TOC block offset, end of file is that block offset,
128+
# starting from the start offset
129+
end_offset = start_offset + header.toc[-1]
130+
return ValidChunk(
131+
start_offset=start_offset,
132+
end_offset=end_offset,
133+
)
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
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
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
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
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
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
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
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
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
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
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
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
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)