diff --git a/__main__.py b/__main__.py
index 75f70fc..1c804ac 100755
--- a/__main__.py
+++ b/__main__.py
@@ -55,6 +55,7 @@
✓ Include icons for emblems
"""
+import os
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
@@ -204,9 +205,10 @@ class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
my_file = QtWidgets.QFileDialog.getOpenFileNames(
self, 'Open file', self.last_open_path,
- "eBooks (*.epub *.mobi *.aws *.txt *.pdf *.fb2 *.djvu *.cbz)")
+ "eBooks (*.epub *.cbz *.cbr)")
if my_file[0]:
+ self.last_open_path = os.path.dirname(my_file[0][0])
self.thread = BackGroundBookAddition(self, my_file[0], self.database_path)
self.thread.finished.connect(self.lib_ref.create_proxymodel)
self.thread.start()
diff --git a/ebooklib/LICENSE b/ebooklib/LICENSE
new file mode 100644
index 0000000..2def0e8
--- /dev/null
+++ b/ebooklib/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are 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.
+
+ 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.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ 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 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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 Affero 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 Affero 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 Affero 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.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ 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 AGPL, see
+.
\ No newline at end of file
diff --git a/library.py b/library.py
index c9107fc..cd84f51 100644
--- a/library.py
+++ b/library.py
@@ -83,9 +83,6 @@ class Library:
'tags': tags,
'hash': i[8]}
- # import pprint
- # pprint.pprint(all_metadata)
-
tooltip_string = title + '\nAuthor: ' + author + '\nYear: ' + str(year)
if tags:
tooltip_string += ('\nTags: ' + tags)
diff --git a/parsers/cbr.py b/parsers/cbr.py
new file mode 100644
index 0000000..2b4e246
--- /dev/null
+++ b/parsers/cbr.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+
+import os
+import time
+import collections
+from rarfile import rarfile
+
+
+class ParseCBR:
+ def __init__(self, filename, temp_dir, file_md5):
+ self.filename = filename
+ self.book = None
+ self.temp_dir = temp_dir
+ self.file_md5 = file_md5
+
+ def read_book(self):
+ try:
+ self.book = rarfile.RarFile(self.filename)
+ except: # Specifying no exception types might be warranted here
+ print('Cannot parse ' + self.filename)
+ return
+
+ def get_title(self):
+ filename = os.path.basename(self.filename)
+ filename_proper = os.path.splitext(filename)[0]
+ return filename_proper
+
+ def get_author(self):
+ return None
+
+ def get_year(self):
+ creation_time = time.ctime(os.path.getctime(self.filename))
+ creation_year = creation_time.split()[-1]
+ return creation_year
+
+ def get_cover_image(self):
+ # The first image in the archive may not be the cover
+ # It is implied, however, that the first image in order
+ # will be the cover
+
+ image_list = [i.filename for i in self.book.infolist() if not i.isdir()]
+ image_list.sort()
+ cover_image_filename = image_list[0]
+
+ for i in self.book.infolist():
+ if not i.isdir():
+ if i.filename == cover_image_filename:
+ cover_image = self.book.read(i)
+ return cover_image
+
+ def get_isbn(self):
+ return None
+
+ def get_contents(self):
+ # TODO
+ # CBR files containing multiple directories for multiple chapters
+
+ file_settings = {
+ 'images_only': True}
+
+ extract_path = os.path.join(self.temp_dir, self.file_md5)
+ contents = collections.OrderedDict()
+ # This is a brute force approach
+ # Maybe try reading from the file as everything
+ # matures a little bit more
+
+ contents = collections.OrderedDict()
+
+ # I'm currently choosing not to keep multiple files in memory
+ self.book.extractall(extract_path)
+
+ found_images = []
+ for i in os.walk(extract_path):
+ if i[2]: # Implies files were found
+ image_dir = i[0]
+ found_images = i[2]
+ break
+
+ if not found_images:
+ print('Found nothing in ' + self.filename)
+ return None, file_settings
+
+ found_images.sort()
+
+ for count, i in enumerate(found_images):
+ page_name = 'Page ' + str(count + 1)
+ image_path = os.path.join(extract_path, image_dir, i)
+
+ contents[page_name] = image_path
+
+ return contents, file_settings
diff --git a/parsers/cbz.py b/parsers/cbz.py
index 06b77a5..ff19142 100644
--- a/parsers/cbz.py
+++ b/parsers/cbz.py
@@ -56,13 +56,9 @@ class ParseCBZ:
def get_contents(self):
# TODO
- # Image resizing, formatting
- # Include this as a collection of absolute paths only
- # Post processing can be carried out by the program
# CBZ files containing multiple directories for multiple chapters
file_settings = {
- 'temp_dir': self.temp_dir,
'images_only': True}
extract_path = os.path.join(self.temp_dir, self.file_md5)
@@ -90,10 +86,9 @@ class ParseCBZ:
found_images.sort()
for count, i in enumerate(found_images):
- page_name = 'Page ' + str(count)
+ page_name = 'Page ' + str(count + 1)
image_path = os.path.join(extract_path, image_dir, i)
contents[page_name] = image_path
- # contents[page_name] = "
" % image_path
return contents, file_settings
diff --git a/parsers/epub.py b/parsers/epub.py
index db9fb37..28c3e38 100644
--- a/parsers/epub.py
+++ b/parsers/epub.py
@@ -144,7 +144,6 @@ class ParseEPUB:
# Special settings that have to be returned with the file
# Referenced in sorter.py
file_settings = {
- 'temp_dir': extract_path,
'images_only': False}
return contents, file_settings
diff --git a/rarfile/LICENSE b/rarfile/LICENSE
new file mode 100644
index 0000000..cd53af0
--- /dev/null
+++ b/rarfile/LICENSE
@@ -0,0 +1,15 @@
+
+Copyright (c) 2005-2016 Marko Kreen
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
diff --git a/rarfile/__init__.py b/rarfile/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rarfile/dumprar.py b/rarfile/dumprar.py
new file mode 100755
index 0000000..9153cfd
--- /dev/null
+++ b/rarfile/dumprar.py
@@ -0,0 +1,556 @@
+#! /usr/bin/env python
+
+"""Dump archive contents, test extraction."""
+
+from __future__ import division, absolute_import, print_function
+
+import io
+import sys
+import getopt
+
+from datetime import datetime
+
+import rarfile as rf
+
+
+usage = """
+dumprar [switches] [ARC1 ARC2 ...] [@ARCLIST]
+switches:
+ @file read archive names from file
+ -pPSW set password
+ -Ccharset set fallback charset
+ -v increase verbosity
+ -t attempt to read all files
+ -x write read files out
+ -c show archive comment
+ -h show usage
+ -- stop switch parsing
+""".strip()
+
+os_list = ['DOS', 'OS2', 'WIN', 'UNIX', 'MACOS', 'BEOS']
+
+block_strs = ['MARK', 'MAIN', 'FILE', 'OLD_COMMENT', 'OLD_EXTRA',
+ 'OLD_SUB', 'OLD_RECOVERY', 'OLD_AUTH', 'SUB', 'ENDARC']
+
+r5_block_types = {
+ rf.RAR5_BLOCK_MAIN: 'R5_MAIN',
+ rf.RAR5_BLOCK_FILE: 'R5_FILE',
+ rf.RAR5_BLOCK_SERVICE: 'R5_SVC',
+ rf.RAR5_BLOCK_ENCRYPTION: 'R5_ENC',
+ rf.RAR5_BLOCK_ENDARC: 'R5_ENDARC',
+}
+
+
+def rar3_type(btype):
+ """RAR3 type code as string."""
+ if btype < rf.RAR_BLOCK_MARK or btype > rf.RAR_BLOCK_ENDARC:
+ return "*UNKNOWN*"
+ return block_strs[btype - rf.RAR_BLOCK_MARK]
+
+
+def rar5_type(btype):
+ """RAR5 type code as string."""
+ return r5_block_types.get(btype, '*UNKNOWN*')
+
+
+main_bits = (
+ (rf.RAR_MAIN_VOLUME, "VOL"),
+ (rf.RAR_MAIN_COMMENT, "COMMENT"),
+ (rf.RAR_MAIN_LOCK, "LOCK"),
+ (rf.RAR_MAIN_SOLID, "SOLID"),
+ (rf.RAR_MAIN_NEWNUMBERING, "NEWNR"),
+ (rf.RAR_MAIN_AUTH, "AUTH"),
+ (rf.RAR_MAIN_RECOVERY, "RECOVERY"),
+ (rf.RAR_MAIN_PASSWORD, "PASSWORD"),
+ (rf.RAR_MAIN_FIRSTVOLUME, "FIRSTVOL"),
+ (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"),
+ (rf.RAR_LONG_BLOCK, "LONG"),
+)
+
+endarc_bits = (
+ (rf.RAR_ENDARC_NEXT_VOLUME, "NEXTVOL"),
+ (rf.RAR_ENDARC_DATACRC, "DATACRC"),
+ (rf.RAR_ENDARC_REVSPACE, "REVSPACE"),
+ (rf.RAR_ENDARC_VOLNR, "VOLNR"),
+ (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"),
+ (rf.RAR_LONG_BLOCK, "LONG"),
+)
+
+file_bits = (
+ (rf.RAR_FILE_SPLIT_BEFORE, "SPLIT_BEFORE"),
+ (rf.RAR_FILE_SPLIT_AFTER, "SPLIT_AFTER"),
+ (rf.RAR_FILE_PASSWORD, "PASSWORD"),
+ (rf.RAR_FILE_COMMENT, "COMMENT"),
+ (rf.RAR_FILE_SOLID, "SOLID"),
+ (rf.RAR_FILE_LARGE, "LARGE"),
+ (rf.RAR_FILE_UNICODE, "UNICODE"),
+ (rf.RAR_FILE_SALT, "SALT"),
+ (rf.RAR_FILE_VERSION, "VERSION"),
+ (rf.RAR_FILE_EXTTIME, "EXTTIME"),
+ (rf.RAR_FILE_EXTFLAGS, "EXTFLAGS"),
+ (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"),
+ (rf.RAR_LONG_BLOCK, "LONG"),
+)
+
+generic_bits = (
+ (rf.RAR_SKIP_IF_UNKNOWN, "SKIP"),
+ (rf.RAR_LONG_BLOCK, "LONG"),
+)
+
+file_parms = ("D64", "D128", "D256", "D512",
+ "D1024", "D2048", "D4096", "DIR")
+
+r5_block_flags = (
+ (rf.RAR5_BLOCK_FLAG_EXTRA_DATA, 'EXTRA'),
+ (rf.RAR5_BLOCK_FLAG_DATA_AREA, 'DATA'),
+ (rf.RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN, 'SKIP'),
+ (rf.RAR5_BLOCK_FLAG_SPLIT_BEFORE, 'SPLIT_BEFORE'),
+ (rf.RAR5_BLOCK_FLAG_SPLIT_AFTER, 'SPLIT_AFTER'),
+ (rf.RAR5_BLOCK_FLAG_DEPENDS_PREV, 'DEPENDS'),
+ (rf.RAR5_BLOCK_FLAG_KEEP_WITH_PARENT, 'KEEP'),
+)
+
+r5_main_flags = (
+ (rf.RAR5_MAIN_FLAG_ISVOL, 'ISVOL'),
+ (rf.RAR5_MAIN_FLAG_HAS_VOLNR, 'VOLNR'),
+ (rf.RAR5_MAIN_FLAG_SOLID, 'SOLID'),
+ (rf.RAR5_MAIN_FLAG_RECOVERY, 'RECOVERY'),
+ (rf.RAR5_MAIN_FLAG_LOCKED, 'LOCKED'),
+)
+
+r5_file_flags = (
+ (rf.RAR5_FILE_FLAG_ISDIR, 'DIR'),
+ (rf.RAR5_FILE_FLAG_HAS_MTIME, 'MTIME'),
+ (rf.RAR5_FILE_FLAG_HAS_CRC32, 'CRC32'),
+ (rf.RAR5_FILE_FLAG_UNKNOWN_SIZE, 'NOSIZE'),
+)
+
+r5_enc_flags = (
+ (rf.RAR5_ENC_FLAG_HAS_CHECKVAL, 'CHECKVAL'),
+)
+
+r5_endarc_flags = (
+ (rf.RAR5_ENDARC_FLAG_NEXT_VOL, 'NEXTVOL'),
+)
+
+r5_file_enc_flags = (
+ (rf.RAR5_XENC_CHECKVAL, 'CHECKVAL'),
+ (rf.RAR5_XENC_TWEAKED, 'TWEAKED'),
+)
+
+r5_file_redir_types = {
+ rf.RAR5_XREDIR_UNIX_SYMLINK: 'UNIX_SYMLINK',
+ rf.RAR5_XREDIR_WINDOWS_SYMLINK: 'WINDOWS_SYMLINK',
+ rf.RAR5_XREDIR_WINDOWS_JUNCTION: 'WINDOWS_JUNCTION',
+ rf.RAR5_XREDIR_HARD_LINK: 'HARD_LINK',
+ rf.RAR5_XREDIR_FILE_COPY: 'FILE_COPY',
+}
+
+r5_file_redir_flags = (
+ (rf.RAR5_XREDIR_ISDIR, 'DIR'),
+)
+
+
+def xprint(m, *args):
+ """Print string to stdout.
+
+ Format unicode safely.
+ """
+ if sys.hexversion < 0x3000000:
+ m = m.decode('utf8')
+ if args:
+ m = m % args
+ if sys.hexversion < 0x3000000:
+ m = m.encode('utf8')
+ sys.stdout.write(m)
+ sys.stdout.write('\n')
+
+
+def render_flags(flags, bit_list):
+ """Show bit names.
+ """
+ res = []
+ known = 0
+ for bit in bit_list:
+ known = known | bit[0]
+ if flags & bit[0]:
+ res.append(bit[1])
+ unknown = flags & ~known
+ n = 0
+ while unknown:
+ if unknown & 1:
+ res.append("UNK_%04x" % (1 << n))
+ unknown = unknown >> 1
+ n += 1
+
+ if not res:
+ return '-'
+
+ return ",".join(res)
+
+
+def get_file_flags(flags):
+ """Show flag names and handle dict size.
+ """
+ res = render_flags(flags & ~rf.RAR_FILE_DICTMASK, file_bits)
+
+ xf = (flags & rf.RAR_FILE_DICTMASK) >> 5
+ res += "," + file_parms[xf]
+ return res
+
+
+def fmt_time(t):
+ """Format time.
+ """
+ if t is None:
+ return '(-)'
+ if isinstance(t, datetime):
+ return t.isoformat('T')
+ return "%04d-%02d-%02d %02d:%02d:%02d" % t
+
+
+def show_item(h):
+ """Show any RAR3/5 record.
+ """
+ if isinstance(h, rf.Rar3Info):
+ show_item_v3(h)
+ elif isinstance(h, rf.Rar5Info):
+ show_item_v5(h)
+ else:
+ xprint('Unknown info record')
+
+
+def show_item_v3(h):
+ """Show any RAR3 record.
+ """
+ st = rar3_type(h.type)
+ xprint("%s: hdrlen=%d datlen=%d", st, h.header_size, h.add_size)
+ if h.type in (rf.RAR_BLOCK_FILE, rf.RAR_BLOCK_SUB):
+ if h.host_os == rf.RAR_OS_UNIX:
+ s_mode = "0%o" % h.mode
+ else:
+ s_mode = "0x%x" % h.mode
+ xprint(" flags=0x%04x:%s", h.flags, get_file_flags(h.flags))
+ if h.host_os >= 0 and h.host_os < len(os_list):
+ s_os = os_list[h.host_os]
+ else:
+ s_os = "?"
+ xprint(" os=%d:%s ver=%d mode=%s meth=%c cmp=%d dec=%d vol=%d",
+ h.host_os, s_os,
+ h.extract_version, s_mode, h.compress_type,
+ h.compress_size, h.file_size, h.volume)
+ ucrc = (h.CRC + (1 << 32)) & ((1 << 32) - 1)
+ xprint(" crc=0x%08x (%d) date_time=%s", ucrc, h.CRC, fmt_time(h.date_time))
+ xprint(" name=%s", h.filename)
+ if h.mtime:
+ xprint(" mtime=%s", fmt_time(h.mtime))
+ if h.ctime:
+ xprint(" ctime=%s", fmt_time(h.ctime))
+ if h.atime:
+ xprint(" atime=%s", fmt_time(h.atime))
+ if h.arctime:
+ xprint(" arctime=%s", fmt_time(h.arctime))
+ elif h.type == rf.RAR_BLOCK_MAIN:
+ xprint(" flags=0x%04x:%s", h.flags, render_flags(h.flags, main_bits))
+ elif h.type == rf.RAR_BLOCK_ENDARC:
+ xprint(" flags=0x%04x:%s", h.flags, render_flags(h.flags, endarc_bits))
+ elif h.type == rf.RAR_BLOCK_MARK:
+ xprint(" flags=0x%04x:", h.flags)
+ else:
+ xprint(" flags=0x%04x:%s", h.flags, render_flags(h.flags, generic_bits))
+
+ if h.comment is not None:
+ cm = repr(h.comment)
+ if cm[0] == 'u':
+ cm = cm[1:]
+ xprint(" comment=%s", cm)
+
+
+def show_item_v5(h):
+ """Show any RAR5 record.
+ """
+ st = rar5_type(h.block_type)
+ xprint("%s: hdrlen=%d datlen=%d hdr_extra=%d", st, h.header_size,
+ h.compress_size, h.block_extra_size)
+ xprint(" block_flags=0x%04x:%s", h.block_flags, render_flags(h.block_flags, r5_block_flags))
+ if h.block_type in (rf.RAR5_BLOCK_FILE, rf.RAR5_BLOCK_SERVICE):
+ xprint(" name=%s", h.filename)
+ if h.file_host_os == rf.RAR5_OS_UNIX:
+ s_os = 'UNIX'
+ s_mode = "0%o" % h.mode
+ else:
+ s_os = 'WINDOWS'
+ s_mode = "0x%x" % h.mode
+ xprint(" file_flags=0x%04x:%s", h.file_flags, render_flags(h.file_flags, r5_file_flags))
+
+ cmp_flags = h.file_compress_flags
+ xprint(" cmp_algo=%d cmp_meth=%d dict=%d solid=%r",
+ cmp_flags & 0x3f,
+ (cmp_flags >> 7) & 0x07,
+ cmp_flags >> 10,
+ cmp_flags & rf.RAR5_COMPR_SOLID > 0)
+ xprint(" os=%d:%s mode=%s cmp=%r dec=%r vol=%r",
+ h.file_host_os, s_os, s_mode,
+ h.compress_size, h.file_size, h.volume)
+ if h.CRC is not None:
+ xprint(" crc=0x%08x (%d)", h.CRC, h.CRC)
+ if h.blake2sp_hash is not None:
+ xprint(" blake2sp=%s", rf.tohex(h.blake2sp_hash))
+ if h.date_time is not None:
+ xprint(" date_time=%s", fmt_time(h.date_time))
+ if h.mtime:
+ xprint(" mtime=%s", fmt_time(h.mtime))
+ if h.ctime:
+ xprint(" ctime=%s", fmt_time(h.ctime))
+ if h.atime:
+ xprint(" atime=%s", fmt_time(h.atime))
+ if h.arctime:
+ xprint(" arctime=%s", fmt_time(h.arctime))
+ if h.flags & rf.RAR_FILE_PASSWORD:
+ enc_algo, enc_flags, kdf_count, salt, iv, checkval = h.file_encryption
+ algo_name = 'AES256' if enc_algo == rf.RAR5_XENC_CIPHER_AES256 else 'UnknownAlgo'
+ xprint(' algo=%d:%s enc_flags=%04x:%s kdf_lg=%d kdf_count=%d salt=%s iv=%s checkval=%s',
+ enc_algo, algo_name, enc_flags, render_flags(enc_flags, r5_file_enc_flags),
+ kdf_count, 1 << kdf_count, rf.tohex(salt), rf.tohex(iv),
+ checkval and rf.tohex(checkval) or '-')
+ if h.file_redir:
+ redir_type, redir_flags, redir_name = h.file_redir
+ xprint(' redir: type=%s flags=%d:%s destination=%s',
+ r5_file_redir_types.get(redir_type, 'Unknown'),
+ redir_flags, render_flags(redir_flags, r5_file_redir_flags),
+ redir_name)
+ if h.file_owner:
+ uname, gname, uid, gid = h.file_owner
+ xprint(' owner: name=%r group=%r uid=%r gid=%r',
+ uname, gname, uid, gid)
+ if h.file_version:
+ flags, version = h.file_version
+ xprint(' version: flags=%r version=%r', flags, version)
+ elif h.block_type == rf.RAR5_BLOCK_MAIN:
+ xprint(" flags=0x%04x:%s", h.flags, render_flags(h.main_flags, r5_main_flags))
+ elif h.block_type == rf.RAR5_BLOCK_ENDARC:
+ xprint(" flags=0x%04x:%s", h.flags, render_flags(h.endarc_flags, r5_endarc_flags))
+ elif h.block_type == rf.RAR5_BLOCK_ENCRYPTION:
+ algo_name = 'AES256' if h.encryption_algo == rf.RAR5_XENC_CIPHER_AES256 else 'UnknownAlgo'
+ xprint(" algo=%d:%s flags=0x%04x:%s", h.encryption_algo, algo_name, h.flags,
+ render_flags(h.encryption_flags, r5_enc_flags))
+ xprint(" kdf_lg=%d kdf_count=%d", h.encryption_kdf_count, 1 << h.encryption_kdf_count)
+ xprint(" salt=%s", rf.tohex(h.encryption_salt))
+ else:
+ xprint(" - missing info -")
+
+ if h.comment is not None:
+ cm = repr(h.comment)
+ if cm[0] == 'u':
+ cm = cm[1:]
+ xprint(" comment=%s", cm)
+
+
+cf_show_comment = 0
+cf_verbose = 0
+cf_charset = None
+cf_extract = 0
+cf_test_read = 0
+cf_test_unrar = 0
+cf_test_memory = 0
+
+
+def check_crc(f, inf, desc):
+ """Compare result crc to expected value.
+ """
+ exp = inf._md_expect
+ if exp is None:
+ return
+ ucrc = f._md_context.digest()
+ if ucrc != exp:
+ print('crc error - %s - exp=%r got=%r' % (desc, exp, ucrc))
+
+
+def test_read_long(r, inf):
+ """Test read and readinto.
+ """
+ md_class = inf._md_class or rf.NoHashContext
+ bctx = md_class()
+ f = r.open(inf.filename)
+ total = 0
+ while 1:
+ data = f.read(8192)
+ if not data:
+ break
+ bctx.update(data)
+ total += len(data)
+ if total != inf.file_size:
+ xprint("\n *** %s has corrupt file: %s ***", r.rarfile, inf.filename)
+ xprint(" *** short read: got=%d, need=%d ***\n", total, inf.file_size)
+ check_crc(f, inf, 'read')
+ bhash = bctx.hexdigest()
+ if cf_verbose > 1:
+ if f._md_context.digest() == inf._md_expect:
+ #xprint(" checkhash: %r", bhash)
+ pass
+ else:
+ xprint(" checkhash: %r got=%r exp=%r cls=%r\n",
+ bhash, f._md_context.digest(), inf._md_expect, inf._md_class)
+
+ # test .seek() & .readinto()
+ if cf_test_read > 1:
+ f.seek(0, 0)
+
+ total = 0
+ buf = bytearray(rf.ZERO * 1024)
+ while 1:
+ res = f.readinto(buf)
+ if not res:
+ break
+ total += res
+ if inf.file_size != total:
+ xprint(" *** readinto failed: got=%d, need=%d ***\n", total, inf.file_size)
+ #check_crc(f, inf, 'readinto')
+ f.close()
+
+
+def test_read(r, inf):
+ """Test file read."""
+ test_read_long(r, inf)
+
+
+def test_real(fn, psw):
+ """Actual archive processing.
+ """
+ xprint("Archive: %s", fn)
+
+ cb = None
+ if cf_verbose > 1:
+ cb = show_item
+
+ rfarg = fn
+ if cf_test_memory:
+ rfarg = io.BytesIO(open(fn, 'rb').read())
+
+ # check if rar
+ if not rf.is_rarfile(rfarg):
+ xprint(" --- %s is not a RAR file ---", fn)
+ return
+
+ # open
+ r = rf.RarFile(rfarg, charset=cf_charset, info_callback=cb)
+ # set password
+ if r.needs_password():
+ if psw:
+ r.setpassword(psw)
+ else:
+ xprint(" --- %s requires password ---", fn)
+ return
+
+ # show comment
+ if cf_show_comment and r.comment:
+ for ln in r.comment.split('\n'):
+ xprint(" %s", ln)
+ elif cf_verbose > 0 and r.comment:
+ cm = repr(r.comment)
+ if cm[0] == 'u':
+ cm = cm[1:]
+ xprint(" comment=%s", cm)
+
+ # process
+ for n in r.namelist():
+ inf = r.getinfo(n)
+ if inf.isdir():
+ continue
+ if cf_verbose == 1:
+ show_item(inf)
+ if cf_test_read:
+ test_read(r, inf)
+
+ if cf_extract:
+ r.extractall()
+ for inf in r.infolist():
+ r.extract(inf)
+
+ if cf_test_unrar:
+ r.testrar()
+
+
+def test(fn, psw):
+ """Process one archive with error handling.
+ """
+ try:
+ test_real(fn, psw)
+ except rf.NeedFirstVolume:
+ xprint(" --- %s is middle part of multi-vol archive ---", fn)
+ except rf.Error:
+ exc, msg, tb = sys.exc_info()
+ xprint("\n *** %s: %s ***\n", exc.__name__, msg)
+ del tb
+ except IOError:
+ exc, msg, tb = sys.exc_info()
+ xprint("\n *** %s: %s ***\n", exc.__name__, msg)
+ del tb
+
+
+def main():
+ """Program entry point.
+ """
+ global cf_verbose, cf_show_comment, cf_charset
+ global cf_extract, cf_test_read, cf_test_unrar
+ global cf_test_memory
+
+ psw = None
+
+ # parse args
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'p:C:hvcxtRM')
+ except getopt.error as ex:
+ print(str(ex), file=sys.stderr)
+ sys.exit(1)
+
+ for o, v in opts:
+ if o == '-p':
+ psw = v
+ elif o == '-h':
+ xprint(usage)
+ return
+ elif o == '-v':
+ cf_verbose += 1
+ elif o == '-c':
+ cf_show_comment = 1
+ elif o == '-x':
+ cf_extract = 1
+ elif o == '-t':
+ cf_test_read += 1
+ elif o == '-T':
+ cf_test_unrar = 1
+ elif o == '-M':
+ cf_test_memory = 1
+ elif o == '-C':
+ cf_charset = v
+ else:
+ raise Exception("unhandled switch: " + o)
+
+ args2 = []
+ for a in args:
+ if a[0] == "@":
+ for ln in open(a[1:], 'r'):
+ fn = ln[:-1]
+ args2.append(fn)
+ else:
+ args2.append(a)
+ args = args2
+
+ if not args:
+ xprint(usage)
+
+ # pypy .readinto()+memoryview() is buggy
+ #if cf_test_read > 1 and hasattr(sys, 'pypy_version_info'):
+ # cf_test_read = 1
+
+ for fn in args:
+ test(fn, psw)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except KeyboardInterrupt:
+ pass
+
diff --git a/rarfile/rarfile.py b/rarfile/rarfile.py
new file mode 100644
index 0000000..10e242c
--- /dev/null
+++ b/rarfile/rarfile.py
@@ -0,0 +1,3011 @@
+# rarfile.py
+#
+# Copyright (c) 2005-2016 Marko Kreen
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+r"""RAR archive reader.
+
+This is Python module for Rar archive reading. The interface
+is made as :mod:`zipfile`-like as possible.
+
+Basic logic:
+ - Parse archive structure with Python.
+ - Extract non-compressed files with Python
+ - Extract compressed files with unrar.
+ - Optionally write compressed data to temp file to speed up unrar,
+ otherwise it needs to scan whole archive on each execution.
+
+Example::
+
+ import rarfile
+
+ rf = rarfile.RarFile('myarchive.rar')
+ for f in rf.infolist():
+ print f.filename, f.file_size
+ if f.filename == 'README':
+ print(rf.read(f))
+
+Archive files can also be accessed via file-like object returned
+by :meth:`RarFile.open`::
+
+ import rarfile
+
+ with rarfile.RarFile('archive.rar') as rf:
+ with rf.open('README') as f:
+ for ln in f:
+ print(ln.strip())
+
+There are few module-level parameters to tune behaviour,
+here they are with defaults, and reason to change it::
+
+ import rarfile
+
+ # Set to full path of unrar.exe if it is not in PATH
+ rarfile.UNRAR_TOOL = "unrar"
+
+ # Set to '\\' to be more compatible with old rarfile
+ rarfile.PATH_SEP = '/'
+
+For more details, refer to source.
+
+"""
+
+from __future__ import division, print_function
+
+##
+## Imports and compat - support both Python 2.x and 3.x
+##
+
+import sys
+import os
+import errno
+import struct
+
+from struct import pack, unpack, Struct
+from binascii import crc32, hexlify
+from tempfile import mkstemp
+from subprocess import Popen, PIPE, STDOUT
+from io import RawIOBase
+from hashlib import sha1, sha256
+from hmac import HMAC
+from datetime import datetime, timedelta, tzinfo
+
+# fixed offset timezone, for UTC
+try:
+ from datetime import timezone
+except ImportError:
+ class timezone(tzinfo):
+ """Compat timezone."""
+ __slots__ = ('_ofs', '_name')
+ _DST = timedelta(0)
+
+ def __init__(self, offset, name):
+ super(timezone, self).__init__()
+ self._ofs, self._name = offset, name
+
+ def utcoffset(self, dt):
+ return self._ofs
+
+ def tzname(self, dt):
+ return self._name
+
+ def dst(self, dt):
+ return self._DST
+
+# only needed for encryped headers
+try:
+ try:
+ from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives.kdf import pbkdf2
+
+ class AES_CBC_Decrypt(object):
+ """Decrypt API"""
+ def __init__(self, key, iv):
+ ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend())
+ self.decrypt = ciph.decryptor().update
+
+ def pbkdf2_sha256(password, salt, iters):
+ """PBKDF2 with HMAC-SHA256"""
+ ctx = pbkdf2.PBKDF2HMAC(hashes.SHA256(), 32, salt, iters, default_backend())
+ return ctx.derive(password)
+
+ except ImportError:
+ from Crypto.Cipher import AES
+ from Crypto.Protocol import KDF
+
+ class AES_CBC_Decrypt(object):
+ """Decrypt API"""
+ def __init__(self, key, iv):
+ self.decrypt = AES.new(key, AES.MODE_CBC, iv).decrypt
+
+ def pbkdf2_sha256(password, salt, iters):
+ """PBKDF2 with HMAC-SHA256"""
+ return KDF.PBKDF2(password, salt, 32, iters, hmac_sha256)
+
+ _have_crypto = 1
+except ImportError:
+ _have_crypto = 0
+
+try:
+ try:
+ from hashlib import blake2s
+ _have_blake2 = True
+ except ImportError:
+ from pyblake2 import blake2s
+ _have_blake2 = True
+except ImportError:
+ _have_blake2 = False
+
+# compat with 2.x
+if sys.hexversion < 0x3000000:
+ def rar_crc32(data, prev=0):
+ """CRC32 with unsigned values.
+ """
+ if (prev > 0) and (prev & 0x80000000):
+ prev -= (1 << 32)
+ res = crc32(data, prev)
+ if res < 0:
+ res += (1 << 32)
+ return res
+ tohex = hexlify
+ _byte_code = ord
+else: # pragma: no cover
+ def tohex(data):
+ """Return hex string."""
+ return hexlify(data).decode('ascii')
+ rar_crc32 = crc32
+ unicode = str
+ _byte_code = int # noqa
+
+# don't break 2.6 completely
+if sys.hexversion < 0x2070000:
+ memoryview = lambda x: x # noqa
+
+__version__ = '3.0'
+
+# export only interesting items
+__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile']
+
+##
+## Module configuration. Can be tuned after importing.
+##
+
+#: default fallback charset
+DEFAULT_CHARSET = "windows-1252"
+
+#: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
+TRY_ENCODINGS = ('utf8', 'utf-16le')
+
+#: 'unrar', 'rar' or full path to either one
+UNRAR_TOOL = "unrar"
+
+#: Command line args to use for opening file for reading.
+OPEN_ARGS = ('p', '-inul')
+
+#: Command line args to use for extracting file to disk.
+EXTRACT_ARGS = ('x', '-y', '-idq')
+
+#: args for testrar()
+TEST_ARGS = ('t', '-idq')
+
+#
+# Allow use of tool that is not compatible with unrar.
+#
+# By default use 'bsdtar' which is 'tar' program that
+# sits on top of libarchive.
+#
+# Problems with libarchive RAR backend:
+# - Does not support solid archives.
+# - Does not support password-protected archives.
+#
+
+ALT_TOOL = 'bsdtar'
+ALT_OPEN_ARGS = ('-x', '--to-stdout', '-f')
+ALT_EXTRACT_ARGS = ('-x', '-f')
+ALT_TEST_ARGS = ('-t', '-f')
+ALT_CHECK_ARGS = ('--help',)
+
+#ALT_TOOL = 'unar'
+#ALT_OPEN_ARGS = ('-o', '-')
+#ALT_EXTRACT_ARGS = ()
+#ALT_TEST_ARGS = ('-test',) # does not work
+#ALT_CHECK_ARGS = ('-v',)
+
+#: whether to speed up decompression by using tmp archive
+USE_EXTRACT_HACK = 1
+
+#: limit the filesize for tmp archive usage
+HACK_SIZE_LIMIT = 20 * 1024 * 1024
+
+#: Separator for path name components. RAR internally uses '\\'.
+#: Use '/' to be similar with zipfile.
+PATH_SEP = '/'
+
+##
+## rar constants
+##
+
+# block types
+RAR_BLOCK_MARK = 0x72 # r
+RAR_BLOCK_MAIN = 0x73 # s
+RAR_BLOCK_FILE = 0x74 # t
+RAR_BLOCK_OLD_COMMENT = 0x75 # u
+RAR_BLOCK_OLD_EXTRA = 0x76 # v
+RAR_BLOCK_OLD_SUB = 0x77 # w
+RAR_BLOCK_OLD_RECOVERY = 0x78 # x
+RAR_BLOCK_OLD_AUTH = 0x79 # y
+RAR_BLOCK_SUB = 0x7a # z
+RAR_BLOCK_ENDARC = 0x7b # {
+
+# flags for RAR_BLOCK_MAIN
+RAR_MAIN_VOLUME = 0x0001
+RAR_MAIN_COMMENT = 0x0002
+RAR_MAIN_LOCK = 0x0004
+RAR_MAIN_SOLID = 0x0008
+RAR_MAIN_NEWNUMBERING = 0x0010
+RAR_MAIN_AUTH = 0x0020
+RAR_MAIN_RECOVERY = 0x0040
+RAR_MAIN_PASSWORD = 0x0080
+RAR_MAIN_FIRSTVOLUME = 0x0100
+RAR_MAIN_ENCRYPTVER = 0x0200
+
+# flags for RAR_BLOCK_FILE
+RAR_FILE_SPLIT_BEFORE = 0x0001
+RAR_FILE_SPLIT_AFTER = 0x0002
+RAR_FILE_PASSWORD = 0x0004
+RAR_FILE_COMMENT = 0x0008
+RAR_FILE_SOLID = 0x0010
+RAR_FILE_DICTMASK = 0x00e0
+RAR_FILE_DICT64 = 0x0000
+RAR_FILE_DICT128 = 0x0020
+RAR_FILE_DICT256 = 0x0040
+RAR_FILE_DICT512 = 0x0060
+RAR_FILE_DICT1024 = 0x0080
+RAR_FILE_DICT2048 = 0x00a0
+RAR_FILE_DICT4096 = 0x00c0
+RAR_FILE_DIRECTORY = 0x00e0
+RAR_FILE_LARGE = 0x0100
+RAR_FILE_UNICODE = 0x0200
+RAR_FILE_SALT = 0x0400
+RAR_FILE_VERSION = 0x0800
+RAR_FILE_EXTTIME = 0x1000
+RAR_FILE_EXTFLAGS = 0x2000
+
+# flags for RAR_BLOCK_ENDARC
+RAR_ENDARC_NEXT_VOLUME = 0x0001
+RAR_ENDARC_DATACRC = 0x0002
+RAR_ENDARC_REVSPACE = 0x0004
+RAR_ENDARC_VOLNR = 0x0008
+
+# flags common to all blocks
+RAR_SKIP_IF_UNKNOWN = 0x4000
+RAR_LONG_BLOCK = 0x8000
+
+# Host OS types
+RAR_OS_MSDOS = 0
+RAR_OS_OS2 = 1
+RAR_OS_WIN32 = 2
+RAR_OS_UNIX = 3
+RAR_OS_MACOS = 4
+RAR_OS_BEOS = 5
+
+# Compression methods - '0'..'5'
+RAR_M0 = 0x30
+RAR_M1 = 0x31
+RAR_M2 = 0x32
+RAR_M3 = 0x33
+RAR_M4 = 0x34
+RAR_M5 = 0x35
+
+#
+# RAR5 constants
+#
+
+RAR5_BLOCK_MAIN = 1
+RAR5_BLOCK_FILE = 2
+RAR5_BLOCK_SERVICE = 3
+RAR5_BLOCK_ENCRYPTION = 4
+RAR5_BLOCK_ENDARC = 5
+
+RAR5_BLOCK_FLAG_EXTRA_DATA = 0x01
+RAR5_BLOCK_FLAG_DATA_AREA = 0x02
+RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN = 0x04
+RAR5_BLOCK_FLAG_SPLIT_BEFORE = 0x08
+RAR5_BLOCK_FLAG_SPLIT_AFTER = 0x10
+RAR5_BLOCK_FLAG_DEPENDS_PREV = 0x20
+RAR5_BLOCK_FLAG_KEEP_WITH_PARENT = 0x40
+
+RAR5_MAIN_FLAG_ISVOL = 0x01
+RAR5_MAIN_FLAG_HAS_VOLNR = 0x02
+RAR5_MAIN_FLAG_SOLID = 0x04
+RAR5_MAIN_FLAG_RECOVERY = 0x08
+RAR5_MAIN_FLAG_LOCKED = 0x10
+
+RAR5_FILE_FLAG_ISDIR = 0x01
+RAR5_FILE_FLAG_HAS_MTIME = 0x02
+RAR5_FILE_FLAG_HAS_CRC32 = 0x04
+RAR5_FILE_FLAG_UNKNOWN_SIZE = 0x08
+
+RAR5_COMPR_SOLID = 0x40
+
+RAR5_ENC_FLAG_HAS_CHECKVAL = 0x01
+
+RAR5_ENDARC_FLAG_NEXT_VOL = 0x01
+
+RAR5_XFILE_ENCRYPTION = 1
+RAR5_XFILE_HASH = 2
+RAR5_XFILE_TIME = 3
+RAR5_XFILE_VERSION = 4
+RAR5_XFILE_REDIR = 5
+RAR5_XFILE_OWNER = 6
+RAR5_XFILE_SERVICE = 7
+
+RAR5_XTIME_UNIXTIME = 0x01
+RAR5_XTIME_HAS_MTIME = 0x02
+RAR5_XTIME_HAS_CTIME = 0x04
+RAR5_XTIME_HAS_ATIME = 0x08
+
+RAR5_XENC_CIPHER_AES256 = 0
+
+RAR5_XENC_CHECKVAL = 0x01
+RAR5_XENC_TWEAKED = 0x02
+
+RAR5_XHASH_BLAKE2SP = 0
+
+RAR5_XREDIR_UNIX_SYMLINK = 1
+RAR5_XREDIR_WINDOWS_SYMLINK = 2
+RAR5_XREDIR_WINDOWS_JUNCTION = 3
+RAR5_XREDIR_HARD_LINK = 4
+RAR5_XREDIR_FILE_COPY = 5
+
+RAR5_XREDIR_ISDIR = 0x01
+
+RAR5_XOWNER_UNAME = 0x01
+RAR5_XOWNER_GNAME = 0x02
+RAR5_XOWNER_UID = 0x04
+RAR5_XOWNER_GID = 0x08
+
+RAR5_OS_WINDOWS = 0
+RAR5_OS_UNIX = 1
+
+##
+## internal constants
+##
+
+RAR_ID = b"Rar!\x1a\x07\x00"
+RAR5_ID = b"Rar!\x1a\x07\x01\x00"
+ZERO = b'\0'
+EMPTY = b''
+UTC = timezone(timedelta(0), 'UTC')
+BSIZE = 32 * 1024
+
+def _get_rar_version(xfile):
+ """Check quickly whether file is rar archive.
+ """
+ with XFile(xfile) as fd:
+ buf = fd.read(len(RAR5_ID))
+ if buf.startswith(RAR_ID):
+ return 3
+ elif buf.startswith(RAR5_ID):
+ return 5
+ return 0
+
+##
+## Public interface
+##
+
+def is_rarfile(xfile):
+ """Check quickly whether file is rar archive.
+ """
+ return _get_rar_version(xfile) > 0
+
+class Error(Exception):
+ """Base class for rarfile errors."""
+
+class BadRarFile(Error):
+ """Incorrect data in archive."""
+
+class NotRarFile(Error):
+ """The file is not RAR archive."""
+
+class BadRarName(Error):
+ """Cannot guess multipart name components."""
+
+class NoRarEntry(Error):
+ """File not found in RAR"""
+
+class PasswordRequired(Error):
+ """File requires password"""
+
+class NeedFirstVolume(Error):
+ """Need to start from first volume."""
+
+class NoCrypto(Error):
+ """Cannot parse encrypted headers - no crypto available."""
+
+class RarExecError(Error):
+ """Problem reported by unrar/rar."""
+
+class RarWarning(RarExecError):
+ """Non-fatal error"""
+
+class RarFatalError(RarExecError):
+ """Fatal error"""
+
+class RarCRCError(RarExecError):
+ """CRC error during unpacking"""
+
+class RarLockedArchiveError(RarExecError):
+ """Must not modify locked archive"""
+
+class RarWriteError(RarExecError):
+ """Write error"""
+
+class RarOpenError(RarExecError):
+ """Open error"""
+
+class RarUserError(RarExecError):
+ """User error"""
+
+class RarMemoryError(RarExecError):
+ """Memory error"""
+
+class RarCreateError(RarExecError):
+ """Create error"""
+
+class RarNoFilesError(RarExecError):
+ """No files that match pattern were found"""
+
+class RarUserBreak(RarExecError):
+ """User stop"""
+
+class RarWrongPassword(RarExecError):
+ """Incorrect password"""
+
+class RarUnknownError(RarExecError):
+ """Unknown exit code"""
+
+class RarSignalExit(RarExecError):
+ """Unrar exited with signal"""
+
+class RarCannotExec(RarExecError):
+ """Executable not found."""
+
+
+class RarInfo(object):
+ r"""An entry in rar archive.
+
+ RAR3 extended timestamps are :class:`datetime.datetime` objects without timezone.
+ RAR5 extended timestamps are :class:`datetime.datetime` objects with UTC timezone.
+
+ Attributes:
+
+ filename
+ File name with relative path.
+ Path separator is '/'. Always unicode string.
+
+ date_time
+ File modification timestamp. As tuple of (year, month, day, hour, minute, second).
+ RAR5 allows archives where it is missing, it's None then.
+
+ file_size
+ Uncompressed size.
+
+ compress_size
+ Compressed size.
+
+ compress_type
+ Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
+
+ extract_version
+ Minimal Rar version needed for decompressing. As (major*10 + minor),
+ so 2.9 is 29.
+
+ RAR3: 10, 20, 29
+
+ RAR5 does not have such field in archive, it's simply set to 50.
+
+ host_os
+ Host OS type, one of RAR_OS_* constants.
+
+ RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
+ :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
+
+ RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
+
+ mode
+ File attributes. May be either dos-style or unix-style, depending on host_os.
+
+ mtime
+ File modification time. Same value as :attr:`date_time`
+ but as :class:`datetime.datetime` object with extended precision.
+
+ ctime
+ Optional time field: creation time. As :class:`datetime.datetime` object.
+
+ atime
+ Optional time field: last access time. As :class:`datetime.datetime` object.
+
+ arctime
+ Optional time field: archival time. As :class:`datetime.datetime` object.
+ (RAR3-only)
+
+ CRC
+ CRC-32 of uncompressed file, unsigned int.
+
+ RAR5: may be None.
+
+ blake2sp_hash
+ Blake2SP hash over decompressed data. (RAR5-only)
+
+ comment
+ Optional file comment field. Unicode string. (RAR3-only)
+
+ file_redir
+ If not None, file is link of some sort. Contains tuple of (type, flags, target).
+ (RAR5-only)
+
+ Type is one of constants:
+
+ :data:`RAR5_XREDIR_UNIX_SYMLINK`
+ unix symlink to target.
+ :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
+ windows symlink to target.
+ :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
+ windows junction.
+ :data:`RAR5_XREDIR_HARD_LINK`
+ hard link to target.
+ :data:`RAR5_XREDIR_FILE_COPY`
+ current file is copy of another archive entry.
+
+ Flags may contain :data:`RAR5_XREDIR_ISDIR` bit.
+
+ volume
+ Volume nr, starting from 0.
+
+ volume_file
+ Volume file name, where file starts.
+
+ """
+
+ # zipfile-compatible fields
+ filename = None
+ file_size = None
+ compress_size = None
+ date_time = None
+ comment = None
+ CRC = None
+ volume = None
+ orig_filename = None
+
+ # optional extended time fields, datetime() objects.
+ mtime = None
+ ctime = None
+ atime = None
+
+ extract_version = None
+ mode = None
+ host_os = None
+ compress_type = None
+
+ # rar3-only fields
+ comment = None
+ arctime = None
+
+ # rar5-only fields
+ blake2sp_hash = None
+ file_redir = None
+
+ # internal fields
+ flags = 0
+ type = None
+
+ def isdir(self):
+ """Returns True if entry is a directory.
+ """
+ if self.type == RAR_BLOCK_FILE:
+ return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY
+ return False
+
+ def needs_password(self):
+ """Returns True if data is stored password-protected.
+ """
+ if self.type == RAR_BLOCK_FILE:
+ return (self.flags & RAR_FILE_PASSWORD) > 0
+ return False
+
+
+class RarFile(object):
+ """Parse RAR structure, provide access to files in archive.
+ """
+
+ #: Archive comment. Unicode string or None.
+ comment = None
+
+ def __init__(self, rarfile, mode="r", charset=None, info_callback=None,
+ crc_check=True, errors="stop"):
+ """Open and parse a RAR archive.
+
+ Parameters:
+
+ rarfile
+ archive file name
+ mode
+ only 'r' is supported.
+ charset
+ fallback charset to use, if filenames are not already Unicode-enabled.
+ info_callback
+ debug callback, gets to see all archive entries.
+ crc_check
+ set to False to disable CRC checks
+ errors
+ Either "stop" to quietly stop parsing on errors,
+ or "strict" to raise errors. Default is "stop".
+ """
+ self._rarfile = rarfile
+ self._charset = charset or DEFAULT_CHARSET
+ self._info_callback = info_callback
+ self._crc_check = crc_check
+ self._password = None
+ self._file_parser = None
+
+ if errors == "stop":
+ self._strict = False
+ elif errors == "strict":
+ self._strict = True
+ else:
+ raise ValueError("Invalid value for 'errors' parameter.")
+
+ if mode != "r":
+ raise NotImplementedError("RarFile supports only mode=r")
+
+ self._parse()
+
+ def __enter__(self):
+ """Open context."""
+ return self
+
+ def __exit__(self, typ, value, traceback):
+ """Exit context"""
+ self.close()
+
+ def setpassword(self, password):
+ """Sets the password to use when extracting.
+ """
+ self._password = password
+ if self._file_parser:
+ if self._file_parser.has_header_encryption():
+ self._file_parser = None
+ if not self._file_parser:
+ self._parse()
+ else:
+ self._file_parser.setpassword(self._password)
+
+ def needs_password(self):
+ """Returns True if any archive entries require password for extraction.
+ """
+ return self._file_parser.needs_password()
+
+ def namelist(self):
+ """Return list of filenames in archive.
+ """
+ return [f.filename for f in self.infolist()]
+
+ def infolist(self):
+ """Return RarInfo objects for all files/directories in archive.
+ """
+ return self._file_parser.infolist()
+
+ def volumelist(self):
+ """Returns filenames of archive volumes.
+
+ In case of single-volume archive, the list contains
+ just the name of main archive file.
+ """
+ return self._file_parser.volumelist()
+
+ def getinfo(self, fname):
+ """Return RarInfo for file.
+ """
+ return self._file_parser.getinfo(fname)
+
+ def open(self, fname, mode='r', psw=None):
+ """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
+
+ The object implements :class:`io.RawIOBase` interface, so it can
+ be further wrapped with :class:`io.BufferedReader`
+ and :class:`io.TextIOWrapper`.
+
+ On older Python where io module is not available, it implements
+ only .read(), .seek(), .tell() and .close() methods.
+
+ The object is seekable, although the seeking is fast only on
+ uncompressed files, on compressed files the seeking is implemented
+ by reading ahead and/or restarting the decompression.
+
+ Parameters:
+
+ fname
+ file name or RarInfo instance.
+ mode
+ must be 'r'
+ psw
+ password to use for extracting.
+ """
+
+ if mode != 'r':
+ raise NotImplementedError("RarFile.open() supports only mode=r")
+
+ # entry lookup
+ inf = self.getinfo(fname)
+ if inf.isdir():
+ raise TypeError("Directory does not have any data: " + inf.filename)
+
+ # check password
+ if inf.needs_password():
+ psw = psw or self._password
+ if psw is None:
+ raise PasswordRequired("File %s requires password" % inf.filename)
+ else:
+ psw = None
+
+ return self._file_parser.open(inf, psw)
+
+ def read(self, fname, psw=None):
+ """Return uncompressed data for archive entry.
+
+ For longer files using :meth:`RarFile.open` may be better idea.
+
+ Parameters:
+
+ fname
+ filename or RarInfo instance
+ psw
+ password to use for extracting.
+ """
+
+ with self.open(fname, 'r', psw) as f:
+ return f.read()
+
+ def close(self):
+ """Release open resources."""
+ pass
+
+ def printdir(self):
+ """Print archive file list to stdout."""
+ for f in self.infolist():
+ print(f.filename)
+
+ def extract(self, member, path=None, pwd=None):
+ """Extract single file into current directory.
+
+ Parameters:
+
+ member
+ filename or :class:`RarInfo` instance
+ path
+ optional destination path
+ pwd
+ optional password to use
+ """
+ if isinstance(member, RarInfo):
+ fname = member.filename
+ else:
+ fname = member
+ self._extract([fname], path, pwd)
+
+ def extractall(self, path=None, members=None, pwd=None):
+ """Extract all files into current directory.
+
+ Parameters:
+
+ path
+ optional destination path
+ members
+ optional filename or :class:`RarInfo` instance list to extract
+ pwd
+ optional password to use
+ """
+ fnlist = []
+ if members is not None:
+ for m in members:
+ if isinstance(m, RarInfo):
+ fnlist.append(m.filename)
+ else:
+ fnlist.append(m)
+ self._extract(fnlist, path, pwd)
+
+ def testrar(self):
+ """Let 'unrar' test the archive.
+ """
+ cmd = [UNRAR_TOOL] + list(TEST_ARGS)
+ add_password_arg(cmd, self._password)
+ cmd.append('--')
+ with XTempFile(self._rarfile) as rarfile:
+ cmd.append(rarfile)
+ p = custom_popen(cmd)
+ output = p.communicate()[0]
+ check_returncode(p, output)
+
+ def strerror(self):
+ """Return error string if parsing failed or None if no problems.
+ """
+ if not self._file_parser:
+ return "Not a RAR file"
+ return self._file_parser.strerror()
+
+ ##
+ ## private methods
+ ##
+
+ def _parse(self):
+ ver = _get_rar_version(self._rarfile)
+ if ver == 3:
+ p3 = RAR3Parser(self._rarfile, self._password, self._crc_check,
+ self._charset, self._strict, self._info_callback)
+ self._file_parser = p3 # noqa
+ elif ver == 5:
+ p5 = RAR5Parser(self._rarfile, self._password, self._crc_check,
+ self._charset, self._strict, self._info_callback)
+ self._file_parser = p5 # noqa
+ else:
+ raise BadRarFile("Not a RAR file")
+
+ self._file_parser.parse()
+ self.comment = self._file_parser.comment
+
+ # call unrar to extract a file
+ def _extract(self, fnlist, path=None, psw=None):
+ cmd = [UNRAR_TOOL] + list(EXTRACT_ARGS)
+
+ # pasoword
+ psw = psw or self._password
+ add_password_arg(cmd, psw)
+ cmd.append('--')
+
+ # rar file
+ with XTempFile(self._rarfile) as rarfn:
+ cmd.append(rarfn)
+
+ # file list
+ for fn in fnlist:
+ if os.sep != PATH_SEP:
+ fn = fn.replace(PATH_SEP, os.sep)
+ cmd.append(fn)
+
+ # destination path
+ if path is not None:
+ cmd.append(path + os.sep)
+
+ # call
+ p = custom_popen(cmd)
+ output = p.communicate()[0]
+ check_returncode(p, output)
+
+#
+# File format parsing
+#
+
+class CommonParser(object):
+ """Shared parser parts."""
+ _main = None
+ _hdrenc_main = None
+ _needs_password = False
+ _fd = None
+ _expect_sig = None
+ _parse_error = None
+ _password = None
+ comment = None
+
+ def __init__(self, rarfile, password, crc_check, charset, strict, info_cb):
+ self._rarfile = rarfile
+ self._password = password
+ self._crc_check = crc_check
+ self._charset = charset
+ self._strict = strict
+ self._info_callback = info_cb
+ self._info_list = []
+ self._info_map = {}
+ self._vol_list = []
+
+ def has_header_encryption(self):
+ """Returns True if headers are encrypted
+ """
+ if self._hdrenc_main:
+ return True
+ if self._main:
+ if self._main.flags & RAR_MAIN_PASSWORD:
+ return True
+ return False
+
+ def setpassword(self, psw):
+ """Set cached password."""
+ self._password = psw
+
+ def volumelist(self):
+ """Volume files"""
+ return self._vol_list
+
+ def needs_password(self):
+ """Is password required"""
+ return self._needs_password
+
+ def strerror(self):
+ """Last error"""
+ return self._parse_error
+
+ def infolist(self):
+ """List of RarInfo records.
+ """
+ return self._info_list
+
+ def getinfo(self, member):
+ """Return RarInfo for filename
+ """
+ if isinstance(member, RarInfo):
+ fname = member.filename
+ else:
+ fname = member
+
+ # accept both ways here
+ if PATH_SEP == '/':
+ fname2 = fname.replace("\\", "/")
+ else:
+ fname2 = fname.replace("/", "\\")
+
+ try:
+ return self._info_map[fname]
+ except KeyError:
+ try:
+ return self._info_map[fname2]
+ except KeyError:
+ raise NoRarEntry("No such file: %s" % fname)
+
+ # read rar
+ def parse(self):
+ """Process file."""
+ self._fd = None
+ try:
+ self._parse_real()
+ finally:
+ if self._fd:
+ self._fd.close()
+ self._fd = None
+
+ def _parse_real(self):
+ fd = XFile(self._rarfile)
+ self._fd = fd
+ sig = fd.read(len(self._expect_sig))
+ if sig != self._expect_sig:
+ if isinstance(self._rarfile, (str, unicode)):
+ raise NotRarFile("Not a Rar archive: {}".format(self._rarfile))
+ raise NotRarFile("Not a Rar archive")
+
+ volume = 0 # first vol (.rar) is 0
+ more_vols = False
+ endarc = False
+ volfile = self._rarfile
+ self._vol_list = [self._rarfile]
+ while 1:
+ if endarc:
+ h = None # don't read past ENDARC
+ else:
+ h = self._parse_header(fd)
+ if not h:
+ if more_vols:
+ volume += 1
+ fd.close()
+ try:
+ volfile = self._next_volname(volfile)
+ fd = XFile(volfile)
+ except IOError:
+ self._set_error("Cannot open next volume: %s", volfile)
+ break
+ self._fd = fd
+ sig = fd.read(len(self._expect_sig))
+ if sig != self._expect_sig:
+ self._set_error("Invalid volume sig: %s", volfile)
+ break
+ more_vols = False
+ endarc = False
+ self._vol_list.append(volfile)
+ continue
+ break
+ h.volume = volume
+ h.volume_file = volfile
+
+ if h.type == RAR_BLOCK_MAIN and not self._main:
+ self._main = h
+ if h.flags & RAR_MAIN_NEWNUMBERING:
+ # RAR 2.x does not set FIRSTVOLUME,
+ # so check it only if NEWNUMBERING is used
+ if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0:
+ raise NeedFirstVolume("Need to start from first volume")
+ if h.flags & RAR_MAIN_PASSWORD:
+ self._needs_password = True
+ if not self._password:
+ break
+ elif h.type == RAR_BLOCK_ENDARC:
+ more_vols = (h.flags & RAR_ENDARC_NEXT_VOLUME) > 0
+ endarc = True
+ elif h.type == RAR_BLOCK_FILE:
+ # RAR 2.x does not write RAR_BLOCK_ENDARC
+ if h.flags & RAR_FILE_SPLIT_AFTER:
+ more_vols = True
+ # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
+ if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
+ raise NeedFirstVolume("Need to start from first volume")
+
+ if h.needs_password():
+ self._needs_password = True
+
+ # store it
+ self.process_entry(fd, h)
+
+ if self._info_callback:
+ self._info_callback(h)
+
+ # go to next header
+ if h.add_size > 0:
+ fd.seek(h.data_offset + h.add_size, 0)
+
+ def process_entry(self, fd, item):
+ """Examine item, add into lookup cache."""
+ raise NotImplementedError()
+
+ def _decrypt_header(self, fd):
+ raise NotImplementedError('_decrypt_header')
+
+ def _parse_block_header(self, fd):
+ raise NotImplementedError('_parse_block_header')
+
+ def _open_hack(self, inf, psw):
+ raise NotImplementedError('_open_hack')
+
+ # read single header
+ def _parse_header(self, fd):
+ try:
+ # handle encrypted headers
+ if (self._main and self._main.flags & RAR_MAIN_PASSWORD) or self._hdrenc_main:
+ if not self._password:
+ return
+ fd = self._decrypt_header(fd)
+
+ # now read actual header
+ return self._parse_block_header(fd)
+ except struct.error:
+ self._set_error('Broken header in RAR file')
+ return None
+
+ # given current vol name, construct next one
+ def _next_volname(self, volfile):
+ if is_filelike(volfile):
+ raise IOError("Working on single FD")
+ if self._main.flags & RAR_MAIN_NEWNUMBERING:
+ return _next_newvol(volfile)
+ return _next_oldvol(volfile)
+
+ def _set_error(self, msg, *args):
+ if args:
+ msg = msg % args
+ self._parse_error = msg
+ if self._strict:
+ raise BadRarFile(msg)
+
+ def open(self, inf, psw):
+ """Return stream object for file data."""
+
+ if inf.file_redir:
+ # cannot leave to unrar as it expects copied file to exist
+ if inf.file_redir[0] in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK):
+ inf = self.getinfo(inf.file_redir[2])
+ if not inf:
+ raise BadRarFile('cannot find copied file')
+
+ if inf.flags & RAR_FILE_SPLIT_BEFORE:
+ raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename)
+
+ # is temp write usable?
+ use_hack = 1
+ if not self._main:
+ use_hack = 0
+ elif self._main._must_disable_hack():
+ use_hack = 0
+ elif inf._must_disable_hack():
+ use_hack = 0
+ elif is_filelike(self._rarfile):
+ pass
+ elif inf.file_size > HACK_SIZE_LIMIT:
+ use_hack = 0
+ elif not USE_EXTRACT_HACK:
+ use_hack = 0
+
+ # now extract
+ if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0 and inf.file_redir is None:
+ return self._open_clear(inf)
+ elif use_hack:
+ return self._open_hack(inf, psw)
+ elif is_filelike(self._rarfile):
+ return self._open_unrar_membuf(self._rarfile, inf, psw)
+ else:
+ return self._open_unrar(self._rarfile, inf, psw)
+
+ def _open_clear(self, inf):
+ return DirectReader(self, inf)
+
+ def _open_hack_core(self, inf, psw, prefix, suffix):
+
+ size = inf.compress_size + inf.header_size
+ rf = XFile(inf.volume_file, 0)
+ rf.seek(inf.header_offset)
+
+ tmpfd, tmpname = mkstemp(suffix='.rar')
+ tmpf = os.fdopen(tmpfd, "wb")
+
+ try:
+ tmpf.write(prefix)
+ while size > 0:
+ if size > BSIZE:
+ buf = rf.read(BSIZE)
+ else:
+ buf = rf.read(size)
+ if not buf:
+ raise BadRarFile('read failed: ' + inf.filename)
+ tmpf.write(buf)
+ size -= len(buf)
+ tmpf.write(suffix)
+ tmpf.close()
+ rf.close()
+ except:
+ rf.close()
+ tmpf.close()
+ os.unlink(tmpname)
+ raise
+
+ return self._open_unrar(tmpname, inf, psw, tmpname)
+
+ # write in-memory archive to temp file - needed for solid archives
+ def _open_unrar_membuf(self, memfile, inf, psw):
+ tmpname = membuf_tempfile(memfile)
+ return self._open_unrar(tmpname, inf, psw, tmpname, force_file=True)
+
+ # extract using unrar
+ def _open_unrar(self, rarfile, inf, psw=None, tmpfile=None, force_file=False):
+ cmd = [UNRAR_TOOL] + list(OPEN_ARGS)
+ add_password_arg(cmd, psw)
+ cmd.append("--")
+ cmd.append(rarfile)
+
+ # not giving filename avoids encoding related problems
+ if not tmpfile or force_file:
+ fn = inf.filename
+ if PATH_SEP != os.sep:
+ fn = fn.replace(PATH_SEP, os.sep)
+ cmd.append(fn)
+
+ # read from unrar pipe
+ return PipeReader(self, inf, cmd, tmpfile)
+
+#
+# RAR3 format
+#
+
+class Rar3Info(RarInfo):
+ """RAR3 specific fields."""
+ extract_version = 15
+ salt = None
+ add_size = 0
+ header_crc = None
+ header_size = None
+ header_offset = None
+ data_offset = None
+ _md_class = None
+ _md_expect = None
+
+ # make sure some rar5 fields are always present
+ file_redir = None
+ blake2sp_hash = None
+
+ def _must_disable_hack(self):
+ if self.type == RAR_BLOCK_FILE:
+ if self.flags & RAR_FILE_PASSWORD:
+ return True
+ elif self.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
+ return True
+ elif self.type == RAR_BLOCK_MAIN:
+ if self.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD):
+ return True
+ return False
+
+
+class RAR3Parser(CommonParser):
+ """Parse RAR3 file format.
+ """
+ _expect_sig = RAR_ID
+ _last_aes_key = (None, None, None) # (salt, key, iv)
+
+ def _decrypt_header(self, fd):
+ if not _have_crypto:
+ raise NoCrypto('Cannot parse encrypted headers - no crypto')
+ salt = fd.read(8)
+ if self._last_aes_key[0] == salt:
+ key, iv = self._last_aes_key[1:]
+ else:
+ key, iv = rar3_s2k(self._password, salt)
+ self._last_aes_key = (salt, key, iv)
+ return HeaderDecrypt(fd, key, iv)
+
+ # common header
+ def _parse_block_header(self, fd):
+ h = Rar3Info()
+ h.header_offset = fd.tell()
+
+ # read and parse base header
+ buf = fd.read(S_BLK_HDR.size)
+ if not buf:
+ return None
+ t = S_BLK_HDR.unpack_from(buf)
+ h.header_crc, h.type, h.flags, h.header_size = t
+
+ # read full header
+ if h.header_size > S_BLK_HDR.size:
+ hdata = buf + fd.read(h.header_size - S_BLK_HDR.size)
+ else:
+ hdata = buf
+ h.data_offset = fd.tell()
+
+ # unexpected EOF?
+ if len(hdata) != h.header_size:
+ self._set_error('Unexpected EOF when reading header')
+ return None
+
+ pos = S_BLK_HDR.size
+
+ # block has data assiciated with it?
+ if h.flags & RAR_LONG_BLOCK:
+ h.add_size, pos = load_le32(hdata, pos)
+ else:
+ h.add_size = 0
+
+ # parse interesting ones, decide header boundaries for crc
+ if h.type == RAR_BLOCK_MARK:
+ return h
+ elif h.type == RAR_BLOCK_MAIN:
+ pos += 6
+ if h.flags & RAR_MAIN_ENCRYPTVER:
+ pos += 1
+ crc_pos = pos
+ if h.flags & RAR_MAIN_COMMENT:
+ self._parse_subblocks(h, hdata, pos)
+ elif h.type == RAR_BLOCK_FILE:
+ pos = self._parse_file_header(h, hdata, pos - 4)
+ crc_pos = pos
+ if h.flags & RAR_FILE_COMMENT:
+ pos = self._parse_subblocks(h, hdata, pos)
+ elif h.type == RAR_BLOCK_SUB:
+ pos = self._parse_file_header(h, hdata, pos - 4)
+ crc_pos = h.header_size
+ elif h.type == RAR_BLOCK_OLD_AUTH:
+ pos += 8
+ crc_pos = pos
+ elif h.type == RAR_BLOCK_OLD_EXTRA:
+ pos += 7
+ crc_pos = pos
+ else:
+ crc_pos = h.header_size
+
+ # check crc
+ if h.type == RAR_BLOCK_OLD_SUB:
+ crcdat = hdata[2:] + fd.read(h.add_size)
+ else:
+ crcdat = hdata[2:crc_pos]
+
+ calc_crc = rar_crc32(crcdat) & 0xFFFF
+
+ # return good header
+ if h.header_crc == calc_crc:
+ return h
+
+ # header parsing failed.
+ self._set_error('Header CRC error (%02x): exp=%x got=%x (xlen = %d)',
+ h.type, h.header_crc, calc_crc, len(crcdat))
+
+ # instead panicing, send eof
+ return None
+
+ # read file-specific header
+ def _parse_file_header(self, h, hdata, pos):
+ fld = S_FILE_HDR.unpack_from(hdata, pos)
+ pos += S_FILE_HDR.size
+
+ h.compress_size = fld[0]
+ h.file_size = fld[1]
+ h.host_os = fld[2]
+ h.CRC = fld[3]
+ h.date_time = parse_dos_time(fld[4])
+ h.mtime = to_datetime(h.date_time)
+ h.extract_version = fld[5]
+ h.compress_type = fld[6]
+ name_size = fld[7]
+ h.mode = fld[8]
+
+ h._md_class = CRC32Context
+ h._md_expect = h.CRC
+
+ if h.flags & RAR_FILE_LARGE:
+ h1, pos = load_le32(hdata, pos)
+ h2, pos = load_le32(hdata, pos)
+ h.compress_size |= h1 << 32
+ h.file_size |= h2 << 32
+ h.add_size = h.compress_size
+
+ name, pos = load_bytes(hdata, name_size, pos)
+ if h.flags & RAR_FILE_UNICODE:
+ nul = name.find(ZERO)
+ h.orig_filename = name[:nul]
+ u = UnicodeFilename(h.orig_filename, name[nul + 1:])
+ h.filename = u.decode()
+
+ # if parsing failed fall back to simple name
+ if u.failed:
+ h.filename = self._decode(h.orig_filename)
+ else:
+ h.orig_filename = name
+ h.filename = self._decode(name)
+
+ # change separator, if requested
+ if PATH_SEP != '\\':
+ h.filename = h.filename.replace('\\', PATH_SEP)
+
+ if h.flags & RAR_FILE_SALT:
+ h.salt, pos = load_bytes(hdata, 8, pos)
+ else:
+ h.salt = None
+
+ # optional extended time stamps
+ if h.flags & RAR_FILE_EXTTIME:
+ pos = _parse_ext_time(h, hdata, pos)
+ else:
+ h.mtime = h.atime = h.ctime = h.arctime = None
+
+ return pos
+
+ # find old-style comment subblock
+ def _parse_subblocks(self, h, hdata, pos):
+ while pos < len(hdata):
+ # ordinary block header
+ t = S_BLK_HDR.unpack_from(hdata, pos)
+ ___scrc, stype, sflags, slen = t
+ pos_next = pos + slen
+ pos += S_BLK_HDR.size
+
+ # corrupt header
+ if pos_next < pos:
+ break
+
+ # followed by block-specific header
+ if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= pos_next:
+ declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos)
+ pos += S_COMMENT_HDR.size
+ data = hdata[pos : pos_next]
+ cmt = rar3_decompress(ver, meth, data, declen, sflags,
+ crc, self._password)
+ if not self._crc_check:
+ h.comment = self._decode_comment(cmt)
+ elif rar_crc32(cmt) & 0xFFFF == crc:
+ h.comment = self._decode_comment(cmt)
+
+ pos = pos_next
+ return pos
+
+ def _read_comment_v3(self, inf, psw=None):
+
+ # read data
+ with XFile(inf.volume_file) as rf:
+ rf.seek(inf.data_offset)
+ data = rf.read(inf.compress_size)
+
+ # decompress
+ cmt = rar3_decompress(inf.extract_version, inf.compress_type, data,
+ inf.file_size, inf.flags, inf.CRC, psw, inf.salt)
+
+ # check crc
+ if self._crc_check:
+ crc = rar_crc32(cmt)
+ if crc != inf.CRC:
+ return None
+
+ return self._decode_comment(cmt)
+
+ def _decode(self, val):
+ for c in TRY_ENCODINGS:
+ try:
+ return val.decode(c)
+ except UnicodeError:
+ pass
+ return val.decode(self._charset, 'replace')
+
+ def _decode_comment(self, val):
+ return self._decode(val)
+
+ def process_entry(self, fd, item):
+ if item.type == RAR_BLOCK_FILE:
+ # use only first part
+ if (item.flags & RAR_FILE_SPLIT_BEFORE) == 0:
+ self._info_map[item.filename] = item
+ self._info_list.append(item)
+ elif len(self._info_list) > 0:
+ # final crc is in last block
+ old = self._info_list[-1]
+ old.CRC = item.CRC
+ old._md_expect = item._md_expect
+ old.compress_size += item.compress_size
+
+ # parse new-style comment
+ if item.type == RAR_BLOCK_SUB and item.filename == 'CMT':
+ if item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
+ pass
+ elif item.flags & RAR_FILE_SOLID:
+ # file comment
+ cmt = self._read_comment_v3(item, self._password)
+ if len(self._info_list) > 0:
+ old = self._info_list[-1]
+ old.comment = cmt
+ else:
+ # archive comment
+ cmt = self._read_comment_v3(item, self._password)
+ self.comment = cmt
+
+ if item.type == RAR_BLOCK_MAIN:
+ if item.flags & RAR_MAIN_COMMENT:
+ self.comment = item.comment
+ if item.flags & RAR_MAIN_PASSWORD:
+ self._needs_password = True
+
+ # put file compressed data into temporary .rar archive, and run
+ # unrar on that, thus avoiding unrar going over whole archive
+ def _open_hack(self, inf, psw):
+ # create main header: crc, type, flags, size, res1, res2
+ prefix = RAR_ID + S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2 + 4)
+ return self._open_hack_core(inf, psw, prefix, EMPTY)
+
+#
+# RAR5 format
+#
+
+class Rar5Info(RarInfo):
+ """Shared fields for RAR5 records.
+ """
+ extract_version = 50
+ header_crc = None
+ header_size = None
+ header_offset = None
+ data_offset = None
+
+ # type=all
+ block_type = None
+ block_flags = None
+ add_size = 0
+ block_extra_size = 0
+
+ # type=MAIN
+ volume_number = None
+ _md_class = None
+ _md_expect = None
+
+ def _must_disable_hack(self):
+ return False
+
+
+class Rar5BaseFile(Rar5Info):
+ """Shared sturct for file & service record.
+ """
+ type = -1
+ file_flags = None
+ file_encryption = (0, 0, 0, EMPTY, EMPTY, EMPTY)
+ file_compress_flags = None
+ file_redir = None
+ file_owner = None
+ file_version = None
+ blake2sp_hash = None
+
+ def _must_disable_hack(self):
+ if self.flags & RAR_FILE_PASSWORD:
+ return True
+ if self.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
+ return True
+ if self.file_compress_flags & RAR5_COMPR_SOLID:
+ return True
+ if self.file_redir:
+ return True
+ return False
+
+
+class Rar5FileInfo(Rar5BaseFile):
+ """RAR5 file record.
+ """
+ type = RAR_BLOCK_FILE
+
+
+class Rar5ServiceInfo(Rar5BaseFile):
+ """RAR5 service record.
+ """
+ type = RAR_BLOCK_SUB
+
+
+class Rar5MainInfo(Rar5Info):
+ """RAR5 archive main record.
+ """
+ type = RAR_BLOCK_MAIN
+ main_flags = None
+ main_volume_number = None
+
+ def _must_disable_hack(self):
+ if self.main_flags & RAR5_MAIN_FLAG_SOLID:
+ return True
+ return False
+
+
+class Rar5EncryptionInfo(Rar5Info):
+ """RAR5 archive header encryption record.
+ """
+ type = RAR5_BLOCK_ENCRYPTION
+ encryption_algo = None
+ encryption_flags = None
+ encryption_kdf_count = None
+ encryption_salt = None
+ encryption_check_value = None
+
+ def needs_password(self):
+ return True
+
+
+class Rar5EndArcInfo(Rar5Info):
+ """RAR5 end of archive record.
+ """
+ type = RAR_BLOCK_ENDARC
+ endarc_flags = None
+
+
+class RAR5Parser(CommonParser):
+ """Parse RAR5 format.
+ """
+ _expect_sig = RAR5_ID
+ _hdrenc_main = None
+
+ # AES encrypted headers
+ _last_aes256_key = (-1, None, None) # (kdf_count, salt, key)
+
+ def _gen_key(self, kdf_count, salt):
+ if self._last_aes256_key[:2] == (kdf_count, salt):
+ return self._last_aes256_key[2]
+ if kdf_count > 24:
+ raise BadRarFile('Too large kdf_count')
+ psw = self._password
+ if isinstance(psw, unicode):
+ psw = psw.encode('utf8')
+ key = pbkdf2_sha256(psw, salt, 1 << kdf_count)
+ self._last_aes256_key = (kdf_count, salt, key)
+ return key
+
+ def _decrypt_header(self, fd):
+ if not _have_crypto:
+ raise NoCrypto('Cannot parse encrypted headers - no crypto')
+ h = self._hdrenc_main
+ key = self._gen_key(h.encryption_kdf_count, h.encryption_salt)
+ iv = fd.read(16)
+ return HeaderDecrypt(fd, key, iv)
+
+ # common header
+ def _parse_block_header(self, fd):
+ header_offset = fd.tell()
+
+ preload = 4 + 3
+ start_bytes = fd.read(preload)
+ header_crc, pos = load_le32(start_bytes, 0)
+ hdrlen, pos = load_vint(start_bytes, pos)
+ if hdrlen > 2 * 1024 * 1024:
+ return None
+ header_size = pos + hdrlen
+
+ # read full header, check for EOF
+ hdata = start_bytes + fd.read(header_size - len(start_bytes))
+ if len(hdata) != header_size:
+ self._set_error('Unexpected EOF when reading header')
+ return None
+ data_offset = fd.tell()
+
+ calc_crc = rar_crc32(memoryview(hdata)[4:])
+ if header_crc != calc_crc:
+ # header parsing failed.
+ self._set_error('Header CRC error: exp=%x got=%x (xlen = %d)',
+ header_crc, calc_crc, len(hdata))
+ return None
+
+ block_type, pos = load_vint(hdata, pos)
+
+ if block_type == RAR5_BLOCK_MAIN:
+ h, pos = self._parse_block_common(Rar5MainInfo(), hdata)
+ h = self._parse_main_block(h, hdata, pos)
+ elif block_type == RAR5_BLOCK_FILE:
+ h, pos = self._parse_block_common(Rar5FileInfo(), hdata)
+ h = self._parse_file_block(h, hdata, pos)
+ elif block_type == RAR5_BLOCK_SERVICE:
+ h, pos = self._parse_block_common(Rar5ServiceInfo(), hdata)
+ h = self._parse_file_block(h, hdata, pos)
+ elif block_type == RAR5_BLOCK_ENCRYPTION:
+ h, pos = self._parse_block_common(Rar5EncryptionInfo(), hdata)
+ h = self._parse_encryption_block(h, hdata, pos)
+ elif block_type == RAR5_BLOCK_ENDARC:
+ h, pos = self._parse_block_common(Rar5EndArcInfo(), hdata)
+ h = self._parse_endarc_block(h, hdata, pos)
+ else:
+ h = None
+ if h:
+ h.header_offset = header_offset
+ h.data_offset = data_offset
+ return h
+
+ def _parse_block_common(self, h, hdata):
+ h.header_crc, pos = load_le32(hdata, 0)
+ hdrlen, pos = load_vint(hdata, pos)
+ h.header_size = hdrlen + pos
+ h.block_type, pos = load_vint(hdata, pos)
+ h.block_flags, pos = load_vint(hdata, pos)
+
+ if h.block_flags & RAR5_BLOCK_FLAG_EXTRA_DATA:
+ h.block_extra_size, pos = load_vint(hdata, pos)
+ if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
+ h.add_size, pos = load_vint(hdata, pos)
+
+ h.compress_size = h.add_size
+
+ if h.block_flags & RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN:
+ h.flags |= RAR_SKIP_IF_UNKNOWN
+ if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
+ h.flags |= RAR_LONG_BLOCK
+ return h, pos
+
+ def _parse_main_block(self, h, hdata, pos):
+ h.main_flags, pos = load_vint(hdata, pos)
+ if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR:
+ h.main_volume_number = load_vint(hdata, pos)
+
+ h.flags |= RAR_MAIN_NEWNUMBERING
+ if h.main_flags & RAR5_MAIN_FLAG_SOLID:
+ h.flags |= RAR_MAIN_SOLID
+ if h.main_flags & RAR5_MAIN_FLAG_ISVOL:
+ h.flags |= RAR_MAIN_VOLUME
+ if h.main_flags & RAR5_MAIN_FLAG_RECOVERY:
+ h.flags |= RAR_MAIN_RECOVERY
+ if self._hdrenc_main:
+ h.flags |= RAR_MAIN_PASSWORD
+ if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR == 0:
+ h.flags |= RAR_MAIN_FIRSTVOLUME
+
+ return h
+
+ def _parse_file_block(self, h, hdata, pos):
+ h.file_flags, pos = load_vint(hdata, pos)
+ h.file_size, pos = load_vint(hdata, pos)
+ h.mode, pos = load_vint(hdata, pos)
+
+ if h.file_flags & RAR5_FILE_FLAG_HAS_MTIME:
+ h.mtime, pos = load_unixtime(hdata, pos)
+ h.date_time = h.mtime.timetuple()[:6]
+ if h.file_flags & RAR5_FILE_FLAG_HAS_CRC32:
+ h.CRC, pos = load_le32(hdata, pos)
+ h._md_class = CRC32Context
+ h._md_expect = h.CRC
+
+ h.file_compress_flags, pos = load_vint(hdata, pos)
+ h.file_host_os, pos = load_vint(hdata, pos)
+ h.orig_filename, pos = load_vstr(hdata, pos)
+ h.filename = h.orig_filename.decode('utf8', 'replace')
+
+ # use compatible values
+ if h.file_host_os == RAR5_OS_WINDOWS:
+ h.host_os = RAR_OS_WIN32
+ else:
+ h.host_os = RAR_OS_UNIX
+ h.compress_type = RAR_M0 + ((h.file_compress_flags >> 7) & 7)
+
+ if h.block_extra_size:
+ # allow 1 byte of garbage
+ while pos < len(hdata) - 1:
+ xsize, pos = load_vint(hdata, pos)
+ xdata, pos = load_bytes(hdata, xsize, pos)
+ self._process_file_extra(h, xdata)
+
+ if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE:
+ h.flags |= RAR_FILE_SPLIT_BEFORE
+ if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_AFTER:
+ h.flags |= RAR_FILE_SPLIT_AFTER
+ if h.file_flags & RAR5_FILE_FLAG_ISDIR:
+ h.flags |= RAR_FILE_DIRECTORY
+ if h.file_compress_flags & RAR5_COMPR_SOLID:
+ h.flags |= RAR_FILE_SOLID
+
+ return h
+
+ def _parse_endarc_block(self, h, hdata, pos):
+ h.endarc_flags, pos = load_vint(hdata, pos)
+ if h.endarc_flags & RAR5_ENDARC_FLAG_NEXT_VOL:
+ h.flags |= RAR_ENDARC_NEXT_VOLUME
+ return h
+
+ def _parse_encryption_block(self, h, hdata, pos):
+ h.encryption_algo, pos = load_vint(hdata, pos)
+ h.encryption_flags, pos = load_vint(hdata, pos)
+ h.encryption_kdf_count, pos = load_byte(hdata, pos)
+ h.encryption_salt, pos = load_bytes(hdata, 16, pos)
+ if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL:
+ h.encryption_check_value = load_bytes(hdata, 12, pos)
+ if h.encryption_algo != RAR5_XENC_CIPHER_AES256:
+ raise BadRarFile('Unsupported header encryption cipher')
+ self._hdrenc_main = h
+ return h
+
+ # file extra record
+ def _process_file_extra(self, h, xdata):
+ xtype, pos = load_vint(xdata, 0)
+ if xtype == RAR5_XFILE_TIME:
+ self._parse_file_xtime(h, xdata, pos)
+ elif xtype == RAR5_XFILE_ENCRYPTION:
+ self._parse_file_encryption(h, xdata, pos)
+ elif xtype == RAR5_XFILE_HASH:
+ self._parse_file_hash(h, xdata, pos)
+ elif xtype == RAR5_XFILE_VERSION:
+ self._parse_file_version(h, xdata, pos)
+ elif xtype == RAR5_XFILE_REDIR:
+ self._parse_file_redir(h, xdata, pos)
+ elif xtype == RAR5_XFILE_OWNER:
+ self._parse_file_owner(h, xdata, pos)
+ elif xtype == RAR5_XFILE_SERVICE:
+ pass
+ else:
+ pass
+
+ # extra block for file time record
+ def _parse_file_xtime(self, h, xdata, pos):
+ tflags, pos = load_vint(xdata, pos)
+ ldr = load_windowstime
+ if tflags & RAR5_XTIME_UNIXTIME:
+ ldr = load_unixtime
+ if tflags & RAR5_XTIME_HAS_MTIME:
+ h.mtime, pos = ldr(xdata, pos)
+ h.date_time = h.mtime.timetuple()[:6]
+ if tflags & RAR5_XTIME_HAS_CTIME:
+ h.ctime, pos = ldr(xdata, pos)
+ if tflags & RAR5_XTIME_HAS_ATIME:
+ h.atime, pos = ldr(xdata, pos)
+
+ # just remember encryption info
+ def _parse_file_encryption(self, h, xdata, pos):
+ algo, pos = load_vint(xdata, pos)
+ flags, pos = load_vint(xdata, pos)
+ kdf_count, pos = load_byte(xdata, pos)
+ salt, pos = load_bytes(xdata, 16, pos)
+ iv, pos = load_bytes(xdata, 16, pos)
+ checkval = None
+ if flags & RAR5_XENC_CHECKVAL:
+ checkval, pos = load_bytes(xdata, 12, pos)
+ if flags & RAR5_XENC_TWEAKED:
+ h._md_expect = None
+ h._md_class = NoHashContext
+
+ h.file_encryption = (algo, flags, kdf_count, salt, iv, checkval)
+ h.flags |= RAR_FILE_PASSWORD
+
+ def _parse_file_hash(self, h, xdata, pos):
+ hash_type, pos = load_vint(xdata, pos)
+ if hash_type == RAR5_XHASH_BLAKE2SP:
+ h.blake2sp_hash, pos = load_bytes(xdata, 32, pos)
+ if _have_blake2 and (h.file_encryption[1] & RAR5_XENC_TWEAKED) == 0:
+ h._md_class = Blake2SP
+ h._md_expect = h.blake2sp_hash
+
+ def _parse_file_version(self, h, xdata, pos):
+ flags, pos = load_vint(xdata, pos)
+ version, pos = load_vint(xdata, pos)
+ h.file_version = (flags, version)
+
+ def _parse_file_redir(self, h, xdata, pos):
+ redir_type, pos = load_vint(xdata, pos)
+ redir_flags, pos = load_vint(xdata, pos)
+ redir_name, pos = load_vstr(xdata, pos)
+ redir_name = redir_name.decode('utf8', 'replace')
+ h.file_redir = (redir_type, redir_flags, redir_name)
+
+ def _parse_file_owner(self, h, xdata, pos):
+ user_name = group_name = user_id = group_id = None
+
+ flags, pos = load_vint(xdata, pos)
+ if flags & RAR5_XOWNER_UNAME:
+ user_name, pos = load_vstr(xdata, pos)
+ if flags & RAR5_XOWNER_GNAME:
+ group_name, pos = load_vstr(xdata, pos)
+ if flags & RAR5_XOWNER_UID:
+ user_id, pos = load_vint(xdata, pos)
+ if flags & RAR5_XOWNER_GID:
+ group_id, pos = load_vint(xdata, pos)
+
+ h.file_owner = (user_name, group_name, user_id, group_id)
+
+ def process_entry(self, fd, item):
+ if item.block_type == RAR5_BLOCK_FILE:
+ # use only first part
+ if (item.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE) == 0:
+ self._info_map[item.filename] = item
+ self._info_list.append(item)
+ elif len(self._info_list) > 0:
+ # final crc is in last block
+ old = self._info_list[-1]
+ old.CRC = item.CRC
+ old._md_expect = item._md_expect
+ old.blake2sp_hash = item.blake2sp_hash
+ old.compress_size += item.compress_size
+ elif item.block_type == RAR5_BLOCK_SERVICE:
+ if item.filename == 'CMT':
+ self._load_comment(fd, item)
+
+ def _load_comment(self, fd, item):
+ if item.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
+ return None
+ if item.compress_type != RAR_M0:
+ return None
+
+ if item.flags & RAR_FILE_PASSWORD:
+ algo, ___flags, kdf_count, salt, iv, ___checkval = item.file_encryption
+ if algo != RAR5_XENC_CIPHER_AES256:
+ return None
+ key = self._gen_key(kdf_count, salt)
+ f = HeaderDecrypt(fd, key, iv)
+ cmt = f.read(item.file_size)
+ else:
+ # archive comment
+ with self._open_clear(item) as cmtstream:
+ cmt = cmtstream.read()
+
+ # rar bug? - appends zero to comment
+ cmt = cmt.split(ZERO, 1)[0]
+ self.comment = cmt.decode('utf8')
+
+ def _open_hack(self, inf, psw):
+ # len, type, blk_flags, flags
+ main_hdr = b'\x03\x01\x00\x00'
+ endarc_hdr = b'\x03\x05\x00\x00'
+ main_hdr = S_LONG.pack(rar_crc32(main_hdr)) + main_hdr
+ endarc_hdr = S_LONG.pack(rar_crc32(endarc_hdr)) + endarc_hdr
+ return self._open_hack_core(inf, psw, RAR5_ID + main_hdr, endarc_hdr)
+
+##
+## Utility classes
+##
+
+class UnicodeFilename(object):
+ """Handle RAR3 unicode filename decompression.
+ """
+ def __init__(self, name, encdata):
+ self.std_name = bytearray(name)
+ self.encdata = bytearray(encdata)
+ self.pos = self.encpos = 0
+ self.buf = bytearray()
+ self.failed = 0
+
+ def enc_byte(self):
+ """Copy encoded byte."""
+ try:
+ c = self.encdata[self.encpos]
+ self.encpos += 1
+ return c
+ except IndexError:
+ self.failed = 1
+ return 0
+
+ def std_byte(self):
+ """Copy byte from 8-bit representation."""
+ try:
+ return self.std_name[self.pos]
+ except IndexError:
+ self.failed = 1
+ return ord('?')
+
+ def put(self, lo, hi):
+ """Copy 16-bit value to result."""
+ self.buf.append(lo)
+ self.buf.append(hi)
+ self.pos += 1
+
+ def decode(self):
+ """Decompress compressed UTF16 value."""
+ hi = self.enc_byte()
+ flagbits = 0
+ while self.encpos < len(self.encdata):
+ if flagbits == 0:
+ flags = self.enc_byte()
+ flagbits = 8
+ flagbits -= 2
+ t = (flags >> flagbits) & 3
+ if t == 0:
+ self.put(self.enc_byte(), 0)
+ elif t == 1:
+ self.put(self.enc_byte(), hi)
+ elif t == 2:
+ self.put(self.enc_byte(), self.enc_byte())
+ else:
+ n = self.enc_byte()
+ if n & 0x80:
+ c = self.enc_byte()
+ for _ in range((n & 0x7f) + 2):
+ lo = (self.std_byte() + c) & 0xFF
+ self.put(lo, hi)
+ else:
+ for _ in range(n + 2):
+ self.put(self.std_byte(), 0)
+ return self.buf.decode("utf-16le", "replace")
+
+
+class RarExtFile(RawIOBase):
+ """Base class for file-like object that :meth:`RarFile.open` returns.
+
+ Provides public methods and common crc checking.
+
+ Behaviour:
+ - no short reads - .read() and .readinfo() read as much as requested.
+ - no internal buffer, use io.BufferedReader for that.
+ """
+
+ #: Filename of the archive entry
+ name = None
+
+ def __init__(self, parser, inf):
+ """Open archive entry.
+ """
+ super(RarExtFile, self).__init__()
+
+ # standard io.* properties
+ self.name = inf.filename
+ self.mode = 'rb'
+
+ self._parser = parser
+ self._inf = inf
+ self._fd = None
+ self._remain = 0
+ self._returncode = 0
+
+ self._md_context = None
+
+ self._open()
+
+ def _open(self):
+ if self._fd:
+ self._fd.close()
+ md_class = self._inf._md_class or NoHashContext
+ self._md_context = md_class()
+ self._fd = None
+ self._remain = self._inf.file_size
+
+ def read(self, cnt=None):
+ """Read all or specified amount of data from archive entry."""
+
+ # sanitize cnt
+ if cnt is None or cnt < 0:
+ cnt = self._remain
+ elif cnt > self._remain:
+ cnt = self._remain
+ if cnt == 0:
+ return EMPTY
+
+ # actual read
+ data = self._read(cnt)
+ if data:
+ self._md_context.update(data)
+ self._remain -= len(data)
+ if len(data) != cnt:
+ raise BadRarFile("Failed the read enough data")
+
+ # done?
+ if not data or self._remain == 0:
+ # self.close()
+ self._check()
+ return data
+
+ def _check(self):
+ """Check final CRC."""
+ final = self._md_context.digest()
+ exp = self._inf._md_expect
+ if exp is None:
+ return
+ if final is None:
+ return
+ if self._returncode:
+ check_returncode(self, '')
+ if self._remain != 0:
+ raise BadRarFile("Failed the read enough data")
+ if final != exp:
+ raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
+ self._inf.filename, exp, final))
+
+ def _read(self, cnt):
+ """Actual read that gets sanitized cnt."""
+
+ def close(self):
+ """Close open resources."""
+
+ super(RarExtFile, self).close()
+
+ if self._fd:
+ self._fd.close()
+ self._fd = None
+
+ def __del__(self):
+ """Hook delete to make sure tempfile is removed."""
+ self.close()
+
+ def readinto(self, buf):
+ """Zero-copy read directly into buffer.
+
+ Returns bytes read.
+ """
+ raise NotImplementedError('readinto')
+
+ def tell(self):
+ """Return current reading position in uncompressed data."""
+ return self._inf.file_size - self._remain
+
+ def seek(self, ofs, whence=0):
+ """Seek in data.
+
+ On uncompressed files, the seeking works by actual
+ seeks so it's fast. On compresses files its slow
+ - forward seeking happends by reading ahead,
+ backwards by re-opening and decompressing from the start.
+ """
+
+ # disable crc check when seeking
+ self._md_context = NoHashContext()
+
+ fsize = self._inf.file_size
+ cur_ofs = self.tell()
+
+ if whence == 0: # seek from beginning of file
+ new_ofs = ofs
+ elif whence == 1: # seek from current position
+ new_ofs = cur_ofs + ofs
+ elif whence == 2: # seek from end of file
+ new_ofs = fsize + ofs
+ else:
+ raise ValueError('Invalid value for whence')
+
+ # sanity check
+ if new_ofs < 0:
+ new_ofs = 0
+ elif new_ofs > fsize:
+ new_ofs = fsize
+
+ # do the actual seek
+ if new_ofs >= cur_ofs:
+ self._skip(new_ofs - cur_ofs)
+ else:
+ # reopen and seek
+ self._open()
+ self._skip(new_ofs)
+ return self.tell()
+
+ def _skip(self, cnt):
+ """Read and discard data"""
+ while cnt > 0:
+ if cnt > 8192:
+ buf = self.read(8192)
+ else:
+ buf = self.read(cnt)
+ if not buf:
+ break
+ cnt -= len(buf)
+
+ def readable(self):
+ """Returns True"""
+ return True
+
+ def writable(self):
+ """Returns False.
+
+ Writing is not supported.
+ """
+ return False
+
+ def seekable(self):
+ """Returns True.
+
+ Seeking is supported, although it's slow on compressed files.
+ """
+ return True
+
+ def readall(self):
+ """Read all remaining data"""
+ # avoid RawIOBase default impl
+ return self.read()
+
+
+class PipeReader(RarExtFile):
+ """Read data from pipe, handle tempfile cleanup."""
+
+ def __init__(self, rf, inf, cmd, tempfile=None):
+ self._cmd = cmd
+ self._proc = None
+ self._tempfile = tempfile
+ super(PipeReader, self).__init__(rf, inf)
+
+ def _close_proc(self):
+ if not self._proc:
+ return
+ if self._proc.stdout:
+ self._proc.stdout.close()
+ if self._proc.stdin:
+ self._proc.stdin.close()
+ if self._proc.stderr:
+ self._proc.stderr.close()
+ self._proc.wait()
+ self._returncode = self._proc.returncode
+ self._proc = None
+
+ def _open(self):
+ super(PipeReader, self)._open()
+
+ # stop old process
+ self._close_proc()
+
+ # launch new process
+ self._returncode = 0
+ self._proc = custom_popen(self._cmd)
+ self._fd = self._proc.stdout
+
+ # avoid situation where unrar waits on stdin
+ if self._proc.stdin:
+ self._proc.stdin.close()
+
+ def _read(self, cnt):
+ """Read from pipe."""
+
+ # normal read is usually enough
+ data = self._fd.read(cnt)
+ if len(data) == cnt or not data:
+ return data
+
+ # short read, try looping
+ buf = [data]
+ cnt -= len(data)
+ while cnt > 0:
+ data = self._fd.read(cnt)
+ if not data:
+ break
+ cnt -= len(data)
+ buf.append(data)
+ return EMPTY.join(buf)
+
+ def close(self):
+ """Close open resources."""
+
+ self._close_proc()
+ super(PipeReader, self).close()
+
+ if self._tempfile:
+ try:
+ os.unlink(self._tempfile)
+ except OSError:
+ pass
+ self._tempfile = None
+
+ def readinto(self, buf):
+ """Zero-copy read directly into buffer."""
+ cnt = len(buf)
+ if cnt > self._remain:
+ cnt = self._remain
+ vbuf = memoryview(buf)
+ res = got = 0
+ while got < cnt:
+ res = self._fd.readinto(vbuf[got : cnt])
+ if not res:
+ break
+ self._md_context.update(vbuf[got : got + res])
+ self._remain -= res
+ got += res
+ return got
+
+
+class DirectReader(RarExtFile):
+ """Read uncompressed data directly from archive.
+ """
+ _cur = None
+ _cur_avail = None
+ _volfile = None
+
+ def _open(self):
+ super(DirectReader, self)._open()
+
+ self._volfile = self._inf.volume_file
+ self._fd = XFile(self._volfile, 0)
+ self._fd.seek(self._inf.header_offset, 0)
+ self._cur = self._parser._parse_header(self._fd)
+ self._cur_avail = self._cur.add_size
+
+ def _skip(self, cnt):
+ """RAR Seek, skipping through rar files to get to correct position
+ """
+
+ while cnt > 0:
+ # next vol needed?
+ if self._cur_avail == 0:
+ if not self._open_next():
+ break
+
+ # fd is in read pos, do the read
+ if cnt > self._cur_avail:
+ cnt -= self._cur_avail
+ self._remain -= self._cur_avail
+ self._cur_avail = 0
+ else:
+ self._fd.seek(cnt, 1)
+ self._cur_avail -= cnt
+ self._remain -= cnt
+ cnt = 0
+
+ def _read(self, cnt):
+ """Read from potentially multi-volume archive."""
+
+ buf = []
+ while cnt > 0:
+ # next vol needed?
+ if self._cur_avail == 0:
+ if not self._open_next():
+ break
+
+ # fd is in read pos, do the read
+ if cnt > self._cur_avail:
+ data = self._fd.read(self._cur_avail)
+ else:
+ data = self._fd.read(cnt)
+ if not data:
+ break
+
+ # got some data
+ cnt -= len(data)
+ self._cur_avail -= len(data)
+ buf.append(data)
+
+ if len(buf) == 1:
+ return buf[0]
+ return EMPTY.join(buf)
+
+ def _open_next(self):
+ """Proceed to next volume."""
+
+ # is the file split over archives?
+ if (self._cur.flags & RAR_FILE_SPLIT_AFTER) == 0:
+ return False
+
+ if self._fd:
+ self._fd.close()
+ self._fd = None
+
+ # open next part
+ self._volfile = self._parser._next_volname(self._volfile)
+ fd = open(self._volfile, "rb", 0)
+ self._fd = fd
+ sig = fd.read(len(self._parser._expect_sig))
+ if sig != self._parser._expect_sig:
+ raise BadRarFile("Invalid signature")
+
+ # loop until first file header
+ while 1:
+ cur = self._parser._parse_header(fd)
+ if not cur:
+ raise BadRarFile("Unexpected EOF")
+ if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN):
+ if cur.add_size:
+ fd.seek(cur.add_size, 1)
+ continue
+ if cur.orig_filename != self._inf.orig_filename:
+ raise BadRarFile("Did not found file entry")
+ self._cur = cur
+ self._cur_avail = cur.add_size
+ return True
+
+ def readinto(self, buf):
+ """Zero-copy read directly into buffer."""
+ got = 0
+ vbuf = memoryview(buf)
+ while got < len(buf):
+ # next vol needed?
+ if self._cur_avail == 0:
+ if not self._open_next():
+ break
+
+ # length for next read
+ cnt = len(buf) - got
+ if cnt > self._cur_avail:
+ cnt = self._cur_avail
+
+ # read into temp view
+ res = self._fd.readinto(vbuf[got : got + cnt])
+ if not res:
+ break
+ self._md_context.update(vbuf[got : got + res])
+ self._cur_avail -= res
+ self._remain -= res
+ got += res
+ return got
+
+
+class HeaderDecrypt(object):
+ """File-like object that decrypts from another file"""
+ def __init__(self, f, key, iv):
+ self.f = f
+ self.ciph = AES_CBC_Decrypt(key, iv)
+ self.buf = EMPTY
+
+ def tell(self):
+ """Current file pos - works only on block boundaries."""
+ return self.f.tell()
+
+ def read(self, cnt=None):
+ """Read and decrypt."""
+ if cnt > 8 * 1024:
+ raise BadRarFile('Bad count to header decrypt - wrong password?')
+
+ # consume old data
+ if cnt <= len(self.buf):
+ res = self.buf[:cnt]
+ self.buf = self.buf[cnt:]
+ return res
+ res = self.buf
+ self.buf = EMPTY
+ cnt -= len(res)
+
+ # decrypt new data
+ blklen = 16
+ while cnt > 0:
+ enc = self.f.read(blklen)
+ if len(enc) < blklen:
+ break
+ dec = self.ciph.decrypt(enc)
+ if cnt >= len(dec):
+ res += dec
+ cnt -= len(dec)
+ else:
+ res += dec[:cnt]
+ self.buf = dec[cnt:]
+ cnt = 0
+
+ return res
+
+
+# handle (filename|filelike) object
+class XFile(object):
+ """Input may be filename or file object.
+ """
+ __slots__ = ('_fd', '_need_close')
+
+ def __init__(self, xfile, bufsize=1024):
+ if is_filelike(xfile):
+ self._need_close = False
+ self._fd = xfile
+ self._fd.seek(0)
+ else:
+ self._need_close = True
+ self._fd = open(xfile, 'rb', bufsize)
+
+ def read(self, n=None):
+ """Read from file."""
+ return self._fd.read(n)
+
+ def tell(self):
+ """Return file pos."""
+ return self._fd.tell()
+
+ def seek(self, ofs, whence=0):
+ """Move file pos."""
+ return self._fd.seek(ofs, whence)
+
+ def readinto(self, dst):
+ """Read into buffer."""
+ return self._fd.readinto(dst)
+
+ def close(self):
+ """Close file object."""
+ if self._need_close:
+ self._fd.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, typ, val, tb):
+ self.close()
+
+
+class NoHashContext(object):
+ """No-op hash function."""
+ def __init__(self, data=None):
+ """Initialize"""
+ def update(self, data):
+ """Update data"""
+ def digest(self):
+ """Final hash"""
+ def hexdigest(self):
+ """Hexadecimal digest."""
+
+
+class CRC32Context(object):
+ """Hash context that uses CRC32."""
+ __slots__ = ['_crc']
+
+ def __init__(self, data=None):
+ self._crc = 0
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Process data."""
+ self._crc = rar_crc32(data, self._crc)
+
+ def digest(self):
+ """Final hash."""
+ return self._crc
+
+ def hexdigest(self):
+ """Hexadecimal digest."""
+ return '%08x' % self.digest()
+
+
+class Blake2SP(object):
+ """Blake2sp hash context.
+ """
+ __slots__ = ['_thread', '_buf', '_cur', '_digest']
+ digest_size = 32
+ block_size = 64
+ parallelism = 8
+
+ def __init__(self, data=None):
+ self._buf = b''
+ self._cur = 0
+ self._digest = None
+ self._thread = []
+
+ for i in range(self.parallelism):
+ ctx = self._blake2s(i, 0, i == (self.parallelism - 1))
+ self._thread.append(ctx)
+
+ if data:
+ self.update(data)
+
+ def _blake2s(self, ofs, depth, is_last):
+ return blake2s(node_offset=ofs, node_depth=depth, last_node=is_last,
+ depth=2, inner_size=32, fanout=self.parallelism)
+
+ def _add_block(self, blk):
+ self._thread[self._cur].update(blk)
+ self._cur = (self._cur + 1) % self.parallelism
+
+ def update(self, data):
+ """Hash data.
+ """
+ view = memoryview(data)
+ bs = self.block_size
+ if self._buf:
+ need = bs - len(self._buf)
+ if len(view) < need:
+ self._buf += view.tobytes()
+ return
+ self._add_block(self._buf + view[:need].tobytes())
+ view = view[need:]
+ while len(view) >= bs:
+ self._add_block(view[:bs])
+ view = view[bs:]
+ self._buf = view.tobytes()
+
+ def digest(self):
+ """Return final digest value.
+ """
+ if self._digest is None:
+ if self._buf:
+ self._add_block(self._buf)
+ self._buf = EMPTY
+ ctx = self._blake2s(0, 1, True)
+ for t in self._thread:
+ ctx.update(t.digest())
+ self._digest = ctx.digest()
+ return self._digest
+
+ def hexdigest(self):
+ """Hexadecimal digest."""
+ return tohex(self.digest())
+
+
+class Rar3Sha1(object):
+ """Bug-compat for SHA1
+ """
+ digest_size = 20
+ block_size = 64
+
+ _BLK_BE = struct.Struct(b'>16L')
+ _BLK_LE = struct.Struct(b'<16L')
+
+ __slots__ = ('_nbytes', '_md', '_rarbug')
+
+ def __init__(self, data=b'', rarbug=False):
+ self._md = sha1()
+ self._nbytes = 0
+ self._rarbug = rarbug
+ self.update(data)
+
+ def update(self, data):
+ """Process more data."""
+ self._md.update(data)
+ bufpos = self._nbytes & 63
+ self._nbytes += len(data)
+
+ if self._rarbug and len(data) > 64:
+ dpos = self.block_size - bufpos
+ while dpos + self.block_size <= len(data):
+ self._corrupt(data, dpos)
+ dpos += self.block_size
+
+ def digest(self):
+ """Return final state."""
+ return self._md.digest()
+
+ def hexdigest(self):
+ """Return final state as hex string."""
+ return self._md.hexdigest()
+
+ def _corrupt(self, data, dpos):
+ """Corruption from SHA1 core."""
+ ws = list(self._BLK_BE.unpack_from(data, dpos))
+ for t in range(16, 80):
+ tmp = ws[(t - 3) & 15] ^ ws[(t - 8) & 15] ^ ws[(t - 14) & 15] ^ ws[(t - 16) & 15]
+ ws[t & 15] = ((tmp << 1) | (tmp >> (32 - 1))) & 0xFFFFFFFF
+ self._BLK_LE.pack_into(data, dpos, *ws)
+
+
+##
+## Utility functions
+##
+
+S_LONG = Struct(' len(buf):
+ raise BadRarFile('cannot load byte')
+ return S_BYTE.unpack_from(buf, pos)[0], end
+
+def load_le32(buf, pos):
+ """Load little-endian 32-bit integer"""
+ end = pos + 4
+ if end > len(buf):
+ raise BadRarFile('cannot load le32')
+ return S_LONG.unpack_from(buf, pos)[0], pos + 4
+
+def load_bytes(buf, num, pos):
+ """Load sequence of bytes"""
+ end = pos + num
+ if end > len(buf):
+ raise BadRarFile('cannot load bytes')
+ return buf[pos : end], end
+
+def load_vstr(buf, pos):
+ """Load bytes prefixed by vint length"""
+ slen, pos = load_vint(buf, pos)
+ return load_bytes(buf, slen, pos)
+
+def load_dostime(buf, pos):
+ """Load LE32 dos timestamp"""
+ stamp, pos = load_le32(buf, pos)
+ tup = parse_dos_time(stamp)
+ return to_datetime(tup), pos
+
+def load_unixtime(buf, pos):
+ """Load LE32 unix timestamp"""
+ secs, pos = load_le32(buf, pos)
+ dt = datetime.fromtimestamp(secs, UTC)
+ return dt, pos
+
+def load_windowstime(buf, pos):
+ """Load LE64 windows timestamp"""
+ # unix epoch (1970) in seconds from windows epoch (1601)
+ unix_epoch = 11644473600
+ val1, pos = load_le32(buf, pos)
+ val2, pos = load_le32(buf, pos)
+ secs, n1secs = divmod((val2 << 32) | val1, 10000000)
+ dt = datetime.fromtimestamp(secs - unix_epoch, UTC)
+ dt = dt.replace(microsecond=n1secs // 10)
+ return dt, pos
+
+# new-style next volume
+def _next_newvol(volfile):
+ i = len(volfile) - 1
+ while i >= 0:
+ if volfile[i] >= '0' and volfile[i] <= '9':
+ return _inc_volname(volfile, i)
+ i -= 1
+ raise BadRarName("Cannot construct volume name: " + volfile)
+
+# old-style next volume
+def _next_oldvol(volfile):
+ # rar -> r00
+ if volfile[-4:].lower() == '.rar':
+ return volfile[:-2] + '00'
+ return _inc_volname(volfile, len(volfile) - 1)
+
+# increase digits with carry, otherwise just increment char
+def _inc_volname(volfile, i):
+ fn = list(volfile)
+ while i >= 0:
+ if fn[i] != '9':
+ fn[i] = chr(ord(fn[i]) + 1)
+ break
+ fn[i] = '0'
+ i -= 1
+ return ''.join(fn)
+
+# rar3 extended time fields
+def _parse_ext_time(h, data, pos):
+ # flags and rest of data can be missing
+ flags = 0
+ if pos + 2 <= len(data):
+ flags = S_SHORT.unpack_from(data, pos)[0]
+ pos += 2
+
+ mtime, pos = _parse_xtime(flags >> 3 * 4, data, pos, h.mtime)
+ h.ctime, pos = _parse_xtime(flags >> 2 * 4, data, pos)
+ h.atime, pos = _parse_xtime(flags >> 1 * 4, data, pos)
+ h.arctime, pos = _parse_xtime(flags >> 0 * 4, data, pos)
+ if mtime:
+ h.mtime = mtime
+ h.date_time = mtime.timetuple()[:6]
+ return pos
+
+# rar3 one extended time field
+def _parse_xtime(flag, data, pos, basetime=None):
+ res = None
+ if flag & 8:
+ if not basetime:
+ basetime, pos = load_dostime(data, pos)
+
+ # load second fractions
+ rem = 0
+ cnt = flag & 3
+ for _ in range(cnt):
+ b, pos = load_byte(data, pos)
+ rem = (b << 16) | (rem >> 8)
+
+ # convert 100ns units to microseconds
+ usec = rem // 10
+ if usec > 1000000:
+ usec = 999999
+
+ # dostime has room for 30 seconds only, correct if needed
+ if flag & 4 and basetime.second < 59:
+ res = basetime.replace(microsecond=usec, second=basetime.second + 1)
+ else:
+ res = basetime.replace(microsecond=usec)
+ return res, pos
+
+def is_filelike(obj):
+ """Filename or file object?
+ """
+ if isinstance(obj, (bytes, unicode)):
+ return False
+ res = True
+ for a in ('read', 'tell', 'seek'):
+ res = res and hasattr(obj, a)
+ if not res:
+ raise ValueError("Invalid object passed as file")
+ return True
+
+def rar3_s2k(psw, salt):
+ """String-to-key hash for RAR3.
+ """
+ if not isinstance(psw, unicode):
+ psw = psw.decode('utf8')
+ seed = bytearray(psw.encode('utf-16le') + salt)
+ h = Rar3Sha1(rarbug=True)
+ iv = EMPTY
+ for i in range(16):
+ for j in range(0x4000):
+ cnt = S_LONG.pack(i * 0x4000 + j)
+ h.update(seed)
+ h.update(cnt[:3])
+ if j == 0:
+ iv += h.digest()[19:20]
+ key_be = h.digest()[:16]
+ key_le = pack("LLLL", key_be))
+ return key_le, iv
+
+def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None):
+ """Decompress blob of compressed data.
+
+ Used for data with non-standard header - eg. comments.
+ """
+ # already uncompressed?
+ if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0:
+ return data
+
+ # take only necessary flags
+ flags = flags & (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK)
+ flags |= RAR_LONG_BLOCK
+
+ # file header
+ fname = b'data'
+ date = 0
+ mode = 0x20
+ fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
+ date, vers, meth, len(fname), mode)
+ fhdr += fname
+ if flags & RAR_FILE_SALT:
+ if not salt:
+ return EMPTY
+ fhdr += salt
+
+ # full header
+ hlen = S_BLK_HDR.size + len(fhdr)
+ hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr
+ hcrc = rar_crc32(hdr[2:]) & 0xFFFF
+ hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr
+
+ # archive main header
+ mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4)
+
+ # decompress via temp rar
+ tmpfd, tmpname = mkstemp(suffix='.rar')
+ tmpf = os.fdopen(tmpfd, "wb")
+ try:
+ tmpf.write(RAR_ID + mh + hdr + data)
+ tmpf.close()
+
+ cmd = [UNRAR_TOOL] + list(OPEN_ARGS)
+ add_password_arg(cmd, psw, (flags & RAR_FILE_PASSWORD))
+ cmd.append(tmpname)
+
+ p = custom_popen(cmd)
+ return p.communicate()[0]
+ finally:
+ tmpf.close()
+ os.unlink(tmpname)
+
+def to_datetime(t):
+ """Convert 6-part time tuple into datetime object.
+ """
+ if t is None:
+ return None
+
+ # extract values
+ year, mon, day, h, m, s = t
+
+ # assume the values are valid
+ try:
+ return datetime(year, mon, day, h, m, s)
+ except ValueError:
+ pass
+
+ # sanitize invalid values
+ mday = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
+ if mon < 1:
+ mon = 1
+ if mon > 12:
+ mon = 12
+ if day < 1:
+ day = 1
+ if day > mday[mon]:
+ day = mday[mon]
+ if h > 23:
+ h = 23
+ if m > 59:
+ m = 59
+ if s > 59:
+ s = 59
+ if mon == 2 and day == 29:
+ try:
+ return datetime(year, mon, day, h, m, s)
+ except ValueError:
+ day = 28
+ return datetime(year, mon, day, h, m, s)
+
+def parse_dos_time(stamp):
+ """Parse standard 32-bit DOS timestamp.
+ """
+ sec, stamp = stamp & 0x1F, stamp >> 5
+ mn, stamp = stamp & 0x3F, stamp >> 6
+ hr, stamp = stamp & 0x1F, stamp >> 5
+ day, stamp = stamp & 0x1F, stamp >> 5
+ mon, stamp = stamp & 0x0F, stamp >> 4
+ yr = (stamp & 0x7F) + 1980
+ return (yr, mon, day, hr, mn, sec * 2)
+
+def custom_popen(cmd):
+ """Disconnect cmd from parent fds, read only from stdout.
+ """
+ # needed for py2exe
+ creationflags = 0
+ if sys.platform == 'win32':
+ creationflags = 0x08000000 # CREATE_NO_WINDOW
+
+ # run command
+ try:
+ p = Popen(cmd, bufsize=0, stdout=PIPE, stdin=PIPE, stderr=STDOUT,
+ creationflags=creationflags)
+ except OSError as ex:
+ if ex.errno == errno.ENOENT:
+ raise RarCannotExec("Unrar not installed? (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL)
+ if ex.errno == errno.EACCES or ex.errno == errno.EPERM:
+ raise RarCannotExec("Cannot execute unrar (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL)
+ raise
+ return p
+
+def custom_check(cmd, ignore_retcode=False):
+ """Run command, collect output, raise error if needed.
+ """
+ p = custom_popen(cmd)
+ out, _ = p.communicate()
+ if p.returncode and not ignore_retcode:
+ raise RarExecError("Check-run failed")
+ return out
+
+def add_password_arg(cmd, psw, ___required=False):
+ """Append password switch to commandline.
+ """
+ if UNRAR_TOOL == ALT_TOOL:
+ return
+ if psw is not None:
+ cmd.append('-p' + psw)
+ else:
+ cmd.append('-p-')
+
+def check_returncode(p, out):
+ """Raise exception according to unrar exit code.
+ """
+ code = p.returncode
+ if code == 0:
+ return
+
+ # map return code to exception class, codes from rar.txt
+ errmap = [None,
+ RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, # 1..4
+ RarWriteError, RarOpenError, RarUserError, RarMemoryError, # 5..8
+ RarCreateError, RarNoFilesError, RarWrongPassword] # 9..11
+ if UNRAR_TOOL == ALT_TOOL:
+ errmap = [None]
+ if code > 0 and code < len(errmap):
+ exc = errmap[code]
+ elif code == 255:
+ exc = RarUserBreak
+ elif code < 0:
+ exc = RarSignalExit
+ else:
+ exc = RarUnknownError
+
+ # format message
+ if out:
+ msg = "%s [%d]: %s" % (exc.__doc__, p.returncode, out)
+ else:
+ msg = "%s [%d]" % (exc.__doc__, p.returncode)
+
+ raise exc(msg)
+
+def hmac_sha256(key, data):
+ """HMAC-SHA256"""
+ return HMAC(key, data, sha256).digest()
+
+def membuf_tempfile(memfile):
+ """Write in-memory file object to real file."""
+ memfile.seek(0, 0)
+
+ tmpfd, tmpname = mkstemp(suffix='.rar')
+ tmpf = os.fdopen(tmpfd, "wb")
+
+ try:
+ while True:
+ buf = memfile.read(BSIZE)
+ if not buf:
+ break
+ tmpf.write(buf)
+ tmpf.close()
+ except:
+ tmpf.close()
+ os.unlink(tmpname)
+ raise
+ return tmpname
+
+class XTempFile(object):
+ """Real file for archive.
+ """
+ __slots__ = ('_tmpfile', '_filename')
+
+ def __init__(self, rarfile):
+ if is_filelike(rarfile):
+ self._tmpfile = membuf_tempfile(rarfile)
+ self._filename = self._tmpfile
+ else:
+ self._tmpfile = None
+ self._filename = rarfile
+
+ def __enter__(self):
+ return self._filename
+
+ def __exit__(self, exc_type, exc_value, tb):
+ if self._tmpfile:
+ try:
+ os.unlink(self._tmpfile)
+ except OSError:
+ pass
+ self._tmpfile = None
+
+#
+# Check if unrar works
+#
+
+ORIG_UNRAR_TOOL = UNRAR_TOOL
+ORIG_OPEN_ARGS = OPEN_ARGS
+ORIG_EXTRACT_ARGS = EXTRACT_ARGS
+ORIG_TEST_ARGS = TEST_ARGS
+
+def _check_unrar_tool():
+ global UNRAR_TOOL, OPEN_ARGS, EXTRACT_ARGS, TEST_ARGS
+ try:
+ # does UNRAR_TOOL work?
+ custom_check([ORIG_UNRAR_TOOL], True)
+
+ UNRAR_TOOL = ORIG_UNRAR_TOOL
+ OPEN_ARGS = ORIG_OPEN_ARGS
+ EXTRACT_ARGS = ORIG_EXTRACT_ARGS
+ TEST_ARGS = ORIG_TEST_ARGS
+ except RarCannotExec:
+ try:
+ # does ALT_TOOL work?
+ custom_check([ALT_TOOL] + list(ALT_CHECK_ARGS), True)
+ # replace config
+ UNRAR_TOOL = ALT_TOOL
+ OPEN_ARGS = ALT_OPEN_ARGS
+ EXTRACT_ARGS = ALT_EXTRACT_ARGS
+ TEST_ARGS = ALT_TEST_ARGS
+ except RarCannotExec:
+ # no usable tool, only uncompressed archives work
+ return False
+ return True
+
+_check_unrar_tool()
+
diff --git a/sorter.py b/sorter.py
index ef2f72f..f0a2c0e 100644
--- a/sorter.py
+++ b/sorter.py
@@ -19,9 +19,12 @@ import database
# get_cover_image()
# get_isbn()
# get_contents() - Should return a tuple with 0: TOC 1: Deletable temp_directory
+# Parsers for files containing only images need to return only
+# the image path, and images_only = True
from parsers.epub import ParseEPUB
from parsers.cbz import ParseCBZ
+from parsers.cbr import ParseCBR
class BookSorter:
@@ -95,6 +98,8 @@ class BookSorter:
book_ref = ParseEPUB(filename, self.temp_dir, file_md5)
if file_extension == 'cbz':
book_ref = ParseCBZ(filename, self.temp_dir, file_md5)
+ if file_extension == 'cbr':
+ book_ref = ParseCBR(filename, self.temp_dir, file_md5)
except IndexError:
return
@@ -128,12 +133,10 @@ class BookSorter:
# get_contents() returns a tuple. Index 1 is a collection of
# special settings that depend on the kind of data being parsed.
# Currently, this includes:
- # Temporary Directory temp_dir STR Deleted upon exit
# Only images included images_only BOOL Specify only paths to images
# File will not be cached on exit
content = all_content[0]
- temp_dir = all_content[1]['temp_dir']
images_only = all_content[1]['images_only']
if not content.keys():
@@ -149,7 +152,6 @@ class BookSorter:
'path': filename,
'position': position,
'content': content,
- 'temp_dir': temp_dir,
'images_only': images_only}
diff --git a/widgets.py b/widgets.py
index ff85c77..b3bc443 100644
--- a/widgets.py
+++ b/widgets.py
@@ -307,7 +307,8 @@ class Tab(QtWidgets.QWidget):
else:
self.contentView = PliantQTextBrowser(self.window())
- relative_path_root = self.metadata['temp_dir']
+ relative_path_root = os.path.join(
+ self.window().temp_dir.path(), self.metadata['hash'])
relative_paths = []
for i in os.walk(relative_path_root):
relative_paths.append(os.path.join(relative_path_root, i[0]))
@@ -517,7 +518,8 @@ class PliantWidgetsCommonFunctions():
self.pw.verticalScrollBar().setValue(
self.pw.verticalScrollBar().maximum())
- self.pw.ignore_wheel_event = True
+ if not was_button_pressed:
+ self.pw.ignore_wheel_event = True
class LibraryDelegate(QtWidgets.QStyledItemDelegate):