mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 01:03:34 +02:00
Use more faithful packet frame parsing
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user