PyMemoryEditor 2.0.0__tar.gz → 2.0.1__tar.gz

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.
Files changed (161) hide show
  1. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PKG-INFO +1 -1
  2. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/__init__.py +1 -1
  3. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/linux/functions.py +15 -2
  4. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/linux/process.py +5 -0
  5. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/macos/functions.py +13 -0
  6. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/macos/process.py +5 -0
  7. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/abstract.py +43 -0
  8. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/util/__init__.py +1 -0
  9. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/util/convert.py +48 -0
  10. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/functions.py +35 -0
  11. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/process.py +6 -0
  12. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/openprocess.md +17 -0
  13. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/read-write.md +54 -0
  14. pymemoryeditor-2.0.1/tests/memory/test_read_into.py +132 -0
  15. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/.flake8 +0 -0
  16. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/.gitignore +0 -0
  17. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/.readthedocs.yaml +0 -0
  18. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/CODE_OF_CONDUCT.md +0 -0
  19. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/CONTRIBUTING.md +0 -0
  20. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/LICENSE +0 -0
  21. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/Makefile +0 -0
  22. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/__main__.py +0 -0
  23. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/__init__.py +0 -0
  24. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
  25. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/_icon.py +0 -0
  26. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/_widgets.py +0 -0
  27. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/application.py +0 -0
  28. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/assets/icon.svg +0 -0
  29. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/cheat_entry.py +0 -0
  30. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
  31. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/cheat_table.py +0 -0
  32. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/log_console_dialog.py +0 -0
  33. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/main_window.py +0 -0
  34. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
  35. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/memory_viewer_dialog.py +0 -0
  36. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/modules_dialog.py +0 -0
  37. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/open_process_dialog.py +0 -0
  38. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/pointer_chain_dialog.py +0 -0
  39. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/pointer_scan_dialog.py +0 -0
  40. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/results_view.py +0 -0
  41. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/scan_types.py +0 -0
  42. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/scan_worker.py +0 -0
  43. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/scanner_panel.py +0 -0
  44. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/threads_dialog.py +0 -0
  45. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/app/value_types.py +0 -0
  46. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/enums.py +0 -0
  47. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/linux/libc.py +0 -0
  48. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/linux/types.py +0 -0
  49. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/macos/__init__.py +0 -0
  50. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/macos/libsystem.py +0 -0
  51. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/macos/types.py +0 -0
  52. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/__init__.py +0 -0
  53. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/errors.py +0 -0
  54. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/info.py +0 -0
  55. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/module_info.py +0 -0
  56. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/pointer_scan.py +0 -0
  57. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/region.py +0 -0
  58. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/remote_pointer.py +0 -0
  59. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/scanning.py +0 -0
  60. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/thread_info.py +0 -0
  61. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/process/util.py +0 -0
  62. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/py.typed +0 -0
  63. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/util/pattern.py +0 -0
  64. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/util/scan.py +0 -0
  65. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/util/scan_numpy.py +0 -0
  66. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/enums/__init__.py +0 -0
  67. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  68. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  69. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  70. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  71. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  72. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/PyMemoryEditor/win32/types.py +0 -0
  73. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/README.md +0 -0
  74. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/SECURITY.md +0 -0
  75. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/assets/screenshots/app.png +0 -0
  76. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/assets/social-preview.html +0 -0
  77. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/assets/social-preview.png +0 -0
  78. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/_static/.gitkeep +0 -0
  79. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/_static/custom.css +0 -0
  80. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/_templates/layout.html +0 -0
  81. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/enums.md +0 -0
  82. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/errors.md +0 -0
  83. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/index.md +0 -0
  84. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/memory-region.md +0 -0
  85. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/module-info.md +0 -0
  86. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/pointer-path.md +0 -0
  87. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/remote-pointer.md +0 -0
  88. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/thread-info.md +0 -0
  89. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/api/utilities.md +0 -0
  90. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/app.md +0 -0
  91. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/conf.py +0 -0
  92. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/contributing.md +0 -0
  93. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/funding.md +0 -0
  94. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/glossary.md +0 -0
  95. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/allocate-free.md +0 -0
  96. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/index.md +0 -0
  97. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/logging.md +0 -0
  98. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/memory-regions.md +0 -0
  99. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/modules-threads.md +0 -0
  100. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/opening-process.md +0 -0
  101. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/pattern-scan.md +0 -0
  102. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/pointer-scan.md +0 -0
  103. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/pointers.md +0 -0
  104. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/guide/searching.md +0 -0
  105. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/index.md +0 -0
  106. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/installation.md +0 -0
  107. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/license.md +0 -0
  108. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/platform-notes.md +0 -0
  109. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/quickstart.md +0 -0
  110. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/requirements.txt +0 -0
  111. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/troubleshooting.md +0 -0
  112. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/docs/why.md +0 -0
  113. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/pyproject.toml +0 -0
  114. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/__init__.py +0 -0
  115. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_app_cheat_entry.py +0 -0
  116. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_app_pointer_parsing.py +0 -0
  117. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_app_scan_request.py +0 -0
  118. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_app_smoke.py +0 -0
  119. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_app_value_types.py +0 -0
  120. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_cheat_poll_worker.py +0 -0
  121. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/app/test_refine_scan_worker.py +0 -0
  122. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/conftest.py +0 -0
  123. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/core/__init__.py +0 -0
  124. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/core/test_errors.py +0 -0
  125. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/core/test_logger.py +0 -0
  126. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/__init__.py +0 -0
  127. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_allocate_free.py +0 -0
  128. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_bufflength_inference.py +0 -0
  129. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_editor.py +0 -0
  130. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_partial_io.py +0 -0
  131. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_str_boundary.py +0 -0
  132. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_str_decode_consistency.py +0 -0
  133. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_typed_accessors.py +0 -0
  134. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_write_safety.py +0 -0
  135. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/memory/test_write_str_bytes_width.py +0 -0
  136. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/platforms/__init__.py +0 -0
  137. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/platforms/test_bitness.py +0 -0
  138. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/platforms/test_bitness_fallback_warns.py +0 -0
  139. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/platforms/test_linux_types.py +0 -0
  140. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/platforms/test_macos_protect.py +0 -0
  141. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/platforms/test_win32_permissions.py +0 -0
  142. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/pointer/__init__.py +0 -0
  143. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/pointer/test_pointer_chain.py +0 -0
  144. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/pointer/test_pointer_scan.py +0 -0
  145. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/pointer/test_remote_pointer.py +0 -0
  146. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/__init__.py +0 -0
  147. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/test_memory_region.py +0 -0
  148. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/test_module_enumeration.py +0 -0
  149. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/test_partial_name_match.py +0 -0
  150. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/test_process_enumeration.py +0 -0
  151. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/test_process_lookup.py +0 -0
  152. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/process/test_thread_enumeration.py +0 -0
  153. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/__init__.py +0 -0
  154. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_chunking_integration.py +0 -0
  155. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_pattern_compile.py +0 -0
  156. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_pattern_scan.py +0 -0
  157. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_region_snapshot.py +0 -0
  158. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_scan.py +0 -0
  159. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_scan_numpy.py +0 -0
  160. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_scan_properties.py +0 -0
  161. {pymemoryeditor-2.0.0 → pymemoryeditor-2.0.1}/tests/scan/test_scanning_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyMemoryEditor
3
- Version: 2.0.0
3
+ Version: 2.0.1
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
@@ -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__ = "2.0.0"
11
+ __version__ = "2.0.1"
12
12
 
13
13
  import logging
14
14
  import sys
@@ -30,6 +30,7 @@ from ..process.scanning import (
30
30
  from ..process.thread_info import ThreadInfo
31
31
  from ..util import (
32
32
  _validate_pytype,
33
+ as_writable_c_buffer,
33
34
  get_c_type_of,
34
35
  values_to_bytes,
35
36
  )
@@ -187,13 +188,13 @@ def get_memory_regions(pid: int) -> Generator["MemoryRegion", None, None]:
187
188
 
188
189
  with mapping_file:
189
190
  for line in mapping_file:
190
- region_information = line.split()
191
+ region_information = line.split(maxsplit=5)
191
192
 
192
193
  try:
193
194
  addressing_range, privileges, offset, device, inode = (
194
195
  region_information[0:5]
195
196
  )
196
- path = region_information[5] if len(region_information) >= 6 else ""
197
+ path = region_information[5].strip() if len(region_information) >= 6 else ""
197
198
 
198
199
  start_address, end_address = [
199
200
  int(addr, 16) for addr in addressing_range.split("-")
@@ -378,6 +379,18 @@ def read_process_memory(pid: int, address: int, pytype: Type[T], bufflength: int
378
379
  return data.value
379
380
 
380
381
 
382
+ def read_process_memory_into(pid: int, address: int, buffer) -> int:
383
+ """
384
+ Read ``len(buffer)`` bytes from ``address`` directly into the writable
385
+ ``buffer``, with no intermediate allocation. Returns the number of bytes
386
+ read (always the buffer's byte length on success; a short read raises
387
+ ``_LinuxPartialIOError``).
388
+ """
389
+ c_buffer = as_writable_c_buffer(buffer)
390
+ size = len(c_buffer)
391
+ return _process_vm_readv(pid, addressof(c_buffer), address, size)
392
+
393
+
381
394
  def search_addresses_by_value(
382
395
  pid: int,
383
396
  pytype: Type[T],
@@ -21,6 +21,7 @@ from .functions import (
21
21
  get_modules,
22
22
  get_threads,
23
23
  read_process_memory,
24
+ read_process_memory_into,
24
25
  search_addresses_by_pattern,
25
26
  search_addresses_by_value,
26
27
  search_values_by_addresses,
@@ -117,6 +118,10 @@ class LinuxProcess(AbstractProcess):
117
118
  self.pid, address, pytype, resolve_bufflength(pytype, bufflength)
118
119
  )
119
120
 
121
+ def read_process_memory_into(self, address: int, buffer: object) -> int:
122
+ self.__require_open()
123
+ return read_process_memory_into(self.pid, address, buffer)
124
+
120
125
  def search_by_addresses(
121
126
  self,
122
127
  pytype: Type[T],
@@ -28,6 +28,7 @@ from ..process.scanning import (
28
28
  from ..process.thread_info import ThreadInfo
29
29
  from ..util import (
30
30
  _validate_pytype,
31
+ as_writable_c_buffer,
31
32
  get_c_type_of,
32
33
  values_to_bytes,
33
34
  )
@@ -545,6 +546,18 @@ def read_process_memory(
545
546
  return data.value
546
547
 
547
548
 
549
+ def read_process_memory_into(task: int, address: int, buffer) -> int:
550
+ """
551
+ Read ``len(buffer)`` bytes from ``address`` directly into the writable
552
+ ``buffer``, with no intermediate allocation. Returns the number of bytes
553
+ read (always the buffer's byte length on success; a short read raises
554
+ ``MachPartialReadError``).
555
+ """
556
+ c_buffer = as_writable_c_buffer(buffer)
557
+ size = len(c_buffer)
558
+ return _mach_read(task, address, ctypes.addressof(c_buffer), size)
559
+
560
+
548
561
  def write_process_memory(
549
562
  task: int,
550
563
  address: int,
@@ -25,6 +25,7 @@ from .functions import (
25
25
  get_task_for_pid,
26
26
  get_threads,
27
27
  read_process_memory,
28
+ read_process_memory_into,
28
29
  release_task,
29
30
  search_addresses_by_pattern,
30
31
  search_addresses_by_value,
@@ -292,6 +293,10 @@ class MacProcess(AbstractProcess):
292
293
  self.__task, address, pytype, resolve_bufflength(pytype, bufflength)
293
294
  )
294
295
 
296
+ def read_process_memory_into(self, address: int, buffer: object) -> int:
297
+ self.__require_open()
298
+ return read_process_memory_into(self.__task, address, buffer)
299
+
295
300
  def write_process_memory(
296
301
  self,
297
302
  address: int,
@@ -449,6 +449,49 @@ class AbstractProcess(ABC):
449
449
  """
450
450
  raise NotImplementedError()
451
451
 
452
+ @abstractmethod
453
+ def read_process_memory_into(self, address: int, buffer: object) -> int:
454
+ """
455
+ Read ``len(buffer)`` bytes from ``address`` directly into a
456
+ pre-allocated, writable ``buffer`` and return the number of bytes read.
457
+
458
+ This is the zero-copy counterpart of :meth:`read_process_memory`: where
459
+ that method allocates a fresh object on every call, this one fills a
460
+ buffer you own and reuse. In a tight polling / recording loop that reads
461
+ the same-sized region over and over, reusing one buffer keeps memory use
462
+ constant instead of producing a stream of short-lived objects for the
463
+ garbage collector to reclaim.
464
+
465
+ :param address: target memory address (ex: 0x006A9EC0).
466
+ :param buffer: any writable, contiguous buffer-protocol object — a
467
+ ``bytearray``, a ``ctypes`` array, a writable ``memoryview``, a
468
+ ``numpy`` array, etc. Its byte length determines how many bytes are
469
+ read; an element-typed buffer (e.g. a ``numpy`` ``int32`` array) is
470
+ sized in **bytes**, not elements. The bytes are written in place; no
471
+ decoding is performed (this is the raw-``bytes`` read path). Decode
472
+ or reinterpret them yourself afterwards
473
+ (``int.from_bytes`` / ``struct.unpack`` / ``numpy`` views / ...).
474
+ :return: the number of bytes read — always equal to the buffer's byte
475
+ length on success (a short read raises instead, mirroring
476
+ :meth:`read_process_memory`).
477
+
478
+ :raises TypeError: if ``buffer`` is not a writable buffer (e.g. an
479
+ immutable ``bytes`` object).
480
+ :raises ValueError: if ``buffer`` is empty or not contiguous.
481
+ :raises OSError: if the read fails, or returns fewer bytes than
482
+ requested (e.g. the range crosses an unreadable/freed page).
483
+
484
+ Example
485
+ -------
486
+ ::
487
+
488
+ buffer = bytearray(16)
489
+ while recording:
490
+ process.read_process_memory_into(addr, buffer)
491
+ handle(buffer) # same buffer reused every iteration
492
+ """
493
+ raise NotImplementedError()
494
+
452
495
  @abstractmethod
453
496
  def write_process_memory(
454
497
  self,
@@ -4,6 +4,7 @@ from .convert import (
4
4
  UNSET,
5
5
  _check_int_fits,
6
6
  _validate_pytype,
7
+ as_writable_c_buffer,
7
8
  convert_from_byte_array,
8
9
  get_c_type_of,
9
10
  prepare_write,
@@ -209,6 +209,54 @@ def prepare_write(
209
209
  return pytype, length, value
210
210
 
211
211
 
212
+ def as_writable_c_buffer(buffer: Any) -> "ctypes.Array":
213
+ """
214
+ Wrap a writable buffer-protocol object as a ``ctypes`` ``c_char`` array
215
+ that shares the same underlying storage, so a backend can read process
216
+ memory *directly into the caller's buffer* with no intermediate
217
+ allocation. Returning the ``ctypes`` array (rather than a bare address)
218
+ keeps a reference to the source alive for as long as the caller holds it,
219
+ which is what makes the address safe to pass to the OS read call.
220
+
221
+ Accepts anything exposing a writable, contiguous buffer — ``bytearray``,
222
+ a ``ctypes`` array, a writable ``memoryview``, a ``numpy`` array, etc. The
223
+ array's byte length is taken from the buffer itself (``memoryview.nbytes``),
224
+ so element-typed buffers like a ``numpy`` ``int32`` array are sized in
225
+ bytes, not elements.
226
+
227
+ :raises TypeError: if ``buffer`` does not support the buffer protocol or is
228
+ read-only (e.g. ``bytes`` — use ``bytearray`` instead).
229
+ :raises ValueError: if ``buffer`` is empty or not contiguous.
230
+ """
231
+ try:
232
+ view = memoryview(buffer)
233
+ except TypeError:
234
+ raise TypeError(
235
+ "buffer must support the writable buffer protocol "
236
+ "(e.g. bytearray, a ctypes array, or a numpy array), got %s."
237
+ % type(buffer).__name__
238
+ )
239
+
240
+ try:
241
+ if view.readonly:
242
+ raise TypeError(
243
+ "buffer must be writable; got a read-only buffer "
244
+ "(e.g. bytes). Use bytearray or another writable buffer."
245
+ )
246
+ if not view.contiguous:
247
+ raise ValueError("buffer must be contiguous.")
248
+ nbytes = view.nbytes
249
+ finally:
250
+ # Release the inspection view promptly so it never lingers as an extra
251
+ # export on the source object (e.g. blocking a later bytearray resize).
252
+ view.release()
253
+
254
+ if nbytes == 0:
255
+ raise ValueError("buffer must have a non-zero length.")
256
+
257
+ return (ctypes.c_char * nbytes).from_buffer(buffer)
258
+
259
+
212
260
  def convert_from_byte_array(
213
261
  byte_array: ctypes.Array, pytype: Type[T], length: int
214
262
  ) -> T:
@@ -27,6 +27,7 @@ from ..process.scanning import (
27
27
  from ..process.thread_info import ThreadInfo
28
28
  from ..util import (
29
29
  _validate_pytype,
30
+ as_writable_c_buffer,
30
31
  get_c_type_of,
31
32
  values_to_bytes,
32
33
  )
@@ -501,6 +502,40 @@ def ReadProcessMemory(
501
502
  return data.value
502
503
 
503
504
 
505
+ def ReadProcessMemoryInto(process_handle: int, address: int, buffer) -> int:
506
+ """
507
+ Read ``len(buffer)`` bytes from ``address`` directly into the writable
508
+ ``buffer``, with no intermediate allocation. Returns the number of bytes
509
+ read.
510
+
511
+ Raises OSError if the read fails or returns fewer bytes than requested
512
+ (same partial-read guard as :func:`ReadProcessMemory`).
513
+ """
514
+ c_buffer = as_writable_c_buffer(buffer)
515
+ size = len(c_buffer)
516
+ bytes_read = ctypes.c_size_t(0)
517
+
518
+ ctypes.set_last_error(0)
519
+ success = kernel32.ReadProcessMemory(
520
+ process_handle,
521
+ ctypes.c_void_p(address),
522
+ ctypes.byref(c_buffer),
523
+ size,
524
+ ctypes.byref(bytes_read),
525
+ )
526
+
527
+ if not success:
528
+ _raise_last_error("ReadProcessMemory")
529
+
530
+ if bytes_read.value != size:
531
+ raise OSError(
532
+ "ReadProcessMemory partial read at 0x%X: %d of %d bytes read."
533
+ % (address, bytes_read.value, size)
534
+ )
535
+
536
+ return bytes_read.value
537
+
538
+
504
539
  def _read_region(process_handle: int, address: int, size: int):
505
540
  """Read a memory region; returns the byte buffer or None on failure."""
506
541
  region_data = (ctypes.c_byte * size)()
@@ -27,6 +27,7 @@ from .functions import (
27
27
  GetProcessHandle,
28
28
  GetThreads,
29
29
  ReadProcessMemory,
30
+ ReadProcessMemoryInto,
30
31
  SearchAddressesByPattern,
31
32
  SearchAddressesByValue,
32
33
  SearchValuesByAddresses,
@@ -313,6 +314,11 @@ class WindowsProcess(AbstractProcess):
313
314
  resolve_bufflength(pytype, bufflength),
314
315
  )
315
316
 
317
+ def read_process_memory_into(self, address: int, buffer: object) -> int:
318
+ self.__require_open()
319
+ self.__require_read()
320
+ return ReadProcessMemoryInto(self.__process_handle, address, buffer)
321
+
316
322
  def write_process_memory(
317
323
  self,
318
324
  address: int,
@@ -158,6 +158,23 @@ with OpenProcess(
158
158
  :returns: the original ``value`` you passed in — **not** the truncated/encoded
159
159
  form actually written (a capped ``str``/``bytes`` write returns the full
160
160
  original value).
161
+
162
+ .. py:method:: read_process_memory_into(address, buffer)
163
+
164
+ Read ``len(buffer)`` raw bytes from ``address`` directly into a
165
+ pre-allocated, writable ``buffer`` (no intermediate allocation) — the
166
+ zero-copy counterpart of :py:meth:`read_process_memory` for tight
167
+ read-the-same-region loops. See :doc:`../guide/read-write` for examples.
168
+
169
+ :param int address: target memory address.
170
+ :param buffer: any writable, contiguous buffer-protocol object
171
+ (``bytearray``, ``ctypes`` array, writable ``memoryview``, ``numpy``
172
+ array, …). Its byte length sets how many bytes are read; the bytes land
173
+ verbatim (no decoding).
174
+ :returns: the number of bytes read (the buffer's byte length on success).
175
+ :raises TypeError: if ``buffer`` is not a writable buffer (e.g. ``bytes``).
176
+ :raises ValueError: if ``buffer`` is empty or not contiguous.
177
+ :raises OSError: if the read fails or returns fewer bytes than requested.
161
178
  ```
162
179
 
163
180
  ### Typed shortcuts
@@ -195,6 +195,60 @@ Need the raw bytes with zero interpretation? Use `read_bytes(address, length)`
195
195
  and `write_bytes(address, data)`.
196
196
  ```
197
197
 
198
+ ## Reusing a buffer (zero-copy reads)
199
+
200
+ Every `read_process_memory` call **allocates a fresh Python object** for the
201
+ result. That's fine for one-off reads, but in a tight loop that reads the same
202
+ region thousands of times — a recorder, a live overlay, a poller — those
203
+ throwaway objects pile up and keep the garbage collector busy.
204
+
205
+ `read_process_memory_into(address, buffer)` reads straight into a buffer **you
206
+ own and reuse**, so a long-running loop runs with constant memory instead of a
207
+ steady stream of allocations:
208
+
209
+ ```python
210
+ buffer = bytearray(16) # allocate once
211
+
212
+ with OpenProcess(name="game.exe") as process:
213
+ while recording:
214
+ process.read_process_memory_into(0x7FF40010, buffer)
215
+ handle(buffer) # same buffer, refilled in place every loop
216
+ ```
217
+
218
+ It fills **`len(buffer)` bytes** (the buffer's size decides how much is read)
219
+ and returns the number of bytes read. The bytes land verbatim — no decoding —
220
+ so reinterpret them yourself with `int.from_bytes`, `struct.unpack`, a `numpy`
221
+ view, and so on.
222
+
223
+ Any writable, contiguous buffer works — a `bytearray`, a `ctypes` array, a
224
+ writable `memoryview`, or a `numpy` array (sized in **bytes**, so a 4-element
225
+ `int32` array reads 16 bytes):
226
+
227
+ ```python
228
+ import numpy as np
229
+
230
+ frame = np.zeros(4, dtype=np.int32) # 16 bytes
231
+ process.read_process_memory_into(0x7FF40010, frame)
232
+ # frame now holds the four int32 values, no per-read allocation
233
+ ```
234
+
235
+ ### Method signature
236
+
237
+ ```{eval-rst}
238
+ .. py:method:: read_process_memory_into(address, buffer)
239
+ :no-index:
240
+
241
+ :param int address: target memory address.
242
+ :param buffer: a writable, contiguous buffer-protocol object
243
+ (``bytearray``, ``ctypes`` array, writable ``memoryview``, ``numpy``
244
+ array, …). Its byte length sets how many bytes are read; the bytes are
245
+ written in place with no decoding.
246
+ :return: the number of bytes read (the buffer's byte length on success).
247
+ :raises TypeError: if ``buffer`` is not a writable buffer (e.g. ``bytes``).
248
+ :raises ValueError: if ``buffer`` is empty or not contiguous.
249
+ :raises OSError: if the read fails or returns fewer bytes than requested.
250
+ ```
251
+
198
252
  ## Common errors
199
253
 
200
254
  - **`OSError`** — the address may have been freed between scan and write, or
@@ -0,0 +1,132 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ Tests for ``read_process_memory_into`` — the zero-copy read that fills a
5
+ caller-owned, reusable buffer instead of allocating a fresh object per call
6
+ (GitHub issue #71).
7
+
8
+ As with the typed-accessor tests, we plant ctypes values on the test's own
9
+ heap and read them back through the public API.
10
+ """
11
+
12
+ import ctypes
13
+ import os
14
+ import sys
15
+
16
+ import pytest
17
+
18
+ if sys.platform not in ("win32", "darwin") and not sys.platform.startswith("linux"):
19
+ pytest.skip("Platform not supported by PyMemoryEditor", allow_module_level=True)
20
+
21
+
22
+ from PyMemoryEditor import OpenProcess # noqa: E402
23
+
24
+
25
+ @pytest.fixture
26
+ def process():
27
+ handle = OpenProcess(pid=os.getpid())
28
+ try:
29
+ yield handle
30
+ finally:
31
+ handle.close()
32
+
33
+
34
+ def test_reads_into_bytearray(process):
35
+ source = (ctypes.c_uint8 * 4)(0xDE, 0xAD, 0xBE, 0xEF)
36
+ buffer = bytearray(4)
37
+
38
+ read = process.read_process_memory_into(ctypes.addressof(source), buffer)
39
+
40
+ assert read == 4
41
+ assert bytes(buffer) == b"\xde\xad\xbe\xef"
42
+
43
+
44
+ def test_returns_buffer_byte_length(process):
45
+ source = (ctypes.c_uint8 * 16)(*range(16))
46
+ buffer = bytearray(16)
47
+
48
+ assert process.read_process_memory_into(ctypes.addressof(source), buffer) == 16
49
+ assert bytes(buffer) == bytes(source)
50
+
51
+
52
+ def test_buffer_can_be_reused_across_reads(process):
53
+ """The whole point of the API: one buffer, many reads, no new objects."""
54
+ first = (ctypes.c_uint8 * 8)(1, 2, 3, 4, 5, 6, 7, 8)
55
+ second = (ctypes.c_uint8 * 8)(8, 7, 6, 5, 4, 3, 2, 1)
56
+ buffer = bytearray(8)
57
+
58
+ process.read_process_memory_into(ctypes.addressof(first), buffer)
59
+ assert bytes(buffer) == bytes(first)
60
+
61
+ process.read_process_memory_into(ctypes.addressof(second), buffer)
62
+ assert bytes(buffer) == bytes(second)
63
+
64
+
65
+ def test_buffer_length_decides_how_many_bytes_are_read(process):
66
+ """A 4-byte buffer reads exactly 4 bytes from an 8-byte source."""
67
+ source = (ctypes.c_uint8 * 8)(0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44)
68
+ buffer = bytearray(4)
69
+
70
+ process.read_process_memory_into(ctypes.addressof(source), buffer)
71
+
72
+ assert bytes(buffer) == b"\xaa\xbb\xcc\xdd"
73
+
74
+
75
+ def test_reads_into_ctypes_array(process):
76
+ source = (ctypes.c_uint8 * 4)(0x01, 0x02, 0x03, 0x04)
77
+ destination = (ctypes.c_uint8 * 4)()
78
+
79
+ process.read_process_memory_into(ctypes.addressof(source), destination)
80
+
81
+ assert bytes(destination) == b"\x01\x02\x03\x04"
82
+
83
+
84
+ def test_reads_into_writable_memoryview(process):
85
+ source = (ctypes.c_uint8 * 4)(0x10, 0x20, 0x30, 0x40)
86
+ backing = bytearray(4)
87
+
88
+ process.read_process_memory_into(ctypes.addressof(source), memoryview(backing))
89
+
90
+ assert bytes(backing) == b"\x10\x20\x30\x40"
91
+
92
+
93
+ def test_element_typed_buffer_sized_in_bytes(process):
94
+ """A 2-element int32 buffer is 8 bytes, not 2."""
95
+ source = (ctypes.c_int32 * 2)(0x01020304, 0x05060708)
96
+ destination = (ctypes.c_int32 * 2)()
97
+
98
+ read = process.read_process_memory_into(ctypes.addressof(source), destination)
99
+
100
+ assert read == 8
101
+ assert destination[0] == 0x01020304
102
+ assert destination[1] == 0x05060708
103
+
104
+
105
+ def test_matches_read_process_memory(process):
106
+ """The bytes filled in must equal the plain read_process_memory result."""
107
+ source = (ctypes.c_uint8 * 6)(0x09, 0x08, 0x07, 0x06, 0x05, 0x04)
108
+ addr = ctypes.addressof(source)
109
+
110
+ expected = process.read_process_memory(addr, bytes, 6)
111
+ buffer = bytearray(6)
112
+ process.read_process_memory_into(addr, buffer)
113
+
114
+ assert bytes(buffer) == expected
115
+
116
+
117
+ def test_read_only_buffer_is_rejected(process):
118
+ source = (ctypes.c_uint8 * 4)(0, 0, 0, 0)
119
+ with pytest.raises(TypeError):
120
+ process.read_process_memory_into(ctypes.addressof(source), b"immutable")
121
+
122
+
123
+ def test_empty_buffer_is_rejected(process):
124
+ source = (ctypes.c_uint8 * 4)(0, 0, 0, 0)
125
+ with pytest.raises(ValueError):
126
+ process.read_process_memory_into(ctypes.addressof(source), bytearray(0))
127
+
128
+
129
+ def test_non_buffer_is_rejected(process):
130
+ source = (ctypes.c_uint8 * 4)(0, 0, 0, 0)
131
+ with pytest.raises(TypeError):
132
+ process.read_process_memory_into(ctypes.addressof(source), 12345)
File without changes
File without changes
File without changes
File without changes