]> sigrok.org Git - sigrok-util.git/blob - firmware/kingst-la/sigrok-fwextract-kingst-la2016
sigrok-fwextract-kingst-la2016: concentrate RCC flags in one spot
[sigrok-util.git] / firmware / kingst-la / sigrok-fwextract-kingst-la2016
1 #!/usr/bin/python3
2 ##
3 ## This file is part of the sigrok-util project.
4 ##
5 ## Copyright (C) 2020 Florian Schmidt <schmidt_florian@gmx.de>
6 ##
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 3 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ## GNU General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, see <http://www.gnu.org/licenses/>.
19 ##
20
21 # This utility extracts FX2 MCU firmware and FPGA bitstream images from
22 # the "KingstVIS" vendor software. The blobs are kept in Qt resources
23 # sections. The script was tested with several v3.5 software versions.
24
25 import argparse
26 import os
27 import sys
28 import re
29 import struct
30 import codecs
31 import importlib.util
32 import zlib
33
34 # Reuse the parseelf.py module from saleae-logic16.
35 fwdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
36 parseelf_py = os.path.join(fwdir, "saleae-logic16", "parseelf.py")
37 spec = importlib.util.spec_from_file_location("parseelf", parseelf_py)
38 parseelf = importlib.util.module_from_spec(spec)
39 spec.loader.exec_module(parseelf)
40
41 class qt_resources(object):
42     RCCFileInfo_Compressed = 0x01
43     RCCFileInfo_Directory = 0x02
44
45     def __init__(self, program):
46         self._elf = parseelf.elf(program)
47         self._elf_sections = {} # idx -> data
48         self._read_resources()
49
50     def _get_elf_section(self, idx):
51         s = self._elf_sections.get(idx)
52         if s is None:
53             shdr = self._elf.shdrs[idx]
54             s = self._elf.read_section(shdr), shdr
55             self._elf_sections[idx] = s
56         return s
57
58     def _get_elf_sym_value(self, sname):
59         sym = self._elf.symtab[sname]
60         section, shdr = self._get_elf_section(sym["st_shndx"])
61         addr = sym["st_value"] - shdr["sh_addr"]
62         value = section[addr:addr + sym["st_size"]]
63         if len(value) != sym["st_size"]:
64             print("warning: symbol %s should be %d bytes, but in section is only %d bytes" % (
65                 sname, sym["st_size"], len(value)))
66         return value
67
68     # Qt resource stuff.
69     def _get_resource_name(self, offset):
70         length, i = struct.unpack(">HI", self._res_names[offset:offset + 2 + 4])
71         offset += 2 + 4
72         name = self._res_names[offset:offset + 2 * length].decode("utf-16be")
73         return name
74
75     def _get_resource_data(self, offset):
76         length = struct.unpack(">I", self._res_datas[offset:offset + 4])[0]
77         offset += 4
78         return self._res_datas[offset:offset + length]
79
80     def _read_resources(self):
81         def read_table():
82             table = []
83             offset = 0
84             while offset < len(self._res_struct):
85                 name_offset, flags = struct.unpack(">IH", self._res_struct[offset:offset+4+2])
86                 offset += 6
87                 name = self._get_resource_name(name_offset)
88                 if flags & self.RCCFileInfo_Directory:
89                     child_count, first_child_offset = struct.unpack(">II", self._res_struct[offset:offset + 4 + 4])
90                     offset += 4 + 4
91                     table.append((name, flags, child_count, first_child_offset))
92                 else:
93                     country, language, data_offset = struct.unpack(">HHI", self._res_struct[offset:offset + 2 + 2 + 4])
94                     offset += 2 + 2 + 4
95                     table.append((name, flags, country, language, data_offset))
96             return table
97         def read_dir_entries(table, which, parents=[]):
98             name, flags = which[:2]
99             if not flags & self.RCCFileInfo_Directory:
100                 raise Exception("not a directory!")
101             child_count, first_child = which[2:]
102             for i in range(child_count):
103                 child = table[first_child + i]
104                 flags = child[1]
105                 if flags & self.RCCFileInfo_Directory:
106                     read_dir_entries(table, child, parents + [child[0]])
107                 else:
108                     country, language, data_offset = child[2:]
109                     full_name = "/".join(parents + [child[0]])
110                     self._resources[full_name] = data_offset
111                     self._resource_flags[full_name] = flags
112
113         self._res_datas = self._get_elf_sym_value("_ZL16qt_resource_data")
114         self._res_names = self._get_elf_sym_value("_ZL16qt_resource_name")
115         self._res_struct = self._get_elf_sym_value("_ZL18qt_resource_struct")
116
117         self._resources = {} # res_fn -> res_offset
118         self._resource_flags = {} # res_fn -> RCC_flags
119         table = read_table()
120         read_dir_entries(table, table[0])
121
122     def get_resource(self, res_fn):
123         offset = self._resources[res_fn]
124         flags = self._resource_flags[res_fn]
125         data = self._get_resource_data(offset)
126         if flags & self.RCCFileInfo_Compressed:
127             data = zlib.decompress(data[4:])
128         return data
129
130     def find_resource_names(self, res_fn_re):
131         for key in self._resources.keys():
132             m = re.match(res_fn_re, key)
133             if m is not None:
134                 yield key
135
136 class res_writer(object):
137     def __init__(self, res):
138         self.res = res
139
140     def _decode_crc(self, data, decoder=None):
141         if decoder is not None:
142             data = decoder(data)
143         data = bytearray(data)
144         crc = zlib.crc32(data) & 0xffffffff
145         return data, crc
146
147     def _write_file(self, fn, data):
148         with open(fn, "wb") as fp:
149             fp.write(data)
150
151     def extract_re(self, resource_pattern, fname_pattern, decoder=None):
152         resources = sorted(res.find_resource_names(resource_pattern))
153         for resource in resources:
154             fname = re.sub(resource_pattern, fname_pattern, resource)
155             fname = fname.lower()
156             data = self.res.get_resource(resource)
157             data, crc = self._decode_crc(data, decoder=decoder)
158             self._write_file(fname, data)
159             print("resource {rsc}, file {fname}, size {size}, checksum {crc:08x}".format(
160                 rsc = resource, fname = fname, size = len(data), crc = crc,
161             ))
162
163 def decode_intel_hex(hexdata):
164     """ return list of (address, data)
165     """
166     datas = []
167     # Assume LF-only or CR-LF style end-of-line.
168     for line in hexdata.split(b"\n"):
169         line = line.strip()
170         if chr(line[0]) != ":": raise Exception("invalid line: %r" % line)
171         offset = 1
172         record = codecs.decode(line[offset:], "hex")
173         byte_count, address, record_type = struct.unpack(">BHB", record[:1 + 2 + 1])
174         offset = 1 + 2 + 1
175         if byte_count > 0:
176             data = record[offset:offset + byte_count]
177             offset += byte_count
178         checksum = record[offset]
179         ex_checksum = (~sum(record[:offset]) + 1) & 0xff
180         if ex_checksum != checksum: raise Exception("invalid checksum %#x in %r" % (checksum, line))
181         if record_type == 0:
182             datas.append((address, data))
183         elif record_type == 1:
184             break
185     return datas
186
187 def intel_hex_as_blob(hexdata):
188     """ return continuous bytes sequence including all data
189     (loosing start address here)
190     """
191     data = decode_intel_hex(hexdata)
192     data.sort()
193     last = data[-1]
194     length = last[0] + len(last[1])
195     img = bytearray(length)
196     for off, part in data:
197         img[off:off + len(part)] = part
198     return img
199
200 def maybe_intel_hex_as_blob(data):
201     if data[0] == ord(":") and max(data) < 127:
202           return intel_hex_as_blob(data)
203     return data # Keep binary data.
204
205 if __name__ == "__main__":
206     parser = argparse.ArgumentParser(description = "KingstVIS firmware extraction")
207     parser.add_argument('executable', help = "KingstVIS executable file")
208     options = parser.parse_args()
209     exe_fn = options.executable
210
211     res = qt_resources(exe_fn)
212     writer = res_writer(res)
213
214     # Extract all MCU firmware and FPGA bitstream images. The sigrok
215     # project may not cover all KingstVIS supported devices. Users can
216     # either just copy those files which are strictly required for their
217     # specific device (diagnostics will identify those). Or just copy a
218     # few more files while some of them remain unused later (their size
219     # is small). Seeing which files would be contained is considered
220     # valuable, to identify device variants or candidate models.
221     writer.extract_re(r"fwusb/fw(.*)", r"kingst-la-\1.fw", decoder=maybe_intel_hex_as_blob)
222     writer.extract_re(r"fwfpga/(.*)", r"kingst-\1-fpga.bitstream")