1from pathlib import Path
2
3from structlog import get_logger
4
5from unblob.extractor import carve_chunk_to_file
6from unblob.file_utils import Endian, File, InvalidInputFormat, StructParser
7from unblob.models import (
8 Chunk,
9 Extractor,
10 HandlerDoc,
11 HandlerType,
12 HexString,
13 StructHandler,
14 ValidChunk,
15)
16
17logger = get_logger()
18
19C_DEFINITIONS = r"""
20 typedef struct bneg_header {
21 uint32 magic; /* BNEG */
22 uint32 major;
23 uint32 minor;
24 uint32 partition_1_size;
25 uint32 partition_2_size;
26 } bneg_header_t;
27"""
28
29
30class BNEGExtractor(Extractor):
31 def __init__(self):
32 self._struct_parser = StructParser(C_DEFINITIONS)
33
34 def extract(self, inpath: Path, outdir: Path):
35 with File.from_path(inpath) as file:
36 header = self._struct_parser.parse("bneg_header_t", file, Endian.LITTLE)
37 header_end_offset = len(header)
38
39 start_offset = header_end_offset
40 end_offset = header_end_offset + header.partition_1_size
41
42 logger.debug(
43 "extracting partition 1",
44 start_offset=start_offset,
45 end_offset=end_offset,
46 _verbosity=3,
47 )
48 carve_chunk_to_file(
49 file=file,
50 chunk=Chunk(start_offset=start_offset, end_offset=end_offset),
51 carve_path=outdir.joinpath("part1"),
52 )
53
54 start_offset = end_offset
55 end_offset = end_offset + header.partition_2_size
56
57 logger.debug(
58 "extracting partition 2",
59 start_offset=start_offset,
60 end_offset=end_offset,
61 _verbosity=3,
62 )
63 carve_chunk_to_file(
64 file=file,
65 chunk=Chunk(start_offset=start_offset, end_offset=end_offset),
66 carve_path=outdir.joinpath("part2"),
67 )
68
69
70class BNEGHandler(StructHandler):
71 NAME = "bneg"
72
73 PATTERNS = [HexString("42 4E 45 47")]
74
75 C_DEFINITIONS = C_DEFINITIONS
76 HEADER_STRUCT = "bneg_header_t"
77 EXTRACTOR = BNEGExtractor()
78
79 DOC = HandlerDoc(
80 name="Instar BNEG",
81 description="BNEG firmware files consist of a custom header followed by two partitions containing firmware components. The header specifies metadata such as magic value, version, and partition sizes.",
82 handler_type=HandlerType.ARCHIVE,
83 vendor="Instar",
84 references=[],
85 limitations=[],
86 )
87
88 def is_valid_header(self, header) -> bool:
89 if header.partition_1_size == 0:
90 return False
91 if header.partition_2_size == 0:
92 return False
93 if header.major != 0x1:
94 return False
95 if header.minor != 0x1: # noqa: SIM103
96 return False
97
98 return True
99
100 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
101 header = self.parse_header(file, endian=Endian.LITTLE)
102
103 if not self.is_valid_header(header):
104 raise InvalidInputFormat("Invalid bneg header.")
105
106 return ValidChunk(
107 start_offset=start_offset,
108 end_offset=start_offset
109 + len(header)
110 + header.partition_1_size
111 + header.partition_2_size,
112 )