You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
7.9 KiB
262 lines
7.9 KiB
#!/usr/bin/env python3 |
|
|
|
import enum |
|
import io |
|
import sys |
|
import struct |
|
from argparse import ArgumentParser |
|
from dataclasses import dataclass |
|
from elftools.elf.elffile import ELFFile |
|
from typing import Any |
|
|
|
|
|
class FirmwareVersionType(enum.Enum): |
|
Dev = 0 |
|
Alpha = 64 |
|
Beta = 128 |
|
RC = 192 |
|
Official = 255 |
|
EnumEnd = 256 |
|
|
|
|
|
class VehicleType(enum.Enum): |
|
Rover = 1 |
|
ArduCopter = 2 |
|
ArduPlane = 3 |
|
AntennaTracker = 4 |
|
UNKNOWN = 5 |
|
Replay = 6 |
|
ArduSub = 7 |
|
iofirmware = 8 |
|
AP_Periph = 9 |
|
|
|
|
|
class BoardType(enum.Enum): |
|
SITL = 3 |
|
SMACCM = 4 |
|
PX4 = 5 |
|
LINUX = 7 |
|
VRBRAIN = 8 |
|
CHIBIOS = 10 |
|
F4LIGHT = 11 |
|
EMPTY = 99 |
|
|
|
|
|
class BoardSubType(enum.Enum): |
|
NONE = 65535 |
|
|
|
LINUX_NONE = 1000 |
|
LINUX_ERLEBOARD = 1001 |
|
LINUX_PXF = 1002 |
|
LINUX_NAVIO = 1003 |
|
LINUX_ZYNQ = 1004 |
|
LINUX_BBBMINI = 1005 |
|
LINUX_BEBOP = 1006 |
|
LINUX_ERLEBRAIN2 = 1009 |
|
LINUX_BH = 1010 |
|
LINUX_PXFMINI = 1012 |
|
LINUX_NAVIO2 = 1013 |
|
LINUX_DISCO = 1014 |
|
LINUX_AERO = 1015 |
|
LINUX_DARK = 1016 |
|
LINUX_BLUE = 1018 |
|
LINUX_OCPOC_ZYNQ = 1019 |
|
LINUX_EDGE = 1020 |
|
LINUX_RST_ZYNQ = 1021 |
|
LINUX_POCKET = 1022 |
|
LINUX_NAVIGATOR = 1023 |
|
LINUX_VNAV = 1024 |
|
LINUX_OBAL = 1025 |
|
CHIBIOS_SKYVIPER_F412 = 5000 |
|
CHIBIOS_FMUV3 = 5001 |
|
CHIBIOS_FMUV4 = 5002 |
|
CHIBIOS_GENERIC = 5009 |
|
CHIBIOS_FMUV5 = 5013 |
|
CHIBIOS_VRBRAIN_V51 = 5016 |
|
CHIBIOS_VRBRAIN_V52 = 5017 |
|
CHIBIOS_VRUBRAIN_V51 = 5018 |
|
CHIBIOS_VRCORE_V10 = 5019 |
|
CHIBIOS_VRBRAIN_V54 = 5020 |
|
|
|
|
|
@dataclass |
|
class FWVersion: |
|
header: int = 0x61706677766572FB |
|
header_version: bytes = bytes([0, 0]) |
|
pointer_size: int = 0 |
|
vehicle_type: int = 0 |
|
board_type: int = 0 |
|
board_subtype: int = 0 |
|
major: int = 0 |
|
minor: int = 0 |
|
patch: int = 0 |
|
firmware_type: FirmwareVersionType = FirmwareVersionType.EnumEnd |
|
os_software_version: int = 0 |
|
firmware_string: str = "" |
|
firmware_hash_string: str = "" |
|
firmware_hash: int = 0 |
|
middleware_name: str = "" |
|
middleware_hash_string: str = "" |
|
os_name: str = "" |
|
os_hash_string: str = "" |
|
|
|
def __str__(self): |
|
header = self.header.to_bytes(8, byteorder="big") |
|
header_version = self.header_version.to_bytes(2, byteorder="big") |
|
firmware_day = self.os_software_version % 100 |
|
firmware_month = self.os_software_version % 10000 - firmware_day |
|
firmware_year = self.os_software_version - firmware_month - firmware_day |
|
firmware_month = int(firmware_month / 100) |
|
firmware_year = int(firmware_year / 10000) |
|
return f""" |
|
{self.__class__.__name__}: |
|
header: |
|
magic: {header[0:7].decode("utf-8")} |
|
checksum: {hex(header[-1])} |
|
version: {header_version[0]}.{header_version[1]} |
|
pointer_size: {self.pointer_size} |
|
firmware: |
|
string: {self.firmware_string} |
|
vehicle: {VehicleType(self.vehicle_type).name} |
|
board: {BoardType(self.board_type).name} |
|
board subtype: {BoardSubType(self.board_subtype).name} |
|
hash: {self.firmware_hash_string} |
|
hash integer: 0x{self.firmware_hash:02x} |
|
version: {self.major}.{self.minor}.{self.patch} |
|
type: {FirmwareVersionType(self.firmware_type).name} |
|
os: |
|
name: {self.os_name} |
|
hash: {self.os_hash_string} |
|
software_version: {firmware_day}/{firmware_month}/{firmware_year} |
|
middleware: |
|
name: {self.middleware_name} |
|
hash: {self.middleware_hash_string} |
|
""" |
|
|
|
|
|
class Decoder: |
|
def __init__(self) -> None: |
|
self.bytesio = io.BytesIO() |
|
self.fwversion = FWVersion() |
|
self.byteorder = "" |
|
self.pointer_size = 0 |
|
self.elffile = None |
|
|
|
def unpack(self, struct_format: str) -> Any: |
|
struct_format = f"{self.byteorder}{struct_format}" |
|
size = struct.calcsize(struct_format) |
|
return struct.unpack(struct_format, self.bytesio.read(size))[0] |
|
|
|
def unpack_string_from_pointer(self) -> str: |
|
pointer_format = "Q" if self.pointer_size == 8 else "I" |
|
address = self.unpack(pointer_format) |
|
|
|
# nullptr, return empty string |
|
if address == 0: |
|
return "" |
|
|
|
# Calculate address offset for PIE (Position Independent Executables) binaries |
|
address = next(self.elffile.address_offsets(address)) |
|
|
|
current_address = self.bytesio.seek(0, io.SEEK_CUR) |
|
self.bytesio.seek(address) |
|
string = [] |
|
while True: |
|
string += self.bytesio.read(1) |
|
if string[-1] == 0: |
|
string = string[0 : len(string) - 1] |
|
break |
|
self.bytesio.seek(current_address) |
|
return bytes(string).decode("UTF-8") |
|
|
|
@staticmethod |
|
def locate_header(data: bytes, byteorder: str) -> int: |
|
return data.find(struct.pack(f"{byteorder}Q", FWVersion.header)) |
|
|
|
def unpack_fwversion(self) -> None: |
|
assert self.bytesio.read(8) == struct.pack( |
|
f"{self.byteorder}Q", FWVersion.header |
|
) |
|
|
|
self.fwversion.header_version = self.unpack("H") |
|
major_version = self.fwversion.header_version >> 8 |
|
|
|
self.pointer_size = self.unpack("B") |
|
self.fwversion.pointer_size = self.pointer_size |
|
self.unpack("B") # reserved |
|
self.fwversion.vehicle_type = self.unpack("B") |
|
self.fwversion.board_type = self.unpack("B") |
|
self.fwversion.board_subtype = self.unpack("H") |
|
|
|
self.fwversion.major = self.unpack("B") |
|
self.fwversion.minor = self.unpack("B") |
|
self.fwversion.patch = self.unpack("B") |
|
self.fwversion.firmware_type = self.unpack("B") |
|
self.fwversion.os_software_version = self.unpack("I") |
|
|
|
self.fwversion.firmware_string = self.unpack_string_from_pointer() |
|
self.fwversion.firmware_hash_string = self.unpack_string_from_pointer() |
|
if major_version >= 2: |
|
self.fwversion.firmware_hash = self.unpack("I") |
|
|
|
self.fwversion.middleware_name = self.unpack_string_from_pointer() |
|
self.fwversion.middleware_hash_string = self.unpack_string_from_pointer() |
|
self.fwversion.os_name = self.unpack_string_from_pointer() |
|
self.fwversion.os_hash_string = self.unpack_string_from_pointer() |
|
|
|
def process(self, filename) -> FWVersion: |
|
# We need the file open for ELFFile |
|
file = open(filename, "rb") |
|
data = file.read() |
|
self.elffile = ELFFile(file) |
|
|
|
if not data: |
|
raise RuntimeError("Failed to find FWVersion.") |
|
|
|
# Detect endianness |
|
for order in [">", "<"]: |
|
position = Decoder.locate_header(data, order) |
|
if position != -1: |
|
self.byteorder = order |
|
self.bytesio = io.BytesIO(data) |
|
self.bytesio.seek(position) |
|
break |
|
else: |
|
raise RuntimeError("Failed to find FWVersion.") |
|
|
|
# Unpack struct and print it |
|
self.unpack_fwversion() |
|
return self.fwversion |
|
|
|
|
|
if __name__ == "__main__": |
|
assert ( |
|
sys.version_info.major >= 3 and sys.version_info.minor >= 7 |
|
), "Python version should be at least 3.7" |
|
|
|
# Parse arguments |
|
parser = ArgumentParser(description=__doc__) |
|
parser.add_argument( |
|
"-f", |
|
dest="file", |
|
required=True, |
|
help="File that contains a valid ardupilot firmware in ELF format.", |
|
) |
|
parser.add_argument( |
|
"--expected-hash", |
|
dest="expected_hash", |
|
help="Expected git hash. The script fails if this doesn't match the git hash in the binary file. Used in CI", |
|
) |
|
args = parser.parse_args() |
|
|
|
decoder = Decoder() |
|
try: |
|
firmware_data = decoder.process(args.file) |
|
except Exception as e: |
|
print(f"Error decoding FWVersion: {type(e)}") |
|
exit(-1) |
|
|
|
print(firmware_data) |
|
if args.expected_hash and args.expected_hash != firmware_data.firmware_hash_string: |
|
print(f"Git hashes don't match! expected: {args.expected_hash}, got {firmware_data.firmware_hash_string}") |
|
exit(-1)
|
|
|