Add compressed 171-byte OTA transport
This commit is contained in:
1 parent
ef92505429
commit
3f6eddd5c2
20 files changed
+2273
-46
No files matched your search
@@ -17,10 +17,17 @@
|
||||
// OP_COUNT 0x01 args: - resp payload: count(1)
|
||||
// OP_DESCRIBE 0x02 args: idx(1) resp payload: MotaDesc wire (38 B, see below) [status OK]
|
||||
// OP_READ 0x03 args: idx(1) off(4) len(2) resp payload: len bytes [status OK]
|
||||
// OP_DEFLATE_BLOCK 0x09 args: idx(1) block(2) off(2) len(2)
|
||||
// resp payload: total_encoded_len(2) + len encoded bytes [status OK]
|
||||
// `len=0` queries the size. Chunks are <=190 bytes so the full response payload remains
|
||||
// <=192. The host returns ERR unless independent raw-RFC1951 compression is smaller than
|
||||
// the original logical block; callers then use ordinary OP_READ/raw OTA_DATA v2.
|
||||
//
|
||||
// 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)
|
||||
// block_size_log2(1) reserved(3)
|
||||
// block_size_log2(1) source_caps(1) reserved(2)
|
||||
// source_caps bit 0 = OP_DEFLATE_BLOCK is implemented. Deployed hosts send zero, so a new node never
|
||||
// waits on an unsupported operation during a rolling host upgrade.
|
||||
//
|
||||
// --- STORAGE ops (device -> host WRITE): "pull to folder". The device is fetching a `.mota` off the mesh
|
||||
// (e.g. a neighbour's self-served firmware it has no local copy of) and streams it into the host folder as
|
||||
@@ -52,14 +59,16 @@ static const uint8_t MS_OP_BEGIN = 0x05; // storage: create/truncate <mid>
|
||||
static const uint8_t MS_OP_WRITE = 0x06; // storage: write bytes at offset
|
||||
static const uint8_t MS_OP_SREAD = 0x07; // storage: read bytes back (resume: recompute missing blocks)
|
||||
static const uint8_t MS_OP_FIN = 0x08; // storage: transfer complete - validate + make servable
|
||||
static const uint8_t MS_OP_DEFLATE_BLOCK = 0x09; // source: independently compressed payload block/chunk
|
||||
|
||||
static const uint8_t MS_STATUS_OK = 0x00;
|
||||
static const uint8_t MS_STATUS_ERR = 0x01;
|
||||
|
||||
static const uint16_t MOTA_DESC_WIRE = 38; // existing reserved bytes carry block geometry; wire size is unchanged
|
||||
static const uint16_t MOTA_DESC_WIRE = 38; // existing reserved bytes carry geometry/caps; wire size is unchanged
|
||||
// Keep host-to-device replies below common 256-byte USB CDC/UART receive rings. Manifest leaves can exceed
|
||||
// that ring, and payload reads are normally 1 KB; splitting them here prevents silent overrun/checksum loss.
|
||||
static const uint16_t MOTA_SEEDER_READ_MAX = 192;
|
||||
static const uint16_t MOTA_SEEDER_DEFLATE_CHUNK_MAX = 190; // plus uint16 total length = 192-byte response payload
|
||||
static const uint16_t MOTA_SEEDER_WRITE_MAX = 512; // max data bytes per OP_WRITE/OP_SREAD (bounds frames)
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -86,7 +86,8 @@ bool SerialMotaSource::describe(uint8_t idx, MotaDesc& out) {
|
||||
out.payload_off = rd_u32le(w + 26);
|
||||
out.payload_size = rd_u32le(w + 30);
|
||||
out.block_size_log2 = w[34];
|
||||
// bytes [35,38) remain reserved (zero) for forward compatibility.
|
||||
out.source_caps = w[35];
|
||||
// bytes [36,38) remain reserved (zero) for forward compatibility.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,5 +108,35 @@ bool SerialMotaSource::read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t le
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SerialMotaSource::read_deflated_block(uint8_t idx, uint16_t block, uint8_t* buf,
|
||||
uint16_t cap, uint16_t* len) {
|
||||
if (!buf || !len || cap == 0) return false;
|
||||
*len = 0;
|
||||
|
||||
uint8_t args[7] = { idx };
|
||||
args[1] = (uint8_t)(block & 0xFF); args[2] = (uint8_t)(block >> 8);
|
||||
// Query the independently compressed representation's exact size first. The old host returns an error for
|
||||
// the unknown operation, which is the intentional raw fallback during a rolling deployment.
|
||||
uint8_t total_wire[2];
|
||||
if (!txn(MS_OP_DEFLATE_BLOCK, args, sizeof(args), total_wire, sizeof(total_wire))) return false;
|
||||
const uint16_t total = rd_u16le(total_wire);
|
||||
if (total == 0 || total > cap) return false;
|
||||
|
||||
uint16_t done = 0;
|
||||
while (done < total) {
|
||||
const uint16_t chunk = (uint16_t)((total - done > MOTA_SEEDER_DEFLATE_CHUNK_MAX)
|
||||
? MOTA_SEEDER_DEFLATE_CHUNK_MAX : total - done);
|
||||
args[3] = (uint8_t)(done & 0xFF); args[4] = (uint8_t)(done >> 8);
|
||||
args[5] = (uint8_t)(chunk & 0xFF); args[6] = (uint8_t)(chunk >> 8);
|
||||
uint8_t response[2 + MOTA_SEEDER_DEFLATE_CHUNK_MAX];
|
||||
if (!txn(MS_OP_DEFLATE_BLOCK, args, sizeof(args), response, (uint16_t)(2 + chunk)) ||
|
||||
rd_u16le(response) != total) return false;
|
||||
memcpy(buf + done, response + 2, chunk);
|
||||
done = (uint16_t)(done + chunk);
|
||||
}
|
||||
*len = total;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -22,6 +22,8 @@ public:
|
||||
uint8_t count() override;
|
||||
bool describe(uint8_t idx, MotaDesc& out) override;
|
||||
bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) override;
|
||||
bool read_deflated_block(uint8_t idx, uint16_t block, uint8_t* buf,
|
||||
uint16_t cap, uint16_t* len) override;
|
||||
|
||||
private:
|
||||
// Send a request (op+args) and read its response header; on OK, `payload` (if non-null) receives
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdlib.h> // malloc/free (lazy ESP32 manual-stage buffer)
|
||||
#include <string.h> // strncmp/strncpy (hw_id)
|
||||
#include "OtaManager.h"
|
||||
#include "OtaDeflate.h"
|
||||
#include "OtaStore.h"
|
||||
#include "SignerAllowlist.h"
|
||||
#include "OtaApply.h"
|
||||
@@ -562,6 +563,10 @@ struct OtaContext {
|
||||
target_id = 0;
|
||||
#endif
|
||||
manager.begin(target_id, send, ctx);
|
||||
// Every OTA-capable application can receive ordinary raw RFC1951 transport blocks. OtaManager advertises
|
||||
// that permission only because this exact, bounded decoder is registered; non-OTA builds do not compile
|
||||
// OtaContext or link the decoder implementation.
|
||||
manager.set_transport_deflate_decoder(ota_transport_inflate);
|
||||
manager.set_auto_version_floor(_fi.valid ? _fi.fw_version : 0, true);
|
||||
fetch_to_folder = false;
|
||||
if (hw) { strncpy(hw_id, hw, sizeof(hw_id) - 1); hw_id[sizeof(hw_id) - 1] = 0; }
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "OtaDeflate.h"
|
||||
|
||||
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST)
|
||||
|
||||
extern "C" {
|
||||
#include "tinf/tinf.h"
|
||||
}
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif // ENABLE_OTA || OTA_TRANSPORT_DEFLATE_TEST
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.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);
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
+278
-28
@@ -69,6 +69,7 @@ bool OtaManager::serve(const uint8_t* mota, uint32_t len) {
|
||||
if (!mota_parse(mota, len, _view0.m)) return false;
|
||||
_view0.mfl = (uint16_t)(_view0.m.leaves - _view0.m.manifest_start); // contiguous container
|
||||
_view0.read = nullptr; _view0.read_ctx = nullptr; // payload is contiguous _view0.m.payload
|
||||
_view0.read_deflated = nullptr; _view0.deflate_ctx = nullptr;
|
||||
_view0.scratch = scratch; _view0.scratch_sz = OTA_PROOFGEN_SCRATCH; // <=1024 blocks (RAM .mota is small)
|
||||
_view0.valid = true;
|
||||
clearPendingEgress();
|
||||
@@ -86,6 +87,7 @@ bool OtaManager::serve_self(const uint8_t* manifest, uint16_t mfl, const uint8_t
|
||||
_view0.m.payload = nullptr; // read on demand via `read`
|
||||
_view0.m.block_count = block_count;
|
||||
_view0.mfl = mfl; _view0.read = read; _view0.read_ctx = ctx;
|
||||
_view0.read_deflated = nullptr; _view0.deflate_ctx = nullptr;
|
||||
_view0.scratch = proof_scratch; _view0.scratch_sz = proof_scratch_sz; // sized for our (large) image
|
||||
_view0.valid = true;
|
||||
clearPendingEgress();
|
||||
@@ -259,6 +261,9 @@ bool OtaManager::loadSource(const ServeEntry& e) {
|
||||
_srcv_rdctx.src = e.src; _srcv_rdctx.idx = e.src_idx;
|
||||
_srcv_rdctx.payload_off = d.payload_off;
|
||||
_srcv.read = srcReadTramp; _srcv.read_ctx = &_srcv_rdctx;
|
||||
_srcv.read_deflated = (d.source_caps & MOTA_SOURCE_CAP_DEFLATE_BLOCK)
|
||||
? srcDeflateReadTramp : nullptr;
|
||||
_srcv.deflate_ctx = _srcv.read_deflated ? &_srcv_rdctx : nullptr;
|
||||
_srcv.scratch = scratch; _srcv.scratch_sz = OTA_PROOFGEN_SCRATCH;
|
||||
memcpy(_srcv_mid, d.mid, 4);
|
||||
_srcv.valid = true;
|
||||
@@ -271,6 +276,12 @@ bool OtaManager::srcReadTramp(void* c, uint32_t off, uint8_t* buf, uint32_t len)
|
||||
return x->src->read(x->idx, x->payload_off + off, buf, len);
|
||||
}
|
||||
|
||||
bool OtaManager::srcDeflateReadTramp(void* c, uint16_t block, uint8_t* buf,
|
||||
uint16_t cap, uint16_t* len) {
|
||||
SrcReadCtx* x = (SrcReadCtx*)c;
|
||||
return x->src->read_deflated_block(x->idx, block, buf, cap, len);
|
||||
}
|
||||
|
||||
// sha2-256:4 over the SORTED set of mids we serve - peers use it to tell if our offering changed. Sorting
|
||||
// makes it canonical across nodes regardless of insert order; for a single mota it is mh4(mid) (unchanged).
|
||||
void OtaManager::setDigest(uint8_t out[4]) const {
|
||||
@@ -414,10 +425,15 @@ static inline uint16_t frag_full_mask(uint32_t nf) {
|
||||
// Retain a bounded descriptor instead of allocating every DATA packet inline. Repeated requests that arrive
|
||||
// while a block is already queued merge only fragments not yet admitted to the radio queue; a later retry can
|
||||
// enqueue the block again if one of those admitted packets was actually lost over the air.
|
||||
bool OtaManager::queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want_mask) {
|
||||
bool OtaManager::queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want_mask,
|
||||
bool wire_v2, bool allow_deflate) {
|
||||
for (uint8_t i = 0; i < _n_serve_jobs; i++) {
|
||||
ServeJob& job = _serve_jobs[i];
|
||||
if (job.block != block || memcmp(job.mid, mid, 4) != 0) continue;
|
||||
// A proof-only request can complete whichever representation is already queued. DATA requests with
|
||||
// different geometries remain distinct because their bitmap positions do not describe the same bytes.
|
||||
if (want_mask != 0 && (job.wire_v2 != wire_v2 ||
|
||||
(wire_v2 && job.allow_deflate != allow_deflate))) continue;
|
||||
job.pending_mask |= (uint16_t)(want_mask & ~job.emitted_mask);
|
||||
if (want_mask == 0) job.proof_requested = true;
|
||||
return true;
|
||||
@@ -430,6 +446,8 @@ bool OtaManager::queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want
|
||||
job.emitted_mask = 0;
|
||||
job.proof_ready_at = 0;
|
||||
job.proof_requested = want_mask == 0;
|
||||
job.wire_v2 = wire_v2;
|
||||
job.allow_deflate = allow_deflate;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -475,6 +493,10 @@ void OtaManager::clearPendingEgress() {
|
||||
_n_serve_jobs = 0;
|
||||
_serve_block_len = 0;
|
||||
_serve_block_loaded = false;
|
||||
_serve_wire_block = nullptr;
|
||||
_serve_wire_len = 0;
|
||||
_serve_wire_deflated = false;
|
||||
memset(_serve_wire_id, 0, sizeof(_serve_wire_id));
|
||||
}
|
||||
|
||||
void OtaManager::popManifestJob() {
|
||||
@@ -546,6 +568,10 @@ void OtaManager::popServeJob() {
|
||||
_n_serve_jobs--;
|
||||
_serve_block_len = 0;
|
||||
_serve_block_loaded = false;
|
||||
_serve_wire_block = nullptr;
|
||||
_serve_wire_len = 0;
|
||||
_serve_wire_deflated = false;
|
||||
memset(_serve_wire_id, 0, sizeof(_serve_wire_id));
|
||||
}
|
||||
|
||||
bool OtaManager::loadActiveServeBlock() {
|
||||
@@ -563,6 +589,30 @@ bool OtaManager::loadActiveServeBlock() {
|
||||
memcpy(_serve_block, v->m.payload + off, blen);
|
||||
}
|
||||
_serve_block_len = (uint16_t)blen;
|
||||
_serve_wire_block = _serve_block;
|
||||
_serve_wire_len = _serve_block_len;
|
||||
_serve_wire_deflated = false;
|
||||
|
||||
// A host-backed MotaSource can provide a pre-compressed block without putting an encoder in firmware.
|
||||
// Other sources may opt into a caller-supplied encoder. Both write into the proof scratch, which is safe
|
||||
// until every DATA fragment has left; proof generation reuses it only after that point.
|
||||
if (job.wire_v2 && job.allow_deflate && v->scratch && v->scratch_sz >= blen) {
|
||||
uint16_t encoded_len = 0;
|
||||
bool encoded = false;
|
||||
if (v->read_deflated) {
|
||||
encoded = v->read_deflated(v->deflate_ctx, job.block, v->scratch,
|
||||
(uint16_t)blen, &encoded_len);
|
||||
} else if (_deflate_encode) {
|
||||
encoded = _deflate_encode(_deflate_encode_ctx, _serve_block, (uint16_t)blen,
|
||||
v->scratch, (uint16_t)blen, &encoded_len);
|
||||
}
|
||||
if (encoded && encoded_len > 0 && encoded_len < blen) {
|
||||
_serve_wire_block = v->scratch;
|
||||
_serve_wire_len = encoded_len;
|
||||
_serve_wire_deflated = true;
|
||||
}
|
||||
}
|
||||
mh4(_serve_wire_id, _serve_wire_block, _serve_wire_len);
|
||||
_serve_block_loaded = true;
|
||||
return true;
|
||||
}
|
||||
@@ -580,9 +630,17 @@ bool OtaManager::handleReq(const uint8_t* m, uint16_t n) {
|
||||
uint32_t blen = block_size;
|
||||
const uint32_t off = idx * block_size;
|
||||
if (off + blen > v->m.payload_size) blen = v->m.payload_size - off;
|
||||
const uint16_t valid_mask = frag_full_mask((blen + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA);
|
||||
const uint16_t want = (uint16_t)(rq.items[i].want_mask & valid_mask);
|
||||
if (want != 0) accepted |= queueServeJob(v->m.merkle_root, (uint16_t)idx, want);
|
||||
const bool wire_v2 = ota_req_is_v2(rq.items[i].want_mask);
|
||||
const bool allow_deflate = wire_v2 &&
|
||||
(rq.items[i].want_mask & OTA_REQ_V2_ALLOW_DEFLATE) != 0;
|
||||
const uint16_t valid_mask = frag_full_mask((blen +
|
||||
(wire_v2 ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA) - 1) /
|
||||
(wire_v2 ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA));
|
||||
const uint16_t requested = wire_v2
|
||||
? ota_req_v2_fragments(rq.items[i].want_mask) : rq.items[i].want_mask;
|
||||
const uint16_t want = (uint16_t)(requested & valid_mask);
|
||||
if (want != 0) accepted |= queueServeJob(v->m.merkle_root, (uint16_t)idx, want,
|
||||
wire_v2, allow_deflate);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
@@ -630,21 +688,41 @@ void OtaManager::serviceEgress() {
|
||||
popServeJob();
|
||||
return;
|
||||
}
|
||||
const uint16_t fragment_data = job.wire_v2 ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA;
|
||||
const uint16_t valid_mask = frag_full_mask(
|
||||
(_serve_wire_len + fragment_data - 1u) / fragment_data);
|
||||
job.pending_mask &= valid_mask;
|
||||
if (job.pending_mask == 0) {
|
||||
job.proof_ready_at = _now_ms + proofEgressGapMs();
|
||||
return;
|
||||
}
|
||||
uint8_t fragment = 0;
|
||||
while (fragment < 16 && !(job.pending_mask & (1u << fragment))) fragment++;
|
||||
uint32_t frag_off = (uint32_t)fragment * OTA_FRAG_DATA;
|
||||
if (fragment >= 16 || frag_off >= _serve_block_len) {
|
||||
uint32_t frag_off = (uint32_t)fragment * fragment_data;
|
||||
if (fragment >= 16 || frag_off >= _serve_wire_len) {
|
||||
job.pending_mask = 0;
|
||||
return;
|
||||
}
|
||||
uint32_t frag_len = _serve_block_len - frag_off;
|
||||
if (frag_len > OTA_FRAG_DATA) frag_len = OTA_FRAG_DATA;
|
||||
uint32_t frag_len = _serve_wire_len - frag_off;
|
||||
if (frag_len > fragment_data) frag_len = fragment_data;
|
||||
DataMsg dm;
|
||||
memcpy(dm.manifest_id, job.mid, 4);
|
||||
dm.block_idx = job.block;
|
||||
dm.frag_off = (uint16_t)frag_off;
|
||||
dm.data = _serve_block + frag_off;
|
||||
dm.data_len = (uint16_t)frag_len;
|
||||
uint8_t v2_data[OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2];
|
||||
if (job.wire_v2) {
|
||||
if (!ota_data_v2_pack(fragment, _serve_wire_len, _serve_wire_deflated, dm.frag_off)) {
|
||||
popServeJob();
|
||||
return;
|
||||
}
|
||||
memcpy(v2_data, _serve_wire_id, OTA_DATA_V2_STREAM_ID_BYTES);
|
||||
memcpy(v2_data + OTA_DATA_V2_STREAM_ID_BYTES, _serve_wire_block + frag_off, frag_len);
|
||||
dm.data = v2_data;
|
||||
dm.data_len = (uint16_t)(OTA_DATA_V2_STREAM_ID_BYTES + frag_len);
|
||||
} else {
|
||||
dm.frag_off = (uint16_t)frag_off;
|
||||
dm.data = _serve_block + frag_off;
|
||||
dm.data_len = (uint16_t)frag_len;
|
||||
}
|
||||
uint8_t b[MAX_PACKET_PAYLOAD];
|
||||
if (emit(b, encode_data(b, sizeof(b), dm), false)) {
|
||||
const uint16_t bit = (uint16_t)(1u << fragment);
|
||||
@@ -944,6 +1022,11 @@ void OtaManager::clearReassemblySlot(uint8_t slot) {
|
||||
_reasm[slot].need = 0;
|
||||
_reasm[slot].awaiting_proof = false;
|
||||
_reasm[slot].proof_request_at = 0;
|
||||
_reasm[slot].encoded_len = 0;
|
||||
_reasm[slot].wire_v2 = false;
|
||||
_reasm[slot].deflated = false;
|
||||
memset(_reasm[slot].stream_id, 0, sizeof(_reasm[slot].stream_id));
|
||||
_reasm[slot].wire_stalls = 0;
|
||||
}
|
||||
|
||||
// Forget all blocks currently being reassembled or awaiting proofs.
|
||||
@@ -956,6 +1039,35 @@ void OtaManager::clearReassembly() {
|
||||
noteFetchActivity();
|
||||
}
|
||||
|
||||
bool OtaManager::anyWireDataReceived() const {
|
||||
for (uint8_t slot = 0; slot < OTA_FETCH_PIPELINE; slot++) {
|
||||
if (_reasm[slot].block != NO_BLOCK && _reasm[slot].mask != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void OtaManager::downgradeWireToLegacy() {
|
||||
if (!_wire_v2_session) return;
|
||||
_wire_v2_session = false;
|
||||
_wire_v2_confirmed = false;
|
||||
_wire_allow_deflate = false;
|
||||
_wire_empty_stalls = 0;
|
||||
for (uint8_t i = 0; i < OTA_FETCH_PIPELINE; i++) {
|
||||
ReassemblySlot& slot = _reasm[i];
|
||||
if (slot.block == NO_BLOCK) continue;
|
||||
slot.mask = 0;
|
||||
slot.need = frag_full_mask((blockLen(slot.block) + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA);
|
||||
slot.awaiting_proof = false;
|
||||
slot.proof_request_at = 0;
|
||||
slot.encoded_len = 0;
|
||||
slot.wire_v2 = false;
|
||||
slot.deflated = false;
|
||||
memset(slot.stream_id, 0, sizeof(slot.stream_id));
|
||||
slot.wire_stalls = 0;
|
||||
}
|
||||
OTA_DBG("OTA: source uses legacy 160-byte DATA geometry\n");
|
||||
}
|
||||
|
||||
int OtaManager::findReassemblySlot(uint32_t block) const {
|
||||
for (uint8_t slot = 0; slot < OTA_FETCH_PIPELINE; slot++) {
|
||||
if (_reasm[slot].block == block) return slot;
|
||||
@@ -1007,7 +1119,8 @@ uint32_t OtaManager::fetchRetryTimeoutMs() const {
|
||||
: (uint32_t)count_mask_bits((uint16_t)(slot.need & ~slot.mask)) + 1u;
|
||||
}
|
||||
if (active == 0) {
|
||||
const uint32_t fragments = (_fbs + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA;
|
||||
const uint32_t fragment_data = _wire_v2_session ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA;
|
||||
const uint32_t fragments = (_fbs + fragment_data - 1) / fragment_data;
|
||||
packets += (fragments + 1) * _pipeline_width;
|
||||
}
|
||||
uint32_t path_transmissions = _observed_path_transmissions;
|
||||
@@ -1129,6 +1242,10 @@ OtaManager::PullResult OtaManager::startFetch(const uint8_t* mid, uint32_t targe
|
||||
if (!_fetch) return PULL_NO_STORE;
|
||||
if (fetchActive()) return PULL_BUSY;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_wire_v2_session = _wire_v2_enabled;
|
||||
_wire_v2_confirmed = false;
|
||||
_wire_allow_deflate = _wire_v2_session && _deflate_decode != nullptr;
|
||||
_wire_empty_stalls = 0;
|
||||
_fexpected_target = target;
|
||||
_validate = validate; // motatool folder-capture warm-start (seed leaf-diff)
|
||||
// A validate pull is a FRESH seed capture, not a resume: the store already holds the seed's payload (not a
|
||||
@@ -1149,6 +1266,10 @@ OtaManager::PullResult OtaManager::startFetch(const uint8_t* mid, uint32_t targe
|
||||
bool OtaManager::resumeStagedExplicit(const uint8_t* want_mid, uint32_t expected_target) {
|
||||
if (!want_mid || !_fetch || fetchActive()) return false;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_wire_v2_session = _wire_v2_enabled;
|
||||
_wire_v2_confirmed = false;
|
||||
_wire_allow_deflate = _wire_v2_session && _deflate_decode != nullptr;
|
||||
_wire_empty_stalls = 0;
|
||||
_validate = false;
|
||||
_archive_fetch = false;
|
||||
_desired_target = expected_target;
|
||||
@@ -1350,6 +1471,10 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
|
||||
|| _fstate == WANT_LEAVES || _fstate == VERIFYING_STAGED) {
|
||||
return false;
|
||||
}
|
||||
_wire_v2_session = _wire_v2_enabled;
|
||||
_wire_v2_confirmed = false;
|
||||
_wire_allow_deflate = _wire_v2_session && _deflate_decode != nullptr;
|
||||
_wire_empty_stalls = 0;
|
||||
if (!_fetch->reopen()) return false; // nothing persisted in the store
|
||||
uint32_t total = _fetch->staged_size();
|
||||
uint8_t hdr[8];
|
||||
@@ -1509,19 +1634,100 @@ bool OtaManager::handleData(const uint8_t* m, uint16_t n) {
|
||||
if (slot_index < 0) return false; // not one of this node's requested blocks
|
||||
ReassemblySlot& slot = _reasm[slot_index];
|
||||
uint32_t blen = blockLen(dm.block_idx);
|
||||
if (dm.frag_off % OTA_FRAG_DATA != 0) return false; // canonical FRAG_DATA-aligned slices only
|
||||
if ((uint32_t)dm.frag_off + dm.data_len > blen) return false; // slice out of the block
|
||||
uint32_t expected_len = blen - dm.frag_off;
|
||||
if (expected_len > OTA_FRAG_DATA) expected_len = OTA_FRAG_DATA;
|
||||
if (dm.data_len != expected_len) return false; // do not mark a short slice as complete
|
||||
uint32_t kf = dm.frag_off / OTA_FRAG_DATA;
|
||||
if (kf >= 16) return false;
|
||||
const uint16_t bit = (uint16_t)(1u << kf);
|
||||
if (slot.mask & bit) return true; // duplicate flood/retry copy is terminal too
|
||||
memcpy(slot.buf + dm.frag_off, dm.data, dm.data_len);
|
||||
slot.mask |= bit;
|
||||
|
||||
if ((dm.frag_off & OTA_DATA_V2_MARK) == 0) {
|
||||
if (dm.frag_off % OTA_FRAG_DATA != 0) return false; // canonical legacy-aligned slices only
|
||||
if ((uint32_t)dm.frag_off + dm.data_len > blen) return false;
|
||||
uint32_t expected_len = blen - dm.frag_off;
|
||||
if (expected_len > OTA_FRAG_DATA) expected_len = OTA_FRAG_DATA;
|
||||
if (dm.data_len != expected_len) return false; // do not mark a short slice as complete
|
||||
uint32_t kf = dm.frag_off / OTA_FRAG_DATA;
|
||||
if (kf >= 16) return false;
|
||||
// A deployed source ignores the v2 request bits, masks them against its seven-fragment geometry, and
|
||||
// responds here. Only a fully canonical legacy slice may select that profile; malformed matching-MID
|
||||
// traffic must not force a session-wide downgrade. Never mix it with already accepted v2 bytes.
|
||||
if (_wire_v2_session) {
|
||||
if (_wire_v2_confirmed || anyWireDataReceived()) return false;
|
||||
downgradeWireToLegacy();
|
||||
}
|
||||
const uint16_t bit = (uint16_t)(1u << kf);
|
||||
if (slot.mask & bit) return true; // duplicate flood/retry copy is terminal too
|
||||
memcpy(slot.buf + dm.frag_off, dm.data, dm.data_len);
|
||||
slot.mask |= bit;
|
||||
} else {
|
||||
if (!_wire_v2_session || dm.data_len <= OTA_DATA_V2_STREAM_ID_BYTES) return false;
|
||||
uint8_t fragment = 0;
|
||||
uint16_t encoded_len = 0;
|
||||
bool deflated = false;
|
||||
if (!ota_data_v2_unpack(dm.frag_off, fragment, encoded_len, deflated)) return false;
|
||||
if (encoded_len > blen || (deflated ? encoded_len >= blen : encoded_len != blen)) return false;
|
||||
const uint16_t fragment_len = (uint16_t)(dm.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;
|
||||
uint32_t expected_len = encoded_len - fragment_off;
|
||||
if (expected_len > OTA_FRAG_DATA_V2) expected_len = OTA_FRAG_DATA_V2;
|
||||
if (fragment_len != expected_len) return false;
|
||||
_wire_v2_confirmed = true;
|
||||
|
||||
const uint8_t* stream_id = dm.data;
|
||||
if (slot.encoded_len == 0) {
|
||||
slot.encoded_len = encoded_len;
|
||||
slot.wire_v2 = true;
|
||||
slot.deflated = deflated;
|
||||
memcpy(slot.stream_id, stream_id, sizeof(slot.stream_id));
|
||||
slot.mask = 0;
|
||||
slot.need = frag_full_mask((encoded_len + OTA_FRAG_DATA_V2 - 1) / OTA_FRAG_DATA_V2);
|
||||
} else if (!slot.wire_v2 || slot.encoded_len != encoded_len || slot.deflated != deflated ||
|
||||
memcmp(slot.stream_id, stream_id, sizeof(slot.stream_id)) != 0) {
|
||||
return false; // another seeder's representation; never mix
|
||||
}
|
||||
_wire_empty_stalls = 0; // a valid v2 source is still serving this flight
|
||||
const uint16_t bit = (uint16_t)(1u << fragment);
|
||||
if (!(slot.need & bit)) return false;
|
||||
if (slot.mask & bit) return true;
|
||||
memcpy(slot.buf + fragment_off, dm.data + OTA_DATA_V2_STREAM_ID_BYTES, fragment_len);
|
||||
slot.mask |= bit;
|
||||
slot.wire_stalls = 0;
|
||||
}
|
||||
noteFetchActivity();
|
||||
if (slot.mask != slot.need || slot.awaiting_proof) return true;
|
||||
|
||||
if (slot.wire_v2) {
|
||||
uint8_t stream_id[4];
|
||||
mh4(stream_id, slot.buf, slot.encoded_len);
|
||||
if (memcmp(stream_id, slot.stream_id, sizeof(stream_id)) != 0) {
|
||||
slot.mask = 0; // complete bytes do not match their representation id
|
||||
slot.encoded_len = 0;
|
||||
slot.wire_v2 = false;
|
||||
slot.deflated = false;
|
||||
memset(slot.stream_id, 0, sizeof(slot.stream_id));
|
||||
notePipelineStall();
|
||||
return true;
|
||||
}
|
||||
if (slot.deflated) {
|
||||
uint16_t decoded_len = 0;
|
||||
bool decoded = _deflate_decode && _deflate_decode(
|
||||
_deflate_decode_ctx, slot.buf, slot.encoded_len,
|
||||
_serve_block, (uint16_t)blen, &decoded_len);
|
||||
// _serve_block is also the outbound server cache. Invalidate it even on failure so a simultaneous
|
||||
// serve job reloads its own authenticated block rather than transmitting decoder workspace.
|
||||
_serve_block_loaded = false;
|
||||
_serve_wire_block = nullptr;
|
||||
_serve_wire_len = 0;
|
||||
if (!decoded || decoded_len != blen) {
|
||||
_wire_allow_deflate = false; // retry this/future blocks as raw v2
|
||||
slot.mask = 0;
|
||||
slot.need = frag_full_mask((blen + OTA_FRAG_DATA_V2 - 1) / OTA_FRAG_DATA_V2);
|
||||
slot.encoded_len = 0;
|
||||
slot.wire_v2 = false;
|
||||
slot.deflated = false;
|
||||
memset(slot.stream_id, 0, sizeof(slot.stream_id));
|
||||
notePipelineStall();
|
||||
return true;
|
||||
}
|
||||
memcpy(slot.buf, _serve_block, blen);
|
||||
}
|
||||
}
|
||||
// The paced server sends a proof immediately after this block's requested DATA. Wait briefly so that proof
|
||||
// can arrive without another request/response turn. An older server, or a lost proof, falls back through
|
||||
// serviceEgress() to the existing REQ_PROOF wire message.
|
||||
@@ -1579,6 +1785,22 @@ bool OtaManager::handleProof(const uint8_t* m, uint16_t n) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t OtaManager::wireRequestMask(uint32_t block, uint16_t need, uint16_t received,
|
||||
uint16_t encoded_len) const {
|
||||
uint16_t want = (uint16_t)(need & ~received);
|
||||
if (_wire_v2_session) {
|
||||
// Until a v2 representation answers, include every legacy fragment bit. A deployed source ignores the
|
||||
// high profile flags and can therefore return a complete seven-fragment block on this very first request.
|
||||
if (encoded_len == 0) {
|
||||
want = frag_full_mask((blockLen(block) + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA);
|
||||
}
|
||||
if (want == 0) want = need;
|
||||
return ota_req_make_v2(want, _wire_allow_deflate);
|
||||
}
|
||||
if (want == 0) want = need;
|
||||
return want;
|
||||
}
|
||||
|
||||
bool OtaManager::requestSlot(uint8_t slot_index) {
|
||||
if (_fstate != FETCHING || slot_index >= OTA_FETCH_PIPELINE) return false;
|
||||
ReassemblySlot& slot = _reasm[slot_index];
|
||||
@@ -1597,8 +1819,7 @@ bool OtaManager::requestSlot(uint8_t slot_index) {
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
uint16_t want = (uint16_t)(slot.need & ~slot.mask);
|
||||
if (want == 0) want = slot.need; // safety: never send an empty request
|
||||
uint16_t want = wireRequestMask(slot.block, slot.need, slot.mask, slot.encoded_len);
|
||||
ReqMsg rq; memcpy(rq.manifest_id, _fid, 4);
|
||||
rq.block_idx = (uint16_t)slot.block; rq.want_mask = want;
|
||||
uint8_t b[16];
|
||||
@@ -1619,8 +1840,7 @@ bool OtaManager::requestFlight() {
|
||||
if (slot.block == NO_BLOCK || slot.awaiting_proof) continue;
|
||||
ReqItem& item = request.items[request.n_items++];
|
||||
item.block_idx = (uint16_t)slot.block;
|
||||
item.want_mask = (uint16_t)(slot.need & ~slot.mask);
|
||||
if (item.want_mask == 0) item.want_mask = slot.need;
|
||||
item.want_mask = wireRequestMask(slot.block, slot.need, slot.mask, slot.encoded_len);
|
||||
}
|
||||
if (request.n_items == 0) return false;
|
||||
_req_start = request.items[0].block_idx;
|
||||
@@ -1645,7 +1865,8 @@ bool OtaManager::fillPipeline() {
|
||||
ReassemblySlot& slot = _reasm[slot_index];
|
||||
slot.block = block;
|
||||
slot.mask = 0;
|
||||
slot.need = frag_full_mask((blockLen(block) + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA);
|
||||
const uint16_t fragment_data = _wire_v2_session ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA;
|
||||
slot.need = frag_full_mask((blockLen(block) + fragment_data - 1) / fragment_data);
|
||||
slot.awaiting_proof = false;
|
||||
slot.proof_request_at = 0;
|
||||
assigned++;
|
||||
@@ -1660,13 +1881,42 @@ void OtaManager::requestMissing() {
|
||||
if (_fstate != FETCHING) return;
|
||||
if (activePipelineSlots() == 0) { fillPipeline(); return; }
|
||||
|
||||
// No v2 DATA at all means the request may have reached only deployed sources which ignored its profile
|
||||
// bits (or the first response burst was lost). Retry once in the universal legacy profile instead of
|
||||
// pinning the session to a feature it has not observed on air.
|
||||
if (_wire_v2_session && !_wire_v2_confirmed && !anyWireDataReceived()) {
|
||||
downgradeWireToLegacy();
|
||||
notePipelineStall();
|
||||
requestFlight();
|
||||
return;
|
||||
}
|
||||
|
||||
// The airtime-aware deadline expired. Retry one slot's holes per deadline, round-robin, so a weak block
|
||||
// cannot trigger a simultaneous multi-block re-burst on a half-duplex channel. Recovery dirties the
|
||||
// flight; its successor is reduced (4->2, 3->2, 2->1) after all current slots finish.
|
||||
notePipelineStall();
|
||||
|
||||
// Once v2 has been confirmed, the source can still disappear cleanly between blocks (or after a bad
|
||||
// representation is discarded), leaving every active slot empty. Give the v2 flight one sparse retry,
|
||||
// then clear the whole flight and ask again in the universal legacy profile so an old seeder can take over.
|
||||
if (_wire_v2_session && _wire_v2_confirmed && !anyWireDataReceived() &&
|
||||
++_wire_empty_stalls >= 2) {
|
||||
downgradeWireToLegacy();
|
||||
requestFlight();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t offset = 0; offset < OTA_FETCH_PIPELINE; offset++) {
|
||||
uint8_t slot = (uint8_t)((_retry_slot + offset) % OTA_FETCH_PIPELINE);
|
||||
if (_reasm[slot].block == NO_BLOCK) continue;
|
||||
ReassemblySlot& reassembly = _reasm[slot];
|
||||
if (_wire_v2_session && reassembly.wire_v2 && reassembly.mask != 0 &&
|
||||
!reassembly.awaiting_proof && ++reassembly.wire_stalls >= 2) {
|
||||
// A v2 seeder may disappear after establishing a representation while only deployed legacy seeders
|
||||
// remain. Keep one sparse retry, then fall back the whole flight to the universal profile. Clearing
|
||||
// every slot prevents geometry/representation mixing and guarantees that old sources can take over.
|
||||
downgradeWireToLegacy();
|
||||
}
|
||||
if (requestSlot(slot)) _retry_slot = (uint8_t)((slot + 1) % OTA_FETCH_PIPELINE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,22 @@ typedef bool (*OtaSend)(void* ctx, const uint8_t* msg, uint16_t len, bool flood)
|
||||
// error. nullptr means the payload is a contiguous RAM buffer (the staged `.mota`).
|
||||
typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len);
|
||||
|
||||
// Optional full raw-RFC1951 transport codec (stored, fixed-Huffman, and dynamic-Huffman blocks). Compression
|
||||
// is a wire optimization only: the receiver inflates exactly one authenticated logical block before Merkle
|
||||
// verification and writes the original `.mota` bytes to its store. Returning false, an empty result, or a
|
||||
// result no smaller than the source selects raw v2 DATA. A decoder must reject output overflow, truncated
|
||||
// streams, and trailing compressed input; OtaManager also checks that it produced exactly the manifest-derived
|
||||
// block length before accepting a proof.
|
||||
typedef bool (*OtaDeflateEncodeFn)(void* ctx, const uint8_t* src, uint16_t src_len,
|
||||
uint8_t* dst, uint16_t dst_cap, uint16_t* dst_len);
|
||||
typedef bool (*OtaDeflateDecodeFn)(void* ctx, const uint8_t* src, uint16_t src_len,
|
||||
uint8_t* dst, uint16_t dst_cap, uint16_t* dst_len);
|
||||
// Optional source-side fast path for a host/archive that already has an independently compressed logical
|
||||
// block. It avoids carrying a DEFLATE encoder on the embedded seeder. The result has the same strict bounds
|
||||
// and deterministic-per-block contract as OtaDeflateEncodeFn.
|
||||
typedef bool (*ServeDeflateReadFn)(void* ctx, uint16_t block, uint8_t* dst,
|
||||
uint16_t dst_cap, uint16_t* dst_len);
|
||||
|
||||
#ifndef OTA_PROOFGEN_SCRATCH
|
||||
#if defined(OTA_SD_STORE)
|
||||
#define OTA_PROOFGEN_SCRATCH 8192 // SD archive can seed <=2048 blocks (about 2 MB at 1 KB/block)
|
||||
@@ -147,7 +163,10 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len)
|
||||
#define OTA_SERVE_SUPPRESS_MS 1500 // don't re-serve a block whose DATA we just overheard another holder send
|
||||
#endif // (so multiple sources of the same mota don't duplicate-broadcast it)
|
||||
#ifndef OTA_FRAG_DATA
|
||||
#define OTA_FRAG_DATA 160 // data bytes per DATA fragment (<= MAX_PACKET_PAYLOAD - 9-byte header)
|
||||
#define OTA_FRAG_DATA 160 // deployed legacy DATA geometry; never change in place
|
||||
#endif
|
||||
#ifndef OTA_FRAG_DATA_V2
|
||||
#define OTA_FRAG_DATA_V2 171 // negotiated DATA geometry (13-byte overhead => 184-byte packet payload)
|
||||
#endif
|
||||
#ifndef OTA_SERVE_QUEUE
|
||||
#define OTA_SERVE_QUEUE 4 // requested blocks retained without allocating one packet per fragment
|
||||
@@ -245,6 +264,8 @@ public:
|
||||
uint16_t mfl = 0; // manifest-minus-leaves length (the OTA_MANIFEST payload)
|
||||
ServeReadFn read = nullptr; // payload reader (nullptr => m.payload is contiguous in RAM)
|
||||
void* read_ctx = nullptr;
|
||||
ServeDeflateReadFn read_deflated = nullptr; // optional host/source-generated raw-DEFLATE block
|
||||
void* deflate_ctx = nullptr;
|
||||
uint8_t* scratch = nullptr; // proof-gen working buffer (>= block_count*4)
|
||||
uint32_t scratch_sz = 0;
|
||||
};
|
||||
@@ -302,6 +323,18 @@ public:
|
||||
|
||||
// --- fetch --- Provide the staging store; fetching starts on a matching OTA_ADV.
|
||||
void set_fetch_store(OtaStore* s) { _fetch = s; }
|
||||
// Install either side of the optional per-block transport codec. A receiver with no decoder still uses
|
||||
// negotiated 171-byte raw DATA; a source with no encoder automatically returns raw DATA. Context lifetimes
|
||||
// are caller-owned and independent because a relay/seeder may fetch and serve at the same time.
|
||||
void set_transport_deflate_encoder(OtaDeflateEncodeFn fn, void* ctx = nullptr) {
|
||||
_deflate_encode = fn; _deflate_encode_ctx = ctx;
|
||||
}
|
||||
void set_transport_deflate_decoder(OtaDeflateDecodeFn fn, void* ctx = nullptr) {
|
||||
_deflate_decode = fn; _deflate_decode_ctx = ctx;
|
||||
}
|
||||
// Defaults on for moving-forward 171-byte transfers. This switch exists for qualification and emergency
|
||||
// rollback; disabling it produces byte-for-byte legacy OTA_REQ/OTA_DATA behavior.
|
||||
void set_wire_v2_enabled(bool on) { _wire_v2_enabled = on; }
|
||||
|
||||
// Resume a fetch from a container already persisted in the store (after a reboot). want_mid=nullptr is
|
||||
// automatic adoption and rechecks current autofetch/target/version policy; a non-null MID is an explicit
|
||||
@@ -441,6 +474,7 @@ public:
|
||||
_lv_retries = 0; _loop_last_lvmask = 0;
|
||||
_resume_verify_idx = 0; _resume_invalidated = false;
|
||||
_resume_merkle.reset();
|
||||
_wire_empty_stalls = 0;
|
||||
}
|
||||
|
||||
FetchState fetchState() const { return _fstate; }
|
||||
@@ -525,7 +559,10 @@ private:
|
||||
bool loadSource(const ServeEntry& e); // load an external mota into _srcv (head+leaves)
|
||||
void registerSelfEntry(); // (re)build entry[0] from view0
|
||||
static bool srcReadTramp(void* c, uint32_t off, uint8_t* buf, uint32_t len); // source payload reader
|
||||
bool queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want_mask);
|
||||
static bool srcDeflateReadTramp(void* c, uint16_t block, uint8_t* buf,
|
||||
uint16_t cap, uint16_t* len);
|
||||
bool queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want_mask,
|
||||
bool wire_v2 = false, bool allow_deflate = false);
|
||||
bool queueManifestJob(const uint8_t* mid, uint16_t want_mask);
|
||||
uint32_t manifestEgressGapMs() const;
|
||||
uint32_t proofEgressGapMs() const;
|
||||
@@ -556,6 +593,10 @@ private:
|
||||
bool requestFlight(); // send all newly assigned blocks in one OTA_REQ
|
||||
bool fillPipeline(); // assign + send a new flight only when the old one ended
|
||||
void requestMissing(); // start a flight, or recover one stalled slot
|
||||
uint16_t wireRequestMask(uint32_t block, uint16_t need, uint16_t received,
|
||||
uint16_t encoded_len) const;
|
||||
void downgradeWireToLegacy(); // old source answered/ignored the v2 request profile
|
||||
bool anyWireDataReceived() const;
|
||||
uint32_t blockLen(uint32_t i) const;
|
||||
|
||||
uint32_t _target = 0;
|
||||
@@ -593,6 +634,8 @@ private:
|
||||
uint16_t emitted_mask = 0;
|
||||
uint32_t proof_ready_at = 0;
|
||||
bool proof_requested = false;
|
||||
bool wire_v2 = false;
|
||||
bool allow_deflate = false;
|
||||
};
|
||||
ServeJob _serve_jobs[OTA_SERVE_QUEUE];
|
||||
uint8_t _n_serve_jobs = 0;
|
||||
@@ -607,6 +650,15 @@ private:
|
||||
uint8_t _serve_block[OTA_MAX_BLOCK];
|
||||
uint16_t _serve_block_len = 0;
|
||||
bool _serve_block_loaded = false;
|
||||
const uint8_t* _serve_wire_block = nullptr; // raw _serve_block or DEFLATE bytes in ServeView::scratch
|
||||
uint16_t _serve_wire_len = 0;
|
||||
bool _serve_wire_deflated = false;
|
||||
uint8_t _serve_wire_id[4] = {0}; // SHA-256:4 of this exact encoded representation
|
||||
|
||||
OtaDeflateEncodeFn _deflate_encode = nullptr;
|
||||
void* _deflate_encode_ctx = nullptr;
|
||||
OtaDeflateDecodeFn _deflate_decode = nullptr;
|
||||
void* _deflate_decode_ctx = nullptr;
|
||||
|
||||
// fetch
|
||||
OtaStore* _fetch = nullptr;
|
||||
@@ -638,6 +690,11 @@ private:
|
||||
uint16_t _advert_mins = OTA_ADVERT_INTERVAL_MINS; // beacon re-advertise cadence, minutes; 0=off (persisted)
|
||||
uint8_t _max_hops = OTA_HOP_LIMIT_DEFAULT; // OTA flood reach in hops; 0=direct only (persisted)
|
||||
uint8_t _fflags = 0; // flags of the manifest currently being fetched
|
||||
bool _wire_v2_enabled = true; // advertise v2 in OTA_REQ while retaining transparent fallback
|
||||
bool _wire_v2_session = true; // false after an old source responds or the first request stalls
|
||||
bool _wire_v2_confirmed = false; // at least one structurally valid v2 DATA packet was observed
|
||||
bool _wire_allow_deflate = false; // session may request compressed blocks only with a decoder
|
||||
uint8_t _wire_empty_stalls = 0; // confirmed v2 source may vanish between otherwise empty flights
|
||||
// Bounded adaptive multi-block client flight. Each slot independently reassembles DATA and awaits its
|
||||
// Merkle proof. One append-only OTA_REQ packet opens the whole flight; the receiver then stays quiet until
|
||||
// every slot completes or an airtime-aware deadline expires. Clean flights grow 1->2->3->4; any recovery
|
||||
@@ -648,6 +705,11 @@ private:
|
||||
uint16_t need = 0; // full bitmap for this block
|
||||
bool awaiting_proof = false;
|
||||
uint32_t proof_request_at = 0; // proactive-proof grace deadline; 0 after fallback is sent
|
||||
uint16_t encoded_len = 0; // v2 wire length; zero until the first DATA fragment establishes it
|
||||
bool wire_v2 = false;
|
||||
bool deflated = false;
|
||||
uint8_t stream_id[4] = {0}; // locks fragments to one encoded representation across seeders
|
||||
uint8_t wire_stalls = 0; // eventually unlock a vanished representation and retry cleanly
|
||||
uint8_t buf[OTA_MAX_BLOCK];
|
||||
};
|
||||
ReassemblySlot _reasm[OTA_FETCH_PIPELINE];
|
||||
|
||||
@@ -69,8 +69,11 @@ struct ManifestMsg {
|
||||
// source queues every row. This lets a fetcher open one adaptive flight with one packet, then remain silent
|
||||
// while the source and relays return the requested DATA/PROOF train.
|
||||
//
|
||||
// want_mask bit k = "send fragment k" (the slice at byte offset k*OTA_FRAG_DATA). A recovery request uses
|
||||
// only the missing bits, so one lost fragment costs one fragment to re-send rather than the whole block.
|
||||
// Legacy want_mask bit k = "send fragment k" (the slice at byte offset k*OTA_FRAG_DATA). A recovery request
|
||||
// uses only the missing bits, so one lost fragment costs one fragment to re-send rather than the whole block.
|
||||
// New receivers reserve bits 15..14 as an in-band request profile: bit 15 asks for the v2 171-byte geometry
|
||||
// and bit 14 permits per-block raw DEFLATE. An old source masks those bits away and returns the ordinary
|
||||
// 160-byte DATA train; the new receiver detects that response and downgrades without stranding old fleets.
|
||||
struct ReqMsg { uint8_t manifest_id[4]; uint16_t block_idx; uint16_t want_mask; };
|
||||
struct ReqItem { uint16_t block_idx; uint16_t want_mask; };
|
||||
#ifndef OTA_REQ_MAX_ITEMS
|
||||
@@ -84,8 +87,13 @@ struct ReqWindowMsg {
|
||||
|
||||
// ---- OTA_DATA: one self-describing fragment of a block's data (proof stays in a separate packet) ----
|
||||
// body: manifest_id(4) block_idx(2) frag_off(2) data[]
|
||||
// `frag_off` is the byte offset of `data` within block `block_idx` (global position = block_idx*block_size
|
||||
// + frag_off), so a fragment is self-placing when received from the selected source.
|
||||
// In the legacy profile, `frag_off` is the byte offset of `data` within block `block_idx` (global position =
|
||||
// block_idx*block_size + frag_off), so a fragment is self-placing when received from the selected source.
|
||||
// In the negotiated v2 profile its high bit is set and the field packs the per-block wire encoding, fragment
|
||||
// index, and total encoded size; data[] starts with SHA-256:4 of the complete encoded representation, followed
|
||||
// by up to 171 bytes. Repeating that id in every fragment prevents different seeders' valid-but-different
|
||||
// DEFLATE streams from being mixed. Keeping the existing OTA_DATA type means deployed opaque relays retain the
|
||||
// same transfer priority. Old endpoints reject the tagged offset and new endpoints only request it explicitly.
|
||||
struct DataMsg {
|
||||
uint8_t manifest_id[4];
|
||||
uint16_t block_idx;
|
||||
@@ -93,6 +101,59 @@ struct DataMsg {
|
||||
const uint8_t* data; uint16_t data_len;
|
||||
};
|
||||
|
||||
// OTA_REQ want_mask extension. The v2 marker and DEFLATE permission are deliberately outside every valid
|
||||
// fragment bit for the fixed <=1 KiB blocks used by this protocol. A v2 request includes all seven legacy
|
||||
// fragment bits on its first attempt, so a deployed source which ignores these flags can answer completely.
|
||||
static const uint16_t OTA_REQ_V2_MARK = 0x8000u;
|
||||
static const uint16_t OTA_REQ_V2_ALLOW_DEFLATE = 0x4000u;
|
||||
static const uint16_t OTA_REQ_V2_RESERVED = 0x2000u;
|
||||
static const uint16_t OTA_REQ_V2_FRAGMENT_MASK = 0x1FFFu;
|
||||
|
||||
// OTA_DATA v2 frag_off packing:
|
||||
// bit 15 v2 marker
|
||||
// bit 14 data[] is one raw-RFC1951-DEFLATE stream fragment (clear = raw block bytes)
|
||||
// bits 13..10 fragment index (0..15; byte offset = index * OTA_FRAG_DATA_V2)
|
||||
// bits 9..0 total encoded block length minus one (1..1024 bytes)
|
||||
static const uint16_t OTA_DATA_V2_MARK = 0x8000u;
|
||||
static const uint16_t OTA_DATA_V2_DEFLATED = 0x4000u;
|
||||
static const uint16_t OTA_DATA_V2_FRAGMENT_BITS = 0x3C00u;
|
||||
static const uint16_t OTA_DATA_V2_LENGTH_BITS = 0x03FFu;
|
||||
static const uint8_t OTA_DATA_V2_FRAGMENT_SHIFT = 10;
|
||||
static const uint16_t OTA_DATA_V2_MAX_ENCODED = 1024;
|
||||
static const uint8_t OTA_DATA_V2_STREAM_ID_BYTES = 4;
|
||||
|
||||
inline bool ota_req_is_v2(uint16_t want_mask) {
|
||||
return (want_mask & OTA_REQ_V2_MARK) != 0 && (want_mask & OTA_REQ_V2_RESERVED) == 0;
|
||||
}
|
||||
|
||||
inline uint16_t ota_req_v2_fragments(uint16_t want_mask) {
|
||||
return (uint16_t)(want_mask & OTA_REQ_V2_FRAGMENT_MASK);
|
||||
}
|
||||
|
||||
inline uint16_t ota_req_make_v2(uint16_t fragments, bool allow_deflate) {
|
||||
return (uint16_t)((fragments & OTA_REQ_V2_FRAGMENT_MASK) | OTA_REQ_V2_MARK |
|
||||
(allow_deflate ? OTA_REQ_V2_ALLOW_DEFLATE : 0));
|
||||
}
|
||||
|
||||
inline bool ota_data_v2_pack(uint8_t fragment, uint16_t encoded_len, bool deflated,
|
||||
uint16_t& packed) {
|
||||
if (fragment >= 16 || encoded_len == 0 || encoded_len > OTA_DATA_V2_MAX_ENCODED) return false;
|
||||
packed = (uint16_t)(OTA_DATA_V2_MARK |
|
||||
(deflated ? OTA_DATA_V2_DEFLATED : 0) |
|
||||
((uint16_t)fragment << OTA_DATA_V2_FRAGMENT_SHIFT) |
|
||||
(encoded_len - 1u));
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ota_data_v2_unpack(uint16_t packed, uint8_t& fragment, uint16_t& encoded_len,
|
||||
bool& deflated) {
|
||||
if ((packed & OTA_DATA_V2_MARK) == 0) return false;
|
||||
fragment = (uint8_t)((packed & OTA_DATA_V2_FRAGMENT_BITS) >> OTA_DATA_V2_FRAGMENT_SHIFT);
|
||||
encoded_len = (uint16_t)((packed & OTA_DATA_V2_LENGTH_BITS) + 1u);
|
||||
deflated = (packed & OTA_DATA_V2_DEFLATED) != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- OTA_GET_LEAVES: request the target's merkle leaves[] in bulk (direct; motatool warm-start only) ----
|
||||
// body: manifest_id(4) want_mask(2). want_mask bit k = "send leaves fragment k"; 0xFFFF = "send all" on the
|
||||
// first ask (before frag_total is known); a retry requests only the still-missing fragments (anti-burst,
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Optional operations exposed by a MotaSource. Serial/TCP sources carry these
|
||||
// bits in byte 35 of their unchanged 38-byte descriptor; deployed hosts leave
|
||||
// the formerly-reserved byte zero.
|
||||
static const uint8_t MOTA_SOURCE_CAP_DEFLATE_BLOCK = 0x01;
|
||||
|
||||
// A parsed top-level descriptor of one `.mota` a source provides: enough to advertise it in the catalog
|
||||
// AND to locate every region for serving, WITHOUT holding the whole image in RAM. Offsets are absolute
|
||||
// byte positions within the `.mota` container (which always begins MAGIC(4) total(4) manifest...).
|
||||
@@ -28,6 +33,7 @@ struct MotaDesc {
|
||||
uint8_t codec_id = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t block_size_log2 = 0; // serve geometry; 0 means a legacy descriptor that must be inspected
|
||||
uint8_t source_caps = 0; // MOTA_SOURCE_CAP_*; zero preserves deployed source behavior
|
||||
uint32_t total_size = 0; // full `.mota` length (bytes)
|
||||
uint32_t leaves_off = 0; // byte offset of the merkle leaves[] (manifest-minus-leaves = [8, leaves_off))
|
||||
uint32_t block_count = 0; // == number of leaves (== number of payload blocks)
|
||||
@@ -50,6 +56,15 @@ public:
|
||||
// exactly `len` bytes were produced. May block on the transport (serial round-trip); implementations
|
||||
// should keep active-transfer reads prompt because requested OTA traffic is primary traffic.
|
||||
virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) = 0;
|
||||
// Optional transport-only raw-DEFLATE representation of one logical payload block. Implementations which
|
||||
// can generate/cache it off-device return a stream strictly smaller than the raw block. The default keeps
|
||||
// every existing source source-compatible and makes OtaManager fall back to raw 171-byte DATA.
|
||||
virtual bool read_deflated_block(uint8_t idx, uint16_t block, uint8_t* buf,
|
||||
uint16_t cap, uint16_t* len) {
|
||||
(void)idx; (void)block; (void)buf; (void)cap;
|
||||
if (len) *len = 0;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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,22 @@
|
||||
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,23 @@
|
||||
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,154 @@
|
||||
/*
|
||||
* 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,666 @@
|
||||
/*
|
||||
* 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 */
|
||||
Reference in new issue
Block a user