python-snap7 3.2.0__py3-none-any.whl → 3.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
snap7/s7protocol.py CHANGED
@@ -546,22 +546,15 @@ class S7Protocol:
546
546
  Returns:
547
547
  Complete S7 PDU for start upload request
548
548
  """
549
- # Block address string: e.g., "0A00001P" for DB1
550
- # Format: block_type (2 hex) + block_num (5 digits) + file_system (1 char)
551
- block_addr = f"{block_type:02X}{block_num:05d}A".encode("ascii")
549
+ if not 0 <= block_type <= 0xFF:
550
+ raise ValueError("block_type must fit in one byte")
551
+ if not 0 <= block_num <= 99999:
552
+ raise ValueError("block_num must be between 0 and 99999")
552
553
 
553
- # Parameters: function + status + reserved + upload_id + block_addr_len + block_addr
554
- param_data = (
555
- struct.pack(
556
- ">BBBIB",
557
- S7Function.START_UPLOAD, # Function code
558
- 0x00, # Status
559
- 0x00, # Reserved (error code)
560
- 0x00000000, # Upload ID (0 for start)
561
- len(block_addr), # Block address length
562
- )
563
- + block_addr
564
- )
554
+ # TReqFunStartUploadParams: function, six reserved bytes, one-byte
555
+ # upload ID, then the nine-byte block address `_0TNNNNNA`.
556
+ block_addr = b"_0" + bytes((block_type,)) + f"{block_num:05d}".encode("ascii") + b"A"
557
+ param_data = bytes((S7Function.START_UPLOAD,)) + bytes(7) + bytes((len(block_addr),)) + block_addr
565
558
 
566
559
  header = struct.pack(
567
560
  ">BBHHHH",
@@ -585,13 +578,9 @@ class S7Protocol:
585
578
  Returns:
586
579
  Complete S7 PDU for upload request
587
580
  """
588
- param_data = struct.pack(
589
- ">BBBI",
590
- S7Function.UPLOAD, # Function code
591
- 0x00, # Status
592
- 0x00, # Reserved
593
- upload_id, # Upload ID
594
- )
581
+ if not 0 <= upload_id <= 0xFF:
582
+ raise ValueError("upload_id must fit in one byte")
583
+ param_data = bytes((S7Function.UPLOAD,)) + bytes(6) + bytes((upload_id,))
595
584
 
596
585
  header = struct.pack(
597
586
  ">BBHHHH",
@@ -615,13 +604,9 @@ class S7Protocol:
615
604
  Returns:
616
605
  Complete S7 PDU for end upload request
617
606
  """
618
- param_data = struct.pack(
619
- ">BBBI",
620
- S7Function.END_UPLOAD, # Function code
621
- 0x00, # Status
622
- 0x00, # Reserved
623
- upload_id, # Upload ID
624
- )
607
+ if not 0 <= upload_id <= 0xFF:
608
+ raise ValueError("upload_id must fit in one byte")
609
+ param_data = bytes((S7Function.END_UPLOAD,)) + bytes(6) + bytes((upload_id,))
625
610
 
626
611
  header = struct.pack(
627
612
  ">BBHHHH",
@@ -642,24 +627,42 @@ class S7Protocol:
642
627
  Returns:
643
628
  Dictionary with upload_id and block_length
644
629
  """
645
- result = {"upload_id": 0, "block_length": 0}
646
-
647
630
  raw_params = response.get("raw_parameters", b"")
648
-
649
- if len(raw_params) >= 8:
650
- # Parse: function + status + reserved + upload_id
651
- result["upload_id"] = struct.unpack(">I", raw_params[4:8])[0]
652
- if len(raw_params) > 8:
653
- # Block length string follows
654
- len_field = raw_params[8]
655
- if len(raw_params) > 9 + len_field:
656
- length_str = raw_params[9 : 9 + len_field]
657
- try:
658
- result["block_length"] = int(length_str)
659
- except ValueError:
660
- pass
661
-
662
- return result
631
+ if len(raw_params) != 16 or raw_params[0] != S7Function.START_UPLOAD:
632
+ raise S7ProtocolError("Invalid start-upload response parameters")
633
+ try:
634
+ block_length = int(raw_params[11:16].decode("ascii"))
635
+ except (UnicodeDecodeError, ValueError) as exc:
636
+ raise S7ProtocolError("Invalid start-upload block length") from exc
637
+ return {"upload_id": raw_params[7], "block_length": block_length}
638
+
639
+ def parse_upload_fragment(
640
+ self,
641
+ response: Dict[str, Any],
642
+ *,
643
+ first_fragment: bool = False,
644
+ include_block_header: bool = False,
645
+ ) -> Tuple[bytes, bool]:
646
+ """Validate and decode one upload response fragment."""
647
+ raw_params = response.get("raw_parameters", b"")
648
+ if len(raw_params) != 2 or raw_params[0] != S7Function.UPLOAD or raw_params[1] not in (0, 1):
649
+ raise S7ProtocolError("Invalid upload response parameters")
650
+
651
+ raw_data = response.get("raw_data", b"")
652
+ if len(raw_data) < 4:
653
+ raise S7ProtocolError("Upload response data is truncated")
654
+ declared_length, marker = struct.unpack(">HH", raw_data[:4])
655
+ if marker != 0x00FB:
656
+ raise S7ProtocolError("Invalid upload response data marker")
657
+ if declared_length != len(raw_data) - 4:
658
+ raise S7ProtocolError("Upload response data length mismatch")
659
+ payload = raw_data[4:]
660
+ if first_fragment and not include_block_header:
661
+ compact_header_size = 36
662
+ if len(payload) < compact_header_size:
663
+ raise S7ProtocolError("First upload response is missing its compact block header")
664
+ payload = payload[compact_header_size:]
665
+ return payload, raw_params[1] == 0
663
666
 
664
667
  def parse_upload_response(self, response: Dict[str, Any]) -> bytes:
665
668
  """
@@ -668,13 +671,15 @@ class S7Protocol:
668
671
  Returns:
669
672
  Block data bytes
670
673
  """
671
- data_info = response.get("data", {})
672
- raw_data: bytes = data_info.get("data", b"")
674
+ payload, _ = self.parse_upload_fragment(response)
675
+ return payload
673
676
 
674
- # Skip the data header if present (length + unknown bytes)
675
- if len(raw_data) > 2:
676
- return raw_data
677
- return b""
677
+ def check_end_upload_response(self, response: Dict[str, Any]) -> None:
678
+ """Require a canonical END_UPLOAD acknowledgement."""
679
+ if response.get("raw_parameters", b"") != bytes((S7Function.END_UPLOAD,)):
680
+ raise S7ProtocolError("Invalid end-upload response")
681
+ if response.get("raw_data", b""):
682
+ raise S7ProtocolError("End-upload response must not contain data")
678
683
 
679
684
  def build_download_request(self, block_type: int, block_num: int, block_data: bytes) -> bytes:
680
685
  """
@@ -688,26 +693,20 @@ class S7Protocol:
688
693
  Returns:
689
694
  Complete S7 PDU for request download
690
695
  """
691
- # Block address string
692
- block_addr = f"{block_type:02X}{block_num:05d}P".encode("ascii")
696
+ if not 0 <= block_type <= 0xFF or not 0 <= block_num <= 99999:
697
+ raise ValueError("Invalid block type or number")
698
+ if len(block_data) > 999999:
699
+ raise ValueError("Block is too large for the download request")
700
+ if len(block_data) >= 36 and block_data[2] == 1 and block_data[4] == 5:
701
+ mc7_size = struct.unpack(">H", block_data[34:36])[0]
702
+ else:
703
+ mc7_size = len(block_data) - 36 if len(block_data) >= 36 else len(block_data)
704
+ if mc7_size > 999999:
705
+ raise ValueError("MC7 size is too large for the download request")
693
706
 
694
- # Block length as string
695
- length_str = f"{len(block_data):06d}".encode("ascii")
696
-
697
- # Parameters
698
- param_data = (
699
- struct.pack(
700
- ">BBBBB",
701
- S7Function.REQUEST_DOWNLOAD, # Function code
702
- 0x00, # Status
703
- 0x00, # Reserved
704
- 0x00, # Reserved
705
- len(block_addr), # Block address length
706
- )
707
- + block_addr
708
- + struct.pack(">B", len(length_str))
709
- + length_str
710
- )
707
+ block_addr = b"_0" + bytes((block_type,)) + f"{block_num:05d}".encode("ascii") + b"P"
708
+ sizes = b"1" + f"{len(block_data):06d}{mc7_size:06d}".encode("ascii")
709
+ param_data = bytes((S7Function.REQUEST_DOWNLOAD, 0, 1)) + bytes(5) + b"\x09" + block_addr + b"\x0d" + sizes
711
710
 
712
711
  header = struct.pack(
713
712
  ">BBHHHH",
@@ -721,6 +720,37 @@ class S7Protocol:
721
720
 
722
721
  return header + param_data
723
722
 
723
+ @staticmethod
724
+ def parse_download_service_request(pdu: bytes, function: int, block_num: int) -> int:
725
+ """Validate a PLC request for a download slice or completion acknowledgement."""
726
+ if len(pdu) != 28 or pdu[0:2] != bytes((0x32, S7PDUType.REQUEST)):
727
+ raise S7ProtocolError("Invalid PLC download request header")
728
+ sequence, param_len, data_len = struct.unpack(">HHH", pdu[4:10])
729
+ if param_len != 18 or data_len != 0:
730
+ raise S7ProtocolError("Invalid PLC download request lengths")
731
+ params = pdu[10:]
732
+ expected_address = b"_0" + bytes((0x41,)) + f"{block_num:05d}".encode("ascii") + b"P"
733
+ if params[0] != function or params[8] != 9 or params[9:] != expected_address:
734
+ raise S7ProtocolError("Unexpected PLC download request")
735
+ return int(sequence)
736
+
737
+ @staticmethod
738
+ def build_download_fragment_response(sequence: int, is_last: bool, data: bytes) -> bytes:
739
+ """Reply to a PLC DOWNLOAD_BLOCK request with one bounded fragment."""
740
+ if not 0 <= sequence <= 0xFFFF or len(data) > 0xFFFF:
741
+ raise ValueError("Invalid download response sequence or payload length")
742
+ parameters = bytes((S7Function.DOWNLOAD_BLOCK, 0 if is_last else 1))
743
+ payload = struct.pack(">HH", len(data), 0x00FB) + data
744
+ header = struct.pack(">BBHHHHH", 0x32, S7PDUType.ACK_DATA, 0, sequence, 2, len(payload), 0)
745
+ return header + parameters + payload
746
+
747
+ @staticmethod
748
+ def build_download_ended_response(sequence: int) -> bytes:
749
+ """Acknowledge the PLC's DOWNLOAD_ENDED request."""
750
+ if not 0 <= sequence <= 0xFFFF:
751
+ raise ValueError("Invalid download response sequence")
752
+ return struct.pack(">BBHHHHH", 0x32, S7PDUType.ACK_DATA, 0, sequence, 1, 0, 0) + bytes((S7Function.DOWNLOAD_ENDED,))
753
+
724
754
  def build_delete_block_request(self, block_type: int, block_num: int) -> bytes:
725
755
  """
726
756
  Build delete block request.
@@ -845,16 +875,7 @@ class S7Protocol:
845
875
  )
846
876
 
847
877
  # Data section: block type (0x30 prefix + type per Snap7 C format)
848
- data_section = struct.pack(
849
- ">BBHBBBB",
850
- 0x0A, # Return value (request)
851
- 0x00, # Transport size
852
- 0x0004, # Length (4 bytes)
853
- 0x30, # Block type indicator
854
- block_type, # Block type code
855
- 0x0A, # Trailing bytes per Snap7 C
856
- 0x00,
857
- )
878
+ data_section = struct.pack(">BBHBB", 0xFF, 0x09, 2, 0x30, block_type)
858
879
 
859
880
  # S7 header for USER_DATA
860
881
  header = struct.pack(
@@ -894,6 +915,8 @@ class S7Protocol:
894
915
 
895
916
  if not raw_data:
896
917
  return result
918
+ if len(raw_data) % 4 != 0:
919
+ raise S7ProtocolError("Block-count response length is not a multiple of four")
897
920
 
898
921
  # Parse block entries (4 bytes each: 0x30 | type | count_hi | count_lo)
899
922
  # Block type codes
@@ -913,8 +936,9 @@ class S7Protocol:
913
936
  block_type = raw_data[offset + 1]
914
937
  count = struct.unpack(">H", raw_data[offset + 2 : offset + 4])[0]
915
938
 
916
- if indicator == 0x30 and block_type in type_to_name:
917
- result[type_to_name[block_type]] = count
939
+ if indicator != 0x30 or block_type not in type_to_name:
940
+ raise S7ProtocolError("Invalid block-count response entry")
941
+ result[type_to_name[block_type]] = count
918
942
 
919
943
  offset += 4
920
944
 
@@ -937,6 +961,8 @@ class S7Protocol:
937
961
 
938
962
  if not raw_data:
939
963
  return result
964
+ if len(raw_data) % 4 != 0:
965
+ raise S7ProtocolError("Block-list response length is not a multiple of four")
940
966
 
941
967
  # Parse block entries (4 bytes each per TDataFunGetBotItem:
942
968
  # BlockNum(2) + Unknown(1) + BlockLang(1))
@@ -976,15 +1002,7 @@ class S7Protocol:
976
1002
  # Block number is 5-digit zero-padded ASCII (e.g., 1 -> "00001")
977
1003
  block_num_ascii = f"{block_num:05d}".encode("ascii")
978
1004
  data_payload = struct.pack(">BB", 0x30, block_type) + b"A" + block_num_ascii
979
- data_section = (
980
- struct.pack(
981
- ">BBH",
982
- 0x0A, # Return value (request)
983
- 0x00, # Transport size
984
- len(data_payload), # Length
985
- )
986
- + data_payload
987
- )
1005
+ data_section = struct.pack(">BBH", 0xFF, 0x09, len(data_payload)) + data_payload
988
1006
 
989
1007
  # S7 header for USER_DATA
990
1008
  header = struct.pack(
@@ -1030,8 +1048,8 @@ class S7Protocol:
1030
1048
  data_info = response.get("data", {})
1031
1049
  raw_data = data_info.get("data", b"")
1032
1050
 
1033
- if len(raw_data) < 78:
1034
- return result
1051
+ if len(raw_data) != 78:
1052
+ raise S7ProtocolError("Block-info response must contain exactly 78 bytes")
1035
1053
 
1036
1054
  # Parse block info structure per TResDataBlockInfo layout
1037
1055
  result["block_type"] = raw_data[1]
@@ -1257,17 +1275,20 @@ class S7Protocol:
1257
1275
  def to_bcd(value: int) -> int:
1258
1276
  return ((value // 10) << 4) | (value % 10)
1259
1277
 
1260
- year = dt.year % 100 # Only last 2 digits
1278
+ year = dt.year % 100
1279
+ millisecond = dt.microsecond // 1000
1261
1280
  bcd_time = struct.pack(
1262
- ">BBBBBBBB",
1263
- 0x00, # Reserved
1264
- to_bcd(year), # Year (BCD)
1281
+ ">BBBBBBBBBB",
1282
+ 0x00,
1283
+ 0x19, # Siemens date/time prefix
1284
+ to_bcd(year),
1265
1285
  to_bcd(dt.month), # Month (BCD)
1266
1286
  to_bcd(dt.day), # Day (BCD)
1267
1287
  to_bcd(dt.hour), # Hour (BCD)
1268
1288
  to_bcd(dt.minute), # Minute (BCD)
1269
1289
  to_bcd(dt.second), # Second (BCD)
1270
- (dt.weekday() + 1) & 0x0F, # Day of week (1=Monday)
1290
+ to_bcd(millisecond // 10),
1291
+ ((millisecond % 10) << 4) | ((dt.weekday() + 1) & 0x0F),
1271
1292
  )
1272
1293
 
1273
1294
  # Parameter section for USER_DATA clock request
@@ -1284,15 +1305,7 @@ class S7Protocol:
1284
1305
  )
1285
1306
 
1286
1307
  # Data section with BCD time
1287
- data_section = (
1288
- struct.pack(
1289
- ">BBH",
1290
- 0x0A, # Return value (request)
1291
- 0x00, # Transport size
1292
- len(bcd_time), # Length
1293
- )
1294
- + bcd_time
1295
- )
1308
+ data_section = struct.pack(">BBH", 0xFF, 0x09, len(bcd_time)) + bcd_time
1296
1309
 
1297
1310
  # S7 header for USER_DATA
1298
1311
  header = struct.pack(
@@ -1322,9 +1335,8 @@ class S7Protocol:
1322
1335
  data_info = response.get("data", {})
1323
1336
  raw_data = data_info.get("data", b"")
1324
1337
 
1325
- if len(raw_data) < 8:
1326
- # Return current time if no valid data
1327
- return dt_class.now().replace(microsecond=0)
1338
+ if len(raw_data) != 8:
1339
+ raise S7ProtocolError("Clock response must contain exactly eight bytes")
1328
1340
 
1329
1341
  # Parse BCD time
1330
1342
  def from_bcd(value: int) -> int:
@@ -1343,8 +1355,8 @@ class S7Protocol:
1343
1355
 
1344
1356
  try:
1345
1357
  return dt_class(full_year, month, day, hour, minute, second)
1346
- except ValueError:
1347
- return dt_class.now().replace(microsecond=0)
1358
+ except ValueError as exc:
1359
+ raise S7ProtocolError("Clock response contains an invalid BCD timestamp") from exc
1348
1360
 
1349
1361
  # ========================================================================
1350
1362
  # Session Password PDU Builders (Security / Function Group 5)
@@ -1354,8 +1366,8 @@ class S7Protocol:
1354
1366
  def encode_password(password: str) -> bytes:
1355
1367
  """Encode an S7 session password into the 8-byte wire format.
1356
1368
 
1357
- The encoding pads or truncates the password to 8 characters, XORs each
1358
- byte with ``0x55``, then rotates each byte left by 3 bits.
1369
+ Snap7 pads with spaces, XORs with ``0x55``, and chains each byte
1370
+ from the encoded byte two positions earlier.
1359
1371
 
1360
1372
  Args:
1361
1373
  password: Plaintext password (max 8 characters).
@@ -1363,12 +1375,13 @@ class S7Protocol:
1363
1375
  Returns:
1364
1376
  8-byte encoded password block.
1365
1377
  """
1366
- # Pad/truncate to exactly 8 bytes
1367
- raw = password.encode("ascii")[:8].ljust(8, b"\x00")
1378
+ raw = password.encode("ascii")
1379
+ if not 1 <= len(raw) <= 8:
1380
+ raise ValueError("Session password must contain between one and eight ASCII characters")
1381
+ raw = raw.ljust(8, b" ")
1368
1382
  encoded = bytearray(8)
1369
1383
  for i in range(8):
1370
- xored = raw[i] ^ 0x55
1371
- encoded[i] = ((xored << 3) | (xored >> 5)) & 0xFF
1384
+ encoded[i] = raw[i] ^ 0x55 ^ (encoded[i - 2] if i >= 2 else 0)
1372
1385
  return bytes(encoded)
1373
1386
 
1374
1387
  def build_set_session_password_request(self, encoded_password: bytes) -> bytes:
@@ -1396,15 +1409,9 @@ class S7Protocol:
1396
1409
  )
1397
1410
 
1398
1411
  # Data section: encoded password
1399
- data_section = (
1400
- struct.pack(
1401
- ">BBH",
1402
- 0x0A, # Return value (request)
1403
- 0x00, # Transport size
1404
- len(encoded_password), # Length
1405
- )
1406
- + encoded_password
1407
- )
1412
+ if len(encoded_password) != 8:
1413
+ raise ValueError("Encoded session password must contain eight bytes")
1414
+ data_section = struct.pack(">BBH", 0xFF, 0x09, 8) + encoded_password
1408
1415
 
1409
1416
  header = struct.pack(
1410
1417
  ">BBHHHH",
@@ -1457,7 +1464,12 @@ class S7Protocol:
1457
1464
 
1458
1465
  return header + param_data + data_section
1459
1466
 
1460
- def check_userdata_response(self, response: Dict[str, Any]) -> None:
1467
+ def check_userdata_response(
1468
+ self,
1469
+ response: Dict[str, Any],
1470
+ expected_group: int | None = None,
1471
+ expected_subfunction: int | None = None,
1472
+ ) -> None:
1461
1473
  """Check a USERDATA response for errors.
1462
1474
 
1463
1475
  Verifies both the parameter-level error code and the data section
@@ -1475,6 +1487,12 @@ class S7Protocol:
1475
1487
  if param_error != 0:
1476
1488
  error_msg = get_protocol_error_message(param_error)
1477
1489
  raise S7ProtocolError(f"USERDATA request failed: {error_msg} (0x{param_error:04x})")
1490
+ if expected_group is not None and params.get("group") != expected_group:
1491
+ raise S7ProtocolError("Unexpected USERDATA response function group")
1492
+ if expected_subfunction is not None and params.get("subfunction") != expected_subfunction:
1493
+ raise S7ProtocolError("Unexpected USERDATA response subfunction")
1494
+ else:
1495
+ raise S7ProtocolError("Missing USERDATA response parameters")
1478
1496
 
1479
1497
  data_info = response.get("data", {})
1480
1498
  if isinstance(data_info, dict):
@@ -1482,6 +1500,8 @@ class S7Protocol:
1482
1500
  if return_code != 0xFF:
1483
1501
  desc = get_return_code_description(return_code)
1484
1502
  raise S7ProtocolError(f"USERDATA request failed: {desc} (0x{return_code:02x})")
1503
+ else:
1504
+ raise S7ProtocolError("Missing USERDATA response data")
1485
1505
 
1486
1506
  def build_cpu_state_request(self) -> bytes:
1487
1507
  """
@@ -1583,6 +1603,7 @@ class S7Protocol:
1583
1603
  raise S7ProtocolError("Parameter section extends beyond PDU")
1584
1604
 
1585
1605
  param_data = pdu[offset : offset + param_len]
1606
+ response["raw_parameters"] = param_data
1586
1607
  response["parameters"] = self._parse_parameters(param_data)
1587
1608
  offset += param_len
1588
1609
 
@@ -1592,16 +1613,27 @@ class S7Protocol:
1592
1613
  raise S7ProtocolError("Data section extends beyond PDU")
1593
1614
 
1594
1615
  data_section = pdu[offset : offset + data_len]
1595
- if (response.get("parameters") or {}).get("function_code") == S7Function.WRITE_AREA:
1616
+ function_code = (response.get("parameters") or {}).get("function_code")
1617
+ if function_code == S7Function.WRITE_AREA:
1596
1618
  # WRITE data is an array of acknowledgement codes, not a READ item header.
1597
1619
  response["data"] = {"return_code": data_section[0]}
1620
+ elif function_code == S7Function.UPLOAD:
1621
+ response["data"] = {"data": data_section}
1598
1622
  else:
1599
- # Other functions (notably block upload) do not carry READ item headers.
1623
+ parameters = response.get("parameters") or {}
1624
+ validate_item_length = function_code != S7Function.READ_AREA or parameters.get("item_count") == 1
1600
1625
  response["data"] = self._parse_data_section(
1601
1626
  data_section,
1602
- validate_length=(response.get("parameters") or {}).get("function_code") == S7Function.READ_AREA,
1627
+ validate_length=validate_item_length,
1603
1628
  )
1604
1629
  response["raw_data"] = data_section
1630
+ offset += data_len
1631
+
1632
+ if offset != len(pdu):
1633
+ raise S7ProtocolError("S7 response contains trailing bytes")
1634
+
1635
+ if pdu_type == S7PDUType.USERDATA:
1636
+ self.check_userdata_response(response)
1605
1637
 
1606
1638
  return response
1607
1639
 
@@ -1612,7 +1644,9 @@ class S7Protocol:
1612
1644
 
1613
1645
  # Detect USERDATA response parameters:
1614
1646
  # byte 0 = 0x00 (reserved), len >= 12, byte 2 = 0x12, byte 4 = 0x12 (method=response)
1615
- if param_data[0] == 0x00 and len(param_data) >= 12 and param_data[2] == 0x12 and param_data[4] == 0x12:
1647
+ if param_data[0] == 0x00 and len(param_data) >= 5 and param_data[2] == 0x12 and param_data[4] == 0x12:
1648
+ if len(param_data) != 12:
1649
+ raise S7ProtocolError("USERDATA response parameters must contain exactly 12 bytes")
1616
1650
  return self._parse_userdata_response_params(param_data)
1617
1651
 
1618
1652
  function_code = param_data[0]
@@ -1718,7 +1752,9 @@ class S7Protocol:
1718
1752
  byte_length = data_length // 8
1719
1753
 
1720
1754
  if validate_length and 4 + byte_length > len(data_section):
1721
- raise S7ProtocolError("Read data item is truncated")
1755
+ raise S7ProtocolError("Data item is truncated")
1756
+ if validate_length and 4 + byte_length < len(data_section):
1757
+ raise S7ProtocolError("Data item contains trailing bytes")
1722
1758
  actual_data = data_section[4 : 4 + byte_length]
1723
1759
 
1724
1760
  return {"return_code": return_code, "transport_size": transport_size, "data_length": data_length, "data": actual_data}