Files
pm3py/tests/test_sim_trace_14a.py
michael a24f0f803f feat(sim): reject wrong-protocol tags in start_14a/start_15693; export tags; uid getter
- start_14a now raises TypeError for a non-Tag14443A transponder, start_15693
  likewise for a non-Tag15693 (local imports to avoid the base<->sim circular
  import). Prevents silently starting a sim with an incompatible tag model.
- Re-export the full 14a/14b/15693 tag lineup (NTAG21x, Ultralight family, NTAG
  I2C, ST25*, OPTIGA, ISO14443B, TI parts) from pm3py.sim for `from pm3py.sim
  import NTAG213`-style use.
- Add a read-only `uid` property on the Transponder base (mirrors set_uid()).
- Tests for both guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:37 -07:00

140 lines
3.9 KiB
Python

"""ISO 14443-A sim live trace: firmware CMD_HF_ISO14443A_SIM_TRACE -> entries.
Mirrors the SimSession portion of test_trace_scripting.py but for the 14a sim
trace path (protocol tag 1, decode_14443a). Uses synthetic response frames fed
through SimSession._trace_reader with a mocked serial port — no hardware.
"""
import struct
from unittest.mock import MagicMock
from pm3py.core.protocol import RESP_PREAMBLE_MAGIC
from pm3py.core.transport import RESP_POSTAMBLE_NOCRC
from pm3py.sim.sim_session import SimSession, CMD_HF_ISO14443A_SIM_TRACE
from pm3py.trace.decode_iso14a import decode_14443a
from pm3py.trace.format import TraceFormatter
REQA = bytes.fromhex("26") # reader -> tag, short frame
ATQA = bytes.fromhex("0400") # tag -> reader, no CRC
def _fmt():
return TraceFormatter(mode="sim", decoder=decode_14443a, crc_len=2, is_tty=False)
def _trace_frame(direction, payload):
"""Build one CMD_HF_ISO14443A_SIM_TRACE response frame (USB, no CRC)."""
data = bytes([direction]) + payload
header = struct.pack("<IHbbH", RESP_PREAMBLE_MAGIC, len(data) | 0x8000,
0, 0, CMD_HF_ISO14443A_SIM_TRACE)
return header + data + struct.pack("<H", RESP_POSTAMBLE_NOCRC)
def _run_sim_reader(session, buf):
"""Drive _trace_reader once over `buf`, then stop it."""
ser = MagicMock()
ser.in_waiting = len(buf)
n = 0
def side(_):
nonlocal n
n += 1
if n == 1:
return buf
session._active = False
return b""
ser.read.side_effect = side
session._trace_reader(ser)
def _session():
s = SimSession()
s._formatter = _fmt()
s._active = True
return s
def test_14a_reader_frame_decodes():
s = _session()
frames = []
s._on_frame = frames.append
s._print_frames = False
_run_sim_reader(s, _trace_frame(0, REQA))
assert len(frames) == 1 and len(s.entries) == 1
f = frames[0]
assert f["protocol"] == 1 # ISO 14443-A
assert f["direction"] == 0
assert f["data"] == REQA and f["data_hex"] == REQA.hex()
assert f["decoded"] == "REQA"
assert f["crc_fail"] is False
def test_14a_tag_frame_decodes():
s = _session()
_run_sim_reader(s, _trace_frame(1, ATQA))
assert len(s.entries) == 1
f = s.entries[0]
assert f["protocol"] == 1
assert f["direction"] == 1
assert f["decoded"] == "ATQA 04 00"
def test_14a_crc_fail_bit():
s = _session()
# High bit of the dir byte flags a bad-CRC frame (dir 0 | 0x80).
_run_sim_reader(s, _trace_frame(0x80, REQA))
assert len(s.entries) == 1
f = s.entries[0]
assert f["direction"] == 0
assert f["crc_fail"] is True
assert "BAD CRC" in f["decoded"]
def test_14a_quiet_suppresses_console(capsys):
s = _session()
frames = []
s._on_frame = frames.append
s._print_frames = False # quiet
_run_sim_reader(s, _trace_frame(0, REQA))
assert len(frames) == 1
assert capsys.readouterr().out == ""
def test_14a_entries_accumulate_and_clear():
s = _session()
buf = _trace_frame(0, REQA) + _trace_frame(1, ATQA)
_run_sim_reader(s, buf)
assert len(s.entries) == 2
assert [e["direction"] for e in s.entries] == [0, 1]
s.clear()
assert s.entries == []
def test_start_14a_rejects_non_14443a_tag():
"""A 15693 (or any non-Tag14443A) transponder must be refused up front."""
import pytest
from pm3py.sim import Icode3Tag
s = SimSession()
s._port = MagicMock() # would otherwise fail on port resolution
with pytest.raises(TypeError, match="ISO 14443-A"):
s.start_14a(Icode3Tag(uid=None))
def test_start_15693_rejects_non_15693_tag():
"""Symmetric guard: a 14443-A tag must be refused by start_15693."""
import pytest
from pm3py.sim import NTAG213
s = SimSession()
s._port = MagicMock()
with pytest.raises(TypeError, match="ISO 15693"):
s.start_15693(NTAG213())