Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyasn1 for openSUSE:Factory checked in at 2026-11-08 19:21:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyasn1 (Old) and /work/SRC/openSUSE:Factory/.python-pyasn1.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyasn1" Sun Nov 8 19:21:22 2026 rev:51 rq:1395838 version:0.6.5 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyasn1/python-pyasn1.changes 2026-07-20 09:59:46.216998802 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyasn1.new.3759290/python-pyasn1.changes 2026-11-08 19:21:30.045677145 +0100 @@ -1,0 +2,19 @@ +Fri Oct 30 12:25:15 UTC 2026 - Robert Schweikert <[email protected]> + +- Update to 0.6.5 (bsc#1288851, CVE-2026-119194) + * GHSA-mm57-4v3p-rchx: Decoded binary REAL mantissas in linear time + instead of repeatedly shifting a growing integer and slicing the + remaining bytes (thanks for reporting, manus-use, manus-pi, ucsurf, + adriank31) + * GHSA-2qm8-x737-gmw5: Reassembled constructed BIT STRING fragments + in linear time while preserving their decoded bit values, including + intermediate fragment padding (thanks for reporting, manus-use) + * GHSA-9m75-vc3g-4x39: Reassembled constructed OCTET STRING and + character-string fragments and indefinite-length ANY content in + linear time. Nested constructed OCTET STRING fragments now contribute + their decoded octets (thanks for reporting, alimony, manus-pi, adriank31) + * GHSA-fm9c-5rcf-2q5p: Bounded schemaless container type tracking + after two distinct tag sets to prevent quadratic CPU use from + colliding long tag IDs (thanks for reporting, droideck) + +------------------------------------------------------------------- Old: ---- pyasn1-0.6.4.tar.gz New: ---- pyasn1-0.6.5.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyasn1.spec ++++++ --- /var/tmp/diff_new_pack.aiQ98r/_old 2026-11-08 19:21:30.757706600 +0100 +++ /var/tmp/diff_new_pack.aiQ98r/_new 2026-11-08 19:21:30.759706683 +0100 @@ -1,6 +1,7 @@ # # spec file for package python-pyasn1 # +# Copyright (c) 2026 SUSE LLC # Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties @@ -18,7 +19,7 @@ %{?sle15_python_module_pythons} Name: python-pyasn1 -Version: 0.6.4 +Version: 0.6.5 Release: 0 Summary: ASN.1 types and codecs License: BSD-2-Clause ++++++ pyasn1-0.6.4.tar.gz -> pyasn1-0.6.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/CHANGES.rst new/pyasn1-0.6.5/CHANGES.rst --- old/pyasn1-0.6.4/CHANGES.rst 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/CHANGES.rst 2026-10-27 07:32:26.836591500 +0100 @@ -1,3 +1,21 @@ +Revision 0.6.5, released 26-10-2026 +--------------------------------------- + +- GHSA-mm57-4v3p-rchx: Decoded binary REAL mantissas in linear time + instead of repeatedly shifting a growing integer and slicing the + remaining bytes (thanks for reporting, manus-use, manus-pi, ucsurf, + adriank31) +- GHSA-2qm8-x737-gmw5: Reassembled constructed BIT STRING fragments + in linear time while preserving their decoded bit values, including + intermediate fragment padding (thanks for reporting, manus-use) +- GHSA-9m75-vc3g-4x39: Reassembled constructed OCTET STRING and + character-string fragments and indefinite-length ANY content in + linear time. Nested constructed OCTET STRING fragments now contribute + their decoded octets (thanks for reporting, alimony, manus-pi, adriank31) +- GHSA-fm9c-5rcf-2q5p: Bounded schemaless container type tracking + after two distinct tag sets to prevent quadratic CPU use from + colliding long tag IDs (thanks for reporting, droideck) + Revision 0.6.4, released 08-07-2026 --------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/PKG-INFO new/pyasn1-0.6.5/PKG-INFO --- old/pyasn1-0.6.4/PKG-INFO 2026-07-09 03:12:22.895374300 +0200 +++ new/pyasn1-0.6.5/PKG-INFO 2026-10-27 07:32:38.123992200 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pyasn1 -Version: 0.6.4 +Version: 0.6.5 Summary: Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208) Author-email: Ilya Etingof <[email protected]> Maintainer: pyasn1 maintenance organization diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/pyasn1/__init__.py new/pyasn1-0.6.5/pyasn1/__init__.py --- old/pyasn1-0.6.4/pyasn1/__init__.py 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/pyasn1/__init__.py 2026-10-27 07:32:26.839507800 +0100 @@ -1,2 +1,2 @@ # https://www.python.org/dev/peps/pep-0396/ -__version__ = '0.6.4' +__version__ = '0.6.5' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/pyasn1/codec/ber/decoder.py new/pyasn1-0.6.5/pyasn1/codec/ber/decoder.py --- old/pyasn1-0.6.4/pyasn1/codec/ber/decoder.py 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/pyasn1/codec/ber/decoder.py 2026-10-27 07:32:26.839731500 +0100 @@ -174,6 +174,44 @@ protoComponent = univ.BitString(()) supportConstructedForm = True + @staticmethod + def _appendFragment(assembled, component, bitLength): + trailingBits = component[0] + if trailingBits > 7: + raise error.PyAsn1Error( + 'Trailing bits overflow %s' % trailingBits + ) + + payload = component[1:] + if not payload: + if trailingBits: + raise error.PyAsn1Error('Trailing bits overflow %s' % trailingBits) + return bitLength + + if not trailingBits and not bitLength % 8: + assembled.extend(payload) + return bitLength + len(payload) * 8 + + for idx, byte in enumerate(payload): + significantBits = 8 + if idx == len(payload) - 1: + significantBits -= trailingBits + byte >>= trailingBits + + usedBits = bitLength % 8 + if not usedBits: + assembled.append(byte << (8 - significantBits)) + elif usedBits + significantBits <= 8: + assembled[-1] |= byte << (8 - usedBits - significantBits) + else: + remainingBits = usedBits + significantBits - 8 + assembled[-1] |= byte >> remainingBits + assembled.append((byte << (8 - remainingBits)) & 0xff) + + bitLength += significantBits + + return bitLength + def valueDecoder(self, substrate, asn1Spec, tagSet=None, length=None, state=None, decodeFun=None, substrateFun=None, @@ -230,7 +268,8 @@ # All inner fragments are of the same type, treat them as octet string substrateFun = self.substrateCollector - bitString = self.protoComponent.fromOctetString(b'', internalFormat=True) + assembled = bytearray() + bitLength = 0 current_position = substrate.tell() @@ -241,16 +280,11 @@ if isinstance(component, SubstrateUnderrunError): yield component - trailingBits = component[0] - if trailingBits > 7: - raise error.PyAsn1Error( - 'Trailing bits overflow %s' % trailingBits - ) + bitLength = self._appendFragment(assembled, component, bitLength) - bitString = self.protoComponent.fromOctetString( - component[1:], internalFormat=True, - prepend=bitString, padding=trailingBits - ) + bitString = self.protoComponent.fromOctetString( + assembled, internalFormat=True, padding=(-bitLength) % 8 + ) yield self._createComponent(asn1Spec, tagSet, bitString, **options) @@ -270,7 +304,8 @@ # All inner fragments are of the same type, treat them as octet string substrateFun = self.substrateCollector - bitString = self.protoComponent.fromOctetString(b'', internalFormat=True) + assembled = bytearray() + bitLength = 0 while True: # loop over fragments @@ -287,16 +322,11 @@ if component is eoo.endOfOctets: break - trailingBits = component[0] - if trailingBits > 7: - raise error.PyAsn1Error( - 'Trailing bits overflow %s' % trailingBits - ) + bitLength = self._appendFragment(assembled, component, bitLength) - bitString = self.protoComponent.fromOctetString( - component[1:], internalFormat=True, - prepend=bitString, padding=trailingBits - ) + bitString = self.protoComponent.fromOctetString( + assembled, internalFormat=True, padding=(-bitLength) % 8 + ) yield self._createComponent(asn1Spec, tagSet, bitString, **options) @@ -332,23 +362,20 @@ if LOG: LOG('assembling constructed serialization') - # All inner fragments are of the same type, treat them as octet string - substrateFun = self.substrateCollector - - header = b'' + # Assemble the logical value of each fragment, including nested ones. + fragments = [] original_position = substrate.tell() # head = popSubstream(substrate, length) while substrate.tell() - original_position < length: for component in decodeFun( - substrate, self.protoComponent, substrateFun=substrateFun, - **options): + substrate, self.protoComponent, **options): if isinstance(component, SubstrateUnderrunError): yield component - header += component + fragments.append(bytes(component)) - yield self._createComponent(asn1Spec, tagSet, header, **options) + yield self._createComponent(asn1Spec, tagSet, b''.join(fragments), **options) def indefLenValueDecoder(self, substrate, asn1Spec, tagSet=None, length=None, state=None, @@ -362,16 +389,13 @@ return - # All inner fragments are of the same type, treat them as octet string - substrateFun = self.substrateCollector - - header = b'' + # Assemble the logical value of each fragment, including nested ones. + fragments = [] while True: # loop over fragments for component in decodeFun( - substrate, self.protoComponent, substrateFun=substrateFun, - allowEoo=True, **options): + substrate, self.protoComponent, allowEoo=True, **options): if isinstance(component, SubstrateUnderrunError): yield component @@ -382,9 +406,9 @@ if component is eoo.endOfOctets: break - header += component + fragments.append(bytes(component)) - yield self._createComponent(asn1Spec, tagSet, header, **options) + yield self._createComponent(asn1Spec, tagSet, b''.join(fragments), **options) class NullPayloadDecoder(AbstractSimplePayloadDecoder): @@ -587,12 +611,7 @@ elif b == 2: # encbase = 16 e *= 4 - p = 0 - - while chunk: # value - p <<= 8 - p |= chunk[0] - chunk = chunk[1:] + p = int.from_bytes(bytes(chunk), 'big') if fo & 0x40: # sign bit p = -p @@ -676,7 +695,10 @@ break components.append(component) - componentTypes.add(component.tagSet) + # Two distinct types settle the choice; keep the set bounded + # to avoid quadratic comparisons for colliding tag hashes. + if len(componentTypes) < 2: + componentTypes.add(component.tagSet) # Now we have to guess is it SEQUENCE/SET or SEQUENCE OF/SET OF # The heuristics is: @@ -1406,6 +1428,8 @@ # All inner fragments are of the same type, treat them as octet string substrateFun = self.substrateCollector + fragments = [chunk] + while True: # loop over fragments for component in decodeFun( @@ -1421,7 +1445,9 @@ if component is eoo.endOfOctets: break - chunk += component + fragments.append(component) + + chunk = b''.join(fragments) if substrateFun: yield chunk # TODO: Weird diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/pyasn1.egg-info/PKG-INFO new/pyasn1-0.6.5/pyasn1.egg-info/PKG-INFO --- old/pyasn1-0.6.4/pyasn1.egg-info/PKG-INFO 2026-07-09 03:12:22.000000000 +0200 +++ new/pyasn1-0.6.5/pyasn1.egg-info/PKG-INFO 2026-10-27 07:32:38.096046000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pyasn1 -Version: 0.6.4 +Version: 0.6.5 Summary: Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208) Author-email: Ilya Etingof <[email protected]> Maintainer: pyasn1 maintenance organization diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/pyasn1.egg-info/SOURCES.txt new/pyasn1-0.6.5/pyasn1.egg-info/SOURCES.txt --- old/pyasn1-0.6.4/pyasn1.egg-info/SOURCES.txt 2026-07-09 03:12:22.000000000 +0200 +++ new/pyasn1-0.6.5/pyasn1.egg-info/SOURCES.txt 2026-10-27 07:32:38.107046100 +0100 @@ -124,6 +124,7 @@ tests/test_debug.py tests/codec/__init__.py tests/codec/__main__.py +tests/codec/test_decoder_schemaless.py tests/codec/test_streaming.py tests/codec/ber/__init__.py tests/codec/ber/__main__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/tests/codec/__main__.py new/pyasn1-0.6.5/tests/codec/__main__.py --- old/pyasn1-0.6.4/tests/codec/__main__.py 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/tests/codec/__main__.py 2026-10-27 07:32:26.841827600 +0100 @@ -8,6 +8,7 @@ suite = unittest.TestLoader().loadTestsFromNames( ['tests.codec.test_streaming.suite', + 'tests.codec.test_decoder_schemaless.suite', 'tests.codec.ber.__main__.suite', 'tests.codec.cer.__main__.suite', 'tests.codec.der.__main__.suite', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/tests/codec/ber/test_decoder.py new/pyasn1-0.6.5/tests/codec/ber/test_decoder.py --- old/pyasn1-0.6.4/tests/codec/ber/test_decoder.py 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/tests/codec/ber/test_decoder.py 2026-10-27 07:32:26.841891300 +0100 @@ -168,6 +168,27 @@ bytes((35, 128, 3, 2, 0, 169, 3, 2, 1, 138, 0, 0)) ) == ((1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1), b'') + def testChunkedWithIntermediatePadding(self): + fragments = (b'\x03\x02\x03\x10' + b'\x03\x01\x00' + + b'\x03\x02\x05\xa0' + b'\x03\x02\x01\x81') + expected = univ.BitString(binValue='000101011000000') + + for substrate in (b'\x23' + encode_length(len(fragments)) + fragments, + b'\x23\x80' + fragments + b'\x00\x00'): + for asn1Spec in (None, univ.BitString()): + value, rest = decoder.decode(substrate, asn1Spec=asn1Spec) + assert value == expected + assert len(value) == len(expected) + assert rest == b'' + + def testManyDefModeFragments(self): + fragments = b'\x03\x02\x00\x81' * 256 + substrate = b'\x23' + encode_length(len(fragments)) + fragments + value, rest = decoder.decode(substrate) + + assert value == univ.BitString.fromOctetString(b'\x81' * 256) + assert rest == b'' + def testDefModeChunkedSubst(self): assert decoder.decode( bytes((35, 8, 3, 2, 0, 169, 3, 2, 1, 138)), @@ -223,6 +244,32 @@ bytes((36, 128, 4, 4, 81, 117, 105, 99, 4, 4, 107, 32, 98, 114, 4, 4, 111, 119, 110, 32, 4, 3, 102, 111, 120, 0, 0)) ) == (b'Quick brown fox', b'') + def testManyFragments(self): + fragments = b'\x04\x02AB' * 256 + + for substrate in (b'\x24' + encode_length(len(fragments)) + fragments, + b'\x24\x80' + fragments + b'\x00\x00'): + for asn1Spec in (None, univ.OctetString()): + value, rest = decoder.decode(substrate, asn1Spec=asn1Spec) + assert value.asOctets() == b'AB' * 256 + assert rest == b'' + + def testNestedIndefModeChunked(self): + substrate = bytes.fromhex('24 07 24 80 04 01 41 00 00') + + for asn1Spec in (None, univ.OctetString()): + value, rest = decoder.decode(substrate, asn1Spec=asn1Spec) + assert value.asOctets() == b'A' + assert rest == b'' + + def testNestedDefModeChunked(self): + substrate = bytes.fromhex('24 05 24 03 04 01 41') + + for asn1Spec in (None, univ.OctetString()): + value, rest = decoder.decode(substrate, asn1Spec=asn1Spec) + assert value.asOctets() == b'A' + assert rest == b'' + def testDefModeChunkedSubst(self): assert decoder.decode( bytes( @@ -765,6 +812,16 @@ assert tuple(value) == (-1, 2, 76354972) assert rest == b'' + def testLongBinaryMantissa(self): + mantissa = b'\x80' + b'\x01' * 511 + content = b'\x80\x00' + mantissa + substrate = b'\x09' + encode_length(len(content)) + content + + for asn1Spec in (None, univ.Real()): + value, rest = decoder.decode(substrate, asn1Spec=asn1Spec) + assert tuple(value) == (int.from_bytes(mantissa, 'big'), 2, 0) + assert rest == b'' + def testLongFormBinaryRealExponentLength(self): value, rest = decoder.decode( bytes((9, 6, 0x83, 3, 0x0f, 0x42, 0x40, 1)) @@ -1845,6 +1902,14 @@ bytes((4, 3, 102, 111, 120)), asn1Spec=self.s ) == (univ.Any('\004\003fox'), b'') + def testManyIndefiniteLengthComponents(self): + fragments = b'\x04\x01A' * 128 + substrate = b'\x30\x80' + fragments + b'\x00\x00' + value, rest = decoder.decode(substrate, asn1Spec=self.s) + + assert value == b'\x30\x80' + fragments + assert rest == b'' + def testTaggedExIndefMode(self): s = univ.Any('\004\003fox').subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 4)) assert decoder.decode(bytes((164, 128, 4, 3, 102, 111, 120, 0, 0)), asn1Spec=s) == (s, b'') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/tests/codec/cer/test_decoder.py new/pyasn1-0.6.5/tests/codec/cer/test_decoder.py --- old/pyasn1-0.6.4/tests/codec/cer/test_decoder.py 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/tests/codec/cer/test_decoder.py 2026-10-27 07:32:26.842228200 +0100 @@ -49,6 +49,17 @@ bytes((3, 127, 6) + (170,) * 125 + (128,)) ) == (((1, 0) * 501), b'') + def testChunkedWithIntermediatePadding(self): + fragments = b'\x03\x02\x03\x10\x03\x02\x00\xa5' + expected = univ.BitString(binValue='0001010100101') + + for substrate in (b'\x23\x08' + fragments, + b'\x23\x80' + fragments + b'\x00\x00'): + value, rest = decoder.decode(substrate, asn1Spec=univ.BitString()) + assert value == expected + assert len(value) == len(expected) + assert rest == b'' + # TODO: test failures on short chunked and long unchunked substrate samples @@ -63,6 +74,16 @@ bytes((36, 128, 4, 130, 3, 232) + (81,) * 1000 + (4, 1, 81, 0, 0)) ) == (b'Q' * 1001, b'') + def testManyFragments(self): + fragments = b'\x04\x01A' * 64 + definite = b'\x24\x81\xc0' + fragments + indefinite = b'\x24\x80' + fragments + b'\x00\x00' + + for substrate in (definite, indefinite): + value, rest = decoder.decode(substrate, asn1Spec=univ.OctetString()) + assert value.asOctets() == b'A' * 64 + assert rest == b'' + # TODO: test failures on short chunked and long unchunked substrate samples @@ -89,6 +110,25 @@ assert tuple(value) == (-1, 2, 76354972) assert rest == b'' + def testLongBinaryMantissa(self): + mantissa = b'\x80' + b'\x01' * 255 + content = b'\x80\x00' + mantissa + substrate = b'\x09\x82\x01\x02' + content + value, rest = decoder.decode(substrate) + + assert tuple(value) == (int.from_bytes(mantissa, 'big'), 2, 0) + assert rest == b'' + + +class AnyDecoderTestCase(BaseTestCase): + def testManyIndefiniteLengthComponents(self): + fragments = b'\x04\x01A' * 64 + substrate = b'\x30\x80' + fragments + b'\x00\x00' + value, rest = decoder.decode(substrate, asn1Spec=univ.Any()) + + assert value == b'\x30\x80' + fragments + assert rest == b'' + class SequenceDecoderWithUntaggedOpenTypesTestCase(BaseTestCase): def setUp(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/tests/codec/der/test_decoder.py new/pyasn1-0.6.5/tests/codec/der/test_decoder.py --- old/pyasn1-0.6.4/tests/codec/der/test_decoder.py 2026-07-09 03:12:14.000000000 +0200 +++ new/pyasn1-0.6.5/tests/codec/der/test_decoder.py 2026-10-27 07:32:26.842596000 +0100 @@ -9,6 +9,7 @@ from tests.base import BaseTestCase +from pyasn1.type import char from pyasn1.type import tag from pyasn1.type import namedtype from pyasn1.type import opentype @@ -44,6 +45,15 @@ else: assert 0, 'chunked encoding tolerated' + def testSchemaDrivenChunkedWithIntermediatePadding(self): + fragments = b'\x03\x02\x03\x10\x03\x02\x00\xa5' + value, rest = decoder.decode(b'\x23\x08' + fragments, + asn1Spec=univ.BitString()) + + assert value == univ.BitString(binValue='0001010100101') + assert len(value) == 13 + assert rest == b'' + class OctetStringDecoderTestCase(BaseTestCase): def testShortMode(self): @@ -71,6 +81,21 @@ else: assert 0, 'chunked encoding tolerated' + def testSchemaDrivenManyFragments(self): + fragments = b'\x04\x01A' * 64 + value, rest = decoder.decode(b'\x24\x81\xc0' + fragments, + asn1Spec=univ.OctetString()) + + assert value.asOctets() == b'A' * 64 + assert rest == b'' + + def testConstructedCharacterString(self): + fragments = b'\x0c\x01A' * 64 + value, rest = decoder.decode(b'\x2c\x81\xc0' + fragments) + + assert value == char.UTF8String(b'A' * 64) + assert rest == b'' + class LargeTagDecoderTestCase(BaseTestCase): def testExcessiveLongTag(self): @@ -104,6 +129,15 @@ assert tuple(value) == (-1, 2, 76354972) assert rest == b'' + def testLongBinaryMantissa(self): + mantissa = b'\x80' + b'\x01' * 255 + content = b'\x80\x00' + mantissa + substrate = b'\x09\x82\x01\x02' + content + value, rest = decoder.decode(substrate, asn1Spec=univ.Real()) + + assert tuple(value) == (int.from_bytes(mantissa, 'big'), 2, 0) + assert rest == b'' + class SequenceDecoderWithUntaggedOpenTypesTestCase(BaseTestCase): def setUp(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.4/tests/codec/test_decoder_schemaless.py new/pyasn1-0.6.5/tests/codec/test_decoder_schemaless.py --- old/pyasn1-0.6.4/tests/codec/test_decoder_schemaless.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pyasn1-0.6.5/tests/codec/test_decoder_schemaless.py 2026-10-27 07:32:26.842784400 +0100 @@ -0,0 +1,184 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2026, Simon Pichugin <[email protected]> +# License: https://pyasn1.readthedocs.io/en/latest/license.html +# +import io +import sys +import unittest +from itertools import product + +from tests.base import BaseTestCase + +from pyasn1.codec.ber import decoder as ber_decoder +from pyasn1.codec.cer import decoder as cer_decoder +from pyasn1.codec.der import decoder as der_decoder +from pyasn1.error import SubstrateUnderrunError +from pyasn1.type import tag +from pyasn1.type import univ + + +class SchemalessContainerDecoderTestCase(BaseTestCase): + codecs = ( + ('ber-def', ber_decoder, False), + ('ber-indef', ber_decoder, True), + ('cer-def', cer_decoder, False), + ('cer-indef', cer_decoder, True), + ('der', der_decoder, False), + ) + containers = ( + (0x30, univ.SequenceOf, univ.Sequence), + (0x31, univ.SetOf, univ.Set), + ) + + def _encodeLength(self, length): + if length < 128: + return bytes([length]) + + octets = length.to_bytes((length.bit_length() + 7) // 8, 'big') + return bytes([0x80 | len(octets)]) + octets + + def _encodeConstructed(self, identifier, body, indefinite): + if indefinite: + return identifier + b'\x80' + body + b'\x00\x00' + + return identifier + self._encodeLength(len(body)) + body + + def _substrate(self, containerTag, tagIds, indefinite, definiteTags=False): + components = [] + for idx, tagId in enumerate(tagIds): + octets = [tagId & 0x7f] + tagId >>= 7 + while tagId: + octets.append((tagId & 0x7f) | 0x80) + tagId >>= 7 + + # Long-form context tag, explicitly wrapping a one-octet value. + identifier = b'\xbf' + bytes(reversed(octets)) + body = b'\x04\x01' + bytes([idx % 256]) + components.append(self._encodeConstructed( + identifier, body, indefinite and not definiteTags)) + + return self._encodeConstructed( + bytes([containerTag]), b''.join(components), indefinite) + + def _tagSet(self, tagId): + return univ.OctetString.tagSet.tagExplicitly( + tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, tagId)) + + def _assertValue(self, value, expectedType, tagIds): + assert type(value) is expectedType + assert len(value) == len(tagIds) + for idx, tagId in enumerate(tagIds): + component = value.getComponentByPosition(idx) + assert type(component) is univ.OctetString + assert component.asOctets() == bytes([idx % 256]) + assert component.tagSet == self._tagSet(tagId) + + def testHomogeneous(self): + for codec, container, tagIds in product( + self.codecs, self.containers, + ((32,), (32, 32, 32), (32 + sys.hash_info.modulus,) * 3)): + name, decoder, indefinite = codec + containerTag, sequenceType, recordType = container + with self.subTest(codec=name, container=sequenceType, tags=tagIds): + value, tail = decoder.decode( + self._substrate(containerTag, tagIds, indefinite)) + self._assertValue(value, sequenceType, tagIds) + assert tail == b'' + + def testHeterogeneous(self): + for codec, container, tagIds in product( + self.codecs, self.containers, + ((32, 33), (32, 32, 33, 32), (32, 33, 34, 33, 32))): + name, decoder, indefinite = codec + containerTag, sequenceType, recordType = container + with self.subTest(codec=name, container=recordType, tags=tagIds): + value, tail = decoder.decode( + self._substrate(containerTag, tagIds, indefinite)) + self._assertValue(value, recordType, tagIds) + assert tail == b'' + + def testCollidingTags(self): + for codec, container, count in product( + self.codecs, self.containers, (16, 32, 64)): + name, decoder, indefinite = codec + containerTag, sequenceType, recordType = container + with self.subTest(codec=name, container=recordType, count=count): + # Distinct integer IDs separated by the hash modulus collide + # without replacing TagSet's public hash implementation. + tagIds = [32 + idx * sys.hash_info.modulus + for idx in range(count)] + assert len({hash(self._tagSet(tagId)) for tagId in tagIds}) == 1 + substrate = self._substrate(containerTag, tagIds, indefinite) + comparisons = 0 + originalEq = tag.TagSet.__eq__ + + def countedEq(left, right): + nonlocal comparisons + # Exclude fixed dispatch-table lookups and count only + # comparisons between explicitly tagged components. + if (isinstance(right, tag.TagSet) and + len(left) == len(right) == 2 and + left[-1].tagClass == tag.tagClassContext and + right[-1].tagClass == tag.tagClassContext): + comparisons += 1 + return originalEq(left, right) + + tag.TagSet.__eq__ = countedEq + try: + value, tail = decoder.decode(substrate) + finally: + tag.TagSet.__eq__ = originalEq + + self._assertValue(value, recordType, tagIds) + assert tail == b'' + assert comparisons <= count, ( + '%d tag comparisons for %d components' % + (comparisons, count)) + + def testStreaming(self): + class NonBlockingStream(io.BytesIO): + def read(self, size=-1): + chunk = io.BytesIO.read(self, size) + if not chunk and size: + return None + return chunk + + for codec, container, tagIds in product( + self.codecs, self.containers, + ((32, 32, 32), (32, 32, 32 + sys.hash_info.modulus, 32))): + name, decoder, indefinite = codec + containerTag, sequenceType, recordType = container + expectedType = sequenceType if len(set(tagIds)) == 1 else recordType + with self.subTest(codec=name, container=expectedType, tags=tagIds): + # Keep explicit wrappers definite so resumption exercises + # the container's own indefinite-length path. + substrate = self._substrate( + containerTag, tagIds, indefinite, definiteTags=True) + stream = NonBlockingStream() + iterator = iter(decoder.StreamingDecoder(stream)) + supplied = 0 + underruns = 0 + while True: + value = next(iterator) + if not isinstance(value, SubstrateUnderrunError): + break + + assert supplied < len(substrate), 'Decoder did not resume' + position = stream.tell() + stream.seek(0, io.SEEK_END) + supplied += stream.write(substrate[supplied:supplied + 3]) + stream.seek(position) + underruns += 1 + + assert underruns > 1 + assert supplied == stream.tell() == len(substrate) + self._assertValue(value, expectedType, tagIds) + + +suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite)
