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/server/__init__.py CHANGED
@@ -94,6 +94,8 @@ class Server:
94
94
  # Memory areas
95
95
  self.memory_areas: Dict[Tuple[S7Area, int], bytearray] = {}
96
96
  self.area_locks: Dict[Tuple[S7Area, int], threading.Lock] = {}
97
+ self._upload_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
98
+ self._download_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
97
99
 
98
100
  # Protocol handler
99
101
  self.protocol = S7Protocol()
@@ -733,6 +735,10 @@ class Server:
733
735
  # Send response
734
736
  if response_data:
735
737
  connection.send_data(response_data)
738
+ context = self._download_contexts.get(address)
739
+ if context is not None and context["phase"] == "start_ack":
740
+ context["phase"] = "awaiting_fragment"
741
+ connection.send_data(self._build_download_service_request(context, S7Function.DOWNLOAD_BLOCK))
736
742
 
737
743
  except socket.timeout:
738
744
  continue
@@ -758,10 +764,8 @@ class Server:
758
764
  self.clients.remove(current_thread)
759
765
  self.client_count = max(0, self.client_count - 1)
760
766
 
761
- if hasattr(self, "_download_contexts"):
762
- self._download_contexts.pop(address, None)
763
- if hasattr(self, "_upload_contexts"):
764
- self._upload_contexts.pop(address, None)
767
+ self._download_contexts.pop(address, None)
768
+ self._upload_contexts.pop(address, None)
765
769
 
766
770
  self._emit_event(EVC_CLIENT_DISCONNECTED, sender=self._event_sender(address))
767
771
  logger.info(f"Client {address} handler finished")
@@ -778,9 +782,14 @@ class Server:
778
782
  Response PDU data or None
779
783
  """
780
784
  try:
785
+ if len(request_data) >= 2 and request_data[1] == S7PDUType.ACK_DATA:
786
+ return self._handle_download_response(request_data, client_address)
781
787
  # Parse S7 request
782
788
  request = self._parse_request(request_data)
783
789
 
790
+ if client_address in self._download_contexts:
791
+ return self._build_error_response(request, 0x8104)
792
+
784
793
  # Check PDU type first
785
794
  pdu_type = request.get("pdu_type", S7PDUType.REQUEST)
786
795
 
@@ -815,9 +824,9 @@ class Server:
815
824
  elif function_code == S7Function.REQUEST_DOWNLOAD:
816
825
  return self._handle_request_download(request, client_address)
817
826
  elif function_code == S7Function.DOWNLOAD_BLOCK:
818
- return self._handle_download_block(request, client_address)
827
+ return self._build_error_response(request, 0x8104)
819
828
  elif function_code == S7Function.DOWNLOAD_ENDED:
820
- return self._handle_download_ended(request, client_address)
829
+ return self._build_error_response(request, 0x8104)
821
830
  else:
822
831
  logger.warning(f"Unsupported function code: {function_code}")
823
832
  return self._build_error_response(request, 0x8001) # Function not supported
@@ -1335,6 +1344,10 @@ class Server:
1335
1344
 
1336
1345
  data_section = pdu[offset : offset + data_len]
1337
1346
  request["data"] = self._parse_data_section(data_section)
1347
+ offset += data_len
1348
+
1349
+ if offset != len(pdu):
1350
+ raise S7ProtocolError("S7 request contains trailing bytes")
1338
1351
 
1339
1352
  return request
1340
1353
 
@@ -1898,23 +1911,34 @@ class Server:
1898
1911
  data_section = request.get("data", {})
1899
1912
  raw_data = data_section.get("data", b"")
1900
1913
 
1901
- if len(raw_data) >= 8:
1902
-
1903
- def from_bcd(value: int) -> int:
1904
- return ((value >> 4) * 10) + (value & 0x0F)
1914
+ if len(raw_data) != 10 or raw_data[:2] != b"\x00\x19":
1915
+ return self._build_userdata_error_response(request, 0x8104)
1905
1916
 
1906
- year = from_bcd(raw_data[1])
1907
- month = from_bcd(raw_data[2])
1908
- day = from_bcd(raw_data[3])
1909
- hour = from_bcd(raw_data[4])
1910
- minute = from_bcd(raw_data[5])
1911
- second = from_bcd(raw_data[6])
1917
+ def from_bcd(value: int) -> int:
1918
+ if value >> 4 > 9 or value & 0x0F > 9:
1919
+ raise ValueError("Invalid BCD digit")
1920
+ return ((value >> 4) * 10) + (value & 0x0F)
1912
1921
 
1913
- logger.info(
1914
- f"Set clock from {client_address}: 20{year:02d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
1922
+ try:
1923
+ from datetime import datetime
1924
+
1925
+ millisecond = from_bcd(raw_data[8]) * 10 + (raw_data[9] >> 4)
1926
+ if millisecond > 999 or not 1 <= (raw_data[9] & 0x0F) <= 7:
1927
+ raise ValueError("Invalid clock millisecond or weekday")
1928
+ year = from_bcd(raw_data[2])
1929
+ timestamp = datetime(
1930
+ 2000 + year if year < 90 else 1900 + year,
1931
+ from_bcd(raw_data[3]),
1932
+ from_bcd(raw_data[4]),
1933
+ from_bcd(raw_data[5]),
1934
+ from_bcd(raw_data[6]),
1935
+ from_bcd(raw_data[7]),
1936
+ millisecond * 1000,
1915
1937
  )
1916
- else:
1917
- logger.debug(f"Set clock from {client_address}: no time data provided")
1938
+ except ValueError:
1939
+ return self._build_userdata_error_response(request, 0x8104)
1940
+
1941
+ logger.info(f"Set clock from {client_address}: {timestamp}")
1918
1942
 
1919
1943
  # Return success (empty response data)
1920
1944
  return self._build_userdata_success_response(request, userdata_params, b"")
@@ -2243,37 +2267,32 @@ class Server:
2243
2267
  try:
2244
2268
  raw_params = request.get("raw_parameters", b"")
2245
2269
 
2246
- # Parse block address from parameters
2247
- # Format: function + status + reserved + upload_id + block_addr_len + block_addr
2248
- block_type = 0x41 # Default to DB
2249
- block_num = 1
2250
-
2251
- if len(raw_params) >= 10:
2252
- addr_len = raw_params[9]
2253
- if len(raw_params) >= 10 + addr_len:
2254
- block_addr = raw_params[10 : 10 + addr_len]
2255
- # Parse block address: type (2 hex) + num (5 digits) + filesystem
2256
- try:
2257
- block_type = int(block_addr[0:2], 16)
2258
- block_num = int(block_addr[2:7])
2259
- except (ValueError, IndexError):
2260
- pass
2270
+ # TReqFunStartUploadParams: function, six reserved bytes,
2271
+ # upload ID, and `_0TNNNNNA` block address.
2272
+ if (
2273
+ len(raw_params) != 18
2274
+ or raw_params[0] != S7Function.START_UPLOAD
2275
+ or raw_params[8] != 9
2276
+ or raw_params[9:11] != b"_0"
2277
+ or raw_params[17:18] != b"A"
2278
+ ):
2279
+ return self._build_error_response(request, 0x8104)
2280
+ block_type = raw_params[11]
2281
+ try:
2282
+ block_num = int(raw_params[12:17].decode("ascii"))
2283
+ except (UnicodeDecodeError, ValueError):
2284
+ return self._build_error_response(request, 0x8104)
2261
2285
 
2262
2286
  logger.info(f"Start upload request from {client_address}: type={block_type:#02x}, num={block_num}")
2263
2287
 
2264
2288
  # Generate upload ID and get block length
2265
2289
  upload_id = 1 # Simple upload ID
2266
- block_length = 0
2267
-
2268
- # Check if block exists
2269
- if block_type == 0x41: # DB
2270
- area_key = (S7Area.DB, block_num)
2271
- if area_key in self.memory_areas:
2272
- block_length = len(self.memory_areas[area_key])
2290
+ area_key = (S7Area.DB, block_num)
2291
+ if block_type != 0x41 or area_key not in self.memory_areas:
2292
+ return self._build_error_response(request, 0xD209)
2293
+ block_length = len(self.memory_areas[area_key]) + 36
2273
2294
 
2274
2295
  # Store upload context for this client
2275
- if not hasattr(self, "_upload_contexts"):
2276
- self._upload_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
2277
2296
  self._upload_contexts[client_address] = {
2278
2297
  "upload_id": upload_id,
2279
2298
  "block_type": block_type,
@@ -2281,19 +2300,10 @@ class Server:
2281
2300
  "offset": 0,
2282
2301
  }
2283
2302
 
2284
- # Build response: function + status + reserved + upload_id + block_len_string_len + block_len_string
2285
- block_len_str = f"{block_length:06d}".encode("ascii")
2286
- param_data = (
2287
- struct.pack(
2288
- ">BBBIB",
2289
- S7Function.START_UPLOAD,
2290
- 0x00, # Status
2291
- 0x00, # Reserved
2292
- upload_id,
2293
- len(block_len_str),
2294
- )
2295
- + block_len_str
2296
- )
2303
+ # TResFunStartUploadParams: function, six reserved bytes,
2304
+ # upload ID, three reserved bytes, and five ASCII length digits.
2305
+ block_len_str = f"{block_length:05d}".encode("ascii")
2306
+ param_data = bytes((S7Function.START_UPLOAD,)) + bytes(6) + bytes((upload_id,)) + bytes(3) + block_len_str
2297
2307
 
2298
2308
  header = struct.pack(
2299
2309
  ">BBHHHHBB",
@@ -2326,11 +2336,14 @@ class Server:
2326
2336
  """
2327
2337
  try:
2328
2338
  # Get upload context for this client
2329
- if not hasattr(self, "_upload_contexts") or client_address not in self._upload_contexts:
2339
+ if client_address not in self._upload_contexts:
2330
2340
  logger.warning(f"Upload request without start_upload from {client_address}")
2331
2341
  return self._build_error_response(request, 0x8104)
2332
2342
 
2333
2343
  ctx = self._upload_contexts[client_address]
2344
+ raw_params = request.get("raw_parameters", b"")
2345
+ if len(raw_params) != 8 or raw_params[0] != S7Function.UPLOAD or raw_params[7] != ctx["upload_id"]:
2346
+ return self._build_error_response(request, 0x8104)
2334
2347
  block_type = ctx["block_type"]
2335
2348
  block_num = ctx["block_num"]
2336
2349
 
@@ -2342,20 +2355,33 @@ class Server:
2342
2355
  with self.area_locks[area_key]:
2343
2356
  block_data = bytes(self.memory_areas[area_key])
2344
2357
 
2345
- logger.info(f"Upload request from {client_address}: sending {len(block_data)} bytes")
2358
+ offset = ctx["offset"]
2359
+ first_fragment = offset == 0
2360
+ compact_header = b""
2361
+ if first_fragment:
2362
+ compact_header_data = bytearray(36)
2363
+ compact_header_data[2] = 0x01
2364
+ compact_header_data[4] = 0x05 # S7 language: DB
2365
+ compact_header_data[5] = 0x0A # S7 sub-block type: DB
2366
+ struct.pack_into(">H", compact_header_data, 6, block_num)
2367
+ struct.pack_into(">I", compact_header_data, 8, len(block_data) + 36)
2368
+ struct.pack_into(">H", compact_header_data, 34, len(block_data))
2369
+ compact_header = bytes(compact_header_data)
2370
+
2371
+ chunk_capacity = 462 - len(compact_header)
2372
+ chunk = block_data[offset : offset + chunk_capacity]
2373
+ ctx["offset"] += len(chunk)
2374
+ is_last = ctx["offset"] >= len(block_data)
2375
+
2376
+ logger.info(f"Upload request from {client_address}: sending {len(chunk)} bytes")
2346
2377
 
2347
2378
  # Build response with data
2348
- # Status: 0x00 = more data, 0x01 = last packet
2349
- param_data = struct.pack(
2350
- ">BBBI",
2351
- S7Function.UPLOAD,
2352
- 0x01, # Status: last packet
2353
- 0x00, # Reserved
2354
- ctx["upload_id"],
2355
- )
2379
+ # EoU: 0x00 = end of upload, 0x01 = upload in progress.
2380
+ param_data = bytes((S7Function.UPLOAD, 0x00 if is_last else 0x01))
2356
2381
 
2357
- # Data section: length (2 bytes) + unknown (2 bytes) + data
2358
- data_section = struct.pack(">HH", len(block_data), 0x00FB) + block_data
2382
+ # The first fragment starts with the 36-byte compact block header.
2383
+ payload = compact_header + chunk
2384
+ data_section = struct.pack(">HH", len(payload), 0x00FB) + payload
2359
2385
 
2360
2386
  header = struct.pack(
2361
2387
  ">BBHHHHBB",
@@ -2387,9 +2413,18 @@ class Server:
2387
2413
  Response PDU acknowledging end of upload
2388
2414
  """
2389
2415
  try:
2390
- # Clean up upload context
2391
- if hasattr(self, "_upload_contexts") and client_address in self._upload_contexts:
2392
- del self._upload_contexts[client_address]
2416
+ if client_address not in self._upload_contexts:
2417
+ return self._build_error_response(request, 0x8104)
2418
+ ctx = self._upload_contexts[client_address]
2419
+ raw_params = request.get("raw_parameters", b"")
2420
+ if (
2421
+ len(raw_params) != 8
2422
+ or raw_params[0] != S7Function.END_UPLOAD
2423
+ or raw_params[7] != ctx["upload_id"]
2424
+ or ctx["offset"] < len(self.memory_areas[(S7Area.DB, ctx["block_num"])])
2425
+ ):
2426
+ return self._build_error_response(request, 0x8104)
2427
+ self._upload_contexts.pop(client_address, None)
2393
2428
 
2394
2429
  logger.info(f"End upload from {client_address}")
2395
2430
 
@@ -2428,20 +2463,22 @@ class Server:
2428
2463
  try:
2429
2464
  raw_params = request.get("raw_parameters", b"")
2430
2465
 
2431
- # Parse and validate the target before allocating transfer state.
2432
- if len(raw_params) < 6:
2466
+ if (
2467
+ len(raw_params) != 32
2468
+ or raw_params[:3] != bytes((S7Function.REQUEST_DOWNLOAD, 0, 1))
2469
+ or raw_params[3:8] != bytes(5)
2470
+ or raw_params[8:11] != b"\x09_0"
2471
+ or raw_params[17:20] != b"P\x0d1"
2472
+ or request.get("data_length") != 0
2473
+ or client_address in self._download_contexts
2474
+ ):
2433
2475
  return self._build_error_response(request, 0x8104)
2434
-
2435
- addr_len = raw_params[4]
2436
- address_end = 5 + addr_len
2437
- if addr_len < 7 or len(raw_params) < address_end:
2438
- return self._build_error_response(request, 0x8104)
2439
-
2440
- block_addr = raw_params[5:address_end]
2441
2476
  try:
2442
- block_type = int(block_addr[0:2], 16)
2443
- block_num = int(block_addr[2:7])
2444
- except (ValueError, IndexError):
2477
+ block_type = raw_params[11]
2478
+ block_num = int(raw_params[12:17].decode("ascii"))
2479
+ declared_size = int(raw_params[20:26].decode("ascii"))
2480
+ mc7_size = int(raw_params[26:32].decode("ascii"))
2481
+ except (UnicodeDecodeError, ValueError):
2445
2482
  return self._build_error_response(request, 0x8104)
2446
2483
 
2447
2484
  logger.info(f"Request download from {client_address}: type={block_type:#02x}, num={block_num}")
@@ -2456,35 +2493,20 @@ class Server:
2456
2493
 
2457
2494
  area_capacity = len(self.memory_areas[area_key])
2458
2495
 
2459
- # The request includes an ASCII block length. Reject an oversized
2460
- # transfer early, while still enforcing the limit on every chunk.
2461
- if len(raw_params) <= address_end:
2462
- return self._build_error_response(request, 0x8104)
2463
-
2464
- length_len = raw_params[address_end]
2465
- length_start = address_end + 1
2466
- length_end = length_start + length_len
2467
- if not length_len or len(raw_params) < length_end:
2468
- return self._build_error_response(request, 0x8104)
2469
-
2470
- try:
2471
- declared_size = int(raw_params[length_start:length_end])
2472
- except ValueError:
2473
- return self._build_error_response(request, 0x8104)
2474
- if declared_size < 0 or declared_size > area_capacity:
2496
+ if declared_size > area_capacity or mc7_size > declared_size:
2475
2497
  logger.warning(
2476
2498
  f"Download rejected: declared size {declared_size} outside DB{block_num} capacity 0..{area_capacity}"
2477
2499
  )
2478
2500
  return self._build_error_response(request, 0x8104)
2479
2501
 
2480
2502
  # Store download context
2481
- if not hasattr(self, "_download_contexts"):
2482
- self._download_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
2483
2503
  self._download_contexts[client_address] = {
2484
2504
  "block_type": block_type,
2485
2505
  "block_num": block_num,
2486
2506
  "data": bytearray(),
2487
2507
  "max_size": declared_size,
2508
+ "phase": "start_ack",
2509
+ "sequence": request["sequence"],
2488
2510
  }
2489
2511
 
2490
2512
  # Build response acknowledging download
@@ -2508,125 +2530,61 @@ class Server:
2508
2530
  logger.error(f"Error handling request download: {e}")
2509
2531
  return self._build_error_response(request, 0x8000)
2510
2532
 
2511
- def _handle_download_block(self, request: Dict[str, Any], client_address: Tuple[str, int]) -> bytes:
2512
- """
2513
- Handle download block - receive block data.
2514
-
2515
- Args:
2516
- request: Parsed S7 request
2517
- client_address: Client address for logging
2518
-
2519
- Returns:
2520
- Response PDU acknowledging data receipt
2521
- """
2522
- try:
2523
- # Get download context
2524
- if not hasattr(self, "_download_contexts") or client_address not in self._download_contexts:
2525
- logger.warning(f"Download block without request_download from {client_address}")
2526
- return self._build_error_response(request, 0x8104)
2527
-
2528
- ctx = self._download_contexts[client_address]
2529
-
2530
- # Extract data from request
2531
- data_info = request.get("data", {})
2532
- block_data = data_info.get("data", b"")
2533
-
2534
- # Bound accumulated data by the pre-registered target area. The
2535
- # final storage copy is not the only allocation: this bytearray is
2536
- # grown for every DOWNLOAD_BLOCK request.
2537
- if len(ctx["data"]) + len(block_data) > ctx["max_size"]:
2538
- logger.warning(f"Download rejected: data exceeds target capacity {ctx['max_size']}")
2539
- del self._download_contexts[client_address]
2540
- return self._build_error_response(request, 0x8104)
2541
-
2542
- ctx["data"].extend(block_data)
2543
-
2544
- logger.info(f"Download block from {client_address}: received {len(block_data)} bytes")
2545
-
2546
- # Build response
2547
- param_data = struct.pack(">B", S7Function.DOWNLOAD_BLOCK)
2548
-
2549
- header = struct.pack(
2550
- ">BBHHHHBB",
2551
- 0x32, # Protocol ID
2552
- S7PDUType.ACK_DATA, # PDU type
2553
- 0x0000, # Reserved
2554
- request["sequence"], # Sequence
2555
- len(param_data), # Parameter length
2556
- 0x0000, # Data length
2557
- 0x00, # Error class (success)
2558
- 0x00, # Error code (success)
2559
- )
2560
-
2561
- return header + param_data
2562
-
2563
- except Exception as e:
2564
- logger.error(f"Error handling download block: {e}")
2565
- return self._build_error_response(request, 0x8000)
2566
-
2567
- def _handle_download_ended(self, request: Dict[str, Any], client_address: Tuple[str, int]) -> bytes:
2568
- """
2569
- Handle download ended - finalize block storage.
2570
-
2571
- Args:
2572
- request: Parsed S7 request
2573
- client_address: Client address for logging
2574
-
2575
- Returns:
2576
- Response PDU confirming download complete
2577
- """
2578
- try:
2579
- # Get download context
2580
- if not hasattr(self, "_download_contexts") or client_address not in self._download_contexts:
2581
- logger.warning(f"Download ended without download_block from {client_address}")
2582
- return self._build_error_response(request, 0x8104)
2583
-
2584
- ctx = self._download_contexts[client_address]
2585
- block_type = ctx["block_type"]
2586
- block_num = ctx["block_num"]
2587
- block_data = ctx["data"]
2588
-
2589
- # Store block data — only into pre-registered areas.
2590
- # Reject downloads to unknown areas to prevent unbounded memory
2591
- # allocation from attacker-controlled block numbers.
2592
- if block_type == 0x41: # DB
2593
- area_key = (S7Area.DB, block_num)
2594
- if area_key in self.memory_areas:
2595
- # Update existing area - copy data into existing area without resizing
2596
- with self.area_locks[area_key]:
2597
- existing_area = self.memory_areas[area_key]
2598
- copy_len = min(len(block_data), len(existing_area))
2599
- existing_area[0:copy_len] = block_data[0:copy_len]
2600
- else:
2601
- logger.warning(f"Download rejected: area DB{block_num} not registered")
2602
- del self._download_contexts[client_address]
2603
- return self._build_error_response(request, 0x8104)
2604
-
2605
- logger.info(f"Download ended from {client_address}: stored {len(block_data)} bytes to {block_type:#02x}:{block_num}")
2606
-
2607
- # Clean up context
2608
- del self._download_contexts[client_address]
2609
-
2610
- # Build response
2611
- param_data = struct.pack(">B", S7Function.DOWNLOAD_ENDED)
2612
-
2613
- header = struct.pack(
2614
- ">BBHHHHBB",
2615
- 0x32, # Protocol ID
2616
- S7PDUType.ACK_DATA, # PDU type
2617
- 0x0000, # Reserved
2618
- request["sequence"], # Sequence
2619
- len(param_data), # Parameter length
2620
- 0x0000, # Data length
2621
- 0x00, # Error class (success)
2622
- 0x00, # Error code (success)
2623
- )
2624
-
2625
- return header + param_data
2533
+ @staticmethod
2534
+ def _build_download_service_request(context: Dict[str, Any], function: int) -> bytes:
2535
+ """Ask the client for the next block fragment or the final acknowledgement."""
2536
+ context["sequence"] = (context["sequence"] + 1) & 0xFFFF
2537
+ address = b"_0" + bytes((context["block_type"],)) + f"{context['block_num']:05d}".encode("ascii") + b"P"
2538
+ params = bytes((function,)) + bytes(7) + b"\x09" + address
2539
+ return struct.pack(">BBHHHH", 0x32, S7PDUType.REQUEST, 0, context["sequence"], len(params), 0) + params
2540
+
2541
+ def _handle_download_response(self, pdu: bytes, client_address: Tuple[str, int]) -> Optional[bytes]:
2542
+ """Advance a PLC-driven download after checking the client's ACK_DATA."""
2543
+ context = self._download_contexts.get(client_address)
2544
+ sequence = int.from_bytes(pdu[4:6], "big") if len(pdu) >= 6 else 0
2545
+ request = {"sequence": sequence}
2546
+
2547
+ def reject() -> bytes:
2548
+ self._download_contexts.pop(client_address, None)
2549
+ return self._build_error_response(request, 0x8104)
2550
+
2551
+ if context is None or len(pdu) < 12 or pdu[0:2] != bytes((0x32, S7PDUType.ACK_DATA)):
2552
+ return reject()
2553
+ _, _, _, _, param_len, data_len, error = struct.unpack(">BBHHHHH", pdu[:12])
2554
+ if sequence != context["sequence"] or error != 0 or len(pdu) != 12 + param_len + data_len:
2555
+ return reject()
2556
+ params = pdu[12 : 12 + param_len]
2557
+ data = pdu[12 + param_len :]
2558
+
2559
+ if context["phase"] == "awaiting_end":
2560
+ if params != bytes((S7Function.DOWNLOAD_ENDED,)) or data:
2561
+ return reject()
2562
+ area_key = (S7Area.DB, context["block_num"])
2563
+ if area_key not in self.memory_areas or len(context["data"]) != context["max_size"]:
2564
+ return reject()
2565
+ with self.area_locks[area_key]:
2566
+ self.memory_areas[area_key][: context["max_size"]] = context["data"]
2567
+ self._download_contexts.pop(client_address, None)
2568
+ return None
2626
2569
 
2627
- except Exception as e:
2628
- logger.error(f"Error handling download ended: {e}")
2629
- return self._build_error_response(request, 0x8000)
2570
+ if context["phase"] != "awaiting_fragment" or len(params) != 2 or params[0] != S7Function.DOWNLOAD_BLOCK:
2571
+ return reject()
2572
+ if params[1] not in (0, 1) or len(data) < 4:
2573
+ return reject()
2574
+ length, marker = struct.unpack(">HH", data[:4])
2575
+ payload = data[4:]
2576
+ if marker != 0x00FB or length != len(payload) or len(payload) > 462:
2577
+ return reject()
2578
+ new_size = len(context["data"]) + len(payload)
2579
+ if new_size > context["max_size"] or (params[1] == 0) != (new_size == context["max_size"]):
2580
+ return reject()
2581
+ if not payload and params[1] == 1:
2582
+ return reject()
2583
+ context["data"].extend(payload)
2584
+ if params[1] == 0:
2585
+ context["phase"] = "awaiting_end"
2586
+ return self._build_download_service_request(context, S7Function.DOWNLOAD_ENDED)
2587
+ return self._build_download_service_request(context, S7Function.DOWNLOAD_BLOCK)
2630
2588
 
2631
2589
  def __enter__(self) -> "Server":
2632
2590
  """Context manager entry."""
snap7/util/getters.py CHANGED
@@ -32,7 +32,7 @@ def get_bool(bytearray_: Buffer, byte_index: int, bool_index: int) -> bool:
32
32
  return current_value == index_value
33
33
 
34
34
 
35
- def get_byte(bytearray_: Buffer, byte_index: int) -> bytes:
35
+ def get_byte(bytearray_: Buffer, byte_index: int) -> int:
36
36
  """Get byte value from bytearray.
37
37
 
38
38
  Notes:
@@ -48,11 +48,11 @@ def get_byte(bytearray_: Buffer, byte_index: int) -> bytes:
48
48
  data = bytearray_[byte_index : byte_index + 1]
49
49
  data[0] = data[0] & 0xFF
50
50
  packed = struct.pack("B", *data)
51
- value: bytes = struct.unpack("B", packed)[0]
51
+ value: int = struct.unpack("B", packed)[0]
52
52
  return value
53
53
 
54
54
 
55
- def get_word(bytearray_: Buffer, byte_index: int) -> bytearray:
55
+ def get_word(bytearray_: Buffer, byte_index: int) -> int:
56
56
  """Get word value from bytearray.
57
57
 
58
58
  Notes:
@@ -73,7 +73,7 @@ def get_word(bytearray_: Buffer, byte_index: int) -> bytearray:
73
73
  data[1] = data[1] & 0xFF
74
74
  data[0] = data[0] & 0xFF
75
75
  packed = struct.pack("2B", *data)
76
- value: bytearray = struct.unpack(">H", packed)[0]
76
+ value: int = struct.unpack(">H", packed)[0]
77
77
  return value
78
78
 
79
79
 
@@ -122,7 +122,7 @@ def get_uint(bytearray_: Buffer, byte_index: int) -> int:
122
122
  >>> get_uint(data, 0)
123
123
  65535
124
124
  """
125
- return int(get_word(bytearray_, byte_index))
125
+ return get_word(bytearray_, byte_index)
126
126
 
127
127
 
128
128
  def get_real(bytearray_: Buffer, byte_index: int) -> float: