Skip to content

Commit 4927e39

Browse files
michalek-nonordicjm
authored andcommitted
[nrf fromlist] scripts: imgtool: compression
Adds LZMA2 compression to imgtool. Python lzma library is unable to compress with proper parameters while using "ALONE" container, therefore 2 header bytes are calculated and added to payload by imgtool. Upstream PR: mcu-tools/mcuboot#2038 Signed-off-by: Mateusz Michalek <[email protected]>
1 parent 763edd6 commit 4927e39

File tree

2 files changed

+135
-15
lines changed

2 files changed

+135
-15
lines changed

scripts/imgtool/image.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
Image signing and management.
2121
"""
2222

23+
from . import version as versmod
24+
from .boot_record import create_sw_component_data
25+
import click
26+
import copy
27+
from enum import Enum
28+
import array
29+
from intelhex import IntelHex
2330
import hashlib
31+
import array
2432
import os.path
2533
import struct
2634
from enum import Enum
@@ -60,6 +68,8 @@
6068
'NON_BOOTABLE': 0x0000010,
6169
'RAM_LOAD': 0x0000020,
6270
'ROM_FIXED': 0x0000100,
71+
'COMPRESSED_LZMA1': 0x0000200,
72+
'COMPRESSED_LZMA2': 0x0000400,
6373
}
6474

6575
TLV_VALUES = {
@@ -79,6 +89,9 @@
7989
'DEPENDENCY': 0x40,
8090
'SEC_CNT': 0x50,
8191
'BOOT_RECORD': 0x60,
92+
'DECOMP_SIZE': 0x70,
93+
'DECOMP_SHA': 0x71,
94+
'DECOMP_SIGNATURE': 0x72,
8295
}
8396

8497
TLV_SIZE = 4
@@ -237,6 +250,9 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
237250
if load_addr and rom_fixed:
238251
raise click.UsageError("Can not set rom_fixed and load_addr at the same time")
239252

253+
self.image_hash = None
254+
self.image_size = None
255+
self.signature = None
240256
self.version = version or versmod.decode_version("0")
241257
self.header_size = header_size
242258
self.pad_header = pad_header
@@ -252,6 +268,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
252268
self.rom_fixed = rom_fixed
253269
self.erased_val = 0xff if erased_val is None else int(erased_val, 0)
254270
self.payload = []
271+
self.infile_data = []
255272
self.enckey = None
256273
self.save_enctlv = save_enctlv
257274
self.enctlv_len = 0
@@ -300,19 +317,39 @@ def __repr__(self):
300317
self.__class__.__name__,
301318
len(self.payload))
302319

303-
def load(self, path):
320+
def load(self, path, compression_header=None):
304321
"""Load an image from a given file"""
305322
ext = os.path.splitext(path)[1][1:].lower()
306323
try:
307324
if ext == INTEL_HEX_EXT:
308325
ih = IntelHex(path)
309-
self.payload = ih.tobinarray()
326+
self.infile_data = ih.tobinarray()
327+
self.payload = copy.copy(self.infile_data)
310328
self.base_addr = ih.minaddr()
311329
else:
312330
with open(path, 'rb') as f:
313-
self.payload = f.read()
331+
self.infile_data = f.read()
332+
self.payload = copy.copy(self.infile_data)
333+
if compression_header is not None:
334+
self.payload = compression_header + self.payload
314335
except FileNotFoundError:
315336
raise click.UsageError("Input file not found")
337+
self.image_size = len(self.payload)
338+
339+
# Add the image header if needed.
340+
if self.pad_header and self.header_size > 0:
341+
if self.base_addr:
342+
# Adjust base_addr for new header
343+
self.base_addr -= self.header_size
344+
self.payload = bytes([self.erased_val] * self.header_size) + \
345+
self.payload
346+
347+
self.check_header()
348+
349+
def load_compressed(self, data, compression_header):
350+
"""Load an image from buffer"""
351+
self.payload = compression_header + data
352+
self.image_size = len(self.payload)
316353

317354
# Add the image header if needed.
318355
if self.pad_header and self.header_size > 0:
@@ -407,7 +444,8 @@ def ecies_hkdf(self, enckey, plainkey):
407444
return cipherkey, ciphermac, pubk
408445

409446
def create(self, key, public_key_format, enckey, dependencies=None,
410-
sw_type=None, custom_tlvs=None, encrypt_keylen=128, clear=False,
447+
sw_type=None, custom_tlvs=None, compression_tlvs=None,
448+
compression_type=None, encrypt_keylen=128, clear=False,
411449
fixed_sig=None, pub_key=None, vector_to_sign=None, user_sha='auto'):
412450
self.enckey = enckey
413451

@@ -470,6 +508,9 @@ def create(self, key, public_key_format, enckey, dependencies=None,
470508
dependencies_num = len(dependencies[DEP_IMAGES_KEY])
471509
protected_tlv_size += (dependencies_num * 16)
472510

511+
if compression_tlvs is not None:
512+
for value in compression_tlvs.values():
513+
protected_tlv_size += TLV_SIZE + len(value)
473514
if custom_tlvs is not None:
474515
for value in custom_tlvs.values():
475516
protected_tlv_size += TLV_SIZE + len(value)
@@ -491,11 +532,15 @@ def create(self, key, public_key_format, enckey, dependencies=None,
491532
else:
492533
self.payload.extend(pad)
493534

535+
compression_flags = 0x0
536+
if compression_tlvs is not None:
537+
if compression_type == "lzma2":
538+
compression_flags = IMAGE_F['COMPRESSED_LZMA2']
494539
# This adds the header to the payload as well
495540
if encrypt_keylen == 256:
496-
self.add_header(enckey, protected_tlv_size, 256)
541+
self.add_header(enckey, protected_tlv_size, compression_flags, 256)
497542
else:
498-
self.add_header(enckey, protected_tlv_size)
543+
self.add_header(enckey, protected_tlv_size, compression_flags)
499544

500545
prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
501546

@@ -525,6 +570,9 @@ def create(self, key, public_key_format, enckey, dependencies=None,
525570
)
526571
prot_tlv.add('DEPENDENCY', payload)
527572

573+
if compression_tlvs is not None:
574+
for tag, value in compression_tlvs.items():
575+
prot_tlv.add(tag, value)
528576
if custom_tlvs is not None:
529577
for tag, value in custom_tlvs.items():
530578
prot_tlv.add(tag, value)
@@ -543,6 +591,7 @@ def create(self, key, public_key_format, enckey, dependencies=None,
543591
digest = sha.digest()
544592
message = digest;
545593
tlv.add(hash_tlv, digest)
594+
self.image_hash = digest
546595

547596
if vector_to_sign == 'payload':
548597
# Stop amending data to the image
@@ -622,10 +671,16 @@ def create(self, key, public_key_format, enckey, dependencies=None,
622671

623672
self.check_trailer()
624673

674+
def get_struct_endian(self):
675+
return STRUCT_ENDIAN_DICT[self.endian]
676+
625677
def get_signature(self):
626678
return self.signature
627679

628-
def add_header(self, enckey, protected_tlv_size, aes_length=128):
680+
def get_infile_data(self):
681+
return self.infile_data
682+
683+
def add_header(self, enckey, protected_tlv_size, compression_flags, aes_length=128):
629684
"""Install the image header."""
630685

631686
flags = 0
@@ -663,7 +718,7 @@ def add_header(self, enckey, protected_tlv_size, aes_length=128):
663718
protected_tlv_size, # TLV Info header +
664719
# Protected TLVs
665720
len(self.payload) - self.header_size, # ImageSz
666-
flags,
721+
flags | compression_flags,
667722
self.version.major,
668723
self.version.minor or 0,
669724
self.version.revision or 0,

scripts/imgtool/main.py

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,24 @@
2222
import getpass
2323
import imgtool.keys as keys
2424
import sys
25+
import struct
26+
import os
27+
import lzma
28+
import hashlib
2529
import base64
2630
from imgtool import image, imgtool_version
2731
from imgtool.version import decode_version
2832
from imgtool.dumpinfo import dump_imginfo
2933
from .keys import (
3034
RSAUsageError, ECDSAUsageError, Ed25519UsageError, X25519UsageError)
3135

36+
comp_default_dictsize=131072
37+
comp_default_pb=2
38+
comp_default_lc=3
39+
comp_default_lp=1
40+
comp_default_preset=9
41+
42+
3243
MIN_PYTHON_VERSION = (3, 6)
3344
if sys.version_info < MIN_PYTHON_VERSION:
3445
sys.exit("Python %s.%s or newer is required by imgtool."
@@ -300,6 +311,14 @@ def get_dependencies(ctx, param, value):
300311
dependencies[image.DEP_VERSIONS_KEY] = versions
301312
return dependencies
302313

314+
def create_lzma2_header(dictsize, pb, lc, lp):
315+
header = bytearray()
316+
for i in range(0, 40):
317+
if dictsize <= ((2 | ((i) & 1)) << int((i) / 2 + 11)):
318+
header.append(i)
319+
break
320+
header.append( ( pb * 5 + lp) * 9 + lc)
321+
return header
303322

304323
class BasedIntParamType(click.ParamType):
305324
name = 'integer'
@@ -343,6 +362,11 @@ def convert(self, value, param, ctx):
343362
type=click.Choice(['128', '256']),
344363
help='When encrypting the image using AES, select a 128 bit or '
345364
'256 bit key len.')
365+
@click.option('--compression', default='disabled',
366+
type=click.Choice(['disabled', 'lzma2']),
367+
help='Enable image compression using specified type. '
368+
'Will fall back without image compression automatically '
369+
'if the compression increases the image size.')
346370
@click.option('-c', '--clear', required=False, is_flag=True, default=False,
347371
help='Output a non-encrypted image with encryption capabilities,'
348372
'so it can be installed in the primary slot, and encrypted '
@@ -414,10 +438,11 @@ def convert(self, value, param, ctx):
414438
.hex extension, otherwise binary format is used''')
415439
def sign(key, public_key_format, align, version, pad_sig, header_size,
416440
pad_header, slot_size, pad, confirm, max_sectors, overwrite_only,
417-
endian, encrypt_keylen, encrypt, infile, outfile, dependencies,
418-
load_addr, hex_addr, erased_val, save_enctlv, security_counter,
419-
boot_record, custom_tlv, rom_fixed, max_align, clear, fix_sig,
420-
fix_sig_pubkey, sig_out, user_sha, vector_to_sign, non_bootable):
441+
endian, encrypt_keylen, encrypt, compression, infile, outfile,
442+
dependencies, load_addr, hex_addr, erased_val, save_enctlv,
443+
security_counter, boot_record, custom_tlv, rom_fixed, max_align,
444+
clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, vector_to_sign,
445+
non_bootable):
421446

422447
if confirm:
423448
# Confirmed but non-padded images don't make much sense, because
@@ -431,6 +456,7 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
431456
erased_val=erased_val, save_enctlv=save_enctlv,
432457
security_counter=security_counter, max_align=max_align,
433458
non_bootable=non_bootable)
459+
compression_tlvs = {}
434460
img.load(infile)
435461
key = load_key(key) if key else None
436462
enckey = load_key(encrypt) if encrypt else None
@@ -484,10 +510,49 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
484510
}
485511

486512
img.create(key, public_key_format, enckey, dependencies, boot_record,
487-
custom_tlvs, int(encrypt_keylen), clear, baked_signature,
488-
pub_key, vector_to_sign, user_sha)
513+
custom_tlvs, compression_tlvs, int(encrypt_keylen), clear,
514+
baked_signature, pub_key, vector_to_sign, user_sha)
515+
516+
if compression == "lzma2" :
517+
compressed_img = image.Image(version=decode_version(version),
518+
header_size=header_size, pad_header=pad_header,
519+
pad=pad, confirm=confirm, align=int(align),
520+
slot_size=slot_size, max_sectors=max_sectors,
521+
overwrite_only=overwrite_only, endian=endian,
522+
load_addr=load_addr, rom_fixed=rom_fixed,
523+
erased_val=erased_val, save_enctlv=save_enctlv,
524+
security_counter=security_counter, max_align=max_align)
525+
compression_filters = [
526+
{"id": lzma.FILTER_LZMA2, "preset": comp_default_preset,
527+
"dict_size": comp_default_dictsize, "lp": comp_default_lp,
528+
"lc": comp_default_lc}
529+
]
530+
compressed_data = lzma.compress(img.get_infile_data(),filters=compression_filters,
531+
format=lzma.FORMAT_RAW)
532+
uncompressed_size = len(img.get_infile_data())
533+
compressed_size = len(compressed_data)
534+
print("compressed image size:", compressed_size,
535+
"bytes\noriginal image size:", uncompressed_size, "bytes")
536+
compression_tlvs["DECOMP_SIZE"] = struct.pack(
537+
img.get_struct_endian() + 'L', img.image_size)
538+
compression_tlvs["DECOMP_SHA"] = img.image_hash
539+
compression_tlvs_size = len(compression_tlvs["DECOMP_SIZE"])
540+
compression_tlvs_size += len(compression_tlvs["DECOMP_SHA"])
541+
if img.get_signature() is not None and img.get_signature() != "" :
542+
compression_tlvs["DECOMP_SIGNATURE"] = img.get_signature()
543+
compression_tlvs_size += len(compression_tlvs["DECOMP_SIGNATURE"])
544+
if (compressed_size + compression_tlvs_size) < uncompressed_size:
545+
compression_header = create_lzma2_header(
546+
dictsize = comp_default_dictsize, pb = comp_default_pb,
547+
lc = comp_default_lc, lp = comp_default_lp)
548+
compressed_img.load_compressed(compressed_data, compression_header)
549+
compressed_img.base_addr = img.base_addr
550+
compressed_img.create(key, public_key_format, enckey,
551+
dependencies, boot_record, custom_tlvs, compression_tlvs,
552+
compression, int(encrypt_keylen), clear, baked_signature,
553+
pub_key, vector_to_sign)
554+
img = compressed_img
489555
img.save(outfile, hex_addr)
490-
491556
if sig_out is not None:
492557
new_signature = img.get_signature()
493558
save_signature(sig_out, new_signature)

0 commit comments

Comments
 (0)