]> Cypherpunks.ru repositories - pyderasn.git/blobdiff - pyderasn.py
More correct phrase about offset property
[pyderasn.git] / pyderasn.py
index b1f029cdd89e00ec6d99f5400e9bb79ba3f7a2f3..833e785206f26a4f84ec209a793c8c797fba51cc 100755 (executable)
@@ -177,7 +177,7 @@ by specifying ``leavemm=True`` argument.
 When object is decoded, ``decoded`` property is true and you can safely
 use following properties:
 
-* ``offset`` -- position from initial offset where object's tag is started
+* ``offset`` -- position including initial offset where object's tag starts
 * ``tlen`` -- length of object's tag
 * ``llen`` -- length of object's length value
 * ``vlen`` -- length of object's value
@@ -298,6 +298,17 @@ SetOf
 _____
 .. autoclass:: pyderasn.SetOf
    :members: __init__
+
+Various
+-------
+
+.. autofunction:: pyderasn.hexenc
+.. autofunction:: pyderasn.hexdec
+.. autofunction:: pyderasn.tag_encode
+.. autofunction:: pyderasn.tag_decode
+.. autofunction:: pyderasn.tag_ctxp
+.. autofunction:: pyderasn.tag_ctxc
+.. autoclass:: pyderasn.Obj
 """
 
 from codecs import getdecoder
@@ -307,6 +318,7 @@ from collections import OrderedDict
 from datetime import datetime
 from math import ceil
 
+from six import add_metaclass
 from six import binary_type
 from six import byte2int
 from six import indexbytes
@@ -392,6 +404,16 @@ TagClassReprs = {
 
 class DecodeError(Exception):
     def __init__(self, msg="", klass=None, decode_path=(), offset=0):
+        """
+        :param str msg: reason of decode failing
+        :param klass: optional exact DecodeError inherited class (like
+                      :py:exc:`NotEnoughData`, :py:exc:`TagMismatch`,
+                      :py:exc:`InvalidLength`)
+        :param decode_path: tuple of strings. It contains human
+                            readable names of the fields through which
+                            decoding process has passed
+        :param int offset: binary offset where failure happened
+        """
         super(DecodeError, self).__init__()
         self.msg = msg
         self.klass = klass
@@ -496,10 +518,14 @@ _hexencoder = getencoder("hex")
 
 
 def hexdec(data):
+    """Binary data to hexadecimal string convert
+    """
     return _hexdecoder(data)[0]
 
 
 def hexenc(data):
+    """Hexadecimal string to binary data convert
+    """
     return _hexencoder(data)[0].decode("ascii")
 
 
@@ -523,6 +549,16 @@ def zero_ended_encode(num):
 
 
 def tag_encode(num, klass=TagClassUniversal, form=TagFormPrimitive):
+    """Encode tag to binary form
+
+    :param int num: tag's number
+    :param int klass: tag's class (:py:data:`pyderasn.TagClassUniversal`,
+                      :py:data:`pyderasn.TagClassContext`,
+                      :py:data:`pyderasn.TagClassApplication`,
+                      :py:data:`pyderasn.TagClassPrivate`)
+    :param int form: tag's form (:py:data:`pyderasn.TagFormPrimitive`,
+                     :py:data:`pyderasn.TagFormConstructed`)
+    """
     if num < 31:
         # [XX|X|.....]
         return int2byte(klass | form | num)
@@ -531,8 +567,14 @@ def tag_encode(num, klass=TagClassUniversal, form=TagFormPrimitive):
 
 
 def tag_decode(tag):
-    """
-    assume that data is validated
+    """Decode tag from binary form
+
+    .. warning::
+
+       No validation is performed, assuming that it has already passed.
+
+    It returns tuple with three integers, as
+    :py:func:`pyderasn.tag_encode` accepts.
     """
     first_octet = byte2int(tag)
     klass = first_octet & 0xC0
@@ -547,10 +589,14 @@ def tag_decode(tag):
 
 
 def tag_ctxp(num):
+    """Create CONTEXT PRIMITIVE tag
+    """
     return tag_encode(num=num, klass=TagClassContext, form=TagFormPrimitive)
 
 
 def tag_ctxc(num):
+    """Create CONTEXT CONSTRUCTED tag
+    """
     return tag_encode(num=num, klass=TagClassContext, form=TagFormConstructed)
 
 
@@ -610,8 +656,18 @@ def len_decode(data):
 # Base class
 ########################################################################
 
+class AutoAddSlots(type):
+    def __new__(cls, name, bases, _dict):
+        _dict["__slots__"] = _dict.get("__slots__", ())
+        return type.__new__(cls, name, bases, _dict)
+
+
+@add_metaclass(AutoAddSlots)
 class Obj(object):
     """Common ASN.1 object class
+
+    All ASN.1 types are inherited from it. It has metaclass that
+    automatically adds ``__slots__`` to all inherited classes.
     """
     __slots__ = (
         "tag",
@@ -649,6 +705,8 @@ class Obj(object):
 
     @property
     def ready(self):  # pragma: no cover
+        """Is object ready to be encoded?
+        """
         raise NotImplementedError()
 
     def _assert_ready(self):
@@ -657,9 +715,13 @@ class Obj(object):
 
     @property
     def decoded(self):
-        return self.llen > 0
+        """Is object decoded?
+        """
+        return (self.llen + self.vlen) > 0
 
     def copy(self):  # pragma: no cover
+        """Make a copy of object, safe to be mutated
+        """
         raise NotImplementedError()
 
     @property
@@ -673,6 +735,18 @@ class Obj(object):
     def __str__(self):  # pragma: no cover
         return self.__bytes__() if PY2 else self.__unicode__()
 
+    def __ne__(self, their):
+        return not(self == their)
+
+    def __gt__(self, their):  # pragma: no cover
+        return not(self < their)
+
+    def __le__(self, their):  # pragma: no cover
+        return (self == their) or (self < their)
+
+    def __ge__(self, their):  # pragma: no cover
+        return (self == their) or (self > their)
+
     def _encode(self):  # pragma: no cover
         raise NotImplementedError()
 
@@ -686,6 +760,14 @@ class Obj(object):
         return b"".join((self._expl, len_encode(len(raw)), raw))
 
     def decode(self, data, offset=0, leavemm=False, decode_path=()):
+        """Decode the data
+
+        :param data: either binary or memoryview
+        :param int offset: initial data's offset
+        :param bool leavemm: do we need to leave memoryview of remaining
+                    data as is, or convert it to bytes otherwise
+        :returns: (Obj, remaining data)
+        """
         tlv = memoryview(data)
         if self._expl is None:
             obj, tail = self._decode(
@@ -888,6 +970,15 @@ def pp_console_blob(pp):
 
 
 def pprint(obj, oids=None, big_blobs=False):
+    """Pretty print object
+
+    :param Obj obj: object you want to pretty print
+    :param oids: ``OID <-> humand readable string`` dictionary. When OID
+                 from it is met, then its humand readable form is printed
+    :param big_blobs: if large binary objects are met (like OctetString
+                      values), do we need to print them too, on separate
+                      lines
+    """
     def _pprint_pps(pps):
         for pp in pps:
             if hasattr(pp, "_fields"):
@@ -921,10 +1012,6 @@ class Boolean(Obj):
     True
     >>> bool(b)
     True
-    >>> Boolean(optional=True)
-    BOOLEAN OPTIONAL
-    >>> Boolean(impl=tag_ctxp(1), default=False)
-    [1] BOOLEAN False OPTIONAL DEFAULT
     """
     __slots__ = ()
     tag_default = tag_encode(1)
@@ -1119,10 +1206,6 @@ class Integer(Obj):
     True
     >>> int(b)
     -123
-    >>> Integer(optional=True)
-    INTEGER OPTIONAL
-    >>> Integer(impl=tag_ctxp(1), default=123)
-    [1] INTEGER 123 OPTIONAL DEFAULT
 
     >>> Integer(2, bounds=(1, 3))
     INTEGER 2
@@ -1256,10 +1339,7 @@ class Integer(Obj):
         )
 
     def __lt__(self, their):
-        return self._value < their
-
-    def __gt__(self, their):
-        return self._value > their
+        return self._value < their._value
 
     @property
     def named(self):
@@ -1875,6 +1955,9 @@ class OctetString(Obj):
             self._expl == their._expl
         )
 
+    def __lt__(self, their):
+        return self._value < their._value
+
     def __call__(
             self,
             value=None,
@@ -2242,10 +2325,7 @@ class ObjectIdentifier(Obj):
         )
 
     def __lt__(self, their):
-        return self._value < their
-
-    def __gt__(self, their):
-        return self._value > their
+        return self._value < their._value
 
     def __call__(
             self,
@@ -3275,6 +3355,86 @@ class Any(Obj):
 ########################################################################
 
 class Sequence(Obj):
+    """``SEQUENCE`` structure type
+
+    You have to make specification of sequence::
+
+        class Extension(Sequence):
+            __slots__ = ()
+            schema = (
+                ("extnID", ObjectIdentifier()),
+                ("critical", Boolean(default=False)),
+                ("extnValue", OctetString()),
+            )
+
+    Then, you can work with it as with dictionary.
+
+    >>> ext = Extension()
+    >>> Extension().specs
+    OrderedDict([
+        ('extnID', OBJECT IDENTIFIER),
+        ('critical', BOOLEAN False OPTIONAL DEFAULT),
+        ('extnValue', OCTET STRING),
+    ])
+    >>> ext["extnID"] = "1.2.3"
+    Traceback (most recent call last):
+    pyderasn.InvalidValueType: invalid value type, expected: <class 'pyderasn.ObjectIdentifier'>
+    >>> ext["extnID"] = ObjectIdentifier("1.2.3")
+
+    You can know if sequence is ready to be encoded:
+
+    >>> ext.ready
+    False
+    >>> ext.encode()
+    Traceback (most recent call last):
+    pyderasn.ObjNotReady: object is not ready: extnValue
+    >>> ext["extnValue"] = OctetString(b"foobar")
+    >>> ext.ready
+    True
+
+    Value you want to assign, must have the same **type** as in
+    corresponding specification, but it can have different tags,
+    optional/default attributes -- they will be taken from specification
+    automatically::
+
+        class TBSCertificate(Sequence):
+            schema = (
+                ("version", Version(expl=tag_ctxc(0), default="v1")),
+            [...]
+
+    >>> tbs = TBSCertificate()
+    >>> tbs["version"] = Version("v2") # no need to explicitly add ``expl``
+
+    You can know if value exists/set in the sequence and take its value:
+
+    >>> "extnID" in ext, "extnValue" in ext, "critical" in ext
+    (True, True, False)
+    >>> ext["extnID"]
+    OBJECT IDENTIFIER 1.2.3
+
+    But pay attention that if value has default, then it won't be (not
+    in) in the sequence (because ``DEFAULT`` must not be encoded in
+    DER), but you can read its value:
+
+    >>> "critical" in ext, ext["critical"]
+    (False, BOOLEAN False)
+    >>> ext["critical"] = Boolean(True)
+    >>> "critical" in ext, ext["critical"]
+    (True, BOOLEAN True)
+
+    All defaulted values are always optional.
+
+    .. warning::
+
+       When decoded DER contains defaulted value inside, then
+       technically this is not valid DER encoding. But we allow
+       and pass it. Of course reencoding of that kind of DER will
+       result in different binary representation (validly without
+       defaulted value inside).
+
+    Two sequences are equal if they have equal specification (schema),
+    implicit/explicit tagging and the same values.
+    """
     __slots__ = ("specs",)
     tag_default = tag_encode(form=TagFormConstructed, num=16)
     asn1_type_name = "SEQUENCE"
@@ -3518,6 +3678,10 @@ class Sequence(Obj):
 
 
 class Set(Sequence):
+    """``SET`` structure type
+
+    Its usage is identical to :py:class:`pyderasn.Sequence`.
+    """
     __slots__ = ()
     tag_default = tag_encode(form=TagFormConstructed, num=17)
     asn1_type_name = "SET"
@@ -3601,6 +3765,35 @@ class Set(Sequence):
 
 
 class SequenceOf(Obj):
+    """``SEQUENCE OF`` sequence type
+
+    For that kind of type you must specify the object it will carry on
+    (bounds are for example here, not required)::
+
+        class Ints(SequenceOf):
+            schema = Integer()
+            bounds = (0, 2)
+
+    >>> ints = Ints()
+    >>> ints.append(Integer(123))
+    >>> ints.append(Integer(234))
+    >>> ints
+    Ints SEQUENCE OF[INTEGER 123, INTEGER 234]
+    >>> [int(i) for i in ints]
+    [123, 234]
+    >>> ints.append(Integer(345))
+    Traceback (most recent call last):
+    pyderasn.BoundsError: unsatisfied bounds: 0 <= 3 <= 2
+    >>> ints[1]
+    INTEGER 234
+    >>> ints[1] = Integer(345)
+    >>> ints
+    Ints SEQUENCE OF[INTEGER 123, INTEGER 345]
+
+    Also you can initialize sequence with preinitialized values:
+
+    >>> ints = Ints([Integer(123), Integer(234)])
+    """
     __slots__ = ("spec", "_bound_min", "_bound_max")
     tag_default = tag_encode(form=TagFormConstructed, num=16)
     asn1_type_name = "SEQUENCE OF"
@@ -3841,6 +4034,10 @@ class SequenceOf(Obj):
 
 
 class SetOf(SequenceOf):
+    """``SET OF`` sequence type
+
+    Its usage is identical to :py:class:`pyderasn.SequenceOf`.
+    """
     __slots__ = ()
     tag_default = tag_encode(form=TagFormConstructed, num=17)
     asn1_type_name = "SET OF"
@@ -3884,7 +4081,7 @@ def main():  # pragma: no cover
     parser.add_argument(
         "DERFile",
         type=argparse.FileType("rb"),
-        help="Python path to schema definition to use",
+        help="Path to DER file you want to decode",
     )
     args = parser.parse_args()
     der = memoryview(args.DERFile.read())