"""Embed an exact published OTAFIX image in an opt-in infrastructure build.""" from __future__ import annotations import binascii import hashlib import io import json from pathlib import Path import re import stat import struct import sys import tempfile from urllib.request import urlopen import zipfile ROOT = Path(__file__).resolve().parents[1] VERSION = "2.4.12" BOOT_VERSION = 0x02040CFF MANIFEST_SHA256 = "d2f62a4ec9b7c5643d8ed787d9e1710a2fabac34607a0bbb6ea5883a458e6df3" LEGACY_SHA256 = "64bfe69d56eefc2d0fe51770902aff70ea0cc06cad261d3d219e4a3c0efac8dc" BOOT_BASE, BOOT_SIZE = 0xF4000, 0xA000 REPOSITORY = "mikecarper/Adafruit_nRF52_Bootloader_OTAFIX" TAG = "v0.11.0-OTAFIX" + VERSION MAX_ZIP_SIZE = 2 * 1024 * 1024 def _unique_object(pairs): result = {} for key, value in pairs: if key in result: raise ValueError("duplicate JSON key: " + key) result[key] = value return result def _json(data): return json.loads(data.decode("utf-8"), object_pairs_hook=_unique_object) def load_manifest(project=ROOT): data = (Path(project) / "docs/_data/bootloader_manifest.json").read_bytes() if hashlib.sha256(data).hexdigest() != MANIFEST_SHA256: raise ValueError("embedded bootloader requires the pinned OTAFIX 2.4.12 download manifest") manifest = _json(data) if (manifest.get("schemaVersion") != 1 or manifest.get("repository") != REPOSITORY or manifest.get("tag") != TAG or manifest.get("version") != VERSION or manifest.get("prerelease") is not False): raise ValueError("unexpected embedded bootloader release identity") return manifest def select_profile(manifest, target): from generate_mesh_america_release import target_profiles parsed = target_profiles([target])[target] if parsed["role"] not in ("repeater", "room"): raise ValueError("embedded bootloader is limited to repeater and room-server roles") names = {parsed["sourceHardware"], parsed["hardware"]} if any(parsed["sourceHardware"] in item["meshcoreHardware"] for item in manifest.get("unavailableProfiles", ())): raise ValueError("this exact hardware has no qualified OTAFIX download") matches = [item for item in manifest["profiles"] if names.intersection(item["meshcoreHardware"])] if len(matches) != 1: raise ValueError("hardware must map to one exact published bootloader profile") return matches[0], parsed def _asset(profile): asset = profile["files"]["zip"] name, size, digest = asset["name"], asset["size"], asset["sha256"] if (not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*\.zip", name) or type(size) is not int or not 0 < size <= MAX_ZIP_SIZE or not re.fullmatch(r"[0-9a-f]{64}", digest) or asset["url"] != f"https://github.com/{REPOSITORY}/releases/download/{TAG}/{name}"): raise ValueError("invalid pinned bootloader ZIP metadata") return asset def download_zip(profile, cache_dir): asset = _asset(profile) cache = Path(cache_dir) cache.mkdir(parents=True, exist_ok=True) path = cache / (asset["sha256"] + "-" + asset["name"]) def valid(data): return len(data) == asset["size"] and hashlib.sha256(data).hexdigest() == asset["sha256"] if path.is_file() and path.stat().st_size == asset["size"]: data = path.read_bytes() if valid(data): return data with urlopen(asset["url"], timeout=60) as response: data = response.read(asset["size"] + 1) if not valid(data): raise ValueError("downloaded bootloader ZIP size/SHA-256 mismatch") temporary = None try: with tempfile.NamedTemporaryFile(dir=cache, prefix="otafix-", delete=False) as stream: temporary = Path(stream.name) stream.write(data) temporary.replace(path) finally: if temporary is not None: temporary.unlink(missing_ok=True) return data def _unsigned_init(packet, metadata, firmware): if len(packet) < 12 or len(packet) > 4096: raise ValueError("invalid Legacy DFU init-packet length") device, revision, app_version, count = struct.unpack_from(" 64 or len(packet) != 12 + count * 2: raise ValueError("invalid Legacy DFU SoftDevice requirements") requirements = list(struct.unpack_from(f"<{count}H", packet, 10)) crc = struct.unpack_from("> 16) not in (0, stat.S_IFREG) or not 0 < entry.file_size <= (512 * 1024 if entry.filename.endswith(".bin") else 65536)): raise ValueError("unsupported bootloader ZIP member") document = _json(archive.read("manifest.json")) if set(document) != {"manifest"}: raise ValueError("invalid bootloader ZIP manifest root") manifest = document["manifest"] if set(manifest) != {"dfu_version", "softdevice_bootloader"} or manifest["dfu_version"] != 0.5: raise ValueError("expected an unsigned Legacy SoftDevice/bootloader package") entry = manifest["softdevice_bootloader"] if set(entry) != {"bin_file", "dat_file", "sd_size", "bl_size", "init_packet_data"}: raise ValueError("invalid combined bootloader manifest entry") if set(names) != {"manifest.json", entry["bin_file"], entry["dat_file"]}: raise ValueError("bootloader ZIP references do not match its members") if not entry["bin_file"].endswith(".bin") or not entry["dat_file"].endswith(".dat"): raise ValueError("bootloader ZIP has incorrect member extensions") firmware = archive.read(entry["bin_file"]) packet = archive.read(entry["dat_file"]) sd_size, bl_size = entry["sd_size"], entry["bl_size"] if (type(sd_size) is not int or type(bl_size) is not int or sd_size <= 0 or sd_size % 4 or bl_size != BOOT_SIZE or sd_size + bl_size != len(firmware)): raise ValueError("invalid SoftDevice/bootloader payload geometry") _unsigned_init(packet, entry["init_packet_data"], firmware) image = firmware[sd_size:] sys.path.insert(0, str(Path(project) / "tools/mota")) import motalib as ml identity = ml.parse_bootloader_identity(image) sp, reset = struct.unpack_from("