Extend forwarding filters and save moderation rules atomically
This commit is contained in:
1 parent
585393e36c
commit
953ad6defb
31 files changed
+3453
-342
No files matched your search
@@ -230,6 +230,42 @@ bool rowKey(const char* key, size_t length, const char* family) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
while (*text) {
|
||||
while (*text == ' ') ++text;
|
||||
if (!*text) break;
|
||||
const char* key = text;
|
||||
while (*text && *text != '=' && *text != ' ') {
|
||||
if (*text == '|' || *text == '\\') return false;
|
||||
++text;
|
||||
}
|
||||
const size_t key_length = size_t(text - key);
|
||||
const bool pattern = keyEquals(key, key_length, "sender")
|
||||
|| keyEquals(key, key_length, "n") || keyEquals(key, key_length, "text")
|
||||
|| keyEquals(key, key_length, "x");
|
||||
if (*text == '=') ++text;
|
||||
const char quote = *text == '"' || *text == '\'' ? *text++ : 0;
|
||||
bool closed = quote == 0;
|
||||
while (*text) {
|
||||
if (quote ? *text == quote : *text == ' ') {
|
||||
if (quote) { ++text; closed = true; }
|
||||
break;
|
||||
}
|
||||
if (*text == '|' && !pattern) return false;
|
||||
if (*text == '\\') {
|
||||
if (!pattern || !text[1]) return false;
|
||||
text += 2;
|
||||
} else {
|
||||
++text;
|
||||
}
|
||||
}
|
||||
if (!closed || (*text && *text != ' ')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decimalToken(const char* text, size_t length) {
|
||||
if (!length) return false;
|
||||
bool dot = false, digit = false;
|
||||
@@ -458,11 +494,12 @@ bool FleetCommand::withinRadius(int32_t center_latitude_e6, int32_t center_longi
|
||||
bool FleetCommand::commandAllowed(const char* command) {
|
||||
if (!command) return false;
|
||||
size_t length = 0;
|
||||
bool pattern_characters = false;
|
||||
while (command[length]) {
|
||||
const unsigned char byte = command[length];
|
||||
if (++length > MaxCommandLength || byte < 0x20 || byte > 0x7e
|
||||
|| byte == ';' || byte == '|' || byte == '&' || byte == '`'
|
||||
|| byte == '\\') return false;
|
||||
|| byte == ';' || byte == '&' || byte == '`') return false;
|
||||
pattern_characters = pattern_characters || byte == '|' || byte == '\\';
|
||||
}
|
||||
if (!length || command[0] == ' ' || command[length - 1] == ' ') return false;
|
||||
if (!strcmp(command, "clock") || !strcmp(command, "clock sync")) return true;
|
||||
@@ -471,17 +508,40 @@ bool FleetCommand::commandAllowed(const char* command) {
|
||||
return boundedIntegerToken(command + 5, length - 5, UINT32_MAX, value)
|
||||
&& value >= MinEpoch;
|
||||
}
|
||||
bool get = false, set = false, del = false;
|
||||
bool get = false, set = false, del = false, clear = false;
|
||||
if (!strncmp(command, "get ", 4)) get = true;
|
||||
else if (!strncmp(command, "set ", 4)) set = true;
|
||||
else if (!strncmp(command, "del ", 4)) del = true;
|
||||
else if (!strncmp(command, "clear ", 6)) clear = true;
|
||||
else return false;
|
||||
const char* key = command + 4;
|
||||
const char* key = command + (clear ? 6 : 4);
|
||||
const char* separator = strchr(key, ' ');
|
||||
const size_t key_length = separator ? size_t(separator - key) : strlen(key);
|
||||
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");
|
||||
if (pattern_characters && (!set || !filter_rows
|
||||
|| !filterPatternCharactersAllowed(arguments))) return false;
|
||||
|
||||
if (keyEquals(key, key_length, "flood.advert") || keyEquals(key, key_length, "fa")) {
|
||||
if (get) {
|
||||
if (!*arguments) return true;
|
||||
const bool detail = !strncmp(arguments, "key ", 4);
|
||||
const char* number = arguments + (detail ? 4 : 0);
|
||||
uint32_t value;
|
||||
return *number >= '1' && *number <= '9'
|
||||
&& boundedIntegerToken(number, strlen(number), 65535, value);
|
||||
}
|
||||
if (!clear) return false;
|
||||
if (!strcmp(arguments, "all")) return true;
|
||||
if (strlen(arguments) != PUB_KEY_SIZE * 2) return false;
|
||||
for (size_t i = 0; arguments[i]; ++i) if (hexDigit(arguments[i]) < 0) return false;
|
||||
return true;
|
||||
}
|
||||
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")
|
||||
@@ -500,13 +560,21 @@ bool FleetCommand::commandAllowed(const char* command) {
|
||||
return get ? !*arguments : set && (!strcmp(arguments, "on")
|
||||
|| !strcmp(arguments, "off") || !strcmp(arguments, "auto"));
|
||||
}
|
||||
const bool rows = rowKey(key, key_length, "flood.filter")
|
||||
const bool rows = filter_rows
|
||||
|| rowKey(key, key_length, "flood.filter.blacklist")
|
||||
|| rowKey(key, key_length, "flood.rule")
|
||||
|| 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, "flood.channel.scope.require");
|
||||
if (rows) return get ? !*arguments
|
||||
|| 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")
|
||||
&& (!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")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "FloodAdvertLimiter.h"
|
||||
#include "FloodCommandAliases.h"
|
||||
#include <Utils.h>
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -17,10 +18,8 @@ inline const char* floodAdvertLimitReason(uint8_t reasons) {
|
||||
// Row numbers refer to the current filtered list, not permanent table slots.
|
||||
inline bool handleFloodAdvertGet(FloodAdvertLimiter* limiter, const char* command,
|
||||
char* reply, uint32_t now) {
|
||||
static const char prefix[] = "get flood.advert";
|
||||
const size_t prefix_len = sizeof(prefix) - 1;
|
||||
if (strncmp(command, prefix, prefix_len) != 0) return false;
|
||||
const char* arg = command + prefix_len;
|
||||
const char* arg = floodCommandArguments(command, "get flood.advert");
|
||||
if (!arg) return false;
|
||||
if (*arg && *arg != ' ' && *arg != '\t' && *arg != '\r' && *arg != '\n') return false;
|
||||
while (*arg == ' ' || *arg == '\t') ++arg;
|
||||
const bool detail = strncmp(arg, "key", 3) == 0 && (arg[3] == ' ' || arg[3] == '\t');
|
||||
@@ -81,10 +80,8 @@ inline bool handleFloodAdvertGet(FloodAdvertLimiter* limiter, const char* comman
|
||||
// Dispatch only after the role's ordinary CLI authorization checks. Requiring
|
||||
// an explicit "all" avoids treating missing/malformed keys as a global reset.
|
||||
inline bool handleFloodAdvertClear(FloodAdvertLimiter* limiter, const char* command, char* reply) {
|
||||
static const char prefix[] = "clear flood.advert";
|
||||
const size_t prefix_len = sizeof(prefix) - 1;
|
||||
if (strncmp(command, prefix, prefix_len) != 0) return false;
|
||||
const char* arg = command + prefix_len;
|
||||
const char* arg = floodCommandArguments(command, "clear flood.advert");
|
||||
if (!arg) return false;
|
||||
if (*arg && *arg != ' ' && *arg != '\t') return false;
|
||||
while (*arg == ' ' || *arg == '\t') ++arg;
|
||||
const char* end = arg;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace cli {
|
||||
|
||||
// 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] == '.'))
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace mesh
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <Packet.h>
|
||||
#include "FloodRuleExtensions.h"
|
||||
|
||||
namespace FloodFilterPolicy {
|
||||
|
||||
@@ -52,6 +53,14 @@ enum ChannelScopeGate {
|
||||
// Rules can be reordered/compacted in RAM without changing their identity.
|
||||
// Compare configured fields explicitly: padding and live rate-window state
|
||||
// must never decide whether an unnumbered setter reuses an existing slot.
|
||||
template<typename Entry>
|
||||
auto sameRuleExtras(const Entry& left, const Entry& right, int)
|
||||
-> decltype(left.extra, right.extra, bool()) {
|
||||
return FloodRuleExtensions::sameFields(left.extra, right.extra);
|
||||
}
|
||||
template<typename Entry>
|
||||
bool sameRuleExtras(const Entry&, const Entry&, long) { return true; }
|
||||
|
||||
template<typename Entry>
|
||||
bool sameRuleConfiguration(const Entry& left, const Entry& right) {
|
||||
return left.active == right.active
|
||||
@@ -81,7 +90,8 @@ bool sameRuleConfiguration(const Entry& left, const Entry& right) {
|
||||
&& left.priority == right.priority
|
||||
&& left.stop_on_match == right.stop_on_match
|
||||
&& left.retry_on_match == right.retry_on_match
|
||||
&& left.transport_modes == right.transport_modes;
|
||||
&& left.transport_modes == right.transport_modes
|
||||
&& sameRuleExtras(left, right, 0);
|
||||
}
|
||||
|
||||
inline bool channelKeyLengthSupported(uint8_t key_len) {
|
||||
@@ -184,10 +194,10 @@ inline bool parseRuleModes(const char* text, uint8_t& modes) {
|
||||
// These are separate admission phases. A transport rule cannot suppress
|
||||
// ordinary LoRa forwarding, nor can a radio stop rule bypass a transport deny.
|
||||
inline bool ruleModeMatches(uint8_t modes, uint8_t context,
|
||||
const mesh::Packet* packet) {
|
||||
const mesh::Packet* packet, uint8_t route = 0) {
|
||||
return packet && (modes == RULE_MODE_RADIO ? context == RULE_MODE_RADIO
|
||||
: (modes & context) != 0)
|
||||
&& (context != RULE_MODE_RADIO || packet->isRouteFlood());
|
||||
&& (route != 0 || context != RULE_MODE_RADIO || packet->isRouteFlood());
|
||||
}
|
||||
|
||||
inline bool transportActionsSupported(uint8_t modes, bool scope_action,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace FloodGroupPlainText {
|
||||
|
||||
struct Span {
|
||||
const uint8_t* data;
|
||||
size_t size;
|
||||
Span(const uint8_t* bytes = nullptr, size_t length = 0)
|
||||
: data(bytes), size(length) {}
|
||||
};
|
||||
|
||||
struct View {
|
||||
uint32_t timestamp = 0;
|
||||
Span sender;
|
||||
Span text;
|
||||
bool has_sender = false;
|
||||
};
|
||||
|
||||
// Views borrow the decrypted buffer. Never truncate fields before matching:
|
||||
// a long display name still needs to match wildcard or content rules.
|
||||
inline bool parse(const uint8_t* data, size_t size, uint8_t plain_type,
|
||||
View& view) {
|
||||
view = View{};
|
||||
if (!data || size <= 5 || (data[4] >> 2) != plain_type) return false;
|
||||
memcpy(&view.timestamp, data, sizeof(view.timestamp));
|
||||
view.text = {data + 5, size - 5};
|
||||
// MACThenDecrypt returns complete AES blocks, including zero padding.
|
||||
const uint8_t* end = static_cast<const uint8_t*>(
|
||||
memchr(view.text.data, 0, view.text.size));
|
||||
if (!end) end = data + size;
|
||||
view.text.size = size_t(end - view.text.data);
|
||||
const uint8_t* colon = static_cast<const uint8_t*>(
|
||||
memchr(view.text.data, ':', view.text.size));
|
||||
if (!colon) return true;
|
||||
|
||||
size_t sender_size = size_t(colon - view.text.data);
|
||||
while (sender_size && view.text.data[sender_size - 1] == ' ') --sender_size;
|
||||
bool valid_sender = sender_size != 0;
|
||||
for (size_t i = 0; i < sender_size; ++i) {
|
||||
if (view.text.data[i] < 0x20) valid_sender = false;
|
||||
}
|
||||
if (valid_sender) {
|
||||
view.sender = {view.text.data, sender_size};
|
||||
view.has_sender = true;
|
||||
}
|
||||
const uint8_t* body = colon + 1;
|
||||
if (body < end && *body == ' ') ++body;
|
||||
view.text = {body, size_t(end - body)};
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool senderEquals(const View& view, const char* name) {
|
||||
if (!view.has_sender || !name) return false;
|
||||
const size_t size = strlen(name);
|
||||
if (size != view.sender.size) return false;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
uint8_t left = view.sender.data[i];
|
||||
uint8_t right = uint8_t(name[i]);
|
||||
if (left >= 'A' && left <= 'Z') left += 'a' - 'A';
|
||||
if (right >= 'A' && right <= 'Z') right += 'a' - 'A';
|
||||
if (left != right) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} } // namespace mesh::FloodGroupPlainText
|
||||
@@ -66,6 +66,27 @@ public:
|
||||
|
||||
// Generalized repeater and room-server rows have the same named fields.
|
||||
// Defaults are omitted only when the setter reconstructs exactly that value.
|
||||
template<typename Entry>
|
||||
auto appendPath(CommandWriter& text, const Entry& entry, int)
|
||||
-> decltype(entry.extra, void()) {
|
||||
FloodRuleExtensions::appendPath(text, entry.extra, entry.path_hash_size,
|
||||
entry.path_hops, entry.path, true);
|
||||
}
|
||||
template<typename Entry>
|
||||
void appendPath(CommandWriter& text, const Entry& entry, long) {
|
||||
text.append(" p=");
|
||||
for (unsigned hop = 0; hop < entry.path_hops; ++hop) {
|
||||
if (hop) text.append(",");
|
||||
for (unsigned byte = 0; byte < entry.path_hash_size; ++byte)
|
||||
text.append("%02X", entry.path[hop * entry.path_hash_size + byte]);
|
||||
}
|
||||
}
|
||||
template<typename Entry>
|
||||
auto appendExtras(CommandWriter& text, const Entry& entry, int)
|
||||
-> decltype(entry.extra, void()) { FloodRuleExtensions::appendFields(text, entry.extra, true); }
|
||||
template<typename Entry>
|
||||
void appendExtras(CommandWriter&, const Entry&, long) {}
|
||||
|
||||
template<typename Entry>
|
||||
bool formatCompact(char* out, size_t capacity, unsigned slot, const Entry& entry) {
|
||||
using namespace FloodFilterPolicy;
|
||||
@@ -84,14 +105,8 @@ bool formatCompact(char* out, size_t capacity, unsigned slot, const Entry& entry
|
||||
if (entry.path_hops == 0 && entry.path_hash_size != 0)
|
||||
text.append(" pb=%s", pathHashBytesName(entry.path_hash_size));
|
||||
if (entry.match_blacklisted_path) text.append(" p=bl");
|
||||
else if (entry.path_hops) {
|
||||
text.append(" p=");
|
||||
for (unsigned hop = 0; hop < entry.path_hops; hop++) {
|
||||
if (hop) text.append(",");
|
||||
for (unsigned byte = 0; byte < entry.path_hash_size; byte++)
|
||||
text.append("%02X", entry.path[hop * entry.path_hash_size + byte]);
|
||||
}
|
||||
}
|
||||
else if (entry.path_hops) appendPath(text, entry, 0);
|
||||
appendExtras(text, entry, 0);
|
||||
switch (entry.incoming_scope_kind) {
|
||||
case RULE_IN_NONE: text.append(" i=n"); break;
|
||||
case RULE_IN_SCOPED: text.append(" i=s"); break;
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <Packet.h>
|
||||
#include "TxtDataHelpers.h"
|
||||
#include "FloodTextPattern.h"
|
||||
#include "FloodGroupPlainText.h"
|
||||
|
||||
namespace mesh { class Utils; }
|
||||
|
||||
namespace FloodRuleExtensions {
|
||||
|
||||
enum Route : uint8_t { LegacyRoute, FloodRoute, DirectRoute, AnyRoute };
|
||||
enum PathPosition : uint8_t { Prefix, Anywhere, Suffix, Exact };
|
||||
|
||||
// Zero initialization retains every FPF7 default. Keep the old path's first
|
||||
// nine bytes in its established field; only a fourth 3-byte ID needs a tail.
|
||||
struct Fields {
|
||||
uint16_t min_payload;
|
||||
uint16_t max_payload;
|
||||
uint8_t route;
|
||||
uint8_t probability;
|
||||
uint8_t path_position;
|
||||
uint8_t disabled;
|
||||
uint8_t payload_set;
|
||||
uint8_t path_tail[3];
|
||||
char sender[64];
|
||||
char text[64];
|
||||
};
|
||||
static_assert(sizeof(Fields) <= 140, "Update the extended filter RAM budget");
|
||||
|
||||
inline bool hasFields(const Fields& fields) {
|
||||
return fields.min_payload || fields.max_payload || fields.route
|
||||
|| fields.probability || fields.path_position || fields.disabled || fields.payload_set
|
||||
|| fields.path_tail[0] || fields.path_tail[1] || fields.path_tail[2]
|
||||
|| fields.sender[0] || fields.text[0];
|
||||
}
|
||||
|
||||
inline bool sameFields(const Fields& left, const Fields& right) {
|
||||
return left.min_payload == right.min_payload && left.max_payload == right.max_payload
|
||||
&& left.route == right.route && left.probability == right.probability
|
||||
&& left.path_position == right.path_position && left.disabled == right.disabled
|
||||
&& left.payload_set == right.payload_set
|
||||
&& !memcmp(left.path_tail, right.path_tail, sizeof(left.path_tail))
|
||||
&& !strcmp(left.sender, right.sender) && !strcmp(left.text, right.text);
|
||||
}
|
||||
|
||||
template<typename Entry>
|
||||
inline bool tableUsesExtensions(const Entry* entries, unsigned count) {
|
||||
for (unsigned i = 0; i < count; ++i)
|
||||
if (entries[i].active && (hasFields(entries[i].extra) || entries[i].path_hops > 3)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool contentRequested(const Fields& fields) {
|
||||
return fields.sender[0] || fields.text[0];
|
||||
}
|
||||
|
||||
inline bool validFields(const Fields& fields) {
|
||||
return fields.route <= AnyRoute && fields.probability <= 100
|
||||
&& fields.path_position <= Exact && fields.disabled <= 1 && fields.payload_set <= 1
|
||||
&& fields.min_payload <= MAX_PACKET_PAYLOAD
|
||||
&& fields.max_payload <= MAX_PACKET_PAYLOAD
|
||||
&& (!fields.payload_set || fields.min_payload <= fields.max_payload)
|
||||
&& memchr(fields.sender, 0, sizeof(fields.sender))
|
||||
&& memchr(fields.text, 0, sizeof(fields.text))
|
||||
&& mesh::flood::textPatternValid(fields.sender, strlen(fields.sender))
|
||||
&& mesh::flood::textPatternValid(fields.text, strlen(fields.text));
|
||||
}
|
||||
|
||||
template<typename Entry>
|
||||
inline bool validRuleFields(const Entry& entry) {
|
||||
if (!validFields(entry.extra)) return false;
|
||||
const bool path_valid = entry.path_hops == 0
|
||||
? entry.path_hash_size <= 3 || entry.path_hash_size == 0x82
|
||||
: entry.path_hash_size >= 1 && entry.path_hash_size <= 3
|
||||
&& entry.path_hops <= 4 && !entry.match_blacklisted_path;
|
||||
const bool content_valid = !contentRequested(entry.extra)
|
||||
|| ((entry.channel_key_len == 16 || entry.channel_key_len == 32)
|
||||
&& (entry.payload_type == 0xFF || entry.payload_type == PAYLOAD_TYPE_GRP_TXT));
|
||||
const bool flood_actions = entry.scope_name[0] || entry.target_region_name[0]
|
||||
|| entry.retry_on_match || entry.scope_uses_slow_timing;
|
||||
return path_valid && content_valid && (!flood_actions
|
||||
|| entry.extra.route == LegacyRoute || entry.extra.route == FloodRoute);
|
||||
}
|
||||
|
||||
// Strip grouping quotes, preserving regex escapes and all quoted whitespace.
|
||||
// Token pointers borrow this buffer; malformed input never reaches a setter.
|
||||
inline int tokenize(char* buffer, char* tokens[], unsigned capacity) {
|
||||
char* read = buffer;
|
||||
char* write = buffer;
|
||||
unsigned count = 0;
|
||||
while (*read) {
|
||||
while (*read == ' ' || *read == '\t') ++read;
|
||||
if (!*read) break;
|
||||
if (count == capacity) return -1;
|
||||
tokens[count++] = write;
|
||||
char quote = 0;
|
||||
while (*read) {
|
||||
char c = *read++;
|
||||
if (c == '\\' && *read) {
|
||||
if (quote && (*read == quote || *read == '\\')) *write++ = *read++;
|
||||
else { *write++ = c; *write++ = *read++; }
|
||||
} else if (quote && c == quote) {
|
||||
quote = 0;
|
||||
} else if (!quote && (c == '"' || c == '\'')) {
|
||||
quote = c;
|
||||
} else if (!quote && (c == ' ' || c == '\t')) {
|
||||
break;
|
||||
} else {
|
||||
*write++ = c;
|
||||
}
|
||||
}
|
||||
if (quote) return -1;
|
||||
*write++ = 0;
|
||||
}
|
||||
return int(count);
|
||||
}
|
||||
|
||||
inline bool unsignedValue(const char* text, uint16_t maximum, uint16_t& value) {
|
||||
if (!text || !*text) return false;
|
||||
uint32_t parsed = 0;
|
||||
do {
|
||||
if (*text < '0' || *text > '9') return false;
|
||||
parsed = parsed * 10U + uint8_t(*text++ - '0');
|
||||
if (parsed > maximum) return false;
|
||||
} while (*text);
|
||||
value = uint16_t(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool enabledValue(const char* value, uint8_t& disabled) {
|
||||
if (!strcmp(value, "on") || !strcmp(value, "1")) disabled = 0;
|
||||
else if (!strcmp(value, "off") || !strcmp(value, "0")) disabled = 1;
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return 0 for an existing-parser token, 1 when handled, -1 on invalid input.
|
||||
inline int parseToken(const char* token, Fields& fields, uint8_t& seen, char* reply) {
|
||||
const char* value = strchr(token, '=');
|
||||
if (!value) return 0;
|
||||
const size_t length = size_t(value++ - token);
|
||||
unsigned bit;
|
||||
if ((length == 6 && !strncmp(token, "sender", 6)) || (length == 1 && *token == 'n')) bit = 0;
|
||||
else if ((length == 4 && !strncmp(token, "text", 4)) || (length == 1 && *token == 'x')) bit = 1;
|
||||
else if ((length == 3 && !strncmp(token, "len", 3)) || (length == 1 && *token == 'l')) bit = 2;
|
||||
else if ((length == 5 && !strncmp(token, "route", 5)) || (length == 2 && !strncmp(token, "rt", 2))) bit = 3;
|
||||
else if ((length == 4 && !strncmp(token, "prob", 4)) || (length == 2 && !strncmp(token, "pr", 2))) bit = 4;
|
||||
else if ((length == 7 && !strncmp(token, "enabled", 7)) || (length == 1 && *token == 'e')) bit = 5;
|
||||
else return 0;
|
||||
if (seen & (1U << bit)) {
|
||||
strcpy(reply, "Err - duplicate rule parameter");
|
||||
return -1;
|
||||
}
|
||||
seen |= uint8_t(1U << bit);
|
||||
bool valid = true;
|
||||
if (bit <= 1) {
|
||||
valid = *value && strlen(value) < sizeof(fields.sender)
|
||||
&& mesh::flood::textPatternValid(value, strlen(value));
|
||||
if (valid) strcpy(bit == 0 ? fields.sender : fields.text, value);
|
||||
else strcpy(reply, "Err - invalid pattern (maximum 63 bytes)");
|
||||
} else if (bit == 2) {
|
||||
char range[20];
|
||||
valid = strlen(value) < sizeof(range);
|
||||
if (valid) {
|
||||
strcpy(range, value);
|
||||
char* separator = strchr(range, '-');
|
||||
char* plus = strchr(range, '+');
|
||||
if (separator) *separator++ = 0;
|
||||
if (plus) *plus = 0;
|
||||
valid = unsignedValue(range, MAX_PACKET_PAYLOAD, fields.min_payload);
|
||||
if (valid && separator) valid = !plus
|
||||
&& unsignedValue(separator, MAX_PACKET_PAYLOAD, fields.max_payload)
|
||||
&& fields.min_payload <= fields.max_payload;
|
||||
else if (valid) {
|
||||
valid = !plus || plus[1] == 0;
|
||||
fields.max_payload = plus ? MAX_PACKET_PAYLOAD : fields.min_payload;
|
||||
}
|
||||
}
|
||||
if (valid) fields.payload_set = 1;
|
||||
if (!valid) strcpy(reply, "Err - len is N, N+, or N-M payload bytes");
|
||||
} else if (bit == 3) {
|
||||
if (!strcmp(value, "flood") || !strcmp(value, "f")) fields.route = FloodRoute;
|
||||
else if (!strcmp(value, "direct") || !strcmp(value, "d")) fields.route = DirectRoute;
|
||||
else if (!strcmp(value, "any") || !strcmp(value, "a")) fields.route = AnyRoute;
|
||||
else { valid = false; strcpy(reply, "Err - route is flood, direct, or any"); }
|
||||
} else if (bit == 4) {
|
||||
uint16_t probability;
|
||||
valid = unsignedValue(value, 100, probability) && probability != 0;
|
||||
if (valid) fields.probability = probability == 100 ? 0 : uint8_t(probability);
|
||||
else strcpy(reply, "Err - prob must be 1-100");
|
||||
} else {
|
||||
valid = enabledValue(value, fields.disabled);
|
||||
if (!valid) strcpy(reply, "Err - enabled is on or off");
|
||||
}
|
||||
return valid ? 1 : -1;
|
||||
}
|
||||
|
||||
inline bool routeMatches(const Fields& fields, const mesh::Packet* packet, uint8_t context) {
|
||||
if (!packet) return false;
|
||||
if (fields.route == LegacyRoute) return context != 0 || packet->isRouteFlood();
|
||||
return fields.route == AnyRoute || (fields.route == FloodRoute ? packet->isRouteFlood() : packet->isRouteDirect());
|
||||
}
|
||||
|
||||
inline bool matchesEnvelope(const Fields& fields, const mesh::Packet* packet, uint8_t context) {
|
||||
return packet && !fields.disabled && routeMatches(fields, packet, context)
|
||||
&& (!fields.payload_set || (packet->payload_len >= fields.min_payload
|
||||
&& packet->payload_len <= fields.max_payload));
|
||||
}
|
||||
|
||||
inline bool matchesProbability(const Fields& fields, uint32_t roll) {
|
||||
return fields.probability == 0 || fields.probability == 100
|
||||
|| roll % 100U < fields.probability;
|
||||
}
|
||||
|
||||
// A received packet can be evaluated during both timing and forwarding.
|
||||
// Use one boot-randomized decision per payload/rule/admission phase, stable
|
||||
// across path growth and scope rewrites. Repeated evaluation cannot reroll it.
|
||||
template <typename PacketType>
|
||||
inline uint32_t probabilityRoll(const PacketType* packet, uint32_t seed,
|
||||
unsigned slot, uint8_t context) {
|
||||
uint32_t hash = 2166136261UL;
|
||||
const auto add = [&hash](uint8_t byte) { hash = (hash ^ byte) * 16777619UL; };
|
||||
for (unsigned i = 0; i < 4; ++i) add(uint8_t(seed >> (8 * i)));
|
||||
add(uint8_t(slot)); add(context);
|
||||
if (packet) {
|
||||
add(packet->getPayloadType());
|
||||
add(uint8_t(packet->payload_len)); add(uint8_t(packet->payload_len >> 8));
|
||||
const unsigned length = packet->payload_len <= MAX_PACKET_PAYLOAD
|
||||
? packet->payload_len : MAX_PACKET_PAYLOAD;
|
||||
for (unsigned i = 0; i < length; ++i) add(packet->payload[i]);
|
||||
}
|
||||
hash ^= hash >> 16;
|
||||
hash *= 0x85ebca6bUL;
|
||||
hash ^= hash >> 13;
|
||||
hash *= 0xc2b2ae35UL;
|
||||
return hash ^ (hash >> 16);
|
||||
}
|
||||
|
||||
inline int hexValue(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline bool parsePath(const char* value, uint8_t& width, uint8_t& hops,
|
||||
uint8_t first[9], Fields& fields, bool prefix_legacy) {
|
||||
memset(first, 0, 9);
|
||||
memset(fields.path_tail, 0, sizeof(fields.path_tail));
|
||||
width = hops = 0;
|
||||
fields.path_position = prefix_legacy ? Prefix : Anywhere;
|
||||
if (!strcmp(value, "*")) { fields.path_position = Prefix; return true; }
|
||||
bool start = *value == '^';
|
||||
if (start) ++value;
|
||||
const size_t size = strlen(value);
|
||||
bool end = size && value[size - 1] == '$';
|
||||
const char* finish = value + size - (end ? 1 : 0);
|
||||
if (start || end) fields.path_position = start ? (end ? Exact : Prefix) : Suffix;
|
||||
uint8_t bytes[12] = {};
|
||||
while (value < finish) {
|
||||
const char* next = value;
|
||||
while (next < finish && *next != '>' && *next != ',') ++next;
|
||||
const size_t digits = size_t(next - value);
|
||||
if ((digits != 2 && digits != 4 && digits != 6) || hops >= 4
|
||||
|| (width && digits != size_t(width) * 2)) return false;
|
||||
width = uint8_t(digits / 2);
|
||||
for (unsigned i = 0; i < width; ++i) {
|
||||
const int high = hexValue(value[i * 2]), low = hexValue(value[i * 2 + 1]);
|
||||
if (high < 0 || low < 0) return false;
|
||||
bytes[hops * width + i] = uint8_t((high << 4) | low);
|
||||
}
|
||||
++hops;
|
||||
if (next == finish) break;
|
||||
value = next + 1;
|
||||
if (value == finish) return false;
|
||||
}
|
||||
if (!hops) return false;
|
||||
memcpy(first, bytes, 9);
|
||||
memcpy(fields.path_tail, bytes + 9, 3);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool pathMatches(const Fields& fields, const mesh::Packet* packet,
|
||||
uint8_t width, uint8_t hops, const uint8_t first[9]) {
|
||||
if (!hops) return true; // Existing hash-width-only condition is checked separately.
|
||||
if (!packet || !packet->isRouteFlood() || packet->getPayloadType() == 9
|
||||
|| width < 1 || width > 3 || width != packet->getPathHashSize()
|
||||
|| hops > 4 || packet->getPathHashCount() < hops
|
||||
|| unsigned(packet->getPathHashCount()) * width > MAX_PATH_SIZE) return false;
|
||||
const unsigned count = packet->getPathHashCount();
|
||||
if (fields.path_position == Exact && count != hops) return false;
|
||||
uint8_t wanted[12];
|
||||
memcpy(wanted, first, 9);
|
||||
memcpy(wanted + 9, fields.path_tail, 3);
|
||||
unsigned begin = fields.path_position == Suffix ? count - hops : 0;
|
||||
unsigned last = fields.path_position == Anywhere ? count - hops : begin;
|
||||
for (unsigned at = begin; at <= last; ++at)
|
||||
if (!memcmp(packet->path + at * width, wanted, hops * width)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename PacketType, typename Crypto = mesh::Utils>
|
||||
inline bool matchesContent(const Fields& fields, const PacketType* packet,
|
||||
const uint8_t* secret, uint8_t key_len,
|
||||
uint32_t* work = nullptr, bool conservative = false,
|
||||
bool* budget_exceeded = nullptr) {
|
||||
if (budget_exceeded) *budget_exceeded = false;
|
||||
if (!contentRequested(fields)) return true;
|
||||
if (!packet || !secret || (key_len != 16 && key_len != 32)
|
||||
|| packet->getPayloadType() != PAYLOAD_TYPE_GRP_TXT
|
||||
|| packet->payload_len <= PATH_HASH_SIZE + CIPHER_MAC_SIZE) return false;
|
||||
uint8_t plain[MAX_PACKET_PAYLOAD];
|
||||
int size = Crypto::MACThenDecrypt(secret, plain,
|
||||
packet->payload + PATH_HASH_SIZE, packet->payload_len - PATH_HASH_SIZE);
|
||||
mesh::FloodGroupPlainText::View view;
|
||||
if (size <= 0 || !mesh::FloodGroupPlainText::parse(
|
||||
plain, size_t(size), TXT_TYPE_PLAIN, view)) return false;
|
||||
using mesh::flood::PatternMatchResult;
|
||||
if (fields.sender[0]) {
|
||||
if (!view.has_sender) return false;
|
||||
const auto result = mesh::flood::matchTextPattern(fields.sender, strlen(fields.sender),
|
||||
view.sender.data, view.sender.size, false, work);
|
||||
if (result == PatternMatchResult::BudgetExceeded) {
|
||||
if (budget_exceeded) *budget_exceeded = true;
|
||||
return conservative;
|
||||
}
|
||||
if (result != PatternMatchResult::Match) return false;
|
||||
}
|
||||
if (!fields.text[0]) return true;
|
||||
const auto result = mesh::flood::matchTextPattern(fields.text, strlen(fields.text),
|
||||
view.text.data, view.text.size, false, work);
|
||||
if (result == PatternMatchResult::BudgetExceeded && budget_exceeded)
|
||||
*budget_exceeded = true;
|
||||
return result == PatternMatchResult::Match
|
||||
|| (result == PatternMatchResult::BudgetExceeded && conservative);
|
||||
}
|
||||
|
||||
template <typename File>
|
||||
bool readFields(File& file, Fields& fields) {
|
||||
memset(&fields, 0, sizeof(fields));
|
||||
auto read = [&file](void* out, size_t size) {
|
||||
return file.read(static_cast<uint8_t*>(out), size) == size;
|
||||
};
|
||||
uint8_t sender_size = 0, text_size = 0;
|
||||
return read(&fields.min_payload, 2) && read(&fields.max_payload, 2)
|
||||
&& read(&fields.route, 1) && read(&fields.probability, 1)
|
||||
&& read(&fields.path_position, 1) && read(&fields.disabled, 1)
|
||||
&& read(&fields.payload_set, 1)
|
||||
&& read(fields.path_tail, 3)
|
||||
&& read(&sender_size, 1) && sender_size < sizeof(fields.sender)
|
||||
&& read(fields.sender, sender_size)
|
||||
&& read(&text_size, 1) && text_size < sizeof(fields.text)
|
||||
&& read(fields.text, text_size) && validFields(fields);
|
||||
}
|
||||
|
||||
template <typename Writer>
|
||||
bool writeFields(Writer& write, const Fields& fields) {
|
||||
uint8_t sender_size = uint8_t(strlen(fields.sender));
|
||||
uint8_t text_size = uint8_t(strlen(fields.text));
|
||||
return write(&fields.min_payload, 2) && write(&fields.max_payload, 2)
|
||||
&& write(&fields.route, 1) && write(&fields.probability, 1)
|
||||
&& write(&fields.path_position, 1) && write(&fields.disabled, 1)
|
||||
&& write(&fields.payload_set, 1)
|
||||
&& write(fields.path_tail, 3)
|
||||
&& write(&sender_size, 1) && write(fields.sender, sender_size)
|
||||
&& write(&text_size, 1) && write(fields.text, text_size);
|
||||
}
|
||||
|
||||
template <typename Writer>
|
||||
void appendQuoted(Writer& writer, const char* key, const char* value) {
|
||||
writer.append(" %s=\"", key);
|
||||
for (const char* p = value; *p; ++p) {
|
||||
if (*p == '"' || *p == '\\') writer.append("\\");
|
||||
writer.append("%c", *p);
|
||||
}
|
||||
writer.append("\"");
|
||||
}
|
||||
|
||||
template <typename Writer>
|
||||
void appendFields(Writer& writer, const Fields& fields, bool compact) {
|
||||
if (fields.sender[0]) appendQuoted(writer, compact ? "n" : "sender", fields.sender);
|
||||
if (fields.text[0]) appendQuoted(writer, compact ? "x" : "text", fields.text);
|
||||
if (fields.payload_set) {
|
||||
const char* key = compact ? "l" : "len";
|
||||
if (fields.max_payload == fields.min_payload) writer.append(" %s=%u", key, unsigned(fields.min_payload));
|
||||
else if (fields.max_payload == MAX_PACKET_PAYLOAD) writer.append(" %s=%u+", key, unsigned(fields.min_payload));
|
||||
else writer.append(" %s=%u-%u", key, unsigned(fields.min_payload), unsigned(fields.max_payload));
|
||||
}
|
||||
if (fields.route) writer.append(" %s=%s", compact ? "rt" : "route",
|
||||
fields.route == FloodRoute ? (compact ? "f" : "flood")
|
||||
: fields.route == DirectRoute ? (compact ? "d" : "direct") : (compact ? "a" : "any"));
|
||||
if (fields.probability)
|
||||
writer.append(" %s=%u", compact ? "pr" : "prob", unsigned(fields.probability));
|
||||
if (fields.disabled) writer.append(" %s=off", compact ? "e" : "enabled");
|
||||
}
|
||||
|
||||
template <typename Writer>
|
||||
void appendPath(Writer& writer, const Fields& fields, uint8_t width,
|
||||
uint8_t hops, const uint8_t first[9], bool compact) {
|
||||
if (!hops) return;
|
||||
const bool legacy = fields.path_position == Prefix && hops <= 3;
|
||||
writer.append(" %s=", legacy ? (compact ? "p" : "prefix") : (compact ? "pa" : "path"));
|
||||
if (!legacy && (fields.path_position == Prefix || fields.path_position == Exact)) writer.append("^");
|
||||
uint8_t path[12];
|
||||
memcpy(path, first, 9); memcpy(path + 9, fields.path_tail, 3);
|
||||
for (unsigned i = 0; i < hops; ++i) {
|
||||
if (i) writer.append(legacy ? "," : ">");
|
||||
for (unsigned j = 0; j < width; ++j) writer.append("%02X", unsigned(path[i * width + j]));
|
||||
}
|
||||
if (fields.path_position == Suffix || fields.path_position == Exact) writer.append("$");
|
||||
}
|
||||
|
||||
} // namespace FloodRuleExtensions
|
||||
@@ -0,0 +1,319 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace flood {
|
||||
|
||||
// Byte patterns: substring search by default, ^/$ anchors, ., escaped literals,
|
||||
// classes/ranges/[^...], groups, alternation, and *, +, ? repetition. Matching is
|
||||
// case-sensitive unless ASCII case folding is explicitly requested. No heap or
|
||||
// backtracking is used; one budget can be shared across every rule for a packet.
|
||||
static constexpr size_t MAX_TEXT_PATTERN_LENGTH = 63;
|
||||
static constexpr uint32_t TEXT_PATTERN_WORK_BUDGET = 32768;
|
||||
|
||||
enum class PatternMatchResult : uint8_t {
|
||||
NoMatch,
|
||||
Match,
|
||||
Invalid,
|
||||
BudgetExceeded,
|
||||
};
|
||||
|
||||
namespace pattern_detail {
|
||||
|
||||
static constexpr uint8_t NO_EDGE = 255;
|
||||
static constexpr uint8_t MAX_STATES = 96;
|
||||
static constexpr uint8_t MAX_GROUP_DEPTH = 8;
|
||||
enum Op : uint8_t { Literal, Any, Class, Begin, End, Split, Jump, Accept };
|
||||
|
||||
struct State {
|
||||
uint8_t op, out, other, value;
|
||||
};
|
||||
|
||||
struct Fragment {
|
||||
uint8_t start, head, tail;
|
||||
};
|
||||
|
||||
// Thompson fragments use unfinished edges as a linked patch list while
|
||||
// compiling. After compilation all those edges contain ordinary state indices.
|
||||
class Compiler {
|
||||
const char* pattern;
|
||||
size_t length, cursor;
|
||||
State* states;
|
||||
uint8_t used, depth;
|
||||
bool good;
|
||||
|
||||
uint8_t emit(Op op, uint8_t value = 0) {
|
||||
if (used == MAX_STATES) { good = false; return 0; }
|
||||
const uint8_t index = used++;
|
||||
states[index] = {op, NO_EDGE, NO_EDGE, value};
|
||||
return index;
|
||||
}
|
||||
|
||||
uint8_t& edge(uint8_t index) {
|
||||
return index & 1 ? states[index / 2].other : states[index / 2].out;
|
||||
}
|
||||
|
||||
Fragment single(Op op, uint8_t value = 0) {
|
||||
const uint8_t index = emit(op, value);
|
||||
return {index, uint8_t(index * 2), uint8_t(index * 2)};
|
||||
}
|
||||
|
||||
void patch(Fragment fragment, uint8_t target) {
|
||||
uint8_t link = fragment.head;
|
||||
while (link != NO_EDGE) {
|
||||
const uint8_t next = edge(link);
|
||||
edge(link) = target;
|
||||
link = next;
|
||||
}
|
||||
}
|
||||
|
||||
Fragment append(Fragment left, Fragment right) {
|
||||
edge(left.tail) = right.head;
|
||||
return {left.start, left.head, right.tail};
|
||||
}
|
||||
|
||||
bool classChar(uint8_t& value) {
|
||||
if (cursor == length || pattern[cursor] == ']') return false;
|
||||
value = uint8_t(pattern[cursor++]);
|
||||
if (value == '\\') {
|
||||
if (cursor == length) return false;
|
||||
value = uint8_t(pattern[cursor++]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool characterClass() {
|
||||
if (cursor < length && pattern[cursor] == '^') ++cursor;
|
||||
bool any = false;
|
||||
while (cursor < length && pattern[cursor] != ']') {
|
||||
uint8_t first, last;
|
||||
if (!classChar(first)) return false;
|
||||
any = true;
|
||||
if (cursor + 1 < length && pattern[cursor] == '-' && pattern[cursor + 1] != ']') {
|
||||
++cursor;
|
||||
if (!classChar(last) || first > last) return false;
|
||||
}
|
||||
}
|
||||
if (!any || cursor == length) return false;
|
||||
++cursor;
|
||||
return true;
|
||||
}
|
||||
|
||||
Fragment atom() {
|
||||
const uint8_t value = uint8_t(pattern[cursor++]);
|
||||
switch (value) {
|
||||
case '(' : {
|
||||
if (++depth > MAX_GROUP_DEPTH) { good = false; return single(Jump); }
|
||||
Fragment result = expression();
|
||||
--depth;
|
||||
if (cursor == length || pattern[cursor++] != ')') good = false;
|
||||
return result;
|
||||
}
|
||||
case '[' : {
|
||||
const uint8_t offset = uint8_t(cursor);
|
||||
if (!characterClass()) good = false;
|
||||
return single(Class, offset);
|
||||
}
|
||||
case '\\' :
|
||||
if (cursor == length) { good = false; return single(Jump); }
|
||||
return single(Literal, uint8_t(pattern[cursor++]));
|
||||
case '.' : return single(Any);
|
||||
case '^' : return single(Begin);
|
||||
case '$' : return single(End);
|
||||
case '*' : case '+' : case '?' : case ']' :
|
||||
good = false;
|
||||
return single(Jump);
|
||||
default : return single(Literal, value);
|
||||
}
|
||||
}
|
||||
|
||||
Fragment sequence() {
|
||||
Fragment result = {NO_EDGE, NO_EDGE, NO_EDGE};
|
||||
while (good && cursor < length && pattern[cursor] != ')' && pattern[cursor] != '|') {
|
||||
const uint8_t first = uint8_t(pattern[cursor]);
|
||||
Fragment next = atom();
|
||||
if (!good) return next;
|
||||
if (good && cursor < length && (pattern[cursor] == '*' || pattern[cursor] == '+' || pattern[cursor] == '?')) {
|
||||
const char repeat = pattern[cursor++];
|
||||
if (first == '^' || first == '$') { good = false; break; }
|
||||
const uint8_t split = emit(Split);
|
||||
if (!good) return next;
|
||||
states[split].out = next.start;
|
||||
const uint8_t tail = uint8_t(split * 2 + 1);
|
||||
if (repeat == '?') {
|
||||
const uint8_t start = split;
|
||||
next = append(next, {start, tail, tail});
|
||||
next.start = start;
|
||||
} else {
|
||||
const uint8_t start = repeat == '*' ? split : next.start;
|
||||
patch(next, split);
|
||||
next = {start, tail, tail};
|
||||
}
|
||||
// Lazy/stacked quantifiers are deliberately unsupported.
|
||||
if (cursor < length && (pattern[cursor] == '*' || pattern[cursor] == '+' || pattern[cursor] == '?')) good = false;
|
||||
}
|
||||
if (result.start == NO_EDGE) result = next;
|
||||
else { patch(result, next.start); result.head = next.head; result.tail = next.tail; }
|
||||
}
|
||||
return result.start == NO_EDGE ? single(Jump) : result;
|
||||
}
|
||||
|
||||
Fragment expression() {
|
||||
Fragment result = sequence();
|
||||
while (good && cursor < length && pattern[cursor] == '|') {
|
||||
++cursor;
|
||||
Fragment right = sequence();
|
||||
if (!good) return right;
|
||||
const uint8_t split = emit(Split);
|
||||
if (!good) return result;
|
||||
states[split].out = result.start;
|
||||
states[split].other = right.start;
|
||||
result = append(result, right);
|
||||
result.start = split;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
Compiler(const char* text, size_t size, State* output)
|
||||
: pattern(text), length(size), cursor(0), states(output), used(0), depth(0), good(true) {}
|
||||
|
||||
bool compile(uint8_t& start) {
|
||||
if (!pattern || length > MAX_TEXT_PATTERN_LENGTH) return false;
|
||||
// NUL is data in the input text, but cannot appear inside a CLI pattern.
|
||||
if (memchr(pattern, 0, length)) return false;
|
||||
Fragment fragment = expression();
|
||||
if (!good || cursor != length) return false;
|
||||
const uint8_t finish = emit(Accept);
|
||||
if (!good) return false;
|
||||
patch(fragment, finish);
|
||||
start = fragment.start;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
inline bool spend(uint32_t& budget) {
|
||||
if (!budget) return false;
|
||||
--budget;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct StateList {
|
||||
uint8_t indices[MAX_STATES];
|
||||
uint8_t seen[(MAX_STATES + 7) / 8];
|
||||
uint8_t count;
|
||||
|
||||
void clear() { memset(seen, 0, sizeof(seen)); count = 0; }
|
||||
void push(uint8_t index) {
|
||||
const uint8_t bit = uint8_t(1U << (index & 7));
|
||||
if (seen[index / 8] & bit) return;
|
||||
seen[index / 8] |= bit;
|
||||
indices[count++] = index;
|
||||
}
|
||||
|
||||
bool add(const State* states, uint8_t start, size_t position, size_t length, uint32_t& budget) {
|
||||
const uint8_t first = count;
|
||||
push(start);
|
||||
for (uint8_t cursor = first; cursor < count; ++cursor) {
|
||||
if (!spend(budget)) return false;
|
||||
const State& state = states[indices[cursor]];
|
||||
switch (state.op) {
|
||||
case Split: push(state.other); push(state.out); break;
|
||||
case Jump: push(state.out); break;
|
||||
case Begin: if (position == 0) push(state.out); break;
|
||||
case End: if (position == length) push(state.out); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
inline uint8_t fold(uint8_t byte) {
|
||||
return byte >= 'A' && byte <= 'Z' ? uint8_t(byte + ('a' - 'A')) : byte;
|
||||
}
|
||||
|
||||
inline uint8_t classChar(const char* pattern, uint8_t& cursor) {
|
||||
uint8_t byte = uint8_t(pattern[cursor++]);
|
||||
if (byte == '\\') byte = uint8_t(pattern[cursor++]);
|
||||
return byte;
|
||||
}
|
||||
|
||||
inline int classMatches(const char* pattern, uint8_t cursor, uint8_t byte, bool ignoreCase, uint32_t& budget) {
|
||||
const bool negate = pattern[cursor] == '^';
|
||||
if (negate) ++cursor;
|
||||
bool matched = false;
|
||||
while (pattern[cursor] != ']') {
|
||||
if (!spend(budget)) return -1;
|
||||
const uint8_t first = classChar(pattern, cursor);
|
||||
uint8_t last = first;
|
||||
if (pattern[cursor] == '-' && pattern[cursor + 1] != ']') {
|
||||
++cursor;
|
||||
last = classChar(pattern, cursor);
|
||||
}
|
||||
// Test the original interval and the opposite ASCII case. This keeps
|
||||
// mixed ranges such as [A-z] faithful when case folding is enabled.
|
||||
matched |= byte >= first && byte <= last;
|
||||
if (ignoreCase) {
|
||||
const uint8_t folded = fold(byte);
|
||||
matched |= folded >= first && folded <= last;
|
||||
if (folded >= 'a' && folded <= 'z') {
|
||||
const uint8_t upper = uint8_t(folded - ('a' - 'A'));
|
||||
matched |= upper >= first && upper <= last;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched != negate;
|
||||
}
|
||||
|
||||
} // namespace pattern_detail
|
||||
|
||||
inline bool textPatternValid(const char* pattern, size_t patternLength) {
|
||||
pattern_detail::State states[pattern_detail::MAX_STATES];
|
||||
uint8_t start;
|
||||
return pattern_detail::Compiler(pattern, patternLength, states).compile(start);
|
||||
}
|
||||
|
||||
inline PatternMatchResult matchTextPattern(const char* pattern, size_t patternLength,
|
||||
const uint8_t* text, size_t textLength,
|
||||
bool ignoreCase = false, uint32_t* remainingWork = nullptr) {
|
||||
using namespace pattern_detail;
|
||||
if (!text && textLength) return PatternMatchResult::Invalid;
|
||||
State states[MAX_STATES];
|
||||
uint8_t start;
|
||||
if (!Compiler(pattern, patternLength, states).compile(start)) return PatternMatchResult::Invalid;
|
||||
uint32_t localBudget = TEXT_PATTERN_WORK_BUDGET;
|
||||
uint32_t& budget = remainingWork ? *remainingWork : localBudget;
|
||||
StateList lists[2];
|
||||
StateList* current = &lists[0];
|
||||
StateList* next = &lists[1];
|
||||
current->clear();
|
||||
for (size_t position = 0;; ++position) {
|
||||
// Injecting the start state at each byte provides substring search without
|
||||
// restarting the automaton or rescanning any input prefix.
|
||||
if (!current->add(states, start, position, textLength, budget)) return PatternMatchResult::BudgetExceeded;
|
||||
next->clear();
|
||||
for (uint8_t cursor = 0; cursor < current->count; ++cursor) {
|
||||
if (!spend(budget)) return PatternMatchResult::BudgetExceeded;
|
||||
const State& state = states[current->indices[cursor]];
|
||||
if (state.op == Accept) return PatternMatchResult::Match;
|
||||
if (position == textLength) continue;
|
||||
bool matches = state.op == Any;
|
||||
if (state.op == Literal) matches = ignoreCase ? fold(text[position]) == fold(state.value) : text[position] == state.value;
|
||||
else if (state.op == Class) {
|
||||
const int result = classMatches(pattern, state.value, text[position], ignoreCase, budget);
|
||||
if (result < 0) return PatternMatchResult::BudgetExceeded;
|
||||
matches = result != 0;
|
||||
}
|
||||
if (matches && !next->add(states, state.out, position + 1, textLength, budget)) return PatternMatchResult::BudgetExceeded;
|
||||
}
|
||||
if (position == textLength) return PatternMatchResult::NoMatch;
|
||||
StateList* old = current; current = next; next = old;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace flood
|
||||
} // namespace mesh
|
||||
Reference in new issue
Block a user