Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c469677

Browse files
authoredMay 20, 2025··
Update gen_esp32part.py
1 parent 13cd0d3 commit c469677

File tree

1 file changed

+259
-147
lines changed

1 file changed

+259
-147
lines changed
 

‎tools/gen_esp32part.py

Lines changed: 259 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
# See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html
88
# for explanation of partition table structure and uses.
99
#
10-
# SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD
10+
# SPDX-FileCopyrightText: 2016-2025 Espressif Systems (Shanghai) CO LTD
1111
# SPDX-License-Identifier: Apache-2.0
12-
13-
from __future__ import division, print_function, unicode_literals
14-
1512
import argparse
1613
import binascii
1714
import errno
@@ -22,26 +19,34 @@
2219
import sys
2320

2421
MAX_PARTITION_LENGTH = 0xC00 # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature
25-
MD5_PARTITION_BEGIN = b"\xEB\xEB" + b"\xFF" * 14 # The first 2 bytes are like magic numbers for MD5 sum
22+
MD5_PARTITION_BEGIN = b'\xeb\xeb' + b'\xff' * 14 # The first 2 bytes are like magic numbers for MD5 sum
2623
PARTITION_TABLE_SIZE = 0x1000 # Size of partition table
2724

2825
MIN_PARTITION_SUBTYPE_APP_OTA = 0x10
2926
NUM_PARTITION_SUBTYPE_APP_OTA = 16
27+
MIN_PARTITION_SUBTYPE_APP_TEE = 0x30
28+
NUM_PARTITION_SUBTYPE_APP_TEE = 2
3029

3130
SECURE_NONE = None
32-
SECURE_V1 = "v1"
33-
SECURE_V2 = "v2"
31+
SECURE_V1 = 'v1'
32+
SECURE_V2 = 'v2'
3433

35-
__version__ = "1.2"
34+
__version__ = '1.5'
3635

3736
APP_TYPE = 0x00
3837
DATA_TYPE = 0x01
38+
BOOTLOADER_TYPE = 0x02
39+
PARTITION_TABLE_TYPE = 0x03
3940

4041
TYPES = {
41-
"app": APP_TYPE,
42-
"data": DATA_TYPE,
42+
'bootloader': BOOTLOADER_TYPE,
43+
'partition_table': PARTITION_TABLE_TYPE,
44+
'app': APP_TYPE,
45+
'data': DATA_TYPE,
4346
}
4447

48+
NVS_RW_MIN_PARTITION_SIZE = 0x3000
49+
4550

4651
def get_ptype_as_int(ptype):
4752
"""Convert a string which might be numeric or the name of a partition type to an integer"""
@@ -56,22 +61,32 @@ def get_ptype_as_int(ptype):
5661

5762
# Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h
5863
SUBTYPES = {
64+
BOOTLOADER_TYPE: {
65+
'primary': 0x00,
66+
'ota': 0x01,
67+
'recovery': 0x02,
68+
},
69+
PARTITION_TABLE_TYPE: {
70+
'primary': 0x00,
71+
'ota': 0x01,
72+
},
5973
APP_TYPE: {
60-
"factory": 0x00,
61-
"test": 0x20,
74+
'factory': 0x00,
75+
'test': 0x20,
6276
},
6377
DATA_TYPE: {
64-
"ota": 0x00,
65-
"phy": 0x01,
66-
"nvs": 0x02,
67-
"coredump": 0x03,
68-
"nvs_keys": 0x04,
69-
"efuse": 0x05,
70-
"undefined": 0x06,
71-
"esphttpd": 0x80,
72-
"fat": 0x81,
73-
"spiffs": 0x82,
74-
"littlefs": 0x83,
78+
'ota': 0x00,
79+
'phy': 0x01,
80+
'nvs': 0x02,
81+
'coredump': 0x03,
82+
'nvs_keys': 0x04,
83+
'efuse': 0x05,
84+
'undefined': 0x06,
85+
'esphttpd': 0x80,
86+
'fat': 0x81,
87+
'spiffs': 0x82,
88+
'littlefs': 0x83,
89+
'tee_ota': 0x90,
7590
},
7691
}
7792

@@ -90,6 +105,8 @@ def get_subtype_as_int(ptype, subtype):
90105
ALIGNMENT = {
91106
APP_TYPE: 0x10000,
92107
DATA_TYPE: 0x1000,
108+
BOOTLOADER_TYPE: 0x1000,
109+
PARTITION_TABLE_TYPE: 0x1000,
93110
}
94111

95112

@@ -98,42 +115,52 @@ def get_alignment_offset_for_type(ptype):
98115

99116

100117
def get_alignment_size_for_type(ptype):
101-
if ptype == APP_TYPE and secure == SECURE_V1:
102-
# For secure boot v1 case, app partition must be 64K aligned
103-
# signature block (68 bytes) lies at the very end of 64K block
104-
return 0x10000
105-
if ptype == APP_TYPE and secure == SECURE_V2:
106-
# For secure boot v2 case, app partition must be 4K aligned
107-
# signature block (4K) is kept after padding the unsigned image to 64K boundary
108-
return 0x1000
118+
if ptype == APP_TYPE:
119+
if secure == SECURE_V1:
120+
# For secure boot v1 case, app partition must be 64K aligned
121+
# signature block (68 bytes) lies at the very end of 64K block
122+
return 0x10000
123+
elif secure == SECURE_V2:
124+
# For secure boot v2 case, app partition must be 4K aligned
125+
# signature block (4K) is kept after padding the unsigned image to 64K boundary
126+
return 0x1000
127+
else:
128+
# For no secure boot enabled case, app partition must be 4K aligned (min. flash erase size)
129+
return 0x1000
109130
# No specific size alignment requirement as such
110131
return 0x1
111132

112133

113134
def get_partition_type(ptype):
114-
if ptype == "app":
135+
if ptype == 'app':
115136
return APP_TYPE
116-
if ptype == "data":
137+
if ptype == 'data':
117138
return DATA_TYPE
118-
raise InputError("Invalid partition type")
139+
if ptype == 'bootloader':
140+
return BOOTLOADER_TYPE
141+
if ptype == 'partition_table':
142+
return PARTITION_TABLE_TYPE
143+
raise InputError('Invalid partition type')
119144

120145

121146
def add_extra_subtypes(csv):
122147
for line_no in csv:
123148
try:
124-
fields = [line.strip() for line in line_no.split(",")]
149+
fields = [line.strip() for line in line_no.split(',')]
125150
for subtype, subtype_values in SUBTYPES.items():
126151
if int(fields[2], 16) in subtype_values.values() and subtype == get_partition_type(fields[0]):
127-
raise ValueError("Found duplicate value in partition subtype")
152+
raise ValueError('Found duplicate value in partition subtype')
128153
SUBTYPES[TYPES[fields[0]]][fields[1]] = int(fields[2], 16)
129154
except InputError as err:
130-
raise InputError("Error parsing custom subtypes: %s" % err)
155+
raise InputError('Error parsing custom subtypes: %s' % err)
131156

132157

133158
quiet = False
134159
md5sum = True
135160
secure = SECURE_NONE
136161
offset_part_table = 0
162+
primary_bootloader_offset = None
163+
recovery_bootloader_offset = None
137164

138165

139166
def status(msg):
@@ -145,7 +172,7 @@ def status(msg):
145172
def critical(msg):
146173
"""Print critical message to stderr"""
147174
sys.stderr.write(msg)
148-
sys.stderr.write("\n")
175+
sys.stderr.write('\n')
149176

150177

151178
class PartitionTable(list):
@@ -157,54 +184,59 @@ def from_file(cls, f):
157184
data = f.read()
158185
data_is_binary = data[0:2] == PartitionDefinition.MAGIC_BYTES
159186
if data_is_binary:
160-
status("Parsing binary partition input...")
187+
status('Parsing binary partition input...')
161188
return cls.from_binary(data), True
162189

163190
data = data.decode()
164-
status("Parsing CSV input...")
191+
status('Parsing CSV input...')
165192
return cls.from_csv(data), False
166193

167194
@classmethod
168-
def from_csv(cls, csv_contents): # noqa: C901
195+
def from_csv(cls, csv_contents):
169196
res = PartitionTable()
170197
lines = csv_contents.splitlines()
171198

172199
def expand_vars(f):
173200
f = os.path.expandvars(f)
174-
m = re.match(r"(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)", f)
201+
m = re.match(r'(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)', f)
175202
if m:
176203
raise InputError("unknown variable '%s'" % m.group(1))
177204
return f
178205

179206
for line_no in range(len(lines)):
180207
line = expand_vars(lines[line_no]).strip()
181-
if line.startswith("#") or len(line) == 0:
208+
if line.startswith('#') or len(line) == 0:
182209
continue
183210
try:
184211
res.append(PartitionDefinition.from_csv(line, line_no + 1))
185212
except InputError as err:
186213
raise InputError(
187-
"Error at line %d: %s\nPlease check extra_partition_subtypes.inc file in build/config directory"
214+
'Error at line %d: %s\nPlease check extra_partition_subtypes.inc file in build/config directory'
188215
% (line_no + 1, err)
189216
)
190217
except Exception:
191-
critical("Unexpected error parsing CSV line %d: %s" % (line_no + 1, line))
218+
critical('Unexpected error parsing CSV line %d: %s' % (line_no + 1, line))
192219
raise
193220

194221
# fix up missing offsets & negative sizes
195222
last_end = offset_part_table + PARTITION_TABLE_SIZE # first offset after partition table
196223
for e in res:
224+
is_primary_bootloader = e.type == BOOTLOADER_TYPE and e.subtype == SUBTYPES[e.type]['primary']
225+
is_primary_partition_table = e.type == PARTITION_TABLE_TYPE and e.subtype == SUBTYPES[e.type]['primary']
226+
if is_primary_bootloader or is_primary_partition_table:
227+
# They do not participate in the restoration of missing offsets
228+
continue
197229
if e.offset is not None and e.offset < last_end:
198230
if e == res[0]:
199231
raise InputError(
200-
"CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. "
201-
"But partition table occupies the whole sector 0x%x. "
202-
"Use a free offset 0x%x or higher." % (e.line_no, e.offset, offset_part_table, last_end)
232+
'CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. '
233+
'But partition table occupies the whole sector 0x%x. '
234+
'Use a free offset 0x%x or higher.' % (e.line_no, e.offset, offset_part_table, last_end)
203235
)
204236
else:
205237
raise InputError(
206-
"CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. Previous partition ends 0x%x" # noqa: E501
207-
% (e.line_no, e.offset, last_end)
238+
'CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. '
239+
'Previous partition ends 0x%x' % (e.line_no, e.offset, last_end)
208240
)
209241
if e.offset is None:
210242
pad_to = get_alignment_offset_for_type(e.type)
@@ -246,49 +278,70 @@ def find_by_name(self, name):
246278
return p
247279
return None
248280

249-
def verify(self): # noqa: C901
281+
def verify(self):
250282
# verify each partition individually
251283
for p in self:
252284
p.verify()
253285

254286
# check on duplicate name
255287
names = [p.name for p in self]
256-
duplicates = set(n for n in names if names.count(n) > 1) # noqa: C401
288+
duplicates = set(n for n in names if names.count(n) > 1)
257289

258290
# print sorted duplicate partitions by name
259291
if len(duplicates) != 0:
260-
critical("A list of partitions that have the same name:")
292+
critical('A list of partitions that have the same name:')
261293
for p in sorted(self, key=lambda x: x.name):
262294
if len(duplicates.intersection([p.name])) != 0:
263-
critical("%s" % (p.to_csv()))
264-
raise InputError("Partition names must be unique")
295+
critical('%s' % (p.to_csv()))
296+
raise InputError('Partition names must be unique')
265297

266298
# check for overlaps
267299
last = None
268300
for p in sorted(self, key=lambda x: x.offset):
269301
if p.offset < offset_part_table + PARTITION_TABLE_SIZE:
270-
raise InputError(
271-
"Partition offset 0x%x is below 0x%x" % (p.offset, offset_part_table + PARTITION_TABLE_SIZE)
272-
)
302+
is_primary_bootloader = p.type == BOOTLOADER_TYPE and p.subtype == SUBTYPES[p.type]['primary']
303+
is_primary_partition_table = p.type == PARTITION_TABLE_TYPE and p.subtype == SUBTYPES[p.type]['primary']
304+
if not (is_primary_bootloader or is_primary_partition_table):
305+
raise InputError(
306+
'Partition offset 0x%x is below 0x%x' % (p.offset, offset_part_table + PARTITION_TABLE_SIZE)
307+
)
273308
if last is not None and p.offset < last.offset + last.size:
274309
raise InputError(
275-
"Partition at 0x%x overlaps 0x%x-0x%x" % (p.offset, last.offset, last.offset + last.size - 1)
310+
'Partition at 0x%x overlaps 0x%x-0x%x' % (p.offset, last.offset, last.offset + last.size - 1)
276311
)
277312
last = p
278313

279314
# check that otadata should be unique
280-
otadata_duplicates = [p for p in self if p.type == TYPES["data"] and p.subtype == SUBTYPES[DATA_TYPE]["ota"]]
315+
otadata_duplicates = [p for p in self if p.type == TYPES['data'] and p.subtype == SUBTYPES[DATA_TYPE]['ota']]
281316
if len(otadata_duplicates) > 1:
282317
for p in otadata_duplicates:
283-
critical("%s" % (p.to_csv()))
318+
critical('%s' % (p.to_csv()))
284319
raise InputError(
285-
'Found multiple otadata partitions. Only one partition can be defined with type="data"(1) and subtype="ota"(0).' # noqa: E501
320+
'Found multiple otadata partitions. Only one partition can be defined with '
321+
'type="data"(1) and subtype="ota"(0).'
286322
)
287323

288324
if len(otadata_duplicates) == 1 and otadata_duplicates[0].size != 0x2000:
289325
p = otadata_duplicates[0]
290-
critical("%s" % (p.to_csv()))
291-
raise InputError("otadata partition must have size = 0x2000")
326+
critical('%s' % (p.to_csv()))
327+
raise InputError('otadata partition must have size = 0x2000')
328+
329+
# Above checks but for TEE otadata
330+
otadata_duplicates = [
331+
p for p in self if p.type == TYPES['data'] and p.subtype == SUBTYPES[DATA_TYPE]['tee_ota']
332+
]
333+
if len(otadata_duplicates) > 1:
334+
for p in otadata_duplicates:
335+
critical('%s' % (p.to_csv()))
336+
raise InputError(
337+
'Found multiple TEE otadata partitions. Only one partition can be defined with '
338+
'type="data"(1) and subtype="tee_ota"(0x90).'
339+
)
340+
341+
if len(otadata_duplicates) == 1 and otadata_duplicates[0].size != 0x2000:
342+
p = otadata_duplicates[0]
343+
critical('%s' % (p.to_csv()))
344+
raise InputError('TEE otadata partition must have size = 0x2000')
292345

293346
def flash_size(self):
294347
"""Return the size that partitions will occupy in flash
@@ -308,7 +361,7 @@ def verify_size_fits(self, flash_size_bytes: int) -> None:
308361
if flash_size_bytes < table_size:
309362
mb = 1024 * 1024
310363
raise InputError(
311-
"Partitions tables occupies %.1fMB of flash (%d bytes) which does not fit in configured "
364+
'Partitions tables occupies %.1fMB of flash (%d bytes) which does not fit in configured '
312365
"flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu."
313366
% (table_size / mb, table_size, flash_size_bytes / mb)
314367
)
@@ -320,8 +373,8 @@ def from_binary(cls, b):
320373
for o in range(0, len(b), 32):
321374
data = b[o : o + 32]
322375
if len(data) != 32:
323-
raise InputError("Partition table length must be a multiple of 32 bytes")
324-
if data == b"\xFF" * 32:
376+
raise InputError('Partition table length must be a multiple of 32 bytes')
377+
if data == b'\xff' * 32:
325378
return result # got end marker
326379
if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]: # check only the magic number part
327380
if data[16:] == md5.digest():
@@ -334,59 +387,64 @@ def from_binary(cls, b):
334387
else:
335388
md5.update(data)
336389
result.append(PartitionDefinition.from_binary(data))
337-
raise InputError("Partition table is missing an end-of-table marker")
390+
raise InputError('Partition table is missing an end-of-table marker')
338391

339392
def to_binary(self):
340-
result = b"".join(e.to_binary() for e in self)
393+
result = b''.join(e.to_binary() for e in self)
341394
if md5sum:
342395
result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest()
343396
if len(result) >= MAX_PARTITION_LENGTH:
344-
raise InputError("Binary partition table length (%d) longer than max" % len(result))
345-
result += b"\xFF" * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing
397+
raise InputError('Binary partition table length (%d) longer than max' % len(result))
398+
result += b'\xff' * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing
346399
return result
347400

348401
def to_csv(self, simple_formatting=False):
349-
rows = ["# ESP-IDF Partition Table", "# Name, Type, SubType, Offset, Size, Flags"]
402+
rows = ['# ESP-IDF Partition Table', '# Name, Type, SubType, Offset, Size, Flags']
350403
rows += [x.to_csv(simple_formatting) for x in self]
351-
return "\n".join(rows) + "\n"
404+
return '\n'.join(rows) + '\n'
352405

353406

354407
class PartitionDefinition(object):
355-
MAGIC_BYTES = b"\xAA\x50"
408+
MAGIC_BYTES = b'\xaa\x50'
356409

357410
# dictionary maps flag name (as used in CSV flags list, property name)
358411
# to bit set in flags words in binary format
359-
FLAGS = {"encrypted": 0}
412+
FLAGS = {'encrypted': 0, 'readonly': 1}
360413

361414
# add subtypes for the 16 OTA slot values ("ota_XX, etc.")
362415
for ota_slot in range(NUM_PARTITION_SUBTYPE_APP_OTA):
363-
SUBTYPES[TYPES["app"]]["ota_%d" % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot
416+
SUBTYPES[TYPES['app']]['ota_%d' % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot
417+
418+
# add subtypes for the 2 TEE OTA slot values ("tee_XX, etc.")
419+
for tee_slot in range(NUM_PARTITION_SUBTYPE_APP_TEE):
420+
SUBTYPES[TYPES['app']]['tee_%d' % tee_slot] = MIN_PARTITION_SUBTYPE_APP_TEE + tee_slot
364421

365422
def __init__(self):
366-
self.name = ""
423+
self.name = ''
367424
self.type = None
368425
self.subtype = None
369426
self.offset = None
370427
self.size = None
371428
self.encrypted = False
429+
self.readonly = False
372430

373431
@classmethod
374432
def from_csv(cls, line, line_no):
375433
"""Parse a line from the CSV"""
376-
line_w_defaults = line + ",,,," # lazy way to support default fields
377-
fields = [f.strip() for f in line_w_defaults.split(",")]
434+
line_w_defaults = line + ',,,,' # lazy way to support default fields
435+
fields = [f.strip() for f in line_w_defaults.split(',')]
378436

379437
res = PartitionDefinition()
380438
res.line_no = line_no
381439
res.name = fields[0]
382440
res.type = res.parse_type(fields[1])
383441
res.subtype = res.parse_subtype(fields[2])
384-
res.offset = res.parse_address(fields[3])
385-
res.size = res.parse_address(fields[4])
442+
res.offset = res.parse_address(fields[3], res.type, res.subtype)
443+
res.size = res.parse_size(fields[4], res.type)
386444
if res.size is None:
387445
raise InputError("Size field can't be empty")
388446

389-
flags = fields[5].split(":")
447+
flags = fields[5].split(':')
390448
for flag in flags:
391449
if flag in cls.FLAGS:
392450
setattr(res, flag, True)
@@ -406,7 +464,7 @@ def __eq__(self, other):
406464

407465
def __repr__(self):
408466
def maybe_hex(x):
409-
return "0x%x" % x if x is not None else "None"
467+
return '0x%x' % x if x is not None else 'None'
410468

411469
return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (
412470
self.name,
@@ -441,73 +499,114 @@ def __ge__(self, other):
441499
return self.offset >= other.offset
442500

443501
def parse_type(self, strval):
444-
if strval == "":
502+
if strval == '':
445503
raise InputError("Field 'type' can't be left empty.")
446504
return parse_int(strval, TYPES)
447505

448506
def parse_subtype(self, strval):
449-
if strval == "":
450-
if self.type == TYPES["app"]:
451-
raise InputError("App partition cannot have an empty subtype")
452-
return SUBTYPES[DATA_TYPE]["undefined"]
507+
if strval == '':
508+
if self.type == TYPES['app']:
509+
raise InputError('App partition cannot have an empty subtype')
510+
return SUBTYPES[DATA_TYPE]['undefined']
453511
return parse_int(strval, SUBTYPES.get(self.type, {}))
454512

455-
def parse_address(self, strval):
456-
if strval == "":
513+
def parse_size(self, strval, ptype):
514+
if ptype == BOOTLOADER_TYPE:
515+
if primary_bootloader_offset is None:
516+
raise InputError('Primary bootloader offset is not defined. Please use --primary-bootloader-offset')
517+
return offset_part_table - primary_bootloader_offset
518+
if ptype == PARTITION_TABLE_TYPE:
519+
return PARTITION_TABLE_SIZE
520+
if strval == '':
457521
return None # PartitionTable will fill in default
458522
return parse_int(strval)
459523

460-
def verify(self): # noqa: C901
524+
def parse_address(self, strval, ptype, psubtype):
525+
if ptype == BOOTLOADER_TYPE:
526+
if psubtype == SUBTYPES[ptype]['primary']:
527+
if primary_bootloader_offset is None:
528+
raise InputError('Primary bootloader offset is not defined. Please use --primary-bootloader-offset')
529+
return primary_bootloader_offset
530+
if psubtype == SUBTYPES[ptype]['recovery']:
531+
if recovery_bootloader_offset is None:
532+
raise InputError(
533+
'Recovery bootloader offset is not defined. Please use --recovery-bootloader-offset'
534+
)
535+
return recovery_bootloader_offset
536+
if ptype == PARTITION_TABLE_TYPE and psubtype == SUBTYPES[ptype]['primary']:
537+
return offset_part_table
538+
if strval == '':
539+
return None # PartitionTable will fill in default
540+
return parse_int(strval)
541+
542+
def verify(self):
461543
if self.type is None:
462-
raise ValidationError(self, "Type field is not set")
544+
raise ValidationError(self, 'Type field is not set')
463545
if self.subtype is None:
464-
raise ValidationError(self, "Subtype field is not set")
546+
raise ValidationError(self, 'Subtype field is not set')
465547
if self.offset is None:
466-
raise ValidationError(self, "Offset field is not set")
548+
raise ValidationError(self, 'Offset field is not set')
467549
if self.size is None:
468-
raise ValidationError(self, "Size field is not set")
550+
raise ValidationError(self, 'Size field is not set')
469551
offset_align = get_alignment_offset_for_type(self.type)
470552
if self.offset % offset_align:
471-
raise ValidationError(self, "Offset 0x%x is not aligned to 0x%x" % (self.offset, offset_align))
472-
if self.type == APP_TYPE and secure is not SECURE_NONE:
553+
raise ValidationError(self, 'Offset 0x%x is not aligned to 0x%x' % (self.offset, offset_align))
554+
if self.type == APP_TYPE:
473555
size_align = get_alignment_size_for_type(self.type)
474556
if self.size % size_align:
475-
raise ValidationError(self, "Size 0x%x is not aligned to 0x%x" % (self.size, size_align))
557+
raise ValidationError(self, 'Size 0x%x is not aligned to 0x%x' % (self.size, size_align))
476558

477-
if self.name in TYPES and TYPES.get(self.name, "") != self.type:
559+
if self.name in TYPES and TYPES.get(self.name, '') != self.type:
478560
critical(
479561
"WARNING: Partition has name '%s' which is a partition type, but does not match this partition's "
480-
"type (0x%x). Mistake in partition table?" % (self.name, self.type)
562+
'type (0x%x). Mistake in partition table?' % (self.name, self.type)
481563
)
482564
all_subtype_names = []
483565
for names in (t.keys() for t in SUBTYPES.values()):
484566
all_subtype_names += names
485-
if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, "") != self.subtype:
567+
if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, '') != self.subtype:
486568
critical(
487569
"WARNING: Partition has name '%s' which is a partition subtype, but this partition has "
488-
"non-matching type 0x%x and subtype 0x%x. Mistake in partition table?"
570+
'non-matching type 0x%x and subtype 0x%x. Mistake in partition table?'
489571
% (self.name, self.type, self.subtype)
490572
)
491573

492-
STRUCT_FORMAT = b"<2sBBLL16sL"
574+
always_rw_data_subtypes = [SUBTYPES[DATA_TYPE]['ota'], SUBTYPES[DATA_TYPE]['coredump']]
575+
if self.type == TYPES['data'] and self.subtype in always_rw_data_subtypes and self.readonly is True:
576+
raise ValidationError(
577+
self,
578+
"'%s' partition of type %s and subtype %s is always read-write and cannot be read-only"
579+
% (self.name, self.type, self.subtype),
580+
)
581+
582+
if self.type == TYPES['data'] and self.subtype == SUBTYPES[DATA_TYPE]['nvs']:
583+
if self.size < NVS_RW_MIN_PARTITION_SIZE and self.readonly is False:
584+
raise ValidationError(
585+
self,
586+
"""'%s' partition of type %s and subtype %s of this size (0x%x) must be flagged as 'readonly' \
587+
(the size of read/write NVS has to be at least 0x%x)"""
588+
% (self.name, self.type, self.subtype, self.size, NVS_RW_MIN_PARTITION_SIZE),
589+
)
590+
591+
STRUCT_FORMAT = b'<2sBBLL16sL'
493592

494593
@classmethod
495594
def from_binary(cls, b):
496595
if len(b) != 32:
497-
raise InputError("Partition definition length must be exactly 32 bytes. Got %d bytes." % len(b))
596+
raise InputError('Partition definition length must be exactly 32 bytes. Got %d bytes.' % len(b))
498597
res = cls()
499598
(magic, res.type, res.subtype, res.offset, res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b)
500-
if b"\x00" in res.name: # strip null byte padding from name string
501-
res.name = res.name[: res.name.index(b"\x00")]
599+
if b'\x00' in res.name: # strip null byte padding from name string
600+
res.name = res.name[: res.name.index(b'\x00')]
502601
res.name = res.name.decode()
503602
if magic != cls.MAGIC_BYTES:
504-
raise InputError("Invalid magic bytes (%r) for partition definition" % magic)
603+
raise InputError('Invalid magic bytes (%r) for partition definition' % magic)
505604
for flag, bit in cls.FLAGS.items():
506605
if flags & (1 << bit):
507606
setattr(res, flag, True)
508607
flags &= ~(1 << bit)
509608
if flags != 0:
510-
critical("WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?" % flags)
609+
critical('WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?' % flags)
511610
return res
512611

513612
def get_flags_list(self):
@@ -529,22 +628,22 @@ def to_binary(self):
529628
def to_csv(self, simple_formatting=False):
530629
def addr_format(a, include_sizes):
531630
if not simple_formatting and include_sizes:
532-
for val, suffix in [(0x100000, "M"), (0x400, "K")]:
631+
for val, suffix in [(0x100000, 'M'), (0x400, 'K')]:
533632
if a % val == 0:
534-
return "%d%s" % (a // val, suffix)
535-
return "0x%x" % a
633+
return '%d%s' % (a // val, suffix)
634+
return '0x%x' % a
536635

537636
def lookup_keyword(t, keywords):
538637
for k, v in keywords.items():
539638
if simple_formatting is False and t == v:
540639
return k
541-
return "%d" % t
640+
return '%d' % t
542641

543642
def generate_text_flags():
544643
"""colon-delimited list of flags"""
545-
return ":".join(self.get_flags_list())
644+
return ':'.join(self.get_flags_list())
546645

547-
return ",".join(
646+
return ','.join(
548647
[
549648
self.name,
550649
lookup_keyword(self.type, TYPES),
@@ -561,59 +660,63 @@ def parse_int(v, keywords={}):
561660
k/m/K/M suffixes and 'keyword' value lookup.
562661
"""
563662
try:
564-
for letter, multiplier in [("k", 1024), ("m", 1024 * 1024)]:
663+
for letter, multiplier in [('k', 1024), ('m', 1024 * 1024)]:
565664
if v.lower().endswith(letter):
566665
return parse_int(v[:-1], keywords) * multiplier
567666
return int(v, 0)
568667
except ValueError:
569668
if len(keywords) == 0:
570-
raise InputError("Invalid field value %s" % v)
669+
raise InputError('Invalid field value %s' % v)
571670
try:
572671
return keywords[v.lower()]
573672
except KeyError:
574-
raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ", ".join(keywords)))
673+
raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ', '.join(keywords)))
575674

576675

577-
def main(): # noqa: C901
676+
def main():
578677
global quiet
579678
global md5sum
580679
global offset_part_table
581680
global secure
582-
parser = argparse.ArgumentParser(description="ESP32 partition table utility")
681+
global primary_bootloader_offset
682+
global recovery_bootloader_offset
683+
parser = argparse.ArgumentParser(description='ESP32 partition table utility')
583684

584685
parser.add_argument(
585-
"--flash-size",
586-
help="Optional flash size limit, checks partition table fits in flash",
587-
nargs="?",
588-
choices=["1MB", "2MB", "4MB", "8MB", "16MB", "32MB", "64MB", "128MB"],
686+
'--flash-size',
687+
help='Optional flash size limit, checks partition table fits in flash',
688+
nargs='?',
689+
choices=['1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB'],
589690
)
590691
parser.add_argument(
591-
"--disable-md5sum", help="Disable md5 checksum for the partition table", default=False, action="store_true"
692+
'--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true'
592693
)
593-
parser.add_argument("--no-verify", help="Don't verify partition table fields", action="store_true")
694+
parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true')
594695
parser.add_argument(
595-
"--verify",
596-
"-v",
597-
help="Verify partition table fields (deprecated, this behavior is "
598-
"enabled by default and this flag does nothing.",
599-
action="store_true",
696+
'--verify',
697+
'-v',
698+
help='Verify partition table fields (deprecated, this behaviour is '
699+
'enabled by default and this flag does nothing.',
700+
action='store_true',
600701
)
601-
parser.add_argument("--quiet", "-q", help="Don't print non-critical status messages to stderr", action="store_true")
602-
parser.add_argument("--offset", "-o", help="Set offset partition table", default="0x8000")
702+
parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true')
703+
parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000')
704+
parser.add_argument('--primary-bootloader-offset', help='Set primary bootloader offset', default=None)
705+
parser.add_argument('--recovery-bootloader-offset', help='Set recovery bootloader offset', default=None)
603706
parser.add_argument(
604-
"--secure",
605-
help="Require app partitions to be suitable for secure boot",
606-
nargs="?",
707+
'--secure',
708+
help='Require app partitions to be suitable for secure boot',
709+
nargs='?',
607710
const=SECURE_V1,
608711
choices=[SECURE_V1, SECURE_V2],
609712
)
610-
parser.add_argument("--extra-partition-subtypes", help="Extra partition subtype entries", nargs="*")
611-
parser.add_argument("input", help="Path to CSV or binary file to parse.", type=argparse.FileType("rb"))
713+
parser.add_argument('--extra-partition-subtypes', help='Extra partition subtype entries', nargs='*')
714+
parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb'))
612715
parser.add_argument(
613-
"output",
614-
help="Path to output converted binary or CSV file. Will use stdout if omitted.",
615-
nargs="?",
616-
default="-",
716+
'output',
717+
help='Path to output converted binary or CSV file. Will use stdout if omitted.',
718+
nargs='?',
719+
default='-',
617720
)
618721

619722
args = parser.parse_args()
@@ -622,17 +725,26 @@ def main(): # noqa: C901
622725
md5sum = not args.disable_md5sum
623726
secure = args.secure
624727
offset_part_table = int(args.offset, 0)
728+
if args.primary_bootloader_offset is not None:
729+
primary_bootloader_offset = int(args.primary_bootloader_offset, 0)
730+
if primary_bootloader_offset >= offset_part_table:
731+
raise InputError(
732+
f'Unsupported configuration. Primary bootloader must be below partition table. '
733+
f'Check --primary-bootloader-offset={primary_bootloader_offset:#x} and --offset={offset_part_table:#x}'
734+
)
735+
if args.recovery_bootloader_offset is not None:
736+
recovery_bootloader_offset = int(args.recovery_bootloader_offset, 0)
625737
if args.extra_partition_subtypes:
626738
add_extra_subtypes(args.extra_partition_subtypes)
627739

628740
table, input_is_binary = PartitionTable.from_file(args.input)
629741

630742
if not args.no_verify:
631-
status("Verifying table...")
743+
status('Verifying table...')
632744
table.verify()
633745

634746
if args.flash_size:
635-
size_mb = int(args.flash_size.replace("MB", ""))
747+
size_mb = int(args.flash_size.replace('MB', ''))
636748
table.verify_size_fits(size_mb * 1024 * 1024)
637749

638750
# Make sure that the output directory is created
@@ -647,15 +759,15 @@ def main(): # noqa: C901
647759

648760
if input_is_binary:
649761
output = table.to_csv()
650-
with sys.stdout if args.output == "-" else open(args.output, "w") as f:
762+
with sys.stdout if args.output == '-' else open(args.output, 'w', encoding='utf-8') as f:
651763
f.write(output)
652764
else:
653765
output = table.to_binary()
654766
try:
655767
stdout_binary = sys.stdout.buffer # Python 3
656768
except AttributeError:
657769
stdout_binary = sys.stdout
658-
with stdout_binary if args.output == "-" else open(args.output, "wb") as f:
770+
with stdout_binary if args.output == '-' else open(args.output, 'wb') as f:
659771
f.write(output)
660772

661773

@@ -666,10 +778,10 @@ def __init__(self, e):
666778

667779
class ValidationError(InputError):
668780
def __init__(self, partition, message):
669-
super(ValidationError, self).__init__("Partition %s invalid: %s" % (partition.name, message))
781+
super(ValidationError, self).__init__('Partition %s invalid: %s' % (partition.name, message))
670782

671783

672-
if __name__ == "__main__":
784+
if __name__ == '__main__':
673785
try:
674786
main()
675787
except InputError as e:

0 commit comments

Comments
 (0)
Please sign in to comment.