fix(rawcli): memory locations live only in suggestions; base-10 call args
Two corrections:
1. Memory-location names are a SUGGESTION, never command output. Removed the region annotation
printed after each exchange (both the function-call and raw-hex paths) along with its dead
machinery (region_annotation, pages_for_payload, TagCommand.pages). The location now shows in
exactly two places: the completion menu (page landmarks) and the live bottom-bar hint —
which now also covers raw byte entry (30 04 -> "READ(page) … · page 0x04 → user memory")
via Catalog.by_opcode.
2. Function-call arguments are base 10 by default. READ(04) crashed on int("04", 0) (Python
rejects leading-zero decimals in base 0). parser.parse_arg_int parses a call argument as
decimal unless it carries a 0x/0b/0o prefix, so READ(04)/READ(40) work and READ(0x28) still
overrides to hex. Raw byte entry keeps its opposite default (hex, 0b to intermix) — unchanged.
Hardware-verified on NTAG213: READ(04) sends 30 04 with no region line in the output; the hint
shows the page role for READ(4), READ(04), READ(0x28), raw 30 04 and 30 28; completion still
lists the memory map. 1331 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -225,16 +225,16 @@ class TestIdentify:
|
||||
assert s.transponder == "NTAG216" # recovered, not the generic name
|
||||
assert s.device.hf.iso14a.raw.call_count == 3
|
||||
|
||||
def test_14a_getversion_fails_generic_still_annotates(self):
|
||||
def test_14a_getversion_fails_generic_still_resolves(self):
|
||||
# GET_VERSION never answers -> generic family name, but the universal Type 2 pages still
|
||||
# annotate (this is the path that silently broke before)
|
||||
from pm3py.cli.rawcli.memory import region_annotation
|
||||
# resolve a role (this is the path that silently broke before)
|
||||
from pm3py.cli.rawcli.memory import page_role
|
||||
s = RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": 0x00, "atqa": "4400", "uid": "04a1b2c3d4e5f6"}))
|
||||
s.device.hf.iso14a.raw.return_value = {"raw": None}
|
||||
identify(s)
|
||||
assert s.transponder == "MIFARE Ultralight / NTAG"
|
||||
assert region_annotation(s.transponder, [4, 5, 6, 7]) == "04-07 → user memory"
|
||||
assert page_role(s.transponder, 4) == "user memory"
|
||||
|
||||
def test_15693_when_e0_uid(self):
|
||||
s = RawSession(device=_mock_device(scan15={"found": True, "uid": "e004010203040506"}))
|
||||
@@ -396,15 +396,18 @@ class TestCatalog:
|
||||
assert TYPE2.get("GET_VERSION").opcode() == 0x60
|
||||
assert LF_READONLY.get("INFO").opcode() is None # run-only, no build
|
||||
|
||||
def test_pages_for_payload(self):
|
||||
# raw bytes reverse-map to the pages a page/block command touches
|
||||
from pm3py.cli.rawcli.catalog import pages_for_payload
|
||||
assert pages_for_payload(TYPE2, b"\x30\x04") == [4, 5, 6, 7] # READ(4)
|
||||
assert pages_for_payload(TYPE2, b"\x3A\x00\x06") == [0, 1, 2, 3, 4, 5, 6] # FAST_READ(0,6)
|
||||
assert pages_for_payload(TYPE2, b"\xA2\x04\x01\x02\x03\x04") == [4] # WRITE(4, ..)
|
||||
assert pages_for_payload(TYPE2, b"\x60") is None # GET_VERSION — not a page command
|
||||
assert pages_for_payload(TYPE2, b"\x30") is None # opcode only, no page byte
|
||||
assert pages_for_payload(None, b"\x30\x04") is None # no catalog
|
||||
def test_call_arg_is_base_ten(self):
|
||||
# a function-call page argument is base 10 unless prefixed — READ(04)/READ(40) must not crash
|
||||
assert TYPE2.get("READ").build("04") == b"\x30\x04" # leading-zero decimal
|
||||
assert TYPE2.get("READ").build("40") == b"\x30\x28" # 40 decimal = page 0x28
|
||||
assert TYPE2.get("READ").build("0x28") == b"\x30\x28" # hex override
|
||||
assert TYPE2.get("READ").build("0b101000") == b"\x30\x28" # binary override
|
||||
assert TYPE2.get("READ").build("4") == b"\x30\x04"
|
||||
|
||||
def test_by_opcode(self):
|
||||
assert TYPE2.by_opcode(0x30).name == "READ"
|
||||
assert TYPE2.by_opcode(0xA2).name == "WRITE"
|
||||
assert TYPE2.by_opcode(0x99) is None
|
||||
|
||||
|
||||
class TestFunctionCalls:
|
||||
@@ -433,8 +436,8 @@ class TestFunctionCalls:
|
||||
assert calls[0].args[0] == b"\xA0\x04"
|
||||
assert calls[1].args[0] == b"\x01\x02\x03\x04" + b"\x00" * 12
|
||||
|
||||
def test_read_annotates_memory_region(self, capsys):
|
||||
# READ output names the datasheet regions the pages cover (the user's ask)
|
||||
def test_memory_region_stays_out_of_output(self, capsys):
|
||||
# the memory-location info belongs ONLY in the hint/completion — never printed as output
|
||||
s = RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": 0x00, "atqa": "4400", "uid": "04a1b2c3d4e5f6"},
|
||||
version=NTAG216_VERSION))
|
||||
@@ -442,27 +445,9 @@ class TestFunctionCalls:
|
||||
assert s.transponder == "NTAG216"
|
||||
s.device.hf.iso14a.raw.return_value = {"raw": b"\x03\x00\xFE\x00" * 4}
|
||||
capsys.readouterr()
|
||||
dispatch(s, "READ(4)")
|
||||
assert "04-07 → user memory" in _plain(capsys.readouterr().out)
|
||||
dispatch(s, "READ(0)")
|
||||
o = _plain(capsys.readouterr().out)
|
||||
assert "UID / serial number" in o and "Capability Container" in o
|
||||
|
||||
def test_raw_hex_annotates_memory_region(self, capsys):
|
||||
# raw '30 04' is a READ(4) — it earns the same region annotation as the function form
|
||||
s = RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": 0x00, "atqa": "4400", "uid": "04a1b2c3d4e5f6"},
|
||||
version=NTAG216_VERSION))
|
||||
dispatch(s, "identify")
|
||||
assert s.transponder == "NTAG216"
|
||||
s.device.hf.iso14a.raw.return_value = {"raw": b"\x03\x00\xFE\x00" * 4}
|
||||
capsys.readouterr()
|
||||
dispatch(s, "30 04") # raw READ(4)
|
||||
assert "04-07 → user memory" in _plain(capsys.readouterr().out)
|
||||
dispatch(s, "3A 00 06") # raw FAST_READ(0,6)
|
||||
o = _plain(capsys.readouterr().out)
|
||||
assert "UID / serial number" in o and "04-06 → user memory" in o
|
||||
dispatch(s, "60") # GET_VERSION — no page op, no region
|
||||
dispatch(s, "READ(4)") # function call
|
||||
assert "user memory" not in _plain(capsys.readouterr().out)
|
||||
dispatch(s, "30 04") # raw hex
|
||||
assert "user memory" not in _plain(capsys.readouterr().out)
|
||||
|
||||
def test_call_without_identify(self, capsys):
|
||||
@@ -653,25 +638,25 @@ class TestMemoryHint:
|
||||
from pm3py.cli.rawcli.memory import input_hint
|
||||
assert input_hint(RawSession(), "READ(4)") is None
|
||||
|
||||
def test_region_annotation(self):
|
||||
from pm3py.cli.rawcli.memory import region_annotation
|
||||
assert region_annotation("NTAG216", [0, 1, 2, 3]) == \
|
||||
"00-02 → UID / serial number · 03 → Capability Container (CC)"
|
||||
assert region_annotation("NTAG216", [4, 5, 6, 7]) == "04-07 → user memory"
|
||||
assert "PWD" in region_annotation("NTAG213", [0x2B])
|
||||
assert region_annotation("MIFARE Classic 1K", [4]) is None # no layout -> no annotation
|
||||
assert region_annotation("NTAG216", []) is None
|
||||
def test_hint_raw_bytes(self):
|
||||
# raw hex (30 04) gets the location hint too — the opcode maps to READ, byte 1 is the page
|
||||
from pm3py.cli.rawcli.memory import input_hint
|
||||
s = self._sess() # NTAG213
|
||||
h = input_hint(s, "30 04")
|
||||
assert h.startswith("READ(page)") and "page 0x04 → user memory" in h
|
||||
assert "PWD" in input_hint(s, "30 2B") # page 0x2B = PWD on a 213
|
||||
assert input_hint(s, "60").startswith("GET_VERSION") # opcode-only command, no page part
|
||||
|
||||
def test_generic_type2_fallback(self):
|
||||
from pm3py.cli.rawcli.memory import region_annotation, landmark_pages, page_role
|
||||
from pm3py.cli.rawcli.memory import landmark_pages, page_role
|
||||
# only the family is known (flaky/absent GET_VERSION) -> universal pages still resolve
|
||||
for name in ("MIFARE Ultralight / NTAG", "NTAG (144 B)"):
|
||||
assert region_annotation(name, [4, 5, 6, 7]) == "04-07 → user memory"
|
||||
assert page_role(name, 4) == "user memory"
|
||||
assert page_role(name, 0) == "UID / serial number"
|
||||
assert page_role(name, 3) == "Capability Container (CC)"
|
||||
assert page_role(name, 0x20) is None # model-specific -> not guessed
|
||||
assert [p for p, _ in landmark_pages(name)] == [0, 3, 4] # no config landmarks
|
||||
assert region_annotation("MIFARE Classic 1K", [4]) is None # not Type 2 -> nothing
|
||||
assert page_role("MIFARE Classic 1K", 4) is None # not Type 2 -> nothing
|
||||
|
||||
def test_t5577_block_roles(self):
|
||||
from pm3py.cli.rawcli.memory import page_role, input_hint
|
||||
|
||||
Reference in New Issue
Block a user