From 872413231da469e0a2bd2fae9f12d13936dce4a2 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Sat, 11 Jul 2026 14:47:11 +0200 Subject: [PATCH] ota: extract the motatool CLI to its own repo (github.com/vk496/motatool) The host-side build/verify/inspect/serve CLI now lives as a standalone Rust project at https://github.com/vk496/motatool. Remove the in-tree C++ copy (tools/motatool/) and repoint the docs (ota_protocol.md, ota_user_guide.md, tools/mota/README.md) at the standalone repo. No cross-dependency either direction: MeshCore's firmware build never invoked motatool (only tools/mota/ Python glue runs in the build), and motatool depends only on the shared .mota wire spec in docs/ota_protocol.md. tools/mota/ (pio_endf.py EndF hook, motalib.py reference lib, gen_targets.py) is unchanged. --- docs/ota_protocol.md | 6 +- docs/ota_user_guide.md | 13 +- tools/mota/README.md | 8 +- tools/motatool/.gitignore | 1 - tools/motatool/CMakeLists.txt | 42 -- tools/motatool/README.md | 127 ------ tools/motatool/src/crypto.cpp | 61 --- tools/motatool/src/crypto.h | 28 -- tools/motatool/src/input.cpp | 102 ----- tools/motatool/src/input.h | 15 - tools/motatool/src/main.cpp | 545 ------------------------- tools/motatool/src/mota.cpp | 413 ------------------- tools/motatool/src/mota.h | 83 ---- tools/motatool/src/mota_format.h | 106 ----- tools/motatool/src/serve.cpp | 344 ---------------- tools/motatool/src/serve.h | 95 ----- tools/motatool/src/util.h | 77 ---- tools/motatool/tests/test_motatool.cpp | 334 --------------- 18 files changed, 14 insertions(+), 2386 deletions(-) delete mode 100644 tools/motatool/.gitignore delete mode 100644 tools/motatool/CMakeLists.txt delete mode 100644 tools/motatool/README.md delete mode 100644 tools/motatool/src/crypto.cpp delete mode 100644 tools/motatool/src/crypto.h delete mode 100644 tools/motatool/src/input.cpp delete mode 100644 tools/motatool/src/input.h delete mode 100644 tools/motatool/src/main.cpp delete mode 100644 tools/motatool/src/mota.cpp delete mode 100644 tools/motatool/src/mota.h delete mode 100644 tools/motatool/src/mota_format.h delete mode 100644 tools/motatool/src/serve.cpp delete mode 100644 tools/motatool/src/serve.h delete mode 100644 tools/motatool/src/util.h delete mode 100644 tools/motatool/tests/test_motatool.cpp diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md index f173b9e3..5f0b8bcc 100644 --- a/docs/ota_protocol.md +++ b/docs/ota_protocol.md @@ -33,7 +33,7 @@ where a section names a source file, that file is the authoritative reference fo | Staging stores | `OtaStore.h`, `OtaStoreFlashNrf52.*`, `OtaStoreFlashEsp32.*` | | Apply | `OtaApply.*`, bootloader `Adafruit_nRF52_Bootloader_OTAFIX` | | Device glue (CLI/context) | `OtaCli.cpp`, `OtaContext.h` | -| Host tooling | `tools/motatool/` (C++ CLI: build/verify/inspect/serve); `tools/mota/` (Python reference lib `motalib.py` + build/test glue) | +| Host tooling | [`motatool`](https://github.com/vk496/motatool) (standalone Rust CLI: build/verify/inspect/serve); `tools/mota/` (Python reference lib `motalib.py` + build/test glue) | --- @@ -540,8 +540,8 @@ blocks) and streams payload blocks from the source on demand; proofs are generat A `MotaSource` is fed by a host that serves a folder over the device's **USB serial** (the same console the CLI uses — no extra hardware) or, on an ESP32 WiFi companion, over **WiFi (TCP)**. The host is the -self-contained C++ tool `tools/motatool/` (`motatool serve --serial ` / `--tcp `, which -also builds + validates `.mota` and runs on small hardware). The device only emits request frames *while +standalone Rust tool [`motatool`](https://github.com/vk496/motatool) (`motatool serve --serial ` / +`--tcp `, which also builds + verifies + inspects `.mota`). The device only emits request frames *while actively serving a fetch*, and reads the reply synchronously, so over the shared USB console binary frames coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed: diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md index 10831ad6..57e8dddf 100644 --- a/docs/ota_user_guide.md +++ b/docs/ota_user_guide.md @@ -203,19 +203,20 @@ folder of firmware files to the mesh — without storing them itself. Useful for remote area. 1. Put the firmware files (`.mota` files — see below) in a folder on the computer. -2. Build the helper tool once (`tools/motatool/`), then point it at your node and the folder — over the - node's **USB serial**, or over **WiFi** if it's an ESP32 companion on your network: +2. Install the helper tool once — the standalone `motatool` CLI () — + then point it at your node and the folder — over the node's **USB serial**, or over **WiFi** if it's + an ESP32 companion on your network: ``` - cmake -S tools/motatool -B tools/motatool/build && cmake --build tools/motatool/build + git clone https://github.com/vk496/motatool && cargo install --path ./motatool # over USB serial: - ./tools/motatool/build/motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v + motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v # …or over WiFi (ESP32 companion): the seeder is on a DEDICATED port (5001), separate from the # phone-app port (5000), so a phone can stay connected while you serve: - ./tools/motatool/build/motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v + motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v ``` It answers the node's requests; your node then advertises those updates to neighbours, who can `ota get` them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. - Details: [tools/motatool/README.md](../tools/motatool/README.md).) + Details: .) To stop, just stop the daemon — over WiFi the node auto-detaches when the connection closes; over USB you can also run `ota folder off` on the node. `ota folder` on its own lists what your node is offering. diff --git a/tools/mota/README.md b/tools/mota/README.md index c3f8a77d..a3239353 100644 --- a/tools/mota/README.md +++ b/tools/mota/README.md @@ -4,10 +4,10 @@ The Python side of MeshCore's `.mota` OTA system. It is the **reference implemen spec ([`docs/ota_protocol.md`](../../docs/ota_protocol.md)) and the **build + test infrastructure** — it is no longer a user-facing CLI. -> **Want to build / verify / inspect / serve `.mota` from the command line?** Use the self-contained C++ -> tool [`tools/motatool/`](../motatool/). It supersedes the old `mota.py` / `mota_seeder.py` (now removed), -> produces byte-identical containers, and runs on small hardware. The files here are the spec oracle and -> the firmware build/test glue. +> **Want to build / verify / inspect / serve `.mota` from the command line?** Use the standalone +> [`motatool`](https://github.com/vk496/motatool) Rust CLI (its own repository). It supersedes the old +> `mota.py` / `mota_seeder.py` (now removed) and produces byte-identical containers. The files here are +> the spec oracle and the firmware build/test glue. ## Setup diff --git a/tools/motatool/.gitignore b/tools/motatool/.gitignore deleted file mode 100644 index 567609b1..00000000 --- a/tools/motatool/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/tools/motatool/CMakeLists.txt b/tools/motatool/CMakeLists.txt deleted file mode 100644 index 4a153063..00000000 --- a/tools/motatool/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -cmake_minimum_required(VERSION 3.16) -project(motatool LANGUAGES CXX) - -# Self-contained MeshCore OTA host tool: build / verify / inspect / serve `.mota` containers. -# Portable C++17 — builds on Ubuntu, Raspberry Pi, and any arch with a C++17 compiler + OpenSSL. -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() -add_compile_options(-Wall -Wextra) - -# OpenSSL libcrypto provides SHA-256 + Ed25519 (sign/verify/keygen). On Debian/Ubuntu/Pi: -# sudo apt install libssl-dev -find_package(OpenSSL REQUIRED) - -# Core logic (everything except the CLI front-end) — shared by the tool and the test runner. -add_library(motatool_core STATIC - src/mota.cpp - src/crypto.cpp - src/input.cpp - src/serve.cpp -) -# `src` for the tool's own headers; the repo's helpers/ota for the shared, generated OtaTargets.h -# (target_id -> env-name table — single source of truth with the firmware, no hand-maintained copy). -target_include_directories(motatool_core PUBLIC - src - "${CMAKE_CURRENT_SOURCE_DIR}/../../src/helpers/ota" -) -target_link_libraries(motatool_core PUBLIC OpenSSL::Crypto) - -add_executable(motatool src/main.cpp) -target_link_libraries(motatool PRIVATE motatool_core) -install(TARGETS motatool RUNTIME DESTINATION bin) - -# Unit tests (no external test framework — a tiny built-in harness keeps the project self-contained). -# Run with: cmake --build build && ctest --test-dir build --output-on-failure -# or: ./build/motatool_tests -enable_testing() -add_executable(motatool_tests tests/test_motatool.cpp) -target_link_libraries(motatool_tests PRIVATE motatool_core) -add_test(NAME motatool_tests COMMAND motatool_tests) diff --git a/tools/motatool/README.md b/tools/motatool/README.md deleted file mode 100644 index 9f90e122..00000000 --- a/tools/motatool/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# motatool — self-contained MeshCore OTA host tool (C++) - -A small, portable C++17 tool that **builds**, **verifies**, and **serves** MeshCore `.mota` -firmware-update containers. It is an independent project (its own `CMakeLists.txt`) that compiles on a -laptop, a Raspberry Pi, or any architecture with a C++17 compiler — so it can run as a lightweight -folder-relay daemon on small hardware. - -It implements the wire spec in [`../../docs/ota_protocol.md`](../../docs/ota_protocol.md) and is -cross-checked byte-for-byte against the Python reference packager `tools/mota/motalib.py` (a full signed -`.mota` built by `motatool` is identical to the reference's output). - -## What it does - -| Command | Purpose | -|---|---| -| `build` | Create a **full** or **delta** `.mota` from a firmware (local file **or** http(s) URL). | -| `verify` | Validate one or more `.mota` (merkle tree, leaves vs payload, image hash, Ed25519 signature). `--pub` requires a specific signer; `--base` confirms a sequential delta rebuilds its image. | -| `inspect`| Print every field of a `.mota`'s manifest (debugging). | -| `serve` | Serve a **folder** of `.mota` to a node over USB serial (`--serial`) or WiFi (`--tcp`). Invalid files are warned about and skipped — one corrupt file never sinks the rest. The same link also **stores** a node's `ota pull … folder` captures into that folder as `.mota` — pull a device's exact firmware off the mesh to build deltas against firmware you don't otherwise have. | -| `keygen` | Generate an Ed25519 signing keypair (hex). | - -Every command has detailed, example-rich help: `motatool --help`. - -## Build - -Dependencies: a C++17 compiler, CMake, and **OpenSSL** (libcrypto: SHA-256 + Ed25519). For URL input -and delta creation, see the notes below. - -```bash -sudo apt install build-essential cmake libssl-dev # Debian / Ubuntu / Raspberry Pi OS -cmake -S tools/motatool -B tools/motatool/build -cmake --build tools/motatool/build -j -# -> tools/motatool/build/motatool -``` - -### Tests - -Unit tests (a tiny built-in harness — no external framework) cover the crypto, EndF, build/verify, -merkle, parse rejection, the folder scanner + seeder protocol, and a real detools delta round-trip -(auto-skipped if `detools` isn't installed): - -```bash -ctest --test-dir tools/motatool/build --output-on-failure -# or directly: tools/motatool/build/motatool_tests -``` - -## Usage - -```bash -MT=tools/motatool/build/motatool - -# 1. signing keypair (64-char hex) -$MT keygen --out signer.key # writes signer.key + signer.key.pub - -# 2a. FULL .mota — payload IS the flashable image (identity is auto-read from the firmware's EndF) -$MT build --fw firmware.bin --sign signer.key --out-dir ./motas/ - -# 2b. FULL .mota straight from a URL -$MT build --fw https://example.org/Heltec_v3_repeater.bin --sign signer.key --out-dir ./motas/ - -# 2c. DELTA .mota against a previous release (codec auto-selected from the hardware tag: -# nRF52 -> in-place, ESP32 -> sequential; override with --codec) -$MT build --fw new.bin --base old.bin --sign signer.key --out-dir ./motas/ -$MT build --fw new.bin --base old.bin --codec sequential --out-dir ./motas/ - -# 3. validate a folder of .mota (optionally require a signer / check a delta against its base) -$MT verify ./motas/*.mota -$MT verify update.mota --pub signer.key.pub -$MT verify delta.mota --base old_firmware.bin - -# 4. dump a single .mota's manifest fields -$MT inspect ./motas/RAK4631_04D413FD_v1.16.0_full_ABCD1234.mota - -# 5. serve a folder to a node (recursive; skips non-.mota; warns on corrupt) -$MT serve --dir ./motas --serial /dev/ttyUSB0 --baud 115200 -v # over USB serial -$MT serve --dir ./motas --tcp 192.168.1.50:5001 -v # …or over WiFi (ESP32 companion, port 5001) - -# 5b. warm-start a capture: seed the pull with a SIMILAR build, then `ota pull <#> folder validate` on the -# node pulls only the blocks that differ (fast capture of a non-deterministic rebuild of the same fw) -$MT serve --dir ./captured --tcp 192.168.1.50:5001 --seed ./similar_build.mota -v -``` - -`serve` doubles as the **pull-to-folder storage**: a node's `ota pull <#> folder` captures the firmware it's -fetching off-mesh into `--dir` as `.mota`. With `--seed `, that seed's payload is staged -into each capture so a `ota pull <#> folder validate` on the node bulk-fetches the target's merkle leaves, -keeps every block the seed already matches, and transfers over LoRa only the differing ones — a byte-exact, -verifiable capture in seconds instead of a full-image download (see [`../../docs/ota_protocol.md`](../../docs/ota_protocol.md) §8, "warm-start"). - -`build` notes: -- **Identity is self-described.** A firmware built by the project's `pio_endf.py` carries its - `target_id`/`fw_version`/`hw_id` in its 56-byte `EndF` trailer; `build` reads them, so - `--target-env`/`--target-id`, `--fw-version`, and `--hw-id` are **optional** (explicit flags override). -- **Cross-hardware delta guard.** A delta is refused if the base and target firmware identities differ - (read from their `EndF`, not filenames); pass `--force` to override. -- **Delta codec** needs the `detools` encoder (the project's pinned codec — never reimplemented). Install - it (`pip install detools`) and ensure it's on `PATH`, or pass `--detools `. **Full** builds, - **verify**, and **serve** need no detools. In-place defaults: `--inplace-memory 0xAE000` - (nRF52 workspace), `--inplace-segment 4096`. -- **URL input** uses the system `curl` (falling back to `wget`) — no link-time dependency. -- All output is written into one folder (`--out-dir`, default `.`) with a descriptive, unique name: - `__v__.mota`. - -## Serving is transport-agnostic (USB serial today, BLE later) - -The protocol is split so the serving logic is reusable across links: - -- **`SeederCore`** (`src/serve.{h,cpp}`) is transport-free: it maps a request `(op, args)` to a response - `(status, payload)` — `COUNT` / `DESCRIBE(idx)` / `READ(idx, off, len)` over the validated catalog. -- **`Transport` + `serve_loop()`** wrap it with the byte-stream framing (magic + XOR checksum + resync, - `MotaSeederProto.h`). Two transports ship: **`SerialTransport`** (`--serial`, USB-UART) and - **`TcpTransport`** (`--tcp `, WiFi — connects to the node's dedicated seeder port, default - `5001`, which the ESP32 companion runs alongside its phone-app port so both work at once). - -To serve the same folder over **BLE** (e.g. an Android phone relaying to a MeshCore node), implement the -transport at the GATT layer — a request characteristic write hands `(op, args)` straight to -`SeederCore::handle()` and the reply is sent as a notification. No byte-stream framing is needed there -(BLE is reliable/segmented), and `SeederCore` + `Folder` are reused unchanged. OpenSSL builds for the -Android NDK, so the validation path ports as-is. - -## Relationship to `tools/mota/` - -`motatool` is the user-facing CLI: it replaced the old Python `mota.py` (build/verify/inspect/keygen), -`mota_seeder.py` (serve), and `dev_motas.py` (CI `.mota` packaging — its nRF52 `.hex` extraction is now a -built-in `motatool` input format), all removed. The Python `tools/mota/` directory remains as the -**reference implementation** (`motalib.py`, the spec oracle + unit tests) and the **firmware build/test -glue** (`pio_endf.py` build hook, `gen_vectors.py` test vectors). Their `.mota` output is byte-identical -(verified by cross-checks). diff --git a/tools/motatool/src/crypto.cpp b/tools/motatool/src/crypto.cpp deleted file mode 100644 index a4076a9c..00000000 --- a/tools/motatool/src/crypto.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "crypto.h" -#include -#include -#include - -namespace mota { - -void sha256_trunc(uint8_t* out, size_t out_len, const uint8_t* data, size_t len) { - uint8_t full[32]; - SHA256(data, len, full); - if (out_len > 32) out_len = 32; - std::memcpy(out, full, out_len); -} - -bool ed25519_verify(const uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t pub[32]) { - EVP_PKEY* key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub, 32); - if (!key) return false; - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); - bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, nullptr, nullptr, key) == 1 && - EVP_DigestVerify(ctx, sig, 64, msg, msg_len) == 1; - if (ctx) EVP_MD_CTX_free(ctx); - EVP_PKEY_free(key); - return ok; -} - -bool ed25519_sign(uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t priv[32]) { - EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, priv, 32); - if (!key) return false; - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); - size_t siglen = 64; - bool ok = ctx && EVP_DigestSignInit(ctx, nullptr, nullptr, nullptr, key) == 1 && - EVP_DigestSign(ctx, sig, &siglen, msg, msg_len) == 1 && siglen == 64; - if (ctx) EVP_MD_CTX_free(ctx); - EVP_PKEY_free(key); - return ok; -} - -bool ed25519_pub_from_priv(uint8_t pub[32], const uint8_t priv[32]) { - EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, priv, 32); - if (!key) return false; - size_t len = 32; - bool ok = EVP_PKEY_get_raw_public_key(key, pub, &len) == 1 && len == 32; - EVP_PKEY_free(key); - return ok; -} - -bool ed25519_keygen(uint8_t priv[32], uint8_t pub[32]) { - EVP_PKEY* key = nullptr; - EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); - bool ok = pctx && EVP_PKEY_keygen_init(pctx) == 1 && EVP_PKEY_keygen(pctx, &key) == 1; - if (ok) { - size_t lp = 32, lk = 32; - ok = EVP_PKEY_get_raw_private_key(key, priv, &lp) == 1 && lp == 32 && - EVP_PKEY_get_raw_public_key(key, pub, &lk) == 1 && lk == 32; - } - if (key) EVP_PKEY_free(key); - if (pctx) EVP_PKEY_CTX_free(pctx); - return ok; -} - -} // namespace mota diff --git a/tools/motatool/src/crypto.h b/tools/motatool/src/crypto.h deleted file mode 100644 index eac5d8d3..00000000 --- a/tools/motatool/src/crypto.h +++ /dev/null @@ -1,28 +0,0 @@ -// SHA-256 (multihash truncations) and Ed25519 — backed by OpenSSL libcrypto, so the tool is portable -// across Ubuntu / Raspberry Pi / any arch that has OpenSSL (and Android NDK for a future BLE seeder). -#pragma once -#include -#include -#include -#include -#include - -namespace mota { - -// SHA-256 of `data`, truncated to out_len (sha2-256:N). out_len <= 32. -void sha256_trunc(uint8_t* out, size_t out_len, const uint8_t* data, size_t len); - -inline std::array mh4(const uint8_t* d, size_t n) { std::array o; sha256_trunc(o.data(),4,d,n); return o; } -inline std::array mh8(const uint8_t* d, size_t n) { std::array o; sha256_trunc(o.data(),8,d,n); return o; } -inline std::array mh32(const uint8_t* d, size_t n) { std::array o; sha256_trunc(o.data(),32,d,n); return o; } - -// Ed25519. Keys are raw 32-byte (private seed / public key), matching motalib / the device. -bool ed25519_verify(const uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t pub[32]); -// Sign `msg` with a 32-byte raw private seed -> 64-byte signature. Returns false on error. -bool ed25519_sign(uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t priv[32]); -// Derive the 32-byte raw public key from a 32-byte raw private seed. -bool ed25519_pub_from_priv(uint8_t pub[32], const uint8_t priv[32]); -// Generate a fresh keypair (raw 32-byte each). -bool ed25519_keygen(uint8_t priv[32], uint8_t pub[32]); - -} // namespace mota diff --git a/tools/motatool/src/input.cpp b/tools/motatool/src/input.cpp deleted file mode 100644 index bb0a4b12..00000000 --- a/tools/motatool/src/input.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "input.h" -#include "util.h" -#include -#include -#include - -namespace mota { - -bool is_url(const std::string& s) { - return s.rfind("http://", 0) == 0 || s.rfind("https://", 0) == 0; -} - -static bool ends_with_ci(const std::string& s, const std::string& suf) { - if (s.size() < suf.size()) return false; - for (size_t i = 0; i < suf.size(); i++) - if (std::tolower((unsigned char)s[s.size() - suf.size() + i]) != std::tolower((unsigned char)suf[i])) return false; - return true; -} - -// Parse Intel HEX text into the flat binary it represents (min..max address, gaps filled 0xFF). -// nRF52/STM32 PlatformIO builds emit firmware.hex; pio_endf appends the EndF inside it, so the extracted -// binary IS the OTA image (BODY||EndF) starting at the app base — exactly what `build` wants. -static std::string parse_intel_hex(const std::vector& in, std::vector& out) { - const std::string s((const char*)in.data(), in.size()); - auto nib = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - }; - struct Seg { uint64_t addr; std::vector data; }; - std::vector segs; - uint64_t base = 0, lo = UINT64_MAX, hi = 0; - size_t i = 0; - bool saw_eof = false; - while (i < s.size()) { - if (s[i] != ':') { i++; continue; } // tolerate CR/LF/whitespace between records - size_t p = i + 1; - auto rb = [&](uint8_t& v) -> bool { - if (p + 2 > s.size()) return false; - int h = nib(s[p]), l = nib(s[p + 1]); - if (h < 0 || l < 0) return false; - v = (uint8_t)((h << 4) | l); p += 2; return true; - }; - uint8_t len, ah, al, type; - if (!rb(len) || !rb(ah) || !rb(al) || !rb(type)) return "malformed Intel HEX record"; - uint8_t sum = (uint8_t)(len + ah + al + type); - std::vector data(len); - for (uint8_t k = 0; k < len; k++) { if (!rb(data[k])) return "truncated Intel HEX data"; sum += data[k]; } - uint8_t cks; - if (!rb(cks)) return "missing Intel HEX checksum"; - if ((uint8_t)(sum + cks) != 0) return "Intel HEX checksum error"; - uint16_t addr = (uint16_t)((ah << 8) | al); - if (type == 0x00) { // data - uint64_t a = base + addr; - if (a < lo) lo = a; - if (a + len > hi) hi = a + len; - segs.push_back({a, std::move(data)}); - } else if (type == 0x04) { // extended linear address (upper 16 bits) - if (len != 2) return "bad Intel HEX ELA record"; - base = (uint64_t)((data[0] << 8) | data[1]) << 16; - } else if (type == 0x02) { // extended segment address - if (len != 2) return "bad Intel HEX ESA record"; - base = (uint64_t)((data[0] << 8) | data[1]) << 4; - } else if (type == 0x01) { // EOF - saw_eof = true; break; - } // 0x03/0x05 (start address) ignored - i = p; - } - if (segs.empty()) return "no data records in Intel HEX"; - if (!saw_eof) return "Intel HEX missing EOF record"; - out.assign((size_t)(hi - lo), 0xFF); - for (auto& sg : segs) std::memcpy(out.data() + (size_t)(sg.addr - lo), sg.data.data(), sg.data.size()); - return ""; -} - -std::string read_input(const std::string& src, std::vector& out) { - std::vector raw; - if (!is_url(src)) { - if (!read_file(src, raw)) return "cannot read file: " + src; - } else { - // URL: download to a temp file via curl (fallback wget), then read it back. - char tmp[] = "/tmp/motadlXXXXXX"; - int fd = mkstemp(tmp); - if (fd < 0) return "mkstemp failed"; - close(fd); - int rc = run_argv({"curl", "-fLsS", "-o", tmp, src}); // -f fail, -L follow, -sS silent+show-errors - if (rc == 127) rc = run_argv({"wget", "-q", "-O", tmp, src}); - std::string err; - if (rc == 127) err = "neither curl nor wget is available to fetch " + src; - else if (rc != 0) err = "download failed (exit " + std::to_string(rc) + "): " + src; - else if (!read_file(tmp, raw)) err = "could not read the downloaded file: " + src; - unlink(tmp); - if (!err.empty()) return err; - } - if (raw.empty()) return "input is empty: " + src; - if (ends_with_ci(src, ".hex")) return parse_intel_hex(raw, out); // Intel HEX -> flat image - out = std::move(raw); - return ""; -} - -} // namespace mota diff --git a/tools/motatool/src/input.h b/tools/motatool/src/input.h deleted file mode 100644 index ff92f5fd..00000000 --- a/tools/motatool/src/input.h +++ /dev/null @@ -1,15 +0,0 @@ -// Read a firmware blob from a local file OR an http(s):// URL. URLs are fetched with the system `curl` -// (or `wget`) binary — no link-time dependency, so the tool stays self-contained and builds anywhere. -#pragma once -#include -#include -#include - -namespace mota { - -// Returns "" on success (fills `out`), else an error string. `what` is a label for error messages. -std::string read_input(const std::string& path_or_url, std::vector& out); - -bool is_url(const std::string& s); - -} // namespace mota diff --git a/tools/motatool/src/main.cpp b/tools/motatool/src/main.cpp deleted file mode 100644 index 042717c8..00000000 --- a/tools/motatool/src/main.cpp +++ /dev/null @@ -1,545 +0,0 @@ -// motatool — build, verify, and serve MeshCore `.mota` firmware-update containers. -// build create a full or delta .mota from a firmware (file or http(s) URL) -// verify validate one or more .mota (merkle / hashes / signature) -// serve serve a folder of .mota to a node (USB serial); invalid files are warned + skipped -// keygen generate an Ed25519 signing keypair (64-char hex) -#include "mota.h" -#include "input.h" -#include "serve.h" -#include "crypto.h" -#include "util.h" -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; -using namespace mota; - -static volatile bool g_stop = false; -static void on_sigint(int) { g_stop = true; } - -// minimal flag parser: --key value / --flag -struct Args { - std::map opt; - std::vector pos; - bool has(const std::string& k) const { return opt.count(k); } - std::string get(const std::string& k, const std::string& d = "") const { - auto it = opt.find(k); return it == opt.end() ? d : it->second; - } -}; -static Args parse_args(int argc, char** argv, int start, - const std::vector& bool_flags) { - Args a; - for (int i = start; i < argc; i++) { - std::string s = argv[i]; - if (s == "-h" || s == "--help" || s == "help") { a.opt["help"] = "1"; continue; } - if (s == "-v") { a.opt["verbose"] = "1"; continue; } - if (s.rfind("--", 0) == 0) { - std::string k = s.substr(2); - if (std::find(bool_flags.begin(), bool_flags.end(), k) != bool_flags.end()) a.opt[k] = "1"; - else if (i + 1 < argc) a.opt[k] = argv[++i]; - else a.opt[k] = ""; - } else a.pos.push_back(s); - } - return a; -} - -static bool load_priv(const std::string& path, std::vector& priv) { - std::vector raw; - if (!read_file(path, raw)) return false; - std::string txt((const char*)raw.data(), raw.size()); // try hex (mota.py format) first - while (!txt.empty() && (std::isspace((unsigned char)txt.back()))) txt.pop_back(); - std::vector hx; - if (from_hex(txt, hx) && hx.size() == 32) { priv = hx; return true; } - if (raw.size() == 32) { priv = raw; return true; } // else accept a raw 32-byte file - return false; -} - -static std::string version_str(uint32_t v) { - char b[24]; std::snprintf(b, sizeof(b), "%u.%u.%u", (v>>24)&0xFF, (v>>16)&0xFF, (v>>8)&0xFF); - return b; -} - -// ---- help text (the tool is meant to be usable from --help alone) --------------------------------- -static void help_top() { - std::cout << -"motatool — build, verify, and serve MeshCore .mota firmware-update containers.\n" -"\n" -"A .mota is a signed, self-verifying package of a firmware update that MeshCore nodes fetch\n" -"over LoRa, block by block. This tool makes those packages, checks them, and serves a folder\n" -"of them to a node over USB.\n" -"\n" -"USAGE\n" -" motatool [options]\n" -" motatool --help detailed help + examples for a command\n" -"\n" -"COMMANDS\n" -" build Package a firmware as a .mota (a full image, or a small delta vs a previous build).\n" -" verify Check that .mota files are valid (block hashes, image hash, signature).\n" -" inspect Print every field of a .mota's manifest (debugging).\n" -" serve Serve a folder of .mota to a node over USB serial or WiFi (corrupt files are skipped).\n" -" keygen Generate an Ed25519 signing keypair.\n" -"\n" -"TYPICAL WORKFLOW\n" -" 1. Make a signing key once:\n" -" motatool keygen --out signer.key\n" -" 2. Package a firmware (identity is read from the firmware itself):\n" -" motatool build --fw firmware.bin --sign signer.key --out-dir ./motas\n" -" 3. Serve the folder to a node plugged in over USB:\n" -" motatool serve --dir ./motas --serial /dev/ttyUSB0\n"; -} - -static void help_build() { - std::cout << -"motatool build — package a firmware as a .mota update container.\n" -"\n" -"USAGE\n" -" motatool build --fw [--base ] [options]\n" -"\n" -"WHAT IT DOES\n" -" With only --fw: builds a FULL update (the payload is the whole firmware image).\n" -" With --base too: builds a DELTA (a small patch from the base firmware to the new one),\n" -" which is far smaller to send over LoRa. The delta codec is picked for the target hardware\n" -" automatically: nRF52 (single-slot) -> in-place, ESP32 (A/B slots) -> sequential.\n" -"\n" -" Firmware identity (target, version, hardware tag) is read automatically from the firmware's\n" -" EndF trailer, so you normally don't pass --target-*/--fw-version/--hw-id. Pass them only to\n" -" override, or for a raw .bin that has no EndF identity.\n" -"\n" -"INPUT (--fw is required)\n" -" --fw NEW firmware. A local path OR an http(s):// URL (downloaded with curl/wget).\n" -" A .bin is used as-is; a .hex (nRF52/STM32 build) is parsed to its flat image first.\n" -" --base previous firmware to diff against -> makes a delta (omit it for a full image).\n" -"\n" -"IDENTITY (optional — auto-read from the firmware's EndF)\n" -" --target-env PlatformIO env name (e.g. RAK_4631_repeater); hashed into the target id.\n" -" --target-id raw target id instead of --target-env (e.g. 0x04D413FD).\n" -" --fw-version firmware version (e.g. 1.16.0).\n" -" --hw-id hardware tag (e.g. RAK4631, Heltec_v3). Same tag = bootable-compatible;\n" -" a node refuses a .mota whose hw-id is for different hardware (brick-safety).\n" -"\n" -"DELTA OPTIONS\n" -" --codec full|sequential|inplace force the codec (default: full with no --base, else auto-from-hw).\n" -" --inplace-memory nRF52 in-place workspace size (default 0xAE000).\n" -" --inplace-segment nRF52 in-place erase/segment size (default 4096).\n" -" --detools the detools encoder to call (default: 'detools' on PATH). Needed only for deltas.\n" -" --force build the delta even if base and target hardware identities differ.\n" -"\n" -"SIGNING\n" -" --sign Ed25519 private key (hex or raw 32 bytes, from 'motatool keygen'). Signing lets a\n" -" node auto-install the update if it trusts the matching public key. Unsigned still works\n" -" for manual installs.\n" -"\n" -"OUTPUT\n" -" --out-dir where to write the .mota (default: current directory). Keep all your .mota in one\n" -" folder and point 'serve' at it. The file is auto-named:\n" -" __v__.mota\n" -" --out write to exactly this path instead (overrides --out-dir and the auto-name).\n" -"\n" -"EXAMPLES\n" -" # full image, signed, into ./motas\n" -" motatool build --fw firmware.bin --sign signer.key --out-dir ./motas\n" -"\n" -" # full image fetched straight from a release URL\n" -" motatool build --fw https://example.org/RAK_4631_repeater.bin --sign signer.key --out-dir ./motas\n" -"\n" -" # delta from the previous release (codec auto-selected from the hardware)\n" -" motatool build --fw new.bin --base old.bin --sign signer.key --out-dir ./motas\n" -"\n" -" # raw .bin with no EndF identity: supply it explicitly\n" -" motatool build --fw app.bin --hw-id Heltec_v3 --target-env Heltec_v3_repeater --fw-version 1.16.0\n"; -} - -static void help_verify() { - std::cout << -"motatool verify — check that .mota files are valid.\n" -"\n" -"USAGE\n" -" motatool verify [more.mota ...] [--pub ] [--base ]\n" -"\n" -"For each file it checks the structure, that the per-block hashes match the payload, the merkle\n" -"root, the full-image hash (for full images), and the Ed25519 signature (for signed containers).\n" -"It prints 'OK' or 'FAIL ' per file; the exit code is non-zero if any file fails. This is\n" -"the same validation 'serve' runs on a folder before serving.\n" -"\n" -"OPTIONS\n" -" --pub require the container to be signed by THIS public key (hex/raw, *.pub from keygen).\n" -" --base for a sequential delta, apply it to this base and confirm it rebuilds the image.\n" -" (Applies to every file given; full images ignore it; in-place deltas are skipped.)\n" -"\n" -"EXAMPLES\n" -" motatool verify ./motas/*.mota\n" -" motatool verify update.mota --pub signer.key.pub\n" -" motatool verify delta.mota --base old_firmware.bin\n"; -} - -static void help_inspect() { - std::cout << -"motatool inspect — print every field of a .mota's manifest.\n" -"\n" -"USAGE\n" -" motatool inspect \n" -"\n" -"Dumps the parsed manifest (versions, sizes, target/hardware, codec, merkle root, image hash,\n" -"base hash, signer key + signature, approval state, block count) — handy for debugging a package.\n" -"It does not validate integrity; use 'verify' for that.\n" -"\n" -"EXAMPLE\n" -" motatool inspect ./motas/RAK4631_04D413FD_v1.16.0_full_ABCD1234.mota\n"; -} - -static void help_serve() { - std::cout << -"motatool serve — serve a folder of .mota to a MeshCore node over USB serial or WiFi (TCP).\n" -"\n" -"USAGE\n" -" motatool serve --dir --serial [options] # over USB serial\n" -" motatool serve --dir --tcp [options] # over WiFi (ESP32 companion)\n" -"\n" -"It scans the folder for .mota files, validates each, and serves the valid ones to the node.\n" -"Corrupt/invalid files are reported and skipped — one bad file never stops the rest. The node\n" -"advertises them to the mesh as if it held them, and any node whose hardware matches can fetch\n" -"them. The relay is trustless: fetchers verify every block, so this host never needs the keys.\n" -"\n" -"OPTIONS (--dir and one of --serial / --tcp are required)\n" -" --dir folder of .mota to serve (searched recursively by default).\n" -" --serial the node's USB serial port (e.g. /dev/ttyUSB0 or /dev/ttyACM0).\n" -" --tcp the node's WiFi seeder address (default port 5001). This is a DEDICATED\n" -" port, separate from the companion port (5000), so serving doesn't disturb a\n" -" phone app connected to the node. The node auto-enables relaying on connect.\n" -" --baud serial speed (default 115200; --serial only).\n" -" --no-recursive serve only the top folder; don't descend into sub-folders.\n" -" --no-enable (--serial only) don't auto-send 'ota folder on'/'off' on the node's CLI.\n" -" --seed warm-start: stage this (similar) build's payload into each captured .part so an\n" -" `ota pull <#> folder validate` diffs it against the target's merkle leaves and pulls\n" -" only the differing blocks — capture a non-deterministic rebuild in seconds.\n" -" -v, --verbose log each request the node makes (COUNT / DESCRIBE / READ).\n" -"\n" -"Leave it running; press Ctrl-C to stop. Over serial it shares the USB cable with the node's text\n" -"console; over TCP it uses the node's dedicated seeder port. The node only pulls while fetching.\n" -"\n" -"EXAMPLES\n" -" motatool serve --dir ./motas --serial /dev/ttyUSB0 -v\n" -" motatool serve --dir ./motas --tcp 192.168.4.234 -v\n"; -} - -static void help_keygen() { - std::cout << -"motatool keygen — generate an Ed25519 signing keypair.\n" -"\n" -"USAGE\n" -" motatool keygen [--out ]\n" -"\n" -"Prints the public key. With --out it writes the private key to and the public key to\n" -".pub (hex). Sign updates with the private key ('build --sign '); trust the\n" -"public key on a node to let it auto-install updates signed by you.\n" -"\n" -"EXAMPLE\n" -" motatool keygen --out signer.key\n"; -} - -static std::string hex8(uint32_t v) { char x[9]; std::snprintf(x, 9, "%08X", v); return x; } - -// human-readable label for a target_id (its PlatformIO env name), or "N/A" if not in the known table -static std::string target_label(uint32_t t) { - std::string n = target_env_name(t); - return n.empty() ? "N/A" : n; -} - -static int cmd_verify(const Args& a) { - if (a.has("help")) { help_verify(); return 0; } - if (a.pos.empty()) { help_verify(); return 2; } - - std::vector expect_pub; - if (a.has("pub") && !load_priv(a.get("pub"), expect_pub)) { // load_priv accepts a 32-byte hex/raw key - std::cerr << "cannot load --pub key (expect 32-byte hex or raw)\n"; return 2; - } - std::vector base_img; - if (a.has("base")) { - std::vector b; std::string e = read_input(a.get("base"), b); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } - std::array bh; base_img = ensure_endf(b, parse_endf_ident(b), bh); - } - - int bad = 0; - for (const auto& f : a.pos) { - std::vector blob; - if (!read_file(f, blob)) { std::cout << "FAIL " << f << " : cannot read\n"; bad++; continue; } - auto probs = verify(blob); - Manifest m; bool parsed = parse(blob, m).empty(); - if (parsed && !expect_pub.empty()) { // --pub: must be signed by this exact key - if (!m.is_signed()) probs.push_back("not signed (but --pub was given)"); - else if (std::memcmp(m.signer.data(), expect_pub.data(), 32) != 0) probs.push_back("signed by a different key than --pub"); - } - if (parsed && !base_img.empty() && !m.is_full()) { // --base: prove a delta rebuilds the image - if (m.codec_id == CODEC_DETOOLS_SEQUENTIAL) { - std::vector patch(blob.begin() + m.payload_off(), blob.begin() + m.payload_off() + m.payload_size); - std::vector recon; - std::string e = detools_apply_seq(a.get("detools", "detools"), base_img, patch, recon); - if (!e.empty()) probs.push_back("delta apply: " + e); - else { auto h = mota::mh32(recon.data(), recon.size()); - if (std::memcmp(h.data(), m.image_hash.data(), 32) != 0) probs.push_back("delta does not rebuild image_hash against --base"); } - } else { - std::cout << "note " << f << " : in-place delta — --base apply-check skipped (bootloader-applied)\n"; - } - } - if (probs.empty()) { - std::cout << "OK " << f << " : " << (m.is_full() ? "full" : "delta") - << " target=" << hex8(m.target_id) << " [" << target_label(m.target_id) << "]" - << " v" << version_str(m.fw_version) << " hw=" << (m.hw_id_str().empty()? "?":m.hw_id_str()) - << " " << (m.is_signed() ? "signed" : "unsigned") - << " blocks=" << m.block_count << " size=" << blob.size() << "\n"; - } else { - bad++; - std::cout << "FAIL " << f << " :"; - for (auto& p : probs) std::cout << " [" << p << "]"; - std::cout << "\n"; - } - } - return bad ? 1 : 0; -} - -static int cmd_inspect(const Args& a) { - if (a.has("help")) { help_inspect(); return 0; } - if (a.pos.empty()) { help_inspect(); return 2; } - std::vector blob; - if (!read_file(a.pos[0], blob)) { std::cerr << "cannot read " << a.pos[0] << "\n"; return 1; } - Manifest m; - std::string e = parse(blob, m); - if (!e.empty()) { std::cerr << "not a valid .mota: " << e << "\n"; return 1; } - auto z = [](const uint8_t* p, size_t n){ for (size_t i=0;i in-place, else sequential) -static int infer_codec(const std::string& hw) { - std::string h; for (char c : hw) h.push_back((char)std::tolower((unsigned char)c)); - if (h.rfind("rak", 0) == 0 || h.find("nrf") != std::string::npos || h.find("nordic") != std::string::npos) - return CODEC_DETOOLS_INPLACE; - if (h.find("heltec") != std::string::npos || h.find("esp32") != std::string::npos || - h.find("xiao") != std::string::npos || h.find("tbeam") != std::string::npos || - h.find("tlora") != std::string::npos || h.find("tdeck") != std::string::npos) - return CODEC_DETOOLS_SEQUENTIAL; - return -1; // unknown -} - -static int cmd_build(const Args& a) { - if (a.has("help")) { help_build(); return 0; } - if (!a.has("fw")) { std::cerr << "error: --fw is required\n\n"; help_build(); return 2; } - BuildOpts o; - std::string e = read_input(a.get("fw"), o.fw); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } - if (a.has("base")) { - e = read_input(a.get("base"), o.base); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } - } - if (a.has("target-id")) { o.have_target = true; o.target_id = (uint32_t)std::strtoul(a.get("target-id").c_str(), nullptr, 0); } - else if (a.has("target-env")) { o.have_target = true; o.target_id = target_id_for_env(a.get("target-env")); } - if (a.has("fw-version")) { - uint32_t v; if (!pack_version(a.get("fw-version"), v)) { std::cerr << "bad --fw-version\n"; return 2; } - o.have_fwver = true; o.fw_version = v; - } - if (a.has("hw-id")) o.hw_id = a.get("hw-id"); - o.force = a.has("force"); - if (a.has("detools")) o.detools = a.get("detools"); - if (a.has("inplace-memory")) o.inplace_memory = (uint32_t)std::strtoul(a.get("inplace-memory").c_str(), nullptr, 0); - if (a.has("inplace-segment")) o.inplace_segment = (uint32_t)std::strtoul(a.get("inplace-segment").c_str(), nullptr, 0); - if (a.has("sign")) { - if (!load_priv(a.get("sign"), o.sign_priv)) { std::cerr << "cannot load signing key (expect 32-byte hex or raw)\n"; return 1; } - } - - bool is_delta = !o.base.empty(); - // resolve hw (flags override EndF) to pick the codec - FwIdent fid = parse_endf_ident(o.fw); - std::string hw = o.hw_id.empty() ? fid.hw_id : o.hw_id; - std::string codec = a.get("codec", is_delta ? "auto" : "full"); - if (!is_delta) o.codec = CODEC_FULL; - else if (codec == "sequential") o.codec = CODEC_DETOOLS_SEQUENTIAL; - else if (codec == "inplace") o.codec = CODEC_DETOOLS_INPLACE; - else if (codec == "full") { std::cerr << "a --base delta cannot use --codec full\n"; return 2; } - else { // auto - int c = infer_codec(hw); - if (c < 0) { std::cerr << "cannot infer codec from hw '" << hw << "' — pass --codec sequential|inplace\n"; return 2; } - o.codec = (uint8_t)c; - std::cerr << "note: codec auto-selected = " << (c == CODEC_DETOOLS_INPLACE ? "inplace" : "sequential") - << " (from hw '" << (hw.empty()?"?":hw) << "')\n"; - } - - std::vector blob; std::string name; - e = build(o, blob, name); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } - - // sanity: the tool's own output must verify - auto probs = verify(blob); - if (!probs.empty()) { - std::cerr << "internal error: built .mota fails verification:"; - for (auto& p : probs) std::cerr << " [" << p << "]"; - std::cerr << "\n"; return 1; - } - - std::string outpath; - if (a.has("out")) { // explicit output path (overrides --out-dir + auto-name) - outpath = a.get("out"); - fs::path parent = fs::path(outpath).parent_path(); - if (!parent.empty()) { std::error_code ec; fs::create_directories(parent, ec); } - } else { - std::string outdir = a.get("out-dir", "."); - std::error_code ec; fs::create_directories(outdir, ec); - outpath = (fs::path(outdir) / name).string(); - } - if (!write_file(outpath, blob.data(), blob.size())) { std::cerr << "cannot write " << outpath << "\n"; return 1; } - - Manifest m; parse(blob, m); - std::cout << "wrote " << outpath << "\n" - << " " << (m.is_full() ? "full" : (m.codec_id == CODEC_DETOOLS_INPLACE ? "in-place delta" : "sequential delta")) - << " target=" << [&]{char x[9];std::snprintf(x,9,"%08X",m.target_id);return std::string(x);}() - << " v" << version_str(m.fw_version) << " hw=" << (m.hw_id_str().empty()?"?":m.hw_id_str()) - << " " << (m.is_signed()?"signed":"unsigned") << "\n" - << " image=" << m.image_size << "B payload=" << m.payload_size << "B blocks=" << m.block_count - << " total=" << blob.size() << "B\n"; - return 0; -} - -static int cmd_keygen(const Args& a) { - if (a.has("help")) { help_keygen(); return 0; } - uint8_t priv[32], pub[32]; - if (!ed25519_keygen(priv, pub)) { std::cerr << "keygen failed\n"; return 1; } - std::string out = a.get("out", a.get("out-priv")); - std::string ph = to_hex(priv, 32), kh = to_hex(pub, 32); - if (!out.empty()) { - std::string pp = ph + "\n", kp = kh + "\n"; - if (!write_file(out, (const uint8_t*)pp.data(), pp.size()) || - !write_file(out + ".pub", (const uint8_t*)kp.data(), kp.size())) { std::cerr << "write failed\n"; return 1; } - std::cout << "private -> " << out << "\npublic -> " << out << ".pub\n"; - } - std::cout << "pubkey: " << kh << "\n"; - return 0; -} - -static int cmd_serve(const Args& a) { - if (a.has("help")) { help_serve(); return 0; } - bool use_tcp = a.has("tcp"); - if (!a.has("dir") || (!a.has("serial") && !use_tcp)) { - std::cerr << "error: --dir and one of --serial / --tcp are required\n\n"; help_serve(); return 2; - } - bool recursive = !a.has("no-recursive"); - bool verbose = a.has("verbose"); - Folder folder; - size_t n = folder.scan(a.get("dir"), recursive, - [](const std::string& p, const std::string& why) { - std::cerr << " ! skip " << p << " : " << why << "\n"; - }); - std::cout << "motatool serve: " << n << " valid .mota in " << a.get("dir") - << (recursive ? " (recursive)" : "") << "\n"; - for (const auto& s : folder.all()) { - std::cout << " - " << fs::path(s.path).filename().string() - << " : mid=" << to_hex(s.m.merkle_root.data(), 4) - << " target=" << hex8(s.m.target_id) << " [" << target_label(s.m.target_id) << "]" - << " v" << version_str(s.m.fw_version) - << " " << (s.m.is_full() ? "full" : (s.m.codec_id == CODEC_DETOOLS_INPLACE ? "ipdelta" : "seqdelta")) - << " " << (s.m.is_signed() ? "signed" : "unsigned") - << " blocks=" << s.m.block_count << " size=" << s.bytes.size() << "\n"; - } - if (n == 0) std::cerr << " (nothing valid to serve)\n"; - - // Pick the transport: a serial port, or a TCP connection to the node's WiFi seeder port (host[:port], - // default port 5001). The node runs the seeder on a DEDICATED port, separate from its companion port, - // so serving over WiFi doesn't disturb a phone app connected to the companion. - SerialTransport st; - TcpTransport tt; - Transport* t = nullptr; - std::string target; - if (use_tcp) { - std::string hp = a.get("tcp"); - size_t colon = hp.rfind(':'); - std::string host = (colon == std::string::npos) ? hp : hp.substr(0, colon); - int port = (colon == std::string::npos) ? 5001 : std::atoi(hp.substr(colon + 1).c_str()); - std::string e = tt.open(host, port); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } - t = &tt; target = host + ":" + std::to_string(port); - } else { - std::string e = st.open(a.get("serial"), std::atoi(a.get("baud", "115200").c_str())); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } - t = &st; target = a.get("serial") + " @ " + a.get("baud", "115200"); - } - - std::signal(SIGINT, on_sigint); - // The CLI auto-enable only applies to the serial console; the TCP seeder port auto-enables relaying on - // the node side when this connection opens (and stops when it closes), so there's nothing to send. - bool enable = !use_tcp && !a.has("no-enable"); - if (enable) { usleep(500000); t->write_str("ota folder on\r\n"); std::cout << "sent `ota folder on`\n"; } - std::cout << "serving on " << target << " — Ctrl-C to stop\n"; - - SeederCore core(folder, a.get("dir")); // same folder doubles as "pull to folder" storage - // Optional warm-start seed: a *similar* build's .mota. Its payload is dropped into each captured `.part` - // so a `ota pull … folder validate` diffs it against the target's leaves and pulls only the differing - // blocks (huge airtime saving when capturing a non-deterministic rebuild of the same firmware). - if (a.has("seed")) { - std::vector sb; - if (!read_file(a.get("seed"), sb)) { std::cerr << "error: cannot read seed " << a.get("seed") << "\n"; return 1; } - Manifest sm; std::string e = parse(sb, sm); - if (!e.empty()) { std::cerr << "error: bad seed .mota: " << e << "\n"; return 1; } - uint32_t poff = sm.payload_off(); - if ((uint64_t)poff + sm.payload_size > sb.size()) { std::cerr << "error: seed payload out of range\n"; return 1; } - core.set_seed(std::vector(sb.begin() + poff, sb.begin() + poff + sm.payload_size), sm.block_count); - std::cout << "seed: " << fs::path(a.get("seed")).filename().string() - << " mid=" << to_hex(sm.merkle_root.data(), 4) - << " blocks=" << sm.block_count << " payload=" << sm.payload_size - << " (staged into each capture for `ota pull … validate`)\n"; - } - serve_loop(*t, core, verbose, - [](const std::string& l) { std::cout << " [dev] " << l << "\n"; }, &g_stop); - - if (enable) { t->write_str("ota folder off\r\n"); usleep(200000); } - std::cout << "\nbye\n"; - return 0; -} - -int main(int argc, char** argv) { - if (argc < 2) { help_top(); return 2; } - std::string cmd = argv[1]; - if (cmd == "help" || cmd == "-h" || cmd == "--help") { help_top(); return 0; } - - std::vector bools = {"force","verbose","no-recursive","no-enable"}; - Args a = parse_args(argc, argv, 2, bools); - if (cmd == "build") return cmd_build(a); - if (cmd == "verify") return cmd_verify(a); - if (cmd == "inspect") return cmd_inspect(a); - if (cmd == "serve") return cmd_serve(a); - if (cmd == "keygen") return cmd_keygen(a); - std::cerr << "unknown command: " << cmd << "\n\n"; - help_top(); - return 2; -} diff --git a/tools/motatool/src/mota.cpp b/tools/motatool/src/mota.cpp deleted file mode 100644 index 87478bb1..00000000 --- a/tools/motatool/src/mota.cpp +++ /dev/null @@ -1,413 +0,0 @@ -#include "mota.h" - -#include "OtaTargets.h" // shared with the firmware (generated): target_id -> env name -#include "crypto.h" -#include "util.h" - -#include -#include -#include -#include - -namespace mota { - -// ---- merkle (mirror of src/helpers/ota/MerkleTree.cpp) -------------------------------------------- -static void merkle_leaf(uint8_t out[4], const uint8_t *block, size_t len) { - sha256_trunc(out, 4, block, len); -} -static void merkle_combine(uint8_t out[4], const uint8_t *left, const uint8_t *right) { - uint8_t buf[8]; - std::memcpy(buf, left, 4); - std::memcpy(buf + 4, right, 4); - sha256_trunc(out, 4, buf, 8); -} -// Root via binary-counter / Merkle-Mountain-Range with right-to-left bagging (must stay byte-identical to -// src/helpers/ota/MerkleTree.cpp; the native tests cross-check both against the Python reference). -// peaks[k] holds the root of a complete 2^k-leaf subtree; adding a leaf "carries" upward like incrementing -// a binary counter, then the leftover peaks are bagged right-to-left into the final root. -static void merkle_root(uint8_t out[4], const uint8_t *leaves, uint32_t count) { - if (count == 0) { - std::memset(out, 0, 4); - return; - } - if (count == 1) { - std::memcpy(out, leaves, 4); - return; - } - uint8_t peaks[32][4]; - bool valid[32] = { false }; - for (uint32_t i = 0; i < count; i++) { - uint8_t cur[4]; - std::memcpy(cur, leaves + (size_t)i * 4, 4); - uint32_t level = 0; - while (valid[level]) { // carry: combine with the pending peak at this level - merkle_combine(cur, peaks[level], cur); // peak is earlier (left), cur is right - valid[level] = false; - level++; - } - std::memcpy(peaks[level], cur, 4); - valid[level] = true; - } - // bag peaks right-to-left: acc starts at the lowest set level (rightmost peak) - int level = 0; - while (level < 32 && !valid[level]) - level++; - uint8_t acc[4]; - std::memcpy(acc, peaks[level], 4); - for (int l = level + 1; l < 32; l++) - if (valid[l]) merkle_combine(acc, peaks[l], acc); // higher peak is left, acc is right - std::memcpy(out, acc, 4); -} -// leaves[] over the payload (last block short, no padding) -static std::vector leaf_hashes(const uint8_t *payload, uint32_t size, uint32_t bs) { - uint32_t bc = (size + bs - 1) / bs; - std::vector leaves(bc * 4); - for (uint32_t i = 0; i < bc; i++) { - uint32_t off = i * bs, blen = (off + bs <= size) ? bs : (size - off); - merkle_leaf(leaves.data() + (size_t)i * 4, payload + off, blen); - } - return leaves; -} - -std::string Manifest::hw_id_str() const { - size_t n = 0; - while (n < hw_id.size() && hw_id[n]) - n++; - return std::string((const char *)hw_id.data(), n); -} - -// ---- parse ----------------------------------------------------------------------------------------- -std::string parse(const std::vector &b, Manifest &m) { - m = Manifest(); - if (b.size() < 8 + MOTA_MFL + 5) return "too small for a .mota"; - if (std::memcmp(b.data(), MOTA_MAGIC, 4) != 0) return "bad MAGIC (not a .mota)"; - uint32_t total = rd_u32(b.data() + 4); - if (total != b.size()) return "MOTA_TOTAL_SIZE != file length"; - if (std::memcmp(b.data() + b.size() - 5, MOTA_TRAILER, 5) != 0) return "bad TRAILER"; - const uint8_t *mf = b.data() + 8; // manifest start - m.format_ver = mf[M_OFF_FORMAT_VER]; - if (m.format_ver != FORMAT_VER) return "unsupported format_ver"; - m.flags = mf[M_OFF_FLAGS]; - m.hash_algo = mf[M_OFF_HASH_ALGO]; - m.target_id = rd_u32(mf + M_OFF_TARGET_ID); - m.fw_version = rd_u32(mf + M_OFF_FW_VERSION); - m.image_size = rd_u32(mf + M_OFF_IMAGE_SIZE); - m.payload_size = rd_u32(mf + M_OFF_PAYLOAD_SIZE); - m.block_size_log2 = mf[M_OFF_BLOCK_SIZE_LOG2]; - std::memcpy(m.merkle_root.data(), mf + M_OFF_MERKLE_ROOT, 4); - std::memcpy(m.image_hash.data(), mf + M_OFF_IMAGE_HASH, 32); - m.codec_id = mf[M_OFF_CODEC_ID]; - std::memcpy(m.hw_id.data(), mf + M_OFF_HW_ID, 32); - std::memcpy(m.base_hash.data(), mf + M_OFF_BASE_HASH, 8); - std::memcpy(m.signer.data(), mf + M_OFF_SIGNER, 32); - std::memcpy(m.signature.data(), mf + M_OFF_SIGNATURE, 64); - std::memcpy(m.approval.data(), mf + M_OFF_APPROVAL, 4); - if (m.block_size_log2 == 0 || m.block_size_log2 > 24 || m.payload_size == 0) - return "bad block_size/payload"; - m.block_count = (m.payload_size + m.block_size() - 1) / m.block_size(); - if (m.block_count == 0 || m.block_count > 0xFFFFu) return "block_count out of range"; - if (m.total_size() != b.size()) return "geometry (leaves+payload) != file length"; - return ""; -} - -// ---- verify ---------------------------------------------------------------------------------------- -std::vector verify(const std::vector &b) { - std::vector probs; - Manifest m; - std::string e = parse(b, m); - if (!e.empty()) { - probs.push_back(e); - return probs; - } // unparseable: a single, fatal problem - - const uint8_t *leaves = b.data() + m.leaves_off(); - const uint8_t *payload = b.data() + m.payload_off(); - - // recompute leaves[] from the payload -> catches payload corruption - std::vector calc = leaf_hashes(payload, m.payload_size, m.block_size()); - if (calc.size() != m.block_count * 4u || std::memcmp(calc.data(), leaves, calc.size()) != 0) - probs.push_back("leaves[] do not match the payload (corruption)"); - - uint8_t root[4]; - merkle_root(root, leaves, m.block_count); - if (std::memcmp(root, m.merkle_root.data(), 4) != 0) probs.push_back("merkle_root mismatch"); - - if (m.is_full()) { - auto h = mh32(payload, m.payload_size); // full: payload IS the image - if (std::memcmp(h.data(), m.image_hash.data(), 32) != 0) - probs.push_back("image_hash mismatch (full image)"); - } - // (delta image_hash needs the base image -> not checked at relay time; payload integrity is covered above) - - if (m.is_signed()) { - if (!ed25519_verify(m.signature.data(), b.data() + 8, MOTA_SIGNED_LEN, m.signer.data())) - probs.push_back("Ed25519 signature INVALID"); - } - // a distributed .mota must not be pre-approved - if (std::memcmp(m.approval.data(), APPROVAL_YES, 4) == 0) - probs.push_back("container is pre-approved (must be FF FF FF FF on the wire)"); - return probs; -} - -// ---- EndF ----------------------------------------------------------------------------------------- -bool has_endf(const std::vector &img) { - if (img.size() < ENDF_LEN) return false; - const uint8_t *t = img.data() + img.size() - ENDF_LEN; - if (std::memcmp(t, ENDF_MAGIC, 4) != 0) return false; - if (rd_u32(t + 4) != img.size() - ENDF_LEN) return false; - auto h = mh8(img.data(), img.size() - ENDF_LEN); - return std::memcmp(h.data(), t + 8, 8) == 0; -} - -FwIdent parse_endf_ident(const std::vector &img) { - FwIdent id; - if (!has_endf(img)) return id; - const uint8_t *t = img.data() + img.size() - ENDF_LEN; - id.fw_version = rd_u32(t + ENDF_OFF_FWVER); - id.target_id = rd_u32(t + ENDF_OFF_TARGET); - size_t n = 0; - while (n < HW_ID_LEN && t[ENDF_OFF_HWID + n]) - n++; - id.hw_id.assign((const char *)t + ENDF_OFF_HWID, n); - return id; -} - -std::vector ensure_endf(const std::vector &img, const FwIdent &id, - std::array &body_hash8) { - if (has_endf(img)) { // already trailed: keep its identity, read its hash - const uint8_t *t = img.data() + img.size() - ENDF_LEN; - std::memcpy(body_hash8.data(), t + 8, 8); - return img; - } - body_hash8 = mh8(img.data(), img.size()); - std::vector out = img; - out.insert(out.end(), ENDF_MAGIC, ENDF_MAGIC + 4); - uint8_t u[4]; - wr_u32(u, (uint32_t)img.size()); - out.insert(out.end(), u, u + 4); - out.insert(out.end(), body_hash8.begin(), body_hash8.end()); - wr_u32(u, id.fw_version); - out.insert(out.end(), u, u + 4); - wr_u32(u, id.target_id); - out.insert(out.end(), u, u + 4); - uint8_t hw[HW_ID_LEN] = { 0 }; - std::memcpy(hw, id.hw_id.data(), id.hw_id.size() < HW_ID_LEN ? id.hw_id.size() : HW_ID_LEN); - out.insert(out.end(), hw, hw + HW_ID_LEN); - return out; -} - -uint32_t target_id_for_env(const std::string &env) { - auto h = mh4((const uint8_t *)env.data(), env.size()); - return rd_u32(h.data()); -} - -std::string target_env_name(uint32_t target_id) { - // Exhaustive target_id -> env-name table, shared verbatim with the firmware and generated from the live - // PlatformIO config (every ENABLE_OTA env) by tools/mota/gen_targets.py. See OtaTargets.h. - const char *env = mesh::ota::ota_target_env_name(target_id); - return env ? env : ""; -} - -bool pack_version(const std::string &s, uint32_t &out) { - uint32_t parts[4] = { 0, 0, 0, 0 }; - int n = 0; - bool any = false; - std::stringstream ss(s); - std::string tok; - while (std::getline(ss, tok, '.') && n < 4) { - if (tok.empty()) return false; - for (char c : tok) - if (c < '0' || c > '9') return false; - parts[n++] = (uint32_t)std::strtoul(tok.c_str(), nullptr, 10); - any = true; - } - if (!any) return false; - out = ((parts[0] & 0xFF) << 24) | ((parts[1] & 0xFF) << 16) | ((parts[2] & 0xFF) << 8) | (parts[3] & 0xFF); - return true; -} - -// ---- build ---------------------------------------------------------------------------------------- -// detools delta encode: write base/new images to temp files, run the detools CLI, read the patch back. -static std::string detools_delta(const std::string &detools, uint8_t codec, - const std::vector &base_img, const std::vector &new_img, - uint32_t mem, uint32_t seg, std::vector &patch) { - char fb[] = "/tmp/motaXXXXXX", fn[] = "/tmp/motaXXXXXX", fp[] = "/tmp/motaXXXXXX"; - int a = mkstemp(fb), c = mkstemp(fn), d = mkstemp(fp); - if (a < 0 || c < 0 || d < 0) return "mkstemp failed"; - close(a); - close(c); - close(d); - std::string err; - if (!write_file(fb, base_img.data(), base_img.size()) || !write_file(fn, new_img.data(), new_img.size())) { - err = "writing temp images failed"; - } else { - std::vector argv; - if (codec == CODEC_DETOOLS_SEQUENTIAL) - argv = { detools, "create_patch", "-c", "crle", "-t", "sequential", fb, fn, fp }; - else - argv = { detools, - "create_patch_in_place", - "-c", - "crle", - "--memory-size", - std::to_string(mem), - "--segment-size", - std::to_string(seg), - fb, - fn, - fp }; - int rc = run_argv(argv, /*quiet=*/true); // hide detools' success chatter; errors keep stderr - if (rc == 127) - err = "could not run detools (install it / pass --detools )"; - else if (rc != 0) - err = "detools exited with code " + std::to_string(rc); - else if (!read_file(fp, patch) || patch.empty()) - err = "reading detools patch failed"; - } - unlink(fb); - unlink(fn); - unlink(fp); - return err; -} - -std::string detools_apply_seq(const std::string &detools, const std::vector &base_img, - const std::vector &patch, std::vector &out) { - char ff[] = "/tmp/motaXXXXXX", fpp[] = "/tmp/motaXXXXXX", ft[] = "/tmp/motaXXXXXX"; - int a = mkstemp(ff), c = mkstemp(fpp), d = mkstemp(ft); - if (a < 0 || c < 0 || d < 0) return "mkstemp failed"; - close(a); - close(c); - close(d); - std::string err; - if (!write_file(ff, base_img.data(), base_img.size()) || !write_file(fpp, patch.data(), patch.size())) { - err = "writing temp files failed"; - } else { - int rc = run_argv({ detools, "apply_patch", ff, fpp, ft }, /*quiet=*/true); // - if (rc == 127) - err = "could not run detools (install it / pass --detools )"; - else if (rc != 0) - err = "detools apply_patch exited with code " + std::to_string(rc); - else if (!read_file(ft, out) || out.empty()) - err = "reading reconstructed image failed"; - } - unlink(ff); - unlink(fpp); - unlink(ft); - return err; -} - -std::string build(const BuildOpts &o, std::vector &out, std::string &suggested_name) { - bool is_delta = !o.base.empty(); - - // resolve identity: explicit flags override the firmware's self-describing EndF - FwIdent fid = parse_endf_ident(o.fw); - uint32_t target = o.have_target ? o.target_id : fid.target_id; - uint32_t fwver = o.have_fwver ? o.fw_version : fid.fw_version; - std::string hw = !o.hw_id.empty() ? o.hw_id : fid.hw_id; - FwIdent ident{ fwver, target, hw }; - - std::array new_bh{}, base_bh{}; - std::vector new_img = ensure_endf(o.fw, ident, new_bh); - - uint8_t codec = o.codec; - std::vector payload; - std::array base_hash{}; - - if (!is_delta) { - codec = CODEC_FULL; - payload = new_img; // full: payload IS the image - } else { - if (codec == CODEC_FULL) return "a base image was given but --codec is full"; - FwIdent base_id = parse_endf_ident(o.base); - std::vector base_img = ensure_endf(o.base, base_id, base_bh); - base_hash = base_bh; - // cross-hardware delta guard (read from EndF identity, not filenames) - if (!o.force && base_id.any() && fid.any()) { - bool hw_ok = base_id.hw_id.empty() || fid.hw_id.empty() || base_id.hw_id == fid.hw_id; - bool tgt_ok = !base_id.target_id || !fid.target_id || base_id.target_id == fid.target_id; - if (!hw_ok || !tgt_ok) - return "base/target firmware identity differ (hw '" + base_id.hw_id + "' vs '" + fid.hw_id + - "') — refusing cross-hardware delta (use --force to override)"; - } - std::string e = - detools_delta(o.detools, codec, base_img, new_img, o.inplace_memory, o.inplace_segment, payload); - if (!e.empty()) return e; - } - - uint32_t bs = o.block_size, image_size = (uint32_t)new_img.size(); - std::vector leaves = leaf_hashes(payload.data(), (uint32_t)payload.size(), bs); - uint32_t bc = (uint32_t)leaves.size() / 4; - if (bc == 0 || bc > 0xFFFFu) return "payload yields an invalid block count"; - uint8_t root[4]; - merkle_root(root, leaves.data(), bc); - auto image_hash = mh32(new_img.data(), new_img.size()); - - bool signed_ = !o.sign_priv.empty(); - uint8_t flags = (is_delta ? 0 : MFLAG_FULL) | (signed_ ? MFLAG_SIGNED : 0); - - // assemble the fixed 197-byte manifest-minus-leaves - std::vector mf(MOTA_MFL, 0); - mf[M_OFF_FORMAT_VER] = FORMAT_VER; - mf[M_OFF_FLAGS] = flags; - mf[M_OFF_HASH_ALGO] = HASH_ALGO_SHA256; - wr_u32(mf.data() + M_OFF_TARGET_ID, target); - wr_u32(mf.data() + M_OFF_FW_VERSION, fwver); - wr_u32(mf.data() + M_OFF_IMAGE_SIZE, image_size); - wr_u32(mf.data() + M_OFF_PAYLOAD_SIZE, (uint32_t)payload.size()); - // block_size_log2 - { - uint32_t v = bs, l = 0; - while (v > 1) { - v >>= 1; - l++; - } - mf[M_OFF_BLOCK_SIZE_LOG2] = (uint8_t)l; - } - std::memcpy(mf.data() + M_OFF_MERKLE_ROOT, root, 4); - std::memcpy(mf.data() + M_OFF_IMAGE_HASH, image_hash.data(), 32); - mf[M_OFF_CODEC_ID] = codec; - std::memcpy(mf.data() + M_OFF_HW_ID, hw.data(), hw.size() < HW_ID_LEN ? hw.size() : HW_ID_LEN); - if (is_delta) std::memcpy(mf.data() + M_OFF_BASE_HASH, base_hash.data(), 8); // zero for full - if (signed_) { - uint8_t pub[32]; - if (o.sign_priv.size() != 32) return "signing key must be a 32-byte raw private seed"; - if (!ed25519_pub_from_priv(pub, o.sign_priv.data())) return "bad signing key"; - std::memcpy(mf.data() + M_OFF_SIGNER, pub, 32); - uint8_t sig[64]; - if (!ed25519_sign(sig, mf.data(), MOTA_SIGNED_LEN, o.sign_priv.data())) return "signing failed"; - std::memcpy(mf.data() + M_OFF_SIGNATURE, sig, 64); - } - std::memcpy(mf.data() + M_OFF_APPROVAL, APPROVAL_NOT, 4); - - // container = MAGIC(4) total(4) manifest leaves[] payload trailer(5) - uint32_t total = 8 + MOTA_MFL + bc * 4 + (uint32_t)payload.size() + 5; - // nRF52 in-place: the staged container sits below the apply workspace; if it's too big it overruns the - // workspace and the bootloader apply fails (DETOOLS_IO_FAILED). Warn so it's caught before shipping. - if (is_delta && codec == CODEC_DETOOLS_INPLACE && total > NRF52_MAX_INPLACE_MOTA) - std::fprintf(stderr, "warning: in-place delta is %u B, exceeding the nRF52 staging room (%u B) — it will " - "NOT apply on the device. Shrink the delta (smaller change) or use a smaller image.\n", - total, NRF52_MAX_INPLACE_MOTA); - out.clear(); - out.reserve(total); - out.insert(out.end(), MOTA_MAGIC, MOTA_MAGIC + 4); - uint8_t u[4]; - wr_u32(u, total); - out.insert(out.end(), u, u + 4); - out.insert(out.end(), mf.begin(), mf.end()); - out.insert(out.end(), leaves.begin(), leaves.end()); - out.insert(out.end(), payload.begin(), payload.end()); - out.insert(out.end(), MOTA_TRAILER, MOTA_TRAILER + 5); - - // suggested name: __v__.mota (descriptive + unique, one folder) - const char *kind = !is_delta ? "full" : (codec == CODEC_DETOOLS_INPLACE ? "ipdelta" : "seqdelta"); - char tgt[9]; - std::snprintf(tgt, sizeof(tgt), "%08X", target); - char vbuf[24]; - std::snprintf(vbuf, sizeof(vbuf), "%u.%u.%u", (fwver >> 24) & 0xFF, (fwver >> 16) & 0xFF, - (fwver >> 8) & 0xFF); - suggested_name = (hw.empty() ? std::string("fw") : hw) + "_" + tgt + "_v" + vbuf + "_" + kind + "_" + - to_hex(root, 4) + ".mota"; - return ""; -} - -} // namespace mota diff --git a/tools/motatool/src/mota.h b/tools/motatool/src/mota.h deleted file mode 100644 index e3024027..00000000 --- a/tools/motatool/src/mota.h +++ /dev/null @@ -1,83 +0,0 @@ -// `.mota` container: parse, integrity-verify, and build (full / detools-delta). EndF identity helpers. -// Mirrors tools/mota/motalib.py and src/helpers/ota/MotaContainer.cpp (the single source of truth). -#pragma once -#include "mota_format.h" -#include -#include -#include - -namespace mota { - -struct FwIdent { - uint32_t fw_version = 0; - uint32_t target_id = 0; - std::string hw_id; // NUL-trimmed - bool any() const { return fw_version || target_id || !hw_id.empty(); } -}; - -struct Manifest { - uint8_t format_ver = 0, flags = 0, hash_algo = 0, codec_id = 0, block_size_log2 = 0; - uint32_t target_id = 0, fw_version = 0, image_size = 0, payload_size = 0, block_count = 0; - std::array merkle_root{}; - std::array image_hash{}; - std::array hw_id{}; // raw 32 bytes (NUL-padded) - std::array base_hash{}; - std::array signer{}; - std::array signature{}; - std::array approval{}; - - bool is_full() const { return flags & MFLAG_FULL; } - bool is_signed() const { return flags & MFLAG_SIGNED; } - uint32_t block_size() const { return 1u << block_size_log2; } - uint32_t leaves_off() const { return 8 + MOTA_MFL; } - uint32_t payload_off() const { return leaves_off() + block_count * 4; } - uint32_t total_size() const { return payload_off() + payload_size + 5; } - std::string hw_id_str() const; -}; - -// Parse + validate framing and the fixed layout. Returns "" on success (fills `out`), else an error. -std::string parse(const std::vector& blob, Manifest& out); - -// Content-integrity check: recompute leaves[] from the payload vs merkle_root, the merkle root, the -// FULL image_hash, and (if signed) the Ed25519 signature against the embedded signer key. Returns a list -// of problems (empty => valid). A delta's image_hash is not checked here (it needs the base image). -std::vector verify(const std::vector& blob); - -// ---- EndF identity (fixed 56-byte trailer) ---- -bool has_endf(const std::vector& image); -FwIdent parse_endf_ident(const std::vector& image); // zeros if no EndF -// Append a 56-byte EndF (with identity) if absent; returns the image and sets body_hash8. Idempotent. -std::vector ensure_endf(const std::vector& image, const FwIdent& id, - std::array& body_hash8); - -uint32_t target_id_for_env(const std::string& env); // sha2-256:4(env) as LE uint32 -bool pack_version(const std::string& s, uint32_t& out); // "1.16.0[.pre]" -> packed uint32 - -// Reverse-lookup a target_id to its PlatformIO env name from a static table of known OTA-capable envs -// (target_id = sha2-256:4(env_name)). Returns "" if not in the table. -std::string target_env_name(uint32_t target_id); - -// ---- build ---- -struct BuildOpts { - std::vector fw; // NEW firmware (raw or already-EndF'd) - std::vector base; // base image for a delta (empty => full) - uint8_t codec = CODEC_FULL; // CODEC_FULL / _SEQUENTIAL / _INPLACE - bool have_target = false; uint32_t target_id = 0; // overrides EndF - bool have_fwver = false; uint32_t fw_version = 0; // overrides EndF - std::string hw_id; // override; else from EndF - std::vector sign_priv; // 32-byte raw private seed (empty => unsigned) - uint32_t block_size = DEFAULT_BLOCK_SIZE; - uint32_t inplace_memory = NRF52_INPLACE_MEMORY, inplace_segment = NRF52_INPLACE_SEGMENT; - std::string detools = "detools"; // detools CLI path (delta encoding only) - bool force = false; // override the cross-hardware delta guard -}; -// Returns "" + fills `out` and a suggested file name on success; else an error string. -std::string build(const BuildOpts& o, std::vector& out, std::string& suggested_name); - -// Apply a SEQUENTIAL delta `patch` to `base_img` via the detools CLI -> reconstructed image in `out`. -// Used by `verify --base` to confirm a delta actually rebuilds the expected image (matches mota.py: -// in-place deltas aren't apply-checked here — the bootloader host-harness covers that path). -std::string detools_apply_seq(const std::string& detools, const std::vector& base_img, - const std::vector& patch, std::vector& out); - -} // namespace mota diff --git a/tools/motatool/src/mota_format.h b/tools/motatool/src/mota_format.h deleted file mode 100644 index 8a46c10f..00000000 --- a/tools/motatool/src/mota_format.h +++ /dev/null @@ -1,106 +0,0 @@ -// MeshCore `.mota` on-wire constants and fixed layout — a C++-friendly mirror of -// src/helpers/ota/OtaFormat.h. Keep byte-identical with that file and docs/ota_protocol.md. -// -// The format is FIXED-LAYOUT: every manifest field is at a constant offset and always present -// (base_hash/signer_pubkey/signature are zero-filled when not applicable). Only leaves[] varies. -#pragma once -#include -#include - -namespace mota { - -// container framing -static constexpr uint8_t MOTA_MAGIC[4] = {'m','O','T','A'}; -static constexpr uint8_t MOTA_TRAILER[5] = {'v','k','4','9','6'}; -static constexpr uint8_t ENDF_MAGIC[4] = {'E','n','d','F'}; - -static constexpr uint8_t HASH_ALGO_SHA256 = 0x12; -static constexpr uint8_t FORMAT_VER = 0x02; - -static constexpr uint8_t MFLAG_FULL = 0x01; -static constexpr uint8_t MFLAG_SIGNED = 0x02; - -static constexpr uint8_t CODEC_FULL = 0; -static constexpr uint8_t CODEC_DETOOLS_SEQUENTIAL = 1; // ESP32 A/B -static constexpr uint8_t CODEC_DETOOLS_INPLACE = 2; // nRF52 single-slot - -// hash truncations (sha2-256:N = first N bytes of the SHA-256 digest) -static constexpr size_t MH4 = 4, MH8 = 8, MH32 = 32; - -static constexpr uint8_t APPROVAL_NOT[4] = {0xFF,0xFF,0xFF,0xFF}; -static constexpr uint8_t APPROVAL_YES[4] = {'A','P','R','V'}; - -// EndF trailer (fixed 56 bytes): marker(4) body_len(4) body_hash8(8) fw_version(4) target_id(4) hw_id(32) -static constexpr uint32_t ENDF_LEN = 56; -static constexpr uint32_t ENDF_OFF_FWVER = 16; -static constexpr uint32_t ENDF_OFF_TARGET = 20; -static constexpr uint32_t ENDF_OFF_HWID = 24; - -static constexpr uint8_t HW_ID_LEN = 32; - -// manifest fixed offsets (within the manifest, i.e. container offset = 8 + these) -static constexpr uint32_t M_OFF_FORMAT_VER = 0; -static constexpr uint32_t M_OFF_FLAGS = 1; -static constexpr uint32_t M_OFF_HASH_ALGO = 2; -static constexpr uint32_t M_OFF_TARGET_ID = 3; -static constexpr uint32_t M_OFF_FW_VERSION = 7; -static constexpr uint32_t M_OFF_IMAGE_SIZE = 11; -static constexpr uint32_t M_OFF_PAYLOAD_SIZE = 15; -static constexpr uint32_t M_OFF_BLOCK_SIZE_LOG2 = 19; -static constexpr uint32_t M_OFF_MERKLE_ROOT = 20; // 4 -static constexpr uint32_t M_OFF_IMAGE_HASH = 24; // 32 -static constexpr uint32_t M_OFF_CODEC_ID = 56; // 1 -static constexpr uint32_t M_OFF_HW_ID = 57; // 32 -static constexpr uint32_t M_OFF_BASE_HASH = 89; // 8 (zero if FULL) -static constexpr uint32_t M_OFF_SIGNER = 97; // 32 (zero if unsigned) -static constexpr uint32_t M_OFF_SIGNATURE = 129; // 64 (zero if unsigned) -static constexpr uint32_t M_OFF_APPROVAL = 193; // 4 -static constexpr uint32_t MOTA_MFL = 197; // manifest-minus-leaves length (constant) -static constexpr uint32_t MOTA_SIGNED_LEN = 129; // signature covers manifest[0, 129) - -static constexpr uint32_t DEFAULT_BLOCK_SIZE = 1024; -static constexpr uint8_t DEFAULT_BLOCK_SIZE_LOG2 = 10; - -// nRF52 in-place apply workspace — MUST match src/helpers/ota/OtaFlashLayout_nrf52.h -// (MOTA_NRF52_INPLACE_MEMORY). It is NOT the full [APP_BASE, FS_START) span: the staged .mota itself sits -// just below FS_START, so the bootloader's workspace ends at the staged container (ws_hi = mota_addr), not -// at FS_START. The workspace is [APP_BASE, 0xBE000) = 0x98000, leaving 0xBE000..0xD4000 (~88 KB) for the -// staged delta. A patch built with a larger memory_size overruns the workspace at apply -> DETOOLS_IO_FAILED -// (the apply silently fails and the device just reboots). Reproduced + verified by the bootloader apply -// simulation (Adafruit_nRF52_Bootloader_OTAFIX/test/apply_sim). -static constexpr uint32_t NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB: [APP_BASE, 0xBE000) -static constexpr uint32_t NRF52_INPLACE_SEGMENT = 4096; -// The staged .mota sits in [APP_BASE+memory, FS_START); a larger container would push its start below the -// workspace end and break the apply the same way. Warn (motatool) / fail (bootloader) past this. -static constexpr uint32_t NRF52_FLASH_SPAN = 0x000D4000u - 0x00026000u; // 0xAE000 -static constexpr uint32_t NRF52_MAX_INPLACE_MOTA = NRF52_FLASH_SPAN - NRF52_INPLACE_MEMORY; // 0x16000 (~90 KB) -static_assert(NRF52_INPLACE_MEMORY < NRF52_FLASH_SPAN, "in-place workspace must leave room below FS_START for the staged .mota"); - -// ---- mota-seeder transport protocol (mirror of src/helpers/ota/MotaSeederProto.h) ---- -// Request (client -> server): 'M' 'S' op(1) args... xsum(1 = XOR of op+args) -// Response (server -> client): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior) -static constexpr uint8_t MS_REQ_MAGIC0 = 'M', MS_REQ_MAGIC1 = 'S'; -static constexpr uint8_t MS_RSP_MAGIC0 = 'm', MS_RSP_MAGIC1 = 's'; -static constexpr uint8_t MS_OP_COUNT = 0x01; // -> count(1) -static constexpr uint8_t MS_OP_DESCRIBE = 0x02; // idx(1) -> MotaDesc(38) -static constexpr uint8_t MS_OP_READ = 0x03; // idx(1) off(4) len(2) -> bytes -static constexpr uint8_t MS_OP_STAT = 0x04; // storage: mid(4) -> present(1) total(4) -static constexpr uint8_t MS_OP_BEGIN = 0x05; // storage: mid(4) total(4) -> OK (create 0xFF-filled) -static constexpr uint8_t MS_OP_WRITE = 0x06; // storage: mid(4) off(4) len(2) data(len) -> OK -static constexpr uint8_t MS_OP_SREAD = 0x07; // storage: mid(4) off(4) len(2) -> bytes (0xFF=unwritten) -static constexpr uint8_t MS_OP_FIN = 0x08; // storage: mid(4) -> OK (validate + make servable) -static constexpr uint8_t MS_STATUS_OK = 0x00; -static constexpr uint8_t MS_STATUS_ERR = 0x01; -static constexpr uint16_t MOTA_DESC_WIRE = 38; -static constexpr uint16_t MOTA_SEEDER_WRITE_MAX = 512; // max data bytes per OP_WRITE/OP_SREAD -// MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1) total_size(4) -// leaves_off(4) block_count(4) payload_off(4) payload_size(4) [+2 reserved] - -inline uint32_t rd_u32(const uint8_t* p) { - return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); -} -inline void wr_u32(uint8_t* p, uint32_t v) { - p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); -} - -} // namespace mota diff --git a/tools/motatool/src/serve.cpp b/tools/motatool/src/serve.cpp deleted file mode 100644 index 4430217a..00000000 --- a/tools/motatool/src/serve.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include "serve.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "util.h" - -namespace fs = std::filesystem; - -namespace mota { - -// ---- Folder: recursive scan + per-file validation ------------------------------------------------- -size_t Folder::scan(const std::string& dir, bool recursive, - const std::function& warn) { - motas_.clear(); - std::error_code ec; - auto consider = [&](const fs::path& p) { - if (p.extension() != ".mota") return; // skip non-.mota files - ServedMota sm; sm.path = p.string(); - if (!read_file(sm.path, sm.bytes)) { warn(sm.path, "cannot read"); return; } - auto probs = verify(sm.bytes); // merkle + image_hash + signature - if (!probs.empty()) { - std::string why = probs[0]; - for (size_t i = 1; i < probs.size(); i++) why += "; " + probs[i]; - warn(sm.path, why); // warn + exclude (don't sink the rest) - return; - } - parse(sm.bytes, sm.m); // already validated above - motas_.push_back(std::move(sm)); - }; - if (recursive) { - for (auto it = fs::recursive_directory_iterator(dir, ec); - !ec && it != fs::recursive_directory_iterator(); it.increment(ec)) - if (it->is_regular_file(ec)) consider(it->path()); - } else { - for (auto it = fs::directory_iterator(dir, ec); - !ec && it != fs::directory_iterator(); it.increment(ec)) - if (it->is_regular_file(ec)) consider(it->path()); - } - // stable, deterministic catalog order (indices are how the device addresses motas) - std::sort(motas_.begin(), motas_.end(), - [](const ServedMota& a, const ServedMota& b) { return a.path < b.path; }); - return motas_.size(); -} - -// ---- SeederCore (transport-agnostic) -------------------------------------------------------------- -std::array SeederCore::describe(const ServedMota& s) { - std::array w{}; - std::memcpy(w.data(), s.m.merkle_root.data(), 4); // mid - wr_u32(w.data() + 4, s.m.target_id); - wr_u32(w.data() + 8, s.m.fw_version); - w[12] = s.m.codec_id; - w[13] = s.m.flags; - wr_u32(w.data() + 14, (uint32_t)s.bytes.size()); // total_size - wr_u32(w.data() + 18, s.m.leaves_off()); - wr_u32(w.data() + 22, s.m.block_count); - wr_u32(w.data() + 26, s.m.payload_off()); - wr_u32(w.data() + 30, s.m.payload_size); - // [34..38) reserved 0 - return w; -} - -std::string SeederCore::store_path(const uint8_t mid[4], bool part) const { - static const char* H = "0123456789abcdef"; - std::string name; - for (int i = 0; i < 4; i++) { name += H[mid[i] >> 4]; name += H[mid[i] & 0xF]; } - name += part ? ".mota.part" : ".mota"; - return store_dir_ + "/" + name; -} - -bool SeederCore::handle(uint8_t op, const uint8_t* args, size_t arglen, - uint8_t& status, std::vector& payload) const { - payload.clear(); - status = MS_STATUS_OK; - if (op == MS_OP_COUNT) { - payload.push_back((uint8_t)(folder_.count() > 255 ? 255 : folder_.count())); - return true; - } - if (op == MS_OP_DESCRIBE) { - if (arglen < 1) return false; - const ServedMota* s = folder_.at(args[0]); - if (!s) { status = MS_STATUS_ERR; return true; } - auto w = describe(*s); - payload.assign(w.begin(), w.end()); - return true; - } - if (op == MS_OP_READ) { - if (arglen < 7) return false; - const ServedMota* s = folder_.at(args[0]); - uint32_t off = rd_u32(args + 1); - uint16_t len = (uint16_t)(args[5] | (args[6] << 8)); - if (!s || (uint64_t)off + len > s->bytes.size()) { status = MS_STATUS_ERR; return true; } - payload.assign(s->bytes.begin() + off, s->bytes.begin() + off + len); - return true; - } - // --- STORAGE ops: "pull to folder" — capture a .mota the device is fetching off-mesh into . - // All keyed by mid[4]; a partial pull is .mota.part, published to .mota on OP_FIN. --- - if (op == MS_OP_STAT || op == MS_OP_BEGIN || op == MS_OP_WRITE || op == MS_OP_SREAD || op == MS_OP_FIN) { - if (store_dir_.empty() || arglen < 4) { status = MS_STATUS_ERR; return true; } - const std::string part = store_path(args, true), done = store_path(args, false); - - if (op == MS_OP_STAT) { // present + size (completed wins over partial) - struct stat sx{}; uint8_t present = 0; uint32_t total = 0; - if (::stat(done.c_str(), &sx) == 0) { present = 1; total = (uint32_t)sx.st_size; } - else if (::stat(part.c_str(), &sx) == 0) { present = 1; total = (uint32_t)sx.st_size; } - payload.push_back(present); - uint8_t tb[4]; wr_u32(tb, total); payload.insert(payload.end(), tb, tb + 4); - return true; - } - if (op == MS_OP_BEGIN) { // fresh 0xFF-filled partial (start from 0) - if (arglen < 8) { status = MS_STATUS_ERR; return true; } - uint32_t total = rd_u32(args + 4); - int fd = ::open(part.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd < 0) { status = MS_STATUS_ERR; return true; } - uint8_t ff[4096]; memset(ff, 0xFF, sizeof ff); - uint32_t left = total; bool ok = true; - while (left && ok) { uint32_t c = left < sizeof ff ? left : (uint32_t)sizeof ff; ok = ::write(fd, ff, c) == (ssize_t)c; left -= c; } - // Warm-start seed: drop a similar build's payload into the payload region so the device's leaf-diff - // (`ota pull … validate`) keeps the blocks that match the target and pulls only the differing ones. - // Header + leaves stay 0xFF (the device writes the real manifest; leaves mark "present" as they pass). - if (ok && !seed_payload_.empty()) { - uint32_t payload_off = 8 + MOTA_MFL + seed_bc_ * 4; - uint32_t n = (uint32_t)seed_payload_.size(); - if (payload_off + n > total) n = (payload_off < total) ? total - payload_off : 0; // clamp to the file - if (n) ok = ::pwrite(fd, seed_payload_.data(), n, payload_off) == (ssize_t)n; - } - ::close(fd); - status = ok ? MS_STATUS_OK : MS_STATUS_ERR; - return true; - } - if (op == MS_OP_WRITE) { - if (arglen < 10) { status = MS_STATUS_ERR; return true; } - uint32_t off = rd_u32(args + 4); - uint16_t len = (uint16_t)(args[8] | (args[9] << 8)); - if (arglen < (size_t)10 + len) { status = MS_STATUS_ERR; return true; } - int fd = ::open(part.c_str(), O_WRONLY); - if (fd < 0) { status = MS_STATUS_ERR; return true; } - bool ok = ::pwrite(fd, args + 10, len, off) == (ssize_t)len; - ::close(fd); - status = ok ? MS_STATUS_OK : MS_STATUS_ERR; - return true; - } - if (op == MS_OP_SREAD) { // read back (resume: recompute missing blocks) - if (arglen < 10) { status = MS_STATUS_ERR; return true; } - uint32_t off = rd_u32(args + 4); - uint16_t len = (uint16_t)(args[8] | (args[9] << 8)); - const std::string src = (::access(part.c_str(), F_OK) == 0) ? part : done; - int fd = ::open(src.c_str(), O_RDONLY); - if (fd < 0) { status = MS_STATUS_ERR; return true; } - payload.resize(len); - bool ok = ::pread(fd, payload.data(), len, off) == (ssize_t)len; - ::close(fd); - if (!ok) { payload.clear(); status = MS_STATUS_ERR; } - return true; - } - if (op == MS_OP_FIN) { // light-validate (MAGIC + size) then publish - struct stat sx{}; - if (::stat(part.c_str(), &sx) != 0) { status = MS_STATUS_ERR; return true; } - uint8_t hd[8] = {0}; int fd = ::open(part.c_str(), O_RDONLY); - bool got = fd >= 0 && ::read(fd, hd, 8) == 8; - if (fd >= 0) ::close(fd); - bool magic = got && hd[0] == 'm' && hd[1] == 'O' && hd[2] == 'T' && hd[3] == 'A'; - if (!magic || rd_u32(hd + 4) != (uint32_t)sx.st_size) { status = MS_STATUS_ERR; return true; } - std::error_code ec; fs::rename(part, done, ec); - status = ec ? MS_STATUS_ERR : MS_STATUS_OK; - return true; - } - } - return false; // unknown op -> ignore (device retries) -} - -// ---- SerialTransport ------------------------------------------------------------------------------ -static speed_t baud_const(int b) { - switch (b) { - case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; - case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; - case 460800: return B460800; case 921600: return B921600; default: return B115200; - } -} - -std::string SerialTransport::open(const std::string& dev, int baud) { - fd_ = ::open(dev.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK); - if (fd_ < 0) return "cannot open serial device: " + dev; - termios t{}; - if (tcgetattr(fd_, &t) != 0) { ::close(fd_); fd_ = -1; return "tcgetattr failed"; } - cfmakeraw(&t); - speed_t s = baud_const(baud); - cfsetispeed(&t, s); cfsetospeed(&t, s); - t.c_cflag |= (CLOCAL | CREAD); - t.c_cflag &= ~CRTSCTS; // no flow control (matches the daemon) - t.c_cc[VMIN] = 0; t.c_cc[VTIME] = 0; - if (tcsetattr(fd_, TCSANOW, &t) != 0) { ::close(fd_); fd_ = -1; return "tcsetattr failed"; } - return ""; -} - -SerialTransport::~SerialTransport() { if (fd_ >= 0) ::close(fd_); } - -int SerialTransport::read_byte(int timeout_ms) { - if (fd_ < 0) return -1; - fd_set rs; FD_ZERO(&rs); FD_SET(fd_, &rs); - timeval tv{ timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; - int r = select(fd_ + 1, &rs, nullptr, nullptr, &tv); - if (r <= 0) return -1; - uint8_t b; - ssize_t n = ::read(fd_, &b, 1); - return n == 1 ? (int)b : -1; -} - -bool SerialTransport::write(const uint8_t* p, size_t n) { - if (fd_ < 0) return false; - size_t off = 0; - while (off < n) { - ssize_t w = ::write(fd_, p + off, n - off); - if (w < 0) return false; - off += (size_t)w; - } - return true; -} - -// ---- TcpTransport --------------------------------------------------------------------------------- -std::string TcpTransport::open(const std::string& host, int port) { - addrinfo hints{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - addrinfo* res = nullptr; - std::string portstr = std::to_string(port); - if (getaddrinfo(host.c_str(), portstr.c_str(), &hints, &res) != 0 || !res) - return "cannot resolve host: " + host; - std::string err = "cannot connect to " + host + ":" + portstr; - for (addrinfo* p = res; p; p = p->ai_next) { - int fd = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (fd < 0) continue; - if (::connect(fd, p->ai_addr, p->ai_addrlen) == 0) { fd_ = fd; err.clear(); break; } - ::close(fd); - } - freeaddrinfo(res); - return err; -} - -TcpTransport::~TcpTransport() { if (fd_ >= 0) ::close(fd_); } - -int TcpTransport::read_byte(int timeout_ms) { - if (fd_ < 0) return -1; - fd_set rs; FD_ZERO(&rs); FD_SET(fd_, &rs); - timeval tv{ timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; - if (select(fd_ + 1, &rs, nullptr, nullptr, &tv) <= 0) return -1; - uint8_t b; - ssize_t n = ::recv(fd_, &b, 1, 0); - return n == 1 ? (int)b : -1; // 0 == peer closed -> treated as timeout/closed -} - -bool TcpTransport::write(const uint8_t* p, size_t n) { - if (fd_ < 0) return false; - size_t off = 0; - while (off < n) { - ssize_t w = ::send(fd_, p + off, n - off, 0); - if (w < 0) return false; - off += (size_t)w; - } - return true; -} - -// ---- seeder framing loop -------------------------------------------------------------------------- -static uint8_t xor_bytes(const uint8_t* p, size_t n, uint8_t seed = 0) { - uint8_t x = seed; for (size_t i = 0; i < n; i++) x ^= p[i]; return x; -} - -static bool read_exact(Transport& t, uint8_t* buf, size_t n) { - for (size_t i = 0; i < n; i++) { - int b = t.read_byte(500); - if (b < 0) return false; - buf[i] = (uint8_t)b; - } - return true; -} - -static void send_response(Transport& t, uint8_t op, uint8_t status, const std::vector& payload) { - std::vector frame; - frame.reserve(4 + payload.size() + 1); - frame.push_back(MS_RSP_MAGIC0); frame.push_back(MS_RSP_MAGIC1); - frame.push_back(op); frame.push_back(status); - frame.insert(frame.end(), payload.begin(), payload.end()); - frame.push_back(xor_bytes(frame.data(), frame.size())); // xsum over all prior bytes (incl. magic) - t.write(frame.data(), frame.size()); -} - -void serve_loop(Transport& t, const SeederCore& core, bool verbose, - const std::function& devline, - const volatile bool* stop) { - std::string line; - int prev = -1; - auto flush_line = [&]() { - while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back(); - if (!line.empty() && devline) devline(line); - line.clear(); - }; - while (!stop || !*stop) { - int b = t.read_byte(200); - if (b < 0) continue; - if (prev == MS_REQ_MAGIC0 && b == MS_REQ_MAGIC1) { // 'M''S' -> a request follows - prev = -1; - uint8_t op; - if (!read_exact(t, &op, 1)) continue; - // fixed header bytes per op; OP_WRITE additionally reads `len` data bytes after its 10-byte header - size_t hdr = (op == MS_OP_COUNT) ? 0 : (op == MS_OP_DESCRIBE) ? 1 : (op == MS_OP_READ) ? 7 - : (op == MS_OP_STAT || op == MS_OP_FIN) ? 4 : (op == MS_OP_BEGIN) ? 8 - : (op == MS_OP_SREAD || op == MS_OP_WRITE) ? 10 : SIZE_MAX; - if (hdr == SIZE_MAX) continue; // unknown op - uint8_t args[10 + MOTA_SEEDER_WRITE_MAX]; - if (hdr && !read_exact(t, args, hdr)) continue; - size_t arglen = hdr; - if (op == MS_OP_WRITE) { // variable payload: header(10) + data(len) - uint16_t dlen = (uint16_t)(args[8] | (args[9] << 8)); - if (dlen > MOTA_SEEDER_WRITE_MAX) continue; // guard a runaway frame - if (dlen && !read_exact(t, args + 10, dlen)) continue; - arglen = (size_t)10 + dlen; - } - uint8_t xs; - if (!read_exact(t, &xs, 1)) continue; - if (xs != xor_bytes(args, arglen, op)) continue; // bad checksum -> ignore; device retries - uint8_t status; std::vector payload; - if (!core.handle(op, args, arglen, status, payload)) continue; - send_response(t, op, status, payload); - if (verbose) { - if (op == MS_OP_COUNT) devline("COUNT -> " + std::to_string(payload.empty() ? 0 : payload[0])); - else if (op == MS_OP_DESCRIBE) devline("DESCRIBE " + std::to_string(args[0]) + (status ? " ERR" : " OK")); - else if (op == MS_OP_READ) devline("READ " + std::to_string(args[0]) + " @" + - std::to_string(rd_u32(args + 1)) + (status ? " ERR" : " OK")); - } - continue; - } - if (prev >= 0) { // confirmed device text (not a frame start) - line.push_back((char)prev); - if (prev == '\n') flush_line(); - if (line.size() > 512) flush_line(); // guard runaway lines - } - prev = b; - } -} - -} // namespace mota diff --git a/tools/motatool/src/serve.h b/tools/motatool/src/serve.h deleted file mode 100644 index f551da9d..00000000 --- a/tools/motatool/src/serve.h +++ /dev/null @@ -1,95 +0,0 @@ -// Serve a folder of .mota to a MeshCore node. Split into a transport-AGNOSTIC protocol core (reusable -// over USB-serial today and BLE/GATT in the future) and a serial byte-stream framing layer. -#pragma once -#include "mota.h" -#include "mota_format.h" -#include -#include -#include -#include - -namespace mota { - -struct ServedMota { - std::string path; - std::vector bytes; - Manifest m; -}; - -// Recursively collect every *.mota under `dir`, validate each, keep only the valid ones. Corrupt/invalid -// files are reported through `warn(path, reason)` and excluded — one bad file never sinks the rest. -class Folder { -public: - size_t scan(const std::string& dir, bool recursive, - const std::function& warn); - size_t count() const { return motas_.size(); } - const ServedMota* at(size_t i) const { return i < motas_.size() ? &motas_[i] : nullptr; } - const std::vector& all() const { return motas_; } -private: - std::vector motas_; -}; - -// Transport-agnostic seeder: turns a (op, args) request into a (status, payload) response. The BLE path -// would call this directly from a characteristic-write handler and notify the reply — no framing needed. -class SeederCore { -public: - // `store_dir` (optional): folder where the "pull to folder" storage ops capture a `.mota` a device is - // fetching off-mesh — the SAME --dir as serving. A partial pull is `.mota.part`; OP_FIN publishes - // it as `.mota`. Empty store_dir = storage ops are refused (serve-only). - explicit SeederCore(const Folder& f, std::string store_dir = "") - : folder_(f), store_dir_(std::move(store_dir)) {} - bool handle(uint8_t op, const uint8_t* args, size_t arglen, - uint8_t& status, std::vector& payload) const; - static std::array describe(const ServedMota& s); - // Warm-start seed (motatool folder-capture): a *similar* build's payload. On OP_BEGIN, after the 0xFF-fill, - // the seed payload is written into the new `.part` at its payload region so the device can validate matching - // blocks against the target's merkle leaves and pull DATA only for the ones that differ (`ota pull … validate`). - void set_seed(std::vector payload, uint32_t block_count) { - seed_payload_ = std::move(payload); seed_bc_ = block_count; - } -private: - std::string store_path(const uint8_t mid[4], bool part) const; // /.mota[.part] - const Folder& folder_; - std::string store_dir_; - std::vector seed_payload_; // similar-build payload injected on OP_BEGIN (empty = no seed) - uint32_t seed_bc_ = 0; // seed's block_count (fixes payload_off = 8 + MOTA_MFL + bc*4) -}; - -// A bidirectional byte link (the serial seeder needs this; BLE would reuse SeederCore directly). -struct Transport { - virtual ~Transport() {} - virtual int read_byte(int timeout_ms) = 0; // a byte 0..255, or -1 on timeout/closed - virtual bool write(const uint8_t* p, size_t n) = 0; - bool write_str(const std::string& s) { return write((const uint8_t*)s.data(), s.size()); } -}; - -class SerialTransport : public Transport { -public: - std::string open(const std::string& dev, int baud); // "" on success - ~SerialTransport() override; - int read_byte(int timeout_ms) override; - bool write(const uint8_t* p, size_t n) override; -private: - int fd_ = -1; -}; - -// TCP client transport: connect to a node's WiFi seeder port (a dedicated port, NOT the companion port), -// then run the same seeder framing over the socket. Lets `serve` feed a WiFi node the same way as serial. -class TcpTransport : public Transport { -public: - std::string open(const std::string& host, int port); // "" on success - ~TcpTransport() override; - int read_byte(int timeout_ms) override; - bool write(const uint8_t* p, size_t n) override; -private: - int fd_ = -1; -}; - -// Seeder framing loop (transport-agnostic): resync on 'M''S', verify the request checksum, dispatch to -// `core`, frame the reply. Device text/log lines sharing the wire are surfaced via `devline` (serial -// only; the TCP seeder port carries no log text). Runs until *stop becomes true. -void serve_loop(Transport& t, const SeederCore& core, bool verbose, - const std::function& devline, - const volatile bool* stop); - -} // namespace mota diff --git a/tools/motatool/src/util.h b/tools/motatool/src/util.h deleted file mode 100644 index 9e5e51af..00000000 --- a/tools/motatool/src/util.h +++ /dev/null @@ -1,77 +0,0 @@ -// Small host-side helpers: file I/O, subprocess exec (no shell), hex. Header-only. -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include - -namespace mota { - -inline bool read_file(const std::string& path, std::vector& out) { - std::ifstream f(path, std::ios::binary); - if (!f) return false; - out.assign(std::istreambuf_iterator(f), std::istreambuf_iterator()); - return (bool)f || f.eof(); -} - -inline bool write_file(const std::string& path, const uint8_t* data, size_t len) { - std::ofstream f(path, std::ios::binary | std::ios::trunc); - if (!f) return false; - f.write((const char*)data, (std::streamsize)len); - return (bool)f; -} - -// Run argv[0] with argv (no shell, so no quoting pitfalls). Returns the child exit code, or -1 to spawn. -// `quiet` redirects the child's stdout to /dev/null (its chatter), keeping stderr so errors still show. -inline int run_argv(const std::vector& argv, bool quiet = false) { - if (argv.empty()) return -1; - pid_t pid = fork(); - if (pid < 0) return -1; - if (pid == 0) { - if (quiet) { - int n = ::open("/dev/null", O_WRONLY); - if (n >= 0) { dup2(n, 1); if (n > 2) ::close(n); } - } - std::vector a; - for (auto& s : argv) a.push_back(const_cast(s.c_str())); - a.push_back(nullptr); - execvp(a[0], a.data()); - _exit(127); // exec failed - } - int st = 0; - if (waitpid(pid, &st, 0) < 0) return -1; - return WIFEXITED(st) ? WEXITSTATUS(st) : -1; -} - -inline std::string to_hex(const uint8_t* p, size_t n) { - static const char* H = "0123456789ABCDEF"; - std::string s; s.reserve(n * 2); - for (size_t i = 0; i < n; i++) { s.push_back(H[p[i] >> 4]); s.push_back(H[p[i] & 0xF]); } - return s; -} - -// Parse a hex string (optionally 0x-prefixed) into bytes. Returns false on odd length / bad char. -inline bool from_hex(const std::string& in, std::vector& out) { - std::string s = in; - if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s = s.substr(2); - if (s.size() % 2) return false; - out.clear(); - auto nib = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - }; - for (size_t i = 0; i < s.size(); i += 2) { - int hi = nib(s[i]), lo = nib(s[i + 1]); - if (hi < 0 || lo < 0) return false; - out.push_back((uint8_t)((hi << 4) | lo)); - } - return true; -} - -} // namespace mota diff --git a/tools/motatool/tests/test_motatool.cpp b/tools/motatool/tests/test_motatool.cpp deleted file mode 100644 index 371a5f63..00000000 --- a/tools/motatool/tests/test_motatool.cpp +++ /dev/null @@ -1,334 +0,0 @@ -// Unit tests for motatool's core logic (no external framework — a tiny built-in harness keeps the -// project self-contained). Run: ./build/motatool_tests or ctest --test-dir build --output-on-failure -#include "mota.h" -#include "crypto.h" -#include "serve.h" -#include "input.h" -#include "util.h" -#include "mota_format.h" -#include -#include -#include -#include - -namespace fs = std::filesystem; -using namespace mota; - -static int g_checks = 0, g_fail = 0; -static const char* g_test = ""; -#define CHECK(cond) do { g_checks++; if (!(cond)) { g_fail++; \ - std::cerr << " FAIL [" << g_test << "] " << __LINE__ << ": " #cond "\n"; } } while (0) - -// deterministic synthetic firmware body -static std::vector body(unsigned seed, size_t n) { - std::vector v(n); - uint32_t s = seed * 2654435761u + 1; - for (size_t i = 0; i < n; i++) { s = s * 1103515245u + 12345u; v[i] = (uint8_t)(s >> 16); } - return v; -} - -static bool detools_available() { - return run_argv({"detools", "--help"}, /*quiet=*/true) != 127; // 127 = exec failed (not installed) -} - -// --------------------------------------------------------------------------- -static void t_crypto() { - g_test = "crypto"; - // sha256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad - const char* abc = "abc"; - auto h4 = mh4((const uint8_t*)abc, 3); - auto h8 = mh8((const uint8_t*)abc, 3); - auto h32 = mh32((const uint8_t*)abc, 3); - std::vector exp; - from_hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", exp); - CHECK(std::memcmp(h4.data(), exp.data(), 4) == 0); - CHECK(std::memcmp(h8.data(), exp.data(), 8) == 0); - CHECK(std::memcmp(h32.data(), exp.data(), 32) == 0); - - uint8_t priv[32], pub[32], pub2[32]; - CHECK(ed25519_keygen(priv, pub)); - CHECK(ed25519_pub_from_priv(pub2, priv)); - CHECK(std::memcmp(pub, pub2, 32) == 0); - auto msg = body(1, 100); - uint8_t sig[64]; - CHECK(ed25519_sign(sig, msg.data(), msg.size(), priv)); - CHECK(ed25519_verify(sig, msg.data(), msg.size(), pub)); - msg[0] ^= 0xFF; // tampered message -> reject - CHECK(!ed25519_verify(sig, msg.data(), msg.size(), pub)); - msg[0] ^= 0xFF; - uint8_t other[32], opub[32]; ed25519_keygen(other, opub); - CHECK(!ed25519_verify(sig, msg.data(), msg.size(), opub)); // wrong key -> reject -} - -static void t_version_target() { - g_test = "version/target"; - uint32_t v; - CHECK(pack_version("1.16.0", v) && v == 0x01100000u); - CHECK(pack_version("2.0.0", v) && v == 0x02000000u); - CHECK(pack_version("1.16.0.2", v) && (v & 0xFF) == 2); - CHECK(!pack_version("", v)); - CHECK(!pack_version("x.y", v)); - // known target ids from the firmware builds (ties motatool's hashing to the device) - CHECK(target_id_for_env("Heltec_v3_repeater") == 0xd1b29b18u); - CHECK(target_id_for_env("RAK_4631_repeater") == 0x04d413fdu); - // reverse lookup table (human-readable target_id) -> env name, "" when unknown - CHECK(target_env_name(0xd1b29b18u) == "Heltec_v3_repeater"); - CHECK(target_env_name(0x04d413fdu) == "RAK_4631_repeater"); - CHECK(target_env_name(0xDEADBEEFu).empty()); - // the shared (generated) table must round-trip with our own hashing for a spread of OTA envs — - // this catches any drift between the table's stored ids and sha2-256:4(env) - for (const char* e : {"Tbeam_SX1262_repeater", "Xiao_C3_repeater", "ThinkNode_M2_room_server", - "Ebyte_EoRa-S3_companion_radio_ble", "Heltec_v3_companion_radio_usb"}) - CHECK(target_env_name(target_id_for_env(e)) == e); -} - -static void t_intel_hex() { - g_test = "intel hex"; - fs::path hx = fs::temp_directory_path() / "motatool_test.hex"; - // 3 data bytes (01 02 03) at addr 0, then EOF (checksum F7 = two's-complement of the byte sum) - std::string ok = ":03000000010203F7\n:00000001FF\n"; - write_file(hx.string(), (const uint8_t*)ok.data(), ok.size()); - std::vector out; - CHECK(read_input(hx.string(), out).empty()); - CHECK(out.size() == 3 && out[0] == 1 && out[1] == 2 && out[2] == 3); - // wrong checksum -> rejected - std::string bad = ":03000000010203F8\n:00000001FF\n"; - write_file(hx.string(), (const uint8_t*)bad.data(), bad.size()); - std::vector o2; - CHECK(!read_input(hx.string(), o2).empty()); - fs::remove(hx); -} - -static void t_endf() { - g_test = "endf"; - auto b = body(2, 1500); - std::array bh{}; - FwIdent id{0x01100000u, 0x04d413fdu, "RAK4631"}; - auto img = ensure_endf(b, id, bh); - CHECK(img.size() == b.size() + ENDF_LEN); - CHECK(has_endf(img)); - CHECK(std::memcmp(bh.data(), mh8(b.data(), b.size()).data(), 8) == 0); // body_hash is over BODY only - auto gi = parse_endf_ident(img); - CHECK(gi.fw_version == 0x01100000u && gi.target_id == 0x04d413fdu && gi.hw_id == "RAK4631"); - // idempotent: feeding an already-EndF'd image keeps it + reads the same hash - std::array bh2{}; - auto img2 = ensure_endf(img, FwIdent{}, bh2); - CHECK(img2 == img && bh2 == bh); - // zero identity -> still a 56-byte trailer, identity reads empty - std::array bz{}; - auto z = ensure_endf(b, FwIdent{}, bz); - CHECK(z.size() == b.size() + ENDF_LEN); - auto zi = parse_endf_ident(z); - CHECK(zi.fw_version == 0 && zi.target_id == 0 && zi.hw_id.empty()); - CHECK(!has_endf(std::vector{1,2,3})); // too short -} - -static BuildOpts full_opts(const std::vector& fw, const std::vector& priv = {}) { - BuildOpts o; o.fw = fw; o.codec = CODEC_FULL; - o.have_target = true; o.target_id = 0x04d413fdu; - o.have_fwver = true; o.fw_version = 0x01100000u; - o.hw_id = "RAK4631"; o.sign_priv = priv; - return o; -} - -static void t_build_full() { - g_test = "build full"; - auto fw = body(3, 5 * 1024 + 100); // 6 blocks @1024 - std::vector blob; std::string name; - CHECK(build(full_opts(fw), blob, name).empty()); - Manifest m; - CHECK(parse(blob, m).empty()); - CHECK(m.format_ver == FORMAT_VER && m.is_full() && !m.is_signed()); - CHECK(m.codec_id == CODEC_FULL); - CHECK(m.target_id == 0x04d413fdu && m.fw_version == 0x01100000u && m.hw_id_str() == "RAK4631"); - CHECK(m.block_count == 6); - // fixed layout offsets - CHECK(m.leaves_off() == 8 + MOTA_MFL && m.leaves_off() == 205); - CHECK(m.payload_off() == 205 + m.block_count * 4); - CHECK(m.total_size() == blob.size()); - // payload IS the image (body+EndF); image_hash matches - uint32_t poff = m.payload_off(); - auto img_h = mh32(blob.data() + poff, m.payload_size); - CHECK(std::memcmp(img_h.data(), m.image_hash.data(), 32) == 0); - CHECK(m.image_size == m.payload_size); - CHECK(verify(blob).empty()); // clean - CHECK(name.find(".mota") != std::string::npos && name.find("full") != std::string::npos); -} - -static void t_build_signed_and_tamper() { - g_test = "build signed/tamper"; - uint8_t priv[32], pub[32]; ed25519_keygen(priv, pub); - std::vector pv(priv, priv + 32); - auto fw = body(4, 3000); - std::vector blob; std::string name; - CHECK(build(full_opts(fw, pv), blob, name).empty()); - Manifest m; CHECK(parse(blob, m).empty()); - CHECK(m.is_signed()); - CHECK(std::memcmp(m.signer.data(), pub, 32) == 0); - CHECK(verify(blob).empty()); - // tamper a payload byte -> integrity fails - auto t1 = blob; t1[m.payload_off() + 10] ^= 0xFF; - CHECK(!verify(t1).empty()); - // tamper a signed-region byte (target_id @ manifest+3 = blob+11) -> signature invalid - auto t2 = blob; t2[8 + M_OFF_TARGET_ID] ^= 0xFF; - bool sig_flagged = false; - for (auto& p : verify(t2)) if (p.find("signature") != std::string::npos) sig_flagged = true; - CHECK(sig_flagged); -} - -static void t_corruption_and_approval() { - g_test = "corruption/approval"; - auto fw = body(5, 4096); - std::vector blob; std::string name; - build(full_opts(fw), blob, name); - Manifest m; parse(blob, m); - // flip a payload byte -> leaves/merkle/image_hash problems reported - auto c = blob; c[m.payload_off() + 1] ^= 0xFF; - CHECK(!verify(c).empty()); - // a pre-approved container must be flagged (approval is outside the signed region) - auto ap = blob; std::memcpy(ap.data() + 8 + M_OFF_APPROVAL, APPROVAL_YES, 4); - bool approved_flagged = false; - for (auto& p : verify(ap)) if (p.find("approved") != std::string::npos) approved_flagged = true; - CHECK(approved_flagged); -} - -static void t_parse_rejects() { - g_test = "parse rejects"; - auto fw = body(6, 2048); - std::vector blob; std::string name; build(full_opts(fw), blob, name); - Manifest m; - CHECK(!parse(std::vector(10, 0), m).empty()); // too small - auto bad = blob; bad[0] ^= 0xFF; CHECK(!parse(bad, m).empty()); // bad magic - bad = blob; bad[bad.size() - 1] ^= 0xFF; CHECK(!parse(bad, m).empty()); // bad trailer - bad = blob; bad[4] ^= 0xFF; CHECK(!parse(bad, m).empty()); // wrong total_size - bad = blob; bad[8 + M_OFF_FORMAT_VER] = 9; CHECK(!parse(bad, m).empty()); // bad format_ver -} - -static void t_delta() { - g_test = "delta"; - if (!detools_available()) { std::cerr << " SKIP [delta] detools not on PATH\n"; return; } - auto base_body = body(10, 4000); - auto new_body = base_body; - for (int i : {100, 101, 2000, 3999}) new_body[i] ^= 0x33; - auto tail = body(11, 250); - new_body.insert(new_body.end(), tail.begin(), tail.end()); - - for (uint8_t codec : {CODEC_DETOOLS_SEQUENTIAL, CODEC_DETOOLS_INPLACE}) { - BuildOpts o; o.fw = new_body; o.base = base_body; o.codec = codec; - o.have_target = true; o.target_id = 0x04d413fdu; o.have_fwver = true; o.fw_version = 0x02000000u; - o.hw_id = "RAK4631"; - std::vector blob; std::string name; - std::string e = build(o, blob, name); - CHECK(e.empty()); - if (!e.empty()) continue; - Manifest m; CHECK(parse(blob, m).empty()); - CHECK(!m.is_full() && m.codec_id == codec); - // base_hash == mh8 of the base BODY - CHECK(std::memcmp(m.base_hash.data(), mh8(base_body.data(), base_body.size()).data(), 8) == 0); - CHECK(verify(blob).empty()); - if (codec == CODEC_DETOOLS_SEQUENTIAL) { // round-trip: apply -> rebuilds the new image - std::array bh{}; - auto base_img = ensure_endf(base_body, FwIdent{}, bh); - std::vector patch(blob.begin() + m.payload_off(), blob.begin() + m.payload_off() + m.payload_size); - std::vector recon; - CHECK(detools_apply_seq("detools", base_img, patch, recon).empty()); - CHECK(std::memcmp(mh32(recon.data(), recon.size()).data(), m.image_hash.data(), 32) == 0); - } - } -} - -static void t_folder_and_seeder() { - g_test = "folder/seeder"; - fs::path dir = fs::temp_directory_path() / "motatool_test_folder"; - fs::remove_all(dir); fs::create_directories(dir / "sub"); - - // two valid motas (one in a sub-folder, to exercise recursion) + one corrupt - std::vector a, b; std::string n; - build(full_opts(body(20, 3000)), a, n); write_file((dir / "a.mota").string(), a.data(), a.size()); - build(full_opts(body(21, 6000)), b, n); write_file((dir / "sub" / "b.mota").string(), b.data(), b.size()); - auto bad = a; bad[a.size() / 2] ^= 0xFF; write_file((dir / "bad.mota").string(), bad.data(), bad.size()); - write_file((dir / "ignore.txt").string(), a.data(), 10); // non-.mota -> skipped silently - - int warns = 0; std::string warned_path; - Folder folder; - size_t kept = folder.scan(dir.string(), true, - [&](const std::string& p, const std::string&){ warns++; warned_path = p; }); - CHECK(kept == 2); // two valid kept - CHECK(warns == 1 && warned_path.find("bad.mota") != std::string::npos); // corrupt warned + excluded - - SeederCore core(folder); - uint8_t status; std::vector pl; - CHECK(core.handle(MS_OP_COUNT, nullptr, 0, status, pl) && status == MS_STATUS_OK && pl.size() == 1 && pl[0] == 2); - - uint8_t arg0[1] = {0}; - CHECK(core.handle(MS_OP_DESCRIBE, arg0, 1, status, pl) && status == MS_STATUS_OK && pl.size() == MOTA_DESC_WIRE); - const ServedMota* s0 = folder.at(0); - CHECK(std::memcmp(pl.data(), s0->m.merkle_root.data(), 4) == 0); // mid - CHECK(rd_u32(pl.data() + 4) == s0->m.target_id); - CHECK(rd_u32(pl.data() + 14) == (uint32_t)s0->bytes.size()); // total_size - CHECK(rd_u32(pl.data() + 18) == s0->m.leaves_off() && rd_u32(pl.data() + 18) == 205); - CHECK(rd_u32(pl.data() + 22) == s0->m.block_count); - CHECK(rd_u32(pl.data() + 26) == s0->m.payload_off()); - CHECK(rd_u32(pl.data() + 30) == s0->m.payload_size); - - uint8_t bad_idx[1] = {99}; - CHECK(core.handle(MS_OP_DESCRIBE, bad_idx, 1, status, pl) && status == MS_STATUS_ERR); - - // READ idx=0 off=0 len=8 -> first 8 bytes (MAGIC + total_size) - uint8_t rd[7] = {0, 0,0,0,0, 8,0}; - CHECK(core.handle(MS_OP_READ, rd, 7, status, pl) && status == MS_STATUS_OK && pl.size() == 8); - CHECK(std::memcmp(pl.data(), s0->bytes.data(), 8) == 0); - // READ past EOF -> error - uint8_t reof[7]; reof[0] = 0; wr_u32(reof + 1, (uint32_t)s0->bytes.size()); reof[5] = 16; reof[6] = 0; - CHECK(core.handle(MS_OP_READ, reof, 7, status, pl) && status == MS_STATUS_ERR); - - // --- STORAGE ops ("pull to folder"): STAT -> BEGIN -> WRITE -> SREAD -> FIN round-trip --- - fs::path sdir = dir / "store"; fs::create_directories(sdir); - SeederCore store(folder, sdir.string()); - uint8_t mid[4] = {0xDE, 0xAD, 0xBE, 0xEF}; - auto midpath = [&](bool part) { - static const char* H = "0123456789abcdef"; std::string nm; - for (int i = 0; i < 4; i++) { nm += H[mid[i] >> 4]; nm += H[mid[i] & 0xF]; } - nm += part ? ".mota.part" : ".mota"; return (sdir / nm).string(); - }; - // STAT before anything -> present=0 - CHECK(store.handle(MS_OP_STAT, mid, 4, status, pl) && status == MS_STATUS_OK && pl.size() == 5 && pl[0] == 0); - // BEGIN total=64 -> 0xFF-filled partial - uint8_t beg[8]; std::memcpy(beg, mid, 4); wr_u32(beg + 4, 64); - CHECK(store.handle(MS_OP_BEGIN, beg, 8, status, pl) && status == MS_STATUS_OK); - CHECK(store.handle(MS_OP_STAT, mid, 4, status, pl) && pl[0] == 1 && rd_u32(pl.data() + 1) == 64); - // WRITE the header (MAGIC + total) + a blob at offset 0 - const uint8_t blob[16] = {'m','O','T','A', 64,0,0,0, 1,2,3,4,5,6,7,8}; - uint8_t wr[10 + 16]; std::memcpy(wr, mid, 4); wr_u32(wr + 4, 0); wr[8] = 16; wr[9] = 0; std::memcpy(wr + 10, blob, 16); - CHECK(store.handle(MS_OP_WRITE, wr, 10 + 16, status, pl) && status == MS_STATUS_OK); - // SREAD back the 16 written bytes; an unwritten region reads 0xFF - uint8_t srd[10]; std::memcpy(srd, mid, 4); wr_u32(srd + 4, 0); srd[8] = 16; srd[9] = 0; - CHECK(store.handle(MS_OP_SREAD, srd, 10, status, pl) && pl.size() == 16 && std::memcmp(pl.data(), blob, 16) == 0); - wr_u32(srd + 4, 32); srd[8] = 8; srd[9] = 0; - CHECK(store.handle(MS_OP_SREAD, srd, 10, status, pl) && pl.size() == 8 && pl[0] == 0xFF && pl[7] == 0xFF); - // FIN -> validates + publishes .mota - CHECK(fs::exists(midpath(true)) && !fs::exists(midpath(false))); - CHECK(store.handle(MS_OP_FIN, mid, 4, status, pl) && status == MS_STATUS_OK); - CHECK(!fs::exists(midpath(true)) && fs::exists(midpath(false)) && fs::file_size(midpath(false)) == 64); - CHECK(store.handle(MS_OP_STAT, mid, 4, status, pl) && pl[0] == 1 && rd_u32(pl.data() + 1) == 64); - // storage refused on a serve-only core (empty store_dir) - CHECK(core.handle(MS_OP_STAT, mid, 4, status, pl) && status == MS_STATUS_ERR); - - fs::remove_all(dir); -} - -int main() { - t_crypto(); - t_version_target(); - t_intel_hex(); - t_endf(); - t_build_full(); - t_build_signed_and_tamper(); - t_corruption_and_approval(); - t_parse_rejects(); - t_delta(); - t_folder_and_seeder(); - - std::cout << (g_fail ? "FAILED " : "OK ") << (g_checks - g_fail) << "/" << g_checks << " checks passed\n"; - return g_fail ? 1 : 0; -}