--- /dev/null
+[run]
+branch = True
+
+[report]
+show_missing = True
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+
+ if __name__ == .__main__.:
--- /dev/null
+*.pyc
+.coverage
+.coverage.*
+.hypothesis
+__pycache__
+dist
--- /dev/null
+* Sergey Matveev <stargrave@stargrave.org>
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
--- /dev/null
+doc/install.rst
\ No newline at end of file
--- /dev/null
+include .coveragerc
+include AUTHORS
+include COPYING*
+include INSTALL
+include NEWS
+include nose.cfg
+include pip-requirements*
+include PUBKEY.asc
+include pyderasn.pyi
+include README
+include THANKS
+include VERSION
--- /dev/null
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFnCv/YBCAC8TD+EhE5qDTwHj25OKer4baCpvIMgGYprAmwiUkVq0Wq38on/
+xszHcKcLShApVQXfx32bx+laXdokv1BAPJygrbAo3ocwuo/tBDNlqQ1Tm9vdAhz9
+8S6B7UTLsyO4WkhC9h6wHPMEG4VuSxyHwF+lg5wcbDIHZRn029UctYSjdBXYL/YL
+kPfywzLW6sB7FdDWv2Eb7SosTGz9T4kgco3a8cqUbtroawRmXw+AdyPDsxKf2fMP
+xpBHhPQvWCnd294keJJ7EiqM11u9f0yRfSotVJU6UHETTgtdPwtP19RLfEqFUWUc
+ZYTq2da1cAzBuseczZXbLk01njg0io2YPB0BABEBAAG0K1B5REVSQVNOIHJlbGVh
+c2VzIDxweWRlcmFzbkBjeXBoZXJwdW5rcy5ydT6JAVcEEwEIAEEWIQQu1shGMFEC
+31tOA4MEqTPRuiAyegUCWcK/9gIbAwwLCgkNCAwHCwMEAQIHFQoJCAsDAgUWAgED
+AAIeAQIXgAAKCRAEqTPRuiAyelKFCACAM/hmkkjGDcZn9zRma717CrRr84LrbdOF
+EfS+cWwcLpqeI3YmGPBW6hP94MnZuVcUJIVIhZ1C1/DHP32u3xDW0uj2VXwBLCQt
+k9regkGOYVMW0l+MKY4Z81KgJSfX+kOq8RfLW0sq1bf91a/id8u/IEsyPHN34XLI
+kptAFf6b0Wl6VU7nJiie25XI8DaYX98q7tYoD5yOSxzcCJ4IRAbAKg1B5RNTTZuG
+y8RmHsszF3sJ5wLuGk1vpSh1jgq61RUquQYJa1iE2B8fxpL6Qr+T8IR2Jan4TFIn
+vzGeBXtCD2yUIeJgSeF/3VoEq8lxJ+rwHwcsIqHF7QdqJCc7S0wviHUEEBEIAB0W
+IQTPYOiaWSMeduJjZCKuGoEJ5JhX7wUCWcLAIAAKCRCuGoEJ5JhX7+lbAP9+WNA4
+Uk0pNH5BAASabuT+zllnHZ5SqZoKWbs7bzWfogD+NWmjTfSJCr7GSZ4Suy3Vw4nn
+hUu3L6dceWUU+hAEOBw=
+=Qodb
+-----END PGP PUBLIC KEY BLOCK-----
--- /dev/null
+PyDERASN -- ASN.1 DER library for Python
+
+* Basic ASN.1 data types (X.208): BOOLEAN, INTEGER, BIT STRING, OCTET
+ STRING, NULL, OBJECT IDENTIFIER, ENUMERATED, all strings, UTCTime,
+ GeneralizedTime, CHOICE, ANY, SEQUENCE (OF), SET (OF)
+* Size constraints checking
+* Working with sequences as high level data objects with ability to
+ (un)marshall them
+* Python 2.7/3.5 compatibility
+* __slots__ friendliness
+* Ability to know exact decoded objects offset and lengths in the binary
+* Pretty printer and command-line decoder, that could conveniently
+ replace utilities like either dumpasn1 or openssl asn1parse
+
+pyderasn is free software: see the file COPYING.LESSER for copying conditions.
+
+PyDERASN home page is: http://pyderasn.cypherpunks.ru/
+
+Please send questions, bug reports and patches to
+https://lists.cypherpunks.ru/mailman/listinfo/pyderasn-devel
+mailing list. Announcements also go to this mailing list.
+
+Development Git source code repository currently is located here:
+https://git.cypherpunks.ru/cgit.cgi/pyderasn.git/
--- /dev/null
+html:
+ python -msphinx . _build/html
--- /dev/null
+extensions = ["sphinx.ext.autodoc"]
+templates_path = ["_templates"]
+source_suffix = ".rst"
+master_doc = "index"
+project = "pyderasn"
+copyright = "2017, Sergey Matveev"
+author = "Sergey Matveev"
+version = "1.0"
+release = "1.0"
+language = None
+exclude_patterns = ["_build"]
+pygments_style = "sphinx"
+todo_include_todos = False
+html_theme = "classic"
+html_static_path = ["_static"]
+html_sidebars = {}
--- /dev/null
+.. _download:
+
+Download
+========
+
+.. list-table::
+ :widths: 10 10 20 60
+ :header-rows: 1
+
+ * - Package
+ - Size
+ - Tarball
+ - SHA256 checksum
+ * - ``attrs`` 17.2.0
+ - 59 KiB
+ - `link <download/attrs-17.2.0.tar.xz>`__
+ `sign <download/attrs-17.2.0.tar.xz.sig>`__
+ - ``612F3F53 90F2D0C7 FCA6A32A B5B1E750 5BC56C00 1D68B28F 56B7446D 6970DC0A``
+ * - ``coverage`` 4.4.1
+ - 287 KiB
+ - `link <coverage-4.4.1.tar.xz>`__
+ `sign <coverage-4.4.1.tar.xz.sig>`__
+ - ``DF312773 C59A0CB5 1EB793F5 BA14A1D5 54D467D6 C46375F1 8E066DAA B8A86271``
+ * - ``enum34`` 1.1.6
+ - 31 KiB
+ - `link <enum34-1.1.6.tar.xz>`__
+ `sign <enum34-1.1.6.tar.xz.sig>`__
+ - ``CC26B270 E58910E6 B54ACEE9 EC36C388 4C9BE18B 7A55FA46 305D4BA9 18D00177``
+ * - ``hypothesis`` 3.30.4
+ - 102 KiB
+ - `link <hypothesis-3.30.4.tar.xz>`__
+ `sign <hypothesis-3.30.4.tar.xz.sig>`__
+ - ``A6281672 88FDCC15 EA806C45 9EBEF827 8D2A8BAD 01DB7C61 BD45D14A 905F53D6``
+ * - ``six`` 1.11.0
+ - 25 KiB
+ - `link <six-1.11.0.tar.xz>`__
+ `sign <six-1.11.0.tar.xz.sig>`__
+ - ``890AC076 5EF9AEFA 5079CEBA ADE9C680 DBFB0E84 E7CFA1F9 9B9B43A8 5FA80126``
+
+Development Git source code repository is located here:
+https://git.cypherpunks.ru/cgit.cgi/pyderasn.git/.
--- /dev/null
+Examples
+========
+
+.. contents::
+
+Schema definition
+-----------------
+
+Let's try to parse X.509 certificate. We have to define our structures
+based on ASN.1 schema descriptions.
+
+.. list-table::
+ :header-rows: 1
+
+ * - ASN.1 specification
+ - pyderasn's code
+ * - ::
+
+ Certificate ::= SEQUENCE {
+ tbsCertificate TBSCertificate,
+ signatureAlgorithm AlgorithmIdentifier,
+ signatureValue BIT STRING }
+ - ::
+
+ class Certificate(Sequence):
+ schema = (
+ ("tbsCertificate", TBSCertificate()),
+ ("signatureAlgorithm", AlgorithmIdentifier()),
+ ("signatureValue", BitString()),
+ )
+ * - ::
+
+ AlgorithmIdentifier ::= SEQUENCE {
+ algorithm OBJECT IDENTIFIER,
+ parameters ANY DEFINED BY algorithm OPTIONAL }
+ - ::
+
+ class AlgorithmIdentifier(Sequence):
+ schema = (
+ ("algorithm", ObjectIdentifier()),
+ ("parameters", Any(optional=True)),
+ )
+ * - ::
+
+ TBSCertificate ::= SEQUENCE {
+ version [0] EXPLICIT Version DEFAULT v1,
+ serialNumber CertificateSerialNumber,
+ signature AlgorithmIdentifier,
+ issuer Name,
+ validity Validity,
+ subject Name,
+ subjectPublicKeyInfo SubjectPublicKeyInfo,
+ issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
+ subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
+ extensions [3] EXPLICIT Extensions OPTIONAL }
+ - ::
+
+ class TBSCertificate(Sequence):
+ schema = (
+ ("version", Version(expl=tag_ctxc(0), default="v1")),
+ ("serialNumber", CertificateSerialNumber()),
+ ("signature", AlgorithmIdentifier()),
+ ("issuer", Name()),
+ ("validity", Validity()),
+ ("subject", Name()),
+ ("subjectPublicKeyInfo", SubjectPublicKeyInfo()),
+ ("issuerUniqueID", UniqueIdentifier(impl=tag_ctxp(1), optional=True)),
+ ("subjectUniqueID", UniqueIdentifier(impl=tag_ctxp(2), optional=True)),
+ ("extensions", Extensions(expl=tag_ctxc(3), optional=True)),
+ )
+ * - ::
+
+ Version ::= INTEGER { v1(0), v2(1), v3(2) }
+ - ::
+
+ class Version(Integer):
+ schema = (("v1", 0), ("v2", 1), ("v3", 2))
+ * - ::
+
+ CertificateSerialNumber ::= INTEGER
+ - ::
+
+ class CertificateSerialNumber(Integer):
+ pass
+ * - ::
+
+ Validity ::= SEQUENCE {
+ notBefore Time,
+ notAfter Time }
+ Time ::= CHOICE {
+ utcTime UTCTime,
+ generalTime GeneralizedTime }
+ - ::
+
+ class Validity(Sequence):
+ schema = (
+ ("notBefore", Time()),
+ ("notAfter", Time()),
+ )
+ class Time(Choice):
+ schema = (
+ ("utcTime", UTCTime()),
+ ("generalTime", GeneralizedTime()),
+ )
+ * - ::
+
+ SubjectPublicKeyInfo ::= SEQUENCE {
+ algorithm AlgorithmIdentifier,
+ subjectPublicKey BIT STRING }
+ - ::
+
+ class SubjectPublicKeyInfo(Sequence):
+ schema = (
+ ("algorithm", AlgorithmIdentifier()),
+ ("subjectPublicKey", BitString()),
+ )
+ * - ::
+
+ UniqueIdentifier ::= BIT STRING
+ - ::
+
+ class UniqueIdentifier(BitString):
+ pass
+ * - ::
+
+ Name ::= CHOICE { rdnSequence RDNSequence }
+
+ RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
+
+ RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
+
+ AttributeTypeAndValue ::= SEQUENCE { type AttributeType, value AttributeValue }
+
+ AttributeType ::= OBJECT IDENTIFIER
+
+ AttributeValue ::= ANY -- DEFINED BY AttributeType
+ - ::
+
+ class Name(Choice):
+ schema = (("rdnSequence", RDNSequence()),)
+ class RDNSequence(SequenceOf):
+ schema = RelativeDistinguishedName()
+ class RelativeDistinguishedName(SetOf):
+ schema = AttributeTypeAndValue()
+ bounds = (1, float("+inf"))
+ class AttributeTypeAndValue(Sequence):
+ schema = (
+ ("type", AttributeType()),
+ ("value", AttributeValue()),
+ )
+ class AttributeType(ObjectIdentifier):
+ pass
+ class AttributeValue(Any):
+ pass
+ * - ::
+
+ Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
+
+ Extension ::= SEQUENCE {
+ extnID OBJECT IDENTIFIER,
+ critical BOOLEAN DEFAULT FALSE,
+ extnValue OCTET STRING
+ }
+ - ::
+
+ class Extensions(SequenceOf):
+ schema = Extension()
+ bounds = (1, float("+inf"))
+ class Extension(Sequence):
+ schema = (
+ ("extnID", ObjectIdentifier()),
+ ("critical", Boolean(default=False)),
+ ("extnValue", OctetString()),
+ )
+
+We are ready to decode PayPal's certificate from Go `encoding/asn1
+<https://golang.org/pkg/encoding/asn1/>`__ test suite (assuming that
+it's DER encoded representation is already in ``raw`` variable)::
+
+ >>> crt, tail = Certificate().decode(raw)
+ >>> crt
+ Certificate SEQUENCE[TBSCertificate SEQUENCE[[0] EXPLICIT Version
+ INTEGER v3 OPTIONAL, CertificateSerialNumber INTEGER 61595,
+ AlgorithmIdentifier SEQUENCE[OBJECT IDENTIFIER 1.2.840.113549.1.1.5...
+
+Pretty printing
+---------------
+
+There is huge output. Let's pretty print it::
+
+ >>> print(pprint(crt))
+ 0 [1,3,1604] Certificate SEQUENCE
+ 4 [1,3,1453] . tbsCertificate: TBSCertificate SEQUENCE
+ 10-2 [1,1, 1] . . version: [0] EXPLICIT Version INTEGER v3 OPTIONAL
+ 13 [1,1, 3] . . serialNumber: CertificateSerialNumber INTEGER 61595
+ 18 [1,1, 13] . . signature: AlgorithmIdentifier SEQUENCE
+ 20 [1,1, 9] . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+ 31 [0,0, 2] . . . parameters: [UNIV 5] ANY OPTIONAL
+ . . . . 05:00
+ 33 [0,0, 278] . . issuer: Name CHOICE rdnSequence
+ 33 [1,3, 274] . . . rdnSequence: RDNSequence SEQUENCE OF
+ 37 [1,1, 11] . . . . 0: RelativeDistinguishedName SET OF
+ 39 [1,1, 9] . . . . . 0: AttributeTypeAndValue SEQUENCE
+ 41 [1,1, 3] . . . . . . type: AttributeType OBJECT IDENTIFIER 2.5.4.6
+ 46 [0,0, 4] . . . . . . value: [UNIV 19] AttributeValue ANY
+ . . . . . . . 13:02:45:53
+ [...]
+ 1461 [1,1, 13] . signatureAlgorithm: AlgorithmIdentifier SEQUENCE
+ 1463 [1,1, 9] . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+ 1474 [0,0, 2] . . parameters: [UNIV 5] ANY OPTIONAL
+ . . . 05:00
+ 1476 [1,2, 129] . signatureValue: BIT STRING 1024 bits
+ . . 68:EE:79:97:97:DD:3B:EF:16:6A:06:F2:14:9A:6E:CD
+ . . 9E:12:F7:AA:83:10:BD:D1:7C:98:FA:C7:AE:D4:0E:2C
+ [...]
+
+ Trailing data: 0a
+
+Let's parse that output, human::
+
+ 10-2 [1,1, 1] . . version: [0] EXPLICIT Version INTEGER v3 OPTIONAL
+ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
+ 0 1 2 3 4 5 6 7 8 9 10 11
+
+::
+
+ 20 [1,1, 9] . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+ ^ ^ ^ ^ ^ ^ ^ ^
+ 0 2 3 4 5 6 9 10
+
+::
+
+ 33 [0,0, 278] . . issuer: Name CHOICE rdnSequence
+ ^ ^ ^ ^ ^ ^ ^ ^ ^
+ 0 2 3 4 5 6 8 9 10
+
+:0:
+ Offset of the object, where its DER encoding begins.
+ Pay attention that it does **not** include explicit tag.
+:1:
+ If explicit tag exists, then this is its length (tag + encoded length).
+:2:
+ Length of object's tag. For example CHOICE does not have its own tag,
+ so it is zero.
+:3:
+ Length of encoded length.
+:4:
+ Length of encoded value.
+:5:
+ Visual indentation to show the depth of object in the hierarchy.
+:6:
+ Object's name inside SEQUENCE/CHOICE.
+:7:
+ If either IMPLICIT or EXPLICIT tag is set, then it will be shown
+ here. "IMPLICIT" is omitted.
+:8:
+ Object's class name, if set. Omitted if it is just an ordinary simple
+ value (like with ``algorithm`` in example above).
+:9:
+ Object's ASN.1 type.
+:10:
+ Object's value, if set. Can consist of multiple words (like OCTET/BIT
+ STRINGs above). We see ``v3`` value in Version, because it is named.
+ ``rdnSequence`` is the choice of CHOICE type.
+:11:
+ Possible other flags like OPTIONAL and DEFAULT, if value equals to the
+ default one, specified in the schema.
+
+As command line utility
+-----------------------
+
+You can decode DER files using command line abilities and get the same
+picture as above by executing::
+
+ % pyderasn.py --schema tests.test_crts:Certificate path/to/file
+
+If there is no schema for you file, then you can try parsing it without,
+but of course IMPLICIT tags will often make it impossible. But result is
+good enough for the certificate above::
+
+ % pyderasn.py path/to/file
+ 0 [1,3,1604] . >: SEQUENCE OF
+ 4 [1,3,1453] . . >: SEQUENCE OF
+ 8 [0,0, 5] . . . . >: [0] ANY
+ . . . . . A0:03:02:01:02
+ 13 [1,1, 3] . . . . >: INTEGER 61595
+ 18 [1,1, 13] . . . . >: SEQUENCE OF
+ 20 [1,1, 9] . . . . . . >: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+ 31 [1,1, 0] . . . . . . >: NULL
+ 33 [1,3, 274] . . . . >: SEQUENCE OF
+ 37 [1,1, 11] . . . . . . >: SET OF
+ 39 [1,1, 9] . . . . . . . . >: SEQUENCE OF
+ 41 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER 2.5.4.6
+ 46 [1,1, 2] . . . . . . . . . . >: PrintableString PrintableString ES
+ [...]
+ 1409 [1,1, 50] . . . . . . >: SEQUENCE OF
+ 1411 [1,1, 8] . . . . . . . . >: OBJECT IDENTIFIER 1.3.6.1.5.5.7.1.1
+ 1421 [1,1, 38] . . . . . . . . >: OCTET STRING 38 bytes
+ . . . . . . . . . 30:24:30:22:06:08:2B:06:01:05:05:07:30:01:86:16
+ . . . . . . . . . 68:74:74:70:3A:2F:2F:6F:63:73:70:2E:69:70:73:63
+ . . . . . . . . . 61:2E:63:6F:6D:2F
+ 1461 [1,1, 13] . . >: SEQUENCE OF
+ 1463 [1,1, 9] . . . . >: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+ 1474 [1,1, 0] . . . . >: NULL
+ 1476 [1,2, 129] . . >: BIT STRING 1024 bits
+ . . . 68:EE:79:97:97:DD:3B:EF:16:6A:06:F2:14:9A:6E:CD
+ . . . 9E:12:F7:AA:83:10:BD:D1:7C:98:FA:C7:AE:D4:0E:2C
+ [...]
+
+If you have got dictionaries with ObjectIdentifiers, like example one
+from ``tests/test_crts.py``::
+
+ some_oids = {
+ "1.2.840.113549.1.1.1": "id-rsaEncryption",
+ "1.2.840.113549.1.1.5": "id-sha1WithRSAEncryption",
+ [...]
+ "2.5.4.10": "id-at-organizationName",
+ "2.5.4.11": "id-at-organizationalUnitName",
+ }
+
+then you can pass it to pretty printer to see human readable OIDs::
+
+ % pyderasn.py --oids tests.test_crts:some_oids path/to/file
+ [...]
+ 37 [1,1, 11] . . . . . . >: SET OF
+ 39 [1,1, 9] . . . . . . . . >: SEQUENCE OF
+ 41 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER id-at-countryName (2.5.4.6)
+ 46 [1,1, 2] . . . . . . . . . . >: PrintableString PrintableString ES
+ 50 [1,1, 18] . . . . . . >: SET OF
+ 52 [1,1, 16] . . . . . . . . >: SEQUENCE OF
+ 54 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER id-at-stateOrProvinceName (2.5.4.8)
+ 59 [1,1, 9] . . . . . . . . . . >: PrintableString PrintableString Barcelona
+ 70 [1,1, 18] . . . . . . >: SET OF
+ 72 [1,1, 16] . . . . . . . . >: SEQUENCE OF
+ 74 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER id-at-localityName (2.5.4.7)
+ 79 [1,1, 9] . . . . . . . . . . >: PrintableString PrintableString Barcelona
+ [...]
+
+Descriptive errors
+------------------
+
+If you have bad DER, then errors will show you where error occurred::
+
+ % pyderasn.py --schema tests.test_crts:Certificate path/to/bad/file
+ Traceback (most recent call last):
+ [...]
+ pyderasn.DecodeError: UTCTime (tbsCertificate.validity.notAfter.utcTime) (at 328) invalid UTCTime format
+
+::
+
+ % pyderasn.py path/to/bad/file
+ [...]
+ pyderasn.DecodeError: UTCTime (0.SequenceOf.4.SequenceOf.1.UTCTime) (at 328) invalid UTCTime format
+
+You can see, so called, decode path inside the structures:
+``tbsCertificate`` -> ``validity`` -> ``notAfter`` -> ``utcTime`` and
+that object at byte 328 is invalid.
+
+X.509 certificate creation
+--------------------------
+
+Let's create some simple self-signed X.509 certificate from the ground::
+
+ tbs = TBSCertificate()
+ tbs["serialNumber"] = CertificateSerialNumber(10143011886257155224)
+
+ sign_algo_id = AlgorithmIdentifier()
+ sign_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5")
+ sign_algo_id["parameters"] = Any(Null())
+ tbs["signature"] = sign_algo_id
+
+ rdnSeq = RDNSequence()
+ for oid, klass, text in (
+ ("2.5.4.6", PrintableString, "XX"),
+ ("2.5.4.8", PrintableString, "Some-State"),
+ ("2.5.4.7", PrintableString, "City"),
+ ("2.5.4.10", PrintableString, "Internet Widgits Pty Ltd"),
+ ("2.5.4.3", PrintableString, "false.example.com"),
+ ("1.2.840.113549.1.9.1", IA5String, "false@example.com"),
+ ):
+ attr = AttributeTypeAndValue()
+ attr["type"] = AttributeType(oid)
+ attr["value"] = AttributeValue(klass(text))
+ rdn = RelativeDistinguishedName()
+ rdn.append(attr)
+ rdnSeq.append(rdn)
+ issuer = Name()
+ issuer["rdnSequence"] = rdnSeq
+ tbs["issuer"] = issuer
+ tbs["subject"] = issuer
+
+ validity = Validity()
+ validity["notBefore"] = Time(("utcTime", UTCTime(datetime(2009, 10, 8, 0, 25, 53))))
+ validity["notAfter"] = Time(("utcTime", UTCTime(datetime(2010, 10, 8, 0, 25, 53))))
+ tbs["validity"] = validity
+
+ spki = SubjectPublicKeyInfo()
+ spki_algo_id = sign_algo_id.copy()
+ spki_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.1")
+ spki["algorithm"] = spki_algo_id
+ spki["subjectPublicKey"] = BitString(hexdec("".join((
+ "3048024100cdb7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695",
+ "a331b1deadeadd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8f",
+ "deec357d0203010001",
+ ))))
+ tbs["subjectPublicKeyInfo"] = spki
+
+ crt = Certificate()
+ crt["tbsCertificate"] = tbs
+ crt["signatureAlgorithm"] = sign_algo_id
+ crt["signatureValue"] = BitString(hexdec("".join((
+ "a67b06ec5ece92772ca413cba3ca12568fdc6c7b4511cd40a7f659980402df2b",
+ "998bb9a4a8cbeb34c0f0a78cf8d91ede14a5ed76bf116fe360aafa8821490435",
+ ))))
+ crt.encode()
+
+And we will get the same certificate used in Go's library tests.
--- /dev/null
+Features
+========
+
+* Basic ASN.1 data types (X.208): BOOLEAN, INTEGER, BIT STRING, OCTET
+ STRING, NULL, OBJECT IDENTIFIER, ENUMERATED, all strings, UTCTime,
+ GeneralizedTime, CHOICE, ANY, SEQUENCE (OF), SET (OF)
+* Size constraints checking
+* Working with sequences as high level data objects with ability to
+ (un)marshall them
+* Python 2.7/3.5 compatibility
+
+Why yet another library? `pyasn1 <https://github.com/etingof/pyasn1>`__
+had all of this a long time ago. PyDERASN resembles it in many ways. In
+practice it should be relatively easy to convert ``pyasn1``'s code to
+``pyderasn``'s one. But additionally it offers:
+
+* Small, simple and trying to be reviewable code. Just a single file
+* ``__slots__`` friendliness
+* Ability to know exact decoded objects offsets and lengths in the binary
+* Pretty printer and command-line decoder, that could conveniently
+ replace utilities like either ``dumpasn1`` or ``openssl asn1parse``
+* Some kind of strong typing: SEQUENCEs require the exact **type** of
+ settable values, even when they are inherited
+* However they do not require tags matching: IMPLICIT/EXPLICIT tags will
+ be set automatically in the given sequence
+* Could be significantly faster. For example parsing of CACert.org's CRL
+ under Python 3.5.2:
+
+ :``pyderasn.py revoke.crl``:
+ ~2 min
+ :``pyderasn.py --schema path.to.CertificateList revoke.crl``:
+ ~38 sec
+ :``pyasn1.decode(asn1Spec=pyasn1.CertificateList())``:
+ ~22 min (``pyasn1 == 0.2.3``)
+
+There are drawbacks:
+
+* No old Python versions support
+* No BER/CER support
+* PyDERASN does **not** have object recreation capable ``repr``-s::
+
+ pyderasn>>> repr(algo_id)
+ AlgorithmIdentifier SEQUENCE[OBJECT IDENTIFIER 1.3.14.3.2.26, [UNIV 5] ANY 0500 OPTIONAL]
+
+ pyasn1>>> repr(algo_id)
+ AlgorithmIdentifier().setComponents(ObjectIdentifier('1.3.14.3.2.26'), Any(hexValue='0500'))
+
+* Strings are not validated in any way, except just trying to be decoded
+ in ``ascii``, ``iso-8859-1``, ``utf-8/16/32`` correspondingly
+* No REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING
--- /dev/null
+Feedback
+========
+
+Please send questions regarding the use of PyDERASN, bug reports and
+patches to `pyderasn-devel <https://lists.cypherpunks.ru/pipermail/pyderasn-devel/>`__
+mailing list. Announcements also go to this mailing list.
+
+Official website is http://pyderasn.cypherpunks.ru/.
+Development Git source code repository is located here:
+https://git.cypherpunks.ru/cgit.cgi/pyderasn.git/.
--- /dev/null
+========================================
+PyDERASN -- ASN.1 DER library for Python
+========================================
+
+..
+
+ I'm going to build my own ASN.1 library with slots and blobs!
+ (C) PyDERASN's author
+
+`ASN.1 <https://en.wikipedia.org/wiki/ASN.1>`__ (Abstract Syntax
+Notation One) is a standard for abstract data serialization.
+`DER <https://en.wikipedia.org/wiki/Distinguished_Encoding_Rules>`__
+(Distinguished Encoding Rules) is a subset of encoding rules suitable
+and widely used in cryptography-related stuff. PyDERASN is yet another
+library for dealing with the data encoded that way. Although ASN.1 is
+written more than 30 years ago by wise Ancients (taken from ``pyasn1``'s
+README), it is still often can be seen anywhere in our life.
+
+PyDERASN is `free software <https://www.gnu.org/philosophy/free-sw.html>`__,
+licenced under `GNU LGPLv3+ <https://www.gnu.org/licenses/lgpl-3.0.html>`__.
+
+.. toctree::
+ :maxdepth: 1
+
+ features
+ examples
+ reference
+ install
+ download
+ feedback
--- /dev/null
+Install
+=======
+
+Preferable way is to :ref:`download <download>` tarball with the
+signature from `official website <http://pyderasn.cypherpunks.ru/>`__::
+
+ % 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
+ % python setup.py install
+
+PyDERASN depends on `six <https://pypi.python.org/pypi/six>`__ package
+for keeping compatibility with Py27/Py35. If it is not installed on your
+system, then ``setup.py install`` will try to download it from PyPI. You
+can also find it mirrored on :ref:`download <download>` page.
+
+You could use PIP (**no** authentication is performed!)::
+
+ % pip install pyderasn
+
+You have to verify downloaded tarballs integrity and authenticity to be
+sure that you retrieved trusted and untampered software. `GNU Privacy
+Guard <https://www.gnupg.org/>`__ is used for that purpose.
+
+For the very first time it is necessary to get signing public key and
+import it. It is provided below, but you should check alternative
+resources.
+
+::
+
+ pub rsa2048/0x04A933D1BA20327A 2017-09-20
+ 2ED6 C846 3051 02DF 5B4E 0383 04A9 33D1 BA20 327A
+ uid PyDERASN releases <pyderasn@cypherpunks.ru>
+
+ % gpg --keyserver hkp://keys.gnupg.net/ --recv-keys 0x04A933D1BA20327A
+ % gpg --auto-key-locate dane --locate-keys pyderasn at cypherpunks dot ru
+ % gpg --auto-key-locate wkd --locate-keys pyderasn at cypherpunks dot ru
+ % gpg --auto-key-locate pka --locate-keys pyderasn at cypherpunks dot ru
+
+.. literalinclude:: ../PUBKEY.asc
--- /dev/null
+alabaster == 0.7.10
+babel == 2.5.1
+certifi == 2017.7.27.1
+chardet == 3.0.4
+docutils == 0.14
+idna == 2.6
+imagesize == 0.7.1
+Jinja2 == 2.9.6
+MarkupSafe == 1.0
+Pygments == 2.2.0
+pytz == 2017.2
+requests == 2.18.4
+snowballstemmer == 1.2.1
+sphinx == 1.6.3
+sphinxcontrib-websupport == 1.0.1
+urllib3 == 1.22
--- /dev/null
+Library reference
+=================
+
+.. contents::
+
+.. automodule:: pyderasn
--- /dev/null
+[nosetests]
+tests=tests
+verbosity=2
+with-runnable-test-names=1
+nocapture=1
+with-coverage=1
+cover-erase=1
+cover-package=pyderasn
+processes=-1
+process-timeout=1800
--- /dev/null
+attrs==17.2.0
+coverage==4.4.1
+enum34==1.1.6 ; python_version == '2.7'
+hypothesis==3.30.4
--- /dev/null
+six==1.11.0
--- /dev/null
+#!/usr/bin/env python
+# coding: utf-8
+# PyDERASN -- Python ASN.1 DER codec with abstract structures
+# Copyright (C) 2017 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+"""Python ASN.1 DER codec with abstract structures
+
+This library allows you to marshal and unmarshal various structures in
+ASN.1 DER format, like this:
+
+ >>> i = Integer(123)
+ >>> raw = i.encode()
+ >>> Integer().decode(raw) == i
+ True
+
+There are primitive types, holding single values
+(:py:class:`pyderasn.BitString`,
+:py:class:`pyderasn.Boolean`,
+:py:class:`pyderasn.Enumerated`,
+:py:class:`pyderasn.GeneralizedTime`,
+:py:class:`pyderasn.Integer`,
+:py:class:`pyderasn.Null`,
+:py:class:`pyderasn.ObjectIdentifier`,
+:py:class:`pyderasn.OctetString`,
+:py:class:`pyderasn.UTCTime`,
+:py:class:`various strings <pyderasn.CommonString>`
+(:py:class:`pyderasn.BMPString`,
+:py:class:`pyderasn.GeneralString`,
+:py:class:`pyderasn.GraphicString`,
+:py:class:`pyderasn.IA5String`,
+:py:class:`pyderasn.ISO646String`,
+:py:class:`pyderasn.NumericString`,
+:py:class:`pyderasn.PrintableString`,
+:py:class:`pyderasn.T61String`,
+:py:class:`pyderasn.TeletexString`,
+:py:class:`pyderasn.UniversalString`,
+:py:class:`pyderasn.UTF8String`,
+:py:class:`pyderasn.VideotexString`,
+:py:class:`pyderasn.VisibleString`)),
+constructed types, holding multiple primitive types
+(:py:class:`pyderasn.Sequence`,
+:py:class:`pyderasn.SequenceOf`,
+:py:class:`pyderasn.Set`,
+:py:class:`pyderasn.SetOf`),
+and special types like
+:py:class:`pyderasn.Any` and
+:py:class:`pyderasn.Choice`.
+
+Common for most types
+---------------------
+
+Tags
+____
+
+Most types in ASN.1 has specific tag for them. ``Obj.tag_default`` is
+the default tag used during coding process. You can override it with
+either ``IMPLICIT`` (using ``impl`` keyword argument), or
+``EXPLICIT`` one (using ``expl`` keyword argument). Both arguments takes
+raw binary string, containing that tag. You can **not** set implicit and
+explicit tags simultaneously.
+
+There are :py:func:`pyderasn.tag_ctxp` and :py:func:`pyderasn.tag_ctxc`
+functions, allowing you to easily create ``CONTEXT``
+``PRIMITIVE``/``CONSTRUCTED`` tags, by specifying only the required tag
+number. Pay attention that explicit tags always have *constructed* tag
+(``tag_ctxc``), but implicit tags for primitive types are primitive
+(``tag_ctxp``).
+
+::
+
+ >>> Integer(impl=tag_ctxp(1))
+ [1] INTEGER
+ >>> Integer(expl=tag_ctxc(2))
+ [2] EXPLICIT INTEGER
+
+Implicit tag is not explicitly shown.
+
+Two object of the same type, but with different implicit/explicit tags
+are **not** equal.
+
+You can get objects effective tag (either default or implicited) through
+``tag`` property. You can decode it using :py:func:`pyderasn.tag_decode`
+function::
+
+ >>> tag_decode(tag_ctxc(123))
+ (128, 32, 123)
+ >>> klass, form, num = tag_decode(tag_ctxc(123))
+ >>> klass == TagClassContext
+ True
+ >>> form == TagFormConstructed
+ True
+
+To determine if object has explicit tag, use ``expled`` boolean property
+and ``expl_tag`` property, returning explicit tag's value.
+
+Default/optional
+________________
+
+Many objects in sequences could be ``OPTIONAL`` and could have
+``DEFAULT`` value. You can specify that object's property using
+corresponding keyword arguments.
+
+ >>> Integer(optional=True, default=123)
+ INTEGER 123 OPTIONAL DEFAULT
+
+Those specifications do not play any role in primitive value encoding,
+but are taken into account when dealing with sequences holding them. For
+example ``TBSCertificate`` sequence holds defaulted, explicitly tagged
+``version`` field::
+
+ class Version(Integer):
+ schema = (
+ ("v1", 0),
+ ("v2", 1),
+ ("v3", 2),
+ )
+ class TBSCertificate(Sequence):
+ schema = (
+ ("version", Version(expl=tag_ctxc(0), default="v1")),
+ [...]
+
+When default argument is used and value is not specified, then it equals
+to default one.
+
+Size constraints
+________________
+
+Some objects give ability to set value size constraints. This is either
+possible integer value, or allowed length of various strings and
+sequences. Constraints are set in the following way::
+
+ class X(...):
+ bounds = (MIN, MAX)
+
+And values satisfaction is checked as: ``MIN <= X <= MAX``.
+
+For simplicity you can also set bounds the following way::
+
+ bounded_x = X(bounds=(MIN, MAX))
+
+If bounds are not satisfied, then :py:exc:`pyderasn.BoundsError` is
+raised.
+
+Common methods
+______________
+
+All objects have ``ready`` boolean property, that tells if it is ready
+to be encoded. If that kind of action is performed on unready object,
+then :py:exc:`pyderasn.ObjNotReady` exception will be raised.
+
+All objects have ``copy()`` method, returning its copy, that can be safely
+mutated.
+
+Decoding
+________
+
+Decoding is performed using ``decode()`` method. ``offset`` optional
+argument could be used to set initial object's offset in the binary
+data, for convenience. It returns decoded object and remaining
+unmarshalled data (tail). Internally all work is done on
+``memoryview(data)``, and you can leave returning tail as a memoryview,
+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
+* ``tlen`` -- length of object's tag
+* ``llen`` -- length of object's length value
+* ``vlen`` -- length of object's value
+* ``tlvlen`` -- length of the whole object
+
+Pay attention that those values do **not** include anything related to
+explicit tag. If you want to know information about it, then use:
+``expled`` (to know if explicit tag is set), ``expl_offset`` (it is
+lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen``
+(that actually equals to ordinary ``tlvlen``).
+
+When error occurs, then :py:exc:`pyderasn.DecodeError` is raised.
+
+Pretty printing
+_______________
+
+All objects have ``pps()`` method, that is a generator of
+:py:class:`pyderasn.PP` namedtuple, holding various raw information
+about the object. If ``pps`` is called on sequences, then all underlying
+``PP`` will be yielded.
+
+You can use :py:func:`pyderasn.pp_console_row` function, converting
+those ``PP`` to human readable string. Actually exactly it is used for
+all object ``repr``. But it is easy to write custom formatters.
+
+ >>> from pyderasn import pprint
+ >>> encoded = Integer(-12345).encode()
+ >>> obj, tail = Integer().decode(encoded)
+ >>> print(pprint(obj))
+ 0 [1,1, 2] INTEGER -12345
+
+Primitive types
+---------------
+
+Boolean
+_______
+.. autoclass:: pyderasn.Boolean
+ :members: __init__
+
+Integer
+_______
+.. autoclass:: pyderasn.Integer
+ :members: __init__
+
+BitString
+_________
+.. autoclass:: pyderasn.BitString
+ :members: __init__
+
+OctetString
+___________
+.. autoclass:: pyderasn.OctetString
+ :members: __init__
+
+Null
+____
+.. autoclass:: pyderasn.Null
+ :members: __init__
+
+ObjectIdentifier
+________________
+.. autoclass:: pyderasn.ObjectIdentifier
+ :members: __init__
+
+Enumerated
+__________
+.. autoclass:: pyderasn.Enumerated
+
+CommonString
+____________
+.. autoclass:: pyderasn.CommonString
+
+UTCTime
+_______
+.. autoclass:: pyderasn.UTCTime
+ :members: __init__, todatetime
+
+GeneralizedTime
+_______________
+.. autoclass:: pyderasn.GeneralizedTime
+
+Special types
+-------------
+
+Choice
+______
+.. autoclass:: pyderasn.Choice
+ :members: __init__
+
+PrimitiveTypes
+______________
+.. autoclass:: PrimitiveTypes
+
+Any
+___
+.. autoclass:: pyderasn.Any
+ :members: __init__
+
+Constructed types
+-----------------
+
+Sequence
+________
+.. autoclass:: pyderasn.Sequence
+ :members: __init__
+
+Set
+___
+.. autoclass:: pyderasn.Set
+ :members: __init__
+
+SequenceOf
+__________
+.. autoclass:: pyderasn.SequenceOf
+ :members: __init__
+
+SetOf
+_____
+.. autoclass:: pyderasn.SetOf
+ :members: __init__
+"""
+
+from codecs import getdecoder
+from codecs import getencoder
+from collections import namedtuple
+from collections import OrderedDict
+from datetime import datetime
+from math import ceil
+
+from six import binary_type
+from six import byte2int
+from six import indexbytes
+from six import int2byte
+from six import integer_types
+from six import iterbytes
+from six import PY2
+from six import string_types
+from six import text_type
+from six.moves import xrange as six_xrange
+
+
+__all__ = (
+ "Any",
+ "BitString",
+ "BMPString",
+ "Boolean",
+ "BoundsError",
+ "Choice",
+ "DecodeError",
+ "Enumerated",
+ "GeneralizedTime",
+ "GeneralString",
+ "GraphicString",
+ "hexdec",
+ "hexenc",
+ "IA5String",
+ "Integer",
+ "InvalidLength",
+ "InvalidOID",
+ "InvalidValueType",
+ "ISO646String",
+ "NotEnoughData",
+ "Null",
+ "NumericString",
+ "obj_by_path",
+ "ObjectIdentifier",
+ "ObjNotReady",
+ "ObjUnknown",
+ "OctetString",
+ "PrimitiveTypes",
+ "PrintableString",
+ "Sequence",
+ "SequenceOf",
+ "Set",
+ "SetOf",
+ "T61String",
+ "tag_ctxc",
+ "tag_ctxp",
+ "tag_decode",
+ "TagClassApplication",
+ "TagClassContext",
+ "TagClassPrivate",
+ "TagClassUniversal",
+ "TagFormConstructed",
+ "TagFormPrimitive",
+ "TagMismatch",
+ "TeletexString",
+ "UniversalString",
+ "UTCTime",
+ "UTF8String",
+ "VideotexString",
+ "VisibleString",
+)
+
+TagClassUniversal = 0
+TagClassApplication = 1 << 6
+TagClassContext = 1 << 7
+TagClassPrivate = 1 << 6 | 1 << 7
+TagFormPrimitive = 0
+TagFormConstructed = 1 << 5
+TagClassReprs = {
+ TagClassContext: "",
+ TagClassApplication: "APPLICATION ",
+ TagClassPrivate: "PRIVATE ",
+ TagClassUniversal: "UNIV ",
+}
+
+
+########################################################################
+# Errors
+########################################################################
+
+class DecodeError(Exception):
+ def __init__(self, msg="", klass=None, decode_path=(), offset=0):
+ super(DecodeError, self).__init__()
+ self.msg = msg
+ self.klass = klass
+ self.decode_path = decode_path
+ self.offset = offset
+
+ def __str__(self):
+ return " ".join(
+ c for c in (
+ "" if self.klass is None else self.klass.__name__,
+ (
+ ("(%s)" % ".".join(self.decode_path))
+ if len(self.decode_path) > 0 else ""
+ ),
+ ("(at %d)" % self.offset) if self.offset > 0 else "",
+ self.msg,
+ ) if c != ""
+ )
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self)
+
+
+class NotEnoughData(DecodeError):
+ pass
+
+
+class TagMismatch(DecodeError):
+ pass
+
+
+class InvalidLength(DecodeError):
+ pass
+
+
+class InvalidOID(DecodeError):
+ pass
+
+
+class ObjUnknown(ValueError):
+ def __init__(self, name):
+ super(ObjUnknown, self).__init__()
+ self.name = name
+
+ def __str__(self):
+ return "object is unknown: %s" % self.name
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self)
+
+
+class ObjNotReady(ValueError):
+ def __init__(self, name):
+ super(ObjNotReady, self).__init__()
+ self.name = name
+
+ def __str__(self):
+ return "object is not ready: %s" % self.name
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self)
+
+
+class InvalidValueType(ValueError):
+ def __init__(self, expected_types):
+ super(InvalidValueType, self).__init__()
+ self.expected_types = expected_types
+
+ def __str__(self):
+ return "invalid value type, expected: %s" % ", ".join(
+ [repr(t) for t in self.expected_types]
+ )
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self)
+
+
+class BoundsError(ValueError):
+ def __init__(self, bound_min, value, bound_max):
+ super(BoundsError, self).__init__()
+ self.bound_min = bound_min
+ self.value = value
+ self.bound_max = bound_max
+
+ def __str__(self):
+ return "unsatisfied bounds: %s <= %s <= %s" % (
+ self.bound_min,
+ self.value,
+ self.bound_max,
+ )
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self)
+
+
+########################################################################
+# Basic coders
+########################################################################
+
+_hexdecoder = getdecoder("hex")
+_hexencoder = getencoder("hex")
+
+
+def hexdec(data):
+ return _hexdecoder(data)[0]
+
+
+def hexenc(data):
+ return _hexencoder(data)[0].decode("ascii")
+
+
+def int_bytes_len(num, byte_len=8):
+ if num == 0:
+ return 1
+ return int(ceil(float(num.bit_length()) / byte_len))
+
+
+def zero_ended_encode(num):
+ octets = bytearray(int_bytes_len(num, 7))
+ i = len(octets) - 1
+ octets[i] = num & 0x7F
+ num >>= 7
+ i -= 1
+ while num > 0:
+ octets[i] = 0x80 | (num & 0x7F)
+ num >>= 7
+ i -= 1
+ return bytes(octets)
+
+
+def tag_encode(num, klass=TagClassUniversal, form=TagFormPrimitive):
+ if num < 31:
+ # [XX|X|.....]
+ return int2byte(klass | form | num)
+ # [XX|X|11111][1.......][1.......] ... [0.......]
+ return int2byte(klass | form | 31) + zero_ended_encode(num)
+
+
+def tag_decode(tag):
+ """
+ assume that data is validated
+ """
+ first_octet = byte2int(tag)
+ klass = first_octet & 0xC0
+ form = first_octet & 0x20
+ if first_octet & 0x1F < 0x1F:
+ return (klass, form, first_octet & 0x1F)
+ num = 0
+ for octet in iterbytes(tag[1:]):
+ num <<= 7
+ num |= octet & 0x7F
+ return (klass, form, num)
+
+
+def tag_ctxp(num):
+ return tag_encode(num=num, klass=TagClassContext, form=TagFormPrimitive)
+
+
+def tag_ctxc(num):
+ return tag_encode(num=num, klass=TagClassContext, form=TagFormConstructed)
+
+
+def tag_strip(data):
+ """Take off tag from the data
+
+ :returns: (encoded tag, tag length, remaining data)
+ """
+ if len(data) == 0:
+ raise NotEnoughData("no data at all")
+ if byte2int(data) & 0x1F < 31:
+ return data[:1], 1, data[1:]
+ i = 0
+ while True:
+ i += 1
+ if i == len(data):
+ raise DecodeError("unfinished tag")
+ if indexbytes(data, i) & 0x80 == 0:
+ break
+ i += 1
+ return data[:i], i, data[i:]
+
+
+def len_encode(l):
+ if l < 0x80:
+ return int2byte(l)
+ octets = bytearray(int_bytes_len(l) + 1)
+ octets[0] = 0x80 | (len(octets) - 1)
+ for i in six_xrange(len(octets) - 1, 0, -1):
+ octets[i] = l & 0xFF
+ l >>= 8
+ return bytes(octets)
+
+
+def len_decode(data):
+ if len(data) == 0:
+ raise NotEnoughData("no data at all")
+ first_octet = byte2int(data)
+ if first_octet & 0x80 == 0:
+ return first_octet, 1, data[1:]
+ octets_num = first_octet & 0x7F
+ if octets_num + 1 > len(data):
+ raise NotEnoughData("encoded length is longer than data")
+ if octets_num == 0:
+ raise DecodeError("long form instead of short one")
+ if byte2int(data[1:]) == 0:
+ raise DecodeError("leading zeros")
+ l = 0
+ for v in iterbytes(data[1:1 + octets_num]):
+ l = (l << 8) | v
+ if l <= 127:
+ raise DecodeError("long form instead of short one")
+ return l, 1 + octets_num, data[1 + octets_num:]
+
+
+########################################################################
+# Base class
+########################################################################
+
+class Obj(object):
+ """Common ASN.1 object class
+ """
+ __slots__ = (
+ "tag",
+ "_value",
+ "_expl",
+ "default",
+ "optional",
+ "offset",
+ "llen",
+ "vlen",
+ )
+
+ def __init__(
+ self,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ if impl is None:
+ self.tag = getattr(self, "impl", self.tag_default)
+ else:
+ self.tag = impl
+ self._expl = getattr(self, "expl", None) if expl is None else expl
+ if self.tag != self.tag_default and self._expl is not None:
+ raise ValueError(
+ "implicit and explicit tags can not be set simultaneously"
+ )
+ if default is not None:
+ optional = True
+ self.optional = optional
+ self.offset, self.llen, self.vlen = _decoded
+ self.default = None
+
+ @property
+ def ready(self): # pragma: no cover
+ raise NotImplementedError()
+
+ def _assert_ready(self):
+ if not self.ready:
+ raise ObjNotReady(self.__class__.__name__)
+
+ @property
+ def decoded(self):
+ return self.llen > 0
+
+ def copy(self): # pragma: no cover
+ raise NotImplementedError()
+
+ @property
+ def tlen(self):
+ return len(self.tag)
+
+ @property
+ def tlvlen(self):
+ return self.tlen + self.llen + self.vlen
+
+ def __str__(self): # pragma: no cover
+ return self.__bytes__() if PY2 else self.__unicode__()
+
+ def _encode(self): # pragma: no cover
+ raise NotImplementedError()
+
+ def _decode(self, tlv, offset=0, decode_path=()): # pragma: no cover
+ raise NotImplementedError()
+
+ def encode(self):
+ raw = self._encode()
+ if self._expl is None:
+ return raw
+ return b"".join((self._expl, len_encode(len(raw)), raw))
+
+ def decode(self, data, offset=0, leavemm=False, decode_path=()):
+ tlv = memoryview(data)
+ if self._expl is None:
+ obj, tail = self._decode(
+ tlv,
+ offset,
+ decode_path=decode_path,
+ )
+ else:
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self._expl:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj, tail = self._decode(
+ v,
+ offset=offset + tlen + llen,
+ decode_path=(),
+ )
+ return obj, (tail if leavemm else tail.tobytes())
+
+ @property
+ def expled(self):
+ return self._expl is not None
+
+ @property
+ def expl_tag(self):
+ return self._expl
+
+ @property
+ def expl_tlen(self):
+ return len(self._expl)
+
+ @property
+ def expl_llen(self):
+ return len(len_encode(self.tlvlen))
+
+ @property
+ def expl_offset(self):
+ return self.offset - self.expl_tlen - self.expl_llen
+
+ @property
+ def expl_vlen(self):
+ return self.tlvlen
+
+ @property
+ def expl_tlvlen(self):
+ return self.expl_tlen + self.expl_llen + self.expl_vlen
+
+
+########################################################################
+# Pretty printing
+########################################################################
+
+PP = namedtuple("PP", (
+ "asn1_type_name",
+ "obj_name",
+ "decode_path",
+ "value",
+ "blob",
+ "optional",
+ "default",
+ "impl",
+ "expl",
+ "offset",
+ "tlen",
+ "llen",
+ "vlen",
+ "expl_offset",
+ "expl_tlen",
+ "expl_llen",
+ "expl_vlen",
+))
+
+
+def _pp(
+ asn1_type_name="unknown",
+ obj_name="unknown",
+ decode_path=(),
+ value=None,
+ blob=None,
+ optional=False,
+ default=False,
+ impl=None,
+ expl=None,
+ offset=0,
+ tlen=0,
+ llen=0,
+ vlen=0,
+ expl_offset=None,
+ expl_tlen=None,
+ expl_llen=None,
+ expl_vlen=None,
+):
+ return PP(
+ asn1_type_name,
+ obj_name,
+ decode_path,
+ value,
+ blob,
+ optional,
+ default,
+ impl,
+ expl,
+ offset,
+ tlen,
+ llen,
+ vlen,
+ expl_offset,
+ expl_tlen,
+ expl_llen,
+ expl_vlen,
+ )
+
+
+def pp_console_row(pp, oids=None, with_offsets=False, with_blob=True):
+ cols = []
+ if with_offsets:
+ cols.append("%5d%s [%d,%d,%4d]" % (
+ pp.offset,
+ (
+ " " if pp.expl_offset is None else
+ ("-%d" % (pp.offset - pp.expl_offset))
+ ),
+ pp.tlen,
+ pp.llen,
+ pp.vlen,
+ ))
+ if len(pp.decode_path) > 0:
+ cols.append(" ." * (len(pp.decode_path)))
+ cols.append("%s:" % pp.decode_path[-1])
+ if pp.expl is not None:
+ klass, _, num = pp.expl
+ cols.append("[%s%d] EXPLICIT" % (TagClassReprs[klass], num))
+ if pp.impl is not None:
+ klass, _, num = pp.impl
+ cols.append("[%s%d]" % (TagClassReprs[klass], num))
+ if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper():
+ cols.append(pp.obj_name)
+ cols.append(pp.asn1_type_name)
+ if pp.value is not None:
+ value = pp.value
+ 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)
+ 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")
+ if pp.default:
+ cols.append("DEFAULT")
+ return " ".join(cols)
+
+
+def pp_console_blob(pp):
+ cols = [" " * len("XXXXXYY [X,X,XXXX]")]
+ if len(pp.decode_path) > 0:
+ cols.append(" ." * (len(pp.decode_path) + 1))
+ if isinstance(pp.blob, binary_type):
+ blob = hexenc(pp.blob).upper()
+ for i in range(0, len(blob), 32):
+ chunk = blob[i:i + 32]
+ yield " ".join(cols + [":".join(
+ chunk[j:j + 2] for j in range(0, len(chunk), 2)
+ )])
+ elif isinstance(pp.blob, tuple):
+ yield " ".join(cols + [", ".join(pp.blob)])
+
+
+def pprint(obj, oids=None, big_blobs=False):
+ def _pprint_pps(pps):
+ for pp in pps:
+ if hasattr(pp, "_fields"):
+ if big_blobs:
+ yield pp_console_row(
+ pp,
+ oids=oids,
+ with_offsets=True,
+ with_blob=False,
+ )
+ for row in pp_console_blob(pp):
+ yield row
+ else:
+ yield pp_console_row(pp, oids=oids, with_offsets=True)
+ else:
+ for row in _pprint_pps(pp):
+ yield row
+ return "\n".join(_pprint_pps(obj.pps()))
+
+
+########################################################################
+# ASN.1 primitive types
+########################################################################
+
+class Boolean(Obj):
+ """``BOOLEAN`` boolean type
+
+ >>> b = Boolean(True)
+ BOOLEAN True
+ >>> b == Boolean(True)
+ 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)
+ asn1_type_name = "BOOLEAN"
+
+ def __init__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either boolean type, or
+ :py:class:`pyderasn.Boolean` object
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(Boolean, self).__init__(impl, expl, default, optional, _decoded)
+ self._value = None if value is None else self._value_sanitize(value)
+ if default is not None:
+ default = self._value_sanitize(default)
+ self.default = self.__class__(
+ value=default,
+ impl=self.tag,
+ expl=self._expl,
+ )
+ if value is None:
+ self._value = default
+
+ def _value_sanitize(self, value):
+ if issubclass(value.__class__, Boolean):
+ return value._value
+ if isinstance(value, bool):
+ return value
+ raise InvalidValueType((self.__class__, bool))
+
+ @property
+ def ready(self):
+ return self._value is not None
+
+ def copy(self):
+ obj = self.__class__()
+ obj._value = self._value
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __nonzero__(self):
+ self._assert_ready()
+ return self._value
+
+ def __bool__(self):
+ self._assert_ready()
+ return self._value
+
+ def __eq__(self, their):
+ if isinstance(their, bool):
+ return self._value == their
+ if not issubclass(their.__class__, Boolean):
+ return False
+ return (
+ self._value == their._value and
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ def __call__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def _encode(self):
+ self._assert_ready()
+ return b"".join((
+ self.tag,
+ len_encode(1),
+ (b"\xFF" if self._value else b"\x00"),
+ ))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, _, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, _, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l != 1:
+ raise InvalidLength(
+ "Boolean's length must be equal to 1",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ first_octet = byte2int(v)
+ if first_octet == 0:
+ value = False
+ elif first_octet == 0xFF:
+ value = True
+ else:
+ raise DecodeError(
+ "unacceptable Boolean value",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj = self.__class__(
+ value=value,
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, 1, 1),
+ )
+ return obj, v[1:]
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=str(self._value) if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+class Integer(Obj):
+ """``INTEGER`` integer type
+
+ >>> b = Integer(-123)
+ INTEGER -123
+ >>> b == Integer(-123)
+ 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
+ >>> Integer(5, bounds=(1, 3))
+ Traceback (most recent call last):
+ pyderasn.BoundsError: unsatisfied bounds: 1 <= 5 <= 3
+
+ ::
+
+ class Version(Integer):
+ schema = (
+ ("v1", 0),
+ ("v2", 1),
+ ("v3", 2),
+ )
+
+ >>> v = Version("v1")
+ Version INTEGER v1
+ >>> int(v)
+ 0
+ >>> v.named
+ 'v1'
+ >>> v.specs
+ {'v3': 2, 'v1': 0, 'v2': 1}
+ """
+ __slots__ = ("specs", "_bound_min", "_bound_max")
+ tag_default = tag_encode(2)
+ asn1_type_name = "INTEGER"
+
+ def __init__(
+ self,
+ value=None,
+ bounds=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _specs=None,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either integer type, named value
+ (if ``schema`` is specified in the class), or
+ :py:class:`pyderasn.Integer` object
+ :param bounds: set ``(MIN, MAX)`` value constraint.
+ (-inf, +inf) by default
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(Integer, self).__init__(impl, expl, default, optional, _decoded)
+ self._value = value
+ specs = getattr(self, "schema", {}) if _specs is None else _specs
+ self.specs = specs if isinstance(specs, dict) else dict(specs)
+ if bounds is None:
+ self._bound_min, self._bound_max = getattr(
+ self,
+ "bounds",
+ (float("-inf"), float("+inf")),
+ )
+ else:
+ self._bound_min, self._bound_max = bounds
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default = self._value_sanitize(default)
+ self.default = self.__class__(
+ value=default,
+ impl=self.tag,
+ expl=self._expl,
+ _specs=self.specs,
+ )
+ if self._value is None:
+ self._value = default
+
+ def _value_sanitize(self, value):
+ if issubclass(value.__class__, Integer):
+ value = value._value
+ elif isinstance(value, integer_types):
+ pass
+ elif isinstance(value, str):
+ value = self.specs.get(value)
+ if value is None:
+ raise ObjUnknown("integer value: %s" % value)
+ else:
+ raise InvalidValueType((self.__class__, int, str))
+ if not self._bound_min <= value <= self._bound_max:
+ raise BoundsError(self._bound_min, value, self._bound_max)
+ return value
+
+ @property
+ def ready(self):
+ return self._value is not None
+
+ def copy(self):
+ obj = self.__class__(_specs=self.specs)
+ obj._value = self._value
+ obj._bound_min = self._bound_min
+ obj._bound_max = self._bound_max
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __int__(self):
+ self._assert_ready()
+ return int(self._value)
+
+ def __hash__(self):
+ self._assert_ready()
+ return hash(
+ self.tag +
+ bytes(self._expl or b"") +
+ str(self._value).encode("ascii"),
+ )
+
+ def __eq__(self, their):
+ if isinstance(their, integer_types):
+ return self._value == their
+ if not issubclass(their.__class__, Integer):
+ return False
+ return (
+ self._value == their._value and
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ def __lt__(self, their):
+ return self._value < their
+
+ def __gt__(self, their):
+ return self._value > their
+
+ @property
+ def named(self):
+ for name, value in self.specs.items():
+ if value == self._value:
+ return name
+
+ def __call__(
+ self,
+ value=None,
+ bounds=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ bounds=(
+ (self._bound_min, self._bound_max)
+ if bounds is None else bounds
+ ),
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ _specs=self.specs,
+ )
+
+ def _encode(self):
+ self._assert_ready()
+ value = self._value
+ if PY2:
+ if value == 0:
+ octets = bytearray([0])
+ elif value < 0:
+ value = -value
+ value -= 1
+ octets = bytearray()
+ while value > 0:
+ octets.append((value & 0xFF) ^ 0xFF)
+ value >>= 8
+ if len(octets) == 0 or octets[-1] & 0x80 == 0:
+ octets.append(0xFF)
+ else:
+ octets = bytearray()
+ while value > 0:
+ octets.append(value & 0xFF)
+ value >>= 8
+ if octets[-1] & 0x80 > 0:
+ octets.append(0x00)
+ octets.reverse()
+ octets = bytes(octets)
+ else:
+ bytes_len = ceil(value.bit_length() / 8) or 1
+ while True:
+ try:
+ octets = value.to_bytes(
+ bytes_len,
+ byteorder="big",
+ signed=True,
+ )
+ except OverflowError:
+ bytes_len += 1
+ else:
+ break
+ return b"".join((self.tag, len_encode(len(octets)), octets))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, _, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l == 0:
+ raise NotEnoughData(
+ "zero length",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ first_octet = byte2int(v)
+ if l > 1:
+ second_octet = byte2int(v[1:])
+ if (
+ ((first_octet == 0x00) and (second_octet & 0x80 == 0)) or
+ ((first_octet == 0xFF) and (second_octet & 0x80 != 0))
+ ):
+ raise DecodeError(
+ "non normalized integer",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if PY2:
+ value = 0
+ if first_octet & 0x80 > 0:
+ octets = bytearray()
+ for octet in bytearray(v):
+ octets.append(octet ^ 0xFF)
+ for octet in octets:
+ value = (value << 8) | octet
+ value += 1
+ value = -value
+ else:
+ for octet in bytearray(v):
+ value = (value << 8) | octet
+ else:
+ value = int.from_bytes(v, byteorder="big", signed=True)
+ try:
+ obj = self.__class__(
+ value=value,
+ bounds=(self._bound_min, self._bound_max),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _specs=self.specs,
+ _decoded=(offset, llen, l),
+ )
+ except BoundsError as err:
+ raise DecodeError(
+ msg=str(err),
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ return obj, tail
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=(self.named or str(self._value)) if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+class BitString(Obj):
+ """``BIT STRING`` bit string type
+
+ >>> BitString(b"hello world")
+ BIT STRING 88 bits 68656c6c6f20776f726c64
+ >>> bytes(b)
+ b'hello world'
+ >>> b == b"hello world"
+ True
+ >>> b.bit_len
+ 88
+
+ >>> b = BitString("'010110000000'B")
+ BIT STRING 12 bits 5800
+ >>> b.bit_len
+ 12
+ >>> b[0], b[1], b[2], b[3]
+ (False, True, False, True)
+ >>> b[1000]
+ False
+ >>> [v for v in b]
+ [False, True, False, True, True, False, False, False, False, False, False, False]
+
+ ::
+
+ class KeyUsage(BitString):
+ schema = (
+ ('digitalSignature', 0),
+ ('nonRepudiation', 1),
+ ('keyEncipherment', 2),
+ )
+
+ >>> b = KeyUsage(('keyEncipherment', 'nonRepudiation'))
+ KeyUsage BIT STRING 3 bits nonRepudiation, keyEncipherment
+ >>> b.named
+ ['nonRepudiation', 'keyEncipherment']
+ >>> b.specs
+ {'nonRepudiation': 1, 'digitalSignature': 0, 'keyEncipherment': 2}
+ """
+ __slots__ = ("specs",)
+ tag_default = tag_encode(3)
+ asn1_type_name = "BIT STRING"
+
+ def __init__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _specs=None,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either binary type, tuple of named
+ values (if ``schema`` is specified in the class),
+ string in ``'XXX...'B`` form, or
+ :py:class:`pyderasn.BitString` object
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(BitString, self).__init__(impl, expl, default, optional, _decoded)
+ specs = getattr(self, "schema", {}) if _specs is None else _specs
+ self.specs = specs if isinstance(specs, dict) else dict(specs)
+ self._value = None if value is None else self._value_sanitize(value)
+ if default is not None:
+ default = self._value_sanitize(default)
+ self.default = self.__class__(
+ value=default,
+ impl=self.tag,
+ expl=self._expl,
+ )
+ if value is None:
+ self._value = default
+
+ def _bits2octets(self, bits):
+ if len(self.specs) > 0:
+ bits = bits.rstrip("0")
+ bit_len = len(bits)
+ bits += "0" * ((8 - (bit_len % 8)) % 8)
+ octets = bytearray(len(bits) // 8)
+ for i in six_xrange(len(octets)):
+ octets[i] = int(bits[i * 8:(i * 8) + 8], 2)
+ return bit_len, bytes(octets)
+
+ def _value_sanitize(self, value):
+ if issubclass(value.__class__, BitString):
+ return value._value
+ if isinstance(value, (string_types, binary_type)):
+ if (
+ isinstance(value, string_types) and
+ value.startswith("'") and
+ value.endswith("'B")
+ ):
+ value = value[1:-2]
+ if not set(value) <= set(("0", "1")):
+ raise ValueError("B's coding contains unacceptable chars")
+ return self._bits2octets(value)
+ elif isinstance(value, binary_type):
+ return (len(value) * 8, value)
+ else:
+ raise InvalidValueType((
+ self.__class__,
+ string_types,
+ binary_type,
+ ))
+ if isinstance(value, tuple):
+ if (
+ len(value) == 2 and
+ isinstance(value[0], integer_types) and
+ isinstance(value[1], binary_type)
+ ):
+ return value
+ bits = []
+ for name in value:
+ bit = self.specs.get(name)
+ if bit is None:
+ raise ObjUnknown("BitString value: %s" % name)
+ bits.append(bit)
+ if len(bits) == 0:
+ return self._bits2octets("")
+ bits = set(bits)
+ return self._bits2octets("".join(
+ ("1" if bit in bits else "0")
+ for bit in six_xrange(max(bits) + 1)
+ ))
+ raise InvalidValueType((self.__class__, binary_type, string_types))
+
+ @property
+ def ready(self):
+ return self._value is not None
+
+ def copy(self):
+ obj = self.__class__(_specs=self.specs)
+ obj._value = self._value
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __iter__(self):
+ self._assert_ready()
+ for i in six_xrange(self._value[0]):
+ yield self[i]
+
+ @property
+ def bit_len(self):
+ self._assert_ready()
+ return self._value[0]
+
+ def __bytes__(self):
+ self._assert_ready()
+ return self._value[1]
+
+ def __eq__(self, their):
+ if isinstance(their, bytes):
+ return self._value[1] == their
+ if not issubclass(their.__class__, BitString):
+ return False
+ return (
+ self._value == their._value and
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ @property
+ def named(self):
+ return [name for name, bit in self.specs.items() if self[bit]]
+
+ def __call__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ _specs=self.specs,
+ )
+
+ def __getitem__(self, key):
+ if isinstance(key, int):
+ bit_len, octets = self._value
+ if key >= bit_len:
+ return False
+ return (
+ byte2int(memoryview(octets)[key // 8:]) >>
+ (7 - (key % 8))
+ ) & 1 == 1
+ if isinstance(key, string_types):
+ value = self.specs.get(key)
+ if value is None:
+ raise ObjUnknown("BitString value: %s" % key)
+ return self[value]
+ raise InvalidValueType((int, str))
+
+ def _encode(self):
+ self._assert_ready()
+ bit_len, octets = self._value
+ return b"".join((
+ self.tag,
+ len_encode(len(octets) + 1),
+ int2byte((8 - bit_len % 8) % 8),
+ octets,
+ ))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, _, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l == 0:
+ raise NotEnoughData(
+ "zero length",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ pad_size = byte2int(v)
+ if l == 1 and pad_size != 0:
+ raise DecodeError(
+ "invalid empty value",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if pad_size > 7:
+ raise DecodeError(
+ "too big pad",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if byte2int(v[-1:]) & ((1 << pad_size) - 1) != 0:
+ raise DecodeError(
+ "invalid pad",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ obj = self.__class__(
+ value=((len(v) - 1) * 8 - pad_size, v[1:].tobytes()),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _specs=self.specs,
+ _decoded=(offset, llen, l),
+ )
+ return obj, tail
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ value = None
+ blob = None
+ if self.ready:
+ bit_len, blob = self._value
+ value = "%d bits" % bit_len
+ if len(self.specs) > 0:
+ blob = tuple(self.named)
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=value,
+ blob=blob,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+class OctetString(Obj):
+ """``OCTET STRING`` binary string type
+
+ >>> s = OctetString(b"hello world")
+ OCTET STRING 11 bytes 68656c6c6f20776f726c64
+ >>> s == OctetString(b"hello world")
+ True
+ >>> bytes(s)
+ b'hello world'
+
+ >>> OctetString(b"hello", bounds=(4, 4))
+ Traceback (most recent call last):
+ pyderasn.BoundsError: unsatisfied bounds: 4 <= 5 <= 4
+ >>> OctetString(b"hell", bounds=(4, 4))
+ OCTET STRING 4 bytes 68656c6c
+ """
+ __slots__ = ("_bound_min", "_bound_max")
+ tag_default = tag_encode(4)
+ asn1_type_name = "OCTET STRING"
+
+ def __init__(
+ self,
+ value=None,
+ bounds=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either binary type, or
+ :py:class:`pyderasn.OctetString` object
+ :param bounds: set ``(MIN, MAX)`` value size constraint.
+ (-inf, +inf) by default
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(OctetString, self).__init__(
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ )
+ self._value = value
+ if bounds is None:
+ self._bound_min, self._bound_max = getattr(
+ self,
+ "bounds",
+ (0, float("+inf")),
+ )
+ else:
+ self._bound_min, self._bound_max = bounds
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default = self._value_sanitize(default)
+ self.default = self.__class__(
+ value=default,
+ impl=self.tag,
+ expl=self._expl,
+ )
+ if self._value is None:
+ self._value = default
+
+ def _value_sanitize(self, value):
+ if issubclass(value.__class__, OctetString):
+ value = value._value
+ elif isinstance(value, binary_type):
+ pass
+ else:
+ raise InvalidValueType((self.__class__, bytes))
+ if not self._bound_min <= len(value) <= self._bound_max:
+ raise BoundsError(self._bound_min, len(value), self._bound_max)
+ return value
+
+ @property
+ def ready(self):
+ return self._value is not None
+
+ def copy(self):
+ obj = self.__class__()
+ obj._value = self._value
+ obj._bound_min = self._bound_min
+ obj._bound_max = self._bound_max
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __bytes__(self):
+ self._assert_ready()
+ return self._value
+
+ def __eq__(self, their):
+ if isinstance(their, binary_type):
+ return self._value == their
+ if not issubclass(their.__class__, OctetString):
+ return False
+ return (
+ self._value == their._value and
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ def __call__(
+ self,
+ value=None,
+ bounds=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ bounds=(
+ (self._bound_min, self._bound_max)
+ if bounds is None else bounds
+ ),
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def _encode(self):
+ self._assert_ready()
+ return b"".join((
+ self.tag,
+ len_encode(len(self._value)),
+ self._value,
+ ))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, _, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ try:
+ obj = self.__class__(
+ value=v.tobytes(),
+ bounds=(self._bound_min, self._bound_max),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, llen, l),
+ )
+ except BoundsError as err:
+ raise DecodeError(
+ msg=str(err),
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ return obj, tail
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=("%d bytes" % len(self._value)) if self.ready else None,
+ blob=self._value if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+class Null(Obj):
+ """``NULL`` null object
+
+ >>> n = Null()
+ NULL
+ >>> n.ready
+ True
+ """
+ __slots__ = ()
+ tag_default = tag_encode(5)
+ asn1_type_name = "NULL"
+
+ def __init__(
+ self,
+ value=None, # unused, but Sequence passes it
+ impl=None,
+ expl=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(Null, self).__init__(impl, expl, None, optional, _decoded)
+ self.default = None
+
+ @property
+ def ready(self):
+ return True
+
+ def copy(self):
+ obj = self.__class__()
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __eq__(self, their):
+ if not issubclass(their.__class__, Null):
+ return False
+ return (
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ def __call__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ optional=None,
+ ):
+ return self.__class__(
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def _encode(self):
+ return self.tag + len_encode(0)
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, _, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, _, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l != 0:
+ raise InvalidLength(
+ "Null must have zero length",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj = self.__class__(
+ impl=self.tag,
+ expl=self._expl,
+ optional=self.optional,
+ _decoded=(offset, 1, 0),
+ )
+ return obj, v
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ optional=self.optional,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+class ObjectIdentifier(Obj):
+ """``OBJECT IDENTIFIER`` OID type
+
+ >>> oid = ObjectIdentifier((1, 2, 3))
+ OBJECT IDENTIFIER 1.2.3
+ >>> oid == ObjectIdentifier("1.2.3")
+ True
+ >>> tuple(oid)
+ (1, 2, 3)
+ >>> str(oid)
+ '1.2.3'
+ >>> oid + (4, 5) + ObjectIdentifier("1.7")
+ OBJECT IDENTIFIER 1.2.3.4.5.1.7
+
+ >>> str(ObjectIdentifier((3, 1)))
+ Traceback (most recent call last):
+ pyderasn.InvalidOID: unacceptable first arc value
+ """
+ __slots__ = ()
+ tag_default = tag_encode(6)
+ asn1_type_name = "OBJECT IDENTIFIER"
+
+ def __init__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either tuples of integers,
+ string of "."-concatenated integers, or
+ :py:class:`pyderasn.ObjectIdentifier` object
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(ObjectIdentifier, self).__init__(
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ )
+ self._value = value
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default = self._value_sanitize(default)
+ self.default = self.__class__(
+ value=default,
+ impl=self.tag,
+ expl=self._expl,
+ )
+ if self._value is None:
+ self._value = default
+
+ def __add__(self, their):
+ if isinstance(their, self.__class__):
+ return self.__class__(self._value + their._value)
+ if isinstance(their, tuple):
+ return self.__class__(self._value + their)
+ raise InvalidValueType((self.__class__, tuple))
+
+ def _value_sanitize(self, value):
+ if issubclass(value.__class__, ObjectIdentifier):
+ return value._value
+ if isinstance(value, string_types):
+ try:
+ value = tuple(int(arc) for arc in value.split("."))
+ except ValueError:
+ raise InvalidOID("unacceptable arcs values")
+ if isinstance(value, tuple):
+ if len(value) < 2:
+ raise InvalidOID("less than 2 arcs")
+ first_arc = value[0]
+ if first_arc in (0, 1):
+ if not (0 <= value[1] <= 39):
+ raise InvalidOID("second arc is too wide")
+ elif first_arc == 2:
+ pass
+ else:
+ raise InvalidOID("unacceptable first arc value")
+ return value
+ raise InvalidValueType((self.__class__, str, tuple))
+
+ @property
+ def ready(self):
+ return self._value is not None
+
+ def copy(self):
+ obj = self.__class__()
+ obj._value = self._value
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __iter__(self):
+ self._assert_ready()
+ return iter(self._value)
+
+ def __str__(self):
+ return ".".join(str(arc) for arc in self._value or ())
+
+ def __hash__(self):
+ self._assert_ready()
+ return hash(
+ self.tag +
+ bytes(self._expl or b"") +
+ str(self._value).encode("ascii"),
+ )
+
+ def __eq__(self, their):
+ if isinstance(their, tuple):
+ return self._value == their
+ if not issubclass(their.__class__, ObjectIdentifier):
+ return False
+ return (
+ self.tag == their.tag and
+ self._expl == their._expl and
+ self._value == their._value
+ )
+
+ def __lt__(self, their):
+ return self._value < their
+
+ def __gt__(self, their):
+ return self._value > their
+
+ def __call__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def _encode(self):
+ self._assert_ready()
+ value = self._value
+ first_value = value[1]
+ first_arc = value[0]
+ if first_arc == 0:
+ pass
+ elif first_arc == 1:
+ first_value += 40
+ elif first_arc == 2:
+ first_value += 80
+ else: # pragma: no cover
+ raise RuntimeError("invalid arc is stored")
+ octets = [zero_ended_encode(first_value)]
+ for arc in value[2:]:
+ octets.append(zero_ended_encode(arc))
+ v = b"".join(octets)
+ return b"".join((self.tag, len_encode(len(v)), v))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, _, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l == 0:
+ raise NotEnoughData(
+ "zero length",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ arcs = []
+ while len(v) > 0:
+ i = 0
+ arc = 0
+ while True:
+ octet = indexbytes(v, i)
+ arc = (arc << 7) | (octet & 0x7F)
+ if octet & 0x80 == 0:
+ arcs.append(arc)
+ v = v[i + 1:]
+ break
+ i += 1
+ if i == len(v):
+ raise DecodeError(
+ "unfinished OID",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ first_arc = 0
+ second_arc = arcs[0]
+ if 0 <= second_arc <= 39:
+ first_arc = 0
+ elif 40 <= second_arc <= 79:
+ first_arc = 1
+ second_arc -= 40
+ else:
+ first_arc = 2
+ second_arc -= 80
+ obj = self.__class__(
+ value=tuple([first_arc, second_arc] + arcs[1:]),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, llen, l),
+ )
+ return obj, tail
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=str(self) if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+class Enumerated(Integer):
+ """``ENUMERATED`` integer type
+
+ This type is identical to :py:class:`pyderasn.Integer`, but requires
+ schema to be specified and does not accept values missing from it.
+ """
+ __slots__ = ()
+ tag_default = tag_encode(10)
+ asn1_type_name = "ENUMERATED"
+
+ def __init__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _specs=None,
+ _decoded=(0, 0, 0),
+ bounds=None, # dummy argument, workability for Integer.decode
+ ):
+ super(Enumerated, self).__init__(
+ value=value,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ _specs=_specs,
+ _decoded=_decoded,
+ )
+ if len(self.specs) == 0:
+ raise ValueError("schema must be specified")
+
+ def _value_sanitize(self, value):
+ if isinstance(value, self.__class__):
+ value = value._value
+ elif isinstance(value, integer_types):
+ if value not in list(self.specs.values()):
+ raise DecodeError(
+ "unknown integer value: %s" % value,
+ klass=self.__class__,
+ )
+ elif isinstance(value, string_types):
+ value = self.specs.get(value)
+ if value is None:
+ raise ObjUnknown("integer value: %s" % value)
+ else:
+ raise InvalidValueType((self.__class__, int, str))
+ return value
+
+ def copy(self):
+ obj = self.__class__(_specs=self.specs)
+ obj._value = self._value
+ obj._bound_min = self._bound_min
+ obj._bound_max = self._bound_max
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __call__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ _specs=None,
+ ):
+ return self.__class__(
+ value=value,
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ _specs=self.specs,
+ )
+
+
+class CommonString(OctetString):
+ """Common class for all strings
+
+ Everything resembles :py:class:`pyderasn.OctetString`, except
+ ability to deal with unicode text strings.
+
+ >>> hexenc("привет мир".encode("utf-8"))
+ 'd0bfd180d0b8d0b2d0b5d18220d0bcd0b8d180'
+ >>> UTF8String("привет мир") == UTF8String(hexdec("d0...80"))
+ True
+ >>> s = UTF8String("привет мир")
+ UTF8String UTF8String привет мир
+ >>> str(s)
+ 'привет мир'
+ >>> hexenc(bytes(s))
+ 'd0bfd180d0b8d0b2d0b5d18220d0bcd0b8d180'
+
+ >>> PrintableString("привет мир")
+ Traceback (most recent call last):
+ UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
+
+ >>> BMPString("ада", bounds=(2, 2))
+ Traceback (most recent call last):
+ pyderasn.BoundsError: unsatisfied bounds: 2 <= 3 <= 2
+ >>> s = BMPString("ад", bounds=(2, 2))
+ >>> s.encoding
+ 'utf-16-be'
+ >>> hexenc(bytes(s))
+ '04300434'
+
+ .. list-table::
+ :header-rows: 1
+
+ * - Class
+ - Text Encoding
+ * - :py:class:`pyderasn.UTF8String`
+ - utf-8
+ * - :py:class:`pyderasn.NumericString`
+ - ascii
+ * - :py:class:`pyderasn.PrintableString`
+ - ascii
+ * - :py:class:`pyderasn.TeletexString`
+ - ascii
+ * - :py:class:`pyderasn.T61String`
+ - ascii
+ * - :py:class:`pyderasn.VideotexString`
+ - iso-8859-1
+ * - :py:class:`pyderasn.IA5String`
+ - ascii
+ * - :py:class:`pyderasn.GraphicString`
+ - iso-8859-1
+ * - :py:class:`pyderasn.VisibleString`
+ - ascii
+ * - :py:class:`pyderasn.ISO646String`
+ - ascii
+ * - :py:class:`pyderasn.GeneralString`
+ - iso-8859-1
+ * - :py:class:`pyderasn.UniversalString`
+ - utf-32-be
+ * - :py:class:`pyderasn.BMPString`
+ - utf-16-be
+ """
+ __slots__ = ("encoding",)
+
+ def _value_sanitize(self, value):
+ value_raw = None
+ value_decoded = None
+ if isinstance(value, self.__class__):
+ value_raw = value._value
+ elif isinstance(value, text_type):
+ value_decoded = value
+ elif isinstance(value, binary_type):
+ value_raw = value
+ else:
+ raise InvalidValueType((self.__class__, text_type, binary_type))
+ value_raw = (
+ value_decoded.encode(self.encoding)
+ if value_raw is None else value_raw
+ )
+ value_decoded = (
+ value_raw.decode(self.encoding)
+ if value_decoded is None else value_decoded
+ )
+ if not self._bound_min <= len(value_decoded) <= self._bound_max:
+ raise BoundsError(
+ self._bound_min,
+ len(value_decoded),
+ self._bound_max,
+ )
+ return value_raw
+
+ def __eq__(self, their):
+ if isinstance(their, binary_type):
+ return self._value == their
+ if isinstance(their, text_type):
+ return self._value == their.encode(self.encoding)
+ if not isinstance(their, self.__class__):
+ return False
+ return (
+ self._value == their._value and
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ def __unicode__(self):
+ if self.ready:
+ return self._value.decode(self.encoding)
+ return text_type(self._value)
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps(no_unicode=PY2)))
+
+ def pps(self, decode_path=(), no_unicode=False):
+ value = None
+ if self.ready:
+ value = hexenc(bytes(self)) if no_unicode else self.__unicode__()
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=value,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ )
+
+
+class UTF8String(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(12)
+ encoding = "utf-8"
+ asn1_type_name = "UTF8String"
+
+
+class NumericString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(18)
+ encoding = "ascii"
+ asn1_type_name = "NumericString"
+
+
+class PrintableString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(19)
+ encoding = "ascii"
+ asn1_type_name = "PrintableString"
+
+
+class TeletexString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(20)
+ encoding = "ascii"
+ asn1_type_name = "TeletexString"
+
+
+class T61String(TeletexString):
+ __slots__ = ()
+ asn1_type_name = "T61String"
+
+
+class VideotexString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(21)
+ encoding = "iso-8859-1"
+ asn1_type_name = "VideotexString"
+
+
+class IA5String(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(22)
+ encoding = "ascii"
+ asn1_type_name = "IA5"
+
+
+class UTCTime(CommonString):
+ """``UTCTime`` datetime type
+
+ >>> t = UTCTime(datetime(2017, 9, 30, 22, 7, 50, 123))
+ UTCTime UTCTime 2017-09-30T22:07:50
+ >>> str(t)
+ '170930220750Z'
+ >>> bytes(t)
+ b'170930220750Z'
+ >>> t.todatetime()
+ datetime.datetime(2017, 9, 30, 22, 7, 50)
+ >>> UTCTime(datetime(2057, 9, 30, 22, 7, 50)).todatetime()
+ datetime.datetime(1957, 9, 30, 22, 7, 50)
+ """
+ __slots__ = ()
+ tag_default = tag_encode(23)
+ encoding = "ascii"
+ asn1_type_name = "UTCTime"
+
+ fmt = "%y%m%d%H%M%SZ"
+
+ def __init__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ bounds=None, # dummy argument, workability for OctetString.decode
+ ):
+ """
+ :param value: set the value. Either datetime type, or
+ :py:class:`pyderasn.UTCTime` object
+ :param bytes impl: override default tag with ``IMPLICIT`` one
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(UTCTime, self).__init__(
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ self._value = value
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default = self._value_sanitize(default)
+ self.default = self.__class__(
+ value=default,
+ impl=self.tag,
+ expl=self._expl,
+ )
+ if self._value is None:
+ self._value = default
+
+ def _value_sanitize(self, value):
+ if isinstance(value, self.__class__):
+ return value._value
+ if isinstance(value, datetime):
+ return value.strftime(self.fmt).encode("ascii")
+ if isinstance(value, binary_type):
+ value_decoded = value.decode("ascii")
+ if len(value_decoded) == 2 + 2 + 2 + 2 + 2 + 2 + 1:
+ try:
+ datetime.strptime(value_decoded, self.fmt)
+ except ValueError:
+ raise DecodeError("invalid UTCTime format")
+ return value
+ else:
+ raise DecodeError("invalid UTCTime length")
+ raise InvalidValueType((self.__class__, datetime))
+
+ def __eq__(self, their):
+ if isinstance(their, binary_type):
+ return self._value == their
+ if isinstance(their, datetime):
+ return self.todatetime() == their
+ if not isinstance(their, self.__class__):
+ return False
+ return (
+ self._value == their._value and
+ self.tag == their.tag and
+ self._expl == their._expl
+ )
+
+ def todatetime(self):
+ """Convert to datetime
+
+ :returns: datetime
+
+ Pay attention that UTCTime can not hold full year, so all years
+ having < 50 years are treated as 20xx, 19xx otherwise, according
+ to X.509 recomendation.
+ """
+ value = datetime.strptime(self._value.decode("ascii"), self.fmt)
+ year = value.year % 100
+ return datetime(
+ year=(2000 + year) if year < 50 else (1900 + year),
+ month=value.month,
+ day=value.day,
+ hour=value.hour,
+ minute=value.minute,
+ second=value.second,
+ )
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=self.todatetime().isoformat() if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ )
+
+
+class GeneralizedTime(UTCTime):
+ """``GeneralizedTime`` datetime type
+
+ This type is similar to :py:class:`pyderasn.UTCTime`.
+
+ >>> t = GeneralizedTime(datetime(2017, 9, 30, 22, 7, 50, 123))
+ GeneralizedTime GeneralizedTime 2017-09-30T22:07:50.000123
+ >>> str(t)
+ '20170930220750.000123Z'
+ >>> t = GeneralizedTime(datetime(2057, 9, 30, 22, 7, 50))
+ GeneralizedTime GeneralizedTime 2057-09-30T22:07:50
+ """
+ __slots__ = ()
+ tag_default = tag_encode(24)
+ asn1_type_name = "GeneralizedTime"
+
+ fmt = "%Y%m%d%H%M%SZ"
+ fmt_ms = "%Y%m%d%H%M%S.%fZ"
+
+ def _value_sanitize(self, value):
+ if isinstance(value, self.__class__):
+ return value._value
+ if isinstance(value, datetime):
+ return value.strftime(
+ self.fmt_ms if value.microsecond > 0 else self.fmt
+ ).encode("ascii")
+ if isinstance(value, binary_type):
+ value_decoded = value.decode("ascii")
+ if len(value_decoded) == 4 + 2 + 2 + 2 + 2 + 2 + 1:
+ try:
+ datetime.strptime(value_decoded, self.fmt)
+ except ValueError:
+ raise DecodeError(
+ "invalid GeneralizedTime (without ms) format",
+ )
+ return value
+ elif len(value_decoded) >= 4 + 2 + 2 + 2 + 2 + 2 + 1 + 1 + 1:
+ try:
+ datetime.strptime(value_decoded, self.fmt_ms)
+ except ValueError:
+ raise DecodeError(
+ "invalid GeneralizedTime (with ms) format",
+ )
+ return value
+ else:
+ raise DecodeError(
+ "invalid GeneralizedTime length",
+ klass=self.__class__,
+ )
+ raise InvalidValueType((self.__class__, datetime))
+
+ def todatetime(self):
+ value = self._value.decode("ascii")
+ if len(value) == 4 + 2 + 2 + 2 + 2 + 2 + 1:
+ return datetime.strptime(value, self.fmt)
+ return datetime.strptime(value, self.fmt_ms)
+
+
+class GraphicString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(25)
+ encoding = "iso-8859-1"
+ asn1_type_name = "GraphicString"
+
+
+class VisibleString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(26)
+ encoding = "ascii"
+ asn1_type_name = "VisibleString"
+
+
+class ISO646String(VisibleString):
+ __slots__ = ()
+ asn1_type_name = "ISO646String"
+
+
+class GeneralString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(27)
+ encoding = "iso-8859-1"
+ asn1_type_name = "GeneralString"
+
+
+class UniversalString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(28)
+ encoding = "utf-32-be"
+ asn1_type_name = "UniversalString"
+
+
+class BMPString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(30)
+ encoding = "utf-16-be"
+ asn1_type_name = "BMPString"
+
+
+class Choice(Obj):
+ """``CHOICE`` special type
+
+ ::
+
+ class GeneralName(Choice):
+ schema = (
+ ('rfc822Name', IA5String(impl=tag_ctxp(1))),
+ ('dNSName', IA5String(impl=tag_ctxp(2))),
+ )
+
+ >>> gn = GeneralName()
+ GeneralName CHOICE
+ >>> gn["rfc822Name"] = IA5String("foo@bar.baz")
+ GeneralName CHOICE rfc822Name[[1] IA5String IA5 foo@bar.baz]
+ >>> gn["dNSName"] = IA5String("bar.baz")
+ GeneralName CHOICE dNSName[[2] IA5String IA5 bar.baz]
+ >>> gn["rfc822Name"]
+ None
+ >>> gn["dNSName"]
+ [2] IA5String IA5 bar.baz
+ >>> gn.choice
+ 'dNSName'
+ >>> gn.value == gn["dNSName"]
+ True
+ >>> gn.specs
+ OrderedDict([('rfc822Name', [1] IA5String IA5), ('dNSName', [2] IA5String IA5)])
+
+ >>> GeneralName(("rfc822Name", IA5String("foo@bar.baz")))
+ GeneralName CHOICE rfc822Name[[1] IA5String IA5 foo@bar.baz]
+ """
+ __slots__ = ("specs",)
+ tag_default = None
+ asn1_type_name = "CHOICE"
+
+ def __init__(
+ self,
+ value=None,
+ schema=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either ``(choice, value)`` tuple, or
+ :py:class:`pyderasn.Choice` object
+ :param bytes impl: can not be set, do **not** use it
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param default: set default value. Type same as in ``value``
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ if impl is not None:
+ raise ValueError("no implicit tag allowed for CHOICE")
+ super(Choice, self).__init__(None, expl, default, optional, _decoded)
+ if schema is None:
+ schema = getattr(self, "schema", ())
+ if len(schema) == 0:
+ raise ValueError("schema must be specified")
+ self.specs = (
+ schema if isinstance(schema, OrderedDict) else OrderedDict(schema)
+ )
+ self._value = None
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default_value = self._value_sanitize(default)
+ default_obj = self.__class__(impl=self.tag, expl=self._expl)
+ default_obj.specs = self.specs
+ default_obj._value = default_value
+ self.default = default_obj
+ if value is None:
+ self._value = default_obj.copy()._value
+
+ def _value_sanitize(self, value):
+ if isinstance(value, self.__class__):
+ return value._value
+ if isinstance(value, tuple) and len(value) == 2:
+ choice, obj = value
+ spec = self.specs.get(choice)
+ if spec is None:
+ raise ObjUnknown(choice)
+ if not isinstance(obj, spec.__class__):
+ raise InvalidValueType((spec,))
+ return (choice, spec(obj))
+ raise InvalidValueType((self.__class__, tuple))
+
+ @property
+ def ready(self):
+ return self._value is not None and self._value[1].ready
+
+ def copy(self):
+ obj = self.__class__(schema=self.specs)
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ value = self._value
+ if value is not None:
+ obj._value = (value[0], value[1].copy())
+ return obj
+
+ def __eq__(self, their):
+ if isinstance(their, tuple) and len(their) == 2:
+ return self._value == their
+ if not isinstance(their, self.__class__):
+ return False
+ return (
+ self.specs == their.specs and
+ self._value == their._value
+ )
+
+ def __call__(
+ self,
+ value=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ schema=self.specs,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ )
+
+ @property
+ def choice(self):
+ self._assert_ready()
+ return self._value[0]
+
+ @property
+ def value(self):
+ self._assert_ready()
+ return self._value[1]
+
+ def __getitem__(self, key):
+ if key not in self.specs:
+ raise ObjUnknown(key)
+ if self._value is None:
+ return None
+ choice, value = self._value
+ if choice != key:
+ return None
+ return value
+
+ def __setitem__(self, key, value):
+ spec = self.specs.get(key)
+ if spec is None:
+ raise ObjUnknown(key)
+ if not isinstance(value, spec.__class__):
+ raise InvalidValueType((spec.__class__,))
+ self._value = (key, spec(value))
+
+ @property
+ def tlen(self):
+ return 0
+
+ @property
+ def decoded(self):
+ return self._value[1].decoded if self.ready else False
+
+ def _encode(self):
+ self._assert_ready()
+ return self._value[1].encode()
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ for choice, spec in self.specs.items():
+ try:
+ value, tail = spec.decode(
+ tlv,
+ offset=offset,
+ leavemm=True,
+ decode_path=decode_path + (choice,),
+ )
+ except TagMismatch:
+ continue
+ obj = self.__class__(
+ schema=self.specs,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, 0, value.tlvlen),
+ )
+ obj._value = (choice, value)
+ return obj, tail
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+
+ def __repr__(self):
+ value = pp_console_row(next(self.pps()))
+ if self.ready:
+ value = "%s[%r]" % (value, self.value)
+ return value
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ value=self.choice if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ )
+ if self.ready:
+ yield self.value.pps(decode_path=decode_path + (self.choice,))
+
+
+class PrimitiveTypes(Choice):
+ """Predefined ``CHOICE`` for all generic primitive types
+
+ It could be useful for general decoding of some unspecified values:
+
+ >>> PrimitiveTypes().decode(hexdec("0403666f6f"))[0].value
+ OCTET STRING 3 bytes 666f6f
+ >>> PrimitiveTypes().decode(hexdec("0203123456"))[0].value
+ INTEGER 1193046
+ """
+ __slots__ = ()
+ schema = tuple((klass.__name__, klass()) for klass in (
+ Boolean,
+ Integer,
+ BitString,
+ OctetString,
+ Null,
+ ObjectIdentifier,
+ UTF8String,
+ NumericString,
+ PrintableString,
+ TeletexString,
+ VideotexString,
+ IA5String,
+ UTCTime,
+ GeneralizedTime,
+ GraphicString,
+ VisibleString,
+ ISO646String,
+ GeneralString,
+ UniversalString,
+ BMPString,
+ ))
+
+
+class Any(Obj):
+ """``ANY`` special type
+
+ >>> Any(Integer(-123))
+ ANY 020185
+ >>> a = Any(OctetString(b"hello world").encode())
+ ANY 040b68656c6c6f20776f726c64
+ >>> hexenc(bytes(a))
+ b'0x040x0bhello world'
+ """
+ __slots__ = ()
+ tag_default = tag_encode(0)
+ asn1_type_name = "ANY"
+
+ def __init__(
+ self,
+ value=None,
+ expl=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ """
+ :param value: set the value. Either any kind of pyderasn's
+ **ready** object, or bytes. Pay attention that
+ **no** validation is performed is raw binary value
+ is valid TLV
+ :param bytes expl: override default tag with ``EXPLICIT`` one
+ :param bool optional: is object ``OPTIONAL`` in sequence
+ """
+ super(Any, self).__init__(None, expl, None, optional, _decoded)
+ self._value = None if value is None else self._value_sanitize(value)
+
+ def _value_sanitize(self, value):
+ if isinstance(value, self.__class__):
+ return value._value
+ if isinstance(value, Obj):
+ return value.encode()
+ if isinstance(value, binary_type):
+ return value
+ raise InvalidValueType((self.__class__, Obj, binary_type))
+
+ @property
+ def ready(self):
+ return self._value is not None
+
+ def copy(self):
+ obj = self.__class__()
+ obj._value = self._value
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ return obj
+
+ def __eq__(self, their):
+ if isinstance(their, binary_type):
+ return self._value == their
+ if issubclass(their.__class__, Any):
+ return self._value == their._value
+ return False
+
+ def __call__(
+ self,
+ value=None,
+ expl=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ expl=self._expl if expl is None else expl,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def __bytes__(self):
+ self._assert_ready()
+ return self._value
+
+ @property
+ def tlen(self):
+ return 0
+
+ def _encode(self):
+ self._assert_ready()
+ return self._value
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ tlvlen = tlen + llen + l
+ v, tail = tlv[:tlvlen], v[l:]
+ obj = self.__class__(
+ value=v.tobytes(),
+ expl=self._expl,
+ optional=self.optional,
+ _decoded=(offset, 0, tlvlen),
+ )
+ obj.tag = t
+ return obj, tail
+
+ def __repr__(self):
+ return pp_console_row(next(self.pps()))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ blob=self._value if self.ready else None,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+
+
+########################################################################
+# ASN.1 constructed types
+########################################################################
+
+class Sequence(Obj):
+ __slots__ = ("specs",)
+ tag_default = tag_encode(form=TagFormConstructed, num=16)
+ asn1_type_name = "SEQUENCE"
+
+ def __init__(
+ self,
+ value=None,
+ schema=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ super(Sequence, self).__init__(impl, expl, default, optional, _decoded)
+ if schema is None:
+ schema = getattr(self, "schema", ())
+ self.specs = (
+ schema if isinstance(schema, OrderedDict) else OrderedDict(schema)
+ )
+ self._value = {}
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default_value = self._value_sanitize(default)
+ default_obj = self.__class__(impl=self.tag, expl=self._expl)
+ default_obj.specs = self.specs
+ default_obj._value = default_value
+ self.default = default_obj
+ if value is None:
+ self._value = default_obj.copy()._value
+
+ def _value_sanitize(self, value):
+ if not issubclass(value.__class__, Sequence):
+ raise InvalidValueType((Sequence,))
+ return value._value
+
+ @property
+ def ready(self):
+ for name, spec in self.specs.items():
+ value = self._value.get(name)
+ if value is None:
+ if spec.optional:
+ continue
+ return False
+ else:
+ if not value.ready:
+ return False
+ return True
+
+ def copy(self):
+ obj = self.__class__(schema=self.specs)
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ obj._value = {k: v.copy() for k, v in self._value.items()}
+ return obj
+
+ def __eq__(self, their):
+ if not isinstance(their, self.__class__):
+ return False
+ return (
+ self.specs == their.specs and
+ self.tag == their.tag and
+ self._expl == their._expl and
+ self._value == their._value
+ )
+
+ def __call__(
+ self,
+ value=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ schema=self.specs,
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def __contains__(self, key):
+ return key in self._value
+
+ def __setitem__(self, key, value):
+ spec = self.specs.get(key)
+ if spec is None:
+ raise ObjUnknown(key)
+ if value is None:
+ self._value.pop(key, None)
+ return
+ if not isinstance(value, spec.__class__):
+ raise InvalidValueType((spec.__class__,))
+ value = spec(value=value)
+ if spec.default is not None and value == spec.default:
+ self._value.pop(key, None)
+ return
+ self._value[key] = value
+
+ def __getitem__(self, key):
+ value = self._value.get(key)
+ if value is not None:
+ return value
+ spec = self.specs.get(key)
+ if spec is None:
+ raise ObjUnknown(key)
+ if spec.default is not None:
+ return spec.default
+ return None
+
+ def _encoded_values(self):
+ raws = []
+ for name, spec in self.specs.items():
+ value = self._value.get(name)
+ if value is None:
+ if spec.optional:
+ continue
+ raise ObjNotReady(name)
+ raws.append(value.encode())
+ return raws
+
+ def _encode(self):
+ v = b"".join(self._encoded_values())
+ return b"".join((self.tag, len_encode(len(v)), v))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ sub_offset = offset + tlen + llen
+ values = {}
+ for name, spec in self.specs.items():
+ if len(v) == 0 and spec.optional:
+ continue
+ try:
+ value, v_tail = spec.decode(
+ v,
+ sub_offset,
+ leavemm=True,
+ decode_path=decode_path + (name,),
+ )
+ except TagMismatch:
+ if spec.optional:
+ continue
+ raise
+ sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
+ v = v_tail
+ if spec.default is not None and value == spec.default:
+ # Encoded default values are not valid in DER,
+ # but we still allow that
+ continue
+ values[name] = value
+ if len(v) > 0:
+ raise DecodeError(
+ "remaining data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj = self.__class__(
+ schema=self.specs,
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, llen, l),
+ )
+ obj._value = values
+ return obj, tail
+
+ def __repr__(self):
+ value = pp_console_row(next(self.pps()))
+ cols = []
+ for name in self.specs:
+ _value = self._value.get(name)
+ if _value is None:
+ continue
+ cols.append(repr(_value))
+ return "%s[%s]" % (value, ", ".join(cols))
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+ for name in self.specs:
+ value = self._value.get(name)
+ if value is None:
+ continue
+ yield value.pps(decode_path=decode_path + (name,))
+
+
+class Set(Sequence):
+ __slots__ = ()
+ tag_default = tag_encode(form=TagFormConstructed, num=17)
+ asn1_type_name = "SET"
+
+ def _encode(self):
+ raws = self._encoded_values()
+ raws.sort()
+ v = b"".join(raws)
+ return b"".join((self.tag, len_encode(len(v)), v))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ sub_offset = offset + tlen + llen
+ values = {}
+ specs_items = self.specs.items
+ while len(v) > 0:
+ for name, spec in specs_items():
+ try:
+ value, v_tail = spec.decode(
+ v,
+ sub_offset,
+ leavemm=True,
+ decode_path=decode_path + (name,),
+ )
+ except TagMismatch:
+ continue
+ sub_offset += (
+ value.expl_tlvlen if value.expled else value.tlvlen
+ )
+ v = v_tail
+ if spec.default is None or value != spec.default: # pragma: no cover
+ # SeqMixing.test_encoded_default_accepted covers that place
+ values[name] = value
+ break
+ else:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj = self.__class__(
+ schema=self.specs,
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, llen, l),
+ )
+ obj._value = values
+ return obj, tail
+
+
+class SequenceOf(Obj):
+ __slots__ = ("spec", "_bound_min", "_bound_max")
+ tag_default = tag_encode(form=TagFormConstructed, num=16)
+ asn1_type_name = "SEQUENCE OF"
+
+ def __init__(
+ self,
+ value=None,
+ schema=None,
+ bounds=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=False,
+ _decoded=(0, 0, 0),
+ ):
+ super(SequenceOf, self).__init__(
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ )
+ if schema is None:
+ schema = getattr(self, "schema", None)
+ if schema is None:
+ raise ValueError("schema must be specified")
+ self.spec = schema
+ if bounds is None:
+ self._bound_min, self._bound_max = getattr(
+ self,
+ "bounds",
+ (0, float("+inf")),
+ )
+ else:
+ self._bound_min, self._bound_max = bounds
+ self._value = []
+ if value is not None:
+ self._value = self._value_sanitize(value)
+ if default is not None:
+ default_value = self._value_sanitize(default)
+ default_obj = self.__class__(
+ schema=schema,
+ impl=self.tag,
+ expl=self._expl,
+ )
+ default_obj._value = default_value
+ self.default = default_obj
+ if value is None:
+ self._value = default_obj.copy()._value
+
+ def _value_sanitize(self, value):
+ if issubclass(value.__class__, SequenceOf):
+ value = value._value
+ elif hasattr(value, "__iter__"):
+ value = list(value)
+ else:
+ raise InvalidValueType((self.__class__, iter))
+ if not self._bound_min <= len(value) <= self._bound_max:
+ raise BoundsError(self._bound_min, len(value), self._bound_max)
+ for v in value:
+ if not isinstance(v, self.spec.__class__):
+ raise InvalidValueType((self.spec.__class__,))
+ return value
+
+ @property
+ def ready(self):
+ return all(v.ready for v in self._value)
+
+ def copy(self):
+ obj = self.__class__(schema=self.spec)
+ obj._bound_min = self._bound_min
+ obj._bound_max = self._bound_max
+ obj.tag = self.tag
+ obj._expl = self._expl
+ obj.default = self.default
+ obj.optional = self.optional
+ obj.offset = self.offset
+ obj.llen = self.llen
+ obj.vlen = self.vlen
+ obj._value = [v.copy() for v in self._value]
+ return obj
+
+ def __eq__(self, their):
+ if isinstance(their, self.__class__):
+ return (
+ self.spec == their.spec and
+ self.tag == their.tag and
+ self._expl == their._expl and
+ self._value == their._value
+ )
+ if hasattr(their, "__iter__"):
+ return self._value == list(their)
+ return False
+
+ def __call__(
+ self,
+ value=None,
+ bounds=None,
+ impl=None,
+ expl=None,
+ default=None,
+ optional=None,
+ ):
+ return self.__class__(
+ value=value,
+ schema=self.spec,
+ bounds=(
+ (self._bound_min, self._bound_max)
+ if bounds is None else bounds
+ ),
+ impl=self.tag if impl is None else impl,
+ expl=self._expl if expl is None else expl,
+ default=self.default if default is None else default,
+ optional=self.optional if optional is None else optional,
+ )
+
+ def __contains__(self, key):
+ return key in self._value
+
+ def append(self, value):
+ if not isinstance(value, self.spec.__class__):
+ raise InvalidValueType((self.spec.__class__,))
+ if len(self._value) + 1 > self._bound_max:
+ raise BoundsError(
+ self._bound_min,
+ len(self._value) + 1,
+ self._bound_max,
+ )
+ self._value.append(value)
+
+ def __iter__(self):
+ self._assert_ready()
+ return iter(self._value)
+
+ def __len__(self):
+ self._assert_ready()
+ return len(self._value)
+
+ def __setitem__(self, key, value):
+ if not isinstance(value, self.spec.__class__):
+ raise InvalidValueType((self.spec.__class__,))
+ self._value[key] = self.spec(value=value)
+
+ def __getitem__(self, key):
+ return self._value[key]
+
+ def _encoded_values(self):
+ return [v.encode() for v in self._value]
+
+ def _encode(self):
+ v = b"".join(self._encoded_values())
+ return b"".join((self.tag, len_encode(len(v)), v))
+
+ def _decode(self, tlv, offset=0, decode_path=()):
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t != self.tag:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
+ l, llen, v = len_decode(lv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
+ sub_offset = offset + tlen + llen
+ _value = []
+ spec = self.spec
+ while len(v) > 0:
+ value, v_tail = spec.decode(
+ v,
+ sub_offset,
+ leavemm=True,
+ decode_path=decode_path + (str(len(_value)),),
+ )
+ sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
+ v = v_tail
+ _value.append(value)
+ obj = self.__class__(
+ value=_value,
+ schema=spec,
+ bounds=(self._bound_min, self._bound_max),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, llen, l),
+ )
+ return obj, tail
+
+ def __repr__(self):
+ return "%s[%s]" % (
+ pp_console_row(next(self.pps())),
+ ", ".join(repr(v) for v in self._value),
+ )
+
+ def pps(self, decode_path=()):
+ yield _pp(
+ asn1_type_name=self.asn1_type_name,
+ obj_name=self.__class__.__name__,
+ decode_path=decode_path,
+ optional=self.optional,
+ default=self == self.default,
+ impl=None if self.tag == self.tag_default else tag_decode(self.tag),
+ expl=None if self._expl is None else tag_decode(self._expl),
+ offset=self.offset,
+ tlen=self.tlen,
+ llen=self.llen,
+ vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ )
+ for i, value in enumerate(self._value):
+ yield value.pps(decode_path=decode_path + (str(i),))
+
+
+class SetOf(SequenceOf):
+ __slots__ = ()
+ tag_default = tag_encode(form=TagFormConstructed, num=17)
+ asn1_type_name = "SET OF"
+
+ def _encode(self):
+ raws = self._encoded_values()
+ raws.sort()
+ v = b"".join(raws)
+ return b"".join((self.tag, len_encode(len(v)), v))
+
+
+def obj_by_path(pypath): # pragma: no cover
+ """Import object specified as string Python path
+
+ Modules must be separated from classes/functions with ``:``.
+
+ >>> obj_by_path("foo.bar:Baz")
+ <class 'foo.bar.Baz'>
+ >>> obj_by_path("foo.bar:Baz.boo")
+ <classmethod 'foo.bar.Baz.boo'>
+ """
+ mod, objs = pypath.rsplit(":", 1)
+ from importlib import import_module
+ obj = import_module(mod)
+ for obj_name in objs.split("."):
+ obj = getattr(obj, obj_name)
+ return obj
+
+
+def main(): # pragma: no cover
+ import argparse
+ parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder")
+ parser.add_argument(
+ "--oids",
+ help="Python path to dictionary with OIDs",
+ )
+ parser.add_argument(
+ "--schema",
+ help="Python path to schema definition to use",
+ )
+ parser.add_argument(
+ "DERFile",
+ type=argparse.FileType("rb"),
+ help="Python path to schema definition to use",
+ )
+ args = parser.parse_args()
+ der = memoryview(args.DERFile.read())
+ args.DERFile.close()
+ oids = obj_by_path(args.oids) if args.oids else {}
+ if args.schema:
+ schema = obj_by_path(args.schema)
+ from functools import partial
+ pprinter = partial(pprint, big_blobs=True)
+ else:
+ # All of this below is a big hack with self references
+ choice = PrimitiveTypes()
+ choice.specs["SequenceOf"] = SequenceOf(schema=choice)
+ choice.specs["SetOf"] = SetOf(schema=choice)
+ for i in range(31):
+ choice.specs["SequenceOf%d" % i] = SequenceOf(
+ schema=choice,
+ expl=tag_ctxc(i),
+ )
+ choice.specs["Any"] = Any()
+
+ # Class name equals to type name, to omit it from output
+ class SEQUENCEOF(SequenceOf):
+ __slots__ = ()
+ schema = choice
+ schema = SEQUENCEOF()
+
+ def pprint_any(obj, oids=None):
+ def _pprint_pps(pps):
+ for pp in pps:
+ if hasattr(pp, "_fields"):
+ if pp.asn1_type_name == Choice.asn1_type_name:
+ continue
+ pp_kwargs = pp._asdict()
+ pp_kwargs["decode_path"] = pp.decode_path[:-1] + (">",)
+ pp = _pp(**pp_kwargs)
+ yield pp_console_row(
+ pp,
+ oids=oids,
+ with_offsets=True,
+ with_blob=False,
+ )
+ for row in pp_console_blob(pp):
+ yield row
+ else:
+ for row in _pprint_pps(pp):
+ yield row
+ return "\n".join(_pprint_pps(obj.pps()))
+ pprinter = pprint_any
+ obj, tail = schema().decode(der)
+ print(pprinter(obj, oids=oids))
+ if tail != b"":
+ print("\nTrailing data: %s" % hexenc(tail))
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+from datetime import datetime
+from typing import Any as TAny
+from typing import Dict
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence as TSequence
+from typing import Tuple
+from typing import Type
+from typing import Union
+
+
+TagClassUniversal = ... # type: int
+TagClassApplication = ... # type: int
+TagClassContext = ... # type: int
+TagClassPrivate = ... # type: int
+TagFormPrimitive = ... # type: int
+TagFormConstructed = ... # type: int
+TagClassReprs = ... # type: Dict[int, str]
+
+
+class DecodeError(Exception):
+ msg = ... # type: str
+ klass = ... # type: Type
+ decode_path = ... # type: Tuple[str, ...]
+ offset = ... # type: int
+
+ def __init__(
+ self,
+ msg: str=...,
+ klass: Optional[TAny]=...,
+ decode_path: TAny=...,
+ offset: int=...,
+ ) -> None: ...
+
+class NotEnoughData(DecodeError): ...
+
+class TagMismatch(DecodeError): ...
+
+class InvalidLength(DecodeError): ...
+
+class InvalidOID(DecodeError): ...
+
+class ObjUnknown(ValueError):
+ name = ... # type: str
+
+ def __init__(self, name: str) -> None: ...
+
+class ObjNotReady(ValueError):
+ name = ... # type: str
+
+ def __init__(self, str) -> None: ...
+
+class InvalidValueType(ValueError):
+ expected_types = ... # type: Tuple[Type, ...]
+
+ def __init__(self, expected_types: Tuple[Type, ...]) -> None: ...
+
+class BoundsError(ValueError):
+ bound_min = ... # type: int
+ value = ... # type: int
+ bound_max = ... # type: int
+
+ def __init__(self, bound_min: int, value: int, bound_max: int) -> None: ...
+
+def hexdec(data: str) -> bytes: ...
+
+def hexenc(data: bytes) -> str: ...
+
+def int_bytes_len(num: int, byte_len: int=...) -> int: ...
+
+def zero_ended_encode(num: int) -> bytes: ...
+
+def tag_encode(num: int, klass: int=..., form: int=...) -> bytes: ...
+
+def tag_decode(tag: bytes) -> Tuple[int, int, int]: ...
+
+def tag_ctxp(num: int) -> bytes: ...
+
+def tag_ctxc(num: int) -> bytes: ...
+
+def tag_strip(data: memoryview) -> Tuple[memoryview, int, memoryview]: ...
+
+def len_encode(l: int) -> bytes: ...
+
+def len_decode(data: memoryview) -> Tuple[int, int, memoryview]: ...
+
+class Obj:
+ tag = ... # type: bytes
+ optional = ... # type: bool
+
+ def __init__(
+ self,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[TAny]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ @property
+ def decoded(self) -> bool: ...
+
+ def copy(self) -> "Obj": ...
+
+ @property
+ def tlen(self) -> int: ...
+
+ @property
+ def tlvlen(self) -> int: ...
+
+ def encode(self) -> bytes: ...
+
+ def decode(
+ self,
+ data: bytes,
+ offset: int=...,
+ leavemm: bool=...,
+ decode_path: Tuple[str, ...]=...,
+ ) -> Tuple[Obj, bytes]: ...
+
+ @property
+ def expled(self) -> bool: ...
+
+ @property
+ def expl_tag(self) -> bytes: ...
+
+ @property
+ def expl_tlen(self) -> int: ...
+
+ @property
+ def expl_llen(self) -> int: ...
+
+ @property
+ def expl_offset(self) -> int: ...
+
+ @property
+ def expl_vlen(self) -> int: ...
+
+ @property
+ def expl_tlvlen(self) -> int: ...
+
+
+PP = NamedTuple("PP", (
+ ("asn1_type_name", str),
+ ("obj_name", str),
+ ("decode_path", Tuple[str, ...]),
+ ("value", Optional[str]),
+ ("blob", Optional[Union[bytes, Tuple[str, ...]]]),
+ ("optional", bool),
+ ("default", bool),
+ ("impl", Optional[Tuple[int, int, int]]),
+ ("expl", Optional[Tuple[int, int, int]]),
+ ("offset", int),
+ ("tlen", int),
+ ("llen", int),
+ ("vlen", int),
+ ("expl_offset", int),
+ ("expl_tlen", int),
+ ("expl_llen", int),
+ ("expl_vlen", int),
+))
+
+
+def pp_console_row(
+ pp: PP,
+ oids: Optional[Dict[str, str]]=...,
+ with_offsets: bool=...,
+ with_blob: bool=...,
+): ...
+
+def pp_console_blob(pp: PP) -> TSequence[str]: ...
+
+def pprint(
+ obj: Obj,
+ oids: Optional[Dict[str, str]]=...,
+ big_blobs: bool=...,
+): ...
+
+
+class Boolean(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "Boolean"
+
+ def __init__(
+ self,
+ value: Optional[Union["Boolean", bool]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Boolean", bool]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "Boolean": ...
+
+ def __call__(
+ self,
+ value: Optional[Union["Boolean", bool]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Boolean", bool]]=...,
+ optional: Optional[bool]=...,
+ ) -> "Boolean": ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class Integer(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ specs = ... # type: Dict[str, int]
+ default = ... # type: "Integer"
+
+ def __init__(
+ self,
+ value: Optional[Union["Integer", int, str]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Integer", int, str]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "Integer": ...
+
+ @property
+ def named(self) -> Optional[str]: ...
+
+ def __call__(
+ self,
+ value: Optional[Union["Integer", int, str]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Integer", int, str]]=...,
+ optional: Optional[bool]=...,
+ ) -> "Integer": ...
+
+ def __int__(self) -> int: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class BitString(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ specs = ... # type: Dict[str, int]
+ default = ... # type: "BitString"
+
+ def __init__(
+ self,
+ value: Optional[Union["BitString", bytes, Tuple[str, ...]]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["BitString", bytes, Tuple[str, ...]]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "BitString": ...
+
+ @property
+ def bit_len(self) -> int: ...
+
+ @property
+ def named(self) -> TSequence[str]: ...
+
+ def __call__(
+ self,
+ value: Optional[Union["BitString", bytes, Tuple[str, ...]]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["BitString", bytes, Tuple[str, ...]]]=...,
+ optional: Optional[bool]=...,
+ ) -> "BitString": ...
+
+ def __bytes__(self) -> bytes: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class OctetString(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "OctetString"
+
+ def __init__(
+ self,
+ value: Optional[Union["OctetString", bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["OctetString", bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "OctetString": ...
+
+ def __call__(
+ self,
+ value: Optional[Union["OctetString", bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["OctetString", bytes]]=...,
+ optional: Optional[bool]=...,
+ ) -> "OctetString": ...
+
+ def __bytes__(self) -> bytes: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class Null(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "Null"
+
+ def __init__(
+ self,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "Null": ...
+
+ def __call__(
+ self,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ optional: Optional[bool]=...,
+ ) -> "Null": ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class ObjectIdentifier(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "ObjectIdentifier"
+
+ def __init__(
+ self,
+ value: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "ObjectIdentifier": ...
+
+ def __call__(
+ self,
+ value: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=...,
+ optional: Optional[bool]=...,
+ ) -> "ObjectIdentifier": ...
+
+ def __add__(
+ self,
+ their: Union["ObjectIdentifier", Tuple[int, ...]],
+ ) -> "ObjectIdentifier": ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class Enumerated(Integer):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "Enumerated"
+
+ def __init__(
+ self,
+ value: Optional[Union["Enumerated", str, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Enumerated", str, int]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ def copy(self) -> "Enumerated": ...
+
+ def __call__( # type: ignore
+ self,
+ value: Optional[Union["Enumerated", str, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Enumerated", str, int]]=...,
+ optional: Optional[bool]=...,
+ ) -> "Enumerated": ...
+
+
+class CommonString(OctetString):
+ def pps(
+ self,
+ decode_path: Tuple[str, ...]=...,
+ no_unicode: bool=...,
+ ) -> TSequence[PP]: ...
+
+
+class UTF8String(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "UTF8String"
+
+ def __init__(
+ self,
+ value: Optional[Union["UTF8String", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["UTF8String", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ def __str__(self) -> str: ...
+
+
+class NumericString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "NumericString"
+
+ def __init__(
+ self,
+ value: Optional[Union["NumericString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["NumericString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class PrintableString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "PrintableString"
+
+ def __init__(
+ self,
+ value: Optional[Union["PrintableString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["PrintableString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class TeletexString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "TeletexString"
+
+ def __init__(
+ self,
+ value: Optional[Union["TeletexString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["TeletexString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class T61String(TeletexString):
+ asn1_type_name = ... # type: str
+ default = ... # type: "T61String"
+
+
+class VideotexString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "VideotexString"
+
+ def __init__(
+ self,
+ value: Optional[Union["VideotexString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["VideotexString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class IA5String(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "IA5String"
+
+ def __init__(
+ self,
+ value: Optional[Union["IA5String", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["IA5String", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class UTCTime(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "UTCTime"
+
+ def __init__(
+ self,
+ value: Optional[Union["UTCTime", datetime]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["UTCTime", datetime]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ def todatetime(self) -> datetime: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... # type: ignore
+
+
+class GeneralizedTime(UTCTime):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "GeneralizedTime"
+
+ def todatetime(self) -> datetime: ...
+
+
+class GraphicString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "GraphicString"
+
+ def __init__(
+ self,
+ value: Optional[Union["GraphicString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["GraphicString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class VisibleString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "VisibleString"
+
+ def __init__(
+ self,
+ value: Optional[Union["VisibleString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["VisibleString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class ISO646String(VisibleString):
+ asn1_type_name = ... # type: str
+ default = ... # type: "ISO646String"
+
+
+class GeneralString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "GeneralString"
+
+ def __init__(
+ self,
+ value: Optional[Union["GeneralString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["GeneralString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class UniversalString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "UniversalString"
+
+ def __init__(
+ self,
+ value: Optional[Union["UniversalString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["UniversalString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class BMPString(CommonString):
+ tag_default = ... # type: bytes
+ encoding = ... # type: str
+ asn1_type_name = ... # type: str
+ default = ... # type: "BMPString"
+
+ def __init__(
+ self,
+ value: Optional[Union["BMPString", str, bytes]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["BMPString", str, bytes]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+
+class Choice(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ specs = ... # type: Dict[str, Obj]
+ default = ... # type: "Choice"
+
+ def __init__(
+ self,
+ value: Optional[Union["Choice", Tuple[str, Obj]]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Choice", Tuple[str, Obj]]]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "Choice": ...
+
+ def __call__(
+ self,
+ value: Optional[Union["Choice", Tuple[str, Obj]]]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["Choice", Tuple[str, Obj]]]=...,
+ optional: Optional[bool]=...,
+ ) -> "Choice": ...
+
+ def __getitem__(self, key: str) -> Optional[Obj]: ...
+
+ def __setitem__(self, key: str, value: Obj) -> None: ...
+
+ @property
+ def choice(self) -> str: ...
+
+ @property
+ def value(self) -> Obj: ...
+
+ @property
+ def tlen(self) -> int: ...
+
+ @property
+ def decoded(self) -> bool: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class PrimitiveTypes(Choice):
+ schema = ... # type: Dict[str, Obj]
+
+
+class Any(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "Any"
+
+ def __init__(
+ self,
+ value: Optional[Union[Obj, bytes]]=...,
+ expl: Optional[bytes]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "Any": ...
+
+ def __call__(
+ self,
+ value: Optional[Union[Obj, bytes]]=...,
+ expl: Optional[bytes]=...,
+ optional: Optional[bool]=...,
+ ) -> "Any": ...
+
+ def __bytes__(self) -> bytes: ...
+
+ @property
+ def tlen(self) -> int: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class Sequence(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ specs = ... # type: Dict[str, Obj]
+ default = ... # type: "Sequence"
+
+ def __init__(
+ self,
+ value: Optional["Sequence"]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional["Sequence"]=...,
+ optional: bool=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "Sequence": ...
+
+ def __call__(
+ self,
+ value: Optional["Sequence"]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional["Sequence"]=...,
+ optional: Optional[bool]=...,
+ ) -> "Sequence": ...
+
+ def __getitem__(self, key: str) -> Optional[Obj]: ...
+
+ def __setitem__(self, key: str, value: Obj) -> None: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class Set(Sequence):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "Set"
+
+
+class SequenceOf(Obj):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ spec = ... # type: Obj
+ default = ... # type: "SequenceOf"
+
+ def __init__(
+ self,
+ value: Optional[Union["SequenceOf", TSequence[Obj]]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["SequenceOf", TSequence[Obj]]]=...,
+ optional: Optional[bool]=...,
+ ) -> None: ...
+
+ @property
+ def ready(self) -> bool: ...
+
+ def copy(self) -> "SequenceOf": ...
+
+ def __call__(
+ self,
+ value: Optional[Union["SequenceOf", TSequence[Obj]]]=...,
+ bounds: Optional[Tuple[int, int]]=...,
+ impl: Optional[bytes]=...,
+ expl: Optional[bytes]=...,
+ default: Optional[Union["SequenceOf", TSequence[Obj]]]=...,
+ optional: Optional[bool]=...,
+ ) -> "SequenceOf": ...
+
+ def __getitem__(self, key: int) -> Obj: ...
+
+ def __iter__(self) -> TSequence[Obj]: ...
+
+ def append(self, value: Obj) -> None: ...
+
+ def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ...
+
+
+class SetOf(SequenceOf):
+ tag_default = ... # type: bytes
+ asn1_type_name = ... # type: str
+ default = ... # type: "SetOf"
+
+
+def obj_by_path(pypath: str) -> TAny: ...
--- /dev/null
+# coding: utf-8
+
+from setuptools import setup
+
+
+version = open("VERSION", "rb").read().strip().decode("ascii")
+
+setup(
+ name="pyderasn",
+ version=version,
+ description="Python ASN.1 DER codec with abstract structures",
+ long_description=open("README", "rb").read().decode("utf-8"),
+ author="Sergey Matveev",
+ author_email="stargrave@stargrave.org",
+ url="http://pyderasn.cypherpunks.ru/",
+ license="LGPLv3+",
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "Intended Audience :: System Administrators",
+ "Intended Audience :: Telecommunications Industry",
+ "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 3",
+ "Topic :: Communications",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ py_modules=["pyderasn"],
+ install_requires=["six"],
+)
--- /dev/null
+# coding: utf-8
+# PyDERASN -- Python ASN.1 DER codec with abstract structures
+# Copyright (C) 2017 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+from datetime import datetime
+from unittest import TestCase
+
+from pyderasn import Any
+from pyderasn import BitString
+from pyderasn import Boolean
+from pyderasn import Choice
+from pyderasn import GeneralizedTime
+from pyderasn import hexdec
+from pyderasn import IA5String
+from pyderasn import Integer
+from pyderasn import Null
+from pyderasn import ObjectIdentifier
+from pyderasn import OctetString
+from pyderasn import pprint
+from pyderasn import PrintableString
+from pyderasn import Sequence
+from pyderasn import SequenceOf
+from pyderasn import SetOf
+from pyderasn import tag_ctxc
+from pyderasn import tag_ctxp
+from pyderasn import UTCTime
+
+
+some_oids = {
+ "1.2.840.113549.1.1.1": "id-rsaEncryption",
+ "1.2.840.113549.1.1.5": "id-sha1WithRSAEncryption",
+ "1.2.840.113549.1.9.1": "id-emailAddress",
+ "2.5.29.14": "id-ce-subjectKeyIdentifier",
+ "2.5.29.15": "id-ce-keyUsage",
+ "2.5.29.17": "id-ce-subjectAltName",
+ "2.5.29.18": "id-ce-issuerAltName",
+ "2.5.29.19": "id-ce-basicConstraints",
+ "2.5.29.31": "id-ce-cRLDistributionPoints",
+ "2.5.29.35": "id-ce-authorityKeyIdentifier",
+ "2.5.29.37": "id-ce-extKeyUsage",
+ "2.5.4.3": "id-at-commonName",
+ "2.5.4.6": "id-at-countryName",
+ "2.5.4.7": "id-at-localityName",
+ "2.5.4.8": "id-at-stateOrProvinceName",
+ "2.5.4.10": "id-at-organizationName",
+ "2.5.4.11": "id-at-organizationalUnitName",
+}
+
+
+class Version(Integer):
+ __slots__ = ()
+ schema = (
+ ("v1", 0),
+ ("v2", 1),
+ ("v3", 2),
+ )
+
+
+class CertificateSerialNumber(Integer):
+ __slots__ = ()
+ pass
+
+
+class AlgorithmIdentifier(Sequence):
+ __slots__ = ()
+ schema = (
+ ("algorithm", ObjectIdentifier()),
+ ("parameters", Any(optional=True)),
+ )
+
+
+class AttributeType(ObjectIdentifier):
+ __slots__ = ()
+ pass
+
+
+class AttributeValue(Any):
+ __slots__ = ()
+ pass
+
+
+class AttributeTypeAndValue(Sequence):
+ __slots__ = ()
+ schema = (
+ ("type", AttributeType()),
+ ("value", AttributeValue()),
+ )
+
+
+class RelativeDistinguishedName(SetOf):
+ __slots__ = ()
+ schema = AttributeTypeAndValue()
+ bounds = (1, float("+inf"))
+
+
+class RDNSequence(SequenceOf):
+ __slots__ = ()
+ schema = RelativeDistinguishedName()
+
+
+class Name(Choice):
+ __slots__ = ()
+ schema = (
+ ("rdnSequence", RDNSequence()),
+ )
+
+
+class Time(Choice):
+ __slots__ = ()
+ schema = (
+ ("utcTime", UTCTime()),
+ ("generalTime", GeneralizedTime()),
+ )
+
+
+class Validity(Sequence):
+ __slots__ = ()
+ schema = (
+ ("notBefore", Time()),
+ ("notAfter", Time()),
+ )
+
+
+class SubjectPublicKeyInfo(Sequence):
+ __slots__ = ()
+ schema = (
+ ("algorithm", AlgorithmIdentifier()),
+ ("subjectPublicKey", BitString()),
+ )
+
+
+class UniqueIdentifier(BitString):
+ __slots__ = ()
+ pass
+
+
+class Extension(Sequence):
+ __slots__ = ()
+ schema = (
+ ("extnID", ObjectIdentifier()),
+ ("critical", Boolean(default=False)),
+ ("extnValue", OctetString()),
+ )
+
+
+class Extensions(SequenceOf):
+ __slots__ = ()
+ schema = Extension()
+ bounds = (1, float("+inf"))
+
+
+class TBSCertificate(Sequence):
+ __slots__ = ()
+ schema = (
+ ("version", Version(expl=tag_ctxc(0), default="v1")),
+ ("serialNumber", CertificateSerialNumber()),
+ ("signature", AlgorithmIdentifier()),
+ ("issuer", Name()),
+ ("validity", Validity()),
+ ("subject", Name()),
+ ("subjectPublicKeyInfo", SubjectPublicKeyInfo()),
+ ("issuerUniqueID", UniqueIdentifier(impl=tag_ctxp(1), optional=True)),
+ ("subjectUniqueID", UniqueIdentifier(impl=tag_ctxp(2), optional=True)),
+ ("extensions", Extensions(expl=tag_ctxc(3), optional=True)),
+ )
+
+
+class Certificate(Sequence):
+ __slots__ = ()
+ schema = (
+ ("tbsCertificate", TBSCertificate()),
+ ("signatureAlgorithm", AlgorithmIdentifier()),
+ ("signatureValue", BitString()),
+ )
+
+
+class TestGoSelfSignedVector(TestCase):
+ def runTest(self):
+ raw = hexdec("".join((
+ "30820218308201c20209008cc3379210ec2c98300d06092a864886f70d0101050",
+ "500308192310b3009060355040613025858311330110603550408130a536f6d65",
+ "2d5374617465310d300b06035504071304436974793121301f060355040a13184",
+ "96e7465726e6574205769646769747320507479204c7464311a30180603550403",
+ "131166616c73652e6578616d706c652e636f6d3120301e06092a864886f70d010",
+ "901161166616c7365406578616d706c652e636f6d301e170d3039313030383030",
+ "323535335a170d3130313030383030323535335a308192310b300906035504061",
+ "3025858311330110603550408130a536f6d652d5374617465310d300b06035504",
+ "071304436974793121301f060355040a1318496e7465726e65742057696467697",
+ "47320507479204c7464311a30180603550403131166616c73652e6578616d706c",
+ "652e636f6d3120301e06092a864886f70d010901161166616c7365406578616d7",
+ "06c652e636f6d305c300d06092a864886f70d0101010500034b003048024100cd",
+ "b7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695a331b1deadea",
+ "dd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8fdeec357d0203",
+ "010001300d06092a864886f70d0101050500034100a67b06ec5ece92772ca413c",
+ "ba3ca12568fdc6c7b4511cd40a7f659980402df2b998bb9a4a8cbeb34c0f0a78c",
+ "f8d91ede14a5ed76bf116fe360aafa8821490435",
+ )))
+ crt, tail = Certificate().decode(raw)
+ self.assertSequenceEqual(tail, b"")
+ tbs = crt["tbsCertificate"]
+ self.assertEqual(tbs["version"], 0)
+ self.assertFalse(tbs["version"].decoded)
+ self.assertNotIn("version", tbs)
+ self.assertEqual(tbs["serialNumber"], 10143011886257155224)
+
+ def assert_raw_equals(obj, expect):
+ self.assertTrue(obj.decoded)
+ self.assertSequenceEqual(
+ raw[obj.offset:obj.offset + obj.tlvlen],
+ expect.encode(),
+ )
+ assert_raw_equals(tbs["serialNumber"], Integer(10143011886257155224))
+ algo_id = AlgorithmIdentifier()
+ algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5")
+ algo_id["parameters"] = Any(Null())
+ self.assertEqual(tbs["signature"], algo_id)
+ assert_raw_equals(tbs["signature"], algo_id)
+ issuer = Name()
+ rdnSeq = RDNSequence()
+ for oid, klass, text in (
+ ("2.5.4.6", PrintableString, "XX"),
+ ("2.5.4.8", PrintableString, "Some-State"),
+ ("2.5.4.7", PrintableString, "City"),
+ ("2.5.4.10", PrintableString, "Internet Widgits Pty Ltd"),
+ ("2.5.4.3", PrintableString, "false.example.com"),
+ ("1.2.840.113549.1.9.1", IA5String, "false@example.com"),
+ ):
+ attr = AttributeTypeAndValue()
+ attr["type"] = AttributeType(oid)
+ attr["value"] = AttributeValue(klass(text))
+ rdn = RelativeDistinguishedName()
+ rdn.append(attr)
+ rdnSeq.append(rdn)
+ issuer["rdnSequence"] = rdnSeq
+ self.assertEqual(tbs["issuer"], issuer)
+ assert_raw_equals(tbs["issuer"], issuer)
+ validity = Validity()
+ validity["notBefore"] = Time(
+ ("utcTime", UTCTime(datetime(2009, 10, 8, 0, 25, 53)))
+ )
+ validity["notAfter"] = Time(
+ ("utcTime", UTCTime(datetime(2010, 10, 8, 0, 25, 53)))
+ )
+ self.assertEqual(tbs["validity"], validity)
+ assert_raw_equals(tbs["validity"], validity)
+ self.assertEqual(tbs["subject"], issuer)
+ assert_raw_equals(tbs["subject"], issuer)
+ spki = SubjectPublicKeyInfo()
+ algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.1")
+ spki["algorithm"] = algo_id
+ spki["subjectPublicKey"] = BitString(hexdec("".join((
+ "3048024100cdb7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695",
+ "a331b1deadeadd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8f",
+ "deec357d0203010001",
+ ))))
+ self.assertEqual(tbs["subjectPublicKeyInfo"], spki)
+ assert_raw_equals(tbs["subjectPublicKeyInfo"], spki)
+ self.assertNotIn("issuerUniqueID", tbs)
+ self.assertNotIn("subjectUniqueID", tbs)
+ self.assertNotIn("extensions", tbs)
+ algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5")
+ self.assertEqual(crt["signatureAlgorithm"], algo_id)
+ self.assertEqual(crt["signatureValue"], BitString(hexdec("".join((
+ "a67b06ec5ece92772ca413cba3ca12568fdc6c7b4511cd40a7f659980402df2b",
+ "998bb9a4a8cbeb34c0f0a78cf8d91ede14a5ed76bf116fe360aafa8821490435",
+ )))))
+ self.assertSequenceEqual(crt.encode(), raw)
+ pprint(crt)
+ repr(crt)
+
+ tbs = TBSCertificate()
+ tbs["serialNumber"] = CertificateSerialNumber(10143011886257155224)
+
+ sign_algo_id = AlgorithmIdentifier()
+ sign_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5")
+ sign_algo_id["parameters"] = Any(Null())
+ tbs["signature"] = sign_algo_id
+
+ rdnSeq = RDNSequence()
+ for oid, klass, text in (
+ ("2.5.4.6", PrintableString, "XX"),
+ ("2.5.4.8", PrintableString, "Some-State"),
+ ("2.5.4.7", PrintableString, "City"),
+ ("2.5.4.10", PrintableString, "Internet Widgits Pty Ltd"),
+ ("2.5.4.3", PrintableString, "false.example.com"),
+ ("1.2.840.113549.1.9.1", IA5String, "false@example.com"),
+ ):
+ attr = AttributeTypeAndValue()
+ attr["type"] = AttributeType(oid)
+ attr["value"] = AttributeValue(klass(text))
+ rdn = RelativeDistinguishedName()
+ rdn.append(attr)
+ rdnSeq.append(rdn)
+ issuer = Name()
+ issuer["rdnSequence"] = rdnSeq
+ tbs["issuer"] = issuer
+ tbs["subject"] = issuer
+
+ validity = Validity()
+ validity["notBefore"] = Time(("utcTime", UTCTime(datetime(2009, 10, 8, 0, 25, 53))))
+ validity["notAfter"] = Time(("utcTime", UTCTime(datetime(2010, 10, 8, 0, 25, 53))))
+ tbs["validity"] = validity
+
+ spki = SubjectPublicKeyInfo()
+ spki_algo_id = sign_algo_id.copy()
+ spki_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.1")
+ spki["algorithm"] = spki_algo_id
+ spki["subjectPublicKey"] = BitString(hexdec("".join((
+ "3048024100cdb7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695",
+ "a331b1deadeadd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8f",
+ "deec357d0203010001",
+ ))))
+ tbs["subjectPublicKeyInfo"] = spki
+
+ crt = Certificate()
+ crt["tbsCertificate"] = tbs
+ crt["signatureAlgorithm"] = sign_algo_id
+ crt["signatureValue"] = BitString(hexdec("".join((
+ "a67b06ec5ece92772ca413cba3ca12568fdc6c7b4511cd40a7f659980402df2b",
+ "998bb9a4a8cbeb34c0f0a78cf8d91ede14a5ed76bf116fe360aafa8821490435",
+ ))))
+ self.assertSequenceEqual(crt.encode(), raw)
+
+
+class TestGoPayPalVector(TestCase):
+ def runTest(self):
+ raw = hexdec("".join((
+ "30820644308205ada003020102020300f09b300d06092a864886f70d010105050",
+ "030820112310b3009060355040613024553311230100603550408130942617263",
+ "656c6f6e61311230100603550407130942617263656c6f6e61312930270603550",
+ "40a13204950532043657274696669636174696f6e20417574686f726974792073",
+ "2e6c2e312e302c060355040a142567656e6572616c4069707363612e636f6d204",
+ "32e492e462e2020422d423632323130363935312e302c060355040b1325697073",
+ "434120434c41534541312043657274696669636174696f6e20417574686f72697",
+ "479312e302c06035504031325697073434120434c415345413120436572746966",
+ "69636174696f6e20417574686f726974793120301e06092a864886f70d0109011",
+ "61167656e6572616c4069707363612e636f6d301e170d30393032323432333034",
+ "31375a170d3131303232343233303431375a308194310b3009060355040613025",
+ "553311330110603550408130a43616c69666f726e696131163014060355040713",
+ "0d53616e204672616e636973636f3111300f060355040a1308536563757269747",
+ "931143012060355040b130b53656375726520556e6974312f302d060355040313",
+ "267777772e70617970616c2e636f6d0073736c2e736563757265636f6e6e65637",
+ "4696f6e2e636330819f300d06092a864886f70d010101050003818d0030818902",
+ "818100d269fa6f3a00b4211bc8b102d73f19b2c46db454f88b8accdb72c29e3c6",
+ "0b9c6913d82b77d99ffd12984c173539c82ddfc248c77d541f3e81e42a1ad2d9e",
+ "ff5b1026ce9d571773162338c8d6f1baa3965b16674a4f73973a4d14a4f4e23f8",
+ "b058342d1d0dc2f7ae5b610b211c0dc212a90ffae97715a4981ac40f33bb859b2",
+ "4f0203010001a38203213082031d30090603551d1304023000301106096086480",
+ "186f8420101040403020640300b0603551d0f0404030203f830130603551d2504",
+ "0c300a06082b06010505070301301d0603551d0e04160414618f61344355147f2",
+ "709ce4c8bea9b7b1925bc6e301f0603551d230418301680140e0760d439c91b5b",
+ "5d907b23c8d2349d4a9a463930090603551d1104023000301c0603551d1204153",
+ "013811167656e6572616c4069707363612e636f6d307206096086480186f84201",
+ "0d046516634f7267616e697a6174696f6e20496e666f726d6174696f6e204e4f5",
+ "42056414c4944415445442e20434c415345413120536572766572204365727469",
+ "666963617465206973737565642062792068747470733a2f2f7777772e6970736",
+ "3612e636f6d2f302f06096086480186f84201020422162068747470733a2f2f77",
+ "77772e69707363612e636f6d2f6970736361323030322f304306096086480186f",
+ "84201040436163468747470733a2f2f7777772e69707363612e636f6d2f697073",
+ "6361323030322f697073636132303032434c41534541312e63726c30460609608",
+ "6480186f84201030439163768747470733a2f2f7777772e69707363612e636f6d",
+ "2f6970736361323030322f7265766f636174696f6e434c41534541312e68746d6",
+ "c3f304306096086480186f84201070436163468747470733a2f2f7777772e6970",
+ "7363612e636f6d2f6970736361323030322f72656e6577616c434c41534541312",
+ "e68746d6c3f304106096086480186f84201080434163268747470733a2f2f7777",
+ "772e69707363612e636f6d2f6970736361323030322f706f6c696379434c41534",
+ "541312e68746d6c3081830603551d1f047c307a3039a037a0358633687474703a",
+ "2f2f7777772e69707363612e636f6d2f6970736361323030322f6970736361323",
+ "03032434c41534541312e63726c303da03ba0398637687474703a2f2f77777762",
+ "61636b2e69707363612e636f6d2f6970736361323030322f69707363613230303",
+ "2434c41534541312e63726c303206082b0601050507010104263024302206082b",
+ "060105050730018616687474703a2f2f6f6373702e69707363612e636f6d2f300",
+ "d06092a864886f70d01010505000381810068ee799797dd3bef166a06f2149a6e",
+ "cd9e12f7aa8310bdd17c98fac7aed40e2c9e38059d5260a9990a81b498901daeb",
+ "b4ad7b9dc889e3778415bf782a5f2ba41255a901a1e4538a1525875942644fb20",
+ "07ba44cce54a2d723f9847f626dc054605076321ab469b9c78d5545b3d0c1ec86",
+ "48cb55023826fdbb8221c439607a8bb",
+ )))
+ crt, tail = Certificate().decode(raw)
+ self.assertSequenceEqual(tail, b"")
+ self.assertSequenceEqual(crt.encode(), raw)
+ pprint(crt)
+ repr(crt)
--- /dev/null
+# coding: utf-8
+# PyDERASN -- Python ASN.1 DER codec with abstract structures
+# Copyright (C) 2017 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+from datetime import datetime
+from string import ascii_letters
+from string import printable
+from string import whitespace
+from unittest import TestCase
+
+from hypothesis import assume
+from hypothesis import given
+from hypothesis import settings
+from hypothesis.strategies import binary
+from hypothesis.strategies import booleans
+from hypothesis.strategies import composite
+from hypothesis.strategies import data as data_strategy
+from hypothesis.strategies import datetimes
+from hypothesis.strategies import dictionaries
+from hypothesis.strategies import integers
+from hypothesis.strategies import just
+from hypothesis.strategies import lists
+from hypothesis.strategies import none
+from hypothesis.strategies import one_of
+from hypothesis.strategies import permutations
+from hypothesis.strategies import sampled_from
+from hypothesis.strategies import sets
+from hypothesis.strategies import text
+from hypothesis.strategies import tuples
+from six import assertRaisesRegex
+from six import byte2int
+from six import indexbytes
+from six import int2byte
+from six import iterbytes
+from six import PY2
+from six import text_type
+
+from pyderasn import _pp
+from pyderasn import Any
+from pyderasn import BitString
+from pyderasn import BMPString
+from pyderasn import Boolean
+from pyderasn import BoundsError
+from pyderasn import Choice
+from pyderasn import DecodeError
+from pyderasn import Enumerated
+from pyderasn import GeneralizedTime
+from pyderasn import GeneralString
+from pyderasn import GraphicString
+from pyderasn import hexdec
+from pyderasn import hexenc
+from pyderasn import IA5String
+from pyderasn import Integer
+from pyderasn import InvalidLength
+from pyderasn import InvalidOID
+from pyderasn import InvalidValueType
+from pyderasn import len_decode
+from pyderasn import len_encode
+from pyderasn import NotEnoughData
+from pyderasn import Null
+from pyderasn import NumericString
+from pyderasn import ObjectIdentifier
+from pyderasn import ObjNotReady
+from pyderasn import ObjUnknown
+from pyderasn import OctetString
+from pyderasn import pp_console_row
+from pyderasn import pprint
+from pyderasn import PrintableString
+from pyderasn import Sequence
+from pyderasn import SequenceOf
+from pyderasn import Set
+from pyderasn import SetOf
+from pyderasn import tag_ctxc
+from pyderasn import tag_decode
+from pyderasn import tag_encode
+from pyderasn import tag_strip
+from pyderasn import TagClassApplication
+from pyderasn import TagClassContext
+from pyderasn import TagClassPrivate
+from pyderasn import TagClassUniversal
+from pyderasn import TagFormConstructed
+from pyderasn import TagFormPrimitive
+from pyderasn import TagMismatch
+from pyderasn import TeletexString
+from pyderasn import UniversalString
+from pyderasn import UTCTime
+from pyderasn import UTF8String
+from pyderasn import VideotexString
+from pyderasn import VisibleString
+
+
+settings.register_profile('local', settings(
+ deadline=5000,
+ perform_health_check=False,
+))
+settings.load_profile('local')
+LONG_TEST_MAX_EXAMPLES = settings().max_examples * 4
+
+tag_classes = sampled_from((
+ TagClassApplication,
+ TagClassContext,
+ TagClassPrivate,
+ TagClassUniversal,
+))
+tag_forms = sampled_from((TagFormConstructed, TagFormPrimitive))
+
+
+class TestHex(TestCase):
+ @given(binary())
+ def test_symmetric(self, data):
+ self.assertEqual(hexdec(hexenc(data)), data)
+
+
+class TestTagCoder(TestCase):
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ tag_classes,
+ tag_forms,
+ integers(min_value=0, max_value=30),
+ binary(max_size=5),
+ )
+ def test_short(self, klass, form, num, junk):
+ raw = tag_encode(klass=klass, form=form, num=num)
+ self.assertEqual(tag_decode(raw), (klass, form, num))
+ self.assertEqual(len(raw), 1)
+ self.assertEqual(
+ byte2int(tag_encode(klass=klass, form=form, num=0)),
+ byte2int(raw) & (1 << 7 | 1 << 6 | 1 << 5),
+ )
+ stripped, tlen, tail = tag_strip(memoryview(raw + junk))
+ self.assertSequenceEqual(stripped.tobytes(), raw)
+ self.assertEqual(tlen, len(raw))
+ self.assertSequenceEqual(tail, junk)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ tag_classes,
+ tag_forms,
+ integers(min_value=31),
+ binary(max_size=5),
+ )
+ def test_long(self, klass, form, num, junk):
+ raw = tag_encode(klass=klass, form=form, num=num)
+ self.assertEqual(tag_decode(raw), (klass, form, num))
+ self.assertGreater(len(raw), 1)
+ self.assertEqual(
+ byte2int(tag_encode(klass=klass, form=form, num=0)) | 31,
+ byte2int(raw[:1]),
+ )
+ self.assertEqual(byte2int(raw[-1:]) & 0x80, 0)
+ self.assertTrue(all(b & 0x80 > 0 for b in iterbytes(raw[1:-1])))
+ stripped, tlen, tail = tag_strip(memoryview(raw + junk))
+ self.assertSequenceEqual(stripped.tobytes(), raw)
+ self.assertEqual(tlen, len(raw))
+ self.assertSequenceEqual(tail, junk)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(integers(min_value=31))
+ def test_unfinished_tag(self, num):
+ raw = bytearray(tag_encode(num=num))
+ for i in range(1, len(raw)):
+ raw[i] |= 0x80
+ with assertRaisesRegex(self, DecodeError, "unfinished tag"):
+ tag_strip(bytes(raw))
+
+ def test_go_vectors_valid(self):
+ for data, (eklass, etag, elen, eform) in (
+ (b"\x80\x01", (TagClassContext, 0, 1, TagFormPrimitive)),
+ (b"\xa0\x01", (TagClassContext, 0, 1, TagFormConstructed)),
+ (b"\x02\x00", (TagClassUniversal, 2, 0, TagFormPrimitive)),
+ (b"\xfe\x00", (TagClassPrivate, 30, 0, TagFormConstructed)),
+ (b"\x1f\x1f\x00", (TagClassUniversal, 31, 0, TagFormPrimitive)),
+ (b"\x1f\x81\x00\x00", (TagClassUniversal, 128, 0, TagFormPrimitive)),
+ (b"\x1f\x81\x80\x01\x00", (TagClassUniversal, 0x4001, 0, TagFormPrimitive)),
+ (b"\x00\x81\x80", (TagClassUniversal, 0, 128, TagFormPrimitive)),
+ (b"\x00\x82\x01\x00", (TagClassUniversal, 0, 256, TagFormPrimitive)),
+ (b"\xa0\x84\x7f\xff\xff\xff", (TagClassContext, 0, 0x7fffffff, TagFormConstructed)),
+ ):
+ tag, _, len_encoded = tag_strip(memoryview(data))
+ klass, form, num = tag_decode(tag)
+ _len, _, tail = len_decode(len_encoded)
+ self.assertSequenceEqual(tail, b"")
+ self.assertEqual(klass, eklass)
+ self.assertEqual(num, etag)
+ self.assertEqual(_len, elen)
+ self.assertEqual(form, eform)
+
+ def test_go_vectors_invalid(self):
+ for data in (
+ b"\x00\x83\x01\x00",
+ b"\x1f\x85",
+ b"\x30\x80",
+ b"\xa0\x82\x00\xff",
+ b"\xa0\x81\x7f",
+ ):
+ with self.assertRaises(DecodeError):
+ _, _, len_encoded = tag_strip(memoryview(data))
+ len_decode(len_encoded)
+
+ @given(
+ integers(min_value=0, max_value=127),
+ integers(min_value=0, max_value=2),
+ )
+ def test_long_instead_of_short(self, l, dummy_num):
+ octets = (b"\x00" * dummy_num) + int2byte(l)
+ octets = int2byte((dummy_num + 1) | 0x80) + octets
+ with self.assertRaises(DecodeError):
+ len_decode(octets)
+
+
+class TestLenCoder(TestCase):
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ integers(min_value=0, max_value=127),
+ binary(max_size=5),
+ )
+ def test_short(self, l, junk):
+ raw = len_encode(l) + junk
+ decoded, llen, tail = len_decode(memoryview(raw))
+ self.assertEqual(decoded, l)
+ self.assertEqual(llen, 1)
+ self.assertEqual(len(raw), 1 + len(junk))
+ self.assertEqual(tail.tobytes(), junk)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ integers(min_value=128),
+ binary(max_size=5),
+ )
+ def test_long(self, l, junk):
+ raw = len_encode(l) + junk
+ decoded, llen, tail = len_decode(memoryview(raw))
+ self.assertEqual(decoded, l)
+ self.assertEqual((llen - 1) | 0x80, byte2int(raw))
+ self.assertEqual(llen, len(raw) - len(junk))
+ self.assertNotEqual(indexbytes(raw, 1), 0)
+ self.assertSequenceEqual(tail.tobytes(), junk)
+
+ def test_empty(self):
+ with self.assertRaises(NotEnoughData):
+ len_decode(b"")
+
+ @given(integers(min_value=128))
+ def test_stripped(self, _len):
+ with self.assertRaises(NotEnoughData):
+ len_decode(len_encode(_len)[:-1])
+
+
+text_printable = text(alphabet=printable, min_size=1)
+
+
+@composite
+def text_letters(draw):
+ result = draw(text(alphabet=ascii_letters, min_size=1))
+ if PY2:
+ result = result.encode("ascii")
+ return result
+
+
+class CommonMixin(object):
+ def test_tag_default(self):
+ obj = self.base_klass()
+ self.assertEqual(obj.tag, obj.tag_default)
+
+ def test_simultaneous_impl_expl(self):
+ with self.assertRaises(ValueError):
+ self.base_klass(impl=b"whatever", expl=b"whenever")
+
+ @given(binary(), integers(), integers(), integers())
+ def test_decoded(self, impl, offset, llen, vlen):
+ obj = self.base_klass(impl=impl, _decoded=(offset, llen, vlen))
+ self.assertEqual(obj.offset, offset)
+ self.assertEqual(obj.llen, llen)
+ self.assertEqual(obj.vlen, vlen)
+ self.assertEqual(obj.tlen, len(impl))
+ self.assertEqual(obj.tlvlen, obj.tlen + obj.llen + obj.vlen)
+
+ @given(binary())
+ def test_impl_inherited(self, impl_tag):
+ class Inherited(self.base_klass):
+ __slots__ = ()
+ impl = impl_tag
+ obj = Inherited()
+ self.assertSequenceEqual(obj.impl, impl_tag)
+ self.assertFalse(obj.expled)
+
+ @given(binary())
+ def test_expl_inherited(self, expl_tag):
+ class Inherited(self.base_klass):
+ __slots__ = ()
+ expl = expl_tag
+ obj = Inherited()
+ self.assertSequenceEqual(obj.expl, expl_tag)
+ self.assertTrue(obj.expled)
+
+ def assert_copied_basic_fields(self, obj, obj_copied):
+ self.assertEqual(obj, obj_copied)
+ self.assertSequenceEqual(obj.tag, obj_copied.tag)
+ self.assertEqual(obj.expl_tag, obj_copied.expl_tag)
+ self.assertEqual(obj.default, obj_copied.default)
+ self.assertEqual(obj.optional, obj_copied.optional)
+ self.assertEqual(obj.offset, obj_copied.offset)
+ self.assertEqual(obj.llen, obj_copied.llen)
+ self.assertEqual(obj.vlen, obj_copied.vlen)
+
+
+@composite
+def boolean_values_strat(draw, do_expl=False):
+ value = draw(one_of(none(), booleans()))
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ default = draw(one_of(none(), booleans()))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, impl, expl, default, optional, _decoded)
+
+
+class BooleanInherited(Boolean):
+ __slots__ = ()
+
+
+class TestBoolean(CommonMixin, TestCase):
+ base_klass = Boolean
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ Boolean(123)
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = Boolean(default=Boolean(False), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(booleans())
+ def test_ready(self, value):
+ obj = Boolean()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = Boolean(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(booleans(), booleans(), binary(), binary())
+ def test_comparison(self, value1, value2, tag1, tag2):
+ for klass in (Boolean, BooleanInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == bool(obj2), value1 == value2)
+ obj1 = klass(value1, impl=tag1)
+ obj2 = klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (Boolean, BooleanInherited):
+ (
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(boolean_values_strat())
+ obj_initial = klass(
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial or False,
+ _decoded_initial,
+ )
+ (
+ value,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(boolean_values_strat(do_expl=impl_initial is None))
+ obj = obj_initial(value, impl, expl, default, optional)
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+
+ @given(boolean_values_strat())
+ def test_copy(self, values):
+ for klass in (Boolean, BooleanInherited):
+ obj = klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+
+ @given(
+ booleans(),
+ integers(min_value=1).map(tag_encode),
+ )
+ def test_stripped(self, value, tag_impl):
+ obj = Boolean(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ booleans(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = Boolean(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Boolean().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_expl_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Boolean(expl=Boolean.tag_default).decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Boolean().decode(
+ Boolean.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_expl_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Boolean(expl=Boolean.tag_default).decode(
+ Boolean.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ boolean_values_strat(),
+ booleans(),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, value, tag_expl, offset):
+ for klass in (Boolean, BooleanInherited):
+ _, _, _, default, optional, _decoded = values
+ obj = klass(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(bool(obj_decoded), bool(obj_expled))
+ self.assertEqual(bool(obj_decoded), bool(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+ @given(integers(min_value=2))
+ def test_invalid_len(self, l):
+ with self.assertRaises(InvalidLength):
+ Boolean().decode(b"".join((
+ Boolean.tag_default,
+ len_encode(l),
+ b"\x00" * l,
+ )))
+
+ @given(integers(min_value=0 + 1, max_value=255 - 1))
+ def test_invalid_value(self, value):
+ with assertRaisesRegex(self, DecodeError, "unacceptable Boolean value"):
+ Boolean().decode(b"".join((
+ Boolean.tag_default,
+ len_encode(1),
+ int2byte(value),
+ )))
+
+
+@composite
+def integer_values_strat(draw, do_expl=False):
+ bound_min, value, default, bound_max = sorted(draw(sets(
+ integers(),
+ min_size=4,
+ max_size=4,
+ )))
+ if draw(booleans()):
+ value = None
+ _specs = None
+ if draw(booleans()):
+ _specs = draw(sets(text_letters()))
+ values = draw(sets(
+ integers(),
+ min_size=len(_specs),
+ max_size=len(_specs),
+ ))
+ _specs = list(zip(_specs, values))
+ bounds = None
+ if draw(booleans()):
+ bounds = (bound_min, bound_max)
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ if draw(booleans()):
+ default = None
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, bounds, impl, expl, default, optional, _specs, _decoded)
+
+
+class IntegerInherited(Integer):
+ __slots__ = ()
+
+
+class TestInteger(CommonMixin, TestCase):
+ base_klass = Integer
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ Integer(12.3)
+ repr(err.exception)
+
+ @given(sets(text_letters(), min_size=2))
+ def test_unknown_name(self, names_input):
+ missing = names_input.pop()
+
+ class Int(Integer):
+ __slots__ = ()
+ schema = [(n, 123) for n in names_input]
+ with self.assertRaises(ObjUnknown) as err:
+ Int(missing)
+ repr(err.exception)
+
+ @given(sets(text_letters(), min_size=2))
+ def test_known_name(self, names_input):
+ class Int(Integer):
+ __slots__ = ()
+ schema = [(n, 123) for n in names_input]
+ Int(names_input.pop())
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = Integer(default=Integer(0), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(integers())
+ def test_ready(self, value):
+ obj = Integer()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = Integer(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+ hash(obj)
+
+ @given(integers(), integers(), binary(), binary())
+ def test_comparison(self, value1, value2, tag1, tag2):
+ for klass in (Integer, IntegerInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == int(obj2), value1 == value2)
+ obj1 = klass(value1, impl=tag1)
+ obj2 = klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(lists(integers()))
+ def test_sorted_works(self, values):
+ self.assertSequenceEqual(
+ [int(v) for v in sorted(Integer(v) for v in values)],
+ sorted(values),
+ )
+
+ @given(data_strategy())
+ def test_named(self, d):
+ names_input = list(d.draw(sets(text_letters(), min_size=1)))
+ values_input = list(d.draw(sets(
+ integers(),
+ min_size=len(names_input),
+ max_size=len(names_input),
+ )))
+ chosen_name = d.draw(sampled_from(names_input))
+ names_input = dict(zip(names_input, values_input))
+
+ class Int(Integer):
+ __slots__ = ()
+ schema = names_input
+ _int = Int(chosen_name)
+ self.assertEqual(_int.named, chosen_name)
+ self.assertEqual(int(_int), names_input[chosen_name])
+
+ @given(integers(), integers(min_value=0), integers(min_value=0))
+ def test_bounds_satisfied(self, bound_min, bound_delta, value_delta):
+ value = bound_min + value_delta
+ bound_max = value + bound_delta
+ Integer(value=value, bounds=(bound_min, bound_max))
+
+ @given(sets(integers(), min_size=3, max_size=3))
+ def test_bounds_unsatisfied(self, values):
+ values = sorted(values)
+ with self.assertRaises(BoundsError) as err:
+ Integer(value=values[0], bounds=(values[1], values[2]))
+ repr(err.exception)
+ with self.assertRaises(BoundsError) as err:
+ Integer(value=values[2], bounds=(values[0], values[1]))
+ repr(err.exception)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (Integer, IntegerInherited):
+ (
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _specs_initial,
+ _decoded_initial,
+ ) = d.draw(integer_values_strat())
+ obj_initial = klass(
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial or False,
+ _specs_initial,
+ _decoded_initial,
+ )
+ (
+ value,
+ bounds,
+ impl,
+ expl,
+ default,
+ optional,
+ _,
+ _decoded,
+ ) = d.draw(integer_values_strat(do_expl=impl_initial is None))
+ if (default is None) and (obj_initial.default is not None):
+ bounds = None
+ if (
+ (bounds is None) and
+ (value is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= value <= bounds_initial[1])
+ ):
+ value = None
+ if (
+ (bounds is None) and
+ (default is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= default <= bounds_initial[1])
+ ):
+ default = None
+ obj = obj_initial(value, bounds, impl, expl, default, optional)
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(
+ (obj._bound_min, obj._bound_max),
+ bounds or bounds_initial or (float("-inf"), float("+inf")),
+ )
+ self.assertEqual(
+ obj.specs,
+ {} if _specs_initial is None else dict(_specs_initial),
+ )
+
+ @given(integer_values_strat())
+ def test_copy(self, values):
+ for klass in (Integer, IntegerInherited):
+ obj = klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj.specs, obj_copied.specs)
+ self.assertEqual(obj._bound_min, obj_copied._bound_min)
+ self.assertEqual(obj._bound_max, obj_copied._bound_max)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(
+ integers(),
+ integers(min_value=1).map(tag_encode),
+ )
+ def test_stripped(self, value, tag_impl):
+ obj = Integer(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = Integer(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ def test_zero_len(self):
+ with self.assertRaises(NotEnoughData):
+ Integer().decode(b"".join((
+ Integer.tag_default,
+ len_encode(0),
+ )))
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Integer().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Integer().decode(
+ Integer.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ sets(integers(), min_size=2, max_size=2),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_invalid_bounds_while_decoding(self, ints, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ value, bound_min = list(sorted(ints))
+
+ class Int(Integer):
+ bounds = (bound_min, bound_min)
+ with self.assertRaises(DecodeError) as err:
+ Int().decode(
+ Integer(value).encode(),
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ integer_values_strat(),
+ integers(),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, value, tag_expl, offset):
+ for klass in (Integer, IntegerInherited):
+ _, _, _, _, default, optional, _, _decoded = values
+ obj = klass(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(int(obj_decoded), int(obj_expled))
+ self.assertEqual(int(obj_decoded), int(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+ def test_go_vectors_valid(self):
+ for data, expect in ((
+ (b"\x00", 0),
+ (b"\x7f", 127),
+ (b"\x80", -128),
+ (b"\xff\x7f", -129),
+ (b"\xff", -1),
+ (b"\x01", 1),
+ (b"\x00\xff", 255),
+ (b"\xff\x00", -256),
+ (b"\x01\x00", 256),
+ (b"\x00\x80", 128),
+ (b"\x01\x00", 256),
+ (b"\x80\x00\x00\x00\x00\x00\x00\x00", -9223372036854775808),
+ (b"\x80\x00\x00\x00", -2147483648),
+ )):
+ self.assertEqual(
+ Integer().decode(b"".join((
+ Integer.tag_default,
+ len_encode(len(data)),
+ data,
+ )))[0],
+ expect,
+ )
+
+ def test_go_vectors_invalid(self):
+ for data in ((
+ b"\x00\x7f",
+ b"\xff\xf0",
+ )):
+ with self.assertRaises(DecodeError):
+ Integer().decode(b"".join((
+ Integer.tag_default,
+ len_encode(len(data)),
+ data,
+ )))
+
+
+@composite
+def bit_string_values_strat(draw, schema=None, value_required=False, do_expl=False):
+ if schema is None:
+ schema = ()
+ if draw(booleans()):
+ schema = draw(sets(text_letters(), min_size=1, max_size=256))
+ bits = draw(sets(
+ integers(min_value=0, max_value=255),
+ min_size=len(schema),
+ max_size=len(schema),
+ ))
+ schema = list(zip(schema, bits))
+
+ def _value(value_required):
+ if not value_required and draw(booleans()):
+ return
+ generation_choice = 0
+ if value_required:
+ generation_choice = draw(sampled_from((1, 2, 3)))
+ if generation_choice == 1 or draw(booleans()):
+ return "'%s'B" % "".join(draw(lists(
+ sampled_from(("0", "1")),
+ max_size=len(schema),
+ )))
+ elif generation_choice == 2 or draw(booleans()):
+ return draw(binary(max_size=len(schema) // 8))
+ elif generation_choice == 3 or draw(booleans()):
+ return tuple(draw(lists(sampled_from([name for name, _ in schema]))))
+ return None
+ value = _value(value_required)
+ default = _value(value_required=False)
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (schema, value, impl, expl, default, optional, _decoded)
+
+
+class BitStringInherited(BitString):
+ __slots__ = ()
+
+
+class TestBitString(CommonMixin, TestCase):
+ base_klass = BitString
+
+ @given(lists(booleans()))
+ def test_b_encoding(self, bits):
+ obj = BitString("'%s'B" % "".join("1" if bit else "0" for bit in bits))
+ self.assertEqual(obj.bit_len, len(bits))
+ self.assertSequenceEqual(list(obj), bits)
+ for i, bit in enumerate(bits):
+ self.assertEqual(obj[i], bit)
+
+ @given(lists(booleans()))
+ def test_out_of_bounds_bits(self, bits):
+ obj = BitString("'%s'B" % "".join("1" if bit else "0" for bit in bits))
+ for i in range(len(bits), len(bits) * 2):
+ self.assertFalse(obj[i])
+
+ def test_bad_b_encoding(self):
+ with self.assertRaises(ValueError):
+ BitString("'010120101'B")
+
+ @given(
+ integers(min_value=1, max_value=255),
+ integers(min_value=1, max_value=255),
+ )
+ def test_named_are_stripped(self, leading_zeros, trailing_zeros):
+ obj = BitString("'%s1%s'B" % (("0" * leading_zeros), ("0" * trailing_zeros)))
+ self.assertEqual(obj.bit_len, leading_zeros + 1 + trailing_zeros)
+ self.assertGreater(len(obj.encode()), (leading_zeros + 1 + trailing_zeros) // 8)
+
+ class BS(BitString):
+ __slots__ = ()
+ schema = (("whatever", 0),)
+ obj = BS("'%s1%s'B" % (("0" * leading_zeros), ("0" * trailing_zeros)))
+ self.assertEqual(obj.bit_len, leading_zeros + 1)
+ self.assertGreater(len(obj.encode()), (leading_zeros + 1) // 8)
+
+ def test_zero_len(self):
+ with self.assertRaises(NotEnoughData):
+ BitString().decode(b"".join((
+ BitString.tag_default,
+ len_encode(0),
+ )))
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ BitString(123)
+ repr(err.exception)
+ with self.assertRaises(InvalidValueType) as err:
+ BitString(u"123")
+ repr(err.exception)
+
+ def test_obj_unknown(self):
+ with self.assertRaises(ObjUnknown) as err:
+ BitString(b"whatever")["whenever"]
+ repr(err.exception)
+
+ def test_get_invalid_typ(self):
+ with self.assertRaises(InvalidValueType) as err:
+ BitString(b"whatever")[(1, 2, 3)]
+ repr(err.exception)
+
+ @given(data_strategy())
+ def test_unknown_name(self, d):
+ _schema = d.draw(sets(text_letters(), min_size=2, max_size=5))
+ missing = _schema.pop()
+
+ class BS(BitString):
+ __slots__ = ()
+ schema = [(n, i) for i, n in enumerate(_schema)]
+ with self.assertRaises(ObjUnknown) as err:
+ BS((missing,))
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = BitString(default=BitString(b""), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(binary())
+ def test_ready(self, value):
+ obj = BitString()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = BitString(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(
+ tuples(integers(min_value=0), binary()),
+ tuples(integers(min_value=0), binary()),
+ binary(),
+ binary(),
+ )
+ def test_comparison(self, value1, value2, tag1, tag2):
+ for klass in (BitString, BitStringInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == bytes(obj2), value1[1] == value2[1])
+ obj1 = klass(value1, impl=tag1)
+ obj2 = klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (BitString, BitStringInherited):
+ (
+ schema_initial,
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(bit_string_values_strat())
+
+ class BS(klass):
+ __slots__ = ()
+ schema = schema_initial
+ obj_initial = BS(
+ value=value_initial,
+ impl=impl_initial,
+ expl=expl_initial,
+ default=default_initial,
+ optional=optional_initial or False,
+ _decoded=_decoded_initial,
+ )
+ (
+ _,
+ value,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(bit_string_values_strat(
+ schema=schema_initial,
+ do_expl=impl_initial is None,
+ ))
+ obj = obj_initial(
+ value=value,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ )
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(obj.specs, obj_initial.specs)
+
+ @given(bit_string_values_strat())
+ def test_copy(self, values):
+ for klass in (BitString, BitStringInherited):
+ _schema, value, impl, expl, default, optional, _decoded = values
+
+ class BS(klass):
+ __slots__ = ()
+ schema = _schema
+ obj = BS(
+ value=value,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional or False,
+ _decoded=_decoded,
+ )
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj.specs, obj_copied.specs)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(
+ binary(),
+ integers(min_value=1).map(tag_encode),
+ )
+ def test_stripped(self, value, tag_impl):
+ obj = BitString(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ binary(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = BitString(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ BitString().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ BitString().decode(
+ BitString.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_symmetric(self, d):
+ (
+ _schema,
+ value,
+ _,
+ _,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(bit_string_values_strat(value_required=True))
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+ offset = d.draw(integers(min_value=0))
+ for klass in (BitString, BitStringInherited):
+ class BS(klass):
+ __slots__ = ()
+ schema = _schema
+ obj = BS(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(bytes(obj_decoded), bytes(obj_expled))
+ self.assertEqual(bytes(obj_decoded), bytes(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+ if isinstance(value, tuple):
+ self.assertSetEqual(set(value), set(obj_decoded.named))
+ for name in value:
+ obj_decoded[name]
+
+ @given(integers(min_value=1, max_value=255))
+ def test_bad_zero_value(self, pad_size):
+ with self.assertRaises(DecodeError):
+ BitString().decode(b"".join((
+ BitString.tag_default,
+ len_encode(1),
+ int2byte(pad_size),
+ )))
+
+ def test_go_vectors_invalid(self):
+ for data in ((
+ b"\x07\x01",
+ b"\x07\x40",
+ b"\x08\x00",
+ )):
+ with self.assertRaises(DecodeError):
+ BitString().decode(b"".join((
+ BitString.tag_default,
+ len_encode(2),
+ data,
+ )))
+
+ def test_go_vectors_valid(self):
+ obj, _ = BitString().decode(b"".join((
+ BitString.tag_default,
+ len_encode(1),
+ b"\x00",
+ )))
+ self.assertEqual(bytes(obj), b"")
+ self.assertEqual(obj.bit_len, 0)
+
+ obj, _ = BitString().decode(b"".join((
+ BitString.tag_default,
+ len_encode(2),
+ b"\x07\x00",
+ )))
+ self.assertEqual(bytes(obj), b"\x00")
+ self.assertEqual(obj.bit_len, 1)
+
+ obj = BitString((16, b"\x82\x40"))
+ self.assertTrue(obj[0])
+ self.assertFalse(obj[1])
+ self.assertTrue(obj[6])
+ self.assertTrue(obj[9])
+ self.assertFalse(obj[17])
+
+
+@composite
+def octet_string_values_strat(draw, do_expl=False):
+ bound_min, bound_max = sorted(draw(sets(
+ integers(min_value=0, max_value=1 << 7),
+ min_size=2,
+ max_size=2,
+ )))
+ value = draw(one_of(
+ none(),
+ binary(min_size=bound_min, max_size=bound_max),
+ ))
+ default = draw(one_of(
+ none(),
+ binary(min_size=bound_min, max_size=bound_max),
+ ))
+ bounds = None
+ if draw(booleans()):
+ bounds = (bound_min, bound_max)
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, bounds, impl, expl, default, optional, _decoded)
+
+
+class OctetStringInherited(OctetString):
+ __slots__ = ()
+
+
+class TestOctetString(CommonMixin, TestCase):
+ base_klass = OctetString
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ OctetString(text_type(123))
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = OctetString(default=OctetString(b""), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(binary())
+ def test_ready(self, value):
+ obj = OctetString()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = OctetString(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(binary(), binary(), binary(), binary())
+ def test_comparison(self, value1, value2, tag1, tag2):
+ for klass in (OctetString, OctetStringInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == bytes(obj2), value1 == value2)
+ obj1 = klass(value1, impl=tag1)
+ obj2 = klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(data_strategy())
+ def test_bounds_satisfied(self, d):
+ bound_min = d.draw(integers(min_value=0, max_value=1 << 7))
+ bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7))
+ value = d.draw(binary(min_size=bound_min, max_size=bound_max))
+ OctetString(value=value, bounds=(bound_min, bound_max))
+
+ @given(data_strategy())
+ def test_bounds_unsatisfied(self, d):
+ bound_min = d.draw(integers(min_value=1, max_value=1 << 7))
+ bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7))
+ value = d.draw(binary(max_size=bound_min - 1))
+ with self.assertRaises(BoundsError) as err:
+ OctetString(value=value, bounds=(bound_min, bound_max))
+ repr(err.exception)
+ value = d.draw(binary(min_size=bound_max + 1))
+ with self.assertRaises(BoundsError) as err:
+ OctetString(value=value, bounds=(bound_min, bound_max))
+ repr(err.exception)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (OctetString, OctetStringInherited):
+ (
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(octet_string_values_strat())
+ obj_initial = klass(
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial or False,
+ _decoded_initial,
+ )
+ (
+ value,
+ bounds,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(octet_string_values_strat(do_expl=impl_initial is None))
+ if (default is None) and (obj_initial.default is not None):
+ bounds = None
+ if (
+ (bounds is None) and
+ (value is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= len(value) <= bounds_initial[1])
+ ):
+ value = None
+ if (
+ (bounds is None) and
+ (default is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= len(default) <= bounds_initial[1])
+ ):
+ default = None
+ obj = obj_initial(value, bounds, impl, expl, default, optional)
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(
+ (obj._bound_min, obj._bound_max),
+ bounds or bounds_initial or (0, float("+inf")),
+ )
+
+ @given(octet_string_values_strat())
+ def test_copy(self, values):
+ for klass in (OctetString, OctetStringInherited):
+ obj = klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._bound_min, obj_copied._bound_min)
+ self.assertEqual(obj._bound_max, obj_copied._bound_max)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(
+ binary(),
+ integers(min_value=1).map(tag_encode),
+ )
+ def test_stripped(self, value, tag_impl):
+ obj = OctetString(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ binary(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = OctetString(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ OctetString().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ OctetString().decode(
+ OctetString.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ sets(integers(min_value=0, max_value=10), min_size=2, max_size=2),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_invalid_bounds_while_decoding(self, ints, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ value, bound_min = list(sorted(ints))
+
+ class String(OctetString):
+ bounds = (bound_min, bound_min)
+ with self.assertRaises(DecodeError) as err:
+ String().decode(
+ OctetString(b"\x00" * value).encode(),
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ octet_string_values_strat(),
+ binary(),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, value, tag_expl, offset):
+ for klass in (OctetString, OctetStringInherited):
+ _, _, _, _, default, optional, _decoded = values
+ obj = klass(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(bytes(obj_decoded), bytes(obj_expled))
+ self.assertEqual(bytes(obj_decoded), bytes(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+
+@composite
+def null_values_strat(draw, do_expl=False):
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (impl, expl, optional, _decoded)
+
+
+class NullInherited(Null):
+ __slots__ = ()
+
+
+class TestNull(CommonMixin, TestCase):
+ base_klass = Null
+
+ def test_ready(self):
+ obj = Null()
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(binary(), binary())
+ def test_comparison(self, tag1, tag2):
+ for klass in (Null, NullInherited):
+ obj1 = klass(impl=tag1)
+ obj2 = klass(impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+ self.assertNotEqual(obj1, tag2)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (Null, NullInherited):
+ (
+ impl_initial,
+ expl_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(null_values_strat())
+ obj_initial = klass(
+ impl=impl_initial,
+ expl=expl_initial,
+ optional=optional_initial or False,
+ _decoded=_decoded_initial,
+ )
+ (
+ impl,
+ expl,
+ optional,
+ _decoded,
+ ) = d.draw(null_values_strat(do_expl=impl_initial is None))
+ obj = obj_initial(impl=impl, expl=expl, optional=optional)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ self.assertEqual(obj.optional, optional)
+
+ @given(null_values_strat())
+ def test_copy(self, values):
+ for klass in (Null, NullInherited):
+ impl, expl, optional, _decoded = values
+ obj = klass(
+ impl=impl,
+ expl=expl,
+ optional=optional or False,
+ _decoded=_decoded,
+ )
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+
+ @given(integers(min_value=1).map(tag_encode))
+ def test_stripped(self, tag_impl):
+ obj = Null(impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(integers(min_value=1).map(tag_ctxc))
+ def test_stripped_expl(self, tag_expl):
+ obj = Null(expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Null().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Null().decode(
+ Null.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(binary(min_size=1))
+ def test_tag_mismatch(self, impl):
+ assume(impl != Null.tag_default)
+ with self.assertRaises(TagMismatch):
+ Null(impl=impl).decode(Null().encode())
+
+ @given(
+ null_values_strat(),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, tag_expl, offset):
+ for klass in (Null, NullInherited):
+ _, _, optional, _decoded = values
+ obj = klass(optional=optional, _decoded=_decoded)
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+ @given(integers(min_value=1))
+ def test_invalid_len(self, l):
+ with self.assertRaises(InvalidLength):
+ Null().decode(b"".join((
+ Null.tag_default,
+ len_encode(l),
+ )))
+
+
+@composite
+def oid_strategy(draw):
+ first_arc = draw(integers(min_value=0, max_value=2))
+ second_arc = 0
+ if first_arc in (0, 1):
+ second_arc = draw(integers(min_value=0, max_value=39))
+ else:
+ second_arc = draw(integers(min_value=0))
+ other_arcs = draw(lists(integers(min_value=0)))
+ return tuple([first_arc, second_arc] + other_arcs)
+
+
+@composite
+def oid_values_strat(draw, do_expl=False):
+ value = draw(one_of(none(), oid_strategy()))
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ default = draw(one_of(none(), oid_strategy()))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, impl, expl, default, optional, _decoded)
+
+
+class ObjectIdentifierInherited(ObjectIdentifier):
+ __slots__ = ()
+
+
+class TestObjectIdentifier(CommonMixin, TestCase):
+ base_klass = ObjectIdentifier
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ ObjectIdentifier(123)
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = ObjectIdentifier(default=ObjectIdentifier("1.2.3"), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(oid_strategy())
+ def test_ready(self, value):
+ obj = ObjectIdentifier()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = ObjectIdentifier(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+ hash(obj)
+
+ @given(oid_strategy(), oid_strategy(), binary(), binary())
+ def test_comparison(self, value1, value2, tag1, tag2):
+ for klass in (ObjectIdentifier, ObjectIdentifierInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == tuple(obj2), value1 == value2)
+ self.assertEqual(str(obj1) == str(obj2), value1 == value2)
+ obj1 = klass(value1, impl=tag1)
+ obj2 = klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(lists(oid_strategy()))
+ def test_sorted_works(self, values):
+ self.assertSequenceEqual(
+ [tuple(v) for v in sorted(ObjectIdentifier(v) for v in values)],
+ sorted(values),
+ )
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (ObjectIdentifier, ObjectIdentifierInherited):
+ (
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(oid_values_strat())
+ obj_initial = klass(
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial or False,
+ _decoded_initial,
+ )
+ (
+ value,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(oid_values_strat(do_expl=impl_initial is None))
+ obj = obj_initial(value, impl, expl, default, optional)
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+
+ @given(oid_values_strat())
+ def test_copy(self, values):
+ for klass in (ObjectIdentifier, ObjectIdentifierInherited):
+ obj = klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ oid_strategy(),
+ integers(min_value=1).map(tag_encode),
+ )
+ def test_stripped(self, value, tag_impl):
+ obj = ObjectIdentifier(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ oid_strategy(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = ObjectIdentifier(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ ObjectIdentifier().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ ObjectIdentifier().decode(
+ ObjectIdentifier.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ def test_zero_oid(self):
+ with self.assertRaises(NotEnoughData):
+ ObjectIdentifier().decode(
+ b"".join((ObjectIdentifier.tag_default, len_encode(0)))
+ )
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(oid_strategy())
+ def test_unfinished_oid(self, value):
+ assume(list(value)[-1] > 255)
+ obj_encoded = ObjectIdentifier(value).encode()
+ obj, _ = ObjectIdentifier().decode(obj_encoded)
+ data = obj_encoded[obj.tlen + obj.llen:-1]
+ data = b"".join((
+ ObjectIdentifier.tag_default,
+ len_encode(len(data)),
+ data,
+ ))
+ with assertRaisesRegex(self, DecodeError, "unfinished OID"):
+ obj.decode(data)
+
+ @given(integers(min_value=0))
+ def test_invalid_short(self, value):
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier((value,))
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier("%d" % value)
+
+ @given(integers(min_value=3), integers(min_value=0))
+ def test_invalid_first_arc(self, first_arc, second_arc):
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier((first_arc, second_arc))
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier("%d.%d" % (first_arc, second_arc))
+
+ @given(integers(min_value=0, max_value=1), integers(min_value=40))
+ def test_invalid_second_arc(self, first_arc, second_arc):
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier((first_arc, second_arc))
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier("%d.%d" % (first_arc, second_arc))
+
+ @given(text(alphabet=ascii_letters + ".", min_size=1))
+ def test_junk(self, oid):
+ with self.assertRaises(InvalidOID):
+ ObjectIdentifier(oid)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(oid_strategy())
+ def test_validness(self, oid):
+ obj = ObjectIdentifier(oid)
+ self.assertEqual(obj, ObjectIdentifier(".".join(str(arc) for arc in oid)))
+ str(obj)
+ repr(obj)
+ pprint(obj)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ oid_values_strat(),
+ oid_strategy(),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, value, tag_expl, offset):
+ for klass in (ObjectIdentifier, ObjectIdentifierInherited):
+ _, _, _, default, optional, _decoded = values
+ obj = klass(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(tuple(obj_decoded), tuple(obj_expled))
+ self.assertEqual(tuple(obj_decoded), tuple(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+ @given(
+ oid_strategy().map(ObjectIdentifier),
+ oid_strategy().map(ObjectIdentifier),
+ )
+ def test_add(self, oid1, oid2):
+ oid_expect = ObjectIdentifier(str(oid1) + "." + str(oid2))
+ for oid_to_add in (oid2, tuple(oid2)):
+ self.assertEqual(oid1 + oid_to_add, oid_expect)
+ with self.assertRaises(InvalidValueType):
+ oid1 + str(oid2)
+
+ def test_go_vectors_valid(self):
+ for data, expect in (
+ (b"\x55", (2, 5)),
+ (b"\x55\x02", (2, 5, 2)),
+ (b"\x55\x02\xc0\x00", (2, 5, 2, 8192)),
+ (b"\x81\x34\x03", (2, 100, 3)),
+ ):
+ self.assertEqual(
+ ObjectIdentifier().decode(b"".join((
+ ObjectIdentifier.tag_default,
+ len_encode(len(data)),
+ data,
+ )))[0],
+ expect,
+ )
+
+ def test_go_vectors_invalid(self):
+ data = b"\x55\x02\xc0\x80\x80\x80\x80"
+ with self.assertRaises(DecodeError):
+ ObjectIdentifier().decode(b"".join((
+ Integer.tag_default,
+ len_encode(len(data)),
+ data,
+ )))
+
+
+@composite
+def enumerated_values_strat(draw, schema=None, do_expl=False):
+ if schema is None:
+ schema = list(draw(sets(text_printable, min_size=1, max_size=3)))
+ values = list(draw(sets(
+ integers(),
+ min_size=len(schema),
+ max_size=len(schema),
+ )))
+ schema = list(zip(schema, values))
+ value = draw(one_of(none(), sampled_from([k for k, v in schema])))
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ default = draw(one_of(none(), sampled_from([v for k, v in schema])))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (schema, value, impl, expl, default, optional, _decoded)
+
+
+class TestEnumerated(CommonMixin, TestCase):
+ class EWhatever(Enumerated):
+ __slots__ = ()
+ schema = (("whatever", 0),)
+
+ base_klass = EWhatever
+
+ def test_schema_required(self):
+ with assertRaisesRegex(self, ValueError, "schema must be specified"):
+ Enumerated()
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass((1, 2))
+ repr(err.exception)
+
+ @given(sets(text_letters(), min_size=2))
+ def test_unknown_name(self, schema_input):
+ missing = schema_input.pop()
+
+ class E(Enumerated):
+ __slots__ = ()
+ schema = [(n, 123) for n in schema_input]
+ with self.assertRaises(ObjUnknown) as err:
+ E(missing)
+ repr(err.exception)
+
+ @given(
+ sets(text_letters(), min_size=2),
+ sets(integers(), min_size=2),
+ )
+ def test_unknown_value(self, schema_input, values_input):
+ schema_input.pop()
+ missing_value = values_input.pop()
+ _input = list(zip(schema_input, values_input))
+
+ class E(Enumerated):
+ __slots__ = ()
+ schema = _input
+ with self.assertRaises(DecodeError) as err:
+ E(missing_value)
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = self.base_klass(default="whatever", optional=optional)
+ self.assertTrue(obj.optional)
+
+ def test_ready(self):
+ obj = self.base_klass()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = self.base_klass("whatever")
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(integers(), integers(), binary(), binary())
+ def test_comparison(self, value1, value2, tag1, tag2):
+ class E(Enumerated):
+ __slots__ = ()
+ schema = (
+ ("whatever0", value1),
+ ("whatever1", value2),
+ )
+
+ class EInherited(E):
+ __slots__ = ()
+ for klass in (E, EInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == int(obj2), value1 == value2)
+ obj1 = klass(value1, impl=tag1)
+ obj2 = klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ (
+ schema_initial,
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(enumerated_values_strat())
+
+ class E(Enumerated):
+ __slots__ = ()
+ schema = schema_initial
+ obj_initial = E(
+ value=value_initial,
+ impl=impl_initial,
+ expl=expl_initial,
+ default=default_initial,
+ optional=optional_initial or False,
+ _decoded=_decoded_initial,
+ )
+ (
+ _,
+ value,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(enumerated_values_strat(
+ schema=schema_initial,
+ do_expl=impl_initial is None,
+ ))
+ obj = obj_initial(
+ value=value,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ )
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(
+ int(obj),
+ dict(schema_initial).get(value_expected, value_expected),
+ )
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(obj.specs, dict(schema_initial))
+
+ @given(enumerated_values_strat())
+ def test_copy(self, values):
+ schema_input, value, impl, expl, default, optional, _decoded = values
+
+ class E(Enumerated):
+ __slots__ = ()
+ schema = schema_input
+ obj = E(
+ value=value,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj.specs, obj_copied.specs)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_symmetric(self, d):
+ schema_input, _, _, _, default, optional, _decoded = d.draw(
+ enumerated_values_strat(),
+ )
+ tag_expl = d.draw(integers(min_value=1).map(tag_ctxc))
+ offset = d.draw(integers(min_value=0))
+ value = d.draw(sampled_from(sorted([v for _, v in schema_input])))
+
+ class E(Enumerated):
+ __slots__ = ()
+ schema = schema_input
+ obj = E(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(int(obj_decoded), int(obj_expled))
+ self.assertEqual(int(obj_decoded), int(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+
+@composite
+def string_values_strat(draw, alphabet, do_expl=False):
+ bound_min, bound_max = sorted(draw(sets(
+ integers(min_value=0, max_value=1 << 7),
+ min_size=2,
+ max_size=2,
+ )))
+ value = draw(one_of(
+ none(),
+ text(alphabet=alphabet, min_size=bound_min, max_size=bound_max),
+ ))
+ default = draw(one_of(
+ none(),
+ text(alphabet=alphabet, min_size=bound_min, max_size=bound_max),
+ ))
+ bounds = None
+ if draw(booleans()):
+ bounds = (bound_min, bound_max)
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, bounds, impl, expl, default, optional, _decoded)
+
+
+class StringMixin(object):
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass((1, 2))
+ repr(err.exception)
+
+ def text_alphabet(self):
+ if self.base_klass.encoding in ("ascii", "iso-8859-1"):
+ return printable + whitespace
+ return None
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = self.base_klass(default=self.base_klass(""), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(data_strategy())
+ def test_ready(self, d):
+ obj = self.base_klass()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ text_type(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ value = d.draw(text(alphabet=self.text_alphabet()))
+ obj = self.base_klass(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+ text_type(obj)
+
+ @given(data_strategy())
+ def test_comparison(self, d):
+ value1 = d.draw(text(alphabet=self.text_alphabet()))
+ value2 = d.draw(text(alphabet=self.text_alphabet()))
+ tag1 = d.draw(binary())
+ tag2 = d.draw(binary())
+ obj1 = self.base_klass(value1)
+ obj2 = self.base_klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == bytes(obj2), value1 == value2)
+ self.assertEqual(obj1 == text_type(obj2), value1 == value2)
+ obj1 = self.base_klass(value1, impl=tag1)
+ obj2 = self.base_klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(data_strategy())
+ def test_bounds_satisfied(self, d):
+ bound_min = d.draw(integers(min_value=0, max_value=1 << 7))
+ bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7))
+ value = d.draw(text(
+ alphabet=self.text_alphabet(),
+ min_size=bound_min,
+ max_size=bound_max,
+ ))
+ self.base_klass(value=value, bounds=(bound_min, bound_max))
+
+ @given(data_strategy())
+ def test_bounds_unsatisfied(self, d):
+ bound_min = d.draw(integers(min_value=1, max_value=1 << 7))
+ bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7))
+ value = d.draw(text(alphabet=self.text_alphabet(), max_size=bound_min - 1))
+ with self.assertRaises(BoundsError) as err:
+ self.base_klass(value=value, bounds=(bound_min, bound_max))
+ repr(err.exception)
+ value = d.draw(text(alphabet=self.text_alphabet(), min_size=bound_max + 1))
+ with self.assertRaises(BoundsError) as err:
+ self.base_klass(value=value, bounds=(bound_min, bound_max))
+ repr(err.exception)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ (
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(string_values_strat(self.text_alphabet()))
+ obj_initial = self.base_klass(
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial or False,
+ _decoded_initial,
+ )
+ (
+ value,
+ bounds,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(string_values_strat(
+ self.text_alphabet(),
+ do_expl=impl_initial is None,
+ ))
+ if (default is None) and (obj_initial.default is not None):
+ bounds = None
+ if (
+ (bounds is None) and
+ (value is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= len(value) <= bounds_initial[1])
+ ):
+ value = None
+ if (
+ (bounds is None) and
+ (default is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= len(default) <= bounds_initial[1])
+ ):
+ default = None
+ obj = obj_initial(value, bounds, impl, expl, default, optional)
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(
+ (obj._bound_min, obj._bound_max),
+ bounds or bounds_initial or (0, float("+inf")),
+ )
+
+ @given(data_strategy())
+ def test_copy(self, d):
+ values = d.draw(string_values_strat(self.text_alphabet()))
+ obj = self.base_klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._bound_min, obj_copied._bound_min)
+ self.assertEqual(obj._bound_max, obj_copied._bound_max)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(data_strategy())
+ def test_stripped(self, d):
+ value = d.draw(text(alphabet=self.text_alphabet()))
+ tag_impl = tag_encode(d.draw(integers(min_value=1)))
+ obj = self.base_klass(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(data_strategy())
+ def test_stripped_expl(self, d):
+ value = d.draw(text(alphabet=self.text_alphabet()))
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+ obj = self.base_klass(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ self.base_klass().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ self.base_klass().decode(
+ self.base_klass.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ sets(integers(min_value=0, max_value=10), min_size=2, max_size=2),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_invalid_bounds_while_decoding(self, ints, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ value, bound_min = list(sorted(ints))
+
+ class String(self.base_klass):
+ # Multiply this value by four, to satisfy UTF-32 bounds
+ # (4 bytes per character) validation
+ bounds = (bound_min * 4, bound_min * 4)
+ with self.assertRaises(DecodeError) as err:
+ String().decode(
+ self.base_klass(b"\x00\x00\x00\x00" * value).encode(),
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(data_strategy())
+ def test_symmetric(self, d):
+ values = d.draw(string_values_strat(self.text_alphabet()))
+ value = d.draw(text(alphabet=self.text_alphabet()))
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+ offset = d.draw(integers(min_value=0))
+ _, _, _, _, default, optional, _decoded = values
+ obj = self.base_klass(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertNotEqual(obj_decoded, obj)
+ self.assertEqual(bytes(obj_decoded), bytes(obj_expled))
+ self.assertEqual(bytes(obj_decoded), bytes(obj))
+ self.assertEqual(text_type(obj_decoded), text_type(obj_expled))
+ self.assertEqual(text_type(obj_decoded), text_type(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+
+class TestUTF8String(StringMixin, CommonMixin, TestCase):
+ base_klass = UTF8String
+
+
+class TestNumericString(StringMixin, CommonMixin, TestCase):
+ base_klass = NumericString
+
+
+class TestPrintableString(StringMixin, CommonMixin, TestCase):
+ base_klass = PrintableString
+
+
+class TestTeletexString(StringMixin, CommonMixin, TestCase):
+ base_klass = TeletexString
+
+
+class TestVideotexString(StringMixin, CommonMixin, TestCase):
+ base_klass = VideotexString
+
+
+class TestIA5String(StringMixin, CommonMixin, TestCase):
+ base_klass = IA5String
+
+
+class TestGraphicString(StringMixin, CommonMixin, TestCase):
+ base_klass = GraphicString
+
+
+class TestVisibleString(StringMixin, CommonMixin, TestCase):
+ base_klass = VisibleString
+
+
+class TestGeneralString(StringMixin, CommonMixin, TestCase):
+ base_klass = GeneralString
+
+
+class TestUniversalString(StringMixin, CommonMixin, TestCase):
+ base_klass = UniversalString
+
+
+class TestBMPString(StringMixin, CommonMixin, TestCase):
+ base_klass = BMPString
+
+
+@composite
+def generalized_time_values_strat(
+ draw,
+ min_datetime,
+ max_datetime,
+ omit_ms=False,
+ do_expl=False,
+):
+ value = None
+ if draw(booleans()):
+ value = draw(datetimes(min_value=min_datetime, max_value=max_datetime))
+ if omit_ms:
+ value = value.replace(microsecond=0)
+ default = None
+ if draw(booleans()):
+ default = draw(datetimes(min_value=min_datetime, max_value=max_datetime))
+ if omit_ms:
+ default = default.replace(microsecond=0)
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, impl, expl, default, optional, _decoded)
+
+
+class TimeMixin(object):
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass(datetime.now().timetuple())
+ repr(err.exception)
+
+ @given(data_strategy())
+ def test_optional(self, d):
+ default = d.draw(datetimes(
+ min_value=self.min_datetime,
+ max_value=self.max_datetime,
+ ))
+ optional = d.draw(booleans())
+ obj = self.base_klass(default=default, optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(data_strategy())
+ def test_ready(self, d):
+ obj = self.base_klass()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ value = d.draw(datetimes(min_value=self.min_datetime))
+ obj = self.base_klass(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(data_strategy())
+ def test_comparison(self, d):
+ value1 = d.draw(datetimes(
+ min_value=self.min_datetime,
+ max_value=self.max_datetime,
+ ))
+ value2 = d.draw(datetimes(
+ min_value=self.min_datetime,
+ max_value=self.max_datetime,
+ ))
+ tag1 = d.draw(binary())
+ tag2 = d.draw(binary())
+ if self.omit_ms:
+ value1 = value1.replace(microsecond=0)
+ value2 = value2.replace(microsecond=0)
+ obj1 = self.base_klass(value1)
+ obj2 = self.base_klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == obj2.todatetime(), value1 == value2)
+ self.assertEqual(obj1 == bytes(obj2), value1 == value2)
+ obj1 = self.base_klass(value1, impl=tag1)
+ obj2 = self.base_klass(value1, impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ (
+ value_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(generalized_time_values_strat(
+ min_datetime=self.min_datetime,
+ max_datetime=self.max_datetime,
+ omit_ms=self.omit_ms,
+ ))
+ obj_initial = self.base_klass(
+ value=value_initial,
+ impl=impl_initial,
+ expl=expl_initial,
+ default=default_initial,
+ optional=optional_initial or False,
+ _decoded=_decoded_initial,
+ )
+ (
+ value,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(generalized_time_values_strat(
+ min_datetime=self.min_datetime,
+ max_datetime=self.max_datetime,
+ omit_ms=self.omit_ms,
+ do_expl=impl_initial is None,
+ ))
+ obj = obj_initial(
+ value=value,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ )
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+
+ @given(data_strategy())
+ def test_copy(self, d):
+ values = d.draw(generalized_time_values_strat(
+ min_datetime=self.min_datetime,
+ max_datetime=self.max_datetime,
+ ))
+ obj = self.base_klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(data_strategy())
+ def test_stripped(self, d):
+ value = d.draw(datetimes(
+ min_value=self.min_datetime,
+ max_value=self.max_datetime,
+ ))
+ tag_impl = tag_encode(d.draw(integers(min_value=1)))
+ obj = self.base_klass(value, impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(data_strategy())
+ def test_stripped_expl(self, d):
+ value = d.draw(datetimes(
+ min_value=self.min_datetime,
+ max_value=self.max_datetime,
+ ))
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+ obj = self.base_klass(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_symmetric(self, d):
+ values = d.draw(generalized_time_values_strat(
+ min_datetime=self.min_datetime,
+ max_datetime=self.max_datetime,
+ ))
+ value = d.draw(datetimes(
+ min_value=self.min_datetime,
+ max_value=self.max_datetime,
+ ))
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+ offset = d.draw(integers(min_value=0))
+ _, _, _, default, optional, _decoded = values
+ obj = self.base_klass(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertEqual(obj_decoded.todatetime(), obj_expled.todatetime())
+ self.assertEqual(obj_decoded.todatetime(), obj.todatetime())
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+
+
+class TestGeneralizedTime(TimeMixin, CommonMixin, TestCase):
+ base_klass = GeneralizedTime
+ omit_ms = False
+ min_datetime = datetime(1900, 1, 1)
+ max_datetime = datetime(9999, 12, 31)
+
+ def test_go_vectors_invalid(self):
+ for data in ((
+ b"20100102030405",
+ b"00000100000000Z",
+ b"20101302030405Z",
+ b"20100002030405Z",
+ b"20100100030405Z",
+ b"20100132030405Z",
+ b"20100231030405Z",
+ b"20100102240405Z",
+ b"20100102036005Z",
+ b"20100102030460Z",
+ b"-20100102030410Z",
+ b"2010-0102030410Z",
+ b"2010-0002030410Z",
+ b"201001-02030410Z",
+ b"20100102-030410Z",
+ b"2010010203-0410Z",
+ b"201001020304-10Z",
+ # These ones are INVALID in *DER*, but accepted
+ # by Go's encoding/asn1
+ b"20100102030405+0607",
+ b"20100102030405-0607",
+ )):
+ with self.assertRaises(DecodeError) as err:
+ GeneralizedTime(data)
+ repr(err.exception)
+
+ def test_go_vectors_valid(self):
+ self.assertEqual(
+ GeneralizedTime(b"20100102030405Z").todatetime(),
+ datetime(2010, 1, 2, 3, 4, 5, 0),
+ )
+
+
+class TestUTCTime(TimeMixin, CommonMixin, TestCase):
+ base_klass = UTCTime
+ omit_ms = True
+ min_datetime = datetime(2000, 1, 1)
+ max_datetime = datetime(2049, 12, 31)
+
+ def test_go_vectors_invalid(self):
+ for data in ((
+ b"a10506234540Z",
+ b"91a506234540Z",
+ b"9105a6234540Z",
+ b"910506a34540Z",
+ b"910506334a40Z",
+ b"91050633444aZ",
+ b"910506334461Z",
+ b"910506334400Za",
+ b"000100000000Z",
+ b"101302030405Z",
+ b"100002030405Z",
+ b"100100030405Z",
+ b"100132030405Z",
+ b"100231030405Z",
+ b"100102240405Z",
+ b"100102036005Z",
+ b"100102030460Z",
+ b"-100102030410Z",
+ b"10-0102030410Z",
+ b"10-0002030410Z",
+ b"1001-02030410Z",
+ b"100102-030410Z",
+ b"10010203-0410Z",
+ b"1001020304-10Z",
+ # These ones are INVALID in *DER*, but accepted
+ # by Go's encoding/asn1
+ b"910506164540-0700",
+ b"910506164540+0730",
+ b"9105062345Z",
+ b"5105062345Z",
+ )):
+ with self.assertRaises(DecodeError) as err:
+ UTCTime(data)
+ repr(err.exception)
+
+ def test_go_vectors_valid(self):
+ self.assertEqual(
+ UTCTime(b"910506234540Z").todatetime(),
+ datetime(1991, 5, 6, 23, 45, 40, 0),
+ )
+
+ @given(integers(min_value=0, max_value=49))
+ def test_pre50(self, year):
+ self.assertEqual(
+ UTCTime(("%02d1231235959Z" % year).encode("ascii")).todatetime().year,
+ 2000 + year,
+ )
+
+ @given(integers(min_value=50, max_value=99))
+ def test_post50(self, year):
+ self.assertEqual(
+ UTCTime(("%02d1231235959Z" % year).encode("ascii")).todatetime().year,
+ 1900 + year,
+ )
+
+
+@composite
+def any_values_strat(draw, do_expl=False):
+ value = draw(one_of(none(), binary()))
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, expl, optional, _decoded)
+
+
+class AnyInherited(Any):
+ __slots__ = ()
+
+
+class TestAny(CommonMixin, TestCase):
+ base_klass = Any
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ Any(123)
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = Any(optional=optional)
+ self.assertEqual(obj.optional, optional)
+
+ @given(binary())
+ def test_ready(self, value):
+ obj = Any()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj = Any(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(integers())
+ def test_basic(self, value):
+ integer_encoded = Integer(value).encode()
+ for obj in (
+ Any(integer_encoded),
+ Any(Integer(value)),
+ Any(Any(Integer(value))),
+ ):
+ self.assertSequenceEqual(bytes(obj), integer_encoded)
+ self.assertEqual(
+ obj.decode(obj.encode())[0].vlen,
+ len(integer_encoded),
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertSequenceEqual(obj.encode(), integer_encoded)
+
+ @given(binary(), binary())
+ def test_comparison(self, value1, value2):
+ for klass in (Any, AnyInherited):
+ obj1 = klass(value1)
+ obj2 = klass(value2)
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == bytes(obj2), value1 == value2)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (Any, AnyInherited):
+ (
+ value_initial,
+ expl_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(any_values_strat())
+ obj_initial = klass(
+ value_initial,
+ expl_initial,
+ optional_initial or False,
+ _decoded_initial,
+ )
+ (
+ value,
+ expl,
+ optional,
+ _decoded,
+ ) = d.draw(any_values_strat(do_expl=True))
+ obj = obj_initial(value, expl, optional)
+ if obj.ready:
+ value_expected = None if value is None else value
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ self.assertEqual(obj.optional, optional)
+
+ def test_simultaneous_impl_expl(self):
+ # override it, as Any does not have implicit tag
+ pass
+
+ def test_decoded(self):
+ # override it, as Any does not have implicit tag
+ pass
+
+ @given(any_values_strat())
+ def test_copy(self, values):
+ for klass in (Any, AnyInherited):
+ obj = klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(binary().map(OctetString))
+ def test_stripped(self, value):
+ obj = Any(value)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ binary(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = Any(value, expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Any().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ Any().decode(
+ Any.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ any_values_strat(),
+ integers().map(lambda x: Integer(x).encode()),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, value, tag_expl, offset):
+ for klass in (Any, AnyInherited):
+ _, _, optional, _decoded = values
+ obj = klass(value=value, optional=optional, _decoded=_decoded)
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertEqual(bytes(obj_decoded), bytes(obj_expled))
+ self.assertEqual(bytes(obj_decoded), bytes(obj))
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+ self.assertEqual(obj_decoded.tlen, 0)
+ self.assertEqual(obj_decoded.llen, 0)
+ self.assertEqual(obj_decoded.vlen, len(value))
+
+
+@composite
+def choice_values_strat(draw, value_required=False, schema=None, do_expl=False):
+ if schema is None:
+ names = list(draw(sets(text_letters(), min_size=1, max_size=5)))
+ tags = [tag_encode(tag) for tag in draw(sets(
+ integers(min_value=0),
+ min_size=len(names),
+ max_size=len(names),
+ ))]
+ schema = [(name, Integer(impl=tag)) for name, tag in zip(names, tags)]
+ value = None
+ if value_required or draw(booleans()):
+ value = draw(tuples(
+ sampled_from([name for name, _ in schema]),
+ integers().map(Integer),
+ ))
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ default = draw(one_of(
+ none(),
+ tuples(sampled_from([name for name, _ in schema]), integers().map(Integer)),
+ ))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (schema, value, expl, default, optional, _decoded)
+
+
+class ChoiceInherited(Choice):
+ __slots__ = ()
+
+
+class TestChoice(CommonMixin, TestCase):
+ class Wahl(Choice):
+ schema = (("whatever", Boolean()),)
+ base_klass = Wahl
+
+ def test_schema_required(self):
+ with assertRaisesRegex(self, ValueError, "schema must be specified"):
+ Choice()
+
+ def test_impl_forbidden(self):
+ with assertRaisesRegex(self, ValueError, "no implicit tag allowed"):
+ Choice(impl=b"whatever")
+
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass(123)
+ repr(err.exception)
+ with self.assertRaises(ObjUnknown) as err:
+ self.base_klass(("whenever", Boolean(False)))
+ repr(err.exception)
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass(("whatever", Integer(123)))
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = self.base_klass(
+ default=self.base_klass(("whatever", Boolean(False))),
+ optional=optional,
+ )
+ self.assertTrue(obj.optional)
+
+ @given(booleans())
+ def test_ready(self, value):
+ obj = self.base_klass()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ self.assertIsNone(obj["whatever"])
+ with self.assertRaises(ObjNotReady) as err:
+ obj.encode()
+ repr(err.exception)
+ obj["whatever"] = Boolean()
+ self.assertFalse(obj.ready)
+ repr(obj)
+ pprint(obj)
+ obj["whatever"] = Boolean(value)
+ self.assertTrue(obj.ready)
+ repr(obj)
+ pprint(obj)
+
+ @given(booleans(), booleans())
+ def test_comparison(self, value1, value2):
+ class WahlInherited(self.base_klass):
+ __slots__ = ()
+ for klass in (self.base_klass, WahlInherited):
+ obj1 = klass(("whatever", Boolean(value1)))
+ obj2 = klass(("whatever", Boolean(value2)))
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == obj2._value, value1 == value2)
+ self.assertFalse(obj1 == obj2._value[1])
+
+ @given(data_strategy())
+ def test_call(self, d):
+ for klass in (Choice, ChoiceInherited):
+ (
+ schema_initial,
+ value_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(choice_values_strat())
+
+ class Wahl(klass):
+ __slots__ = ()
+ schema = schema_initial
+ obj_initial = Wahl(
+ value=value_initial,
+ expl=expl_initial,
+ default=default_initial,
+ optional=optional_initial or False,
+ _decoded=_decoded_initial,
+ )
+ (
+ _,
+ value,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(choice_values_strat(schema=schema_initial, do_expl=True))
+ obj = obj_initial(value, expl, default, optional)
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj.choice, value_expected[0])
+ self.assertEqual(obj.value, int(value_expected[1]))
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ default_expect = default_initial if default is None else default
+ if default_expect is not None:
+ self.assertEqual(obj.default.choice, default_expect[0])
+ self.assertEqual(obj.default.value, int(default_expect[1]))
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(obj.specs, obj_initial.specs)
+
+ def test_simultaneous_impl_expl(self):
+ # override it, as Any does not have implicit tag
+ pass
+
+ def test_decoded(self):
+ # override it, as Any does not have implicit tag
+ pass
+
+ @given(choice_values_strat())
+ def test_copy(self, values):
+ _schema, value, expl, default, optional, _decoded = values
+
+ class Wahl(self.base_klass):
+ __slots__ = ()
+ schema = _schema
+ obj = Wahl(
+ value=value,
+ expl=expl,
+ default=default,
+ optional=optional or False,
+ _decoded=_decoded,
+ )
+ obj_copied = obj.copy()
+ self.assertIsNone(obj.tag)
+ self.assertIsNone(obj_copied.tag)
+ # hack for assert_copied_basic_fields
+ obj.tag = "whatever"
+ obj_copied.tag = "whatever"
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._value, obj_copied._value)
+ self.assertEqual(obj.specs, obj_copied.specs)
+
+ @given(booleans())
+ def test_stripped(self, value):
+ obj = self.base_klass(("whatever", Boolean(value)))
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ booleans(),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, value, tag_expl):
+ obj = self.base_klass(("whatever", Boolean(value)), expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_symmetric(self, d):
+ _schema, value, _, default, optional, _decoded = d.draw(
+ choice_values_strat(value_required=True)
+ )
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+ offset = d.draw(integers(min_value=0))
+
+ class Wahl(self.base_klass):
+ __slots__ = ()
+ schema = _schema
+ obj = Wahl(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self.assertEqual(obj_decoded, obj_expled)
+ self.assertEqual(obj_decoded.choice, obj_expled.choice)
+ self.assertEqual(obj_decoded.value, obj_expled.value)
+ self.assertEqual(obj_decoded.choice, obj.choice)
+ self.assertEqual(obj_decoded.value, obj.value)
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+ self.assertSequenceEqual(
+ obj_expled_encoded[
+ obj_decoded.value.offset - offset:
+ obj_decoded.value.offset + obj_decoded.value.tlvlen - offset
+ ],
+ obj_encoded,
+ )
+
+ @given(integers())
+ def test_set_get(self, value):
+ class Wahl(Choice):
+ schema = (
+ ("erste", Boolean()),
+ ("zweite", Integer()),
+ )
+ obj = Wahl()
+ with self.assertRaises(ObjUnknown) as err:
+ obj["whatever"] = "whenever"
+ with self.assertRaises(InvalidValueType) as err:
+ obj["zweite"] = Boolean(False)
+ obj["zweite"] = Integer(value)
+ repr(err.exception)
+ with self.assertRaises(ObjUnknown) as err:
+ obj["whatever"]
+ repr(err.exception)
+ self.assertIsNone(obj["erste"])
+ self.assertEqual(obj["zweite"], Integer(value))
+
+ def test_tag_mismatch(self):
+ class Wahl(Choice):
+ schema = (
+ ("erste", Boolean()),
+ )
+ int_encoded = Integer(123).encode()
+ bool_encoded = Boolean(False).encode()
+ obj = Wahl()
+ obj.decode(bool_encoded)
+ with self.assertRaises(TagMismatch):
+ obj.decode(int_encoded)
+
+
+@composite
+def seq_values_strat(draw, seq_klass, do_expl=False):
+ value = None
+ if draw(booleans()):
+ value = seq_klass()
+ value._value = {
+ k: v for k, v in draw(dictionaries(
+ integers(),
+ one_of(
+ booleans().map(Boolean),
+ integers().map(Integer),
+ ),
+ )).items()
+ }
+ schema = None
+ if draw(booleans()):
+ schema = list(draw(dictionaries(
+ integers(),
+ one_of(
+ booleans().map(Boolean),
+ integers().map(Integer),
+ ),
+ )).items())
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ default = None
+ if draw(booleans()):
+ default = seq_klass()
+ default._value = {
+ k: v for k, v in draw(dictionaries(
+ integers(),
+ one_of(
+ booleans().map(Boolean),
+ integers().map(Integer),
+ ),
+ )).items()
+ }
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (value, schema, impl, expl, default, optional, _decoded)
+
+
+@composite
+def sequence_strat(draw, seq_klass):
+ inputs = draw(lists(
+ one_of(
+ tuples(just(Boolean), booleans(), one_of(none(), booleans())),
+ tuples(just(Integer), integers(), one_of(none(), integers())),
+ ),
+ max_size=6,
+ ))
+ tags = draw(sets(
+ integers(min_value=1),
+ min_size=len(inputs),
+ max_size=len(inputs),
+ ))
+ inits = [
+ ({"expl": tag_ctxc(tag)} if expled else {"impl": tag_encode(tag)})
+ for tag, expled in zip(tags, draw(lists(
+ booleans(),
+ min_size=len(inputs),
+ max_size=len(inputs),
+ )))
+ ]
+ empties = []
+ for i, optional in enumerate(draw(lists(
+ sampled_from(("required", "optional", "empty")),
+ min_size=len(inputs),
+ max_size=len(inputs),
+ ))):
+ if optional in ("optional", "empty"):
+ inits[i]["optional"] = True
+ if optional == "empty":
+ empties.append(i)
+ empties = set(empties)
+ names = list(draw(sets(
+ text_printable,
+ min_size=len(inputs),
+ max_size=len(inputs),
+ )))
+ schema = []
+ for i, (klass, value, default) in enumerate(inputs):
+ schema.append((names[i], klass(default=default, **inits[i])))
+ seq_name = draw(text_letters())
+ Seq = type(seq_name, (seq_klass,), {"__slots__": (), "schema": tuple(schema)})
+ seq = Seq()
+ expects = []
+ for i, (klass, value, default) in enumerate(inputs):
+ name = names[i]
+ _, spec = schema[i]
+ expect = {
+ "name": name,
+ "optional": False,
+ "presented": False,
+ "default_value": None if spec.default is None else default,
+ "value": None,
+ }
+ if i in empties:
+ expect["optional"] = True
+ else:
+ expect["presented"] = True
+ expect["value"] = value
+ if spec.optional:
+ expect["optional"] = True
+ if default is not None and default == value:
+ expect["presented"] = False
+ seq[name] = klass(value)
+ expects.append(expect)
+ return seq, expects
+
+
+@composite
+def sequences_strat(draw, seq_klass):
+ tags = draw(sets(integers(min_value=1), min_size=0, max_size=5))
+ inits = [
+ ({"expl": tag_ctxc(tag)} if expled else {"impl": tag_encode(tag)})
+ for tag, expled in zip(tags, draw(lists(
+ booleans(),
+ min_size=len(tags),
+ max_size=len(tags),
+ )))
+ ]
+ defaulted = set(
+ i for i, is_default in enumerate(draw(lists(
+ booleans(),
+ min_size=len(tags),
+ max_size=len(tags),
+ ))) if is_default
+ )
+ names = list(draw(sets(
+ text_printable,
+ min_size=len(tags),
+ max_size=len(tags),
+ )))
+ seq_expectses = draw(lists(
+ sequence_strat(seq_klass=seq_klass),
+ min_size=len(tags),
+ max_size=len(tags),
+ ))
+ seqs = [seq for seq, _ in seq_expectses]
+ schema = []
+ for i, (name, seq) in enumerate(zip(names, seqs)):
+ schema.append((
+ name,
+ seq(default=(seq if i in defaulted else None), **inits[i]),
+ ))
+ seq_name = draw(text_letters())
+ Seq = type(seq_name, (seq_klass,), {"__slots__": (), "schema": tuple(schema)})
+ seq_outer = Seq()
+ expect_outers = []
+ for name, (seq_inner, expects_inner) in zip(names, seq_expectses):
+ expect = {
+ "name": name,
+ "expects": expects_inner,
+ "presented": False,
+ }
+ seq_outer[name] = seq_inner
+ if seq_outer.specs[name].default is None:
+ expect["presented"] = True
+ expect_outers.append(expect)
+ return seq_outer, expect_outers
+
+
+class SeqMixing(object):
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass((1, 2, 3))
+ repr(err.exception)
+
+ def test_invalid_value_type_set(self):
+ class Seq(self.base_klass):
+ __slots__ = ()
+ schema = (("whatever", Boolean()),)
+ seq = Seq()
+ with self.assertRaises(InvalidValueType) as err:
+ seq["whatever"] = Integer(123)
+ repr(err.exception)
+
+ @given(booleans())
+ def test_optional(self, optional):
+ obj = self.base_klass(default=self.base_klass(), optional=optional)
+ self.assertTrue(obj.optional)
+
+ @given(data_strategy())
+ def test_ready(self, d):
+ ready = {
+ str(i): v for i, v in enumerate(d.draw(lists(
+ booleans(),
+ min_size=1,
+ max_size=3,
+ )))
+ }
+ non_ready = {
+ str(i + len(ready)): v for i, v in enumerate(d.draw(lists(
+ booleans(),
+ min_size=1,
+ max_size=3,
+ )))
+ }
+ schema_input = []
+ for name in d.draw(permutations(
+ list(ready.keys()) + list(non_ready.keys()),
+ )):
+ schema_input.append((name, Boolean()))
+
+ class Seq(self.base_klass):
+ __slots__ = ()
+ schema = tuple(schema_input)
+ seq = Seq()
+ for name in ready.keys():
+ seq[name]
+ seq[name] = Boolean()
+ self.assertFalse(seq.ready)
+ repr(seq)
+ pprint(seq)
+ for name, value in ready.items():
+ seq[name] = Boolean(value)
+ self.assertFalse(seq.ready)
+ repr(seq)
+ pprint(seq)
+ with self.assertRaises(ObjNotReady) as err:
+ seq.encode()
+ repr(err.exception)
+ for name, value in non_ready.items():
+ seq[name] = Boolean(value)
+ self.assertTrue(seq.ready)
+ repr(seq)
+ pprint(seq)
+
+ @given(data_strategy())
+ def test_call(self, d):
+ class SeqInherited(self.base_klass):
+ __slots__ = ()
+ for klass in (self.base_klass, SeqInherited):
+ (
+ value_initial,
+ schema_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(seq_values_strat(seq_klass=klass))
+ obj_initial = klass(
+ value_initial,
+ schema_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial or False,
+ _decoded_initial,
+ )
+ (
+ value,
+ _,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(seq_values_strat(
+ seq_klass=klass,
+ do_expl=impl_initial is None,
+ ))
+ obj = obj_initial(value, impl, expl, default, optional)
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ self.assertEqual(obj._value, getattr(value_expected, "_value", {}))
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ {} if obj.default is None else obj.default._value,
+ getattr(default_initial if default is None else default, "_value", {}),
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(list(obj.specs.items()), schema_initial or [])
+ self.assertEqual(obj.optional, optional)
+
+ @given(data_strategy())
+ def test_copy(self, d):
+ class SeqInherited(self.base_klass):
+ __slots__ = ()
+ for klass in (self.base_klass, SeqInherited):
+ values = d.draw(seq_values_strat(seq_klass=klass))
+ obj = klass(*values)
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj.specs, obj_copied.specs)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(data_strategy())
+ def test_stripped(self, d):
+ value = d.draw(integers())
+ tag_impl = tag_encode(d.draw(integers(min_value=1)))
+
+ class Seq(self.base_klass):
+ __slots__ = ()
+ impl = tag_impl
+ schema = (("whatever", Integer()),)
+ seq = Seq()
+ seq["whatever"] = Integer(value)
+ with self.assertRaises(NotEnoughData):
+ seq.decode(seq.encode()[:-1])
+
+ @given(data_strategy())
+ def test_stripped_expl(self, d):
+ value = d.draw(integers())
+ tag_expl = tag_ctxc(d.draw(integers(min_value=1)))
+
+ class Seq(self.base_klass):
+ __slots__ = ()
+ expl = tag_expl
+ schema = (("whatever", Integer()),)
+ seq = Seq()
+ seq["whatever"] = Integer(value)
+ with self.assertRaises(NotEnoughData):
+ seq.decode(seq.encode()[:-1])
+
+ @given(binary(min_size=2))
+ def test_non_tag_mismatch_raised(self, junk):
+ try:
+ _, _, len_encoded = tag_strip(memoryview(junk))
+ len_decode(len_encoded)
+ except Exception:
+ assume(True)
+ else:
+ assume(False)
+
+ class Seq(self.base_klass):
+ __slots__ = ()
+ schema = (
+ ("whatever", Integer()),
+ ("junk", Any()),
+ ("whenever", Integer()),
+ )
+ seq = Seq()
+ seq["whatever"] = Integer(123)
+ seq["junk"] = Any(junk)
+ seq["whenever"] = Integer(123)
+ with self.assertRaises(DecodeError):
+ seq.decode(seq.encode())
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ self.base_klass().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ self.base_klass().decode(
+ self.base_klass.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ def _assert_expects(self, seq, expects):
+ for expect in expects:
+ self.assertEqual(
+ seq.specs[expect["name"]].optional,
+ expect["optional"],
+ )
+ if expect["default_value"] is not None:
+ self.assertEqual(
+ seq.specs[expect["name"]].default,
+ expect["default_value"],
+ )
+ if expect["presented"]:
+ self.assertIn(expect["name"], seq)
+ self.assertEqual(seq[expect["name"]], expect["value"])
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_symmetric(self, d):
+ seq, expects = d.draw(sequence_strat(seq_klass=self.base_klass))
+ self.assertTrue(seq.ready)
+ self.assertFalse(seq.decoded)
+ self._assert_expects(seq, expects)
+ repr(seq)
+ pprint(seq)
+ seq_encoded = seq.encode()
+ seq_decoded, tail = seq.decode(seq_encoded)
+ self.assertEqual(tail, b"")
+ self.assertTrue(seq.ready)
+ self._assert_expects(seq_decoded, expects)
+ self.assertEqual(seq, seq_decoded)
+ self.assertEqual(seq_decoded.encode(), seq_encoded)
+ for expect in expects:
+ if not expect["presented"]:
+ self.assertNotIn(expect["name"], seq_decoded)
+ continue
+ self.assertIn(expect["name"], seq_decoded)
+ obj = seq_decoded[expect["name"]]
+ self.assertTrue(obj.decoded)
+ offset = obj.expl_offset if obj.expled else obj.offset
+ tlvlen = obj.expl_tlvlen if obj.expled else obj.tlvlen
+ self.assertSequenceEqual(
+ seq_encoded[offset:offset + tlvlen],
+ obj.encode(),
+ )
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_symmetric_with_seq(self, d):
+ seq, expect_outers = d.draw(sequences_strat(seq_klass=self.base_klass))
+ self.assertTrue(seq.ready)
+ seq_encoded = seq.encode()
+ seq_decoded, tail = seq.decode(seq_encoded)
+ self.assertEqual(tail, b"")
+ self.assertTrue(seq.ready)
+ self.assertEqual(seq, seq_decoded)
+ self.assertEqual(seq_decoded.encode(), seq_encoded)
+ for expect_outer in expect_outers:
+ if not expect_outer["presented"]:
+ self.assertNotIn(expect_outer["name"], seq_decoded)
+ continue
+ self.assertIn(expect_outer["name"], seq_decoded)
+ obj = seq_decoded[expect_outer["name"]]
+ self.assertTrue(obj.decoded)
+ offset = obj.expl_offset if obj.expled else obj.offset
+ tlvlen = obj.expl_tlvlen if obj.expled else obj.tlvlen
+ self.assertSequenceEqual(
+ seq_encoded[offset:offset + tlvlen],
+ obj.encode(),
+ )
+ self._assert_expects(obj, expect_outer["expects"])
+
+ @given(data_strategy())
+ def test_default_disappears(self, d):
+ _schema = list(d.draw(dictionaries(
+ text_letters(),
+ sets(integers(), min_size=2, max_size=2),
+ min_size=1,
+ )).items())
+
+ class Seq(self.base_klass):
+ __slots__ = ()
+ schema = [
+ (n, Integer(default=d))
+ for n, (_, d) in _schema
+ ]
+ seq = Seq()
+ for name, (value, _) in _schema:
+ seq[name] = Integer(value)
+ self.assertEqual(len(seq._value), len(_schema))
+ empty_seq = b"".join((self.base_klass.tag_default, len_encode(0)))
+ self.assertGreater(len(seq.encode()), len(empty_seq))
+ for name, (_, default) in _schema:
+ seq[name] = Integer(default)
+ self.assertEqual(len(seq._value), 0)
+ self.assertSequenceEqual(seq.encode(), empty_seq)
+
+ @given(data_strategy())
+ def test_encoded_default_accepted(self, d):
+ _schema = list(d.draw(dictionaries(
+ text_letters(),
+ integers(),
+ min_size=1,
+ )).items())
+ tags = [tag_encode(tag) for tag in d.draw(sets(
+ integers(min_value=0),
+ min_size=len(_schema),
+ max_size=len(_schema),
+ ))]
+
+ class SeqWithoutDefault(self.base_klass):
+ __slots__ = ()
+ schema = [
+ (n, Integer(impl=t))
+ for (n, _), t in zip(_schema, tags)
+ ]
+ seq_without_default = SeqWithoutDefault()
+ for name, value in _schema:
+ seq_without_default[name] = Integer(value)
+ seq_encoded = seq_without_default.encode()
+
+ class SeqWithDefault(self.base_klass):
+ __slots__ = ()
+ schema = [
+ (n, Integer(default=v, impl=t))
+ for (n, v), t in zip(_schema, tags)
+ ]
+ seq_with_default = SeqWithDefault()
+ seq_decoded, _ = seq_with_default.decode(seq_encoded)
+ for name, value in _schema:
+ self.assertEqual(seq_decoded[name], seq_with_default[name])
+ self.assertEqual(seq_decoded[name], value)
+
+ @given(data_strategy())
+ def test_missing_from_spec(self, d):
+ names = list(d.draw(sets(text_letters(), min_size=2)))
+ tags = [tag_encode(tag) for tag in d.draw(sets(
+ integers(min_value=0),
+ min_size=len(names),
+ max_size=len(names),
+ ))]
+ names_tags = [(name, tag) for tag, name in sorted(zip(tags, names))]
+
+ class SeqFull(self.base_klass):
+ __slots__ = ()
+ schema = [(n, Integer(impl=t)) for n, t in names_tags]
+ seq_full = SeqFull()
+ for i, name in enumerate(names):
+ seq_full[name] = Integer(i)
+ seq_encoded = seq_full.encode()
+ altered = names_tags[:-2] + names_tags[-1:]
+
+ class SeqMissing(self.base_klass):
+ __slots__ = ()
+ schema = [(n, Integer(impl=t)) for n, t in altered]
+ seq_missing = SeqMissing()
+ with self.assertRaises(TagMismatch):
+ seq_missing.decode(seq_encoded)
+
+
+class TestSequence(SeqMixing, CommonMixin, TestCase):
+ base_klass = Sequence
+
+ @given(
+ integers(),
+ binary(min_size=1),
+ )
+ def test_remaining(self, value, junk):
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("whatever", Integer()),
+ )
+ int_encoded = Integer(value).encode()
+ junked = b"".join((
+ Sequence.tag_default,
+ len_encode(len(int_encoded + junk)),
+ int_encoded + junk,
+ ))
+ with assertRaisesRegex(self, DecodeError, "remaining"):
+ Seq().decode(junked)
+
+ @given(sets(text_letters(), min_size=2))
+ def test_obj_unknown(self, names):
+ missing = names.pop()
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = [(n, Boolean()) for n in names]
+ seq = Seq()
+ with self.assertRaises(ObjUnknown) as err:
+ seq[missing]
+ repr(err.exception)
+ with self.assertRaises(ObjUnknown) as err:
+ seq[missing] = Boolean()
+ repr(err.exception)
+
+
+class TestSet(SeqMixing, CommonMixin, TestCase):
+ base_klass = Set
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_sorted(self, d):
+ tags = [
+ tag_encode(tag) for tag in
+ d.draw(sets(integers(min_value=1), min_size=1, max_size=10))
+ ]
+
+ class Seq(Set):
+ __slots__ = ()
+ schema = [(str(i), OctetString(impl=t)) for i, t in enumerate(tags)]
+ seq = Seq()
+ for name, _ in Seq.schema:
+ seq[name] = OctetString(b"")
+ seq_encoded = seq.encode()
+ seq_decoded, _ = seq.decode(seq_encoded)
+ self.assertSequenceEqual(
+ seq_encoded[seq_decoded.tlen + seq_decoded.llen:],
+ b"".join(sorted([seq[name].encode() for name, _ in Seq.schema])),
+ )
+
+
+@composite
+def seqof_values_strat(draw, schema=None, do_expl=False):
+ if schema is None:
+ schema = draw(sampled_from((Boolean(), Integer())))
+ bound_min, bound_max = sorted(draw(sets(
+ integers(min_value=0, max_value=10),
+ min_size=2,
+ max_size=2,
+ )))
+ if isinstance(schema, Boolean):
+ values_generator = booleans().map(Boolean)
+ elif isinstance(schema, Integer):
+ values_generator = integers().map(Integer)
+ values_generator = lists(
+ values_generator,
+ min_size=bound_min,
+ max_size=bound_max,
+ )
+ values = draw(one_of(none(), values_generator))
+ impl = None
+ expl = None
+ if do_expl:
+ expl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ else:
+ impl = draw(one_of(none(), integers(min_value=1).map(tag_encode)))
+ default = draw(one_of(none(), values_generator))
+ optional = draw(one_of(none(), booleans()))
+ _decoded = (
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ draw(integers(min_value=0)),
+ )
+ return (
+ schema,
+ values,
+ (bound_min, bound_max),
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ )
+
+
+class SeqOfMixing(object):
+ def test_invalid_value_type(self):
+ with self.assertRaises(InvalidValueType) as err:
+ self.base_klass(123)
+ repr(err.exception)
+
+ def test_invalid_values_type(self):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Integer()
+ with self.assertRaises(InvalidValueType) as err:
+ SeqOf([Integer(123), Boolean(False), Integer(234)])
+ repr(err.exception)
+
+ def test_schema_required(self):
+ with assertRaisesRegex(self, ValueError, "schema must be specified"):
+ self.base_klass.__mro__[1]()
+
+ @given(booleans(), booleans(), binary(), binary())
+ def test_comparison(self, value1, value2, tag1, tag2):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Boolean()
+ obj1 = SeqOf([Boolean(value1)])
+ obj2 = SeqOf([Boolean(value2)])
+ self.assertEqual(obj1 == obj2, value1 == value2)
+ self.assertEqual(obj1 == list(obj2), value1 == value2)
+ self.assertEqual(obj1 == tuple(obj2), value1 == value2)
+ obj1 = SeqOf([Boolean(value1)], impl=tag1)
+ obj2 = SeqOf([Boolean(value1)], impl=tag2)
+ self.assertEqual(obj1 == obj2, tag1 == tag2)
+
+ @given(lists(booleans()))
+ def test_iter(self, values):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Boolean()
+ obj = SeqOf([Boolean(value) for value in values])
+ self.assertEqual(len(obj), len(values))
+ for i, value in enumerate(obj):
+ self.assertEqual(value, values[i])
+
+ @given(data_strategy())
+ def test_ready(self, d):
+ ready = [Integer(v) for v in d.draw(lists(
+ integers(),
+ min_size=1,
+ max_size=3,
+ ))]
+ non_ready = [
+ Integer() for _ in
+ range(d.draw(integers(min_value=1, max_value=5)))
+ ]
+
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Integer()
+ values = d.draw(permutations(ready + non_ready))
+ seqof = SeqOf()
+ for value in values:
+ seqof.append(value)
+ self.assertFalse(seqof.ready)
+ repr(seqof)
+ pprint(seqof)
+ with self.assertRaises(ObjNotReady) as err:
+ seqof.encode()
+ repr(err.exception)
+ for i, value in enumerate(values):
+ self.assertEqual(seqof[i], value)
+ if not seqof[i].ready:
+ seqof[i] = Integer(i)
+ self.assertTrue(seqof.ready)
+ repr(seqof)
+ pprint(seqof)
+
+ def test_spec_mismatch(self):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Integer()
+ seqof = SeqOf()
+ seqof.append(Integer(123))
+ with self.assertRaises(ValueError):
+ seqof.append(Boolean(False))
+ with self.assertRaises(ValueError):
+ seqof[0] = Boolean(False)
+
+ @given(data_strategy())
+ def test_bounds_satisfied(self, d):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Boolean()
+ bound_min = d.draw(integers(min_value=0, max_value=1 << 7))
+ bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7))
+ value = [Boolean()] * d.draw(integers(min_value=bound_min, max_value=bound_max))
+ SeqOf(value=value, bounds=(bound_min, bound_max))
+
+ @given(data_strategy())
+ def test_bounds_unsatisfied(self, d):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Boolean()
+ bound_min = d.draw(integers(min_value=1, max_value=1 << 7))
+ bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7))
+ value = [Boolean()] * d.draw(integers(max_value=bound_min - 1))
+ with self.assertRaises(BoundsError) as err:
+ SeqOf(value=value, bounds=(bound_min, bound_max))
+ repr(err.exception)
+ value = [Boolean()] * d.draw(integers(
+ min_value=bound_max + 1,
+ max_value=bound_max + 10,
+ ))
+ with self.assertRaises(BoundsError) as err:
+ SeqOf(value=value, bounds=(bound_min, bound_max))
+ repr(err.exception)
+
+ @given(integers(min_value=1, max_value=10))
+ def test_out_of_bounds(self, bound_max):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Integer()
+ bounds = (0, bound_max)
+ seqof = SeqOf()
+ for _ in range(bound_max):
+ seqof.append(Integer(123))
+ with self.assertRaises(BoundsError):
+ seqof.append(Integer(123))
+
+ @given(data_strategy())
+ def test_call(self, d):
+ (
+ schema_initial,
+ value_initial,
+ bounds_initial,
+ impl_initial,
+ expl_initial,
+ default_initial,
+ optional_initial,
+ _decoded_initial,
+ ) = d.draw(seqof_values_strat())
+
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = schema_initial
+ obj_initial = SeqOf(
+ value=value_initial,
+ bounds=bounds_initial,
+ impl=impl_initial,
+ expl=expl_initial,
+ default=default_initial,
+ optional=optional_initial or False,
+ _decoded=_decoded_initial,
+ )
+ (
+ _,
+ value,
+ bounds,
+ impl,
+ expl,
+ default,
+ optional,
+ _decoded,
+ ) = d.draw(seqof_values_strat(
+ schema=schema_initial,
+ do_expl=impl_initial is None,
+ ))
+ if (default is None) and (obj_initial.default is not None):
+ bounds = None
+ if (
+ (bounds is None) and
+ (value is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= len(value) <= bounds_initial[1])
+ ):
+ value = None
+ if (
+ (bounds is None) and
+ (default is not None) and
+ (bounds_initial is not None) and
+ not (bounds_initial[0] <= len(default) <= bounds_initial[1])
+ ):
+ default = None
+ obj = obj_initial(
+ value=value,
+ bounds=bounds,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional,
+ )
+ if obj.ready:
+ value_expected = default if value is None else value
+ value_expected = (
+ default_initial if value_expected is None
+ else value_expected
+ )
+ value_expected = () if value_expected is None else value_expected
+ self.assertEqual(obj, value_expected)
+ self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default)
+ self.assertEqual(obj.expl_tag, expl or expl_initial)
+ self.assertEqual(
+ obj.default,
+ default_initial if default is None else default,
+ )
+ if obj.default is None:
+ optional = optional_initial if optional is None else optional
+ optional = False if optional is None else optional
+ else:
+ optional = True
+ self.assertEqual(obj.optional, optional)
+ self.assertEqual(
+ (obj._bound_min, obj._bound_max),
+ bounds or bounds_initial or (0, float("+inf")),
+ )
+
+ @given(seqof_values_strat())
+ def test_copy(self, values):
+ _schema, value, bounds, impl, expl, default, optional, _decoded = values
+
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = _schema
+ obj = SeqOf(
+ value=value,
+ bounds=bounds,
+ impl=impl,
+ expl=expl,
+ default=default,
+ optional=optional or False,
+ _decoded=_decoded,
+ )
+ obj_copied = obj.copy()
+ self.assert_copied_basic_fields(obj, obj_copied)
+ self.assertEqual(obj._bound_min, obj_copied._bound_min)
+ self.assertEqual(obj._bound_max, obj_copied._bound_max)
+ self.assertEqual(obj._value, obj_copied._value)
+
+ @given(
+ lists(binary()),
+ integers(min_value=1).map(tag_encode),
+ )
+ def test_stripped(self, values, tag_impl):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = OctetString()
+ obj = SeqOf([OctetString(v) for v in values], impl=tag_impl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ lists(binary()),
+ integers(min_value=1).map(tag_ctxc),
+ )
+ def test_stripped_expl(self, values, tag_expl):
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = OctetString()
+ obj = SeqOf([OctetString(v) for v in values], expl=tag_expl)
+ with self.assertRaises(NotEnoughData):
+ obj.decode(obj.encode()[:-1])
+
+ @given(
+ integers(min_value=31),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_tag(self, tag, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ self.base_klass().decode(
+ tag_encode(tag)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(
+ integers(min_value=128),
+ integers(min_value=0),
+ lists(integers()),
+ )
+ def test_bad_len(self, l, offset, decode_path):
+ decode_path = tuple(str(i) for i in decode_path)
+ with self.assertRaises(DecodeError) as err:
+ self.base_klass().decode(
+ self.base_klass.tag_default + len_encode(l)[:-1],
+ offset=offset,
+ decode_path=decode_path,
+ )
+ repr(err.exception)
+ self.assertEqual(err.exception.offset, offset)
+ self.assertEqual(err.exception.decode_path, decode_path)
+
+ @given(binary(min_size=1))
+ def test_tag_mismatch(self, impl):
+ assume(impl != self.base_klass.tag_default)
+ with self.assertRaises(TagMismatch):
+ self.base_klass(impl=impl).decode(self.base_klass().encode())
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(
+ seqof_values_strat(schema=Integer()),
+ lists(integers().map(Integer)),
+ integers(min_value=1).map(tag_ctxc),
+ integers(min_value=0),
+ )
+ def test_symmetric(self, values, value, tag_expl, offset):
+ _, _, _, _, _, default, optional, _decoded = values
+
+ class SeqOf(self.base_klass):
+ __slots__ = ()
+ schema = Integer()
+ obj = SeqOf(
+ value=value,
+ default=default,
+ optional=optional,
+ _decoded=_decoded,
+ )
+ repr(obj)
+ pprint(obj)
+ self.assertFalse(obj.expled)
+ obj_encoded = obj.encode()
+ obj_expled = obj(value, expl=tag_expl)
+ self.assertTrue(obj_expled.expled)
+ repr(obj_expled)
+ pprint(obj_expled)
+ obj_expled_encoded = obj_expled.encode()
+ obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset)
+ repr(obj_decoded)
+ pprint(obj_decoded)
+ self.assertEqual(tail, b"")
+ self._test_symmetric_compare_objs(obj_decoded, obj_expled)
+ self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded)
+ self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl)
+ self.assertEqual(obj_decoded.expl_tlen, len(tag_expl))
+ self.assertEqual(
+ obj_decoded.expl_llen,
+ len(len_encode(len(obj_encoded))),
+ )
+ self.assertEqual(obj_decoded.tlvlen, len(obj_encoded))
+ self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded))
+ self.assertEqual(
+ obj_decoded.offset,
+ offset + obj_decoded.expl_tlen + obj_decoded.expl_llen,
+ )
+ self.assertEqual(obj_decoded.expl_offset, offset)
+ for obj_inner in obj_decoded:
+ self.assertIn(obj_inner, obj_decoded)
+ self.assertSequenceEqual(
+ obj_inner.encode(),
+ obj_expled_encoded[
+ obj_inner.offset - offset:
+ obj_inner.offset + obj_inner.tlvlen - offset
+ ],
+ )
+
+
+class TestSequenceOf(SeqOfMixing, CommonMixin, TestCase):
+ class SeqOf(SequenceOf):
+ __slots__ = ()
+ schema = "whatever"
+ base_klass = SeqOf
+
+ def _test_symmetric_compare_objs(self, obj1, obj2):
+ self.assertEqual(obj1, obj2)
+ self.assertSequenceEqual(list(obj1), list(obj2))
+
+
+class TestSetOf(SeqOfMixing, CommonMixin, TestCase):
+ class SeqOf(SetOf):
+ __slots__ = ()
+ schema = "whatever"
+ base_klass = SeqOf
+
+ def _test_symmetric_compare_objs(self, obj1, obj2):
+ self.assertSetEqual(
+ set(int(v) for v in obj1),
+ set(int(v) for v in obj2),
+ )
+
+ @settings(max_examples=LONG_TEST_MAX_EXAMPLES)
+ @given(data_strategy())
+ def test_sorted(self, d):
+ values = [OctetString(v) for v in d.draw(lists(binary()))]
+
+ class Seq(SetOf):
+ __slots__ = ()
+ schema = OctetString()
+ seq = Seq(values)
+ seq_encoded = seq.encode()
+ seq_decoded, _ = seq.decode(seq_encoded)
+ self.assertSequenceEqual(
+ seq_encoded[seq_decoded.tlen + seq_decoded.llen:],
+ b"".join(sorted([v.encode() for v in values])),
+ )
+
+
+class TestGoMarshalVectors(TestCase):
+ def runTest(self):
+ self.assertSequenceEqual(Integer(10).encode(), hexdec("02010a"))
+ self.assertSequenceEqual(Integer(127).encode(), hexdec("02017f"))
+ self.assertSequenceEqual(Integer(128).encode(), hexdec("02020080"))
+ self.assertSequenceEqual(Integer(-128).encode(), hexdec("020180"))
+ self.assertSequenceEqual(Integer(-129).encode(), hexdec("0202ff7f"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", Integer()),
+ ("zweite", Integer(optional=True))
+ )
+ seq = Seq()
+ seq["erste"] = Integer(64)
+ self.assertSequenceEqual(seq.encode(), hexdec("3003020140"))
+ seq["erste"] = Integer(0x123456)
+ self.assertSequenceEqual(seq.encode(), hexdec("30050203123456"))
+ seq["erste"] = Integer(64)
+ seq["zweite"] = Integer(65)
+ self.assertSequenceEqual(seq.encode(), hexdec("3006020140020141"))
+
+ class NestedSeq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("nest", Seq()),
+ )
+ seq["erste"] = Integer(127)
+ seq["zweite"] = None
+ nested = NestedSeq()
+ nested["nest"] = seq
+ self.assertSequenceEqual(nested.encode(), hexdec("3005300302017f"))
+
+ self.assertSequenceEqual(
+ OctetString(b"\x01\x02\x03").encode(),
+ hexdec("0403010203"),
+ )
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", Integer(impl=tag_encode(5, klass=TagClassContext))),
+ )
+ seq = Seq()
+ seq["erste"] = Integer(64)
+ self.assertSequenceEqual(seq.encode(), hexdec("3003850140"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", Integer(expl=tag_ctxc(5))),
+ )
+ seq = Seq()
+ seq["erste"] = Integer(64)
+ self.assertSequenceEqual(seq.encode(), hexdec("3005a503020140"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", Null(
+ impl=tag_encode(0, klass=TagClassContext),
+ optional=True,
+ )),
+ )
+ seq = Seq()
+ seq["erste"] = Null()
+ self.assertSequenceEqual(seq.encode(), hexdec("30028000"))
+ seq["erste"] = None
+ self.assertSequenceEqual(seq.encode(), hexdec("3000"))
+
+ self.assertSequenceEqual(
+ UTCTime(datetime(1970, 1, 1, 0, 0)).encode(),
+ hexdec("170d3730303130313030303030305a"),
+ )
+ self.assertSequenceEqual(
+ UTCTime(datetime(2009, 11, 15, 22, 56, 16)).encode(),
+ hexdec("170d3039313131353232353631365a"),
+ )
+ self.assertSequenceEqual(
+ GeneralizedTime(datetime(2100, 4, 5, 12, 1, 1)).encode(),
+ hexdec("180f32313030303430353132303130315a"),
+ )
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", GeneralizedTime()),
+ )
+ seq = Seq()
+ seq["erste"] = GeneralizedTime(datetime(2009, 11, 15, 22, 56, 16))
+ self.assertSequenceEqual(
+ seq.encode(),
+ hexdec("3011180f32303039313131353232353631365a"),
+ )
+
+ self.assertSequenceEqual(
+ BitString((1, b"\x80")).encode(),
+ hexdec("03020780"),
+ )
+ self.assertSequenceEqual(
+ BitString((12, b"\x81\xF0")).encode(),
+ hexdec("03030481f0"),
+ )
+
+ self.assertSequenceEqual(
+ ObjectIdentifier("1.2.3.4").encode(),
+ hexdec("06032a0304"),
+ )
+ self.assertSequenceEqual(
+ ObjectIdentifier("1.2.840.133549.1.1.5").encode(),
+ hexdec("06092a864888932d010105"),
+ )
+ self.assertSequenceEqual(
+ ObjectIdentifier("2.100.3").encode(),
+ hexdec("0603813403"),
+ )
+
+ self.assertSequenceEqual(
+ PrintableString("test").encode(),
+ hexdec("130474657374"),
+ )
+ self.assertSequenceEqual(
+ PrintableString("x" * 127).encode(),
+ hexdec("137F" + "78" * 127),
+ )
+ self.assertSequenceEqual(
+ PrintableString("x" * 128).encode(),
+ hexdec("138180" + "78" * 128),
+ )
+ self.assertSequenceEqual(UTF8String("Σ").encode(), hexdec("0c02cea3"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", IA5String()),
+ )
+ seq = Seq()
+ seq["erste"] = IA5String("test")
+ self.assertSequenceEqual(seq.encode(), hexdec("3006160474657374"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", PrintableString()),
+ )
+ seq = Seq()
+ seq["erste"] = PrintableString("test")
+ self.assertSequenceEqual(seq.encode(), hexdec("3006130474657374"))
+ seq["erste"] = PrintableString("test*")
+ self.assertSequenceEqual(seq.encode(), hexdec("30071305746573742a"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", Any(optional=True)),
+ ("zweite", Integer()),
+ )
+ seq = Seq()
+ seq["zweite"] = Integer(64)
+ self.assertSequenceEqual(seq.encode(), hexdec("3003020140"))
+
+ class Seq(SetOf):
+ __slots__ = ()
+ schema = Integer()
+ seq = Seq()
+ seq.append(Integer(10))
+ self.assertSequenceEqual(seq.encode(), hexdec("310302010a"))
+
+ class _SeqOf(SequenceOf):
+ __slots__ = ()
+ schema = PrintableString()
+
+ class SeqOf(SequenceOf):
+ __slots__ = ()
+ schema = _SeqOf()
+ _seqof = _SeqOf()
+ _seqof.append(PrintableString("1"))
+ seqof = SeqOf()
+ seqof.append(_seqof)
+ self.assertSequenceEqual(seqof.encode(), hexdec("30053003130131"))
+
+ class Seq(Sequence):
+ __slots__ = ()
+ schema = (
+ ("erste", Integer(default=1)),
+ )
+ seq = Seq()
+ seq["erste"] = Integer(0)
+ self.assertSequenceEqual(seq.encode(), hexdec("3003020100"))
+ seq["erste"] = Integer(1)
+ self.assertSequenceEqual(seq.encode(), hexdec("3000"))
+ seq["erste"] = Integer(2)
+ self.assertSequenceEqual(seq.encode(), hexdec("3003020102"))
+
+
+class TestPP(TestCase):
+ @given(data_strategy())
+ def test_oid_printing(self, d):
+ oids = {
+ str(ObjectIdentifier(k)): v * 2
+ for k, v in d.draw(dictionaries(oid_strategy(), text_letters())).items()
+ }
+ chosen = d.draw(sampled_from(sorted(oids)))
+ chosen_id = oids[chosen]
+ pp = _pp(asn1_type_name=ObjectIdentifier.asn1_type_name, value=chosen)
+ self.assertNotIn(chosen_id, pp_console_row(pp))
+ self.assertIn(chosen_id, pp_console_row(pp, oids=oids))