1import io
2from pathlib import Path
3
4from structlog import get_logger
5
6from unblob.extractor import carve_chunk_to_file
7from unblob.file_utils import Endian, File, InvalidInputFormat, StructParser, snull
8from unblob.models import (
9 Chunk,
10 Extractor,
11 HandlerDoc,
12 HandlerType,
13 HexString,
14 Reference,
15 StructHandler,
16 ValidChunk,
17)
18
19logger = get_logger()
20
21C_DEFINITIONS = r"""
22 typedef struct bdl_toc_entry {
23 uint64 offset;
24 uint64 size;
25 } bdl_toc_entry_t;
26
27 typedef struct bdl_header {
28 char magic[4];
29 uint16 major;
30 uint16 minor;
31 uint32 toc_offset;
32 char unknown[4];
33 uint32 toc_entries;
34 uint32 unknowns_2[3];
35 char release[256];
36 char brand[256];
37 char device_id[256];
38 char unknown_3[9];
39 char version[256];
40 char revision[256];
41 } bdl_header_t;
42"""
43
44
45def is_valid_header(header) -> bool:
46 if header.toc_offset == 0 or header.toc_entries == 0:
47 return False
48 try:
49 snull(header.release).decode("utf-8")
50 snull(header.brand).decode("utf-8")
51 snull(header.device_id).decode("utf-8")
52 snull(header.version).decode("utf-8")
53 snull(header.revision).decode("utf-8")
54 except UnicodeDecodeError:
55 return False
56 return True
57
58
59class HPBDLExtractor(Extractor):
60 def __init__(self):
61 self._struct_parser = StructParser(C_DEFINITIONS)
62
63 def extract(self, inpath: Path, outdir: Path):
64 entries = []
65 with File.from_path(inpath) as file:
66 header = self._struct_parser.parse("bdl_header_t", file, Endian.LITTLE)
67 file.seek(header.toc_offset, io.SEEK_SET)
68 for i in range(header.toc_entries):
69 entry = self._struct_parser.parse(
70 "bdl_toc_entry_t", file, Endian.LITTLE
71 )
72 entries.append(
73 (
74 outdir.joinpath(outdir.joinpath(Path(f"ipkg{i:03}"))),
75 Chunk(
76 start_offset=entry.offset,
77 end_offset=entry.offset + entry.size,
78 ),
79 )
80 )
81
82 for carve_path, chunk in entries:
83 carve_chunk_to_file(
84 file=file,
85 chunk=chunk,
86 carve_path=carve_path,
87 )
88
89
90class HPBDLHandler(StructHandler):
91 NAME = "bdl"
92
93 PATTERNS = [HexString("69 62 64 6C 01 00 01 00")]
94
95 C_DEFINITIONS = C_DEFINITIONS
96 HEADER_STRUCT = "bdl_header_t"
97 EXTRACTOR = HPBDLExtractor()
98
99 DOC = HandlerDoc(
100 name="HP BDL",
101 description="The HP BDL format is a firmware archive containing a custom header and a table of contents that specifies offsets and sizes of embedded firmware components. It includes metadata such as release, brand, device ID, version, and revision.",
102 handler_type=HandlerType.ARCHIVE,
103 vendor="HP",
104 references=[
105 Reference(
106 title="hpbdl",
107 url="https://github.com/tylerwhall/hpbdl",
108 )
109 ],
110 limitations=[],
111 )
112
113 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
114 header = self.parse_header(file, endian=Endian.LITTLE)
115
116 if not is_valid_header(header):
117 raise InvalidInputFormat("Invalid BDL header.")
118
119 file.seek(start_offset + header.toc_offset, io.SEEK_SET)
120 end_offset = -1
121 for _ in range(header.toc_entries):
122 entry = self._struct_parser.parse("bdl_toc_entry_t", file, Endian.LITTLE)
123 end_offset = max(end_offset, start_offset + entry.offset + entry.size)
124
125 return ValidChunk(start_offset=start_offset, end_offset=end_offset)