Fix firmware budget regressions and flood alias delimiters
Share nRF52 checked-record reads and command-family lookups without changing storage formats, error protection, fleet authorization, or delegated scopes. Use the existing bounded Merkle proof workspace for qualified ESP32 repeaters while retaining source capacity, wire format, and every enabled feature. Restore advert tab/CRLF delimiters and expose nested qualification build logs. Validation: all 1932 native CI test cases; exact canonical RAK4631 Full, Wio-E5 Mini, Station G3 Full MQTT, and Wireless Tracker Full wrappers; linked RAK crypto/workspace checks and retained Reader code equivalence; focused persistence, authorization, source-proof, and RAM policy regressions.
This commit is contained in:
1 parent
99724cd1d3
commit
db1c4704e0
15 files changed
+370
-130
No files matched your search
@@ -16,19 +16,28 @@ namespace mesh { namespace storage {
|
||||
|
||||
// Share the filesystem boundary between small settings records. Callers keep
|
||||
// their own format/version checks; no preference layout changes here.
|
||||
MESH_SETTINGS_FILE_SHARED inline bool readFixedSettingsFile(
|
||||
enum class FixedSettingsRead : uint8_t { Complete, WrongSize, Unreadable };
|
||||
|
||||
MESH_SETTINGS_FILE_SHARED inline FixedSettingsRead readFixedSettingsFileResult(
|
||||
FILESYSTEM* fs, const char* path, uint8_t* data, size_t size) {
|
||||
if (!fs) return false;
|
||||
if (!fs) return FixedSettingsRead::Unreadable;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
File file(*fs);
|
||||
if (!file.open(path, FILE_O_READ)) return false;
|
||||
if (!file.open(path, FILE_O_READ)) return FixedSettingsRead::Unreadable;
|
||||
#else
|
||||
File file = fs->open(path, "r");
|
||||
if (!file) return false;
|
||||
if (!file) return FixedSettingsRead::Unreadable;
|
||||
#endif
|
||||
const bool complete = file.size() == size && file.read(data, size) == size;
|
||||
const bool right_size = file.size() == size;
|
||||
const bool complete = right_size && file.read(data, size) == size;
|
||||
file.close();
|
||||
return complete;
|
||||
return !right_size ? FixedSettingsRead::WrongSize
|
||||
: complete ? FixedSettingsRead::Complete : FixedSettingsRead::Unreadable;
|
||||
}
|
||||
|
||||
inline bool readFixedSettingsFile(
|
||||
FILESYSTEM* fs, const char* path, uint8_t* data, size_t size) {
|
||||
return readFixedSettingsFileResult(fs, path, data, size) == FixedSettingsRead::Complete;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -230,6 +230,54 @@ bool rowKey(const char* key, size_t length, const char* family) {
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class CommandKind : uint8_t {
|
||||
Filter, Row, Advert, Schedule, SecondarySchedule, Radio, RadioStatus, Cross, Setting
|
||||
};
|
||||
|
||||
struct CommandFamily {
|
||||
const char* name;
|
||||
CommandKind kind;
|
||||
};
|
||||
|
||||
// Match the signed namespace once. Row families alone accept a numeric suffix;
|
||||
// sharing the lookup also keeps the sender's full authorization policy compact.
|
||||
const CommandFamily* commandFamily(const char* key, size_t length) {
|
||||
static const CommandFamily families[] = {
|
||||
{"flood.filter", CommandKind::Filter},
|
||||
{"flood.rule", CommandKind::Filter},
|
||||
{"fr", CommandKind::Filter},
|
||||
{"flood.filter.blacklist", CommandKind::Row},
|
||||
{"fb", CommandKind::Row},
|
||||
{"flood.moderation", CommandKind::Row},
|
||||
{"fm", CommandKind::Row},
|
||||
{"flood.channel.scope", CommandKind::Row},
|
||||
{"fs", CommandKind::Row},
|
||||
{"flood.channel.scope.require", CommandKind::Row},
|
||||
{"fsr", CommandKind::Row},
|
||||
{"flood.advert", CommandKind::Advert},
|
||||
{"fa", CommandKind::Advert},
|
||||
{"radioat", CommandKind::Schedule},
|
||||
{"tempradioat", CommandKind::Schedule},
|
||||
{"radioat2", CommandKind::SecondarySchedule},
|
||||
{"tempradioat2", CommandKind::SecondarySchedule},
|
||||
{"radio2", CommandKind::Radio},
|
||||
{"tempradio2", CommandKind::Radio},
|
||||
{"radio2.status", CommandKind::RadioStatus},
|
||||
{"radio2.timing", CommandKind::RadioStatus},
|
||||
{"radio2.cross", CommandKind::Cross},
|
||||
{"flood.channel.data", CommandKind::Setting},
|
||||
{"flood.channel.data.hops", CommandKind::Setting},
|
||||
{"flood.max", CommandKind::Setting},
|
||||
{"flood.max.unscoped", CommandKind::Setting},
|
||||
{"flood.max.advert", CommandKind::Setting},
|
||||
};
|
||||
for (const auto& family : families) {
|
||||
const bool row = family.kind == CommandKind::Filter || family.kind == CommandKind::Row;
|
||||
if (row ? rowKey(key, length, family.name) : keyEquals(key, length, family.name)) return &family;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Regex punctuation is data only in the two content-pattern fields. Keep the
|
||||
// existing management-command character policy for every other argument.
|
||||
bool filterPatternCharactersAllowed(const char* text) {
|
||||
@@ -520,12 +568,14 @@ bool FleetCommand::commandAllowed(const char* command) {
|
||||
const char* arguments = separator ? separator + 1 : "";
|
||||
if (!key_length || (separator && (!*arguments || *arguments == ' '))) return false;
|
||||
|
||||
const bool filter_rows = rowKey(key, key_length, "flood.filter")
|
||||
|| rowKey(key, key_length, "flood.rule") || rowKey(key, key_length, "fr");
|
||||
const CommandFamily* family = commandFamily(key, key_length);
|
||||
if (!family) return false;
|
||||
const CommandKind kind = family->kind;
|
||||
const bool filter_rows = kind == CommandKind::Filter;
|
||||
if (pattern_characters && (!set || !filter_rows
|
||||
|| !filterPatternCharactersAllowed(arguments))) return false;
|
||||
|
||||
if (keyEquals(key, key_length, "flood.advert") || keyEquals(key, key_length, "fa")) {
|
||||
if (kind == CommandKind::Advert) {
|
||||
if (get) {
|
||||
if (!*arguments) return true;
|
||||
const bool detail = !strncmp(arguments, "key ", 4);
|
||||
@@ -542,46 +592,25 @@ bool FleetCommand::commandAllowed(const char* command) {
|
||||
}
|
||||
if (clear) return false;
|
||||
|
||||
const bool scheduled_primary = keyEquals(key, key_length, "radioat")
|
||||
|| keyEquals(key, key_length, "tempradioat");
|
||||
const bool scheduled_secondary = keyEquals(key, key_length, "radioat2")
|
||||
|| keyEquals(key, key_length, "tempradioat2");
|
||||
if (scheduled_primary || scheduled_secondary) {
|
||||
const bool scheduled_secondary = kind == CommandKind::SecondarySchedule;
|
||||
if (kind == CommandKind::Schedule || scheduled_secondary) {
|
||||
return set ? scheduledRadioTuple(arguments, key[0] == 't', scheduled_secondary)
|
||||
: (get || del) && scheduleSelector(arguments, scheduled_secondary);
|
||||
}
|
||||
if (keyEquals(key, key_length, "radio2")
|
||||
|| keyEquals(key, key_length, "tempradio2")) {
|
||||
if (kind == CommandKind::Radio) {
|
||||
return get ? !*arguments : set && radioTuple(arguments, key_length == 10);
|
||||
}
|
||||
if (keyEquals(key, key_length, "radio2.status")
|
||||
|| keyEquals(key, key_length, "radio2.timing")) return get && !*arguments;
|
||||
if (keyEquals(key, key_length, "radio2.cross")) {
|
||||
if (kind == CommandKind::RadioStatus) return get && !*arguments;
|
||||
if (kind == CommandKind::Cross) {
|
||||
return get ? !*arguments : set && (!strcmp(arguments, "on")
|
||||
|| !strcmp(arguments, "off") || !strcmp(arguments, "auto"));
|
||||
}
|
||||
const bool rows = filter_rows
|
||||
|| rowKey(key, key_length, "flood.filter.blacklist")
|
||||
|| rowKey(key, key_length, "fb")
|
||||
|| rowKey(key, key_length, "flood.moderation")
|
||||
|| rowKey(key, key_length, "fm")
|
||||
|| rowKey(key, key_length, "flood.channel.scope")
|
||||
|| rowKey(key, key_length, "fs")
|
||||
|| rowKey(key, key_length, "flood.channel.scope.require")
|
||||
|| rowKey(key, key_length, "fsr");
|
||||
if (rows) return get ? (!*arguments || (filter_rows
|
||||
&& !keyEquals(key, key_length, "flood.filter")
|
||||
&& !keyEquals(key, key_length, "flood.rule")
|
||||
&& !keyEquals(key, key_length, "fr")
|
||||
if (filter_rows || kind == CommandKind::Row) return get ? (!*arguments || (filter_rows
|
||||
&& key_length > strlen(family->name)
|
||||
&& (!strcmp(arguments, "sender") || !strcmp(arguments, "n")
|
||||
|| !strcmp(arguments, "text") || !strcmp(arguments, "x"))))
|
||||
: set ? *arguments != 0 : del && (!*arguments || !strcmp(arguments, "all"));
|
||||
const bool settings = keyEquals(key, key_length, "flood.channel.data")
|
||||
|| keyEquals(key, key_length, "flood.channel.data.hops")
|
||||
|| keyEquals(key, key_length, "flood.max")
|
||||
|| keyEquals(key, key_length, "flood.max.unscoped")
|
||||
|| keyEquals(key, key_length, "flood.max.advert");
|
||||
return settings && (get ? !*arguments : set && *arguments);
|
||||
return kind == CommandKind::Setting && (get ? !*arguments : set && *arguments);
|
||||
}
|
||||
|
||||
size_t FleetCommand::encode(const LocalIdentity& publisher, const uint8_t* key,
|
||||
|
||||
@@ -6,32 +6,44 @@
|
||||
namespace mesh {
|
||||
namespace cli {
|
||||
|
||||
inline bool floodCommandBoundary(char c) {
|
||||
return c == 0 || c == ' ' || c == '.' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
// Return the untouched argument suffix. Aliases do not expand the caller's
|
||||
// bounded command buffer or change the bytes covered by a fleet signature.
|
||||
inline const char* floodCommandArguments(const char* command, const char* family) {
|
||||
if (!command || !family) return nullptr;
|
||||
const size_t length = strlen(family);
|
||||
if (strncmp(command, family, length) == 0
|
||||
&& (command[length] == 0 || command[length] == ' ' || command[length] == '.'))
|
||||
&& floodCommandBoundary(command[length]))
|
||||
return command + length;
|
||||
|
||||
const char* name = strchr(family, ' ');
|
||||
if (!name) return nullptr;
|
||||
const size_t verb_length = size_t(++name - family);
|
||||
if (strncmp(command, family, verb_length) != 0) return nullptr;
|
||||
const char* alias = nullptr;
|
||||
if (strcmp(name, "flood.rule") == 0) alias = "fr";
|
||||
else if (strcmp(name, "flood.moderation") == 0) alias = "fm";
|
||||
else if (strcmp(name, "flood.channel.scope") == 0) alias = "fs";
|
||||
else if (strcmp(name, "flood.channel.scope.require") == 0) alias = "fsr";
|
||||
else if (strcmp(name, "flood.filter.blacklist") == 0) alias = "fb";
|
||||
else if (strcmp(name, "flood.advert") == 0) alias = "fa";
|
||||
if (!alias) return nullptr;
|
||||
const size_t alias_length = strlen(alias);
|
||||
const char* key = command + verb_length;
|
||||
if (strncmp(key, alias, alias_length) != 0) return nullptr;
|
||||
const char* suffix = key + alias_length;
|
||||
return *suffix == 0 || *suffix == ' ' || *suffix == '.' ? suffix : nullptr;
|
||||
struct Alias {
|
||||
const char* name;
|
||||
const char* short_name;
|
||||
};
|
||||
static const Alias aliases[] = {
|
||||
{"flood.rule", "fr"},
|
||||
{"flood.moderation", "fm"},
|
||||
{"flood.channel.scope", "fs"},
|
||||
{"flood.channel.scope.require", "fsr"},
|
||||
{"flood.filter.blacklist", "fb"},
|
||||
{"flood.advert", "fa"},
|
||||
};
|
||||
for (const auto& alias : aliases) {
|
||||
if (strcmp(name, alias.name) != 0) continue;
|
||||
const size_t alias_length = strlen(alias.short_name);
|
||||
if (strncmp(key, alias.short_name, alias_length) != 0) return nullptr;
|
||||
const char* suffix = key + alias_length;
|
||||
return floodCommandBoundary(*suffix) ? suffix : nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "MorseSettings.h"
|
||||
#include "FilePresence.h"
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/CheckedSettingsFile.h>
|
||||
#elif defined(STM32_PLATFORM)
|
||||
#include "AtomicFileWriter.h"
|
||||
#elif defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
#include "ContactFileTransaction.h"
|
||||
@@ -22,19 +24,27 @@ ReadResult read(const char* path, MorseSettings& result) {
|
||||
bool present = false;
|
||||
if (!filePresence(settings_fs, path, present)) return ReadResult::Unreadable;
|
||||
if (!present) return ReadResult::Missing;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
uint8_t image[MorseSettingsImageSize];
|
||||
#if defined(NRF52_PLATFORM)
|
||||
const storage::FixedSettingsRead read = storage::readFixedSettingsFileResult(
|
||||
settings_fs, path, image, sizeof(image));
|
||||
if (read != storage::FixedSettingsRead::Complete)
|
||||
return read == storage::FixedSettingsRead::WrongSize
|
||||
? ReadResult::Invalid : ReadResult::Unreadable;
|
||||
#else
|
||||
#if defined(STM32_PLATFORM)
|
||||
File file(*settings_fs);
|
||||
if (!file.open(path, FILE_O_READ)) return ReadResult::Unreadable;
|
||||
#else
|
||||
File file = settings_fs->open(path, "r");
|
||||
if (!file) return ReadResult::Unreadable;
|
||||
#endif
|
||||
uint8_t image[MorseSettingsImageSize];
|
||||
const bool right_size = file.size() == sizeof(image);
|
||||
const bool complete = right_size && file.read(image, sizeof(image)) == int(sizeof(image));
|
||||
file.close();
|
||||
if (!right_size) return ReadResult::Invalid;
|
||||
if (!complete) return ReadResult::Unreadable;
|
||||
#endif
|
||||
return decodeMorseSettings(image, sizeof(image), result)
|
||||
? ReadResult::Valid : ReadResult::Invalid;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#if defined(ENABLE_OTA) && (defined(COMPANION_RADIO_FULL) || COMPANION_FEATURE_OTA_CLI)
|
||||
#include "CompanionOtaConfig.h"
|
||||
#include <helpers/PersistentStoreFormat.h>
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/CheckedSettingsFile.h>
|
||||
#include <helpers/FilePresence.h>
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <errno.h>
|
||||
@@ -21,7 +25,9 @@ const char* const Backup = "/ota_config.bak";
|
||||
// Some FS::exists implementations open the file and report an unreadable
|
||||
// existing image as absent. Metadata errors must not authorize replacement.
|
||||
bool pathPresence(const char* path, bool& present) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return filePresence(settings_fs, path, present);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
struct lfs_info info;
|
||||
settings_fs->_lockFS();
|
||||
const int result = lfs_stat(settings_fs->_getFS(), path, &info);
|
||||
@@ -77,9 +83,13 @@ ImageReadResult readImage(const char* path, uint8_t* image, OtaConfigState& stat
|
||||
if (!settings_fs || !pathPresence(path, present)) return ImageReadResult::Unreadable;
|
||||
if (!present) return ImageReadResult::Missing;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
File file(*settings_fs);
|
||||
if (!file.open(path, FILE_O_READ)) return ImageReadResult::Unreadable;
|
||||
#elif defined(STM32_PLATFORM)
|
||||
const storage::FixedSettingsRead read = storage::readFixedSettingsFileResult(
|
||||
settings_fs, path, image, ImageSize);
|
||||
if (read != storage::FixedSettingsRead::Complete)
|
||||
return read == storage::FixedSettingsRead::WrongSize
|
||||
? ImageReadResult::Invalid : ImageReadResult::Unreadable;
|
||||
#else
|
||||
#if defined(STM32_PLATFORM)
|
||||
File file = settings_fs->open(path, FILE_O_READ);
|
||||
#else
|
||||
File file = settings_fs->open(path, "r");
|
||||
@@ -90,6 +100,7 @@ ImageReadResult readImage(const char* path, uint8_t* image, OtaConfigState& stat
|
||||
file.close();
|
||||
if (!right_size) return ImageReadResult::Invalid;
|
||||
if (!complete) return ImageReadResult::Unreadable;
|
||||
#endif
|
||||
return decode(image, state) ? ImageReadResult::Valid : ImageReadResult::Invalid;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// Non-PSRAM Full Companions retain the complete source geometry without
|
||||
// retaining every Merkle leaf in internal RAM. An explicit 0 keeps the legacy
|
||||
// Non-PSRAM Full Companions and qualified ESP32 repeaters retain the complete source
|
||||
// geometry without caching every Merkle leaf. An explicit 0 keeps the legacy
|
||||
// cache for matched qualification builds.
|
||||
#ifndef OTA_BOUNDED_SOURCE_PROOFS
|
||||
#if defined(ESP32_PLATFORM) && defined(COMPANION_RADIO_FULL) && !defined(BOARD_HAS_PSRAM) && \
|
||||
#if defined(ESP32_PLATFORM) && \
|
||||
((defined(COMPANION_RADIO_FULL) && !defined(BOARD_HAS_PSRAM)) || \
|
||||
(defined(MESH_CLIENT_REPEATER_ONLY) && MESH_CLIENT_REPEATER_ONLY)) && \
|
||||
(!defined(OTA_PROOFGEN_SCRATCH) || \
|
||||
(OTA_PROOFGEN_SCRATCH >= 4 && OTA_PROOFGEN_SCRATCH <= 16384))
|
||||
#define OTA_BOUNDED_SOURCE_PROOFS 1
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#include "OtaSpeedConfig.h"
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/PersistentStoreFormat.h>
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <helpers/CheckedSettingsFile.h>
|
||||
#include <helpers/FilePresence.h>
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -26,7 +30,9 @@ enum class ImageReadResult : uint8_t { Missing, Valid, Invalid, Unreadable };
|
||||
// A failed open is not enough to prove a path is absent: some filesystems
|
||||
// report an I/O error as a failed exists() call. Keep those images protected.
|
||||
bool pathPresence(const char* path, bool& present) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return filePresence(settings_fs, path, present);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
struct lfs_info info;
|
||||
settings_fs->_lockFS();
|
||||
const int result = lfs_stat(settings_fs->_getFS(), path, &info);
|
||||
@@ -61,9 +67,13 @@ ImageReadResult readImage(const char* path, uint8_t* image) {
|
||||
if (!settings_fs || !pathPresence(path, present)) return ImageReadResult::Unreadable;
|
||||
if (!present) return ImageReadResult::Missing;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
File file(*settings_fs);
|
||||
if (!file.open(path, FILE_O_READ)) return ImageReadResult::Unreadable;
|
||||
#elif defined(STM32_PLATFORM)
|
||||
const storage::FixedSettingsRead read = storage::readFixedSettingsFileResult(
|
||||
settings_fs, path, image, ImageSize);
|
||||
if (read != storage::FixedSettingsRead::Complete)
|
||||
return read == storage::FixedSettingsRead::WrongSize
|
||||
? ImageReadResult::Invalid : ImageReadResult::Unreadable;
|
||||
#else
|
||||
#if defined(STM32_PLATFORM)
|
||||
File file = settings_fs->open(path, FILE_O_READ);
|
||||
#else
|
||||
File file = settings_fs->open(path, "r");
|
||||
@@ -74,6 +84,7 @@ ImageReadResult readImage(const char* path, uint8_t* image) {
|
||||
file.close();
|
||||
if (!right_size) return ImageReadResult::Invalid;
|
||||
if (!complete) return ImageReadResult::Unreadable;
|
||||
#endif
|
||||
float value;
|
||||
memcpy(&value, image + 4, sizeof(value));
|
||||
return !memcmp(image, "OS\1\0", 4) && validSpeed(value)
|
||||
|
||||
Reference in new issue
Block a user