| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | #!/usr/bin/env python | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # ESP32 partition table generation tool | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Converts partition tables to/from CSV and binary formats. | 
					
						
							|  |  |  | # | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  | # See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  | # for explanation of partition table structure and uses. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  | # you may not use this file except in compliance with the License. | 
					
						
							|  |  |  | # You may obtain a copy of the License at | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #     http:#www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | # distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | # See the License for the specific language governing permissions and | 
					
						
							|  |  |  | # limitations under the License. | 
					
						
							|  |  |  | from __future__ import print_function, division | 
					
						
							| 
									
										
										
										
											2018-11-26 23:22:11 +01:00
										 |  |  | from __future__ import unicode_literals | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | import argparse | 
					
						
							| 
									
										
										
										
											2017-02-23 01:11:57 +02:00
										 |  |  | import os | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | import struct | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  | import hashlib | 
					
						
							|  |  |  | import binascii | 
					
						
							| 
									
										
										
										
											2018-11-30 17:21:06 +01:00
										 |  |  | import errno | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | MAX_PARTITION_LENGTH = 0xC00   # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | MD5_PARTITION_BEGIN = b"\xEB\xEB" + b"\xFF" * 14  # The first 2 bytes are like magic numbers for MD5 sum | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  | PARTITION_TABLE_SIZE  = 0x1000  # Size of partition table | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-15 17:38:34 +01:00
										 |  |  | MIN_PARTITION_SUBTYPE_APP_OTA = 0x10 | 
					
						
							|  |  |  | NUM_PARTITION_SUBTYPE_APP_OTA = 16 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  | __version__ = '1.2' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | APP_TYPE = 0x00 | 
					
						
							|  |  |  | DATA_TYPE = 0x01 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | TYPES = { | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |     "app": APP_TYPE, | 
					
						
							|  |  |  |     "data": DATA_TYPE, | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h | 
					
						
							|  |  |  | SUBTYPES = { | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |     APP_TYPE: { | 
					
						
							|  |  |  |         "factory": 0x00, | 
					
						
							|  |  |  |         "test": 0x20, | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |     DATA_TYPE: { | 
					
						
							|  |  |  |         "ota": 0x00, | 
					
						
							|  |  |  |         "phy": 0x01, | 
					
						
							|  |  |  |         "nvs": 0x02, | 
					
						
							|  |  |  |         "coredump": 0x03, | 
					
						
							|  |  |  |         "nvs_keys": 0x04, | 
					
						
							|  |  |  |         "esphttpd": 0x80, | 
					
						
							|  |  |  |         "fat": 0x81, | 
					
						
							|  |  |  |         "spiffs": 0x82, | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     }, | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | quiet = False | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  | md5sum = True | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  | secure = False | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  | offset_part_table = 0 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | def status(msg): | 
					
						
							|  |  |  |     """ Print status message to stderr """ | 
					
						
							|  |  |  |     if not quiet: | 
					
						
							|  |  |  |         critical(msg) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | def critical(msg): | 
					
						
							|  |  |  |     """ Print critical message to stderr """ | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     sys.stderr.write(msg) | 
					
						
							|  |  |  |     sys.stderr.write('\n') | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | class PartitionTable(list): | 
					
						
							|  |  |  |     def __init__(self): | 
					
						
							|  |  |  |         super(PartitionTable, self).__init__(self) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_csv(cls, csv_contents): | 
					
						
							|  |  |  |         res = PartitionTable() | 
					
						
							| 
									
										
										
										
											2017-09-22 17:28:54 +08: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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         for line_no in range(len(lines)): | 
					
						
							| 
									
										
										
										
											2017-09-22 17:28:54 +08:00
										 |  |  |             line = expand_vars(lines[line_no]).strip() | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             if line.startswith("#") or len(line) == 0: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             try: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 res.append(PartitionDefinition.from_csv(line, line_no + 1)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             except InputError as e: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 raise InputError("Error at line %d: %s" % (line_no + 1, e)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             except Exception: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 critical("Unexpected error parsing CSV line %d: %s" % (line_no + 1, line)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # fix up missing offsets & negative sizes | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         last_end = offset_part_table + PARTITION_TABLE_SIZE  # first offset after partition table | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         for e in res: | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |             if e.offset is not None and e.offset < last_end: | 
					
						
							|  |  |  |                 if e == res[0]: | 
					
						
							|  |  |  |                     raise InputError("CSV Error: First partition offset 0x%x overlaps end of partition table 0x%x" | 
					
						
							|  |  |  |                                      % (e.offset, last_end)) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     raise InputError("CSV Error: Partitions overlap. Partition at line %d sets offset 0x%x. Previous partition ends 0x%x" | 
					
						
							|  |  |  |                                      % (e.line_no, e.offset, last_end)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             if e.offset is None: | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |                 pad_to = 0x10000 if e.type == APP_TYPE else 4 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02: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-27 09:01:06 +02: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 | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             ptype = TYPES[ptype] | 
					
						
							|  |  |  |         except KeyError: | 
					
						
							|  |  |  |             try: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 ptype = int(ptype, 0) | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |             except TypeError: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             subtype = SUBTYPES[int(ptype)][subtype] | 
					
						
							|  |  |  |         except KeyError: | 
					
						
							|  |  |  |             try: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 ptype = int(ptype, 0) | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |             except TypeError: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for p in self: | 
					
						
							|  |  |  |             if p.type == ptype and p.subtype == subtype: | 
					
						
							|  |  |  |                 return p | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def find_by_name(self, name): | 
					
						
							|  |  |  |         for p in self: | 
					
						
							|  |  |  |             if p.name == name: | 
					
						
							|  |  |  |                 return p | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     def verify(self): | 
					
						
							|  |  |  |         # verify each partition individually | 
					
						
							|  |  |  |         for p in self: | 
					
						
							|  |  |  |             p.verify() | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |         # check on duplicate name | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         names = [p.name for p in self] | 
					
						
							|  |  |  |         duplicates = set(n for n in names if names.count(n) > 1) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |         # print sorted duplicate partitions by name | 
					
						
							|  |  |  |         if len(duplicates) != 0: | 
					
						
							|  |  |  |             print("A list of partitions that have the same name:") | 
					
						
							|  |  |  |             for p in sorted(self, key=lambda x:x.name): | 
					
						
							|  |  |  |                 if len(duplicates.intersection([p.name])) != 0: | 
					
						
							|  |  |  |                     print("%s" % (p.to_csv())) | 
					
						
							|  |  |  |             raise InputError("Partition names must be unique") | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         # check for overlaps | 
					
						
							|  |  |  |         last = None | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |         for p in sorted(self, key=lambda x:x.offset): | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |             if p.offset < offset_part_table + PARTITION_TABLE_SIZE: | 
					
						
							|  |  |  |                 raise InputError("Partition offset 0x%x is below 0x%x" % (p.offset, offset_part_table + PARTITION_TABLE_SIZE)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             if last is not None and p.offset < last.offset + last.size: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 raise InputError("Partition at 0x%x overlaps 0x%x-0x%x" % (p.offset, last.offset, last.offset + last.size - 1)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             last = p | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02: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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_binary(cls, b): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         md5 = hashlib.md5() | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         result = cls() | 
					
						
							|  |  |  |         for o in range(0,len(b),32): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             data = b[o:o + 32] | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             if len(data) != 32: | 
					
						
							|  |  |  |                 raise InputError("Partition table length must be a multiple of 32 bytes") | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             if data == b'\xFF' * 32: | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |                 return result  # got end marker | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]:  # check only the magic number part | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  |                 if data[16:] == md5.digest(): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                     continue  # the next iteration will check for the end marker | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03: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) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             result.append(PartitionDefinition.from_binary(data)) | 
					
						
							|  |  |  |         raise InputError("Partition table is missing an end-of-table marker") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def to_binary(self): | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |         result = b"".join(e.to_binary() for e in self) | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  |         if md5sum: | 
					
						
							|  |  |  |             result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest() | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         if len(result) >= MAX_PARTITION_LENGTH: | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             raise InputError("Binary partition table length (%d) longer than max" % len(result)) | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |         result += b"\xFF" * (MAX_PARTITION_LENGTH - len(result))  # pad the sector, for signing | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         return result | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def to_csv(self, simple_formatting=False): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         rows = ["# Espressif ESP32 Partition Table", | 
					
						
							|  |  |  |                 "# Name, Type, SubType, Offset, Size, Flags"] | 
					
						
							|  |  |  |         rows += [x.to_csv(simple_formatting) for x in self] | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         return "\n".join(rows) + "\n" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | class PartitionDefinition(object): | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |     MAGIC_BYTES = b"\xAA\x50" | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     ALIGNMENT = { | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         APP_TYPE: 0x10000, | 
					
						
							|  |  |  |         DATA_TYPE: 0x04, | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # dictionary maps flag name (as used in CSV flags list, property name) | 
					
						
							|  |  |  |     # to bit set in flags words in binary format | 
					
						
							|  |  |  |     FLAGS = { | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         "encrypted": 0 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     # add subtypes for the 16 OTA slot values ("ota_XX, etc.") | 
					
						
							| 
									
										
										
										
											2018-12-15 17:38:34 +01:00
										 |  |  |     for ota_slot in range(NUM_PARTITION_SUBTYPE_APP_OTA): | 
					
						
							|  |  |  |         SUBTYPES[TYPES["app"]]["ota_%d" % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self): | 
					
						
							|  |  |  |         self.name = "" | 
					
						
							|  |  |  |         self.type = None | 
					
						
							|  |  |  |         self.subtype = None | 
					
						
							|  |  |  |         self.offset = None | 
					
						
							|  |  |  |         self.size = None | 
					
						
							|  |  |  |         self.encrypted = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |     def from_csv(cls, line, line_no): | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         """ Parse a line from the CSV """ | 
					
						
							|  |  |  |         line_w_defaults = line + ",,,,"  # lazy way to support default fields | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         fields = [f.strip() for f in line_w_defaults.split(",")] | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         res = PartitionDefinition() | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |         res.line_no = line_no | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02: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") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         flags = fields[5].split(":") | 
					
						
							|  |  |  |         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)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         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): | 
					
						
							|  |  |  |             return "0x%x" % x if x is not None else "None" | 
					
						
							|  |  |  |         return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0, | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                                                                   maybe_hex(self.offset), maybe_hex(self.size)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02: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-06-27 09:01:06 +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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     def parse_type(self, strval): | 
					
						
							|  |  |  |         if strval == "": | 
					
						
							|  |  |  |             raise InputError("Field 'type' can't be left empty.") | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |         return parse_int(strval, TYPES) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def parse_subtype(self, strval): | 
					
						
							|  |  |  |         if strval == "": | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             return 0  # default | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |         return parse_int(strval, SUBTYPES.get(self.type, {})) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def parse_address(self, strval): | 
					
						
							|  |  |  |         if strval == "": | 
					
						
							|  |  |  |             return None  # PartitionTable will fill in default | 
					
						
							|  |  |  |         return parse_int(strval) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def verify(self): | 
					
						
							|  |  |  |         if self.type is None: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |             raise ValidationError(self, "Type field is not set") | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         if self.subtype is None: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |             raise ValidationError(self, "Subtype field is not set") | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         if self.offset is None: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |             raise ValidationError(self, "Offset field is not set") | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         align = self.ALIGNMENT.get(self.type, 4) | 
					
						
							|  |  |  |         if self.offset % align: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |             raise ValidationError(self, "Offset 0x%x is not aligned to 0x%x" % (self.offset, align)) | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |         if self.size % align and secure: | 
					
						
							|  |  |  |             raise ValidationError(self, "Size 0x%x is not aligned to 0x%x" % (self.size, align)) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         if self.size is None: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |             raise ValidationError(self, "Size field is not set") | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |         if self.name in TYPES and TYPES.get(self.name, "") != self.type: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             critical("WARNING: Partition has name '%s' which is a partition type, but does not match this partition's " | 
					
						
							|  |  |  |                      "type (0x%x). Mistake in partition table?" % (self.name, self.type)) | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |         all_subtype_names = [] | 
					
						
							|  |  |  |         for names in (t.keys() for t in SUBTYPES.values()): | 
					
						
							|  |  |  |             all_subtype_names += names | 
					
						
							|  |  |  |         if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, "") != self.subtype: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             critical("WARNING: Partition has name '%s' which is a partition subtype, but this partition has " | 
					
						
							|  |  |  |                      "non-matching type 0x%x and subtype 0x%x. Mistake in partition table?" % (self.name, self.type, self.subtype)) | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-26 23:22:11 +01:00
										 |  |  |     STRUCT_FORMAT = b"<2sBBLL16sL" | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_binary(cls, b): | 
					
						
							|  |  |  |         if len(b) != 32: | 
					
						
							|  |  |  |             raise InputError("Partition definition length must be exactly 32 bytes. Got %d bytes." % len(b)) | 
					
						
							|  |  |  |         res = cls() | 
					
						
							|  |  |  |         (magic, res.type, res.subtype, res.offset, | 
					
						
							|  |  |  |          res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b) | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         if b"\x00" in res.name:  # strip null byte padding from name string | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |             res.name = res.name[:res.name.index(b"\x00")] | 
					
						
							|  |  |  |         res.name = res.name.decode() | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         if magic != cls.MAGIC_BYTES: | 
					
						
							|  |  |  |             raise InputError("Invalid magic bytes (%r) for partition definition" % magic) | 
					
						
							|  |  |  |         for flag,bit in cls.FLAGS.items(): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             if flags & (1 << bit): | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |                 setattr(res, flag, True) | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 flags &= ~(1 << bit) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         if flags != 0: | 
					
						
							|  |  |  |             critical("WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?" % flags) | 
					
						
							|  |  |  |         return res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_flags_list(self): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         return [flag for flag in self.FLAGS.keys() if getattr(self, flag)] | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def to_binary(self): | 
					
						
							|  |  |  |         flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list()) | 
					
						
							|  |  |  |         return struct.pack(self.STRUCT_FORMAT, | 
					
						
							|  |  |  |                            self.MAGIC_BYTES, | 
					
						
							|  |  |  |                            self.type, self.subtype, | 
					
						
							|  |  |  |                            self.offset, self.size, | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |                            self.name.encode(), | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |                            flags) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def to_csv(self, simple_formatting=False): | 
					
						
							|  |  |  |         def addr_format(a, include_sizes): | 
					
						
							|  |  |  |             if not simple_formatting and include_sizes: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 for (val, suffix) in [(0x100000, "M"), (0x400, "K")]: | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |                     if a % val == 0: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |                         return "%d%s" % (a // val, suffix) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             return "0x%x" % a | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def lookup_keyword(t, keywords): | 
					
						
							|  |  |  |             for k,v in keywords.items(): | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                 if simple_formatting is False and t == v: | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |                     return k | 
					
						
							|  |  |  |             return "%d" % t | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def generate_text_flags(): | 
					
						
							|  |  |  |             """ colon-delimited list of flags """ | 
					
						
							|  |  |  |             return ":".join(self.get_flags_list()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         return ",".join([self.name, | 
					
						
							|  |  |  |                          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()]) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02: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: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |         for letter, multiplier in [("k", 1024), ("m", 1024 * 1024)]: | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |             if v.lower().endswith(letter): | 
					
						
							|  |  |  |                 return parse_int(v[:-1], keywords) * multiplier | 
					
						
							|  |  |  |         return int(v, 0) | 
					
						
							|  |  |  |     except ValueError: | 
					
						
							|  |  |  |         if len(keywords) == 0: | 
					
						
							|  |  |  |             raise InputError("Invalid field value %s" % v) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             return keywords[v.lower()] | 
					
						
							|  |  |  |         except KeyError: | 
					
						
							|  |  |  |             raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ", ".join(keywords))) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | def main(): | 
					
						
							|  |  |  |     global quiet | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  |     global md5sum | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     global offset_part_table | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |     global secure | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     parser = argparse.ArgumentParser(description='ESP32 partition table utility') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash', | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |                         nargs='?', choices=['1MB', '2MB', '4MB', '8MB', '16MB']) | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  |     parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true') | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true') | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02: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-27 09:01:06 +02:00
										 |  |  |     parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true') | 
					
						
							|  |  |  |     parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000') | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |     parser.add_argument('--secure', help="Require app partitions to be suitable for secure boot", action='store_true') | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02: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='-') | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     args = parser.parse_args() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     quiet = args.quiet | 
					
						
							| 
									
										
										
										
											2018-04-07 09:45:18 +03:00
										 |  |  |     md5sum = not args.disable_md5sum | 
					
						
							| 
									
										
										
										
											2018-09-21 08:39:36 +02:00
										 |  |  |     secure = args.secure | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     offset_part_table = int(args.offset, 0) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     input = args.input.read() | 
					
						
							|  |  |  |     input_is_binary = input[0:2] == PartitionDefinition.MAGIC_BYTES | 
					
						
							|  |  |  |     if input_is_binary: | 
					
						
							|  |  |  |         status("Parsing binary partition input...") | 
					
						
							|  |  |  |         table = PartitionTable.from_binary(input) | 
					
						
							|  |  |  |     else: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |         input = input.decode() | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         status("Parsing CSV input...") | 
					
						
							|  |  |  |         table = PartitionTable.from_csv(input) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     if not args.no_verify: | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         status("Verifying table...") | 
					
						
							|  |  |  |         table.verify() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |     if args.flash_size: | 
					
						
							|  |  |  |         size_mb = int(args.flash_size.replace("MB", "")) | 
					
						
							|  |  |  |         size = size_mb * 1024 * 1024  # flash memory uses honest megabytes! | 
					
						
							|  |  |  |         table_size = table.flash_size() | 
					
						
							|  |  |  |         if size < table_size: | 
					
						
							| 
									
										
										
										
											2018-12-28 20:37:33 +02:00
										 |  |  |             raise InputError("Partitions defined in '%s' occupy %.1fMB of flash (%d bytes) which does not fit in configured " | 
					
						
							|  |  |  |                              "flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu." % | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02:00
										 |  |  |                              (args.input.name, table_size / 1024.0 / 1024.0, table_size, size_mb)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-30 17:21:06 +01: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-28 20:37:33 +02:00
										 |  |  |         except OSError as exc: | 
					
						
							| 
									
										
										
										
											2018-11-30 17:21:06 +01:00
										 |  |  |             if exc.errno != errno.EEXIST: | 
					
						
							|  |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     if input_is_binary: | 
					
						
							|  |  |  |         output = table.to_csv() | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |         with sys.stdout if args.output == '-' else open(args.output, 'w') as f: | 
					
						
							|  |  |  |             f.write(output) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |     else: | 
					
						
							|  |  |  |         output = table.to_binary() | 
					
						
							| 
									
										
										
										
											2018-06-27 09:01:06 +02: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-08-01 08:51:04 +03:00
										 |  |  |             f.write(output) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class InputError(RuntimeError): | 
					
						
							|  |  |  |     def __init__(self, e): | 
					
						
							|  |  |  |         super(InputError, self).__init__(e) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ValidationError(InputError): | 
					
						
							|  |  |  |     def __init__(self, partition, message): | 
					
						
							|  |  |  |         super(ValidationError, self).__init__( | 
					
						
							|  |  |  |             "Partition %s invalid: %s" % (partition.name, message)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         main() | 
					
						
							|  |  |  |     except InputError as e: | 
					
						
							| 
									
										
										
										
											2017-08-01 08:51:04 +03:00
										 |  |  |         print(e, file=sys.stderr) | 
					
						
							| 
									
										
										
										
											2017-02-06 15:17:11 +02:00
										 |  |  |         sys.exit(2) |