From 4a495264263088ee33eb59ad52f260a5121c9a35 Mon Sep 17 00:00:00 2001 From: TommiG1 <78618252+TommiG1@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:43:53 +0200 Subject: [PATCH] Reassemble RSCP frames split across multiple socket reads E3DC_RSCP_local._receive() performed a single socket.recv(BUFFER_SIZE) call and assumed the entire encrypted RSCP frame arrived in that one read. TCP gives no such guarantee: larger responses (e.g. an extensive EH_REQ_GET_SAVED_ERRORS saved-error history) can be split across multiple reads, which crashed rscpFrameDecode() with: struct.error: unpack requires a buffer of N bytes _receive() now loops over recv() calls until the frame length declared in the header has been fully received. Each iteration re-decrypts the accumulated ciphertext from scratch on a throwaway copy of the encrypt/decrypt state (RSCPEncryptDecrypt.decrypt()'s default incremental bookkeeping only tracks the immediately preceding chunk and silently corrupts data across three or more reads), and only commits the real, persistent encdec state once the full frame is known to be available -- matching exactly what happens today when a response arrives in a single read. --- e3dc/_e3dc_rscp_local.py | 58 +++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/e3dc/_e3dc_rscp_local.py b/e3dc/_e3dc_rscp_local.py index 33da6af..b3be70c 100644 --- a/e3dc/_e3dc_rscp_local.py +++ b/e3dc/_e3dc_rscp_local.py @@ -4,15 +4,23 @@ # Copyright 2017 Francesco Santini # Licensed under a MIT license. See LICENSE for details +import copy import socket +import struct from ._RSCPEncryptDecrypt import RSCPEncryptDecrypt -from ._rscpLib import RscpMessage, rscpDecode, rscpEncode, rscpFrame +from ._rscpLib import RscpMessage, endianSwapUint16, rscpDecode, rscpEncode, rscpFrame from ._rscpTags import RscpError, RscpTag, RscpType DEFAULT_PORT = 5033 BUFFER_SIZE = 1024 * 32 +# Mirrors the frame header format used by rscpFrameDecode() in _rscpLib.py: +# magic, ctrl, sec1, sec2, ns, length (all little-endian). +_FRAME_HEADER_FMT = " None: self.socket.send(encData) def _receive(self): - data = self.socket.recv(BUFFER_SIZE) - if len(data) == 0: - raise RSCPKeyError - decData = rscpDecode(self.encdec.decrypt(data))[0] - return decData + """Read and decrypt a complete RSCP frame. + + A single ``socket.recv()`` call is not guaranteed to return an + entire frame -- TCP may split larger responses (e.g. an + extensive ``EH_REQ_GET_SAVED_ERRORS`` history) across multiple + reads. This keeps reading until the frame length declared in + the header has been fully received, instead of assuming one + read is always enough. + + ``RSCPEncryptDecrypt.decrypt()`` is stateful across calls, but + its default (``previouslyProcessedData=None``) bookkeeping + only accounts for how much of the *immediately preceding* + chunk was consumed. That is not accurate once more than one + chunk has a non-block-aligned leftover, which would silently + corrupt the result for three or more reads. To stay correct + regardless of how many reads are needed, each iteration + re-decrypts the whole accumulated ciphertext from scratch on a + throwaway copy of the encrypt/decrypt state, and only commits + the real, persistent ``self.encdec`` state once the full frame + is known to be available -- matching exactly what happens + today when a response arrives in a single read. + """ + ciphertext = b"" + while True: + chunk = self.socket.recv(BUFFER_SIZE) + if len(chunk) == 0: + raise RSCPKeyError + ciphertext += chunk + + plaintext = copy.copy(self.encdec).decrypt(ciphertext) + if len(plaintext) < _FRAME_HEADER_SIZE: + continue + + _, ctrl, _, _, _, length = struct.unpack( + _FRAME_HEADER_FMT, plaintext[:_FRAME_HEADER_SIZE] + ) + ctrl = endianSwapUint16(ctrl) + crc_len = _FRAME_CRC_SIZE if ctrl & 0x10 else 0 + needed = _FRAME_HEADER_SIZE + length + crc_len + + if len(plaintext) >= needed: + decData = rscpDecode(self.encdec.decrypt(ciphertext))[0] + return decData def sendCommand(self, plainMsg: RscpMessage) -> None: """Sending RSCP command.