Fix unit test fixtures and fit constrained firmware builds
This commit is contained in:
1 parent
329ec3848c
commit
6c3bf00691
23 files changed
+475
-125
No files matched your search
@@ -274,6 +274,9 @@ jobs:
|
||||
mkdir -p .pio/libdeps/room-mail-test
|
||||
pio pkg install --global --library 'rweather/Crypto@0.4.0' --storage-dir .pio/libdeps/room-mail-test
|
||||
|
||||
- name: Verify shared SHA-512 signing against the original backend and RFC8032
|
||||
run: python3 -B test/test_ed25519_shared_sha512.py -v
|
||||
|
||||
- name: Verify ArduinoJson pin
|
||||
run: |
|
||||
python3 -B scripts/check_arduinojson_pin.py --self-test
|
||||
|
||||
@@ -3531,8 +3531,8 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
const auto& secondary = profiles->secondary;
|
||||
data[length - 1] = 1 | (static_cast<uint8_t>(secondary.mode) << 1)
|
||||
| (profiles->secondary_temporary ? 0x08 : 0);
|
||||
const uint32_t frequency = static_cast<uint32_t>(lroundf(secondary.params.freq * 1000.0f));
|
||||
const uint32_t bandwidth = static_cast<uint32_t>(lroundf(secondary.params.bw * 1000.0f));
|
||||
const uint32_t frequency = static_cast<uint32_t>(secondary.params.freq * 1000.0f + 0.5f);
|
||||
const uint32_t bandwidth = static_cast<uint32_t>(secondary.params.bw * 1000.0f + 0.5f);
|
||||
for (unsigned shift = 0; shift < 32; shift += 8) data[length++] = frequency >> shift;
|
||||
for (unsigned shift = 0; shift < 32; shift += 8) data[length++] = bandwidth >> shift;
|
||||
data[length++] = secondary.params.sf;
|
||||
@@ -5503,12 +5503,11 @@ bool MyMesh::handleGuestCommand(const char* command, char* reply, size_t capacit
|
||||
} else if (get) {
|
||||
snprintf(reply, capacity, "> %s%s", guest_login.enabled() ? "on" : "off",
|
||||
guest_login.healthy() ? "" : " (settings unavailable)");
|
||||
} else if (!guest_login.setEnabled(strcmp(value, "on") == 0)) {
|
||||
if (!guest_login.enabled()) acl.removeGuestClients();
|
||||
snprintf(reply, capacity, "Err - guest setting save unconfirmed; reboot may differ");
|
||||
} else {
|
||||
const bool saved = guest_login.setEnabled(strcmp(value, "on") == 0);
|
||||
if (!guest_login.enabled()) acl.removeGuestClients();
|
||||
snprintf(reply, capacity, "OK - guest %s", guest_login.enabled() ? "on" : "off");
|
||||
if (saved) snprintf(reply, capacity, "OK - guest %s", guest_login.enabled() ? "on" : "off");
|
||||
else snprintf(reply, capacity, "Err - guest setting save unconfirmed; reboot may differ");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "ed_25519.h"
|
||||
#include "ge.h"
|
||||
#include "sc.h"
|
||||
#include "sha512.h"
|
||||
#include "ed_sha512.h"
|
||||
|
||||
|
||||
/* see http://crypto.stackexchange.com/a/6215/4697 */
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef ED25519_SHA512_H
|
||||
#define ED25519_SHA512_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "fixedint.h"
|
||||
|
||||
#ifndef ED25519_USE_CRYPTO_SHA512
|
||||
#define ED25519_USE_CRYPTO_SHA512 0
|
||||
#endif
|
||||
|
||||
#if ED25519_USE_CRYPTO_SHA512
|
||||
/* C-owned, aligned storage for the optional C++ SHA512 implementation. */
|
||||
typedef union sha512_context_ {
|
||||
uint64_t alignment;
|
||||
unsigned char opaque[256];
|
||||
} sha512_context;
|
||||
#else
|
||||
/* state */
|
||||
typedef struct sha512_context_ {
|
||||
uint64_t length, state[8];
|
||||
size_t curlen;
|
||||
unsigned char buf[128];
|
||||
} sha512_context;
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* A completed context must be initialized again before reuse. */
|
||||
int sha512_init(sha512_context * md);
|
||||
int sha512_final(sha512_context * md, unsigned char *out);
|
||||
int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen);
|
||||
int sha512(const unsigned char *message, size_t message_len, unsigned char *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "ed_sha512.h"
|
||||
|
||||
#if ED25519_USE_CRYPTO_SHA512
|
||||
#include <SHA512.h>
|
||||
#include <new>
|
||||
|
||||
// Share the SHA-512 already used by Crypto's Ed25519 verifier. Contexts remain
|
||||
// caller-owned; hashing and signing allocate no heap and share no mutable state.
|
||||
static_assert(sizeof(SHA512) <= sizeof(sha512_context), "SHA512 context capacity");
|
||||
static_assert(alignof(SHA512) <= alignof(sha512_context), "SHA512 context alignment");
|
||||
|
||||
namespace {
|
||||
SHA512* implementation(sha512_context* context) {
|
||||
return reinterpret_cast<SHA512*>(context->opaque);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int sha512_init(sha512_context* context) {
|
||||
if (!context) return 1;
|
||||
new (context->opaque) SHA512();
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int sha512_update(sha512_context* context, const unsigned char* input,
|
||||
size_t length) {
|
||||
if (!context || !input) return 1;
|
||||
implementation(context)->SHA512::update(input, length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int sha512_final(sha512_context* context, unsigned char* output) {
|
||||
if (!context || !output) return 1;
|
||||
SHA512* hash = implementation(context);
|
||||
hash->SHA512::finalize(output, SHA512::HASH_SIZE);
|
||||
hash->SHA512::~SHA512();
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int sha512(const unsigned char* message, size_t length,
|
||||
unsigned char* output) {
|
||||
if (!message || !output) return 1;
|
||||
sha512_context context;
|
||||
sha512_init(&context);
|
||||
sha512_update(&context, message, length);
|
||||
sha512_final(&context, output);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "ed_25519.h"
|
||||
#include "sha512.h"
|
||||
#include "ed_sha512.h"
|
||||
#include "ge.h"
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
*/
|
||||
|
||||
#include "fixedint.h"
|
||||
#include "sha512.h"
|
||||
#include "ed_sha512.h"
|
||||
|
||||
#if !ED25519_USE_CRYPTO_SHA512
|
||||
|
||||
/* the K array */
|
||||
static const uint64_t K[80] = {
|
||||
@@ -303,3 +305,5 @@ int sha512(const unsigned char *message, size_t message_len, unsigned char *out)
|
||||
if ((ret = sha512_final(&ctx, out))) return ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,21 +0,0 @@
|
||||
#ifndef SHA512_H
|
||||
#define SHA512_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "fixedint.h"
|
||||
|
||||
/* state */
|
||||
typedef struct sha512_context_ {
|
||||
uint64_t length, state[8];
|
||||
size_t curlen;
|
||||
unsigned char buf[128];
|
||||
} sha512_context;
|
||||
|
||||
|
||||
int sha512_init(sha512_context * md);
|
||||
int sha512_final(sha512_context * md, unsigned char *out);
|
||||
int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen);
|
||||
int sha512(const unsigned char *message, size_t message_len, unsigned char *out);
|
||||
|
||||
#endif
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#include "ed_25519.h"
|
||||
#include "sha512.h"
|
||||
#include "ed_sha512.h"
|
||||
#include "ge.h"
|
||||
#include "sc.h"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "ed_25519.h"
|
||||
#include "sha512.h"
|
||||
#include "ed_sha512.h"
|
||||
#include "ge.h"
|
||||
#include "sc.h"
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ build_flags = ${arduino_base.build_flags}
|
||||
; PendSV reaches this FreeRTOS function from assembly, outside the LTO graph.
|
||||
-Wl,--undefined=vTaskSwitchContext
|
||||
-D NRF52_PLATFORM
|
||||
-D ED25519_USE_CRYPTO_SHA512=1 ; share the SHA-512 used by signature verification
|
||||
-D LFS_NO_ASSERT=1
|
||||
-D EXTRAFS=1
|
||||
-D MESH_NRF52_LOOP_STACK_WORDS=2048
|
||||
@@ -373,6 +374,7 @@ extra_scripts = post:arch/stm32/build_hex.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D STM32_PLATFORM
|
||||
-D ED25519_USE_CRYPTO_SHA512=1 ; avoid linking two SHA-512 implementations
|
||||
-D ED25519_COMPACT_BASE=1
|
||||
-I src/helpers/stm32
|
||||
-I $PROJECT_PACKAGES_DIR/framework-arduinoststm32/libraries/SubGhz/src
|
||||
|
||||
+4
-3
@@ -248,14 +248,15 @@ class RadioProfiles {
|
||||
}
|
||||
uint32_t listenUs(uint8_t profile, uint16_t slow_preamble = 0) const {
|
||||
const uint8_t slow = slowerProfile();
|
||||
const double minimum = minimumListenUs(params(profile), enabled() && profile == slow);
|
||||
if (!enabled() || profile == slow) return (uint32_t)ceil(minimum);
|
||||
const uint32_t minimum = minimumListenUs(params(profile), enabled() && profile == slow);
|
||||
if (!enabled() || profile == slow) return minimum;
|
||||
if (!slow_preamble) slow_preamble = preamble(slow, 32);
|
||||
const double available = double(slow_preamble) / 2.0 * symbolUs(params(slow))
|
||||
- minimumListenUs(params(slow), true) - 2.0 * switchBudgetUs() - LoopBudgetUs;
|
||||
// Rejected previews must not cause an out-of-range floating-to-int cast.
|
||||
// In the positive branch, truncation already rounds down to whole us.
|
||||
return available > UINT32_MAX ? UINT32_MAX
|
||||
: available > minimum ? (uint32_t)floor(available) : (uint32_t)minimum;
|
||||
: available > minimum ? (uint32_t)available : minimum;
|
||||
}
|
||||
ChirpTiming chirpTiming() const {
|
||||
if (!enabled()) return {};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
namespace mesh {
|
||||
namespace cli {
|
||||
|
||||
// Empty arguments select page 1. Zero signals invalid decimal page syntax.
|
||||
inline unsigned parseCliPage(const char* arg) {
|
||||
while (*arg == ' ' || *arg == '\t') ++arg;
|
||||
unsigned page = 1;
|
||||
if (*arg >= '0' && *arg <= '9') {
|
||||
page = 0;
|
||||
do {
|
||||
page = page * 10 + unsigned(*arg++ - '0');
|
||||
if (page > 65535) return 0;
|
||||
} while (*arg >= '0' && *arg <= '9');
|
||||
}
|
||||
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
|
||||
return *arg ? 0 : page;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace mesh
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ClientACL.h"
|
||||
#include "CLIPage.h"
|
||||
#include <Utils.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -21,17 +22,8 @@ inline bool handleACLGet(const ClientACL& acl, const char* command,
|
||||
if (local && *arg == 0) return false;
|
||||
if (reply == nullptr || capacity == 0) return true;
|
||||
|
||||
while (*arg == ' ' || *arg == '\t') ++arg;
|
||||
unsigned page = 1;
|
||||
if (*arg >= '0' && *arg <= '9') {
|
||||
page = 0;
|
||||
do {
|
||||
page = page * 10 + unsigned(*arg++ - '0');
|
||||
if (page > 65535) break;
|
||||
} while (*arg >= '0' && *arg <= '9');
|
||||
}
|
||||
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
|
||||
if (*arg || page == 0 || page > 65535) {
|
||||
const unsigned page = parseCliPage(arg);
|
||||
if (page == 0) {
|
||||
snprintf(reply, capacity, "Err - usage: get acl [page]");
|
||||
return true;
|
||||
}
|
||||
|
||||
+10
-24
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ClientACL.h"
|
||||
#include "CLIPage.h"
|
||||
#include <Utils.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -29,24 +30,17 @@ inline bool handleGuestsGet(const ClientACL& acl, const char* command,
|
||||
if (*arg && *arg != ' ' && *arg != '\t' && *arg != '\r' && *arg != '\n') return false;
|
||||
if (reply == nullptr || capacity == 0) return true;
|
||||
|
||||
while (*arg == ' ' || *arg == '\t') ++arg;
|
||||
unsigned page = 1;
|
||||
if (*arg >= '0' && *arg <= '9') {
|
||||
page = 0;
|
||||
do {
|
||||
page = page * 10 + unsigned(*arg++ - '0');
|
||||
if (page > 65535) break;
|
||||
} while (*arg >= '0' && *arg <= '9');
|
||||
}
|
||||
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
|
||||
if (*arg || page == 0 || page > 65535) {
|
||||
const unsigned page = parseCliPage(arg);
|
||||
if (page == 0) {
|
||||
snprintf(reply, capacity, "Err - usage: get guests [page]");
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned total = 0;
|
||||
const ClientInfo* selected = nullptr;
|
||||
for (int i = 0; i < acl.getNumClients(); ++i) {
|
||||
if (isLiveGuest(*acl.getClientByIdx(i))) ++total;
|
||||
const auto* client = acl.getClientByIdx(i);
|
||||
if (isLiveGuest(*client) && ++total == page) selected = client;
|
||||
}
|
||||
if (total == 0 && page == 1) {
|
||||
snprintf(reply, capacity, "Guests: empty");
|
||||
@@ -58,24 +52,16 @@ inline bool handleGuestsGet(const ClientACL& acl, const char* command,
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned index = 0;
|
||||
for (int i = 0; i < acl.getNumClients(); ++i) {
|
||||
const auto* client = acl.getClientByIdx(i);
|
||||
if (!isLiveGuest(*client) || ++index != page) continue;
|
||||
if (selected) {
|
||||
char key[PUB_KEY_SIZE * 2 + 1];
|
||||
Utils::toHex(key, client->id.pub_key, PUB_KEY_SIZE);
|
||||
const int needed = snprintf(nullptr, 0, "Guests %u/%u (%u)\n%s last=%lu",
|
||||
Utils::toHex(key, selected->id.pub_key, PUB_KEY_SIZE);
|
||||
const int needed = snprintf(reply, capacity, "Guests %u/%u (%u)\n%s last=%lu",
|
||||
page, pages, total, key,
|
||||
static_cast<unsigned long>(client->last_activity));
|
||||
static_cast<unsigned long>(selected->last_activity));
|
||||
// A small caller buffer must never turn a complete key into a prefix.
|
||||
if (needed < 0 || size_t(needed) >= capacity) {
|
||||
snprintf(reply, capacity, "Err - guest reply too small");
|
||||
} else {
|
||||
snprintf(reply, capacity, "Guests %u/%u (%u)\n%s last=%lu",
|
||||
page, pages, total, key,
|
||||
static_cast<unsigned long>(client->last_activity));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+13
-25
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "CLIPage.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -35,11 +36,11 @@ class LoginHistory {
|
||||
while (index < _size && memcmp(_entries[index].key, key, KEY_SIZE) != 0) ++index;
|
||||
|
||||
Entry latest = {};
|
||||
memcpy(latest.key, key, KEY_SIZE);
|
||||
if (index < _size) {
|
||||
latest = _entries[index];
|
||||
latest.attempts = _entries[index].attempts;
|
||||
if (latest.attempts != UINT32_MAX) ++latest.attempts;
|
||||
} else {
|
||||
memcpy(latest.key, key, KEY_SIZE);
|
||||
latest.attempts = 1;
|
||||
if (_size < CAPACITY) ++_size;
|
||||
index = _size - 1;
|
||||
@@ -82,17 +83,8 @@ inline bool handleLoginsGet(const LoginHistory& history, const char* command,
|
||||
if (*arg && *arg != ' ' && *arg != '\t' && *arg != '\r' && *arg != '\n') return false;
|
||||
if (reply == nullptr || capacity == 0) return true;
|
||||
|
||||
while (*arg == ' ' || *arg == '\t') ++arg;
|
||||
unsigned page = 1;
|
||||
if (*arg >= '0' && *arg <= '9') {
|
||||
page = 0;
|
||||
do {
|
||||
page = page * 10 + unsigned(*arg++ - '0');
|
||||
if (page > 65535) break;
|
||||
} while (*arg >= '0' && *arg <= '9');
|
||||
}
|
||||
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
|
||||
if (*arg || page == 0 || page > 65535) {
|
||||
const unsigned page = parseCliPage(arg);
|
||||
if (page == 0) {
|
||||
snprintf(reply, capacity, "Err - usage: get logins [page]");
|
||||
return true;
|
||||
}
|
||||
@@ -108,9 +100,10 @@ inline bool handleLoginsGet(const LoginHistory& history, const char* command,
|
||||
return true;
|
||||
}
|
||||
|
||||
char rows[2][80] = {};
|
||||
unsigned count = 0;
|
||||
for (unsigned index = (page - 1) * 2; index < total && count < 2; ++index) {
|
||||
int used = snprintf(reply, capacity, "Logins %u/%u (%u)", page, pages, total);
|
||||
const unsigned end = page * 2 < total ? page * 2 : total;
|
||||
for (unsigned index = (page - 1) * 2; index < end
|
||||
&& used >= 0 && size_t(used) < capacity; ++index) {
|
||||
const auto& entry = *history.get(index);
|
||||
char key[13];
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
@@ -119,20 +112,15 @@ inline bool handleLoginsGet(const LoginHistory& history, const char* command,
|
||||
key[i * 2 + 1] = hex[entry.key[i] & 0x0F];
|
||||
}
|
||||
key[12] = 0;
|
||||
snprintf(rows[count], sizeof(rows[count]), "%s n=%lu last=%lu %s", key,
|
||||
const int written = snprintf(reply + used, capacity - size_t(used), "\n%s n=%lu last=%lu %s", key,
|
||||
static_cast<unsigned long>(entry.attempts),
|
||||
static_cast<unsigned long>(entry.timestamp), loginResultName(entry.result));
|
||||
++count;
|
||||
if (written < 0) { used = -1; break; }
|
||||
used += written;
|
||||
}
|
||||
const char* separator = count == 2 ? "\n" : "";
|
||||
const int needed = snprintf(nullptr, 0, "Logins %u/%u (%u)\n%s%s%s",
|
||||
page, pages, total, rows[0], separator, rows[1]);
|
||||
// Never accidentally expose a partly formatted public-key prefix.
|
||||
if (needed < 0 || size_t(needed) >= capacity) {
|
||||
if (used < 0 || size_t(used) >= capacity) {
|
||||
snprintf(reply, capacity, "Err - login reply too small");
|
||||
} else {
|
||||
snprintf(reply, capacity, "Logins %u/%u (%u)\n%s%s%s",
|
||||
page, pages, total, rows[0], separator, rows[1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -726,10 +726,11 @@ void RadioProfileCLI::appendChirpWarning(char* reply, size_t capacity, const Rad
|
||||
const bool short_a = preview.primary.preamble && preview.primary.preamble < timing.preamble[0];
|
||||
const bool short_b = preview.secondary.params.preamble && preview.secondary.params.preamble < timing.preamble[1];
|
||||
const bool a = timing.preamble[0] > 32 || short_a, b = timing.preamble[1] > 32 || short_b;
|
||||
if (a && b) appendReply(reply, capacity, "; WARN recommended preamble: radio=%.0f,radio2=%.0f",
|
||||
timing.preamble[0], timing.preamble[1]);
|
||||
else if (a || b) appendReply(reply, capacity, "; WARN recommended preamble: %s=%.0f",
|
||||
a ? "radio" : "radio2", timing.preamble[a ? 0 : 1]);
|
||||
if (a || b) {
|
||||
appendReply(reply, capacity, "; WARN recommended preamble: %s=%.0f",
|
||||
a ? "radio" : "radio2", timing.preamble[a ? 0 : 1]);
|
||||
if (a && b) appendReply(reply, capacity, ",radio2=%.0f", timing.preamble[1]);
|
||||
}
|
||||
if (short_a || short_b)
|
||||
appendReply(reply, capacity, "; short override: %s",
|
||||
short_a && short_b ? "radio,radio2" : short_a ? "radio" : "radio2");
|
||||
@@ -783,16 +784,26 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
memcpy(key, text, n); key[n] = 0;
|
||||
const char* args = text + n;
|
||||
while (*args == ' ' || *args == '\t') ++args;
|
||||
const bool base = !strcmp(key, "radio2");
|
||||
const bool temporary = !strcmp(key, "tempradio2");
|
||||
const bool scheduled = !strcmp(key, "radioat2") || !strcmp(key, "tempradioat2");
|
||||
const bool scheduled_temp = !strcmp(key, "tempradioat2");
|
||||
const bool crossing = !strcmp(key, "radio2.cross");
|
||||
const bool replies = infrastructure_replies_ && !strcmp(key, "tx.reply");
|
||||
const bool status = !strcmp(key, "radio2.status");
|
||||
const bool scan = !strcmp(key, "radio2.scan");
|
||||
const bool timing = !strcmp(key, "radio2.timing") || !strcmp(key, "radio.timing");
|
||||
if (!base && !temporary && !scheduled && !crossing && !replies && !status && !scan && !timing) return false;
|
||||
enum { Base, Temporary, Scheduled, ScheduledTemporary, Crossing, Replies,
|
||||
Status, Scan, Timing, TimingAlias, KeyCount };
|
||||
static const char keys[] = "radio2\0tempradio2\0radioat2\0tempradioat2\0radio2.cross\0"
|
||||
"tx.reply\0radio2.status\0radio2.scan\0radio2.timing\0radio.timing";
|
||||
const char* candidate = keys;
|
||||
unsigned kind = 0;
|
||||
while (kind < KeyCount && strcmp(key, candidate)) {
|
||||
candidate += strlen(candidate) + 1;
|
||||
++kind;
|
||||
}
|
||||
if (kind == KeyCount || (kind == Replies && !infrastructure_replies_)) return false;
|
||||
const bool base = kind == Base;
|
||||
const bool temporary = kind == Temporary;
|
||||
const bool scheduled = kind == Scheduled || kind == ScheduledTemporary;
|
||||
const bool scheduled_temp = kind == ScheduledTemporary;
|
||||
const bool crossing = kind == Crossing;
|
||||
const bool replies = kind == Replies;
|
||||
const bool status = kind == Status;
|
||||
const bool scan = kind == Scan;
|
||||
const bool timing = kind == Timing || kind == TimingAlias;
|
||||
if (!radio_ || !radio_->profiles()) { cli::formatError(reply, capacity, "radio profiles unsupported"); return true; }
|
||||
if (command == text && *args && verb == Get && (temporary || base)) verb = Set;
|
||||
if (remote_origin && !remote_command_
|
||||
@@ -802,6 +813,10 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
if (hasReplyMutation() && verb != Get) {
|
||||
cli::formatError(reply, capacity, "radio acknowledgement pending"); return true;
|
||||
}
|
||||
if ((timing || scan || status) && (verb != Get || *args)) {
|
||||
snprintf(reply, capacity, "Error: %s is read-only", timing ? "radio timing" : key);
|
||||
return true;
|
||||
}
|
||||
if (replies) {
|
||||
const auto* p = radio_->profiles();
|
||||
if (verb == Get && !*args) {
|
||||
@@ -836,7 +851,6 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
return true;
|
||||
}
|
||||
if (timing) {
|
||||
if (verb != Get || *args) { cli::formatError(reply, capacity, "radio timing is read-only"); return true; }
|
||||
const auto& p = *radio_->profiles();
|
||||
const auto t = p.chirpTiming();
|
||||
if (!p.enabled()) snprintf(reply, capacity, "> off");
|
||||
@@ -872,7 +886,6 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
return true;
|
||||
}
|
||||
if (scan) {
|
||||
if (verb != Get || *args) { cli::formatError(reply, capacity, "radio2.scan is read-only"); return true; }
|
||||
const auto& p = *radio_->profiles();
|
||||
if (!p.enabled()) snprintf(reply, capacity, "> off");
|
||||
else {
|
||||
@@ -886,7 +899,6 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
return true;
|
||||
}
|
||||
if (status) {
|
||||
if (verb != Get || *args) { cli::formatError(reply, capacity, "radio2.status is read-only"); return true; }
|
||||
if (hasReplyMutation()) {
|
||||
snprintf(reply, capacity, "> radio2 change pending %s; previous channel retained",
|
||||
remote_delivered_ ? "storage commit (retrying)" : "reply transmission");
|
||||
|
||||
+2
-1
@@ -128,7 +128,8 @@ static void rejects_bad_pages() {
|
||||
acl.load(&fs, self);
|
||||
add(acl, 0, 3);
|
||||
for (const char* command : {
|
||||
"get acl 0", "get acl -1", "get acl +1", "get acl 1x", "get acl 1 2",
|
||||
"get acl 0", "get acl 0000000000000000000000000000000000000000",
|
||||
"get acl -1", "get acl +1", "get acl 1x", "get acl 1 2", "get acl\n1",
|
||||
"get acl 1.0", "get acl page 1", "get acl 65536", "get acl 4294967296",
|
||||
"get acl 9999999999999999999999999999999999", "get acl 1\nsetperm key 3"}) {
|
||||
CHECK(query(acl, command) == "Err - usage: get acl [page]");
|
||||
|
||||
+2
-1
@@ -119,7 +119,8 @@ static void rejects_bad_pages() {
|
||||
acl.load(&fs, self);
|
||||
add(acl, 0, PERM_ACL_GUEST);
|
||||
for (const char* command : {
|
||||
"get guests 0", "get guests -1", "get guests +1", "get guests 1x",
|
||||
"get guests 0", "get guests 0000000000000000000000000000000000000000",
|
||||
"get guests -1", "get guests +1", "get guests 1x", "get guests\n1",
|
||||
"get guests 1 2", "get guests 1.0", "get guests page 1",
|
||||
"get guests 65536", "get guests 4294967296",
|
||||
"get guests 99999999999999999999999999", "get guests 1\nset guest off"}) {
|
||||
|
||||
+2
-1
@@ -144,7 +144,8 @@ static void strict_page_and_command_parsing() {
|
||||
LoginHistory history;
|
||||
record(history, 0, 0);
|
||||
const char* invalid[] = {
|
||||
"get logins 0", "get logins -1", "get logins +1", "get logins 1x",
|
||||
"get logins 0", "get logins 0000000000000000000000000000000000000000",
|
||||
"get logins -1", "get logins +1", "get logins 1x", "get logins\n1",
|
||||
"get logins 1 2", "get logins 1.0", "get logins page 1", "get logins 65536",
|
||||
"get logins 4294967296", "get logins 9999999999999999999999999999",
|
||||
"get logins 1\nset guest on",
|
||||
|
||||
+13
@@ -24,6 +24,19 @@ static void checkSafePreamble(unsigned sf, float bw, uint16_t symbols) {
|
||||
int main() {
|
||||
Profiles fresh;
|
||||
assert(fresh.cross==mesh::RadioCrossMode::Off && !fresh.canCross());
|
||||
// Listen durations round upward for the minimum, downward for a larger
|
||||
// fast visit, and saturate rejected previews before converting to integers.
|
||||
Profiles listen_bounds;
|
||||
listen_bounds.primary=params(5,1000);
|
||||
assert(listen_bounds.listenUs(0)==148);
|
||||
listen_bounds.secondary.params=params(5,500);
|
||||
listen_bounds.secondary.mode=mesh::RadioProfileMode::Rx;
|
||||
assert(listen_bounds.listenUs(0,1)==148);
|
||||
assert(listen_bounds.listenUs(0,32)==729);
|
||||
listen_bounds.secondary.params.bw=7.8f;
|
||||
assert(listen_bounds.listenUs(0,32)==46769);
|
||||
listen_bounds.secondary.params.bw=1e-30f;
|
||||
assert(listen_bounds.listenUs(0,1)==UINT32_MAX);
|
||||
// Sharing the read-only validation table must preserve every supported
|
||||
// value and the strict float tolerance on both sides of its boundary.
|
||||
for (float bw : {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f,
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Exercise both Ed25519 signing hash backends with the real Crypto verifier."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ED25519 = ROOT / "lib/ed25519"
|
||||
LENGTHS = (0, 1, 63, 64, 111, 112, 127, 128, 129, 239, 240, 255,
|
||||
256, 257, 1024, 4097, 16385)
|
||||
|
||||
HARNESS = r'''
|
||||
#include "ed_sha512.h"
|
||||
#include "ed_25519.h"
|
||||
#include <Ed25519.h>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#if defined(_WIN32)
|
||||
#include <RNG.h>
|
||||
// PE linkers resolve the library's unused key-generation references before
|
||||
// discarding them. Verification and seeded signing must never call this API.
|
||||
RNGClass::RNGClass() {}
|
||||
RNGClass::~RNGClass() {}
|
||||
void RNGClass::rand(uint8_t*, size_t) { std::abort(); }
|
||||
RNGClass RNG;
|
||||
#endif
|
||||
|
||||
static void printHex(const unsigned char* bytes, size_t length) {
|
||||
for (size_t i = 0; i < length; ++i) printf("%02x", bytes[i]);
|
||||
putchar('\n');
|
||||
}
|
||||
static const unsigned char* bytes(const std::vector<unsigned char>& data) {
|
||||
static const unsigned char empty = 0;
|
||||
return data.empty() ? &empty : data.data();
|
||||
}
|
||||
static std::vector<unsigned char> message(size_t length, unsigned salt) {
|
||||
std::vector<unsigned char> result(length);
|
||||
for (size_t i = 0; i < length; ++i) result[i] = (i * 73 + salt) & 255;
|
||||
return result;
|
||||
}
|
||||
static void decode(unsigned char* output, size_t length, const char* hex) {
|
||||
assert(strlen(hex) == length * 2);
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
unsigned value;
|
||||
assert(sscanf(hex + i * 2, "%2x", &value) == 1);
|
||||
output[i] = value;
|
||||
}
|
||||
}
|
||||
static void verifySignature(unsigned char* signature, const unsigned char* key,
|
||||
const unsigned char* data, size_t length) {
|
||||
// Retain the production C++ verification path; do not use the bundled
|
||||
// C verifier which Identity deliberately excludes.
|
||||
assert(Ed25519::verify(signature, key, data, length));
|
||||
for (size_t index : {size_t(0), size_t(17), size_t(32), size_t(63)}) {
|
||||
signature[index] ^= 1;
|
||||
assert(!Ed25519::verify(signature, key, data, length));
|
||||
signature[index] ^= 1;
|
||||
}
|
||||
std::vector<unsigned char> changed(data, data + length);
|
||||
if (changed.empty()) changed.push_back(1); else changed[0] ^= 1;
|
||||
assert(!Ed25519::verify(signature, key, bytes(changed), changed.size()));
|
||||
unsigned char wrong_key[32];
|
||||
memcpy(wrong_key, key, 32); wrong_key[0] ^= 1;
|
||||
assert(!Ed25519::verify(signature, wrong_key, data, length));
|
||||
}
|
||||
int main() {
|
||||
const size_t lengths[] = {@LENGTHS@};
|
||||
const size_t chunks[] = {1, 7, 63, 64, 111, 112, 127, 128, 129, 257, 1024};
|
||||
unsigned char result[64];
|
||||
assert(sha512_init(nullptr) == 1);
|
||||
assert(sha512_update(nullptr, result, 0) == 1);
|
||||
assert(sha512_final(nullptr, result) == 1);
|
||||
assert(sha512(nullptr, 0, result) == 1);
|
||||
assert(sha512(result, 0, nullptr) == 1);
|
||||
for (size_t length : lengths) {
|
||||
const auto first = message(length, 19), second = message(length + 137, 91);
|
||||
assert(sha512(bytes(first), first.size(), result) == 0);
|
||||
printHex(result, sizeof(result));
|
||||
sha512_context contexts[2];
|
||||
size_t positions[2] = {}, turn = 0;
|
||||
for (auto& context : contexts) {
|
||||
assert(sha512_init(&context) == 0);
|
||||
assert(sha512_update(&context, nullptr, 0) == 1);
|
||||
assert(sha512_final(&context, nullptr) == 1);
|
||||
assert(sha512_update(&context, result, 0) == 0);
|
||||
}
|
||||
while (positions[0] < first.size() || positions[1] < second.size()) {
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
const auto& data = i ? second : first;
|
||||
size_t count = chunks[turn++ % (sizeof(chunks) / sizeof(chunks[0]))];
|
||||
if (count > data.size() - positions[i]) count = data.size() - positions[i];
|
||||
assert(sha512_update(&contexts[i], bytes(data) + positions[i], count) == 0);
|
||||
positions[i] += count;
|
||||
}
|
||||
}
|
||||
for (auto& context : contexts) {
|
||||
assert(sha512_final(&context, result) == 0);
|
||||
printHex(result, sizeof(result));
|
||||
// Restart a completed context and reset an unfinished one. Both
|
||||
// must discard the old message without touching another context.
|
||||
assert(sha512_init(&context) == 0);
|
||||
assert(sha512_update(&context, bytes(second), second.size()) == 0);
|
||||
assert(sha512_init(&context) == 0);
|
||||
assert(sha512_update(&context, bytes(first), first.size()) == 0);
|
||||
assert(sha512_final(&context, result) == 0);
|
||||
unsigned char expected[64];
|
||||
assert(sha512(bytes(first), first.size(), expected) == 0);
|
||||
assert(!memcmp(result, expected, 64));
|
||||
}
|
||||
}
|
||||
struct Vector { const char* seed; const char* key; const char* data; const char* signature; };
|
||||
// RFC 8032 section 7.1, tests 1-3.
|
||||
// https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1
|
||||
const Vector vectors[] = {
|
||||
{"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||||
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "",
|
||||
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f"
|
||||
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"},
|
||||
{"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72",
|
||||
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085"
|
||||
"ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"},
|
||||
{"c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
|
||||
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", "af82",
|
||||
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac"
|
||||
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"}
|
||||
};
|
||||
for (const auto& vector : vectors) {
|
||||
unsigned char seed[32], key[32], expected_key[32], secret[64], signature[64], expected[64];
|
||||
std::vector<unsigned char> data(strlen(vector.data) / 2);
|
||||
decode(seed, 32, vector.seed); decode(expected_key, 32, vector.key);
|
||||
decode(expected, 64, vector.signature);
|
||||
if (!data.empty()) decode(data.data(), data.size(), vector.data);
|
||||
ed25519_create_keypair(key, secret, seed);
|
||||
assert(!memcmp(key, expected_key, 32));
|
||||
ed25519_sign(signature, bytes(data), data.size(), key, secret);
|
||||
assert(!memcmp(signature, expected, 64));
|
||||
verifySignature(signature, key, bytes(data), data.size());
|
||||
}
|
||||
// Cross-backend byte-for-byte comparison covers long, irregular messages
|
||||
// and existing expanded private-key storage, beyond the short RFC cases.
|
||||
for (size_t length : lengths) {
|
||||
unsigned char seed[32], key[32], secret[64], signature[64];
|
||||
const auto data = message(length, 37);
|
||||
for (unsigned i = 0; i < 32; ++i) seed[i] = (length + i * 17) & 255;
|
||||
ed25519_create_keypair(key, secret, seed);
|
||||
ed25519_sign(signature, bytes(data), data.size(), key, secret);
|
||||
verifySignature(signature, key, bytes(data), data.size());
|
||||
printHex(key, 32); printHex(secret, 64); printHex(signature, 64);
|
||||
}
|
||||
puts("Shared SHA512 and RFC8032 checks passed");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class SharedSHA512Test(unittest.TestCase):
|
||||
def test_c_api_hashes_and_signatures_match_original_and_rfc8032(self):
|
||||
compiler = shutil.which("gcc")
|
||||
cpp = shutil.which("g++")
|
||||
self.assertIsNotNone(compiler, "C compiler required")
|
||||
self.assertIsNotNone(cpp, "C++ compiler required")
|
||||
override = os.environ.get("MESHCORE_CRYPTO_DIR")
|
||||
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/Crypto"))
|
||||
names = ("SHA512.cpp", "Hash.cpp", "Crypto.cpp", "Ed25519.cpp", "Curve25519.cpp",
|
||||
"BigNumberUtil.cpp")
|
||||
crypto = next((path for path in candidates if all((path / name).is_file() for name in names)), None)
|
||||
self.assertIsNotNone(crypto, "Install rweather/Crypto or set MESHCORE_CRYPTO_DIR (no crypto mocks)")
|
||||
flags = ["-O1", "-g", "-Wall", "-Wextra", "-Werror", "-DHOST_BUILD=1",
|
||||
"-DED25519_NO_SEED=1", "-DED25519_SMALL_BASE=1",
|
||||
"-DED25519_COMPACT_SHA512=1", "-DED25519_COMPACT_SCALAR=1",
|
||||
"-DED25519_COMPACT_FIELD=1", "-ffunction-sections", "-fdata-sections",
|
||||
"-I" + str(ED25519), "-I" + str(crypto)]
|
||||
if os.name != "nt":
|
||||
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
with tempfile.TemporaryDirectory(prefix="ed25519-shared-sha512-") as directory:
|
||||
work = Path(directory)
|
||||
if os.name == "nt":
|
||||
# Crypto's HOST_BUILD expects the POSIX byte-order header.
|
||||
# MinGW supplies the same conversions as compiler intrinsics.
|
||||
endian = "#pragma once\n#include <stdint.h>\n"
|
||||
endian += "static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, \"host byte order\");\n"
|
||||
for bits in (16, 32, 64):
|
||||
for name in (f"htobe{bits}", f"be{bits}toh"):
|
||||
endian += f"static inline uint{bits}_t {name}(uint{bits}_t n) {{ return __builtin_bswap{bits}(n); }}\n"
|
||||
for name in (f"htole{bits}", f"le{bits}toh"):
|
||||
endian += f"static inline uint{bits}_t {name}(uint{bits}_t n) {{ return n; }}\n"
|
||||
(work / "endian.h").write_text(endian)
|
||||
flags += ["-I" + str(work)]
|
||||
harness = work / "harness.cpp"
|
||||
source = HARNESS.replace("@LENGTHS@", ",".join(map(str, LENGTHS)))
|
||||
harness.write_text(source)
|
||||
outputs = []
|
||||
for shared in (0, 1):
|
||||
with self.subTest(shared=shared):
|
||||
build_flags = flags + [f"-DED25519_USE_CRYPTO_SHA512={shared}"]
|
||||
objects = []
|
||||
for name in ("fe.c", "ge.c", "keypair.c", "sc.c", "sha512.c", "sign.c"):
|
||||
obj = work / (name + f"-{shared}.o")
|
||||
# Legacy ref10 carries shift negative signed values.
|
||||
# Keep ASan and all other UBSan checks on those files;
|
||||
# the new adapter and both hash implementations retain
|
||||
# the complete sanitizer set.
|
||||
legacy = ["-fno-sanitize=shift"] if os.name != "nt" and name in ("fe.c", "ge.c") else []
|
||||
built = subprocess.run([compiler, "-std=c99", *build_flags, *legacy, "-c", str(ED25519 / name),
|
||||
"-o", str(obj)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
objects.append(str(obj))
|
||||
binary = work / f"shared-{shared}"
|
||||
built = subprocess.run([cpp, "-std=c++11", *build_flags, str(harness),
|
||||
str(ED25519 / "ed_sha512_crypto.cpp"),
|
||||
*[str(crypto / name) for name in names], *objects,
|
||||
"-Wl,--gc-sections", "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=90)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
run = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
||||
lines = run.stdout.splitlines()
|
||||
expected = []
|
||||
for length in LENGTHS:
|
||||
first = bytes((i * 73 + 19) & 255 for i in range(length))
|
||||
second = bytes((i * 73 + 91) & 255 for i in range(length + 137))
|
||||
expected += [hashlib.sha512(first).hexdigest()] * 2
|
||||
expected += [hashlib.sha512(second).hexdigest()]
|
||||
self.assertEqual(lines[:len(expected)], expected)
|
||||
self.assertEqual(lines[-1], "Shared SHA512 and RFC8032 checks passed")
|
||||
outputs.append(lines)
|
||||
self.assertEqual(len(outputs), 2)
|
||||
self.assertEqual(outputs[0], outputs[1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -401,22 +401,40 @@ RECENT_TEST = r'''
|
||||
|
||||
class CooperativeOutputTest(unittest.TestCase):
|
||||
def role_methods(self, text, role):
|
||||
dump_start = text.index("void MyMesh::dumpLogFile()")
|
||||
signature = "void MyMesh::dumpLogFile()"
|
||||
dump = body(text, signature)
|
||||
dump_start = text.index(dump)
|
||||
guard = "#if MESH_ENABLE_PACKET_FILE_LOGGING\n"
|
||||
if text[:dump_start].endswith(guard):
|
||||
dump_start -= len(guard)
|
||||
dump_end = text.index("\n#if MESH_USB_CONSOLE_COOPERATIVE\nbool MyMesh::hasPendingSerialOutput", dump_start)
|
||||
methods = text[dump_start:dump_end] + "\n" + "\n".join(body(text, signature) for signature in (
|
||||
"bool MyMesh::hasPendingSerialOutput()", "void MyMesh::cancelPendingSerialOutput()",
|
||||
active_end = dump_start + len(dump)
|
||||
fallback = body(text[active_end:], signature)
|
||||
fallback_start = text.index(fallback, active_end)
|
||||
self.assertEqual(text[active_end:fallback_start].strip(), "#else")
|
||||
fallback_end = fallback_start + len(fallback)
|
||||
dump_end = text.index("\n#endif", fallback_end) + len("\n#endif")
|
||||
self.assertEqual(text[fallback_end:dump_end].strip(), "#endif")
|
||||
dump = text[dump_start - len(guard):dump_end]
|
||||
methods = dump + "\n" + "\n".join(body(text, signature) for signature in (
|
||||
"void MyMesh::printAclSerial()", "bool MyMesh::hasPendingSerialOutput()",
|
||||
"void MyMesh::cancelPendingSerialOutput()",
|
||||
"void MyMesh::servicePendingSerialOutput()"))
|
||||
if role == "simple_repeater":
|
||||
format_start = text.index("static void formatLocalSnrX4(")
|
||||
format_end = text.index("\nvoid MyMesh::formatRecentRepeatersReply", format_start)
|
||||
recent_start = text.index("void MyMesh::printRecentRepeatersSerial()")
|
||||
recent_end = text.index("\nbool MyMesh::setRecentRepeater(", recent_start)
|
||||
methods = text[format_start:format_end] + "\n" + text[recent_start:recent_end] + "\n" + methods
|
||||
methods = "\n".join(body(text, signature) for signature in (
|
||||
"static void formatLocalSnrX4(", "void MyMesh::printRecentRepeatersSerial()"
|
||||
)) + "\n" + methods
|
||||
return methods
|
||||
|
||||
def test_role_method_extraction_ignores_adjacent_definitions(self):
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role):
|
||||
text = (ROOT / f"examples/{role}/MyMesh.cpp").read_text()
|
||||
text = text.replace("void MyMesh::printAclSerial()",
|
||||
"bool MyMesh::unrelatedCommand() { return true; }\n\n"
|
||||
"void MyMesh::printAclSerial()", 1)
|
||||
methods = self.role_methods(text, role)
|
||||
self.assertNotIn("unrelatedCommand", methods)
|
||||
self.assertNotIn("handleGuestCommand", methods)
|
||||
|
||||
def test_usb_acl_commands_route_to_the_cooperative_pump(self):
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role):
|
||||
|
||||
Reference in new issue
Block a user