summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Bogatov <KAction@debian.org>2018-12-02 05:36:55 +0000
committerDmitry Bogatov <KAction@debian.org>2018-12-02 05:36:55 +0000
commitd621b3047b2b9dd96c952b8e0c420368796672eb (patch)
tree338b6702f16dce8e1c6a31df14dcb5cb77bc3404
Import Upstream version 6.4
-rw-r--r--COPYING674
-rw-r--r--INSTALL83
-rw-r--r--NEWS172
-rw-r--r--PKG-INFO10
-rw-r--r--README109
-rwxr-xr-xscripts/dtrx1210
-rw-r--r--setup.py13
-rw-r--r--tests/compare.py237
-rw-r--r--tests/test-1.23.7zbin0 -> 183 bytes
-rw-r--r--tests/test-1.23.cpiobin0 -> 512 bytes
-rw-r--r--tests/test-1.23.gembin0 -> 10240 bytes
-rw-r--r--tests/test-1.23.tarbin0 -> 10240 bytes
-rw-r--r--tests/test-1.23.tar.bz2bin0 -> 197 bytes
-rw-r--r--tests/test-1.23.tar.gzbin0 -> 220 bytes
-rw-r--r--tests/test-1.23.tar.lzmabin0 -> 193 bytes
-rw-r--r--tests/test-1.23.zipbin0 -> 380 bytes
-rw-r--r--tests/test-1.23_all.debbin0 -> 604 bytes
-rw-r--r--tests/test-deep-recursion.tarbin0 -> 10240 bytes
-rw-r--r--tests/test-dot-first-bomb.tar.gzbin0 -> 145 bytes
-rw-r--r--tests/test-dot-first-onedir.tar.gzbin0 -> 151 bytes
-rw-r--r--tests/test-empty.tar.bz2bin0 -> 46 bytes
-rw-r--r--tests/test-one-archive.tar.gzbin0 -> 175 bytes
-rw-r--r--tests/test-onedir.tar.bz2bin0 -> 160 bytes
-rw-r--r--tests/test-onedir.tar.gzbin0 -> 158 bytes
-rw-r--r--tests/test-onefile.tar.gzbin0 -> 125 bytes
-rw-r--r--tests/test-recursive-badperms.tar.bz2bin0 -> 206 bytes
-rw-r--r--tests/test-tar-with-node.tar.gzbin0 -> 166 bytes
-rw-r--r--tests/test-text.bz2bin0 -> 40 bytes
-rw-r--r--tests/test-text.gzbin0 -> 23 bytes
-rw-r--r--tests/tests.yml657
30 files changed, 3165 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..b2408c2
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,83 @@
+dtrx Installation Documentation
+===============================
+
+Requirements
+------------
+
+dtrx will work out of the box with Python_ 2.4 or greater. You can also
+use Python 2.3 if you separately install the `subprocess module`_.
+
+.. _Python: http://www.python.org/
+.. _`subprocess module`: http://www.lysator.liu.se/~astrand/popen5/
+
+dtrx calls out to different external tools to support different archive
+types. Most of these are already installed on most GNU/Linux systems, so
+you probably won't have to worry about these too much, but just for
+completeness, the exact requirements for each format are as follows:
+
+tar archives
+ tar
+
+zip archives
+ unzip, zipinfo
+
+cpio archives
+ cpio
+
+rpm archives
+ rpm2cpio, cpio
+
+deb archives
+ ar, tar, zcat
+
+gem archives
+ tar, zcat
+
+7z archives
+ 7z
+
+Microsoft Cabinet archives
+ cabextract
+
+InstallShield archives
+ unshield
+
+rar archives
+ unrar
+
+Files compressed with gzip or compress
+ zcat
+
+Files compressed with bzip2
+ bzcat
+
+Files compressed with lzma
+ lzcat
+
+Installation
+------------
+
+dtrx is just a simple script, making it easy to stash wherever you need it.
+Just copy ``scripts/dtrx`` to a location that's convenient for you. If
+you'd like to install the program system-wide, you can run the following as
+root or equivalent::
+
+ python setup.py install --prefix=/usr/local
+
+Running Tests
+-------------
+
+dtrx comes with a suite of tests that are designed to ensure it's running
+properly. If you'd like, you can run these tests on your own system.
+Simply run the following command from the dtrx source directory::
+
+ python tests/compare.py
+
+To run the tests, you'll need the `syck module`_.
+
+.. _syck module: http://whytheluckystiff.net/syck/
+
+If everything's in order, all the tests should pass. Note that some of
+them will fail if some of the programs listed above aren't installed on
+your system. Many of the tests will fail if for some reason you're missing
+the very common commands, like tar and zcat.
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..b1638f7
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,172 @@
+Changes in dtrx
+===============
+
+Version 6.4
+-----------
+
+Enhancements
+~~~~~~~~~~~~
+
+ * Support detection of LZMA archives by magic.
+
+ * Interactive prompts are wrapped much more cleanly.
+
+Bug fixes
+~~~~~~~~~
+
+ * Fix a bug where dtrx would crash when extracting an archive with no
+ files inside it.
+
+Version 6.3
+-----------
+
+New features
+~~~~~~~~~~~~
+
+ * Add support for RAR archives. Thanks to Peter Kelemen for the patch.
+
+Bug fixes
+~~~~~~~~~
+
+ * Previous versions of dtrx would fail to extract certain archive types
+ with the ``-v`` option specified. This has been fixed.
+
+ * dtrx 6.3 no longer imports the sets module unless it's running under a
+ very old version of Python, to avoid deprecation warnings under Python
+ 2.6.
+
+Version 6.2
+-----------
+
+New features
+~~~~~~~~~~~~
+
+ * --one-entry option: Normally, if an archive only contains one file or
+ directory with a name that doesn't match the archive's, dtrx will ask
+ you how to handle it. With this option, you can specify ahead of time
+ what should happen.
+
+Bug fixes
+~~~~~~~~~
+
+ * Since version 6.0, when you extracted or listed the contents of a cpio
+ archive, dtrx would display a warning that simply said "1234 blocks."
+ dtrx 6.2 suppresses this message.
+
+ * When you try to list the contents of an archive, dtrx will now cope with
+ misnamed files more gracefully, giving more accurate results and showing
+ fewer error messages.
+
+ * dtrx 6.2 will only show you error messages from archive extraction if it
+ is completely unable to extract the file. If one of its extraction
+ methods succeeds, it will no longer show you the error messages from
+ previous extraction attempts.
+
+ * dtrx is now better about cleaning up partially extracted archives when
+ it encounters an error or signal.
+
+ * Users will no longer see error messages about broken pipes from dtrx.
+
+Version 6.1
+-----------
+
+New features
+~~~~~~~~~~~~
+
+ * Add support for InstallShield archives, using the unshield command.
+
+ * The wording of many of the interactive prompts has been adjusted,
+ hopefully to be clearer and provide more information to the user
+ immediately.
+
+Bug fixes
+~~~~~~~~~
+
+ * dtrx 6.1 does a better job protecting against race conditions when
+ extracting a single file.
+
+ * If you used the -f option, and extracted an archive that only contained
+ one file or directory, dtrx 6.0 would still prompt you to ask how it
+ should be extracted. dtrx 6.1 fixes this, extracting the contents to
+ the current directory as -f requires.
+
+ * Recursive extraction would not work well in dtrx 6.0 when the contents
+ of the original archive were a single file. This has been fixed in dtrx
+ 6.1.
+
+Version 6.0
+-----------
+
+New features
+~~~~~~~~~~~~
+
+ * When you specify -v at the command line, dtrx will display the files it
+ extracts, much like tar.
+
+ * When dtrx prompts you about how to handle recursive archives, you now
+ have the option of listing what those archives before making a decision.
+
+ * dtrx will now provide more information about why a particular extraction
+ attempt failed. It will show you error messages from all the attempts
+ it made, rather than only the last error it got. It will also detect
+ and warn you when one of the underlying extraction tools, like
+ cabextract, cannot be found.
+
+ * dtrx does a better job of cleaning up after itself. It wouldn't always
+ clean up temporary files after certain errors; that has been fixed. It
+ also catches SIGINT and SIGTERM and cleans up before finishing
+ execution.
+
+Bug fixes
+~~~~~~~~~
+
+ * Version 5.0 introduced a regression such that dtrx would not offer to
+ extract recursive archives that were hidden under subdirectories.
+ Version 6.0 fixes that.
+
+ * dtrx would not properly extract recursive archives when the original
+ archive contained a single directory. This has been fixed.
+
+Version 5.1
+-----------
+
+Bug fixes
+~~~~~~~~~
+
+ * Version 5.0 did not work with Python 2.3; it used a new language
+ feature. This release fixes that.
+
+Version 5.0
+-----------
+
+New features
+~~~~~~~~~~~~
+
+ * dtrx can now extract Ruby gems, 7z archives, and Microsoft Cabinet
+ archives. It can also handle files compressed with lzma, and extract
+ the metadata from Debian packages and Ruby gems.
+
+ * dtrx will now use several strategies to try to figure out what kind of
+ file you have, and extract it accordingly. If one doesn't work, it'll
+ try something else if it can.
+
+ * dtrx now displays more helpful errors when things go wrong.
+
+ * Previous versions of dtrx would look at what files were included in an
+ archive, and then make a decision about how to extract it. Now, it
+ always extracts files to a temporary directory, and figures out what to
+ do with that directory afterward. This should be slightly faster and
+ nicer to the system.
+
+Version 4.0
+-----------
+
+New features
+~~~~~~~~~~~~
+
+ * dtrx is now interactive. If the archive only contains one item, or
+ contains other archives, dtrx will ask you how you would like to handle
+ it. You can turn these questions off the the -n option.
+
+ * There is a new -l option, which simply lists the archive's contents
+ rather than extracting them.
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..f95cb1a
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: dtrx
+Version: 6.4
+Summary: Script to intelligently extract multiple archive types
+Home-page: http://www.brettcsmith.org/2007/dtrx/
+Author: Brett Smith
+Author-email: brettcsmith@brettcsmith.org
+License: GNU General Public License, version 3 or later
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/README b/README
new file mode 100644
index 0000000..962db19
--- /dev/null
+++ b/README
@@ -0,0 +1,109 @@
+dtrx - Intelligent archive extraction
+=====================================
+
+Introduction
+------------
+
+dtrx extracts archives in a number of different formats; it currently
+supports tar, zip, cpio, rpm, deb, gem, 7z, cab, and rar files. It can
+also decompress files compressed with gzip, bzip2, lzma, or compress.
+
+In addition to providing one command to handle many different archive
+types, dtrx also aids the user by extracting contents consistently. By
+default, everything will be written to a dedicated directory that's named
+after the archive. dtrx will also change the permissions to ensure that the
+owner can read and write all those files.
+
+Running dtrx
+------------
+
+To run dtrx, simply call it with the archive(s) you wish to extract as
+arguments. For example::
+
+ $ dtrx coreutils-5.*.tar.gz
+
+dtrx supports a number of options to mandate specific behavior:
+
+-r, --recursive
+ With this option, dtrx will search inside the archives you specify to see
+ if any of the contents are themselves archives, and extract those as
+ well.
+
+--one, --one-entry
+ Normally, if an archive only contains one file or directory with a name
+ that doesn't match the archive's, dtrx will ask you how to handle it.
+ With this option, you can specify ahead of time what should happen.
+ Possible values are:
+
+ inside
+ Extract the file/directory inside another directory named after the
+ archive. This is the default.
+
+ rename
+ Extract the file/directory in the current directory, and then rename
+ it to match the name of the archive.
+
+ here
+ Extract the file/directory in the current directory.
+
+-o, --overwrite
+ Normally, dtrx will avoid extracting into a directory that already exists,
+ and instead try to find an alternative name to use. If this option is
+ listed, dtrx will use the default directory name no matter what.
+
+-f, --flat
+ Extract all archive contents into the current directory, instead of
+ their own dedicated directory. This is handy if you have multiple
+ archive files which all need to be extracted into the same directory
+ structure. Note that existing files may be overwritten with this
+ option.
+
+-n, --noninteractive
+ dtrx will normally ask the user how to handle certain corner cases, such
+ as how to handle an archive that only contains one file. This option
+ suppresses those questions; dtrx will instead use sane, conservative
+ defaults.
+
+-l, -t, --list, --table
+ Don't extract the archives; just list their contents on standard output.
+
+-m, --metadata
+ Extract the metadata from .deb and .gem archives, instead of their normal
+ contents.
+
+-q, --quiet
+ Suppress warning messages. Listing this option twice will cause dtrx to
+ be silent.
+
+-v, --verbose
+ Show the files that are being extracted. Listing this option twice will
+ cause dtrx to print debugging information.
+
+--help
+ Display basic help.
+
+--version
+ Display dtrx's version, copyright, and license information.
+
+Other Useful Information
+------------------------
+
+dtrx 6.4 is copyright ⓒ 2006, 2007, 2008 `Brett Smith`_ and others. Feel
+free to send comments, bug reports, patches, and so on. You can find the
+latest version of dtrx on `its home page`_.
+
+.. _`Brett Smith`: mailto:brettcsmith@brettcsmith.org
+.. _`its home page`: http://www.brettcsmith.org/2007/dtrx/
+
+dtrx is free software; you can redistribute it and/or modify it under the
+terms of the GNU General Public License as published by the Free Software
+Foundation; either version 3 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, see <http://www.gnu.org/licenses/>.
diff --git a/scripts/dtrx b/scripts/dtrx
new file mode 100755
index 0000000..70e7965
--- /dev/null
+++ b/scripts/dtrx
@@ -0,0 +1,1210 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# dtrx -- Intelligently extract various archive types.
+# Copyright ⓒ 2006, 2007, 2008 Brett Smith <brettcsmith@brettcsmith.org>
+# Copyright ⓒ 2008 Peter Kelemen <Peter.Kelemen@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
+
+# Python 2.3 string methods: 'rfind', 'rindex', 'rjust', 'rstrip'
+
+import errno
+import logging
+import mimetypes
+import optparse
+import os
+import re
+import shutil
+import signal
+import stat
+import subprocess
+import sys
+import tempfile
+import textwrap
+import traceback
+
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+VERSION = "6.4"
+VERSION_BANNER = """dtrx version %s
+Copyright ⓒ 2006, 2007, 2008 Brett Smith <brettcsmith@brettcsmith.org>
+Copyright ⓒ 2008 Peter Kelemen <Peter.Kelemen@gmail.com>
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation; either version 3 of the License, or (at your
+option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+Public License for more details.""" % (VERSION,)
+
+MATCHING_DIRECTORY = 1
+ONE_ENTRY_KNOWN = 2
+BOMB = 3
+EMPTY = 4
+ONE_ENTRY_FILE = 'file'
+ONE_ENTRY_DIRECTORY = 'directory'
+
+ONE_ENTRY_UNKNOWN = [ONE_ENTRY_FILE, ONE_ENTRY_DIRECTORY]
+
+EXTRACT_HERE = 1
+EXTRACT_WRAP = 2
+EXTRACT_RENAME = 3
+
+RECURSE_ALWAYS = 1
+RECURSE_ONCE = 2
+RECURSE_NOT_NOW = 3
+RECURSE_NEVER = 4
+RECURSE_LIST = 5
+
+mimetypes.encodings_map.setdefault('.bz2', 'bzip2')
+mimetypes.encodings_map.setdefault('.lzma', 'lzma')
+mimetypes.types_map.setdefault('.gem', 'application/x-ruby-gem')
+
+logger = logging.getLogger('dtrx-log')
+
+class FilenameChecker(object):
+ free_func = os.open
+ free_args = (os.O_CREAT | os.O_EXCL,)
+ free_close = os.close
+
+ def __init__(self, original_name):
+ self.original_name = original_name
+
+ def is_free(self, filename):
+ try:
+ result = self.free_func(filename, *self.free_args)
+ except OSError, error:
+ if error.errno == errno.EEXIST:
+ return False
+ raise
+ if self.free_close:
+ self.free_close(result)
+ return True
+
+ def create(self):
+ fd, filename = tempfile.mkstemp(prefix=self.original_name + '.',
+ dir='.')
+ os.close(fd)
+ return filename
+
+ def check(self):
+ for suffix in [''] + ['.%s' % (x,) for x in range(1, 10)]:
+ filename = '%s%s' % (self.original_name, suffix)
+ if self.is_free(filename):
+ return filename
+ return self.create()
+
+
+class DirectoryChecker(FilenameChecker):
+ free_func = os.mkdir
+ free_args = ()
+ free_close = None
+
+ def create(self):
+ return tempfile.mkdtemp(prefix=self.original_name + '.', dir='.')
+
+
+class ExtractorError(Exception):
+ pass
+
+
+class ExtractorUnusable(Exception):
+ pass
+
+
+EXTRACTION_ERRORS = (ExtractorError, ExtractorUnusable, OSError, IOError)
+
+class BaseExtractor(object):
+ decoders = {'bzip2': 'bzcat', 'gzip': 'zcat', 'compress': 'zcat',
+ 'lzma': 'lzcat'}
+ name_checker = DirectoryChecker
+
+ def __init__(self, filename, encoding):
+ if encoding and (not self.decoders.has_key(encoding)):
+ raise ValueError("unrecognized encoding %s" % (encoding,))
+ self.filename = os.path.realpath(filename)
+ self.encoding = encoding
+ self.file_count = 0
+ self.included_archives = []
+ self.target = None
+ self.content_type = None
+ self.content_name = None
+ self.pipes = []
+ self.stderr = tempfile.TemporaryFile()
+ self.exit_codes = []
+ try:
+ self.archive = open(filename, 'r')
+ except (IOError, OSError), error:
+ raise ExtractorError("could not open %s: %s" %
+ (filename, error.strerror))
+ if encoding:
+ self.pipe([self.decoders[encoding]], "decoding")
+ self.prepare()
+
+ def pipe(self, command, description="extraction"):
+ self.pipes.append((command, description))
+
+ def first_bad_exit_code(self):
+ for index, code in enumerate(self.exit_codes):
+ if code != 0:
+ return index
+ return None
+
+ def run_pipes(self, final_stdout=None):
+ if not self.pipes:
+ return
+ elif final_stdout is None:
+ # FIXME: Buffering this might be dumb.
+ final_stdout = tempfile.TemporaryFile()
+ num_pipes = len(self.pipes)
+ last_pipe = num_pipes - 1
+ processes = []
+ for index, command in enumerate([pipe[0] for pipe in self.pipes]):
+ if index == 0:
+ stdin = self.archive
+ else:
+ stdin = processes[-1].stdout
+ if index == last_pipe:
+ stdout = final_stdout
+ else:
+ stdout = subprocess.PIPE
+ try:
+ processes.append(subprocess.Popen(command, stdin=stdin,
+ stdout=stdout,
+ stderr=self.stderr))
+ except OSError, error:
+ if error.errno == errno.ENOENT:
+ raise ExtractorUnusable("could not run %s" % (command[0],))
+ raise
+ self.exit_codes = [pipe.wait() for pipe in processes]
+ self.archive.close()
+ for index in range(last_pipe):
+ processes[index].stdout.close()
+ self.archive = final_stdout
+
+ def prepare(self):
+ pass
+
+ def check_included_archives(self):
+ if (self.content_name is None) or (not self.content_name.endswith('/')):
+ self.included_root = './'
+ else:
+ self.included_root = self.content_name
+ start_index = len(self.included_root)
+ for path, dirname, filenames in os.walk(self.included_root):
+ self.file_count += len(filenames)
+ path = path[start_index:]
+ for filename in filenames:
+ if (ExtractorBuilder.try_by_mimetype(filename) or
+ ExtractorBuilder.try_by_extension(filename)):
+ self.included_archives.append(os.path.join(path, filename))
+
+ def check_contents(self):
+ if not self.contents:
+ self.content_type = EMPTY
+ elif len(self.contents) == 1:
+ if self.basename() == self.contents[0]:
+ self.content_type = MATCHING_DIRECTORY
+ elif os.path.isdir(self.contents[0]):
+ self.content_type = ONE_ENTRY_DIRECTORY
+ else:
+ self.content_type = ONE_ENTRY_FILE
+ self.content_name = self.contents[0]
+ if os.path.isdir(self.contents[0]):
+ self.content_name += '/'
+ else:
+ self.content_type = BOMB
+ self.check_included_archives()
+
+ def basename(self):
+ pieces = os.path.basename(self.filename).split('.')
+ extension = '.' + pieces[-1]
+ if mimetypes.encodings_map.has_key(extension):
+ pieces.pop()
+ extension = '.' + pieces[-1]
+ if (mimetypes.types_map.has_key(extension) or
+ mimetypes.common_types.has_key(extension) or
+ mimetypes.suffix_map.has_key(extension)):
+ pieces.pop()
+ return '.'.join(pieces)
+
+ def get_stderr(self):
+ self.stderr.seek(0, 0)
+ errors = self.stderr.read(-1)
+ self.stderr.close()
+ return errors
+
+ def check_success(self, got_output):
+ error_index = self.first_bad_exit_code()
+ if (not got_output) and (error_index is not None):
+ command = ' '.join(self.pipes[error_index][0])
+ raise ExtractorError("%s error: '%s' returned status code %s" %
+ (self.pipes[error_index][1], command,
+ self.exit_codes[error_index]))
+
+ def extract_archive(self):
+ self.pipe(self.extract_pipe)
+ self.run_pipes()
+
+ def extract(self):
+ try:
+ self.target = tempfile.mkdtemp(prefix='.dtrx-', dir='.')
+ except (OSError, IOError), error:
+ raise ExtractorError("cannot extract here: %s" % (error.strerror,))
+ old_path = os.path.realpath(os.curdir)
+ os.chdir(self.target)
+ try:
+ self.archive.seek(0, 0)
+ self.extract_archive()
+ self.contents = os.listdir('.')
+ self.check_contents()
+ self.check_success(self.content_type != EMPTY)
+ except EXTRACTION_ERRORS:
+ self.archive.close()
+ os.chdir(old_path)
+ shutil.rmtree(self.target, ignore_errors=True)
+ raise
+ self.archive.close()
+ os.chdir(old_path)
+
+ def get_filenames(self):
+ self.pipe(self.list_pipe, "listing")
+ self.run_pipes()
+ self.check_success(False)
+ self.archive.seek(0, 0)
+ while True:
+ line = self.archive.readline()
+ if not line:
+ self.archive.close()
+ return
+ yield line.rstrip('\n')
+
+
+class CompressionExtractor(BaseExtractor):
+ file_type = 'compressed file'
+ name_checker = FilenameChecker
+
+ def basename(self):
+ pieces = os.path.basename(self.filename).split('.')
+ extension = '.' + pieces[-1]
+ if mimetypes.encodings_map.has_key(extension):
+ pieces.pop()
+ return '.'.join(pieces)
+
+ def get_filenames(self):
+ # This code used to just immediately yield the basename, under the
+ # assumption that that would be the filename. However, if that
+ # happens, dtrx -l will report this as a valid result for files with
+ # compression extensions, even if those files shouldn't actually be
+ # handled this way. So, we call out to the file command to do a quick
+ # check and make sure this actually looks like a compressed file.
+ if 'compress' not in [match[0] for match in
+ ExtractorBuilder.try_by_magic(self.filename)]:
+ raise ExtractorError("doesn't look like a compressed file")
+ yield self.basename()
+
+ def extract(self):
+ self.content_type = ONE_ENTRY_KNOWN
+ self.content_name = self.basename()
+ self.contents = None
+ self.included_root = './'
+ try:
+ output_fd, self.target = tempfile.mkstemp(prefix='.dtrx-', dir='.')
+ except (OSError, IOError), error:
+ raise ExtractorError("cannot extract here: %s" % (error.strerror,))
+ self.run_pipes(output_fd)
+ os.close(output_fd)
+ try:
+ self.check_success(os.stat(self.target)[stat.ST_SIZE] > 0)
+ except EXTRACTION_ERRORS:
+ os.unlink(self.target)
+ raise
+
+class TarExtractor(BaseExtractor):
+ file_type = 'tar file'
+ extract_pipe = ['tar', '-x']
+ list_pipe = ['tar', '-t']
+
+
+class CpioExtractor(BaseExtractor):
+ file_type = 'cpio file'
+ extract_pipe = ['cpio', '-i', '--make-directories', '--quiet',
+ '--no-absolute-filenames']
+ list_pipe = ['cpio', '-t', '--quiet']
+
+
+class RPMExtractor(CpioExtractor):
+ file_type = 'RPM'
+
+ def prepare(self):
+ self.pipe(['rpm2cpio', '-'], "rpm2cpio")
+
+ def basename(self):
+ pieces = os.path.basename(self.filename).split('.')
+ if len(pieces) == 1:
+ return pieces[0]
+ elif pieces[-1] != 'rpm':
+ return BaseExtractor.basename(self)
+ pieces.pop()
+ if len(pieces) == 1:
+ return pieces[0]
+ elif len(pieces[-1]) < 8:
+ pieces.pop()
+ return '.'.join(pieces)
+
+ def check_contents(self):
+ self.check_included_archives()
+ self.content_type = BOMB
+
+
+class DebExtractor(TarExtractor):
+ file_type = 'Debian package'
+
+ def prepare(self):
+ self.pipe(['ar', 'p', self.filename, 'data.tar.gz'],
+ "data.tar.gz extraction")
+ self.pipe(['zcat'], "data.tar.gz decompression")
+
+ def basename(self):
+ pieces = os.path.basename(self.filename).split('_')
+ if len(pieces) == 1:
+ return pieces[0]
+ last_piece = pieces.pop()
+ if (len(last_piece) > 10) or (not last_piece.endswith('.deb')):
+ return BaseExtractor.basename(self)
+ return '_'.join(pieces)
+
+ def check_contents(self):
+ self.check_included_archives()
+ self.content_type = BOMB
+
+
+class DebMetadataExtractor(DebExtractor):
+ def prepare(self):
+ self.pipe(['ar', 'p', self.filename, 'control.tar.gz'],
+ "control.tar.gz extraction")
+ self.pipe(['zcat'], "control.tar.gz decompression")
+
+
+class GemExtractor(TarExtractor):
+ file_type = 'Ruby gem'
+
+ def prepare(self):
+ self.pipe(['tar', '-xO', 'data.tar.gz'], "data.tar.gz extraction")
+ self.pipe(['zcat'], "data.tar.gz decompression")
+
+ def check_contents(self):
+ self.check_included_archives()
+ self.content_type = BOMB
+
+
+class GemMetadataExtractor(CompressionExtractor):
+ file_type = 'Ruby gem'
+
+ def prepare(self):
+ self.pipe(['tar', '-xO', 'metadata.gz'], "metadata.gz extraction")
+ self.pipe(['zcat'], "metadata.gz decompression")
+
+ def basename(self):
+ return os.path.basename(self.filename) + '-metadata.txt'
+
+
+class NoPipeExtractor(BaseExtractor):
+ # Some extraction tools won't accept the archive from stdin. With
+ # these, the piping infrastructure we normally set up generally doesn't
+ # work, at least at first. We can still use most of it; we just don't
+ # want to seed self.archive with the archive file, since that sucks up
+ # memory. So instead we seed it with /dev/null, and specify the
+ # filename on the command line as necessary. We also open the actual
+ # file with os.open, to make sure we can actually do it (permissions
+ # are good, etc.). This class doesn't do anything by itself; it's just
+ # meant to be a base class for extractors that rely on these dumb
+ # tools.
+ def __init__(self, filename, encoding):
+ os.close(os.open(filename, os.O_RDONLY))
+ BaseExtractor.__init__(self, '/dev/null', None)
+ self.filename = os.path.realpath(filename)
+
+ def extract_archive(self):
+ self.extract_pipe = self.extract_command + [self.filename]
+ BaseExtractor.extract_archive(self)
+
+ def get_filenames(self):
+ self.list_pipe = self.list_command + [self.filename]
+ return BaseExtractor.get_filenames(self)
+
+
+class ZipExtractor(NoPipeExtractor):
+ file_type = 'Zip file'
+ extract_command = ['unzip', '-q']
+ list_command = ['zipinfo', '-1']
+
+
+class SevenExtractor(NoPipeExtractor):
+ file_type = '7z file'
+ extract_command = ['7z', 'x']
+ list_command = ['7z', 'l']
+ border_re = re.compile('^[- ]+$')
+
+ def get_filenames(self):
+ fn_index = None
+ for line in NoPipeExtractor.get_filenames(self):
+ if self.border_re.match(line):
+ if fn_index is not None:
+ break
+ else:
+ fn_index = line.rindex(' ') + 1
+ elif fn_index is not None:
+ yield line[fn_index:]
+ self.archive.close()
+
+
+class CABExtractor(NoPipeExtractor):
+ file_type = 'CAB archive'
+ extract_command = ['cabextract', '-q']
+ list_command = ['cabextract', '-l']
+ border_re = re.compile(r'^[-\+]+$')
+
+ def get_filenames(self):
+ fn_index = None
+ filenames = NoPipeExtractor.get_filenames(self)
+ for line in filenames:
+ if self.border_re.match(line):
+ break
+ for line in filenames:
+ try:
+ yield line.split(' | ', 2)[2]
+ except IndexError:
+ break
+ self.archive.close()
+
+
+class ShieldExtractor(NoPipeExtractor):
+ file_type = 'InstallShield archive'
+ extract_command = ['unshield', 'x']
+ list_command = ['unshield', 'l']
+ prefix_re = re.compile(r'^\s+\d+\s+')
+ end_re = re.compile(r'^\s+-+\s+-+\s*$')
+
+ def get_filenames(self):
+ for line in NoPipeExtractor.get_filenames(self):
+ if self.end_re.match(line):
+ break
+ else:
+ match = self.prefix_re.match(line)
+ if match:
+ yield line[match.end():]
+ self.archive.close()
+
+ def basename(self):
+ result = NoPipeExtractor.basename(self)
+ if result.endswith('.hdr'):
+ result = result[:-4]
+ return result
+
+
+class RarExtractor(NoPipeExtractor):
+ file_type = 'RAR archive'
+ extract_command = ['unrar', 'x']
+ list_command = ['unrar', 'l']
+ border_re = re.compile('^-+$')
+
+ def get_filenames(self):
+ inside = False
+ for line in NoPipeExtractor.get_filenames(self):
+ if self.border_re.match(line):
+ if inside:
+ break
+ else:
+ inside = True
+ elif inside:
+ yield line.split(' ')[1]
+ self.archive.close()
+
+
+class BaseHandler(object):
+ def __init__(self, extractor, options):
+ self.extractor = extractor
+ self.options = options
+ self.target = None
+
+ def handle(self):
+ command = 'find'
+ status = subprocess.call(['find', self.extractor.target, '-type', 'd',
+ '-exec', 'chmod', 'u+rwx', '{}', ';'])
+ if status == 0:
+ command = 'chmod'
+ status = subprocess.call(['chmod', '-R', 'u+rwX',
+ self.extractor.target])
+ if status != 0:
+ return "%s returned with exit status %s" % (command, status)
+ return self.organize()
+
+ def set_target(self, target, checker):
+ self.target = checker(target).check()
+ if self.target != target:
+ logger.warning("extracting %s to %s" %
+ (self.extractor.filename, self.target))
+
+
+# The "where to extract" table, with options and archive types.
+# This dictates the contents of each can_handle method.
+#
+# Flat Overwrite None
+# File basename basename FilenameChecked
+# Match . . tempdir + checked
+# Bomb . basename DirectoryChecked
+
+class FlatHandler(BaseHandler):
+ def can_handle(contents, options):
+ return ((options.flat and (contents != ONE_ENTRY_KNOWN)) or
+ (options.overwrite and (contents == MATCHING_DIRECTORY)))
+ can_handle = staticmethod(can_handle)
+
+ def organize(self):
+ self.target = '.'
+ for curdir, dirs, filenames in os.walk(self.extractor.target,
+ topdown=False):
+ path_parts = curdir.split(os.sep)
+ if path_parts[0] == '.':
+ del path_parts[1]
+ else:
+ del path_parts[0]
+ newdir = os.path.join(*path_parts)
+ if not os.path.isdir(newdir):
+ os.makedirs(newdir)
+ for filename in filenames:
+ os.rename(os.path.join(curdir, filename),
+ os.path.join(newdir, filename))
+ os.rmdir(curdir)
+
+
+class OverwriteHandler(BaseHandler):
+ def can_handle(contents, options):
+ return ((options.flat and (contents == ONE_ENTRY_KNOWN)) or
+ (options.overwrite and (contents != MATCHING_DIRECTORY)))
+ can_handle = staticmethod(can_handle)
+
+ def organize(self):
+ self.target = self.extractor.basename()
+ if os.path.isdir(self.target):
+ shutil.rmtree(self.target)
+ os.rename(self.extractor.target, self.target)
+
+
+class MatchHandler(BaseHandler):
+ def can_handle(contents, options):
+ return ((contents == MATCHING_DIRECTORY) or
+ ((contents in ONE_ENTRY_UNKNOWN) and
+ options.one_entry_policy.ok_for_match()))
+ can_handle = staticmethod(can_handle)
+
+ def organize(self):
+ source = os.path.join(self.extractor.target,
+ os.listdir(self.extractor.target)[0])
+ if os.path.isdir(source):
+ checker = DirectoryChecker
+ else:
+ checker = FilenameChecker
+ if self.options.one_entry_policy == EXTRACT_HERE:
+ destination = self.extractor.content_name.rstrip('/')
+ else:
+ destination = self.extractor.basename()
+ self.set_target(destination, checker)
+ if os.path.isdir(self.extractor.target):
+ os.rename(source, self.target)
+ os.rmdir(self.extractor.target)
+ else:
+ os.rename(self.extractor.target, self.target)
+ self.extractor.included_root = './'
+
+
+class EmptyHandler(object):
+ target = ''
+
+ def can_handle(contents, options):
+ return contents == EMPTY
+ can_handle = staticmethod(can_handle)
+
+ def __init__(self, extractor, options): pass
+ def handle(self): pass
+
+
+class BombHandler(BaseHandler):
+ def can_handle(contents, options):
+ return True
+ can_handle = staticmethod(can_handle)
+
+ def organize(self):
+ basename = self.extractor.basename()
+ self.set_target(basename, self.extractor.name_checker)
+ os.rename(self.extractor.target, self.target)
+
+
+class BasePolicy(object):
+ try:
+ width = int(os.environ['COLUMNS'])
+ except (KeyError, ValueError):
+ width = 80
+ wrapper = textwrap.TextWrapper(width=width - 1)
+
+ def __init__(self, options):
+ self.current_policy = None
+ if options.batch:
+ self.permanent_policy = self.answers['']
+ else:
+ self.permanent_policy = None
+
+ def wrap(self, question, filename):
+ # Note: This function assumes the filename is the first thing in the
+ # question text, and that's the only place it appears.
+ if len(self.wrapper.wrap(filename + ' a')) > 1:
+ return [filename] + self.wrapper.wrap(question[3:])
+ return self.wrapper.wrap(question % (filename,))
+
+ def ask_question(self, question):
+ question = question + self.choices
+ while True:
+ print "\n".join(question)
+ try:
+ answer = raw_input(self.prompt)
+ except EOFError:
+ return self.answers['']
+ try:
+ return self.answers[answer.lower()]
+ except KeyError:
+ print
+
+ def __cmp__(self, other):
+ return cmp(self.current_policy, other)
+
+
+class OneEntryPolicy(BasePolicy):
+ answers = {'h': EXTRACT_HERE, 'i': EXTRACT_WRAP, 'r': EXTRACT_RENAME,
+ '': EXTRACT_WRAP}
+ choices = ["You can:",
+ " * extract it Inside another directory",
+ " * extract it and Rename the directory",
+ " * extract it Here"]
+ prompt = "What do you want to do? (I/r/h) "
+
+ def __init__(self, options):
+ BasePolicy.__init__(self, options)
+ if options.flat:
+ default = 'h'
+ elif options.one_entry_default is not None:
+ default = options.one_entry_default.lower()
+ else:
+ return
+ if 'here'.startswith(default):
+ self.permanent_policy = EXTRACT_HERE
+ elif 'rename'.startswith(default):
+ self.permanent_policy = EXTRACT_RENAME
+ elif 'inside'.startswith(default):
+ self.permanent_policy = EXTRACT_WRAP
+ elif default is not None:
+ raise ValueError("bad value %s for default policy" % (default,))
+
+ def prep(self, archive_filename, extractor):
+ question = self.wrap(("%%s contains one %s, but its name " +
+ "doesn't match.") %
+ (extractor.content_type,), archive_filename)
+ question.append(" Expected: " + extractor.basename())
+ question.append(" Actual: " + extractor.content_name)
+ self.current_policy = (self.permanent_policy or
+ self.ask_question(question))
+
+ def ok_for_match(self):
+ return self.current_policy in (EXTRACT_RENAME, EXTRACT_HERE)
+
+
+class RecursionPolicy(BasePolicy):
+ answers = {'o': RECURSE_ONCE, 'a': RECURSE_ALWAYS, 'n': RECURSE_NOT_NOW,
+ 'v': RECURSE_NEVER, 'l': RECURSE_LIST, '': RECURSE_NOT_NOW}
+ choices = ["You can:",
+ " * Always extract included archives",
+ " * extract included archives this Once",
+ " * choose Not to extract included archives",
+ " * neVer extract included archives",
+ " * List included archives"]
+ prompt = "What do you want to do? (a/o/N/v/l) "
+
+ def __init__(self, options):
+ BasePolicy.__init__(self, options)
+ if options.show_list:
+ self.permanent_policy = RECURSE_NEVER
+ elif options.recursive:
+ self.permanent_policy = RECURSE_ALWAYS
+
+ def prep(self, current_filename, target, extractor):
+ archive_count = len(extractor.included_archives)
+ if (self.permanent_policy is not None) or (archive_count == 0):
+ self.current_policy = self.permanent_policy or RECURSE_NOT_NOW
+ return
+ question = self.wrap(("%%s contains %s other archive file(s), " +
+ "out of %s file(s) total.") %
+ (archive_count, extractor.file_count),
+ current_filename)
+ if target == '.':
+ target = ''
+ included_root = extractor.included_root
+ if included_root == './':
+ included_root = ''
+ while True:
+ self.current_policy = self.ask_question(question)
+ if self.current_policy != RECURSE_LIST:
+ break
+ print ("\n%s\n" %
+ '\n'.join([os.path.join(target, included_root, filename)
+ for filename in extractor.included_archives]))
+ if self.current_policy in (RECURSE_ALWAYS, RECURSE_NEVER):
+ self.permanent_policy = self.current_policy
+
+ def ok_to_recurse(self):
+ return self.current_policy in (RECURSE_ALWAYS, RECURSE_ONCE)
+
+
+class ExtractorBuilder(object):
+ extractor_map = {'tar': {'extractor': TarExtractor,
+ 'mimetypes': ('x-tar',),
+ 'extensions': ('tar',),
+ 'magic': ('POSIX tar archive',)},
+ 'zip': {'extractor': ZipExtractor,
+ 'mimetypes': ('zip',),
+ 'extensions': ('zip',),
+ 'magic': ('(Zip|ZIP self-extracting) archive',)},
+ 'rpm': {'extractor': RPMExtractor,
+ 'mimetypes': ('x-redhat-package-manager', 'x-rpm'),
+ 'extensions': ('rpm',),
+ 'magic': ('RPM',)},
+ 'deb': {'extractor': DebExtractor,
+ 'metadata': DebMetadataExtractor,
+ 'mimetypes': ('x-debian-package',),
+ 'extensions': ('deb',),
+ 'magic': ('Debian binary package',)},
+ 'cpio': {'extractor': CpioExtractor,
+ 'mimetypes': ('x-cpio',),
+ 'extensions': ('cpio',),
+ 'magic': ('cpio archive',)},
+ 'gem': {'extractor': GemExtractor,
+ 'metadata': GemMetadataExtractor,
+ 'mimetypes': ('x-ruby-gem',),
+ 'extensions': ('gem',)},
+ '7z': {'extractor': SevenExtractor,
+ 'mimetypes': ('x-7z-compressed',),
+ 'extensions': ('7z',),
+ 'magic': ('7-zip archive',)},
+ 'cab': {'extractor': CABExtractor,
+ 'mimetypes': ('x-cab',),
+ 'extensions': ('cab',),
+ 'magic': ('Microsoft Cabinet Archive',)},
+ 'rar': {'extractor': RarExtractor,
+ 'mimetypes': ('rar',),
+ 'extensions': ('rar',),
+ 'magic': ('RAR archive',)},
+ 'shield': {'extractor': ShieldExtractor,
+ 'mimetypes': ('x-cab',),
+ 'extensions': ('cab', 'hdr'),
+ 'magic': ('InstallShield CAB',)},
+ 'compress': {'extractor': CompressionExtractor}
+ }
+
+ mimetype_map = {}
+ magic_mime_map = {}
+ extension_map = {}
+ for ext_name, ext_info in extractor_map.items():
+ for mimetype in ext_info.get('mimetypes', ()):
+ if '/' not in mimetype:
+ mimetype = 'application/' + mimetype
+ mimetype_map[mimetype] = ext_name
+ for magic_re in ext_info.get('magic', ()):
+ magic_mime_map[re.compile(magic_re)] = ext_name
+ for extension in ext_info.get('extensions', ()):
+ extension_map.setdefault(extension, []).append((ext_name, None))
+
+ for mapping in (('tar', 'bzip2', 'tar.bz2'),
+ ('tar', 'gzip', 'tar.gz', 'tgz'),
+ ('compress', 'gzip', 'Z', 'gz'),
+ ('compress', 'bzip2', 'bz2'),
+ ('compress', 'lzma', 'lzma')):
+ for extension in mapping[2:]:
+ extension_map.setdefault(extension, []).append(mapping[:2])
+
+ magic_encoding_map = {}
+ for mapping in (('bzip2', 'bzip2 compressed'),
+ ('gzip', 'gzip compressed'),
+ ('lzma', 'LZMA compressed')):
+ for pattern in mapping[1:]:
+ magic_encoding_map[re.compile(pattern)] = mapping[0]
+
+ def __init__(self, filename, options):
+ self.filename = filename
+ self.options = options
+
+ def build_extractor(self, archive_type, encoding):
+ extractors = self.extractor_map[archive_type]
+ if self.options.metadata and extractors.has_key('metadata'):
+ extractor = extractors['metadata']
+ else:
+ extractor = extractors['extractor']
+ return extractor(self.filename, encoding)
+
+ def get_extractor(self):
+ tried_types = set()
+ # As smart as it is, the magic test can't go first, because at least
+ # on my system it just recognizes gem files as tar files. I guess
+ # it's possible for the opposite problem to occur -- where the mimetype
+ # or extension suggests something less than ideal -- but it seems less
+ # likely so I'm sticking with this.
+ for func_name in ('mimetype', 'extension', 'magic'):
+ logger.debug("getting extractors by %s" % (func_name,))
+ extractor_types = \
+ getattr(self, 'try_by_' + func_name)(self.filename)
+ logger.debug("done getting extractors")
+ for ext_args in extractor_types:
+ if ext_args in tried_types:
+ continue
+ tried_types.add(ext_args)
+ logger.debug("trying %s extractor from %s" %
+ (ext_args, func_name))
+ yield self.build_extractor(*ext_args)
+
+ def try_by_mimetype(cls, filename):
+ mimetype, encoding = mimetypes.guess_type(filename)
+ try:
+ return [(cls.mimetype_map[mimetype], encoding)]
+ except KeyError:
+ if encoding:
+ return [('compress', encoding)]
+ return []
+ try_by_mimetype = classmethod(try_by_mimetype)
+
+ def magic_map_matches(cls, output, magic_map):
+ return [result for regexp, result in magic_map.items()
+ if regexp.search(output)]
+ magic_map_matches = classmethod(magic_map_matches)
+
+ def try_by_magic(cls, filename):
+ process = subprocess.Popen(['file', '-z', filename],
+ stdout=subprocess.PIPE)
+ status = process.wait()
+ if status != 0:
+ return []
+ output = process.stdout.readline()
+ process.stdout.close()
+ if output.startswith('%s: ' % filename):
+ output = output[len(filename) + 2:]
+ mimes = cls.magic_map_matches(output, cls.magic_mime_map)
+ encodings = cls.magic_map_matches(output, cls.magic_encoding_map)
+ if mimes and not encodings:
+ encodings = [None]
+ elif encodings and not mimes:
+ mimes = ['compress']
+ return [(m, e) for m in mimes for e in encodings]
+ try_by_magic = classmethod(try_by_magic)
+
+ def try_by_extension(cls, filename):
+ parts = filename.split('.')[-2:]
+ results = []
+ while parts:
+ results.extend(cls.extension_map.get('.'.join(parts), []))
+ del parts[0]
+ return results
+ try_by_extension = classmethod(try_by_extension)
+
+
+class BaseAction(object):
+ def __init__(self, options, filenames):
+ self.options = options
+ self.filenames = filenames
+ self.target = None
+
+ def report(self, function, *args):
+ try:
+ error = function(*args)
+ except EXTRACTION_ERRORS, exception:
+ error = str(exception)
+ logger.debug(''.join(traceback.format_exception(*sys.exc_info())))
+ return error
+
+
+class ExtractionAction(BaseAction):
+ handlers = [FlatHandler, OverwriteHandler, MatchHandler, EmptyHandler,
+ BombHandler]
+
+ def __init__(self, options, filenames):
+ BaseAction.__init__(self, options, filenames)
+ self.did_print = False
+
+ def get_handler(self, extractor):
+ if extractor.content_type in ONE_ENTRY_UNKNOWN:
+ self.options.one_entry_policy.prep(self.current_filename,
+ extractor)
+ for handler in self.handlers:
+ if handler.can_handle(extractor.content_type, self.options):
+ logger.debug("using %s handler" % (handler.__name__,))
+ self.current_handler = handler(extractor, self.options)
+ break
+
+ def show_extraction(self, extractor):
+ if self.options.log_level > logging.INFO:
+ return
+ elif self.did_print:
+ print
+ else:
+ self.did_print = True
+ print "%s:" % (self.current_filename,)
+ if extractor.contents is None:
+ print self.current_handler.target
+ return
+ def reverser(x, y):
+ return cmp(y, x)
+ if self.current_handler.target == '.':
+ filenames = extractor.contents
+ filenames.sort(reverser)
+ else:
+ filenames = [self.current_handler.target]
+ pathjoin = os.path.join
+ isdir = os.path.isdir
+ while filenames:
+ filename = filenames.pop()
+ if isdir(filename):
+ print "%s/" % (filename,)
+ new_filenames = os.listdir(filename)
+ new_filenames.sort(reverser)
+ filenames.extend([pathjoin(filename, new_filename)
+ for new_filename in new_filenames])
+ else:
+ print filename
+
+ def run(self, filename, extractor):
+ self.current_filename = filename
+ error = (self.report(extractor.extract) or
+ self.report(self.get_handler, extractor) or
+ self.report(self.current_handler.handle) or
+ self.report(self.show_extraction, extractor))
+ if not error:
+ self.target = self.current_handler.target
+ return error
+
+
+class ListAction(BaseAction):
+ def __init__(self, options, filenames):
+ BaseAction.__init__(self, options, filenames)
+ self.count = 0
+
+ def get_list(self, extractor):
+ # Note: The reason I'm getting all the filenames up front is
+ # because if we run into trouble partway through the archive, we'll
+ # try another extractor. So before we display anything we have to
+ # be sure this one is successful. We maybe don't have to be quite
+ # this conservative but this is the easy way out for now.
+ self.filelist = list(extractor.get_filenames())
+
+ def show_list(self, filename):
+ self.count += 1
+ if len(self.filenames) != 1:
+ if self.count > 1:
+ print
+ print "%s:" % (filename,)
+ print '\n'.join(self.filelist)
+
+ def run(self, filename, extractor):
+ return (self.report(self.get_list, extractor) or
+ self.report(self.show_list, filename))
+
+
+class ExtractorApplication(object):
+ def __init__(self, arguments):
+ for signal_num in (signal.SIGINT, signal.SIGTERM):
+ signal.signal(signal_num, self.abort)
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+ self.parse_options(arguments)
+ self.setup_logger()
+ self.successes = []
+ self.failures = []
+
+ def clean_destination(self, dest_name):
+ try:
+ os.unlink(dest_name)
+ except OSError, error:
+ if error.errno == errno.EISDIR:
+ shutil.rmtree(dest_name, ignore_errors=True)
+
+ def abort(self, signal_num, frame):
+ signal.signal(signal_num, signal.SIG_IGN)
+ print
+ logger.debug("traceback:\n" +
+ ''.join(traceback.format_stack(frame)).rstrip())
+ logger.debug("got signal %s" % (signal_num,))
+ try:
+ basename = self.current_extractor.target
+ except AttributeError:
+ basename = None
+ if basename is not None:
+ logger.debug("cleaning up %s" % (basename,))
+ clean_targets = set([os.path.realpath('.')])
+ if hasattr(self, 'current_directory'):
+ clean_targets.add(os.path.realpath(self.current_directory))
+ for directory in clean_targets:
+ self.clean_destination(os.path.join(directory, basename))
+ sys.exit(1)
+
+ def parse_options(self, arguments):
+ parser = optparse.OptionParser(
+ usage="%prog [options] archive [archive2 ...]",
+ description="Intelligent archive extractor",
+ version=VERSION_BANNER
+ )
+ parser.add_option('-l', '-t', '--list', '--table', dest='show_list',
+ action='store_true', default=False,
+ help="list contents of archives on standard output")
+ parser.add_option('-m', '--metadata', dest='metadata',
+ action='store_true', default=False,
+ help="extract metadata from a .deb/.gem")
+ parser.add_option('-r', '--recursive', dest='recursive',
+ action='store_true', default=False,
+ help="extract archives contained in the ones listed")
+ parser.add_option('--one', '--one-entry', dest='one_entry_default',
+ default=None,
+ help=("specify extraction policy for one-entry " +
+ "archives: inside/rename/here"))
+ parser.add_option('-n', '--noninteractive', dest='batch',
+ action='store_true', default=False,
+ help="don't ask how to handle special cases")
+ parser.add_option('-o', '--overwrite', dest='overwrite',
+ action='store_true', default=False,
+ help="overwrite any existing target output")
+ parser.add_option('-f', '--flat', '--no-directory', dest='flat',
+ action='store_true', default=False,
+ help="extract everything to the current directory")
+ parser.add_option('-v', '--verbose', dest='verbose',
+ action='count', default=0,
+ help="be verbose/print debugging information")
+ parser.add_option('-q', '--quiet', dest='quiet',
+ action='count', default=3,
+ help="suppress warning/error messages")
+ self.options, filenames = parser.parse_args(arguments)
+ if not filenames:
+ parser.error("you did not list any archives")
+ # This makes WARNING is the default.
+ self.options.log_level = (10 * (self.options.quiet -
+ self.options.verbose))
+ try:
+ self.options.one_entry_policy = OneEntryPolicy(self.options)
+ except ValueError:
+ parser.error("invalid value for --one-entry option")
+ self.options.recursion_policy = RecursionPolicy(self.options)
+ self.archives = {os.path.realpath(os.curdir): filenames}
+
+ def setup_logger(self):
+ logging.getLogger().setLevel(self.options.log_level)
+ handler = logging.StreamHandler()
+ handler.setLevel(self.options.log_level)
+ formatter = logging.Formatter("dtrx: %(levelname)s: %(message)s")
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+ logger.debug("logger is set up")
+
+ def recurse(self, filename, extractor, action):
+ self.options.recursion_policy.prep(filename, action.target, extractor)
+ if self.options.recursion_policy.ok_to_recurse():
+ for filename in extractor.included_archives:
+ logger.debug("recursing with %s archive" %
+ (extractor.content_type,))
+ tail_path, basename = os.path.split(filename)
+ path_args = [self.current_directory, extractor.included_root,
+ tail_path]
+ logger.debug("included root: %s" % (extractor.included_root,))
+ logger.debug("tail path: %s" % (tail_path,))
+ if os.path.isdir(action.target):
+ logger.debug("action target: %s" % (action.target,))
+ path_args.insert(1, action.target)
+ directory = os.path.join(*path_args)
+ self.archives.setdefault(directory, []).append(basename)
+
+ def check_file(self, filename):
+ try:
+ result = os.stat(filename)
+ except OSError, error:
+ return error.strerror
+ if stat.S_ISDIR(result.st_mode):
+ return "cannot work with a directory"
+
+ def show_stderr(self, logger_func, stderr):
+ if stderr:
+ logger_func("Error output from this process:\n" +
+ stderr.rstrip('\n'))
+
+ def try_extractors(self, filename, builder):
+ errors = []
+ for extractor in builder:
+ self.current_extractor = extractor # For the abort() method.
+ error = self.action.run(filename, extractor)
+ if error:
+ errors.append((extractor.file_type, extractor.encoding, error,
+ extractor.get_stderr()))
+ if extractor.target is not None:
+ self.clean_destination(extractor.target)
+ else:
+ self.show_stderr(logger.warn, extractor.get_stderr())
+ self.recurse(filename, extractor, self.action)
+ return
+ logger.error("could not handle %s" % (filename,))
+ if not errors:
+ logger.error("not a known archive type")
+ return True
+ for file_type, encoding, error, stderr in errors:
+ message = ["treating as", file_type, "failed:", error]
+ if encoding:
+ message.insert(1, "%s-encoded" % (encoding,))
+ logger.error(' '.join(message))
+ self.show_stderr(logger.error, stderr)
+ return True
+
+ def run(self):
+ if self.options.show_list:
+ action = ListAction
+ else:
+ action = ExtractionAction
+ self.action = action(self.options, self.archives.values()[0])
+ while self.archives:
+ self.current_directory, self.filenames = self.archives.popitem()
+ os.chdir(self.current_directory)
+ for filename in self.filenames:
+ builder = ExtractorBuilder(filename, self.options)
+ error = (self.check_file(filename) or
+ self.try_extractors(filename, builder.get_extractor()))
+ if error:
+ if error != True:
+ logger.error("%s: %s" % (filename, error))
+ self.failures.append(filename)
+ else:
+ self.successes.append(filename)
+ self.options.one_entry_policy.permanent_policy = EXTRACT_WRAP
+ if self.failures:
+ return 1
+ return 0
+
+
+if __name__ == '__main__':
+ app = ExtractorApplication(sys.argv[1:])
+ sys.exit(app.run())
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..e6cdbe7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+
+setup(name="dtrx",
+ version = "6.4",
+ description = "Script to intelligently extract multiple archive types",
+ author = "Brett Smith",
+ author_email = "brettcsmith@brettcsmith.org",
+ url = "http://www.brettcsmith.org/2007/dtrx/",
+ scripts = ['scripts/dtrx'],
+ license = "GNU General Public License, version 3 or later"
+ )
diff --git a/tests/compare.py b/tests/compare.py
new file mode 100644
index 0000000..cdbcc5d
--- /dev/null
+++ b/tests/compare.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python
+#
+# compare.py -- High-level tests for dtrx.
+# Copyright (c) 2006, 2007, 2008 Brett Smith <brettcsmith@brettcsmith.org>.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import subprocess
+import syck
+import sys
+import tempfile
+
+from sets import Set as set
+
+if os.path.exists('scripts/dtrx') and os.path.exists('tests'):
+ os.chdir('tests')
+elif os.path.exists('../scripts/dtrx') and os.path.exists('../tests'):
+ pass
+else:
+ print "ERROR: Can't run tests in this directory!"
+ sys.exit(2)
+
+X_SCRIPT = os.path.realpath('../scripts/dtrx')
+ROOT_DIR = os.path.realpath(os.curdir)
+OUTCOMES = ['error', 'failed', 'passed']
+TESTSCRIPT_NAME = 'testscript.sh'
+SCRIPT_PROLOGUE = """#!/bin/sh
+set -e
+"""
+
+input_buffer = tempfile.TemporaryFile()
+output_buffer = tempfile.TemporaryFile()
+
+class ExtractorTestError(Exception):
+ pass
+
+
+class ExtractorTest(object):
+ def __init__(self, **kwargs):
+ setattr(self, 'name', kwargs['name'])
+ setattr(self, 'options', kwargs.get('options', '-n').split())
+ setattr(self, 'filenames', kwargs.get('filenames', '').split())
+ for key in ('directory', 'prerun', 'posttest', 'baseline', 'error',
+ 'grep', 'antigrep', 'input', 'output', 'cleanup'):
+ setattr(self, key, kwargs.get(key, None))
+
+ def get_results(self, commands, stdin=None):
+ print >>output_buffer, "Output from %s:" % (' '.join(commands),)
+ output_buffer.flush()
+ status = subprocess.call(commands, stdout=output_buffer,
+ stderr=output_buffer, stdin=stdin)
+ process = subprocess.Popen(['find', '!', '-name', TESTSCRIPT_NAME],
+ stdout=subprocess.PIPE)
+ process.wait()
+ output = process.stdout.read(-1)
+ process.stdout.close()
+ return status, set(output.split('\n'))
+
+ def write_script(self, commands):
+ script = open(TESTSCRIPT_NAME, 'w')
+ script.write("%s%s\n" % (SCRIPT_PROLOGUE, commands))
+ script.close()
+ subprocess.call(['chmod', 'u+w', TESTSCRIPT_NAME])
+
+ def run_script(self, key):
+ commands = getattr(self, key)
+ if commands is not None:
+ if self.directory:
+ directory_hint = '../'
+ else:
+ directory_hint = ''
+ self.write_script(commands)
+ subprocess.call(['sh', TESTSCRIPT_NAME, directory_hint])
+
+ def get_shell_results(self):
+ self.run_script('prerun')
+ self.write_script(self.baseline)
+ return self.get_results(['sh', TESTSCRIPT_NAME] + self.filenames)
+
+ def get_extractor_results(self):
+ self.run_script('prerun')
+ input_buffer.seek(0, 0)
+ input_buffer.truncate()
+ if self.input:
+ input_buffer.write(self.input)
+ if not self.input.endswith('\n'):
+ input_buffer.write('\n')
+ input_buffer.seek(0, 0)
+ input_buffer.flush()
+ return self.get_results([X_SCRIPT] + self.options + self.filenames,
+ input_buffer)
+
+ def get_posttest_result(self):
+ if not self.posttest:
+ return 0
+ self.write_script(self.posttest)
+ return subprocess.call(['sh', TESTSCRIPT_NAME])
+
+ def clean(self):
+ self.run_script('cleanup')
+ if self.directory:
+ target = os.path.join(ROOT_DIR, self.directory)
+ extra_options = ['!', '-name', TESTSCRIPT_NAME]
+ else:
+ target = ROOT_DIR
+ extra_options = ['(', '(', '-type', 'd',
+ '!', '-name', 'CVS',
+ '!', '-name', '.svn', ')',
+ '-or', '-name', 'test-text',
+ '-or', '-name', 'test-onefile', ')']
+ status = subprocess.call(['find', target,
+ '-mindepth', '1', '-maxdepth', '1'] +
+ extra_options +
+ ['-exec', 'rm', '-rf', '{}', ';'])
+ if status != 0:
+ raise ExtractorTestError("cleanup exited with status code %s" %
+ (status,))
+
+ def show_status(self, status, message=None):
+ raw_status = status.lower()
+ if raw_status != 'passed':
+ output_buffer.seek(0, 0)
+ sys.stdout.write(output_buffer.read(-1))
+ if message is None:
+ last_part = ''
+ else:
+ last_part = ': %s' % (message,)
+ print "%7s: %s%s" % (status, self.name, last_part)
+ return raw_status
+
+ def compare_results(self, actual):
+ posttest_result = self.get_posttest_result()
+ self.clean()
+ status, expected = self.get_shell_results()
+ self.clean()
+ if expected != actual:
+ print >>output_buffer, "Only in baseline results:"
+ print >>output_buffer, '\n'.join(expected.difference(actual))
+ print >>output_buffer, "Only in actual results:"
+ print >>output_buffer, '\n'.join(actual.difference(expected))
+ return self.show_status('FAILED')
+ elif posttest_result != 0:
+ print >>output_buffer, "Posttest gave status code", posttest_result
+ return self.show_status('FAILED')
+ return self.show_status('Passed')
+
+ def have_error_mismatch(self, status):
+ if self.error and (status == 0):
+ return "dtrx did not return expected error"
+ elif (not self.error) and (status != 0):
+ return "dtrx returned error code %s" % (status,)
+ return None
+
+ def grep_output(self, output):
+ if self.grep and (not re.search(self.grep.replace(' ', '\\s+'),
+ output, re.MULTILINE)):
+ return "output did not match %s" % (self.grep)
+ elif self.antigrep and re.search(self.antigrep.replace(' ', '\\s+'),
+ output, re.MULTILINE):
+ return "output matched antigrep %s" % (self.antigrep)
+ return None
+
+ def check_output(self, output):
+ if ((self.output is not None) and
+ (self.output.strip() != output.strip())):
+ return "output did not match provided text"
+ return None
+
+ def check_results(self):
+ output_buffer.seek(0, 0)
+ output_buffer.truncate()
+ self.clean()
+ status, actual = self.get_extractor_results()
+ output_buffer.seek(0, 0)
+ output_buffer.readline()
+ output = output_buffer.read(-1)
+ problem = (self.have_error_mismatch(status) or
+ self.check_output(output) or self.grep_output(output))
+ if problem:
+ return self.show_status('FAILED', problem)
+ if self.baseline:
+ return self.compare_results(actual)
+ else:
+ self.clean()
+ return self.show_status('Passed')
+
+ def run(self):
+ if self.directory:
+ os.mkdir(self.directory)
+ os.chdir(self.directory)
+ try:
+ result = self.check_results()
+ except ExtractorTestError, error:
+ result = self.show_status('ERROR', error)
+ if self.directory:
+ os.chdir(ROOT_DIR)
+ subprocess.call(['chmod', '-R', '700', self.directory])
+ subprocess.call(['rm', '-rf', self.directory])
+ return result
+
+
+test_db = open('tests.yml')
+test_data = syck.load(test_db.read(-1))
+test_db.close()
+tests = [ExtractorTest(**data) for data in test_data]
+for original_data in test_data:
+ if (original_data.has_key('directory') or
+ (not original_data.has_key('baseline'))):
+ continue
+ data = original_data.copy()
+ data['name'] += ' in ..'
+ data['directory'] = 'inside-dir'
+ data['filenames'] = ' '.join(['../%s' % filename for filename in
+ data.get('filenames', '').split()])
+ tests.append(ExtractorTest(**data))
+results = [test.run() for test in tests]
+counts = {}
+for outcome in OUTCOMES:
+ counts[outcome] = 0
+for result in results:
+ counts[result] += 1
+print " Totals:", ', '.join(["%s %s" % (counts[key], key) for key in OUTCOMES])
+input_buffer.close()
+output_buffer.close()
diff --git a/tests/test-1.23.7z b/tests/test-1.23.7z
new file mode 100644
index 0000000..b3e1ea7
--- /dev/null
+++ b/tests/test-1.23.7z
Binary files differ
diff --git a/tests/test-1.23.cpio b/tests/test-1.23.cpio
new file mode 100644
index 0000000..0777739
--- /dev/null
+++ b/tests/test-1.23.cpio
Binary files differ
diff --git a/tests/test-1.23.gem b/tests/test-1.23.gem
new file mode 100644
index 0000000..3649657
--- /dev/null
+++ b/tests/test-1.23.gem
Binary files differ
diff --git a/tests/test-1.23.tar b/tests/test-1.23.tar
new file mode 100644
index 0000000..7374619
--- /dev/null
+++ b/tests/test-1.23.tar
Binary files differ
diff --git a/tests/test-1.23.tar.bz2 b/tests/test-1.23.tar.bz2
new file mode 100644
index 0000000..2d511b8
--- /dev/null
+++ b/tests/test-1.23.tar.bz2
Binary files differ
diff --git a/tests/test-1.23.tar.gz b/tests/test-1.23.tar.gz
new file mode 100644
index 0000000..b32be49
--- /dev/null
+++ b/tests/test-1.23.tar.gz
Binary files differ
diff --git a/tests/test-1.23.tar.lzma b/tests/test-1.23.tar.lzma
new file mode 100644
index 0000000..27d783c
--- /dev/null
+++ b/tests/test-1.23.tar.lzma
Binary files differ
diff --git a/tests/test-1.23.zip b/tests/test-1.23.zip
new file mode 100644
index 0000000..aa0ff55
--- /dev/null
+++ b/tests/test-1.23.zip
Binary files differ
diff --git a/tests/test-1.23_all.deb b/tests/test-1.23_all.deb
new file mode 100644
index 0000000..2deb1b8
--- /dev/null
+++ b/tests/test-1.23_all.deb
Binary files differ
diff --git a/tests/test-deep-recursion.tar b/tests/test-deep-recursion.tar
new file mode 100644
index 0000000..aa62164
--- /dev/null
+++ b/tests/test-deep-recursion.tar
Binary files differ
diff --git a/tests/test-dot-first-bomb.tar.gz b/tests/test-dot-first-bomb.tar.gz
new file mode 100644
index 0000000..3f81d5c
--- /dev/null
+++ b/tests/test-dot-first-bomb.tar.gz
Binary files differ
diff --git a/tests/test-dot-first-onedir.tar.gz b/tests/test-dot-first-onedir.tar.gz
new file mode 100644
index 0000000..bbb406d
--- /dev/null
+++ b/tests/test-dot-first-onedir.tar.gz
Binary files differ
diff --git a/tests/test-empty.tar.bz2 b/tests/test-empty.tar.bz2
new file mode 100644
index 0000000..a704637
--- /dev/null
+++ b/tests/test-empty.tar.bz2
Binary files differ
diff --git a/tests/test-one-archive.tar.gz b/tests/test-one-archive.tar.gz
new file mode 100644
index 0000000..5799800
--- /dev/null
+++ b/tests/test-one-archive.tar.gz
Binary files differ
diff --git a/tests/test-onedir.tar.bz2 b/tests/test-onedir.tar.bz2
new file mode 100644
index 0000000..7e734c1
--- /dev/null
+++ b/tests/test-onedir.tar.bz2
Binary files differ
diff --git a/tests/test-onedir.tar.gz b/tests/test-onedir.tar.gz
new file mode 100644
index 0000000..a7b3ee1
--- /dev/null
+++ b/tests/test-onedir.tar.gz
Binary files differ
diff --git a/tests/test-onefile.tar.gz b/tests/test-onefile.tar.gz
new file mode 100644
index 0000000..e4fed68
--- /dev/null
+++ b/tests/test-onefile.tar.gz
Binary files differ
diff --git a/tests/test-recursive-badperms.tar.bz2 b/tests/test-recursive-badperms.tar.bz2
new file mode 100644
index 0000000..7c720bf
--- /dev/null
+++ b/tests/test-recursive-badperms.tar.bz2
Binary files differ
diff --git a/tests/test-tar-with-node.tar.gz b/tests/test-tar-with-node.tar.gz
new file mode 100644
index 0000000..f12ba42
--- /dev/null
+++ b/tests/test-tar-with-node.tar.gz
Binary files differ
diff --git a/tests/test-text.bz2 b/tests/test-text.bz2
new file mode 100644
index 0000000..92bfd31
--- /dev/null
+++ b/tests/test-text.bz2
Binary files differ
diff --git a/tests/test-text.gz b/tests/test-text.gz
new file mode 100644
index 0000000..e427562
--- /dev/null
+++ b/tests/test-text.gz
Binary files differ
diff --git a/tests/tests.yml b/tests/tests.yml
new file mode 100644
index 0000000..54e79cd
--- /dev/null
+++ b/tests/tests.yml
@@ -0,0 +1,657 @@
+- name: basic .tar
+ filenames: test-1.23.tar
+ baseline: |
+ tar -xf $1
+
+- name: basic .tar.gz
+ filenames: test-1.23.tar.gz
+ baseline: |
+ tar -zxf $1
+
+- name: basic .tar.bz2
+ filenames: test-1.23.tar.bz2
+ baseline: |
+ mkdir test-1.23
+ cd test-1.23
+ tar -jxf ../$1
+
+- name: basic .zip
+ filenames: test-1.23.zip
+ baseline: |
+ mkdir test-1.23
+ cd test-1.23
+ unzip -q ../$1
+
+- name: basic .deb
+ filenames: test-1.23_all.deb
+ baseline: |
+ mkdir test-1.23
+ cd test-1.23
+ ar p ../$1 data.tar.gz | tar -zx
+
+- name: basic .gem
+ filenames: test-1.23.gem
+ baseline: |
+ mkdir test-1.23
+ cd test-1.23
+ tar -xOf ../$1 data.tar.gz | tar -zx
+
+- name: basic .7z
+ filenames: test-1.23.7z
+ baseline: |
+ 7z x $1
+
+- name: basic .lzma
+ filenames: test-1.23.tar.lzma
+ baseline: |
+ lzcat $1 | tar -x
+
+- name: basic .cpio
+ filenames: test-1.23.cpio
+ baseline: |
+ cpio -i --make-directories <$1
+ antigrep: blocks?
+
+- name: .deb metadata
+ filenames: test-1.23_all.deb
+ options: --metadata
+ baseline: |
+ mkdir test-1.23
+ cd test-1.23
+ ar p ../$1 control.tar.gz | tar -zx
+
+- name: .gem metadata
+ filenames: test-1.23.gem
+ options: -m
+ baseline: |
+ tar -xOf $1 metadata.gz | zcat > test-1.23.gem-metadata.txt
+ cleanup: rm -f test-1.23.gem-metadata.txt
+ posttest: |
+ if [ "x`cat test-1.23.gem-metadata.txt`" != "xhi" ]; then exit 1; fi
+
+- name: recursion and permissions
+ filenames: test-recursive-badperms.tar.bz2
+ options: -n -r
+ baseline: |
+ mkdir test-recursive-badperms
+ cd test-recursive-badperms
+ tar -jxf ../$1
+ mkdir test-badperms
+ cd test-badperms
+ tar -xf ../test-badperms.tar
+ chmod 700 testdir
+ posttest: |
+ if [ "x`cat test-recursive-badperms/test-badperms/testdir/testfile`" != \
+ "xhey" ]; then exit 1; fi
+
+- name: decompressing gz
+ directory: inside-dir
+ filenames: ../test-text.gz
+ baseline: |
+ zcat $1 >test-text
+ posttest: |
+ if [ "x`cat test-text`" != "xhi" ]; then exit 1; fi
+
+- name: decompressing bz2
+ directory: inside-dir
+ filenames: ../test-text.bz2
+ baseline: |
+ bzcat $1 >test-text
+ posttest: |
+ if [ "x`cat test-text`" != "xhi" ]; then exit 1; fi
+
+- name: decompression with -r
+ directory: inside-dir
+ filenames: ../test-text.gz
+ options: -n -r
+ baseline: |
+ zcat $1 >test-text
+
+- name: decompression with -fr
+ directory: inside-dir
+ filenames: ../test-text.gz
+ options: -n -fr
+ baseline: |
+ zcat $1 >test-text
+
+- name: overwrite protection
+ filenames: test-1.23.tar.bz2
+ baseline: |
+ mkdir test-1.23.1
+ cd test-1.23.1
+ tar -jxf ../$1
+ prerun: |
+ mkdir test-1.23
+
+- name: overwrite option
+ filenames: test-1.23.tar.bz2
+ options: -n -o
+ baseline: |
+ cd test-1.23
+ tar -jxf ../$1
+ prerun: |
+ mkdir test-1.23
+
+- name: flat option
+ directory: inside-dir
+ filenames: ../test-1.23.tar.bz2
+ options: -n -f
+ baseline: |
+ tar -jxf $1
+
+- name: flat recursion and permissions
+ directory: inside-dir
+ filenames: ../test-recursive-badperms.tar.bz2
+ options: -n -fr
+ baseline: |
+ tar -jxf $1
+ tar -xf test-badperms.tar
+ chmod 700 testdir
+ posttest: |
+ if [ "x`cat testdir/testfile`" != "xhey" ]; then exit 1; fi
+
+- name: no files
+ error: true
+ grep: "[Uu]sage"
+
+- name: bad file
+ error: true
+ filenames: nonexistent-file.tar
+
+- name: not an archive
+ error: true
+ filenames: tests.yml
+
+- name: bad options
+ options: -n --nonexistent-option
+ filenames: test-1.23.tar
+ error: true
+
+- name: --version
+ options: -n --version
+ grep: ersion \d+\.\d+
+ filenames: test-1.23.tar
+ baseline: |
+ exit 0
+
+- name: one good archive of many
+ filenames: tests.yml test-1.23.tar nonexistent-file.tar
+ error: true
+ baseline: |
+ tar -xf $2
+
+- name: silence
+ filenames: tests.yml
+ options: -n -qq
+ error: true
+ antigrep: .
+
+- name: can't write to directory
+ directory: inside-dir
+ filenames: ../test-1.23.tar
+ error: true
+ grep: ERROR
+ antigrep: Traceback
+ prerun: |
+ chmod 500 .
+
+- name: list contents of one file
+ options: -n -l
+ filenames: test-1.23.tar
+ output: |
+ test-1.23/
+ test-1.23/1/
+ test-1.23/1/2/
+ test-1.23/1/2/3
+ test-1.23/a/
+ test-1.23/a/b
+ test-1.23/foobar
+
+- name: list contents of .cpio
+ options: -n -l
+ filenames: test-1.23.cpio
+ grep: ^test-1\.23/1/2/3$
+ antigrep: blocks?
+
+- name: list contents of multiple files
+ options: -n --table
+ filenames: test-1.23_all.deb test-1.23.zip
+ output: |
+ test-1.23_all.deb:
+ 1/
+ 1/2/
+ 1/2/3
+ a/
+ a/b
+ foobar
+
+ test-1.23.zip:
+ 1/2/3
+ a/b
+ foobar
+
+- name: list contents of compressed file
+ options: -n -t
+ filenames: test-text.gz
+ output: test-text
+
+- name: default behavior with one directory (gz)
+ options: -n
+ filenames: test-onedir.tar.gz
+ baseline: |
+ mkdir test-onedir
+ cd test-onedir
+ tar -zxf ../$1
+
+- name: one directory extracted inside another interactively (gz)
+ options: ""
+ filenames: test-onedir.tar.gz
+ grep: one directory
+ input: i
+ baseline: |
+ mkdir test-onedir
+ cd test-onedir
+ tar -zxf ../$1
+
+- name: one directory extracted with rename interactively (gz)
+ options: ""
+ filenames: test-onedir.tar.gz
+ input: r
+ baseline: |
+ tar -zxf $1
+ mv test test-onedir
+
+- name: one directory extracted here interactively (gz)
+ options: ""
+ filenames: test-onedir.tar.gz
+ input: h
+ baseline: |
+ tar -zxf $1
+
+- name: --one=inside
+ options: "--one=inside -n"
+ filenames: test-onedir.tar.gz
+ baseline: |
+ mkdir test-onedir
+ cd test-onedir
+ tar -zxf ../$1
+
+- name: --one=rename
+ options: "--one-entry=rename -n"
+ filenames: test-onedir.tar.gz
+ baseline: |
+ tar -zxf $1
+ mv test test-onedir
+
+- name: --one=here
+ options: "--one=here -n"
+ filenames: test-onedir.tar.gz
+ baseline: |
+ tar -zxf $1
+
+- name: default behavior with one directory (bz2)
+ options: -n
+ filenames: test-onedir.tar.gz
+ baseline: |
+ mkdir test-onedir
+ cd test-onedir
+ tar -zxf ../$1
+
+- name: one directory extracted inside another (bz2)
+ options: ""
+ filenames: test-onedir.tar.gz
+ input: i
+ baseline: |
+ mkdir test-onedir
+ cd test-onedir
+ tar -zxf ../$1
+
+- name: one directory extracted with rename (bz2)
+ options: ""
+ filenames: test-onedir.tar.gz
+ input: r
+ baseline: |
+ tar -zxf $1
+ mv test test-onedir
+
+- name: one directory extracted here (bz2)
+ options: ""
+ filenames: test-onedir.tar.gz
+ input: h
+ baseline: |
+ tar -zxf $1
+
+- name: default behavior with one file
+ options: -n
+ filenames: test-onefile.tar.gz
+ baseline: |
+ mkdir test-onefile
+ cd test-onefile
+ tar -zxf ../$1
+
+- name: one file extracted inside a directory
+ options: ""
+ filenames: test-onefile.tar.gz
+ input: i
+ grep: one file
+ baseline: |
+ mkdir test-onefile
+ cd test-onefile
+ tar -zxf ../$1
+
+- name: one file extracted with rename, with Expected text
+ options: ""
+ filenames: test-onefile.tar.gz
+ input: r
+ grep: "Expected: test-onefile"
+ baseline: |
+ tar -zxOf $1 >test-onefile
+
+- name: one file extracted here, with Actual text
+ options: ""
+ filenames: test-onefile.tar.gz
+ input: h
+ grep: " Actual: test-text"
+ baseline: |
+ tar -zxf $1
+
+- name: bomb with preceding dot in the table
+ filenames: test-dot-first-bomb.tar.gz
+ options: ""
+ antigrep: one entry
+ baseline: |
+ mkdir test-dot-first-bomb
+ cd test-dot-first-bomb
+ tar -zxf ../$1
+
+- name: one directory preceded by dot in the table
+ filenames: test-dot-first-onedir.tar.gz
+ options: ""
+ grep: "Actual: (./)?dir/"
+ input: h
+ baseline: |
+ tar -zxf $1
+
+- name: two one-item archives with different answers
+ filenames: test-onedir.tar.gz test-onedir.tar.gz
+ options: ""
+ input: |
+ h
+ r
+ baseline: |
+ tar -zxf $1
+ mv test test-onedir
+ tar -zxf $1
+
+- name: interactive recursion (always)
+ filenames: test-recursive-badperms.tar.bz2 test-recursive-badperms.tar.bz2
+ options: ""
+ input: |
+ i
+ a
+ i
+ baseline: |
+ extract() {
+ mkdir test-recursive-badperms$2
+ cd test-recursive-badperms$2
+ tar -jxf ../$1
+ mkdir test-badperms
+ cd test-badperms
+ tar -xf ../test-badperms.tar
+ chmod 700 testdir
+ cd ../..
+ }
+ extract $1
+ extract $1 .1
+
+- name: interactive recursion (once)
+ filenames: test-recursive-badperms.tar.bz2 test-recursive-badperms.tar.bz2
+ options: ""
+ input: |
+ i
+ o
+ i
+ n
+ baseline: |
+ mkdir test-recursive-badperms
+ cd test-recursive-badperms
+ tar -jxf ../$1
+ mkdir test-badperms
+ cd test-badperms
+ tar -xf ../test-badperms.tar
+ chmod 700 testdir
+ cd ../..
+ mkdir test-recursive-badperms.1
+ cd test-recursive-badperms.1
+ tar -jxf ../$1
+
+- name: interactive recursion (never)
+ filenames: test-recursive-badperms.tar.bz2 test-recursive-badperms.tar.bz2
+ options: ""
+ input: |
+ i
+ v
+ i
+ baseline: |
+ extract() {
+ mkdir test-recursive-badperms$2
+ cd test-recursive-badperms$2
+ tar -jxf ../$1
+ cd ..
+ }
+ extract $1
+ extract $1 .1
+
+- name: recursion in subdirectories here
+ filenames: test-deep-recursion.tar
+ options: ""
+ input: |
+ h
+ o
+ grep: "contains 2 other archive file\(s\), out of 2 file\(s\)"
+ baseline: |
+ tar -xf $1
+ cd subdir
+ zcat test-text.gz > test-text
+ cd subsubdir
+ zcat test-text.gz > test-text
+
+- name: recursion in subdirectories with rename
+ filenames: test-deep-recursion.tar
+ options: ""
+ input: |
+ r
+ o
+ grep: "contains 2"
+ baseline: |
+ tar -xf $1
+ mv subdir test-deep-recursion
+ cd test-deep-recursion
+ zcat test-text.gz > test-text
+ cd subsubdir
+ zcat test-text.gz > test-text
+
+- name: recursion in subdirectories inside new dir
+ filenames: test-deep-recursion.tar
+ options: ""
+ input: |
+ i
+ o
+ grep: "contains 2"
+ baseline: |
+ mkdir test-deep-recursion
+ cd test-deep-recursion
+ tar -xf ../$1
+ cd subdir
+ zcat test-text.gz > test-text
+ cd subsubdir
+ zcat test-text.gz > test-text
+
+- name: extracting file with bad extension
+ filenames: test-1.23.bin
+ prerun: cp ${1}test-1.23.tar.gz ${1}test-1.23.bin
+ cleanup: rm -f ${1}test-1.23.bin
+ baseline: |
+ tar -zxf $1
+
+- name: extracting file with misleading extension
+ filenames: trickery.tar.gz
+ prerun: cp ${1}test-1.23.zip ${1}trickery.tar.gz
+ cleanup: rm -f ${1}trickery.tar.gz
+ antigrep: .
+ baseline: |
+ mkdir trickery
+ cd trickery
+ unzip -q ../$1
+
+- name: listing file with misleading extension
+ options: -l
+ filenames: trickery.tar.gz
+ prerun: cp ${1}test-1.23.zip ${1}trickery.tar.gz
+ cleanup: rm -f ${1}trickery.tar.gz
+ grep: "^1/2/3$"
+ antigrep: "^dtrx:"
+
+- name: non-archive error
+ filenames: /dev/null
+ error: true
+ grep: "not a known archive type"
+
+- name: no such file error
+ filenames: nonexistent-file.tar.gz
+ error: true
+ grep: "[Nn]o such file"
+
+- name: no such file error with no extension
+ filenames: nonexistent-file
+ error: true
+ grep: "[Nn]o such file"
+
+- name: try to extract a directory error
+ filenames: test-directory
+ prerun: mkdir test-directory
+ error: true
+ grep: "cannot work with a directory"
+
+- name: permission denied error
+ filenames: unreadable-file.tar.gz
+ prerun: |
+ touch unreadable-file.tar.gz
+ chmod 000 unreadable-file.tar.gz
+ cleanup: rm -f unreadable-file.tar.gz
+ error: true
+ grep: "[Pp]ermission denied"
+
+- name: permission denied no-pipe file error
+ filenames: unreadable-file.zip
+ prerun: |
+ touch unreadable-file.zip
+ chmod 000 unreadable-file.zip
+ cleanup: rm -f unreadable-file.zip
+ error: true
+ grep: "[Pp]ermission denied"
+
+- name: bad file error
+ filenames: bogus-file.tar.gz
+ prerun: |
+ touch bogus-file.tar.gz
+ cleanup: rm -f bogus-file.tar.gz
+ error: true
+ grep: "returned status code [^0]"
+
+- name: try to extract in unwritable directory
+ directory: unwritable-dir
+ filenames: ../test-1.23.tar.gz
+ prerun: chmod 500 .
+ error: true
+ grep: "cannot extract here: [Pp]ermission denied"
+
+- name: recursive listing is a no-op
+ options: -rl
+ filenames: test-recursive-badperms.tar.bz2
+ grep: test-badperms.tar
+ antigrep: testdir/
+
+- name: graceful coping when many extraction directories are taken
+ directory: busydir
+ prerun: |
+ mkdir test-1.23
+ for i in $(seq 1 10); do mkdir test-1.23.$i; done
+ filenames: ../test-1.23.tar.gz
+ grep: "WARNING: extracting"
+
+- name: graceful coping when many decompression targets are taken
+ directory: busydir
+ prerun: |
+ touch test-text
+ for i in $(seq 1 10); do touch test-text.$i; done
+ filenames: ../test-text.gz
+ grep: "WARNING: extracting"
+
+- name: output filenames with -v
+ options: -v -n
+ filenames: test-onedir.tar.gz test-text.gz
+ output: |
+ test-onedir.tar.gz:
+ test-onedir/
+ test-onedir/test/
+ test-onedir/test/foobar
+ test-onedir/test/quux
+
+ test-text.gz:
+ test-text
+
+- name: output filenames with -v and -f
+ options: -nvf
+ directory: busydir
+ filenames: ../test-onedir.tar.gz
+ output: |
+ ../test-onedir.tar.gz:
+ test/
+ test/foobar
+ test/quux
+
+- name: list recursive archives
+ options: ""
+ filenames: test-deep-recursion.tar
+ input: |
+ r
+ l
+ n
+ grep: "^test-deep-recursion/subsubdir/test-text\.gz$"
+
+- name: partly failed extraction
+ options: -n
+ filenames: test-tar-with-node.tar.gz
+ baseline: |
+ mkdir test-tar-with-node
+ cd test-tar-with-node
+ tar -zxf ../$1
+ grep: Cannot mknod
+
+- name: flat extraction of one-file archive
+ directory: inside-dir
+ options: -f
+ filenames: ../test-onefile.tar.gz
+ baseline: tar -zxf $1
+ antigrep: "contains"
+
+- name: test recursive extraction of one archive
+ directory: inside-dir
+ options: ""
+ filenames: ../test-one-archive.tar.gz
+ baseline: |
+ tar -zxf $1
+ zcat test-text.gz >test-text
+ input: |
+ h
+ o
+
+- name: extracting empty archive
+ filenames: test-empty.tar.bz2
+ baseline: ""
+
+- name: listing empty archive
+ filenames: test-empty.tar.bz2
+ options: -l
+ antigrep: .