Skip to content

Commit 9592034

Browse files
committed
feat(handler): add geom_uzip handler
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 that is stored on 10 bytes. > Version, which can change and is stored on 13 bytes. > Command, which can change and is stored on 105 bytes. > 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 uzip file. It will find the compressed blocks, decompress them using zlib and parses them together to recover the decompressed 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
1 parent 90dd7fa commit 9592034

File tree

4 files changed

+87
-0
lines changed

4 files changed

+87
-0
lines changed

python/unblob/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .compression import (
2626
bzip2,
2727
compress,
28+
geom_uzip,
2829
gzip,
2930
lz4,
3031
lzh,
@@ -116,6 +117,7 @@
116117
zlib.ZlibHandler,
117118
engenius.EngeniusHandler,
118119
ecc.AutelECCHandler,
120+
geom_uzip.UZIPHandler,
119121
)
120122

121123
BUILTIN_DIR_HANDLERS: DirectoryHandlers = (
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from io import SEEK_SET
2+
from pathlib import Path
3+
from typing import Optional
4+
from zlib import decompress
5+
6+
from structlog import get_logger
7+
8+
from unblob.file_utils import Endian, FileSystem, StructParser, read_until_past
9+
from unblob.models import (
10+
Extractor,
11+
ExtractResult,
12+
File,
13+
HexString,
14+
StructHandler,
15+
ValidChunk,
16+
)
17+
18+
""",
19+
The geom_uzip header follows the following structure:
20+
10 bytes shebang, with newline suffix: #!/bin/sh\n
21+
13 bytes version, with newline suffix: #V2.0 Format\n or #L3.0 Format\n
22+
105 bytes command, with null bytes suffix: (kldstat -qm g_uzip||kldload geom_uzip)>&-&&mount_cd9660 /dev/`mdconfig -af $0`.uzip $1\nexit $?\n\x00\x00\x00\x00\x00\x00\x00\x00\x00
23+
"""
24+
25+
C_DEFINITIONS = r"""
26+
typedef struct uzip_header{
27+
char magic[10]; /* '10 bytes '*/
28+
char version[13]; /* 13 bytes */
29+
char format[105]; /* 105 bytes */
30+
uint32_t block_size; /* 4 bytes */
31+
uint32_t block_count; /* 4 bytes - Number of blocks */
32+
uint64_t toc[block_count]; /* table of content */
33+
} uzip_header_t;
34+
"""
35+
HEADER_STRUCT = "uzip_header_t"
36+
37+
logger = get_logger()
38+
39+
40+
class UZIPExtractor(Extractor):
41+
def extract(self, inpath: Path, outdir: Path):
42+
infile = File.from_path(inpath)
43+
parser = StructParser(C_DEFINITIONS)
44+
header = parser.parse(HEADER_STRUCT, infile, Endian.BIG)
45+
fs = FileSystem(outdir)
46+
outpath = Path(inpath.stem)
47+
with fs.open(outpath, "wb+") as outfile:
48+
for current_offset, next_offset in zip(header.toc[:-1], header.toc[1:]):
49+
compressed_len = next_offset - current_offset
50+
if compressed_len == 0:
51+
continue
52+
infile.seek(current_offset, SEEK_SET)
53+
outfile.write(decompress(infile.read(compressed_len)))
54+
return ExtractResult(reports=fs.problems)
55+
56+
57+
class UZIPHandler(StructHandler):
58+
NAME = "uzip"
59+
PATTERNS = [
60+
HexString(
61+
"23 21 2F 62 69 6E 2F 73 68 0A 23 (56 32 | 4c 33) 2e 30 20 46 6f 72 6d 61 74 0A"
62+
)
63+
]
64+
65+
HEADER_STRUCT = HEADER_STRUCT
66+
C_DEFINITIONS = C_DEFINITIONS
67+
EXTRACTOR = UZIPExtractor()
68+
69+
def calculate_chunk(self, file: File, start_offset: int) -> Optional[ValidChunk]:
70+
header = self.parse_header(file, Endian.BIG)
71+
# take the last TOC block offset, end of file is that block offset + block size,
72+
# starting from the start offset
73+
end_offset = start_offset + header.toc[-1]
74+
file.seek(end_offset, SEEK_SET)
75+
end_offset = read_until_past(file, b"\x00")
76+
return ValidChunk(
77+
start_offset=start_offset,
78+
end_offset=end_offset,
79+
)
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:e04449191a0c3eab172e5819c5c1e9c10a9cd2e4ffca2abacf065ac1e3bd1328
3+
size 458752

0 commit comments

Comments
 (0)