From 6cc4b86dbc92d4f76c45f86c9828308ce6e77ef5 Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Mon, 19 Aug 2019 21:09:46 +0200 Subject: Add `SET ... WITH COMPONENTS ...` ASN.1 construct support Added `WithComponentsConstraint` along with related `ComponentPresentConstraint` and `ComponentAbsentConstraint` classes to be used with `Sequence`/`Set` types representing `SET ... WITH COMPONENTS ...` like ASN.1 constructs. --- CHANGES.rst | 4 + .../pyasn1/type/base/constructedasn1type.rst | 2 +- docs/source/pyasn1/type/constraint/contents.rst | 1 + .../pyasn1/type/constraint/withcomponents.rst | 16 +++ pyasn1/type/base.py | 18 --- pyasn1/type/constraint.py | 133 +++++++++++++++++++++ pyasn1/type/univ.py | 73 +++++++++++ tests/type/test_constraint.py | 63 ++++++++++ tests/type/test_univ.py | 102 ++++++++++++++++ 9 files changed, 393 insertions(+), 19 deletions(-) create mode 100644 docs/source/pyasn1/type/constraint/withcomponents.rst diff --git a/CHANGES.rst b/CHANGES.rst index f9c00bb..88f5c63 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,10 @@ Revision 0.4.7, released XX-09-2019 objects as the type in field definition. When a bare Python value is assigned, then field type object is cloned and initialized with the bare value (constraints verificaton would run at this moment). +- Added `WithComponentsConstraint` along with related + `ComponentPresentConstraint` and `ComponentAbsentConstraint` classes + to be used with `Sequence`/`Set` types representing + `SET ... WITH COMPONENTS ...` like ASN.1 constructs. Revision 0.4.6, released 31-07-2019 ----------------------------------- diff --git a/docs/source/pyasn1/type/base/constructedasn1type.rst b/docs/source/pyasn1/type/base/constructedasn1type.rst index cf7f665..8709066 100644 --- a/docs/source/pyasn1/type/base/constructedasn1type.rst +++ b/docs/source/pyasn1/type/base/constructedasn1type.rst @@ -7,4 +7,4 @@ ------------ .. autoclass:: pyasn1.type.base.ConstructedAsn1Type(tagSet=TagSet(), subtypeSpec=ConstraintsIntersection(), componentType=None) - :members: isSameTypeWith, isSuperTypeOf, tagSet, effectiveTagSet, tagMap, subtypeSpec, isInconsistent + :members: isSameTypeWith, isSuperTypeOf, tagSet, effectiveTagSet, tagMap, subtypeSpec diff --git a/docs/source/pyasn1/type/constraint/contents.rst b/docs/source/pyasn1/type/constraint/contents.rst index 8e4db7c..a4e2424 100644 --- a/docs/source/pyasn1/type/constraint/contents.rst +++ b/docs/source/pyasn1/type/constraint/contents.rst @@ -32,6 +32,7 @@ they get attached to ASN.1 type object at a *.subtypeSpec* attribute. /pyasn1/type/constraint/valuerange /pyasn1/type/constraint/valuesize /pyasn1/type/constraint/permittedalphabet + /pyasn1/type/constraint/withcomponents Logic operations on constraints diff --git a/docs/source/pyasn1/type/constraint/withcomponents.rst b/docs/source/pyasn1/type/constraint/withcomponents.rst new file mode 100644 index 0000000..f1556b0 --- /dev/null +++ b/docs/source/pyasn1/type/constraint/withcomponents.rst @@ -0,0 +1,16 @@ + +.. _constrain.WithComponentsConstraint: + +.. |Constraint| replace:: WithComponentsConstraint + +WITH COMPONENTS constraint +-------------------------- + +.. autoclass:: pyasn1.type.constraint.WithComponentsConstraint(*fields) + :members: + +.. autoclass:: pyasn1.type.constraint.ComponentPresentConstraint() + :members: + +.. autoclass:: pyasn1.type.constraint.ComponentAbsentConstraint() + :members: diff --git a/pyasn1/type/base.py b/pyasn1/type/base.py index 834b76e..994f1c9 100644 --- a/pyasn1/type/base.py +++ b/pyasn1/type/base.py @@ -677,24 +677,6 @@ class ConstructedAsn1Type(Asn1Type): return clone - @property - def isInconsistent(self): - """Run necessary checks to ensure object consistency. - - Default action is to verify |ASN.1| object against constraints imposed - by `subtypeSpec`. - - Raises - ------ - :py:class:`~pyasn1.error.PyAsn1tError` on any inconsistencies found - """ - try: - self.subtypeSpec(self) - - except error.PyAsn1Error: - exc = sys.exc_info()[1] - return exc - def getComponentByPosition(self, idx): raise error.PyAsn1Error('Method not implemented') diff --git a/pyasn1/type/constraint.py b/pyasn1/type/constraint.py index 75db38a..b8aa0af 100644 --- a/pyasn1/type/constraint.py +++ b/pyasn1/type/constraint.py @@ -342,6 +342,139 @@ class PermittedAlphabetConstraint(SingleValueConstraint): raise error.ValueConstraintError(value) +class ComponentPresentConstraint(AbstractConstraint): + """Create a ComponentPresentConstraint object. + + The ComponentPresentConstraint is only satisfied when the value + is not `None`. + + The ComponentPresentConstraint object is typically used with + `WithComponentsConstraint`. + + Examples + -------- + .. code-block:: python + + present = ComponentPresentConstraint() + + # this will succeed + present('whatever') + + # this will raise ValueConstraintError + present(None) + """ + def _setValues(self, values): + self._values = ('',) + + if values: + raise error.PyAsn1Error('No arguments expected') + + def _testValue(self, value, idx): + if value is None: + raise error.ValueConstraintError( + 'Component is not present:') + + +class ComponentAbsentConstraint(AbstractConstraint): + """Create a ComponentAbsentConstraint object. + + The ComponentAbsentConstraint is only satisfied when the value + is `None`. + + The ComponentAbsentConstraint object is typically used with + `WithComponentsConstraint`. + + Examples + -------- + .. code-block:: python + + absent = ComponentAbsentConstraint() + + # this will succeed + absent(None) + + # this will raise ValueConstraintError + absent('whatever') + """ + def _setValues(self, values): + self._values = ('',) + + if values: + raise error.PyAsn1Error('No arguments expected') + + def _testValue(self, value, idx): + if value is not None: + raise error.ValueConstraintError( + 'Component is not absent: %r' % value) + + +class WithComponentsConstraint(AbstractConstraint): + """Create a WithComponentsConstraint object. + + The WithComponentsConstraint satisfies any mapping object that has + constrained fields present or absent, what is indicated by + `ComponentPresentConstraint` and `ComponentAbsentConstraint` + objects respectively. + + The WithComponentsConstraint object is typically applied + to :class:`~pyasn1.type.univ.Set` or + :class:`~pyasn1.type.univ.Sequence` types. + + Parameters + ---------- + *fields: :class:`tuple` + Zero or more tuples of (`field`, `constraint`) indicating constrained + fields. + + Examples + -------- + + .. code-block:: python + + class Item(Sequence): # Set is similar + ''' + ASN.1 specification: + + Item ::= SEQUENCE { + id INTEGER OPTIONAL, + name OCTET STRING OPTIONAL + } WITH COMPONENTS id PRESENT, name ABSENT | id ABSENT, name PRESENT + ''' + componentType = NamedTypes( + OptionalNamedType('id', Integer()), + OptionalNamedType('name', OctetString()) + ) + withComponents = ConstraintsIntersection( + WithComponentsConstraint( + ('id', ComponentPresentConstraint()), + ('name', ComponentAbsentConstraint()) + ), + WithComponentsConstraint( + ('id', ComponentAbsentConstraint()), + ('name', ComponentPresentConstraint()) + ) + ) + + item = Item() + + # This will succeed + item['id'] = 1 + + # This will succeed + item['name'] = 'John' + + # This will fail on encoding + descr['id'] = 1 + descr['name'] = 'John' + """ + def _testValue(self, value, idx): + for field, constraint in self._values: + constraint(value.get(field)) + + def _setValues(self, values): + AbstractConstraint._setValues(self, values) + + # This is a bit kludgy, meaning two op modes within a single constraint class InnerTypeConstraint(AbstractConstraint): """Value must satisfy the type and presence constraints""" diff --git a/pyasn1/type/univ.py b/pyasn1/type/univ.py index fbf8ed5..aa688b2 100644 --- a/pyasn1/type/univ.py +++ b/pyasn1/type/univ.py @@ -2042,6 +2042,41 @@ class SequenceOfAndSetOfBase(base.ConstructedAsn1Type): return True + @property + def isInconsistent(self): + """Run necessary checks to ensure |ASN.1| object consistency. + + Default action is to verify |ASN.1| object against constraints imposed + by `subtypeSpec`. + + Raises + ------ + :py:class:`~pyasn1.error.PyAsn1tError` on any inconsistencies found + """ + if self.componentType is noValue or not self.subtypeSpec: + return False + + if self._componentValues is noValue: + return True + + mapping = {} + + for idx, value in self._componentValues.items(): + # Absent fields are not in the mapping + if value is noValue: + continue + + mapping[idx] = value + + try: + # Represent SequenceOf/SetOf as a bare dict to constraints chain + self.subtypeSpec(mapping) + + except error.PyAsn1Error: + exc = sys.exc_info()[1] + return exc + + return False class SequenceOf(SequenceOfAndSetOfBase): __doc__ = SequenceOfAndSetOfBase.__doc__ @@ -2637,6 +2672,44 @@ class SequenceAndSetBase(base.ConstructedAsn1Type): return True + @property + def isInconsistent(self): + """Run necessary checks to ensure |ASN.1| object consistency. + + Default action is to verify |ASN.1| object against constraints imposed + by `subtypeSpec`. + + Raises + ------ + :py:class:`~pyasn1.error.PyAsn1tError` on any inconsistencies found + """ + if self.componentType is noValue or not self.subtypeSpec: + return False + + if self._componentValues is noValue: + return True + + mapping = {} + + for idx, value in enumerate(self._componentValues): + # Absent fields are not in the mapping + if value is noValue: + continue + + name = self.componentType.getNameByPosition(idx) + + mapping[name] = value + + try: + # Represent Sequence/Set as a bare dict to constraints chain + self.subtypeSpec(mapping) + + except error.PyAsn1Error: + exc = sys.exc_info()[1] + return exc + + return False + def prettyPrint(self, scope=0): """Return an object representation string. diff --git a/tests/type/test_constraint.py b/tests/type/test_constraint.py index b5276cd..0f49c78 100644 --- a/tests/type/test_constraint.py +++ b/tests/type/test_constraint.py @@ -128,6 +128,69 @@ class PermittedAlphabetConstraintTestCase(SingleValueConstraintTestCase): assert 0, 'constraint check fails' +class WithComponentsConstraintTestCase(BaseTestCase): + + def testGoodVal(self): + c = constraint.WithComponentsConstraint( + ('A', constraint.ComponentPresentConstraint()), + ('B', constraint.ComponentAbsentConstraint())) + + try: + c({'A': 1}) + + except error.ValueConstraintError: + assert 0, 'constraint check fails' + + def testGoodValWithExtraFields(self): + c = constraint.WithComponentsConstraint( + ('A', constraint.ComponentPresentConstraint()), + ('B', constraint.ComponentAbsentConstraint()) + ) + + try: + c({'A': 1, 'C': 2}) + + except error.ValueConstraintError: + assert 0, 'constraint check fails' + + def testEmptyConstraint(self): + c = constraint.WithComponentsConstraint() + + try: + c({'A': 1}) + + except error.ValueConstraintError: + assert 0, 'constraint check fails' + + def testBadVal(self): + c = constraint.WithComponentsConstraint( + ('A', constraint.ComponentPresentConstraint()) + ) + + try: + c({'B': 2}) + + except error.ValueConstraintError: + pass + + else: + assert 0, 'constraint check fails' + + def testBadValExtraFields(self): + c = constraint.WithComponentsConstraint( + ('A', constraint.ComponentPresentConstraint()) + ) + + try: + c({'B': 2, 'C': 3}) + + except error.ValueConstraintError: + pass + + else: + assert 0, 'constraint check fails' + + class ConstraintsIntersectionTestCase(BaseTestCase): def setUp(self): BaseTestCase.setUp(self) diff --git a/tests/type/test_univ.py b/tests/type/test_univ.py index d9f921b..9762959 100644 --- a/tests/type/test_univ.py +++ b/tests/type/test_univ.py @@ -1249,6 +1249,37 @@ class SequenceOf(BaseTestCase): assert not s.isValue + def testIsInconsistentSizeConstraint(self): + + class SequenceOf(univ.SequenceOf): + componentType = univ.OctetString() + subtypeSpec = constraint.ValueSizeConstraint(0, 1) + + s = SequenceOf() + + assert s.isInconsistent + + s[0] = 'test' + + assert not s.isInconsistent + + s[0] = 'test' + s[1] = 'test' + + assert s.isInconsistent + + s.clear() + + assert not s.isInconsistent + + s.reset() + + assert s.isInconsistent + + s[1] = 'test' + + assert not s.isInconsistent + class SequenceOfPicklingTestCase(unittest.TestCase): @@ -1585,6 +1616,77 @@ class Sequence(BaseTestCase): assert not s.isValue + def testIsInconsistentWithComponentsConstraint(self): + + class Sequence(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('name', univ.OctetString()), + namedtype.DefaultedNamedType('age', univ.Integer(65)) + ) + subtypeSpec = constraint.WithComponentsConstraint( + ('name', constraint.ComponentPresentConstraint()), + ('age', constraint.ComponentAbsentConstraint()) + ) + + s = Sequence() + + assert s.isInconsistent + + s[0] = 'test' + + assert not s.isInconsistent + + s[0] = 'test' + s[1] = 23 + + assert s.isInconsistent + + s.clear() + + assert s.isInconsistent + + s.reset() + + assert s.isInconsistent + + s[1] = 23 + + assert s.isInconsistent + + def testIsInconsistentSizeConstraint(self): + + class Sequence(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType('name', univ.OctetString()), + namedtype.DefaultedNamedType('age', univ.Integer(65)) + ) + subtypeSpec = constraint.ValueSizeConstraint(0, 1) + + s = Sequence() + + assert not s.isInconsistent + + s[0] = 'test' + + assert not s.isInconsistent + + s[0] = 'test' + s[1] = 23 + + assert s.isInconsistent + + s.clear() + + assert not s.isInconsistent + + s.reset() + + assert s.isInconsistent + + s[1] = 23 + + assert not s.isInconsistent + class SequenceWithoutSchema(BaseTestCase): -- cgit v1.2.1