Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests) from .worktrees/sim-framework/ into pm3py/sim/. Import fixes: - 4 files: ..protocol/..transport → ..core.protocol/..core.transport - trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef - test_sim_pm3medium.py: flat imports → core.* - test_sim_trace_fmt.py: flat imports → core.* - Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*) 751 tests passing (686 sim + 65 core). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
211 lines
7.5 KiB
Python
211 lines
7.5 KiB
Python
"""Tests for DirtyByteArray, MemoryRegion, BlockAccess, Transponder regions."""
|
|
import pytest
|
|
from pm3py.sim.memory import DirtyByteArray, MemoryRegion, BlockAccess
|
|
from pm3py.sim.transponder import Transponder
|
|
from pm3py.sim.frame import RFFrame
|
|
|
|
|
|
class TestDirtyByteArray:
|
|
def test_starts_clean(self):
|
|
d = DirtyByteArray(16)
|
|
assert not d.dirty
|
|
|
|
def test_setitem_marks_dirty(self):
|
|
d = DirtyByteArray(16)
|
|
d[0] = 0xFF
|
|
assert d.dirty
|
|
|
|
def test_slice_setitem_marks_dirty(self):
|
|
d = DirtyByteArray(16)
|
|
d[0:4] = b"\xDE\xAD\xBE\xEF"
|
|
assert d.dirty
|
|
|
|
def test_clear_dirty(self):
|
|
d = DirtyByteArray(16)
|
|
d[0] = 0xFF
|
|
d.clear_dirty()
|
|
assert not d.dirty
|
|
|
|
def test_extend_marks_dirty(self):
|
|
d = DirtyByteArray(b"\x00")
|
|
d.extend(b"\xFF")
|
|
assert d.dirty
|
|
|
|
def test_from_bytes(self):
|
|
d = DirtyByteArray(b"\x01\x02\x03")
|
|
assert len(d) == 3
|
|
assert not d.dirty
|
|
|
|
def test_callback_on_dirty(self):
|
|
called = []
|
|
d = DirtyByteArray(16)
|
|
d.on_dirty = lambda: called.append(True)
|
|
d[0] = 0xFF
|
|
assert len(called) == 1
|
|
|
|
|
|
class TestBlockAccess:
|
|
def test_defaults_open(self):
|
|
a = BlockAccess()
|
|
assert a.read == "open"
|
|
assert a.write == "open"
|
|
assert a.read_key is None
|
|
|
|
def test_password_protected(self):
|
|
a = BlockAccess(read="password", read_key=1)
|
|
assert a.read == "password"
|
|
assert a.read_key == 1
|
|
|
|
|
|
class TestMemoryRegion:
|
|
def test_construction(self):
|
|
r = MemoryRegion(name="user", data=DirtyByteArray(112), block_size=4)
|
|
assert r.name == "user"
|
|
assert len(r.data) == 112
|
|
assert r.block_size == 4
|
|
|
|
def test_eml_offset_default_negative(self):
|
|
r = MemoryRegion(name="config", data=DirtyByteArray(8))
|
|
assert r.eml_offset == -1
|
|
|
|
def test_num_blocks(self):
|
|
r = MemoryRegion(name="user", data=DirtyByteArray(112), block_size=4)
|
|
assert r.num_blocks == 28
|
|
|
|
def test_access_for_block_default(self):
|
|
r = MemoryRegion(name="user", data=DirtyByteArray(16), block_size=4)
|
|
assert r.access_for_block(0).read == "open"
|
|
|
|
def test_access_for_block_with_map(self):
|
|
r = MemoryRegion(name="user", data=DirtyByteArray(16), block_size=4,
|
|
access_map=[
|
|
BlockAccess(),
|
|
BlockAccess(),
|
|
BlockAccess(read="password", read_key=1),
|
|
BlockAccess(read="password", read_key=1),
|
|
])
|
|
assert r.access_for_block(0).read == "open"
|
|
assert r.access_for_block(2).read == "password"
|
|
|
|
def test_dirty_propagates_from_data(self):
|
|
r = MemoryRegion(name="user", data=DirtyByteArray(16), block_size=4)
|
|
assert not r.data.dirty
|
|
r.data[0:4] = b"\xDE\xAD\xBE\xEF"
|
|
assert r.data.dirty
|
|
|
|
|
|
class StubTransponder(Transponder):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.regions["user"] = MemoryRegion(
|
|
name="user", data=DirtyByteArray(16), block_size=4, eml_offset=100)
|
|
self.regions["config"] = MemoryRegion(
|
|
name="config", data=DirtyByteArray(8), block_size=0, eml_offset=-1)
|
|
|
|
async def power_on(self): pass
|
|
async def power_off(self): pass
|
|
async def handle_frame(self, frame): return None
|
|
@property
|
|
def state(self): return "TEST"
|
|
|
|
|
|
class TestTransponderRegions:
|
|
def test_has_regions_dict(self):
|
|
t = StubTransponder()
|
|
assert "user" in t.regions
|
|
assert "config" in t.regions
|
|
|
|
def test_authenticate(self):
|
|
t = StubTransponder()
|
|
assert len(t._authenticated) == 0
|
|
t.authenticate(1)
|
|
assert 1 in t._authenticated
|
|
assert t._access_dirty
|
|
|
|
def test_deauthenticate(self):
|
|
t = StubTransponder()
|
|
t.authenticate(1)
|
|
t._access_dirty = False
|
|
t.deauthenticate(1)
|
|
assert 1 not in t._authenticated
|
|
assert t._access_dirty
|
|
|
|
def test_deauthenticate_all(self):
|
|
t = StubTransponder()
|
|
t.authenticate(1)
|
|
t.authenticate(2)
|
|
t.deauthenticate()
|
|
assert len(t._authenticated) == 0
|
|
|
|
|
|
class TestTag15693Regions:
|
|
def test_has_user_region(self):
|
|
from pm3py.sim.iso15693 import Tag15693
|
|
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28)
|
|
assert "user" in tag.regions
|
|
assert tag.regions["user"].block_size == 4
|
|
assert tag.regions["user"].eml_offset == 175
|
|
|
|
def test_memory_property_aliases_region(self):
|
|
from pm3py.sim.iso15693 import Tag15693
|
|
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28)
|
|
tag._memory[0:4] = b"\xDE\xAD\xBE\xEF"
|
|
assert tag.regions["user"].data[0:4] == b"\xDE\xAD\xBE\xEF"
|
|
assert tag.regions["user"].data.dirty
|
|
|
|
|
|
class TestIcodeSlix2AccessMap:
|
|
def test_protection_pointer_sets_access_map(self):
|
|
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
|
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=4,
|
|
read_password=0xAABBCCDD)
|
|
user = tag.regions["user"]
|
|
assert user.access_for_block(0).read == "open"
|
|
assert user.access_for_block(3).read == "open"
|
|
assert user.access_for_block(4).read == "password"
|
|
assert user.access_for_block(4).read_key == 1
|
|
|
|
def test_no_protection_pointer_all_open(self):
|
|
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
|
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
|
|
user = tag.regions["user"]
|
|
assert user.access_for_block(0).read == "open"
|
|
# access_map exists for block 79 counter, but all reads are open
|
|
assert user.access_for_block(78).read == "open"
|
|
assert user.access_for_block(79).write_mode == "counter"
|
|
|
|
|
|
class TestCompileFromRegions:
|
|
def test_open_blocks_have_data_entries(self):
|
|
from pm3py.sim.iso15693 import Tag15693
|
|
from pm3py.sim.table_compiler import TableCompiler
|
|
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=4)
|
|
tag._memory[0:4] = b"\xDE\xAD\xBE\xEF"
|
|
table = TableCompiler.compile_from_regions(tag)
|
|
entry = table.lookup(bytes([0x02, 0x20, 0x00]))
|
|
assert entry is not None
|
|
assert entry.response[1:5] == b"\xDE\xAD\xBE\xEF"
|
|
|
|
def test_protected_blocks_without_auth(self):
|
|
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
|
from pm3py.sim.table_compiler import TableCompiler
|
|
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2,
|
|
read_password=0xAABBCCDD, block_size=4, num_blocks=4)
|
|
table = TableCompiler.compile_from_regions(tag)
|
|
# Block 0 (open) should have entry
|
|
assert table.lookup(bytes([0x02, 0x20, 0x00])) is not None
|
|
# Block 2 (protected) should have no entry (relay)
|
|
assert table.lookup(bytes([0x02, 0x20, 0x02])) is None
|
|
|
|
def test_protected_blocks_with_auth(self):
|
|
from pm3py.sim.icode_slix2 import IcodeSlix2Tag, PWD_READ
|
|
from pm3py.sim.table_compiler import TableCompiler
|
|
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=2,
|
|
read_password=0xAABBCCDD, block_size=4, num_blocks=4)
|
|
tag.authenticate(PWD_READ)
|
|
table = TableCompiler.compile_from_regions(tag)
|
|
# Block 2 now readable
|
|
entry = table.lookup(bytes([0x02, 0x20, 0x02]))
|
|
assert entry is not None
|
|
assert entry.response[0] == 0x00 # no error
|