1"""7-zip handlers.
2
37-zip archive file format SHALL consist of three part. 7-zip archive
4file SHALL start with signature header. The data block SHOULD placed
5after the signature header. The data block is shown as Packed
6Streams. A header database SHOULD be placed after the data block.
7The data block MAY be empty when no archived contents exists. So
8Packed Streams is optional. Since Header database CAN be encoded then
9it SHOULD place after data block, that is Packed Streams for Headers.
10When Header database is encoded, Header encode Information SHALL
11placed instead of Header.
12
13[Signature Header] [Data] [Header Database]
14
15https://fastapi.metacpan.org/source/BJOERN/Compress-Deflate7-1.0/7zip/DOC/7zFormat.txt
16"7z uses little endian encoding."
17
18https://py7zr.readthedocs.io/en/latest/archive_format.html
19"""
20
21import binascii
22from pathlib import Path
23
24from structlog import get_logger
25
26from unblob.extractors import Command
27from unblob.report import MetadataReport, register_report_type
28
29from ...extractors.command import MultiFileCommand
30from ...file_utils import Endian, InvalidInputFormat, StructParser
31from ...models import (
32 DirectoryHandler,
33 File,
34 Glob,
35 HandlerDoc,
36 HandlerType,
37 HexString,
38 MultiFile,
39 Reference,
40 StructHandler,
41 ValidChunk,
42)
43
44logger = get_logger()
45
46C_DEFINITIONS = r"""
47 typedef struct sevenzip_header {
48 char magic[6];
49 uint8 version_maj;
50 uint8 version_min;
51 uint32 crc;
52 uint64 next_header_offset;
53 uint64 next_header_size;
54 uint32 next_header_crc;
55 } sevenzip_header_t;
56"""
57HEADER_STRUCT = "sevenzip_header_t"
58HEADER_SIZE = 6 + 1 + 1 + 4 + 8 + 8 + 4
59
60HEADER_PARSER = StructParser(C_DEFINITIONS)
61
62# StartHeader (next_header_offset, next_header_size, next_header_crc)
63START_HEADER_SIZE = 8 + 8 + 4
64
65
66SEVENZIP_MAGIC = b"7z\xbc\xaf\x27\x1c"
67
68
69def check_header_crc(header):
70 # CRC includes the StartHeader (next_header_offset, next_header_size, next_header_crc)
71 # CPP/7zip/Archive/7z/7zOut.cpp COutArchive::WriteStartHeader
72 calculated_crc = binascii.crc32(header.dumps()[-START_HEADER_SIZE:])
73 if header.crc != calculated_crc:
74 raise InvalidInputFormat("Invalid sevenzip header CRC")
75
76
77def calculate_sevenzip_size(header) -> int:
78 return len(header) + header.next_header_offset + header.next_header_size
79
80
81@register_report_type
82class SevenZipMetadataReport(MetadataReport):
83 """Report for 7-Zip metadata."""
84
85 version_major: int
86 version_minor: int
87
88
89class SevenZipHandler(StructHandler):
90 NAME = "sevenzip"
91
92 PATTERNS = [
93 HexString(
94 """
95 // '7', 'z', 0xBC, 0xAF, 0x27, 0x1C
96 37 7A BC AF 27 1C
97 """
98 )
99 ]
100 C_DEFINITIONS = C_DEFINITIONS
101 HEADER_STRUCT = HEADER_STRUCT
102 EXTRACTOR = Command("7z", "x", "-p", "-y", "{inpath}", "-o{outdir}")
103
104 DOC = HandlerDoc(
105 name="7-Zip",
106 description="The 7-Zip file format is a compressed archive format with high compression ratios, supporting multiple algorithms, CRC checks, and multi-volume archives.",
107 handler_type=HandlerType.ARCHIVE,
108 vendor=None,
109 references=[
110 Reference(
111 title="7-Zip Technical Documentation",
112 url="https://fastapi.metacpan.org/source/BJOERN/Compress-Deflate7-1.0/7zip/DOC/7zFormat.txt",
113 ),
114 ],
115 limitations=[],
116 )
117
118 def get_metadata_reports(self, header) -> list[MetadataReport]:
119 return [
120 SevenZipMetadataReport(
121 version_major=int(header.version_maj),
122 version_minor=int(header.version_min),
123 )
124 ]
125
126 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
127 header = self.parse_header(file)
128
129 check_header_crc(header)
130
131 size = calculate_sevenzip_size(header)
132
133 metadata_reports = self.get_metadata_reports(header)
134
135 return ValidChunk(
136 start_offset=start_offset,
137 end_offset=start_offset + size,
138 metadata_reports=metadata_reports,
139 )
140
141
142class MultiVolumeSevenZipHandler(DirectoryHandler):
143 NAME = "multi-sevenzip"
144 EXTRACTOR = MultiFileCommand("7z", "x", "-p", "-y", "{inpath}", "-o{outdir}")
145
146 PATTERN = Glob("*.7z.001")
147
148 DOC = HandlerDoc(
149 name=NAME,
150 description="The 7-Zip file format is a compressed archive format with high compression ratios, supporting multiple algorithms, CRC checks, and multi-volume archives.",
151 handler_type=HandlerType.ARCHIVE,
152 vendor=None,
153 references=[
154 Reference(
155 title="7-Zip Technical Documentation",
156 url="https://fastapi.metacpan.org/source/BJOERN/Compress-Deflate7-1.0/7zip/DOC/7zFormat.txt",
157 ),
158 ],
159 limitations=[],
160 )
161
162 def calculate_multifile(self, file: Path) -> MultiFile | None:
163 paths = sorted(
164 [p for p in file.parent.glob(f"{file.stem}.*") if p.resolve().exists()]
165 )
166 if not paths:
167 return None
168
169 with file.open("rb") as f:
170 header_data = f.read(HEADER_SIZE)
171
172 header = HEADER_PARSER.parse(HEADER_STRUCT, header_data, Endian.LITTLE)
173 if header.magic != SEVENZIP_MAGIC:
174 return None
175
176 check_header_crc(header)
177 size = calculate_sevenzip_size(header)
178 logger.debug("Sevenzip header", header=header, size=size, _verbosity=3)
179
180 files_size = sum(path.stat().st_size for path in paths)
181 logger.debug(
182 "Multi-volume files", paths=paths, files_size=files_size, _verbosity=2
183 )
184 if files_size != size:
185 return None
186
187 return MultiFile(
188 name=file.stem,
189 paths=paths,
190 )