]> granicus.if.org Git - esp-idf/blob - components/partition_table/gen_esp32part.py
gen_esp32part: Fix input/output handling, regression when Python 3 was supported
[esp-idf] / components / partition_table / gen_esp32part.py
1 #!/usr/bin/env python
2 #
3 # ESP32 partition table generation tool
4 #
5 # Converts partition tables to/from CSV and binary formats.
6 #
7 # See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html
8 # for explanation of partition table structure and uses.
9 #
10 # Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
11 #
12 # Licensed under the Apache License, Version 2.0 (the "License");
13 # you may not use this file except in compliance with the License.
14 # You may obtain a copy of the License at
15 #
16 #     http:#www.apache.org/licenses/LICENSE-2.0
17 #
18 # Unless required by applicable law or agreed to in writing, software
19 # distributed under the License is distributed on an "AS IS" BASIS,
20 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 # See the License for the specific language governing permissions and
22 # limitations under the License.
23 from __future__ import print_function, division
24 import argparse
25 import os
26 import re
27 import struct
28 import sys
29 import hashlib
30 import binascii
31
32 MAX_PARTITION_LENGTH = 0xC00   # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature
33 MD5_PARTITION_BEGIN = b"\xEB\xEB" + b"\xFF" * 14 # The first 2 bytes are like magic numbers for MD5 sum
34 PARTITION_TABLE_SIZE  = 0x1000  # Size of partition table
35
36 __version__ = '1.1'
37
38 quiet = False
39 md5sum = True
40 offset_part_table = 0
41
42 def status(msg):
43     """ Print status message to stderr """
44     if not quiet:
45         critical(msg)
46
47 def critical(msg):
48     """ Print critical message to stderr """
49     if not quiet:
50         sys.stderr.write(msg)
51         sys.stderr.write('\n')
52
53 class PartitionTable(list):
54     def __init__(self):
55         super(PartitionTable, self).__init__(self)
56
57     @classmethod
58     def from_csv(cls, csv_contents):
59         res = PartitionTable()
60         lines = csv_contents.splitlines()
61
62         def expand_vars(f):
63             f = os.path.expandvars(f)
64             m = re.match(r'(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)', f)
65             if m:
66                 raise InputError("unknown variable '%s'" % m.group(1))
67             return f
68
69         for line_no in range(len(lines)):
70             line = expand_vars(lines[line_no]).strip()
71             if line.startswith("#") or len(line) == 0:
72                 continue
73             try:
74                 res.append(PartitionDefinition.from_csv(line))
75             except InputError as e:
76                 raise InputError("Error at line %d: %s" % (line_no+1, e))
77             except Exception:
78                 critical("Unexpected error parsing line %d: %s" % (line_no+1, line))
79                 raise
80
81         # fix up missing offsets & negative sizes
82         last_end = offset_part_table + PARTITION_TABLE_SIZE # first offset after partition table
83         for e in res:
84             if offset_part_table != 0 and e.offset is not None and e.offset < last_end:
85                 critical("WARNING: 0x%x address in the partition table is below 0x%x" % (e.offset, last_end))
86                 e.offset = None
87             if e.offset is None:
88                 pad_to = 0x10000 if e.type == PartitionDefinition.APP_TYPE else 4
89                 if last_end % pad_to != 0:
90                     last_end += pad_to - (last_end % pad_to)
91                 e.offset = last_end
92             if e.size < 0:
93                 e.size = -e.size - e.offset
94             last_end = e.offset + e.size
95
96         return res
97
98     def __getitem__(self, item):
99         """ Allow partition table access via name as well as by
100         numeric index. """
101         if isinstance(item, str):
102             for x in self:
103                 if x.name == item:
104                     return x
105             raise ValueError("No partition entry named '%s'" % item)
106         else:
107             return super(PartitionTable, self).__getitem__(item)
108
109     def find_by_type(self, ptype, subtype):
110         """ Return a partition by type & subtype, returns
111         None if not found """
112         TYPES = PartitionDefinition.TYPES
113         SUBTYPES = PartitionDefinition.SUBTYPES
114         # convert ptype & subtypes names (if supplied this way) to integer values
115         try:
116             ptype = TYPES[ptype]
117         except KeyError:
118             try:
119                 ptypes = int(ptype, 0)
120             except TypeError:
121                 pass
122         try:
123             subtype = SUBTYPES[int(ptype)][subtype]
124         except KeyError:
125             try:
126                 ptypes = int(ptype, 0)
127             except TypeError:
128                 pass
129
130         for p in self:
131             if p.type == ptype and p.subtype == subtype:
132                 return p
133         return None
134
135     def find_by_name(self, name):
136         for p in self:
137             if p.name == name:
138                 return p
139         return None
140
141     def verify(self):
142         # verify each partition individually
143         for p in self:
144             p.verify()
145         # check for overlaps
146         last = None
147         for p in sorted(self, key=lambda x:x.offset):
148             if p.offset < offset_part_table + PARTITION_TABLE_SIZE:
149                 raise InputError("Partition offset 0x%x is below 0x%x" % (p.offset, offset_part_table + PARTITION_TABLE_SIZE))
150             if last is not None and p.offset < last.offset + last.size:
151                 raise InputError("Partition at 0x%x overlaps 0x%x-0x%x" % (p.offset, last.offset, last.offset+last.size-1))
152             last = p
153
154     def flash_size(self):
155         """ Return the size that partitions will occupy in flash
156             (ie the offset the last partition ends at)
157         """
158         try:
159             last = sorted(self, reverse=True)[0]
160         except IndexError:
161             return 0  # empty table!
162         return last.offset + last.size
163
164     @classmethod
165     def from_binary(cls, b):
166         md5 = hashlib.md5();
167         result = cls()
168         for o in range(0,len(b),32):
169             data = b[o:o+32]
170             if len(data) != 32:
171                 raise InputError("Partition table length must be a multiple of 32 bytes")
172             if data == b'\xFF'*32:
173                 return result  # got end marker
174             if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]: #check only the magic number part
175                 if data[16:] == md5.digest():
176                     continue # the next iteration will check for the end marker
177                 else:
178                     raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:])))
179             else:
180                 md5.update(data)
181             result.append(PartitionDefinition.from_binary(data))
182         raise InputError("Partition table is missing an end-of-table marker")
183
184     def to_binary(self):
185         result = b"".join(e.to_binary() for e in self)
186         if md5sum:
187             result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest()
188         if len(result )>= MAX_PARTITION_LENGTH:
189             raise InputError("Binary partition table length (%d) longer than max" % len(result))
190         result += b"\xFF" * (MAX_PARTITION_LENGTH - len(result))  # pad the sector, for signing
191         return result
192
193     def to_csv(self, simple_formatting=False):
194         rows = [ "# Espressif ESP32 Partition Table",
195                  "# Name, Type, SubType, Offset, Size, Flags" ]
196         rows += [ x.to_csv(simple_formatting) for x in self ]
197         return "\n".join(rows) + "\n"
198
199 class PartitionDefinition(object):
200     APP_TYPE = 0x00
201     DATA_TYPE = 0x01
202     TYPES = {
203         "app" : APP_TYPE,
204         "data" : DATA_TYPE,
205     }
206
207     # Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h 
208     SUBTYPES = {
209         APP_TYPE : {
210             "factory" : 0x00,
211             "test" : 0x20,
212             },
213         DATA_TYPE : {
214             "ota" : 0x00,
215             "phy" : 0x01,
216             "nvs" : 0x02,
217             "coredump" : 0x03,
218             "esphttpd" : 0x80,
219             "fat" : 0x81,
220             "spiffs" : 0x82,
221             },
222     }
223
224     MAGIC_BYTES = b"\xAA\x50"
225
226     ALIGNMENT = {
227         APP_TYPE : 0x10000,
228         DATA_TYPE : 0x04,
229     }
230
231     # dictionary maps flag name (as used in CSV flags list, property name)
232     # to bit set in flags words in binary format
233     FLAGS = {
234         "encrypted" : 0
235     }
236
237     # add subtypes for the 16 OTA slot values ("ota_XX, etc.")
238     for ota_slot in range(16):
239         SUBTYPES[TYPES["app"]]["ota_%d" % ota_slot] = 0x10 + ota_slot
240
241     def __init__(self):
242         self.name = ""
243         self.type = None
244         self.subtype = None
245         self.offset = None
246         self.size = None
247         self.encrypted = False
248
249     @classmethod
250     def from_csv(cls, line):
251         """ Parse a line from the CSV """
252         line_w_defaults = line + ",,,,"  # lazy way to support default fields
253         fields = [ f.strip() for f in line_w_defaults.split(",") ]
254
255         res = PartitionDefinition()
256         res.name = fields[0]
257         res.type = res.parse_type(fields[1])
258         res.subtype = res.parse_subtype(fields[2])
259         res.offset = res.parse_address(fields[3])
260         res.size = res.parse_address(fields[4])
261         if res.size is None:
262             raise InputError("Size field can't be empty")
263
264         flags = fields[5].split(":")
265         for flag in flags:
266             if flag in cls.FLAGS:
267                 setattr(res, flag, True)
268             elif len(flag) > 0:
269                 raise InputError("CSV flag column contains unknown flag '%s'" % (flag))
270
271         return res
272
273     def __eq__(self, other):
274         return self.name == other.name and self.type == other.type \
275             and self.subtype == other.subtype and self.offset == other.offset \
276             and self.size == other.size
277
278     def __repr__(self):
279         def maybe_hex(x):
280             return "0x%x" % x if x is not None else "None"
281         return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0,
282                                                               maybe_hex(self.offset), maybe_hex(self.size))
283
284     def __str__(self):
285         return "Part '%s' %d/%d @ 0x%x size 0x%x" % (self.name, self.type, self.subtype, self.offset or -1, self.size or -1)
286
287     def __cmp__(self, other):
288         return self.offset - other.offset
289
290     def __lt__(self, other):
291         return self.offset < other.offset
292
293     def __gt__(self, other):
294         return self.offset > other.offset
295
296     def __le__(self, other):
297         return self.offset <= other.offset
298
299     def __ge__(self, other):
300         return self.offset >= other.offset
301
302     def parse_type(self, strval):
303         if strval == "":
304             raise InputError("Field 'type' can't be left empty.")
305         return parse_int(strval, self.TYPES)
306
307     def parse_subtype(self, strval):
308         if strval == "":
309             return 0 # default
310         return parse_int(strval, self.SUBTYPES.get(self.type, {}))
311
312     def parse_address(self, strval):
313         if strval == "":
314             return None  # PartitionTable will fill in default
315         return parse_int(strval)
316
317     def verify(self):
318         if self.type is None:
319             raise ValidationError(self, "Type field is not set")
320         if self.subtype is None:
321             raise ValidationError(self, "Subtype field is not set")
322         if self.offset is None:
323             raise ValidationError(self, "Offset field is not set")
324         align = self.ALIGNMENT.get(self.type, 4)
325         if self.offset % align:
326             raise ValidationError(self, "Offset 0x%x is not aligned to 0x%x" % (self.offset, align))
327         if self.size is None:
328             raise ValidationError(self, "Size field is not set")
329
330     STRUCT_FORMAT = "<2sBBLL16sL"
331
332     @classmethod
333     def from_binary(cls, b):
334         if len(b) != 32:
335             raise InputError("Partition definition length must be exactly 32 bytes. Got %d bytes." % len(b))
336         res = cls()
337         (magic, res.type, res.subtype, res.offset,
338          res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b)
339         if b"\x00" in res.name: # strip null byte padding from name string
340             res.name = res.name[:res.name.index(b"\x00")]
341         res.name = res.name.decode()
342         if magic != cls.MAGIC_BYTES:
343             raise InputError("Invalid magic bytes (%r) for partition definition" % magic)
344         for flag,bit in cls.FLAGS.items():
345             if flags & (1<<bit):
346                 setattr(res, flag, True)
347                 flags &= ~(1<<bit)
348         if flags != 0:
349             critical("WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?" % flags)
350         return res
351
352     def get_flags_list(self):
353         return [ flag for flag in self.FLAGS.keys() if getattr(self, flag) ]
354
355     def to_binary(self):
356         flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list())
357         return struct.pack(self.STRUCT_FORMAT,
358                            self.MAGIC_BYTES,
359                            self.type, self.subtype,
360                            self.offset, self.size,
361                            self.name.encode(),
362                            flags)
363
364     def to_csv(self, simple_formatting=False):
365         def addr_format(a, include_sizes):
366             if not simple_formatting and include_sizes:
367                 for (val, suffix) in [ (0x100000, "M"), (0x400, "K") ]:
368                     if a % val == 0:
369                         return "%d%s" % (a // val, suffix)
370             return "0x%x" % a
371
372         def lookup_keyword(t, keywords):
373             for k,v in keywords.items():
374                 if simple_formatting == False and t == v:
375                     return k
376             return "%d" % t
377
378         def generate_text_flags():
379             """ colon-delimited list of flags """
380             return ":".join(self.get_flags_list())
381
382         return ",".join([ self.name,
383                           lookup_keyword(self.type, self.TYPES),
384                           lookup_keyword(self.subtype, self.SUBTYPES.get(self.type, {})),
385                           addr_format(self.offset, False),
386                           addr_format(self.size, True),
387                           generate_text_flags()])
388
389
390 def parse_int(v, keywords={}):
391     """Generic parser for integer fields - int(x,0) with provision for
392     k/m/K/M suffixes and 'keyword' value lookup.
393     """
394     try:
395         for letter, multiplier in [ ("k",1024), ("m",1024*1024) ]:
396             if v.lower().endswith(letter):
397                 return parse_int(v[:-1], keywords) * multiplier
398         return int(v, 0)
399     except ValueError:
400         if len(keywords) == 0:
401             raise InputError("Invalid field value %s" % v)
402         try:
403             return keywords[v.lower()]
404         except KeyError:
405             raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ", ".join(keywords)))
406
407 def main():
408     global quiet
409     global md5sum
410     global offset_part_table
411     parser = argparse.ArgumentParser(description='ESP32 partition table utility')
412
413     parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash',
414                         nargs='?', choices=[ '1MB', '2MB', '4MB', '8MB', '16MB' ])
415     parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true')
416     parser.add_argument('--verify', '-v', help='Verify partition table fields', default=True, action='store_false')
417     parser.add_argument('--quiet', '-q', help="Don't print status messages to stderr", action='store_true')
418     parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000')
419     
420     parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb'))
421     parser.add_argument('output', help='Path to output converted binary or CSV file. Will use stdout if omitted.',
422                         nargs='?', default='-')
423
424     args = parser.parse_args()
425
426     quiet = args.quiet
427     md5sum = not args.disable_md5sum
428     offset_part_table = int(args.offset, 0)
429     input = args.input.read()
430     input_is_binary = input[0:2] == PartitionDefinition.MAGIC_BYTES
431     if input_is_binary:
432         status("Parsing binary partition input...")
433         table = PartitionTable.from_binary(input)
434     else:
435         input = input.decode()
436         status("Parsing CSV input...")
437         table = PartitionTable.from_csv(input)
438
439     if args.verify:
440         status("Verifying table...")
441         table.verify()
442
443     if args.flash_size:
444         size_mb = int(args.flash_size.replace("MB", ""))
445         size = size_mb * 1024 * 1024  # flash memory uses honest megabytes!
446         table_size = table.flash_size()
447         if size < table_size:
448             raise InputError("Partitions defined in '%s' occupy %.1fMB of flash (%d bytes) which does not fit in configured flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu." %
449                              (args.input.name, table_size / 1024.0 / 1024.0, table_size, size_mb))
450
451     if input_is_binary:
452         output = table.to_csv()
453         with sys.stdout if args.output == '-' else open(args.output, 'w') as f:
454             f.write(output)
455     else:
456         output = table.to_binary()
457         try:
458             stdout_binary = sys.stdout.buffer  # Python 3
459         except AttributeError:
460             stdout_binary = sys.stdout
461         with stdout_binary if args.output == '-' else open(args.output, 'wb') as f:
462             f.write(output)
463
464
465 class InputError(RuntimeError):
466     def __init__(self, e):
467         super(InputError, self).__init__(e)
468
469
470 class ValidationError(InputError):
471     def __init__(self, partition, message):
472         super(ValidationError, self).__init__(
473             "Partition %s invalid: %s" % (partition.name, message))
474
475
476 if __name__ == '__main__':
477     try:
478         main()
479     except InputError as e:
480         print(e, file=sys.stderr)
481         sys.exit(2)