1#!/usr/bin/python
2
3# Written by Antonio Galea - 2010/11/18
4# Distributed under Gnu LGPL 3.0
5# see http://www.gnu.org/licenses/lgpl-3.0.txt
6
7import sys, struct, zlib, os
8from optparse import OptionParser
9
10DEFAULT_DEVICE = "0x0483:0xdf11"
11
12
13def named(tuple, names):
14    return dict(zip(names.split(), tuple))
15
16
17def consume(fmt, data, names):
18    n = struct.calcsize(fmt)
19    return named(struct.unpack(fmt, data[:n]), names), data[n:]
20
21
22def cstring(string):
23    return string.split("\0", 1)[0]
24
25
26def compute_crc(data):
27    return 0xFFFFFFFF & -zlib.crc32(data) - 1
28
29
30def parse(file, dump_images=False):
31    print('File: "%s"' % file)
32    data = open(file, "rb").read()
33    crc = compute_crc(data[:-4])
34    prefix, data = consume("<5sBIB", data, "signature version size targets")
35    print("%(signature)s v%(version)d, image size: %(size)d, targets: %(targets)d" % prefix)
36    for t in range(prefix["targets"]):
37        tprefix, data = consume(
38            "<6sBI255s2I", data, "signature altsetting named name size elements"
39        )
40        tprefix["num"] = t
41        if tprefix["named"]:
42            tprefix["name"] = cstring(tprefix["name"])
43        else:
44            tprefix["name"] = ""
45        print(
46            '%(signature)s %(num)d, alt setting: %(altsetting)s, name: "%(name)s", size: %(size)d, elements: %(elements)d'
47            % tprefix
48        )
49        tsize = tprefix["size"]
50        target, data = data[:tsize], data[tsize:]
51        for e in range(tprefix["elements"]):
52            eprefix, target = consume("<2I", target, "address size")
53            eprefix["num"] = e
54            print("  %(num)d, address: 0x%(address)08x, size: %(size)d" % eprefix)
55            esize = eprefix["size"]
56            image, target = target[:esize], target[esize:]
57            if dump_images:
58                out = "%s.target%d.image%d.bin" % (file, t, e)
59                open(out, "wb").write(image)
60                print('    DUMPED IMAGE TO "%s"' % out)
61        if len(target):
62            print("target %d: PARSE ERROR" % t)
63    suffix = named(struct.unpack("<4H3sBI", data[:16]), "device product vendor dfu ufd len crc")
64    print(
65        "usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x"
66        % suffix
67    )
68    if crc != suffix["crc"]:
69        print("CRC ERROR: computed crc32 is 0x%08x" % crc)
70    data = data[16:]
71    if data:
72        print("PARSE ERROR")
73
74
75def build(file, targets, device=DEFAULT_DEVICE):
76    data = b""
77    for t, target in enumerate(targets):
78        tdata = b""
79        for image in target:
80            # pad image to 8 bytes (needed at least for L476)
81            pad = (8 - len(image["data"]) % 8) % 8
82            image["data"] = image["data"] + bytes(bytearray(8)[0:pad])
83            #
84            tdata += struct.pack("<2I", image["address"], len(image["data"])) + image["data"]
85        tdata = (
86            struct.pack("<6sBI255s2I", b"Target", 0, 1, b"ST...", len(tdata), len(target)) + tdata
87        )
88        data += tdata
89    data = struct.pack("<5sBIB", b"DfuSe", 1, len(data) + 11, len(targets)) + data
90    v, d = map(lambda x: int(x, 0) & 0xFFFF, device.split(":", 1))
91    data += struct.pack("<4H3sB", 0, d, v, 0x011A, b"UFD", 16)
92    crc = compute_crc(data)
93    data += struct.pack("<I", crc)
94    open(file, "wb").write(data)
95
96
97if __name__ == "__main__":
98    usage = """
99%prog [-d|--dump] infile.dfu
100%prog {-b|--build} address:file.bin [-b address:file.bin ...] [{-D|--device}=vendor:device] outfile.dfu"""
101    parser = OptionParser(usage=usage)
102    parser.add_option(
103        "-b",
104        "--build",
105        action="append",
106        dest="binfiles",
107        help="build a DFU file from given BINFILES",
108        metavar="BINFILES",
109    )
110    parser.add_option(
111        "-D",
112        "--device",
113        action="store",
114        dest="device",
115        help="build for DEVICE, defaults to %s" % DEFAULT_DEVICE,
116        metavar="DEVICE",
117    )
118    parser.add_option(
119        "-d",
120        "--dump",
121        action="store_true",
122        dest="dump_images",
123        default=False,
124        help="dump contained images to current directory",
125    )
126    (options, args) = parser.parse_args()
127
128    if options.binfiles and len(args) == 1:
129        target = []
130        for arg in options.binfiles:
131            try:
132                address, binfile = arg.split(":", 1)
133            except ValueError:
134                print("Address:file couple '%s' invalid." % arg)
135                sys.exit(1)
136            try:
137                address = int(address, 0) & 0xFFFFFFFF
138            except ValueError:
139                print("Address %s invalid." % address)
140                sys.exit(1)
141            if not os.path.isfile(binfile):
142                print("Unreadable file '%s'." % binfile)
143                sys.exit(1)
144            target.append({"address": address, "data": open(binfile, "rb").read()})
145        outfile = args[0]
146        device = DEFAULT_DEVICE
147        if options.device:
148            device = options.device
149        try:
150            v, d = map(lambda x: int(x, 0) & 0xFFFF, device.split(":", 1))
151        except:
152            print("Invalid device '%s'." % device)
153            sys.exit(1)
154        build(outfile, [target], device)
155    elif len(args) == 1:
156        infile = args[0]
157        if not os.path.isfile(infile):
158            print("Unreadable file '%s'." % infile)
159            sys.exit(1)
160        parse(infile, dump_images=options.dump_images)
161    else:
162        parser.print_help()
163        sys.exit(1)
164