// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qpcscmanager_p.h" #include "qpcscslot_p.h" #include "qpcsccard_p.h" #include #include #include QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_NFC_PCSC) static constexpr auto PollIntervalEnvVar = "QT_NFC_POLL_INTERVAL_MS"; static constexpr int DefaultPollIntervalMs = 100; QPcscManager::QPcscManager(QObject *parent) : QObject(parent) { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; int pollInterval = DefaultPollIntervalMs; QByteArray intervalEnv = qgetenv(PollIntervalEnvVar); if (!intervalEnv.isEmpty()) { if (int intervalFromEnv = intervalEnv.toInt(); intervalFromEnv > 0) pollInterval = intervalFromEnv; else qCWarning(QT_NFC_PCSC) << PollIntervalEnvVar << "set to an invalid value"; } m_stateUpdateTimer = new QTimer(this); m_stateUpdateTimer->setInterval(pollInterval); connect(m_stateUpdateTimer, &QTimer::timeout, this, &QPcscManager::onStateUpdate); } QPcscManager::~QPcscManager() { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; if (m_hasContext) { // Destroy all card handles before destroying the PCSC context. for (auto slot : std::as_const(m_slots)) slot->invalidateInsertedCard(); SCardReleaseContext(m_context); } // Stop the worker thread. thread()->quit(); } void QPcscManager::processSlotUpdates() { for (auto &state : m_slotStates) { auto slot = static_cast(state.pvUserData); Q_ASSERT(slot != nullptr); if ((state.dwEventState & SCARD_STATE_UNKNOWN) != 0) continue; if (state.dwEventState == state.dwCurrentState) continue; qCDebug(QT_NFC_PCSC) << Qt::hex << state.dwCurrentState << "=>" << state.dwEventState << ":" << slot->name(); state.dwCurrentState = state.dwEventState; slot->processStateChange(state.dwEventState, m_targetDetectionRunning); } } /* Remove slots that no longer need to be tracked. */ void QPcscManager::removeSlots() { for (auto &state : m_slotStates) { auto slot = static_cast(state.pvUserData); Q_ASSERT(slot != nullptr); // Remove slots that no longer exist, or all slots without cards if // target detection is stopped. if ((state.dwEventState & SCARD_STATE_UNKNOWN) != 0 || !(m_targetDetectionRunning || slot->hasCard())) { qCDebug(QT_NFC_PCSC) << "Removing slot:" << slot; state.dwEventState = SCARD_STATE_UNKNOWN; slot->invalidateInsertedCard(); m_slots.remove(slot->name()); slot->deleteLater(); state.pvUserData = nullptr; } } // Remove state tracking entries for slots that no longer exist. m_slotStates.removeIf( [](const auto &state) { return (state.dwEventState & SCARD_STATE_UNKNOWN) != 0; }); } /* Reads the system slot lists and marks slots that no longer exists, also creates new slot entries if target detection is currently running. */ void QPcscManager::updateSlotList() { Q_ASSERT(m_hasContext); #ifndef SCARD_AUTOALLOCATE // macOS does not support automatic allocation. Try using a fixed-size // buffer first, extending it if it is not sufficient. #define LIST_READER_BUFFER_EXTRA 1024 QPcscSlotName buf(nullptr); DWORD listSize = LIST_READER_BUFFER_EXTRA; buf.resize(listSize); QPcscSlotName::Ptr list = buf.ptr(); auto ret = SCardListReaders(m_context, nullptr, list, &listSize); #else QPcscSlotName::Ptr list; DWORD listSize = SCARD_AUTOALLOCATE; auto ret = SCardListReaders(m_context, nullptr, reinterpret_cast(&list), &listSize); #endif if (ret == LONG(SCARD_E_NO_READERS_AVAILABLE)) { list = nullptr; ret = SCARD_S_SUCCESS; } #ifndef SCARD_AUTOALLOCATE else if (ret == LONG(SCARD_E_INSUFFICIENT_BUFFER)) { // SCardListReaders() has set listSize to the required size. We add // extra space to reduce possibility of failure if the reader list has // changed since the last call. listSize += LIST_READER_BUFFER_EXTRA; buf.resize(listSize); list = buf.ptr(); ret = SCardListReaders(m_context, nullptr, list, &listSize); if (ret == LONG(SCARD_E_NO_READERS_AVAILABLE)) { list = nullptr; ret = SCARD_S_SUCCESS; } } #undef LIST_READER_BUFFER_EXTRA #endif if (ret != SCARD_S_SUCCESS) { qCDebug(QT_NFC_PCSC) << "Failed to list readers:" << QPcsc::errorMessage(ret); return; } #ifdef SCARD_AUTOALLOCATE auto freeList = qScopeGuard([this, list] { if (list) { Q_ASSERT(m_hasContext); SCardFreeMemory(m_context, list); } }); #endif QSet presentSlots; if (list != nullptr) { for (const auto *p = list; *p; p += QPcscSlotName::nameSize(p) + 1) presentSlots.insert(QPcscSlotName(p)); } // Check current state list and mark slots that are not present anymore to // be removed later. for (auto &state : m_slotStates) { auto slot = static_cast(state.pvUserData); Q_ASSERT(slot != nullptr); if (presentSlots.contains(slot->name())) presentSlots.remove(slot->name()); else state.dwEventState = SCARD_STATE_UNKNOWN; } if (!m_targetDetectionRunning) return; // Add new slots for (auto &&slotName : std::as_const(presentSlots)) { QPcscSlot *slot = new QPcscSlot(slotName, this); qCDebug(QT_NFC_PCSC) << "New slot:" << slot; m_slots[slotName] = slot; SCARD_READERSTATE state {}; state.pvUserData = slot; state.szReader = slot->name().ptr(); state.dwCurrentState = SCARD_STATE_UNAWARE; m_slotStates.append(state); } } bool QPcscManager::establishContext() { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; Q_ASSERT(!m_hasContext); LONG ret = SCardEstablishContext(SCARD_SCOPE_USER, nullptr, nullptr, &m_context); if (ret != SCARD_S_SUCCESS) { qCWarning(QT_NFC_PCSC) << "Failed to establish context:" << QPcsc::errorMessage(ret); return false; } m_hasContext = true; return true; } void QPcscManager::onStateUpdate() { if (!m_hasContext) { if (!m_targetDetectionRunning) { m_stateUpdateTimer->stop(); return; } if (!establishContext()) return; } updateSlotList(); removeSlots(); if (m_slotStates.isEmpty()) { if (!m_targetDetectionRunning) { // Free the context if it is no longer needed to card tracking. SCardReleaseContext(m_context); m_hasContext = false; m_stateUpdateTimer->stop(); } return; } // Both Windows and PCSCLite support interruptable continuos state detection // where SCardCancel() can be used to abort it from another thread. // But that introduces a problem of reliable cancelling of the status change // call and no other. Reliable use of SCardCancel() would probably require // some form of synchronization and polling, and would make the code much // more complicated. // Alternatively, the state detection code could run in a yet another thread // that will not need to be cancelled too often. // For simplicity, the current code just checks for status changes every // second. LONG ret = SCardGetStatusChange(m_context, 0, m_slotStates.data(), m_slotStates.size()); if (ret == SCARD_S_SUCCESS || ret == LONG(SCARD_E_UNKNOWN_READER)) { processSlotUpdates(); removeSlots(); } else if (ret == LONG(SCARD_E_CANCELLED) || ret == LONG(SCARD_E_UNKNOWN_READER) || ret == LONG(SCARD_E_TIMEOUT)) { /* ignore */ } else { qCWarning(QT_NFC_PCSC) << "SCardGetStatusChange failed:" << QPcsc::errorMessage(ret); // Unknown failure. It is likely that the current context will not // recover from it, so destroy it and try to create a new context at the // next iteration. Q_ASSERT(m_hasContext); m_hasContext = false; for (auto slot : std::as_const(m_slots)) { slot->invalidateInsertedCard(); slot->deleteLater(); } SCardReleaseContext(m_context); m_slots.clear(); m_slotStates.clear(); } } void QPcscManager::onStartTargetDetectionRequest(QNearFieldTarget::AccessMethod accessMethod) { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; m_requestedMethod = accessMethod; if (m_targetDetectionRunning) return; m_targetDetectionRunning = true; m_stateUpdateTimer->start(); } void QPcscManager::onStopTargetDetectionRequest() { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; m_targetDetectionRunning = false; } QPcscCard *QPcscManager::connectToCard(QPcscSlot *slot) { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; Q_ASSERT(slot != nullptr); Q_ASSERT(m_hasContext); SCARDHANDLE cardHandle; DWORD activeProtocol; LONG ret = SCardConnect(m_context, slot->name().ptr(), SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &cardHandle, &activeProtocol); if (ret != SCARD_S_SUCCESS) { qCDebug(QT_NFC_PCSC) << "Failed to connect to card:" << QPcsc::errorMessage(ret); retryCardDetection(slot); return nullptr; } auto card = new QPcscCard(cardHandle, activeProtocol, this); auto uid = card->readUid(); auto maxInputLength = card->readMaxInputLength(); QNearFieldTarget::AccessMethods accessMethods = QNearFieldTarget::TagTypeSpecificAccess; if (card->supportsNdef()) accessMethods |= QNearFieldTarget::NdefAccess; if (m_requestedMethod != QNearFieldTarget::UnknownAccess && (accessMethods & m_requestedMethod) == 0) { qCDebug(QT_NFC_PCSC) << "Dropping card without required access support"; card->deleteLater(); return nullptr; } if (!card->isValid()) { qCDebug(QT_NFC_PCSC) << "Card became invalid"; card->deleteLater(); retryCardDetection(slot); return nullptr; } Q_EMIT cardInserted(card, uid, accessMethods, maxInputLength); return card; } /* Setup states list so that the card detection for the given slot will be retried on the next iteration. This is useful to try to get cards working after reset. */ void QPcscManager::retryCardDetection(const QPcscSlot *slot) { qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO; for (auto &state : m_slotStates) { if (state.pvUserData == slot) { state.dwCurrentState = SCARD_STATE_UNAWARE; break; } } } QT_END_NAMESPACE