VERSION | 2 +- doc/index.rst | 6 ++++++ doc/install.rst | 10 +++++----- doc/news.rst | 9 +++++++++ pyderasn.py | 130 ++++++++++++++++++++++++++++++++++++++++------------- tests/test_pyderasn.py | 8 ++++---- diff --git a/VERSION b/VERSION index 24b3f98e572ed79b336550ac9fc62a86d9a902822eacf155ba85853af320aa89..a233cfcdb584133924ee353c0e37f95386fd922c8fe14b78fef107e3d103bae5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1 +3.0 diff --git a/doc/index.rst b/doc/index.rst index 4776e1b35d9739319e0d511fd9219597286eb3ddc81d178b626ec30896583748..d394dd9fcf3538d64a2dbca1ddb7487276654ecd28dbe48ae779feaa157dbea8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,12 @@ PyDERASN is `free software `__, licenced under `GNU LGPLv3+ `__. +.. figure:: pprinting.png + :alt: Pretty printing example output + + An example of pretty printed X.509 certificate with automatically + parsed DEFINED BY fields. + .. toctree:: :maxdepth: 1 diff --git a/doc/install.rst b/doc/install.rst index 34f1b7aac98a97bf80c39e8a6d11fcd7da27fb274d0d58ed2516d24bd6d7dffd..666064e086158b3a04032178b6506e4458ee29dcbbb13af3dc4637d9f7c9ed3f 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -4,11 +4,11 @@ Preferable way is to :ref:`download ` tarball with the signature from `official website `__:: - % wget http://pyderasn.cypherpunks.ru/pyderasn-1.0.tar.xz - % wget http://pyderasn.cypherpunks.ru/pyderasn-1.0.tar.xz.sig - % gpg --verify pyderasn-1.0.tar.xz.sig pyderasn-1.0.tar.xz - % xz -d < pyderasn-1.0.tar.xz | tar xf - - % cd pyderasn-1.0 + % wget http://pyderasn.cypherpunks.ru/pyderasn-3.0.tar.xz + % wget http://pyderasn.cypherpunks.ru/pyderasn-3.0.tar.xz.sig + % gpg --verify pyderasn-3.0.tar.xz.sig pyderasn-3.0.tar.xz + % xz -d < pyderasn-3.0.tar.xz | tar xf - + % cd pyderasn-3.0 % python setup.py install # or copy pyderasn.py (+six.py) to your PYTHONPATH diff --git a/doc/news.rst b/doc/news.rst index 5d74df9c91cb3f042bb4f6e6930fdc9505d552214f30ce1c4e0d4fdb0779f548..d2b6bbfce554bceda337e5dece12524b581e1b53ebe0b590a4c7be8cc4ff4263 100644 --- a/doc/news.rst +++ b/doc/news.rst @@ -1,6 +1,15 @@ News ==== +.. _release3.0: + +3.0 +--- +* :py:func:`pyderasn.decode_path_defby` is replaced with + :py:class:`pyderasn.DecodePathDefBy` +* Ability to turn colourized terminal output by calling + ``pprint(..., with_colours=True)`` + .. _release2.1: 2.1 diff --git a/doc/pprinting.png b/doc/pprinting.png new file mode 100644 index 0000000000000000000000000000000000000000..91812dd5300d296dae6f4386260c1ed76d133365d9ae8135d41ce5fa0b36609b Binary files /dev/null and b/doc/pprinting.png differ diff --git a/pyderasn.py b/pyderasn.py index b0b38d530c9995618de7a29dcc54aed7359330c0bb7ba57ccf09ece0d9d16c3f..e09e82b83642f6eb46e7177d4423867eee203f4e9b14f0fda25af64bc788e64d 100755 --- a/pyderasn.py +++ b/pyderasn.py @@ -329,7 +329,7 @@ ), ( ( "content", - decode_path_defby(id_signedData), + DecodePathDefBy(id_signedData), "encapContentInfo", "eContentType", ), @@ -341,10 +341,10 @@ ), ( ( "content", - decode_path_defby(id_signedData), + DecodePathDefBy(id_signedData), "encapContentInfo", "eContent", - decode_path_defby(id_cct_PKIResponse), + DecodePathDefBy(id_cct_PKIResponse), "controlSequence", any, "attrType", @@ -358,7 +358,7 @@ })), ), )) -Pay attention for :py:func:`pyderasn.decode_path_defby` and ``any``. +Pay attention for :py:class:`pyderasn.DecodePathDefBy` and ``any``. First function is useful for path construction when some automatic decoding is already done. ``any`` means literally any value it meet -- useful for SEQUENCE/SET OF-s. @@ -486,6 +486,13 @@ from six import text_type from six.moves import xrange as six_xrange +try: + from termcolor import colored +except ImportError: + def colored(what, *args): + return what + + __all__ = ( "Any", "BitString", @@ -493,8 +500,8 @@ "BMPString", "Boolean", "BoundsError", "Choice", - "decode_path_defby", "DecodeError", + "DecodePathDefBy", "Enumerated", "GeneralizedTime", "GeneralString", @@ -1001,10 +1008,24 @@ def expl_tlvlen(self): return self.expl_tlen + self.expl_llen + self.expl_vlen -def decode_path_defby(defined_by): +class DecodePathDefBy(object): """DEFINED BY representation inside decode path """ - return "DEFINED BY (%s)" % defined_by + __slots__ = ('defined_by',) + + def __init__(self, defined_by): + self.defined_by = defined_by + + def __eq__(self, their): + if not isinstance(their, self.__class__): + return False + return self.defined_by == their.defined_by + + def __str__(self): + return "DEFINED BY " + str(self.defined_by) + + def __repr__(self): + return "<%s: %s>" % (self.__class__.__name__, self.defined_by) ######################################################################## @@ -1072,49 +1093,75 @@ expl_vlen, ) -def pp_console_row(pp, oids=None, with_offsets=False, with_blob=True): +def _colorize(what, colour, with_colours, attrs=("bold",)): + return colored(what, colour, attrs=attrs) if with_colours else what + + +def pp_console_row( + pp, + oids=None, + with_offsets=False, + with_blob=True, + with_colours=False, +): cols = [] if with_offsets: - cols.append("%5d%s [%d,%d,%4d]" % ( + col = "%5d%s" % ( pp.offset, ( " " if pp.expl_offset is None else ("-%d" % (pp.offset - pp.expl_offset)) ), - pp.tlen, - pp.llen, - pp.vlen, - )) + ) + cols.append(_colorize(col, "red", with_colours, ())) + col = "[%d,%d,%4d]" % (pp.tlen, pp.llen, pp.vlen) + cols.append(_colorize(col, "green", with_colours, ())) if len(pp.decode_path) > 0: cols.append(" ." * (len(pp.decode_path))) - cols.append("%s:" % pp.decode_path[-1]) + ent = pp.decode_path[-1] + if isinstance(ent, DecodePathDefBy): + cols.append(_colorize("DEFINED BY", "red", with_colours, ("reverse",))) + value = str(ent.defined_by) + if ( + oids is not None and + ent.defined_by.asn1_type_name == + ObjectIdentifier.asn1_type_name and + value in oids + ): + cols.append(_colorize("%s:" % oids[value], "green", with_colours)) + else: + cols.append(_colorize("%s:" % value, "white", with_colours)) + else: + cols.append(_colorize("%s:" % ent, "yellow", with_colours)) if pp.expl is not None: klass, _, num = pp.expl - cols.append("[%s%d] EXPLICIT" % (TagClassReprs[klass], num)) + col = "[%s%d] EXPLICIT" % (TagClassReprs[klass], num) + cols.append(_colorize(col, "blue", with_colours)) if pp.impl is not None: klass, _, num = pp.impl - cols.append("[%s%d]" % (TagClassReprs[klass], num)) + col = "[%s%d]" % (TagClassReprs[klass], num) + cols.append(_colorize(col, "blue", with_colours)) if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper(): - cols.append(pp.obj_name) - cols.append(pp.asn1_type_name) + cols.append(_colorize(pp.obj_name, "magenta", with_colours)) + cols.append(_colorize(pp.asn1_type_name, "cyan", with_colours)) if pp.value is not None: value = pp.value + cols.append(_colorize(value, "white", with_colours)) if ( oids is not None and pp.asn1_type_name == ObjectIdentifier.asn1_type_name and value in oids ): - value = "%s (%s)" % (oids[value], pp.value) - cols.append(value) + cols.append(_colorize("(%s)" % oids[value], "green", with_colours)) if with_blob: if isinstance(pp.blob, binary_type): cols.append(hexenc(pp.blob)) elif isinstance(pp.blob, tuple): cols.append(", ".join(pp.blob)) if pp.optional: - cols.append("OPTIONAL") + cols.append(_colorize("OPTIONAL", "red", with_colours)) if pp.default: - cols.append("DEFAULT") + cols.append(_colorize("DEFAULT", "red", with_colours)) return " ".join(cols) @@ -1133,7 +1180,7 @@ elif isinstance(pp.blob, tuple): yield " ".join(cols + [", ".join(pp.blob)]) -def pprint(obj, oids=None, big_blobs=False): +def pprint(obj, oids=None, big_blobs=False, with_colours=False): """Pretty print object :param Obj obj: object you want to pretty print @@ -1142,6 +1189,8 @@ 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 + :param with_colours: colourize output, if ``termcolor`` library + is available """ def _pprint_pps(pps): for pp in pps: @@ -1152,11 +1201,18 @@ pp, oids=oids, with_offsets=True, with_blob=False, + with_colours=with_colours, ) for row in pp_console_blob(pp): yield row else: - yield pp_console_row(pp, oids=oids, with_offsets=True) + yield pp_console_row( + pp, + oids=oids, + with_offsets=True, + with_blob=True, + with_colours=with_colours, + ) else: for row in _pprint_pps(pp): yield row @@ -2009,7 +2065,7 @@ ) defined_by, defined = self.defined or (None, None) if defined_by is not None: yield defined.pps( - decode_path=decode_path + (decode_path_defby(defined_by),) + decode_path=decode_path + (DecodePathDefBy(defined_by),) ) @@ -2233,7 +2289,7 @@ ) defined_by, defined = self.defined or (None, None) if defined_by is not None: yield defined.pps( - decode_path=decode_path + (decode_path_defby(defined_by),) + decode_path=decode_path + (DecodePathDefBy(defined_by),) ) @@ -3545,7 +3601,7 @@ ) defined_by, defined = self.defined or (None, None) if defined_by is not None: yield defined.pps( - decode_path=decode_path + (decode_path_defby(defined_by),) + decode_path=decode_path + (DecodePathDefBy(defined_by),) ) @@ -3871,7 +3927,7 @@ if issubclass(value.__class__, SequenceOf): for i, _value in enumerate(value): sub_sub_decode_path = sub_decode_path + ( str(i), - decode_path_defby(defined_by), + DecodePathDefBy(defined_by), ) defined_value, defined_tail = defined_spec.decode( memoryview(bytes(_value)), @@ -3899,14 +3955,14 @@ (value.tlen + value.llen + value.expl_tlen + value.expl_llen) if value.expled else (value.tlen + value.llen) ), leavemm=True, - decode_path=sub_decode_path + (decode_path_defby(defined_by),), + decode_path=sub_decode_path + (DecodePathDefBy(defined_by),), ctx=ctx, ) if len(defined_tail) > 0: raise DecodeError( "remaining data", klass=self.__class__, - decode_path=sub_decode_path + (decode_path_defby(defined_by),), + decode_path=sub_decode_path + (DecodePathDefBy(defined_by),), offset=offset, ) value.defined = (defined_by, defined_value) @@ -4397,7 +4453,7 @@ class SEQUENCEOF(SequenceOf): __slots__ = () schema = choice - def pprint_any(obj, oids=None): + def pprint_any(obj, oids=None, with_colours=False): def _pprint_pps(pps): for pp in pps: if hasattr(pp, "_fields"): @@ -4411,6 +4467,7 @@ pp, oids=oids, with_offsets=True, with_blob=False, + with_colours=with_colours, ) for row in pp_console_blob(pp): yield row @@ -4443,6 +4500,11 @@ "--defines-by-path", help="Python path to decoder's defines_by_path", ) parser.add_argument( + "--with-colours", + action='store_true', + help="Enable coloured output", + ) + parser.add_argument( "DERFile", type=argparse.FileType("rb"), help="Path to DER file you want to decode", @@ -4465,7 +4527,11 @@ None if args.defines_by_path is None else {"defines_by_path": obj_by_path(args.defines_by_path)} ), ) - print(pprinter(obj, oids=oids)) + print(pprinter( + obj, + oids=oids, + with_colours=True if args.with_colours else False, + )) if tail != b"": print("\nTrailing data: %s" % hexenc(tail)) diff --git a/tests/test_pyderasn.py b/tests/test_pyderasn.py index 69c8a3925da522f061caa970e02976fa28bcc04788e9e51454aa6505262d45e9..ddcdf1e49ca985e2a598178c948c25b72872ffbd497e5182d8c5265c0ee11708 100644 --- a/tests/test_pyderasn.py +++ b/tests/test_pyderasn.py @@ -57,8 +57,8 @@ from pyderasn import BMPString from pyderasn import Boolean from pyderasn import BoundsError from pyderasn import Choice -from pyderasn import decode_path_defby from pyderasn import DecodeError +from pyderasn import DecodePathDefBy from pyderasn import Enumerated from pyderasn import GeneralizedTime from pyderasn import GeneralString @@ -5017,7 +5017,7 @@ seq_inner = seq_sequenced["value"].defined[1] self.assertIsNone(seq_inner["valueInner"].defined) defines_by_path.append(( - ("value", decode_path_defby(type_sequenced), "typeInner"), + ("value", DecodePathDefBy(type_sequenced), "typeInner"), ((("valueInner",), {type_innered: Pairs()}),), )) seq_sequenced, _ = Seq().decode( @@ -5036,9 +5036,9 @@ defines_by_path.append(( ( "value", - decode_path_defby(type_sequenced), + DecodePathDefBy(type_sequenced), "valueInner", - decode_path_defby(type_innered), + DecodePathDefBy(type_innered), any, "type", ),