PyMemoryEditor 3.3.2__py3-none-any.whl → 3.4.0__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.
@@ -8,7 +8,7 @@ Supported platforms: Windows, Linux and macOS (32-bit and 64-bit).
8
8
  """
9
9
 
10
10
  __author__ = "Jean Loui Bernard Silva de Jesus"
11
- __version__ = "3.3.2"
11
+ __version__ = "3.4.0"
12
12
 
13
13
  import logging
14
14
  import sys
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Run Python's cyclic garbage collector on the GUI thread only.
4
+
5
+ The automatic collector runs in whichever thread happens to allocate when a
6
+ threshold is crossed -- in this app, often a ``QThread`` worker in the middle
7
+ of a scan. If that collection frees a reference cycle holding Qt objects that
8
+ belong to the GUI thread, they are destroyed from the worker, which crashes
9
+ the process (a segfault on macOS, an access violation on Windows; seen on CI
10
+ in the pointer-scan worker's ``_hand_over``).
11
+
12
+ So while the app runs, automatic collection is switched off and a timer on the
13
+ GUI thread collects instead, generation by generation, as the automatic
14
+ collector would have. Plain reference counting still frees everything that is
15
+ not in a cycle immediately, in any thread.
16
+ """
17
+
18
+ import contextlib
19
+ import gc
20
+ from typing import Iterator, Optional
21
+
22
+ from PySide6.QtCore import QObject, QTimer
23
+
24
+
25
+ #: How often the GUI thread checks whether a collection is due.
26
+ COLLECT_INTERVAL_MS = 500
27
+
28
+
29
+ class MainThreadGarbageCollector(QObject):
30
+ """Disable automatic collection and collect from a GUI-thread timer.
31
+
32
+ Create it on the GUI thread; :meth:`stop` restores the collector's previous
33
+ state, so an embedder's own setting survives the app.
34
+ """
35
+
36
+ def __init__(
37
+ self, parent: Optional[QObject] = None, interval_ms: int = COLLECT_INTERVAL_MS
38
+ ) -> None:
39
+ super().__init__(parent)
40
+ self._was_enabled = gc.isenabled()
41
+ self._threshold = gc.get_threshold()
42
+ gc.disable()
43
+ self._timer = QTimer(self)
44
+ self._timer.timeout.connect(self.collect_if_due)
45
+ self._timer.start(interval_ms)
46
+
47
+ def collect_if_due(self) -> None:
48
+ """Collect each generation whose allocation count passed its threshold."""
49
+ count0, count1, count2 = gc.get_count()
50
+ if count0 > self._threshold[0]:
51
+ gc.collect(0)
52
+ if count1 > self._threshold[1]:
53
+ gc.collect(1)
54
+ if count2 > self._threshold[2]:
55
+ gc.collect(2)
56
+
57
+ def stop(self) -> None:
58
+ """Stop the timer and give automatic collection back."""
59
+ self._timer.stop()
60
+ if self._was_enabled:
61
+ gc.enable()
62
+
63
+
64
+ @contextlib.contextmanager
65
+ def garbage_collected_on_gui_thread(parent: Optional[QObject] = None) -> Iterator[None]:
66
+ """Run the body with :class:`MainThreadGarbageCollector` in charge."""
67
+ collector = MainThreadGarbageCollector(parent)
68
+ try:
69
+ yield
70
+ finally:
71
+ collector.stop()
@@ -560,10 +560,7 @@ def main(argv=None):
560
560
  from PySide6.QtCore import QSettings
561
561
  from PySide6.QtWidgets import QApplication
562
562
 
563
- from ._widgets import detached_workers_running
564
- from .main_window import MainWindow
565
- from .open_process_dialog import OpenProcessDialog
566
-
563
+ from ._gc import garbage_collected_on_gui_thread
567
564
  from ._icon import app_icon
568
565
 
569
566
  app = QApplication.instance() or QApplication(argv)
@@ -577,6 +574,18 @@ def main(argv=None):
577
574
  saved_theme = str(QSettings().value("theme", DEFAULT_THEME_ID))
578
575
  apply_theme(app, saved_theme)
579
576
 
577
+ # Every dialog from here on runs QThread workers; see _gc for why the
578
+ # cyclic collector must not run inside them.
579
+ with garbage_collected_on_gui_thread(app):
580
+ _pick_process_and_run(app)
581
+
582
+
583
+ def _pick_process_and_run(app) -> None:
584
+ """The picker, then the main window until the event loop exits."""
585
+ from ._widgets import detached_workers_running
586
+ from .main_window import MainWindow
587
+ from .open_process_dialog import OpenProcessDialog
588
+
580
589
  picker = OpenProcessDialog()
581
590
  if picker.exec() != picker.DialogCode.Accepted:
582
591
  return
@@ -196,12 +196,14 @@ class LinuxProcess(AbstractProcess):
196
196
  bufflength: Optional[int] = None,
197
197
  ) -> T:
198
198
  self.__require_target(hot_path=True)
199
+ self._require_address(address)
199
200
  return read_process_memory(
200
201
  self.pid, address, pytype, resolve_bufflength(pytype, bufflength)
201
202
  )
202
203
 
203
204
  def read_process_memory_into(self, address: int, buffer: object) -> int:
204
205
  self.__require_target(hot_path=True)
206
+ self._require_address(address)
205
207
  return read_process_memory_into(self.pid, address, buffer)
206
208
 
207
209
  def _scan_addresses(
@@ -270,6 +272,7 @@ class LinuxProcess(AbstractProcess):
270
272
  value: Union[bool, int, float, str, bytes] = UNSET,
271
273
  ) -> Union[bool, int, float, str, bytes]:
272
274
  self.__require_target()
275
+ self._require_address(address)
273
276
  w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value)
274
277
  write_process_memory(self.pid, address, w_pytype, w_length, w_value)
275
278
  return value
@@ -255,12 +255,14 @@ class MacProcess(AbstractProcess):
255
255
  bufflength: Optional[int] = None,
256
256
  ) -> T:
257
257
  self._require_open()
258
+ self._require_address(address)
258
259
  return read_process_memory(
259
260
  self.__task, address, pytype, resolve_bufflength(pytype, bufflength)
260
261
  )
261
262
 
262
263
  def read_process_memory_into(self, address: int, buffer: object) -> int:
263
264
  self._require_open()
265
+ self._require_address(address)
264
266
  return read_process_memory_into(self.__task, address, buffer)
265
267
 
266
268
  def write_process_memory(
@@ -296,6 +298,7 @@ class MacProcess(AbstractProcess):
296
298
  :param value: value to be written.
297
299
  """
298
300
  self._require_open()
301
+ self._require_address(address)
299
302
  w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value)
300
303
  write_process_memory(self.__task, address, w_pytype, w_length, w_value)
301
304
  return value
@@ -308,6 +311,7 @@ class MacProcess(AbstractProcess):
308
311
 
309
312
  def free_memory(self, address: int, size: int = 0) -> bool:
310
313
  self._require_open()
314
+ self._require_address(address)
311
315
  # mach_vm_deallocate needs the exact size. Reuse the tracked size when
312
316
  # the caller doesn't supply one.
313
317
  actual_size = size or self.__allocations.get(address, 0)
@@ -218,6 +218,12 @@ class AccessDecision:
218
218
  needs_approval: bool = False
219
219
 
220
220
 
221
+ def _self_and_ancestor_pids() -> FrozenSet[int]:
222
+ """This process, its parent and every ancestor: what ``protect_self``
223
+ refuses. A function of its own so tests can swap the real chain out."""
224
+ return frozenset({os.getpid(), os.getppid()}) | ancestor_pids()
225
+
226
+
221
227
  class ProcessPolicy:
222
228
  """Decides whether a given ``(pid, name)`` may be attached to.
223
229
 
@@ -282,9 +288,7 @@ class ProcessPolicy:
282
288
  # further up. Taken once: the chain that launched the server does not
283
289
  # change while it runs.
284
290
  self._protected: FrozenSet[int] = (
285
- frozenset({os.getpid(), os.getppid()}) | ancestor_pids()
286
- if protect_self
287
- else frozenset()
291
+ _self_and_ancestor_pids() if protect_self else frozenset()
288
292
  )
289
293
  self._identify = identify
290
294
  # Only Linux truncates names; applying the rule elsewhere would let a
@@ -424,6 +424,10 @@ def _parse_signed_int(raw: Any, field: str) -> int:
424
424
  negative = text.startswith("-")
425
425
  body = text[1:] if negative else text
426
426
  try:
427
+ # int() accepts a sign of its own, so "--5" would strip to "-5" and
428
+ # the two minuses cancel: a write of "--5" stored +5. One sign only.
429
+ if negative and body.lstrip()[:1] in ("-", "+"):
430
+ raise ValueError(body)
427
431
  value = int(body, 16) if body[:2].lower() == "0x" else int(body, 10)
428
432
  except (ValueError, IndexError):
429
433
  raise ToolError(
@@ -53,15 +53,22 @@ _POINTER_SCAN_FORMAT = "pymemoryeditor-pointerscan"
53
53
  _POINTER_SCAN_VERSION = 1
54
54
 
55
55
 
56
- class _PointerOutOfRangeError(ValueError, OSError):
57
- """A pointer chain step landed outside the address space.
56
+ class _AddressOutOfRangeError(ValueError, OSError):
57
+ """An address (or a pointer chain step) lies outside the address space.
58
58
 
59
59
  A ``ValueError`` as documented, and also an ``OSError`` because the failed
60
60
  read of the wrapped address it replaces raised one: callers that guard a
61
- stale chain with ``except OSError`` keep working.
61
+ stale chain or a bad address with ``except OSError`` keep working.
62
62
  """
63
63
 
64
64
 
65
+ # One past the highest address this host can pass to the OS. ctypes masks an
66
+ # address argument to the width of the C type it is converted to instead of
67
+ # rejecting it, so a 32-bit Python would turn 0x1_0000_1000 into 0x1000 just as
68
+ # a 64-bit one turns 2**64 + 0x1000 into 0x1000.
69
+ _HOST_ADDRESS_END = min(ADDRESS_SPACE_END, 1 << (8 * ctypes.sizeof(ctypes.c_void_p)))
70
+
71
+
65
72
  class AbstractProcess(ABC):
66
73
  """
67
74
  Abstract class to represent a process.
@@ -205,6 +212,23 @@ class AbstractProcess(ABC):
205
212
  if not self._target_alive():
206
213
  raise ProcessIDNotExistsError(self.pid)
207
214
 
215
+ @staticmethod
216
+ def _require_address(address: int) -> None:
217
+ """
218
+ Refuse an address outside ``[0, host address space)`` before it
219
+ reaches ctypes, which would mask it to the C type's width and read,
220
+ write or free a *different*, wrapped address instead of failing.
221
+ """
222
+ if not 0 <= address < _HOST_ADDRESS_END:
223
+ raise _AddressOutOfRangeError(
224
+ "address %s is outside the %d-bit address space; it would "
225
+ "wrap around to a different address."
226
+ % (
227
+ ("-0x%X" % -address) if address < 0 else ("0x%X" % address),
228
+ _HOST_ADDRESS_END.bit_length() - 1,
229
+ )
230
+ )
231
+
208
232
  @abstractmethod
209
233
  def _detect_is_64bit(self) -> Optional[bool]:
210
234
  """
@@ -736,7 +760,9 @@ class AbstractProcess(ABC):
736
760
  and ``convert_from_byte_array``. Callers that need the original
737
761
  bytes verbatim (no decoding) should pass ``pytype=bytes``.
738
762
 
739
- :raises ValueError: if ``pytype`` is unsupported.
763
+ :raises ValueError: if ``pytype`` is unsupported, or ``address`` is
764
+ negative or past the end of the address space (also an
765
+ ``OSError``, so ``except OSError`` still catches it).
740
766
  :raises OSError: if the read fails, or returns fewer bytes than
741
767
  requested (e.g. the range crosses an unreadable/freed page).
742
768
  """
@@ -770,7 +796,9 @@ class AbstractProcess(ABC):
770
796
 
771
797
  :raises TypeError: if ``buffer`` is not a writable buffer (e.g. an
772
798
  immutable ``bytes`` object).
773
- :raises ValueError: if ``buffer`` is empty or not contiguous.
799
+ :raises ValueError: if ``buffer`` is empty or not contiguous, or
800
+ ``address`` is outside the address space (as in
801
+ :meth:`read_process_memory`).
774
802
  :raises OSError: if the read fails, or returns fewer bytes than
775
803
  requested (e.g. the range crosses an unreadable/freed page).
776
804
 
@@ -821,7 +849,8 @@ class AbstractProcess(ABC):
821
849
  (``write_process_memory(address, int, 4, 99)``).
822
850
  :raises ValueError: if ``pytype`` is unsupported, or if an ``int``
823
851
  ``value`` does not fit in ``bufflength`` bytes (rejected up front
824
- rather than silently truncated).
852
+ rather than silently truncated), or if ``address`` is outside the
853
+ address space (as in :meth:`read_process_memory`).
825
854
  :raises OSError: if the write fails, or fewer bytes than requested are
826
855
  written (e.g. the range crosses an unwritable/freed page).
827
856
  :return: the original ``value`` passed in.
@@ -1080,7 +1109,8 @@ class AbstractProcess(ABC):
1080
1109
  :return: ``True`` on success.
1081
1110
 
1082
1111
  :raises ValueError: on macOS and Windows, for a region this object did
1083
- not allocate when no ``size`` is given.
1112
+ not allocate when no ``size`` is given, or for an ``address``
1113
+ outside the address space.
1084
1114
  :raises NotImplementedError: on Linux (see :meth:`allocate_memory`).
1085
1115
  """
1086
1116
  raise NotImplementedError()
@@ -1190,7 +1220,7 @@ class AbstractProcess(ABC):
1190
1220
  # it, so `ptr + offset` below zero or past 2**64 would silently
1191
1221
  # read a *different*, wrapped address. Refuse it by name instead.
1192
1222
  if not 0 <= addr < ADDRESS_SPACE_END:
1193
- raise _PointerOutOfRangeError(
1223
+ raise _AddressOutOfRangeError(
1194
1224
  "pointer chain step produced address %s, outside the "
1195
1225
  "64-bit address space; the chain is stale (a hop read "
1196
1226
  "NULL or garbage) or an offset is wrong."
@@ -1303,6 +1333,15 @@ class AbstractProcess(ABC):
1303
1333
  bounds it. (To abort the map-building phase, raise from
1304
1334
  ``progress_callback``.)
1305
1335
 
1336
+ :raises ValueError: ``ptr_size`` other than 4 or 8, ``max_depth`` or
1337
+ ``max_results`` below 1, or a negative ``max_offset``.
1338
+ :raises ClosedProcess: the process was closed (also raised by the
1339
+ returned generator once it is).
1340
+
1341
+ Arguments and access are checked at the call, like every ``search_*``
1342
+ method; the pointer map is built and walked lazily, as the generator
1343
+ is consumed.
1344
+
1306
1345
  Example
1307
1346
  -------
1308
1347
  ::
@@ -1316,11 +1355,26 @@ class AbstractProcess(ABC):
1316
1355
  live = path.rebase(process).to_pointer(process, pytype=int, bufflength=4)
1317
1356
  live.value = 9999
1318
1357
  """
1319
- from .pointer_scan import (
1320
- AddressRanges,
1321
- build_pointer_map,
1322
- find_pointer_paths,
1323
- )
1358
+ # Validate eagerly, like every search_* method: a bad argument or a
1359
+ # closed process raises here, at the call, not on the first next().
1360
+ # The map build and path search stay lazy in _iter_pointer_paths.
1361
+ #
1362
+ # A subclass written against the 3.3 contract implements the listings
1363
+ # itself and need not define these hooks (see __init_subclass__): each
1364
+ # check runs only where the subclass provides it, instead of hitting
1365
+ # the NotImplementedError of the base _require_open.
1366
+ cls = type(self)
1367
+ guarded = cls._require_open is not AbstractProcess._require_open
1368
+ if guarded or cls._require_scanning is not AbstractProcess._require_scanning:
1369
+ self._require_scanning()
1370
+
1371
+ if max_depth < 1:
1372
+ raise ValueError("max_depth must be at least 1.")
1373
+ if max_offset < 0:
1374
+ raise ValueError("max_offset must not be negative.")
1375
+ if max_results is not None and max_results < 1:
1376
+ # 0 would build the whole pointer map only to yield nothing.
1377
+ raise ValueError("max_results must be at least 1 (or None for no cap).")
1324
1378
 
1325
1379
  if ptr_size is None:
1326
1380
  ptr_size = self.pointer_size
@@ -1330,6 +1384,44 @@ class AbstractProcess(ABC):
1330
1384
  "ptr_size must be 4 (32-bit target) or 8 (64-bit target)."
1331
1385
  )
1332
1386
 
1387
+ scan = self._iter_pointer_paths(
1388
+ target_address,
1389
+ max_depth=max_depth,
1390
+ max_offset=max_offset,
1391
+ ptr_size=ptr_size,
1392
+ aligned=aligned,
1393
+ writable_only=writable_only,
1394
+ static_ranges=static_ranges,
1395
+ max_results=max_results,
1396
+ memory_regions=memory_regions,
1397
+ progress_callback=progress_callback,
1398
+ should_stop=should_stop,
1399
+ )
1400
+ return guard_generator(scan, self._require_open) if guarded else scan
1401
+
1402
+ def _iter_pointer_paths(
1403
+ self,
1404
+ target_address: int,
1405
+ *,
1406
+ max_depth: int,
1407
+ max_offset: int,
1408
+ ptr_size: int,
1409
+ aligned: bool,
1410
+ writable_only: bool,
1411
+ static_ranges: Optional[Sequence[Tuple[int, int]]],
1412
+ max_results: Optional[int],
1413
+ memory_regions: Optional[Sequence[MemoryRegion]],
1414
+ progress_callback: Optional["Callable[[float], None]"],
1415
+ should_stop: Optional["Callable[[], bool]"],
1416
+ ) -> Generator["PointerPath", None, None]:
1417
+ """The lazy body of :meth:`scan_pointer_paths`, run once arguments are
1418
+ validated."""
1419
+ from .pointer_scan import (
1420
+ AddressRanges,
1421
+ build_pointer_map,
1422
+ find_pointer_paths,
1423
+ )
1424
+
1333
1425
  if memory_regions is None:
1334
1426
  memory_regions = list(self.get_memory_regions())
1335
1427
 
@@ -71,9 +71,18 @@ def compile_pattern(
71
71
  :raises ValueError: malformed IDA-style token, or ``byte_length`` omitted
72
72
  for a regex / pre-compiled pattern.
73
73
  :raises TypeError: if ``pattern`` is not a ``str``, ``bytes`` or
74
- ``re.Pattern[bytes]``.
74
+ ``re.Pattern[bytes]`` — a compiled ``str`` regex included, as it can
75
+ never match process memory.
75
76
  """
76
77
  if isinstance(pattern, re.Pattern):
78
+ # A str pattern can't match process memory: it would only fail later,
79
+ # deep inside the scan, with "cannot use a string pattern on a
80
+ # bytes-like object".
81
+ if not isinstance(pattern.pattern, bytes):
82
+ raise TypeError(
83
+ "Pattern must be a compiled bytes regex (re.compile(rb'...')), "
84
+ "not a str one."
85
+ )
77
86
  if byte_length <= 0:
78
87
  raise ValueError(
79
88
  "byte_length= is required when passing a pre-compiled regex "
@@ -251,6 +251,8 @@ _PROCESSOR_ARCHITECTURE_64BIT = (6, 9, 12)
251
251
  _OS_IS_64BIT = (
252
252
  _native_system_information.wProcessorArchitecture in _PROCESSOR_ARCHITECTURE_64BIT
253
253
  )
254
+ # The running Python's bitness: the fallback guess when a target's can't be read.
255
+ _HOST_IS_64BIT = ctypes.sizeof(ctypes.c_void_p) == 8
254
256
 
255
257
 
256
258
  def mbi_class_for_handle(process_handle: int):
@@ -318,12 +320,13 @@ def IsProcess64Bit(process_handle: int) -> bool:
318
320
  return detected
319
321
 
320
322
  _logger.warning(
321
- "IsWow64Process failed (err=%d); assuming the target is 64-bit "
322
- "(host bitness). Pointer-width detection may be wrong if the target "
323
- "is actually a 32-bit (WOW64) process.",
323
+ "IsWow64Process failed (err=%d); assuming the target matches the "
324
+ "host bitness (%d-bit). Pointer-width detection may be wrong if it "
325
+ "does not.",
324
326
  ctypes.get_last_error(),
327
+ 64 if _HOST_IS_64BIT else 32,
325
328
  )
326
- return True
329
+ return _HOST_IS_64BIT
327
330
 
328
331
 
329
332
  T = TypeVar("T")
@@ -303,6 +303,7 @@ class WindowsProcess(AbstractProcess):
303
303
  ) -> T:
304
304
  self._require_open()
305
305
  self.__require_read()
306
+ self._require_address(address)
306
307
  return ReadProcessMemory(
307
308
  self.__process_handle,
308
309
  address,
@@ -313,6 +314,7 @@ class WindowsProcess(AbstractProcess):
313
314
  def read_process_memory_into(self, address: int, buffer: object) -> int:
314
315
  self._require_open()
315
316
  self.__require_read()
317
+ self._require_address(address)
316
318
  return ReadProcessMemoryInto(self.__process_handle, address, buffer)
317
319
 
318
320
  def write_process_memory(
@@ -324,6 +326,7 @@ class WindowsProcess(AbstractProcess):
324
326
  ) -> Union[bool, int, float, str, bytes]:
325
327
  self._require_open()
326
328
  self.__require_write()
329
+ self._require_address(address)
327
330
  w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value)
328
331
  WriteProcessMemory(
329
332
  self.__process_handle,
@@ -344,6 +347,7 @@ class WindowsProcess(AbstractProcess):
344
347
  def free_memory(self, address: int, size: int = 0) -> bool:
345
348
  self._require_open()
346
349
  self.__require_operation()
350
+ self._require_address(address)
347
351
  # MEM_RELEASE tears down whatever VirtualAlloc'd block starts at
348
352
  # `address` -- a heap segment or a thread stack just as readily as one
349
353
  # of ours -- so an address this object did not allocate is refused
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: PyMemoryEditor
3
- Version: 3.3.2
3
+ Version: 3.4.0
4
4
  Summary: Read, write and scan process memory in a few lines of Python — Cheat Engine-style scans, pointer chains and AOB search on Windows, Linux and macOS.
5
5
  Project-URL: Homepage, https://github.com/JeanExtreme002/PyMemoryEditor
6
6
  Project-URL: Documentation, https://pymemoryeditor.readthedocs.io
@@ -1,11 +1,12 @@
1
- PyMemoryEditor/__init__.py,sha256=8avktxpgstr25QlSa_QUiTFYqDRD29h3KhvNQQ0qKD0,3234
1
+ PyMemoryEditor/__init__.py,sha256=xLgUi2R-C4CS8imhiSFr5bOQrYjqc-P1amRkKBBXChQ,3234
2
2
  PyMemoryEditor/__main__.py,sha256=xukvvLjQ0BnYnnxMZda1cvyZ-BurSUk8dAGQybIqMD4,95
3
3
  PyMemoryEditor/enums.py,sha256=q_SWQYgh4F7yyAa7sZvNo2Sw91TSm3aTqPfTA7OqldM,322
4
4
  PyMemoryEditor/app/__init__.py,sha256=DGCYWpqGKPSeBa7cvzL7GmmhIfDDgF6vtim0VtLcfOo,357
5
5
  PyMemoryEditor/app/_auto_refresh_dialog.py,sha256=ktvsOaXUooQ9Hvfw2Y__IzWNZby0sW_0QlhNGrPseoE,10048
6
+ PyMemoryEditor/app/_gc.py,sha256=ulvh129qptIBxzFjfbIDQm4DMPt6WwDEniiRngv3FQ8,2533
6
7
  PyMemoryEditor/app/_icon.py,sha256=1HaaMX3HRCUD2EVIRwj_0BwPkdfj1nsVjwoTRwriJ_k,1447
7
8
  PyMemoryEditor/app/_widgets.py,sha256=vMqBfOHylEHB0jlxWmfbILEIRzjj7d6EIAFG5f7oxE0,12639
8
- PyMemoryEditor/app/application.py,sha256=Ew0KYFhIBafr7vLA1QrYU9PDpG-W645TMaG22GsJFWg,19907
9
+ PyMemoryEditor/app/application.py,sha256=RzPAPKmMmXjT6La5O_lSyqPWont6xM356DjDkWFttmY,20278
9
10
  PyMemoryEditor/app/cheat_entry.py,sha256=KOLcams-xkVEkV7rfrzhEI7IrsVvmGwsIr8cGcWdLM0,2841
10
11
  PyMemoryEditor/app/cheat_poll_worker.py,sha256=ZhDOXAGI9ObAEBSgkZLLOySUTekhlwICIfuA1XOp2Rw,12716
11
12
  PyMemoryEditor/app/cheat_table.py,sha256=FK0ZUkuEvvuXXOB64-hpxSDoGFlHNKhMFyFGgpsyXU0,46278
@@ -25,24 +26,24 @@ PyMemoryEditor/app/threads_dialog.py,sha256=HnPS7Mte6CboM0DaoqkTqnEg8Ck65L1FKW-e
25
26
  PyMemoryEditor/app/value_types.py,sha256=Ya8_n8OTkvRqeG1Zwfzsh5y990PBsjcOOfF4ZPrzlhs,18388
26
27
  PyMemoryEditor/linux/functions.py,sha256=aNFgbTBzt3U9nIBhtyKHEvWdlCRQQJhhTAmFBsu0l5Q,31766
27
28
  PyMemoryEditor/linux/libc.py,sha256=kF2GCckU5lFEOQNis29tfKzBhvafFjhtiUIBotFScZg,1385
28
- PyMemoryEditor/linux/process.py,sha256=9H6o3Nx3S9Sg4uOt4u_L6hRz7nRzM888F5VEb7O59N0,11031
29
+ PyMemoryEditor/linux/process.py,sha256=TwKlzw5hJWIV_CirLndMJbW75tndgvqApTKgf1HLR1Y,11148
29
30
  PyMemoryEditor/linux/types.py,sha256=sOgO-s9oqKrKKCHMG4N-Pe7sNw49V06PeMENSNnNFl8,1924
30
31
  PyMemoryEditor/macos/__init__.py,sha256=85mOA4U5i9EW2r1ggKqRJkqRq0e9bV3I2HdfHIvox10,72
31
32
  PyMemoryEditor/macos/functions.py,sha256=eEawTDM3i_EocxU2ENvRm2XwdrecdZ5Uigt8UdFYxMc,61157
32
33
  PyMemoryEditor/macos/libsystem.py,sha256=1MDSrp3oC5Ay1CSDt7TGEEGcScBhHGbK3aZxNKj8qTM,9245
33
- PyMemoryEditor/macos/process.py,sha256=kd_k2ZPYRGTDLEIoppT8z3D4GjHTAc1lSklxh9ooKr0,12525
34
+ PyMemoryEditor/macos/process.py,sha256=fcOkFyolK9S3ebiG83MyMQ-dOae-E1vMNXDayI3MgQo,12681
34
35
  PyMemoryEditor/macos/types.py,sha256=pYVGpROHKt8-s1C-sFsG8xEk1vmBVdh5_5-BIqtsSak,7577
35
36
  PyMemoryEditor/mcp/__init__.py,sha256=g30FgXeR3_eHq5hsGSGsPEJQlMPQ3A0jstD-MAFbU2M,2393
36
37
  PyMemoryEditor/mcp/__main__.py,sha256=GXMMDJMMZfFMjzYnRcw20wRRb3v_IKxvv_-ZOyxkK1U,475
37
38
  PyMemoryEditor/mcp/config.py,sha256=bpqkaWMI314byflMrMnySdLuuyqQJT3XTbuKIDgVLME,15862
38
- PyMemoryEditor/mcp/policy.py,sha256=QDu4I2gZRMyA6WvpzJKMNhKc9NwiUOsDSjQuqEwVKmw,25630
39
+ PyMemoryEditor/mcp/policy.py,sha256=cfG1A-IR1kTe_neLZFtd-JAqkZ_vcE4QYrPgNUv3yb0,25844
39
40
  PyMemoryEditor/mcp/procinfo.py,sha256=ufT07yoffyfA7KiBrdaQm7Hkf1WbpGveZMwcSmR8NQQ,10136
40
41
  PyMemoryEditor/mcp/server.py,sha256=YBJDMM2pXaavG_o-CnO8yL6aFecdsNfv6wEIqqard-o,33564
41
42
  PyMemoryEditor/mcp/session.py,sha256=0gy629j8lJrq4VxqUTMbxJ-D-D1IJo47T-UwtAIEzZE,31645
42
- PyMemoryEditor/mcp/toolset.py,sha256=kxkByx5YUowX7ldj1o1k0VXCPMoRNR4kxH1NHn3Z8ts,118338
43
+ PyMemoryEditor/mcp/toolset.py,sha256=Po_qLROe9WfACNHoyn1OvmHKkzEOvNhZeP8R89PDqYg,118583
43
44
  PyMemoryEditor/process/__init__.py,sha256=Xh5jngBqZyNIA9y3uwE49LxK8uH8YYsu6qotx1mP_WE,213
44
45
  PyMemoryEditor/process/_guard.py,sha256=91TKDEpFthFn99QNOioMD9hp2kzAOHA_qtkEgW9tJtI,1471
45
- PyMemoryEditor/process/abstract.py,sha256=_e9y8-4Te3VQYWwjH1ogw4KJ1FNZcKMhXR06QCF8zyc,71287
46
+ PyMemoryEditor/process/abstract.py,sha256=1cmPkKqZgb4RCZGlvEgYbL4D_RJUq6PXrB9v5YCGgoQ,75572
46
47
  PyMemoryEditor/process/errors.py,sha256=485SXkidg9_E_KjuhuF6AsQNuFZlY-Ag-sG1C6NsGMw,2054
47
48
  PyMemoryEditor/process/info.py,sha256=yF7kE-wMEs_h3rpn2dPgBpuwn2EhhVq3lfl5u7GwCfY,1484
48
49
  PyMemoryEditor/process/module_info.py,sha256=D7qAoBymhe6vrL0IYc2R6ZhWE-OPmYQzf6hSp_f2KmA,2365
@@ -54,11 +55,11 @@ PyMemoryEditor/process/thread_info.py,sha256=vwTDd3Yg2gOidU7I630Nsut-iryE5imSXnX
54
55
  PyMemoryEditor/process/util.py,sha256=QHwwjX79nygYB9ochCXtXCa8wYZ8CwU57q-cIhNajvQ,4014
55
56
  PyMemoryEditor/util/__init__.py,sha256=QI_tG9V3HA68IP-BA2-C-rLZWAADhZrbpxXGypBc69s,611
56
57
  PyMemoryEditor/util/convert.py,sha256=L-jhVBhmedd6g9IO0rL4KMKU8EZpxMNbB4eiRQkevZc,21954
57
- PyMemoryEditor/util/pattern.py,sha256=tO-4uaNIpm8tM1H0TS1qqGnDN_NTZd_yVBgm3z1wrK8,7253
58
+ PyMemoryEditor/util/pattern.py,sha256=ONRljRxGYhiElQG1E5D5LusgyZR_YLLdFtZrHFjYYkE,7724
58
59
  PyMemoryEditor/util/scan.py,sha256=-VZdwrBeXB7CQ_DKePUWjb-h-ox6_ogwYrloGkw0v6A,28752
59
60
  PyMemoryEditor/util/scan_numpy.py,sha256=SRTuhrhYP5azSab3k8FBTPVCMK9_1vZHogSSofkFCAA,6940
60
- PyMemoryEditor/win32/functions.py,sha256=35zNISzpVZmQuBY5QM_6BlLlyycWThV8ml0XPzLEhdQ,47816
61
- PyMemoryEditor/win32/process.py,sha256=9t9xtmW_rBIWh6-Jdr3CPLshmmuBNBonBgzQA3bZGc8,13562
61
+ PyMemoryEditor/win32/functions.py,sha256=mzgo1DZDFGgK0gFFaw3Hc2ljPDWjeEBzfqmac4F233U,47972
62
+ PyMemoryEditor/win32/process.py,sha256=LzTcp439esldptdAht5ZPVo0QIVqzk4pcUfGm8ktdO0,13718
62
63
  PyMemoryEditor/win32/types.py,sha256=QkFRFKviS-7DNGgsK2eCTeiinYm2BZdxsPh_pw6I570,5177
63
64
  PyMemoryEditor/win32/enums/__init__.py,sha256=RZXhXb-yCGHVDEi0OxV27w0g2l6b68BIbKhHXyTM3cw,301
64
65
  PyMemoryEditor/win32/enums/memory_allocation_states.py,sha256=NTwC9XkUqlCjoSalwykEG2OZUiqXVylcLP2nYSd39zI,953
@@ -68,8 +69,8 @@ PyMemoryEditor/win32/enums/process_operations.py,sha256=wn-aMl01VVg-2rFavR9szBNR
68
69
  PyMemoryEditor/win32/enums/standard_access_rights.py,sha256=VGWlTagW4hvbm2AjZ4NK56ZrQwRmEl9Kz9GSCG8o-HI,738
69
70
  PyMemoryEditor/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
71
  PyMemoryEditor/app/assets/icon.svg,sha256=T7OqR7ldqC_dK2Ib2ntsH2rWO-Z5RzNK4WdD1wFwgM4,7132
71
- pymemoryeditor-3.3.2.dist-info/METADATA,sha256=cH3qLkLOMOVvz73bw1jFV9zgn27si1eDzapY0zs5tEw,11037
72
- pymemoryeditor-3.3.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
73
- pymemoryeditor-3.3.2.dist-info/entry_points.txt,sha256=qxto0CWCXsjLmE0jClEgz1Xriljr6IV6d3AySjIlw7E,127
74
- pymemoryeditor-3.3.2.dist-info/licenses/LICENSE,sha256=YkOx6Nvkavq36bgiAc3eF85SBG0Qu14KMAvxmv6Ssjc,1089
75
- pymemoryeditor-3.3.2.dist-info/RECORD,,
72
+ pymemoryeditor-3.4.0.dist-info/METADATA,sha256=ol6qiQvMO8L2cJBK-YAwXI1x6lwkvHnxxX8NzZeX6S4,11037
73
+ pymemoryeditor-3.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
74
+ pymemoryeditor-3.4.0.dist-info/entry_points.txt,sha256=qxto0CWCXsjLmE0jClEgz1Xriljr6IV6d3AySjIlw7E,127
75
+ pymemoryeditor-3.4.0.dist-info/licenses/LICENSE,sha256=YkOx6Nvkavq36bgiAc3eF85SBG0Qu14KMAvxmv6Ssjc,1089
76
+ pymemoryeditor-3.4.0.dist-info/RECORD,,