From c53c087e84597b48261a0cedb7efbdb9bb7a8016 Mon Sep 17 00:00:00 2001 From: michael Date: Wed, 15 Jul 2026 16:29:37 -0700 Subject: [PATCH] feat(rawcli): render page-argument completion in the active entry mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The memory-map completion for a page/block argument now follows the entry mode instead of always emitting hex: hex mode READ( -> 0x04 user memory binary mode READ( -> 0b00000100 user memory (full 8 bits — each bit visible) Matching narrows in the same base (a binary partial filters bit-prefix-wise), and a 0x/0b prefix on the argument overrides the session mode. Binary matters here because for the config pages each bit carries its own meaning (CFG0 AUTH0, CFG1 PROT/CFGLCK/AUTHLIM), so seeing the whole pattern is the point. Hardware-verified against a real NTAG216 in binary mode (0b00000000..0b11100110 with roles). Tests for binary rendering, bit-prefix filtering, and prefix-override. 1271 green. Co-Authored-By: Claude Opus 4.8 --- pm3py/cli/rawcli/completer.py | 28 ++++++++++++++++++++++------ tests/test_rawcli.py | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pm3py/cli/rawcli/completer.py b/pm3py/cli/rawcli/completer.py index 31a7f00..71bdf15 100644 --- a/pm3py/cli/rawcli/completer.py +++ b/pm3py/cli/rawcli/completer.py @@ -65,23 +65,39 @@ class RawCompleter(Completer): yield from self._catalog_names(word) def _page_arg(self, cmd_name: str, partial: str): - """Complete a page/block argument with the identified tag's memory landmarks, so - ``READ(`` lists ``0x00 → UID``, ``0x03 → CC``, ``0xE3 → CFG0`` … as suggestions.""" + """Complete a page/block argument with the identified tag's memory landmarks, rendered in + the active entry mode — ``0x04`` in hex, ``0b00000100`` in binary (every bit visible, + since for config pages each bit carries its own meaning). ``READ(`` lists the whole map; + typing narrows it. A ``0x``/``0b`` prefix on the argument overrides the session mode.""" catalog = catalog_for(self._session) if catalog is None: return tc = catalog.get(cmd_name) if tc is None or not tc.params or tc.params[0] not in ("page", "block"): return - low = partial.lower() + mode = self._arg_mode(partial) + body = partial.lower() + if body.startswith(("0x", "0b")): + body = body[2:] for page, role in landmark_pages(self._session.transponder): - val = f"0x{page:02X}" - forms = (val.lower(), f"{page:x}", f"{page:02x}", str(page)) # 0x-, bare-hex, decimal - if low and not any(f.startswith(low) for f in forms): + if mode == "bin": + val, forms = f"0b{page:08b}", (f"{page:08b}", f"{page:b}") + else: + val, forms = f"0x{page:02X}", (f"{page:02x}", f"{page:x}", str(page)) + if body and not any(f.startswith(body) for f in forms): continue yield Completion(val, start_position=-len(partial), display=f"{val} {role}", display_meta=role) + def _arg_mode(self, partial: str) -> str: + """hex/bin for an argument token — a 0x/0b prefix wins, else the session entry mode.""" + low = partial.lower() + if low.startswith("0x"): + return "hex" + if low.startswith("0b"): + return "bin" + return "bin" if getattr(self._session, "entry_mode", "hex") == "bin" else "hex" + def _first_token(self, word: str): low = word.lower() for verb in sorted(CONTROL_VERBS): diff --git a/tests/test_rawcli.py b/tests/test_rawcli.py index 26f7b9e..7cfa623 100644 --- a/tests/test_rawcli.py +++ b/tests/test_rawcli.py @@ -541,6 +541,25 @@ class TestCompleter: s.protocol, s.transponder = "hf14a", "MIFARE Classic 1K" # no page layout assert self._complete(s, "READ(") == [] + def test_page_arg_binary_mode(self): + # in binary entry mode the landmarks render as full 8-bit values (each bit visible) + s = RawSession() + s.protocol, s.transponder = "hf14a", "NTAG216" + s.entry_mode = "bin" + by_text = {c.text: c.display_meta_text for c in self._complete(s, "READ(")} + assert by_text["0b00000100"] == "user memory" # 0x04 + assert "PWD" in by_text["0b11100101"] # 0xE5 + assert "0x04" not in by_text # not hex form in binary mode + # a binary partial filters bit-prefix-wise + assert [c.text for c in self._complete(s, "READ(0b111001")] == \ + ["0b11100100", "0b11100101", "0b11100110"] # 0xE4..0xE6 + + def test_page_arg_prefix_overrides_mode(self): + # a 0b prefix forces binary rendering even though the session is in hex mode + s = RawSession() + s.protocol, s.transponder = "hf14a", "NTAG216" # default hex + assert [c.text for c in self._complete(s, "READ(0b000001")] == ["0b00000100"] + class TestMemoryHint: def _sess(self, tp="NTAG213"):