Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/screenshots/terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
try:
from picklock import __version__ as release
except Exception: # pragma: no cover - docs can build without the package installed
release = "0.1.1"
release = "0.2.0"

version = ".".join(release.split(".")[:2])

Expand Down
17 changes: 9 additions & 8 deletions docs/guide/scanning.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ the thing on your screen changed.

```
picklock [game.exe:41902]> scan:value int32 100 --writable
Showing 20 of 3184 rows — page 1 of 160 (1.42 sec)
Showing 20 of 3184 rows — page 1 of 160 — writable regions only (1.42 sec)

picklock [game.exe:41902]> scan:next 95
2 rows in set (0.02 sec)
Expand All @@ -30,17 +30,18 @@ faster. `--all-regions` overrides it when you are looking for something in
read-only data.

Because that restriction is easy to forget and expensive to forget, a
result set that skipped read-only memory says so — on the scan, on every
refine, and again on `scan:results`:
result set that skipped read-only memory says so in its footer — on the scan,
on every refine, and again on `scan:results`:

```
Note: Writable regions only — nothing in read-only memory was searched.
Use '--all-regions' on the first scan to include it.
20 rows in set — writable regions only (0.02 sec)
```

You will also see it without having typed `--writable`, if the
`writable_only` setting is on. That is the point of the line: the reason an
address is missing should not be a setting you turned on last week.
It sits under the rows because that is where the question gets asked: you are
looking at what came back and wondering whether it is everything. You will also
see it without having typed `--writable`, if the `writable_only` setting is on
— the reason an address is missing should not be a setting you turned on last
week.

## When you cannot see the number

Expand Down
4 changes: 2 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ picklock
```

```
Welcome to Picklock 0.1.1, a terminal client for PyMemoryEditor.
Welcome to Picklock 0.2.0, a terminal client for PyMemoryEditor.
Type 'help' for the command list, or 'help scanning' for a walkthrough.

picklock>
Expand Down Expand Up @@ -67,7 +67,7 @@ for it:

```
picklock [game.exe:41902]> scan:value int32 100 --writable
Showing 20 of 3184 rows — page 1 of 160 (1.42 sec)
Showing 20 of 3184 rows — page 1 of 160 — writable regions only (1.42 sec)
Next page: scan:results --page 2
```

Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Work through these in order:
instead: `scan:value float --between 0.7 0.8`.
3. **`--writable` excluded it.** A value that never changes may live in
read-only data. `--all-regions` searches everything. Picklock says so when
a result set skipped read-only memory, so check for the
"Writable regions only" note — and remember the `writable_only` setting
a result set skipped read-only memory, so check the footer under the table
for "writable regions only" — and remember the `writable_only` setting
produces it too.
4. **It moved.** Between your first scan and your refine, the target may have
reallocated. Start over with `scan:reset`.
Expand Down
2 changes: 1 addition & 1 deletion picklock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"""

__author__ = "Jean Loui Bernard Silva de Jesus"
__version__ = "0.1.1"
__version__ = "0.2.0"

from .errors import CommandError, NoProcessError, PicklockError
from .session import Session
Expand Down
2 changes: 1 addition & 1 deletion picklock/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class Namespace:
"Scanning",
"Search memory for a value, then narrow what you found.",
"picklock> scan:value int32 100 --writable\n"
"Showing 20 of 3184 rows (1.42 sec)\n"
"Showing 20 of 3184 rows — writable regions only (1.42 sec)\n"
"\n"
"picklock> scan:next 95\n"
"+-----+--------------------+-------+\n"
Expand Down
22 changes: 8 additions & 14 deletions picklock/commands/scan_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,12 @@ def _read_values(
return [value_type.decode(found.get(address)) for address in addresses]


#: Said whenever a result set that skipped read-only memory is shown. A scan
#: that quietly searched a tenth of the address space is the kind of thing you
#: work out an hour later, from an address that should have been found and was
#: not.
_WRITABLE_ONLY = (
"Writable regions only — nothing in read-only memory was searched. "
"Use '--all-regions' on the first scan to include it."
)
#: Marks every result set that skipped read-only memory. A scan that quietly
#: searched a tenth of the address space is the kind of thing you work out an
#: hour later, from an address that should have been found and was not — so it
#: rides in the footer, under the rows it applies to, on the scan and on every
#: later look at the results.
_WRITABLE_ONLY = "writable regions only"


def _store(
Expand Down Expand Up @@ -362,9 +360,6 @@ def _print_results(
The next page is fetched with ``scan:results``, not by re-running the scan
— which is why the hint names that command whatever produced the rows.
"""
if state.writable_only:
session.printer.note(_WRITABLE_ONLY)

process = session.require_process()
hex_output = bool(session.option("hex"))
indexes = range(len(state.addresses))
Expand All @@ -390,6 +385,7 @@ def _print_results(
page=page.number,
pages=page.count,
next_page=page.next_page,
marker=_WRITABLE_ONLY if state.writable_only else None,
)


Expand Down Expand Up @@ -874,9 +870,6 @@ def cmd_results(session: Session, args: List[str]) -> None:
session.printer.write()
return

if state.writable_only:
session.printer.note(_WRITABLE_ONLY)

page = paginate(
session,
range(len(state.addresses)),
Expand Down Expand Up @@ -919,6 +912,7 @@ def cmd_results(session: Session, args: List[str]) -> None:
page=page.number,
pages=page.count,
next_page=page.next_page,
marker=_WRITABLE_ONLY if state.writable_only else None,
)


Expand Down
3 changes: 2 additions & 1 deletion picklock/commands/session_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ def _print_overview(session: Session) -> None:
"Attached to game.exe (PID 4242, 64-bit). (0.00 sec)\n"
"\n"
"picklock> scan:value int32 100 --writable\n"
"Showing 20 of 3184 rows — page 1 of 160 (1.42 sec)\n"
"Showing 20 of 3184 rows — page 1 of 160 — writable regions only "
"(1.42 sec)\n"
"Next page: scan:results --page 2",
indent=4,
)
Expand Down
14 changes: 12 additions & 2 deletions picklock/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,13 +353,15 @@ def table(
page: Optional[int] = None,
pages: Optional[int] = None,
next_page: Optional[str] = None,
marker: Optional[str] = None,
) -> None:
"""Print a result table plus its footer.

``total`` names the number of rows that *matched* when ``rows`` only
carries the ones that fit the display limit, so the footer can say how
many were left out instead of pretending they do not exist, and
``next_page`` is the command that shows them.
``next_page`` is the command that shows them. ``marker`` is a short
caveat about the result set itself, carried in the same footer.
"""
self.clear_progress()
if rows:
Expand All @@ -371,6 +373,7 @@ def table(
page=page,
pages=pages,
next_page=next_page,
marker=marker,
)

def footer(
Expand All @@ -382,8 +385,13 @@ def footer(
page: Optional[int] = None,
pages: Optional[int] = None,
next_page: Optional[str] = None,
marker: Optional[str] = None,
) -> None:
"""Print the ``N rows in set (0.01 sec)`` line, and how to see more."""
"""Print the ``N rows in set (0.01 sec)`` line, and how to see more.

``marker`` rides in that line rather than above the table, where a
caveat about the rows sits next to the rows it is about.
"""
if count == 0 and not total:
text = "Empty set"
elif total is not None and total != count:
Expand All @@ -396,6 +404,8 @@ def footer(
text += f" — page {page} of {pages}"
else:
text = f"{count} row{'' if count == 1 else 's'} in set"
if marker:
text += f" — {marker}"
if self.timing and elapsed is not None:
text += f" ({format_duration(elapsed)})"
self.write(text)
Expand Down
11 changes: 8 additions & 3 deletions tests/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,19 @@ def test_a_range_written_backwards_is_refused(target, capture):
def test_a_writable_only_scan_says_so_and_keeps_saying_so(target, capture, block):
"""The restriction is easy to forget and expensive to forget."""
out = run(target, capture, f"scan:value int32 {MARKER} --writable")
assert "Writable regions only" in out
assert "writable regions only" in out

# under the rows rather than above them: a caveat about the results
# belongs next to the results, where the eye already is when it asks
# whether they are all of them.
assert out.index("writable regions only") > out.rindex("+--")

# and again when the results are looked at later, which is when the
# question "why is my address not here?" actually gets asked
assert "Writable regions only" in run(target, capture, "scan:results")
assert "writable regions only" in run(target, capture, "scan:results")

# a refine narrows what that scan found, so it inherits the caveat
assert "Writable regions only" in run(target, capture, "scan:next --unchanged")
assert "writable regions only" in run(target, capture, "scan:next --unchanged")


@slow
Expand Down
13 changes: 13 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ def test_footer_counts_rows_and_says_when_it_is_showing_fewer(capture):
assert lines == ["3 rows in set", "Empty set", "Showing 3 of 90 rows"]


def test_footer_carries_a_marker_before_the_timing(capture):
"""A caveat about the result set travels with the count line."""
capture.printer.timing = True
capture.printer.footer(3, total=90, marker="writable regions only")
assert "Showing 3 of 90 rows — writable regions only" in capture.out


def test_an_empty_result_set_still_carries_its_marker(capture):
""""Nothing found" and "nothing found *there*" are different answers."""
capture.printer.footer(0, marker="writable regions only")
assert "Empty set — writable regions only" in capture.out


def test_footer_reports_one_row_in_the_singular(capture):
capture.printer.footer(1)
assert "1 row in set" in capture.out
Expand Down
Loading