diff options
-rw-r--r-- | README.md | 36 | ||||
-rw-r--r-- | pyasn1/codec/python/decoder.py | 7 | ||||
-rw-r--r-- | pyasn1/codec/python/encoder.py | 26 | ||||
-rw-r--r-- | test/codec/python/__init__.py | 1 | ||||
-rw-r--r-- | test/codec/python/suite.py | 33 | ||||
-rw-r--r-- | test/codec/python/test_decoder.py | 116 | ||||
-rw-r--r-- | test/codec/python/test_encoder.py | 131 | ||||
-rw-r--r-- | test/codec/suite.py | 5 |
8 files changed, 337 insertions, 18 deletions
@@ -17,7 +17,7 @@ Features -------- * Generic implementation of ASN.1 types (X.208) -* Fully standard compliant BER/CER/DER codecs +* Standards compliant BER/CER/DER codecs * 100% Python, works with Python 2.4 up to Python 3.6 * MT-safe * Contributed ASN.1 compiler [Asn1ate](https://github.com/kimgr/asn1ate) @@ -100,6 +100,7 @@ Part of the power of ASN.1 comes from its serialization features. You can serialize your data structure and send it over the network. ```python +>>> from pyasn1.codec.der.encoder import encode >>> substrate = encode(record) >>> hexdump(substrate) 00000: 30 07 02 01 7B 80 02 01 41 @@ -110,6 +111,7 @@ network or read from a file, into a Python object which you can introspect, modify, encode and send back. ```python +>>> from pyasn1.codec.der.decoder import decode >>> received_record, rest_of_substrate = decode(substrate, asn1Spec=Record()) >>> >>> for field in received_record: @@ -126,11 +128,35 @@ True 00000: 30 06 02 01 7B 80 01 7B ``` +The pyasn1 classes struggle to emulate their Python prototypes (e.g. int, +list, dict etc.). But ASN.1 types exhibit more complicated behaviour. +To make life easier for a Pythonista, they can turn their pyasn1 +classes into Python built-ins: + +```python +>>> from pyasn1.codec.python.encoder import encode +>>> encode(record) +{'id': 123, 'room': 321, 'house': 0} +``` + +Or vice-versa -- you can initialize an ASN.1 structure from a tree of +Python objects: + +```python +>>> from pyasn1.codec.python.decoder import decode +>>> record = decode({'id': 123, 'room': 321, 'house': 0}, asn1Spec=Record()) +>>> print(record.prettyPrint()) +Record: + id=123 + room=321 +>>> +``` + With ASN.1 design, serialization codecs are decoupled from data objects, so you could turn every single ASN.1 object into many different -serialized forms. As of this moment, pyasn1 supports BER, DER and CER -formats. The extremely compact PER encoding is expected to be introduced -in the upcoming pyasn1 release. +serialized forms. As of this moment, pyasn1 supports BER, DER, CER and +Python built-ins codecs. The extremely compact PER encoding is expected +to be introduced in the upcoming pyasn1 release. More information on pyasn1 APIs can be found in the [documentation](http://pyasn1.sourceforge.net), @@ -149,7 +175,7 @@ You could `pip install pyasn1` or download it from [PyPI](https://pypi.python.or If something does not work as expected, [open an issue](https://github.com/etingof/pyasn1/issues) at GitHub or -post your question [to Stack Overflow](http://stackoverflow.com/questions/ask) +post your question [on Stack Overflow](http://stackoverflow.com/questions/ask) or try browsing pyasn1 [mailing list archives](https://sourceforge.net/p/pyasn1/mailman/pyasn1-users/). diff --git a/pyasn1/codec/python/decoder.py b/pyasn1/codec/python/decoder.py index 53f47cf..aef79b4 100644 --- a/pyasn1/codec/python/decoder.py +++ b/pyasn1/codec/python/decoder.py @@ -105,7 +105,9 @@ class Decoder(object): def __call__(self, substrate, asn1Spec): if debug.logger & debug.flagDecoder: + debug.scope.push(type(substrate).__name__) debug.logger('decoder called at scope %s, working with type %s' % (debug.scope, type(substrate).__name__)) + if asn1Spec is None or not isinstance(asn1Spec, base.Asn1Item): raise error.PyAsn1Error('asn1Spec is not valid (should be an instance of an ASN.1 Item, not %s)' % asn1Spec.__class__.__name__) @@ -117,13 +119,12 @@ class Decoder(object): raise error.PyAsn1Error('Unknown ASN.1 tag %s' % asn1Spec.tagSet) if debug.logger & debug.flagDecoder: - debug.logger('calling decoder %s on Python type %s' % (valueDecoder, type(substrate).__name__)) - debug.scope.push(type(substrate).__name__) + debug.logger('calling decoder %s on Python type %s <%s>' % (type(valueDecoder).__name__, type(substrate).__name__, repr(substrate))) value = valueDecoder(substrate, asn1Spec, self) if debug.logger & debug.flagDecoder: - debug.logger('decoder %s produced ASN.1 type %s' % (valueDecoder, type(value).__name__)) + debug.logger('decoder %s produced ASN.1 type %s <%s>' % (type(valueDecoder).__name__, type(value).__name__, repr(value))) debug.scope.pop() return value diff --git a/pyasn1/codec/python/encoder.py b/pyasn1/codec/python/encoder.py index 9113271..887df78 100644 --- a/pyasn1/codec/python/encoder.py +++ b/pyasn1/codec/python/encoder.py @@ -8,7 +8,7 @@ try: from collections import OrderedDict except ImportError: - from ordereddict import OrderedDict + OrderedDict = dict from pyasn1.type import base, univ, char, useful from pyasn1 import debug, error @@ -155,8 +155,11 @@ class Encoder(object): def __call__(self, value): if not isinstance(value, base.Asn1Item): raise error.PyAsn1Error('value is not valid (should be an instance of an ASN.1 Item)') - debug.logger & debug.flagEncoder and debug.logger( - 'encoder called for type %s, value:\n%s' % (value.prettyPrintType(), value.prettyPrint())) + + if debug.logger & debug.flagEncoder: + debug.scope.push(type(value).__name__) + debug.logger('encoder called for type %s <%s>' % (type(value).__name__, value.prettyPrint())) + tagSet = value.getTagSet() if len(tagSet) > 1: concreteEncoder = explicitlyTaggedItemEncoder @@ -171,11 +174,15 @@ class Encoder(object): concreteEncoder = self.__tagMap[tagSet] else: raise error.PyAsn1Error('No encoder for %s' % (value,)) - debug.logger & debug.flagEncoder and debug.logger( - 'using value codec %s chosen by %s' % (concreteEncoder.__class__.__name__, tagSet)) + + debug.logger & debug.flagEncoder and debug.logger('using value codec %s chosen by %s' % (type(concreteEncoder).__name__, tagSet)) + substrate = concreteEncoder.encode(self, value) - debug.logger & debug.flagEncoder and debug.logger( - 'encoder produced: %s\nencoder completed' % repr(substrate)) + + if debug.logger & debug.flagEncoder: + debug.logger('encoder %s produced: %s' % (type(concreteEncoder).__name__, repr(substrate))) + debug.scope.pop() + return substrate @@ -185,8 +192,9 @@ class Encoder(object): #: walks all its components recursively and produces a Python built-in type or a tree #: of those. #: -#: One important exception is that instead of :py:class:`dict`, the :py:class:`OrderedDict` -#: is produced to preserve ordering of the components in ASN.1 SEQUENCE. +#: One exception is that instead of :py:class:`dict`, the :py:class:`OrderedDict` +#: can be produced (whenever available) to preserve ordering of the components +#: in ASN.1 SEQUENCE. #: #: Parameters #: ---------- diff --git a/test/codec/python/__init__.py b/test/codec/python/__init__.py new file mode 100644 index 0000000..8c3066b --- /dev/null +++ b/test/codec/python/__init__.py @@ -0,0 +1 @@ +# This file is necessary to make this directory a package. diff --git a/test/codec/python/suite.py b/test/codec/python/suite.py new file mode 100644 index 0000000..57ce67d --- /dev/null +++ b/test/codec/python/suite.py @@ -0,0 +1,33 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2017, Ilya Etingof <etingof@gmail.com> +# License: http://pyasn1.sf.net/license.html +# +from sys import path, version_info +from os.path import sep + +path.insert(1, path[0] + sep + 'python') +import test_encoder, test_decoder + +if version_info[0:2] < (2, 7) or \ + version_info[0:2] in ((3, 0), (3, 1)): + try: + import unittest2 as unittest + except ImportError: + import unittest +else: + import unittest + +suite = unittest.TestSuite() +loader = unittest.TestLoader() +for m in (test_encoder, test_decoder): + suite.addTest(loader.loadTestsFromModule(m)) + + +def runTests(): + unittest.TextTestRunner(verbosity=2).run(suite) + + +if __name__ == '__main__': + runTests() diff --git a/test/codec/python/test_decoder.py b/test/codec/python/test_decoder.py new file mode 100644 index 0000000..af79bd3 --- /dev/null +++ b/test/codec/python/test_decoder.py @@ -0,0 +1,116 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2017, Ilya Etingof <etingof@gmail.com> +# License: http://pyasn1.sf.net/license.html +# +from pyasn1.type import tag, namedtype, univ, char +from pyasn1.codec.python import decoder +from pyasn1.error import PyAsn1Error +from sys import version_info + +if version_info[0:2] < (2, 7) or \ + version_info[0:2] in ((3, 0), (3, 1)): + try: + import unittest2 as unittest + except ImportError: + import unittest +else: + import unittest + + +class BadAsn1SpecTestCase(unittest.TestCase): + def testBadSpec(self): + try: + decoder.decode('', asn1Spec='not an Asn1Item') + except PyAsn1Error: + pass + else: + assert 0, 'Invalid asn1Spec accepted' + + +class IntegerDecoderTestCase(unittest.TestCase): + def testPosInt(self): + assert decoder.decode(12, asn1Spec=univ.Integer()) == univ.Integer(12) + + def testNegInt(self): + assert decoder.decode(-12, asn1Spec=univ.Integer()) == univ.Integer(-12) + + +class BooleanDecoderTestCase(unittest.TestCase): + def testTrue(self): + assert decoder.decode(True, asn1Spec=univ.Boolean()) == univ.Boolean(True) + + def testTrueNeg(self): + assert decoder.decode(False, asn1Spec=univ.Boolean()) == univ.Boolean(False) + + +class BitStringDecoderTestCase(unittest.TestCase): + def testSimple(self): + assert decoder.decode('11111111', asn1Spec=univ.BitString()) == univ.BitString(hexValue='ff') + + +class OctetStringDecoderTestCase(unittest.TestCase): + def testSimple(self): + assert decoder.decode('Quick brown fox', asn1Spec=univ.OctetString()) == univ.OctetString('Quick brown fox') + + +class NullDecoderTestCase(unittest.TestCase): + def testNull(self): + assert decoder.decode(None, asn1Spec=univ.Null()) == univ.Null() + + +class ObjectIdentifierDecoderTestCase(unittest.TestCase): + def testOne(self): + assert decoder.decode('1.3.6.11', asn1Spec=univ.ObjectIdentifier()) == univ.ObjectIdentifier('1.3.6.11') + + +class RealDecoderTestCase(unittest.TestCase): + def testSimple(self): + assert decoder.decode(1.33, asn1Spec=univ.Real()) == univ.Real(1.33) + + +class SequenceDecoderTestCase(unittest.TestCase): + def setUp(self): + self.s = univ.Sequence( + componentType=namedtype.NamedTypes( + namedtype.NamedType('place-holder', univ.Null()), + namedtype.NamedType('first-name', univ.OctetString()), + namedtype.NamedType('age', univ.Integer(33)) + ) + ) + + def testSimple(self): + s = self.s.clone() + s[0] = univ.Null() + s[1] = univ.OctetString('xx') + s[2] = univ.Integer(33) + assert decoder.decode({'place-holder': None, 'first-name': 'xx', 'age': 33}, asn1Spec=self.s) == s + + +class ChoiceDecoderTestCase(unittest.TestCase): + def setUp(self): + self.s = univ.Choice( + componentType=namedtype.NamedTypes( + namedtype.NamedType('place-holder', univ.Null()), + namedtype.NamedType('first-name', univ.OctetString()), + namedtype.NamedType('age', univ.Integer(33)) + ) + ) + + def testSimple(self): + s = self.s.clone() + s[1] = univ.OctetString('xx') + assert decoder.decode({'first-name': 'xx'}, asn1Spec=self.s) == s + + +class AnyDecoderTestCase(unittest.TestCase): + def setUp(self): + self.s = univ.Any() + + def testSimple(self): + assert decoder.decode('fox', asn1Spec=univ.Any()) == univ.Any('fox') + + +if __name__ == '__main__': + unittest.main() diff --git a/test/codec/python/test_encoder.py b/test/codec/python/test_encoder.py new file mode 100644 index 0000000..202248c --- /dev/null +++ b/test/codec/python/test_encoder.py @@ -0,0 +1,131 @@ +# +# This file is part of pyasn1 software. +# +# Copyright (c) 2005-2017, Ilya Etingof <etingof@gmail.com> +# License: http://pyasn1.sf.net/license.html +# +from pyasn1.type import tag, namedtype, univ, char +from pyasn1.codec.python import encoder +from pyasn1.error import PyAsn1Error +from sys import version_info + +if version_info[0:2] < (2, 7) or \ + version_info[0:2] in ((3, 0), (3, 1)): + try: + import unittest2 as unittest + except ImportError: + import unittest +else: + import unittest + + +class BadAsn1SpecTestCase(unittest.TestCase): + def testBadValueType(self): + try: + encoder.encode('not an Asn1Item') + except PyAsn1Error: + pass + else: + assert 0, 'Invalid value type accepted' + + +class IntegerEncoderTestCase(unittest.TestCase): + def testPosInt(self): + assert encoder.encode(univ.Integer(12)) == 12 + + def testNegInt(self): + assert encoder.encode(univ.Integer(-12)) == -12 + + +class BooleanEncoderTestCase(unittest.TestCase): + def testTrue(self): + assert encoder.encode(univ.Boolean(1)) is True + + def testFalse(self): + assert encoder.encode(univ.Boolean(0)) is False + + +class BitStringEncoderTestCase(unittest.TestCase): + def setUp(self): + self.b = univ.BitString((1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1)) + + def testValue(self): + assert encoder.encode(self.b) == '101010011000101' + + +class OctetStringEncoderTestCase(unittest.TestCase): + def setUp(self): + self.o = univ.OctetString('Quick brown fox') + + def testValue(self): + assert encoder.encode(self.o) == 'Quick brown fox' + + +class NullEncoderTestCase(unittest.TestCase): + def testNull(self): + assert encoder.encode(univ.Null('')) is None + + +class ObjectIdentifierEncoderTestCase(unittest.TestCase): + def testOne(self): + assert encoder.encode(univ.ObjectIdentifier((1, 3, 6, 0, 12345))) == '1.3.6.0.12345' + + +class RealEncoderTestCase(unittest.TestCase): + def testChar(self): + assert encoder.encode(univ.Real((123, 10, 11))) == 1.23e+13 + + def testPlusInf(self): + assert encoder.encode(univ.Real('inf')) == float('inf') + + def testMinusInf(self): + assert encoder.encode(univ.Real('-inf')) == float('-inf') + + +class SequenceEncoderTestCase(unittest.TestCase): + def setUp(self): + self.s = univ.Sequence(componentType=namedtype.NamedTypes( + namedtype.NamedType('place-holder', univ.Null('')), + namedtype.OptionalNamedType('first-name', univ.OctetString('')), + namedtype.DefaultedNamedType('age', univ.Integer(33)), + )) + + def testSimple(self): + s = self.s.clone() + s[0] = univ.Null('') + s[1] = 'abc' + s[2] = 123 + assert encoder.encode(s) == {'place-holder': None, 'first-name': 'abc', 'age': 123} + + +class ChoiceEncoderTestCase(unittest.TestCase): + def setUp(self): + self.s = univ.Choice(componentType=namedtype.NamedTypes( + namedtype.NamedType('place-holder', univ.Null('')), + namedtype.NamedType('number', univ.Integer(0)), + namedtype.NamedType('string', univ.OctetString()) + )) + + def testEmpty(self): + try: + encoder.encode(self.s) + except PyAsn1Error: + pass + else: + assert False, 'encoded unset choice' + + def testFilled(self): + self.s.setComponentByPosition(0, univ.Null('')) + assert encoder.encode(self.s) == {'place-holder': None} + + +class AnyEncoderTestCase(unittest.TestCase): + def setUp(self): + self.s = univ.Any(encoder.encode(univ.OctetString('fox'))) + + def testSimple(self): + assert encoder.encode(self.s) == 'fox' + + +if __name__ == '__main__': + unittest.main() diff --git a/test/codec/suite.py b/test/codec/suite.py index fcdf328..344e430 100644 --- a/test/codec/suite.py +++ b/test/codec/suite.py @@ -16,6 +16,9 @@ import cer.suite path.insert(1, path[0] + sep + 'codec' + sep + 'der') import der.suite +path.insert(1, path[0] + sep + 'codec' + sep + 'python') +import python.suite + if version_info[0:2] < (2, 7) or \ version_info[0:2] in ((3, 0), (3, 1)): try: @@ -26,7 +29,7 @@ else: import unittest suite = unittest.TestSuite() -for m in (ber.suite, cer.suite, der.suite): +for m in (ber.suite, cer.suite, der.suite, python.suite): suite.addTest(getattr(m, 'suite')) |