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