gen_esp32part.py 22 KB

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