import hashlib import os import os.path import re import struct import subprocess import sys import math from conftest import need_to_install_package_err from elftools.elf.elffile import ELFFile import pytest TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "elf2image") try: import esptool except ImportError: need_to_install_package_err() def try_delete(path): try: os.remove(path) except OSError: pass def segment_matches_section(segment, section): """segment is an ImageSegment from an esptool binary. section is an elftools ELF section Returns True if they match """ sh_size = (section.header.sh_size + 0x3) & ~3 # pad length of ELF sections return section.header.sh_addr == segment.addr and sh_size == len(segment.data) @pytest.mark.host_test class BaseTestCase: @classmethod def setup_class(self): # Save the current working directory to be restored later self.stored_dir = os.getcwd() os.chdir(TEST_DIR) @classmethod def teardown_class(self): # Restore the stored working directory os.chdir(self.stored_dir) def assertEqualHex(self, expected, actual, message=None): try: expected = hex(expected) except TypeError: # if expected is character expected = hex(ord(expected)) try: actual = hex(actual) except TypeError: # if actual is character actual = hex(ord(actual)) assert expected == actual, message def assertImageDoesNotContainSection(self, image, elf, section_name): """ Assert an esptool binary image object does not contain the data for a particular ELF section. """ with open(elf, "rb") as f: e = ELFFile(f) section = e.get_section_by_name(section_name) assert section, f"{section_name} should be in the ELF" sh_addr = section.header.sh_addr data = section.data() # no section should start at the same address as the ELF section. for seg in sorted(image.segments, key=lambda s: s.addr): print( f"comparing seg {seg.addr:#x} sec {sh_addr:#x} len {len(data):#x}" ) assert seg.addr != sh_addr, ( f"{section_name} should not be in the binary image" ) def assertImageContainsSection(self, image, elf, section_name): """ Assert an esptool binary image object contains the data for a particular ELF section. """ with open(elf, "rb") as f: e = ELFFile(f) section = e.get_section_by_name(section_name) assert section, f"{section_name} should be in the ELF" sh_addr = section.header.sh_addr data = section.data() # section contents may be smeared across multiple image segments, # so look through each segment and remove it from ELF section 'data' # as we find it in the image segments. When we're done 'data' should # all be accounted for for seg in sorted(image.segments, key=lambda s: s.addr): print( f"comparing seg {seg.addr:#x} sec {sh_addr:#x} len {len(data):#x}" ) if seg.addr == sh_addr: overlap_len = min(len(seg.data), len(data)) assert data[:overlap_len] == seg.data[:overlap_len], ( f"ELF '{section_name}' section has mis-matching bin image data" ) sh_addr += overlap_len data = data[overlap_len:] # no bytes in 'data' should be left unmatched assert len(data) == 0, ( f"ELF {elf} section '{section_name}' has no encompassing" f" segment(s) in bin image (image segments: {image.segments})" ) def assertImageInfo(self, binpath, chip="esp8266", assert_sha=False): """ Run esptool image-info on a binary file, assert no red flags about contents. """ cmd = [sys.executable, "-m", "esptool", "--chip", chip, "image-info", binpath] try: output = subprocess.check_output(cmd) output = output.decode("utf-8") print(output) except subprocess.CalledProcessError as e: print(e.output) raise assert re.search(r"Checksum: 0x[a-fA-F0-9]{2} \(valid\)", output), ( "Checksum calculation should be valid" ) if assert_sha: assert re.search(r"Validation hash: [a-fA-F0-9]{64} \(valid\)", output), ( "SHA256 should be valid" ) assert "warning" not in output.lower(), ( "Should be no warnings in image-info output" ) def run_elf2image( self, chip, elf_path, version=None, extra_args=[], allow_warnings=False ): """Run elf2image on elf_path""" cmd = [sys.executable, "-m", "esptool", "--chip", chip, "elf2image"] if version is not None: cmd += ["--version", str(version)] cmd += [elf_path] + extra_args print("\nExecuting {}".format(" ".join(cmd))) try: output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) output = output.decode("utf-8") print(output) if not allow_warnings: assert "warning" not in output.lower(), ( "elf2image should not output warnings" ) except subprocess.CalledProcessError as e: print(e.output) raise @staticmethod def assertAllFF(some_bytes): """Assert that the given bytes are all 0xFF (erased flash state)""" assert b"\xff" * len(some_bytes) == some_bytes, ( "Expected all 0xFF bytes, but found different values" ) class TestESP8266V1Image(BaseTestCase): ELF = "esp8266-nonosssdk20-iotdemo.elf" BIN_LOAD = "esp8266-nonosssdk20-iotdemo.elf-0x00000.bin" BIN_IROM = "esp8266-nonosssdk20-iotdemo.elf-0x10000.bin" @classmethod def setup_class(self): super().setup_class() self.run_elf2image(self, "esp8266", self.ELF, 1) @classmethod def teardown_class(self): try_delete(self.BIN_LOAD) try_delete(self.BIN_IROM) super().teardown_class() def test_irom_bin(self): with open(self.ELF, "rb") as f: e = ELFFile(f) irom_section = e.get_section_by_name(".irom0.text") assert irom_section.header.sh_size == os.stat(self.BIN_IROM).st_size, ( "IROM raw binary file should be same length as .irom0.text section" ) def test_loaded_sections(self): image = esptool.bin_image.LoadFirmwareImage("esp8266", self.BIN_LOAD) # Adjacent sections are now merged, len(image.segments) should # equal 2 (instead of 3). assert len(image.segments) == 2 self.assertImageContainsSection(image, self.ELF, ".data") self.assertImageContainsSection(image, self.ELF, ".text") # Section .rodata is merged in the binary with the previous one, # so it won't be found in the binary image. self.assertImageDoesNotContainSection(image, self.ELF, ".rodata") class TestESP8266V12SectionHeaderNotAtEnd(BaseTestCase): """Ref https://github.com/espressif/esptool/issues/197 - this ELF image has the section header not at the end of the file""" ELF = "esp8266-nonossdkv12-example.elf" BIN_LOAD = ELF + "-0x00000.bin" BIN_IROM = ELF + "-0x40000.bin" @classmethod def teardown_class(self): try_delete(self.BIN_LOAD) try_delete(self.BIN_IROM) def test_elf_section_header_not_at_end(self): self.run_elf2image("esp8266", self.ELF) image = esptool.bin_image.LoadFirmwareImage("esp8266", self.BIN_LOAD) assert len(image.segments) == 3 self.assertImageContainsSection(image, self.ELF, ".data") self.assertImageContainsSection(image, self.ELF, ".text") self.assertImageContainsSection(image, self.ELF, ".rodata") class TestESP8266V2Image(BaseTestCase): def _test_elf2image(self, elfpath, binpath, mergedsections=[]): try: self.run_elf2image("esp8266", elfpath, 2) image = esptool.bin_image.LoadFirmwareImage("esp8266", binpath) print("In test_elf2image", len(image.segments)) assert 4 - len(mergedsections) == len(image.segments) sections = [".data", ".text", ".rodata"] # Remove the merged sections from the `sections` list sections = [sec for sec in sections if sec not in mergedsections] for sec in sections: self.assertImageContainsSection(image, elfpath, sec) for sec in mergedsections: self.assertImageDoesNotContainSection(image, elfpath, sec) irom_segment = image.segments[0] assert irom_segment.addr == 0, "IROM segment 'load address' should be zero" with open(elfpath, "rb") as f: e = ELFFile(f) sh_size = ( e.get_section_by_name(".irom0.text").header.sh_size + 15 ) & ~15 assert len(irom_segment.data) == sh_size, ( f"irom segment ({len(irom_segment.data):#x}) should be same size " f"(16 padded) as .irom0.text section ({sh_size:#x})" ) # check V2 CRC (for ESP8266 SDK bootloader) with open(binpath, "rb") as f: f.seek(-4, os.SEEK_END) image_len = f.tell() crc_stored = struct.unpack("