Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyscard for openSUSE:Factory checked in at 2025-01-15 17:44:05 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyscard (Old) and /work/SRC/openSUSE:Factory/.python-pyscard.new.1881 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyscard" Wed Jan 15 17:44:05 2025 rev:22 rq:1237933 version:2.2.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyscard/python-pyscard.changes 2024-10-21 16:26:53.310682073 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyscard.new.1881/python-pyscard.changes 2025-01-15 17:44:08.303013652 +0100 @@ -1,0 +2,7 @@ +Tue Jan 14 19:57:53 UTC 2025 - Martin Hauke <mar...@gmx.de> + +- Update to version 2.2.1 + * waitforcardevent(): do not miss events between 2 calls. + * Test, fix, and simplify ATR parsing. + +------------------------------------------------------------------- Old: ---- pyscard-2.2.0.tar.gz New: ---- pyscard-2.2.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyscard.spec ++++++ --- /var/tmp/diff_new_pack.yWb4bf/_old 2025-01-15 17:44:09.323055886 +0100 +++ /var/tmp/diff_new_pack.yWb4bf/_new 2025-01-15 17:44:09.327056051 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pyscard # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2025 SUSE LLC # Copyright (c) 2011 LISA GmbH, Bingen, Germany. # # All modifications and additions to the file contributed by third parties @@ -19,7 +19,7 @@ %define modname pyscard Name: python-pyscard -Version: 2.2.0 +Version: 2.2.1 Release: 0 Summary: Python module adding smart card support License: LGPL-2.0-or-later ++++++ pyscard-2.2.0.tar.gz -> pyscard-2.2.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/ACKS new/pyscard-2.2.1/ACKS --- old/pyscard-2.2.0/ACKS 2024-10-02 21:32:52.000000000 +0200 +++ new/pyscard-2.2.1/ACKS 2024-11-18 14:18:38.000000000 +0100 @@ -11,12 +11,29 @@ Antonio Aranda Frank Aune Michel Beziat +Peter Bittner +Mark Bokil Mattias Brändström +Dave Cahill +Eduardo Castellanos +Norman Denayer +Stefan Droege Luc Duche +Jonathan Giannuzzi +Kevin Griffin Nodir Gulyamov +Max Hausch +Merrick Heley Yong David Huang +Valdur Kana +Kristiine Adam Laurie +Kurt McKee +Elouan Petereau Henryk Plotz +Chris Post Michael Roehner Ludovic Rousseau David Wagner +Harald Welte +Alex Willmer diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/ChangeLog new/pyscard-2.2.1/ChangeLog --- old/pyscard-2.2.0/ChangeLog 2024-10-20 15:57:31.000000000 +0200 +++ new/pyscard-2.2.1/ChangeLog 2025-01-12 15:48:05.000000000 +0100 @@ -1,7 +1,16 @@ +2.2.1 (January 2025) +==================== +- patches from Ludovic Rousseau + * waitforcardevent(): do not miss events between 2 calls + * Use Windows locale to decode Unicode text + * ACKS: add missing contributors +- patches from Kurt McKee + * Test, fix, and simplify ATR parsing + 2.2.0 (October 2024) ==================== - patches from Ludovic Rousseau - * PCSCCardRequest: + * PCSCCardRequest: - handle KeyboardInterrupt in waitforcard() & waitforcardevent() - use a local PC/SC context to avoid locks * smartcard.util.padd(): do NOT modify the input parameter diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/PKG-INFO new/pyscard-2.2.1/PKG-INFO --- old/pyscard-2.2.0/PKG-INFO 2024-10-20 16:07:11.435020200 +0200 +++ new/pyscard-2.2.1/PKG-INFO 2025-01-12 15:49:38.694440400 +0100 @@ -1,9 +1,9 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.2 Name: pyscard -Version: 2.2.0 +Version: 2.2.1 Summary: Smartcard module for Python. Home-page: https://github.com/LudovicRousseau/pyscard -Download-URL: https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.0/pyscard-2.2.0.tar.gz/download +Download-URL: https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.1/pyscard-2.2.1.tar.gz/download Author: Ludovic Rousseau Author-email: ludovic.rouss...@free.fr Platform: linux @@ -25,5 +25,16 @@ Requires-Dist: typing_extensions; python_version == "3.9" Provides-Extra: gui Requires-Dist: wxPython; extra == "gui" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: download-url +Dynamic: home-page +Dynamic: platform +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary Smartcard package for Python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/setup.py new/pyscard-2.2.1/setup.py --- old/pyscard-2.2.0/setup.py 2024-10-20 15:34:20.000000000 +0200 +++ new/pyscard-2.2.1/setup.py 2025-01-12 15:48:05.000000000 +0100 @@ -64,7 +64,7 @@ except: platform_include_dirs = ["/usr/include/PCSC", "/usr/local/include/PCSC"] -VERSION_INFO = (2, 2, 0, 0) +VERSION_INFO = (2, 2, 1, 0) VERSION_STR = "%i.%i.%i" % VERSION_INFO[:3] VERSION_ALT = "%i,%01i,%01i,%04i" % VERSION_INFO diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/src/pyscard.egg-info/PKG-INFO new/pyscard-2.2.1/src/pyscard.egg-info/PKG-INFO --- old/pyscard-2.2.0/src/pyscard.egg-info/PKG-INFO 2024-10-20 16:07:11.000000000 +0200 +++ new/pyscard-2.2.1/src/pyscard.egg-info/PKG-INFO 2025-01-12 15:49:38.000000000 +0100 @@ -1,9 +1,9 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.2 Name: pyscard -Version: 2.2.0 +Version: 2.2.1 Summary: Smartcard module for Python. Home-page: https://github.com/LudovicRousseau/pyscard -Download-URL: https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.0/pyscard-2.2.0.tar.gz/download +Download-URL: https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.1/pyscard-2.2.1.tar.gz/download Author: Ludovic Rousseau Author-email: ludovic.rouss...@free.fr Platform: linux @@ -25,5 +25,16 @@ Requires-Dist: typing_extensions; python_version == "3.9" Provides-Extra: gui Requires-Dist: wxPython; extra == "gui" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: download-url +Dynamic: home-page +Dynamic: platform +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary Smartcard package for Python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/ATR.py new/pyscard-2.2.1/src/smartcard/ATR.py --- old/pyscard-2.2.0/src/smartcard/ATR.py 2024-10-20 15:28:52.000000000 +0200 +++ new/pyscard-2.2.1/src/smartcard/ATR.py 2024-10-20 22:35:42.000000000 +0200 @@ -22,14 +22,22 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +from __future__ import annotations + +import functools +import operator +import warnings + from smartcard.Exceptions import SmartcardException -from smartcard.util import toHexString class ATR: - """ATR class.""" + """Parse and represent Answer to Reset sequences. - clockrateconversion = [ + Answer to Reset sequences are defined in ISO 7816-3, section 8. + """ + + clockrateconversion: list[int | str] = [ 372, 372, 558, @@ -47,7 +55,7 @@ "RFU", "RFU", ] - bitratefactor = [ + bitratefactor: list[int | str] = [ "RFU", 1, 2, @@ -65,21 +73,11 @@ "RFU", "RFU", ] - currenttable = [25, 50, 100, "RFU"] - - def __init__(self, atr): - """Construct a new atr from atr.""" - self.atr = atr - self.__initInstance__() + currenttable: list[int | str] = [25, 50, 100, "RFU"] - def __checksyncbyte__(self): - """Check validity of TS.""" - if not 0x3B == self.atr[0] and not 0x03F == self.atr[0]: - raise SmartcardException("invalid TS 0x%-0.2x" % self.atr[0]) + def __init__(self, atr: list[int]) -> None: + """Parse ATR and initialize members: - def __initInstance__(self): - """ - Parse ATR and initialize members: - TS: initial character - T0: format character - TA[n], TB[n], TC[n], TD[n], for n=0,1,...: protocol parameters @@ -96,7 +94,13 @@ - II: maximum programming current factor - N: extra guard time """ - self.__checksyncbyte__() + + if len(atr) < 2: + raise SmartcardException(f"ATR sequences must be at least 2 bytes long") + if atr[0] not in {0x3B, 0x3F}: + raise SmartcardException(f"invalid TS 0x{atr[0]:02x}") + + self.atr = atr # initial character self.TS = self.atr[0] @@ -108,244 +112,223 @@ self.K = self.T0 & 0x0F # initialize optional characters lists - self.TA = [] - self.TB = [] - self.TC = [] - self.TD = [] - self.Y = [] - self.hasTA = [] - self.hasTB = [] - self.hasTC = [] - self.hasTD = [] - - TD = self.T0 - hasTD = 1 - n = 0 + self.TA: list[None | int] = [] + self.TB: list[None | int] = [] + self.TC: list[None | int] = [] + self.TD: list[None | int] = [] + self.Y: list[int] = [] + + td: None | int = self.T0 offset = 1 - self.interfaceBytesCount = 0 - while hasTD: - self.Y += [TD >> 4 & 0x0F] - - self.hasTD += [(self.Y[n] & 0x08) != 0] - self.hasTC += [(self.Y[n] & 0x04) != 0] - self.hasTB += [(self.Y[n] & 0x02) != 0] - self.hasTA += [(self.Y[n] & 0x01) != 0] + while td is not None: + self.Y.append(td >> 4 & 0x0F) self.TA += [None] self.TB += [None] self.TC += [None] self.TD += [None] - if self.hasTA[n]: - self.TA[n] = self.atr[offset + self.hasTA[n]] - if self.hasTB[n]: - self.TB[n] = self.atr[offset + self.hasTA[n] + self.hasTB[n]] - if self.hasTC[n]: - self.TC[n] = self.atr[ - offset + self.hasTA[n] + self.hasTB[n] + self.hasTC[n] - ] - if self.hasTD[n]: - self.TD[n] = self.atr[ - offset - + self.hasTA[n] - + self.hasTB[n] - + self.hasTC[n] - + self.hasTD[n] - ] - - self.interfaceBytesCount += ( - self.hasTA[n] + self.hasTB[n] + self.hasTC[n] + self.hasTD[n] - ) - TD = self.TD[n] - hasTD = self.hasTD[n] - offset = ( - offset + self.hasTA[n] + self.hasTB[n] + self.hasTC[n] + self.hasTD[n] - ) - n = n + 1 + if self.Y[-1] & 0x01: # TA + offset += 1 + self.TA[-1] = self.atr[offset] + if self.Y[-1] & 0x02: # TB + offset += 1 + self.TB[-1] = self.atr[offset] + if self.Y[-1] & 0x04: # TC + offset += 1 + self.TC[-1] = self.atr[offset] + if self.Y[-1] & 0x08: # TD + offset += 1 + self.TD[-1] = self.atr[offset] + + td = self.TD[-1] + + self.interfaceBytesCount = offset - 1 # historical bytes self.historicalBytes = self.atr[offset + 1 : offset + 1 + self.K] # checksum + self.TCK: int | None = None + self.checksumOK: bool | None = None self.hasChecksum = len(self.atr) == offset + 1 + self.K + 1 if self.hasChecksum: self.TCK = self.atr[-1] - checksum = 0 - for b in self.atr[1:]: - checksum = checksum ^ b - self.checksumOK = checksum == 0 - else: - self.TCK = None + self.checksumOK = functools.reduce(operator.xor, self.atr[1:]) == 0 # clock-rate conversion factor - if self.hasTA[0]: + self.FI: int | None = None + if self.TA[0] is not None: self.FI = self.TA[0] >> 4 & 0x0F - else: - self.FI = None # bit-rate adjustment factor - if self.hasTA[0]: + self.DI: int | None = None + if self.TA[0] is not None: self.DI = self.TA[0] & 0x0F - else: - self.DI = None # maximum programming current factor - if self.hasTB[0]: + self.II: int | None = None + if self.TB[0] is not None: self.II = self.TB[0] >> 5 & 0x03 - else: - self.II = None # programming voltage factor - if self.hasTB[0]: + self.PI1: int | None = None + if self.TB[0] is not None: self.PI1 = self.TB[0] & 0x1F - else: - self.PI1 = None # extra guard time self.N = self.TC[0] - def getChecksum(self): + @property + def hasTA(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TA[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TA[i] is not None`", DeprecationWarning) + return [ta is not None for ta in self.TA] + + @property + def hasTB(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TB[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TB[i] is not None`", DeprecationWarning) + return [tb is not None for tb in self.TB] + + @property + def hasTC(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TC[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TC[i] is not None`", DeprecationWarning) + return [tc is not None for tc in self.TC] + + @property + def hasTD(self) -> list[bool]: + """Deprecated. Replace usage with `ATR.TD[i] is not None`.""" + + warnings.warn("Replace usage with `ATR.TD[i] is not None`", DeprecationWarning) + return [td is not None for td in self.TD] + + def getChecksum(self) -> int | None: """Return the checksum of the ATR. Checksum is mandatory only for T=1.""" return self.TCK - def getHistoricalBytes(self): + def getHistoricalBytes(self) -> list[int]: """Return historical bytes.""" return self.historicalBytes - def getHistoricalBytesCount(self): + def getHistoricalBytesCount(self) -> int: """Return count of historical bytes.""" return len(self.historicalBytes) - def getInterfaceBytesCount(self): + def getInterfaceBytesCount(self) -> int: """Return count of interface bytes.""" return self.interfaceBytesCount - def getTA1(self): + def getTA1(self) -> int | None: """Return TA1 byte.""" return self.TA[0] - def getTB1(self): + def getTB1(self) -> int | None: """Return TB1 byte.""" return self.TB[0] - def getTC1(self): + def getTC1(self) -> int | None: """Return TC1 byte.""" return self.TC[0] - def getTD1(self): + def getTD1(self) -> int | None: """Return TD1 byte.""" return self.TD[0] - def getBitRateFactor(self): + def getBitRateFactor(self) -> int | str: """Return bit rate factor.""" if self.DI is not None: return ATR.bitratefactor[self.DI] - else: - return 1 + return 1 - def getClockRateConversion(self): + def getClockRateConversion(self) -> int | str: """Return clock rate conversion.""" if self.FI is not None: return ATR.clockrateconversion[self.FI] - else: - return 372 + return 372 - def getProgrammingCurrent(self): + def getProgrammingCurrent(self) -> int | str: """Return maximum programming current.""" if self.II is not None: return ATR.currenttable[self.II] - else: - return 50 + return 50 - def getProgrammingVoltage(self): + def getProgrammingVoltage(self) -> int: """Return programming voltage.""" if self.PI1 is not None: return 5 * (1 + self.PI1) - else: - return 5 + return 5 - def getGuardTime(self): + def getGuardTime(self) -> int | None: """Return extra guard time.""" return self.N - def getSupportedProtocols(self): + def getSupportedProtocols(self) -> dict[str, bool]: """Returns a dictionary of supported protocols.""" - protocols = {} + protocols: dict[str, bool] = {} for td in self.TD: if td is not None: - strprotocol = "T=%d" % (td & 0x0F) - protocols[strprotocol] = True - if not self.hasTD[0]: + protocols[f"T={td & 0x0F}"] = True + if self.TD[0] is None: protocols["T=0"] = True return protocols - def isT0Supported(self): + def isT0Supported(self) -> bool: """Return True if T=0 is supported.""" - protocols = self.getSupportedProtocols() - return "T=0" in protocols + return "T=0" in self.getSupportedProtocols() - def isT1Supported(self): + def isT1Supported(self) -> bool: """Return True if T=1 is supported.""" - protocols = self.getSupportedProtocols() - return "T=1" in protocols + return "T=1" in self.getSupportedProtocols() - def isT15Supported(self): + def isT15Supported(self) -> bool: """Return True if T=15 is supported.""" - protocols = self.getSupportedProtocols() - return "T=15" in protocols + return "T=15" in self.getSupportedProtocols() - def dump(self): - """Dump the details of an ATR.""" + def render(self) -> str: + """Render the ATR to a readable format.""" - for i in range(0, len(self.TA)): - if self.TA[i] is not None: - print("TA%d: %x" % (i + 1, self.TA[i])) - if self.TB[i] is not None: - print("TB%d: %x" % (i + 1, self.TB[i])) - if self.TC[i] is not None: - print("TC%d: %x" % (i + 1, self.TC[i])) - if self.TD[i] is not None: - print("TD%d: %x" % (i + 1, self.TD[i])) - - print("supported protocols " + ",".join(self.getSupportedProtocols())) - print("T=0 supported: " + str(self.isT0Supported())) - print("T=1 supported: " + str(self.isT1Supported())) - - if self.getChecksum(): - print("checksum: %d" % self.getChecksum()) - - print("\tclock rate conversion factor: " + str(self.getClockRateConversion())) - print("\tbit rate adjustment factor: " + str(self.getBitRateFactor())) - - print("\tmaximum programming current: " + str(self.getProgrammingCurrent())) - print("\tprogramming voltage: " + str(self.getProgrammingVoltage())) - - print("\tguard time: " + str(self.getGuardTime())) - - print("nb of interface bytes: %d" % self.getInterfaceBytesCount()) - print("nb of historical bytes: %d" % self.getHistoricalBytesCount()) - - def __str__(self): - """Returns a string representation of the ATR as a stream of bytes.""" - return toHexString(self.atr) - - -if __name__ == "__main__": - """Small sample illustrating the use of ATR.""" - - atrs = [ - [0x3F, 0x65, 0x25, 0x00, 0x2C, 0x09, 0x69, 0x90, 0x00], - [0x3F, 0x65, 0x25, 0x08, 0x93, 0x04, 0x6C, 0x90, 0x00], - [0x3B, 0x16, 0x94, 0x7C, 0x03, 0x01, 0x00, 0x00, 0x0D], - [0x3B, 0x65, 0x00, 0x00, 0x9C, 0x11, 0x01, 0x01, 0x03], - [0x3B, 0xE3, 0x00, 0xFF, 0x81, 0x31, 0x52, 0x45, 0xA1, 0xA2, 0xA3, 0x1B], - [0x3B, 0xE5, 0x00, 0x00, 0x81, 0x21, 0x45, 0x9C, 0x10, 0x01, 0x00, 0x80, 0x0D], - ] + lines: list[str] = [] + enumerated_tx_values = enumerate(zip(self.TA, self.TB, self.TC, self.TD), 1) + for i, (ta, tb, tc, td) in enumerated_tx_values: + if ta is not None: + lines.append(f"TA{i}: {ta:x}") + if tb is not None: + lines.append(f"TB{i}: {tb:x}") + if tc is not None: + lines.append(f"TC{i}: {tc:x}") + if td is not None: + lines.append(f"TD{i}: {td:x}") + + lines.append(f"supported protocols {','.join(self.getSupportedProtocols())}") + lines.append(f"T=0 supported: {self.isT0Supported()}") + lines.append(f"T=1 supported: {self.isT1Supported()}") + + if self.getChecksum() is not None: + lines.append(f"checksum: {self.getChecksum()}") + + lines.append(f"\tclock rate conversion factor: {self.getClockRateConversion()}") + lines.append(f"\tbit rate adjustment factor: {self.getBitRateFactor()}") + lines.append(f"\tmaximum programming current: {self.getProgrammingCurrent()}") + lines.append(f"\tprogramming voltage: {self.getProgrammingVoltage()}") + lines.append(f"\tguard time: {self.getGuardTime()}") + lines.append(f"nb of interface bytes: {self.getInterfaceBytesCount()}") + lines.append(f"nb of historical bytes: {self.getHistoricalBytesCount()}") + + return "\n".join(lines) + + def dump(self) -> None: + """Deprecated. Replace usage with `print(ATR.render())`""" + + warnings.warn("Replace usage with `print(ATR.render())`", DeprecationWarning) + print(self.render()) + + def __str__(self) -> str: + """Render the ATR as a space-separated string of uppercase hexadecimal pairs.""" - for atr in atrs: - a = ATR(atr) - print(80 * "-") - print(a) - a.dump() - print(toHexString(a.getHistoricalBytes())) + return bytes(self.atr).hex(" ").upper() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/CardMonitoring.py new/pyscard-2.2.1/src/smartcard/CardMonitoring.py --- old/pyscard-2.2.0/src/smartcard/CardMonitoring.py 2024-10-02 21:32:52.000000000 +0200 +++ new/pyscard-2.2.1/src/smartcard/CardMonitoring.py 2025-01-07 21:43:07.000000000 +0100 @@ -157,7 +157,7 @@ """Runs until stopEvent is notified, and notify observers of all card insertion/removal. """ - self.cardrequest = CardRequest(timeout=10) + self.cardrequest = CardRequest(timeout=60) while self.stopEvent.is_set() != 1: try: currentcards = self.cardrequest.waitforcardevent() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/pcsc/PCSCCardRequest.py new/pyscard-2.2.1/src/smartcard/pcsc/PCSCCardRequest.py --- old/pyscard-2.2.0/src/smartcard/pcsc/PCSCCardRequest.py 2024-10-20 15:29:45.000000000 +0200 +++ new/pyscard-2.2.1/src/smartcard/pcsc/PCSCCardRequest.py 2025-01-07 21:43:07.000000000 +0100 @@ -302,7 +302,6 @@ """Wait for card insertion or removal.""" AbstractCardRequest.waitforcardevent(self) presentcards = [] - readerstates = {} startDate = datetime.now() eventfound = False @@ -310,6 +309,9 @@ previous_readernames = self.getReaderNames() while not eventfound: + # get states from previous run + readerstates = self.readerstates + # reinitialize at each iteration just in case a new reader appeared _readernames = self.getReaderNames() readernames = _readernames @@ -318,35 +320,60 @@ # add PnP special reader readernames.append("\\\\?PnP?\\Notification") - readerstates = {} - for reader in readernames: - # create a dictionary entry for new readers - readerstates[reader] = (reader, SCARD_STATE_UNAWARE) - - hresult, newstates = SCardGetStatusChange( - self.hcontext, 0, list(readerstates.values()) - ) + # first call? + if len(readerstates) == 0: + # init + for reader in readernames: + # create a dictionary entry for new readers + readerstates[reader] = (reader, SCARD_STATE_UNAWARE) + + hresult, newstates = SCardGetStatusChange( + self.hcontext, 0, list(readerstates.values()) + ) # check if a new reader with a card has just been connected - for state in newstates: - readername, eventstate, _ = state + for reader in _readernames: + # is the reader a new one? + if reader not in readerstates: + # create a dictionary entry for new reader + readerstates[reader] = (reader, SCARD_STATE_UNAWARE) + + hresult, newstates = SCardGetStatusChange( + self.hcontext, 0, list(readerstates.values()) + ) + + # added reader is the last one (index is -1) + _, state, _ = newstates[-1] + if state & SCARD_STATE_PRESENT: + eventfound = True - # the reader is a new one - if readername not in previous_readernames: - if eventstate & SCARD_STATE_PRESENT: + # check if a reader has been removed + to_remove = [] + for reader in readerstates: + if reader not in readernames: + _, state = readerstates[reader] + # was the card present? + if state & SCARD_STATE_PRESENT: eventfound = True + to_remove.append(reader) + + if to_remove: + for reader in to_remove: + # remove reader + del readerstates[reader] + + # get newstates with new reader list + hresult, newstates = SCardGetStatusChange( + self.hcontext, 0, list(readerstates.values()) + ) + if eventfound: break - # update previous readers list + # update previous readers list (without PnP special reader) previous_readernames = _readernames - # update readerstate - for state in newstates: - readername, eventstate, atr = state - readerstates[readername] = (readername, eventstate) - # wait for card insertion self.readerstates = readerstates waitThread = threading.Thread(target=self.getStatusChange) @@ -401,31 +428,17 @@ for state in newstates: readername, eventstate, atr = state - # ignore Pnp reader + # ignore PnP reader if readername == "\\\\?PnP?\\Notification": continue - _, oldstate = readerstates[readername] - - # the status can change on a card already inserted, e.g. - # unpowered, in use, ... Clear the state changed bit if - # the card was already inserted and is still inserted - if ( - oldstate & SCARD_STATE_PRESENT - and eventstate & (SCARD_STATE_CHANGED | SCARD_STATE_PRESENT) - == SCARD_STATE_CHANGED | SCARD_STATE_PRESENT - ): - eventstate = eventstate & (0xFFFFFFFF ^ SCARD_STATE_CHANGED) - if eventstate & SCARD_STATE_CHANGED: - if ( - # a reader or a card has been removed - oldstate & SCARD_STATE_PRESENT - or - # a card has been inserted - eventstate & SCARD_STATE_PRESENT - ): - eventfound = True + eventfound = True + + # update readerstates for next SCardGetStatusChange() call + self.readerstates = {} + for reader, state, atr in newstates: + self.readerstates[reader] = (reader, state) # return all the cards present for state in newstates: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/scard/helpers.c new/pyscard-2.2.1/src/smartcard/scard/helpers.c --- old/pyscard-2.2.0/src/smartcard/scard/helpers.c 2024-10-02 21:32:52.000000000 +0200 +++ new/pyscard-2.2.1/src/smartcard/scard/helpers.c 2025-01-07 21:43:07.000000000 +0100 @@ -309,7 +309,7 @@ if( NULL!=source ) { #if (PY_MAJOR_VERSION >= 3) && defined(WIN32) - pystr = PyUnicode_Decode( source, strlen(source), "cp1250" , NULL); + pystr = PyUnicode_DecodeLocale(source, NULL); #else pystr = PyString_FromString( source ); #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyscard-2.2.0/test/test_ATR.py new/pyscard-2.2.1/test/test_ATR.py --- old/pyscard-2.2.0/test/test_ATR.py 2024-10-20 15:28:52.000000000 +0200 +++ new/pyscard-2.2.1/test/test_ATR.py 2024-10-20 22:35:42.000000000 +0200 @@ -1,3 +1,6 @@ +import re +import textwrap + import pytest from smartcard.ATR import ATR @@ -7,142 +10,173 @@ def test_atr1(capsys): atr = [0x3F, 0x65, 0x25, 0x00, 0x2C, 0x09, 0x69, 0x90, 0x00] - data_out = """TB1: 25 -TC1: 0 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 50 -\tprogramming voltage: 30 -\tguard time: 0 -nb of interface bytes: 2 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 25 + TC1: 0 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 50 + \tprogramming voltage: 30 + \tguard time: 0 + nb of interface bytes: 2 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr2(capsys): atr = [0x3F, 0x65, 0x25, 0x08, 0x93, 0x04, 0x6C, 0x90, 0x00] - data_out = """TB1: 25 -TC1: 8 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 50 -\tprogramming voltage: 30 -\tguard time: 8 -nb of interface bytes: 2 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 25 + TC1: 8 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 50 + \tprogramming voltage: 30 + \tguard time: 8 + nb of interface bytes: 2 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() - + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr3(capsys): atr = [0x3B, 0x16, 0x94, 0x7C, 0x03, 0x01, 0x00, 0x00, 0x0D] - data_out = """TA1: 94 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 512 -\tbit rate adjustment factor: 8 -\tmaximum programming current: 50 -\tprogramming voltage: 5 -\tguard time: None -nb of interface bytes: 1 -nb of historical bytes: 6 -""" + data_out = textwrap.dedent( + """\ + TA1: 94 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 512 + \tbit rate adjustment factor: 8 + \tmaximum programming current: 50 + \tprogramming voltage: 5 + \tguard time: None + nb of interface bytes: 1 + nb of historical bytes: 6 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr4(capsys): atr = [0x3B, 0x65, 0x00, 0x00, 0x9C, 0x11, 0x01, 0x01, 0x03] - data_out = """TB1: 0 -TC1: 0 -supported protocols T=0 -T=0 supported: True -T=1 supported: False -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 25 -\tprogramming voltage: 5 -\tguard time: 0 -nb of interface bytes: 2 -nb of historical bytes: 5 -""" + data_out = textwrap.dedent( + """\ + TB1: 0 + TC1: 0 + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 25 + \tprogramming voltage: 5 + \tguard time: 0 + nb of interface bytes: 2 + nb of historical bytes: 5 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr5(capsys): atr = [0x3B, 0xE3, 0x00, 0xFF, 0x81, 0x31, 0x52, 0x45, 0xA1, 0xA2, 0xA3, 0x1B] - data_out = """TB1: 0 -TC1: ff -TD1: 81 -TD2: 31 -TA3: 52 -TB3: 45 -supported protocols T=1 -T=0 supported: False -T=1 supported: True -checksum: 27 -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 25 -\tprogramming voltage: 5 -\tguard time: 255 -nb of interface bytes: 6 -nb of historical bytes: 3 -""" + data_out = textwrap.dedent( + """\ + TB1: 0 + TC1: ff + TD1: 81 + TD2: 31 + TA3: 52 + TB3: 45 + supported protocols T=1 + T=0 supported: False + T=1 supported: True + checksum: 27 + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 25 + \tprogramming voltage: 5 + \tguard time: 255 + nb of interface bytes: 6 + nb of historical bytes: 3 + """ + ) a = ATR(atr) - a.dump() + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out def test_atr6(capsys): atr = [0x3B, 0xE5, 0x00, 0x00, 0x81, 0x21, 0x45, 0x9C, 0x10, 0x01, 0x00, 0x80, 0x0D] - data_out = """TB1: 0 -TC1: 0 -TD1: 81 -TD2: 21 -TB3: 45 -supported protocols T=1 -T=0 supported: False -T=1 supported: True -checksum: 13 -\tclock rate conversion factor: 372 -\tbit rate adjustment factor: 1 -\tmaximum programming current: 25 -\tprogramming voltage: 5 -\tguard time: 0 -nb of interface bytes: 5 -nb of historical bytes: 5 -""" - a = ATR(atr) - a.dump() + data_out = textwrap.dedent( + """\ + TB1: 0 + TC1: 0 + TD1: 81 + TD2: 21 + TB3: 45 + supported protocols T=1 + T=0 supported: False + T=1 supported: True + checksum: 13 + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 25 + \tprogramming voltage: 5 + \tguard time: 0 + nb of interface bytes: 5 + nb of historical bytes: 5 + """ + ) + a = ATR(atr) + with pytest.warns(DeprecationWarning, match=re.escape("print(ATR.render())")): + a.dump() stdout, _ = capsys.readouterr() assert stdout == data_out -def test_atr_ts(): - atr = [0x42] - with pytest.raises(SmartcardException): +@pytest.mark.parametrize( + "ts", + ( + pytest.param("0x42", id="numeric"), + pytest.param("0xaa", id="lowercase"), + pytest.param("0x00", id="zero padding"), + ), +) +def test_invalid_ts(ts: str): + atr = [int(ts[2:], 16), 0x00] + with pytest.raises(SmartcardException, match=f"invalid TS {ts}"): ATR(atr) @@ -172,3 +206,210 @@ """ assert len(getattr(ATR, field)) == expected_length + + +@pytest.mark.parametrize( + "atr,", + ( + pytest.param([], id="ATR is too short (0 bytes)"), + pytest.param([0x3B], id="ATR is too short (1 byte, valid TS)"), + ), +) +def test_invalid_atr_lengths(atr: list[int]): + """Verify that short ATRs raise exceptions.""" + + with pytest.raises(SmartcardException, match="at least 2 bytes"): + ATR(atr) + + +@pytest.mark.parametrize("ts", (0x3B, 0x3F)) +def test_2_bytes(ts): + """Verify that a completely empty ATR parses well.""" + + atr = ATR([ts, 0b0000_0000]) + # |||| `-- no historical bytes + # |||`-- no TA + # ||`-- no TB + # |`-- no TC + # `-- no TD + assert atr.getTA1() is None + assert atr.getTB1() is None + assert atr.II is None + assert atr.PI1 is None + assert atr.getTC1() is None + assert atr.getTD1() is None + assert atr.getChecksum() is None + assert atr.getGuardTime() is None + assert atr.getHistoricalBytesCount() == 0 + assert atr.getHistoricalBytes() == [] + assert atr.getInterfaceBytesCount() == 0 + + # Default values + assert atr.getBitRateFactor() == 1 + assert atr.getClockRateConversion() == 372 + assert atr.getProgrammingCurrent() == 50 + assert atr.getProgrammingVoltage() == 5 + + # Protocols + assert len(atr.getSupportedProtocols()) == 1 + assert "T=0" in atr.getSupportedProtocols() + assert atr.isT0Supported() is True + assert atr.isT1Supported() is False + assert atr.isT15Supported() is False + + # Rendering + expected_rendering = textwrap.dedent( + """\ + supported protocols T=0 + T=0 supported: True + T=1 supported: False + \tclock rate conversion factor: 372 + \tbit rate adjustment factor: 1 + \tmaximum programming current: 50 + \tprogramming voltage: 5 + \tguard time: None + nb of interface bytes: 0 + nb of historical bytes: 0 + """.rstrip() + ) + assert atr.render() == expected_rendering + + # Warnings + with pytest.warns(DeprecationWarning, match="ATR.TA"): + assert atr.hasTA == [False] + with pytest.warns(DeprecationWarning, match="ATR.TB"): + assert atr.hasTB == [False] + with pytest.warns(DeprecationWarning, match="ATR.TC"): + assert atr.hasTC == [False] + with pytest.warns(DeprecationWarning, match="ATR.TD"): + assert atr.hasTD == [False] + + +def test_only_ta1(): + """Verify that TA1 can be conveyed standalone.""" + + atr = ATR([0x3B, 0b0001_0000, 0xA7]) + # `-- only enable TA + assert atr.TA == [0xA7] + assert "TA1: a7\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TA"): + assert atr.hasTA == [True] + # TA1 affects these values + assert atr.getClockRateConversion() == 768 + assert atr.getBitRateFactor() == 64 + # Sanity check + assert atr.TB == atr.TC == atr.TD == [None] + assert atr.N is None + assert atr.getInterfaceBytesCount() == 1 + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_only_tb1(): + """Verify that TB1 can be conveyed standalone. + + TB1 and TB2 are deprecated in ISO 7816-3 2006, so no values are checked here. + """ + + atr = ATR([0x3B, 0b0010_0000, 0b0_10_11111]) + # `-- only enable TB + assert atr.TB == [0b0_10_11111] + assert "TB1: 5f\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TB"): + assert atr.hasTB == [True] + # TB1 affects these values + assert atr.II == 0b10 + assert atr.PI1 == 0b11111 + assert atr.getProgrammingVoltage() != 5 + assert atr.getProgrammingCurrent() != 50 + # Sanity check + assert atr.TA == atr.TC == atr.TD == [None] + assert atr.N is None + assert atr.getInterfaceBytesCount() == 1 + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_only_tc1(): + """Verify that TC1 can be conveyed standalone.""" + + atr = ATR([0x3B, 0b0100_0000, 0xC1]) + # `-- only enable TC + assert atr.TC == [0xC1] + assert "TC1: c1\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TC"): + assert atr.hasTC == [True] + # TC1 affects these values + assert atr.N == 0xC1 + # Sanity check + assert atr.TA == atr.TB == atr.TD == [None] + assert atr.getInterfaceBytesCount() == 1 + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_only_td1(): + """Verify that TD1 can be conveyed standalone.""" + + atr = ATR([0x3B, 0b1000_0000, 0x00]) + # `-- only enable TD + assert atr.TD == [0x00, None] + assert atr.isT0Supported() is True + assert atr.isT1Supported() is False + assert atr.isT15Supported() is False + assert "TD1: 0\n" in atr.render() + with pytest.warns(DeprecationWarning, match="ATR.TD"): + assert atr.hasTD == [True, False] + # Sanity check + assert atr.TA == atr.TB == atr.TC == [None, None] + assert atr.N is None + assert atr.getHistoricalBytesCount() == 0 + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +def test_historical_bytes(): + """Verify that historical bytes can be conveyed standalone.""" + + atr = ATR([0x3B, 0x0F, *list(range(15))]) + # `-- indicate 15 historical bytes + assert atr.K == 15 + assert atr.getHistoricalBytesCount() == 15 + assert atr.getHistoricalBytes() == list(range(15)) + # Sanity check + assert atr.TA == atr.TB == atr.TC == atr.TD == [None] + assert atr.N is None + assert atr.hasChecksum is False + assert atr.checksumOK is None + assert atr.getChecksum() is None + + +@pytest.mark.parametrize("ts", (0x3B, 0x3F)) +@pytest.mark.parametrize("atr_bytes", ([0x00, 0x00], [0x1, 0xFE, 0xFF])) +def test_valid_checksums(ts, atr_bytes): + """Verify behavior of valid checksums.""" + + atr = ATR([ts] + atr_bytes) + assert atr.hasChecksum is True + assert atr.checksumOK is True + assert atr.getChecksum() == atr_bytes[-1] + assert f"checksum: {atr_bytes[-1]}\n" in atr.render() + + +@pytest.mark.parametrize("ts", (0x3B, 0x3F)) +@pytest.mark.parametrize("atr_bytes", ([0x00, 0x01], [0x01, 0xFE, 0x0])) +def test_invalid_checksums(ts, atr_bytes): + """Verify behavior of invalid checksums.""" + + atr = ATR([ts] + atr_bytes) + assert atr.hasChecksum is True + assert atr.checksumOK is False + assert atr.getChecksum() == atr_bytes[-1] + assert f"checksum: {atr_bytes[-1]:x}\n" in atr.render()