PyMemoryEditor 3.3.0__tar.gz → 3.3.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.
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PKG-INFO +1 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/__init__.py +1 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/_auto_refresh_dialog.py +6 -7
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/_widgets.py +7 -7
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/application.py +3 -4
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/cheat_poll_worker.py +5 -5
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/cheat_table.py +17 -15
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/main_window.py +11 -11
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/memory_viewer_dialog.py +5 -5
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/open_process_dialog.py +1 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/pointer_chain_dialog.py +2 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/pointer_scan_dialog.py +5 -5
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/results_view.py +6 -6
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/scanner_panel.py +1 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/value_types.py +3 -3
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/linux/functions.py +29 -18
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/linux/libc.py +2 -3
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/linux/process.py +49 -112
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/macos/functions.py +38 -25
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/macos/process.py +47 -121
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/config.py +12 -13
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/policy.py +6 -6
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/server.py +9 -9
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/session.py +21 -23
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/toolset.py +83 -88
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/_guard.py +6 -5
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/abstract.py +224 -25
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/pointer_scan.py +2 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/region.py +10 -2
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/scanning.py +9 -13
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/util/scan.py +1 -2
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/enums/memory_protections.py +1 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/functions.py +26 -15
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/process.py +52 -128
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/conf.py +1 -1
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/pyproject.toml +9 -13
- pymemoryeditor-3.3.1/tests/core/test_abstract_process.py +208 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_search_argument_errors.py +56 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/pointer/test_pointer_scan.py +302 -2
- pymemoryeditor-3.3.1/tests/scan/_fakes.py +65 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_chunking_integration.py +27 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_pattern_scan.py +101 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_region_snapshot.py +58 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_scan.py +51 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_scan_numpy.py +51 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_scanning_helper.py +141 -0
- pymemoryeditor-3.3.0/tests/pointer/test_pointer_review_fixes.py +0 -344
- pymemoryeditor-3.3.0/tests/scan/test_scan_review_fixes.py +0 -518
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/.flake8 +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/.gitignore +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/.readthedocs.yaml +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/CODE_OF_CONDUCT.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/CONTRIBUTING.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/LICENSE +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/Makefile +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/__main__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/_icon.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/assets/icon.svg +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/cheat_entry.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/log_console_dialog.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/modules_dialog.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/scan_types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/scan_worker.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/app/threads_dialog.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/enums.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/linux/types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/macos/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/macos/libsystem.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/macos/types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/__main__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/mcp/procinfo.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/errors.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/info.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/module_info.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/remote_pointer.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/thread_info.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/process/util.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/py.typed +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/util/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/util/convert.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/util/pattern.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/util/scan_numpy.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/enums/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/PyMemoryEditor/win32/types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/README.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/SECURITY.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/assets/screenshots/app.png +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/assets/social-preview.html +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/assets/social-preview.png +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/_static/.gitkeep +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/_static/custom.css +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/_templates/layout.html +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/enums.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/errors.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/index.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/memory-region.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/module-info.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/openprocess.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/pointer-path.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/remote-pointer.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/thread-info.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/api/utilities.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/app.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/contributing.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/funding.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/glossary.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/allocate-free.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/logging.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/memory-regions.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/modules-threads.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/opening-process.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/pattern-scan.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/pointer-scan.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/pointers.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/read-write.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/guide/searching.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/index.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/installation.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/license.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/mcp.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/platform-notes.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/quickstart.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/requirements.txt +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/troubleshooting.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/docs/why.md +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_app_cheat_entry.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_app_pointer_parsing.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_app_scan_request.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_app_smoke.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_app_value_types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_app_widgets.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_cheat_poll_worker.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_main_window_lifecycle.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_open_process_dialog.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_refine_scan_worker.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/app/test_results_model.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/conftest.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/core/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/core/test_errors.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/core/test_logger.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/conftest.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_argument_fuzz.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_fake_fidelity.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_live.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_parsing.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_policy.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_registry.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_server.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_session.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/mcp_server/test_toolset.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_allocate_free.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_bufflength_inference.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_editor.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_narrow_width_consistency.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_partial_io.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_read_into.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_str_boundary.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_str_decode_consistency.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_typed_accessors.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_unaligned_scan.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_write_safety.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_write_span.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/memory/test_write_str_bytes_width.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_bitness.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_bitness_fallback_warns.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_closed_generators.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_linux_error_hints.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_linux_pid_reuse.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_linux_proc_decoding.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_linux_snapshot_passthrough.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_linux_types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_close.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_load_commands.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_protect.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_region_walk.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_snapshot.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_threads.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_transient_reads.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_macos_types.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_win32_backend_guards.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_win32_permissions.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_win32_region_walk.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_win32_toolhelp.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/platforms/test_win32_wow64.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/pointer/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/pointer/test_pointer_chain.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/pointer/test_remote_pointer.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_foreign_process.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_memory_region.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_module_enumeration.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_partial_name_match.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_process_enumeration.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_process_lookup.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/process/test_thread_enumeration.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/__init__.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_pattern_compile.py +0 -0
- {pymemoryeditor-3.3.0 → pymemoryeditor-3.3.1}/tests/scan/test_scan_properties.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyMemoryEditor
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.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
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
Shared base for the read-only, auto-refreshing table dialogs (Memory Map,
|
|
4
4
|
Modules, Threads).
|
|
5
5
|
|
|
6
|
-
All three
|
|
6
|
+
All three do the same dance — run a one-shot read off the UI thread, repopulate
|
|
7
7
|
a sortable table when it returns, poll on a timer, and tear the worker down
|
|
8
|
-
safely on close
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
only what differs: how to fetch the data, and how to render it.
|
|
8
|
+
safely on close. This owns that lifecycle once, so the fiddly bits (the
|
|
9
|
+
in-flight guard that self-throttles the poll, the detach-on-shutdown safety)
|
|
10
|
+
live in one place; subclasses provide only what differs: how to fetch the
|
|
11
|
+
data, and how to render it.
|
|
13
12
|
|
|
14
13
|
It also owns *when* a failure is worth interrupting the user — see
|
|
15
14
|
:meth:`AutoRefreshTableDialog._handle_failed`.
|
|
@@ -182,7 +181,7 @@ class AutoRefreshTableDialog(TearsDownOnClose, QDialog):
|
|
|
182
181
|
Process) *every* tick fails, and ``_on_data_failed`` pops a **modal**
|
|
183
182
|
dialog. A modal runs a nested event loop, so the timer fires again while
|
|
184
183
|
it is up and the failures stack — hundreds of focus-stealing dialogs the
|
|
185
|
-
user could only escape by killing the app from another TTY
|
|
184
|
+
user could only escape by killing the app from another TTY.
|
|
186
185
|
|
|
187
186
|
Reporting is therefore tied to the two moments where a failure changes
|
|
188
187
|
what the user sees: the **first** fetch (empty window) and the one that
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
"""Small Qt widgets shared between dialogs.
|
|
4
4
|
|
|
5
|
-
Centralises tiny helpers (numeric sort items, hex address parsing)
|
|
6
|
-
|
|
5
|
+
Centralises tiny helpers (numeric sort items, hex address parsing) shared by
|
|
6
|
+
several dialog modules.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import threading
|
|
@@ -46,9 +46,9 @@ def shutdown_worker_thread(worker: Optional[QThread], wait_ms: int = 2000) -> No
|
|
|
46
46
|
cancel()
|
|
47
47
|
try:
|
|
48
48
|
# The four-argument static form on purpose: bare `worker.disconnect()`
|
|
49
|
-
# raises TypeError in PySide6 (
|
|
50
|
-
#
|
|
51
|
-
#
|
|
49
|
+
# raises TypeError in PySide6, and `worker.disconnect(worker)` only
|
|
50
|
+
# drops connections whose *receiver* is the worker — ours belong to the
|
|
51
|
+
# dialog.
|
|
52
52
|
QObject.disconnect(worker, None, None, None)
|
|
53
53
|
except (RuntimeError, TypeError):
|
|
54
54
|
pass
|
|
@@ -141,8 +141,8 @@ class TearsDownOnClose:
|
|
|
141
141
|
``QDialog`` delivers a ``QCloseEvent`` only for a real close. The app's own
|
|
142
142
|
"Close" buttons call ``accept()`` and Esc reaches ``reject()``; both land in
|
|
143
143
|
``done()``, which hides the dialog and emits ``finished`` *without* any
|
|
144
|
-
close event — so a teardown living in ``closeEvent`` never
|
|
145
|
-
dialog
|
|
144
|
+
close event — so a teardown living in ``closeEvent`` would never run, and
|
|
145
|
+
the dialog would keep polling, hidden and unreachable.
|
|
146
146
|
|
|
147
147
|
Subclasses implement :meth:`_teardown`; the mixin runs it exactly once,
|
|
148
148
|
whichever way the dialog is dismissed, and must precede ``QDialog`` in the
|
|
@@ -542,8 +542,7 @@ def main(argv=None):
|
|
|
542
542
|
|
|
543
543
|
``argv`` defaults to ``sys.argv`` so packaging tools (which call
|
|
544
544
|
``main()`` with no arguments) keep working. Tests and embedders can pass
|
|
545
|
-
an explicit list
|
|
546
|
-
ignored, which made the parameter meaningless.
|
|
545
|
+
an explicit list.
|
|
547
546
|
|
|
548
547
|
Terminal users want Ctrl+C to kill the app, which takes a process-wide
|
|
549
548
|
signal change — see :func:`main_cli`, the console-script entry point.
|
|
@@ -611,9 +610,9 @@ def main_cli(argv=None):
|
|
|
611
610
|
Entry point for the ``pymemoryeditor`` console script and ``python -m``.
|
|
612
611
|
|
|
613
612
|
Qt's event loop blocks inside C++, so Python's default SIGINT handler only
|
|
614
|
-
runs once the interpreter next regains control. In practice it
|
|
613
|
+
runs once the interpreter next regains control. In practice it raises
|
|
615
614
|
KeyboardInterrupt inside our own _PointerCursorFilter.eventFilter override,
|
|
616
|
-
where PySide6 swallows it and merely prints a traceback
|
|
615
|
+
where PySide6 swallows it and merely prints a traceback. Hand SIGINT
|
|
617
616
|
back to the OS for the duration of the run so Ctrl+C from a terminal
|
|
618
617
|
terminates the app immediately.
|
|
619
618
|
|
|
@@ -56,8 +56,8 @@ class _CheatPollWorker(QThread):
|
|
|
56
56
|
with the current ``{key: "ErrorType: message"}`` map. The signal fires only
|
|
57
57
|
when that set *changes* (a freeze starts or stops failing), so the UI gets a
|
|
58
58
|
persistent cue without 10 Hz spam, and the first failure of each entry is
|
|
59
|
-
logged once.
|
|
60
|
-
|
|
59
|
+
logged once. Swallowing the exception instead would make a freeze that
|
|
60
|
+
never landed look identical to one that did.
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
values_ready = Signal(object) # list[tuple[int, type, int, Any]]
|
|
@@ -96,9 +96,9 @@ class _CheatPollWorker(QThread):
|
|
|
96
96
|
) -> None:
|
|
97
97
|
"""Queue a one-shot manual write to be performed on the worker thread.
|
|
98
98
|
|
|
99
|
-
The cheat table's inline value edits
|
|
100
|
-
``write_process_memory`` directly on the UI thread
|
|
101
|
-
the target
|
|
99
|
+
The cheat table's inline value edits come through here: calling
|
|
100
|
+
``write_process_memory`` directly on the UI thread would freeze the UI
|
|
101
|
+
when the target is slow or the page faults. Routing them here keeps the
|
|
102
102
|
syscall off the UI thread; a write that fails comes back via the
|
|
103
103
|
``write_failed`` signal. Latency is at most one tick (~100 ms), which is
|
|
104
104
|
imperceptible for a manual edit.
|
|
@@ -12,8 +12,8 @@ stays fresh.
|
|
|
12
12
|
This module hosts only the Qt widget; the dataclass and the worker thread
|
|
13
13
|
live in ``cheat_entry.py`` and ``cheat_poll_worker.py`` respectively. The
|
|
14
14
|
``CheatEntry`` and ``_CheatPollWorker`` names are re-exported from here for
|
|
15
|
-
backward compatibility with code (and tests) that
|
|
16
|
-
module
|
|
15
|
+
backward compatibility with code (and tests) that import them from this
|
|
16
|
+
module.
|
|
17
17
|
"""
|
|
18
18
|
import copy
|
|
19
19
|
import json
|
|
@@ -95,8 +95,8 @@ class CheatTable(QWidget):
|
|
|
95
95
|
self._poller.start()
|
|
96
96
|
|
|
97
97
|
# A short cadence to push fresh entry snapshots into the worker. This
|
|
98
|
-
# is
|
|
99
|
-
#
|
|
98
|
+
# is cheap — it does no syscalls, it only copies a small list of
|
|
99
|
+
# tuples.
|
|
100
100
|
self._publish_timer = QTimer(self)
|
|
101
101
|
self._publish_timer.setInterval(TICK_INTERVAL_MS)
|
|
102
102
|
self._publish_timer.timeout.connect(self._publish_snapshot_to_worker)
|
|
@@ -221,8 +221,8 @@ class CheatTable(QWidget):
|
|
|
221
221
|
self._table.customContextMenuRequested.connect(self._show_context_menu)
|
|
222
222
|
layout.addWidget(self._table, 1)
|
|
223
223
|
|
|
224
|
-
# Delete removes the selected rows — the standard gesture,
|
|
225
|
-
#
|
|
224
|
+
# Delete removes the selected rows — the standard gesture, alongside
|
|
225
|
+
# the context menu. Scoped to the table widget
|
|
226
226
|
# (WidgetShortcut) so Delete still edits text while a cell is in
|
|
227
227
|
# inline-edit mode (the editor child, not the table, has focus then).
|
|
228
228
|
delete_shortcut = QShortcut(QKeySequence(QKeySequence.Delete), self._table)
|
|
@@ -247,8 +247,9 @@ class CheatTable(QWidget):
|
|
|
247
247
|
next, so that is refused -- the import loop reports and skips it.
|
|
248
248
|
|
|
249
249
|
Batched so a bulk promotion costs one address lookup per row and one
|
|
250
|
-
rebuild in total: one ``add_entry`` per row
|
|
251
|
-
scan plus a full table rebuild each, quadratic in the
|
|
250
|
+
rebuild in total: one ``add_entry`` per row would do a linear
|
|
251
|
+
duplicate scan plus a full table rebuild each, quadratic in the
|
|
252
|
+
selection.
|
|
252
253
|
|
|
253
254
|
:raises ValueError: if an entry's length is over ``MAX_ENTRY_LENGTH``;
|
|
254
255
|
checked for every entry before any is added.
|
|
@@ -261,7 +262,7 @@ class CheatTable(QWidget):
|
|
|
261
262
|
% (entry.address, length, MAX_ENTRY_LENGTH)
|
|
262
263
|
)
|
|
263
264
|
|
|
264
|
-
# First entry per address,
|
|
265
|
+
# First entry per address wins, should the table hold duplicates.
|
|
265
266
|
by_address: Dict[int, CheatEntry] = {}
|
|
266
267
|
for existing in self._entries:
|
|
267
268
|
by_address.setdefault(existing.address, existing)
|
|
@@ -347,8 +348,8 @@ class CheatTable(QWidget):
|
|
|
347
348
|
type_label = entry.spec_label
|
|
348
349
|
# Show the width whenever it belongs to the entry rather than to the
|
|
349
350
|
# spec. An IDA pattern declares none (length 0) yet carries a real one
|
|
350
|
-
# here — writes are refused against it — so hiding it
|
|
351
|
-
# told to "widen the entry" with no way to see what it holds.
|
|
351
|
+
# here — writes are refused against it — so hiding it would leave the
|
|
352
|
+
# user told to "widen the entry" with no way to see what it holds.
|
|
352
353
|
if entry.spec.accepts_length_override or not entry.spec.length:
|
|
353
354
|
type_label += f" · {entry.length}B"
|
|
354
355
|
type_item = QTableWidgetItem(type_label)
|
|
@@ -428,8 +429,8 @@ class CheatTable(QWidget):
|
|
|
428
429
|
# and nothing to write to while the target is known to be gone.
|
|
429
430
|
if self._poller_stopped or self._paused:
|
|
430
431
|
# Put the cell back and say so, as the bulk edit does —
|
|
431
|
-
# returning quietly
|
|
432
|
-
# been written.
|
|
432
|
+
# returning quietly would leave the typed text on screen as if
|
|
433
|
+
# it had been written.
|
|
433
434
|
self._suspend_signals = True
|
|
434
435
|
item.setText(self._value_text_for(entry))
|
|
435
436
|
self._suspend_signals = False
|
|
@@ -525,7 +526,8 @@ class CheatTable(QWidget):
|
|
|
525
526
|
against it here is cheap and keeps the cue in sync: a red value cell plus
|
|
526
527
|
an explanatory tooltip while a freeze keeps failing, restored to normal
|
|
527
528
|
the moment the write lands again. Without this, a frozen value that the
|
|
528
|
-
backend silently refuses to write
|
|
529
|
+
backend silently refuses to write would look identical to one that
|
|
530
|
+
worked.
|
|
529
531
|
"""
|
|
530
532
|
rows_by_key: Dict[Tuple[int, type, int], int] = {}
|
|
531
533
|
for row, entry in enumerate(self._entries):
|
|
@@ -911,7 +913,7 @@ class CheatTable(QWidget):
|
|
|
911
913
|
|
|
912
914
|
|
|
913
915
|
# Ceiling for an entry's buffer width. Entries are promoted at the width of the
|
|
914
|
-
# value scanned for, which the scanner doesn't cap, so 1024
|
|
916
|
+
# value scanned for, which the scanner doesn't cap, so 1024 is too tight — but
|
|
915
917
|
# the poll worker allocates this many bytes per entry on every 100 ms tick, so
|
|
916
918
|
# an unbounded field turns one typo into a multi-gigabyte allocation the tick's
|
|
917
919
|
# blanket except swallows and retries forever. A megabyte is far past any real
|
|
@@ -471,8 +471,8 @@ class MainWindow(QMainWindow):
|
|
|
471
471
|
self._cleanup_worker()
|
|
472
472
|
# A cancelled scan still emits finished_ok (with the partial count) and
|
|
473
473
|
# a failed one emits error; either way the user asked for no more
|
|
474
|
-
# work — Cancel
|
|
475
|
-
# a second error dialog from the refresh.
|
|
474
|
+
# work — otherwise Cancel would start "Updating values…", and a dead
|
|
475
|
+
# target would pop a second error dialog from the refresh.
|
|
476
476
|
if completed:
|
|
477
477
|
self._fill_initial_values(request)
|
|
478
478
|
|
|
@@ -547,9 +547,9 @@ class MainWindow(QMainWindow):
|
|
|
547
547
|
def _cleanup_worker(self) -> None:
|
|
548
548
|
# Runs from the worker's `finished`, and only one scan runs at a time,
|
|
549
549
|
# so self._worker is the worker that just finished. It is a child of
|
|
550
|
-
# this window: dropping the reference alone
|
|
551
|
-
# (and the address list / value map it was handed) alive until
|
|
552
|
-
# window closed. `finished` is emitted as run() returns, so wait()
|
|
550
|
+
# this window: dropping the reference alone would keep every scan's
|
|
551
|
+
# QThread (and the address list / value map it was handed) alive until
|
|
552
|
+
# the window closed. `finished` is emitted as run() returns, so wait()
|
|
553
553
|
# comes back at once; it makes sure the thread is really over before
|
|
554
554
|
# deleteLater() (destroying a running QThread aborts the process).
|
|
555
555
|
worker = self._worker
|
|
@@ -603,8 +603,8 @@ class MainWindow(QMainWindow):
|
|
|
603
603
|
"""Forget a closed tool dialog and free it.
|
|
604
604
|
|
|
605
605
|
The dialogs are children of this window and are rebuilt on the next
|
|
606
|
-
open, so dropping the reference alone
|
|
607
|
-
the tables it held) alive for the life of the window. By the time
|
|
606
|
+
open, so dropping the reference alone would keep every closed instance
|
|
607
|
+
(and the tables it held) alive for the life of the window. By the time
|
|
608
608
|
``finished`` gets here the dialog's teardown has already run — it is
|
|
609
609
|
driven from ``done()`` — so its background worker is stopped, or
|
|
610
610
|
detached from it if it would not stop, and the delete can't take a
|
|
@@ -919,8 +919,8 @@ class MainWindow(QMainWindow):
|
|
|
919
919
|
|
|
920
920
|
# Tear down auxiliary dialogs that hold a reference to the old
|
|
921
921
|
# process — reopening them rebuilds against the new target. The Memory
|
|
922
|
-
# Map belongs here too: it polls on a timer, so leaving it open
|
|
923
|
-
# reading the handle closed just below
|
|
922
|
+
# Map belongs here too: it polls on a timer, so leaving it open would
|
|
923
|
+
# leave it reading the handle closed just below.
|
|
924
924
|
for dialog_attr in (
|
|
925
925
|
"_memory_map",
|
|
926
926
|
"_threads_dialog",
|
|
@@ -1019,8 +1019,8 @@ class MainWindow(QMainWindow):
|
|
|
1019
1019
|
"""Ask before quitting with unsaved cheat entries. True = go ahead.
|
|
1020
1020
|
|
|
1021
1021
|
The table is not persisted between runs (Export… is the way to keep
|
|
1022
|
-
it), so a stray Cmd+Q / window close
|
|
1023
|
-
address silently.
|
|
1022
|
+
it), so a stray Cmd+Q / window close would otherwise throw away every
|
|
1023
|
+
saved address silently.
|
|
1024
1024
|
"""
|
|
1025
1025
|
count = self._cheat.entry_count()
|
|
1026
1026
|
if count == 0 or not self.confirm_close_with_entries:
|
|
@@ -151,9 +151,9 @@ class MemoryViewerDialog(TearsDownOnClose, QDialog):
|
|
|
151
151
|
self._timer = QTimer(self)
|
|
152
152
|
self._timer.timeout.connect(self.refresh)
|
|
153
153
|
|
|
154
|
-
# Failure bookkeeping, same shape as the auto-refresh dialogs:
|
|
155
|
-
# target
|
|
156
|
-
# floor). Keyed by (address, size, message) so moving the viewer to
|
|
154
|
+
# Failure bookkeeping, same shape as the auto-refresh dialogs: without
|
|
155
|
+
# it a dead target would log a warning every tick forever (~20/s at the
|
|
156
|
+
# 50 ms floor). Keyed by (address, size, message) so moving the viewer to
|
|
157
157
|
# another failing range still gets logged.
|
|
158
158
|
self._last_failure: Optional[tuple] = None
|
|
159
159
|
self._failing_since: Optional[QElapsedTimer] = None
|
|
@@ -171,7 +171,7 @@ class MemoryViewerDialog(TearsDownOnClose, QDialog):
|
|
|
171
171
|
# Skip if a read is still running. The auto-refresh timer fires this
|
|
172
172
|
# repeatedly, and a large length (up to 65536) on a slow target
|
|
173
173
|
# (notably macOS Mach-VM) takes long enough that running it on the UI
|
|
174
|
-
# thread
|
|
174
|
+
# thread would freeze input — so the read runs on a worker, and this guard
|
|
175
175
|
# self-throttles the cadence to the real read time instead of stacking
|
|
176
176
|
# workers.
|
|
177
177
|
if self._worker is not None and self._worker.isRunning():
|
|
@@ -188,7 +188,7 @@ class MemoryViewerDialog(TearsDownOnClose, QDialog):
|
|
|
188
188
|
# travel back in the result so the rendered dump stays labelled with
|
|
189
189
|
# the range actually read even if the user edits the fields mid-read.
|
|
190
190
|
# Errors are returned (not raised) so the one result handler renders
|
|
191
|
-
#
|
|
191
|
+
# every outcome through a single message/log path.
|
|
192
192
|
try:
|
|
193
193
|
data = process.read_process_memory(addr, bytes, size)
|
|
194
194
|
# A backend returning a non-buffer object would make bytes(data)
|
|
@@ -288,7 +288,7 @@ class OpenProcessDialog(TearsDownOnClose, QDialog):
|
|
|
288
288
|
selected_pid = self._selected_pid()
|
|
289
289
|
|
|
290
290
|
# Restoring the selection below re-triggers Qt's scrollTo(current), which
|
|
291
|
-
#
|
|
291
|
+
# would jump the list back to the selected row on every tick. The
|
|
292
292
|
# rebuild is harmless on its own: Qt defers the scrollbar range update.
|
|
293
293
|
scrollbar = self._table.verticalScrollBar()
|
|
294
294
|
scroll_offset = scrollbar.value()
|
|
@@ -45,6 +45,7 @@ from PySide6.QtWidgets import (
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
from PyMemoryEditor import AbstractProcess
|
|
48
|
+
from PyMemoryEditor.process.region import ADDRESS_SPACE_END
|
|
48
49
|
|
|
49
50
|
from ._widgets import (
|
|
50
51
|
MONOSPACE_FAMILY,
|
|
@@ -469,7 +470,7 @@ class PointerChainDialog(QDialog):
|
|
|
469
470
|
# and formatted as hex; a negative or >= 2**64 value (raw mode with
|
|
470
471
|
# large/negative offsets) would raise OverflowError/ValueError. Reject
|
|
471
472
|
# it cleanly instead of crashing.
|
|
472
|
-
if not 0 <= resolved <
|
|
473
|
+
if not 0 <= resolved < ADDRESS_SPACE_END:
|
|
473
474
|
self._resolved_address = None
|
|
474
475
|
self._output_label.setText("Resolved address: <out of 64-bit range>")
|
|
475
476
|
self._value_label.setText("Value: —")
|
|
@@ -1059,9 +1059,9 @@ class PointerScanDialog(TearsDownOnClose, QDialog):
|
|
|
1059
1059
|
for saved in survivors:
|
|
1060
1060
|
# Survivors are all module paths (the intersection drops the
|
|
1061
1061
|
# rest). One whose module isn't loaded now has no live base: the
|
|
1062
|
-
# rescan drops it, and falling back to its saved absolute base
|
|
1063
|
-
#
|
|
1064
|
-
#
|
|
1062
|
+
# rescan drops it, and falling back to its saved absolute base
|
|
1063
|
+
# would walk the previous run's addresses and could show a value
|
|
1064
|
+
# read from unrelated memory as if the path resolved. Its
|
|
1065
1065
|
# membership is already decided, so it stays, shown unresolved.
|
|
1066
1066
|
live = saved._rebased_onto(module_bases)
|
|
1067
1067
|
if live is None:
|
|
@@ -1090,8 +1090,8 @@ class PointerScanDialog(TearsDownOnClose, QDialog):
|
|
|
1090
1090
|
self._export_btn.setEnabled(True)
|
|
1091
1091
|
|
|
1092
1092
|
def _teardown(self) -> None:
|
|
1093
|
-
# Unhook every signal (
|
|
1094
|
-
#
|
|
1093
|
+
# Unhook every signal (`error`/`finished` included, or a late emit
|
|
1094
|
+
# could land on a destroyed dialog) and join the worker; if
|
|
1095
1095
|
# it can't stop in time it's detached rather than destroyed under us.
|
|
1096
1096
|
shutdown_worker_thread(self._worker, wait_ms=2000)
|
|
1097
1097
|
self._worker = None
|
|
@@ -67,7 +67,7 @@ class ResultsModel(QAbstractTableModel):
|
|
|
67
67
|
# row). Rows dropped since then are listed, ascending, in ``_removed``:
|
|
68
68
|
# an address's current row is its base minus the dropped rows above it
|
|
69
69
|
# (see ``_row_of``). Rebuilding the whole dict after every refine chunk
|
|
70
|
-
#
|
|
70
|
+
# would make a refine quadratic in the result count.
|
|
71
71
|
self._index: Dict[int, int] = {}
|
|
72
72
|
self._removed: List[int] = []
|
|
73
73
|
# Set when an address was appended twice: the base-row bookkeeping
|
|
@@ -201,8 +201,8 @@ class ResultsModel(QAbstractTableModel):
|
|
|
201
201
|
|
|
202
202
|
# One dataChanged per contiguous run of patched rows rather than one
|
|
203
203
|
# per row: the view repaints on every emit, and a refine chunk is a
|
|
204
|
-
# few thousand rows, so per-row emits
|
|
205
|
-
# storm.
|
|
204
|
+
# few thousand rows, so per-row emits would turn each chunk into a
|
|
205
|
+
# redraw storm.
|
|
206
206
|
for first, last in _contiguous_runs(sorted(set(rows_changed))):
|
|
207
207
|
self.dataChanged.emit(
|
|
208
208
|
self.index(first, COL_VALUE), self.index(last, COL_PREVIOUS)
|
|
@@ -219,9 +219,9 @@ class ResultsModel(QAbstractTableModel):
|
|
|
219
219
|
a block of neighbours costs one signal, not one per row. Past
|
|
220
220
|
``_RESET_RUN_THRESHOLD`` runs (a refine that keeps a sparse scatter of
|
|
221
221
|
a huge result set) the model is reset instead: one O(n) rebuild and
|
|
222
|
-
one repaint, at the price of the view's selection.
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
one repaint, at the price of the view's selection. One pair plus a
|
|
223
|
+
mid-list ``list.pop`` per row would be quadratic: a refine dropping
|
|
224
|
+
900k of 1M rows would do 900k pops and fire 900k signals.
|
|
225
225
|
"""
|
|
226
226
|
count = len(self._addresses)
|
|
227
227
|
rows = [row for row in rows if 0 <= row < count]
|
|
@@ -558,7 +558,7 @@ class ScannerPanel(QWidget):
|
|
|
558
558
|
# its own width, and there is no earlier width to fall back on. Dropping
|
|
559
559
|
# it would send the next Changed/Unchanged refine to the spec default —
|
|
560
560
|
# 16 for a String scanned at 4 — which is the failure this whole branch
|
|
561
|
-
# exists to
|
|
561
|
+
# exists to prevent. _has_results tells the two apart: a first scan only
|
|
562
562
|
# runs when there are none yet.
|
|
563
563
|
if self._has_results:
|
|
564
564
|
self._pending_scan_length = None
|
|
@@ -469,9 +469,9 @@ def parse_value_for_write(
|
|
|
469
469
|
# One guard for every path that reaches an address. An entry's length is a
|
|
470
470
|
# *byte* width — it is the bufflength each poll tick reads — while
|
|
471
471
|
# prepare_write treats it as a hard cap and truncates past it, counting
|
|
472
|
-
# characters for str. So a wider value
|
|
473
|
-
# while the cell kept showing all of it, and a multibyte str could slip
|
|
474
|
-
# other way: "ábc" is 3 characters but 4 bytes, one past a 3-byte entry.
|
|
472
|
+
# characters for str. So a wider value would be written short with no word
|
|
473
|
+
# said while the cell kept showing all of it, and a multibyte str could slip
|
|
474
|
+
# the other way: "ábc" is 3 characters but 4 bytes, one past a 3-byte entry.
|
|
475
475
|
# Measuring what actually goes on the wire covers both.
|
|
476
476
|
written = _written_width(value)
|
|
477
477
|
if length_override is not None and written > length_override:
|
|
@@ -12,7 +12,7 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
import signal
|
|
14
14
|
from ctypes import addressof, sizeof
|
|
15
|
-
from typing import Dict, Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
|
|
15
|
+
from typing import Dict, Generator, Optional, Pattern, Sequence, Tuple, Type, TypeVar, Union, cast
|
|
16
16
|
|
|
17
17
|
from ..enums import ScanTypesEnum
|
|
18
18
|
from ..process.errors import ProcessIDNotExistsError
|
|
@@ -60,9 +60,9 @@ class _LinuxPartialIOError(OSError):
|
|
|
60
60
|
|
|
61
61
|
In practice this happens when the target range straddles a freed or
|
|
62
62
|
inaccessible page — the kernel transfers what it can and reports the
|
|
63
|
-
short count.
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
short count. Accepting the short result would leave the caller's buffer
|
|
64
|
+
half-filled with real bytes and half with zeros (which downstream
|
|
65
|
+
decoding would treat as valid), so it raises instead.
|
|
66
66
|
Mirrors the partial-read/write check the Win32 backend already does
|
|
67
67
|
against ``ReadProcessMemory`` / ``WriteProcessMemory``.
|
|
68
68
|
"""
|
|
@@ -181,10 +181,9 @@ def _make_read_chunk(pid: int):
|
|
|
181
181
|
"""
|
|
182
182
|
Build a `read_chunk(address, size)` closure bound to ``pid``.
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
One closure per backend, shared by its three scan entry points
|
|
185
185
|
(``search_addresses_by_value``, ``search_addresses_by_pattern``,
|
|
186
|
-
``search_values_by_addresses``)
|
|
187
|
-
of subtle bugs. Owning it once per backend keeps the trio in lockstep.
|
|
186
|
+
``search_values_by_addresses``) so they cannot drift.
|
|
188
187
|
"""
|
|
189
188
|
def read_chunk(address: int, size: int):
|
|
190
189
|
buffer = (ctypes.c_byte * size)()
|
|
@@ -249,7 +248,7 @@ def get_memory_regions(pid: int) -> Generator["MemoryRegion", None, None]:
|
|
|
249
248
|
region_information = line.split(maxsplit=5)
|
|
250
249
|
|
|
251
250
|
try:
|
|
252
|
-
addressing_range, privileges,
|
|
251
|
+
addressing_range, privileges, raw_offset, device, raw_inode = (
|
|
253
252
|
region_information[0:5]
|
|
254
253
|
)
|
|
255
254
|
path = region_information[5].strip() if len(region_information) >= 6 else b""
|
|
@@ -259,8 +258,8 @@ def get_memory_regions(pid: int) -> Generator["MemoryRegion", None, None]:
|
|
|
259
258
|
]
|
|
260
259
|
major_id, minor_id = [int(_id, 16) for _id in device.split(b":")]
|
|
261
260
|
|
|
262
|
-
offset = int(
|
|
263
|
-
inode = int(
|
|
261
|
+
offset = int(raw_offset, 16)
|
|
262
|
+
inode = int(raw_inode) # /proc/<pid>/maps formats the inode as decimal.
|
|
264
263
|
except (ValueError, IndexError) as exc:
|
|
265
264
|
# A single malformed line (kernel quirk, racing teardown) must
|
|
266
265
|
# not abort the whole region walk — skip it and keep going, the
|
|
@@ -437,9 +436,9 @@ def read_process_memory(pid: int, address: int, pytype: Type[T], bufflength: int
|
|
|
437
436
|
_process_vm_readv(pid, addressof(data), address, bufflength)
|
|
438
437
|
|
|
439
438
|
if pytype is str:
|
|
440
|
-
return bytes(data).decode("utf-8", errors="replace")
|
|
439
|
+
return cast(T, bytes(data).decode("utf-8", errors="replace"))
|
|
441
440
|
elif pytype is bytes:
|
|
442
|
-
return bytes(data)
|
|
441
|
+
return cast(T, bytes(data))
|
|
443
442
|
else:
|
|
444
443
|
# Narrow widths pad with zeroes, which reads a signed value as
|
|
445
444
|
# unsigned -- and a scan for the same bytes is signed.
|
|
@@ -463,23 +462,28 @@ def search_addresses_by_value(
|
|
|
463
462
|
pid: int,
|
|
464
463
|
pytype: Type[T],
|
|
465
464
|
bufflength: int,
|
|
466
|
-
value: Union[bool, int, float, str, bytes, tuple],
|
|
465
|
+
value: Optional[Union[bool, int, float, str, bytes, tuple]],
|
|
467
466
|
scan_type: ScanTypesEnum = ScanTypesEnum.EXACT_VALUE,
|
|
468
467
|
progress_information: bool = False,
|
|
469
468
|
writeable_only: bool = False,
|
|
470
469
|
*,
|
|
471
470
|
memory_regions: Optional[Sequence[MemoryRegion]] = None,
|
|
472
471
|
aligned: bool = True,
|
|
472
|
+
target_value_bytes: Optional[Union[bytes, Tuple[bytes, ...]]] = None,
|
|
473
473
|
) -> Generator[Union[int, Tuple[int, dict]], None, None]:
|
|
474
474
|
"""
|
|
475
475
|
Search the whole memory space, accessible to the process,
|
|
476
476
|
for the provided value, returning the found addresses.
|
|
477
477
|
|
|
478
478
|
Passing a `memory_regions` snapshot skips region enumeration.
|
|
479
|
+
``target_value_bytes`` is ``value`` already encoded (see
|
|
480
|
+
:func:`~PyMemoryEditor.util.prepare_search_target`); when it is given,
|
|
481
|
+
``value`` is not used and may be ``None``.
|
|
479
482
|
"""
|
|
480
483
|
_validate_pytype(pytype)
|
|
481
484
|
|
|
482
|
-
target_value_bytes
|
|
485
|
+
if target_value_bytes is None:
|
|
486
|
+
target_value_bytes = values_to_bytes(pytype, bufflength, value)
|
|
483
487
|
|
|
484
488
|
source_regions = (
|
|
485
489
|
memory_regions if memory_regions is not None else get_memory_regions(pid)
|
|
@@ -512,12 +516,19 @@ def search_addresses_by_pattern(
|
|
|
512
516
|
byte_length: int = 0,
|
|
513
517
|
progress_information: bool = False,
|
|
514
518
|
memory_regions: Optional[Sequence[MemoryRegion]] = None,
|
|
519
|
+
compiled_pattern: Optional[Tuple[Pattern[bytes], int]] = None,
|
|
515
520
|
) -> Generator[Union[int, Tuple[int, dict]], None, None]:
|
|
516
521
|
"""
|
|
517
522
|
AOB scan against every readable, non-shared region of the target. See
|
|
518
|
-
:meth:`AbstractProcess.search_by_pattern`.
|
|
519
|
-
|
|
520
|
-
|
|
523
|
+
:meth:`AbstractProcess.search_by_pattern`. ``compiled_pattern`` is what
|
|
524
|
+
:func:`~PyMemoryEditor.util.pattern.compile_pattern` returned for
|
|
525
|
+
``pattern``, when the caller already compiled it.
|
|
526
|
+
"""
|
|
527
|
+
compiled, length = (
|
|
528
|
+
compiled_pattern
|
|
529
|
+
if compiled_pattern is not None
|
|
530
|
+
else compile_pattern(pattern, byte_length=byte_length)
|
|
531
|
+
)
|
|
521
532
|
|
|
522
533
|
source_regions = (
|
|
523
534
|
memory_regions if memory_regions is not None else get_memory_regions(pid)
|
|
@@ -703,7 +714,7 @@ def get_processes() -> Generator[Tuple[int, str], None, None]:
|
|
|
703
714
|
|
|
704
715
|
``comm`` is raw bytes a process can set to anything (``prctl(PR_SET_NAME)``),
|
|
705
716
|
so it is decoded as UTF-8 with replacement characters, independent of the
|
|
706
|
-
locale
|
|
717
|
+
locale, so one process with a non-UTF-8 name cannot raise
|
|
707
718
|
``UnicodeDecodeError`` here and break ``OpenProcess(name=...)`` for every
|
|
708
719
|
other process.
|
|
709
720
|
"""
|
|
@@ -21,9 +21,8 @@ libc = ctypes.CDLL(find_library("c"), use_errno=True)
|
|
|
21
21
|
# Configuring `argtypes` is not cosmetic: without it, ctypes passes Python ints
|
|
22
22
|
# through the platform's default C-int width. On a 32-bit Linux build (or any
|
|
23
23
|
# host where the default int is narrower than the iovec pointer's representation)
|
|
24
|
-
# the address gets silently truncated before the kernel sees it
|
|
25
|
-
#
|
|
26
|
-
# declares argtypes/restype explicitly.
|
|
24
|
+
# the address gets silently truncated before the kernel sees it. The Win32
|
|
25
|
+
# backend declares argtypes/restype explicitly on every API for the same reason.
|
|
27
26
|
_PROCESS_VM_ARGTYPES = (
|
|
28
27
|
ctypes.c_int, # pid_t
|
|
29
28
|
ctypes.POINTER(iovec), # local_iov
|