1import io
2from pathlib import Path
3
4from structlog import get_logger
5
6from unblob.file_utils import (
7 Endian,
8 File,
9 FileSystem,
10 InvalidInputFormat,
11 StructParser,
12 snull,
13)
14from unblob.models import (
15 Extractor,
16 ExtractResult,
17 HandlerDoc,
18 HandlerType,
19 HexString,
20 Reference,
21 StructHandler,
22 ValidChunk,
23)
24
25logger = get_logger()
26
27C_DEFINITIONS = r"""
28 typedef struct ipkg_file_entry {
29 char name[256];
30 uint64 offset;
31 uint64 size;
32 uint32 crc32;
33 } ipkg_toc_entry_t;
34
35 typedef struct ipkg_header {
36 char magic[4];
37 uint16 major;
38 uint16 minor;
39 uint32 toc_offset;
40 uint32 unknown_1;
41 uint32 toc_entries;
42 uint32 unknown_2[2];
43 uint32 always_null;
44 char file_version[256];
45 char product_name[256];
46 char ipkg_name[256];
47 char signature[256];
48 } ipkg_header_t;
49"""
50
51
52def is_valid_header(header) -> bool:
53 if header.toc_offset == 0 or header.toc_entries == 0:
54 return False
55 try:
56 snull(header.ipkg_name).decode("utf-8")
57 snull(header.file_version).decode("utf-8")
58 snull(header.product_name).decode("utf-8")
59 except UnicodeDecodeError:
60 return False
61 return True
62
63
64class HPIPKGExtractor(Extractor):
65 def __init__(self):
66 self._struct_parser = StructParser(C_DEFINITIONS)
67
68 def extract(self, inpath: Path, outdir: Path):
69 entries = []
70 fs = FileSystem(outdir)
71 with File.from_path(inpath) as file:
72 header = self._struct_parser.parse("ipkg_header_t", file, Endian.LITTLE)
73 file.seek(header.toc_offset, io.SEEK_SET)
74 for _ in range(header.toc_entries):
75 entry = self._struct_parser.parse(
76 "ipkg_toc_entry_t", file, Endian.LITTLE
77 )
78 entry_path = Path(snull(entry.name).decode("utf-8"))
79 if entry_path.parent.name:
80 raise InvalidInputFormat("Entry name contains directories.")
81 entries.append(
82 (
83 Path(entry_path.name),
84 entry.offset,
85 entry.size,
86 )
87 )
88
89 for carve_path, start_offset, size in entries:
90 fs.carve(carve_path, file, start_offset, size)
91
92 return ExtractResult(reports=fs.problems)
93
94
95class HPIPKGHandler(StructHandler):
96 NAME = "ipkg"
97
98 PATTERNS = [HexString("69 70 6B 67 01 00 03 00")]
99
100 C_DEFINITIONS = C_DEFINITIONS
101 HEADER_STRUCT = "ipkg_header_t"
102 EXTRACTOR = HPIPKGExtractor()
103
104 DOC = HandlerDoc(
105 name="HP IPKG",
106 description="HP IPKG firmware archives consist of a custom header, followed by a table of contents and file entries. Each entry specifies metadata such as file name, offset, size, and CRC32 checksum.",
107 handler_type=HandlerType.ARCHIVE,
108 vendor="HP",
109 references=[
110 Reference(
111 title="hpbdl",
112 url="https://github.com/tylerwhall/hpbdl",
113 )
114 ],
115 limitations=[],
116 )
117
118 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
119 header = self.parse_header(file, endian=Endian.LITTLE)
120
121 if not is_valid_header(header):
122 raise InvalidInputFormat("Invalid IPKG header.")
123
124 file.seek(start_offset + header.toc_offset, io.SEEK_SET)
125 end_offset = -1
126 for _ in range(header.toc_entries):
127 entry = self._struct_parser.parse("ipkg_toc_entry_t", file, Endian.LITTLE)
128 end_offset = max(end_offset, start_offset + entry.offset + entry.size)
129
130 return ValidChunk(start_offset=start_offset, end_offset=end_offset)