diff --git a/pm3py/sim/__init__.py b/pm3py/sim/__init__.py index 99e08b2..23c8ead 100644 --- a/pm3py/sim/__init__.py +++ b/pm3py/sim/__init__.py @@ -16,68 +16,27 @@ from .mcu_bridge import McuBridge from .dual_session import DualInterfaceSession # --- Transponder models (canonical home: pm3py.transponders) --- -from pm3py.transponders.hf.iso14443a.base import ( - Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, State14443A, -) -from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1 -from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import ( - MifareClassicTag, MifareClassicReader, MifareAccessConditions, -) -from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader -from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag -from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag, Type2Config -from pm3py.transponders.hf.iso14443a.nxp.ntag21x import ( - Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, Ntag21xConfig, -) -from pm3py.transponders.hf.iso14443a.nxp.ultralight import ( - MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21, - UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig, -) -from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import ( - NtagI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, -) -from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K -from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT -from pm3py.transponders.hf.iso14443a.st.st25ta import ( - ST25TA, ST25TA512B, ST25TA02KB, ST25TA16K, ST25TA64K, -) -from pm3py.transponders.hf.iso15693.base import Tag15693, Reader15693, State15693 -from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime -from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag -from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag -from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag -from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag, IcodeFeatureFlags -from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag -from pm3py.transponders.hf.iso15693.nxp.auth_aes import NxpKeyPrivileges -from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import ( - Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg, -) -from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag -from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag -from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag -from pm3py.transponders.hf.iso15693.st.st25tv import ST25TV, ST25TV512C, ST25TV02KC -from pm3py.transponders.hf.iso15693.st.st25dv import ( - ST25DV, ST25DV04K, ST25DV16K, ST25DV64K, St25dvRfAreaSS, -) -from pm3py.transponders.hf.iso15693.infineon.myd_vicinity import ( - MydVicinity, SRF55V02P, SRF55V10P, -) -from pm3py.transponders.hf.iso14443b.base import Tag14443B, StateB, Tag14443B_4, StateB4 -from pm3py.transponders.hf.iso14443b.st.st25tb import ( - ST25TB, ST25TB512AC, ST25TB512AT, ST25TB02K, ST25TB04K, -) -from pm3py.transponders.hf.iso14443b.ti.rf430cl330h import RF430CL330H -from pm3py.transponders.hf.iso15693.ti.tagit import TagItHFIPlus -from pm3py.transponders.hf.iso15693.ti.rf430frl import ( - RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H, -) -from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation -from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader, EM4100Code -from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader -from pm3py.transponders.bitfield import BitField, Register -from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config -from pm3py.transponders.lf.nxp.hitag2 import Hitag2Tag, Hitag2Reader -from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag +# Loaded lazily (PEP 562) from ._models. Those models import back into pm3py.sim for the +# Transponder/Reader/RFFrame/Medium primitives, so importing them at package-init time would +# create a transponders<->sim circular import (breaking e.g. `from pm3py.transponders...` as a +# first import). On first access they load from ._models and cache into this module's globals. +def __getattr__(name): + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) # don't trigger a model import for dunder probes + import importlib + + models = importlib.import_module(f"{__name__}._models") + try: + value = getattr(models, name) + except AttributeError: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None + globals()[name] = value # cache: subsequent access is a direct lookup + return value + + +def __dir__(): + return sorted(set(globals()) | set(__all__)) + __all__ = [ "RFFrame", diff --git a/pm3py/sim/_models.py b/pm3py/sim/_models.py new file mode 100644 index 0000000..269b7f7 --- /dev/null +++ b/pm3py/sim/_models.py @@ -0,0 +1,71 @@ +"""Transponder-model re-exports for :mod:`pm3py.sim`. + +Loaded lazily by the package's PEP 562 ``__getattr__`` — these models import back into +``pm3py.sim`` (for the ``Transponder`` / ``Reader`` / ``RFFrame`` / ``Medium`` primitives), so +importing them at ``sim`` package-init time creates a circular import. Their canonical home is +``pm3py.transponders``; this module just re-exposes them under ``pm3py.sim``. +""" +from __future__ import annotations + +from pm3py.transponders.hf.iso14443a.base import ( + Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, State14443A, +) +from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1 +from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import ( + MifareClassicTag, MifareClassicReader, MifareAccessConditions, +) +from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader +from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag +from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag, Type2Config +from pm3py.transponders.hf.iso14443a.nxp.ntag21x import ( + Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, Ntag21xConfig, +) +from pm3py.transponders.hf.iso14443a.nxp.ultralight import ( + MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21, + UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig, +) +from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import ( + NtagI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, +) +from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K +from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT +from pm3py.transponders.hf.iso14443a.st.st25ta import ( + ST25TA, ST25TA512B, ST25TA02KB, ST25TA16K, ST25TA64K, +) +from pm3py.transponders.hf.iso15693.base import Tag15693, Reader15693, State15693 +from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime +from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag +from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag +from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag +from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag, IcodeFeatureFlags +from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag +from pm3py.transponders.hf.iso15693.nxp.auth_aes import NxpKeyPrivileges +from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import ( + Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg, +) +from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag +from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag +from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag +from pm3py.transponders.hf.iso15693.st.st25tv import ST25TV, ST25TV512C, ST25TV02KC +from pm3py.transponders.hf.iso15693.st.st25dv import ( + ST25DV, ST25DV04K, ST25DV16K, ST25DV64K, St25dvRfAreaSS, +) +from pm3py.transponders.hf.iso15693.infineon.myd_vicinity import ( + MydVicinity, SRF55V02P, SRF55V10P, +) +from pm3py.transponders.hf.iso14443b.base import Tag14443B, StateB, Tag14443B_4, StateB4 +from pm3py.transponders.hf.iso14443b.st.st25tb import ( + ST25TB, ST25TB512AC, ST25TB512AT, ST25TB02K, ST25TB04K, +) +from pm3py.transponders.hf.iso14443b.ti.rf430cl330h import RF430CL330H +from pm3py.transponders.hf.iso15693.ti.tagit import TagItHFIPlus +from pm3py.transponders.hf.iso15693.ti.rf430frl import ( + RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H, +) +from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation +from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader, EM4100Code +from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader +from pm3py.transponders.bitfield import BitField, Register +from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config +from pm3py.transponders.lf.nxp.hitag2 import Hitag2Tag, Hitag2Reader +from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag diff --git a/pm3py/sim/fuzzer.py b/pm3py/sim/fuzzer.py index 77442e8..c9f9e39 100644 --- a/pm3py/sim/fuzzer.py +++ b/pm3py/sim/fuzzer.py @@ -6,7 +6,6 @@ from typing import Iterator from bitarray import bitarray from .frame import RFFrame -from pm3py.transponders.hf.iso14443a.base import CL1, NVB_SELECT, _compute_bcc class MutationFuzzer: @@ -76,6 +75,9 @@ class GrammarFuzzer: return None def _gen_14443a(self, command: str, **overrides) -> RFFrame | None: + # deferred: iso14443a.base imports back into pm3py.sim, so a module-level import here would + # create a transponders<->sim circular import when 14443a is the first thing imported + from pm3py.transponders.hf.iso14443a.base import CL1, NVB_SELECT, _compute_bcc match command: case "REQA": return RFFrame.from_bytes(b"\x26") diff --git a/tests/test_import_hygiene.py b/tests/test_import_hygiene.py new file mode 100644 index 0000000..23c260a --- /dev/null +++ b/tests/test_import_hygiene.py @@ -0,0 +1,46 @@ +"""Guards against the transponders<->sim circular import. + +Transponder models import ``pm3py.sim`` primitives (Transponder/Reader/RFFrame/Medium), so +``pm3py.sim`` must NOT eagerly re-import transponder models at package-init time — its model +re-exports are lazy (see ``pm3py/sim/_models.py`` + the package ``__getattr__``), and ``fuzzer.py`` +defers its ``iso14443a.base`` import. Each family must therefore import as a *fresh* first import. +These run in subprocesses because import-order bugs only show in a clean interpreter. +""" +import subprocess +import sys + +import pytest + +# each of these, imported first in a clean interpreter, used to trigger a circular import +_FRESH_IMPORTS = [ + "pm3py.transponders.hf.iso15693.type5", # the reported failure + "pm3py.transponders.hf.iso14443a.base", # the fuzzer-side failure + "pm3py.transponders.hf.iso14443b.base", + "pm3py.transponders.lf.atmel.t5577", + "pm3py.transponders.implants", + "pm3py.sim.fuzzer", + "pm3py.sim", +] + + +@pytest.mark.parametrize("module", _FRESH_IMPORTS) +def test_fresh_import_has_no_cycle(module): + r = subprocess.run([sys.executable, "-c", f"import {module}"], + capture_output=True, text=True) + assert r.returncode == 0, f"importing {module} first failed:\n{r.stderr}" + + +def test_sim_lazy_reexports_resolve(): + import pm3py.sim as sim + from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag + + assert sim.NfcType5Tag is NfcType5Tag # lazy re-export == canonical class + assert "NfcType5Tag" in dir(sim) + assert all(getattr(sim, n, None) is not None for n in sim.__all__) + + +def test_fuzzer_deferred_import_works(): + from pm3py.sim.fuzzer import GrammarFuzzer + + frame = GrammarFuzzer("14443a").generate("SELECT", uid=b"\x04\x11\x22\x33") + assert frame.data.hex() == "93700411223304" # CL1, NVB_SELECT, uid, bcc