| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | #!/usr/bin/env python | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # ESP32 partition table generation tool | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Converts partition tables to/from CSV and binary formats. | 
					
						
							|  |  |  | # | 
					
						
							| 
									
										
										
										
											2018-06-18 17:07:16 +10:00
										 |  |  | # See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html | 
					
						
							| 
									
										
										
										
											2017-05-12 11:58:28 +10:00
										 |  |  | # for explanation of partition table structure and uses. | 
					
						
							|  |  |  | # | 
					
						
							| 
									
										
										
										
											2023-07-17 11:59:28 +02:00
										 |  |  | # SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD | 
					
						
							| 
									
										
										
										
											2022-02-20 22:29:32 +03:00
										 |  |  | # SPDX-License-Identifier: Apache-2.0 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  | from __future__ import division, print_function, unicode_literals | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | import argparse | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  | import binascii | 
					
						
							|  |  |  | import errno | 
					
						
							|  |  |  | import hashlib | 
					
						
							| 
									
										
										
										
											2017-01-26 01:47:53 +00:00
										 |  |  | import os | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | import struct | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | import sys | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-07 15:32:21 +11:00
										 |  |  | MAX_PARTITION_LENGTH = 0xC00   # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  | MD5_PARTITION_BEGIN = b'\xEB\xEB' + b'\xFF' * 14  # The first 2 bytes are like magic numbers for MD5 sum | 
					
						
							| 
									
										
										
										
											2018-04-19 09:42:26 +05:00
										 |  |  | PARTITION_TABLE_SIZE  = 0x1000  # Size of partition table | 
					
						
							| 
									
										
										
										
											2016-11-07 15:32:21 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-16 04:59:37 +08:00
										 |  |  | MIN_PARTITION_SUBTYPE_APP_OTA = 0x10 | 
					
						
							|  |  |  | NUM_PARTITION_SUBTYPE_APP_OTA = 16 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  | SECURE_NONE = None | 
					
						
							|  |  |  | SECURE_V1 = 'v1' | 
					
						
							|  |  |  | SECURE_V2 = 'v2' | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-17 11:59:28 +02:00
										 |  |  | __version__ = '1.3' | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  | APP_TYPE = 0x00 | 
					
						
							|  |  |  | DATA_TYPE = 0x01 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | TYPES = { | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |     'app': APP_TYPE, | 
					
						
							|  |  |  |     'data': DATA_TYPE, | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  | def get_ptype_as_int(ptype): | 
					
						
							|  |  |  |     """ Convert a string which might be numeric or the name of a partition type to an integer """ | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         return TYPES[ptype] | 
					
						
							|  |  |  |     except KeyError: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             return int(ptype, 0) | 
					
						
							|  |  |  |         except TypeError: | 
					
						
							|  |  |  |             return ptype | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  | # Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h | 
					
						
							|  |  |  | SUBTYPES = { | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |     APP_TYPE: { | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         'factory': 0x00, | 
					
						
							|  |  |  |         'test': 0x20, | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |     DATA_TYPE: { | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         'ota': 0x00, | 
					
						
							|  |  |  |         'phy': 0x01, | 
					
						
							|  |  |  |         'nvs': 0x02, | 
					
						
							|  |  |  |         'coredump': 0x03, | 
					
						
							|  |  |  |         'nvs_keys': 0x04, | 
					
						
							|  |  |  |         'efuse': 0x05, | 
					
						
							| 
									
										
										
										
											2021-06-11 17:53:45 +05:00
										 |  |  |         'undefined': 0x06, | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         'esphttpd': 0x80, | 
					
						
							|  |  |  |         'fat': 0x81, | 
					
						
							|  |  |  |         'spiffs': 0x82, | 
					
						
							| 
									
										
										
										
											2023-06-28 15:21:26 +02:00
										 |  |  |         'littlefs': 0x83, | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |     }, | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  | def get_subtype_as_int(ptype, subtype): | 
					
						
							|  |  |  |     """ Convert a string which might be numeric or the name of a partition subtype to an integer """ | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         return SUBTYPES[get_ptype_as_int(ptype)][subtype] | 
					
						
							|  |  |  |     except KeyError: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             return int(subtype, 0) | 
					
						
							|  |  |  |         except TypeError: | 
					
						
							|  |  |  |             return subtype | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-23 08:09:28 +02:00
										 |  |  | ALIGNMENT = { | 
					
						
							|  |  |  |     APP_TYPE: 0x10000, | 
					
						
							| 
									
										
										
										
											2022-03-03 00:16:11 +01:00
										 |  |  |     DATA_TYPE: 0x1000, | 
					
						
							| 
									
										
										
										
											2021-08-23 08:09:28 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  | def get_alignment_offset_for_type(ptype): | 
					
						
							| 
									
										
										
										
											2021-08-23 08:09:28 +02:00
										 |  |  |     return ALIGNMENT.get(ptype, ALIGNMENT[DATA_TYPE]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  | def get_alignment_size_for_type(ptype): | 
					
						
							| 
									
										
										
										
											2023-10-30 18:04:25 +05:30
										 |  |  |     if ptype == APP_TYPE: | 
					
						
							|  |  |  |         if secure == SECURE_V1: | 
					
						
							|  |  |  |             # For secure boot v1 case, app partition must be 64K aligned | 
					
						
							|  |  |  |             # signature block (68 bytes) lies at the very end of 64K block | 
					
						
							|  |  |  |             return 0x10000 | 
					
						
							|  |  |  |         elif secure == SECURE_V2: | 
					
						
							|  |  |  |             # For secure boot v2 case, app partition must be 4K aligned | 
					
						
							|  |  |  |             # signature block (4K) is kept after padding the unsigned image to 64K boundary | 
					
						
							|  |  |  |             return 0x1000 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             # For no secure boot enabled case, app partition must be 4K aligned (min. flash erase size) | 
					
						
							|  |  |  |             return 0x1000 | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  |     # No specific size alignement requirement as such | 
					
						
							|  |  |  |     return 0x1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-21 16:19:26 +05:30
										 |  |  | def get_partition_type(ptype): | 
					
						
							|  |  |  |     if ptype == 'app': | 
					
						
							|  |  |  |         return APP_TYPE | 
					
						
							|  |  |  |     if ptype == 'data': | 
					
						
							|  |  |  |         return DATA_TYPE | 
					
						
							|  |  |  |     raise InputError('Invalid partition type') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def add_extra_subtypes(csv): | 
					
						
							|  |  |  |     for line_no in csv: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             fields = [line.strip() for line in line_no.split(',')] | 
					
						
							|  |  |  |             for subtype, subtype_values in SUBTYPES.items(): | 
					
						
							|  |  |  |                 if (int(fields[2], 16) in subtype_values.values() and subtype == get_partition_type(fields[0])): | 
					
						
							|  |  |  |                     raise ValueError('Found duplicate value in partition subtype') | 
					
						
							|  |  |  |             SUBTYPES[TYPES[fields[0]]][fields[1]] = int(fields[2], 16) | 
					
						
							|  |  |  |         except InputError as err: | 
					
						
							|  |  |  |             raise InputError('Error parsing custom subtypes: %s' % err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | quiet = False | 
					
						
							| 
									
										
										
										
											2018-02-16 11:12:16 +01:00
										 |  |  | md5sum = True | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  | secure = SECURE_NONE | 
					
						
							| 
									
										
										
										
											2018-04-19 09:42:26 +05:00
										 |  |  | offset_part_table = 0 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | def status(msg): | 
					
						
							|  |  |  |     """ Print status message to stderr """ | 
					
						
							|  |  |  |     if not quiet: | 
					
						
							|  |  |  |         critical(msg) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | def critical(msg): | 
					
						
							|  |  |  |     """ Print critical message to stderr """ | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |     sys.stderr.write(msg) | 
					
						
							|  |  |  |     sys.stderr.write('\n') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | class PartitionTable(list): | 
					
						
							|  |  |  |     def __init__(self): | 
					
						
							|  |  |  |         super(PartitionTable, self).__init__(self) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_file(cls, f): | 
					
						
							| 
									
										
										
										
											2021-04-12 17:16:49 +10:00
										 |  |  |         data = f.read() | 
					
						
							|  |  |  |         data_is_binary = data[0:2] == PartitionDefinition.MAGIC_BYTES | 
					
						
							|  |  |  |         if data_is_binary: | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  |             status('Parsing binary partition input...') | 
					
						
							| 
									
										
										
										
											2021-04-12 17:16:49 +10:00
										 |  |  |             return cls.from_binary(data), True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         data = data.decode() | 
					
						
							|  |  |  |         status('Parsing CSV input...') | 
					
						
							|  |  |  |         return cls.from_csv(data), False | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_csv(cls, csv_contents): | 
					
						
							|  |  |  |         res = PartitionTable() | 
					
						
							| 
									
										
										
										
											2017-07-25 18:12:31 +02:00
										 |  |  |         lines = csv_contents.splitlines() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def expand_vars(f): | 
					
						
							|  |  |  |             f = os.path.expandvars(f) | 
					
						
							|  |  |  |             m = re.match(r'(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)', f) | 
					
						
							|  |  |  |             if m: | 
					
						
							|  |  |  |                 raise InputError("unknown variable '%s'" % m.group(1)) | 
					
						
							|  |  |  |             return f | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         for line_no in range(len(lines)): | 
					
						
							| 
									
										
										
										
											2017-07-25 18:12:31 +02:00
										 |  |  |             line = expand_vars(lines[line_no]).strip() | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             if line.startswith('#') or len(line) == 0: | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |                 continue | 
					
						
							|  |  |  |             try: | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |                 res.append(PartitionDefinition.from_csv(line, line_no + 1)) | 
					
						
							| 
									
										
										
										
											2021-06-11 17:53:45 +05:00
										 |  |  |             except InputError as err: | 
					
						
							| 
									
										
										
										
											2022-06-21 16:19:26 +05:30
										 |  |  |                 raise InputError('Error at line %d: %s\nPlease check extra_partition_subtypes.inc file in build/config directory' % (line_no + 1, err)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             except Exception: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                 critical('Unexpected error parsing CSV line %d: %s' % (line_no + 1, line)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # fix up missing offsets & negative sizes | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         last_end = offset_part_table + PARTITION_TABLE_SIZE  # first offset after partition table | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         for e in res: | 
					
						
							| 
									
										
										
										
											2018-07-12 11:32:58 +10:00
										 |  |  |             if e.offset is not None and e.offset < last_end: | 
					
						
							|  |  |  |                 if e == res[0]: | 
					
						
							| 
									
										
										
										
											2022-10-20 01:16:17 +08:00
										 |  |  |                     raise InputError('CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. ' | 
					
						
							|  |  |  |                                      'But partition table occupies the whole sector 0x%x. ' | 
					
						
							|  |  |  |                                      'Use a free offset 0x%x or higher.' | 
					
						
							|  |  |  |                                      % (e.line_no, e.offset, offset_part_table, last_end)) | 
					
						
							| 
									
										
										
										
											2018-07-12 11:32:58 +10:00
										 |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2022-10-20 01:16:17 +08:00
										 |  |  |                     raise InputError('CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. Previous partition ends 0x%x' | 
					
						
							| 
									
										
										
										
											2018-07-12 11:32:58 +10:00
										 |  |  |                                      % (e.line_no, e.offset, last_end)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             if e.offset is None: | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  |                 pad_to = get_alignment_offset_for_type(e.type) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |                 if last_end % pad_to != 0: | 
					
						
							|  |  |  |                     last_end += pad_to - (last_end % pad_to) | 
					
						
							|  |  |  |                 e.offset = last_end | 
					
						
							|  |  |  |             if e.size < 0: | 
					
						
							|  |  |  |                 e.size = -e.size - e.offset | 
					
						
							|  |  |  |             last_end = e.offset + e.size | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __getitem__(self, item): | 
					
						
							|  |  |  |         """ Allow partition table access via name as well as by
 | 
					
						
							|  |  |  |         numeric index. """
 | 
					
						
							|  |  |  |         if isinstance(item, str): | 
					
						
							|  |  |  |             for x in self: | 
					
						
							|  |  |  |                 if x.name == item: | 
					
						
							|  |  |  |                     return x | 
					
						
							|  |  |  |             raise ValueError("No partition entry named '%s'" % item) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             return super(PartitionTable, self).__getitem__(item) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-22 11:14:22 +10:00
										 |  |  |     def find_by_type(self, ptype, subtype): | 
					
						
							|  |  |  |         """ Return a partition by type & subtype, returns
 | 
					
						
							|  |  |  |         None if not found """
 | 
					
						
							|  |  |  |         # convert ptype & subtypes names (if supplied this way) to integer values | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  |         ptype = get_ptype_as_int(ptype) | 
					
						
							| 
									
										
										
										
											2021-04-12 17:16:49 +10:00
										 |  |  |         subtype = get_subtype_as_int(ptype, subtype) | 
					
						
							| 
									
										
										
										
											2018-06-22 11:14:22 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  |         for p in self: | 
					
						
							|  |  |  |             if p.type == ptype and p.subtype == subtype: | 
					
						
							| 
									
										
										
										
											2020-10-01 15:10:00 +08:00
										 |  |  |                 yield p | 
					
						
							|  |  |  |         return | 
					
						
							| 
									
										
										
										
											2018-06-22 11:14:22 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def find_by_name(self, name): | 
					
						
							|  |  |  |         for p in self: | 
					
						
							|  |  |  |             if p.name == name: | 
					
						
							|  |  |  |                 return p | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     def verify(self): | 
					
						
							|  |  |  |         # verify each partition individually | 
					
						
							|  |  |  |         for p in self: | 
					
						
							|  |  |  |             p.verify() | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-06 01:11:59 +08:00
										 |  |  |         # check on duplicate name | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         names = [p.name for p in self] | 
					
						
							|  |  |  |         duplicates = set(n for n in names if names.count(n) > 1) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-06 01:11:59 +08:00
										 |  |  |         # print sorted duplicate partitions by name | 
					
						
							|  |  |  |         if len(duplicates) != 0: | 
					
						
							| 
									
										
										
										
											2021-08-23 08:01:41 +02:00
										 |  |  |             critical('A list of partitions that have the same name:') | 
					
						
							| 
									
										
										
										
											2018-09-06 01:11:59 +08:00
										 |  |  |             for p in sorted(self, key=lambda x:x.name): | 
					
						
							|  |  |  |                 if len(duplicates.intersection([p.name])) != 0: | 
					
						
							| 
									
										
										
										
											2021-08-23 08:01:41 +02:00
										 |  |  |                     critical('%s' % (p.to_csv())) | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise InputError('Partition names must be unique') | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         # check for overlaps | 
					
						
							|  |  |  |         last = None | 
					
						
							| 
									
										
										
										
											2017-05-06 12:47:26 -06:00
										 |  |  |         for p in sorted(self, key=lambda x:x.offset): | 
					
						
							| 
									
										
										
										
											2018-04-19 09:42:26 +05:00
										 |  |  |             if p.offset < offset_part_table + PARTITION_TABLE_SIZE: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                 raise InputError('Partition offset 0x%x is below 0x%x' % (p.offset, offset_part_table + PARTITION_TABLE_SIZE)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             if last is not None and p.offset < last.offset + last.size: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                 raise InputError('Partition at 0x%x overlaps 0x%x-0x%x' % (p.offset, last.offset, last.offset + last.size - 1)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             last = p | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-11 17:53:45 +05:00
										 |  |  |         # check that otadata should be unique | 
					
						
							|  |  |  |         otadata_duplicates = [p for p in self if p.type == TYPES['data'] and p.subtype == SUBTYPES[DATA_TYPE]['ota']] | 
					
						
							|  |  |  |         if len(otadata_duplicates) > 1: | 
					
						
							|  |  |  |             for p in otadata_duplicates: | 
					
						
							| 
									
										
										
										
											2021-08-23 08:01:41 +02:00
										 |  |  |                 critical('%s' % (p.to_csv())) | 
					
						
							| 
									
										
										
										
											2021-06-11 17:53:45 +05:00
										 |  |  |             raise InputError('Found multiple otadata partitions. Only one partition can be defined with type="data"(1) and subtype="ota"(0).') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if len(otadata_duplicates) == 1 and otadata_duplicates[0].size != 0x2000: | 
					
						
							|  |  |  |             p = otadata_duplicates[0] | 
					
						
							| 
									
										
										
										
											2021-08-23 08:01:41 +02:00
										 |  |  |             critical('%s' % (p.to_csv())) | 
					
						
							| 
									
										
										
										
											2021-06-11 17:53:45 +05:00
										 |  |  |             raise InputError('otadata partition must have size = 0x2000') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-20 16:57:15 +10:00
										 |  |  |     def flash_size(self): | 
					
						
							|  |  |  |         """ Return the size that partitions will occupy in flash
 | 
					
						
							|  |  |  |             (ie the offset the last partition ends at) | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             last = sorted(self, reverse=True)[0] | 
					
						
							|  |  |  |         except IndexError: | 
					
						
							|  |  |  |             return 0  # empty table! | 
					
						
							|  |  |  |         return last.offset + last.size | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-20 22:29:32 +03:00
										 |  |  |     def verify_size_fits(self, flash_size_bytes: int) -> None: | 
					
						
							|  |  |  |         """ Check that partition table fits into the given flash size.
 | 
					
						
							|  |  |  |             Raises InputError otherwise. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         table_size = self.flash_size() | 
					
						
							|  |  |  |         if flash_size_bytes < table_size: | 
					
						
							|  |  |  |             mb = 1024 * 1024 | 
					
						
							|  |  |  |             raise InputError('Partitions tables occupies %.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." % | 
					
						
							|  |  |  |                              (table_size / mb, table_size, flash_size_bytes / mb)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_binary(cls, b): | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         md5 = hashlib.md5() | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         result = cls() | 
					
						
							|  |  |  |         for o in range(0,len(b),32): | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |             data = b[o:o + 32] | 
					
						
							| 
									
										
										
										
											2016-11-07 15:45:57 +11:00
										 |  |  |             if len(data) != 32: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                 raise InputError('Partition table length must be a multiple of 32 bytes') | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |             if data == b'\xFF' * 32: | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |                 return result  # got end marker | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |             if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]:  # check only the magic number part | 
					
						
							| 
									
										
										
										
											2018-01-31 14:45:12 +01:00
										 |  |  |                 if data[16:] == md5.digest(): | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |                     continue  # the next iteration will check for the end marker | 
					
						
							| 
									
										
										
										
											2018-01-31 14:45:12 +01:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:]))) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 md5.update(data) | 
					
						
							| 
									
										
										
										
											2016-11-07 15:45:57 +11:00
										 |  |  |             result.append(PartitionDefinition.from_binary(data)) | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         raise InputError('Partition table is missing an end-of-table marker') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def to_binary(self): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         result = b''.join(e.to_binary() for e in self) | 
					
						
							| 
									
										
										
										
											2018-02-16 11:12:16 +01:00
										 |  |  |         if md5sum: | 
					
						
							|  |  |  |             result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest() | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         if len(result) >= MAX_PARTITION_LENGTH: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise InputError('Binary partition table length (%d) longer than max' % len(result)) | 
					
						
							|  |  |  |         result += b'\xFF' * (MAX_PARTITION_LENGTH - len(result))  # pad the sector, for signing | 
					
						
							| 
									
										
										
										
											2016-11-07 15:32:21 +11:00
										 |  |  |         return result | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def to_csv(self, simple_formatting=False): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         rows = ['# ESP-IDF Partition Table', | 
					
						
							|  |  |  |                 '# Name, Type, SubType, Offset, Size, Flags'] | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         rows += [x.to_csv(simple_formatting) for x in self] | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         return '\n'.join(rows) + '\n' | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | class PartitionDefinition(object): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |     MAGIC_BYTES = b'\xAA\x50' | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |     # dictionary maps flag name (as used in CSV flags list, property name) | 
					
						
							|  |  |  |     # to bit set in flags words in binary format | 
					
						
							|  |  |  |     FLAGS = { | 
					
						
							| 
									
										
										
										
											2023-07-17 11:59:28 +02:00
										 |  |  |         'encrypted': 0, | 
					
						
							|  |  |  |         'readonly': 1 | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-22 11:14:22 +10:00
										 |  |  |     # add subtypes for the 16 OTA slot values ("ota_XX, etc.") | 
					
						
							| 
									
										
										
										
											2018-11-16 04:59:37 +08:00
										 |  |  |     for ota_slot in range(NUM_PARTITION_SUBTYPE_APP_OTA): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         SUBTYPES[TYPES['app']]['ota_%d' % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         self.name = '' | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         self.type = None | 
					
						
							|  |  |  |         self.subtype = None | 
					
						
							|  |  |  |         self.offset = None | 
					
						
							|  |  |  |         self.size = None | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |         self.encrypted = False | 
					
						
							| 
									
										
										
										
											2023-07-17 11:59:28 +02:00
										 |  |  |         self.readonly = False | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2018-07-12 11:32:58 +10:00
										 |  |  |     def from_csv(cls, line, line_no): | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         """ Parse a line from the CSV """ | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         line_w_defaults = line + ',,,,'  # lazy way to support default fields | 
					
						
							|  |  |  |         fields = [f.strip() for f in line_w_defaults.split(',')] | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         res = PartitionDefinition() | 
					
						
							| 
									
										
										
										
											2018-07-12 11:32:58 +10:00
										 |  |  |         res.line_no = line_no | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         res.name = fields[0] | 
					
						
							|  |  |  |         res.type = res.parse_type(fields[1]) | 
					
						
							|  |  |  |         res.subtype = res.parse_subtype(fields[2]) | 
					
						
							|  |  |  |         res.offset = res.parse_address(fields[3]) | 
					
						
							|  |  |  |         res.size = res.parse_address(fields[4]) | 
					
						
							|  |  |  |         if res.size is None: | 
					
						
							|  |  |  |             raise InputError("Size field can't be empty") | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         flags = fields[5].split(':') | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |         for flag in flags: | 
					
						
							|  |  |  |             if flag in cls.FLAGS: | 
					
						
							|  |  |  |                 setattr(res, flag, True) | 
					
						
							|  |  |  |             elif len(flag) > 0: | 
					
						
							|  |  |  |                 raise InputError("CSV flag column contains unknown flag '%s'" % (flag)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         return res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __eq__(self, other): | 
					
						
							|  |  |  |         return self.name == other.name and self.type == other.type \ | 
					
						
							|  |  |  |             and self.subtype == other.subtype and self.offset == other.offset \ | 
					
						
							|  |  |  |             and self.size == other.size | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __repr__(self): | 
					
						
							|  |  |  |         def maybe_hex(x): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             return '0x%x' % x if x is not None else 'None' | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0, | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |                                                                   maybe_hex(self.offset), maybe_hex(self.size)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __str__(self): | 
					
						
							|  |  |  |         return "Part '%s' %d/%d @ 0x%x size 0x%x" % (self.name, self.type, self.subtype, self.offset or -1, self.size or -1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __cmp__(self, other): | 
					
						
							|  |  |  |         return self.offset - other.offset | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-31 16:26:38 +02:00
										 |  |  |     def __lt__(self, other): | 
					
						
							|  |  |  |         return self.offset < other.offset | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __gt__(self, other): | 
					
						
							|  |  |  |         return self.offset > other.offset | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __le__(self, other): | 
					
						
							|  |  |  |         return self.offset <= other.offset | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __ge__(self, other): | 
					
						
							|  |  |  |         return self.offset >= other.offset | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     def parse_type(self, strval): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         if strval == '': | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             raise InputError("Field 'type' can't be left empty.") | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |         return parse_int(strval, TYPES) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def parse_subtype(self, strval): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         if strval == '': | 
					
						
							| 
									
										
										
										
											2021-06-11 17:53:45 +05:00
										 |  |  |             if self.type == TYPES['app']: | 
					
						
							|  |  |  |                 raise InputError('App partition cannot have an empty subtype') | 
					
						
							|  |  |  |             return SUBTYPES[DATA_TYPE]['undefined'] | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |         return parse_int(strval, SUBTYPES.get(self.type, {})) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def parse_address(self, strval): | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         if strval == '': | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             return None  # PartitionTable will fill in default | 
					
						
							|  |  |  |         return parse_int(strval) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def verify(self): | 
					
						
							|  |  |  |         if self.type is None: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise ValidationError(self, 'Type field is not set') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         if self.subtype is None: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise ValidationError(self, 'Subtype field is not set') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         if self.offset is None: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise ValidationError(self, 'Offset field is not set') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         if self.size is None: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise ValidationError(self, 'Size field is not set') | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  |         offset_align = get_alignment_offset_for_type(self.type) | 
					
						
							|  |  |  |         if self.offset % offset_align: | 
					
						
							|  |  |  |             raise ValidationError(self, 'Offset 0x%x is not aligned to 0x%x' % (self.offset, offset_align)) | 
					
						
							| 
									
										
										
										
											2023-10-30 18:04:25 +05:30
										 |  |  |         if self.type == APP_TYPE: | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  |             size_align = get_alignment_size_for_type(self.type) | 
					
						
							|  |  |  |             if self.size % size_align: | 
					
						
							|  |  |  |                 raise ValidationError(self, 'Size 0x%x is not aligned to 0x%x' % (self.size, size_align)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         if self.name in TYPES and TYPES.get(self.name, '') != self.type: | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |             critical("WARNING: Partition has name '%s' which is a partition type, but does not match this partition's " | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                      'type (0x%x). Mistake in partition table?' % (self.name, self.type)) | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |         all_subtype_names = [] | 
					
						
							|  |  |  |         for names in (t.keys() for t in SUBTYPES.values()): | 
					
						
							|  |  |  |             all_subtype_names += names | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, '') != self.subtype: | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |             critical("WARNING: Partition has name '%s' which is a partition subtype, but this partition has " | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                      'non-matching type 0x%x and subtype 0x%x. Mistake in partition table?' % (self.name, self.type, self.subtype)) | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-17 11:59:28 +02:00
										 |  |  |         always_rw_data_subtypes = [SUBTYPES[DATA_TYPE]['ota'], SUBTYPES[DATA_TYPE]['coredump']] | 
					
						
							|  |  |  |         if self.type == TYPES['data'] and self.subtype in always_rw_data_subtypes and self.readonly is True: | 
					
						
							|  |  |  |             raise ValidationError(self, "'%s' partition of type %s and subtype %s is always read-write and cannot be read-only" % | 
					
						
							|  |  |  |                                   (self.name, self.type, self.subtype)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |     STRUCT_FORMAT = b'<2sBBLL16sL' | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_binary(cls, b): | 
					
						
							|  |  |  |         if len(b) != 32: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise InputError('Partition definition length must be exactly 32 bytes. Got %d bytes.' % len(b)) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         res = cls() | 
					
						
							|  |  |  |         (magic, res.type, res.subtype, res.offset, | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |          res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b) | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         if b'\x00' in res.name:  # strip null byte padding from name string | 
					
						
							|  |  |  |             res.name = res.name[:res.name.index(b'\x00')] | 
					
						
							| 
									
										
										
										
											2017-05-12 12:25:41 +10:00
										 |  |  |         res.name = res.name.decode() | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         if magic != cls.MAGIC_BYTES: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise InputError('Invalid magic bytes (%r) for partition definition' % magic) | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |         for flag,bit in cls.FLAGS.items(): | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |             if flags & (1 << bit): | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |                 setattr(res, flag, True) | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |                 flags &= ~(1 << bit) | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |         if flags != 0: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             critical('WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?' % flags) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         return res | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |     def get_flags_list(self): | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         return [flag for flag in self.FLAGS.keys() if getattr(self, flag)] | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     def to_binary(self): | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |         flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list()) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         return struct.pack(self.STRUCT_FORMAT, | 
					
						
							|  |  |  |                            self.MAGIC_BYTES, | 
					
						
							|  |  |  |                            self.type, self.subtype, | 
					
						
							|  |  |  |                            self.offset, self.size, | 
					
						
							| 
									
										
										
										
											2017-05-06 12:47:26 -06:00
										 |  |  |                            self.name.encode(), | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |                            flags) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def to_csv(self, simple_formatting=False): | 
					
						
							|  |  |  |         def addr_format(a, include_sizes): | 
					
						
							|  |  |  |             if not simple_formatting and include_sizes: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                 for (val, suffix) in [(0x100000, 'M'), (0x400, 'K')]: | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |                     if a % val == 0: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |                         return '%d%s' % (a // val, suffix) | 
					
						
							|  |  |  |             return '0x%x' % a | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         def lookup_keyword(t, keywords): | 
					
						
							|  |  |  |             for k,v in keywords.items(): | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |                 if simple_formatting is False and t == v: | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |                     return k | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             return '%d' % t | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  |         def generate_text_flags(): | 
					
						
							|  |  |  |             """ colon-delimited list of flags """ | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             return ':'.join(self.get_flags_list()) | 
					
						
							| 
									
										
										
										
											2016-11-11 17:00:34 +11:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         return ','.join([self.name, | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |                          lookup_keyword(self.type, TYPES), | 
					
						
							|  |  |  |                          lookup_keyword(self.subtype, SUBTYPES.get(self.type, {})), | 
					
						
							|  |  |  |                          addr_format(self.offset, False), | 
					
						
							|  |  |  |                          addr_format(self.size, True), | 
					
						
							|  |  |  |                          generate_text_flags()]) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def parse_int(v, keywords={}): | 
					
						
							|  |  |  |     """Generic parser for integer fields - int(x,0) with provision for
 | 
					
						
							|  |  |  |     k/m/K/M suffixes and 'keyword' value lookup. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     try: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         for letter, multiplier in [('k', 1024), ('m', 1024 * 1024)]: | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |             if v.lower().endswith(letter): | 
					
						
							|  |  |  |                 return parse_int(v[:-1], keywords) * multiplier | 
					
						
							|  |  |  |         return int(v, 0) | 
					
						
							|  |  |  |     except ValueError: | 
					
						
							|  |  |  |         if len(keywords) == 0: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise InputError('Invalid field value %s' % v) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         try: | 
					
						
							|  |  |  |             return keywords[v.lower()] | 
					
						
							|  |  |  |         except KeyError: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ', '.join(keywords))) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | def main(): | 
					
						
							|  |  |  |     global quiet | 
					
						
							| 
									
										
										
										
											2018-02-16 11:12:16 +01:00
										 |  |  |     global md5sum | 
					
						
							| 
									
										
										
										
											2018-04-19 09:42:26 +05:00
										 |  |  |     global offset_part_table | 
					
						
							| 
									
										
										
										
											2018-07-13 11:52:57 +10:00
										 |  |  |     global secure | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     parser = argparse.ArgumentParser(description='ESP32 partition table utility') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-20 16:57:15 +10:00
										 |  |  |     parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash', | 
					
						
							| 
									
										
										
										
											2021-10-13 22:37:10 +02:00
										 |  |  |                         nargs='?', choices=['1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB']) | 
					
						
							| 
									
										
										
										
											2018-02-16 11:12:16 +01:00
										 |  |  |     parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true') | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |     parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true') | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |     parser.add_argument('--verify', '-v', help='Verify partition table fields (deprecated, this behaviour is ' | 
					
						
							|  |  |  |                                                'enabled by default and this flag does nothing.', action='store_true') | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |     parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true') | 
					
						
							| 
									
										
										
										
											2018-04-19 09:42:26 +05:00
										 |  |  |     parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000') | 
					
						
							| 
									
										
										
										
											2023-02-01 14:31:42 +05:30
										 |  |  |     parser.add_argument('--secure', help='Require app partitions to be suitable for secure boot', nargs='?', const=SECURE_V1, choices=[SECURE_V1, SECURE_V2]) | 
					
						
							| 
									
										
										
										
											2022-06-21 16:19:26 +05:30
										 |  |  |     parser.add_argument('--extra-partition-subtypes', help='Extra partition subtype entries', nargs='*') | 
					
						
							| 
									
										
										
										
											2018-06-22 11:27:09 +10:00
										 |  |  |     parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb')) | 
					
						
							|  |  |  |     parser.add_argument('output', help='Path to output converted binary or CSV file. Will use stdout if omitted.', | 
					
						
							|  |  |  |                         nargs='?', default='-') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     args = parser.parse_args() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     quiet = args.quiet | 
					
						
							| 
									
										
										
										
											2018-02-16 11:12:16 +01:00
										 |  |  |     md5sum = not args.disable_md5sum | 
					
						
							| 
									
										
										
										
											2018-07-13 11:52:57 +10:00
										 |  |  |     secure = args.secure | 
					
						
							| 
									
										
										
										
											2018-04-19 09:42:26 +05:00
										 |  |  |     offset_part_table = int(args.offset, 0) | 
					
						
							| 
									
										
										
										
											2022-06-21 16:19:26 +05:30
										 |  |  |     if args.extra_partition_subtypes: | 
					
						
							|  |  |  |         add_extra_subtypes(args.extra_partition_subtypes) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-24 15:52:50 +10:00
										 |  |  |     table, input_is_binary = PartitionTable.from_file(args.input) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-22 12:06:40 +10:00
										 |  |  |     if not args.no_verify: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         status('Verifying table...') | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         table.verify() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-20 16:57:15 +10:00
										 |  |  |     if args.flash_size: | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |         size_mb = int(args.flash_size.replace('MB', '')) | 
					
						
							| 
									
										
										
										
											2022-02-20 22:29:32 +03:00
										 |  |  |         table.verify_size_fits(size_mb * 1024 * 1024) | 
					
						
							| 
									
										
										
										
											2018-04-20 16:57:15 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-11 15:36:10 +08:00
										 |  |  |     # Make sure that the output directory is created | 
					
						
							|  |  |  |     output_dir = os.path.abspath(os.path.dirname(args.output)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not os.path.exists(output_dir): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             os.makedirs(output_dir) | 
					
						
							| 
									
										
										
										
											2018-12-04 13:06:46 +01:00
										 |  |  |         except OSError as exc: | 
					
						
							| 
									
										
										
										
											2018-11-11 15:36:10 +08:00
										 |  |  |             if exc.errno != errno.EEXIST: | 
					
						
							|  |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     if input_is_binary: | 
					
						
							|  |  |  |         output = table.to_csv() | 
					
						
							| 
									
										
										
										
											2017-05-06 12:47:26 -06:00
										 |  |  |         with sys.stdout if args.output == '-' else open(args.output, 'w') as f: | 
					
						
							|  |  |  |             f.write(output) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |     else: | 
					
						
							|  |  |  |         output = table.to_binary() | 
					
						
							| 
									
										
										
										
											2018-06-22 11:27:09 +10:00
										 |  |  |         try: | 
					
						
							|  |  |  |             stdout_binary = sys.stdout.buffer  # Python 3 | 
					
						
							|  |  |  |         except AttributeError: | 
					
						
							|  |  |  |             stdout_binary = sys.stdout | 
					
						
							|  |  |  |         with stdout_binary if args.output == '-' else open(args.output, 'wb') as f: | 
					
						
							| 
									
										
										
										
											2017-05-06 12:47:26 -06:00
										 |  |  |             f.write(output) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-12 12:07:59 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  | class InputError(RuntimeError): | 
					
						
							|  |  |  |     def __init__(self, e): | 
					
						
							|  |  |  |         super(InputError, self).__init__(e) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ValidationError(InputError): | 
					
						
							|  |  |  |     def __init__(self, partition, message): | 
					
						
							|  |  |  |         super(ValidationError, self).__init__( | 
					
						
							| 
									
										
										
										
											2021-01-26 10:49:01 +08:00
										 |  |  |             'Partition %s invalid: %s' % (partition.name, message)) | 
					
						
							| 
									
										
										
										
											2017-05-12 12:07:59 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         main() | 
					
						
							|  |  |  |     except InputError as e: | 
					
						
							| 
									
										
										
										
											2017-05-12 12:25:41 +10:00
										 |  |  |         print(e, file=sys.stderr) | 
					
						
							| 
									
										
										
										
											2016-08-17 23:08:22 +08:00
										 |  |  |         sys.exit(2) |