From 180c3687b56c9ae58acc02e6d8d265c0ce88b831 Mon Sep 17 00:00:00 2001 From: Sebastian Andersson Date: Tue, 20 Jan 2026 20:11:43 +0100 Subject: [PATCH 1/4] Fix Get System Information response parsing --- src/pn5180_tagomatic/iso15693.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pn5180_tagomatic/iso15693.py b/src/pn5180_tagomatic/iso15693.py index 3734235..52eeb8a 100644 --- a/src/pn5180_tagomatic/iso15693.py +++ b/src/pn5180_tagomatic/iso15693.py @@ -145,24 +145,26 @@ def get_system_information(self) -> dict[str, int]: "Error getting system information", system_info[1] ) if len(system_info) < 1: - system_info += b"\0" + raise PN5180Error( + "Error getting system information, no answer", 0 + ) - pos = 9 + pos = 10 result = {} - if system_info[0] & 1: + if system_info[1] & 1: result["dsfid"] = system_info[pos] pos += 1 - if system_info[0] & 2: + if system_info[1] & 2: result["afi"] = system_info[pos] pos += 1 - if system_info[0] & 4: + if system_info[1] & 4: result["num_blocks"] = system_info[pos] + 1 pos += 1 result["block_size"] = (system_info[pos] & 31) + 1 pos += 1 - if system_info[0] & 8: - pos += 1 + if system_info[1] & 8: result["ic_reference"] = system_info[pos] + pos += 1 return result From f8793e2bc353fc7f5292957a78155ac496187b51 Mon Sep 17 00:00:00 2001 From: Sebastian Andersson Date: Tue, 20 Jan 2026 20:44:00 +0100 Subject: [PATCH 2/4] Add memory_number_of_blocks ISO15693 prop --- src/pn5180_tagomatic/iso15693.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pn5180_tagomatic/iso15693.py b/src/pn5180_tagomatic/iso15693.py index 52eeb8a..915ecf7 100644 --- a/src/pn5180_tagomatic/iso15693.py +++ b/src/pn5180_tagomatic/iso15693.py @@ -71,6 +71,11 @@ def memory_block_size(self) -> int: self._ensure_sys_info_loaded() return self._block_size + @property + def memory_number_of_blocks(self) -> int: + self._ensure_sys_info_loaded() + return self._num_blocks + def read_memory(self, offset: int = 0, length: int | None = None) -> bytes: """Read memory from card. @@ -145,9 +150,7 @@ def get_system_information(self) -> dict[str, int]: "Error getting system information", system_info[1] ) if len(system_info) < 1: - raise PN5180Error( - "Error getting system information, no answer", 0 - ) + raise PN5180Error("Error getting system information, no answer", 0) pos = 10 result = {} From 8bf84bb650f949bf52424306bebeda86c0293349 Mon Sep 17 00:00:00 2001 From: Sebastian Andersson Date: Tue, 20 Jan 2026 21:55:31 +0100 Subject: [PATCH 3/4] Add NDEF decoding Also fix formatting for black-26.1.0 --- examples/iso_14443-read-memory.py | 9 +++- examples/iso_15693-read-memory.py | 10 ++++ src/pn5180_tagomatic/cards.py | 14 ++++++ src/pn5180_tagomatic/iso14443a.py | 76 +++++++++++++++++++++++++++++ src/pn5180_tagomatic/iso15693.py | 80 +++++++++++++++++++++++++++++++ src/pn5180_tagomatic/session.py | 6 +-- 6 files changed, 190 insertions(+), 5 deletions(-) diff --git a/examples/iso_14443-read-memory.py b/examples/iso_14443-read-memory.py index b0b2535..3bec241 100755 --- a/examples/iso_14443-read-memory.py +++ b/examples/iso_14443-read-memory.py @@ -49,7 +49,7 @@ def main() -> int: print(f"Error: {e}") return 1 - memory = card.read_memory() + memory = card.read_memory(0, 512) # Display memory content for offset in range(0, len(memory), 16): @@ -60,6 +60,13 @@ def main() -> int: ) print(f"({offset:03x}): {chunk.hex(' ')} {ascii_values}") + ndef_result = card.get_ndef(memory) + if ndef_result is not None: + start, mem = ndef_result + print(f"NDEF found, it starts at {start}, len={len(mem)}") + print(f"Content:\n{mem.hex(' ')}") + print(f"Next TLV type: {memory[start + len(mem)]:02x}") + return 0 except Exception as e: diff --git a/examples/iso_15693-read-memory.py b/examples/iso_15693-read-memory.py index de1a18e..3699657 100755 --- a/examples/iso_15693-read-memory.py +++ b/examples/iso_15693-read-memory.py @@ -58,8 +58,10 @@ def main() -> int: card = session.connect_iso15693(uids[0]) try: + memory = b"" for offset in range(0, 512, 16): chunk = card.read_memory(offset, 16) + memory += chunk ascii_values = "".join( chr(byte) if 32 <= byte <= 126 else "." for byte in chunk @@ -70,6 +72,14 @@ def main() -> int: except TimeoutError: # Done pass + ndef_result = card.get_ndef(memory) + if ndef_result is not None: + start, mem = ndef_result + print( + f"NDEF found, it starts at {start}, len={len(mem)}" + ) + print(f"Content:\n{mem.hex(' ')}") + print(f"Next TLV type: {memory[start + len(mem)]:02x}") else: print("\nNo tags found") diff --git a/src/pn5180_tagomatic/cards.py b/src/pn5180_tagomatic/cards.py index 6a88d34..b4318e2 100644 --- a/src/pn5180_tagomatic/cards.py +++ b/src/pn5180_tagomatic/cards.py @@ -121,3 +121,17 @@ def write_memory(self, offset: int, data: bytes) -> None: TimeoutError: If card does not respond. MemoryWriteError: Other memory write failures. """ + + def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None: + """Find the NDEF memory. + + If found, the start index in the input memory and + its bytes are returned. + + Args: + memory: The card's memory, starting from 0 + + Returns: + (start, ndef_bytes), + or None if it wasn't found. + """ diff --git a/src/pn5180_tagomatic/iso14443a.py b/src/pn5180_tagomatic/iso14443a.py index 9de5438..c9c7ad4 100644 --- a/src/pn5180_tagomatic/iso14443a.py +++ b/src/pn5180_tagomatic/iso14443a.py @@ -262,3 +262,79 @@ def authenticate_for_page( if len(key_b) != 6: raise ValueError("key_b must be exactly 6 bytes") self._keys_b[page_num] = bytes(key_b) + + def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None: + """Decode the CC memory block (block 0) + + Args: + cc(bytes): The memory from block 0. + + Returns: + (major_version, minor_version, memory size, is readonly) + or None if CC isn't valid. + + Raises: + PN5180Error: If communication with the card fails. + ValueError: If cc is less than 4 bytes. + """ + if len(cc) < 4: + raise ValueError("cc should be at least 4 bytes") + + if cc[0] != 0xE1: + return None + + major = cc[1] >> 4 + minor = cc[1] & 0xF + + mlen = (cc[2]) * 4 + + is_readonly = bool((cc[3] & 0xF0) == 0xF) + + return (major, minor, mlen, is_readonly) + + def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None: + """Find the NDEF memory. + + Args: + memory: The card's memory, starting from offset 0 + + Returns: + (start, ndef_bytes), + or None if NDEF couldn't be found. + """ + + cc = self.decode_cc(memory[12:16]) + if cc is None: + return None + + major, _minor, mlen, _ = cc + + if major > 1: + return None + + if mlen > len(memory): + return None + + pos = 16 + + def read_val(memory: bytes, pos: int) -> tuple[int, int]: + if memory[pos] < 255: + return memory[pos], pos + 1 + else: + return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3 + + while pos < mlen: + typ, pos = read_val(memory, pos) + if typ == 0: + continue + if typ == 0xFE: + # End of TLV + return None + field_len, pos = read_val(memory, pos) + if typ == 0x03: + if pos + field_len > mlen: + return None + return (pos, memory[pos : pos + field_len]) + pos += field_len + + return None diff --git a/src/pn5180_tagomatic/iso15693.py b/src/pn5180_tagomatic/iso15693.py index 915ecf7..b3b2b2f 100644 --- a/src/pn5180_tagomatic/iso15693.py +++ b/src/pn5180_tagomatic/iso15693.py @@ -73,9 +73,89 @@ def memory_block_size(self) -> int: @property def memory_number_of_blocks(self) -> int: + """Gets the number of blocks the card contains""" self._ensure_sys_info_loaded() return self._num_blocks + def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None: + """Decode the CC memory block (block 0) + + Args: + cc(bytes): The memory from block 0. + + Returns: + (major_version, minor_version, memory size, is readonly) + or None if CC isn't valid. + + Raises: + PN5180Error: If communication with the card fails. + ValueError: If cc is less than 4 bytes. + """ + if len(cc) < 4: + raise ValueError("cc should be at least 4 bytes") + + if cc[0] != 0xE1: + return None + + major = cc[1] >> 4 + minor = cc[1] & 0xF + + mlen = (cc[2] + 1) * 8 + + is_readonly = bool(cc[3] & 1) + + return (major, minor, mlen, is_readonly) + + def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None: + """Find the NDEF memory. + + If found, the start index in the input memory and + its bytes are returned. + + Args: + memory: The card's memory, starting from 0 + + Returns: + (start, ndef_bytes), + or None if it wasn't found. + """ + + cc = self.decode_cc(memory) + if cc is None: + return None + + major, _minor, mlen, _ = cc + + if major > 4: + return None + + if mlen > len(memory): + return None + + pos = 4 + + def read_val(memory: bytes, pos: int) -> tuple[int, int]: + if memory[pos] < 255: + return memory[pos], pos + 1 + else: + return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3 + + while pos < mlen: + typ, pos = read_val(memory, pos) + if typ == 0: + continue + if typ == 0xFE: + # End of TLV + return None + field_len, pos = read_val(memory, pos) + if typ == 0x03: + if pos + field_len > mlen: + return None + return (pos, memory[pos : pos + field_len]) + pos += field_len + + return None + def read_memory(self, offset: int = 0, length: int | None = None) -> bytes: """Read memory from card. diff --git a/src/pn5180_tagomatic/session.py b/src/pn5180_tagomatic/session.py index ba2aae1..b1c034c 100644 --- a/src/pn5180_tagomatic/session.py +++ b/src/pn5180_tagomatic/session.py @@ -204,7 +204,7 @@ def get_all_iso14443a_uids( (0, b"", 0, [], True), ] while len(discovery_stack) > 0: - (cl, mask, coll_bit, uid, restart) = discovery_stack.pop() + cl, mask, coll_bit, uid, restart = discovery_stack.pop() if restart: self._reader.turn_off_crc() @@ -246,9 +246,7 @@ def get_all_iso14443a_uids( self._reader.set_rx_crc_and_first_bit(False, 0) self._reader.turn_off_tx_crc() cmd = self._get_cmd_for_level(cl) - (nvb, final_bits) = self._get_nvb_and_final_bits( - len(mask), coll_bit - ) + nvb, final_bits = self._get_nvb_and_final_bits(len(mask), coll_bit) try: self._reader.set_rx_crc_and_first_bit(False, final_bits) From 7d25bc0df19a33d8086a2af0193716a5f4e708d9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 07:58:03 +0100 Subject: [PATCH 4/4] Add tests for NDEF parsing methods (#34) * Initial plan * Add comprehensive tests for decode_cc and get_ndef methods Co-authored-by: bofh69 <1444315+bofh69@users.noreply.github.com> * Refactor tests to use pytest fixtures Co-authored-by: bofh69 <1444315+bofh69@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bofh69 <1444315+bofh69@users.noreply.github.com> --- src/pn5180_tagomatic/iso14443a.py | 2 +- tests/test_iso14443a_ndef.py | 314 ++++++++++++++++++++++++++++++ tests/test_iso15693.py | 294 ++++++++++++++++++++++++++++ 3 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 tests/test_iso14443a_ndef.py create mode 100644 tests/test_iso15693.py diff --git a/src/pn5180_tagomatic/iso14443a.py b/src/pn5180_tagomatic/iso14443a.py index c9c7ad4..0135d5e 100644 --- a/src/pn5180_tagomatic/iso14443a.py +++ b/src/pn5180_tagomatic/iso14443a.py @@ -288,7 +288,7 @@ def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None: mlen = (cc[2]) * 4 - is_readonly = bool((cc[3] & 0xF0) == 0xF) + is_readonly = bool((cc[3] & 0xF0) == 0xF0) return (major, minor, mlen, is_readonly) diff --git a/tests/test_iso14443a_ndef.py b/tests/test_iso14443a_ndef.py new file mode 100644 index 0000000..ec052d2 --- /dev/null +++ b/tests/test_iso14443a_ndef.py @@ -0,0 +1,314 @@ +# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tests for ISO14443a card NDEF functionality.""" + +from unittest.mock import MagicMock + +import pytest + +from pn5180_tagomatic.cards import Iso14443AUniqueId +from pn5180_tagomatic.iso14443a import ISO14443ACard + + +@pytest.fixture +def iso14443a_card(): + """Create an ISO14443A card instance for testing.""" + mock_comm = MagicMock() + uid = Iso14443AUniqueId(bytes([0x01, 0x02, 0x03, 0x04]), bytes([0x08])) + return ISO14443ACard(mock_comm, uid) + + +def test_iso14443a_decode_cc_valid(iso14443a_card): + """Test decode_cc with valid capability container.""" + # Valid CC: + # * magic byte 0xE1 + # * version 1.0 + # * memory size 12*4=48 bytes + # * read/write access + cc = bytes([0xE1, 0x10, 0x0C, 0x00]) + + result = iso14443a_card.decode_cc(cc) + + assert result is not None + major, minor, mlen, is_readonly = result + assert major == 1 + assert minor == 0 + assert mlen == 48 # 12 * 4 + assert is_readonly is False + + +def test_iso14443a_decode_cc_readonly(iso14443a_card): + """Test decode_cc with readonly access.""" + # CC with readonly flag (cc[3] & 0xF0 == 0xF0) + cc = bytes([0xE1, 0x10, 0x0C, 0xF0]) + + result = iso14443a_card.decode_cc(cc) + + assert result is not None + major, minor, mlen, is_readonly = result + assert major == 1 + assert minor == 0 + assert mlen == 48 + assert is_readonly is True + + +def test_iso14443a_decode_cc_partial_readonly(iso14443a_card): + """Test decode_cc with partial readonly bits set.""" + # CC with some readonly bits but not all (0xF0) + cc = bytes([0xE1, 0x10, 0x0C, 0xE0]) + + result = iso14443a_card.decode_cc(cc) + + assert result is not None + major, minor, mlen, is_readonly = result + assert is_readonly is False # Not all bits set + + +def test_iso14443a_decode_cc_invalid_magic(iso14443a_card): + """Test decode_cc with invalid magic byte.""" + # Invalid magic byte (should be 0xE1) + cc = bytes([0xE2, 0x10, 0x0C, 0x00]) + + result = iso14443a_card.decode_cc(cc) + + assert result is None + + +def test_iso14443a_decode_cc_short_input(iso14443a_card): + """Test decode_cc with input too short.""" + # Too short (only 3 bytes, should be at least 4) + cc = bytes([0xE1, 0x10, 0x0C]) + + with pytest.raises(ValueError): + iso14443a_card.decode_cc(cc) + + +def test_iso14443a_get_ndef_simple_tlv(iso14443a_card): + """Test get_ndef with a simple NDEF TLV structure.""" + # Memory layout for ISO14443a: + # Bytes 0-11: Not used (12 bytes before CC) + # Bytes 12-15: CC (0xE1, version 1.0, 48 bytes, read/write) + # Bytes 16-17: NDEF TLV (Type=0x03, Length=10) + # Bytes 18-27: NDEF message content + memory = bytearray([0x00] * 12) # First 12 bytes + memory.extend( + [ + 0xE1, + 0x10, + 0x0C, + 0x00, # CC at offset 12 + 0x03, + 0x0A, # NDEF TLV at offset 16 + 0xD1, + 0x01, + 0x06, + 0x54, + 0x02, + 0x65, + 0x6E, + 0x68, + 0x69, + 0x00, # NDEF message + ] + ) + # Pad to 48 bytes to match CC + memory.extend([0x00] * (48 - len(memory))) + + result = iso14443a_card.get_ndef(memory) + + assert result is not None + pos, ndef_bytes = result + assert pos == 18 + assert len(ndef_bytes) == 10 + assert ndef_bytes == bytes( + [0xD1, 0x01, 0x06, 0x54, 0x02, 0x65, 0x6E, 0x68, 0x69, 0x00] + ) + + +def test_iso14443a_get_ndef_long_length(iso14443a_card): + """Test get_ndef with 3-byte length encoding (length >= 255).""" + # Memory with 3-byte length encoding + memory = bytearray([0x00] * 12) # First 12 bytes + memory.extend( + [ + 0xE1, + 0x10, + 0xFF, + 0x00, # CC: 255*4=1020 bytes + 0x03, + 0xFF, + 0x01, + 0x00, # NDEF TLV: Type=0x03, Length=0xFF (3-byte), value=256 + ] + ) + # Add 256 bytes of NDEF data + memory.extend([0xBB] * 256) + # Pad to 1020 bytes to match CC + memory.extend([0x00] * (1020 - len(memory))) + + result = iso14443a_card.get_ndef(memory) + + assert result is not None + pos, ndef_bytes = result + assert pos == 20 + assert len(ndef_bytes) == 256 + assert all(b == 0xBB for b in ndef_bytes) + + +def test_iso14443a_get_ndef_with_null_tlvs(iso14443a_card): + """Test get_ndef with NULL TLVs before NDEF.""" + # Memory with NULL TLVs (0x00) before NDEF + memory = bytearray([0x00] * 12) + memory.extend( + [ + 0xE1, + 0x10, + 0x0C, + 0x00, # CC + 0x00, + 0x00, # NULL TLVs + 0x03, + 0x05, # NDEF TLV + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, # "Hello" + ] + ) + # Pad to 48 bytes to match CC + memory.extend([0x00] * (48 - len(memory))) + + result = iso14443a_card.get_ndef(memory) + + assert result is not None + pos, ndef_bytes = result + assert pos == 20 + assert ndef_bytes == bytes([0x48, 0x65, 0x6C, 0x6C, 0x6F]) + + +def test_iso14443a_get_ndef_terminator_tlv(iso14443a_card): + """Test get_ndef returns None when encountering terminator TLV before NDEF.""" + # Memory with terminator TLV (0xFE) before NDEF + memory = bytearray([0x00] * 12) + memory.extend( + [ + 0xE1, + 0x10, + 0x0C, + 0x00, # CC + 0xFE, # Terminator TLV + 0x03, + 0x05, # NDEF TLV (won't be reached) + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso14443a_card.get_ndef(memory) + + assert result is None + + +def test_iso14443a_get_ndef_invalid_cc(iso14443a_card): + """Test get_ndef returns None with invalid CC.""" + # Invalid CC (wrong magic byte) + memory = bytearray([0x00] * 12) + memory.extend( + [ + 0xE2, + 0x10, + 0x0C, + 0x00, # Invalid CC + 0x03, + 0x05, + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso14443a_card.get_ndef(memory) + + assert result is None + + +def test_iso14443a_get_ndef_unsupported_version(iso14443a_card): + """Test get_ndef returns None with unsupported major version.""" + # Major version > 1 (unsupported for ISO14443a) + memory = bytearray([0x00] * 12) + memory.extend( + [ + 0xE1, + 0x20, + 0x0C, + 0x00, # CC with major version 2 + 0x03, + 0x05, + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso14443a_card.get_ndef(memory) + + assert result is None + + +def test_iso14443a_get_ndef_memory_too_small(iso14443a_card): + """Test get_ndef returns None when memory is smaller than CC indicates.""" + # CC indicates 1020 bytes but memory is only 28 bytes + memory = bytearray([0x00] * 12) + memory.extend( + [ + 0xE1, + 0x10, + 0xFF, + 0x00, # CC: 255*4=1020 bytes + 0x03, + 0x05, + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso14443a_card.get_ndef(memory) + + assert result is None + + +def test_iso14443a_get_ndef_field_exceeds_memory(iso14443a_card): + """Test get_ndef returns None when NDEF field exceeds memory length.""" + # NDEF field length exceeds available memory + memory = bytearray([0x00] * 12) + memory.extend( + [ + 0xE1, + 0x10, + 0x0C, + 0x00, # CC: 48 bytes + 0x03, + 0x64, # NDEF TLV: Length=100 (exceeds limit) + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso14443a_card.get_ndef(memory) + + assert result is None diff --git a/tests/test_iso15693.py b/tests/test_iso15693.py new file mode 100644 index 0000000..8506bb3 --- /dev/null +++ b/tests/test_iso15693.py @@ -0,0 +1,294 @@ +# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tests for ISO15693 card functionality.""" + +from unittest.mock import MagicMock + +import pytest + +from pn5180_tagomatic.iso15693 import ISO15693Card + + +@pytest.fixture +def iso15693_card(): + """Create an ISO15693 card instance for testing.""" + mock_comm = MagicMock() + return ISO15693Card( + mock_comm, bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) + ) + + +def test_iso15693_decode_cc_valid(iso15693_card): + """Test decode_cc with valid capability container.""" + # Valid CC: + # * magic byte 0xE1 + # * version 1.0 + # * memory size (7+1)*8=64 bytes + # * read/write access + cc = bytes([0xE1, 0x10, 0x07, 0x00]) + + result = iso15693_card.decode_cc(cc) + + assert result is not None + major, minor, mlen, is_readonly = result + assert major == 1 + assert minor == 0 + assert mlen == 64 # (7 + 1) * 8 + assert is_readonly is False + + +def test_iso15693_decode_cc_readonly(iso15693_card): + """Test decode_cc with readonly access.""" + # CC with readonly bit set (bit 0 of cc[3]) + cc = bytes([0xE1, 0x10, 0x0F, 0x01]) + + result = iso15693_card.decode_cc(cc) + + assert result is not None + major, minor, mlen, is_readonly = result + assert major == 1 + assert minor == 0 + assert mlen == 128 # (15 + 1) * 8 + assert is_readonly is True + + +def test_iso15693_decode_cc_invalid_magic(iso15693_card): + """Test decode_cc with invalid magic byte.""" + # Invalid magic byte (should be 0xE1) + cc = bytes([0xE2, 0x10, 0x07, 0x00]) + + result = iso15693_card.decode_cc(cc) + + assert result is None + + +def test_iso15693_decode_cc_short_input(iso15693_card): + """Test decode_cc with input too short.""" + # Too short (only 3 bytes, should be at least 4) + cc = bytes([0xE1, 0x10, 0x07]) + + with pytest.raises(ValueError): + iso15693_card.decode_cc(cc) + + +def test_iso15693_get_ndef_simple_tlv(iso15693_card): + """Test get_ndef with a simple NDEF TLV structure.""" + # Memory layout: + # Bytes 0-3: CC (0xE1, version 1.0, 64 bytes, read/write) + # Bytes 4-5: NDEF TLV (Type=0x03, Length=10) + # Bytes 6-15: NDEF message content + memory = bytearray( + [ + 0xE1, + 0x10, + 0x07, + 0x00, # CC + 0x03, + 0x0A, # NDEF TLV: Type=0x03, Length=10 + 0xD1, + 0x01, + 0x06, + 0x54, + 0x02, + 0x65, + 0x6E, + 0x68, + 0x69, + 0x00, # NDEF message + ] + ) + # Pad to 64 bytes to match CC + memory.extend([0x00] * (64 - len(memory))) + + result = iso15693_card.get_ndef(memory) + + assert result is not None + pos, ndef_bytes = result + assert pos == 6 + assert len(ndef_bytes) == 10 + assert ndef_bytes == bytes( + [0xD1, 0x01, 0x06, 0x54, 0x02, 0x65, 0x6E, 0x68, 0x69, 0x00] + ) + + +def test_iso15693_get_ndef_long_length(iso15693_card): + """Test get_ndef with 3-byte length encoding (length >= 255).""" + # Memory with 3-byte length encoding + # CC allows up to 2048 bytes + memory = bytearray( + [ + 0xE1, + 0x10, + 0xFF, + 0x00, # CC: 256*8=2048 bytes + 0x03, + 0xFF, + 0x01, + 0x00, # NDEF TLV: Type=0x03, Length=0xFF (3-byte), length value=256 + ] + ) + # Add 256 bytes of NDEF data + memory.extend([0xAA] * 256) + # Pad to 2048 bytes to match CC + memory.extend([0x00] * (2048 - len(memory))) + + result = iso15693_card.get_ndef(memory) + + assert result is not None + pos, ndef_bytes = result + assert pos == 8 + assert len(ndef_bytes) == 256 + assert all(b == 0xAA for b in ndef_bytes) + + +def test_iso15693_get_ndef_with_null_tlvs(iso15693_card): + """Test get_ndef with NULL TLVs before NDEF.""" + # Memory with NULL TLVs (0x00) before NDEF + memory = bytearray( + [ + 0xE1, + 0x10, + 0x07, + 0x00, # CC + 0x00, + 0x00, # NULL TLVs + 0x03, + 0x05, # NDEF TLV + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, # "Hello" + ] + ) + # Pad to 64 bytes to match CC + memory.extend([0x00] * (64 - len(memory))) + + result = iso15693_card.get_ndef(memory) + + assert result is not None + pos, ndef_bytes = result + assert pos == 8 + assert ndef_bytes == bytes([0x48, 0x65, 0x6C, 0x6C, 0x6F]) + + +def test_iso15693_get_ndef_terminator_tlv(iso15693_card): + """Test get_ndef returns None when encountering terminator TLV before NDEF.""" + # Memory with terminator TLV (0xFE) before NDEF + memory = bytearray( + [ + 0xE1, + 0x10, + 0x07, + 0x00, # CC + 0xFE, # Terminator TLV + 0x03, + 0x05, # NDEF TLV (won't be reached) + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso15693_card.get_ndef(memory) + + assert result is None + + +def test_iso15693_get_ndef_invalid_cc(iso15693_card): + """Test get_ndef returns None with invalid CC.""" + # Invalid CC (wrong magic byte) + memory = bytearray( + [ + 0xE2, + 0x10, + 0x07, + 0x00, # Invalid CC + 0x03, + 0x05, + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso15693_card.get_ndef(memory) + + assert result is None + + +def test_iso15693_get_ndef_unsupported_version(iso15693_card): + """Test get_ndef returns None with unsupported major version.""" + # Major version > 4 (unsupported) + memory = bytearray( + [ + 0xE1, + 0x50, + 0x07, + 0x00, # CC with major version 5 + 0x03, + 0x05, + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso15693_card.get_ndef(memory) + + assert result is None + + +def test_iso15693_get_ndef_memory_too_small(iso15693_card): + """Test get_ndef returns None when memory is smaller than CC indicates.""" + # CC indicates 2048 bytes but memory is only 16 bytes + memory = bytearray( + [ + 0xE1, + 0x10, + 0xFF, + 0x00, # CC: 256*8=2048 bytes + 0x03, + 0x05, + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso15693_card.get_ndef(memory) + + assert result is None + + +def test_iso15693_get_ndef_field_exceeds_memory(iso15693_card): + """Test get_ndef returns None when NDEF field exceeds memory length.""" + # NDEF field length exceeds available memory + memory = bytearray( + [ + 0xE1, + 0x10, + 0x07, + 0x00, # CC: 64 bytes + 0x03, + 0x64, # NDEF TLV: Length=100 (exceeds 64 byte limit) + 0x48, + 0x65, + 0x6C, + 0x6C, + 0x6F, + ] + ) + + result = iso15693_card.get_ndef(memory) + + assert result is None