Files
MeshCore/test/test_flood_rule_extensions.py
T

308 lines
15 KiB
Python

"""Run the production extended-rule parser, matching, and wire-field helpers."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
UTILS = r'''#pragma once
#include <stddef.h>
#include <stdint.h>
namespace mesh { struct Utils {
static int MACThenDecrypt(const uint8_t*, uint8_t*, const uint8_t*, int);
}; }
'''
PROGRAM = r'''
#include <algorithm>
#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <Utils.h>
#include <helpers/FloodRuleExtensions.h>
using namespace FloodRuleExtensions;
mesh::Packet::Packet() : header(0), payload_len(0), path_len(0), transport_codes{}, path{}, payload{},
_rssi(0), _snr(0), tx_cr(0), flood_retry_policy(0) {}
static std::vector<uint8_t> plaintext;
static unsigned decrypts;
static bool decrypt_ok = true;
int mesh::Utils::MACThenDecrypt(const uint8_t* secret, uint8_t* out, const uint8_t*, int) {
++decrypts;
if (!decrypt_ok || secret[0] != 0x42) return 0;
assert(plaintext.size() <= MAX_PACKET_PAYLOAD);
memcpy(out, plaintext.data(), plaintext.size());
return int(plaintext.size());
}
struct StringWriter {
std::string text;
void append(const char* format, ...) {
char buffer[512];
va_list args; va_start(args, format);
int n = vsnprintf(buffer, sizeof(buffer), format, args); va_end(args);
assert(n >= 0 && n < int(sizeof(buffer)));
text.append(buffer, size_t(n));
}
};
static Fields parse(const char* command) {
char buffer[512], reply[160] = {};
assert(strlen(command) < sizeof(buffer));
strcpy(buffer, command);
char* tokens[24];
int count = tokenize(buffer, tokens, 24);
assert(count >= 0);
Fields fields{}; uint8_t seen = 0;
for (int i = 0; i < count; ++i) assert(parseToken(tokens[i], fields, seen, reply) == 1);
assert(validFields(fields));
return fields;
}
static void parsing() {
Fields normal = parse(R"(sender="^(Alice|Bob) [0-9]+$" text='[Ww]eather: rain?' len=0-60 route=any prob=25 enabled=off)");
Fields compact = parse(R"(n="^(Alice|Bob) [0-9]+$" x='[Ww]eather: rain?' l=0-60 rt=a pr=25 e=0)");
assert(sameFields(normal, compact));
assert(normal.payload_set && normal.min_payload == 0 && normal.max_payload == 60);
assert(normal.route == AnyRoute && normal.probability == 25 && normal.disabled);
assert(contentRequested(normal) && hasFields(normal));
Fields empty{};
assert(validFields(empty) && !hasFields(empty) && !contentRequested(empty));
assert(sameFields(parse("prob=100"), empty));
Fields terminator = empty;
memset(terminator.sender, 'a', sizeof(terminator.sender));
assert(!validFields(terminator));
terminator = empty; memset(terminator.text, 'a', sizeof(terminator.text));
assert(!validFields(terminator));
for (const char* token : {"sender=", "text=", "sender=[", "text=a**", "len=", "len=-1", "len=3-2",
"len=185", "len=184-185", "len=1+2", "len=1+-2", "len=1-2+", "len=1--2", "len=99999999999999999999",
"route=bad", "prob=0", "prob=101", "prob=-1", "prob=50%", "enabled=yes"}) {
Fields row{}; uint8_t seen = 0; char reply[160] = {};
assert(parseToken(token, row, seen, reply) == -1 && strncmp(reply, "Err -", 5) == 0);
}
const std::string oversized = "sender=" + std::string(64, 'a');
uint8_t seen = 0; char reply[160] = {}; Fields row{};
assert(parseToken(oversized.c_str(), row, seen, reply) == -1);
seen = 0;
assert(parseToken("sender=Alice", row, seen, reply) == 1);
assert(parseToken("n=Bob", row, seen, reply) == -1 && !strcmp(row.sender, "Alice"));
assert(parseToken("mode=radio", row, seen, reply) == 0);
assert(parseToken("drop", row, seen, reply) == 0);
char malformed[] = "text=\"unterminated"; char* tokens[2];
assert(tokenize(malformed, tokens, 2) == -1);
char excess[] = "a b c"; assert(tokenize(excess, tokens, 2) == -1);
char whitespace[] = "\t "; assert(tokenize(whitespace, tokens, 2) == 0);
for (const char* syntax : {"len=0", "l=0-0"}) {
const auto zero = parse(syntax);
mesh::Packet packet; packet.header = ROUTE_TYPE_FLOOD;
assert(matchesEnvelope(zero, &packet, 0));
packet.payload_len = 1; assert(!matchesEnvelope(zero, &packet, 0));
}
for (bool abbreviated : {false, true}) {
for (const char* syntax : {R"(text='has "quote"')", R"(text='has \"quote\"')",
R"(text='\\server\ folder')", R"(text="it\'s both")"}) {
Fields source = parse(syntax);
StringWriter writer; appendFields(writer, source, abbreviated);
Fields restored = parse(writer.text.c_str());
for (const char* sample : {"has \"quote\"", "has quote", "\\server folder", "it's both", "unrelated"}) {
using mesh::flood::matchTextPattern;
const auto* bytes = reinterpret_cast<const uint8_t*>(sample);
assert(matchTextPattern(source.text, strlen(source.text), bytes, strlen(sample))
== matchTextPattern(restored.text, strlen(restored.text), bytes, strlen(sample)));
}
}
StringWriter writer; appendFields(writer, normal, abbreviated);
assert(sameFields(normal, parse(writer.text.c_str())));
}
}
static void envelopesAndProbability() {
Fields row{};
assert(!matchesEnvelope(row, nullptr, 0));
for (uint8_t route = 0; route < 4; ++route) {
mesh::Packet packet; packet.header = route; packet.payload_len = 44;
const bool flood = packet.isRouteFlood();
assert(matchesEnvelope(row, &packet, 0) == flood);
assert(matchesEnvelope(row, &packet, 1));
row.route = FloodRoute; assert(matchesEnvelope(row, &packet, 0) == flood);
row.route = DirectRoute; assert(matchesEnvelope(row, &packet, 0) == !flood);
row.route = AnyRoute; assert(matchesEnvelope(row, &packet, 0));
row.payload_set = 1; row.min_payload = 44; row.max_payload = 44;
assert(matchesEnvelope(row, &packet, 0));
packet.payload_len = 43; assert(!matchesEnvelope(row, &packet, 0));
packet.payload_len = 45; assert(!matchesEnvelope(row, &packet, 0));
packet.payload_len = 44; row.disabled = 1; assert(!matchesEnvelope(row, &packet, 0));
row = {};
}
for (unsigned probability = 0; probability <= 100; ++probability) {
row.probability = uint8_t(probability);
unsigned selected = 0;
for (uint32_t roll = 0; roll < 10000; ++roll) selected += matchesProbability(row, roll);
assert(selected == (probability ? probability * 100 : 10000));
}
}
static void paths() {
mesh::Packet packet; packet.header = ROUTE_TYPE_FLOOD;
packet.setPathHashSizeAndCount(1, 5);
const uint8_t bytes[] = {0x11, 0x22, 0x33, 0x44, 0x55}; memcpy(packet.path, bytes, sizeof(bytes));
for (const char* syntax : {"22>33", "22,33"}) {
Fields row{}; uint8_t width, hops, first[9];
assert(parsePath(syntax, width, hops, first, row, false));
assert(width == 1 && hops == 2 && row.path_position == Anywhere);
assert(pathMatches(row, &packet, width, hops, first));
row.path_position = Prefix; assert(!pathMatches(row, &packet, width, hops, first));
row.path_position = Suffix; assert(!pathMatches(row, &packet, width, hops, first));
}
for (const char* syntax : {"^11>22", "44>55$", "^11>22>33>44$"}) {
Fields row{}; uint8_t width, hops, first[9];
assert(parsePath(syntax, width, hops, first, row, false));
assert(pathMatches(row, &packet, width, hops, first) == (row.path_position != Exact));
}
for (unsigned width = 1; width <= 3; ++width) {
Fields row{}; uint8_t parsedWidth, hops, first[9];
StringWriter wanted;
for (unsigned hop = 0; hop < 4; ++hop) {
if (hop) wanted.append(">");
for (unsigned byte = 0; byte < width; ++byte) wanted.append("%02X", 0x11 + hop * width + byte);
}
assert(parsePath(wanted.text.c_str(), parsedWidth, hops, first, row, false));
assert(parsedWidth == width && hops == 4);
packet.setPathHashSizeAndCount(uint8_t(width), 6);
memset(packet.path, 0x99, sizeof(packet.path));
memcpy(packet.path + width, first, std::min(9U, 4 * width));
if (width == 3) memcpy(packet.path + width + 9, row.path_tail, 3);
assert(pathMatches(row, &packet, parsedWidth, hops, first));
for (bool compact : {false, true}) {
StringWriter writer; appendPath(writer, row, parsedWidth, hops, first, compact);
const char* syntax = strchr(writer.text.c_str(), '=') + 1;
Fields copy{}; uint8_t copiedWidth, copiedHops, copiedFirst[9];
assert(parsePath(syntax, copiedWidth, copiedHops, copiedFirst, copy, false));
assert(copiedWidth == parsedWidth && copiedHops == hops && sameFields(row, copy));
assert(!memcmp(first, copiedFirst, 9));
}
packet.header = ROUTE_TYPE_DIRECT; assert(!pathMatches(row, &packet, parsedWidth, hops, first));
packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
assert(!pathMatches(row, &packet, parsedWidth, hops, first));
packet.header = ROUTE_TYPE_FLOOD;
packet.setPathHashSizeAndCount(uint8_t(width), 63);
if (width > 1) assert(!pathMatches(row, &packet, parsedWidth, hops, first));
}
for (const char* invalid : {"", "^", "$", "^$", "AA>", ">AA", "AA>>BB", "GG", "A", "AAAAA", "AA>BBBB", "AA>BB>CC>DD>EE"}) {
Fields row{}; uint8_t width, hops, first[9];
assert(!parsePath(invalid, width, hops, first, row, false));
}
Fields row{}; uint8_t width, hops, first[9];
assert(parsePath("*", width, hops, first, row, false) && !width && !hops);
assert(pathMatches(row, &packet, width, hops, first));
}
static void content() {
uint8_t key[32] = {0x42};
mesh::Packet packet; packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT); packet.payload_len = 35;
Fields row = parse(R"(sender=^Alice$ text='weather (sun|rain)')");
const auto setPlain = [](const std::string& text) {
plaintext.assign(5, 0); plaintext.insert(plaintext.end(), text.begin(), text.end()); plaintext.push_back(0);
};
setPlain("Alice: weather rain");
assert(matchesContent(row, &packet, key, 16));
assert(matchesContent(row, &packet, key, 32));
uint32_t work = 1000;
assert(matchesContent(row, &packet, key, 16, &work));
assert(work > 0 && work < 1000);
const uint32_t afterFirst = work;
assert(matchesContent(row, &packet, key, 16, &work) && work < afterFirst);
work = 0;
assert(!matchesContent(row, &packet, key, 16, &work, false));
assert(matchesContent(row, &packet, key, 16, &work, true));
bool exhausted = false;
assert(!matchesContent(row, &packet, key, 16, &work, false, &exhausted) && exhausted);
// An allow rule cannot become an unconditional exemption when work runs
// out. A deny rule may conservatively suppress uncertain authenticated data.
Fields textOnly = row; textOnly.sender[0] = 0;
assert(!matchesContent(textOnly, &packet, key, 16, &work, false));
assert(matchesContent(textOnly, &packet, key, 16, &work, true));
exhausted = false;
assert(!matchesContent(textOnly, &packet, key, 16, &work, false, &exhausted) && exhausted);
work = 1000;
assert(matchesContent(textOnly, &packet, key, 16, &work, false, &exhausted) && !exhausted);
assert(!matchesContent(row, &packet, key, 1));
assert(!matchesContent(row, &packet, nullptr, 16));
assert(!matchesContent(row, static_cast<const mesh::Packet*>(nullptr), key, 16));
decrypt_ok = false;
assert(!matchesContent(row, &packet, key, 16));
assert(!matchesContent(row, &packet, key, 16, &work, true));
exhausted = true;
assert(!matchesContent(row, &packet, key, 16, &work, false, &exhausted) && !exhausted);
decrypt_ok = true;
setPlain("alice: weather rain"); assert(!matchesContent(row, &packet, key, 16));
setPlain("Alice: dry"); assert(!matchesContent(row, &packet, key, 16));
setPlain("weather rain"); assert(!matchesContent(row, &packet, key, 16));
row.sender[0] = 0; assert(matchesContent(row, &packet, key, 16));
setPlain(std::string(140, 'x') + "Alice: weather sun");
strcpy(row.sender, "Alice$"); assert(matchesContent(row, &packet, key, 16));
plaintext[4] = TXT_TYPE_CLI_COMMAND << 2; assert(!matchesContent(row, &packet, key, 16));
packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT);
assert(!matchesContent(row, &packet, key, 16));
row = {}; decrypts = 0;
assert(matchesContent(row, static_cast<const mesh::Packet*>(nullptr), nullptr, 0) && decrypts == 0);
}
struct ByteReader {
std::vector<uint8_t> bytes; size_t position = 0;
size_t read(uint8_t* out, size_t size) {
size = std::min(size, bytes.size() - position);
if (size) memcpy(out, bytes.data() + position, size);
position += size; return size;
}
};
static void storage() {
Fields source = parse(R"(sender=Alice text='rain storm' len=0-80 route=direct prob=33 enabled=off)");
source.path_position = Suffix; source.path_tail[2] = 0x99;
std::vector<uint8_t> bytes;
auto writer = [&bytes](const void* data, size_t size) {
const auto* first = static_cast<const uint8_t*>(data); bytes.insert(bytes.end(), first, first + size); return true;
};
assert(writeFields(writer, source));
const std::vector<uint8_t> expected = {0, 0, 80, 0, DirectRoute, 33, Suffix, 1, 1, 0, 0, 0x99,
5, 'A', 'l', 'i', 'c', 'e', 10, 'r', 'a', 'i', 'n', ' ', 's', 't', 'o', 'r', 'm'};
assert(bytes == expected);
ByteReader full{bytes}; Fields loaded{};
assert(readFields(full, loaded) && full.position == bytes.size() && sameFields(source, loaded));
for (size_t length = 0; length < bytes.size(); ++length) {
ByteReader shortRead{{bytes.begin(), bytes.begin() + length}};
assert(!readFields(shortRead, loaded));
}
const size_t senderLength = 12; // Four uint16 bytes, five control bytes, three tail bytes.
assert(bytes[senderLength] == strlen(source.sender));
for (const auto& change : std::vector<std::pair<size_t, uint8_t>>{{4, 4}, {5, 101}, {6, 4}, {7, 2}, {8, 2}, {senderLength, 64}, {senderLength + 1, '*'}}) {
ByteReader corrupt{bytes}; corrupt.bytes[change.first] = change.second;
assert(!readFields(corrupt, loaded));
}
unsigned successfulCalls = 0;
auto counter = [&successfulCalls](const void*, size_t) { ++successfulCalls; return true; };
assert(writeFields(counter, source));
for (unsigned failure = 0; failure < successfulCalls; ++failure) {
unsigned calls = 0;
auto broken = [&calls, failure](const void*, size_t) { return calls++ != failure; };
assert(!writeFields(broken, source));
}
}
int main() { parsing(); envelopesAndProbability(); paths(); content(); storage(); }
'''
class FloodRuleExtensionsTests(unittest.TestCase):
def test_real_parser_matching_and_storage(self):
with tempfile.TemporaryDirectory(prefix="flood-rule-extensions-") as folder:
folder = Path(folder)
(folder / "Utils.h").write_text(UTILS)
source = folder / "extensions.cpp"
source.write_text(PROGRAM)
binary = folder / "extensions"
flags = []
if os.name != "nt":
flags = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
subprocess.run(["g++", "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", *flags,
"-I" + str(folder), "-I" + str(ROOT / "src"), str(source), "-o", str(binary)], check=True)
subprocess.run([str(binary)], check=True)
if __name__ == "__main__":
unittest.main()