feat(rawcli): MIFARE Classic block-level memory map
MFC gets the same memory-map hinting as NTAG, at the block level: block 0 manufacturer block — UID, BCC, SAK, ATQA, vendor data sector trailer Key A [0-5], access bits [6-8] + GPB [9], Key B [10-15] other blocks data block (sector N) Handles 1K (16 sectors), Mini (5 sectors) and 4K — including the eight 16-block sectors 32-39 (trailer = last block, e.g. 143 = sector 32, 255 = sector 39). landmark_pages lists block 0 + every sector trailer (the map skeleton; data blocks are uniform), so READ( / raw 30 surface it in the completion dropdown. Also routes "MIFARE Mini" to the MFC catalog (was falling through to Type 2). Tests cover the block roles, large-sector trailers, bounds, Mini routing, and the dropdown map. Path verified end-to-end with a mocked SAK-0x08 scan (identify -> catalog -> completer). Live hardware verify still needs an actual Classic card on the antenna. 1339 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -370,6 +370,8 @@ class TestCatalog:
|
||||
assert catalog_for(s) is TYPE2
|
||||
s.transponder = "MIFARE Classic 1K"
|
||||
assert catalog_for(s).name == "MIFARE Classic"
|
||||
s.transponder = "MIFARE Mini" # Mini is a Classic -> MFC, not Type 2
|
||||
assert catalog_for(s).name == "MIFARE Classic"
|
||||
s.protocol, s.transponder = "hf15", "ISO15693"
|
||||
assert catalog_for(s) is ISO15
|
||||
|
||||
@@ -600,10 +602,16 @@ class TestCompleter:
|
||||
assert "config" in by_text["0x00"]
|
||||
assert "password" in by_text["0x07"]
|
||||
|
||||
def test_page_arg_none_without_layout(self):
|
||||
def test_page_arg_mfc_block_map(self):
|
||||
# MIFARE Classic gets its block map in the dropdown, same as NTAG pages
|
||||
s = RawSession()
|
||||
s.protocol, s.transponder = "hf14a", "MIFARE Classic 1K" # no page layout
|
||||
assert self._complete(s, "READ(") == []
|
||||
s.protocol, s.transponder = "hf14a", "MIFARE Classic 1K"
|
||||
by_text = {c.text: c.display_meta_text for c in self._complete(s, "READ(")}
|
||||
assert "manufacturer" in by_text["0x00"]
|
||||
assert by_text["0x03"].startswith("sector 0 trailer")
|
||||
assert "0x3F" in by_text # block 63, sector 15 trailer
|
||||
# raw byte form works too
|
||||
assert "manufacturer" in {c.text: c.display_meta_text for c in self._complete(s, "30 ")}["00"]
|
||||
|
||||
def test_page_arg_binary_mode(self):
|
||||
# in binary entry mode the landmarks render as full 8-bit values (each bit visible)
|
||||
@@ -671,7 +679,7 @@ class TestMemoryHint:
|
||||
assert page_role(name, 3) == "Capability Container (CC) / OTP" # family unknown
|
||||
assert page_role(name, 0x20) is None # model-specific -> not guessed
|
||||
assert [p for p, _ in landmark_pages(name)] == [0, 2, 3, 4] # header, no config
|
||||
assert page_role("MIFARE Classic 1K", 4) is None # not Type 2 -> nothing
|
||||
assert page_role("MIFARE DESFire", 4) is None # no map for this tag -> nothing
|
||||
|
||||
def test_t5577_block_roles(self):
|
||||
from pm3py.cli.rawcli.memory import page_role
|
||||
@@ -680,6 +688,27 @@ class TestMemoryHint:
|
||||
assert "password" in page_role(tp, 7)
|
||||
assert "data" in page_role(tp, 3)
|
||||
|
||||
def test_mfc_block_roles(self):
|
||||
from pm3py.cli.rawcli.memory import page_role, landmark_pages
|
||||
# 1K: block 0 manufacturer, every 4th block a sector trailer, rest data
|
||||
assert "manufacturer" in page_role("MIFARE Classic 1K", 0)
|
||||
assert page_role("MIFARE Classic 1K", 1) == "data block (sector 0)"
|
||||
assert page_role("MIFARE Classic 1K", 3).startswith("sector 0 trailer")
|
||||
assert "Key A" in page_role("MIFARE Classic 1K", 3) and "Key B" in page_role("MIFARE Classic 1K", 3)
|
||||
assert page_role("MIFARE Classic 1K", 63).startswith("sector 15 trailer")
|
||||
assert page_role("MIFARE Classic 1K", 64) is None # out of range
|
||||
# 4K large sectors 32-39 are 16 blocks each (trailer = last block)
|
||||
assert page_role("MIFARE Classic 4K", 143).startswith("sector 32 trailer")
|
||||
assert page_role("MIFARE Classic 4K", 255).startswith("sector 39 trailer")
|
||||
assert page_role("MIFARE Classic 4K", 254) == "data block (sector 39)"
|
||||
# Mini is a 5-sector Classic
|
||||
assert page_role("MIFARE Mini", 19).startswith("sector 4 trailer")
|
||||
assert page_role("MIFARE Mini", 20) is None
|
||||
# landmarks = block 0 + every trailer
|
||||
assert [b for b, _ in landmark_pages("MIFARE Classic 1K")] == [0, 3, 7, 11, 15, 19, 23,
|
||||
27, 31, 35, 39, 43, 47, 51, 55, 59, 63]
|
||||
assert len(landmark_pages("MIFARE Classic 4K")) == 41 # block 0 + 40 sectors
|
||||
|
||||
def test_layouts_match_models(self):
|
||||
from pm3py.cli.rawcli.memory import _LAYOUTS
|
||||
from pm3py.sim import NTAG210, NTAG212, NTAG213, NTAG215, NTAG216
|
||||
|
||||
Reference in New Issue
Block a user