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