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