PyMemoryEditor 2.1.0__py3-none-any.whl → 2.2.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.
- PyMemoryEditor/__init__.py +1 -1
- PyMemoryEditor/app/cheat_table.py +114 -12
- PyMemoryEditor/app/pointer_chain_dialog.py +13 -1
- PyMemoryEditor/app/pointer_scan_dialog.py +13 -3
- PyMemoryEditor/app/scan_worker.py +58 -20
- PyMemoryEditor/app/scanner_panel.py +243 -44
- PyMemoryEditor/app/value_types.py +172 -1
- PyMemoryEditor/macos/functions.py +11 -0
- PyMemoryEditor/macos/types.py +12 -0
- PyMemoryEditor/util/pattern.py +36 -11
- {pymemoryeditor-2.1.0.dist-info → pymemoryeditor-2.2.0.dist-info}/METADATA +1 -27
- {pymemoryeditor-2.1.0.dist-info → pymemoryeditor-2.2.0.dist-info}/RECORD +15 -15
- {pymemoryeditor-2.1.0.dist-info → pymemoryeditor-2.2.0.dist-info}/WHEEL +0 -0
- {pymemoryeditor-2.1.0.dist-info → pymemoryeditor-2.2.0.dist-info}/entry_points.txt +0 -0
- {pymemoryeditor-2.1.0.dist-info → pymemoryeditor-2.2.0.dist-info}/licenses/LICENSE +0 -0
PyMemoryEditor/__init__.py
CHANGED
|
@@ -48,7 +48,12 @@ from PyMemoryEditor import AbstractProcess
|
|
|
48
48
|
from ._widgets import parse_hex_address, shutdown_worker_thread
|
|
49
49
|
from .cheat_entry import CheatEntry
|
|
50
50
|
from .cheat_poll_worker import TICK_INTERVAL_MS, _CheatPollWorker
|
|
51
|
-
from .value_types import
|
|
51
|
+
from .value_types import (
|
|
52
|
+
VALUE_TYPES,
|
|
53
|
+
ValueTypeSpec,
|
|
54
|
+
find_spec,
|
|
55
|
+
parse_value_for_write,
|
|
56
|
+
)
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
# Re-exported for backward compatibility with callers that imported the
|
|
@@ -197,10 +202,25 @@ class CheatTable(QWidget):
|
|
|
197
202
|
delete_shortcut.activated.connect(self._on_remove_selected)
|
|
198
203
|
|
|
199
204
|
def add_entry(self, entry: CheatEntry) -> None:
|
|
205
|
+
# Every entry enters here, whichever way it was created — promoted from
|
|
206
|
+
# a scan, from a pointer dialog, added by hand, or loaded from JSON. A
|
|
207
|
+
# zero-width buffer reads back empty on every poll tick and can't be
|
|
208
|
+
# spotted from the table, so the floor is enforced once, at the door,
|
|
209
|
+
# rather than at each of those call sites. (The AOB pattern spec is the
|
|
210
|
+
# one whose declared length is 0 — the scanner derives its real width
|
|
211
|
+
# from the pattern.)
|
|
212
|
+
entry.length = max(1, int(entry.length))
|
|
213
|
+
|
|
200
214
|
# If the address already exists, just refresh its description/type.
|
|
201
215
|
for existing in self._entries:
|
|
202
216
|
if existing.address == entry.address:
|
|
203
217
|
existing.description = entry.description or existing.description
|
|
218
|
+
# Re-promoting an address that is already in the table is the
|
|
219
|
+
# third place a spec_label changes, and the cached value has to
|
|
220
|
+
# go with it here too — _rebuild formats it through the new
|
|
221
|
+
# spec on the way out of this method.
|
|
222
|
+
if entry.spec_label != existing.spec_label:
|
|
223
|
+
_forget_value_read_as_another_type(existing)
|
|
204
224
|
existing.spec_label = entry.spec_label
|
|
205
225
|
existing.length = entry.length
|
|
206
226
|
self._rebuild()
|
|
@@ -257,7 +277,11 @@ class CheatTable(QWidget):
|
|
|
257
277
|
self._table.setItem(row, self.COL_ADDRESS, addr)
|
|
258
278
|
|
|
259
279
|
type_label = entry.spec_label
|
|
260
|
-
|
|
280
|
+
# Show the width whenever it belongs to the entry rather than to the
|
|
281
|
+
# spec. An IDA pattern declares none (length 0) yet carries a real one
|
|
282
|
+
# here — writes are refused against it — so hiding it left the user
|
|
283
|
+
# told to "widen the entry" with no way to see what it holds.
|
|
284
|
+
if entry.spec.accepts_length_override or not entry.spec.length:
|
|
261
285
|
type_label += f" · {entry.length}B"
|
|
262
286
|
type_item = QTableWidgetItem(type_label)
|
|
263
287
|
type_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
@@ -312,7 +336,9 @@ class CheatTable(QWidget):
|
|
|
312
336
|
# Treat empty as "unfreeze and clear" — no-op.
|
|
313
337
|
return
|
|
314
338
|
try:
|
|
315
|
-
value, _length =
|
|
339
|
+
value, _length = parse_value_for_write(
|
|
340
|
+
entry.spec, text, entry.length, entry.last_value
|
|
341
|
+
)
|
|
316
342
|
except ValueError as exc:
|
|
317
343
|
QMessageBox.warning(self, "Invalid Value", str(exc))
|
|
318
344
|
self._suspend_signals = True
|
|
@@ -390,6 +416,13 @@ class CheatTable(QWidget):
|
|
|
390
416
|
continue
|
|
391
417
|
entry = self._entries[row]
|
|
392
418
|
entry.last_value = value
|
|
419
|
+
# A frozen row whose baseline was dropped — its type changed,
|
|
420
|
+
# so what the old spec had read no longer means anything —
|
|
421
|
+
# re-adopts the first value read under the new one. The poll
|
|
422
|
+
# worker skips a frozen entry with no frozen_value, so without
|
|
423
|
+
# this the Active box stays ticked while nothing is written.
|
|
424
|
+
if entry.frozen and entry.frozen_value is None:
|
|
425
|
+
entry.frozen_value = value
|
|
393
426
|
self._update_value_cell(row, entry)
|
|
394
427
|
finally:
|
|
395
428
|
self._suspend_signals = False
|
|
@@ -528,16 +561,40 @@ class CheatTable(QWidget):
|
|
|
528
561
|
if plan.description is not None:
|
|
529
562
|
entry.description = plan.description
|
|
530
563
|
|
|
531
|
-
|
|
564
|
+
# What the row was showing before this plan touched it. A type
|
|
565
|
+
# change forgets it, but a write in the same pass still needs
|
|
566
|
+
# it: an IDA '?' keeps the byte that is already there, and
|
|
567
|
+
# Byte Array → AOB doesn't change what those bytes mean.
|
|
568
|
+
# parse_value_for_write ignores it when the type genuinely
|
|
569
|
+
# changed shape, so a stale int can't be misread as bytes.
|
|
570
|
+
current = entry.last_value
|
|
571
|
+
retyped = False
|
|
572
|
+
if plan.spec is not None and plan.spec.label != entry.spec_label:
|
|
573
|
+
retyped = True
|
|
532
574
|
entry.spec_label = plan.spec.label
|
|
575
|
+
_forget_value_read_as_another_type(entry)
|
|
576
|
+
if plan.spec is not None:
|
|
533
577
|
if not plan.spec.accepts_length_override:
|
|
534
|
-
entry.length
|
|
578
|
+
# `or entry.length`: the AOB pattern spec declares a
|
|
579
|
+
# length of 0 — the scanner derives a match's width from
|
|
580
|
+
# the pattern, and an entry has none to derive from — so
|
|
581
|
+
# keep the width it already has.
|
|
582
|
+
entry.length = plan.spec.length or entry.length
|
|
535
583
|
|
|
536
584
|
if plan.value_text is not None:
|
|
537
585
|
spec = entry.spec
|
|
586
|
+
# A retype to a variable-width spec re-sizes the entry from
|
|
587
|
+
# the value, rather than inheriting the width of the type it
|
|
588
|
+
# replaced. Otherwise three Int32 rows retyped to String
|
|
589
|
+
# with "hello" all fail on "the entry holds 4 — widen it
|
|
590
|
+
# first", and the bulk dialog has no width field, nor does
|
|
591
|
+
# a multi-row selection offer one: a dead end.
|
|
592
|
+
cap: Optional[int] = entry.length
|
|
593
|
+
if retyped and spec.accepts_length_override:
|
|
594
|
+
cap = None
|
|
538
595
|
try:
|
|
539
|
-
value, effective_length =
|
|
540
|
-
spec, plan.value_text,
|
|
596
|
+
value, effective_length = parse_value_for_write(
|
|
597
|
+
spec, plan.value_text, cap, current
|
|
541
598
|
)
|
|
542
599
|
except ValueError as exc:
|
|
543
600
|
failures.append((entry.address, str(exc)))
|
|
@@ -665,10 +722,15 @@ class CheatTable(QWidget):
|
|
|
665
722
|
)
|
|
666
723
|
if not ok:
|
|
667
724
|
return
|
|
668
|
-
self._entries[row]
|
|
725
|
+
entry = self._entries[row]
|
|
726
|
+
if chosen != entry.spec_label:
|
|
727
|
+
entry.spec_label = chosen
|
|
728
|
+
_forget_value_read_as_another_type(entry)
|
|
669
729
|
spec = find_spec(chosen) or VALUE_TYPES[0]
|
|
670
730
|
if not spec.accepts_length_override:
|
|
671
|
-
|
|
731
|
+
# Same as the bulk edit: the AOB pattern spec declares no width of
|
|
732
|
+
# its own, so the entry keeps the one it has.
|
|
733
|
+
entry.length = spec.length or entry.length
|
|
672
734
|
self._rebuild()
|
|
673
735
|
|
|
674
736
|
def _change_length(self, row: int) -> None:
|
|
@@ -678,7 +740,9 @@ class CheatTable(QWidget):
|
|
|
678
740
|
"Length (bytes):",
|
|
679
741
|
value=self._entries[row].length,
|
|
680
742
|
minValue=1,
|
|
681
|
-
|
|
743
|
+
# An entry already wider than the cap can still be shrunk from here;
|
|
744
|
+
# it just can't grow past it.
|
|
745
|
+
maxValue=max(MAX_ENTRY_LENGTH, self._entries[row].length),
|
|
682
746
|
)
|
|
683
747
|
if not ok:
|
|
684
748
|
return
|
|
@@ -736,6 +800,41 @@ class CheatTable(QWidget):
|
|
|
736
800
|
QMessageBox.warning(self, "Import", f"Skipped a bad entry: {exc}")
|
|
737
801
|
|
|
738
802
|
|
|
803
|
+
# Ceiling for an entry's buffer width. Entries are promoted at the width of the
|
|
804
|
+
# value scanned for, which the scanner doesn't cap, so 1024 was too tight — but
|
|
805
|
+
# the poll worker allocates this many bytes per entry on every 100 ms tick, so
|
|
806
|
+
# an unbounded field turns one typo into a multi-gigabyte allocation the tick's
|
|
807
|
+
# blanket except swallows and retries forever. A megabyte is far past any real
|
|
808
|
+
# value and still cheap to read ten times a second.
|
|
809
|
+
MAX_ENTRY_LENGTH = 1_048_576
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _forget_value_read_as_another_type(entry: CheatEntry) -> None:
|
|
813
|
+
"""Drop the cached value when ``new_spec`` can't read what produced it.
|
|
814
|
+
|
|
815
|
+
``last_value`` and ``frozen_value`` hold whatever the *previous* spec
|
|
816
|
+
decoded — an int, a str, raw bytes. Nothing waits for a fresh poll tick
|
|
817
|
+
before the new spec is used on them, and a spec's ``format`` only accepts
|
|
818
|
+
what its own ``pytype`` produces: ``_fmt_bytes(1234)`` raises TypeError from
|
|
819
|
+
inside a Qt slot, and a frozen entry would be re-published to the poll
|
|
820
|
+
worker to write the old type's value through the new type's ``pytype``.
|
|
821
|
+
The next tick refills both, so forgetting them costs a single frame.
|
|
822
|
+
|
|
823
|
+
Unconditional: even a switch that keeps the ``pytype`` can change the
|
|
824
|
+
width, and a value decoded at the old one means nothing at the new. A
|
|
825
|
+
caller that still needs the bytes — the bulk edit writes a value in the
|
|
826
|
+
same pass — must capture them before calling.
|
|
827
|
+
|
|
828
|
+
The freeze is released with it. Leaving the box ticked with no target would
|
|
829
|
+
make the next poll tick adopt whatever the address happens to hold, pinning
|
|
830
|
+
a value the user never chose; a released box is visible and re-arming it is
|
|
831
|
+
one click.
|
|
832
|
+
"""
|
|
833
|
+
entry.last_value = None
|
|
834
|
+
entry.frozen_value = None
|
|
835
|
+
entry.frozen = False
|
|
836
|
+
|
|
837
|
+
|
|
739
838
|
def prompt_for_manual_entry(parent) -> Optional[CheatEntry]:
|
|
740
839
|
"""Sequential QInputDialog flow for the "Add Address Manually" button."""
|
|
741
840
|
description, ok = QInputDialog.getText(
|
|
@@ -763,13 +862,16 @@ def prompt_for_manual_entry(parent) -> Optional[CheatEntry]:
|
|
|
763
862
|
return None
|
|
764
863
|
spec = find_spec(spec_label) or VALUE_TYPES[0]
|
|
765
864
|
|
|
865
|
+
# The AOB pattern spec declares a length of 0 (the scanner derives a match's
|
|
866
|
+
# width from the pattern), and an address added by hand has no pattern to
|
|
867
|
+
# measure — so ask for the width instead of minting a zero-byte buffer.
|
|
766
868
|
length = spec.length
|
|
767
|
-
if spec.accepts_length_override:
|
|
869
|
+
if spec.accepts_length_override or not spec.length:
|
|
768
870
|
length, ok = QInputDialog.getInt(
|
|
769
871
|
parent,
|
|
770
872
|
"Add address",
|
|
771
873
|
"Buffer length (bytes):",
|
|
772
|
-
value=spec.length,
|
|
874
|
+
value=spec.length or 4,
|
|
773
875
|
minValue=1,
|
|
774
876
|
maxValue=1024,
|
|
775
877
|
)
|
|
@@ -47,7 +47,12 @@ from PySide6.QtWidgets import (
|
|
|
47
47
|
from PyMemoryEditor import AbstractProcess
|
|
48
48
|
|
|
49
49
|
from ._widgets import parse_hex_address, parse_offsets, resolve_base_address
|
|
50
|
-
from .value_types import
|
|
50
|
+
from .value_types import (
|
|
51
|
+
VALUE_TYPES,
|
|
52
|
+
ValueTypeSpec,
|
|
53
|
+
find_spec,
|
|
54
|
+
has_readable_width,
|
|
55
|
+
)
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
# Child of "PyMemoryEditor" — surfaced by the Log Console via propagation.
|
|
@@ -216,6 +221,13 @@ class PointerChainDialog(QDialog):
|
|
|
216
221
|
|
|
217
222
|
self._value_type_combo = QComboBox()
|
|
218
223
|
for spec in VALUE_TYPES:
|
|
224
|
+
# The address is already known here, so the only spec that can't be
|
|
225
|
+
# offered is the one with no width of its own — an IDA pattern,
|
|
226
|
+
# which would read zero bytes. A regex is welcome: its width comes
|
|
227
|
+
# from the Length field and it renders the bytes as text up to the
|
|
228
|
+
# first NUL, which "String (UTF-8)" doesn't do.
|
|
229
|
+
if not has_readable_width(spec):
|
|
230
|
+
continue
|
|
219
231
|
self._value_type_combo.addItem(spec.label)
|
|
220
232
|
form.addRow("Read value as:", self._value_type_combo)
|
|
221
233
|
|
|
@@ -59,7 +59,12 @@ from ._widgets import (
|
|
|
59
59
|
parse_hex_address,
|
|
60
60
|
shutdown_worker_thread,
|
|
61
61
|
)
|
|
62
|
-
from .value_types import
|
|
62
|
+
from .value_types import (
|
|
63
|
+
VALUE_TYPES,
|
|
64
|
+
ValueTypeSpec,
|
|
65
|
+
find_spec,
|
|
66
|
+
has_readable_width,
|
|
67
|
+
)
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
_LOG = logging.getLogger(__name__)
|
|
@@ -409,8 +414,13 @@ class PointerScanDialog(TearsDownOnClose, QDialog):
|
|
|
409
414
|
# type into the cheat table on promotion.
|
|
410
415
|
self._value_type_combo = QComboBox()
|
|
411
416
|
for spec in VALUE_TYPES:
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
# The address is already known here, so the only spec that can't be
|
|
418
|
+
# offered is the one with no width of its own — an IDA pattern,
|
|
419
|
+
# which would read zero bytes. A regex is welcome: its width comes
|
|
420
|
+
# from the Length field and it renders the bytes as text up to the
|
|
421
|
+
# first NUL, which "String (UTF-8)" doesn't do.
|
|
422
|
+
if not has_readable_width(spec):
|
|
423
|
+
continue
|
|
414
424
|
self._value_type_combo.addItem(spec.label)
|
|
415
425
|
self._value_type_combo.currentTextChanged.connect(self._on_value_type_changed)
|
|
416
426
|
form.addRow("Read value as:", self._value_type_combo)
|
|
@@ -22,7 +22,12 @@ from PySide6.QtCore import QThread, Signal
|
|
|
22
22
|
|
|
23
23
|
from PyMemoryEditor import AbstractProcess, MemoryRegion, ScanTypesEnum
|
|
24
24
|
|
|
25
|
-
from .scan_types import
|
|
25
|
+
from .scan_types import (
|
|
26
|
+
DELTA_SCAN_TYPES,
|
|
27
|
+
NextScanType,
|
|
28
|
+
NO_VALUE_SCAN_TYPES,
|
|
29
|
+
ScanType,
|
|
30
|
+
)
|
|
26
31
|
from .value_types import parse_value, ValueTypeSpec
|
|
27
32
|
|
|
28
33
|
|
|
@@ -85,6 +90,7 @@ def build_scan_request(
|
|
|
85
90
|
value_text: str,
|
|
86
91
|
second_value_text: str = "",
|
|
87
92
|
length_spin_value: Optional[int] = None,
|
|
93
|
+
previous_scan_length: Optional[int] = None,
|
|
88
94
|
writeable_only: bool = False,
|
|
89
95
|
with_value: bool = True,
|
|
90
96
|
) -> ScanRequest:
|
|
@@ -93,11 +99,19 @@ def build_scan_request(
|
|
|
93
99
|
|
|
94
100
|
This is the pure core of ``ScannerPanel._build_request`` lifted out of the
|
|
95
101
|
widget so the request-assembly rules (pattern short-circuit, the
|
|
96
|
-
|
|
97
|
-
unit-tested without a ``QApplication``. The widget keeps only the
|
|
98
|
-
are genuinely UI: reading the fields and showing a ``QMessageBox``
|
|
99
|
-
``ValueError`` raised here.
|
|
100
|
-
|
|
102
|
+
value-derived width for str / bytes, range parsing, the no-value scan types)
|
|
103
|
+
can be unit-tested without a ``QApplication``. The widget keeps only the
|
|
104
|
+
bits that are genuinely UI: reading the fields and showing a ``QMessageBox``
|
|
105
|
+
on the ``ValueError`` raised here.
|
|
106
|
+
|
|
107
|
+
:param length_spin_value: the Length field. Only the regex type reads it
|
|
108
|
+
(as ``byte_length``) — every other type derives its width from the spec
|
|
109
|
+
or from the value itself.
|
|
110
|
+
:param previous_scan_length: the width the scan that produced the current
|
|
111
|
+
results used. Only the no-value comparisons read it, and only for the
|
|
112
|
+
variable-width types, whose baseline is meaningless at another width.
|
|
113
|
+
(The ``*_BY`` deltas compare against the baseline too, but they are
|
|
114
|
+
rejected outright for those types — see below.)
|
|
101
115
|
:raises ValueError: if a value/pattern fails to parse (message is
|
|
102
116
|
user-facing — the caller picks the dialog title from ``spec.is_pattern``).
|
|
103
117
|
"""
|
|
@@ -116,19 +130,35 @@ def build_scan_request(
|
|
|
116
130
|
writeable_only=writeable_only,
|
|
117
131
|
)
|
|
118
132
|
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
# "Increased/Decreased value BY" adds the delta to the baseline, which only
|
|
134
|
+
# means anything for a number: on str/bytes ``prev + exp`` concatenates (so
|
|
135
|
+
# the comparison is never true) and ``prev - exp`` raises TypeError, which
|
|
136
|
+
# the refine worker swallows into "doesn't match". Either way every address
|
|
137
|
+
# is dropped and the user is told nothing, so reject the combination with a
|
|
138
|
+
# message instead. Checked *after* the pattern short-circuit above: the
|
|
139
|
+
# pattern specs are bytes-typed too, but they force EXACT regardless of the
|
|
140
|
+
# scan type passed, and the comparisons this message points at are disabled
|
|
141
|
+
# in pattern mode anyway.
|
|
142
|
+
if scan_type in DELTA_SCAN_TYPES and spec.pytype in (str, bytes):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"Increased/Decreased Value By adds a numeric amount to the previous "
|
|
145
|
+
"value, which doesn't apply to %s. Use Changed Value or Unchanged "
|
|
146
|
+
"Value to compare against the previous scan." % spec.label
|
|
147
|
+
)
|
|
127
148
|
|
|
128
|
-
# Increased/Decreased/Changed/Unchanged compare
|
|
129
|
-
#
|
|
149
|
+
# Increased/Decreased/Changed/Unchanged compare the value read now against
|
|
150
|
+
# the one the *previous* scan recorded, so they need no target value — but
|
|
151
|
+
# they must re-read at the width that baseline was recorded with. Reading
|
|
152
|
+
# 16 bytes where the first scan recorded 4 yields "olá\0\0…" against "olá",
|
|
153
|
+
# which never compares equal, so every address would report as Changed.
|
|
154
|
+
# ``previous_scan_length`` carries that width for the variable-width types;
|
|
155
|
+
# the fixed-width types own theirs and ignore it.
|
|
130
156
|
if scan_type in NO_VALUE_SCAN_TYPES:
|
|
131
|
-
length =
|
|
157
|
+
length = (
|
|
158
|
+
previous_scan_length
|
|
159
|
+
if spec.accepts_length_override and previous_scan_length
|
|
160
|
+
else spec.length
|
|
161
|
+
)
|
|
132
162
|
return ScanRequest(
|
|
133
163
|
spec=spec,
|
|
134
164
|
length=int(length),
|
|
@@ -137,14 +167,22 @@ def build_scan_request(
|
|
|
137
167
|
writeable_only=writeable_only,
|
|
138
168
|
)
|
|
139
169
|
|
|
170
|
+
# Every scan that carries a value sizes its buffer from that value: the
|
|
171
|
+
# numeric types have a fixed width, and str / bytes derive theirs in
|
|
172
|
+
# parse_value (the text's UTF-8 byte length / the number of hex bytes
|
|
173
|
+
# entered). So no length override is passed here. Letting the Length field
|
|
174
|
+
# win could only break the scan — a width below the value's raises
|
|
175
|
+
# "byte string too long" from the fixed-width ctypes buffer, and a width
|
|
176
|
+
# above it NUL-pads the target, silently searching for "the value followed
|
|
177
|
+
# by zeros". Partial matching has its own value type (AOB Pattern).
|
|
140
178
|
value: Any
|
|
141
179
|
if scan_type in (ScanTypesEnum.VALUE_BETWEEN, ScanTypesEnum.NOT_VALUE_BETWEEN):
|
|
142
|
-
lo, lo_len = parse_value(spec, value_text
|
|
143
|
-
hi, hi_len = parse_value(spec, second_value_text
|
|
180
|
+
lo, lo_len = parse_value(spec, value_text)
|
|
181
|
+
hi, hi_len = parse_value(spec, second_value_text)
|
|
144
182
|
length = max(lo_len, hi_len)
|
|
145
183
|
value = (lo, hi)
|
|
146
184
|
else:
|
|
147
|
-
value, length = parse_value(spec, value_text
|
|
185
|
+
value, length = parse_value(spec, value_text)
|
|
148
186
|
|
|
149
187
|
if not with_value:
|
|
150
188
|
value = None # Used by callers that only need spec/length/scan_type.
|
|
@@ -6,7 +6,7 @@ Inputs:
|
|
|
6
6
|
* primary value (and a second value for "Value Between" / "Not Value Between")
|
|
7
7
|
* value type
|
|
8
8
|
* scan type
|
|
9
|
-
*
|
|
9
|
+
* byte length (fixed per numeric type; derived from the value for str / bytes)
|
|
10
10
|
* "writable regions only" toggle (passed to PyMemoryEditor as ``writeable_only``)
|
|
11
11
|
|
|
12
12
|
Outputs (signals):
|
|
@@ -44,7 +44,30 @@ from .scan_types import (
|
|
|
44
44
|
is_next_scan_type,
|
|
45
45
|
)
|
|
46
46
|
from .scan_worker import build_scan_request, ScanRequest
|
|
47
|
-
from .value_types import VALUE_TYPES, find_spec
|
|
47
|
+
from .value_types import parse_value, ValueTypeSpec, VALUE_TYPES, find_spec
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Shown in the (read-only) Length field of String / Byte Array before a value
|
|
51
|
+
# has been entered: those types take their width from the value itself, so
|
|
52
|
+
# there is genuinely no number to report yet.
|
|
53
|
+
EMPTY_LENGTH_TEXT = "— (set by the value)"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_sized_by_value(spec: ValueTypeSpec) -> bool:
|
|
57
|
+
"""True for the types whose buffer width comes from the value entered.
|
|
58
|
+
|
|
59
|
+
String (UTF-8) and Byte Array (Hex) only. The numeric types have a fixed
|
|
60
|
+
width, an IDA pattern derives one from the pattern, and a regex's Length
|
|
61
|
+
field is a genuine user-set ``byte_length`` — none of them may have their
|
|
62
|
+
width taken from a scan's value.
|
|
63
|
+
"""
|
|
64
|
+
return spec.pytype in (str, bytes) and not spec.is_pattern
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# The two scan types that take a second value; their width is max(lo, hi).
|
|
68
|
+
RANGE_SCAN_TYPES = frozenset(
|
|
69
|
+
(ScanTypesEnum.VALUE_BETWEEN, ScanTypesEnum.NOT_VALUE_BETWEEN)
|
|
70
|
+
)
|
|
48
71
|
|
|
49
72
|
|
|
50
73
|
SCAN_TYPE_CHOICES = (
|
|
@@ -79,6 +102,17 @@ class ScannerPanel(QWidget):
|
|
|
79
102
|
self._has_results = False
|
|
80
103
|
self._busy = False
|
|
81
104
|
self._initial_focus_done = False
|
|
105
|
+
# Width the scan that produced the current results ran at. The no-value
|
|
106
|
+
# comparisons (Increased / Changed / …) must re-read at exactly this
|
|
107
|
+
# width or they compare against a baseline recorded at another one; the
|
|
108
|
+
# Length readout can't stand in, since it tracks whatever value is in
|
|
109
|
+
# the field right now, which the user is free to edit between scans.
|
|
110
|
+
self._last_scan_length: Optional[int] = None
|
|
111
|
+
# Width of a scan that has been dispatched but hasn't landed yet. It is
|
|
112
|
+
# promoted above only when the owner reports results, so a scan that
|
|
113
|
+
# errors out or finds nothing leaves the values on screen described by
|
|
114
|
+
# the width they were actually read at.
|
|
115
|
+
self._pending_scan_length: Optional[int] = None
|
|
82
116
|
self._build_ui()
|
|
83
117
|
self._refresh_buttons()
|
|
84
118
|
|
|
@@ -119,14 +153,17 @@ class ScannerPanel(QWidget):
|
|
|
119
153
|
self._value_edit = QLineEdit()
|
|
120
154
|
self._value_edit.setPlaceholderText("e.g. 100 or 0x64 or Hello")
|
|
121
155
|
self._value_edit.returnPressed.connect(self._on_value_submitted)
|
|
122
|
-
# For String (UTF-8) the length is dictated by the
|
|
123
|
-
# the (disabled) length field in sync as the user types.
|
|
156
|
+
# For String (UTF-8) and Byte Array (Hex) the length is dictated by the
|
|
157
|
+
# typed value, so keep the (disabled) length field in sync as the user types.
|
|
124
158
|
self._value_edit.textChanged.connect(self._on_value_text_changed)
|
|
125
159
|
value_form.addRow("Value:", self._value_edit)
|
|
126
160
|
|
|
127
161
|
self._second_value_edit = QLineEdit()
|
|
128
162
|
self._second_value_edit.setPlaceholderText("Upper bound (for ranges only)")
|
|
129
163
|
self._second_value_edit.returnPressed.connect(self._on_value_submitted)
|
|
164
|
+
# A range scan sizes with max(lo, hi), so the upper bound moves the
|
|
165
|
+
# readout just as the primary value does.
|
|
166
|
+
self._second_value_edit.textChanged.connect(self._on_value_text_changed)
|
|
130
167
|
self._second_value_label = QLabel("Up to:")
|
|
131
168
|
value_form.addRow(self._second_value_label, self._second_value_edit)
|
|
132
169
|
self._second_value_edit.hide()
|
|
@@ -205,7 +242,7 @@ class ScannerPanel(QWidget):
|
|
|
205
242
|
|
|
206
243
|
self._cancel_btn = QPushButton("Cancel scan")
|
|
207
244
|
self._cancel_btn.setObjectName("danger")
|
|
208
|
-
self._cancel_btn.clicked.connect(self.
|
|
245
|
+
self._cancel_btn.clicked.connect(self._on_cancel)
|
|
209
246
|
buttons.addWidget(self._cancel_btn)
|
|
210
247
|
|
|
211
248
|
layout.addWidget(buttons_box)
|
|
@@ -216,11 +253,34 @@ class ScannerPanel(QWidget):
|
|
|
216
253
|
self._on_scan_type_changed(0)
|
|
217
254
|
|
|
218
255
|
def set_has_results(self, has_results: bool) -> None:
|
|
256
|
+
"""Report whether the results table currently holds anything.
|
|
257
|
+
|
|
258
|
+
Called by the owner once a scan has actually finished, which is what
|
|
259
|
+
makes it the right moment to adopt that scan's width as the baseline.
|
|
260
|
+
"""
|
|
219
261
|
self._has_results = has_results
|
|
262
|
+
if has_results:
|
|
263
|
+
if self._pending_scan_length:
|
|
264
|
+
self._last_scan_length = self._pending_scan_length
|
|
265
|
+
else:
|
|
266
|
+
self._last_scan_length = None
|
|
267
|
+
self._pending_scan_length = None
|
|
220
268
|
self._refresh_buttons()
|
|
221
269
|
|
|
222
270
|
def set_busy(self, busy: bool) -> None:
|
|
271
|
+
"""Report whether a scan is running.
|
|
272
|
+
|
|
273
|
+
The falling edge ends the scan cycle, which is where a pending width
|
|
274
|
+
that was never adopted gets dropped. The owner emits it after the
|
|
275
|
+
completion signal (``finished`` follows ``finished_ok``), so a scan that
|
|
276
|
+
landed has already had its width promoted by ``set_has_results``; one
|
|
277
|
+
that errored out never will, and must not leave the width behind for
|
|
278
|
+
an unrelated later completion — an "Update Values" refresh reports
|
|
279
|
+
results too — to pick up.
|
|
280
|
+
"""
|
|
223
281
|
self._busy = busy
|
|
282
|
+
if not busy:
|
|
283
|
+
self._pending_scan_length = None
|
|
224
284
|
self._refresh_buttons()
|
|
225
285
|
|
|
226
286
|
def use_snapshot_cache(self) -> bool:
|
|
@@ -253,7 +313,7 @@ class ScannerPanel(QWidget):
|
|
|
253
313
|
|
|
254
314
|
is_pattern = spec.is_pattern
|
|
255
315
|
is_regex = spec.is_regex
|
|
256
|
-
|
|
316
|
+
sized_by_value = is_sized_by_value(spec)
|
|
257
317
|
|
|
258
318
|
# Pattern modes reuse the "Value" line for the pattern and force the
|
|
259
319
|
# scan-type combo to EXACT (Bigger/Smaller/Between don't apply). The
|
|
@@ -261,15 +321,15 @@ class ScannerPanel(QWidget):
|
|
|
261
321
|
# token count), but a *regex* has no inferable width, so its Length
|
|
262
322
|
# field stays enabled and supplies search_by_pattern's byte_length.
|
|
263
323
|
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
324
|
+
# Regex is the one spec whose width is genuinely the user's to set (its
|
|
325
|
+
# byte_length is the max match width, which nothing can infer). String /
|
|
326
|
+
# Byte Array mirror the value they were given — overriding that could
|
|
327
|
+
# only truncate it, which the fixed-width ctypes buffer rejects, or
|
|
328
|
+
# over-allocate it, which NUL-pads the target into a silent search for
|
|
329
|
+
# "the value followed by zeros". Everything else is fixed by its spec.
|
|
330
|
+
# The field stays visible as a read-only readout throughout, kept in
|
|
331
|
+
# sync by _sync_value_length / _on_value_text_changed.
|
|
332
|
+
self._length_spin.setEnabled(is_regex)
|
|
273
333
|
|
|
274
334
|
if is_regex:
|
|
275
335
|
self._value_edit.setPlaceholderText(
|
|
@@ -282,6 +342,13 @@ class ScannerPanel(QWidget):
|
|
|
282
342
|
else:
|
|
283
343
|
self._value_edit.setPlaceholderText("e.g. 100 or 0x64 or Hello")
|
|
284
344
|
|
|
345
|
+
# Every type but the value-sized pair owns a real number here, so clear
|
|
346
|
+
# the "no width yet" slot the previous type may have opened (0 would
|
|
347
|
+
# otherwise render as EMPTY_LENGTH_TEXT for e.g. "1 Byte (Int8)").
|
|
348
|
+
if not sized_by_value:
|
|
349
|
+
self._length_spin.setSpecialValueText("")
|
|
350
|
+
self._length_spin.setMinimum(1)
|
|
351
|
+
|
|
285
352
|
if is_regex:
|
|
286
353
|
# Length = the regex's max match width in bytes (byte_length); it
|
|
287
354
|
# drives the chunk overlap so a match straddling a chunk boundary is
|
|
@@ -294,16 +361,22 @@ class ScannerPanel(QWidget):
|
|
|
294
361
|
self._length_spin.setMaximum(1024)
|
|
295
362
|
self._length_spin.setValue(1)
|
|
296
363
|
self._length_spin.setSuffix(" bytes")
|
|
297
|
-
elif
|
|
298
|
-
# Length tracks the typed
|
|
299
|
-
# aren't visually clamped, then mirror the current
|
|
364
|
+
elif sized_by_value: # String (UTF-8) / Byte Array (Hex)
|
|
365
|
+
# Length tracks the typed value — raise the ceiling so long strings
|
|
366
|
+
# aren't visually clamped, then mirror the current value's byte size.
|
|
367
|
+
#
|
|
368
|
+
# Until a value has been entered there is no width to report, and
|
|
369
|
+
# the readout is read-only, so the user can't correct a number we
|
|
370
|
+
# invent. Open a 0 slot rendered as EMPTY_LENGTH_TEXT for that
|
|
371
|
+
# state rather than seeding the spec default (which would show
|
|
372
|
+
# "4 bytes" for an empty byte array and then jump to "1 byte" on
|
|
373
|
+
# the first hex digit) or keeping a width the previous type wrote.
|
|
374
|
+
self._length_spin.setMinimum(0)
|
|
375
|
+
self._length_spin.setSpecialValueText(EMPTY_LENGTH_TEXT)
|
|
300
376
|
self._length_spin.setMaximum(2_147_483_647)
|
|
301
377
|
self._length_spin.setSuffix(" bytes")
|
|
302
|
-
self.
|
|
303
|
-
|
|
304
|
-
self._length_spin.setMaximum(1024)
|
|
305
|
-
self._length_spin.setValue(max(4, self._length_spin.value()))
|
|
306
|
-
self._length_spin.setSuffix(" bytes")
|
|
378
|
+
self._length_spin.setValue(0)
|
|
379
|
+
self._sync_value_length()
|
|
307
380
|
else:
|
|
308
381
|
self._length_spin.setMaximum(1024)
|
|
309
382
|
self._length_spin.setValue(spec.length)
|
|
@@ -332,30 +405,62 @@ class ScannerPanel(QWidget):
|
|
|
332
405
|
self._refresh_buttons()
|
|
333
406
|
|
|
334
407
|
def _on_value_text_changed(self, text: str) -> None:
|
|
335
|
-
#
|
|
336
|
-
|
|
408
|
+
# _sync_value_length ignores the types that own their length field.
|
|
409
|
+
self._sync_value_length(text)
|
|
410
|
+
|
|
411
|
+
def _sync_value_length(self, text: Optional[str] = None) -> None:
|
|
412
|
+
"""Mirror the byte size of the value text into the length field.
|
|
413
|
+
|
|
414
|
+
Matches ``parse_value``'s rules for the two variable-width types so the
|
|
415
|
+
read-only readout shows exactly the buffer width the scan will use: the
|
|
416
|
+
UTF-8 byte length for a string (byte length, not character count) and
|
|
417
|
+
the number of parsed hex bytes for a byte array.
|
|
418
|
+
|
|
419
|
+
The readout is exactly what the current value sizes to, and nothing
|
|
420
|
+
else: an empty field, or a half-typed byte array ("00 1"), reports no
|
|
421
|
+
width at all (EMPTY_LENGTH_TEXT) rather than a number no scan would
|
|
422
|
+
use. A range scan sizes with ``max(lo, hi)``, so both bounds count.
|
|
423
|
+
The "Next Scan" comparisons that carry no value of their own don't read
|
|
424
|
+
this field — they refine at ``_last_scan_length``, the width the scan
|
|
425
|
+
holding the current results actually ran at.
|
|
426
|
+
|
|
427
|
+
``text`` is accepted (and ignored) so the method can sit directly on a
|
|
428
|
+
``textChanged`` signal; the width always comes from reading the fields,
|
|
429
|
+
since either of the two can be the one that sets it.
|
|
430
|
+
"""
|
|
431
|
+
del text # Both fields are read below; see the docstring.
|
|
337
432
|
spec = find_spec(self._type_combo.currentText())
|
|
338
|
-
|
|
339
|
-
|
|
433
|
+
# Only the value-sized types have a width to mirror; every other type
|
|
434
|
+
# owns the field (a fixed width, or the regex's editable match width)
|
|
435
|
+
# and must not have it overwritten from here.
|
|
436
|
+
if spec is None or not is_sized_by_value(spec):
|
|
437
|
+
return
|
|
340
438
|
|
|
341
|
-
|
|
342
|
-
|
|
439
|
+
texts = [self._value_edit.text()]
|
|
440
|
+
# Read the scan type rather than the widget's visibility: a child of a
|
|
441
|
+
# panel that hasn't been shown yet reports isVisible() False even after
|
|
442
|
+
# setVisible(True), which would silently drop the upper bound.
|
|
443
|
+
_, scan_type = SCAN_TYPE_CHOICES[self._scan_combo.currentIndex()]
|
|
444
|
+
if scan_type in RANGE_SCAN_TYPES:
|
|
445
|
+
texts.append(self._second_value_edit.text())
|
|
343
446
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
447
|
+
length = 0
|
|
448
|
+
for candidate in texts:
|
|
449
|
+
try:
|
|
450
|
+
_, candidate_length = parse_value(spec, candidate)
|
|
451
|
+
except ValueError:
|
|
452
|
+
continue
|
|
453
|
+
length = max(length, candidate_length)
|
|
454
|
+
|
|
455
|
+
self._length_spin.setValue(length)
|
|
350
456
|
|
|
351
457
|
def _on_scan_type_changed(self, index: int) -> None:
|
|
352
458
|
_, scan_type = SCAN_TYPE_CHOICES[index]
|
|
353
|
-
ranged = scan_type in
|
|
354
|
-
ScanTypesEnum.VALUE_BETWEEN,
|
|
355
|
-
ScanTypesEnum.NOT_VALUE_BETWEEN,
|
|
356
|
-
)
|
|
459
|
+
ranged = scan_type in RANGE_SCAN_TYPES
|
|
357
460
|
self._second_value_edit.setVisible(ranged)
|
|
358
461
|
self._second_value_label.setVisible(ranged)
|
|
462
|
+
# Entering or leaving a range changes which fields size the scan.
|
|
463
|
+
self._sync_value_length()
|
|
359
464
|
|
|
360
465
|
# In pattern mode the Value field holds the AOB pattern and the
|
|
361
466
|
# scan-type combo is forced to EXACT, so leave its value field alone.
|
|
@@ -393,6 +498,7 @@ class ScannerPanel(QWidget):
|
|
|
393
498
|
value_text=self._value_edit.text(),
|
|
394
499
|
second_value_text=self._second_value_edit.text(),
|
|
395
500
|
length_spin_value=self._length_spin.value(),
|
|
501
|
+
previous_scan_length=self._last_scan_length,
|
|
396
502
|
writeable_only=self._writable_check.isChecked(),
|
|
397
503
|
with_value=with_value,
|
|
398
504
|
)
|
|
@@ -415,24 +521,117 @@ class ScannerPanel(QWidget):
|
|
|
415
521
|
return
|
|
416
522
|
request = self._build_request()
|
|
417
523
|
if request is not None:
|
|
524
|
+
self._pending_scan_length = self._dispatched_width(request)
|
|
418
525
|
self.first_scan_requested.emit(request)
|
|
419
526
|
|
|
420
527
|
def _on_next_scan(self) -> None:
|
|
421
528
|
request = self._build_request()
|
|
422
529
|
if request is not None:
|
|
530
|
+
# The refine rewrites every kept value at this width, so it becomes
|
|
531
|
+
# the baseline the next no-value comparison has to match — once it
|
|
532
|
+
# has actually run.
|
|
533
|
+
self._pending_scan_length = self._dispatched_width(request)
|
|
423
534
|
self.next_scan_requested.emit(request)
|
|
424
535
|
|
|
536
|
+
def _dispatched_width(self, request: ScanRequest) -> int:
|
|
537
|
+
"""Width the rows this request finds will have been read at.
|
|
538
|
+
|
|
539
|
+
Normally the request's own length. An IDA pattern is the exception: it
|
|
540
|
+
reports 0, because the scanner derives a match's width from the pattern
|
|
541
|
+
rather than from a field — so the width one hit occupies is the token
|
|
542
|
+
count, which is what a promoted row has to be read at.
|
|
543
|
+
"""
|
|
544
|
+
if request.spec.is_pattern and not request.spec.is_regex:
|
|
545
|
+
return self._pattern_byte_length()
|
|
546
|
+
return request.length
|
|
547
|
+
|
|
548
|
+
def _on_cancel(self) -> None:
|
|
549
|
+
# Both workers still report results after a cancel — they break out of
|
|
550
|
+
# the loop and emit finished_ok with what they have — so what the
|
|
551
|
+
# partial results are worth depends on which scan was running.
|
|
552
|
+
#
|
|
553
|
+
# A refine re-read only the rows it reached at the new width; the rest
|
|
554
|
+
# still hold values recorded at the old one, so neither width describes
|
|
555
|
+
# the table and the previous one (which most rows match) stands.
|
|
556
|
+
#
|
|
557
|
+
# A first scan is the opposite: every address it did find, it found at
|
|
558
|
+
# its own width, and there is no earlier width to fall back on. Dropping
|
|
559
|
+
# it would send the next Changed/Unchanged refine to the spec default —
|
|
560
|
+
# 16 for a String scanned at 4 — which is the failure this whole branch
|
|
561
|
+
# exists to fix. _has_results tells the two apart: a first scan only
|
|
562
|
+
# runs when there are none yet.
|
|
563
|
+
if self._has_results:
|
|
564
|
+
self._pending_scan_length = None
|
|
565
|
+
self.cancel_requested.emit()
|
|
566
|
+
|
|
425
567
|
def _on_update_values(self) -> None:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
568
|
+
# A read-only refresh of the rows already on screen. RefineScanWorker
|
|
569
|
+
# applies no comparison when filter_only is False, so this needs no
|
|
570
|
+
# target value — and must not parse one: the Value box may be empty
|
|
571
|
+
# (a no-value scan type clears it outright), which would abort the
|
|
572
|
+
# refresh with "Invalid value" instead of refreshing.
|
|
573
|
+
spec, length = self.current_spec_and_length()
|
|
574
|
+
# The refresh re-reads and patches every row at this width, so it is the
|
|
575
|
+
# width the table holds once it lands — the same one it was scanned at,
|
|
576
|
+
# recorded here so that a width left behind by a request the owner
|
|
577
|
+
# rejected before the busy cycle began (an early return in its handler)
|
|
578
|
+
# is overwritten rather than adopted in its place.
|
|
579
|
+
self._pending_scan_length = length
|
|
580
|
+
self.update_values_requested.emit(
|
|
581
|
+
ScanRequest(
|
|
582
|
+
spec=spec,
|
|
583
|
+
length=length,
|
|
584
|
+
scan_type=ScanTypesEnum.EXACT_VALUE,
|
|
585
|
+
value=None,
|
|
586
|
+
writeable_only=self._writable_check.isChecked(),
|
|
587
|
+
)
|
|
588
|
+
)
|
|
429
589
|
|
|
430
590
|
def current_spec_and_length(self):
|
|
431
|
-
"""Return the
|
|
591
|
+
"""Return the (spec, width) the rows currently on screen were read at.
|
|
592
|
+
|
|
593
|
+
Used by the Promote-to-Cheat-Table path and by the "Update Values"
|
|
594
|
+
refresh — both act on those rows, so both need the width their scan
|
|
595
|
+
ran at rather than anything the Value box says now. An IDA hit keeps
|
|
596
|
+
this spec: a pattern-typed entry reads and writes correctly now (see
|
|
597
|
+
``parse_value_for_write``), and only its width has to come from the
|
|
598
|
+
pattern, since the spec declares none.
|
|
599
|
+
"""
|
|
432
600
|
spec = find_spec(self._type_combo.currentText())
|
|
433
601
|
if spec is None:
|
|
434
602
|
spec = VALUE_TYPES[0]
|
|
603
|
+
# An IDA pattern has no Length field and a spec length of 0 — the width
|
|
604
|
+
# of one match is the pattern's own, one token per byte. Prefer the
|
|
605
|
+
# width the scan ran at: the Value box stays editable in pattern mode,
|
|
606
|
+
# so re-deriving it here would read at a width the hits were never
|
|
607
|
+
# found at, which is the staleness _last_scan_length exists to avoid.
|
|
608
|
+
if spec.is_pattern and not spec.is_regex:
|
|
609
|
+
return spec, self._last_scan_length or self._pattern_byte_length()
|
|
610
|
+
|
|
611
|
+
# The rows being promoted were read at the width their scan ran at, so
|
|
612
|
+
# that is the width the cheat entry has to keep. The Length readout
|
|
613
|
+
# can't stand in: it follows the Value box, which a no-value scan type
|
|
614
|
+
# clears outright (a 4-byte "olá" scan would promote at the spec's 16,
|
|
615
|
+
# and the entry would read 12 bytes of neighbouring memory into the
|
|
616
|
+
# cell on every poll tick).
|
|
617
|
+
if is_sized_by_value(spec) and self._last_scan_length:
|
|
618
|
+
return spec, self._last_scan_length
|
|
619
|
+
|
|
435
620
|
length = (
|
|
436
621
|
self._length_spin.value() if spec.accepts_length_override else spec.length
|
|
437
622
|
)
|
|
438
|
-
|
|
623
|
+
# No scan has run yet and no value is entered (readout 0): a cheat entry
|
|
624
|
+
# can't have a zero-width buffer, so the spec default stands in.
|
|
625
|
+
return spec, int(length) or spec.length
|
|
626
|
+
|
|
627
|
+
def _pattern_byte_length(self) -> int:
|
|
628
|
+
"""Width of one match of the AOB pattern currently in the Value field."""
|
|
629
|
+
from PyMemoryEditor.util.pattern import compile_pattern
|
|
630
|
+
|
|
631
|
+
try:
|
|
632
|
+
return max(1, compile_pattern(self._value_edit.text().strip())[1])
|
|
633
|
+
except ValueError:
|
|
634
|
+
# The results being promoted came from a pattern that compiled, so
|
|
635
|
+
# this only happens if the field was edited afterwards. One byte is
|
|
636
|
+
# a harmless entry the user can widen from the cheat table.
|
|
637
|
+
return 1
|
|
@@ -35,6 +35,15 @@ class ValueTypeSpec:
|
|
|
35
35
|
# ``byte_length`` (the number of bytes one match consumes) that
|
|
36
36
|
# ``search_by_pattern`` requires for regex input.
|
|
37
37
|
is_regex: bool = False
|
|
38
|
+
# How cell text becomes the bytes to write back, for the types whose
|
|
39
|
+
# ``parse`` answers a different question — "what am I searching for?"
|
|
40
|
+
# rather than "what value is this?". Receives the text and the bytes last
|
|
41
|
+
# read at the address, so a wildcard can mean "leave that byte alone".
|
|
42
|
+
# ``None`` means ``parse`` already answers both, which is the case for
|
|
43
|
+
# every type that isn't a pattern.
|
|
44
|
+
parse_write: Optional[
|
|
45
|
+
Callable[[str, Optional[bytes], Optional[int]], Any]
|
|
46
|
+
] = None
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
def _parse_bool(text: str) -> bool:
|
|
@@ -86,6 +95,23 @@ def _parse_bytes(text: str) -> bytes:
|
|
|
86
95
|
raise ValueError(f"Invalid byte array: {exc}")
|
|
87
96
|
|
|
88
97
|
|
|
98
|
+
def _parse_str(text: str) -> str:
|
|
99
|
+
"""Return the value text verbatim, rejecting an empty one.
|
|
100
|
+
|
|
101
|
+
An empty string sizes to a 1-byte NUL buffer, so *scanning* for it matches
|
|
102
|
+
every zeroed byte in the target, and *writing* it is a no-op (``prepare_write``
|
|
103
|
+
truncates to the value and never pads). Neither is what the user meant.
|
|
104
|
+
``_parse_bytes`` already rejects its own empty input; this keeps the two
|
|
105
|
+
variable-width types consistent.
|
|
106
|
+
|
|
107
|
+
The wording stays neutral because this runs on the cheat table's write
|
|
108
|
+
paths too, not only on a scan.
|
|
109
|
+
"""
|
|
110
|
+
if not text:
|
|
111
|
+
raise ValueError("Empty value.")
|
|
112
|
+
return text
|
|
113
|
+
|
|
114
|
+
|
|
89
115
|
def _parse_pattern(text: str) -> str:
|
|
90
116
|
"""Validate an IDA-style AOB pattern and return it verbatim.
|
|
91
117
|
|
|
@@ -143,6 +169,69 @@ def _parse_regex(text: str) -> bytes:
|
|
|
143
169
|
return pattern
|
|
144
170
|
|
|
145
171
|
|
|
172
|
+
def _parse_pattern_write(
|
|
173
|
+
text: str, current: Optional[bytes], length_override: Optional[int] = None
|
|
174
|
+
) -> bytes:
|
|
175
|
+
"""Turn an IDA pattern typed into a value cell into the bytes to write.
|
|
176
|
+
|
|
177
|
+
``_parse_pattern`` answers the scanner's question and hands back the
|
|
178
|
+
pattern *text*, which is not something that can be written anywhere — the
|
|
179
|
+
cell would store the ASCII spelling of the hex it displays. Here the same
|
|
180
|
+
tokens are resolved to the bytes they name.
|
|
181
|
+
|
|
182
|
+
A ``?`` keeps whatever byte is already at that offset. That mirrors its
|
|
183
|
+
search meaning ("any byte") and is what makes a signature patchable: you
|
|
184
|
+
name the bytes you mean to change and leave the operands alone. It needs
|
|
185
|
+
the current contents, so a wildcard only works once the entry has been
|
|
186
|
+
read at least one poll tick.
|
|
187
|
+
"""
|
|
188
|
+
from PyMemoryEditor.util.pattern import tokenize_pattern
|
|
189
|
+
|
|
190
|
+
tokens = tokenize_pattern(text)
|
|
191
|
+
wildcards = [index for index, token in enumerate(tokens) if token is None]
|
|
192
|
+
|
|
193
|
+
if wildcards:
|
|
194
|
+
# `current` is whatever the entry's spec produced when it was last
|
|
195
|
+
# polled, and a type change doesn't wait for a fresh tick — so it can
|
|
196
|
+
# still be the int/str/float the *previous* type read. Anything but
|
|
197
|
+
# bytes is treated as "nothing read yet" rather than reaching len()
|
|
198
|
+
# and raising a TypeError the callers' `except ValueError` won't catch.
|
|
199
|
+
if not isinstance(current, (bytes, bytearray)):
|
|
200
|
+
current = None
|
|
201
|
+
if current is None:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
"A '?' keeps the byte that is already there, so it can't be "
|
|
204
|
+
"used before the value has been read at least once."
|
|
205
|
+
)
|
|
206
|
+
if len(current) < wildcards[-1] + 1:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"'?' at byte %d has nothing to keep: the last read of this "
|
|
209
|
+
"address returned %d byte(s). Wait for the next refresh, or "
|
|
210
|
+
"spell the byte out." % (wildcards[-1] + 1, len(current))
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return bytes(
|
|
214
|
+
current[index] if token is None else token # type: ignore[index]
|
|
215
|
+
for index, token in enumerate(tokens)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _parse_regex_write(
|
|
220
|
+
text: str, current: Optional[bytes], length_override: Optional[int] = None
|
|
221
|
+
) -> bytes:
|
|
222
|
+
"""Turn a value cell's text into bytes for a regex-typed entry.
|
|
223
|
+
|
|
224
|
+
A regex names a *set* of byte strings, so the pattern itself can't be
|
|
225
|
+
written back. What the cell shows is the text read at the address
|
|
226
|
+
(``_fmt_regex_match``), so an edit is taken literally — the same rule as
|
|
227
|
+
String (UTF-8) — and writes exactly the characters typed.
|
|
228
|
+
"""
|
|
229
|
+
del current, length_override # A literal write depends on neither.
|
|
230
|
+
if not text:
|
|
231
|
+
raise ValueError("Empty value.")
|
|
232
|
+
return text.encode("utf-8")
|
|
233
|
+
|
|
234
|
+
|
|
146
235
|
def _fmt_bytes(value: bytes) -> str:
|
|
147
236
|
if value is None:
|
|
148
237
|
return ""
|
|
@@ -231,7 +320,7 @@ VALUE_TYPES = (
|
|
|
231
320
|
"String (UTF-8)",
|
|
232
321
|
str,
|
|
233
322
|
16,
|
|
234
|
-
|
|
323
|
+
_parse_str,
|
|
235
324
|
lambda v: "" if v is None else str(v),
|
|
236
325
|
accepts_length_override=True,
|
|
237
326
|
),
|
|
@@ -254,6 +343,7 @@ VALUE_TYPES = (
|
|
|
254
343
|
lambda v: "" if v is None else (v if isinstance(v, str) else _fmt_bytes(v)),
|
|
255
344
|
accepts_length_override=False,
|
|
256
345
|
is_pattern=True,
|
|
346
|
+
parse_write=_parse_pattern_write,
|
|
257
347
|
),
|
|
258
348
|
# Text-regex scan — the "Value" input becomes a string regex (e.g.
|
|
259
349
|
# ``Player[0-9]+``) UTF-8 encoded into the bytes pattern, and the Length
|
|
@@ -269,6 +359,7 @@ VALUE_TYPES = (
|
|
|
269
359
|
accepts_length_override=True,
|
|
270
360
|
is_pattern=True,
|
|
271
361
|
is_regex=True,
|
|
362
|
+
parse_write=_parse_regex_write,
|
|
272
363
|
),
|
|
273
364
|
)
|
|
274
365
|
|
|
@@ -310,3 +401,83 @@ def parse_value(
|
|
|
310
401
|
# under-allocating would silently truncate the value the user typed.
|
|
311
402
|
length = max(1, len(value.encode("utf-8")))
|
|
312
403
|
return value, length
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _written_width(value: Any) -> int:
|
|
407
|
+
"""Bytes ``value`` occupies once written, or 0 when its spec sets the size.
|
|
408
|
+
|
|
409
|
+
``str`` is measured encoded: the entry's width is a byte count, so counting
|
|
410
|
+
characters would let a multibyte value overflow it.
|
|
411
|
+
"""
|
|
412
|
+
if isinstance(value, str):
|
|
413
|
+
return len(value.encode("utf-8"))
|
|
414
|
+
if isinstance(value, (bytes, bytearray)):
|
|
415
|
+
return len(value)
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def has_readable_width(spec: ValueTypeSpec) -> bool:
|
|
420
|
+
"""True when the spec can size a read at an address the caller already has.
|
|
421
|
+
|
|
422
|
+
Every spec but the IDA pattern can: the numeric types declare a width, and
|
|
423
|
+
str / bytes / regex take one from the caller. An IDA pattern derives a
|
|
424
|
+
match's width from the pattern itself, so at a bare address it has none —
|
|
425
|
+
it declares a length of 0 *and* refuses a length override, which is exactly
|
|
426
|
+
the combination this tests. Offering it where the address is already known
|
|
427
|
+
would read zero bytes and display nothing forever.
|
|
428
|
+
|
|
429
|
+
Note this says nothing about a *cheat entry*, which carries its own width
|
|
430
|
+
and so can hold an IDA pattern quite happily (see ``parse_value_for_write``).
|
|
431
|
+
"""
|
|
432
|
+
return spec.length > 0 or spec.accepts_length_override
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def parse_value_for_write(
|
|
436
|
+
spec: ValueTypeSpec,
|
|
437
|
+
text: str,
|
|
438
|
+
length_override: Optional[int] = None,
|
|
439
|
+
current: Optional[bytes] = None,
|
|
440
|
+
) -> Tuple[Any, int]:
|
|
441
|
+
"""Parse ``text`` as the value to **write** at an address.
|
|
442
|
+
|
|
443
|
+
:func:`parse_value` answers the scanner's question ("what am I looking
|
|
444
|
+
for?"). For every type but the two pattern ones that is the same answer,
|
|
445
|
+
but a pattern's ``parse`` yields search syntax — an IDA pattern's text, or
|
|
446
|
+
a regex — which is not a value any address can hold. Those specs supply a
|
|
447
|
+
``parse_write`` that resolves the cell text to concrete bytes instead,
|
|
448
|
+
given ``current``: the bytes last read there, which a ``?`` wildcard keeps.
|
|
449
|
+
|
|
450
|
+
Used by the cheat table, whose cells are read *and* written; the scanner
|
|
451
|
+
only ever searches, so it stays on :func:`parse_value`.
|
|
452
|
+
|
|
453
|
+
The returned width follows :func:`parse_value`'s rule — an explicit
|
|
454
|
+
``length_override`` wins for the types that accept one. Writing a value to
|
|
455
|
+
an entry must not resize it: the cheat table stores this width back onto the
|
|
456
|
+
entry, and the entry's width is how many bytes it *reads* on every poll
|
|
457
|
+
tick, which the user set and the write has no business shrinking.
|
|
458
|
+
"""
|
|
459
|
+
if spec.parse_write is None:
|
|
460
|
+
value, length = parse_value(spec, text, length_override)
|
|
461
|
+
else:
|
|
462
|
+
value = spec.parse_write(text, current, length_override)
|
|
463
|
+
length = (
|
|
464
|
+
max(1, int(length_override))
|
|
465
|
+
if spec.accepts_length_override and length_override is not None
|
|
466
|
+
else max(1, _written_width(value))
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# One guard for every path that reaches an address. An entry's length is a
|
|
470
|
+
# *byte* width — it is the bufflength each poll tick reads — while
|
|
471
|
+
# prepare_write treats it as a hard cap and truncates past it, counting
|
|
472
|
+
# characters for str. So a wider value was written short with no word said
|
|
473
|
+
# while the cell kept showing all of it, and a multibyte str could slip the
|
|
474
|
+
# other way: "ábc" is 3 characters but 4 bytes, one past a 3-byte entry.
|
|
475
|
+
# Measuring what actually goes on the wire covers both.
|
|
476
|
+
written = _written_width(value)
|
|
477
|
+
if length_override is not None and written > length_override:
|
|
478
|
+
raise ValueError(
|
|
479
|
+
"Value is %d bytes but the entry holds %d. Widen the entry first, "
|
|
480
|
+
"or the extra would be dropped without warning."
|
|
481
|
+
% (written, length_override)
|
|
482
|
+
)
|
|
483
|
+
return value, length
|
|
@@ -38,6 +38,7 @@ from .libsystem import PROC_ALL_PIDS, libsystem, mach_error_message, mach_task_s
|
|
|
38
38
|
from .types import (
|
|
39
39
|
KERN_INVALID_ADDRESS,
|
|
40
40
|
KERN_INVALID_ARGUMENT,
|
|
41
|
+
KERN_MEMORY_ERROR,
|
|
41
42
|
KERN_NO_ACCESS,
|
|
42
43
|
KERN_PROTECTION_FAILURE,
|
|
43
44
|
KERN_SUCCESS,
|
|
@@ -294,10 +295,20 @@ def get_memory_regions(task: int, pid: int = 0) -> Generator[MemoryRegion, None,
|
|
|
294
295
|
# KERN_NO_ACCESS / KERN_INVALID_ARGUMENT can also surface for guard pages and
|
|
295
296
|
# freshly-unmapped pages on modern macOS; treating them as fatal aborts a scan
|
|
296
297
|
# that should just skip the page.
|
|
298
|
+
#
|
|
299
|
+
# KERN_MEMORY_ERROR is the same story one level down: the pager backing a page
|
|
300
|
+
# declined to produce its data. The kernel header calls that failure
|
|
301
|
+
# "temporary" in as many words, in explicit contrast with the permanent
|
|
302
|
+
# KERN_MEMORY_FAILURE beside it (the pair is spelled out in types.py). It is
|
|
303
|
+
# what file-backed, read-only mappings (code segments, dylibs, the dyld shared
|
|
304
|
+
# cache) return, so any scan that walks them — every pattern scan, and any value
|
|
305
|
+
# scan with "writable regions only" turned off — hits it routinely, and used to
|
|
306
|
+
# abort on the first one.
|
|
297
307
|
_PAGE_GONE_KRS = (
|
|
298
308
|
KERN_INVALID_ADDRESS,
|
|
299
309
|
KERN_NO_ACCESS,
|
|
300
310
|
KERN_INVALID_ARGUMENT,
|
|
311
|
+
KERN_MEMORY_ERROR,
|
|
301
312
|
)
|
|
302
313
|
|
|
303
314
|
|
PyMemoryEditor/macos/types.py
CHANGED
|
@@ -65,6 +65,18 @@ KERN_PROTECTION_FAILURE = 2
|
|
|
65
65
|
KERN_INVALID_ARGUMENT = 4
|
|
66
66
|
KERN_FAILURE = 5
|
|
67
67
|
KERN_NO_ACCESS = 8
|
|
68
|
+
# These two are neighbours in mach/kern_return.h and are the line between a page
|
|
69
|
+
# a scan may walk past and one it may not, so they are defined together:
|
|
70
|
+
#
|
|
71
|
+
# 9 "the target address refers to a memory object that has been destroyed.
|
|
72
|
+
# This failure is permanent."
|
|
73
|
+
# 10 "the memory object indicated that the data could not be returned. This
|
|
74
|
+
# failure may be temporary; future attempts to access this same data may
|
|
75
|
+
# succeed, as defined by the memory object."
|
|
76
|
+
#
|
|
77
|
+
# Only the second belongs in functions._PAGE_GONE_KRS.
|
|
78
|
+
KERN_MEMORY_FAILURE = 9
|
|
79
|
+
KERN_MEMORY_ERROR = 10
|
|
68
80
|
|
|
69
81
|
|
|
70
82
|
class vm_region_basic_info_64(Structure):
|
PyMemoryEditor/util/pattern.py
CHANGED
|
@@ -26,7 +26,7 @@ The function returns a compiled ``re.Pattern[bytes]`` ready to be used with
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import re
|
|
29
|
-
from typing import Pattern, Tuple, Union
|
|
29
|
+
from typing import List, Optional, Pattern, Tuple, Union
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
PatternLike = Union[str, bytes, "re.Pattern[bytes]"]
|
|
@@ -84,33 +84,58 @@ def compile_pattern(
|
|
|
84
84
|
"compiled re.Pattern, not %r" % type(pattern).__name__
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
tokens = pattern
|
|
88
|
-
if not tokens:
|
|
89
|
-
raise ValueError("Empty pattern.")
|
|
87
|
+
tokens = tokenize_pattern(pattern)
|
|
90
88
|
|
|
91
89
|
parts = []
|
|
92
90
|
for token in tokens:
|
|
93
|
-
if token
|
|
91
|
+
if token is None:
|
|
94
92
|
# Single-byte wildcard. ``.`` together with re.DOTALL matches any
|
|
95
93
|
# byte 0x00-0xFF without special-casing 0x0A.
|
|
96
94
|
parts.append(b".")
|
|
97
95
|
continue
|
|
96
|
+
# Escape the byte so e.g. 0x5C (backslash) or 0x28 ('(') don't get
|
|
97
|
+
# interpreted as regex meta chars.
|
|
98
|
+
parts.append(re.escape(bytes((token,))))
|
|
99
|
+
|
|
100
|
+
return re.compile(b"".join(parts), re.DOTALL), len(tokens)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tokenize_pattern(pattern: str) -> List[Optional[int]]:
|
|
104
|
+
"""Split an IDA-style hex pattern into one entry per byte.
|
|
105
|
+
|
|
106
|
+
Each entry is the byte's value, or ``None`` for a ``?`` / ``??`` wildcard.
|
|
107
|
+
``compile_pattern`` turns these into a regex; the app's cheat table uses
|
|
108
|
+
them to write a signature back, where a wildcard means "leave the byte
|
|
109
|
+
that is already there alone". Both need the same token rules and the same
|
|
110
|
+
error messages, so they share this.
|
|
111
|
+
|
|
112
|
+
:raises ValueError: on an empty pattern or a malformed token.
|
|
113
|
+
"""
|
|
114
|
+
tokens = pattern.split()
|
|
115
|
+
if not tokens:
|
|
116
|
+
raise ValueError("Empty pattern.")
|
|
117
|
+
|
|
118
|
+
parsed: List[Optional[int]] = []
|
|
119
|
+
for token in tokens:
|
|
120
|
+
if token in ("?", "??"):
|
|
121
|
+
parsed.append(None)
|
|
122
|
+
continue
|
|
98
123
|
if len(token) != 2:
|
|
99
124
|
raise ValueError(
|
|
100
125
|
"Pattern token %r is not two hex digits or a '?' wildcard. "
|
|
101
126
|
"Example of a valid pattern: '48 8B ? ? 00'." % token
|
|
102
127
|
)
|
|
103
128
|
try:
|
|
104
|
-
|
|
129
|
+
parsed.append(bytes.fromhex(token)[0])
|
|
105
130
|
except ValueError as exc:
|
|
106
131
|
raise ValueError(
|
|
107
132
|
"Pattern token %r is not valid hex: %s" % (token, exc)
|
|
108
133
|
)
|
|
109
|
-
|
|
110
|
-
# interpreted as regex meta chars.
|
|
111
|
-
parts.append(re.escape(byte))
|
|
112
|
-
|
|
113
|
-
return re.compile(b"".join(parts), re.DOTALL), len(tokens)
|
|
134
|
+
return parsed
|
|
114
135
|
|
|
115
136
|
|
|
137
|
+
# tokenize_pattern is deliberately absent: it exists so the scan and write
|
|
138
|
+
# paths share one set of token rules, and nothing outside the package consumes
|
|
139
|
+
# it. Exporting it would promise a documented, supported surface that neither
|
|
140
|
+
# the guide nor the API reference describes.
|
|
116
141
|
__all__ = ("compile_pattern", "PatternLike")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyMemoryEditor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -138,32 +138,6 @@ That's it — read, write or scan another process in three lines, the same way o
|
|
|
138
138
|
|
|
139
139
|
---
|
|
140
140
|
|
|
141
|
-
## What's inside
|
|
142
|
-
|
|
143
|
-
### 🐍 The Python library
|
|
144
|
-
|
|
145
|
-
Full control over another process's memory — in a few lines of Python:
|
|
146
|
-
|
|
147
|
-
- ✅ **Read & write** values (`int`, `float`, `bool`, `str`, `bytes`)
|
|
148
|
-
- 🔍 **Value scan** with eight comparison modes
|
|
149
|
-
- 🎯 **Pattern scan** (IDA-style AOB & regex)
|
|
150
|
-
- 🔗 **Pointer chains** + a live `RemotePointer` handle
|
|
151
|
-
- 🧭 **Pointer scan** — find static pointers that survive ASLR
|
|
152
|
-
- 🗺️ **Memory map**, **modules**, **threads**
|
|
153
|
-
- 🧱 **Allocate & free** remote memory (Windows / macOS)
|
|
154
|
-
|
|
155
|
-
### 🖥️ The bundled GUI app
|
|
156
|
-
|
|
157
|
-
All the library's power — no code required:
|
|
158
|
-
- ⚡ **Zero setup** — attach to a process and start scanning in seconds
|
|
159
|
-
- 🧲 **Refine workflow** — First Scan → Next Scan with live visual feedback
|
|
160
|
-
- 📋 **Cheat table** — freeze / write values on the fly, JSON import/export
|
|
161
|
-
- 🔬 **Hex viewer** — browse raw memory and write back inline
|
|
162
|
-
- 🧩 **Pointer scan UI** — scan, export & rescan across sessions with a few clicks
|
|
163
|
-
- 🎨 **One-click access** — every feature at your fingertips, no code needed
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
141
|
## 📖 Documentation
|
|
168
142
|
|
|
169
143
|
Full documentation lives at **[pymemoryeditor.readthedocs.io](https://pymemoryeditor.readthedocs.io)** — installation, the Cheat Engine workflow, every method and parameter, the GUI app guide, platform notes and troubleshooting.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
PyMemoryEditor/__init__.py,sha256=
|
|
1
|
+
PyMemoryEditor/__init__.py,sha256=7y6oSpnE3E7iBrwEFhKwUq3cE36-hrNyK1gWL8xY9do,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
|
|
@@ -8,30 +8,30 @@ PyMemoryEditor/app/_widgets.py,sha256=Xsu3DXUtlUa_zUR4WyXt7bGDyvKr9uKYY7Ki7lLPXq
|
|
|
8
8
|
PyMemoryEditor/app/application.py,sha256=1S98I-wkccq1-8to6aLLlburgDvvmapbCQplk_ALEe8,16830
|
|
9
9
|
PyMemoryEditor/app/cheat_entry.py,sha256=KOLcams-xkVEkV7rfrzhEI7IrsVvmGwsIr8cGcWdLM0,2841
|
|
10
10
|
PyMemoryEditor/app/cheat_poll_worker.py,sha256=clkUklvuOVxbPfo_EJRjs8dfj_4RQk-3uusYhLcTtYM,12281
|
|
11
|
-
PyMemoryEditor/app/cheat_table.py,sha256=
|
|
11
|
+
PyMemoryEditor/app/cheat_table.py,sha256=TzZ4F2r_k19KQXdi_xOBPud0h-601v9nRaDIFwlTxJY,40843
|
|
12
12
|
PyMemoryEditor/app/log_console_dialog.py,sha256=VpYXxECepcbrtcKNheO0xwSoB63RsP_s7DuQnzyDf_g,7153
|
|
13
13
|
PyMemoryEditor/app/main_window.py,sha256=H-TjgY77PTWYMN-TYOJFAGAq3SnsoI4M1w-B3wpvx-c,41619
|
|
14
14
|
PyMemoryEditor/app/memory_map_dialog.py,sha256=WBhitNAShpektV8o6hUgxroc7T_BnRhQdeX3OhHdy4E,23236
|
|
15
15
|
PyMemoryEditor/app/memory_viewer_dialog.py,sha256=DoR5poOu0xNCI1myOv9K-I-1Hx1PVFZDSUrDFzYZZiQ,12472
|
|
16
16
|
PyMemoryEditor/app/modules_dialog.py,sha256=AaEnTgYC1cP-YJ1XUrDsw1eaF1cE16YRlzTjoTvJMMw,11715
|
|
17
17
|
PyMemoryEditor/app/open_process_dialog.py,sha256=KtqxW16GqAwMVcTKZ6oj3v--eLagEbwWMuXWIarVIKo,17139
|
|
18
|
-
PyMemoryEditor/app/pointer_chain_dialog.py,sha256=
|
|
19
|
-
PyMemoryEditor/app/pointer_scan_dialog.py,sha256=
|
|
18
|
+
PyMemoryEditor/app/pointer_chain_dialog.py,sha256=dUt832Wa3ruzWYpL-WDT_RiAQd1AhsdQmHSC5hHbysI,20461
|
|
19
|
+
PyMemoryEditor/app/pointer_scan_dialog.py,sha256=p-4p919YWoE0qXCMbnHKFlAD7ylPac0SaMWJrqXoyY0,38617
|
|
20
20
|
PyMemoryEditor/app/results_view.py,sha256=mrCJquUL3vJwlka5an8_YUXr8ycWgav4uO4c4pxTktk,11763
|
|
21
21
|
PyMemoryEditor/app/scan_types.py,sha256=QYAERatT169A3QhXYzu1MS5hclHGJ9tbB3BYsP_q_-4,2078
|
|
22
|
-
PyMemoryEditor/app/scan_worker.py,sha256=
|
|
23
|
-
PyMemoryEditor/app/scanner_panel.py,sha256=
|
|
22
|
+
PyMemoryEditor/app/scan_worker.py,sha256=9t4f-ih-G2MhU9vrtiud9_mzWCerv9Ce83PNkq_XDCI,19499
|
|
23
|
+
PyMemoryEditor/app/scanner_panel.py,sha256=J3jBlPEPYp1L6Qd1CkEitHYP5aTlgCsxgtAZizftmJw,29352
|
|
24
24
|
PyMemoryEditor/app/threads_dialog.py,sha256=HnPS7Mte6CboM0DaoqkTqnEg8Ck65L1FKW-eX8m2OEA,7509
|
|
25
|
-
PyMemoryEditor/app/value_types.py,sha256=
|
|
25
|
+
PyMemoryEditor/app/value_types.py,sha256=eCImnr8nJEFcCBLTBD3PzmV1Ms4irQnurZIguVZUkho,18383
|
|
26
26
|
PyMemoryEditor/linux/functions.py,sha256=raBILyavPlliEzM4r3z6qEcOWG_G7GUd_d1aWHF9K5U,21681
|
|
27
27
|
PyMemoryEditor/linux/libc.py,sha256=U4yOpHJ48OAaDxcVk_s57GGXlvmyakInwM2liegf3OA,1431
|
|
28
28
|
PyMemoryEditor/linux/process.py,sha256=_PafbkQHAcwsQlbwcUFd6ERHN_c4wQ3Qcgyt82Gz2Zw,8636
|
|
29
29
|
PyMemoryEditor/linux/types.py,sha256=sOgO-s9oqKrKKCHMG4N-Pe7sNw49V06PeMENSNnNFl8,1924
|
|
30
30
|
PyMemoryEditor/macos/__init__.py,sha256=85mOA4U5i9EW2r1ggKqRJkqRq0e9bV3I2HdfHIvox10,72
|
|
31
|
-
PyMemoryEditor/macos/functions.py,sha256=
|
|
31
|
+
PyMemoryEditor/macos/functions.py,sha256=zL8jylondbDAj7UUuLmU8Fg8wVTCWkmEEpTzAu_384c,42894
|
|
32
32
|
PyMemoryEditor/macos/libsystem.py,sha256=H9p72IqO0C5CrMFCAjnY_iQCzi_AQgw6ZN6CXsopceo,8683
|
|
33
33
|
PyMemoryEditor/macos/process.py,sha256=06zgLaTKIyc1DQZFgS-9lqT2imB_IYFTOs4xEfSgftg,13259
|
|
34
|
-
PyMemoryEditor/macos/types.py,sha256=
|
|
34
|
+
PyMemoryEditor/macos/types.py,sha256=bwPbYRZAOflITeNy0jeEXqFu-M5SVYkaFSsbyxz_K2w,5437
|
|
35
35
|
PyMemoryEditor/process/__init__.py,sha256=Xh5jngBqZyNIA9y3uwE49LxK8uH8YYsu6qotx1mP_WE,213
|
|
36
36
|
PyMemoryEditor/process/abstract.py,sha256=hv7eZyl6izTxz9z48X44QLh3g4fQdIBqVlrp8dJBnV0,53835
|
|
37
37
|
PyMemoryEditor/process/errors.py,sha256=485SXkidg9_E_KjuhuF6AsQNuFZlY-Ag-sG1C6NsGMw,2054
|
|
@@ -45,7 +45,7 @@ PyMemoryEditor/process/thread_info.py,sha256=GeIpfL9TD0YVV75vlBlCUlseCDUNsTDKmxz
|
|
|
45
45
|
PyMemoryEditor/process/util.py,sha256=qwh3agrj8B_EDu5LsfMxKFhFSp9YDafXkoUuOUV7owI,3160
|
|
46
46
|
PyMemoryEditor/util/__init__.py,sha256=Lux1_4twAhkzdWDV4xt4INmshKXZAHje_NU2WuwQhoE,512
|
|
47
47
|
PyMemoryEditor/util/convert.py,sha256=CnnJ0wTc8oN5kliAaH4IZJtxI1J0_hS7YKFyVuNyMWU,14399
|
|
48
|
-
PyMemoryEditor/util/pattern.py,sha256=
|
|
48
|
+
PyMemoryEditor/util/pattern.py,sha256=OKcnTv5v1ny62dutwd850L-yQJMLa8fXybcbwdivcdg,5384
|
|
49
49
|
PyMemoryEditor/util/scan.py,sha256=ZCQP2g_ANFdHNmhXf2CayV8zB44IAqHsG2lA7REMKF0,19811
|
|
50
50
|
PyMemoryEditor/util/scan_numpy.py,sha256=osqZh7hfu9meKGXJPROLqzQ-UdSrCduqkv7EzUfVDp4,5889
|
|
51
51
|
PyMemoryEditor/win32/functions.py,sha256=v-OAHBJZEcmLTCbRuqPpprik_CuvYVEuyVH-POAXlM0,34204
|
|
@@ -59,8 +59,8 @@ PyMemoryEditor/win32/enums/process_operations.py,sha256=wn-aMl01VVg-2rFavR9szBNR
|
|
|
59
59
|
PyMemoryEditor/win32/enums/standard_access_rights.py,sha256=VGWlTagW4hvbm2AjZ4NK56ZrQwRmEl9Kz9GSCG8o-HI,738
|
|
60
60
|
PyMemoryEditor/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
61
|
PyMemoryEditor/app/assets/icon.svg,sha256=T7OqR7ldqC_dK2Ib2ntsH2rWO-Z5RzNK4WdD1wFwgM4,7132
|
|
62
|
-
pymemoryeditor-2.
|
|
63
|
-
pymemoryeditor-2.
|
|
64
|
-
pymemoryeditor-2.
|
|
65
|
-
pymemoryeditor-2.
|
|
66
|
-
pymemoryeditor-2.
|
|
62
|
+
pymemoryeditor-2.2.0.dist-info/METADATA,sha256=hiAocF8rEWox6UUUsnI_PKGgDLD5cbByM03DxF1PofY,8540
|
|
63
|
+
pymemoryeditor-2.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
64
|
+
pymemoryeditor-2.2.0.dist-info/entry_points.txt,sha256=ct14npP5HRKCHdq6-n83KBFi5QwQNSCsWE3MmGaWbzY,75
|
|
65
|
+
pymemoryeditor-2.2.0.dist-info/licenses/LICENSE,sha256=YkOx6Nvkavq36bgiAc3eF85SBG0Qu14KMAvxmv6Ssjc,1089
|
|
66
|
+
pymemoryeditor-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|