Use more faithful packet frame parsing

This commit is contained in:
Jack Kingsman
2026-03-07 22:35:53 -08:00
parent 48dab293ae
commit 34318e4814
11 changed files with 310 additions and 245 deletions
+33 -6
View File
@@ -226,7 +226,7 @@ class TestPacketFormatConversion:
def test_packet_type_extraction(self):
# Header 0x14 = type 5, route 0 (TRANSPORT_FLOOD): header + 4 transport + path_len.
data = {"timestamp": 0, "data": "140102030400", "snr": None, "rssi": None}
data = {"timestamp": 0, "data": "140102030400AA", "snr": None, "rssi": None}
result = _format_raw_packet(data, "Node", "AA" * 32)
assert result["packet_type"] == "5"
assert result["route"] == "F"
@@ -235,10 +235,10 @@ class TestPacketFormatConversion:
# Test all 4 route types (matches meshcore-packet-capture)
# TRANSPORT_FLOOD=0 -> "F", FLOOD=1 -> "F", DIRECT=2 -> "D", TRANSPORT_DIRECT=3 -> "T"
samples = [
("000102030400", "F"), # TRANSPORT_FLOOD: header + transport + path_len
("0100", "F"), # FLOOD: header + path_len
("0200", "D"), # DIRECT: header + path_len
("030102030400", "T"), # TRANSPORT_DIRECT: header + transport + path_len
("000102030400AA", "F"), # TRANSPORT_FLOOD: header + transport + path_len + payload
("0100AA", "F"), # FLOOD: header + path_len + payload
("0200AA", "D"), # DIRECT: header + path_len + payload
("030102030400AA", "T"), # TRANSPORT_DIRECT: header + transport + path_len + payload
]
for raw_hex, expected in samples:
data = {"timestamp": 0, "data": raw_hex, "snr": None, "rssi": None}
@@ -274,7 +274,7 @@ class TestPacketFormatConversion:
assert result["path"] == "aa,bb"
def test_direct_route_includes_empty_path_field(self):
data = {"timestamp": 0, "data": "0200", "snr": 1.0, "rssi": -70}
data = {"timestamp": 0, "data": "0200AA", "snr": 1.0, "rssi": -70}
result = _format_raw_packet(data, "Node", "AA" * 32)
assert result["route"] == "D"
assert "path" in result
@@ -432,6 +432,18 @@ class TestCalculatePacketHash:
raw = bytes([0x09, 0x42, 0xAA, 0xBB])
assert _calculate_packet_hash(raw) == "0" * 16
def test_reserved_mode_returns_zeroes(self):
raw = bytes([0x09, 0xC1, 0xAA, 0xBB, 0xCC])
assert _calculate_packet_hash(raw) == "0" * 16
def test_oversize_path_len_returns_zeroes(self):
raw = bytes([0x09, 0xBF]) + bytes(189) + b"payload"
assert _calculate_packet_hash(raw) == "0" * 16
def test_no_payload_returns_zeroes(self):
raw = bytes([0x09, 0x02, 0xAA, 0xBB])
assert _calculate_packet_hash(raw) == "0" * 16
def test_multibyte_transport_flood_with_2byte_hops(self):
"""TRANSPORT_FLOOD with 2-byte hops correctly skips transport codes + path."""
import hashlib
@@ -527,6 +539,21 @@ class TestDecodePacketFieldsMultibyte:
assert path_values == []
assert plen == "0"
def test_reserved_mode_returns_defaults(self):
raw = bytes([0x09, 0xC1, 0xAA, 0xBB, 0xCC])
route, ptype, plen, path_values, payload_type = _decode_packet_fields(raw)
assert (route, ptype, plen, path_values, payload_type) == ("U", "0", "0", [], None)
def test_oversize_path_len_returns_defaults(self):
raw = bytes([0x09, 0xBF]) + bytes(189) + b"payload"
route, ptype, plen, path_values, payload_type = _decode_packet_fields(raw)
assert (route, ptype, plen, path_values, payload_type) == ("U", "0", "0", [], None)
def test_no_payload_returns_defaults(self):
raw = bytes([0x09, 0x02, 0xAA, 0xBB])
route, ptype, plen, path_values, payload_type = _decode_packet_fields(raw)
assert (route, ptype, plen, path_values, payload_type) == ("U", "0", "0", [], None)
class TestCommunityMqttPublisher:
def test_initial_state(self):
+15
View File
@@ -110,6 +110,11 @@ class TestPacketParsing:
assert parse_packet(header) is None
def test_parse_packet_with_no_payload_returns_none(self):
"""Firmware rejects packets that end exactly after the path."""
packet = bytes([0x15, 0x02, 0xAA, 0xBB])
assert parse_packet(packet) is None
class TestMultiBytePathParsing:
"""Test packet parsing with multi-byte hop path encoding."""
@@ -150,6 +155,11 @@ class TestMultiBytePathParsing:
result = parse_packet(packet)
assert result is None
def test_parse_oversize_path_len_returns_none(self):
"""Oversized-but-well-formed path bytes are invalid per firmware."""
packet = bytes([0x15, 0xBF]) + bytes(189) + b"payload"
assert parse_packet(packet) is None
def test_parse_two_byte_hops_truncated_returns_none(self):
"""Truncated path data for multi-byte hops returns None."""
# path_byte = 0x42 → 2 hops × 2 bytes = 4 bytes needed, only 2 provided
@@ -184,6 +194,11 @@ class TestMultiBytePathParsing:
result = extract_payload(packet)
assert result is None
def test_extract_payload_no_payload_returns_none(self):
"""extract_payload matches firmware and rejects payload-less packets."""
packet = bytes([0x15, 0x02, 0xAA, 0xBB])
assert extract_payload(packet) is None
def test_parse_direct_two_byte_hops_with_transport(self):
"""TRANSPORT_DIRECT with 2-byte hops parses correctly."""
# Header: TRANSPORT_DIRECT = 0x03, GROUP_TEXT = 5 → (5<<2)|3 = 0x17
+22
View File
@@ -1269,3 +1269,25 @@ class TestMigration040:
]
finally:
await conn.close()
class TestMigrationPacketHelpers:
"""Test migration-local packet helpers against canonical path validation."""
def test_extract_payload_for_hash_rejects_oversize_path(self):
from app.migrations import _extract_payload_for_hash
packet = bytes([0x15, 0xBF]) + bytes(189) + b"payload"
assert _extract_payload_for_hash(packet) is None
def test_extract_payload_for_hash_rejects_no_payload_packet(self):
from app.migrations import _extract_payload_for_hash
packet = bytes([0x15, 0x02, 0xAA, 0xBB])
assert _extract_payload_for_hash(packet) is None
def test_extract_path_from_packet_rejects_reserved_mode(self):
from app.migrations import _extract_path_from_packet
packet = bytes([0x15, 0xC1, 0xAA, 0xBB, 0xCC])
assert _extract_path_from_packet(packet) is None
+29
View File
@@ -5,8 +5,10 @@ import pytest
from app.path_utils import (
decode_path_byte,
first_hop_hex,
parse_packet_envelope,
path_wire_len,
split_path_hex,
validate_path_byte,
)
@@ -81,6 +83,33 @@ class TestPathWireLen:
assert path_wire_len(0, 1) == 0
class TestValidatePathByte:
def test_accepts_valid_multibyte_path_len(self):
hop_count, hash_size, byte_len = validate_path_byte(0x42)
assert (hop_count, hash_size, byte_len) == (2, 2, 4)
def test_rejects_oversize_path(self):
with pytest.raises(ValueError, match="MAX_PATH_SIZE"):
validate_path_byte(0xBF)
class TestParsePacketEnvelope:
def test_parses_valid_packet(self):
envelope = parse_packet_envelope(bytes([0x15, 0x42, 0xAA, 0xBB, 0xCC, 0xDD]) + b"hi")
assert envelope is not None
assert envelope.hop_count == 2
assert envelope.hash_size == 2
assert envelope.path == bytes([0xAA, 0xBB, 0xCC, 0xDD])
assert envelope.payload == b"hi"
def test_rejects_packet_with_no_payload(self):
assert parse_packet_envelope(bytes([0x15, 0x02, 0xAA, 0xBB])) is None
def test_rejects_oversize_path_encoding(self):
packet = bytes([0x15, 0xBF]) + bytes(189) + b"x"
assert parse_packet_envelope(packet) is None
class TestSplitPathHex:
def test_one_byte_hops(self):
assert split_path_hex("1a2b3c", 3) == ["1a", "2b", "3c"]