7
7
# See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html
8
8
# for explanation of partition table structure and uses.
9
9
#
10
- # SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD
10
+ # SPDX-FileCopyrightText: 2016-2025 Espressif Systems (Shanghai) CO LTD
11
11
# SPDX-License-Identifier: Apache-2.0
12
-
13
- from __future__ import division , print_function , unicode_literals
14
-
15
12
import argparse
16
13
import binascii
17
14
import errno
22
19
import sys
23
20
24
21
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
26
23
PARTITION_TABLE_SIZE = 0x1000 # Size of partition table
27
24
28
25
MIN_PARTITION_SUBTYPE_APP_OTA = 0x10
29
26
NUM_PARTITION_SUBTYPE_APP_OTA = 16
27
+ MIN_PARTITION_SUBTYPE_APP_TEE = 0x30
28
+ NUM_PARTITION_SUBTYPE_APP_TEE = 2
30
29
31
30
SECURE_NONE = None
32
- SECURE_V1 = "v1"
33
- SECURE_V2 = "v2"
31
+ SECURE_V1 = 'v1'
32
+ SECURE_V2 = 'v2'
34
33
35
- __version__ = "1.2"
34
+ __version__ = '1.5'
36
35
37
36
APP_TYPE = 0x00
38
37
DATA_TYPE = 0x01
38
+ BOOTLOADER_TYPE = 0x02
39
+ PARTITION_TABLE_TYPE = 0x03
39
40
40
41
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 ,
43
46
}
44
47
48
+ NVS_RW_MIN_PARTITION_SIZE = 0x3000
49
+
45
50
46
51
def get_ptype_as_int (ptype ):
47
52
"""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):
56
61
57
62
# Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h
58
63
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
+ },
59
73
APP_TYPE : {
60
- " factory" : 0x00 ,
61
- " test" : 0x20 ,
74
+ ' factory' : 0x00 ,
75
+ ' test' : 0x20 ,
62
76
},
63
77
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 ,
75
90
},
76
91
}
77
92
@@ -90,6 +105,8 @@ def get_subtype_as_int(ptype, subtype):
90
105
ALIGNMENT = {
91
106
APP_TYPE : 0x10000 ,
92
107
DATA_TYPE : 0x1000 ,
108
+ BOOTLOADER_TYPE : 0x1000 ,
109
+ PARTITION_TABLE_TYPE : 0x1000 ,
93
110
}
94
111
95
112
@@ -98,42 +115,52 @@ def get_alignment_offset_for_type(ptype):
98
115
99
116
100
117
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
109
130
# No specific size alignment requirement as such
110
131
return 0x1
111
132
112
133
113
134
def get_partition_type (ptype ):
114
- if ptype == " app" :
135
+ if ptype == ' app' :
115
136
return APP_TYPE
116
- if ptype == " data" :
137
+ if ptype == ' data' :
117
138
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' )
119
144
120
145
121
146
def add_extra_subtypes (csv ):
122
147
for line_no in csv :
123
148
try :
124
- fields = [line .strip () for line in line_no .split ("," )]
149
+ fields = [line .strip () for line in line_no .split (',' )]
125
150
for subtype , subtype_values in SUBTYPES .items ():
126
151
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' )
128
153
SUBTYPES [TYPES [fields [0 ]]][fields [1 ]] = int (fields [2 ], 16 )
129
154
except InputError as err :
130
- raise InputError (" Error parsing custom subtypes: %s" % err )
155
+ raise InputError (' Error parsing custom subtypes: %s' % err )
131
156
132
157
133
158
quiet = False
134
159
md5sum = True
135
160
secure = SECURE_NONE
136
161
offset_part_table = 0
162
+ primary_bootloader_offset = None
163
+ recovery_bootloader_offset = None
137
164
138
165
139
166
def status (msg ):
@@ -145,7 +172,7 @@ def status(msg):
145
172
def critical (msg ):
146
173
"""Print critical message to stderr"""
147
174
sys .stderr .write (msg )
148
- sys .stderr .write (" \n " )
175
+ sys .stderr .write (' \n ' )
149
176
150
177
151
178
class PartitionTable (list ):
@@ -157,54 +184,59 @@ def from_file(cls, f):
157
184
data = f .read ()
158
185
data_is_binary = data [0 :2 ] == PartitionDefinition .MAGIC_BYTES
159
186
if data_is_binary :
160
- status (" Parsing binary partition input..." )
187
+ status (' Parsing binary partition input...' )
161
188
return cls .from_binary (data ), True
162
189
163
190
data = data .decode ()
164
- status (" Parsing CSV input..." )
191
+ status (' Parsing CSV input...' )
165
192
return cls .from_csv (data ), False
166
193
167
194
@classmethod
168
- def from_csv (cls , csv_contents ): # noqa: C901
195
+ def from_csv (cls , csv_contents ):
169
196
res = PartitionTable ()
170
197
lines = csv_contents .splitlines ()
171
198
172
199
def expand_vars (f ):
173
200
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 )
175
202
if m :
176
203
raise InputError ("unknown variable '%s'" % m .group (1 ))
177
204
return f
178
205
179
206
for line_no in range (len (lines )):
180
207
line = expand_vars (lines [line_no ]).strip ()
181
- if line .startswith ("#" ) or len (line ) == 0 :
208
+ if line .startswith ('#' ) or len (line ) == 0 :
182
209
continue
183
210
try :
184
211
res .append (PartitionDefinition .from_csv (line , line_no + 1 ))
185
212
except InputError as err :
186
213
raise InputError (
187
- " Error at line %d: %s\n Please check extra_partition_subtypes.inc file in build/config directory"
214
+ ' Error at line %d: %s\n Please check extra_partition_subtypes.inc file in build/config directory'
188
215
% (line_no + 1 , err )
189
216
)
190
217
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 ))
192
219
raise
193
220
194
221
# fix up missing offsets & negative sizes
195
222
last_end = offset_part_table + PARTITION_TABLE_SIZE # first offset after partition table
196
223
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
197
229
if e .offset is not None and e .offset < last_end :
198
230
if e == res [0 ]:
199
231
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 )
203
235
)
204
236
else :
205
237
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 )
208
240
)
209
241
if e .offset is None :
210
242
pad_to = get_alignment_offset_for_type (e .type )
@@ -246,49 +278,70 @@ def find_by_name(self, name):
246
278
return p
247
279
return None
248
280
249
- def verify (self ): # noqa: C901
281
+ def verify (self ):
250
282
# verify each partition individually
251
283
for p in self :
252
284
p .verify ()
253
285
254
286
# check on duplicate name
255
287
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 )
257
289
258
290
# print sorted duplicate partitions by name
259
291
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:' )
261
293
for p in sorted (self , key = lambda x : x .name ):
262
294
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' )
265
297
266
298
# check for overlaps
267
299
last = None
268
300
for p in sorted (self , key = lambda x : x .offset ):
269
301
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
+ )
273
308
if last is not None and p .offset < last .offset + last .size :
274
309
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 )
276
311
)
277
312
last = p
278
313
279
314
# 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' ]]
281
316
if len (otadata_duplicates ) > 1 :
282
317
for p in otadata_duplicates :
283
- critical ("%s" % (p .to_csv ()))
318
+ critical ('%s' % (p .to_csv ()))
284
319
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).'
286
322
)
287
323
288
324
if len (otadata_duplicates ) == 1 and otadata_duplicates [0 ].size != 0x2000 :
289
325
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' )
292
345
293
346
def flash_size (self ):
294
347
"""Return the size that partitions will occupy in flash
@@ -308,7 +361,7 @@ def verify_size_fits(self, flash_size_bytes: int) -> None:
308
361
if flash_size_bytes < table_size :
309
362
mb = 1024 * 1024
310
363
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 '
312
365
"flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu."
313
366
% (table_size / mb , table_size , flash_size_bytes / mb )
314
367
)
@@ -320,8 +373,8 @@ def from_binary(cls, b):
320
373
for o in range (0 , len (b ), 32 ):
321
374
data = b [o : o + 32 ]
322
375
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 :
325
378
return result # got end marker
326
379
if md5sum and data [:2 ] == MD5_PARTITION_BEGIN [:2 ]: # check only the magic number part
327
380
if data [16 :] == md5 .digest ():
@@ -334,59 +387,64 @@ def from_binary(cls, b):
334
387
else :
335
388
md5 .update (data )
336
389
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' )
338
391
339
392
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 )
341
394
if md5sum :
342
395
result += MD5_PARTITION_BEGIN + hashlib .md5 (result ).digest ()
343
396
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
346
399
return result
347
400
348
401
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' ]
350
403
rows += [x .to_csv (simple_formatting ) for x in self ]
351
- return " \n " .join (rows ) + " \n "
404
+ return ' \n ' .join (rows ) + ' \n '
352
405
353
406
354
407
class PartitionDefinition (object ):
355
- MAGIC_BYTES = b" \xAA \x50 "
408
+ MAGIC_BYTES = b' \xaa \x50 '
356
409
357
410
# dictionary maps flag name (as used in CSV flags list, property name)
358
411
# to bit set in flags words in binary format
359
- FLAGS = {" encrypted" : 0 }
412
+ FLAGS = {' encrypted' : 0 , 'readonly' : 1 }
360
413
361
414
# add subtypes for the 16 OTA slot values ("ota_XX, etc.")
362
415
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
364
421
365
422
def __init__ (self ):
366
- self .name = ""
423
+ self .name = ''
367
424
self .type = None
368
425
self .subtype = None
369
426
self .offset = None
370
427
self .size = None
371
428
self .encrypted = False
429
+ self .readonly = False
372
430
373
431
@classmethod
374
432
def from_csv (cls , line , line_no ):
375
433
"""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 (',' )]
378
436
379
437
res = PartitionDefinition ()
380
438
res .line_no = line_no
381
439
res .name = fields [0 ]
382
440
res .type = res .parse_type (fields [1 ])
383
441
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 )
386
444
if res .size is None :
387
445
raise InputError ("Size field can't be empty" )
388
446
389
- flags = fields [5 ].split (":" )
447
+ flags = fields [5 ].split (':' )
390
448
for flag in flags :
391
449
if flag in cls .FLAGS :
392
450
setattr (res , flag , True )
@@ -406,7 +464,7 @@ def __eq__(self, other):
406
464
407
465
def __repr__ (self ):
408
466
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'
410
468
411
469
return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (
412
470
self .name ,
@@ -441,73 +499,114 @@ def __ge__(self, other):
441
499
return self .offset >= other .offset
442
500
443
501
def parse_type (self , strval ):
444
- if strval == "" :
502
+ if strval == '' :
445
503
raise InputError ("Field 'type' can't be left empty." )
446
504
return parse_int (strval , TYPES )
447
505
448
506
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' ]
453
511
return parse_int (strval , SUBTYPES .get (self .type , {}))
454
512
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 == '' :
457
521
return None # PartitionTable will fill in default
458
522
return parse_int (strval )
459
523
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 ):
461
543
if self .type is None :
462
- raise ValidationError (self , " Type field is not set" )
544
+ raise ValidationError (self , ' Type field is not set' )
463
545
if self .subtype is None :
464
- raise ValidationError (self , " Subtype field is not set" )
546
+ raise ValidationError (self , ' Subtype field is not set' )
465
547
if self .offset is None :
466
- raise ValidationError (self , " Offset field is not set" )
548
+ raise ValidationError (self , ' Offset field is not set' )
467
549
if self .size is None :
468
- raise ValidationError (self , " Size field is not set" )
550
+ raise ValidationError (self , ' Size field is not set' )
469
551
offset_align = get_alignment_offset_for_type (self .type )
470
552
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 :
473
555
size_align = get_alignment_size_for_type (self .type )
474
556
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 ))
476
558
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 :
478
560
critical (
479
561
"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 )
481
563
)
482
564
all_subtype_names = []
483
565
for names in (t .keys () for t in SUBTYPES .values ()):
484
566
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 :
486
568
critical (
487
569
"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?'
489
571
% (self .name , self .type , self .subtype )
490
572
)
491
573
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'
493
592
494
593
@classmethod
495
594
def from_binary (cls , b ):
496
595
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 ))
498
597
res = cls ()
499
598
(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 ' )]
502
601
res .name = res .name .decode ()
503
602
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 )
505
604
for flag , bit in cls .FLAGS .items ():
506
605
if flags & (1 << bit ):
507
606
setattr (res , flag , True )
508
607
flags &= ~ (1 << bit )
509
608
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 )
511
610
return res
512
611
513
612
def get_flags_list (self ):
@@ -529,22 +628,22 @@ def to_binary(self):
529
628
def to_csv (self , simple_formatting = False ):
530
629
def addr_format (a , include_sizes ):
531
630
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' )]:
533
632
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
536
635
537
636
def lookup_keyword (t , keywords ):
538
637
for k , v in keywords .items ():
539
638
if simple_formatting is False and t == v :
540
639
return k
541
- return "%d" % t
640
+ return '%d' % t
542
641
543
642
def generate_text_flags ():
544
643
"""colon-delimited list of flags"""
545
- return ":" .join (self .get_flags_list ())
644
+ return ':' .join (self .get_flags_list ())
546
645
547
- return "," .join (
646
+ return ',' .join (
548
647
[
549
648
self .name ,
550
649
lookup_keyword (self .type , TYPES ),
@@ -561,59 +660,63 @@ def parse_int(v, keywords={}):
561
660
k/m/K/M suffixes and 'keyword' value lookup.
562
661
"""
563
662
try :
564
- for letter , multiplier in [("k" , 1024 ), ("m" , 1024 * 1024 )]:
663
+ for letter , multiplier in [('k' , 1024 ), ('m' , 1024 * 1024 )]:
565
664
if v .lower ().endswith (letter ):
566
665
return parse_int (v [:- 1 ], keywords ) * multiplier
567
666
return int (v , 0 )
568
667
except ValueError :
569
668
if len (keywords ) == 0 :
570
- raise InputError (" Invalid field value %s" % v )
669
+ raise InputError (' Invalid field value %s' % v )
571
670
try :
572
671
return keywords [v .lower ()]
573
672
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 )))
575
674
576
675
577
- def main (): # noqa: C901
676
+ def main ():
578
677
global quiet
579
678
global md5sum
580
679
global offset_part_table
581
680
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' )
583
684
584
685
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' ],
589
690
)
590
691
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'
592
693
)
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' )
594
695
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' ,
600
701
)
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 )
603
706
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 = '?' ,
607
710
const = SECURE_V1 ,
608
711
choices = [SECURE_V1 , SECURE_V2 ],
609
712
)
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' ))
612
715
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 = '-' ,
617
720
)
618
721
619
722
args = parser .parse_args ()
@@ -622,17 +725,26 @@ def main(): # noqa: C901
622
725
md5sum = not args .disable_md5sum
623
726
secure = args .secure
624
727
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 )
625
737
if args .extra_partition_subtypes :
626
738
add_extra_subtypes (args .extra_partition_subtypes )
627
739
628
740
table , input_is_binary = PartitionTable .from_file (args .input )
629
741
630
742
if not args .no_verify :
631
- status (" Verifying table..." )
743
+ status (' Verifying table...' )
632
744
table .verify ()
633
745
634
746
if args .flash_size :
635
- size_mb = int (args .flash_size .replace ("MB" , "" ))
747
+ size_mb = int (args .flash_size .replace ('MB' , '' ))
636
748
table .verify_size_fits (size_mb * 1024 * 1024 )
637
749
638
750
# Make sure that the output directory is created
@@ -647,15 +759,15 @@ def main(): # noqa: C901
647
759
648
760
if input_is_binary :
649
761
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 :
651
763
f .write (output )
652
764
else :
653
765
output = table .to_binary ()
654
766
try :
655
767
stdout_binary = sys .stdout .buffer # Python 3
656
768
except AttributeError :
657
769
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 :
659
771
f .write (output )
660
772
661
773
@@ -666,10 +778,10 @@ def __init__(self, e):
666
778
667
779
class ValidationError (InputError ):
668
780
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 ))
670
782
671
783
672
- if __name__ == " __main__" :
784
+ if __name__ == ' __main__' :
673
785
try :
674
786
main ()
675
787
except InputError as e :
0 commit comments