Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/archive/sevenzip.py: 75%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

63 statements  

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 )