|
|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
-from daemon.nfc_reader import NFCReader, NFCState, _extract_tray_uuid, _raw_blocks, choose_ndef
|
|
|
+from daemon.nfc_reader import NFCReader, NFCState, _extract_tray_uuid, _raw_blocks, capacity_refusal, choose_ndef
|
|
|
|
|
|
# Measured on a real spool: block 9 equals the tray_uuid the AMS reports (#984).
|
|
|
BLOCK9 = bytes.fromhex("9E0B0717BEE94D7887EB1D8DFD1A14F3")
|
|
|
@@ -67,18 +67,44 @@ class TestChooseNdef:
|
|
|
class TestWriteNtag:
|
|
|
UID = bytes.fromhex("04A1B2C3D4E5F6")
|
|
|
|
|
|
- def _reader(self, cc: bytes | None = None, read_error: Exception | None = None):
|
|
|
+ NTAG215_CC = bytes([0xE1, 0x10, 0x3E, 0x00])
|
|
|
+
|
|
|
+ def _reader(
|
|
|
+ self,
|
|
|
+ cc: bytes | None = NTAG215_CC,
|
|
|
+ read_error: Exception | None = None,
|
|
|
+ read_back=None,
|
|
|
+ ):
|
|
|
+ """A reader over a fake PN5180 whose tag keeps what is written to it.
|
|
|
+
|
|
|
+ *read_back* replaces what reading the written pages returns: a
|
|
|
+ callable taking the written bytes, or an exception to raise.
|
|
|
+ """
|
|
|
reader = NFCReader.__new__(NFCReader)
|
|
|
reader._state = NFCState.TAG_PRESENT
|
|
|
reader._current_uid = self.UID.hex().upper()
|
|
|
reader._current_sak = 0x00
|
|
|
nfc = MagicMock()
|
|
|
nfc.reactivate_card.return_value = (self.UID, 0x00)
|
|
|
- if read_error is not None:
|
|
|
- nfc.ntag_read_pages.side_effect = read_error
|
|
|
- else:
|
|
|
- nfc.ntag_read_pages.return_value = cc
|
|
|
- nfc.ntag_write_pages.return_value = True
|
|
|
+ tag = bytearray()
|
|
|
+
|
|
|
+ def write_pages(start_page, data):
|
|
|
+ tag[:] = data + bytes(-len(data) % 4)
|
|
|
+ return True
|
|
|
+
|
|
|
+ def read_pages(start_page, num_pages):
|
|
|
+ if start_page == 3:
|
|
|
+ if read_error is not None:
|
|
|
+ raise read_error
|
|
|
+ return cc
|
|
|
+ if isinstance(read_back, Exception):
|
|
|
+ raise read_back
|
|
|
+ if read_back is not None:
|
|
|
+ return read_back(bytes(tag))
|
|
|
+ return bytes(tag[: num_pages * 4])
|
|
|
+
|
|
|
+ nfc.ntag_read_pages.side_effect = read_pages
|
|
|
+ nfc.ntag_write_pages.side_effect = write_pages
|
|
|
reader._nfc = nfc
|
|
|
return reader, nfc
|
|
|
|
|
|
@@ -92,7 +118,7 @@ class TestWriteNtag:
|
|
|
|
|
|
assert ok
|
|
|
assert "v2" in msg
|
|
|
- nfc.ntag_read_pages.assert_called_once_with(start_page=3, num_pages=1)
|
|
|
+ assert nfc.ntag_read_pages.call_args_list[0].kwargs == {"start_page": 3, "num_pages": 1}
|
|
|
assert self._written(nfc) == TestChooseNdef.V2
|
|
|
assert nfc.ntag_write_pages.call_args.kwargs["start_page"] == 4
|
|
|
|
|
|
@@ -113,11 +139,110 @@ class TestWriteNtag:
|
|
|
assert ok
|
|
|
assert self._written(nfc) == TestChooseNdef.V1
|
|
|
|
|
|
- def test_without_v2_the_tag_is_not_read_first(self):
|
|
|
- reader, nfc = self._reader()
|
|
|
+ def test_without_v2_the_capacity_is_still_checked(self):
|
|
|
+ # #3076: v1 alone is checked against the tag's size too.
|
|
|
+ reader, nfc = self._reader(cc=bytes([0xE1, 0x10, 0x06, 0x00])) # 48 bytes
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1)
|
|
|
+
|
|
|
+ assert not ok
|
|
|
+ assert msg == "Tag too small: 129 bytes to write, 48 bytes of space"
|
|
|
+ nfc.ntag_write_pages.assert_not_called()
|
|
|
+
|
|
|
+ def test_a_tag_without_an_ndef_cc_is_refused(self):
|
|
|
+ # A blank MIFARE Ultralight reports SAK 0x00 like an NTAG, but has no
|
|
|
+ # NDEF Capability Container (#3076).
|
|
|
+ reader, nfc = self._reader(cc=bytes(4))
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert not ok
|
|
|
+ assert "Not an NDEF-formatted NTAG" in msg
|
|
|
+ nfc.ntag_write_pages.assert_not_called()
|
|
|
+
|
|
|
+ def test_v1_that_fits_an_ntag213_is_written(self):
|
|
|
+ reader, nfc = self._reader(cc=bytes([0xE1, 0x10, 0x12, 0x00])) # 144 bytes
|
|
|
|
|
|
ok, _ = reader.write_ntag(TestChooseNdef.V1)
|
|
|
|
|
|
assert ok
|
|
|
- nfc.ntag_read_pages.assert_not_called()
|
|
|
assert self._written(nfc) == TestChooseNdef.V1
|
|
|
+
|
|
|
+
|
|
|
+class TestWriteNtagReadBack:
|
|
|
+ """#3076: the PN5180 can't see the tag's ACK, so the pages are read back."""
|
|
|
+
|
|
|
+ def test_matching_data_is_a_verified_write(self):
|
|
|
+ reader, nfc = TestWriteNtag()._reader()
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert (ok, msg) == (True, "Write successful (OpenTag3D v2)")
|
|
|
+ # Read from page 4 over the whole payload, after a fresh select.
|
|
|
+ assert nfc.ntag_read_pages.call_args_list[-1].kwargs == {"start_page": 4, "num_pages": 48}
|
|
|
+ assert nfc.reactivate_card.call_count == 3
|
|
|
+
|
|
|
+ def test_other_data_fails_the_write_and_names_the_page(self):
|
|
|
+ def corrupt_page_9(written: bytes) -> bytes:
|
|
|
+ data = bytearray(written)
|
|
|
+ data[20] ^= 0xFF # page 4 + 20 // 4
|
|
|
+ return bytes(data)
|
|
|
+
|
|
|
+ reader, _ = TestWriteNtag()._reader(read_back=corrupt_page_9)
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert not ok
|
|
|
+ assert msg == "Verification failed at page 9: the tag holds other data"
|
|
|
+
|
|
|
+ def test_no_answer_to_the_read_back_leaves_it_unverified(self):
|
|
|
+ reader, _ = TestWriteNtag()._reader(read_back=lambda written: None)
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
|
|
|
+
|
|
|
+ def test_a_short_answer_leaves_it_unverified(self):
|
|
|
+ reader, _ = TestWriteNtag()._reader(read_back=lambda written: written[:16])
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
|
|
|
+
|
|
|
+ def test_a_read_error_leaves_it_unverified(self):
|
|
|
+ reader, _ = TestWriteNtag()._reader(read_back=RuntimeError("BUSY timeout"))
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
|
|
|
+
|
|
|
+ def test_no_answer_to_the_reselect_leaves_it_unverified(self):
|
|
|
+ reader, nfc = TestWriteNtag()._reader()
|
|
|
+ nfc.reactivate_card.side_effect = [(TestWriteNtag.UID, 0x00), (TestWriteNtag.UID, 0x00), None]
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
|
|
|
+
|
|
|
+ def test_a_failed_write_is_reported_as_such(self):
|
|
|
+ reader, nfc = TestWriteNtag()._reader()
|
|
|
+ nfc.ntag_write_pages.side_effect = None
|
|
|
+ nfc.ntag_write_pages.return_value = False
|
|
|
+
|
|
|
+ ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
|
|
|
+
|
|
|
+ assert (ok, msg) == (False, "Write failed")
|
|
|
+
|
|
|
+
|
|
|
+class TestCapacityRefusal:
|
|
|
+ def test_an_ntag213_holds_144_bytes(self):
|
|
|
+ cc = bytes([0xE1, 0x10, 0x12, 0x00])
|
|
|
+ assert capacity_refusal(cc, 144) is None
|
|
|
+ assert capacity_refusal(cc, 145) == "Tag too small: 145 bytes to write, 144 bytes of space"
|
|
|
+
|
|
|
+ def test_a_cc_that_is_not_ndef_is_refused(self):
|
|
|
+ assert capacity_refusal(bytes([0x00, 0x00, 0x3E, 0x00]), 10) is not None
|
|
|
+
|
|
|
+ def test_an_unread_cc_proves_nothing(self):
|
|
|
+ assert capacity_refusal(None, 500) is None
|
|
|
+ assert capacity_refusal(b"\xe1", 500) is None
|