diff options
-rw-r--r-- | CHANGES.rst | 4 | ||||
-rw-r--r-- | pyasn1/codec/ber/decoder.py | 32 | ||||
-rw-r--r-- | pyasn1/type/constraint.py | 110 | ||||
-rw-r--r-- | tests/type/test_constraint.py | 36 | ||||
-rw-r--r-- | tests/type/test_univ.py | 16 |
5 files changed, 161 insertions, 37 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index ef005ff..9b4c3af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,9 @@ Revision 0.4.8, released XX-09-2019 ----------------------------------- -No changes yet. +- Added ability of combining `SingleValueConstraint` and + `PermittedAlphabetConstraint` objects into one for proper modeling + `FROM ... EXCEPT ...` ASN.1 clause. Revision 0.4.7, released 01-09-2019 ----------------------------------- diff --git a/pyasn1/codec/ber/decoder.py b/pyasn1/codec/ber/decoder.py index caf9c09..2a6448e 100644 --- a/pyasn1/codec/ber/decoder.py +++ b/pyasn1/codec/ber/decoder.py @@ -804,7 +804,10 @@ class UniversalConstructedTypeDecoder(AbstractConstructedDecoder): openTypes = options.get('openTypes', {}) if LOG: - LOG('using open types map: %r' % openTypes) + LOG('user-specified open types map:') + + for k, v in openTypes.items(): + LOG('%s -> %r' % (k, v)) if openTypes or options.get('decodeOpenTypes', False): @@ -824,6 +827,17 @@ class UniversalConstructedTypeDecoder(AbstractConstructedDecoder): except KeyError: + if LOG: + LOG('default open types map of component ' + '"%s.%s" governed by component "%s.%s"' + ':' % (asn1Object.__class__.__name__, + namedType.name, + asn1Object.__class__.__name__, + namedType.openType.name)) + + for k, v in namedType.openType.items(): + LOG('%s -> %r' % (k, v)) + try: openType = namedType.openType[governingValue] @@ -988,7 +1002,10 @@ class UniversalConstructedTypeDecoder(AbstractConstructedDecoder): openTypes = options.get('openTypes', {}) if LOG: - LOG('using open types map: %r' % openTypes) + LOG('user-specified open types map:') + + for k, v in openTypes.items(): + LOG('%s -> %r' % (k, v)) if openTypes or options.get('decodeOpenTypes', False): @@ -1008,6 +1025,17 @@ class UniversalConstructedTypeDecoder(AbstractConstructedDecoder): except KeyError: + if LOG: + LOG('default open types map of component ' + '"%s.%s" governed by component "%s.%s"' + ':' % (asn1Object.__class__.__name__, + namedType.name, + asn1Object.__class__.__name__, + namedType.openType.name)) + + for k, v in namedType.openType.items(): + LOG('%s -> %r' % (k, v)) + try: openType = namedType.openType[governingValue] diff --git a/pyasn1/type/constraint.py b/pyasn1/type/constraint.py index b66627d..8f152e9 100644 --- a/pyasn1/type/constraint.py +++ b/pyasn1/type/constraint.py @@ -103,6 +103,11 @@ class SingleValueConstraint(AbstractConstraint): The SingleValueConstraint satisfies any value that is present in the set of permitted values. + Objects of this type are iterable (emitting constraint values) and + can act as operands for some arithmetic operations e.g. addition + and subtraction. The latter can be used for combining multiple + SingleValueConstraint objects into one. + The SingleValueConstraint object can be applied to any ASN.1 type. @@ -137,6 +142,23 @@ class SingleValueConstraint(AbstractConstraint): if value not in self._set: raise error.ValueConstraintError(value) + # Constrains can be merged or reduced + + def __contains__(self, item): + return item in self._set + + def __iter__(self): + return iter(self._set) + + def __sub__(self, constraint): + return self.__class__(*(self._set.difference(constraint))) + + def __add__(self, constraint): + return self.__class__(*(self._set.union(constraint))) + + def __sub__(self, constraint): + return self.__class__(*(self._set.difference(constraint))) + class ContainedSubtypeConstraint(AbstractConstraint): """Create a ContainedSubtypeConstraint object. @@ -305,6 +327,10 @@ class PermittedAlphabetConstraint(SingleValueConstraint): string for as long as all its characters are present in the set of permitted characters. + Objects of this type are iterable (emitting constraint values) and + can act as operands for some arithmetic operations e.g. addition + and subtraction. + The PermittedAlphabetConstraint object can only be applied to the :ref:`character ASN.1 types <type.char>` such as :class:`~pyasn1.type.char.IA5String`. @@ -314,8 +340,8 @@ class PermittedAlphabetConstraint(SingleValueConstraint): *alphabet: :class:`str` Full set of characters permitted by this constraint object. - Examples - -------- + Example + ------- .. code-block:: python class BooleanValue(IA5String): @@ -332,6 +358,42 @@ class PermittedAlphabetConstraint(SingleValueConstraint): # this will raise ValueConstraintError garbage = BooleanValue('TAF') + + ASN.1 `FROM ... EXCEPT ...` clause can be modelled by combining multiple + PermittedAlphabetConstraint objects into one: + + Example + ------- + .. code-block:: python + + class Lipogramme(IA5String): + ''' + ASN.1 specification: + + Lipogramme ::= + IA5String (FROM (ALL EXCEPT ("e"|"E"))) + ''' + subtypeSpec = ( + PermittedAlphabetConstraint(*string.printable) - + PermittedAlphabetConstraint('e', 'E') + ) + + # this will succeed + lipogramme = Lipogramme('A work of fiction?') + + # this will raise ValueConstraintError + lipogramme = Lipogramme('Eel') + + Note + ---- + Although `ConstraintsExclusion` object could seemingly be used for this + purpose, practically, for it to work, it needs to represent its operand + constraints as sets and intersect one with the other. That would require + the insight into the constraint values (and their types) that are otherwise + hidden inside the constraint object. + + Therefore it's more practical to model `EXCEPT` clause at + `PermittedAlphabetConstraint` level instead. """ def _setValues(self, values): self._values = values @@ -526,49 +588,41 @@ class ConstraintsExclusion(AbstractConstraint): Parameters ---------- - constraint: - Constraint or logic operator object. + *constraints: + Constraint or logic operator objects. Examples -------- .. code-block:: python - class Lipogramme(IA5STRING): - ''' - ASN.1 specification: - - Lipogramme ::= - IA5String (FROM (ALL EXCEPT ("e"|"E"))) - ''' + class LuckyNumber(Integer): subtypeSpec = ConstraintsExclusion( - PermittedAlphabetConstraint('e', 'E') + SingleValueConstraint(13) ) # this will succeed - lipogramme = Lipogramme('A work of fiction?') + luckyNumber = LuckyNumber(12) # this will raise ValueConstraintError - lipogramme = Lipogramme('Eel') + luckyNumber = LuckyNumber(13) - Warning - ------- - The above example involving PermittedAlphabetConstraint might - not work due to the way how PermittedAlphabetConstraint works. - The other constraints might work with ConstraintsExclusion - though. + Note + ---- + The `FROM ... EXCEPT ...` ASN.1 clause should be modeled by combining + constraint objects into one. See `PermittedAlphabetConstraint` for more + information. """ def _testValue(self, value, idx): - try: - self._values[0](value, idx) - except error.ValueConstraintError: - return - else: + for constraint in self._values: + try: + constraint(value, idx) + + except error.ValueConstraintError: + continue + raise error.ValueConstraintError(value) def _setValues(self, values): - if len(values) != 1: - raise error.PyAsn1Error('Single constraint expected') - AbstractConstraint._setValues(self, values) diff --git a/tests/type/test_constraint.py b/tests/type/test_constraint.py index 0f49c78..7ef6293 100644 --- a/tests/type/test_constraint.py +++ b/tests/type/test_constraint.py @@ -21,8 +21,10 @@ from pyasn1.type import error class SingleValueConstraintTestCase(BaseTestCase): def setUp(self): BaseTestCase.setUp(self) - self.c1 = constraint.SingleValueConstraint(1, 2) - self.c2 = constraint.SingleValueConstraint(3, 4) + self.v1 = 1, 2 + self.v2 = 3, 4 + self.c1 = constraint.SingleValueConstraint(*self.v1) + self.c2 = constraint.SingleValueConstraint(*self.v2) def testCmp(self): assert self.c1 == self.c1, 'comparison fails' @@ -45,6 +47,27 @@ class SingleValueConstraintTestCase(BaseTestCase): else: assert 0, 'constraint check fails' + def testContains(self): + for v in self.v1: + assert v in self.c1 + assert v not in self.c2 + + for v in self.v2: + assert v in self.c2 + assert v not in self.c1 + + def testIter(self): + assert set(self.v1) == set(self.c1) + assert set(self.v2) == set(self.c2) + + def testSub(self): + subconst = self.c1 - constraint.SingleValueConstraint(self.v1[0]) + assert list(subconst) == [self.v1[1]] + + def testAdd(self): + superconst = self.c1 + self.c2 + assert set(superconst) == set(self.v1 + self.v2) + class ContainedSubtypeConstraintTestCase(BaseTestCase): def setUp(self): @@ -110,20 +133,25 @@ class ValueSizeConstraintTestCase(BaseTestCase): class PermittedAlphabetConstraintTestCase(SingleValueConstraintTestCase): def setUp(self): - self.c1 = constraint.PermittedAlphabetConstraint('A', 'B', 'C') - self.c2 = constraint.PermittedAlphabetConstraint('DEF') + self.v1 = 'A', 'B' + self.v2 = 'C', 'D' + self.c1 = constraint.PermittedAlphabetConstraint(*self.v1) + self.c2 = constraint.PermittedAlphabetConstraint(*self.v2) def testGoodVal(self): try: self.c1('A') + except error.ValueConstraintError: assert 0, 'constraint check fails' def testBadVal(self): try: self.c1('E') + except error.ValueConstraintError: pass + else: assert 0, 'constraint check fails' diff --git a/tests/type/test_univ.py b/tests/type/test_univ.py index 9762959..124e5e9 100644 --- a/tests/type/test_univ.py +++ b/tests/type/test_univ.py @@ -551,7 +551,13 @@ class OctetStringWithAsciiTestCase(OctetStringWithUnicodeMixIn, BaseTestCase): class OctetStringUnicodeErrorTestCase(BaseTestCase): def testEncodeError(self): - text = octs2str(ints2octs((0xff, 0xfe))) + serialized = ints2octs((0xff, 0xfe)) + + if sys.version_info < (3, 0): + text = serialized.decode('iso-8859-1') + + else: + text = octs2str(serialized) try: univ.OctetString(text, encoding='us-ascii') @@ -567,8 +573,14 @@ class OctetStringUnicodeErrorTestCase(BaseTestCase): def testDecodeError(self): serialized = ints2octs((0xff, 0xfe)) + octetString = univ.OctetString(serialized, encoding='us-ascii') + try: - str(univ.OctetString(serialized, encoding='us-ascii')) + if sys.version_info < (3, 0): + unicode(octetString) + + else: + str(octetString) except PyAsn1UnicodeDecodeError: pass |