import asyncio import struct from unittest.mock import AsyncMock from pm3py.core.protocol import Cmd from pm3py.core.transport import PM3Response from pm3py.core.hf_iso15 import ( HF15Commands, parse_tracelog, TRACELOG_HDR_SIZE, decode_15693_request, decode_15693_response, decode_15693, format_sniff_line, decode_ndef_annotation, ) def test_15_scan(): t = AsyncMock() iso15 = HF15Commands(t) # Fake response: flags(1) + DSFID(1) + UID(8) resp_data = bytes([0x00, 0x00]) + bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xE0]) t.send_ng.return_value = PM3Response(cmd=Cmd.HF_ISO15693_COMMAND, status=0, reason=0, ng=True, data=resp_data) result = asyncio.get_event_loop().run_until_complete(iso15.scan()) assert "uid" in result def test_15_raw(): from pm3py.core.protocol import crc15_bytes t = AsyncMock() iso15 = HF15Commands(t) resp_data = bytes([0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x96, 0x3A]) # flags + data + crc t.send_ng.return_value = PM3Response(cmd=Cmd.HF_ISO15693_COMMAND, status=0, reason=0, ng=True, data=resp_data) # send a raw request frame (no CRC); it should be CRC'd + wrapped, and the tag reply returned r = asyncio.get_event_loop().run_until_complete(iso15.raw(b"\x02\x20\x00")) assert r["status"] == 0 and r["raw"] == resp_data and r["data"] == resp_data.hex() payload = t.send_ng.call_args.args[1] # [flags(1)][len(2)][iso_cmd][crc15(2)] assert payload[3:6] == b"\x02\x20\x00" # the frame, verbatim assert payload[6:8] == crc15_bytes(b"\x02\x20\x00") # ISO15693 CRC appended host-side # a hex string is accepted too asyncio.get_event_loop().run_until_complete(iso15.raw("02 B2 04")) assert t.send_ng.call_args.args[1][3:6] == b"\x02\xB2\x04" # ---- Tracelog parsing ---- def _make_tracelog_entry(timestamp, duration, is_response, data): """Build a raw tracelog entry matching firmware's tracelog_hdr_t.""" data_len_flags = len(data) | (0x8000 if is_response else 0) hdr = struct.pack("> 8) & 0xFF, len(rec) & 0xFF]) + rec + bytes([0xFE]) ann = decode_ndef_annotation(tlv) assert ann is not None assert "..." in ann class TestResponseNdefDecode: def test_read_single_block_0_cc(self): data = bytes([0x00, 0xE1, 0x40, 0x0D, 0x01]) ann = decode_15693_response(data) assert "CC" in ann assert "MBREAD" in ann def test_read_multiple_blocks_ndef(self): msg = bytes([0xD1, 0x01, 0x07, 0x54, 0x02, 0x65, 0x6E, 0x74, 0x65, 0x73, 0x74]) tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE]) data = bytes([0x00]) + tlv + bytes(50) ann = decode_15693_response(data) assert '"test"' in ann def test_read_multiple_with_cc_and_ndef(self): cc = bytes([0xE1, 0x40, 0x0D, 0x01]) msg = bytes([0xD1, 0x01, 0x07, 0x54, 0x02, 0x65, 0x6E, 0x74, 0x65, 0x73, 0x74]) tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE]) data = bytes([0x00]) + cc + tlv + bytes(40) ann = decode_15693_response(data) assert "CC" in ann assert '"test"' in ann def test_generic_data_no_ndef(self): data = bytes([0x00, 0xDE, 0xAD, 0xBE, 0xEF]) ann = decode_15693_response(data) assert "OK [4B]" in ann def test_inventory_not_affected(self): data = bytes([0x00, 0x00]) + bytes(8) ann = decode_15693_response(data) assert "INVENTORY" in ann class TestSniffLineAnnotationOverflow: def test_long_annotation_wraps(self): cc = bytes([0xE1, 0x40, 0x0D, 0x01]) # Build NDEF text record manually (no sim.type5 dependency) text = "A" * 30 lang = b"en" payload = bytes([len(lang)]) + lang + text.encode("utf-8") msg = bytes([0xD1, 0x01, len(payload), 0x54]) + payload tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE]) payload = bytes([0x00]) + cc + tlv + bytes([0xAA, 0xBB]) entry = {"is_response": True, "data": payload, "timestamp": 0, "duration": 0, "parity": b""} line = format_sniff_line(entry, width=80, is_tty=False) lines = line.split("\n") ann_lines = [l for l in lines if "NDEF" in l] assert len(ann_lines) >= 1 def test_short_annotation_no_wrap(self): data = bytes([0x00, 0xE1, 0x40, 0x0D, 0x01, 0xAA, 0xBB]) entry = {"is_response": True, "data": data, "timestamp": 0, "duration": 0, "parity": b""} line = format_sniff_line(entry, width=120, is_tty=False) assert "CC" in line def _inv_record(uid_display_hex, dsfid=0, flags=0): from pm3py.core.protocol import crc15_bytes body = bytes([flags, dsfid]) + bytes.fromhex(uid_display_hex)[::-1] return body + crc15_bytes(body) def test_15_inventory_single_slot(): t = AsyncMock() iso = HF15Commands(t) t.send_ng.return_value = PM3Response(cmd=Cmd.HF_ISO15693_COMMAND, status=0, reason=0, ng=True, data=_inv_record("e004a2110b37c433")) tag = asyncio.get_event_loop().run_until_complete(iso.inventory(slots=1)) assert tag == {"uid": "e004a2110b37c433", "dsfid": 0, "flags": 0} # SLOT1 flag set in the request (raw[0] bit 0x20) payload = t.send_ng.call_args.args[1] assert payload[3] & 0x20 # raw[0] after 3-byte flags+len header def test_15_inventory_16_slot_list_and_dsfid_filter(): t = AsyncMock() iso = HF15Commands(t) r0 = _inv_record("e004a2110b37c433") r3 = _inv_record("e004a2110b3746e0", dsfid=5) meta = bytearray(16 * 2) meta[0:2] = bytes([1, len(r0)]) # slot 0 valid meta[6:8] = bytes([1, len(r3)]) # slot 3 valid meta[10:12] = bytes([2, 0]) # slot 5 collision data = bytes([16]) + bytes(meta) + r0 + r3 t.send_ng.return_value = PM3Response(cmd=Cmd.HF_ISO15693_COMMAND, status=0, reason=0, ng=True, data=data) tags = asyncio.get_event_loop().run_until_complete(iso.inventory(slots=16)) assert [x["uid"] for x in tags] == ["e004a2110b37c433", "e004a2110b3746e0"] assert tags[0]["slot"] == 0 and tags[1]["slot"] == 3 # client-side DSFID filter t.send_ng.return_value = PM3Response(cmd=Cmd.HF_ISO15693_COMMAND, status=0, reason=0, ng=True, data=data) only5 = asyncio.get_event_loop().run_until_complete(iso.inventory(slots=16, dsfid=5)) assert [x["uid"] for x in only5] == ["e004a2110b3746e0"]