cross-compile/macosx: install IRMP dylib in library directory (DMG creation)
[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     def __init__(self, program):
43         self._elf = parseelf.elf(program)
44         self._elf_sections = {} # idx -> data
45         self._read_resources()
46
47     def _get_elf_section(self, idx):
48         s = self._elf_sections.get(idx)
49         if s is None:
50             shdr = self._elf.shdrs[idx]
51             s = self._elf.read_section(shdr), shdr
52             self._elf_sections[idx] = s
53         return s
54
55     def _get_elf_sym_value(self, sname):
56         sym = self._elf.symtab[sname]
57         section, shdr = self._get_elf_section(sym["st_shndx"])
58         addr = sym["st_value"] - shdr["sh_addr"]
59         value = section[addr:addr + sym["st_size"]]
60         if len(value) != sym["st_size"]:
61             print("warning: symbol %s should be %d bytes, but in section is only %d bytes" % (
62                 sname, sym["st_size"], len(value)))
63         return value
64
65     # Qt resource stuff.
66     def _get_resource_name(self, offset):
67         length, i = struct.unpack(">HI", self._res_names[offset:offset + 2 + 4])
68         offset += 2 + 4
69         name = self._res_names[offset:offset + 2 * length].decode("utf-16be")
70         return name
71
72     def _get_resource_data(self, offset):
73         length = struct.unpack(">I", self._res_datas[offset:offset + 4])[0]
74         offset += 4
75         return self._res_datas[offset:offset + length]
76
77     def _read_resources(self):
78         RCCFileInfo_Directory = 0x02
79         def read_table():
80             table = []
81             offset = 0
82             while offset < len(self._res_struct):
83                 name_offset, flags = struct.unpack(">IH", self._res_struct[offset:offset+4+2])
84                 offset += 6
85                 name = self._get_resource_name(name_offset)
86                 if flags & RCCFileInfo_Directory:
87                     child_count, first_child_offset = struct.unpack(">II", self._res_struct[offset:offset + 4 + 4])
88                     offset += 4 + 4
89                     table.append((name, flags, child_count, first_child_offset))
90                 else:
91                     country, language, data_offset = struct.unpack(">HHI", self._res_struct[offset:offset + 2 + 2 + 4])
92                     offset += 2 + 2 + 4
93                     table.append((name, flags, country, language, data_offset))
94             return table
95         def read_dir_entries(table, which, parents=[]):
96             name, flags = which[:2]
97             if not flags & RCCFileInfo_Directory:
98                 raise Exception("not a directory!")
99             child_count, first_child = which[2:]
100             for i in range(child_count):
101                 child = table[first_child + i]
102                 if child[1] & RCCFileInfo_Directory:
103                     read_dir_entries(table, child, parents + [child[0]])
104                 else:
105                     country, language, data_offset = child[2:]
106                     full_name = "/".join(parents + [child[0]])
107                     self._resources[full_name] = data_offset
108
109         self._res_datas = self._get_elf_sym_value("_ZL16qt_resource_data")
110         self._res_names = self._get_elf_sym_value("_ZL16qt_resource_name")
111         self._res_struct = self._get_elf_sym_value("_ZL18qt_resource_struct")
112
113         self._resources = {} # res_fn -> res_offset
114         table = read_table()
115         read_dir_entries(table, table[0])
116
117     def get_resource(self, res_fn):
118         offset = self._resources[res_fn]
119         data = self._get_resource_data(offset)
120         return data
121
122     def find_resource_names(self, res_fn_re):
123         for key in self._resources.keys():
124             m = re.match(res_fn_re, key)
125             if m is not None:
126                 yield key
127
128 class res_writer(object):
129     def __init__(self, res):
130         self.res = res
131
132     def _decode_crc(self, data, decoder=None):
133         if decoder is not None:
134             data = decoder(data)
135         data = bytearray(data)
136         crc = zlib.crc32(data) & 0xffffffff
137         return data, crc
138
139     def _write_file(self, fn, data):
140         with open(fn, "wb") as fp:
141             fp.write(data)
142
143     def extract_re(self, resource_pattern, fname_pattern, decoder=None):
144         resources = sorted(res.find_resource_names(resource_pattern))
145         for resource in resources:
146             fname = re.sub(resource_pattern, fname_pattern, resource)
147             fname = fname.lower()
148             data = self.res.get_resource(resource)
149             data, crc = self._decode_crc(data, decoder=decoder)
150             self._write_file(fname, data)
151             print("resource {rsc}, file {fname}, size {size}, checksum {crc:08x}".format(
152                 rsc = resource, fname = fname, size = len(data), crc = crc,
153             ))
154
155 def decode_intel_hex(hexdata):
156     """ return list of (address, data)
157     """
158     datas = []
159     # Assume LF-only or CR-LF style end-of-line.
160     for line in hexdata.split(b"\n"):
161         line = line.strip()
162         if chr(line[0]) != ":": raise Exception("invalid line: %r" % line)
163         offset = 1
164         record = codecs.decode(line[offset:], "hex")
165         byte_count, address, record_type = struct.unpack(">BHB", record[:1 + 2 + 1])
166         offset = 1 + 2 + 1
167         if byte_count > 0:
168             data = record[offset:offset + byte_count]
169             offset += byte_count
170         checksum = record[offset]
171         ex_checksum = (~sum(record[:offset]) + 1) & 0xff
172         if ex_checksum != checksum: raise Exception("invalid checksum %#x in %r" % (checksum, line))
173         if record_type == 0:
174             datas.append((address, data))
175         elif record_type == 1:
176             break
177     return datas
178
179 def intel_hex_as_blob(hexdata):
180     """ return continuous bytes sequence including all data
181     (loosing start address here)
182     """
183     data = decode_intel_hex(hexdata)
184     data.sort()
185     last = data[-1]
186     length = last[0] + len(last[1])
187     img = bytearray(length)
188     for off, part in data:
189         img[off:off + len(part)] = part
190     return img
191
192 def maybe_intel_hex_as_blob(data):
193     if data[0] == ord(":") and max(data) < 127:
194           return intel_hex_as_blob(data)
195     return data # Keep binary data.
196
197 if __name__ == "__main__":
198     parser = argparse.ArgumentParser(description = "KingstVIS firmware extraction")
199     parser.add_argument('executable', help = "KingstVIS executable file")
200     options = parser.parse_args()
201     exe_fn = options.executable
202
203     res = qt_resources(exe_fn)
204     writer = res_writer(res)
205
206     # Extract all MCU firmware and FPGA bitstream images. The sigrok
207     # project may not cover all KingstVIS supported devices. Users can
208     # either just copy those files which are strictly required for their
209     # specific device (diagnostics will identify those). Or just copy a
210     # few more files while some of them remain unused later (their size
211     # is small). Seeing which files would be contained is considered
212     # valuable, to identify device variants or candidate models.
213     writer.extract_re(r"fwusb/fw(.*)", r"kingst-la-\1.fw", decoder=maybe_intel_hex_as_blob)
214     writer.extract_re(r"fwfpga/(.*)", r"kingst-\1-fpga.bitstream")