Skip to content

Commit affe61d

Browse files
authored
Merge pull request #2880 from ckoehler/push-sonzsorqmwqz
refactor: split MeshTables::hasSeen into pure query + markSeen
2 parents dd87371 + 4f701b7 commit affe61d

10 files changed

Lines changed: 176 additions & 32 deletions

File tree

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,6 @@ test_build_src = yes
165165
build_src_filter =
166166
-<*>
167167
+<../src/Utils.cpp>
168+
+<../src/Packet.cpp>
168169
lib_deps =
169170
google/googletest @ 1.17.0

src/Mesh.cpp

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
5555
uint16_t offset = (uint16_t)pkt->path_len << path_sz;
5656
if (offset >= len) { // TRACE has reached end of given path
5757
onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len);
58-
} else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) {
58+
} else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->wasSeen(pkt)) {
59+
_tables->markSeen(pkt);
5960
// append SNR (Not hash!)
6061
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
6162

@@ -89,14 +90,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
8990
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
9091
return forwardMultipartDirect(pkt);
9192
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
92-
if (!_tables->hasSeen(pkt)) { // don't retransmit!
93+
if (!_tables->wasSeen(pkt)) { // don't retransmit!
94+
_tables->markSeen(pkt);
9395
removeSelfFromPath(pkt);
9496
routeDirectRecvAcks(pkt, 0);
9597
}
9698
return ACTION_RELEASE;
9799
}
98100

99-
if (!_tables->hasSeen(pkt)) {
101+
if (!_tables->wasSeen(pkt)) {
102+
_tables->markSeen(pkt);
100103
removeSelfFromPath(pkt);
101104

102105
uint32_t d = getDirectRetransmitDelay(pkt);
@@ -117,7 +120,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
117120
memcpy(&ack_crc, &pkt->payload[i], 4); i += 4;
118121
if (i > pkt->payload_len) {
119122
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime());
120-
} else if (!_tables->hasSeen(pkt)) {
123+
} else if (!_tables->wasSeen(pkt)) {
124+
_tables->markSeen(pkt);
121125
onAckRecv(pkt, ack_crc);
122126
action = routeRecvPacket(pkt);
123127
}
@@ -134,7 +138,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
134138
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
135139
if (i + CIPHER_MAC_SIZE >= pkt->payload_len) {
136140
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
137-
} else if (!_tables->hasSeen(pkt)) {
141+
} else if (!_tables->wasSeen(pkt)) {
142+
_tables->markSeen(pkt);
138143
// NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins.
139144
// For flood mode, the path may not be the 'best' in terms of hops.
140145
// FUTURE: could send back multiple paths, using createPathReturn(), and let sender choose which to use(?)
@@ -197,7 +202,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
197202
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
198203
if (i + 2 >= pkt->payload_len) {
199204
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
200-
} else if (!_tables->hasSeen(pkt)) {
205+
} else if (!_tables->wasSeen(pkt)) {
206+
_tables->markSeen(pkt);
201207
if (self_id.isHashMatch(&dest_hash)) {
202208
Identity sender(sender_pub_key);
203209

@@ -224,7 +230,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
224230
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
225231
if (i + 2 >= pkt->payload_len) {
226232
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
227-
} else if (!_tables->hasSeen(pkt)) {
233+
} else if (!_tables->wasSeen(pkt)) {
234+
_tables->markSeen(pkt);
228235
// scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM)
229236
GroupChannel channels[4];
230237
int num = searchChannelsByHash(&channel_hash, channels, 4);
@@ -255,7 +262,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
255262
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime());
256263
} else if (self_id.matches(id.pub_key)) {
257264
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime());
258-
} else if (!_tables->hasSeen(pkt)) {
265+
} else if (!_tables->wasSeen(pkt)) {
266+
_tables->markSeen(pkt);
259267
uint8_t* app_data = &pkt->payload[i];
260268
int app_data_len = pkt->payload_len - i;
261269
if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; }
@@ -282,7 +290,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
282290
break;
283291
}
284292
case PAYLOAD_TYPE_RAW_CUSTOM: {
285-
if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) {
293+
if (pkt->isRouteDirect() && !_tables->wasSeen(pkt)) {
294+
_tables->markSeen(pkt);
286295
onRawDataRecv(pkt);
287296
//action = routeRecvPacket(pkt); don't flood route these (yet)
288297
}
@@ -300,7 +309,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
300309
tmp.payload_len = pkt->payload_len - 1;
301310
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
302311

303-
if (!_tables->hasSeen(&tmp)) {
312+
if (!_tables->wasSeen(&tmp)) {
313+
_tables->markSeen(&tmp);
304314
uint32_t ack_crc;
305315
memcpy(&ack_crc, tmp.payload, 4);
306316

@@ -357,7 +367,8 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
357367
tmp.payload_len = pkt->payload_len - 1;
358368
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
359369

360-
if (!_tables->hasSeen(&tmp)) { // don't retransmit!
370+
if (!_tables->wasSeen(&tmp)) { // don't retransmit!
371+
_tables->markSeen(&tmp);
361372
removeSelfFromPath(&tmp);
362373
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
363374
}
@@ -637,7 +648,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si
637648
packet->header |= ROUTE_TYPE_FLOOD;
638649
packet->setPathHashSizeAndCount(path_hash_size, 0);
639650

640-
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
651+
_tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
641652

642653
uint8_t pri;
643654
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
@@ -666,7 +677,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m
666677
packet->transport_codes[1] = transport_codes[1];
667678
packet->setPathHashSizeAndCount(path_hash_size, 0);
668679

669-
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
680+
_tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
670681

671682
uint8_t pri;
672683
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
@@ -699,7 +710,7 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
699710
pri = 0;
700711
}
701712
}
702-
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
713+
_tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
703714
sendPacket(packet, pri, delay_millis);
704715
}
705716

@@ -709,7 +720,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) {
709720

710721
packet->path_len = 0; // path_len of zero means Zero Hop
711722

712-
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
723+
_tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
713724

714725
sendPacket(packet, 0, delay_millis);
715726
}
@@ -722,7 +733,7 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay
722733

723734
packet->path_len = 0; // path_len of zero means Zero Hop
724735

725-
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
736+
_tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
726737

727738
sendPacket(packet, 0, delay_millis);
728739
}

src/Mesh.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class GroupChannel {
1515
*/
1616
class MeshTables {
1717
public:
18-
virtual bool hasSeen(const Packet* packet) = 0;
19-
virtual void clear(const Packet* packet) = 0; // remove this packet hash from table
18+
virtual bool wasSeen(const Packet* packet) = 0;
19+
virtual void markSeen(const Packet* packet) = 0;
20+
virtual void clear(const Packet* packet) = 0; // remove this packet hash from table
2021
};
2122

2223
/**

src/helpers/SimpleMeshTables.h

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,31 @@ class SimpleMeshTables : public mesh::MeshTables {
3131
}
3232
#endif
3333

34-
bool hasSeen(const mesh::Packet* packet) override {
34+
bool wasSeen(const mesh::Packet* packet) override {
3535
uint8_t hash[MAX_HASH_SIZE];
3636
packet->calculatePacketHash(hash);
3737

3838
const uint8_t* sp = _hashes;
3939
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
40-
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
40+
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
4141
if (packet->isRouteDirect()) {
42-
_direct_dups++; // keep some stats
42+
_direct_dups++;
4343
} else {
4444
_flood_dups++;
4545
}
4646
return true;
4747
}
4848
}
49-
50-
memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
51-
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table
5249
return false;
5350
}
5451

52+
void markSeen(const mesh::Packet* packet) override {
53+
uint8_t hash[MAX_HASH_SIZE];
54+
packet->calculatePacketHash(hash);
55+
memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
56+
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES;
57+
}
58+
5559
void clear(const mesh::Packet* packet) override {
5660
uint8_t hash[MAX_HASH_SIZE];
5761
packet->calculatePacketHash(hash);

src/helpers/bridges/BridgeBase.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
3939
return;
4040
}
4141

42-
if (!_seen_packets.hasSeen(packet)) {
42+
if (!_seen_packets.wasSeen(packet)) {
43+
_seen_packets.markSeen(packet);
4344
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
4445
_mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
4546
} else {

src/helpers/bridges/BridgeBase.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class BridgeBase : public AbstractBridge {
110110
* @brief Common packet handling for received packets
111111
*
112112
* Implements the standard pattern used by all bridges:
113-
* - Check if packet was seen before using _seen_packets.hasSeen()
113+
* - Check if packet was seen before using _seen_packets.wasSeen()
114114
* - Queue packet for mesh processing if not seen before
115115
* - Free packet if already seen to prevent duplicates
116116
*

src/helpers/bridges/ESPNowBridge.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
167167
return;
168168
}
169169

170-
if (!_seen_packets.hasSeen(packet)) {
170+
if (!_seen_packets.wasSeen(packet)) {
171+
_seen_packets.markSeen(packet);
171172
// Create a temporary buffer just for size calculation and reuse for actual writing
172173
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
173174
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);

src/helpers/bridges/RS232Bridge.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
115115
return;
116116
}
117117

118-
if (!_seen_packets.hasSeen(packet)) {
118+
if (!_seen_packets.wasSeen(packet)) {
119+
_seen_packets.markSeen(packet);
119120

120121
uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
121122
uint16_t len = packet->writeTo(buffer + 4);

test/mocks/SHA256.h

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,33 @@
33
#include <stdint.h>
44
#include <stddef.h>
55

6-
// Mock SHA256 class for testing
7-
// Provides minimal interface to allow Utils.cpp to compile
6+
// Mock SHA256 for native testing — deterministic but not cryptographic.
7+
// finalize() writes real (non-garbage) output so calculatePacketHash() produces
8+
// distinguishable results for packets with different payloads.
9+
#include <string.h>
10+
811
class SHA256 {
12+
uint8_t _state[32];
13+
size_t _len;
914
public:
10-
void update(const uint8_t* data, size_t len) {}
11-
void finalize(uint8_t* hash, size_t hashLen) {}
15+
SHA256() : _len(0) { memset(_state, 0, sizeof(_state)); }
16+
17+
void update(const void* data, size_t len) {
18+
const uint8_t* bytes = static_cast<const uint8_t*>(data);
19+
for (size_t i = 0; i < len; i++) {
20+
uint8_t b = bytes[i];
21+
_state[_len % 32] ^= b;
22+
_state[(_len + 1) % 32] += (uint8_t)((b >> 1) | (b << 7));
23+
_len++;
24+
}
25+
}
26+
27+
void finalize(uint8_t* hash, size_t hashLen) {
28+
for (size_t i = 0; i < hashLen; i++) {
29+
hash[i] = _state[i % 32];
30+
}
31+
}
32+
1233
void resetHMAC(const uint8_t* key, size_t keyLen) {}
1334
void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {}
1435
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#include <gtest/gtest.h>
2+
#include "helpers/SimpleMeshTables.h"
3+
4+
using namespace mesh;
5+
6+
// Build a packet that calculatePacketHash() distinguishes by payload content.
7+
// header selects ROUTE_TYPE_FLOOD so isRouteDirect() returns false.
8+
static Packet makeFloodPacket(uint8_t seed) {
9+
Packet p;
10+
p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
11+
p.payload[0] = seed;
12+
p.payload_len = 1;
13+
p.path_len = 0;
14+
return p;
15+
}
16+
17+
static Packet makeDirectPacket(uint8_t seed) {
18+
Packet p;
19+
p.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
20+
p.payload[0] = seed;
21+
p.payload_len = 1;
22+
p.path_len = 0;
23+
return p;
24+
}
25+
26+
// ── wasSeen: pure query ───────────────────────────────────────────────────────
27+
28+
TEST(SimpleMeshTables, WasSeen_ReturnsFalseForUnseen) {
29+
SimpleMeshTables t;
30+
Packet p = makeFloodPacket(0x01);
31+
EXPECT_FALSE(t.wasSeen(&p));
32+
}
33+
34+
// wasSeen shouldn't change state
35+
TEST(SimpleMeshTables, WasSeen_IsPureQuery_DoesNotInsert) {
36+
SimpleMeshTables t;
37+
Packet p = makeFloodPacket(0x01);
38+
EXPECT_FALSE(t.wasSeen(&p));
39+
EXPECT_FALSE(t.wasSeen(&p));
40+
}
41+
42+
// ── markSeen + wasSeen ───────────────────────────────────────────────────────
43+
44+
TEST(SimpleMeshTables, MarkSeen_MakesWasSeenReturnTrue) {
45+
SimpleMeshTables t;
46+
Packet p = makeFloodPacket(0x01);
47+
t.markSeen(&p);
48+
EXPECT_TRUE(t.wasSeen(&p));
49+
}
50+
51+
TEST(SimpleMeshTables, MarkSeen_DoesNotAffectOtherPackets) {
52+
SimpleMeshTables t;
53+
Packet p1 = makeFloodPacket(0x01);
54+
Packet p2 = makeFloodPacket(0x02);
55+
t.markSeen(&p1);
56+
EXPECT_FALSE(t.wasSeen(&p2));
57+
}
58+
59+
// Canonical pattern used at every onRecvPacket call site:
60+
// if (!wasSeen(pkt)) { markSeen(pkt); process(pkt); }
61+
TEST(SimpleMeshTables, QueryThenMark_WorksCorrectly) {
62+
SimpleMeshTables t;
63+
Packet p = makeFloodPacket(0x01);
64+
EXPECT_FALSE(t.wasSeen(&p));
65+
t.markSeen(&p);
66+
EXPECT_TRUE(t.wasSeen(&p));
67+
}
68+
69+
// ── dup stats ────────────────────────────────────────────────────────────────
70+
71+
TEST(SimpleMeshTables, WasSeen_IncrementsFloodDupStat) {
72+
SimpleMeshTables t;
73+
Packet p = makeFloodPacket(0x01);
74+
t.markSeen(&p);
75+
t.wasSeen(&p);
76+
EXPECT_EQ(1u, t.getNumFloodDups());
77+
EXPECT_EQ(0u, t.getNumDirectDups());
78+
}
79+
80+
TEST(SimpleMeshTables, WasSeen_IncrementsDirectDupStat) {
81+
SimpleMeshTables t;
82+
Packet p = makeDirectPacket(0x01);
83+
t.markSeen(&p);
84+
t.wasSeen(&p);
85+
EXPECT_EQ(0u, t.getNumFloodDups());
86+
EXPECT_EQ(1u, t.getNumDirectDups());
87+
}
88+
89+
// ── clear ────────────────────────────────────────────────────────────────────
90+
91+
TEST(SimpleMeshTables, Clear_RemovesSeenPacket) {
92+
SimpleMeshTables t;
93+
Packet p = makeFloodPacket(0x01);
94+
t.markSeen(&p);
95+
ASSERT_TRUE(t.wasSeen(&p));
96+
t.clear(&p);
97+
EXPECT_FALSE(t.wasSeen(&p));
98+
}
99+
100+
int main(int argc, char** argv) {
101+
::testing::InitGoogleTest(&argc, argv);
102+
return RUN_ALL_TESTS();
103+
}

0 commit comments

Comments
 (0)