mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-21 07:21:57 +00:00
494 lines
21 KiB
C++
494 lines
21 KiB
C++
#include <gtest/gtest.h>
|
|
#include <Ed25519.h>
|
|
#include <helpers/FleetCommand.h>
|
|
#include <SHA256.h>
|
|
#include <array>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using Fleet = mesh::FleetCommand;
|
|
constexpr uint32_t Now = 1791500000UL;
|
|
constexpr uint8_t ChannelKey[16] = {
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
|
|
};
|
|
|
|
class FixedRng : public mesh::RNG {
|
|
public:
|
|
void random(uint8_t* dest, size_t size) override {
|
|
for (size_t i = 0; i < size; ++i) dest[i] = uint8_t(i + 1);
|
|
}
|
|
};
|
|
|
|
class FleetCommandTest : public ::testing::Test {
|
|
protected:
|
|
FixedRng rng;
|
|
mesh::LocalIdentity publisher{&rng};
|
|
std::array<uint8_t, Fleet::TargetSize> all{};
|
|
Fleet::Decoded decoded{};
|
|
bool previous_verifier = g_mock_ed25519_verify_result;
|
|
|
|
void SetUp() override { g_mock_ed25519_verify_result = true; }
|
|
void TearDown() override { g_mock_ed25519_verify_result = previous_verifier; }
|
|
|
|
std::vector<uint8_t> encode(const char* command = "get radio2",
|
|
uint32_t sequence = Now,
|
|
uint32_t expires = Now + 120,
|
|
const uint8_t* target = nullptr) {
|
|
std::vector<uint8_t> bytes(Fleet::MaxPayloadLength);
|
|
const size_t size = Fleet::encode(publisher, ChannelKey, sequence, expires,
|
|
target ? target : all.data(), command, bytes.data(), bytes.size());
|
|
bytes.resize(size);
|
|
return bytes;
|
|
}
|
|
|
|
bool decode(const std::vector<uint8_t>& bytes, uint32_t now = Now) {
|
|
return Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
|
|
now, publisher.pub_key, decoded);
|
|
}
|
|
|
|
std::vector<uint8_t> encodeTargets(const Fleet::Targets& targets,
|
|
const char* command = "get radio2") {
|
|
std::vector<uint8_t> bytes(Fleet::MaxPayloadLength);
|
|
const size_t size = Fleet::encode(publisher, ChannelKey, Now, Now + 120,
|
|
targets, command, bytes.data(), bytes.size());
|
|
bytes.resize(size);
|
|
return bytes;
|
|
}
|
|
};
|
|
|
|
TEST_F(FleetCommandTest, ExplicitRadioAndFilterFamiliesAreAllowed) {
|
|
for (const char* command : {
|
|
"get radio2", "get tempradio2", "get radio2.status", "get radio2.timing",
|
|
"get radio2.cross", "set radio2.cross auto", "set radio2.cross on",
|
|
"set radio2.cross off", "set radio2 off", "set tempradio2 off",
|
|
"set radio2 910.5,500,5,5,rxtx",
|
|
"set tempradio2 910.5,500,5,5,rx,10,auto",
|
|
"set radio2 910.5,500,5,5,rx,32",
|
|
"get flood.filter", "get flood.filter.1", "get flood.filter.blacklist.2",
|
|
"set flood.filter.2 grp_txt 4+", "set flood.filter.blacklist A1,B2",
|
|
"del flood.filter.blacklist.2", "del flood.filter all",
|
|
"get flood.rule.2", "set flood.rule.2 type=any rate=10/min stop",
|
|
"del flood.rule.2", "del flood.rule all",
|
|
"get flood.moderation.2", "set flood.moderation public '*' rate=2/min",
|
|
"del flood.moderation all", "get flood.channel.scope.3",
|
|
"set flood.channel.scope public scope=usa", "del flood.channel.scope.3",
|
|
"get flood.channel.scope.require.2",
|
|
"set flood.channel.scope.require.2 #fleet", "del flood.channel.scope.require all",
|
|
"get flood.channel.data", "set flood.channel.data on",
|
|
"get flood.channel.data.hops", "set flood.channel.data.hops 4",
|
|
"get flood.max", "set flood.max 8", "get flood.max.unscoped",
|
|
"set flood.max.unscoped 4", "get flood.max.advert", "set flood.max.advert 6"}) {
|
|
EXPECT_TRUE(Fleet::commandAllowed(command)) << command;
|
|
}
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, NoGenericAdminNamespaceOrPrefixEscapes) {
|
|
for (const char* command : {
|
|
"get prv.key", "backup prv.key 1234567890ABCDEF", "get wifi.password",
|
|
"set wifi.password abc", "setperm abc 3", "reboot", "start ota",
|
|
"get fleet", "set fleet.channel abc", "region add usa", "get radio",
|
|
"set radio 910.5,500,5,5", "get radio2.reply", "get radio2.scan",
|
|
"set tx.reply both", "set radioat2 910.5,500,5,5,rxtx,+5",
|
|
"get flood.filter.secret", "get flood.filtering", "get flood.rulebook",
|
|
"get flood.rule.1.secret", "get flood.filter.blacklistx",
|
|
"get flood.channel.scope.required", "get flood.channel.data.secret",
|
|
"get flood.max.secret", "get flood.max.unscoped.extra", "get flood.rule.0",
|
|
"get flood.rule.01", "get flood.rule.256", "get flood.rule.9999999999",
|
|
"xy|set radio2 off", "GET radio2", "get radio2", " get radio2",
|
|
"get radio2 ", "get radio2 off", "del radio2", "set radio2",
|
|
"set radio2.cross true", "del flood.channel.data", "set flood.filter.2",
|
|
"del flood.rule.2 extra", "get flood.rule.1 all", "set flood.max",
|
|
"set radio2 910.5,500,5,5,rxtx,auto,99",
|
|
"set radio2 910.5,500,5,5,off", "set radio2 910.5,500,5,5,rxtx,",
|
|
"set tempradio2 910.5,500,5,5,rxtx",
|
|
"set tempradio2 910.5,500,5,5,rxtx,+10",
|
|
"set radio2 910.5,500,5,5,rxtx;reboot", "get radio2\nreboot",
|
|
"get radio2\r", "get\tradio2", "get radio2|reboot", "get radio2&reboot",
|
|
"set flood.rule.1 type=any `reboot`", "set flood.rule.1 type=any\\reboot"}) {
|
|
EXPECT_FALSE(Fleet::commandAllowed(command)) << command;
|
|
}
|
|
const std::string non_ascii = "set flood.rule.1 " + std::string(1, char(0x80));
|
|
EXPECT_FALSE(Fleet::commandAllowed(non_ascii.c_str()));
|
|
EXPECT_FALSE(Fleet::commandAllowed(nullptr));
|
|
EXPECT_FALSE(Fleet::commandAllowed(""));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, OnlyCompletePublicKeyTargetsOrExplicitAll) {
|
|
std::array<uint8_t, Fleet::TargetSize> target;
|
|
target.fill(0xAA);
|
|
EXPECT_TRUE(Fleet::parseTarget("all", target.data()));
|
|
EXPECT_EQ(all, target);
|
|
EXPECT_FALSE(Fleet::parseTarget("ALL", target.data()));
|
|
EXPECT_FALSE(Fleet::parseTarget("1234", target.data()));
|
|
EXPECT_FALSE(Fleet::parseTarget(std::string(64, '0').c_str(), target.data()));
|
|
EXPECT_FALSE(Fleet::parseTarget(std::string(64, 'z').c_str(), target.data()));
|
|
EXPECT_FALSE(Fleet::parseTarget(nullptr, target.data()));
|
|
EXPECT_FALSE(Fleet::parseTarget("all", nullptr));
|
|
const char* full = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
|
ASSERT_TRUE(Fleet::parseTarget(full, target.data()));
|
|
const std::array<uint8_t, 16> expected = {
|
|
0xae, 0x21, 0x6c, 0x2e, 0xf5, 0x24, 0x7a, 0x37,
|
|
0x82, 0xc1, 0x35, 0xef, 0xa2, 0x79, 0xa3, 0xe4
|
|
};
|
|
EXPECT_EQ(expected, target); // independently generated SHA-256 vector
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RejectsPublicZeroOrMissingPrivacyKeys) {
|
|
const uint8_t public_key[16] = {
|
|
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
|
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72
|
|
};
|
|
const uint8_t empty[16] = {};
|
|
EXPECT_TRUE(Fleet::privateKeyAllowed(ChannelKey));
|
|
EXPECT_FALSE(Fleet::privateKeyAllowed(public_key));
|
|
EXPECT_FALSE(Fleet::privateKeyAllowed(empty));
|
|
EXPECT_FALSE(Fleet::privateKeyAllowed(nullptr));
|
|
std::array<uint8_t, Fleet::MaxPayloadLength> output;
|
|
for (const uint8_t* key : {public_key, empty, static_cast<const uint8_t*>(nullptr)}) {
|
|
EXPECT_EQ(0U, Fleet::encode(publisher, key, Now, Now + 10, all.data(),
|
|
"get radio2", output.data(), output.size()));
|
|
const auto bytes = encode();
|
|
EXPECT_FALSE(Fleet::decode(publisher, key, bytes.data(), bytes.size(), Now,
|
|
publisher.pub_key, decoded));
|
|
}
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, ExactEnvelopeRoundTripsThroughVerifierBoundary) {
|
|
const auto bytes = encode();
|
|
ASSERT_EQ(Fleet::HeaderSize + 10 + Fleet::SignatureSize, bytes.size());
|
|
EXPECT_EQ(std::string("FMC1"), std::string(bytes.begin(), bytes.begin() + 4));
|
|
EXPECT_EQ(10, bytes[28]);
|
|
const unsigned calls = g_mock_ed25519_verify_calls;
|
|
ASSERT_TRUE(decode(bytes));
|
|
EXPECT_EQ(calls + 1, g_mock_ed25519_verify_calls);
|
|
EXPECT_EQ(Now, decoded.sequence);
|
|
EXPECT_EQ(Now + 120, decoded.expires);
|
|
EXPECT_TRUE(decoded.broadcast);
|
|
EXPECT_STREQ("get radio2", decoded.command);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, FailedSignatureVerificationNeverReturnsCommand) {
|
|
const auto bytes = encode();
|
|
g_mock_ed25519_verify_result = false;
|
|
decoded.sequence = 123;
|
|
strcpy(decoded.command, "stale");
|
|
const unsigned calls = g_mock_ed25519_verify_calls;
|
|
EXPECT_FALSE(decode(bytes));
|
|
EXPECT_EQ(calls + 1, g_mock_ed25519_verify_calls);
|
|
EXPECT_EQ(0U, decoded.sequence);
|
|
EXPECT_EQ(0U, decoded.expires);
|
|
EXPECT_STREQ("", decoded.command);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, FullPayloadLimitFitsWithoutOverflow) {
|
|
const std::string prefix = "set flood.rule.1 ";
|
|
const std::string maximum = prefix + std::string(Fleet::MaxCommandLength - prefix.size(), 'a');
|
|
ASSERT_TRUE(Fleet::commandAllowed(maximum.c_str()));
|
|
const auto bytes = encode(maximum.c_str());
|
|
ASSERT_EQ(Fleet::MaxPayloadLength, bytes.size());
|
|
EXPECT_TRUE(decode(bytes));
|
|
EXPECT_STREQ(maximum.c_str(), decoded.command);
|
|
EXPECT_FALSE(Fleet::commandAllowed((maximum + "a").c_str()));
|
|
EXPECT_TRUE(encode((maximum + "a").c_str()).empty());
|
|
std::array<uint8_t, Fleet::MaxPayloadLength + 2> output;
|
|
output.fill(0xA5);
|
|
EXPECT_EQ(0U, Fleet::encode(publisher, ChannelKey, Now, Now + 10, all.data(),
|
|
maximum.c_str(), output.data() + 1, 164));
|
|
for (uint8_t value : output) EXPECT_EQ(0xA5, value);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RejectsTruncationTrailingPaddingAndWrongMagic) {
|
|
const auto original = encode();
|
|
for (size_t size = 0; size < original.size(); ++size) {
|
|
const std::vector<uint8_t> shortened(original.begin(), original.begin() + size);
|
|
EXPECT_FALSE(decode(shortened)) << size;
|
|
}
|
|
auto bytes = original;
|
|
bytes.push_back(0);
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = original;
|
|
bytes[0] ^= 1;
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = original;
|
|
bytes[28] = 0;
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes[28] = 73;
|
|
EXPECT_FALSE(decode(bytes));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RejectsEmbeddedNulAndForbiddenSignedCommandShape) {
|
|
auto bytes = encode();
|
|
bytes[Fleet::HeaderSize + 4] = 0;
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = encode();
|
|
memcpy(bytes.data() + Fleet::HeaderSize, "get radio1", 10);
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = encode();
|
|
bytes[Fleet::HeaderSize + 9] = '\n';
|
|
EXPECT_FALSE(decode(bytes));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, MatchesHashedCompleteSelfIdentityOrAll) {
|
|
std::array<uint8_t, Fleet::TargetSize> target;
|
|
SHA256 hash;
|
|
hash.update(publisher.pub_key, PUB_KEY_SIZE);
|
|
hash.finalize(target.data(), target.size());
|
|
ASSERT_TRUE(decode(encode("get radio2", Now, Now + 120, target.data())));
|
|
EXPECT_FALSE(decoded.broadcast);
|
|
target[15] ^= 1;
|
|
EXPECT_FALSE(decode(encode("get radio2", Now, Now + 120, target.data())));
|
|
EXPECT_TRUE(decode(encode()));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RequiresPlausibleClockAndBoundedExpiry) {
|
|
EXPECT_TRUE(encode("get radio2", Fleet::MinEpoch - 1, Fleet::MinEpoch + 10).empty());
|
|
EXPECT_TRUE(encode("get radio2", Now, Now - 1).empty());
|
|
EXPECT_TRUE(encode("get radio2", Now, Now + 601).empty());
|
|
EXPECT_FALSE(decode(encode(), Fleet::MinEpoch - 1));
|
|
EXPECT_TRUE(decode(encode(), Now + 120));
|
|
EXPECT_FALSE(decode(encode(), Now + 121));
|
|
EXPECT_TRUE(decode(encode("get radio2", Now + 60, Now + 120)));
|
|
EXPECT_FALSE(decode(encode("get radio2", Now + 61, Now + 120)));
|
|
EXPECT_TRUE(decode(encode("get radio2", Now, Now + 600)));
|
|
EXPECT_TRUE(decode(encode("get radio2", Now, Now)));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RejectsMissingAndZeroIdentityInputs) {
|
|
const auto bytes = encode();
|
|
const uint8_t empty[32] = {};
|
|
const mesh::Identity absent(empty);
|
|
EXPECT_FALSE(Fleet::decode(absent, ChannelKey, bytes.data(), bytes.size(),
|
|
Now, publisher.pub_key, decoded));
|
|
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
|
|
Now, empty, decoded));
|
|
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
|
|
Now, nullptr, decoded));
|
|
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, nullptr, bytes.size(),
|
|
Now, publisher.pub_key, decoded));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, UnixRangeBoundaryDoesNotWrap) {
|
|
const uint32_t maximum = UINT32_MAX;
|
|
EXPECT_TRUE(decode(encode("get radio2", maximum, maximum), maximum - 60));
|
|
EXPECT_FALSE(decode(encode("get radio2", maximum, maximum), maximum - 61));
|
|
EXPECT_TRUE(encode("get radio2", maximum - 50, 1).empty());
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, BoundedTargetTokenParsesMixedPrefixWidthsAndCompleteKeys) {
|
|
Fleet::Targets targets{};
|
|
const std::string full = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
|
const std::string token = "A1B2C3D4,010203040506," + full;
|
|
const std::string joined = token + " get radio2";
|
|
ASSERT_TRUE(Fleet::parseTargets(joined.data(), token.size(), targets));
|
|
EXPECT_EQ(3, targets.count);
|
|
EXPECT_EQ(5 + 7 + 17, targets.length);
|
|
EXPECT_EQ(4, targets.data[0]);
|
|
EXPECT_EQ(0xA1, targets.data[1]);
|
|
EXPECT_EQ(0xD4, targets.data[4]);
|
|
EXPECT_EQ(6, targets.data[5]);
|
|
EXPECT_EQ(6, targets.data[11]);
|
|
EXPECT_EQ(16, targets.data[12]);
|
|
EXPECT_EQ(0xAE, targets.data[13]);
|
|
EXPECT_EQ(0xE4, targets.data[28]);
|
|
const char nonterminated[8] = {'0','1','0','2','0','3','0','4'};
|
|
EXPECT_TRUE(Fleet::parseTargets(nonterminated, sizeof(nonterminated), targets));
|
|
EXPECT_EQ(1, targets.count);
|
|
EXPECT_EQ(5, targets.length);
|
|
EXPECT_TRUE(Fleet::parseTargets("all plus other text", 3, targets));
|
|
EXPECT_EQ(0, targets.count);
|
|
EXPECT_EQ(0, targets.length);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RejectsMalformedWholeTargetListsWithoutPartialResults) {
|
|
for (const std::string& token : {
|
|
std::string(""), std::string("all,01020304"), std::string("01020304,all"),
|
|
std::string("all,all"), std::string("ALL"), std::string(",01020304"),
|
|
std::string("01020304,"), std::string("01020304,,05060708"),
|
|
std::string("01020304,0506070g"), std::string("01020304 05060708"),
|
|
std::string("01020304, 05060708"), std::string("01020304\n"),
|
|
std::string("01"), std::string(10, '1'), std::string(16, '1'),
|
|
std::string(32, '1'), std::string(63, '1'), std::string(65, '1'),
|
|
std::string(64, '0'), std::string("01020304,") + std::string(64, '0')}) {
|
|
Fleet::Targets targets;
|
|
memset(&targets, 0xA5, sizeof(targets));
|
|
EXPECT_FALSE(Fleet::parseTargets(token.data(), token.size(), targets)) << token;
|
|
EXPECT_EQ(0, targets.length);
|
|
EXPECT_EQ(0, targets.count);
|
|
}
|
|
Fleet::Targets targets{};
|
|
EXPECT_FALSE(Fleet::parseTargets(nullptr, 8, targets));
|
|
EXPECT_FALSE(Fleet::parseTargets("01020304", SIZE_MAX, targets));
|
|
EXPECT_FALSE(Fleet::parseTargets("01020304", 0, targets));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, TargetRecordAndPayloadBudgetsAreSeparateAndBounded) {
|
|
std::string list;
|
|
for (unsigned count = 1; count <= 17; ++count) {
|
|
if (!list.empty()) list += ',';
|
|
list += "01020304";
|
|
Fleet::Targets targets{};
|
|
ASSERT_TRUE(Fleet::parseTargets(list.data(), list.size(), targets));
|
|
EXPECT_EQ(count, targets.count);
|
|
EXPECT_EQ(count * 5, targets.length);
|
|
EXPECT_EQ(count <= 15, !encodeTargets(targets).empty()) << count;
|
|
}
|
|
Fleet::Targets targets{};
|
|
list += ",01020304";
|
|
EXPECT_FALSE(Fleet::parseTargets(list.data(), list.size(), targets));
|
|
list.clear();
|
|
const std::string full = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
|
for (unsigned count = 1; count <= 5; ++count) {
|
|
if (!list.empty()) list += ',';
|
|
list += full;
|
|
ASSERT_TRUE(Fleet::parseTargets(list.data(), list.size(), targets));
|
|
EXPECT_EQ(count * 17, targets.length);
|
|
}
|
|
EXPECT_TRUE(encodeTargets(targets).empty());
|
|
list += ',' + full;
|
|
EXPECT_FALSE(Fleet::parseTargets(list.data(), list.size(), targets));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, AllAndSingleCompleteKeyKeepLegacyFmc1Encoding) {
|
|
Fleet::Targets targets{};
|
|
ASSERT_TRUE(Fleet::parseTargets("all", 3, targets));
|
|
EXPECT_EQ(encode(), encodeTargets(targets));
|
|
char full[PUB_KEY_SIZE * 2 + 1];
|
|
mesh::Utils::toHex(full, publisher.pub_key, PUB_KEY_SIZE);
|
|
uint8_t hash[Fleet::TargetSize];
|
|
ASSERT_TRUE(Fleet::parseTarget(full, hash));
|
|
ASSERT_TRUE(Fleet::parseTargets(full, 64, targets));
|
|
EXPECT_EQ(encode("get radio2", Now, Now + 120, hash), encodeTargets(targets));
|
|
EXPECT_TRUE(decode(encodeTargets(targets)));
|
|
EXPECT_FALSE(decoded.broadcast);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, PrefixesAndListsUseFmc2WithSignedFraming) {
|
|
char full[PUB_KEY_SIZE * 2 + 1];
|
|
mesh::Utils::toHex(full, publisher.pub_key, PUB_KEY_SIZE);
|
|
for (size_t width : {8U, 12U}) {
|
|
Fleet::Targets targets{};
|
|
ASSERT_TRUE(Fleet::parseTargets(full, width, targets));
|
|
const auto bytes = encodeTargets(targets);
|
|
ASSERT_FALSE(bytes.empty());
|
|
EXPECT_EQ(std::string("FMC2"), std::string(bytes.begin(), bytes.begin() + 4));
|
|
EXPECT_EQ(1, bytes[12]);
|
|
EXPECT_EQ(width / 2, bytes[13]);
|
|
EXPECT_TRUE(decode(bytes));
|
|
EXPECT_TRUE(decoded.broadcast);
|
|
}
|
|
const std::string list = std::string(full) + ',' + full;
|
|
Fleet::Targets targets{};
|
|
ASSERT_TRUE(Fleet::parseTargets(list.data(), list.size(), targets));
|
|
const auto bytes = encodeTargets(targets);
|
|
EXPECT_TRUE(decode(bytes));
|
|
EXPECT_TRUE(decoded.broadcast);
|
|
EXPECT_EQ(2, bytes[12]);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, AnyMatchingEntryIsAcceptedButMalformedLaterEntryIsRejected) {
|
|
char full[PUB_KEY_SIZE * 2 + 1];
|
|
mesh::Utils::toHex(full, publisher.pub_key, PUB_KEY_SIZE);
|
|
const std::string prefix(full, 8);
|
|
Fleet::Targets targets{};
|
|
const std::string list = prefix + ",01020304," + full;
|
|
ASSERT_TRUE(Fleet::parseTargets(list.data(), list.size(), targets));
|
|
auto bytes = encodeTargets(targets);
|
|
ASSERT_TRUE(decode(bytes));
|
|
bytes[18] = 7; // Unknown type after the first matching four-byte prefix.
|
|
EXPECT_FALSE(decode(bytes));
|
|
EXPECT_FALSE(decoded.broadcast);
|
|
bytes = encodeTargets(targets);
|
|
bytes[12] = 18;
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = encodeTargets(targets);
|
|
bytes[12] = 4;
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = encodeTargets(targets);
|
|
bytes[13 + targets.length] = 0;
|
|
EXPECT_FALSE(decode(bytes));
|
|
bytes = encodeTargets(targets);
|
|
bytes[13 + targets.length] = 73;
|
|
EXPECT_FALSE(decode(bytes));
|
|
for (size_t length = 0; length < bytes.size(); ++length) {
|
|
const std::vector<uint8_t> truncated(bytes.begin(), bytes.begin() + length);
|
|
EXPECT_FALSE(decode(truncated));
|
|
}
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, RawPrefixesIncludeLegitimateZeroPrefixesAndCollisions) {
|
|
Fleet::Targets targets{};
|
|
ASSERT_TRUE(Fleet::parseTargets("00000000", 8, targets));
|
|
const auto bytes = encodeTargets(targets);
|
|
uint8_t first[PUB_KEY_SIZE] = {}, second[PUB_KEY_SIZE] = {};
|
|
first[4] = 1;
|
|
second[4] = 2;
|
|
ASSERT_TRUE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
|
|
Now, first, decoded));
|
|
EXPECT_TRUE(decoded.broadcast);
|
|
ASSERT_TRUE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
|
|
Now, second, decoded));
|
|
ASSERT_TRUE(Fleet::parseTargets("000000000100", 12, targets));
|
|
const auto narrow = encodeTargets(targets);
|
|
EXPECT_TRUE(Fleet::decode(publisher, ChannelKey, narrow.data(), narrow.size(),
|
|
Now, first, decoded));
|
|
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, narrow.data(), narrow.size(),
|
|
Now, second, decoded));
|
|
first[0] = 1;
|
|
EXPECT_FALSE(Fleet::decode(publisher, ChannelKey, bytes.data(), bytes.size(),
|
|
Now, first, decoded));
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, InvalidExternalTargetStructuresNeverWriteOutput) {
|
|
std::array<uint8_t, Fleet::MaxPayloadLength> output;
|
|
output.fill(0xA5);
|
|
for (unsigned invalid = 0; invalid < 7; ++invalid) {
|
|
Fleet::Targets targets{};
|
|
if (invalid == 0) targets.length = 1;
|
|
if (invalid == 1) targets.count = 1;
|
|
if (invalid == 2) targets.count = 18;
|
|
if (invalid == 3) targets.length = 87;
|
|
if (invalid == 4) { targets.count = 1; targets.length = 5; targets.data[0] = 3; }
|
|
if (invalid == 5) { targets.count = 2; targets.length = 5; targets.data[0] = 4; }
|
|
if (invalid == 6) { targets.count = 1; targets.length = 4; targets.data[0] = 4; }
|
|
EXPECT_EQ(0U, Fleet::encode(publisher, ChannelKey, Now, Now + 120,
|
|
targets, "get radio2", output.data(), output.size()));
|
|
for (uint8_t byte : output) EXPECT_EQ(0xA5, byte);
|
|
}
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, LargestFmc2EnvelopeFitsAndOversizedCombinationWritesNothing) {
|
|
const std::string prefix = "set flood.rule.1 ";
|
|
const std::string maximum = prefix + std::string(Fleet::MaxCommandLength - prefix.size(), 'a');
|
|
Fleet::Targets targets{};
|
|
const char* three = "01020304,05060708,090A0B0C";
|
|
ASSERT_TRUE(Fleet::parseTargets(three, strlen(three), targets));
|
|
EXPECT_EQ(Fleet::MaxPayloadLength, encodeTargets(targets, maximum.c_str()).size());
|
|
const char* four = "01020304,05060708,090A0B0C,01020304";
|
|
ASSERT_TRUE(Fleet::parseTargets(four, strlen(four), targets));
|
|
std::array<uint8_t, Fleet::MaxPayloadLength> output;
|
|
output.fill(0xA5);
|
|
EXPECT_EQ(0U, Fleet::encode(publisher, ChannelKey, Now, Now + 120,
|
|
targets, maximum.c_str(), output.data(), output.size()));
|
|
for (uint8_t byte : output) EXPECT_EQ(0xA5, byte);
|
|
}
|
|
|
|
TEST_F(FleetCommandTest, UnrepresentableZeroDigestNeverBecomesAllDuringLegacyEncoding) {
|
|
Fleet::Targets targets{};
|
|
targets.count = 1;
|
|
targets.length = 17;
|
|
targets.data[0] = 16;
|
|
const auto bytes = encodeTargets(targets);
|
|
ASSERT_FALSE(bytes.empty());
|
|
EXPECT_EQ(std::string("FMC2"), std::string(bytes.begin(), bytes.begin() + 4));
|
|
EXPECT_FALSE(decode(bytes));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|