mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-15 01:16:04 +00:00
Build inflater-capable RAK3401 bridge inventory
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build receive-DEFLATE RAK3401 bridge images for the historical mOTA chain.
|
||||
|
||||
The input commits are pinned because each image is a deliberate binary bridge.
|
||||
The script uses one temporary Git worktree, applies the reviewed backport
|
||||
patches plus the pinned receive-only transport shim without committing them,
|
||||
and emits a machine-readable image manifest.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT / "tools" / "mota"))
|
||||
|
||||
import motalib # noqa: E402
|
||||
|
||||
|
||||
ENV_NAME = "RAK_3401_repeater_lora_ota_no_external_sensors"
|
||||
VERSION_SUFFIX = "halo-keymind-cascade-mota-inflate"
|
||||
EXPECTED_TARGET_ID = 0x2FA509C1
|
||||
EXPECTED_HARDWARE = "RAK_3401"
|
||||
COMMON_PATCH = SCRIPT_DIR / "rak3401_terminal_ota_backport.patch"
|
||||
LEGACY_MESH_PATCH = SCRIPT_DIR / "rak3401_terminal_ota_mesh_legacy.patch"
|
||||
GUARDED_MESH_PATCH = SCRIPT_DIR / "rak3401_terminal_ota_mesh_guarded.patch"
|
||||
VERSION_PATCH = SCRIPT_DIR / "rak3401_four_component_endf_backport.patch"
|
||||
WORKSPACE_PATCH = SCRIPT_DIR / "rak3401_dynamic_workspace_backport.patch"
|
||||
SELECTIVE_OS_HOOK = SCRIPT_DIR / "rak3401_selective_os.py"
|
||||
INFLATE_ASSET_ROOT = SCRIPT_DIR / "rak3401_inflate_assets"
|
||||
INFLATE_SNAPSHOT_COMMIT = "add51bf00c46c15ef54318ca766a6daf08a147ee"
|
||||
|
||||
INFLATE_ASSETS = {
|
||||
"src/helpers/ota/OtaDeflate.cpp": (
|
||||
"OtaDeflate.cpp",
|
||||
"7cc464fc3c304cc65f52973593068c8d85a783853da121476c4bc2e196798e8e",
|
||||
),
|
||||
"src/helpers/ota/OtaDeflate.h": (
|
||||
"OtaDeflate.h",
|
||||
"f6cd5b0fe8b2164178881d93664df37722d6434f8ef61c37d08255bce12f842c",
|
||||
),
|
||||
"src/helpers/ota/OtaTinf.c": (
|
||||
"OtaTinf.c",
|
||||
"e1344c73f3ef7ea9b19ab1358990f7aed70b9011aa2b370b5e0872c6699d60e8",
|
||||
),
|
||||
"src/helpers/ota/tinf/LICENSE": (
|
||||
"tinf/LICENSE",
|
||||
"bf5d1b9b3eb2917e32e030d64dd963356b76751b41a7a432f1187bb199cb0b00",
|
||||
),
|
||||
"src/helpers/ota/tinf/README.meshcore.txt": (
|
||||
"tinf/README.meshcore.txt",
|
||||
"8d1f4ae316ff71c58efaf4f85ed8d0ef6d10729919c5db37565c50ccc64ea041",
|
||||
),
|
||||
"src/helpers/ota/tinf/tinf.h": (
|
||||
"tinf/tinf.h",
|
||||
"831b59414b3ddf3ecb2e5082a7d8e03b56b7eecc05385c868c0e6537e9c57941",
|
||||
),
|
||||
"src/helpers/ota/tinf/tinflate.c": (
|
||||
"tinf/tinflate.c",
|
||||
"f5bc68a32a50f7e34c63928a8c176e65a5dc496256338b758ed8b714e24a2982",
|
||||
),
|
||||
}
|
||||
|
||||
TRACKED_BACKPORT_FILES = (
|
||||
"platformio.ini",
|
||||
"src/Mesh.cpp",
|
||||
"src/helpers/ota/OtaContext.h",
|
||||
"src/helpers/ota/OtaFlashLayout_nrf52.h",
|
||||
"src/helpers/ota/OtaManager.cpp",
|
||||
"src/helpers/ota/OtaManager.h",
|
||||
"src/helpers/ota/OtaProtocol.h",
|
||||
"tools/mota/pio_endf.py",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Target:
|
||||
version: str
|
||||
source_commit: str
|
||||
bridge_stage: int = 0
|
||||
os_stage: int = 0
|
||||
os_stage_part: int = 0
|
||||
os_stage_parts: int = 0
|
||||
os_stage_subpart: int = 0
|
||||
os_stage_subparts: int = 0
|
||||
split_stage5: str = ""
|
||||
mymesh_opt: str = ""
|
||||
flood_rule_engine: bool = True
|
||||
|
||||
|
||||
TARGETS = (
|
||||
Target("1.16.7.10", "90abbd110ef4fa7c96fca30205bbc566bf5c966c", bridge_stage=6),
|
||||
Target("1.16.7.11", "90abbd110ef4fa7c96fca30205bbc566bf5c966c", bridge_stage=7),
|
||||
Target("1.16.7.12", "90abbd110ef4fa7c96fca30205bbc566bf5c966c", bridge_stage=8),
|
||||
Target("1.16.7.13", "804f45303ac615362ab56f44da0789301bb3de11"),
|
||||
Target("1.16.8.0", "cd89b5400cbc16b31a87079a793bc88600369c80"),
|
||||
Target("1.16.8.7", "1fa940aa3a5d98d41a7320a25d60798edeae4921"),
|
||||
Target("1.16.8.8", "bbc2c905a1befb83365cccec583c527a24ebb104"),
|
||||
Target("1.16.8.9", "bec98977389bae5116d675107fa6c11eeab1a3f7"),
|
||||
Target("1.16.9.0", "1f7f2a8034f0fc74b378dfb44126ef869c2a9344"),
|
||||
Target("1.16.9.102", "63ab53e4445d726945ce32d530252e34ecf6d1b1"),
|
||||
Target("1.16.9.104", "262542cf3411bfd093978c502ebb94ba3ba3cc0c", flood_rule_engine=False),
|
||||
Target("1.16.9.105", "352d70465276d66cda5cbda955aa3dfffee39bbb", flood_rule_engine=False),
|
||||
Target("1.16.9.108", "352d70465276d66cda5cbda955aa3dfffee39bbb"),
|
||||
Target("1.16.9.109", "03edc027b19086918f567561cab673fe3724b9b8"),
|
||||
Target("1.16.9.110", "07a624af00cea922ae495612b728e4abf51f9f87"),
|
||||
Target("1.16.9.111", "4efd561dcdbafd345641f2a07689d4657b103eed", os_stage=1),
|
||||
Target("1.16.9.112", "217b78aac01f538cac49afe79886371a8b000528", os_stage=1),
|
||||
Target("1.16.9.113", "217b78aac01f538cac49afe79886371a8b000528", os_stage=2),
|
||||
Target("1.16.9.114", "217b78aac01f538cac49afe79886371a8b000528", os_stage=3),
|
||||
Target("1.16.9.115", "217b78aac01f538cac49afe79886371a8b000528", os_stage=4),
|
||||
Target(
|
||||
"1.16.9.116", "217b78aac01f538cac49afe79886371a8b000528",
|
||||
os_stage=5, split_stage5="first",
|
||||
),
|
||||
Target("1.16.9.117", "217b78aac01f538cac49afe79886371a8b000528", os_stage=5),
|
||||
Target("1.16.9.118", "217b78aac01f538cac49afe79886371a8b000528", os_stage=7),
|
||||
Target(
|
||||
"1.16.9.119", "217b78aac01f538cac49afe79886371a8b000528",
|
||||
os_stage=8, mymesh_opt="O2_SIZE_1_INLINE_HOIST",
|
||||
),
|
||||
Target(
|
||||
"1.16.9.120", "217b78aac01f538cac49afe79886371a8b000528",
|
||||
os_stage=8, mymesh_opt="O2_SIZE_1_HOIST",
|
||||
),
|
||||
Target(
|
||||
"1.16.9.121", "217b78aac01f538cac49afe79886371a8b000528",
|
||||
os_stage=8, mymesh_opt="-Os",
|
||||
),
|
||||
Target(
|
||||
"1.16.9.122", "217b78aac01f538cac49afe79886371a8b000528",
|
||||
os_stage=12, mymesh_opt="-Os",
|
||||
),
|
||||
Target(
|
||||
"1.16.10.0", "217b78aac01f538cac49afe79886371a8b000528",
|
||||
os_stage=13, mymesh_opt="-Os",
|
||||
),
|
||||
)
|
||||
|
||||
LEGACY_MESH_COMMITS = {
|
||||
"90abbd110ef4fa7c96fca30205bbc566bf5c966c",
|
||||
"804f45303ac615362ab56f44da0789301bb3de11",
|
||||
}
|
||||
|
||||
|
||||
class BuildError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def replace_exact(path: Path, old: bytes, new: bytes, label: str) -> None:
|
||||
data = path.read_bytes()
|
||||
count = data.count(old)
|
||||
if count != 1:
|
||||
raise BuildError(
|
||||
f"{label}: expected one exact anchor in {path}, found {count}"
|
||||
)
|
||||
path.write_bytes(data.replace(old, new, 1))
|
||||
|
||||
|
||||
def inflate_asset_metadata() -> dict[str, dict[str, str]]:
|
||||
metadata: dict[str, dict[str, str]] = {}
|
||||
for destination, (relative_source, expected_sha256) in INFLATE_ASSETS.items():
|
||||
source = INFLATE_ASSET_ROOT / relative_source
|
||||
if not source.is_file():
|
||||
raise BuildError(f"missing receive-inflate asset: {source}")
|
||||
actual_sha256 = sha256_file(source)
|
||||
if actual_sha256 != expected_sha256:
|
||||
raise BuildError(
|
||||
f"receive-inflate asset hash mismatch for {relative_source}: "
|
||||
f"{actual_sha256}, expected {expected_sha256}"
|
||||
)
|
||||
metadata[destination] = {
|
||||
"asset": relative_source,
|
||||
"sha256": actual_sha256,
|
||||
}
|
||||
return metadata
|
||||
|
||||
|
||||
def install_inflate_assets(source: Path) -> None:
|
||||
for destination, (relative_source, _) in INFLATE_ASSETS.items():
|
||||
output = source / destination
|
||||
if output.exists():
|
||||
raise BuildError(
|
||||
f"historical source unexpectedly already contains {destination}"
|
||||
)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(INFLATE_ASSET_ROOT / relative_source, output)
|
||||
|
||||
|
||||
def remove_inflate_assets(source: Path) -> None:
|
||||
for destination in reversed(tuple(INFLATE_ASSETS)):
|
||||
output = source / destination
|
||||
if output.is_file() or output.is_symlink():
|
||||
output.unlink()
|
||||
elif output.exists():
|
||||
raise BuildError(f"receive-inflate asset is not a file: {output}")
|
||||
tinf_dir = source / "src/helpers/ota/tinf"
|
||||
if tinf_dir.exists():
|
||||
try:
|
||||
tinf_dir.rmdir()
|
||||
except OSError as exc:
|
||||
raise BuildError(f"receive-inflate asset directory is not empty: {tinf_dir}") from exc
|
||||
|
||||
|
||||
def restore_backport(source: Path) -> None:
|
||||
run(
|
||||
[
|
||||
"git", "restore", "--source=HEAD", "--staged", "--worktree", "--",
|
||||
*TRACKED_BACKPORT_FILES,
|
||||
],
|
||||
"restore prior backport",
|
||||
source,
|
||||
)
|
||||
remove_inflate_assets(source)
|
||||
|
||||
|
||||
def validate_inflate_source(source: Path) -> None:
|
||||
manager_h = (source / "src/helpers/ota/OtaManager.h").read_text(encoding="utf-8")
|
||||
manager_cpp = (source / "src/helpers/ota/OtaManager.cpp").read_text(encoding="utf-8")
|
||||
context_h = (source / "src/helpers/ota/OtaContext.h").read_text(encoding="utf-8")
|
||||
protocol_h = (source / "src/helpers/ota/OtaProtocol.h").read_text(encoding="utf-8")
|
||||
platformio = (source / "platformio.ini").read_text(encoding="utf-8")
|
||||
required = {
|
||||
"void OTA send ABI": "typedef void (*OtaSend)",
|
||||
"void manager receive ABI": "void on_message(const uint8_t* msg, uint16_t len)",
|
||||
"requested block hook declaration": "uint16_t requestedBlockLength(",
|
||||
"requested block hook implementation": "uint16_t OtaManager::requestedBlockLength(",
|
||||
"inflater context member": "OtaTransportInflateReceiver transport_inflate;",
|
||||
"inflater context begin": "transport_inflate.begin(manager, target_id, send, ctx);",
|
||||
"v2 request marker": "OTA_REQ_V2_MARK = 0x8000u",
|
||||
"v2 fragment size": "#define OTA_FRAG_DATA_V2 171",
|
||||
"tinf build filter": "+<helpers/ota/OtaTinf.c>",
|
||||
}
|
||||
haystacks = {
|
||||
"void OTA send ABI": manager_h,
|
||||
"void manager receive ABI": manager_h,
|
||||
"requested block hook declaration": manager_h,
|
||||
"requested block hook implementation": manager_cpp,
|
||||
"inflater context member": context_h,
|
||||
"inflater context begin": context_h,
|
||||
"v2 request marker": protocol_h,
|
||||
"v2 fragment size": manager_h,
|
||||
"tinf build filter": platformio,
|
||||
}
|
||||
for label, needle in required.items():
|
||||
if needle not in haystacks[label]:
|
||||
raise BuildError(f"receive-inflate validation lacks {label}: {needle}")
|
||||
for destination, (_, expected_sha256) in INFLATE_ASSETS.items():
|
||||
actual_sha256 = sha256_file(source / destination)
|
||||
if actual_sha256 != expected_sha256:
|
||||
raise BuildError(
|
||||
f"installed receive-inflate asset mismatch for {destination}: {actual_sha256}"
|
||||
)
|
||||
run(["git", "diff", "--check"], "backport whitespace check", source)
|
||||
|
||||
|
||||
def apply_inflate_transforms(source: Path) -> None:
|
||||
"""Apply the add51bf0 receive shim through exact, fail-closed anchors."""
|
||||
|
||||
manager_h = source / "src/helpers/ota/OtaManager.h"
|
||||
replace_exact(
|
||||
manager_h,
|
||||
b"#ifndef OTA_FRAG_DATA\n"
|
||||
b"#define OTA_FRAG_DATA 160 // data bytes per DATA fragment (<= MAX_PACKET_PAYLOAD - 9-byte header)\n"
|
||||
b"#endif\n",
|
||||
b"#ifndef OTA_FRAG_DATA\n"
|
||||
b"#define OTA_FRAG_DATA 160 // deployed legacy DATA geometry; never change in place\n"
|
||||
b"#endif\n"
|
||||
b"#ifndef OTA_FRAG_DATA_V2\n"
|
||||
b"#define OTA_FRAG_DATA_V2 171 // negotiated DATA geometry (13-byte overhead => 184-byte packet payload)\n"
|
||||
b"#endif\n",
|
||||
"OTA fragment constants",
|
||||
)
|
||||
replace_exact(
|
||||
manager_h,
|
||||
b" bool terminallyConsumes(const uint8_t* msg, uint16_t len);\n"
|
||||
b" void on_message(const uint8_t* msg, uint16_t len); // feed one received OTA message\n",
|
||||
b" bool terminallyConsumes(const uint8_t* msg, uint16_t len);\n"
|
||||
b" // Exact logical length of a block this receiver currently requested, or zero for unsolicited DATA.\n"
|
||||
b" // Used by the historical transport shim without exposing or changing the manager's reassembly buffers.\n"
|
||||
b" uint16_t requestedBlockLength(const uint8_t* manifest_id, uint16_t block) const;\n"
|
||||
b" void on_message(const uint8_t* msg, uint16_t len); // feed one received OTA message\n",
|
||||
"OtaManager receive shim declaration",
|
||||
)
|
||||
|
||||
manager_cpp = source / "src/helpers/ota/OtaManager.cpp"
|
||||
replace_exact(
|
||||
manager_cpp,
|
||||
b"bool OtaManager::terminallyConsumes(const uint8_t* msg, uint16_t len) {\n",
|
||||
b"uint16_t OtaManager::requestedBlockLength(const uint8_t* manifest_id, uint16_t block) const {\n"
|
||||
b" if (!manifest_id || !_fetch || _fstate != FETCHING || block >= _fbc ||\n"
|
||||
b" memcmp(manifest_id, _fid, sizeof(_fid)) != 0 || findReassemblySlot(block) < 0) return 0;\n"
|
||||
b" const uint32_t len = blockLen(block);\n"
|
||||
b" return len <= OTA_MAX_BLOCK ? (uint16_t)len : 0;\n"
|
||||
b"}\n\n"
|
||||
b"bool OtaManager::terminallyConsumes(const uint8_t* msg, uint16_t len) {\n",
|
||||
"OtaManager receive shim implementation",
|
||||
)
|
||||
|
||||
protocol_h = source / "src/helpers/ota/OtaProtocol.h"
|
||||
data_message = (
|
||||
b"struct DataMsg {\n"
|
||||
b" uint8_t manifest_id[4];\n"
|
||||
b" uint16_t block_idx;\n"
|
||||
b" uint16_t frag_off;\n"
|
||||
b" const uint8_t* data; uint16_t data_len;\n"
|
||||
b"};\n"
|
||||
)
|
||||
v2_protocol = data_message + (
|
||||
b"\n// OTA_REQ want_mask extension. These bits sit outside every valid fragment bit for <=1 KiB blocks. A first\n"
|
||||
b"// v2 request deliberately includes all seven legacy fragment bits so an old source can answer it completely.\n"
|
||||
b"static const uint16_t OTA_REQ_V2_MARK = 0x8000u;\n"
|
||||
b"static const uint16_t OTA_REQ_V2_ALLOW_DEFLATE = 0x4000u;\n"
|
||||
b"static const uint16_t OTA_REQ_V2_RESERVED = 0x2000u;\n"
|
||||
b"static const uint16_t OTA_REQ_V2_FRAGMENT_MASK = 0x1FFFu;\n"
|
||||
b"\n// OTA_DATA v2 frag_off packing:\n"
|
||||
b"// bit 15 v2 marker\n"
|
||||
b"// bit 14 data[] is one raw-RFC1951-DEFLATE stream fragment (clear = raw block bytes)\n"
|
||||
b"// bits 13..10 fragment index (0..15; byte offset = index * OTA_FRAG_DATA_V2)\n"
|
||||
b"// bits 9..0 total encoded block length minus one (1..1024 bytes)\n"
|
||||
b"static const uint16_t OTA_DATA_V2_MARK = 0x8000u;\n"
|
||||
b"static const uint16_t OTA_DATA_V2_DEFLATED = 0x4000u;\n"
|
||||
b"static const uint16_t OTA_DATA_V2_FRAGMENT_BITS = 0x3C00u;\n"
|
||||
b"static const uint16_t OTA_DATA_V2_LENGTH_BITS = 0x03FFu;\n"
|
||||
b"static const uint8_t OTA_DATA_V2_FRAGMENT_SHIFT = 10;\n"
|
||||
b"static const uint16_t OTA_DATA_V2_MAX_ENCODED = 1024;\n"
|
||||
b"static const uint8_t OTA_DATA_V2_STREAM_ID_BYTES = 4;\n"
|
||||
b"\ninline bool ota_req_is_v2(uint16_t want_mask) {\n"
|
||||
b" return (want_mask & OTA_REQ_V2_MARK) != 0 && (want_mask & OTA_REQ_V2_RESERVED) == 0;\n"
|
||||
b"}\n"
|
||||
b"\ninline uint16_t ota_req_v2_fragments(uint16_t want_mask) {\n"
|
||||
b" return (uint16_t)(want_mask & OTA_REQ_V2_FRAGMENT_MASK);\n"
|
||||
b"}\n"
|
||||
b"\ninline uint16_t ota_req_make_v2(uint16_t fragments, bool allow_deflate) {\n"
|
||||
b" return (uint16_t)((fragments & OTA_REQ_V2_FRAGMENT_MASK) | OTA_REQ_V2_MARK |\n"
|
||||
b" (allow_deflate ? OTA_REQ_V2_ALLOW_DEFLATE : 0));\n"
|
||||
b"}\n"
|
||||
b"\ninline bool ota_data_v2_pack(uint8_t fragment, uint16_t encoded_len, bool deflated,\n"
|
||||
b" uint16_t& packed) {\n"
|
||||
b" if (fragment >= 16 || encoded_len == 0 || encoded_len > OTA_DATA_V2_MAX_ENCODED) return false;\n"
|
||||
b" packed = (uint16_t)(OTA_DATA_V2_MARK |\n"
|
||||
b" (deflated ? OTA_DATA_V2_DEFLATED : 0) |\n"
|
||||
b" ((uint16_t)fragment << OTA_DATA_V2_FRAGMENT_SHIFT) |\n"
|
||||
b" (encoded_len - 1u));\n"
|
||||
b" return true;\n"
|
||||
b"}\n"
|
||||
b"\ninline bool ota_data_v2_unpack(uint16_t packed, uint8_t& fragment, uint16_t& encoded_len,\n"
|
||||
b" bool& deflated) {\n"
|
||||
b" if ((packed & OTA_DATA_V2_MARK) == 0) return false;\n"
|
||||
b" fragment = (uint8_t)((packed & OTA_DATA_V2_FRAGMENT_BITS) >> OTA_DATA_V2_FRAGMENT_SHIFT);\n"
|
||||
b" encoded_len = (uint16_t)((packed & OTA_DATA_V2_LENGTH_BITS) + 1u);\n"
|
||||
b" deflated = (packed & OTA_DATA_V2_DEFLATED) != 0;\n"
|
||||
b" return true;\n"
|
||||
b"}\n"
|
||||
)
|
||||
replace_exact(protocol_h, data_message, v2_protocol, "OTA v2 protocol helpers")
|
||||
|
||||
context_h = source / "src/helpers/ota/OtaContext.h"
|
||||
replace_exact(
|
||||
context_h,
|
||||
b'#include "OtaManager.h"\n#include "OtaStore.h"\n',
|
||||
b'#include "OtaManager.h"\n#include "OtaDeflate.h"\n#include "OtaStore.h"\n',
|
||||
"OtaContext inflater include",
|
||||
)
|
||||
replace_exact(
|
||||
context_h,
|
||||
b"struct OtaContext {\n OtaManager manager;\n",
|
||||
b"struct OtaContext {\n OtaManager manager;\n OtaTransportInflateReceiver transport_inflate;\n",
|
||||
"OtaContext inflater member",
|
||||
)
|
||||
replace_exact(
|
||||
context_h,
|
||||
b" void begin(uint32_t target_id, OtaSend send, void* ctx, const char* hw = nullptr) {\n",
|
||||
b" void on_message(const uint8_t* msg, uint16_t len) {\n"
|
||||
b" transport_inflate.on_message(msg, len);\n"
|
||||
b" }\n\n"
|
||||
b" void begin(uint32_t target_id, OtaSend send, void* ctx, const char* hw = nullptr) {\n",
|
||||
"OtaContext inflater receive wrapper",
|
||||
)
|
||||
replace_exact(
|
||||
context_h,
|
||||
b" manager.begin(target_id, send, ctx);\n",
|
||||
b" transport_inflate.begin(manager, target_id, send, ctx);\n",
|
||||
"OtaContext inflater begin wrapper",
|
||||
)
|
||||
|
||||
mesh_cpp = source / "src/Mesh.cpp"
|
||||
replace_exact(
|
||||
mesh_cpp,
|
||||
b"ota::ota_ctx().manager.on_message(pkt->payload, pkt->payload_len);",
|
||||
b"ota::ota_ctx().on_message(pkt->payload, pkt->payload_len); ",
|
||||
"Mesh inflater receive wrapper",
|
||||
)
|
||||
|
||||
platformio = source / "platformio.ini"
|
||||
replace_exact(
|
||||
platformio,
|
||||
b" +<helpers/*.cpp>\n ; MQTT-only sources",
|
||||
b" +<helpers/*.cpp>\n +<helpers/ota/OtaTinf.c>\n ; MQTT-only sources",
|
||||
"PlatformIO tinf C source",
|
||||
)
|
||||
|
||||
install_inflate_assets(source)
|
||||
|
||||
|
||||
def run(
|
||||
command: list[str],
|
||||
label: str,
|
||||
cwd: Path = REPO_ROOT,
|
||||
timeout: int = 900,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise BuildError(f"{label} failed: {exc}") from exc
|
||||
if result.returncode != 0:
|
||||
raise BuildError(
|
||||
f"{label} exited {result.returncode}:\n{result.stdout[-6000:]}"
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def read_image(path: Path) -> bytes:
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
members = [item for item in archive.infolist() if item.filename == "firmware.bin"]
|
||||
if len(members) != 1:
|
||||
raise BuildError(f"{path} does not contain one root firmware.bin")
|
||||
return archive.read(members[0])
|
||||
|
||||
|
||||
def verify_image(path: Path, target: Target) -> dict[str, object]:
|
||||
image = read_image(path)
|
||||
if not motalib.has_endf(image):
|
||||
raise BuildError(f"{path} has no valid EndF")
|
||||
ident = motalib.parse_endf_ident(image)
|
||||
assert ident is not None
|
||||
version = motalib.unpack_version(ident.fw_version)
|
||||
if version != target.version:
|
||||
raise BuildError(f"{path} version is {version}, expected {target.version}")
|
||||
if ident.target_id != EXPECTED_TARGET_ID or ident.hw_id != EXPECTED_HARDWARE:
|
||||
raise BuildError(
|
||||
f"{path} identity is target={ident.target_id:08X} hw={ident.hw_id!r}"
|
||||
)
|
||||
body, body_hash = motalib.parse_endf(image)
|
||||
return {
|
||||
"version": target.version,
|
||||
"source_commit": target.source_commit,
|
||||
"zip": path.name,
|
||||
"firmware_size": len(image),
|
||||
"firmware_sha256": hashlib.sha256(image).hexdigest(),
|
||||
"body_size": len(body),
|
||||
"body_hash": body_hash.hex(),
|
||||
}
|
||||
|
||||
|
||||
def apply_backport(source: Path, commit: str) -> None:
|
||||
required = (
|
||||
COMMON_PATCH,
|
||||
LEGACY_MESH_PATCH if commit in LEGACY_MESH_COMMITS else GUARDED_MESH_PATCH,
|
||||
WORKSPACE_PATCH,
|
||||
)
|
||||
for patch in required:
|
||||
run(["git", "apply", "--check", str(patch)], f"check {patch.name}", source)
|
||||
run(["git", "apply", str(patch)], f"apply {patch.name}", source)
|
||||
version_check = subprocess.run(
|
||||
["git", "apply", "--check", str(VERSION_PATCH)],
|
||||
cwd=source,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
if version_check.returncode == 0:
|
||||
run(["git", "apply", str(VERSION_PATCH)], f"apply {VERSION_PATCH.name}", source)
|
||||
else:
|
||||
run(
|
||||
["git", "apply", "--reverse", "--check", str(VERSION_PATCH)],
|
||||
f"confirm existing {VERSION_PATCH.name}",
|
||||
source,
|
||||
)
|
||||
apply_inflate_transforms(source)
|
||||
validate_inflate_source(source)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--work-dir", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path)
|
||||
parser.add_argument(
|
||||
"--validate-backports-only",
|
||||
action="store_true",
|
||||
help="apply and verify every unique pinned source without running PlatformIO",
|
||||
)
|
||||
parser.add_argument("--keep-worktree", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
for patch in (
|
||||
COMMON_PATCH, LEGACY_MESH_PATCH, GUARDED_MESH_PATCH, VERSION_PATCH,
|
||||
WORKSPACE_PATCH,
|
||||
):
|
||||
if not patch.is_file():
|
||||
raise BuildError(f"missing backport patch: {patch}")
|
||||
if not SELECTIVE_OS_HOOK.is_file():
|
||||
raise BuildError(f"missing selective optimizer hook: {SELECTIVE_OS_HOOK}")
|
||||
asset_metadata = inflate_asset_metadata()
|
||||
if not args.validate_backports_only and args.output_dir is None:
|
||||
raise BuildError("--output-dir is required unless --validate-backports-only is used")
|
||||
if args.work_dir.exists():
|
||||
raise BuildError(f"work directory already exists: {args.work_dir}")
|
||||
if args.output_dir is not None and args.output_dir.exists():
|
||||
raise BuildError(f"output directory already exists: {args.output_dir}")
|
||||
args.work_dir.mkdir(parents=True)
|
||||
if args.output_dir is not None:
|
||||
args.output_dir.mkdir(parents=True)
|
||||
log_dir = args.work_dir / "logs"
|
||||
if not args.validate_backports_only:
|
||||
log_dir.mkdir()
|
||||
source = args.work_dir / "source"
|
||||
|
||||
run(
|
||||
["git", "worktree", "add", "--detach", str(source), TARGETS[0].source_commit],
|
||||
"create build worktree",
|
||||
)
|
||||
current_commit = ""
|
||||
records: list[dict[str, object]] = []
|
||||
succeeded = False
|
||||
try:
|
||||
targets = TARGETS
|
||||
if args.validate_backports_only:
|
||||
unique: dict[str, Target] = {}
|
||||
for target in TARGETS:
|
||||
unique.setdefault(target.source_commit, target)
|
||||
targets = tuple(unique.values())
|
||||
for index, target in enumerate(targets, 1):
|
||||
if target.source_commit != current_commit:
|
||||
if current_commit:
|
||||
restore_backport(source)
|
||||
run(
|
||||
["git", "checkout", "--detach", target.source_commit],
|
||||
f"checkout {target.source_commit}",
|
||||
source,
|
||||
)
|
||||
apply_backport(source, target.source_commit)
|
||||
current_commit = target.source_commit
|
||||
|
||||
if args.validate_backports_only:
|
||||
print(
|
||||
f"[validate] {index:02d}/{len(targets)} "
|
||||
f"{target.source_commit} receive-inflate backport applies",
|
||||
flush=True,
|
||||
)
|
||||
continue
|
||||
|
||||
firmware_version = f"v{target.version}-{VERSION_SUFFIX}"
|
||||
build_env = dict(os.environ)
|
||||
for name in (
|
||||
"MOTA_BRIDGE_OS_STAGE", "MOTA_BRIDGE_OS_STAGE_PART",
|
||||
"MOTA_BRIDGE_OS_STAGE_PARTS", "MOTA_BRIDGE_OS_STAGE_SUBPART",
|
||||
"MOTA_BRIDGE_OS_STAGE_SUBPARTS", "MOTA_BRIDGE_SPLIT_STAGE5",
|
||||
"MOTA_BRIDGE_MY_MESH_OPT", "MOTA_BRIDGE_OS_VERBOSE",
|
||||
"PLATFORMIO_BUILD_FLAGS", "PLATFORMIO_EXTRA_SCRIPTS",
|
||||
):
|
||||
build_env.pop(name, None)
|
||||
build_env["DISABLE_DEBUG"] = "1"
|
||||
flags = [
|
||||
"-DLORA_FREQ=910.525", "-DLORA_BW=62.5", "-DLORA_SF=7",
|
||||
"-DLORA_CR=5", "-DOTA_FETCH_PIPELINE=1", "-DOTA_TX_PRIORITY=0",
|
||||
]
|
||||
if not target.flood_rule_engine:
|
||||
flags.append("-DMESH_ENABLE_FLOOD_RULE_ENGINE=0")
|
||||
if target.bridge_stage:
|
||||
flags.append(f"-DMOTA_BRIDGE_586_STAGE={target.bridge_stage}")
|
||||
if target.os_stage:
|
||||
build_env["MOTA_BRIDGE_OS_STAGE"] = str(target.os_stage)
|
||||
build_env["PLATFORMIO_EXTRA_SCRIPTS"] = f"pre:{SELECTIVE_OS_HOOK}"
|
||||
if target.os_stage_parts:
|
||||
build_env["MOTA_BRIDGE_OS_STAGE_PART"] = str(target.os_stage_part)
|
||||
build_env["MOTA_BRIDGE_OS_STAGE_PARTS"] = str(target.os_stage_parts)
|
||||
if target.os_stage_subparts:
|
||||
build_env["MOTA_BRIDGE_OS_STAGE_SUBPART"] = str(target.os_stage_subpart)
|
||||
build_env["MOTA_BRIDGE_OS_STAGE_SUBPARTS"] = str(target.os_stage_subparts)
|
||||
if target.split_stage5:
|
||||
build_env["MOTA_BRIDGE_SPLIT_STAGE5"] = target.split_stage5
|
||||
if target.mymesh_opt:
|
||||
build_env["MOTA_BRIDGE_MY_MESH_OPT"] = target.mymesh_opt
|
||||
build_env["PLATFORMIO_BUILD_FLAGS"] = " ".join(flags)
|
||||
run(
|
||||
["pio", "run", "-e", ENV_NAME, "-t", "clean"],
|
||||
f"clean {target.version}",
|
||||
source,
|
||||
)
|
||||
output = run(
|
||||
[
|
||||
"bash", "build.sh", "build-firmware", ENV_NAME,
|
||||
"--profile", "cascade", "--firmware-version", firmware_version,
|
||||
],
|
||||
f"build {target.version}",
|
||||
source,
|
||||
env=build_env,
|
||||
)
|
||||
(log_dir / f"build-{index:02d}-v{target.version}.log").write_text(
|
||||
output, encoding="utf-8"
|
||||
)
|
||||
for required in (*flags, "-DMOTA_TARGET_ID=0x2fa509c1", "SUCCESS"):
|
||||
if required not in output:
|
||||
raise BuildError(f"build {target.version} log lacks {required}")
|
||||
if target.os_stage and f"selective optimizer stage {target.os_stage}/13" not in output:
|
||||
raise BuildError(f"build {target.version} did not run the selective optimizer")
|
||||
stem = f"{ENV_NAME}-ota-{firmware_version}-{target.source_commit[:8]}"
|
||||
built_zip = source / "out" / f"{stem}.zip"
|
||||
built_uf2 = source / "out" / f"{stem}.uf2"
|
||||
if not built_zip.is_file() or not built_uf2.is_file():
|
||||
raise BuildError(f"build {target.version} did not emit the expected ZIP and UF2")
|
||||
assert args.output_dir is not None
|
||||
output_zip = args.output_dir / built_zip.name
|
||||
output_uf2 = args.output_dir / built_uf2.name
|
||||
shutil.copy2(built_zip, output_zip)
|
||||
shutil.copy2(built_uf2, output_uf2)
|
||||
record = verify_image(output_zip, target)
|
||||
record["uf2"] = output_uf2.name
|
||||
record["zip_sha256"] = sha256_file(output_zip)
|
||||
record["uf2_sha256"] = sha256_file(output_uf2)
|
||||
record["build_flags"] = flags
|
||||
record["bridge_stage"] = target.bridge_stage
|
||||
record["os_stage"] = target.os_stage
|
||||
record["os_stage_part"] = target.os_stage_part
|
||||
record["os_stage_parts"] = target.os_stage_parts
|
||||
record["os_stage_subpart"] = target.os_stage_subpart
|
||||
record["os_stage_subparts"] = target.os_stage_subparts
|
||||
record["split_stage5"] = target.split_stage5
|
||||
record["mymesh_opt"] = target.mymesh_opt
|
||||
record["flood_rule_engine"] = target.flood_rule_engine
|
||||
records.append(record)
|
||||
print(
|
||||
f"[build] {index:02d}/{len(TARGETS)} v{target.version} "
|
||||
f"image={record['firmware_size']} sha={str(record['firmware_sha256'])[:16]}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if args.validate_backports_only:
|
||||
succeeded = True
|
||||
print(
|
||||
f"[validate] all {len(targets)} unique pinned source commits passed",
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
assert args.output_dir is not None
|
||||
manifest = {
|
||||
"environment": ENV_NAME,
|
||||
"target_id": f"{EXPECTED_TARGET_ID:08X}",
|
||||
"hardware": EXPECTED_HARDWARE,
|
||||
"version_suffix": VERSION_SUFFIX,
|
||||
"inflate_snapshot_commit": INFLATE_SNAPSHOT_COMMIT,
|
||||
"inflate_assets": asset_metadata,
|
||||
"builder_script": Path(__file__).name,
|
||||
"builder_script_sha256": sha256_file(Path(__file__)),
|
||||
"common_patch": COMMON_PATCH.name,
|
||||
"common_patch_sha256": sha256_file(COMMON_PATCH),
|
||||
"legacy_mesh_patch": LEGACY_MESH_PATCH.name,
|
||||
"legacy_mesh_patch_sha256": sha256_file(LEGACY_MESH_PATCH),
|
||||
"guarded_mesh_patch": GUARDED_MESH_PATCH.name,
|
||||
"guarded_mesh_patch_sha256": sha256_file(GUARDED_MESH_PATCH),
|
||||
"version_patch": VERSION_PATCH.name,
|
||||
"version_patch_sha256": sha256_file(VERSION_PATCH),
|
||||
"workspace_patch": WORKSPACE_PATCH.name,
|
||||
"workspace_patch_sha256": sha256_file(WORKSPACE_PATCH),
|
||||
"selective_os_hook": SELECTIVE_OS_HOOK.name,
|
||||
"selective_os_hook_sha256": sha256_file(SELECTIVE_OS_HOOK),
|
||||
"transport_fragment_bytes": 171,
|
||||
"transport_codec": "raw-deflate-receive-only",
|
||||
"fetch_pipeline": 1,
|
||||
"targets": records,
|
||||
}
|
||||
(args.output_dir / "images.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
succeeded = True
|
||||
print(f"[manifest] {args.output_dir / 'images.json'}", flush=True)
|
||||
finally:
|
||||
if succeeded and not args.keep_worktree:
|
||||
run(
|
||||
["git", "worktree", "remove", "--force", str(source)],
|
||||
"remove build worktree",
|
||||
)
|
||||
if args.validate_backports_only:
|
||||
args.work_dir.rmdir()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except BuildError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
@@ -0,0 +1,250 @@
|
||||
#include "OtaDeflate.h"
|
||||
|
||||
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST)
|
||||
|
||||
#include "Multihash.h"
|
||||
#include <string.h>
|
||||
|
||||
extern "C" {
|
||||
#include "tinf/tinf.h"
|
||||
}
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
static_assert(OTA_MAX_BLOCK <= OTA_DATA_V2_MAX_ENCODED,
|
||||
"transport descriptor must represent every OTA logical block");
|
||||
static_assert(OTA_FETCH_PIPELINE == 1,
|
||||
"historical transport shim requires OTA_FETCH_PIPELINE=1");
|
||||
|
||||
static uint16_t transport_fragment_mask(uint16_t bytes, uint16_t fragment_bytes) {
|
||||
const uint16_t count = (uint16_t)((bytes + fragment_bytes - 1u) / fragment_bytes);
|
||||
return count >= 16 ? 0xFFFFu : (uint16_t)((1u << count) - 1u);
|
||||
}
|
||||
|
||||
bool ota_transport_inflate(void* context, const uint8_t* src, uint16_t src_len,
|
||||
uint8_t* dst, uint16_t dst_cap, uint16_t* dst_len) {
|
||||
(void)context;
|
||||
if (dst_len) *dst_len = 0;
|
||||
if (!src || src_len == 0 || !dst || dst_cap == 0 || !dst_len) return false;
|
||||
|
||||
unsigned int produced = dst_cap;
|
||||
const int result = tinf_uncompress_exact(dst, &produced, src, src_len);
|
||||
if (result != TINF_OK || produced != dst_cap) return false;
|
||||
*dst_len = (uint16_t)produced;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaTransportInflateReceiver::begin(OtaManager& manager, uint32_t target_id,
|
||||
OtaSend send_fn, void* send_ctx) {
|
||||
_manager = &manager;
|
||||
_send = send_fn;
|
||||
_send_ctx = send_ctx;
|
||||
reset_session();
|
||||
manager.begin(target_id, send_trampoline, this);
|
||||
}
|
||||
|
||||
void OtaTransportInflateReceiver::reset_representation() {
|
||||
_encoded_len = 0;
|
||||
_mask = 0;
|
||||
_need = 0;
|
||||
_partial_retries = 0;
|
||||
_request_sent = false;
|
||||
_complete = false;
|
||||
_deflated = false;
|
||||
memset(_stream_id, 0, sizeof(_stream_id));
|
||||
}
|
||||
|
||||
void OtaTransportInflateReceiver::reset_session() {
|
||||
reset_representation();
|
||||
_active = false;
|
||||
_block = 0;
|
||||
memset(_mid, 0, sizeof(_mid));
|
||||
_allow_deflate = true;
|
||||
_legacy_session = false;
|
||||
}
|
||||
|
||||
bool OtaTransportInflateReceiver::is_active(const uint8_t* mid, uint16_t block) const {
|
||||
return _active && mid && block == _block && memcmp(mid, _mid, sizeof(_mid)) == 0;
|
||||
}
|
||||
|
||||
void OtaTransportInflateReceiver::send_trampoline(void* ctx, const uint8_t* msg,
|
||||
uint16_t len, bool flood) {
|
||||
if (ctx) static_cast<OtaTransportInflateReceiver*>(ctx)->send(msg, len, flood);
|
||||
}
|
||||
|
||||
void OtaTransportInflateReceiver::send(const uint8_t* msg, uint16_t len, bool flood) {
|
||||
if (!_send || !msg || len == 0) return;
|
||||
|
||||
// A new manifest request starts a new negotiation. A retry while still waiting for that manifest is also
|
||||
// safe to reset because no payload block can yet be active.
|
||||
if (ota_msg_type(msg, len) == OTA_GET_MANIFEST) reset_session();
|
||||
|
||||
ReqMsg request;
|
||||
if (ota_msg_type(msg, len) == OTA_REQ && decode_req(msg, len, request)) {
|
||||
if (!is_active(request.manifest_id, request.block_idx)) {
|
||||
// resumeStaged() can enter FETCHING without sending GET_MANIFEST first. A different MID is therefore
|
||||
// an independent negotiation even when this shim retained conservative fallback state from an earlier
|
||||
// transfer in the same boot. Preserve that state only while moving between blocks of the same MID.
|
||||
if (!_active || memcmp(request.manifest_id, _mid, sizeof(_mid)) != 0) reset_session();
|
||||
else reset_representation();
|
||||
_active = true;
|
||||
_block = request.block_idx;
|
||||
memcpy(_mid, request.manifest_id, sizeof(_mid));
|
||||
} else if (_complete) {
|
||||
// The unchanged manager asks for this block again only after discarding it (for example, a bad proof).
|
||||
reset_representation();
|
||||
}
|
||||
|
||||
if (!_legacy_session) {
|
||||
uint16_t fragments = request.want_mask;
|
||||
if (_encoded_len != 0 && _mask != _need) {
|
||||
// Preserve one sparse retry for a partially received representation. A second retry without a new
|
||||
// fragment falls back to the universally deployed profile so an old source can take over.
|
||||
if (++_partial_retries >= 2) {
|
||||
_legacy_session = true;
|
||||
reset_representation();
|
||||
} else {
|
||||
fragments = (uint16_t)(_need & ~_mask);
|
||||
}
|
||||
} else if (_request_sent && _encoded_len == 0) {
|
||||
// No valid v2 DATA answered the first request. Retry in the literal legacy profile; this also covers
|
||||
// sources which reject, rather than mask, unknown want_mask bits.
|
||||
_legacy_session = true;
|
||||
reset_representation();
|
||||
}
|
||||
|
||||
if (!_legacy_session) {
|
||||
request.want_mask = ota_req_make_v2(fragments, _allow_deflate);
|
||||
uint8_t wire[16];
|
||||
const uint16_t wire_len = encode_req(wire, sizeof(wire), request);
|
||||
if (wire_len != 0) {
|
||||
_request_sent = true;
|
||||
_send(_send_ctx, wire, wire_len, flood);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_send(_send_ctx, msg, len, flood);
|
||||
}
|
||||
|
||||
void OtaTransportInflateReceiver::on_message(const uint8_t* msg, uint16_t len) {
|
||||
if (!_manager || !msg || len == 0) return;
|
||||
|
||||
DataMsg data;
|
||||
if (ota_msg_type(msg, len) == OTA_DATA && decode_data(msg, len, data)) {
|
||||
if ((data.frag_off & OTA_DATA_V2_MARK) != 0) {
|
||||
(void)handle_v2_data(msg, len); // malformed/unsolicited v2 DATA fails closed
|
||||
return;
|
||||
}
|
||||
|
||||
// A fully canonical legacy fragment is an actual old-source response, not a reason to discard a valid
|
||||
// v2 representation merely because an attacker sent a malformed matching-MID packet.
|
||||
const uint16_t expected = _manager->requestedBlockLength(data.manifest_id, data.block_idx);
|
||||
if (expected != 0 && is_active(data.manifest_id, data.block_idx) &&
|
||||
data.frag_off % OTA_FRAG_DATA == 0 && data.frag_off < expected) {
|
||||
uint16_t expected_slice = (uint16_t)(expected - data.frag_off);
|
||||
if (expected_slice > OTA_FRAG_DATA) expected_slice = OTA_FRAG_DATA;
|
||||
if (data.data_len == expected_slice) {
|
||||
_legacy_session = true;
|
||||
reset_representation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_manager->on_message(msg, len);
|
||||
}
|
||||
|
||||
bool OtaTransportInflateReceiver::handle_v2_data(const uint8_t* msg, uint16_t len) {
|
||||
DataMsg data;
|
||||
if (!decode_data(msg, len, data) || !is_active(data.manifest_id, data.block_idx) ||
|
||||
data.data_len <= OTA_DATA_V2_STREAM_ID_BYTES || _legacy_session) return false;
|
||||
|
||||
const uint16_t block_len = _manager->requestedBlockLength(data.manifest_id, data.block_idx);
|
||||
if (block_len == 0) return false;
|
||||
|
||||
uint8_t fragment = 0;
|
||||
uint16_t encoded_len = 0;
|
||||
bool deflated = false;
|
||||
if (!ota_data_v2_unpack(data.frag_off, fragment, encoded_len, deflated)) return false;
|
||||
if (encoded_len > block_len || (deflated ? encoded_len >= block_len : encoded_len != block_len) ||
|
||||
(deflated && !_allow_deflate)) return false;
|
||||
|
||||
const uint16_t fragment_len = (uint16_t)(data.data_len - OTA_DATA_V2_STREAM_ID_BYTES);
|
||||
const uint32_t fragment_off = (uint32_t)fragment * OTA_FRAG_DATA_V2;
|
||||
if (fragment_off >= encoded_len || fragment_off + fragment_len > encoded_len) return false;
|
||||
uint16_t expected_slice = (uint16_t)(encoded_len - fragment_off);
|
||||
if (expected_slice > OTA_FRAG_DATA_V2) expected_slice = OTA_FRAG_DATA_V2;
|
||||
if (fragment_len != expected_slice) return false;
|
||||
|
||||
const uint8_t* stream_id = data.data;
|
||||
if (_encoded_len == 0) {
|
||||
_encoded_len = encoded_len;
|
||||
_deflated = deflated;
|
||||
memcpy(_stream_id, stream_id, sizeof(_stream_id));
|
||||
_mask = 0;
|
||||
_need = transport_fragment_mask(encoded_len, OTA_FRAG_DATA_V2);
|
||||
} else if (_encoded_len != encoded_len || _deflated != deflated ||
|
||||
memcmp(_stream_id, stream_id, sizeof(_stream_id)) != 0) {
|
||||
return false; // never mix different wire representations
|
||||
}
|
||||
|
||||
const uint16_t bit = (uint16_t)(1u << fragment);
|
||||
if (!(_need & bit)) return false;
|
||||
if (!(_mask & bit)) {
|
||||
memcpy(_encoded + fragment_off, data.data + OTA_DATA_V2_STREAM_ID_BYTES, fragment_len);
|
||||
_mask |= bit;
|
||||
_partial_retries = 0;
|
||||
}
|
||||
if (_mask != _need) return true;
|
||||
|
||||
uint8_t actual_id[4];
|
||||
mh4(actual_id, _encoded, _encoded_len);
|
||||
if (memcmp(actual_id, _stream_id, sizeof(actual_id)) != 0) {
|
||||
reset_representation();
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t* logical = _encoded;
|
||||
if (_deflated) {
|
||||
uint16_t decoded_len = 0;
|
||||
if (!ota_transport_inflate(nullptr, _encoded, _encoded_len,
|
||||
_decoded, block_len, &decoded_len) || decoded_len != block_len) {
|
||||
// Keep the 171-byte profile but stop offering DEFLATE for this session. The next manager retry asks the
|
||||
// source for raw v2 DATA, which still stages the exact same authenticated `.mota` block.
|
||||
_allow_deflate = false;
|
||||
reset_representation();
|
||||
return true;
|
||||
}
|
||||
logical = _decoded;
|
||||
}
|
||||
|
||||
// Feed the historical manager only the canonical representation it already knows. Its existing final
|
||||
// fragment transition requests/accepts the ordinary proof; Merkle verification happens before any write.
|
||||
for (uint16_t off = 0; off < block_len; off = (uint16_t)(off + OTA_FRAG_DATA)) {
|
||||
uint16_t slice = (uint16_t)(block_len - off);
|
||||
if (slice > OTA_FRAG_DATA) slice = OTA_FRAG_DATA;
|
||||
DataMsg canonical;
|
||||
memcpy(canonical.manifest_id, _mid, sizeof(canonical.manifest_id));
|
||||
canonical.block_idx = _block;
|
||||
canonical.frag_off = off;
|
||||
canonical.data = logical + off;
|
||||
canonical.data_len = slice;
|
||||
uint8_t wire[9 + OTA_FRAG_DATA];
|
||||
const uint16_t wire_len = encode_data(wire, sizeof(wire), canonical);
|
||||
if (wire_len == 0) {
|
||||
reset_representation();
|
||||
return false;
|
||||
}
|
||||
_manager->on_message(wire, wire_len);
|
||||
}
|
||||
_complete = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif // ENABLE_OTA || OTA_TRANSPORT_DEFLATE_TEST
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "OtaManager.h"
|
||||
#include "OtaProtocol.h"
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Full raw RFC1951 transport decoder (stored, fixed-Huffman, and dynamic-Huffman blocks).
|
||||
// `dst_cap` is the exact expected logical block length, not merely spare capacity. Success
|
||||
// requires exact output and exact whole-byte input consumption; malformed/truncated streams,
|
||||
// output overflow, and trailing bytes fail closed.
|
||||
bool ota_transport_inflate(void* context, const uint8_t* src, uint16_t src_len,
|
||||
uint8_t* dst, uint16_t dst_cap, uint16_t* dst_len);
|
||||
|
||||
// Receive-only compatibility shim for historical OTA managers. The wrapped manager continues to reassemble
|
||||
// canonical 160-byte DATA, verify the Merkle proof, and stage the original `.mota`. This shim only negotiates
|
||||
// the v2 wire profile, locks and reassembles one encoded representation, exact-inflates it, and injects the
|
||||
// resulting logical block back into that unchanged path. Bridge builds use OTA_FETCH_PIPELINE=1, so one fixed
|
||||
// encoded block and one fixed decoded block cover every in-flight request without heap allocation.
|
||||
class OtaTransportInflateReceiver {
|
||||
public:
|
||||
void begin(OtaManager& manager, uint32_t target_id, OtaSend send, void* send_ctx);
|
||||
void on_message(const uint8_t* msg, uint16_t len);
|
||||
static void send_trampoline(void* ctx, const uint8_t* msg, uint16_t len, bool flood);
|
||||
|
||||
private:
|
||||
void send(const uint8_t* msg, uint16_t len, bool flood);
|
||||
bool handle_v2_data(const uint8_t* msg, uint16_t len);
|
||||
void reset_session();
|
||||
void reset_representation();
|
||||
bool is_active(const uint8_t* mid, uint16_t block) const;
|
||||
|
||||
OtaManager* _manager = nullptr;
|
||||
OtaSend _send = nullptr;
|
||||
void* _send_ctx = nullptr;
|
||||
uint8_t _mid[4] = {0};
|
||||
uint8_t _stream_id[4] = {0};
|
||||
uint16_t _block = 0;
|
||||
uint16_t _encoded_len = 0;
|
||||
uint16_t _mask = 0;
|
||||
uint16_t _need = 0;
|
||||
uint8_t _partial_retries = 0;
|
||||
bool _active = false;
|
||||
bool _request_sent = false;
|
||||
bool _complete = false;
|
||||
bool _deflated = false;
|
||||
bool _allow_deflate = true;
|
||||
bool _legacy_session = false;
|
||||
uint8_t _encoded[OTA_MAX_BLOCK];
|
||||
uint8_t _decoded[OTA_MAX_BLOCK];
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Compile the vendored tinf core as C. Building the same source as C++ costs
|
||||
* several extra kilobytes with the embedded toolchains used by MeshCore.
|
||||
*/
|
||||
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST)
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
/* nRF52's Arduino recipe defaults to -Ofast, which more than doubles this
|
||||
* decoder. Keep the size policy local to tinf instead of changing MeshCore. */
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("Os")
|
||||
#endif
|
||||
#define MESHCORE_TINF_IMPLEMENTATION 1
|
||||
#include "tinf/tinflate.c"
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
#pragma GCC pop_options
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
The zlib License (Zlib)
|
||||
|
||||
Copyright (c) 2003-2019 Joergen Ibsen
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product
|
||||
documentation would be appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must
|
||||
not be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
tinf 1.2.1 raw-DEFLATE decoder
|
||||
================================
|
||||
|
||||
Upstream: https://github.com/jibsen/tinf
|
||||
Revision: 57ffa1f1d5e3dde19011b2127bd26d01689b694b
|
||||
|
||||
Vendored files:
|
||||
LICENSE
|
||||
tinf.h
|
||||
tinflate.c
|
||||
|
||||
MeshCore changes are marked in tinf.h and tinflate.c. They add
|
||||
tinf_uncompress_exact(), which accepts legal padding bits in the byte containing
|
||||
the final end-of-block code but rejects any trailing whole input byte. The C
|
||||
implementation is included by ../OtaTinf.c only for ENABLE_OTA application
|
||||
builds (and the native OTA test profile). ../OtaDeflate.cpp is the small C++
|
||||
callback adapter. Keeping the decoder in a true C translation unit avoids the
|
||||
several-kilobyte penalty produced by the embedded C++ toolchains; non-OTA
|
||||
firmware links no decoder code or persistent RAM.
|
||||
|
||||
Only the raw RFC 1951 decoder is integrated. The zlib/gzip wrappers and checksum
|
||||
sources are intentionally omitted. The original zlib license is preserved in
|
||||
LICENSE and in the source headers.
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* tinf - tiny inflate library (inflate, gzip, zlib)
|
||||
*
|
||||
* Copyright (c) 2003-2019 Joergen Ibsen
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will the authors be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must
|
||||
* not claim that you wrote the original software. If you use this
|
||||
* software in a product, an acknowledgment in the product
|
||||
* documentation would be appreciated but is not required.
|
||||
*
|
||||
* 2. Altered source versions must be plainly marked as such, and must
|
||||
* not be misrepresented as being the original software.
|
||||
*
|
||||
* 3. This notice may not be removed or altered from any source
|
||||
* distribution.
|
||||
*/
|
||||
|
||||
/*
|
||||
* MeshCore modification (2026): declare tinf_uncompress_exact(), a raw-DEFLATE
|
||||
* entry point which rejects trailing whole input bytes. The original API and
|
||||
* behavior remain available as tinf_uncompress().
|
||||
*/
|
||||
|
||||
#ifndef TINF_H_INCLUDED
|
||||
#define TINF_H_INCLUDED
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define TINF_VER_MAJOR 1 /**< Major version number */
|
||||
#define TINF_VER_MINOR 2 /**< Minor version number */
|
||||
#define TINF_VER_PATCH 1 /**< Patch version number */
|
||||
#define TINF_VER_STRING "1.2.1" /**< Version number as a string */
|
||||
|
||||
#ifndef TINFCC
|
||||
# ifdef __WATCOMC__
|
||||
# define TINFCC __cdecl
|
||||
# else
|
||||
# define TINFCC
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Status codes returned.
|
||||
*
|
||||
* @see tinf_uncompress, tinf_gzip_uncompress, tinf_zlib_uncompress
|
||||
*/
|
||||
typedef enum {
|
||||
TINF_OK = 0, /**< Success */
|
||||
TINF_DATA_ERROR = -3, /**< Input error */
|
||||
TINF_BUF_ERROR = -5 /**< Not enough room for output */
|
||||
} tinf_error_code;
|
||||
|
||||
/**
|
||||
* Initialize global data used by tinf.
|
||||
*
|
||||
* @deprecated No longer required, may be removed in a future version.
|
||||
*/
|
||||
void TINFCC tinf_init(void);
|
||||
|
||||
/**
|
||||
* Decompress `sourceLen` bytes of deflate data from `source` to `dest`.
|
||||
*
|
||||
* The variable `destLen` points to must contain the size of `dest` on entry,
|
||||
* and will be set to the size of the decompressed data on success.
|
||||
*
|
||||
* Reads at most `sourceLen` bytes from `source`.
|
||||
* Writes at most `*destLen` bytes to `dest`.
|
||||
*
|
||||
* @param dest pointer to where to place decompressed data
|
||||
* @param destLen pointer to variable containing size of `dest`
|
||||
* @param source pointer to compressed data
|
||||
* @param sourceLen size of compressed data
|
||||
* @return `TINF_OK` on success, error code on error
|
||||
*/
|
||||
int TINFCC tinf_uncompress(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen);
|
||||
|
||||
/**
|
||||
* Decompress exactly one raw DEFLATE stream and reject trailing whole bytes.
|
||||
* Padding bits in the byte containing the final end-of-block code are allowed.
|
||||
* Output bounds and `destLen` semantics match tinf_uncompress().
|
||||
*/
|
||||
int TINFCC tinf_uncompress_exact(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen);
|
||||
|
||||
/**
|
||||
* Decompress `sourceLen` bytes of gzip data from `source` to `dest`.
|
||||
*
|
||||
* The variable `destLen` points to must contain the size of `dest` on entry,
|
||||
* and will be set to the size of the decompressed data on success.
|
||||
*
|
||||
* Reads at most `sourceLen` bytes from `source`.
|
||||
* Writes at most `*destLen` bytes to `dest`.
|
||||
*
|
||||
* @param dest pointer to where to place decompressed data
|
||||
* @param destLen pointer to variable containing size of `dest`
|
||||
* @param source pointer to compressed data
|
||||
* @param sourceLen size of compressed data
|
||||
* @return `TINF_OK` on success, error code on error
|
||||
*/
|
||||
int TINFCC tinf_gzip_uncompress(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen);
|
||||
|
||||
/**
|
||||
* Decompress `sourceLen` bytes of zlib data from `source` to `dest`.
|
||||
*
|
||||
* The variable `destLen` points to must contain the size of `dest` on entry,
|
||||
* and will be set to the size of the decompressed data on success.
|
||||
*
|
||||
* Reads at most `sourceLen` bytes from `source`.
|
||||
* Writes at most `*destLen` bytes to `dest`.
|
||||
*
|
||||
* @param dest pointer to where to place decompressed data
|
||||
* @param destLen pointer to variable containing size of `dest`
|
||||
* @param source pointer to compressed data
|
||||
* @param sourceLen size of compressed data
|
||||
* @return `TINF_OK` on success, error code on error
|
||||
*/
|
||||
int TINFCC tinf_zlib_uncompress(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen);
|
||||
|
||||
/**
|
||||
* Compute Adler-32 checksum of `length` bytes starting at `data`.
|
||||
*
|
||||
* @param data pointer to data
|
||||
* @param length size of data
|
||||
* @return Adler-32 checksum
|
||||
*/
|
||||
unsigned int TINFCC tinf_adler32(const void *data, unsigned int length);
|
||||
|
||||
/**
|
||||
* Compute CRC32 checksum of `length` bytes starting at `data`.
|
||||
*
|
||||
* @param data pointer to data
|
||||
* @param length size of data
|
||||
* @return CRC32 checksum
|
||||
*/
|
||||
unsigned int TINFCC tinf_crc32(const void *data, unsigned int length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* TINF_H_INCLUDED */
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
/*
|
||||
* tinflate - tiny inflate
|
||||
*
|
||||
* Copyright (c) 2003-2019 Joergen Ibsen
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will the authors be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must
|
||||
* not claim that you wrote the original software. If you use this
|
||||
* software in a product, an acknowledgment in the product
|
||||
* documentation would be appreciated but is not required.
|
||||
*
|
||||
* 2. Altered source versions must be plainly marked as such, and must
|
||||
* not be misrepresented as being the original software.
|
||||
*
|
||||
* 3. This notice may not be removed or altered from any source
|
||||
* distribution.
|
||||
*/
|
||||
|
||||
/*
|
||||
* MeshCore modifications (2026):
|
||||
* - expose tinf_uncompress_exact() to reject trailing whole input bytes;
|
||||
* - compile this vendored implementation through OtaTinf.c only, keeping
|
||||
* it out of builds which do not define ENABLE_OTA.
|
||||
* See README.meshcore.txt for the upstream revision and integration details.
|
||||
*/
|
||||
|
||||
#if defined(MESHCORE_TINF_IMPLEMENTATION)
|
||||
|
||||
#include "tinf.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
|
||||
#if defined(UINT_MAX) && (UINT_MAX) < 0xFFFFFFFFUL
|
||||
# error "tinf requires unsigned int to be at least 32-bit"
|
||||
#endif
|
||||
|
||||
/* -- Internal data structures -- */
|
||||
|
||||
struct tinf_tree {
|
||||
unsigned short counts[16]; /* Number of codes with a given length */
|
||||
unsigned short symbols[288]; /* Symbols sorted by code */
|
||||
int max_sym;
|
||||
};
|
||||
|
||||
struct tinf_data {
|
||||
const unsigned char *source;
|
||||
const unsigned char *source_end;
|
||||
unsigned int tag;
|
||||
int bitcount;
|
||||
int overflow;
|
||||
|
||||
unsigned char *dest_start;
|
||||
unsigned char *dest;
|
||||
unsigned char *dest_end;
|
||||
|
||||
struct tinf_tree ltree; /* Literal/length tree */
|
||||
struct tinf_tree dtree; /* Distance tree */
|
||||
};
|
||||
|
||||
/* -- Utility functions -- */
|
||||
|
||||
static unsigned int read_le16(const unsigned char *p)
|
||||
{
|
||||
return ((unsigned int) p[0])
|
||||
| ((unsigned int) p[1] << 8);
|
||||
}
|
||||
|
||||
/* Build fixed Huffman trees */
|
||||
static void tinf_build_fixed_trees(struct tinf_tree *lt, struct tinf_tree *dt)
|
||||
{
|
||||
int i;
|
||||
|
||||
/* Build fixed literal/length tree */
|
||||
for (i = 0; i < 16; ++i) {
|
||||
lt->counts[i] = 0;
|
||||
}
|
||||
|
||||
lt->counts[7] = 24;
|
||||
lt->counts[8] = 152;
|
||||
lt->counts[9] = 112;
|
||||
|
||||
for (i = 0; i < 24; ++i) {
|
||||
lt->symbols[i] = 256 + i;
|
||||
}
|
||||
for (i = 0; i < 144; ++i) {
|
||||
lt->symbols[24 + i] = i;
|
||||
}
|
||||
for (i = 0; i < 8; ++i) {
|
||||
lt->symbols[24 + 144 + i] = 280 + i;
|
||||
}
|
||||
for (i = 0; i < 112; ++i) {
|
||||
lt->symbols[24 + 144 + 8 + i] = 144 + i;
|
||||
}
|
||||
|
||||
lt->max_sym = 285;
|
||||
|
||||
/* Build fixed distance tree */
|
||||
for (i = 0; i < 16; ++i) {
|
||||
dt->counts[i] = 0;
|
||||
}
|
||||
|
||||
dt->counts[5] = 32;
|
||||
|
||||
for (i = 0; i < 32; ++i) {
|
||||
dt->symbols[i] = i;
|
||||
}
|
||||
|
||||
dt->max_sym = 29;
|
||||
}
|
||||
|
||||
/* Given an array of code lengths, build a tree */
|
||||
static int tinf_build_tree(struct tinf_tree *t, const unsigned char *lengths,
|
||||
unsigned int num)
|
||||
{
|
||||
unsigned short offs[16];
|
||||
unsigned int i, num_codes, available;
|
||||
|
||||
assert(num <= 288);
|
||||
|
||||
for (i = 0; i < 16; ++i) {
|
||||
t->counts[i] = 0;
|
||||
}
|
||||
|
||||
t->max_sym = -1;
|
||||
|
||||
/* Count number of codes for each non-zero length */
|
||||
for (i = 0; i < num; ++i) {
|
||||
assert(lengths[i] <= 15);
|
||||
|
||||
if (lengths[i]) {
|
||||
t->max_sym = i;
|
||||
t->counts[lengths[i]]++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compute offset table for distribution sort */
|
||||
for (available = 1, num_codes = 0, i = 0; i < 16; ++i) {
|
||||
unsigned int used = t->counts[i];
|
||||
|
||||
/* Check length contains no more codes than available */
|
||||
if (used > available) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
available = 2 * (available - used);
|
||||
|
||||
offs[i] = num_codes;
|
||||
num_codes += used;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check all codes were used, or for the special case of only one
|
||||
* code that it has length 1
|
||||
*/
|
||||
if ((num_codes > 1 && available > 0)
|
||||
|| (num_codes == 1 && t->counts[1] != 1)) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* Fill in symbols sorted by code */
|
||||
for (i = 0; i < num; ++i) {
|
||||
if (lengths[i]) {
|
||||
t->symbols[offs[lengths[i]]++] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* For the special case of only one code (which will be 0) add a
|
||||
* code 1 which results in a symbol that is too large
|
||||
*/
|
||||
if (num_codes == 1) {
|
||||
t->counts[1] = 2;
|
||||
t->symbols[1] = t->max_sym + 1;
|
||||
}
|
||||
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
/* -- Decode functions -- */
|
||||
|
||||
static void tinf_refill(struct tinf_data *d, int num)
|
||||
{
|
||||
assert(num >= 0 && num <= 32);
|
||||
|
||||
/* Read bytes until at least num bits available */
|
||||
while (d->bitcount < num) {
|
||||
if (d->source != d->source_end) {
|
||||
d->tag |= (unsigned int) *d->source++ << d->bitcount;
|
||||
}
|
||||
else {
|
||||
d->overflow = 1;
|
||||
}
|
||||
d->bitcount += 8;
|
||||
}
|
||||
|
||||
assert(d->bitcount <= 32);
|
||||
}
|
||||
|
||||
static unsigned int tinf_getbits_no_refill(struct tinf_data *d, int num)
|
||||
{
|
||||
unsigned int bits;
|
||||
|
||||
assert(num >= 0 && num <= d->bitcount);
|
||||
|
||||
/* Get bits from tag */
|
||||
bits = d->tag & ((1UL << num) - 1);
|
||||
|
||||
/* Remove bits from tag */
|
||||
d->tag >>= num;
|
||||
d->bitcount -= num;
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/* Get num bits from source stream */
|
||||
static unsigned int tinf_getbits(struct tinf_data *d, int num)
|
||||
{
|
||||
tinf_refill(d, num);
|
||||
return tinf_getbits_no_refill(d, num);
|
||||
}
|
||||
|
||||
/* Read a num bit value from stream and add base */
|
||||
static unsigned int tinf_getbits_base(struct tinf_data *d, int num, int base)
|
||||
{
|
||||
return base + (num ? tinf_getbits(d, num) : 0);
|
||||
}
|
||||
|
||||
/* Given a data stream and a tree, decode a symbol */
|
||||
static int tinf_decode_symbol(struct tinf_data *d, const struct tinf_tree *t)
|
||||
{
|
||||
int base = 0, offs = 0;
|
||||
int len;
|
||||
|
||||
/*
|
||||
* Get more bits while code index is above number of codes
|
||||
*
|
||||
* Rather than the actual code, we are computing the position of the
|
||||
* code in the sorted order of codes, which is the index of the
|
||||
* corresponding symbol.
|
||||
*
|
||||
* Conceptually, for each code length (level in the tree), there are
|
||||
* counts[len] leaves on the left and internal nodes on the right.
|
||||
* The index we have decoded so far is base + offs, and if that
|
||||
* falls within the leaves we are done. Otherwise we adjust the range
|
||||
* of offs and add one more bit to it.
|
||||
*/
|
||||
for (len = 1; ; ++len) {
|
||||
offs = 2 * offs + tinf_getbits(d, 1);
|
||||
|
||||
assert(len <= 15);
|
||||
|
||||
if (offs < t->counts[len]) {
|
||||
break;
|
||||
}
|
||||
|
||||
base += t->counts[len];
|
||||
offs -= t->counts[len];
|
||||
}
|
||||
|
||||
assert(base + offs >= 0 && base + offs < 288);
|
||||
|
||||
return t->symbols[base + offs];
|
||||
}
|
||||
|
||||
/* Given a data stream, decode dynamic trees from it */
|
||||
static int tinf_decode_trees(struct tinf_data *d, struct tinf_tree *lt,
|
||||
struct tinf_tree *dt)
|
||||
{
|
||||
unsigned char lengths[288 + 32];
|
||||
|
||||
/* Special ordering of code length codes */
|
||||
static const unsigned char clcidx[19] = {
|
||||
16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
|
||||
11, 4, 12, 3, 13, 2, 14, 1, 15
|
||||
};
|
||||
|
||||
unsigned int hlit, hdist, hclen;
|
||||
unsigned int i, num, length;
|
||||
int res;
|
||||
|
||||
/* Get 5 bits HLIT (257-286) */
|
||||
hlit = tinf_getbits_base(d, 5, 257);
|
||||
|
||||
/* Get 5 bits HDIST (1-32) */
|
||||
hdist = tinf_getbits_base(d, 5, 1);
|
||||
|
||||
/* Get 4 bits HCLEN (4-19) */
|
||||
hclen = tinf_getbits_base(d, 4, 4);
|
||||
|
||||
/*
|
||||
* The RFC limits the range of HLIT to 286, but lists HDIST as range
|
||||
* 1-32, even though distance codes 30 and 31 have no meaning. While
|
||||
* we could allow the full range of HLIT and HDIST to make it possible
|
||||
* to decode the fixed trees with this function, we consider it an
|
||||
* error here.
|
||||
*
|
||||
* See also: https://github.com/madler/zlib/issues/82
|
||||
*/
|
||||
if (hlit > 286 || hdist > 30) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
for (i = 0; i < 19; ++i) {
|
||||
lengths[i] = 0;
|
||||
}
|
||||
|
||||
/* Read code lengths for code length alphabet */
|
||||
for (i = 0; i < hclen; ++i) {
|
||||
/* Get 3 bits code length (0-7) */
|
||||
unsigned int clen = tinf_getbits(d, 3);
|
||||
|
||||
lengths[clcidx[i]] = clen;
|
||||
}
|
||||
|
||||
/* Build code length tree (in literal/length tree to save space) */
|
||||
res = tinf_build_tree(lt, lengths, 19);
|
||||
|
||||
if (res != TINF_OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Check code length tree is not empty */
|
||||
if (lt->max_sym == -1) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* Decode code lengths for the dynamic trees */
|
||||
for (num = 0; num < hlit + hdist; ) {
|
||||
int sym = tinf_decode_symbol(d, lt);
|
||||
|
||||
if (sym > lt->max_sym) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
switch (sym) {
|
||||
case 16:
|
||||
/* Copy previous code length 3-6 times (read 2 bits) */
|
||||
if (num == 0) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
sym = lengths[num - 1];
|
||||
length = tinf_getbits_base(d, 2, 3);
|
||||
break;
|
||||
case 17:
|
||||
/* Repeat code length 0 for 3-10 times (read 3 bits) */
|
||||
sym = 0;
|
||||
length = tinf_getbits_base(d, 3, 3);
|
||||
break;
|
||||
case 18:
|
||||
/* Repeat code length 0 for 11-138 times (read 7 bits) */
|
||||
sym = 0;
|
||||
length = tinf_getbits_base(d, 7, 11);
|
||||
break;
|
||||
default:
|
||||
/* Values 0-15 represent the actual code lengths */
|
||||
length = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (length > hlit + hdist - num) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
while (length--) {
|
||||
lengths[num++] = sym;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check EOB symbol is present */
|
||||
if (lengths[256] == 0) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* Build dynamic trees */
|
||||
res = tinf_build_tree(lt, lengths, hlit);
|
||||
|
||||
if (res != TINF_OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
res = tinf_build_tree(dt, lengths + hlit, hdist);
|
||||
|
||||
if (res != TINF_OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
/* -- Block inflate functions -- */
|
||||
|
||||
/* Given a stream and two trees, inflate a block of data */
|
||||
static int tinf_inflate_block_data(struct tinf_data *d, struct tinf_tree *lt,
|
||||
struct tinf_tree *dt)
|
||||
{
|
||||
/* Extra bits and base tables for length codes */
|
||||
static const unsigned char length_bits[30] = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
|
||||
4, 4, 4, 4, 5, 5, 5, 5, 0, 127
|
||||
};
|
||||
|
||||
static const unsigned short length_base[30] = {
|
||||
3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
|
||||
15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
|
||||
67, 83, 99, 115, 131, 163, 195, 227, 258, 0
|
||||
};
|
||||
|
||||
/* Extra bits and base tables for distance codes */
|
||||
static const unsigned char dist_bits[30] = {
|
||||
0, 0, 0, 0, 1, 1, 2, 2, 3, 3,
|
||||
4, 4, 5, 5, 6, 6, 7, 7, 8, 8,
|
||||
9, 9, 10, 10, 11, 11, 12, 12, 13, 13
|
||||
};
|
||||
|
||||
static const unsigned short dist_base[30] = {
|
||||
1, 2, 3, 4, 5, 7, 9, 13, 17, 25,
|
||||
33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
|
||||
1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
int sym = tinf_decode_symbol(d, lt);
|
||||
|
||||
/* Check for overflow in bit reader */
|
||||
if (d->overflow) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
if (sym < 256) {
|
||||
if (d->dest == d->dest_end) {
|
||||
return TINF_BUF_ERROR;
|
||||
}
|
||||
*d->dest++ = sym;
|
||||
}
|
||||
else {
|
||||
int length, dist, offs;
|
||||
int i;
|
||||
|
||||
/* Check for end of block */
|
||||
if (sym == 256) {
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
/* Check sym is within range and distance tree is not empty */
|
||||
if (sym > lt->max_sym || sym - 257 > 28 || dt->max_sym == -1) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
sym -= 257;
|
||||
|
||||
/* Possibly get more bits from length code */
|
||||
length = tinf_getbits_base(d, length_bits[sym],
|
||||
length_base[sym]);
|
||||
|
||||
dist = tinf_decode_symbol(d, dt);
|
||||
|
||||
/* Check dist is within range */
|
||||
if (dist > dt->max_sym || dist > 29) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* Possibly get more bits from distance code */
|
||||
offs = tinf_getbits_base(d, dist_bits[dist],
|
||||
dist_base[dist]);
|
||||
|
||||
if (offs > d->dest - d->dest_start) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
if (d->dest_end - d->dest < length) {
|
||||
return TINF_BUF_ERROR;
|
||||
}
|
||||
|
||||
/* Copy match */
|
||||
for (i = 0; i < length; ++i) {
|
||||
d->dest[i] = d->dest[i - offs];
|
||||
}
|
||||
|
||||
d->dest += length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Inflate an uncompressed block of data */
|
||||
static int tinf_inflate_uncompressed_block(struct tinf_data *d)
|
||||
{
|
||||
unsigned int length, invlength;
|
||||
|
||||
if (d->source_end - d->source < 4) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* Get length */
|
||||
length = read_le16(d->source);
|
||||
|
||||
/* Get one's complement of length */
|
||||
invlength = read_le16(d->source + 2);
|
||||
|
||||
/* Check length */
|
||||
if (length != (~invlength & 0x0000FFFF)) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
d->source += 4;
|
||||
|
||||
if (d->source_end - d->source < length) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
if (d->dest_end - d->dest < length) {
|
||||
return TINF_BUF_ERROR;
|
||||
}
|
||||
|
||||
/* Copy block */
|
||||
while (length--) {
|
||||
*d->dest++ = *d->source++;
|
||||
}
|
||||
|
||||
/* Make sure we start next block on a byte boundary */
|
||||
d->tag = 0;
|
||||
d->bitcount = 0;
|
||||
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
/* Inflate a block of data compressed with fixed Huffman trees */
|
||||
static int tinf_inflate_fixed_block(struct tinf_data *d)
|
||||
{
|
||||
/* Build fixed Huffman trees */
|
||||
tinf_build_fixed_trees(&d->ltree, &d->dtree);
|
||||
|
||||
/* Decode block using fixed trees */
|
||||
return tinf_inflate_block_data(d, &d->ltree, &d->dtree);
|
||||
}
|
||||
|
||||
/* Inflate a block of data compressed with dynamic Huffman trees */
|
||||
static int tinf_inflate_dynamic_block(struct tinf_data *d)
|
||||
{
|
||||
/* Decode trees from stream */
|
||||
int res = tinf_decode_trees(d, &d->ltree, &d->dtree);
|
||||
|
||||
if (res != TINF_OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Decode block using decoded trees */
|
||||
return tinf_inflate_block_data(d, &d->ltree, &d->dtree);
|
||||
}
|
||||
|
||||
/* -- Public functions -- */
|
||||
|
||||
/* Initialize global (static) data */
|
||||
void tinf_init(void)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Inflate stream from source to dest. Optionally require the final DEFLATE block to consume all source bytes. */
|
||||
static int tinf_uncompress_internal(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen,
|
||||
int require_exact_source)
|
||||
{
|
||||
struct tinf_data d;
|
||||
int bfinal;
|
||||
|
||||
/* Initialise data */
|
||||
d.source = (const unsigned char *) source;
|
||||
d.source_end = d.source + sourceLen;
|
||||
d.tag = 0;
|
||||
d.bitcount = 0;
|
||||
d.overflow = 0;
|
||||
|
||||
d.dest = (unsigned char *) dest;
|
||||
d.dest_start = d.dest;
|
||||
d.dest_end = d.dest + *destLen;
|
||||
|
||||
do {
|
||||
unsigned int btype;
|
||||
int res;
|
||||
|
||||
/* Read final block flag */
|
||||
bfinal = tinf_getbits(&d, 1);
|
||||
|
||||
/* Read block type (2 bits) */
|
||||
btype = tinf_getbits(&d, 2);
|
||||
|
||||
/* Decompress block */
|
||||
switch (btype) {
|
||||
case 0:
|
||||
/* Decompress uncompressed block */
|
||||
res = tinf_inflate_uncompressed_block(&d);
|
||||
break;
|
||||
case 1:
|
||||
/* Decompress block with fixed Huffman trees */
|
||||
res = tinf_inflate_fixed_block(&d);
|
||||
break;
|
||||
case 2:
|
||||
/* Decompress block with dynamic Huffman trees */
|
||||
res = tinf_inflate_dynamic_block(&d);
|
||||
break;
|
||||
default:
|
||||
res = TINF_DATA_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
if (res != TINF_OK) {
|
||||
return res;
|
||||
}
|
||||
} while (!bfinal);
|
||||
|
||||
/* Check for overflow in bit reader */
|
||||
if (d.overflow) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
/* d.source includes the byte containing any legal final padding bits, but not a following whole byte. */
|
||||
if (require_exact_source && d.source != d.source_end) {
|
||||
return TINF_DATA_ERROR;
|
||||
}
|
||||
|
||||
*destLen = d.dest - d.dest_start;
|
||||
|
||||
return TINF_OK;
|
||||
}
|
||||
|
||||
int tinf_uncompress(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen)
|
||||
{
|
||||
return tinf_uncompress_internal(dest, destLen, source, sourceLen, 0);
|
||||
}
|
||||
|
||||
int tinf_uncompress_exact(void *dest, unsigned int *destLen,
|
||||
const void *source, unsigned int sourceLen)
|
||||
{
|
||||
return tinf_uncompress_internal(dest, destLen, source, sourceLen, 1);
|
||||
}
|
||||
|
||||
/* clang -g -O1 -fsanitize=fuzzer,address -DTINF_FUZZING tinflate.c */
|
||||
#if defined(TINF_FUZZING)
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
unsigned char depacked[64 * 1024];
|
||||
|
||||
extern int
|
||||
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
{
|
||||
if (size > UINT_MAX / 2) { return 0; }
|
||||
unsigned int destLen = sizeof(depacked);
|
||||
tinf_uncompress(depacked, &destLen, data, size);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MESHCORE_TINF_IMPLEMENTATION */
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Focused tests for the reproducible RAK3401 receive-inflate builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import build_rak3401_inflate_bridges as inflate
|
||||
|
||||
|
||||
class InflateAssetTests(unittest.TestCase):
|
||||
def test_vendored_snapshot_assets_match_their_pins(self) -> None:
|
||||
metadata = inflate.inflate_asset_metadata()
|
||||
|
||||
self.assertEqual(set(metadata), set(inflate.INFLATE_ASSETS))
|
||||
for destination, (_asset, expected_hash) in inflate.INFLATE_ASSETS.items():
|
||||
self.assertEqual(metadata[destination]["sha256"], expected_hash)
|
||||
|
||||
def test_install_and_remove_are_exact_inverses(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory)
|
||||
inflate.install_inflate_assets(source)
|
||||
for destination, (_asset, expected_hash) in inflate.INFLATE_ASSETS.items():
|
||||
self.assertEqual(
|
||||
inflate.sha256_file(source / destination), expected_hash
|
||||
)
|
||||
|
||||
inflate.remove_inflate_assets(source)
|
||||
self.assertFalse((source / "src/helpers/ota/tinf").exists())
|
||||
for destination in inflate.INFLATE_ASSETS:
|
||||
self.assertFalse((source / destination).exists())
|
||||
|
||||
|
||||
class ExactTransformTests(unittest.TestCase):
|
||||
def test_replace_exact_replaces_one_anchor(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "source.txt"
|
||||
path.write_bytes(b"before anchor after")
|
||||
inflate.replace_exact(path, b"anchor", b"replacement", "test")
|
||||
self.assertEqual(path.read_bytes(), b"before replacement after")
|
||||
|
||||
def test_replace_exact_rejects_missing_or_ambiguous_anchor(self) -> None:
|
||||
for contents, count in ((b"none", 0), (b"anchor anchor", 2)):
|
||||
with self.subTest(count=count), tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "source.txt"
|
||||
path.write_bytes(contents)
|
||||
with self.assertRaisesRegex(
|
||||
inflate.BuildError, f"found {count}"
|
||||
):
|
||||
inflate.replace_exact(path, b"anchor", b"replacement", "test")
|
||||
|
||||
|
||||
class BuilderConfigurationTests(unittest.TestCase):
|
||||
def test_inflate_builder_has_distinct_reproducibility_identity(self) -> None:
|
||||
self.assertEqual(
|
||||
inflate.VERSION_SUFFIX, "halo-keymind-cascade-mota-inflate"
|
||||
)
|
||||
self.assertEqual(
|
||||
inflate.INFLATE_SNAPSHOT_COMMIT,
|
||||
"add51bf00c46c15ef54318ca766a6daf08a147ee",
|
||||
)
|
||||
self.assertNotIn("6f03eae8", {target.source_commit[:8] for target in inflate.TARGETS})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user