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.