mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-29 17:23:17 +00:00
Fix firmware size limits and refresh CI compatibility checks
Compact the existing STM32 field arithmetic and ESP-NOW tail-frame storage without removing features or lowering memory reserves. Refresh power-saving fixtures and pin official-app tests to a published archive.
This commit is contained in:
1 parent
cf98125c65
commit
c563c40c66
15 files changed
+494
-66
No files matched your search
@@ -796,7 +796,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/meshcore-tests
|
||||
key: official-clients-84bc39a950735aaffa93d912556852a4c22e235d0cc4e4d04790113207f13a68-aff42ad8ef2b332f24d644ff6a783f8272a88ff2
|
||||
key: official-clients-${{ hashFiles('test/test_official_app_compatibility.py', 'test/test_official_config_gui.py', 'test/test_official_web_console.py') }}
|
||||
|
||||
- name: Verify stock app radio, Access Control and room compatibility
|
||||
run: |
|
||||
|
||||
+10
-1
@@ -9,6 +9,7 @@
|
||||
/*
|
||||
helper functions
|
||||
*/
|
||||
#if !defined(ED25519_COMPACT_FIELD) || !ED25519_COMPACT_FIELD
|
||||
static uint64_t load_3(const unsigned char *in) {
|
||||
uint64_t result;
|
||||
|
||||
@@ -29,6 +30,7 @@ static uint64_t load_4(const unsigned char *in) {
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@@ -301,6 +303,7 @@ void fe_copy(fe h, const fe f) {
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined(ED25519_COMPACT_FIELD) || !ED25519_COMPACT_FIELD
|
||||
void fe_frombytes(fe h, const unsigned char *s) {
|
||||
int64_t h0 = load_4(s);
|
||||
int64_t h1 = load_3(s + 4) << 6;
|
||||
@@ -368,6 +371,8 @@ void fe_frombytes(fe h, const unsigned char *s) {
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
void fe_invert(fe out, const fe z) {
|
||||
fe t0;
|
||||
fe t1;
|
||||
@@ -482,6 +487,7 @@ int fe_isnegative(const fe f) {
|
||||
|f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc.
|
||||
*/
|
||||
|
||||
#if !defined(ED25519_COMPACT_FIELD) || !ED25519_COMPACT_FIELD
|
||||
int fe_isnonzero(const fe f) {
|
||||
unsigned char s[32];
|
||||
unsigned char r;
|
||||
@@ -525,6 +531,7 @@ int fe_isnonzero(const fe f) {
|
||||
|
||||
return r != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@@ -785,6 +792,7 @@ Postconditions:
|
||||
|h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc.
|
||||
*/
|
||||
|
||||
#if !defined(ED25519_COMPACT_FIELD) || !ED25519_COMPACT_FIELD
|
||||
void fe_mul121666(fe h, fe f) {
|
||||
int32_t f0 = f[0];
|
||||
int32_t f1 = f[1];
|
||||
@@ -852,7 +860,6 @@ Postconditions:
|
||||
|h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc.
|
||||
*/
|
||||
|
||||
#if !defined(ED25519_COMPACT_FIELD) || !ED25519_COMPACT_FIELD
|
||||
void fe_neg(fe h, const fe f) {
|
||||
int32_t f0 = f[0];
|
||||
int32_t f1 = f[1];
|
||||
@@ -1400,6 +1407,7 @@ Proof:
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined(ED25519_COMPACT_FIELD) || !ED25519_COMPACT_FIELD
|
||||
void fe_tobytes(unsigned char *s, const fe h) {
|
||||
int32_t h0 = h[0];
|
||||
int32_t h1 = h[1];
|
||||
@@ -1506,3 +1514,4 @@ void fe_tobytes(unsigned char *s, const fe h) {
|
||||
s[30] = (unsigned char) (h9 >> 10);
|
||||
s[31] = (unsigned char) (h9 >> 18);
|
||||
}
|
||||
#endif
|
||||
+86
-15
@@ -1,15 +1,18 @@
|
||||
/*
|
||||
* Fixed-loop form of the ref10 field products in fe.c. Only the headless
|
||||
* STM32 Companion size recipes opt in; other builds keep the unrolled code.
|
||||
* Fixed-loop form of the ref10 field arithmetic in fe.c. The existing STM32
|
||||
* size recipes opt in; other builds keep the unrolled code.
|
||||
* The radix alternates 26 and 25 bits. Odd-by-odd products therefore have
|
||||
* weight 2, and terms beyond bit 254 fold back with weight 19, exactly as in
|
||||
* ref10. Product accumulation and the rounded carry order are unchanged.
|
||||
* ref10. Product accumulation, decoding, encoding and carry orders are unchanged.
|
||||
*
|
||||
* All loops, branches and array addresses depend only on public limb indices
|
||||
* or the public operation (multiply, square, double-square), not input values.
|
||||
* Under ref10's 1.65*2^26 / 1.65*2^25 input bounds, even doubled sums fit in
|
||||
* signed 64 bits. Outputs are written only after reading every input, so h
|
||||
* may alias f or g. No additional persistent RAM or heap is needed.
|
||||
* may alias f or g. No additional persistent RAM or heap is needed. Compared
|
||||
* with the unrolled forms, public-index loop overhead trades CPU time for
|
||||
* flash; the fixed number of field operations and secret-independent control
|
||||
* flow are retained for every opted-in Companion and Mini Repeater profile.
|
||||
*/
|
||||
|
||||
void fe_add(fe h, const fe f, const fe g) {
|
||||
@@ -48,6 +51,84 @@ void fe_cswap(fe f, fe g, unsigned int b) {
|
||||
}
|
||||
}
|
||||
|
||||
static void fe_compact_carry(fe h, int64_t limbs[10],
|
||||
const unsigned char *order, int count) {
|
||||
int i;
|
||||
for (i = 0; i < count; ++i) {
|
||||
const int index = order[i];
|
||||
const int bits = 26 - (index & 1);
|
||||
const int64_t radix = (int64_t)1 << bits;
|
||||
const int64_t carry = (limbs[index] + radix / 2) >> bits;
|
||||
if (index == 9) limbs[0] += carry * 19;
|
||||
else limbs[index + 1] += carry;
|
||||
/* A carry may be negative; do not left-shift that signed value. */
|
||||
limbs[index] -= carry * radix;
|
||||
}
|
||||
for (i = 0; i < 10; ++i) h[i] = (int32_t)limbs[i];
|
||||
}
|
||||
|
||||
static const unsigned char fe_decode_carries[] = {9, 1, 3, 5, 7, 0, 2, 4, 6, 8};
|
||||
|
||||
void fe_frombytes(fe h, const unsigned char *s) {
|
||||
static const unsigned char offsets[] = {0, 4, 7, 10, 13, 16, 20, 23, 26, 29};
|
||||
static const unsigned char shifts[] = {0, 6, 5, 3, 2, 0, 7, 5, 4, 2};
|
||||
int64_t limbs[10];
|
||||
int i, j;
|
||||
for (i = 0; i < 10; ++i) {
|
||||
uint32_t word = 0;
|
||||
const int count = i == 0 || i == 5 ? 4 : 3;
|
||||
for (j = 0; j < count; ++j)
|
||||
word |= (uint32_t)s[offsets[i] + j] << (j * 8);
|
||||
if (i == 9) word &= 8388607; /* Ignore the encoded sign bit. */
|
||||
limbs[i] = (int64_t)word << shifts[i];
|
||||
}
|
||||
fe_compact_carry(h, limbs, fe_decode_carries, sizeof(fe_decode_carries));
|
||||
}
|
||||
|
||||
void fe_mul121666(fe h, fe f) {
|
||||
int64_t limbs[10];
|
||||
int i;
|
||||
for (i = 0; i < 10; ++i) limbs[i] = (int64_t)f[i] * 121666;
|
||||
fe_compact_carry(h, limbs, fe_decode_carries, sizeof(fe_decode_carries));
|
||||
}
|
||||
|
||||
void fe_tobytes(unsigned char *s, const fe h) {
|
||||
int32_t limbs[10], q = (19 * h[9] + (1 << 24)) >> 25;
|
||||
int i, index = 0, offset = 0;
|
||||
for (i = 0; i < 10; ++i) {
|
||||
limbs[i] = h[i];
|
||||
q = (limbs[i] + q) >> (26 - (i & 1));
|
||||
}
|
||||
limbs[0] += 19 * q;
|
||||
for (i = 0; i < 10; ++i) {
|
||||
const int bits = 26 - (i & 1);
|
||||
const int32_t carry = limbs[i] >> bits;
|
||||
if (i < 9) limbs[i + 1] += carry;
|
||||
limbs[i] -= carry * ((int32_t)1 << bits);
|
||||
}
|
||||
/* Each byte crosses at most one public radix boundary. Normalized limbs
|
||||
* are nonnegative, and unsigned shifts avoid overflowing signed int32. */
|
||||
for (i = 0; i < 32; ++i) {
|
||||
uint32_t byte = (uint32_t)limbs[index] >> offset;
|
||||
offset += 8;
|
||||
if (offset >= 26 - (index & 1)) {
|
||||
offset -= 26 - (index & 1);
|
||||
++index;
|
||||
if (offset && index < 10)
|
||||
byte |= (uint32_t)limbs[index] << (8 - offset);
|
||||
}
|
||||
s[i] = (unsigned char)byte;
|
||||
}
|
||||
}
|
||||
|
||||
int fe_isnonzero(const fe f) {
|
||||
unsigned char bytes[32], value = 0;
|
||||
int i;
|
||||
fe_tobytes(bytes, f);
|
||||
for (i = 0; i < 32; ++i) value |= bytes[i];
|
||||
return value != 0;
|
||||
}
|
||||
|
||||
static void fe_compact_product(fe h, const fe f, const fe g, int doubled) {
|
||||
static const unsigned char carry_order[] = {0, 4, 1, 5, 2, 6, 3, 7, 4, 8, 9, 0};
|
||||
int64_t limbs[10] = {0};
|
||||
@@ -63,17 +144,7 @@ static void fe_compact_product(fe h, const fe f, const fe g, int doubled) {
|
||||
if (doubled) {
|
||||
for (i = 0; i < 10; ++i) limbs[i] += limbs[i];
|
||||
}
|
||||
for (i = 0; i < (int)sizeof(carry_order); ++i) {
|
||||
const int index = carry_order[i];
|
||||
const int bits = 26 - (index & 1);
|
||||
const int64_t radix = (int64_t)1 << bits;
|
||||
const int64_t carry = (limbs[index] + radix / 2) >> bits;
|
||||
if (index == 9) limbs[0] += carry * 19;
|
||||
else limbs[index + 1] += carry;
|
||||
/* A carry may be negative; do not left-shift that signed value. */
|
||||
limbs[index] -= carry * radix;
|
||||
}
|
||||
for (i = 0; i < 10; ++i) h[i] = (int32_t)limbs[i];
|
||||
fe_compact_carry(h, limbs, carry_order, sizeof(carry_order));
|
||||
}
|
||||
|
||||
void fe_mul(fe h, const fe f, const fe g) {
|
||||
|
||||
@@ -35,20 +35,47 @@ static constexpr size_t ESPNOW_RAW_FRAGMENT_MAGIC_SIZE =
|
||||
static constexpr size_t ESPNOW_RAW_FRAGMENT_HEADER_SIZE = 13;
|
||||
static constexpr size_t ESPNOW_RAW_FRAGMENT_DATA_SIZE =
|
||||
ESPNOW_RAW_MAX_FRAME_SIZE - ESPNOW_RAW_FRAGMENT_HEADER_SIZE;
|
||||
static constexpr size_t ESPNOW_RAW_MAX_SECOND_FRAME_SIZE =
|
||||
ESPNOW_RAW_FRAGMENT_HEADER_SIZE + ESPNOW_RAW_MAX_PACKET_SIZE
|
||||
- ESPNOW_RAW_FRAGMENT_DATA_SIZE;
|
||||
|
||||
static_assert(ESPNOW_RAW_FRAGMENT_DATA_SIZE < ESPNOW_RAW_MAX_FRAME_SIZE,
|
||||
"ESP-NOW fragment header must leave room for data");
|
||||
static_assert(ESPNOW_RAW_FRAGMENT_DATA_SIZE * ESPNOW_RAW_FRAGMENT_COUNT >=
|
||||
ESPNOW_RAW_MAX_PACKET_SIZE,
|
||||
"two ESP-NOW fragments must hold one maximum MeshCore packet");
|
||||
static_assert(ESPNOW_RAW_FRAGMENT_COUNT == 2
|
||||
&& ESPNOW_RAW_MAX_SECOND_FRAME_SIZE <= ESPNOW_RAW_MAX_FRAME_SIZE,
|
||||
"compact frame storage requires one full frame and one bounded tail");
|
||||
|
||||
// Only the first frame can reach 250 bytes. The second holds a 13-byte header
|
||||
// and at most 18 remaining packet bytes. Keep both frames owned by the queue
|
||||
// without reserving another full frame for each of its six transmit slots.
|
||||
struct ESPNowRawFrameData {
|
||||
uint8_t first[ESPNOW_RAW_MAX_FRAME_SIZE];
|
||||
uint8_t second[ESPNOW_RAW_MAX_SECOND_FRAME_SIZE];
|
||||
|
||||
uint8_t* operator[](size_t index) {
|
||||
return index == 0 ? first : index == 1 ? second : nullptr;
|
||||
}
|
||||
const uint8_t* operator[](size_t index) const {
|
||||
return index == 0 ? first : index == 1 ? second : nullptr;
|
||||
}
|
||||
static constexpr size_t capacity(size_t index) {
|
||||
return index == 0 ? ESPNOW_RAW_MAX_FRAME_SIZE
|
||||
: index == 1 ? ESPNOW_RAW_MAX_SECOND_FRAME_SIZE : 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct ESPNowRawFrames {
|
||||
uint8_t count;
|
||||
uint16_t lengths[ESPNOW_RAW_FRAGMENT_COUNT];
|
||||
uint8_t data[ESPNOW_RAW_FRAGMENT_COUNT][ESPNOW_RAW_MAX_FRAME_SIZE];
|
||||
ESPNowRawFrameData data;
|
||||
|
||||
ESPNowRawFrames() : count(0), lengths{0, 0}, data{} {}
|
||||
};
|
||||
static_assert(sizeof(ESPNowRawFrames) <= 288,
|
||||
"raw transmit frames must retain compact second-frame storage");
|
||||
|
||||
inline uint32_t espNowRawCrc32(const uint8_t* data, size_t len) {
|
||||
if (data == nullptr && len != 0) return 0;
|
||||
|
||||
@@ -381,7 +381,7 @@ bool ESPNowBridge::queueTransmitFrames(
|
||||
}
|
||||
for (uint8_t i = 0; i < frames.count; ++i) {
|
||||
if (frames.lengths[i] == 0
|
||||
|| frames.lengths[i] > MAX_ESPNOW_PACKET_SIZE) {
|
||||
|| frames.lengths[i] > frames.data.capacity(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -486,7 +486,8 @@ void ESPNowBridge::pumpTransmitQueue() {
|
||||
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
|
||||
if (queued_tx.next_frame < queued_tx.frames.count) {
|
||||
frame_len = queued_tx.frames.lengths[queued_tx.next_frame];
|
||||
if (frame_len > 0 && frame_len <= sizeof(frame)) {
|
||||
if (frame_len > 0 && frame_len <= sizeof(frame)
|
||||
&& frame_len <= queued_tx.frames.data.capacity(queued_tx.next_frame)) {
|
||||
memcpy(frame, queued_tx.frames.data[queued_tx.next_frame], frame_len);
|
||||
} else {
|
||||
frame_len = 0;
|
||||
@@ -869,7 +870,7 @@ bool ESPNowBridge::sendTest(uint32_t nonce) {
|
||||
probe.nonce = nonce;
|
||||
probe.format = _active_format;
|
||||
probe.channel = _prefs->bridge_channel;
|
||||
if (!encodeProbe(frames.data[0], sizeof(frames.data[0]), probe)) {
|
||||
if (!encodeProbe(frames.data[0], frames.data.capacity(0), probe)) {
|
||||
recordError(Stage::TestQueue, ESP_ERR_INVALID_ARG);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ int main() {
|
||||
assert(h.driver.n_recv==0&&h.driver.n_sent==0&&h.driver.n_recv_errors==0);
|
||||
assert(h.driver._rx_ps_arm_failures==0);
|
||||
assert(h.driver._rx_ps_arm_retry.consecutiveFailures()==0&&h.driver._rx_ps_arm_retry.canAttempt());
|
||||
assert(h.driver._rx_ps_enabled&&h.driver._rx_ps_rx_us==123456&&h.driver._rx_ps_sleep_us==765432);
|
||||
assert(h.dispatcher.n_sent_flood==0&&h.dispatcher.n_sent_direct==0&&h.dispatcher.n_recv_flood==0&&h.dispatcher.n_recv_direct==0);
|
||||
assert(h.dispatcher._err_flags==0&&h.tables._direct_dups==0&&h.tables._flood_dups==0);
|
||||
assert(h.driver.n_wd_soft==11&&h.driver.n_wd_hard==13&&h.driver.last_rx==1234);
|
||||
|
||||
+15
-10
@@ -1,9 +1,12 @@
|
||||
Official app compatibility source witness
|
||||
|
||||
Source: https://app.meshcore.nz/main.dart.js
|
||||
Observed: 2026-10-04
|
||||
Bytes: 9494192
|
||||
SHA-256: 84bc39a950735aaffa93d912556852a4c22e235d0cc4e4d04790113207f13a68
|
||||
Source: https://files.liamcottle.net/MeshCore/v1.50.0/MeshCore-v1.50.0%2B78-b7528a7-web.zip
|
||||
Reviewed: 2026-10-23 (official v1.50.0+78, build b7528a7)
|
||||
Archive bytes: 16950142
|
||||
Archive SHA-256: c28f5549a63ce42a50a8a5bd80081c69ef85aeb1dca221c36bc692b838a92dd7
|
||||
Member: web/main.dart.js
|
||||
JavaScript bytes: 9491330
|
||||
JavaScript SHA-256: aafc714732d1e1af749104da446cd250ea72e288f7cd60ce742f9733da30f309
|
||||
|
||||
Run:
|
||||
python3 -B test/test_official_app_compatibility.py -v
|
||||
@@ -13,16 +16,18 @@ under AddressSanitizer and UndefinedBehaviorSanitizer. No PlatformIO or
|
||||
hardware is used. The complete web application is not copied into Git.
|
||||
|
||||
Default external cache:
|
||||
~/.cache/meshcore-tests/84bc39a950735aaffa93d912556852a4c22e235d0cc4e4d04790113207f13a68/main.dart.js
|
||||
~/.cache/meshcore-tests/aafc714732d1e1af749104da446cd250ea72e288f7cd60ce742f9733da30f309/main.dart.js
|
||||
|
||||
An optional MESHCORE_OFFICIAL_APP_BUNDLE path selects an existing external
|
||||
bundle. Its bytes and SHA must match. Without that variable, a cold cache
|
||||
downloads the public source and verifies its exact size and SHA before saving.
|
||||
downloads the official versioned archive and verifies both the archive and
|
||||
JavaScript's exact size and SHA before saving only the JavaScript member.
|
||||
CI should use actions/setup-node with node-version 22 and actions/cache for
|
||||
~/.cache/meshcore-tests, keyed by the full SHA above. The public URL is mutable:
|
||||
if it changes and no pinned cache exists, the test fails explicitly. Updating
|
||||
the source pin and function witnesses requires review; it never silently uses
|
||||
the new app or replaces its parsers with test implementations.
|
||||
~/.cache/meshcore-tests, keyed by the source-pin tests. The live app URL is no
|
||||
longer used, so its deployments cannot break a cold-cache run. Changed archive
|
||||
or cached bytes still fail explicitly. Updating the source pin and function
|
||||
witnesses requires review; the test never silently uses a new app or replaces
|
||||
its parsers with test implementations.
|
||||
|
||||
The JS harness extracts and evaluates unmodified compiled app function bodies:
|
||||
ACL request builder/decoder, CMD50 builder, binary frame parser, SENT/Binary
|
||||
|
||||
+16
-16
@@ -31,7 +31,7 @@ const errors = [];
|
||||
const type = {a: value => value, b: value => value instanceof Uint8Array,
|
||||
h() { return this; }};
|
||||
const t = new Proxy({}, {get: () => type});
|
||||
const $ = {cT: () => new Uint8Array(0), cqf() {}, dF() {}, ay() {}, fQ() {},
|
||||
const $ = {cT: () => new Uint8Array(0), cq9() {}, dF() {}, ay() {}, fQ() {},
|
||||
vI: () => ({Ia: () => 0x78563412})};
|
||||
const B = {
|
||||
e: {aN: (a, b) => a >> b, qP: (a, b) => Math.trunc(a / b)},
|
||||
@@ -43,13 +43,13 @@ const B = {
|
||||
const J = {
|
||||
a3: () => ({gt: a => a.length, gap: a => a.length === 0, i: (a, i) => a[i]}),
|
||||
i2: (a, b, c) => new Uint8Array(a, b, c),
|
||||
po: a => new Uint8Array(a), lx: (a, b, c) => new DataView(a, b, c),
|
||||
c6: (a, b) => a.push(b), aaW: (a, predicate) => {
|
||||
pn: a => new Uint8Array(a), lx: (a, b, c) => new DataView(a, b, c),
|
||||
c6: (a, b) => a.push(b), aaU: (a, predicate) => {
|
||||
for (let i = a.length - 1; i >= 0; --i) if (predicate.$1(a[i])) a.splice(i, 1);
|
||||
},
|
||||
bf: a => { let i = -1; return {v: () => ++i < a.length, gM: () => a[i]}; },
|
||||
cTH: a => a.a, kz: (a, b) => a.split(b), aA: a => a.length,
|
||||
a1: (a, i) => a[i], p: (a, b) => a === b, cri: (a, n) => a.toFixed(n)
|
||||
cTB: a => a.a, kz: (a, b) => a.split(b), aA: a => a.length,
|
||||
a1: (a, i) => a[i], p: (a, b) => a === b, crc: (a, n) => a.toFixed(n)
|
||||
};
|
||||
function deferred() {
|
||||
const result = {a: 0};
|
||||
@@ -79,12 +79,12 @@ const A = {
|
||||
b3: () => ({$1() {}}), b6() {},
|
||||
bF: (_, __, ___, ms) => ({a: ms * 1000}), bT: function(a) { this.a = a; },
|
||||
fh: (duration, callback) => timers.push({due: now + duration.a / 1000, callback}),
|
||||
csH: callback => queueMicrotask(() => callback.$0()),
|
||||
csB: callback => queueMicrotask(() => callback.$0()),
|
||||
wc: function() {}, an: () => radioConnection,
|
||||
kY: value => { const number = Number(value); assert(Number.isFinite(number)); return number; },
|
||||
cS: value => { const number = Number(value); assert(Number.isInteger(number)); return number; },
|
||||
c: () => ({glp: () => "invalid radio fields", glb: () => "radio error"}),
|
||||
av: (...args) => errors.push(args), L: error => error,
|
||||
c: () => ({glo: () => "invalid radio fields", gla: () => "radio error"}),
|
||||
au: (...args) => errors.push(args), av: (...args) => errors.push(args), L: error => error,
|
||||
aH: (_, error) => errors.push(error)
|
||||
};
|
||||
const context = vm.createContext({A, B, J, t, $, u: {AD: "tag mismatch", b1: "CLI mismatch"},
|
||||
@@ -93,15 +93,15 @@ function constructor(name) {
|
||||
const code = extract(`${name}:function ${name}(`).replace(`${name}:`, `A.${name}=`);
|
||||
vm.runInContext(code, context);
|
||||
}
|
||||
for (const name of ["cZ", "d3", "fo", "ab7", "U1", "aba", "PS", "acs",
|
||||
"aSq", "aSp", "aSr", "aSs", "aSf", "pK", "b0Z", "b0Y",
|
||||
"b0X", "b1_", "b0W"]) constructor(name);
|
||||
for (const name of ["cZ", "d3", "fo", "aba", "aSq", "aSp", "aSr", "aSs", "aSf",
|
||||
"b0V", "b0Z", "b0Y", "b0X", "b1_", "b0W", "wc"]) {
|
||||
for (const name of ["cZ", "d3", "fo", "ab8", "U1", "ab5", "PS", "acr",
|
||||
"aSq", "aSp", "aSr", "aSs", "aSf", "pJ", "b0S", "b0R",
|
||||
"b0Q", "b0T", "b0P"]) constructor(name);
|
||||
for (const name of ["cZ", "d3", "fo", "ab5", "aSq", "aSp", "aSr", "aSs", "aSf",
|
||||
"b0O", "b0S", "b0R", "b0Q", "b0T", "b0P", "wc"]) {
|
||||
if (!A[name]) A[name] = function() {};
|
||||
vm.runInContext(extract(`A.${name}.prototype={`), context);
|
||||
}
|
||||
Object.assign(A, vm.runInContext(`({${extract("eD(a){")},${extract("d_f(a){")},${extract("bO(a,b){")}})`, context));
|
||||
Object.assign(A, vm.runInContext(`({${extract("\neD(a){")},${extract("\nd_9(a){")},${extract("\nbO(a,b){")}})`, context));
|
||||
|
||||
function connection() {
|
||||
const connection = new A.wc();
|
||||
@@ -111,8 +111,8 @@ function connection() {
|
||||
i: (_, key) => map.get(key), Z: (_, key) => map.has(key),
|
||||
ey: (_, fn) => { for (const [key, value] of map) fn.$2(key, value); }
|
||||
};
|
||||
for (const name of ["eY", "N", "bCk", "bu", "dT", "W7"])
|
||||
connection[name] = A.b0V.prototype[name];
|
||||
for (const name of ["eY", "N", "bCk", "bu", "dT", "W6"])
|
||||
connection[name] = A.b0O.prototype[name];
|
||||
connection.writes = [];
|
||||
connection.d6 = async bytes => { connection.writes.push(Buffer.from(bytes)); };
|
||||
return connection;
|
||||
|
||||
@@ -29,15 +29,15 @@ $.lw = $.c5 = () => {};
|
||||
$.a4 = null;
|
||||
A.au = value => Promise.resolve(value);
|
||||
A.aN = bytes => Buffer.from(bytes).toString("hex");
|
||||
A.jV = (items, predicate) => items.find(item => predicate.$1(item)) || null;
|
||||
A.jU = (items, predicate) => items.find(item => predicate.$1(item)) || null;
|
||||
// SQLite's map and insert boundaries retain values produced by real app code.
|
||||
A.z = () => ({fields: {}, l(_, key, value) { this.fields[key] = value.a; }});
|
||||
context.Date = {now: () => 1900000000000};
|
||||
for (const name of ["VU", "Nu", "be", "ax", "VW", "bZ7", "aS1", "aS2", "aS3", "aS4"])
|
||||
for (const name of ["VT", "Nt", "be", "ax", "VV", "bZ1", "aS1", "aS2", "aS3", "aS4"])
|
||||
constructor(name);
|
||||
for (const name of ["VW", "bZ7", "aS1", "aS2", "aS3", "aS4"])
|
||||
for (const name of ["VV", "bZ1", "aS1", "aS2", "aS3", "aS4"])
|
||||
vm.runInContext(extract(`A.${name}.prototype={`), context);
|
||||
Object.assign(A, vm.runInContext(`({${extract("czU(a,b,c,d,e,f,g,h,i,j,k,l,m,n){")}})`, context));
|
||||
Object.assign(A, vm.runInContext(`({${extract("czO(a,b,c,d,e,f,g,h,i,j,k,l,m,n){")}})`, context));
|
||||
const methods = vm.runInContext(`({${extract("\nbA0(a,b,c){")},${extract("\nbCv(a,b,c){")}})`, context);
|
||||
|
||||
function connection() {
|
||||
@@ -47,8 +47,8 @@ function connection() {
|
||||
i: (_, key) => listeners.get(key), Z: (_, key) => listeners.has(key),
|
||||
ey: (_, fn) => { for (const [key, value] of listeners) fn.$2(key, value); }
|
||||
};
|
||||
for (const name of ["eY", "N", "bCk", "bu", "dT", "W7"])
|
||||
client[name] = A.b0V.prototype[name];
|
||||
for (const name of ["eY", "N", "bCk", "bu", "dT", "W6"])
|
||||
client[name] = A.b0O.prototype[name];
|
||||
client.writes = [];
|
||||
client.d6 = async bytes => { client.writes.push(Buffer.from(bytes)); };
|
||||
return client;
|
||||
@@ -75,7 +75,7 @@ async function persist(message) {
|
||||
abP(owner, contact, decoded) { return methods.bA0.call(this, owner, contact, decoded); },
|
||||
a33: async () => null
|
||||
};
|
||||
$.e_ = {d: ownerKey}; $.aj = database;
|
||||
$.dZ = {d: ownerKey}; $.aj = database;
|
||||
// This is the stock app's ordinary/backlog message intake, with widget
|
||||
// notifications disabled and its SQL duplicate-query boundary mocked.
|
||||
await methods.bCv.call({k2: [room]}, message, false, false);
|
||||
|
||||
@@ -399,6 +399,66 @@ int main(){
|
||||
send_result=ESP_OK;now_ms+=20;bridge.loop();complete(bridge);assert(bridge._tx_count==0);
|
||||
bridge.end();
|
||||
}
|
||||
''', (40400, 50200, 50500))
|
||||
|
||||
def test_compact_espnow_frames_keep_every_length_and_all_queue_slots(self):
|
||||
self.run_fixture(r'''
|
||||
int main(){
|
||||
using namespace mesh::espnow;
|
||||
static_assert(ESPNOW_RAW_MAX_SECOND_FRAME_SIZE==31);
|
||||
static_assert(sizeof(ESPNowRawFrames)==288);
|
||||
static_assert(sizeof(ESPNowRawFrameData)==281);
|
||||
const uint8_t mac[6]={1,2,3,4,5,6};
|
||||
for(size_t length=2;length<=255;++length){
|
||||
auto original=packet(1,length);
|
||||
for(size_t i=1;i<length;++i)original.data[i]=uint8_t(i*37+length);
|
||||
ESPNowRawFrames frames;
|
||||
assert(encodeEspNowRawFrames(original.data,length,frames));
|
||||
assert(frames.count==(length<=250?1:2));
|
||||
assert(frames.data.capacity(0)==250&&frames.data.capacity(1)==31);
|
||||
assert(frames.data.capacity(2)==0&&frames.data[2]==nullptr);
|
||||
const ESPNowRawFrames copy=frames;
|
||||
memset(&frames,0,sizeof(frames)); // Queue copies must own both arrays.
|
||||
ESPNowRawReassembler assembler;uint8_t out[255]={};size_t size=0;
|
||||
ESPNowRawReassemblyResult result=ESPNowRawReassemblyResult::REJECTED;
|
||||
// Reverse-order fragments exercise the compact tail independently.
|
||||
for(unsigned index=copy.count;index-->0;){
|
||||
assert(copy.lengths[index]<=copy.data.capacity(index));
|
||||
result=assembler.acceptFrame(mac,copy.data[index],copy.lengths[index],100,out,sizeof(out),size);
|
||||
}
|
||||
assert(result==(length<=250?ESPNowRawReassemblyResult::PASSTHROUGH:ESPNowRawReassemblyResult::PACKET_COMPLETE));
|
||||
assert(size==length&&!memcmp(out,original.data,length));
|
||||
}
|
||||
Backend backend;backend.wifi=true;mesh::wireless::control().begin(backend);
|
||||
sdk_initialized=sdk_started=true;sdk_mode=WIFI_STA;
|
||||
NodePrefs prefs;mesh::PacketManager mgr;mesh::RTCClock rtc;ESPNowBridge bridge(&prefs,&mgr,&rtc);bridge.begin();
|
||||
ESPNowRawFrames invalid;auto source=packet(1,255);
|
||||
assert(encodeEspNowRawFrames(source.data,source.length,invalid));
|
||||
invalid.lengths[1]=32;
|
||||
assert(!bridge.queueTransmitFrames(invalid,source)&&bridge._tx_count==0);
|
||||
invalid.lengths[1]=0;
|
||||
assert(!bridge.queueTransmitFrames(invalid,source)&&bridge._tx_count==0);
|
||||
invalid.lengths[1]=31;invalid.lengths[0]=251;
|
||||
assert(!bridge.queueTransmitFrames(invalid,source)&&bridge._tx_count==0);
|
||||
assert(send_calls==0);
|
||||
// Retain all six largest packets at once, then overwrite the caller's copy.
|
||||
for(uint8_t id=1;id<=6;++id){
|
||||
auto p=packet(id,255);bridge.sendPacket(&p);memset(p.data,0,sizeof(p.data));
|
||||
}
|
||||
assert(bridge._tx_count==6);
|
||||
bridge.loop();
|
||||
for(unsigned i=0;i<12;++i)complete(bridge);
|
||||
assert(bridge._tx_count==0&&sent.size()==12);
|
||||
for(unsigned i=0;i<6;++i){
|
||||
ESPNowRawReassembler assembler;uint8_t out[255];size_t size=0;
|
||||
assert(sent[2*i].size()==250&&sent[2*i+1].size()==31);
|
||||
assembler.acceptFrame(mac,sent[2*i].data(),sent[2*i].size(),100,out,sizeof(out),size);
|
||||
assert(assembler.acceptFrame(mac,sent[2*i+1].data(),sent[2*i+1].size(),101,out,sizeof(out),size)==ESPNowRawReassemblyResult::PACKET_COMPLETE);
|
||||
auto expected=packet(uint8_t(i+1),255);
|
||||
assert(size==255&&!memcmp(out,expected.data,size));
|
||||
}
|
||||
bridge.end();
|
||||
}
|
||||
''', (40400, 50200, 50500))
|
||||
|
||||
def test_missing_callback_restarts_session_and_failed_teardown_blocks_admission(self):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Prove compact ref10 products against unrolled code and integer arithmetic."""
|
||||
"""Prove compact ref10 field operations against unrolled code and integers."""
|
||||
|
||||
import ctypes
|
||||
from pathlib import Path
|
||||
@@ -61,6 +61,14 @@ class Ed25519CompactFieldTest(unittest.TestCase):
|
||||
for name in ("fe_cmov", "fe_cswap"):
|
||||
getattr(library, name).argtypes = [ctypes.POINTER(ctypes.c_int32)] * 2 + [ctypes.c_uint]
|
||||
getattr(library, name).restype = None
|
||||
library.fe_mul121666.argtypes = [ctypes.POINTER(ctypes.c_int32)] * 2
|
||||
library.fe_mul121666.restype = None
|
||||
library.fe_frombytes.argtypes = [ctypes.POINTER(ctypes.c_int32), ctypes.c_void_p]
|
||||
library.fe_frombytes.restype = None
|
||||
library.fe_tobytes.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32)]
|
||||
library.fe_tobytes.restype = None
|
||||
library.fe_isnonzero.argtypes = [ctypes.POINTER(ctypes.c_int32)]
|
||||
library.fe_isnonzero.restype = ctypes.c_int
|
||||
cls.libraries.append(library)
|
||||
|
||||
@classmethod
|
||||
@@ -146,6 +154,123 @@ class Ed25519CompactFieldTest(unittest.TestCase):
|
||||
getattr(self.libraries[1], name)(a, a, select)
|
||||
self.assertEqual(list(a), first)
|
||||
|
||||
def test_decode_matches_original_limbs_and_modular_integer_including_sign_bit(self):
|
||||
rng = random.Random(0xDEC0DE)
|
||||
values = [0, PRIME - 1, PRIME, PRIME + 1, 2**255 - 1]
|
||||
values += [value for bit in range(255)
|
||||
for value in ((1 << bit) - 1, 1 << bit, (1 << bit) + 1)]
|
||||
values += [rng.getrandbits(255) for _ in range(10000)]
|
||||
for index, value in enumerate(values):
|
||||
for sign in (0, 1):
|
||||
encoded = (value | sign << 255).to_bytes(32, "little")
|
||||
outputs = []
|
||||
for library in self.libraries:
|
||||
output = Field()
|
||||
library.fe_frombytes(output, encoded)
|
||||
outputs.append(list(output))
|
||||
self.assertEqual(outputs[0], outputs[1], (index, sign))
|
||||
self.assertEqual(integer(outputs[1]), value % PRIME, (index, sign))
|
||||
# The entire source is consumed before any destination write.
|
||||
overlap = ctypes.create_string_buffer(40)
|
||||
ctypes.memmove(overlap, encoded, 32)
|
||||
self.libraries[1].fe_frombytes(ctypes.cast(overlap, ctypes.POINTER(ctypes.c_int32)), overlap)
|
||||
self.assertEqual(list(Field.from_buffer(overlap)), outputs[0])
|
||||
|
||||
def test_encode_and_nonzero_match_original_and_canonical_integer(self):
|
||||
values = vectors(0xEAC0DE, 10000)
|
||||
# Encoding and multiplication-by-121666 have a tighter ref10 bound.
|
||||
bounds = [110 * (1 << (26 - (i & 1))) // 100 for i in range(10)]
|
||||
values = [[min(bound, max(-bound, limb))
|
||||
for limb, bound in zip(value, bounds)] for value in values]
|
||||
# Include nonzero representations of field zero and reduction edges.
|
||||
values += [[-19, 0, 0, 0, 0, 0, 0, 0, 0, 1 << 25],
|
||||
[19, 0, 0, 0, 0, 0, 0, 0, 0, -(1 << 25)]]
|
||||
for index, value in enumerate(values):
|
||||
outputs = []
|
||||
for library in self.libraries:
|
||||
output = ctypes.create_string_buffer(32)
|
||||
library.fe_tobytes(output, Field(*value))
|
||||
outputs.append(output.raw)
|
||||
self.assertEqual(library.fe_isnonzero(Field(*value)), int(integer(value) != 0), index)
|
||||
self.assertEqual(outputs[0], outputs[1], index)
|
||||
self.assertEqual(int.from_bytes(outputs[1], "little"), integer(value), index)
|
||||
overlap = Field(*value)
|
||||
self.libraries[1].fe_tobytes(overlap, overlap)
|
||||
self.assertEqual(ctypes.string_at(overlap, 32), outputs[0])
|
||||
|
||||
def test_multiply_constant_matches_original_integer_and_aliases(self):
|
||||
bounds = [110 * (1 << (26 - (i & 1))) // 100 for i in range(10)]
|
||||
for index, value in enumerate(vectors(0x121666, 10000)):
|
||||
value = [min(bound, max(-bound, limb)) for limb, bound in zip(value, bounds)]
|
||||
reference = Field()
|
||||
self.libraries[0].fe_mul121666(reference, Field(*value))
|
||||
for alias in (False, True):
|
||||
data = Field(*value)
|
||||
output = data if alias else Field()
|
||||
self.libraries[1].fe_mul121666(output, data)
|
||||
self.assertEqual(list(output), list(reference), (index, alias))
|
||||
self.assertEqual(integer(output), integer(value) * 121666 % PRIME, (index, alias))
|
||||
for limb, result in enumerate(output):
|
||||
self.assertLessEqual(abs(result), 110 * (1 << (25 - (limb & 1))) // 100)
|
||||
|
||||
def test_serialization_and_constant_multiply_under_sanitizers(self):
|
||||
values = vectors(0xC0DE, 128)
|
||||
bounds = [110 * (1 << (26 - (i & 1))) // 100 for i in range(10)]
|
||||
rows = []
|
||||
for index, value in enumerate(values):
|
||||
value = [min(bound, max(-bound, limb)) for limb, bound in zip(value, bounds)]
|
||||
encoded = ctypes.create_string_buffer(32)
|
||||
multiplied = Field()
|
||||
decoded = Field()
|
||||
self.libraries[0].fe_tobytes(encoded, Field(*value))
|
||||
# Alternate ignored high bits while retaining the encoded value.
|
||||
source = encoded.raw[:31] + bytes([encoded.raw[31] | ((index & 1) << 7)])
|
||||
self.libraries[0].fe_frombytes(decoded, source)
|
||||
self.libraries[0].fe_mul121666(multiplied, Field(*value))
|
||||
row = (value, list(multiplied), list(decoded), list(source), list(encoded.raw))
|
||||
rows.append("{" + ",".join("{" + ",".join(map(str, item)) + "}"
|
||||
for item in row) + "}")
|
||||
source = r'''
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include "fe.h"
|
||||
struct Vector { fe input, multiplied, decoded; unsigned char source[32], encoded[32]; };
|
||||
static const struct Vector vectors[] = { @VECTORS@ };
|
||||
int main(void) {
|
||||
for (unsigned int i = 0; i < sizeof(vectors) / sizeof(vectors[0]); ++i) {
|
||||
const struct Vector *v = &vectors[i];
|
||||
fe output;
|
||||
unsigned char bytes[32];
|
||||
fe_frombytes(output, v->source);
|
||||
assert(memcmp(output, v->decoded, sizeof(fe)) == 0);
|
||||
fe_tobytes(bytes, v->input);
|
||||
assert(memcmp(bytes, v->encoded, sizeof(bytes)) == 0);
|
||||
unsigned char nonzero = 0;
|
||||
for (unsigned int j = 0; j < sizeof(bytes); ++j) nonzero |= bytes[j];
|
||||
assert(fe_isnonzero(v->input) == (nonzero != 0));
|
||||
memcpy(output, v->input, sizeof(fe));
|
||||
fe_mul121666(output, output);
|
||||
assert(memcmp(output, v->multiplied, sizeof(fe)) == 0);
|
||||
union { fe limbs; unsigned char bytes[40]; } overlap;
|
||||
memcpy(overlap.bytes, v->source, 32);
|
||||
fe_frombytes(overlap.limbs, overlap.bytes);
|
||||
assert(memcmp(overlap.limbs, v->decoded, sizeof(fe)) == 0);
|
||||
memcpy(overlap.limbs, v->input, sizeof(fe));
|
||||
fe_tobytes(overlap.bytes, overlap.limbs);
|
||||
assert(memcmp(overlap.bytes, v->encoded, 32) == 0);
|
||||
}
|
||||
}
|
||||
'''.replace("@VECTORS@", ",\n".join(rows))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
harness = Path(directory) / "encoding.c"
|
||||
harness.write_text(source)
|
||||
executable = Path(directory) / "encoding"
|
||||
subprocess.run(["cc", "-std=c99", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-no-pie",
|
||||
"-DED25519_COMPACT_FIELD=1", "-I", str(ED25519),
|
||||
str(harness), str(ED25519 / "fe.c"), "-o", str(executable)], check=True)
|
||||
subprocess.run([str(executable)], check=True)
|
||||
|
||||
def test_compact_products_under_address_and_undefined_sanitizers(self):
|
||||
values = vectors(0xC0FE, 128)
|
||||
# Keep every edge/carry-boundary vector and a bounded random selection.
|
||||
|
||||
@@ -247,7 +247,10 @@ class Esp32HwcdcRecipeTests(unittest.TestCase):
|
||||
self.assertFalse(self.sdk["native_primary_enabled"](row["env"]))
|
||||
self.assertEqual(self.config.get(f"env:{name}", "platform"),
|
||||
"platformio/espressif32@6.11.0")
|
||||
self.assertFalse(self.config.get(f"env:{name}", "platform_packages", []))
|
||||
self.assertEqual(self.config.get(f"env:{name}", "platform_packages", []), [
|
||||
"framework-arduinoespressif32 @ https://github.com/IoTThinks/"
|
||||
"powersaving-arduino-lib/releases/download/2.0.17/esp32-2.0.17.zip",
|
||||
])
|
||||
self.assertIn("pre:scripts/esp32_usb_session_fix.py",
|
||||
self.config.get(f"env:{name}", "extra_scripts", []))
|
||||
if name not in PREVIOUS_G2_HWCDC:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Verify explicit HIL observer bindings without editing the installed core."""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
@@ -180,6 +181,9 @@ class Nrf52HilIrqBindingTest(unittest.TestCase):
|
||||
# The original same-object weak aliases reproduce the final-ELF failure.
|
||||
# Explicit vector targets must call each wrapper and each real handler
|
||||
# exactly once, including when the other C++ translation unit uses LTO.
|
||||
# Keep the synthetic real definitions public: native LTO cannot see the
|
||||
# later linker rewrite from __real_* to the real symbol. The separate
|
||||
# ARM-toolchain check below uses unannotated real definitions.
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-hil-irq-link-", dir="/var/tmp") as directory:
|
||||
directory = Path(directory)
|
||||
source = '#include <cassert>\n#include <cstdint>\n'
|
||||
@@ -187,7 +191,8 @@ class Nrf52HilIrqBindingTest(unittest.TestCase):
|
||||
source += 'extern "C" void SVC_Handler() {}\nextern "C" void PendSV_Handler() {}\n'
|
||||
source += f"static unsigned real_calls[{len(PATCH.IRQ_HANDLERS)}], wrapped_calls[{len(PATCH.IRQ_HANDLERS)}];\n"
|
||||
for index, symbol in enumerate(PATCH.IRQ_HANDLERS):
|
||||
source += f'extern "C" void {symbol}() {{ ++real_calls[{index}]; }}\n'
|
||||
source += (f'extern "C" __attribute__((used, externally_visible)) '
|
||||
f'void {symbol}() {{ ++real_calls[{index}]; }}\n')
|
||||
source += f'extern "C" void __real_{symbol}();\n'
|
||||
source += (f'extern "C" void __wrap_{symbol}() {{ ++wrapped_calls[{index}]; '
|
||||
f'__real_{symbol}(); }}\n')
|
||||
@@ -209,6 +214,62 @@ class Nrf52HilIrqBindingTest(unittest.TestCase):
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_target_lto_retains_unannotated_real_handlers_and_explicit_wrappers(self):
|
||||
core = Path(os.environ.get("PLATFORMIO_CORE_DIR", Path.home() / ".platformio"))
|
||||
toolchain = core / "packages/toolchain-gccarmnoneeabi/bin"
|
||||
suffix = ".exe" if os.name == "nt" else ""
|
||||
compiler = toolchain / ("arm-none-eabi-g++" + suffix)
|
||||
nm = toolchain / ("arm-none-eabi-nm" + suffix)
|
||||
if not compiler.is_file() or not nm.is_file():
|
||||
self.skipTest("installed nRF52 target toolchain unavailable")
|
||||
# Link the real target architecture with the nRF52 LTO profile. Native
|
||||
# GCC 11 and target GCC 14 differ in their late --wrap symbol retention.
|
||||
# No used/externally_visible annotations hide a target liveness failure.
|
||||
source = 'extern "C" const unsigned test_vectors[];\n'
|
||||
source += f"static volatile unsigned calls[{len(PATCH.IRQ_HANDLERS)}];\n"
|
||||
for index, symbol in enumerate(PATCH.IRQ_HANDLERS):
|
||||
source += f'extern "C" void {symbol}() {{ ++calls[{index}]; }}\n'
|
||||
source += f'extern "C" void __real_{symbol}();\n'
|
||||
source += f'extern "C" void __wrap_{symbol}() {{ __real_{symbol}(); }}\n'
|
||||
source += 'extern "C" void SVC_Handler() {}\nextern "C" void PendSV_Handler() {}\n'
|
||||
source += f"int main() {{ for (unsigned i=0; i<{len(PATCH.IRQ_HANDLERS)}; ++i) "
|
||||
source += "reinterpret_cast<void(*)()>(test_vectors[i+2])(); return 0; }\n"
|
||||
flags = [
|
||||
"-std=c++17", "-mcpu=cortex-m4", "-mthumb", "-mfloat-abi=hard", "-mfpu=fpv4-sp-d16",
|
||||
"-Oz", "-flto", "-flto-partition=one", "-fno-inline-small-functions", "-fipa-pta",
|
||||
"-fmerge-all-constants", "-fno-ipa-cp-clone", "-fno-partial-inlining", "-fno-jump-tables",
|
||||
"-fno-tree-switch-conversion", "-fno-semantic-interposition", "-fno-unwind-tables",
|
||||
"-fno-asynchronous-unwind-tables", "-fno-schedule-insns2", "-ffunction-sections",
|
||||
"-fdata-sections", "-fno-exceptions", "-fno-rtti", "-nostdlib", "-Wl,-e,main",
|
||||
"-Wl,--gc-sections", "-Wl,--sort-section=alignment",
|
||||
]
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-hil-irq-arm-link-") as directory:
|
||||
directory = Path(directory)
|
||||
handlers = directory / "handlers.cpp"
|
||||
handlers.write_text(source)
|
||||
for patched in (False, True):
|
||||
with self.subTest(patched=patched):
|
||||
startup = PATCH.patched_startup(STARTUP) if patched else STARTUP
|
||||
startup = ".syntax unified\n.thumb\n" + startup.replace(
|
||||
" .text\n", " .text\n .thumb_func\n")
|
||||
startup = startup.replace(" ret\n", " bx lr\n").replace(
|
||||
" .set ", " .thumb_set ").replace("@progbits", "%progbits")
|
||||
assembly = directory / "startup.S"
|
||||
assembly.write_text(startup)
|
||||
binary = directory / "vectors.elf"
|
||||
built = subprocess.run([
|
||||
str(compiler), *flags, str(handlers), str(assembly),
|
||||
*("-Wl,--wrap=" + name for name in PATCH.IRQ_HANDLERS), "-o", str(binary),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
listed = subprocess.run([str(nm), "--defined-only", str(binary)],
|
||||
check=True, capture_output=True, text=True, timeout=20)
|
||||
symbols = {line.split()[-1]: line.split()[-2] for line in listed.stdout.splitlines()
|
||||
if len(line.split()) >= 3}
|
||||
for symbol in PATCH.IRQ_HANDLERS:
|
||||
self.assertEqual(symbols.get(symbol), "T")
|
||||
self.assertEqual("__wrap_" + symbol in symbols, patched)
|
||||
|
||||
def test_nrf52_base_installs_binding_hook_once(self):
|
||||
config = (ROOT / "platformio.ini").read_text()
|
||||
base = config.split("[nrf52_base]", 1)[1].split("\n[", 1)[0]
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute pinned stock app code against actual firmware ACL/radio outputs.
|
||||
|
||||
The public web build is the source witness, not proof of the installed iOS app.
|
||||
The whole app remains outside Git. A changed upstream bundle fails its SHA gate;
|
||||
an explicit source-pin update is required, never silent parser replacement.
|
||||
The official v1.50.0+78 web archive is the source witness, not proof of the
|
||||
installed iOS app. The whole app remains outside Git. Both the archive and its
|
||||
JavaScript have size/SHA gates; updates require an explicit source-pin review.
|
||||
No PlatformIO, devices, app fork, settings writes or real radio are involved.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -16,7 +17,9 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
from test_client_acl_response import production_acl_methods
|
||||
from test_companion_primary_radio_persistence import (
|
||||
@@ -25,9 +28,14 @@ from test_companion_delayed_reply_delivery import production_delayed_reply_input
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_URL = "https://app.meshcore.nz/main.dart.js"
|
||||
APP_SHA256 = "84bc39a950735aaffa93d912556852a4c22e235d0cc4e4d04790113207f13a68"
|
||||
APP_BYTES = 9494192
|
||||
# Use the publisher's versioned archive: the live app URL changes independently
|
||||
# of firmware and cannot reproduce the contract witness on a cold CI cache.
|
||||
APP_URL = ("https://files.liamcottle.net/MeshCore/v1.50.0/"
|
||||
"MeshCore-v1.50.0%2B78-b7528a7-web.zip")
|
||||
APP_ARCHIVE_SHA256 = "c28f5549a63ce42a50a8a5bd80081c69ef85aeb1dca221c36bc692b838a92dd7"
|
||||
APP_ARCHIVE_BYTES = 16950142
|
||||
APP_SHA256 = "aafc714732d1e1af749104da446cd250ea72e288f7cd60ce742f9733da30f309"
|
||||
APP_BYTES = 9491330
|
||||
JS = ROOT / "test/fixtures/official_app_compatibility/app_contract.js"
|
||||
SANITIZERS = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
|
||||
@@ -41,7 +49,15 @@ def official_app_bundle():
|
||||
if explicit:
|
||||
raise AssertionError(f"Missing explicit official app bundle: {path}")
|
||||
with urllib.request.urlopen(APP_URL, timeout=45) as response:
|
||||
data = response.read(APP_BYTES + 1)
|
||||
archive = response.read(APP_ARCHIVE_BYTES + 1)
|
||||
if (len(archive) != APP_ARCHIVE_BYTES or
|
||||
hashlib.sha256(archive).hexdigest() != APP_ARCHIVE_SHA256):
|
||||
raise AssertionError("Official app archive SHA/size mismatch: review its source pin")
|
||||
with zipfile.ZipFile(io.BytesIO(archive)) as release:
|
||||
member = release.getinfo("web/main.dart.js")
|
||||
if member.file_size != APP_BYTES:
|
||||
raise AssertionError("Official app JavaScript archive size mismatch")
|
||||
data = release.read(member)
|
||||
if len(data) != APP_BYTES or hashlib.sha256(data).hexdigest() != APP_SHA256:
|
||||
raise AssertionError("Official app changed: obtain the pinned cached build or review/update its source pin")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -265,6 +281,53 @@ public:
|
||||
return replies
|
||||
|
||||
|
||||
class OfficialAppSourcePinTests(unittest.TestCase):
|
||||
def test_archive_and_member_gates_before_caching(self):
|
||||
script = b"stock app witness"
|
||||
|
||||
def archive_for(contents):
|
||||
output = io.BytesIO()
|
||||
with zipfile.ZipFile(output, "w") as release:
|
||||
release.writestr("web/main.dart.js", contents)
|
||||
return output.getvalue()
|
||||
|
||||
archive = archive_for(script)
|
||||
wrong_member = archive_for(script[:-1] + b"!")
|
||||
wrong_size = archive_for(script + b"!")
|
||||
digest = hashlib.sha256(script).hexdigest()
|
||||
cases = (
|
||||
("short archive", archive[:-1], archive),
|
||||
("changed archive", bytes([archive[0] ^ 1]) + archive[1:], archive),
|
||||
("changed member", wrong_member, wrong_member),
|
||||
("wrong member size", wrong_size, wrong_size),
|
||||
("verified", archive, archive),
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="official-app-source-pin-") as directory:
|
||||
home = Path(directory)
|
||||
cache = home / ".cache/meshcore-tests" / digest / "main.dart.js"
|
||||
with mock.patch.dict(os.environ, {"MESHCORE_OFFICIAL_APP_BUNDLE": ""}), \
|
||||
mock.patch.object(Path, "home", return_value=home):
|
||||
for name, downloaded, expected_archive in cases:
|
||||
with self.subTest(name=name), mock.patch.multiple(
|
||||
sys.modules[__name__], APP_BYTES=len(script), APP_SHA256=digest,
|
||||
APP_ARCHIVE_BYTES=len(expected_archive),
|
||||
APP_ARCHIVE_SHA256=hashlib.sha256(expected_archive).hexdigest()), \
|
||||
mock.patch.object(urllib.request, "urlopen",
|
||||
return_value=io.BytesIO(downloaded)) as fetch:
|
||||
if name != "verified":
|
||||
with self.assertRaises(AssertionError):
|
||||
official_app_bundle()
|
||||
self.assertFalse(cache.exists())
|
||||
else:
|
||||
self.assertEqual(official_app_bundle(), cache.resolve())
|
||||
self.assertEqual(cache.read_bytes(), script)
|
||||
self.assertEqual(official_app_bundle(), cache.resolve())
|
||||
fetch.assert_called_once_with(APP_URL, timeout=45)
|
||||
cache.write_bytes(script[:-1] + b"!")
|
||||
with self.assertRaisesRegex(AssertionError, "source SHA/size mismatch"):
|
||||
official_app_bundle()
|
||||
|
||||
|
||||
class OfficialAppCompatibilityTests(unittest.TestCase):
|
||||
def test_pinned_app_functions_with_production_request_and_outputs(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
|
||||
@@ -292,13 +292,15 @@ class OfficialConfigGuiTests(unittest.TestCase):
|
||||
return result
|
||||
|
||||
def test_raw_uart_first_stock_gui_command_survives_preconnect_idle(self):
|
||||
for idle_ms in (180001, 10800000, 0xFFFFFFFE):
|
||||
for idle_ms in (300001, 10800000, 0xFFFFFFFE):
|
||||
with self.subTest(idle_ms=idle_ms):
|
||||
result = self.run_contract(raw_uart=True, preconnect_idle_ms=idle_ms)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_missing_raw_uart_sleep_guard_breaks_first_stock_gui_command(self):
|
||||
result = self.run_contract(raw_uart=True, preconnect_idle_ms=180001,
|
||||
# Run after the five-minute startup grace so removing the UART veto
|
||||
# actually permits sleep before the first GUI command arrives.
|
||||
result = self.run_contract(raw_uart=True, preconnect_idle_ms=300001,
|
||||
lose_raw_uart_guard=True)
|
||||
self.assertNotEqual(result.returncode, 0, "Negative control did not fail")
|
||||
self.assertIn("initial time missed the stock 5000 ms deadline", result.stderr)
|
||||
|
||||
Reference in new issue
Block a user