diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a155391f57..ea5d382e63 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ on: jobs: analyse: name: Analyse - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout repository @@ -47,10 +47,10 @@ jobs: - name: Setup qbs run: | qbs setup-toolchains --detect - qbs setup-qt --detect + qbs config defaultProfile gcc - name: Build - run: qbs build profile:qt-5-9-5 + run: qbs build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/src/karchive/.gitignore b/src/karchive/.gitignore new file mode 100644 index 0000000000..d41c5cbdff --- /dev/null +++ b/src/karchive/.gitignore @@ -0,0 +1,21 @@ +# Ignore the following files +*~ +*.diff +*.kate-swp +*.kdev4 +.kdev_include_paths +*.kdevelop.pcs +*.moc +*.moc.cpp +*.orig +*.user +.*.swp +.swp.* +Doxyfile +Makefile +avail +random_seed +/build*/ +CMakeLists.txt.user* +*.unc-backup* +.cmake/ diff --git a/src/karchive/AUTHORS b/src/karchive/AUTHORS new file mode 100644 index 0000000000..5fe2b266db --- /dev/null +++ b/src/karchive/AUTHORS @@ -0,0 +1,10 @@ +Maintainers: +Mario Bensi +David Faure + +Many other contributors, see git log. + +For questions about this package, email kde-frameworks-devel@kde.org. + +For bug reports, please use http://bugs.kde.org + diff --git a/src/karchive/CMakeLists.txt b/src/karchive/CMakeLists.txt new file mode 100644 index 0000000000..e9898d8a11 --- /dev/null +++ b/src/karchive/CMakeLists.txt @@ -0,0 +1,109 @@ +cmake_minimum_required(VERSION 3.5) + +set(KF5_VERSION "5.76.0") # handled by release scripts +project(KArchive VERSION ${KF5_VERSION}) + +include(FeatureSummary) +find_package(ECM 5.75.0 NO_MODULE) +set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") +feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) + + +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) +include(KDECMakeSettings) + +include(ECMGenerateExportHeader) + +set(REQUIRED_QT_VERSION 5.12.0) +find_package(Qt5Core ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) + +find_package(ZLIB) +set_package_properties(ZLIB PROPERTIES + URL "http://www.zlib.net" + DESCRIPTION "Support for gzip compressed files and data streams" + TYPE REQUIRED + PURPOSE "Required by the core KDE libraries and some critical kioslaves" +) + +find_package(BZip2) +set_package_properties(BZip2 PROPERTIES + URL "https://sourceware.org/bzip2/" + DESCRIPTION "Support for BZip2 compressed files and data streams" + TYPE RECOMMENDED + PURPOSE "Support for BZip2 compressed files and data streams" +) + +find_package(LibLZMA) +set_package_properties(LibLZMA PROPERTIES + URL "http://tukaani.org/xz/" + DESCRIPTION "Support for xz compressed files and data streams" + PURPOSE "Support for xz compressed files and data streams" +) +include_directories( + ${ZLIB_INCLUDE_DIR} +) + +include(ECMSetupVersion) +include(ECMGenerateHeaders) +include(ECMQtDeclareLoggingCategory) +include(ECMAddQch) + +set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].") + +option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) +add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") + +ecm_setup_version(PROJECT + VARIABLE_PREFIX KARCHIVE + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/karchive_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfigVersion.cmake" + SOVERSION 5) +add_definitions(-DQT_NO_FOREACH) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050d00) + +add_subdirectory(src) +if (BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) +endif() + +# create a Config.cmake and a ConfigVersion.cmake file and install them +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Archive") + +if (BUILD_QCH) + ecm_install_qch_export( + TARGETS KF5Archive_QCH + FILE KF5ArchiveQchTargets.cmake + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel + ) + set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5ArchiveQchTargets.cmake\")") +endif() + +include(CMakePackageConfigHelpers) + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KF5ArchiveConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} +) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/karchive_version.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} + COMPONENT Devel) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfigVersion.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel) + +install(EXPORT KF5ArchiveTargets + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + FILE KF5ArchiveTargets.cmake + NAMESPACE KF5::) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/karchive/KF5ArchiveConfig.cmake.in b/src/karchive/KF5ArchiveConfig.cmake.in new file mode 100644 index 0000000000..0d59d6378d --- /dev/null +++ b/src/karchive/KF5ArchiveConfig.cmake.in @@ -0,0 +1,11 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Qt5Core @REQUIRED_QT_VERSION@) + + +set(KArchive_HAVE_BZIP2 "@BZIP2_FOUND@") +set(KArchive_HAVE_LZMA "@LIBLZMA_FOUND@") + +include("${CMAKE_CURRENT_LIST_DIR}/KF5ArchiveTargets.cmake") +@PACKAGE_INCLUDE_QCHTARGETS@ diff --git a/src/karchive/LICENSES/BSD-2-Clause.txt b/src/karchive/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..2d2bab1127 --- /dev/null +++ b/src/karchive/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,22 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/karchive/LICENSES/LGPL-2.0-only.txt b/src/karchive/LICENSES/LGPL-2.0-only.txt new file mode 100644 index 0000000000..5c96471aaf --- /dev/null +++ b/src/karchive/LICENSES/LGPL-2.0-only.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, 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 this service 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 make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +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 library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library 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 Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/src/karchive/LICENSES/LGPL-2.0-or-later.txt b/src/karchive/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000000..5c96471aaf --- /dev/null +++ b/src/karchive/LICENSES/LGPL-2.0-or-later.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, 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 this service 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 make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +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 library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library 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 Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/src/karchive/LICENSES/LGPL-3.0-only.txt b/src/karchive/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000000..bd405afbef --- /dev/null +++ b/src/karchive/LICENSES/LGPL-3.0-only.txt @@ -0,0 +1,163 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms +and conditions of version 3 of the GNU General Public License, supplemented +by the additional permissions listed below. + + 0. Additional Definitions. + + + +As used herein, "this License" refers to version 3 of the GNU Lesser General +Public License, and the "GNU GPL" refers to version 3 of the GNU General Public +License. + + + +"The Library" refers to a covered work governed by this License, other than +an Application or a Combined Work as defined below. + + + +An "Application" is any work that makes use of an interface provided by the +Library, but which is not otherwise based on the Library. Defining a subclass +of a class defined by the Library is deemed a mode of using an interface provided +by the Library. + + + +A "Combined Work" is a work produced by combining or linking an Application +with the Library. The particular version of the Library with which the Combined +Work was made is also called the "Linked Version". + + + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding +Source for the Combined Work, excluding any source code for portions of the +Combined Work that, considered in isolation, are based on the Application, +and not on the Linked Version. + + + +The "Corresponding Application Code" for a Combined Work means the object +code and/or source code for the Application, including any data and utility +programs needed for reproducing the Combined Work from the Application, but +excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without +being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility +refers to a function or data to be supplied by an Application that uses the +facility (other than as an argument passed when the facility is invoked), +then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure +that, in the event an Application does not supply the function or data, the +facility still operates, and performs whatever part of its purpose remains +meaningful, or + +b) under the GNU GPL, with none of the additional permissions of this License +applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header +file that is part of the Library. You may convey such object code under terms +of your choice, provided that, if the incorporated material is not limited +to numerical parameters, data structure layouts and accessors, or small macros, +inline functions and templates (ten or fewer lines in length), you do both +of the following: + +a) Give prominent notice with each copy of the object code that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the object code with a copy of the GNU GPL and this license document. + + 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, +effectively do not restrict modification of the portions of the Library contained +in the Combined Work and reverse engineering for debugging such modifications, +if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the Combined Work with a copy of the GNU GPL and this license +document. + +c) For a Combined Work that displays copyright notices during execution, include +the copyright notice for the Library among these notices, as well as a reference +directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + +0) Convey the Minimal Corresponding Source under the terms of this License, +and the Corresponding Application Code in a form suitable for, and under terms +that permit, the user to recombine or relink the Application with a modified +version of the Linked Version to produce a modified Combined Work, in the +manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + +1) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (a) uses at run time a copy of the Library +already present on the user's computer system, and (b) will operate properly +with a modified version of the Library that is interface-compatible with the +Linked Version. + +e) Provide Installation Information, but only if you would otherwise be required +to provide such information under section 6 of the GNU GPL, and only to the +extent that such information is necessary to install and execute a modified +version of the Combined Work produced by recombining or relinking the Application +with a modified version of the Linked Version. (If you use option 4d0, the +Installation Information must accompany the Minimal Corresponding Source and +Corresponding Application Code. If you use option 4d1, you must provide the +Installation Information in the manner specified by section 6 of the GNU GPL +for conveying Corresponding Source.) + + 5. Combined Libraries. + +You may place library facilities that are a work based on the Library side +by side in a single library together with other library facilities that are +not Applications and are not covered by this License, and convey such a combined +library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities, conveyed under the +terms of this License. + +b) Give prominent notice with the combined library that part of it is a work +based on the Library, and explaining where to find the accompanying uncombined +form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you +received it specifies that a certain numbered version of the GNU Lesser General +Public License "or any later version" applies to it, you have the option of +following the terms and conditions either of that published version or of +any later version published by the Free Software Foundation. If the Library +as you received it does not specify a version number of the GNU Lesser General +Public License, you may choose any version of the GNU Lesser General Public +License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether +future versions of the GNU Lesser General Public License shall apply, that +proxy's public statement of acceptance of any version is permanent authorization +for you to choose that version for the Library. diff --git a/src/karchive/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/src/karchive/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000000..a8ede546c6 --- /dev/null +++ b/src/karchive/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the license or (at your option) any later version +that is accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 6 of version 3 of the license. + +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. \ No newline at end of file diff --git a/src/karchive/README.md b/src/karchive/README.md new file mode 100644 index 0000000000..db40348235 --- /dev/null +++ b/src/karchive/README.md @@ -0,0 +1,22 @@ +# KArchive + +Reading, creating, and manipulating file archives + +## Introduction + +KArchive provides classes for easy reading, creation and manipulation of +"archive" formats like ZIP and TAR. + +It also provides transparent compression and decompression of data, like the +GZip format, via a subclass of QIODevice. + +## Usage + +If you want to read and write compressed data, just create an instance of +KCompressionDevice and write to or read from that. + +If you want to read and write archive formats, create an instance of the +appropriate subclass of KArchive (eg: K7Zip for 7-Zip files). You may need to +combine this with usage of KCompressionDevice (see the API documentation for the +relevant KArchive subclass for details). + diff --git a/src/karchive/karchive.qbs b/src/karchive/karchive.qbs new file mode 100644 index 0000000000..2aa28d610b --- /dev/null +++ b/src/karchive/karchive.qbs @@ -0,0 +1,65 @@ +import qbs 1.0 + +StaticLibrary { + condition: (Qt.core.versionMajor > 5 || Qt.core.versionMinor >= 12) && !qbs.toolchain.contains("msvc") + + Depends { name: "cpp" } + Depends { name: "Qt.core"; versionAtLeast: "5.12" } + + cpp.includePaths: [ "src" ] + cpp.defines: [ + "KARCHIVE_NO_DEPRECATED", + "KARCHIVE_STATIC_DEFINE", + "QT_NO_CAST_FROM_ASCII", + ] + + files : [ + "src/config-compression.h", +// "src/k7zip.cpp", // requires xz to build + "src/k7zip.h", + "src/kar.cpp", + "src/kar.h", + "src/karchivedirectory.h", + "src/karchiveentry.h", + "src/karchivefile.h", + "src/karchive_export.h", + "src/karchive_p.h", + "src/karchive.cpp", + "src/karchive.h", + "src/kbzip2filter.cpp", + "src/kbzip2filter.h", + "src/kcompressiondevice.cpp", + "src/kcompressiondevice.h", + "src/kcompressiondevice_p.h", + "src/kfilterbase.cpp", + "src/kfilterbase.h", + "src/kfilterdev.cpp", + "src/kfilterdev.h", + "src/kgzipfilter.cpp", + "src/kgzipfilter.h", + "src/klimitediodevice.cpp", + "src/klimitediodevice_p.h", + "src/knonefilter.cpp", + "src/knonefilter.h", + "src/krcc.cpp", + "src/krcc.h", + "src/ktar.cpp", + "src/ktar.h", +// "src/kxzfilter.cpp", + "src/kxzfilter.h", + "src/kzipfileentry.h", + "src/kzip.cpp", + "src/kzip.h", + "src/loggingcategory.cpp", + "src/loggingcategory.h", + ] + + Export { + Depends { name: "cpp" } + cpp.includePaths: "src" + cpp.defines: [ + "KARCHIVE_NO_DEPRECATED", + "KARCHIVE_STATIC_DEFINE", + ] + } +} diff --git a/src/karchive/metainfo.yaml b/src/karchive/metainfo.yaml new file mode 100644 index 0000000000..71d7cbd0fd --- /dev/null +++ b/src/karchive/metainfo.yaml @@ -0,0 +1,21 @@ +maintainer: dfaure +description: File compression +tier: 1 +type: functional +platforms: + - name: Linux + - name: FreeBSD + - name: Windows + - name: MacOSX + - name: Android +portingAid: false +deprecated: false +release: true +libraries: + - qmake: KArchive + cmake: "KF5::Archive" +cmakename: KF5Archive + +public_lib: true +group: Frameworks +subgroup: Tier 1 diff --git a/src/karchive/src/CMakeLists.txt b/src/karchive/src/CMakeLists.txt new file mode 100644 index 0000000000..de0d6f4934 --- /dev/null +++ b/src/karchive/src/CMakeLists.txt @@ -0,0 +1,147 @@ +set(HAVE_BZIP2_SUPPORT ${BZIP2_FOUND}) +if(BZIP2_FOUND AND BZIP2_NEED_PREFIX) + set(NEED_BZ2_PREFIX 1) +endif() + +set(HAVE_XZ_SUPPORT ${LIBLZMA_FOUND}) + +configure_file(config-compression.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-compression.h) +add_definitions(-DQT_NO_CAST_FROM_ASCII) + +if(BZIP2_FOUND) + include_directories(${BZIP2_INCLUDE_DIR}) + set(karchive_OPTIONAL_SRCS ${karchive_OPTIONAL_SRCS} kbzip2filter.cpp) + set(karchive_OPTIONAL_LIBS ${karchive_OPTIONAL_LIBS} ${BZIP2_LIBRARIES}) +endif() + +if(LIBLZMA_FOUND) + include_directories(${LIBLZMA_INCLUDE_DIRS}) + set(karchive_OPTIONAL_SRCS ${karchive_OPTIONAL_SRCS} kxzfilter.cpp k7zip.cpp) + set(karchive_OPTIONAL_LIBS ${karchive_OPTIONAL_LIBS} ${LIBLZMA_LIBRARIES}) +endif() + + +set(karchive_SRCS + karchive.cpp + kar.cpp + kcompressiondevice.cpp + kfilterbase.cpp + kfilterdev.cpp + kgzipfilter.cpp + klimitediodevice.cpp + knonefilter.cpp + ktar.cpp + kzip.cpp + krcc.cpp +) + +ecm_qt_declare_logging_category(karchive_SRCS + HEADER loggingcategory.h + IDENTIFIER KArchiveLog + CATEGORY_NAME kf.archive + OLD_CATEGORY_NAMES kf5.karchive + DEFAULT_SEVERITY Warning + DESCRIPTION "KArchive" + EXPORT KARCHIVE +) + +add_library(KF5Archive ${karchive_SRCS} ${karchive_OPTIONAL_SRCS}) +add_library(KF5::Archive ALIAS KF5Archive) +ecm_generate_export_header(KF5Archive + BASE_NAME KArchive + GROUP_BASE_NAME KF + VERSION ${KF5_VERSION} + DEPRECATED_BASE_VERSION 0 + DEPRECATION_VERSIONS 5.0 + EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} +) + +target_include_directories(KF5Archive + INTERFACE "$" +) + +target_link_libraries(KF5Archive + PUBLIC + Qt5::Core + PRIVATE + ${karchive_OPTIONAL_LIBS} + ${ZLIB_LIBRARY} +) + +set_target_properties(KF5Archive PROPERTIES + VERSION ${KARCHIVE_VERSION_STRING} + SOVERSION ${KARCHIVE_SOVERSION} + EXPORT_NAME "Archive" +) + +ecm_generate_headers(KArchive_HEADERS + HEADER_NAMES + KArchive + KArchiveEntry + KArchiveFile + KArchiveDirectory + KAr + KCompressionDevice + KFilterBase + KFilterDev + KRcc + KTar + KZip + KZipFileEntry + + REQUIRED_HEADERS KArchive_HEADERS +) + +install(TARGETS KF5Archive + EXPORT KF5ArchiveTargets + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + +if(LIBLZMA_FOUND) + ecm_generate_headers(KArchive_HEADERS + HEADER_NAMES + K7Zip + REQUIRED_HEADERS KArchive_HEADERS + ) +endif() + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/karchive_export.h + ${KArchive_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KArchive + COMPONENT Devel) + +ecm_qt_install_logging_categories( + EXPORT KARCHIVE + FILE karchive.categories + DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} +) + +if(BUILD_QCH) + ecm_add_qch( + KF5Archive_QCH + NAME KArchive + BASE_NAME KF5Archive + VERSION ${KF5_VERSION} + ORG_DOMAIN org.kde + SOURCES # using only public headers, to cover only public API + ${KArchive_HEADERS} + MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" + LINK_QCHS + Qt5Core_QCH + INCLUDE_DIRS + ${CMAKE_CURRENT_BINARY_DIR} + BLANK_MACROS + KARCHIVE_EXPORT + KARCHIVE_DEPRECATED + "KARCHIVE_DEPRECATED_VERSION(x, y, t)" + TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} + QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} + COMPONENT Devel + ) +endif() + +include(ECMGeneratePriFile) +ecm_generate_pri_file(BASE_NAME KArchive LIB_NAME KF5Archive DEPS "core" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KArchive) +install(FILES ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) + diff --git a/src/karchive/src/config-compression.h b/src/karchive/src/config-compression.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/karchive/src/config-compression.h.cmake b/src/karchive/src/config-compression.h.cmake new file mode 100644 index 0000000000..dce2ec1922 --- /dev/null +++ b/src/karchive/src/config-compression.h.cmake @@ -0,0 +1,8 @@ +#cmakedefine01 HAVE_BZIP2_SUPPORT + +/* Set to 1 if the libbz2 functions need the BZ2_ prefix */ +#cmakedefine01 NEED_BZ2_PREFIX + +/* Set to 1 if you have xz */ +#cmakedefine01 HAVE_XZ_SUPPORT + diff --git a/src/karchive/src/k7zip.cpp b/src/karchive/src/k7zip.cpp new file mode 100644 index 0000000000..f2f9ab8415 --- /dev/null +++ b/src/karchive/src/k7zip.cpp @@ -0,0 +1,2988 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2011 Mario Bensi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "k7zip.h" +#include "karchive_p.h" +#include "loggingcategory.h" + +#include +#include +#include +#include +#include + +#include "kcompressiondevice.h" +#include +#include +#include "klimitediodevice_p.h" + +#include // time() +#include +#include "zlib.h" + +#ifndef QT_STAT_LNK +# define QT_STAT_LNK 0120000 +#endif // QT_STAT_LNK + +//////////////////////////////////////////////////////////////////////// +/////////////////////////// K7Zip ////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +#define BUFFER_SIZE 8*1024 + +static const unsigned char k7zip_signature[6] = {'7', 'z', 0xBC, 0xAF, 0x27, 0x1C}; +//static const unsigned char XZ_HEADER_MAGIC[6] = { 0xFD, '7', 'z', 'X', 'Z', 0x00 }; + +#define GetUi16(p, offset) (((unsigned char)p[offset+0]) | (((unsigned char)p[1]) << 8)) + +#define GetUi32(p, offset) ( \ + ((unsigned char)p[offset+0]) | \ + (((unsigned char)p[offset+1]) << 8) | \ + (((unsigned char)p[offset+2]) << 16) | \ + (((unsigned char)p[offset+3]) << 24)) + +#define GetUi64(p, offset) ((quint32)GetUi32(p, offset) | (((quint64)GetUi32(p, offset + 4)) << 32)) + +#define LZMA2_DIC_SIZE_FROM_PROP(p) (((quint32)2 | ((p) & 1)) << ((p) / 2 + 11)) + +#define FILE_ATTRIBUTE_READONLY 1 +#define FILE_ATTRIBUTE_HIDDEN 2 +#define FILE_ATTRIBUTE_SYSTEM 4 +#define FILE_ATTRIBUTE_DIRECTORY 16 +#define FILE_ATTRIBUTE_ARCHIVE 32 +#define FILE_ATTRIBUTE_DEVICE 64 +#define FILE_ATTRIBUTE_NORMAL 128 +#define FILE_ATTRIBUTE_TEMPORARY 256 +#define FILE_ATTRIBUTE_SPARSE_FILE 512 +#define FILE_ATTRIBUTE_REPARSE_POINT 1024 +#define FILE_ATTRIBUTE_COMPRESSED 2048 +#define FILE_ATTRIBUTE_OFFLINE 0x1000 +#define FILE_ATTRIBUTE_ENCRYPTED 0x4000 +#define FILE_ATTRIBUTE_UNIX_EXTENSION 0x8000 /* trick for Unix */ + +enum HeaderType { + kEnd, + + kHeader, + + kArchiveProperties, + + kAdditionalStreamsInfo, + kMainStreamsInfo, + kFilesInfo, + + kPackInfo, + kUnpackInfo, + kSubStreamsInfo, + + kSize, + kCRC, + + kFolder, + + kCodersUnpackSize, + kNumUnpackStream, + + kEmptyStream, + kEmptyFile, + kAnti, + + kName, + kCTime, + kATime, + kMTime, + kAttributes, + kComment, + + kEncodedHeader, + + kStartPos, + kDummy +}; + +// Method ID +// static const quint64 k_Copy = 0x00; +// static const quint64 k_Delta = 0x03; +// static const quint64 k_x86 = 0x04; //BCJ +// static const quint64 k_PPC = 0x05; // BIG Endian +// static const quint64 k_IA64 = 0x06; +// static const quint64 k_ARM = 0x07; // little Endian +// static const quint64 k_ARM_Thumb = 0x08; // little Endian +// static const quint64 k_SPARC = 0x09; +static const quint64 k_LZMA2 = 0x21; +// static const quint64 k_Swap2 = 0x020302; +// static const quint64 k_Swap4 = 0x020304; +static const quint64 k_LZMA = 0x030101; +static const quint64 k_BCJ = 0x03030103; +static const quint64 k_BCJ2 = 0x0303011B; +// static const quint64 k_7zPPC = 0x03030205; +// static const quint64 k_Alpha = 0x03030301; +// static const quint64 k_7zIA64 = 0x03030401; +// static const quint64 k_7zARM = 0x03030501; +// static const quint64 k_M68 = 0x03030605; //Big Endian +// static const quint64 k_ARMT = 0x03030701; +// static const quint64 k_7zSPARC = 0x03030805; +static const quint64 k_PPMD = 0x030401; +// static const quint64 k_Experimental = 0x037F01; +// static const quint64 k_Shrink = 0x040101; +// static const quint64 k_Implode = 0x040106; +// static const quint64 k_Deflate = 0x040108; +// static const quint64 k_Deflate64 = 0x040109; +// static const quint64 k_Imploding = 0x040110; +// static const quint64 k_Jpeg = 0x040160; +// static const quint64 k_WavPack = 0x040161; +// static const quint64 k_PPMd = 0x040162; +// static const quint64 k_wzAES = 0x040163; +static const quint64 k_BZip2 = 0x040202; +// static const quint64 k_Rar15 = 0x040301; +// static const quint64 k_Rar20 = 0x040302; +// static const quint64 k_Rar29 = 0x040303; +// static const quint64 k_Arj = 0x040401; //1 2 3 +// static const quint64 k_Arj4 = 0x040402; +// static const quint64 k_Z = 0x0405; +// static const quint64 k_Lzh = 0x0406; +// static const quint64 k_Cab = 0x0408; +// static const quint64 k_DeflateNSIS = 0x040901; +// static const quint64 k_Bzip2NSIS = 0x040902; +static const quint64 k_AES = 0x06F10701; + +/** + * A K7ZipFileEntry represents a file in a 7zip archive. + */ +class KARCHIVE_EXPORT K7ZipFileEntry : public KArchiveFile +{ +public: + K7ZipFileEntry(K7Zip *zip, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, const QString &symlink, + qint64 pos, qint64 size, const QByteArray &data); + + ~K7ZipFileEntry(); + + /** + * @return the content of this file. + * Call data() with care (only once per file), this data isn't cached. + */ + QByteArray data() const override; + + /** + * This method returns QIODevice (internal class: KLimitedIODevice) + * on top of the underlying QIODevice. This is obviously for reading only. + * + * WARNING: Note that the ownership of the device is being transferred to the caller, + * who will have to delete it. + * + * The returned device auto-opens (in readonly mode), no need to open it. + * @return the QIODevice of the file + */ + QIODevice *createDevice() const override; + +private: + const QByteArray m_data; + QBuffer *m_buffer; +}; + +K7ZipFileEntry::K7ZipFileEntry(K7Zip *zip, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, const QString &symlink, + qint64 pos, qint64 size, const QByteArray &data) + : KArchiveFile(zip, name, access, date, user, group, symlink, pos, size) + , m_data(data) + , m_buffer(new QBuffer) +{ + m_buffer->setData(m_data); + m_buffer->open(QIODevice::ReadOnly); +} + +K7ZipFileEntry::~K7ZipFileEntry() +{ + delete m_buffer; +} + +QByteArray K7ZipFileEntry::data() const +{ + return m_data.mid(position(), size()); +} + +QIODevice *K7ZipFileEntry::createDevice() const +{ + return new KLimitedIODevice(m_buffer, position(), size()); +} + +class FileInfo +{ +public: + FileInfo() + : size(0) + , attributes(0) + , crc(0) + , attribDefined(false) + , crcDefined(false) + , hasStream(false) + , isDir(false) + { + } + + QString path; + quint64 size; + quint32 attributes; + quint32 crc; + bool attribDefined; + bool crcDefined; + bool hasStream; + bool isDir; +}; + +class Folder +{ +public: + class FolderInfo + { + public: + FolderInfo() + : numInStreams(0) + , numOutStreams(0) + , methodID(0) + { + } + + bool isSimpleCoder() const + { + return (numInStreams == 1) && (numOutStreams == 1); + } + + int numInStreams; + int numOutStreams; + QVector properties; + quint64 methodID; + }; + + Folder() + : unpackCRCDefined(false) + , unpackCRC(0) + { + } + + ~Folder() + { + qDeleteAll(folderInfos); + } + + Q_DISABLE_COPY(Folder) + + quint64 getUnpackSize() const + { + if (unpackSizes.isEmpty()) { + return 0; + } + for (int i = unpackSizes.size() - 1; i >= 0; i--) { + if (findBindPairForOutStream(i) < 0) { + return unpackSizes.at(i); + } + } + return 0; + } + + int getNumOutStreams() const + { + int result = 0; + for (int i = 0; i < folderInfos.size(); i++) { + result += folderInfos.at(i)->numOutStreams; + } + return result; + } + + quint32 getCoderInStreamIndex(quint32 coderIndex) const + { + quint32 streamIndex = 0; + for (quint32 i = 0; i < coderIndex; i++) { + streamIndex += folderInfos.at(i)->numInStreams; + } + return streamIndex; + } + + quint32 getCoderOutStreamIndex(quint32 coderIndex) const + { + quint32 streamIndex = 0; + for (quint32 i = 0; i < coderIndex; i++) { + streamIndex += folderInfos.at(i)->numOutStreams; + } + return streamIndex; + } + + int findBindPairForInStream(size_t inStreamIndex) const + { + for (int i = 0; i < inIndexes.size(); i++) { + if (inIndexes[i] == inStreamIndex) { + return i; + } + } + return -1; + } + + int findBindPairForOutStream(size_t outStreamIndex) const + { + for (int i = 0; i < outIndexes.size(); i++) { + if (outIndexes[i] == outStreamIndex) { + return i; + } + } + return -1; + } + + int findPackStreamArrayIndex(size_t inStreamIndex) const + { + for (int i = 0; i < packedStreams.size(); i++) { + if (packedStreams[i] == inStreamIndex) { + return i; + } + } + return -1; + } + + void findInStream(quint32 streamIndex, quint32 &coderIndex, quint32 &coderStreamIndex) const + { + for (coderIndex = 0; coderIndex < (quint32)folderInfos.size(); coderIndex++) { + quint32 curSize = folderInfos[coderIndex]->numInStreams; + if (streamIndex < curSize) { + coderStreamIndex = streamIndex; + return; + } + streamIndex -= curSize; + } + } + + void findOutStream(quint32 streamIndex, quint32 &coderIndex, quint32 &coderStreamIndex) const + { + for (coderIndex = 0; coderIndex < (quint32)folderInfos.size(); coderIndex++) { + quint32 curSize = folderInfos[coderIndex]->numOutStreams; + if (streamIndex < curSize) { + coderStreamIndex = streamIndex; + return; + } + streamIndex -= curSize; + } + } + + bool isEncrypted() const + { + for (int i = folderInfos.size() - 1; i >= 0; i--) { + if (folderInfos.at(i)->methodID == k_AES) { + return true; + } + } + return false; + } + + //bool CheckStructure() const; + + bool unpackCRCDefined; + quint32 unpackCRC; + QVector folderInfos; + QVector inIndexes; + QVector outIndexes; + QVector packedStreams; + QVector unpackSizes; +}; + +class Q_DECL_HIDDEN K7Zip::K7ZipPrivate +{ +public: + K7ZipPrivate(K7Zip *parent) + : q(parent) + , packPos(0) + , numPackStreams(0) + , buffer(nullptr) + , pos(0) + , end(0) + , headerSize(0) + , countSize(0) + , m_currentFile(nullptr) + { + } + + ~K7ZipPrivate() + { + qDeleteAll(folders); + qDeleteAll(fileInfos); + } + + K7Zip *q; + + QVector packCRCsDefined; + QVector packCRCs; + QVector numUnpackStreamsInFolders; + + QVector folders; + QVector fileInfos; + // File informations + QVector cTimesDefined; + QVector cTimes; + QVector aTimesDefined; + QVector aTimes; + QVector mTimesDefined; + QVector mTimes; + QVector startPositionsDefined; + QVector startPositions; + QVector fileInfoPopIDs; + + quint64 packPos; + quint64 numPackStreams; + QVector packSizes; + QVector unpackSizes; + QVector digestsDefined; + QVector digests; + + QVector isAnti; + + const char *buffer; + quint64 pos; + quint64 end; + quint64 headerSize; + quint64 countSize; + + //Write + QByteArray header; + QByteArray outData; // Store data in this buffer before compress and write in archive. + K7ZipFileEntry *m_currentFile; + QVector m_entryList; + + void clear() + { + packCRCsDefined.clear(); + packCRCs.clear(); + numUnpackStreamsInFolders.clear(); + qDeleteAll(folders); + folders.clear(); + qDeleteAll(fileInfos); + fileInfos.clear(); + cTimesDefined.clear(); + cTimes.clear(); + aTimesDefined.clear(); + aTimes.clear(); + mTimesDefined.clear(); + mTimes.clear(); + startPositionsDefined.clear(); + startPositions.clear(); + fileInfoPopIDs.clear(); + packSizes.clear(); + unpackSizes.clear(); + digestsDefined.clear(); + digests.clear(); + isAnti.clear(); + + buffer = nullptr; + pos = 0; + end = 0; + headerSize = 0; + countSize = 0; + } + + // Read + int readByte(); + quint32 readUInt32(); + quint64 readUInt64(); + quint64 readNumber(); + QString readString(); + void readHashDigests(int numItems, QVector &digestsDefined, QVector &digests); + void readBoolVector(int numItems, QVector &v); + void readBoolVector2(int numItems, QVector &v); + void skipData(int size); + bool findAttribute(int attribute); + bool readUInt64DefVector(int numFiles, QVector &values, QVector &defined); + + Folder *folderItem(); + bool readMainStreamsInfo(); + bool readPackInfo(); + bool readUnpackInfo(); + bool readSubStreamsInfo(); + QByteArray readAndDecodePackedStreams(bool readMainStreamInfo = true); + + //Write + void createItemsFromEntities(const KArchiveDirectory *, const QString &, QByteArray &); + void writeByte(unsigned char b); + void writeNumber(quint64 value); + void writeBoolVector(const QVector &boolVector); + void writeUInt32(quint32 value); + void writeUInt64(quint64 value); + void writeHashDigests(const QVector &digestsDefined, const QVector &digests); + void writeAlignedBoolHeader(const QVector &v, int numDefined, int type, unsigned itemSize); + void writeUInt64DefVector(const QVector &v, const QVector &defined, int type); + void writeFolder(const Folder *folder); + void writePackInfo(quint64 dataOffset, QVector &packedSizes, QVector &packedCRCsDefined, QVector &packedCRCs); + void writeUnpackInfo(const QVector &folderItems); + void writeSubStreamsInfo(const QVector &unpackSizes, const QVector &digestsDefined, const QVector &digests); + void writeHeader(quint64 &headerOffset); + void writeSignature(); + void writeStartHeader(const quint64 nextHeaderSize, const quint32 nextHeaderCRC, const quint64 nextHeaderOffset); + QByteArray encodeStream(QVector &packSizes, QVector &folds); +}; + +K7Zip::K7Zip(const QString &fileName) + : KArchive(fileName) + , d(new K7ZipPrivate(this)) +{ +} + +K7Zip::K7Zip(QIODevice *dev) + : KArchive(dev) + , d(new K7ZipPrivate(this)) +{ + Q_ASSERT(dev); +} + +K7Zip::~K7Zip() +{ + if (isOpen()) { + close(); + } + + delete d; +} + +int K7Zip::K7ZipPrivate::readByte() +{ + if (!buffer || pos + 1 > end) { + return -1; + } + return buffer[pos++]; +} + +quint32 K7Zip::K7ZipPrivate::readUInt32() +{ + if (!buffer || (quint64)(pos + 4) > end) { + qCDebug(KArchiveLog) << "error size"; + return 0; + } + + quint32 res = GetUi32(buffer, pos); + pos += 4; + return res; +} + +quint64 K7Zip::K7ZipPrivate::readUInt64() +{ + if (!buffer || (quint64)(pos + 8) > end) { + qCDebug(KArchiveLog) << "error size"; + return 0; + } + + quint64 res = GetUi64(buffer, pos); + pos += 8; + return res; +} + +quint64 K7Zip::K7ZipPrivate::readNumber() +{ + if (!buffer || (quint64)(pos + 8) > end) { + return 0; + } + + unsigned char firstByte = buffer[pos++]; + unsigned char mask = 0x80; + quint64 value = 0; + for (int i = 0; i < 8; i++) { + if ((firstByte & mask) == 0) { + quint64 highPart = firstByte & (mask - 1); + value += (highPart << (i * 8)); + return value; + } + value |= ((unsigned char)buffer[pos++] << (8 * i)); + mask >>= 1; + } + return value; +} + +QString K7Zip::K7ZipPrivate::readString() +{ + if (!buffer) { + return QString(); + } + + const char *buf = buffer + pos; + size_t rem = (end - pos) / 2 * 2; + { + size_t i; + for (i = 0; i < rem; i += 2) { + if (buf[i] == 0 && buf[i + 1] == 0) { + break; + } + } + if (i == rem) { + qCDebug(KArchiveLog) << "read string error"; + return QString(); + } + rem = i; + } + + int len = (int)(rem / 2); + if (len < 0 || (size_t)len * 2 != rem) { + qCDebug(KArchiveLog) << "read string unsupported"; + return QString(); + } + + QString p; + for (int i = 0; i < len; i++, buf += 2) { + p += (wchar_t)GetUi16(buf, 0); + } + + pos += rem + 2; + return p; +} + +void K7Zip::K7ZipPrivate::skipData(int size) +{ + if (!buffer || pos + size > end) { + return; + } + pos += size; +} + +bool K7Zip::K7ZipPrivate::findAttribute(int attribute) +{ + if (!buffer) { + return false; + } + + for (;;) { + int type = readByte(); + if (type == attribute) { + return true; + } + if (type == kEnd) { + return false; + } + skipData(readNumber()); + } +} + +void K7Zip::K7ZipPrivate::readBoolVector(int numItems, QVector &v) +{ + if (!buffer) { + return; + } + + unsigned char b = 0; + unsigned char mask = 0; + for (int i = 0; i < numItems; i++) { + if (mask == 0) { + b = readByte(); + mask = 0x80; + } + v.append((b & mask) != 0); + mask >>= 1; + } +} + +void K7Zip::K7ZipPrivate::readBoolVector2(int numItems, QVector &v) +{ + if (!buffer) { + return; + } + + int allAreDefined = readByte(); + if (allAreDefined == 0) { + readBoolVector(numItems, v); + return; + } + + for (int i = 0; i < numItems; i++) { + v.append(true); + } +} + +void K7Zip::K7ZipPrivate::readHashDigests(int numItems, + QVector &digestsDefined, + QVector &digests) +{ + if (!buffer) { + return; + } + + readBoolVector2(numItems, digestsDefined); + for (int i = 0; i < numItems; i++) { + quint32 crc = 0; + if (digestsDefined[i]) { + crc = GetUi32(buffer, pos); + pos += 4; + } + digests.append(crc); + } +} + +Folder *K7Zip::K7ZipPrivate::folderItem() +{ + if (!buffer) { + return nullptr; + } + + Folder *folder = new Folder; + int numCoders = readNumber(); + + quint64 numInStreamsTotal = 0; + quint64 numOutStreamsTotal = 0; + for (int i = 0; i < numCoders; ++i) { + //BYTE + // { + // 0:3 CodecIdSize + // 4: Is Complex Coder + // 5: There Are Attributes + // 6: Reserved + // 7: There are more alternative methods. (Not used + // anymore, must be 0). + // } + unsigned char coderInfo = readByte(); + int codecIdSize = (coderInfo & 0xF); + if (codecIdSize > 8) { + qCDebug(KArchiveLog) << "unsupported codec id size"; + delete folder; + return nullptr; + } + Folder::FolderInfo *info = new Folder::FolderInfo(); + std::unique_ptr codecID(new unsigned char[codecIdSize]); + for (int i = 0; i < codecIdSize; ++i) { + codecID[i] = readByte(); + } + + int id = 0; + for (int j = 0; j < codecIdSize; j++) { + id |= codecID[codecIdSize - 1 - j] << (8 * j); + } + info->methodID = id; + + //if (Is Complex Coder) + if ((coderInfo & 0x10) != 0) { + info->numInStreams = readNumber(); + info->numOutStreams = readNumber(); + } else { + info->numInStreams = 1; + info->numOutStreams = 1; + } + + //if (There Are Attributes) + if ((coderInfo & 0x20) != 0) { + int propertiesSize = readNumber(); + for (int i = 0; i < propertiesSize; ++i) { + info->properties.append(readByte()); + } + } + + if ((coderInfo & 0x80) != 0) { + qCDebug(KArchiveLog) << "unsupported"; + delete info; + delete folder; + return nullptr; + } + + numInStreamsTotal += info->numInStreams; + numOutStreamsTotal += info->numOutStreams; + folder->folderInfos.append(info); + } + + int numBindPairs = numOutStreamsTotal - 1; + for (int i = 0; i < numBindPairs; i++) { + folder->inIndexes.append(readNumber()); + folder->outIndexes.append(readNumber()); + } + + int numPackedStreams = numInStreamsTotal - numBindPairs; + if (numPackedStreams > 1) { + for (int i = 0; i < numPackedStreams; ++i) { + folder->packedStreams.append(readNumber()); + } + } else { + if (numPackedStreams == 1) { + for (quint64 i = 0; i < numInStreamsTotal; i++) { + if (folder->findBindPairForInStream(i) < 0) { + folder->packedStreams.append(i); + break; + } + } + if (folder->packedStreams.size() != 1) { + delete folder; + return nullptr; + } + } + } + return folder; +} + +bool K7Zip::K7ZipPrivate::readUInt64DefVector(int numFiles, QVector &values, QVector &defined) +{ + if (!buffer) { + return false; + } + + readBoolVector2(numFiles, defined); + + int external = readByte(); + if (external != 0) { + int dataIndex = readNumber(); + if (dataIndex < 0 /*|| dataIndex >= dataVector->Size()*/) { + qCDebug(KArchiveLog) << "wrong data index"; + return false; + } + + // TODO : go to the new index + } + + for (int i = 0; i < numFiles; i++) { + quint64 t = 0; + if (defined[i]) { + t = readUInt64(); + } + values.append(t); + } + return true; +} + +bool K7Zip::K7ZipPrivate::readPackInfo() +{ + if (!buffer) { + return false; + } + + packPos = readNumber(); + numPackStreams = readNumber(); + packSizes.clear(); + + packCRCsDefined.clear(); + packCRCs.clear(); + + if (!findAttribute(kSize)) { + qCDebug(KArchiveLog) << "kSize not found"; + return false; + } + + for (quint64 i = 0; i < numPackStreams; ++i) { + packSizes.append(readNumber()); + } + + for (;;) { + int type = readByte(); + if (type == kEnd) { + break; + } + if (type == kCRC) { + readHashDigests(numPackStreams, packCRCsDefined, packCRCs); + continue; + } + skipData(readNumber()); + } + + if (packCRCs.isEmpty()) { + for (quint64 i = 0; i < numPackStreams; ++i) { + packCRCsDefined.append(false); + packCRCs.append(0); + } + } + return true; +} + +bool K7Zip::K7ZipPrivate::readUnpackInfo() +{ + if (!buffer) { + return false; + } + + if (!findAttribute(kFolder)) { + qCDebug(KArchiveLog) << "kFolder not found"; + return false; + } + + int numFolders = readNumber(); + qDeleteAll(folders); + folders.clear(); + int external = readByte(); + switch (external) { + case 0: { + for (int i = 0; i < numFolders; ++i) { + folders.append(folderItem()); + } + break; + } + case 1: { + int dataIndex = readNumber(); + if (dataIndex < 0 /*|| dataIndex >= dataVector->Size()*/) { + qCDebug(KArchiveLog) << "wrong data index"; + } + // TODO : go to the new index + break; + } + default: + qCDebug(KArchiveLog) << "external error"; + return false; + } + + if (!findAttribute(kCodersUnpackSize)) { + qCDebug(KArchiveLog) << "kCodersUnpackSize not found"; + return false; + } + + for (int i = 0; i < numFolders; ++i) { + Folder *folder = folders.at(i); + int numOutStreams = folder->getNumOutStreams(); + for (int j = 0; j < numOutStreams; ++j) { + folder->unpackSizes.append(readNumber()); + } + } + + for (;;) { + int type = readByte(); + if (type == kEnd) { + break; + } + if (type == kCRC) { + QVector crcsDefined; + QVector crcs; + readHashDigests(numFolders, crcsDefined, crcs); + for (int i = 0; i < numFolders; i++) { + Folder *folder = folders.at(i); + folder->unpackCRCDefined = crcsDefined[i]; + folder->unpackCRC = crcs[i]; + } + continue; + } + skipData(readNumber()); + } + return true; +} + +bool K7Zip::K7ZipPrivate::readSubStreamsInfo() +{ + if (!buffer) { + return false; + } + + numUnpackStreamsInFolders.clear(); + + int type; + for (;;) { + type = readByte(); + if (type == kNumUnpackStream) { + for (int i = 0; i < folders.size(); i++) { + numUnpackStreamsInFolders.append(readNumber()); + } + continue; + } + if (type == kCRC || type == kSize) { + break; + } + if (type == kEnd) { + break; + } + skipData(readNumber()); + } + + if (numUnpackStreamsInFolders.isEmpty()) { + for (int i = 0; i < folders.size(); i++) { + numUnpackStreamsInFolders.append(1); + } + } + + for (int i = 0; i < numUnpackStreamsInFolders.size(); i++) { + quint64 numSubstreams = numUnpackStreamsInFolders.at(i); + if (numSubstreams == 0) { + continue; + } + quint64 sum = 0; + for (quint64 j = 1; j < numSubstreams; j++) { + if (type == kSize) { + int size = readNumber(); + unpackSizes.append(size); + sum += size; + } + } + unpackSizes.append(folders.at(i)->getUnpackSize() - sum); + } + + if (type == kSize) { + type = readByte(); + } + + int numDigests = 0; + int numDigestsTotal = 0; + for (int i = 0; i < folders.size(); i++) { + quint64 numSubstreams = numUnpackStreamsInFolders.at(i); + if (numSubstreams != 1 || !folders.at(i)->unpackCRCDefined) { + numDigests += numSubstreams; + } + numDigestsTotal += numSubstreams; + } + + for (;;) { + if (type == kCRC) { + QVector digestsDefined2; + QVector digests2; + readHashDigests(numDigests, digestsDefined2, digests2); + int digestIndex = 0; + for (int i = 0; i < folders.size(); i++) { + quint64 numSubstreams = numUnpackStreamsInFolders.at(i); + const Folder *folder = folders.at(i); + if (numSubstreams == 1 && folder->unpackCRCDefined) { + digestsDefined.append(true); + digests.append(folder->unpackCRC); + } else { + for (quint64 j = 0; j < numSubstreams; j++, digestIndex++) { + digestsDefined.append(digestsDefined2[digestIndex]); + digests.append(digests2[digestIndex]); + } + } + } + } else if (type == kEnd) { + if (digestsDefined.isEmpty()) { + for (int i = 0; i < numDigestsTotal; i++) { + digestsDefined.append(false); + digests.append(0); + } + } + + break; + } else { + skipData(readNumber()); + } + + type = readByte(); + } + return true; +} + +#define TICKSPERSEC 10000000 +#define TICKSPERMSEC 10000 +#define SECSPERDAY 86400 +#define SECSPERHOUR 3600 +#define SECSPERMIN 60 +#define EPOCHWEEKDAY 1 /* Jan 1, 1601 was Monday */ +#define DAYSPERWEEK 7 +#define DAYSPERQUADRICENTENNIUM (365 * 400 + 97) +#define DAYSPERNORMALQUADRENNIUM (365 * 4 + 1) +#define TICKS_1601_TO_1970 (SECS_1601_TO_1970 * TICKSPERSEC) +#define SECS_1601_TO_1970 ((369 * 365 + 89) * (unsigned long long)SECSPERDAY) + +static uint toTimeT(const long long liTime) +{ + long long time = liTime / TICKSPERSEC; + + /* The native version of RtlTimeToTimeFields does not take leap seconds + * into account */ + + /* Split the time into days and seconds within the day */ + long int days = time / SECSPERDAY; + int secondsInDay = time % SECSPERDAY; + + /* compute time of day */ + short hour = (short)(secondsInDay / SECSPERHOUR); + secondsInDay = secondsInDay % SECSPERHOUR; + short minute = (short)(secondsInDay / SECSPERMIN); + short second = (short)(secondsInDay % SECSPERMIN); + + /* compute year, month and day of month. */ + long int cleaps = (3 * ((4 * days + 1227) / DAYSPERQUADRICENTENNIUM) + 3) / 4; + days += 28188 + cleaps; + long int years = (20 * days - 2442) / (5 * DAYSPERNORMALQUADRENNIUM); + long int yearday = days - (years * DAYSPERNORMALQUADRENNIUM) / 4; + long int months = (64 * yearday) / 1959; + /* the result is based on a year starting on March. + * To convert take 12 from Januari and Februari and + * increase the year by one. */ + + short month, year; + if (months < 14) { + month = (short)(months - 1); + year = (short)(years + 1524); + } else { + month = (short)(months - 13); + year = (short)(years + 1525); + } + /* calculation of day of month is based on the wonderful + * sequence of INT( n * 30.6): it reproduces the· + * 31-30-31-30-31-31 month lengths exactly for small n's */ + short day = (short)(yearday - (1959 * months) / 64); + + QDateTime t(QDate(year, month, day), QTime(hour, minute, second)); + t.setTimeSpec(Qt::UTC); + return t.toSecsSinceEpoch(); +} + +long long rtlSecondsSince1970ToSpecTime(quint32 seconds) +{ + long long secs = seconds * (long long)TICKSPERSEC + TICKS_1601_TO_1970; + return secs; +} + +bool K7Zip::K7ZipPrivate::readMainStreamsInfo() +{ + if (!buffer) { + return false; + } + + quint32 type; + for (;;) { + type = readByte(); + if (type > ((quint32)1 << 30)) { + qCDebug(KArchiveLog) << "type error"; + return false; + } + switch (type) { + case kEnd: + return true; + case kPackInfo: { + if (!readPackInfo()) { + qCDebug(KArchiveLog) << "error during read pack information"; + return false; + } + break; + } + case kUnpackInfo: { + if (!readUnpackInfo()) { + qCDebug(KArchiveLog) << "error during read pack information"; + return false; + } + break; + } + case kSubStreamsInfo: { + if (!readSubStreamsInfo()) { + qCDebug(KArchiveLog) << "error during read substreams information"; + return false; + } + break; + } + default: + qCDebug(KArchiveLog) << "Wrong type"; + return false; + } + } + + qCDebug(KArchiveLog) << "should not reach"; + return false; +} + +static bool getInStream(const Folder *folder, quint32 streamIndex, int &seqInStream, quint32 &coderIndex) +{ + for (int i = 0; i < folder->packedStreams.size(); i++) { + if (folder->packedStreams[i] == streamIndex) { + seqInStream = i; + return true; + } + } + + int binderIndex = folder->findBindPairForInStream(streamIndex); + if (binderIndex < 0) { + return false; + } + + quint32 coderStreamIndex; + folder->findOutStream(folder->outIndexes[binderIndex], + coderIndex, coderStreamIndex); + + quint32 startIndex = folder->getCoderInStreamIndex(coderIndex); + + if (folder->folderInfos[coderIndex]->numInStreams > 1) { + return false; + } + + for (int i = 0; i < (int)folder->folderInfos[coderIndex]->numInStreams; i++) { + getInStream(folder, startIndex + i, seqInStream, coderIndex); + } + + return true; +} + +static bool getOutStream(const Folder *folder, quint32 streamIndex, int &seqOutStream) +{ + QVector outStreams; + quint32 outStreamIndex = 0; + for (int i = 0; i < folder->folderInfos.size(); i++) { + const Folder::FolderInfo *coderInfo = folder->folderInfos.at(i); + + for (int j = 0; j < coderInfo->numOutStreams; j++, outStreamIndex++) { + if (folder->findBindPairForOutStream(outStreamIndex) < 0) { + outStreams.append(outStreamIndex); + } + } + } + + for (int i = 0; i < outStreams.size(); i++) { + if (outStreams[i] == streamIndex) { + seqOutStream = i; + return true; + } + } + + int binderIndex = folder->findBindPairForOutStream(streamIndex); + if (binderIndex < 0) { + return false; + } + + quint32 coderIndex, coderStreamIndex; + folder->findInStream(folder->inIndexes[binderIndex], + coderIndex, coderStreamIndex); + + quint32 startIndex = folder->getCoderOutStreamIndex(coderIndex); + + if (folder->folderInfos[coderIndex]->numOutStreams > 1) { + return false; + } + + for (int i = 0; i < (int)folder->folderInfos[coderIndex]->numOutStreams; i++) { + getOutStream(folder, startIndex + i, seqOutStream); + } + + return true; +} + +const int kNumTopBits = 24; +const quint32 kTopValue = (1 << kNumTopBits); + +class RangeDecoder +{ + int pos; + +public: + QByteArray stream; + quint32 range; + quint32 code; + + RangeDecoder() + : pos(0) + { + } + + unsigned char readByte() + { + return stream[pos++]; + } + + void normalize() + { + while (range < kTopValue) { + code = (code << 8) | readByte(); + range <<= 8; + } + } + + void setStream(const QByteArray &s) + { + stream = s; + } + + void init() + { + code = 0; + range = 0xFFFFFFFF; + for (int i = 0; i < 5; i++) { + code = (code << 8) | readByte(); + } + } + + quint32 getThreshold(quint32 total) + { + return (code) / (range /= total); + } + + void decode(quint32 start, quint32 size) + { + code -= start * range; + range *= size; + normalize(); + } + + quint32 decodeDirectBits(int numTotalBits) + { + quint32 r = range; + quint32 c = code; + quint32 result = 0; + for (int i = numTotalBits; i != 0; i--) { + r >>= 1; + quint32 t = (c - r) >> 31; + c -= r & (t - 1); + result = (result << 1) | (1 - t); + + if (r < kTopValue) { + c = (c << 8) | readByte(); + r <<= 8; + } + } + range = r; + code = c; + return result; + } + + quint32 DecodeBit(quint32 size0, quint32 numTotalBits) + { + quint32 newBound = (range >> numTotalBits) * size0; + quint32 symbol; + if (code < newBound) { + symbol = 0; + range = newBound; + } else { + symbol = 1; + code -= newBound; + range -= newBound; + } + normalize(); + return symbol; + } +}; + +const int kNumBitModelTotalBits = 11; +const quint32 kBitModelTotal = (1 << kNumBitModelTotalBits); + +template +class CBitModel +{ +public: + quint32 prob; + void updateModel(quint32 symbol) + { + if (symbol == 0) { + prob += (kBitModelTotal - prob) >> numMoveBits; + } else { + prob -= (prob) >> numMoveBits; + } + } + + void init() + { + prob = kBitModelTotal / 2; + } +}; + +template +class CBitDecoder : public CBitModel +{ +public: + quint32 decode(RangeDecoder *decoder) + { + quint32 newBound = (decoder->range >> kNumBitModelTotalBits) * this->prob; + if (decoder->code < newBound) { + decoder->range = newBound; + this->prob += (kBitModelTotal - this->prob) >> numMoveBits; + if (decoder->range < kTopValue) { + decoder->code = (decoder->code << 8) | decoder->readByte(); + decoder->range <<= 8; + } + return 0; + } else { + decoder->range -= newBound; + decoder->code -= newBound; + this->prob -= (this->prob) >> numMoveBits; + if (decoder->range < kTopValue) { + decoder->code = (decoder->code << 8) | decoder->readByte(); + decoder->range <<= 8; + } + return 1; + } + } +}; + +inline bool isJcc(unsigned char b0, unsigned char b1) +{ + return (b0 == 0x0F && (b1 & 0xF0) == 0x80); +} +inline bool isJ(unsigned char b0, unsigned char b1) +{ + return ((b1 & 0xFE) == 0xE8 || isJcc(b0, b1)); +} +inline unsigned getIndex(unsigned char b0, unsigned char b1) +{ + return ((b1 == 0xE8) ? b0 : ((b1 == 0xE9) ? 256 : 257)); +} + +const int kNumMoveBits = 5; + +static QByteArray decodeBCJ2(const QByteArray &mainStream, const QByteArray &callStream, const QByteArray &jumpStream, const QByteArray &rangeBuffer) +{ + unsigned char prevByte = 0; + QByteArray outStream; + int mainStreamPos = 0; + int callStreamPos = 0; + int jumpStreamPos = 0; + + RangeDecoder rangeDecoder; + rangeDecoder.setStream(rangeBuffer); + rangeDecoder.init(); + + QVector > statusDecoder(256 + 2); + + for (int i = 0; i < 256 + 2; i++) { + statusDecoder[i].init(); + } + + for (;;) { + quint32 i; + unsigned char b = 0; + const quint32 kBurstSize = (1 << 18); + for (i = 0; i < kBurstSize; i++) { + if (mainStreamPos == mainStream.size()) { + return outStream; + } + + b = mainStream[mainStreamPos++]; + outStream.append(b); + + if (isJ(prevByte, b)) { + break; + } + prevByte = b; + } + + if (i == kBurstSize) { + continue; + } + + unsigned index = getIndex(prevByte, b); + if (statusDecoder[index].decode(&rangeDecoder) == 1) { + if (b == 0xE8) { + if (callStreamPos + 4 > callStream.size()) { + return QByteArray(); + } + } else { + if (jumpStreamPos + 4 > jumpStream.size()) { + return QByteArray(); + } + } + quint32 src = 0; + for (int i = 0; i < 4; i++) { + unsigned char b0; + if (b == 0xE8) { + b0 = callStream[callStreamPos++]; + } else { + b0 = jumpStream[jumpStreamPos++]; + } + src <<= 8; + src |= ((quint32)b0); + } + + quint32 dest = src - (quint32(outStream.size()) + 4); + outStream.append((unsigned char)(dest)); + outStream.append((unsigned char)(dest >> 8)); + outStream.append((unsigned char)(dest >> 16)); + outStream.append((unsigned char)(dest >> 24)); + prevByte = (unsigned char)(dest >> 24); + } else { + prevByte = b; + } + } +} + +QByteArray K7Zip::K7ZipPrivate::readAndDecodePackedStreams(bool readMainStreamInfo) +{ + if (!buffer) { + return QByteArray(); + } + + if (readMainStreamInfo) { + readMainStreamsInfo(); + } + + QByteArray inflatedData; + + quint64 startPos = 32 + packPos; + for (int i = 0; i < folders.size(); i++) { + const Folder *folder = folders.at(i); + quint64 unpackSize64 = folder->getUnpackSize();; + size_t unpackSize = (size_t)unpackSize64; + if (unpackSize != unpackSize64) { + qCDebug(KArchiveLog) << "unsupported"; + return inflatedData; + } + + // Find main coder + quint32 mainCoderIndex = 0; + QVector outStreamIndexed; + int outStreamIndex = 0; + for (int j = 0; j < folder->folderInfos.size(); j++) { + const Folder::FolderInfo *info = folder->folderInfos[j]; + for (int k = 0; k < info->numOutStreams; k++, outStreamIndex++) { + if (folder->findBindPairForOutStream(outStreamIndex) < 0) { + outStreamIndexed.append(outStreamIndex); + break; + } + } + } + + quint32 temp = 0; + if (!outStreamIndexed.isEmpty()) { + folder->findOutStream(outStreamIndexed[0], mainCoderIndex, temp); + } + + quint32 startInIndex = folder->getCoderInStreamIndex(mainCoderIndex); + quint32 startOutIndex = folder->getCoderOutStreamIndex(mainCoderIndex); + + Folder::FolderInfo *mainCoder = folder->folderInfos[mainCoderIndex]; + + QVector seqInStreams; + QVector coderIndexes; + seqInStreams.reserve(mainCoder->numInStreams); + coderIndexes.reserve(mainCoder->numInStreams); + for (int j = 0; j < (int)mainCoder->numInStreams; j++) { + int seqInStream; + quint32 coderIndex; + getInStream(folder, startInIndex + j, seqInStream, coderIndex); + seqInStreams.append(seqInStream); + coderIndexes.append(coderIndex); + } + + QVector seqOutStreams; + seqOutStreams.reserve(mainCoder->numOutStreams); + for (int j = 0; j < (int)mainCoder->numOutStreams; j++) { + int seqOutStream; + getOutStream(folder, startOutIndex + j, seqOutStream); + seqOutStreams.append(seqOutStream); + } + + QVector datas; + for (int j = 0; j < (int)mainCoder->numInStreams; j++) { + int size = packSizes[j + i]; + std::unique_ptr encodedBuffer(new char[size]); + QIODevice *dev = q->device(); + dev->seek(startPos); + quint64 n = dev->read(encodedBuffer.get(), size); + if (n != (quint64)size) { + qCDebug(KArchiveLog) << "Failed read next size, should read " << size << ", read " << n; + return inflatedData; + } + QByteArray deflatedData(encodedBuffer.get(), size); + datas.append(deflatedData); + startPos += size; + pos += size; + headerSize += size; + } + + QVector inflatedDatas; + QByteArray deflatedData; + for (int j = 0; j < seqInStreams.size(); ++j) { + Folder::FolderInfo *coder = nullptr; + if ((quint32)j != mainCoderIndex) { + coder = folder->folderInfos[coderIndexes[j]]; + } else { + coder = folder->folderInfos[mainCoderIndex]; + } + + deflatedData = datas[seqInStreams[j]]; + + KFilterBase *filter = nullptr; + + switch (coder->methodID) { + case k_LZMA: + filter = KCompressionDevice::filterForCompressionType(KCompressionDevice::Xz); + if (!filter) { + qCDebug(KArchiveLog) << "filter not found"; + return QByteArray(); + } + static_cast(filter)->init(QIODevice::ReadOnly, KXzFilter::LZMA, coder->properties); + break; + case k_LZMA2: + filter = KCompressionDevice::filterForCompressionType(KCompressionDevice::Xz); + if (!filter) { + qCDebug(KArchiveLog) << "filter not found"; + return QByteArray(); + } + static_cast(filter)->init(QIODevice::ReadOnly, KXzFilter::LZMA2, coder->properties); + break; + case k_PPMD: { + /*if (coder->properties.size() == 5) { + //Byte order = *(const Byte *)coder.Props; + qint32 dicSize = ((unsigned char)coder->properties[1] | + (((unsigned char)coder->properties[2]) << 8) | + (((unsigned char)coder->properties[3]) << 16) | + (((unsigned char)coder->properties[4]) << 24)); + }*/ + break; + } + case k_AES: + if (coder->properties.size() >= 1) { + //const Byte *data = (const Byte *)coder.Props; + //Byte firstByte = *data++; + //UInt32 numCyclesPower = firstByte & 0x3F; + } + break; + case k_BCJ: + filter = KCompressionDevice::filterForCompressionType(KCompressionDevice::Xz); + if (!filter) { + qCDebug(KArchiveLog) << "filter not found"; + return QByteArray(); + } + static_cast(filter)->init(QIODevice::ReadOnly, KXzFilter::BCJ, coder->properties); + break; + case k_BCJ2: { + QByteArray bcj2 = decodeBCJ2(inflatedDatas[0], inflatedDatas[1], inflatedDatas[2], deflatedData); + inflatedDatas.clear(); + inflatedDatas.append(bcj2); + break; + } + case k_BZip2: + filter = KCompressionDevice::filterForCompressionType(KCompressionDevice::BZip2); + if (!filter) { + qCDebug(KArchiveLog) << "filter not found"; + return QByteArray(); + } + filter->init(QIODevice::ReadOnly); + break; + } + + if (coder->methodID == k_BCJ2) { + continue; + } + + if (!filter) { + return QByteArray(); + } + + filter->setInBuffer(deflatedData.data(), deflatedData.size()); + + QByteArray outBuffer; + // reserve memory + outBuffer.resize(unpackSize); + + KFilterBase::Result result = KFilterBase::Ok; + QByteArray inflatedDataTmp; + while (result != KFilterBase::End && result != KFilterBase::Error && !filter->inBufferEmpty()) { + filter->setOutBuffer(outBuffer.data(), outBuffer.size()); + result = filter->uncompress(); + if (result == KFilterBase::Error) { + qCDebug(KArchiveLog) << " decode error"; + filter->terminate(); + delete filter; + return QByteArray(); + } + int uncompressedBytes = outBuffer.size() - filter->outBufferAvailable(); + + // append the uncompressed data to inflate buffer + inflatedDataTmp.append(outBuffer.data(), uncompressedBytes); + + if (result == KFilterBase::End) { + //qCDebug(KArchiveLog) << "Finished unpacking"; + break; // Finished. + } + } + + if (result != KFilterBase::End && !filter->inBufferEmpty()) { + qCDebug(KArchiveLog) << "decode failed result" << result; + filter->terminate(); + delete filter; + return QByteArray(); + } + + filter->terminate(); + delete filter; + + inflatedDatas.append(inflatedDataTmp); + } + + QByteArray inflated; + for (const QByteArray& data : qAsConst(inflatedDatas)) { + inflated.append(data); + } + + inflatedDatas.clear(); + + if (folder->unpackCRCDefined) { + if ((size_t)inflated.size() < unpackSize) { + qCDebug(KArchiveLog) << "wrong crc size data"; + return QByteArray(); + } + quint32 crc = crc32(0, (Bytef *)(inflated.data()), unpackSize); + if (crc != folder->unpackCRC) { + qCDebug(KArchiveLog) << "wrong crc"; + return QByteArray(); + } + } + + inflatedData.append(inflated); + } + + return inflatedData; +} + +///////////////// Write //////////////////// + +void K7Zip::K7ZipPrivate::createItemsFromEntities(const KArchiveDirectory *dir, const QString &path, QByteArray &data) +{ + const QStringList l = dir->entries(); + QStringList::ConstIterator it = l.begin(); + for (; it != l.end(); ++it) { + const KArchiveEntry *entry = dir->entry((*it)); + + FileInfo *fileInfo = new FileInfo; + fileInfo->attribDefined = true; + + fileInfo->path = path + entry->name(); + mTimesDefined.append(true); + mTimes.append(rtlSecondsSince1970ToSpecTime(entry->date().toSecsSinceEpoch())); + + if (entry->isFile()) { + const K7ZipFileEntry *fileEntry = static_cast(entry); + + fileInfo->attributes = FILE_ATTRIBUTE_ARCHIVE; + fileInfo->attributes |= FILE_ATTRIBUTE_UNIX_EXTENSION + ((entry->permissions() & 0xFFFF) << 16); + fileInfo->size = fileEntry->size(); + QString symLink = fileEntry->symLinkTarget(); + if (fileInfo->size > 0) { + fileInfo->hasStream = true; + data.append(outData.mid(fileEntry->position(), fileEntry->size())); + unpackSizes.append(fileInfo->size); + } else if (!symLink.isEmpty()) { + fileInfo->hasStream = true; + data.append(symLink.toUtf8()); + unpackSizes.append(symLink.size()); + } + fileInfos.append(fileInfo); + } else if (entry->isDirectory()) { + fileInfo->attributes = FILE_ATTRIBUTE_DIRECTORY; + fileInfo->attributes |= FILE_ATTRIBUTE_UNIX_EXTENSION + ((entry->permissions() & 0xFFFF) << 16); + fileInfo->isDir = true; + fileInfos.append(fileInfo); + createItemsFromEntities((KArchiveDirectory *)entry, path + (*it) + QLatin1Char('/'), data); + + } + } +} + +void K7Zip::K7ZipPrivate::writeByte(unsigned char b) +{ + header.append(b); + countSize++; +} + +void K7Zip::K7ZipPrivate::writeNumber(quint64 value) +{ + int firstByte = 0; + short mask = 0x80; + int i; + for (i = 0; i < 8; i++) { + if (value < ((quint64(1) << (7 * (i + 1))))) { + firstByte |= (int)(value >> (8 * i)); + break; + } + firstByte |= mask; + mask >>= 1; + } + writeByte(firstByte); + for (; i > 0; i--) { + writeByte((int)value); + value >>= 8; + } +} + +void K7Zip::K7ZipPrivate::writeBoolVector(const QVector &boolVector) +{ + int b = 0; + short mask = 0x80; + for (int i = 0; i < boolVector.size(); i++) { + if (boolVector[i]) { + b |= mask; + } + mask >>= 1; + if (mask == 0) { + writeByte(b); + mask = 0x80; + b = 0; + } + } + if (mask != 0x80) { + writeByte(b); + } +} + +void K7Zip::K7ZipPrivate::writeUInt32(quint32 value) +{ + for (int i = 0; i < 4; i++) { + writeByte((unsigned char)value); + value >>= 8; + } +} + +void K7Zip::K7ZipPrivate::writeUInt64(quint64 value) +{ + for (int i = 0; i < 8; i++) { + writeByte((unsigned char)value); + value >>= 8; + } +} + +void K7Zip::K7ZipPrivate::writeAlignedBoolHeader(const QVector &v, int numDefined, int type, unsigned itemSize) +{ + const unsigned bvSize = (numDefined == v.size()) ? 0 : ((unsigned)v.size() + 7) / 8; + const quint64 dataSize = (quint64)numDefined * itemSize + bvSize + 2; + //SkipAlign(3 + (unsigned)bvSize + (unsigned)GetBigNumberSize(dataSize), itemSize); + + writeByte(type); + writeNumber(dataSize); + if (numDefined == v.size()) { + writeByte(1); + } else { + writeByte(0); + writeBoolVector(v); + } + writeByte(0); +} + +void K7Zip::K7ZipPrivate::writeUInt64DefVector(const QVector &v, const QVector &defined, int type) +{ + int numDefined = 0; + + for (int i = 0; i < defined.size(); i++) { + if (defined[i]) { + numDefined++; + } + } + + if (numDefined == 0) { + return; + } + + writeAlignedBoolHeader(defined, numDefined, type, 8); + + for (int i = 0; i < defined.size(); i++) { + if (defined[i]) { + writeUInt64(v[i]); + } + } +} + +void K7Zip::K7ZipPrivate::writeHashDigests( + const QVector &digestsDefined, + const QVector &digests) +{ + int numDefined = 0; + int i; + for (i = 0; i < digestsDefined.size(); i++) { + if (digestsDefined[i]) { + numDefined++; + } + } + + if (numDefined == 0) { + return; + } + + writeByte(kCRC); + if (numDefined == digestsDefined.size()) { + writeByte(1); + } else { + writeByte(0); + writeBoolVector(digestsDefined); + } + + for (i = 0; i < digests.size(); i++) { + if (digestsDefined[i]) { + writeUInt32(digests[i]); + } + } +} + +void K7Zip::K7ZipPrivate::writePackInfo(quint64 dataOffset, QVector &packedSizes, QVector &packedCRCsDefined, QVector &packedCRCs) +{ + if (packedSizes.isEmpty()) { + return; + } + writeByte(kPackInfo); + writeNumber(dataOffset); + writeNumber(packedSizes.size()); + writeByte(kSize); + + for (int i = 0; i < packedSizes.size(); i++) { + writeNumber(packedSizes[i]); + } + + writeHashDigests(packedCRCsDefined, packedCRCs); + + writeByte(kEnd); +} + +void K7Zip::K7ZipPrivate::writeFolder(const Folder *folder) +{ + writeNumber(folder->folderInfos.size()); + for (int i = 0; i < folder->folderInfos.size(); i++) { + const Folder::FolderInfo *info = folder->folderInfos.at(i); + { + size_t propsSize = info->properties.size(); + + quint64 id = info->methodID; + size_t idSize; + for (idSize = 1; idSize < sizeof (id); idSize++) { + if ((id >> (8 * idSize)) == 0) { + break; + } + } + + int longID[15]; + for (int t = idSize - 1; t >= 0; t--, id >>= 8) { + longID[t] = (int)(id & 0xFF); + } + + int b; + b = (int)(idSize & 0xF); + bool isComplex = !info->isSimpleCoder(); + b |= (isComplex ? 0x10 : 0); + b |= ((propsSize != 0) ? 0x20 : 0); + + writeByte(b); + for (size_t j = 0; j < idSize; ++j) { + writeByte(longID[j]); + } + + if (isComplex) { + writeNumber(info->numInStreams); + writeNumber(info->numOutStreams); + } + + if (propsSize == 0) { + continue; + } + + writeNumber(propsSize); + for (size_t j = 0; j < propsSize; ++j) { + writeByte(info->properties[j]); + } + } + } + + for (int i = 0; i < folder->inIndexes.size(); i++) { + writeNumber(folder->inIndexes[i]); + writeNumber(folder->outIndexes[i]); + } + + if (folder->packedStreams.size() > 1) { + for (int i = 0; i < folder->packedStreams.size(); i++) { + writeNumber(folder->packedStreams[i]); + } + } +} + +void K7Zip::K7ZipPrivate::writeUnpackInfo(const QVector &folderItems) +{ + if (folderItems.isEmpty()) { + return; + } + + writeByte(kUnpackInfo); + + writeByte(kFolder); + writeNumber(folderItems.size()); + { + writeByte(0); + for (int i = 0; i < folderItems.size(); i++) { + writeFolder(folderItems[i]); + } + } + + writeByte(kCodersUnpackSize); + int i; + for (i = 0; i < folderItems.size(); i++) { + const Folder *folder = folderItems[i]; + for (int j = 0; j < folder->unpackSizes.size(); j++) { + writeNumber(folder->unpackSizes.at(j)); + } + } + + QVector unpackCRCsDefined; + QVector unpackCRCs; + unpackCRCsDefined.reserve(folderItems.size()); + unpackCRCs.reserve(folderItems.size()); + for (i = 0; i < folderItems.size(); i++) { + const Folder *folder = folderItems[i]; + unpackCRCsDefined.append(folder->unpackCRCDefined); + unpackCRCs.append(folder->unpackCRC); + } + writeHashDigests(unpackCRCsDefined, unpackCRCs); + + writeByte(kEnd); +} + +void K7Zip::K7ZipPrivate::writeSubStreamsInfo( + const QVector &unpackSizes, + const QVector &digestsDefined, + const QVector &digests) +{ + writeByte(kSubStreamsInfo); + + for (int i = 0; i < numUnpackStreamsInFolders.size(); i++) { + if (numUnpackStreamsInFolders.at(i) != 1) { + writeByte(kNumUnpackStream); + for (int j = 0; j < numUnpackStreamsInFolders.size(); j++) { + writeNumber(numUnpackStreamsInFolders.at(j)); + } + break; + } + } + + bool needFlag = true; + int index = 0; + for (int i = 0; i < numUnpackStreamsInFolders.size(); i++) { + for (quint32 j = 0; j < numUnpackStreamsInFolders.at(i); j++) { + if (j + 1 != numUnpackStreamsInFolders.at(i)) { + if (needFlag) { + writeByte(kSize); + } + needFlag = false; + writeNumber(unpackSizes[index]); + } + index++; + } + } + + QVector digestsDefined2; + QVector digests2; + + int digestIndex = 0; + for (int i = 0; i < folders.size(); i++) { + int numSubStreams = (int)numUnpackStreamsInFolders.at(i); + if (numSubStreams == 1 && folders.at(i)->unpackCRCDefined) { + digestIndex++; + } else { + for (int j = 0; j < numSubStreams; j++, digestIndex++) { + digestsDefined2.append(digestsDefined[digestIndex]); + digests2.append(digests[digestIndex]); + } + } + } + writeHashDigests(digestsDefined2, digests2); + writeByte(kEnd); +} + +QByteArray K7Zip::K7ZipPrivate::encodeStream(QVector &packSizes, QVector &folds) +{ + Folder *folder = new Folder; + folder->unpackCRCDefined = true; + folder->unpackCRC = crc32(0, (Bytef *)(header.data()), header.size()); + folder->unpackSizes.append(header.size()); + + Folder::FolderInfo *info = new Folder::FolderInfo(); + info->numInStreams = 1; + info->numOutStreams = 1; + info->methodID = k_LZMA2; + + quint32 dictSize = header.size(); + const quint32 kMinReduceSize = (1 << 16); + if (dictSize < kMinReduceSize) { + dictSize = kMinReduceSize; + } + + int dict; + for (dict = 0; dict < 40; dict++) { + if (dictSize <= LZMA2_DIC_SIZE_FROM_PROP(dict)) { + break; + } + } + + info->properties.append(dict); + folder->folderInfos.append(info); + + folds.append(folder); + + //compress data + QByteArray encodedData; + if (!header.isEmpty()) { + QByteArray enc; + QBuffer inBuffer(&enc); + + KCompressionDevice flt(&inBuffer, false, KCompressionDevice::Xz); + flt.open(QIODevice::WriteOnly); + + KFilterBase *filter = flt.filterBase(); + + static_cast(filter)->init(QIODevice::WriteOnly, KXzFilter::LZMA2, info->properties); + + const int ret = flt.write(header); + if (ret != header.size()) { + qCDebug(KArchiveLog) << "write error write " << ret << "expected" << header.size(); + return encodedData; + } + + flt.close(); + encodedData = inBuffer.data(); + } + + packSizes.append(encodedData.size()); + return encodedData; +} + +void K7Zip::K7ZipPrivate::writeHeader(quint64 &headerOffset) +{ + quint64 packedSize = 0; + for (int i = 0; i < packSizes.size(); ++i) { + packedSize += packSizes[i]; + } + + headerOffset = packedSize; + + writeByte(kHeader); + + // Archive Properties + + if (!folders.isEmpty()) { + writeByte(kMainStreamsInfo); + writePackInfo(0, packSizes, packCRCsDefined, packCRCs); + + writeUnpackInfo(folders); + + QVector unpackFileSizes; + QVector digestsDefined; + QVector digests; + for (int i = 0; i < fileInfos.size(); i++) { + const FileInfo *file = fileInfos.at(i); + if (!file->hasStream) { + continue; + } + unpackFileSizes.append(file->size); + digestsDefined.append(file->crcDefined); + digests.append(file->crc); + } + + writeSubStreamsInfo(unpackSizes, digestsDefined, digests); + writeByte(kEnd); + } + + if (fileInfos.isEmpty()) { + writeByte(kEnd); + return; + } + + writeByte(kFilesInfo); + writeNumber(fileInfos.size()); + + { + /* ---------- Empty Streams ---------- */ + QVector emptyStreamVector; + int numEmptyStreams = 0; + for (int i = 0; i < fileInfos.size(); i++) { + if (fileInfos.at(i)->hasStream) { + emptyStreamVector.append(false); + } else { + emptyStreamVector.append(true); + numEmptyStreams++; + } + } + + if (numEmptyStreams > 0) { + writeByte(kEmptyStream); + writeNumber(((unsigned)emptyStreamVector.size() + 7) / 8); + writeBoolVector(emptyStreamVector); + + QVector emptyFileVector, antiVector; + int numEmptyFiles = 0, numAntiItems = 0; + for (int i = 0; i < fileInfos.size(); i++) { + const FileInfo *file = fileInfos.at(i); + if (!file->hasStream) { + emptyFileVector.append(!file->isDir); + if (!file->isDir) { + numEmptyFiles++; + bool isAnti = (i < this->isAnti.size() && this->isAnti[i]); + antiVector.append(isAnti); + if (isAnti) { + numAntiItems++; + } + } + } + } + + if (numEmptyFiles > 0) { + writeByte(kEmptyFile); + writeNumber(((unsigned)emptyFileVector.size() + 7) / 8); + writeBoolVector(emptyFileVector); + } + + if (numAntiItems > 0) { + writeByte(kAnti); + writeNumber(((unsigned)antiVector.size() + 7) / 8); + writeBoolVector(antiVector); + } + } + } + + { + /* ---------- Names ---------- */ + + int numDefined = 0; + size_t namesDataSize = 0; + for (int i = 0; i < fileInfos.size(); i++) { + const QString &name = fileInfos.at(i)->path; + if (!name.isEmpty()) { + numDefined++; + namesDataSize += (name.length() + 1) * 2; + } + } + + if (numDefined > 0) { + namesDataSize++; + //SkipAlign(2 + GetBigNumberSize(namesDataSize), 2); + + writeByte(kName); + writeNumber(namesDataSize); + writeByte(0); + for (int i = 0; i < fileInfos.size(); i++) { + const QString &name = fileInfos.at(i)->path; + for (int t = 0; t < name.length(); t++) { + wchar_t c = name[t].toLatin1(); + writeByte((unsigned char)c); + writeByte((unsigned char)(c >> 8)); + } + // End of string + writeByte(0); + writeByte(0); + } + } + } + + writeUInt64DefVector(mTimes, mTimesDefined, kMTime); + + writeUInt64DefVector(startPositions, startPositionsDefined, kStartPos); + + { + /* ---------- Write Attrib ---------- */ + QVector boolVector; + int numDefined = 0; + boolVector.reserve(fileInfos.size()); + for (int i = 0; i < fileInfos.size(); i++) { + bool defined = fileInfos.at(i)->attribDefined; + boolVector.append(defined); + if (defined) { + numDefined++; + } + } + + if (numDefined > 0) { + writeAlignedBoolHeader(boolVector, numDefined, kAttributes, 4); + for (int i = 0; i < fileInfos.size(); i++) { + const FileInfo *file = fileInfos.at(i); + if (file->attribDefined) { + writeUInt32(file->attributes); + } + } + } + } + + writeByte(kEnd); // for files + writeByte(kEnd); // for headers*/ +} + +static void setUInt32(unsigned char *p, quint32 d) +{ + for (int i = 0; i < 4; i++, d >>= 8) { + p[i] = (unsigned)d; + } +} + +static void setUInt64(unsigned char *p, quint64 d) +{ + for (int i = 0; i < 8; i++, d >>= 8) { + p[i] = (unsigned char)d; + } +} + +void K7Zip::K7ZipPrivate::writeStartHeader(const quint64 nextHeaderSize, const quint32 nextHeaderCRC, const quint64 nextHeaderOffset) +{ + unsigned char buf[24]; + setUInt64(buf + 4, nextHeaderOffset); + setUInt64(buf + 12, nextHeaderSize); + setUInt32(buf + 20, nextHeaderCRC); + setUInt32(buf, crc32(0, (Bytef *)(buf + 4), 20)); + q->device()->write((char *)buf, 24); +} + +void K7Zip::K7ZipPrivate::writeSignature() +{ + unsigned char buf[8]; + memcpy(buf, k7zip_signature, 6); + buf[6] = 0/*kMajorVersion*/; + buf[7] = 3; + q->device()->write((char *)buf, 8); +} + +bool K7Zip::openArchive(QIODevice::OpenMode mode) +{ + if (!(mode & QIODevice::ReadOnly)) { + return true; + } + + QIODevice *dev = device(); + + if (!dev) { + setErrorString(tr("Could not get underlying device")); + return false; + } + + char header[32]; + // check signature + qint64 n = dev->read(header, 32); + if (n != 32) { + setErrorString(tr("Read header failed")); + return false; + } + + for (int i = 0; i < 6; ++i) { + if ((unsigned char)header[i] != k7zip_signature[i]) { + setErrorString(tr("Check signature failed")); + return false; + } + } + + // get Archive Version + int major = header[6]; + int minor = header[7]; + + /*if (major > 0 || minor > 2) { + qCDebug(KArchiveLog) << "wrong archive version"; + return false; + }*/ + + // get Start Header CRC + quint32 startHeaderCRC = GetUi32(header, 8); + quint64 nextHeaderOffset = GetUi64(header, 12); + quint64 nextHeaderSize = GetUi64(header, 20); + quint32 nextHeaderCRC = GetUi32(header, 28); + + quint32 crc = crc32(0, (Bytef *)(header + 0xC), 20); + + if (crc != startHeaderCRC) { + setErrorString(tr("Bad CRC")); + return false; + } + + if (nextHeaderSize == 0) { + return true; + } + + if (nextHeaderSize > (quint64)0xFFFFFFFF) { + setErrorString(tr("Next header size is too big")); + return false; + } + + if ((qint64)nextHeaderOffset < 0) { + setErrorString(tr("Next header size is less than zero")); + return false; + } + + dev->seek(nextHeaderOffset + 32); + + QByteArray inBuffer; + inBuffer.resize(nextHeaderSize); + + n = dev->read(inBuffer.data(), inBuffer.size()); + if (n != (qint64)nextHeaderSize) { + setErrorString( + tr("Failed read next header size; should read %1, read %2") + .arg(nextHeaderSize).arg(n)); + return false; + } + d->buffer = inBuffer.data(); + d->end = nextHeaderSize; + + d->headerSize = 32 + nextHeaderSize; + //int physSize = 32 + nextHeaderSize + nextHeaderOffset; + + crc = crc32(0, (Bytef *)(d->buffer), (quint32)nextHeaderSize); + + if (crc != nextHeaderCRC) { + setErrorString(tr("Bad next header CRC")); + return false; + } + + int type = d->readByte(); + QByteArray decodedData; + if (type != kHeader) { + if (type != kEncodedHeader) { + setErrorString(tr("Error in header")); + return false; + } + + decodedData = d->readAndDecodePackedStreams(); + + int external = d->readByte(); + if (external != 0) { + int dataIndex = (int)d->readNumber(); + if (dataIndex < 0) { + //qCDebug(KArchiveLog) << "dataIndex error"; + } + d->buffer = decodedData.constData(); + d->pos = 0; + d->end = decodedData.size(); + } + + type = d->readByte(); + if (type != kHeader) { + setErrorString(tr("Wrong header type")); + return false; + } + } + // read header + + type = d->readByte(); + + if (type == kArchiveProperties) { + // TODO : implement this part + setErrorString(tr("Not implemented")); + return false; + } + + if (type == kAdditionalStreamsInfo) { + // TODO : implement this part + setErrorString(tr("Not implemented")); + return false; + } + + if (type == kMainStreamsInfo) { + if (!d->readMainStreamsInfo()) { + setErrorString(tr("Error while reading main streams information")); + return false; + } + type = d->readByte(); + } else { + for (int i = 0; i < d->folders.size(); ++i) { + Folder *folder = d->folders.at(i); + d->unpackSizes.append(folder->getUnpackSize()); + d->digestsDefined.append(folder->unpackCRCDefined); + d->digests.append(folder->unpackCRC); + } + } + + if (type == kEnd) { + return true; + } + + if (type != kFilesInfo) { + setErrorString(tr("Error while reading header")); + return false; + } + + //read files info + int numFiles = d->readNumber(); + for (int i = 0; i < numFiles; ++i) { + d->fileInfos.append(new FileInfo); + } + + QVector emptyStreamVector; + QVector emptyFileVector; + QVector antiFileVector; + int numEmptyStreams = 0; + + for (;;) { + quint64 type = d->readByte(); + if (type == kEnd) { + break; + } + + quint64 size = d->readNumber(); + + size_t ppp = d->pos; + + bool addPropIdToList = true; + bool isKnownType = true; + + if (type > ((quint32)1 << 30)) { + isKnownType = false; + } else { + switch (type) { + case kEmptyStream: { + d->readBoolVector(numFiles, emptyStreamVector); + for (int i = 0; i < emptyStreamVector.size(); ++i) { + if (emptyStreamVector[i]) { + numEmptyStreams++; + } + } + + break; + } + case kEmptyFile: + d->readBoolVector(numEmptyStreams, emptyFileVector); + break; + case kAnti: + d->readBoolVector(numEmptyStreams, antiFileVector); + break; + case kCTime: + if (!d->readUInt64DefVector(numFiles, d->cTimes, d->cTimesDefined)) { + return false; + } + break; + case kATime: + if (!d->readUInt64DefVector(numFiles, d->aTimes, d->aTimesDefined)) { + return false; + } + break; + case kMTime: + if (!d->readUInt64DefVector(numFiles, d->mTimes, d->mTimesDefined)) { + setErrorString(tr("Error reading modification time")); + return false; + } + break; + case kName: { + int external = d->readByte(); + if (external != 0) { + int dataIndex = d->readNumber(); + if (dataIndex < 0 /*|| dataIndex >= dataVector->Size()*/) { + qCDebug(KArchiveLog) << "wrong data index"; + } + + // TODO : go to the new index + } + + QString name; + for (int i = 0; i < numFiles; i++) { + name = d->readString(); + d->fileInfos.at(i)->path = name; + } + break; + } + case kAttributes: { + QVector attributesAreDefined; + d->readBoolVector2(numFiles, attributesAreDefined); + int external = d->readByte(); + if (external != 0) { + int dataIndex = d->readNumber(); + if (dataIndex < 0) { + qCDebug(KArchiveLog) << "wrong data index"; + } + + // TODO : go to the new index + } + + for (int i = 0; i < numFiles; i++) { + FileInfo *fileInfo = d->fileInfos.at(i); + fileInfo->attribDefined = attributesAreDefined[i]; + if (fileInfo->attribDefined) { + fileInfo->attributes = d->readUInt32(); + } + } + break; + } + case kStartPos: + if (!d->readUInt64DefVector(numFiles, d->startPositions, d->startPositionsDefined)) { + setErrorString(tr("Error reading MTime")); + return false; + } + break; + case kDummy: { + for (quint64 i = 0; i < size; i++) { + if (d->readByte() != 0) { + setErrorString(tr("Invalid")); + return false; + } + } + addPropIdToList = false; + break; + } + default: + addPropIdToList = isKnownType = false; + } + } + + if (isKnownType) { + if (addPropIdToList) { + d->fileInfoPopIDs.append(type); + } + } else { + d->skipData(d->readNumber()); + } + + bool checkRecordsSize = (major > 0 || + minor > 2); + if (checkRecordsSize && d->pos - ppp != size) { + setErrorString( + tr( + "Read size failed " + "(checkRecordsSize: %1, d->pos - ppp: %2, size: %3)") + .arg(checkRecordsSize).arg(d->pos - ppp).arg(size)); + return false; + } + } + + int emptyFileIndex = 0; + int sizeIndex = 0; + + int numAntiItems = 0; + + if (emptyStreamVector.isEmpty()) { + emptyStreamVector.fill(false, numFiles); + } + + if (antiFileVector.isEmpty()) { + antiFileVector.fill(false, numEmptyStreams); + } + if (emptyFileVector.isEmpty()) { + emptyFileVector.fill(false, numEmptyStreams); + } + + for (int i = 0; i < numEmptyStreams; i++) { + if (antiFileVector[i]) { + numAntiItems++; + } + } + + d->outData = d->readAndDecodePackedStreams(false); + + int oldPos = 0; + for (int i = 0; i < numFiles; i++) { + FileInfo *fileInfo = d->fileInfos.at(i); + bool isAnti; + fileInfo->hasStream = !emptyStreamVector[i]; + if (fileInfo->hasStream) { + fileInfo->isDir = false; + isAnti = false; + fileInfo->size = d->unpackSizes[sizeIndex]; + fileInfo->crc = d->digests[sizeIndex]; + fileInfo->crcDefined = d->digestsDefined[sizeIndex]; + sizeIndex++; + } else { + fileInfo->isDir = !emptyFileVector[emptyFileIndex]; + isAnti = antiFileVector[emptyFileIndex]; + emptyFileIndex++; + fileInfo->size = 0; + fileInfo->crcDefined = false; + } + if (numAntiItems != 0) { + d->isAnti.append(isAnti); + } + + int access; + bool symlink = false; + if (fileInfo->attributes & FILE_ATTRIBUTE_UNIX_EXTENSION) { + access = fileInfo->attributes >> 16; + if ((access & QT_STAT_MASK) == QT_STAT_LNK) { + symlink = true; + } + } else { + if (fileInfo->isDir) { + access = S_IFDIR | 0755; + } else { + access = 0100644; + } + } + + qint64 pos = 0; + if (!fileInfo->isDir) { + pos = oldPos; + oldPos += fileInfo->size; + } + + KArchiveEntry *e; + QString entryName; + int index = fileInfo->path.lastIndexOf(QLatin1Char('/')); + if (index == -1) { + entryName = fileInfo->path; + } else { + entryName = fileInfo->path.mid(index + 1); + } + Q_ASSERT(!entryName.isEmpty()); + + QDateTime mTime; + if (d->mTimesDefined[i]) { + mTime = KArchivePrivate::time_tToDateTime(toTimeT(d->mTimes[i])); + } else { + mTime = KArchivePrivate::time_tToDateTime(time(nullptr)); + } + + if (fileInfo->isDir) { + QString path = QDir::cleanPath(fileInfo->path); + const KArchiveEntry *ent = rootDir()->entry(path); + if (ent && ent->isDirectory()) { + e = nullptr; + } else { + e = new KArchiveDirectory(this, entryName, access, mTime, rootDir()->user(), rootDir()->group(), QString()/*symlink*/); + } + } else { + if (!symlink) { + e = new K7ZipFileEntry(this, entryName, access, mTime, rootDir()->user(), rootDir()->group(), QString()/*symlink*/, pos, fileInfo->size, d->outData); + } else { + QString target = QFile::decodeName(d->outData.mid(pos, fileInfo->size)); + e = new K7ZipFileEntry(this, entryName, access, mTime, rootDir()->user(), rootDir()->group(), target, 0, 0, nullptr); + } + } + + if (e) { + if (index == -1) { + rootDir()->addEntry(e); + } else { + QString path = QDir::cleanPath(fileInfo->path.left(index)); + KArchiveDirectory *d = findOrCreate(path); + d->addEntry(e); + } + } + } + + return true; +} + +bool K7Zip::closeArchive() +{ + // Unnecessary check (already checked by KArchive::close()) + if (!isOpen()) { + //qCWarning(KArchiveLog) << "You must open the file before close it\n"; + return false; + } + + if ((mode() == QIODevice::ReadOnly)) { + return true; + } + + d->clear(); + + Folder *folder = new Folder(); + + folder->unpackSizes.clear(); + folder->unpackSizes.append(d->outData.size()); + + Folder::FolderInfo *info = new Folder::FolderInfo(); + + info->numInStreams = 1; + info->numOutStreams = 1; + info->methodID = k_LZMA2; + + quint32 dictSize = d->outData.size(); + + const quint32 kMinReduceSize = (1 << 16); + if (dictSize < kMinReduceSize) { + dictSize = kMinReduceSize; + } + + // k_LZMA2 mehtod + int dict; + for (dict = 0; dict < 40; dict++) { + if (dictSize <= LZMA2_DIC_SIZE_FROM_PROP(dict)) { + break; + } + } + info->properties.append(dict); + + folder->folderInfos.append(info); + d->folders.append(folder); + + const KArchiveDirectory *dir = directory(); + QByteArray data; + d->createItemsFromEntities(dir, QString(), data); + d->outData = data; + + folder->unpackCRCDefined = true; + folder->unpackCRC = crc32(0, (Bytef *)(d->outData.data()), d->outData.size()); + + //compress data + QByteArray encodedData; + if (!d->outData.isEmpty()) { + QByteArray enc; + QBuffer inBuffer(&enc); + + KCompressionDevice flt(&inBuffer, false, KCompressionDevice::Xz); + flt.open(QIODevice::WriteOnly); + + KFilterBase *filter = flt.filterBase(); + + static_cast(filter)->init(QIODevice::WriteOnly, KXzFilter::LZMA2, info->properties); + + const int ret = flt.write(d->outData); + if (ret != d->outData.size()) { + setErrorString(tr("Write error")); + return false; + } + + flt.close(); + encodedData = inBuffer.data(); + } + + d->packSizes.append(encodedData.size()); + + int numUnpackStream = 0; + for (int i = 0; i < d->fileInfos.size(); ++i) { + if (d->fileInfos.at(i)->hasStream) { + numUnpackStream++; + } + } + d->numUnpackStreamsInFolders.append(numUnpackStream); + + quint64 headerOffset; + d->writeHeader(headerOffset); + + // Encode Header + QByteArray encodedStream; + { + QVector packSizes; + QVector folders; + encodedStream = d->encodeStream(packSizes, folders); + + if (folders.isEmpty()) { + // FIXME Not sure why this is an error. Come up with a better message + setErrorString(tr("Failed while encoding header")); + return false; + } + + d->header.clear(); + + d->writeByte(kEncodedHeader); + QVector emptyDefined; + QVector emptyCrcs; + d->writePackInfo(headerOffset, packSizes, emptyDefined, emptyCrcs); + d->writeUnpackInfo(folders); + d->writeByte(kEnd); + for (int i = 0; i < packSizes.size(); i++) { + headerOffset += packSizes.at(i); + } + qDeleteAll(folders); + } + // end encode header + + quint64 nextHeaderSize = d->header.size(); + quint32 nextHeaderCRC = crc32(0, (Bytef *)(d->header.data()), d->header.size()); + quint64 nextHeaderOffset = headerOffset; + + device()->seek(0); + d->writeSignature(); + d->writeStartHeader(nextHeaderSize, nextHeaderCRC, nextHeaderOffset); + device()->write(encodedData.data(), encodedData.size()); + device()->write(encodedStream.data(), encodedStream.size()); + device()->write(d->header.data(), d->header.size()); + + return true; +} + +bool K7Zip::doFinishWriting(qint64 size) +{ + + d->m_currentFile->setSize(size); + d->m_currentFile = nullptr; + + return true; +} + +bool K7Zip::writeData(const char *data, qint64 size) +{ + if (!d->m_currentFile) { + setErrorString(tr("No file currently selected")); + return false; + } + + if (d->m_currentFile->position() == d->outData.size()) { + d->outData.append(data, size); + } else { + d->outData.remove(d->m_currentFile->position(), d->m_currentFile->size()); + d->outData.insert(d->m_currentFile->position(), data, size); + } + + return true; +} + +bool K7Zip::doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 /*size*/, mode_t perm, + const QDateTime & /*atime*/, const QDateTime &mtime, const QDateTime & /*ctime*/) +{ + if (!isOpen()) { + setErrorString(tr("Application error: 7-Zip file must be open before being written into")); + qCWarning(KArchiveLog) << "doPrepareWriting failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { + setErrorString(tr("Application error: attempted to write into non-writable 7-Zip file")); + qCWarning(KArchiveLog) << "doPrepareWriting failed: !(mode() & QIODevice::WriteOnly)"; + return false; + } + + // Find or create parent dir + KArchiveDirectory *parentDir = rootDir(); + //QString fileName( name ); + // In some files we can find dir/./file => call cleanPath + QString fileName(QDir::cleanPath(name)); + int i = name.lastIndexOf(QLatin1Char('/')); + if (i != -1) { + QString dir = name.left(i); + fileName = name.mid(i + 1); + parentDir = findOrCreate(dir); + } + + // test if the entry already exist + const KArchiveEntry *entry = parentDir->entry(fileName); + if (!entry) { + K7ZipFileEntry *e = new K7ZipFileEntry(this, fileName, perm, mtime, user, group, QString()/*symlink*/, d->outData.size(), 0 /*unknown yet*/, d->outData); + if (!parentDir->addEntryV2(e)) + return false; + d->m_entryList << e; + d->m_currentFile = e; + } else { + // TODO : find and replace in m_entryList + //d->m_currentFile = static_cast(entry); + } + + return true; +} + +bool K7Zip::doWriteDir(const QString &name, const QString &user, + const QString &group, mode_t perm, + const QDateTime & /*atime*/, const QDateTime &mtime, const QDateTime & /*ctime*/) +{ + if (!isOpen()) { + setErrorString(tr("Application error: 7-Zip file must be open before being written into")); + qCWarning(KArchiveLog) << "doWriteDir failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { + //qCWarning(KArchiveLog) << "You must open the tar file for writing\n"; + return false; + } + + // In some tar files we can find dir/./ => call cleanPath + QString dirName(QDir::cleanPath(name)); + + // Remove trailing '/' + if (dirName.endsWith(QLatin1Char('/'))) { + dirName.remove(dirName.size() - 1, 1); + } + + KArchiveDirectory *parentDir = rootDir(); + int i = dirName.lastIndexOf(QLatin1Char('/')); + if (i != -1) { + QString dir = name.left(i); + dirName = name.mid(i + 1); + parentDir = findOrCreate(dir); + } + + KArchiveDirectory *e = new KArchiveDirectory(this, dirName, perm, mtime, user, group, QString()/*symlink*/); + parentDir->addEntry(e); + + return true; +} + +bool K7Zip::doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime & /*atime*/, const QDateTime &mtime, const QDateTime & /*ctime*/) +{ + if (!isOpen()) { + setErrorString(tr("Application error: 7-Zip file must be open before being written into")); + qCWarning(KArchiveLog) << "doWriteSymLink failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { + setErrorString(tr("Application error: attempted to write into non-writable 7-Zip file")); + qCWarning(KArchiveLog) << "doWriteSymLink failed: !(mode() & QIODevice::WriteOnly)"; + return false; + } + + // Find or create parent dir + KArchiveDirectory *parentDir = rootDir(); + // In some files we can find dir/./file => call cleanPath + QString fileName(QDir::cleanPath(name)); + int i = name.lastIndexOf(QLatin1Char('/')); + if (i != -1) { + QString dir = name.left(i); + fileName = name.mid(i + 1); + parentDir = findOrCreate(dir); + } + QByteArray encodedTarget = QFile::encodeName(target); + + K7ZipFileEntry *e = new K7ZipFileEntry(this, fileName, perm, mtime, user, group, target, 0, 0, nullptr); + d->outData.append(encodedTarget); + + if (!parentDir->addEntryV2(e)) + return false; + + d->m_entryList << e; + + return true; +} + +void K7Zip::virtual_hook(int id, void *data) +{ + KArchive::virtual_hook(id, data); +} diff --git a/src/karchive/src/k7zip.h b/src/karchive/src/k7zip.h new file mode 100644 index 0000000000..d10d0fd47a --- /dev/null +++ b/src/karchive/src/k7zip.h @@ -0,0 +1,82 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2011 Mario Bensi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef K7ZIP_H +#define K7ZIP_H + +#include + +/** + * @class K7Zip k7zip.h K7Zip + * + * A class for reading / writing p7zip archives. + * + * @author Mario Bensi + */ +class KARCHIVE_EXPORT K7Zip : public KArchive +{ + Q_DECLARE_TR_FUNCTIONS(K7Zip) + +public: + /** + * Creates an instance that operates on the given filename + * using the compression filter associated to given mimetype. + * + * @param filename is a local path (e.g. "/home/user/myfile.7z") + */ + explicit K7Zip(const QString &filename); + + /** + * Creates an instance that operates on the given device. + * The device can be compressed (KFilterDev) or not (QFile, etc.). + * @warning Do not assume that giving a QFile here will decompress the file, + * in case it's compressed! + * @param dev the device to read from. If the source is compressed, the + * QIODevice must take care of decompression + */ + explicit K7Zip(QIODevice *dev); + + /** + * If the archive is still opened, then it will be + * closed automatically by the destructor. + */ + virtual ~K7Zip(); + +protected: + + /// Reimplemented from KArchive + bool doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doWriteDir(const QString &name, const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doFinishWriting(qint64 size) override; + + /// Reimplemented from KArchive + bool writeData(const char *data, qint64 size) override; + + /** + * Opens the archive for reading. + * Parses the directory listing of the archive + * and creates the KArchiveDirectory/KArchiveFile entries. + * @param mode the mode of the file + */ + bool openArchive(QIODevice::OpenMode mode) override; + bool closeArchive() override; + +protected: + void virtual_hook(int id, void *data) override; +private: + class K7ZipPrivate; + K7ZipPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/kar.cpp b/src/karchive/src/kar.cpp new file mode 100644 index 0000000000..5da86182a1 --- /dev/null +++ b/src/karchive/src/kar.cpp @@ -0,0 +1,192 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2002 Laurence Anderson + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "kar.h" +#include "karchive_p.h" +#include "loggingcategory.h" + +#include +#include + +#include + +#include "kfilterdev.h" +//#include "klimitediodevice_p.h" + +// As documented in QByteArray +static constexpr int kMaxQByteArraySize = std::numeric_limits::max() - 32; + +//////////////////////////////////////////////////////////////////////// +/////////////////////////// KAr /////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class Q_DECL_HIDDEN KAr::KArPrivate +{ +public: + KArPrivate() + { + } +}; + +KAr::KAr(const QString &filename) + : KArchive(filename) + , d(new KArPrivate) +{ +} + +KAr::KAr(QIODevice *dev) + : KArchive(dev) + , d(new KArPrivate) +{ +} + +KAr::~KAr() +{ + if (isOpen()) { + close(); + } + delete d; +} + +bool KAr::doPrepareWriting(const QString &, const QString &, const QString &, + qint64, mode_t, const QDateTime &, const QDateTime &, const QDateTime &) +{ + setErrorString(tr("Cannot write to AR file")); + qCWarning(KArchiveLog) << "doPrepareWriting not implemented for KAr"; + return false; +} + +bool KAr::doFinishWriting(qint64) +{ + setErrorString(tr("Cannot write to AR file")); + qCWarning(KArchiveLog) << "doFinishWriting not implemented for KAr"; + return false; +} + +bool KAr::doWriteDir(const QString &, const QString &, const QString &, + mode_t, const QDateTime &, const QDateTime &, const QDateTime &) +{ + setErrorString(tr("Cannot write to AR file")); + qCWarning(KArchiveLog) << "doWriteDir not implemented for KAr"; + return false; +} + +bool KAr::doWriteSymLink(const QString &, const QString &, const QString &, + const QString &, mode_t, const QDateTime &, const QDateTime &, const QDateTime &) +{ + setErrorString(tr("Cannot write to AR file")); + qCWarning(KArchiveLog) << "doWriteSymLink not implemented for KAr"; + return false; +} + +bool KAr::openArchive(QIODevice::OpenMode mode) +{ + // Open archive + + if (mode == QIODevice::WriteOnly) { + return true; + } + if (mode != QIODevice::ReadOnly && mode != QIODevice::ReadWrite) { + setErrorString(tr("Unsupported mode %1").arg(mode)); + return false; + } + + QIODevice *dev = device(); + if (!dev) { + return false; + } + + QByteArray magic = dev->read(7); + if (magic != "!") { + setErrorString(tr("Invalid main magic")); + return false; + } + + QByteArray ar_longnames; + while (! dev->atEnd()) { + QByteArray ar_header; + ar_header.resize(60); + + dev->seek(dev->pos() + (2 - (dev->pos() % 2)) % 2); // Ar headers are padded to byte boundary + + if (dev->read(ar_header.data(), 60) != 60) { // Read ar header + qCWarning(KArchiveLog) << "Couldn't read header"; + return true; // Probably EOF / trailing junk + } + + if (!ar_header.endsWith("`\n")) { // Check header magic // krazy:exclude=strings + setErrorString(tr("Invalid magic")); + return false; + } + + QByteArray name = ar_header.mid(0, 16); // Process header + const int date = ar_header.mid(16, 12).trimmed().toInt(); + //const int uid = ar_header.mid( 28, 6 ).trimmed().toInt(); + //const int gid = ar_header.mid( 34, 6 ).trimmed().toInt(); + const int mode = ar_header.mid(40, 8).trimmed().toInt(nullptr, 8); + const qint64 size = ar_header.mid(48, 10).trimmed().toInt(); + if (size < 0 || size > kMaxQByteArraySize) { + setErrorString(tr("Invalid size")); + return false; + } + + bool skip_entry = false; // Deal with special entries + if (name.mid(0, 1) == "/") { + if (name.mid(1, 1) == "/") { // Longfilename table entry + ar_longnames.resize(size); + // Read the table. Note that the QByteArray will contain NUL characters after each entry. + dev->read(ar_longnames.data(), size); + skip_entry = true; + qCDebug(KArchiveLog) << "Read in longnames entry"; + } else if (name.mid(1, 1) == " ") { // Symbol table entry + qCDebug(KArchiveLog) << "Skipped symbol entry"; + dev->seek(dev->pos() + size); + skip_entry = true; + } else { // Longfilename, look it up in the table + const int ar_longnamesIndex = name.mid(1, 15).trimmed().toInt(); + qCDebug(KArchiveLog) << "Longfilename #" << ar_longnamesIndex; + if (ar_longnames.isEmpty()) { + setErrorString(tr("Invalid longfilename reference")); + return false; + } + if (ar_longnamesIndex < 0 || ar_longnamesIndex >= ar_longnames.size()) { + setErrorString(tr("Invalid longfilename position reference")); + return false; + } + name = QByteArray(ar_longnames.constData() + ar_longnamesIndex); + name.truncate(name.indexOf('/')); + } + } + if (skip_entry) { + continue; + } + + // Process filename + name = name.trimmed(); + name.replace('/', QByteArray()); + qCDebug(KArchiveLog) << "Filename: " << name << " Size: " << size; + + KArchiveEntry *entry = new KArchiveFile(this, QString::fromLocal8Bit(name.constData()), mode, KArchivePrivate::time_tToDateTime(date), + rootDir()->user(), rootDir()->group(), /*symlink*/ QString(), + dev->pos(), size); + rootDir()->addEntry(entry); // Ar files don't support directories, so everything in root + + dev->seek(dev->pos() + size); // Skip contents + } + + return true; +} + +bool KAr::closeArchive() +{ + // Close the archive + return true; +} + +void KAr::virtual_hook(int id, void *data) +{ + KArchive::virtual_hook(id, data); +} diff --git a/src/karchive/src/kar.h b/src/karchive/src/kar.h new file mode 100644 index 0000000000..758444786f --- /dev/null +++ b/src/karchive/src/kar.h @@ -0,0 +1,87 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2002 Laurence Anderson + + SPDX-License-Identifier: LGPL-2.0-only +*/ +#ifndef KAR_H +#define KAR_H + +#include + +/** + * @class KAr kar.h KAr + * + * KAr is a class for reading archives in ar format. Writing + * is not supported. Reading archives that contain files bigger than + * INT_MAX - 32 bytes is not supported. + * @short A class for reading ar archives. + * @author Laurence Anderson + */ +class KARCHIVE_EXPORT KAr : public KArchive +{ + Q_DECLARE_TR_FUNCTIONS(KAr) + +public: + /** + * Creates an instance that operates on the given filename. + * + * @param filename is a local path (e.g. "/home/holger/myfile.ar") + */ + KAr(const QString &filename); + + /** + * Creates an instance that operates on the given device. + * The device can be compressed (KFilterDev) or not (QFile, etc.). + * @param dev the device to read from + */ + KAr(QIODevice *dev); + + /** + * If the ar file is still opened, then it will be + * closed automatically by the destructor. + */ + virtual ~KAr(); + +protected: + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doPrepareWriting(const QString &name, const QString &user, const QString &group, qint64 size, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doFinishWriting(qint64 size) override; + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doWriteDir(const QString &name, const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + + bool doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + + /** + * Opens the archive for reading. + * Parses the directory listing of the archive + * and creates the KArchiveDirectory/KArchiveFile entries. + * + */ + bool openArchive(QIODevice::OpenMode mode) override; + bool closeArchive() override; + +protected: + void virtual_hook(int id, void *data) override; +private: + class KArPrivate; + KArPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/karchive.cpp b/src/karchive/src/karchive.cpp new file mode 100644 index 0000000000..e45223abf8 --- /dev/null +++ b/src/karchive/src/karchive.cpp @@ -0,0 +1,1025 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + Moved from ktar.cpp by Roberto Teixeira + + SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only +*/ + +#include "karchive.h" +#include "karchive_p.h" +#include "klimitediodevice_p.h" +#include "loggingcategory.h" + +#include // QT_STATBUF, QT_LSTAT + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef Q_OS_UNIX +#include +#include +#include // PATH_MAX +#include +#endif +#ifdef Q_OS_WIN +#include // DWORD, GetUserNameW +#endif // Q_OS_WIN + +//////////////////////////////////////////////////////////////////////// +/////////////////// KArchiveDirectoryPrivate /////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class KArchiveDirectoryPrivate +{ +public: + KArchiveDirectoryPrivate(KArchiveDirectory *parent) : q(parent) + { + } + + ~KArchiveDirectoryPrivate() + { + qDeleteAll(entries); + } + + KArchiveDirectoryPrivate(const KArchiveDirectoryPrivate &) = delete; + KArchiveDirectoryPrivate &operator=(const KArchiveDirectoryPrivate &) = delete; + + static KArchiveDirectoryPrivate *get(KArchiveDirectory *directory) + { + return directory->d; + } + + // Returns in containingDirectory the directory that actually contains the returned entry + const KArchiveEntry *entry(const QString &_name, KArchiveDirectory **containingDirectory) const + { + *containingDirectory = q; + + QString name = QDir::cleanPath(_name); + int pos = name.indexOf(QLatin1Char('/')); + if (pos == 0) { // ouch absolute path (see also KArchive::findOrCreate) + if (name.length() > 1) { + name = name.mid(1); // remove leading slash + pos = name.indexOf(QLatin1Char('/')); // look again + } else { // "/" + return q; + } + } + // trailing slash ? -> remove + if (pos != -1 && pos == name.length() - 1) { + name = name.left(pos); + pos = name.indexOf(QLatin1Char('/')); // look again + } + if (pos != -1) { + const QString left = name.left(pos); + const QString right = name.mid(pos + 1); + + //qCDebug(KArchiveLog) << "left=" << left << "right=" << right; + + KArchiveEntry *e = entries.value(left); + if (!e || !e->isDirectory()) { + return nullptr; + } + *containingDirectory = static_cast(e); + return (*containingDirectory)->d->entry(right, containingDirectory); + } + + return entries.value(name); + } + + KArchiveDirectory *q; + QHash entries; +}; + +//////////////////////////////////////////////////////////////////////// +/////////////////////////// KArchive /////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +KArchive::KArchive(const QString &fileName) + : d(new KArchivePrivate(this)) +{ + if (fileName.isEmpty()) { + qCWarning(KArchiveLog) << "KArchive: No file name specified"; + } + d->fileName = fileName; + // This constructor leaves the device set to 0. + // This is for the use of QSaveFile, see open(). +} + +KArchive::KArchive(QIODevice *dev) + : d(new KArchivePrivate(this)) +{ + if (!dev) { + qCWarning(KArchiveLog) << "KArchive: Null device specified"; + } + d->dev = dev; +} + +KArchive::~KArchive() +{ + Q_ASSERT(!isOpen()); // the derived class destructor must have closed already + delete d; +} + +bool KArchive::open(QIODevice::OpenMode mode) +{ + Q_ASSERT(mode != QIODevice::NotOpen); + + if (isOpen()) { + close(); + } + + if (!d->fileName.isEmpty()) { + Q_ASSERT(!d->dev); + if (!createDevice(mode)) { + return false; + } + } + + if (!d->dev) { + setErrorString(tr("No filename or device was specified")); + return false; + } + + if (!d->dev->isOpen() && !d->dev->open(mode)) { + setErrorString(tr("Could not set device mode to %1").arg(mode)); + return false; + } + + d->mode = mode; + + Q_ASSERT(!d->rootDir); + d->rootDir = nullptr; + + return openArchive(mode); +} + +bool KArchive::createDevice(QIODevice::OpenMode mode) +{ + switch (mode) { + case QIODevice::WriteOnly: + if (!d->fileName.isEmpty()) { + // The use of QSaveFile can't be done in the ctor (no mode known yet) + //qCDebug(KArchiveLog) << "Writing to a file using QSaveFile"; + d->saveFile = new QSaveFile(d->fileName); +#ifdef Q_OS_ANDROID + // we cannot rename on to Android content: URLs + if (d->fileName.startsWith(QLatin1String("content://"))) { + d->saveFile->setDirectWriteFallback(true); + } +#endif + if (!d->saveFile->open(QIODevice::WriteOnly)) { + setErrorString( + tr("QSaveFile creation for %1 failed: %2") + .arg(d->fileName, d->saveFile->errorString())); + + delete d->saveFile; + d->saveFile = nullptr; + return false; + } + d->dev = d->saveFile; + Q_ASSERT(d->dev); + } + break; + case QIODevice::ReadOnly: + case QIODevice::ReadWrite: + // ReadWrite mode still uses QFile for now; we'd need to copy to the tempfile, in fact. + if (!d->fileName.isEmpty()) { + d->dev = new QFile(d->fileName); + d->deviceOwned = true; + } + break; // continued below + default: + setErrorString(tr("Unsupported mode %1").arg(d->mode)); + return false; + } + return true; +} + +bool KArchive::close() +{ + if (!isOpen()) { + setErrorString(tr("Archive already closed")); + return false; // already closed (return false or true? arguable...) + } + + // moved by holger to allow kzip to write the zip central dir + // to the file in closeArchive() + // DF: added d->dev so that we skip closeArchive if saving aborted. + bool closeSucceeded = true; + if (d->dev) { + closeSucceeded = closeArchive(); + if (d->mode == QIODevice::WriteOnly && !closeSucceeded) { + d->abortWriting(); + } + } + + if (d->dev && d->dev != d->saveFile) { + d->dev->close(); + } + + // if d->saveFile is not null then it is equal to d->dev. + if (d->saveFile) { + closeSucceeded = d->saveFile->commit(); + delete d->saveFile; + d->saveFile = nullptr; + } + if (d->deviceOwned) { + delete d->dev; // we created it ourselves in open() + } + + delete d->rootDir; + d->rootDir = nullptr; + d->mode = QIODevice::NotOpen; + d->dev = nullptr; + return closeSucceeded; +} + +QString KArchive::errorString() const +{ + return d->errorStr; +} + +const KArchiveDirectory *KArchive::directory() const +{ + // rootDir isn't const so that parsing-on-demand is possible + return const_cast(this)->rootDir(); +} + +bool KArchive::addLocalFile(const QString &fileName, const QString &destName) +{ + QFileInfo fileInfo(fileName); + if (!fileInfo.isFile() && !fileInfo.isSymLink()) { + setErrorString( + tr("%1 doesn't exist or is not a regular file.") + .arg(fileName)); + return false; + } + +#if defined(Q_OS_UNIX) +#define STAT_METHOD QT_LSTAT +#else +#define STAT_METHOD QT_STAT +#endif + QT_STATBUF fi; + if (STAT_METHOD(QFile::encodeName(fileName).constData(), &fi) == -1) { + setErrorString( + tr("Failed accessing the file %1 for adding to the archive. The error was: %2") + .arg(fileName) + .arg(QLatin1String{strerror(errno)})); + return false; + } + + if (fileInfo.isSymLink()) { + QString symLinkTarget; + // Do NOT use fileInfo.symLinkTarget() for unix symlinks! + // It returns the -full- path to the target, while we want the target string "as is". +#if defined(Q_OS_UNIX) && !defined(Q_OS_OS2EMX) + const QByteArray encodedFileName = QFile::encodeName(fileName); + QByteArray s; +#if defined(PATH_MAX) + s.resize(PATH_MAX + 1); +#else + int path_max = pathconf(encodedFileName.data(), _PC_PATH_MAX); + if (path_max <= 0) { + path_max = 4096; + } + s.resize(path_max); +#endif + int len = readlink(encodedFileName.data(), s.data(), s.size() - 1); + if (len >= 0) { + s[len] = '\0'; + symLinkTarget = QFile::decodeName(s.constData()); + } +#endif + if (symLinkTarget.isEmpty()) { // Mac or Windows + symLinkTarget = fileInfo.symLinkTarget(); + } + return writeSymLink(destName, symLinkTarget, fileInfo.owner(), + fileInfo.group(), fi.st_mode, fileInfo.lastRead(), fileInfo.lastModified(), + fileInfo.birthTime()); + }/*end if*/ + + qint64 size = fileInfo.size(); + + // the file must be opened before prepareWriting is called, otherwise + // if the opening fails, no content will follow the already written + // header and the tar file is effectively f*cked up + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + setErrorString( + tr("Couldn't open file %1: %2") + .arg(fileName, file.errorString())); + return false; + } + + if (!prepareWriting(destName, fileInfo.owner(), fileInfo.group(), size, + fi.st_mode, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.birthTime())) { + //qCWarning(KArchiveLog) << " prepareWriting" << destName << "failed"; + return false; + } + + // Read and write data in chunks to minimize memory usage + QByteArray array; + array.resize(int(qMin(qint64(1024 * 1024), size))); + qint64 n; + qint64 total = 0; + while ((n = file.read(array.data(), array.size())) > 0) { + if (!writeData(array.data(), n)) { + //qCWarning(KArchiveLog) << "writeData failed"; + return false; + } + total += n; + } + Q_ASSERT(total == size); + + if (!finishWriting(size)) { + //qCWarning(KArchiveLog) << "finishWriting failed"; + return false; + } + return true; +} + +bool KArchive::addLocalDirectory(const QString &path, const QString &destName) +{ + QDir dir(path); + if (!dir.exists()) { + setErrorString( + tr("Directory %1 does not exist") + .arg(path)); + return false; + } + dir.setFilter(dir.filter() | QDir::Hidden); + const QStringList files = dir.entryList(); + for (QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + if (*it != QLatin1String(".") && *it != QLatin1String("..")) { + QString fileName = path + QLatin1Char('/') + *it; +// qCDebug(KArchiveLog) << "storing " << fileName; + QString dest = destName.isEmpty() ? *it : (destName + QLatin1Char('/') + *it); + QFileInfo fileInfo(fileName); + + if (fileInfo.isFile() || fileInfo.isSymLink()) { + addLocalFile(fileName, dest); + } else if (fileInfo.isDir()) { + addLocalDirectory(fileName, dest); + } + // We omit sockets + } + } + return true; +} + +bool KArchive::writeFile(const QString &name, const QByteArray &data, + mode_t perm, + const QString &user, const QString &group, const QDateTime &atime, + const QDateTime &mtime, const QDateTime &ctime) +{ + const qint64 size = data.size(); + if (!prepareWriting(name, user, group, size, perm, atime, mtime, ctime)) { + //qCWarning(KArchiveLog) << "prepareWriting failed"; + return false; + } + + // Write data + // Note: if data is null, don't call write, it would terminate the KCompressionDevice + if (data.constData() && size && !writeData(data.constData(), size)) { + //qCWarning(KArchiveLog) << "writeData failed"; + return false; + } + + if (!finishWriting(size)) { + //qCWarning(KArchiveLog) << "finishWriting failed"; + return false; + } + return true; +} + +bool KArchive::writeData(const char *data, qint64 size) +{ + bool ok = device()->write(data, size) == size; + if (!ok) { + setErrorString( + tr("Writing failed: %1") + .arg(device()->errorString())); + d->abortWriting(); + } + return ok; +} + +// The writeDir -> doWriteDir pattern allows to avoid propagating the default +// values into all virtual methods of subclasses, and it allows more extensibility: +// if a new argument is needed, we can add a writeDir overload which stores the +// additional argument in the d pointer, and doWriteDir reimplementations can fetch +// it from there. + +bool KArchive::writeDir(const QString &name, const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, + const QDateTime &mtime, const QDateTime &ctime) +{ + return doWriteDir(name, user, group, perm | 040000, atime, mtime, ctime); +} + +bool KArchive::writeSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, + const QDateTime &mtime, const QDateTime &ctime) +{ + return doWriteSymLink(name, target, user, group, perm, atime, mtime, ctime); +} + +bool KArchive::prepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, + mode_t perm, const QDateTime &atime, + const QDateTime &mtime, const QDateTime &ctime) +{ + bool ok = doPrepareWriting(name, user, group, size, perm, atime, mtime, ctime); + if (!ok) { + d->abortWriting(); + } + return ok; +} + +bool KArchive::finishWriting(qint64 size) +{ + return doFinishWriting(size); +} + +void KArchive::setErrorString(const QString &errorStr) +{ + d->errorStr = errorStr; +} + +static QString getCurrentUserName() +{ +#if defined(Q_OS_UNIX) + struct passwd *pw = getpwuid(getuid()); + return pw ? QFile::decodeName(pw->pw_name) : QString::number(getuid()); +#elif defined(Q_OS_WIN) + wchar_t buffer[255]; + DWORD size = 255; + bool ok = GetUserNameW(buffer, &size); + if (!ok) { + return QString(); + } + return QString::fromWCharArray(buffer); +#else + return QString(); +#endif +} + +static QString getCurrentGroupName() +{ +#if defined(Q_OS_UNIX) + struct group *grp = getgrgid(getgid()); + return grp ? QFile::decodeName(grp->gr_name) : QString::number(getgid()); +#elif defined(Q_OS_WIN) + return QString(); +#else + return QString(); +#endif +} + +KArchiveDirectory *KArchive::rootDir() +{ + if (!d->rootDir) { + //qCDebug(KArchiveLog) << "Making root dir "; + QString username = ::getCurrentUserName(); + QString groupname = ::getCurrentGroupName(); + + d->rootDir = new KArchiveDirectory(this, QStringLiteral("/"), int(0777 + S_IFDIR), QDateTime(), username, groupname, QString()); + } + return d->rootDir; +} + +KArchiveDirectory *KArchive::findOrCreate(const QString &path) +{ + return d->findOrCreate(path, 0 /*recursionCounter*/); +} + +KArchiveDirectory *KArchivePrivate::findOrCreate(const QString &path, int recursionCounter) +{ + // Check we're not in a path that is ultra deep, this is most probably fine since PATH_MAX on Linux + // is defined as 4096, so even on /a/a/a/a/a/a 2500 recursions puts us over that limit + // an ultra deep recursion will makes us crash due to not enough stack. Tests show that 1MB stack + // (default on Linux seems to be 8MB) gives us up to around 4000 recursions + if (recursionCounter > 2500) { + qCWarning(KArchiveLog) << "path recursion limit exceeded, bailing out"; + return nullptr; + } + //qCDebug(KArchiveLog) << path; + if (path.isEmpty() || path == QLatin1String("/") || path == QLatin1String(".")) { // root dir => found + //qCDebug(KArchiveLog) << "returning rootdir"; + return q->rootDir(); + } + // Important note : for tar files containing absolute paths + // (i.e. beginning with "/"), this means the leading "/" will + // be removed (no KDirectory for it), which is exactly the way + // the "tar" program works (though it displays a warning about it) + // See also KArchiveDirectory::entry(). + + // Already created ? => found + KArchiveDirectory *existingEntryParentDirectory; + const KArchiveEntry *existingEntry = KArchiveDirectoryPrivate::get(q->rootDir())->entry(path, &existingEntryParentDirectory); + if (existingEntry) { + if (existingEntry->isDirectory()) + //qCDebug(KArchiveLog) << "found it"; + { + const KArchiveDirectory *dir = static_cast(existingEntry); + return const_cast(dir); + } else { + const KArchiveFile *file = static_cast(existingEntry); + if (file->size() > 0) { + qCWarning(KArchiveLog) << path << "is normal file, but there are file paths in the archive assuming it is a directory, bailing out"; + return nullptr; + } + + qCDebug(KArchiveLog) << path << " is an empty file, assuming it is actually a directory and replacing"; + KArchiveEntry *myEntry = const_cast(existingEntry); + existingEntryParentDirectory->removeEntry(myEntry); + delete myEntry; + } + } + + // Otherwise go up and try again + int pos = path.lastIndexOf(QLatin1Char('/')); + KArchiveDirectory *parent; + QString dirname; + if (pos == -1) { // no more slash => create in root dir + parent = q->rootDir(); + dirname = path; + } else { + QString left = path.left(pos); + dirname = path.mid(pos + 1); + parent = findOrCreate(left, recursionCounter + 1); // recursive call... until we find an existing dir. + } + + if (!parent) { + return nullptr; + } + + //qCDebug(KArchiveLog) << "found parent " << parent->name() << " adding " << dirname << " to ensure " << path; + // Found -> add the missing piece + KArchiveDirectory *e = new KArchiveDirectory(q, dirname, rootDir->permissions(), + rootDir->date(), rootDir->user(), + rootDir->group(), QString()); + if (parent->addEntryV2(e)) { + return e; // now a directory to exists + } else { + return nullptr; + } +} + +void KArchive::setDevice(QIODevice *dev) +{ + if (d->deviceOwned) { + delete d->dev; + } + d->dev = dev; + d->deviceOwned = false; +} + +void KArchive::setRootDir(KArchiveDirectory *rootDir) +{ + Q_ASSERT(!d->rootDir); // Call setRootDir only once during parsing please ;) + delete d->rootDir; // but if it happens, don't leak + d->rootDir = rootDir; +} + +QIODevice::OpenMode KArchive::mode() const +{ + return d->mode; +} + +QIODevice *KArchive::device() const +{ + return d->dev; +} + +bool KArchive::isOpen() const +{ + return d->mode != QIODevice::NotOpen; +} + +QString KArchive::fileName() const +{ + return d->fileName; +} + +void KArchivePrivate::abortWriting() +{ + if (saveFile) { + saveFile->cancelWriting(); + delete saveFile; + saveFile = nullptr; + dev = nullptr; + } +} + +// this is a hacky wrapper to check if time_t value is invalid +QDateTime KArchivePrivate::time_tToDateTime(uint time_t) +{ + if (time_t == uint(-1)) { + return QDateTime(); + } + return QDateTime::fromSecsSinceEpoch(time_t); +} + +//////////////////////////////////////////////////////////////////////// +/////////////////////// KArchiveEntry ////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class KArchiveEntryPrivate +{ +public: + KArchiveEntryPrivate(KArchive *_archive, const QString &_name, int _access, + const QDateTime &_date, const QString &_user, const QString &_group, + const QString &_symlink) + : name(_name) + , date(_date) + , access(_access) + , user(_user) + , group(_group) + , symlink(_symlink) + , archive(_archive) + { + } + QString name; + QDateTime date; + mode_t access; + QString user; + QString group; + QString symlink; + KArchive *archive; +}; + +KArchiveEntry::KArchiveEntry(KArchive *t, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, const + QString &symlink) + : d(new KArchiveEntryPrivate(t, name, access, date, user, group, symlink)) +{ +} + +KArchiveEntry::~KArchiveEntry() +{ + delete d; +} + +QDateTime KArchiveEntry::date() const +{ + return d->date; +} + +QString KArchiveEntry::name() const +{ + return d->name; +} + +mode_t KArchiveEntry::permissions() const +{ + return d->access; +} + +QString KArchiveEntry::user() const +{ + return d->user; +} + +QString KArchiveEntry::group() const +{ + return d->group; +} + +QString KArchiveEntry::symLinkTarget() const +{ + return d->symlink; +} + +bool KArchiveEntry::isFile() const +{ + return false; +} + +bool KArchiveEntry::isDirectory() const +{ + return false; +} + +KArchive *KArchiveEntry::archive() const +{ + return d->archive; +} + +//////////////////////////////////////////////////////////////////////// +/////////////////////// KArchiveFile /////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class KArchiveFilePrivate +{ +public: + KArchiveFilePrivate(qint64 _pos, qint64 _size) + : pos(_pos) + , size(_size) + { + } + qint64 pos; + qint64 size; +}; + +KArchiveFile::KArchiveFile(KArchive *t, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, + const QString &symlink, + qint64 pos, qint64 size) + : KArchiveEntry(t, name, access, date, user, group, symlink) + , d(new KArchiveFilePrivate(pos, size)) +{ +} + +KArchiveFile::~KArchiveFile() +{ + delete d; +} + +qint64 KArchiveFile::position() const +{ + return d->pos; +} + +qint64 KArchiveFile::size() const +{ + return d->size; +} + +void KArchiveFile::setSize(qint64 s) +{ + d->size = s; +} + +QByteArray KArchiveFile::data() const +{ + bool ok = archive()->device()->seek(d->pos); + if (!ok) { + //qCWarning(KArchiveLog) << "Failed to sync to" << d->pos << "to read" << name(); + } + + // Read content + QByteArray arr; + if (d->size) { + arr = archive()->device()->read(d->size); + Q_ASSERT(arr.size() == d->size); + } + return arr; +} + +QIODevice *KArchiveFile::createDevice() const +{ + return new KLimitedIODevice(archive()->device(), d->pos, d->size); +} + +bool KArchiveFile::isFile() const +{ + return true; +} + +static QFileDevice::Permissions withExecutablePerms( + QFileDevice::Permissions filePerms, + mode_t perms) +{ + if (perms & 01) + filePerms |= QFileDevice::ExeOther; + + if (perms & 010) + filePerms |= QFileDevice::ExeGroup; + + if (perms & 0100) + filePerms |= QFileDevice::ExeOwner; + + return filePerms; +} + +bool KArchiveFile::copyTo(const QString &dest) const +{ + QFile f(dest + QLatin1Char('/') + name()); + if (f.open(QIODevice::ReadWrite | QIODevice::Truncate)) { + QIODevice *inputDev = createDevice(); + if (!inputDev) { + f.remove(); + return false; + } + + // Read and write data in chunks to minimize memory usage + const qint64 chunkSize = 1024 * 1024; + qint64 remainingSize = d->size; + QByteArray array; + array.resize(int(qMin(chunkSize, remainingSize))); + + while (remainingSize > 0) { + const qint64 currentChunkSize = qMin(chunkSize, remainingSize); + const qint64 n = inputDev->read(array.data(), currentChunkSize); + Q_UNUSED(n) // except in Q_ASSERT + Q_ASSERT(n == currentChunkSize); + f.write(array.data(), currentChunkSize); + remainingSize -= currentChunkSize; + } + f.setPermissions(withExecutablePerms(f.permissions(), permissions())); + f.close(); + + delete inputDev; + return true; + } + return false; +} + +//////////////////////////////////////////////////////////////////////// +//////////////////////// KArchiveDirectory ///////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +KArchiveDirectory::KArchiveDirectory(KArchive *t, const QString &name, int access, + const QDateTime &date, + const QString &user, const QString &group, + const QString &symlink) + : KArchiveEntry(t, name, access, date, user, group, symlink) + , d(new KArchiveDirectoryPrivate(this)) +{ +} + +KArchiveDirectory::~KArchiveDirectory() +{ + delete d; +} + +QStringList KArchiveDirectory::entries() const +{ + return d->entries.keys(); +} + +const KArchiveEntry *KArchiveDirectory::entry(const QString &_name) const +{ + KArchiveDirectory *dummy; + return d->entry(_name, &dummy); +} + +const KArchiveFile *KArchiveDirectory::file(const QString &name) const +{ + const KArchiveEntry *e = entry(name); + if (e && e->isFile()) { + return static_cast(e); + } + return nullptr; +} + +void KArchiveDirectory::addEntry(KArchiveEntry *entry) +{ + addEntryV2(entry); +} + +bool KArchiveDirectory::addEntryV2(KArchiveEntry *entry) +{ + if (d->entries.value(entry->name())) { + qCWarning(KArchiveLog) << "directory " << name() + << "has entry" << entry->name() << "already"; + delete entry; + return false; + } + d->entries.insert(entry->name(), entry); + return true; +} + +void KArchiveDirectory::removeEntry(KArchiveEntry *entry) +{ + if (!entry) { + return; + } + + QHash::Iterator it = d->entries.find(entry->name()); + // nothing removed? + if (it == d->entries.end()) { + qCWarning(KArchiveLog) << "directory " << name() + << "has no entry with name " << entry->name(); + return; + } + if (it.value() != entry) { + qCWarning(KArchiveLog) << "directory " << name() + << "has another entry for name " << entry->name(); + return; + } + d->entries.erase(it); +} + +bool KArchiveDirectory::isDirectory() const +{ + return true; +} + +static bool sortByPosition(const KArchiveFile *file1, const KArchiveFile *file2) +{ + return file1->position() < file2->position(); +} + +bool KArchiveDirectory::copyTo(const QString &dest, bool recursiveCopy) const +{ + QDir root; + const QString destDir(QDir(dest).absolutePath()); // get directory path without any "." or ".." + + QList fileList; + QMap fileToDir; + + // placeholders for iterated items + QStack dirStack; + QStack dirNameStack; + + dirStack.push(this); // init stack at current directory + dirNameStack.push(destDir); // ... with given path + do { + const KArchiveDirectory *curDir = dirStack.pop(); + + // extract only to specified folder if it is located within archive's extraction folder + // otherwise put file under root position in extraction folder + QString curDirName = dirNameStack.pop(); + if (!QDir(curDirName).absolutePath().startsWith(destDir)) { + qCWarning(KArchiveLog) << "Attempted export into folder" << curDirName + << "which is outside of the extraction root folder" << destDir << "." + << "Changing export of contained files to extraction root folder."; + curDirName = destDir; + } + + if (!root.mkpath(curDirName)) { + return false; + } + + const QStringList dirEntries = curDir->entries(); + for (QStringList::const_iterator it = dirEntries.begin(); it != dirEntries.end(); ++it) { + const KArchiveEntry *curEntry = curDir->entry(*it); + if (!curEntry->symLinkTarget().isEmpty()) { + QString linkName = curDirName + QLatin1Char('/') + curEntry->name(); + // To create a valid link on Windows, linkName must have a .lnk file extension. +#ifdef Q_OS_WIN + if (!linkName.endsWith(QLatin1String(".lnk"))) { + linkName += QLatin1String(".lnk"); + } +#endif + QFile symLinkTarget(curEntry->symLinkTarget()); + if (!symLinkTarget.link(linkName)) { + //qCDebug(KArchiveLog) << "symlink(" << curEntry->symLinkTarget() << ',' << linkName << ") failed:" << strerror(errno); + } + } else { + if (curEntry->isFile()) { + const KArchiveFile *curFile = dynamic_cast(curEntry); + if (curFile) { + fileList.append(curFile); + fileToDir.insert(curFile->position(), curDirName); + } + } + + if (curEntry->isDirectory() && recursiveCopy) { + const KArchiveDirectory *ad = dynamic_cast(curEntry); + if (ad) { + dirStack.push(ad); + dirNameStack.push(curDirName + QLatin1Char('/') + curEntry->name()); + } + } + } + } + } while (!dirStack.isEmpty()); + + std::sort(fileList.begin(), fileList.end(), sortByPosition); // sort on d->pos, so we have a linear access + + for (QList::const_iterator it = fileList.constBegin(), end = fileList.constEnd(); + it != end; ++it) { + const KArchiveFile *f = *it; + qint64 pos = f->position(); + if (!f->copyTo(fileToDir[pos])) { + return false; + } + } + return true; +} + +void KArchive::virtual_hook(int, void *) +{ + /*BASE::virtual_hook( id, data )*/; +} + +void KArchiveEntry::virtual_hook(int, void *) +{ + /*BASE::virtual_hook( id, data );*/ +} + +void KArchiveFile::virtual_hook(int id, void *data) +{ + KArchiveEntry::virtual_hook(id, data); +} + +void KArchiveDirectory::virtual_hook(int id, void *data) +{ + KArchiveEntry::virtual_hook(id, data); +} diff --git a/src/karchive/src/karchive.h b/src/karchive/src/karchive.h new file mode 100644 index 0000000000..c0e7aa4436 --- /dev/null +++ b/src/karchive/src/karchive.h @@ -0,0 +1,396 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + Moved from ktar.h by Roberto Teixeira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KARCHIVE_H +#define KARCHIVE_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#ifdef Q_OS_WIN +#include // mode_t +#endif + +class KArchiveDirectory; +class KArchiveFile; + +class KArchivePrivate; +/** + * @class KArchive karchive.h KArchive + * + * KArchive is a base class for reading and writing archives. + * @short generic class for reading/writing archives + * @author David Faure + */ +class KARCHIVE_EXPORT KArchive +{ + Q_DECLARE_TR_FUNCTIONS(KArchive) + +protected: + /** + * Base constructor (protected since this is a pure virtual class). + * @param fileName is a local path (e.g. "/tmp/myfile.ext"), + * from which the archive will be read from, or into which the archive + * will be written, depending on the mode given to open(). + */ + KArchive(const QString &fileName); + + /** + * Base constructor (protected since this is a pure virtual class). + * @param dev the I/O device where the archive reads its data + * Note that this can be a file, but also a data buffer, a compression filter, etc. + * For a file in writing mode it is better to use the other constructor + * though, to benefit from the use of QSaveFile when saving. + */ + KArchive(QIODevice *dev); + +public: + virtual ~KArchive(); + + /** + * Opens the archive for reading or writing. + * Inherited classes might want to reimplement openArchive instead. + * @param mode may be QIODevice::ReadOnly or QIODevice::WriteOnly + * @see close + */ + virtual bool open(QIODevice::OpenMode mode); + + /** + * Closes the archive. + * Inherited classes might want to reimplement closeArchive instead. + * + * @return true if close succeeded without problems + * @see open + */ + virtual bool close(); + + /** + * Returns a description of the last error + * @since 5.29 + */ + QString errorString() const; + + /** + * Checks whether the archive is open. + * @return true if the archive is opened + */ + bool isOpen() const; + + /** + * Returns the mode in which the archive was opened + * @return the mode in which the archive was opened (QIODevice::ReadOnly or QIODevice::WriteOnly) + * @see open() + */ + QIODevice::OpenMode mode() const; + + /** + * The underlying device. + * @return the underlying device. + */ + QIODevice *device() const; + + /** + * The name of the archive file, as passed to the constructor that takes a + * fileName, or an empty string if you used the QIODevice constructor. + * @return the name of the file, or QString() if unknown + */ + QString fileName() const; + + /** + * If an archive is opened for reading, then the contents + * of the archive can be accessed via this function. + * @return the directory of the archive + */ + const KArchiveDirectory *directory() const; + + /** + * Writes a local file into the archive. The main difference with writeFile, + * is that this method minimizes memory usage, by not loading the whole file + * into memory in one go. + * + * If @p fileName is a symbolic link, it will be written as is, i. e. + * it will not be resolved before. + * @param fileName full path to an existing local file, to be added to the archive. + * @param destName the resulting name (or relative path) of the file in the archive. + */ + bool addLocalFile(const QString &fileName, const QString &destName); + + /** + * Writes a local directory into the archive, including all its contents, recursively. + * Calls addLocalFile for each file to be added. + * + * It will also add a @p path that is a symbolic link to a + * directory. The symbolic link will be dereferenced and the content of the + * directory it is pointing to added recursively. However, symbolic links + * *under* @p path will be stored as is. + * @param path full path to an existing local directory, to be added to the archive. + * @param destName the resulting name (or relative path) of the file in the archive. + */ + bool addLocalDirectory(const QString &path, const QString &destName); + + /** + * If an archive is opened for writing then you can add new directories + * using this function. KArchive won't write one directory twice. + * + * This method also allows some file metadata to be set. + * However, depending on the archive type not all metadata might be regarded. + * + * @param name the name of the directory + * @param user the user that owns the directory + * @param group the group that owns the directory + * @param perm permissions of the directory + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + */ + bool writeDir(const QString &name, const QString &user = QString(), const QString &group = QString(), + mode_t perm = 040755, const QDateTime &atime = QDateTime(), + const QDateTime &mtime = QDateTime(), const QDateTime &ctime = QDateTime()); + + /** + * Writes a symbolic link to the archive if supported. + * The archive must be opened for writing. + * + * @param name name of symbolic link + * @param target target of symbolic link + * @param user the user that owns the directory + * @param group the group that owns the directory + * @param perm permissions of the directory + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + */ + bool writeSymLink(const QString &name, const QString &target, + const QString &user = QString(), const QString &group = QString(), + mode_t perm = 0120755, const QDateTime &atime = QDateTime(), + const QDateTime &mtime = QDateTime(), const QDateTime &ctime = QDateTime()); + +#if KARCHIVE_ENABLE_DEPRECATED_SINCE(5, 0) + /** + * @deprecated since 5.0, use writeFile(const QString&,const QByteArray&,mode_t,const QString&,const QString&,const QDateTime&,const QDateTime&,const QDateTime&) + */ + KARCHIVE_DEPRECATED_VERSION(5, 0, "Use KArchive::writeFile(const QString&,const QByteArray&,mode_t,const QString&,const QString&,const QDateTime&,const QDateTime&,const QDateTime&)") + bool writeFile(const QString &name, const QString &user, const QString &group, + const char *data, qint64 size, + mode_t perm = 0100644, const QDateTime &atime = QDateTime(), + const QDateTime &mtime = QDateTime(), const QDateTime &ctime = QDateTime()) + { + QByteArray array(data, size); + return writeFile(name, array, perm, user, group, atime, mtime, ctime); + } + // The above can lead to ambiguous calls when using "..." for the first 4 arguments, + // but that's good, better than unexpected behavior due to the signature change. +#endif + + /** + * Writes a new file into the archive. + * + * The archive must be opened for writing first. + * + * The necessary parent directories are created automatically + * if needed. For instance, writing "mydir/test1" does not + * require creating the directory "mydir" first. + * + * This method also allows some file metadata to be + * set. However, depending on the archive type not all metadata might be + * written out. + * + * @param name the name of the file + * @param data the data to write + * @param perm permissions of the file + * @param user the user that owns the file + * @param group the group that owns the file + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + */ + bool writeFile(const QString &name, const QByteArray &data, + mode_t perm = 0100644, + const QString &user = QString(), const QString &group = QString(), + const QDateTime &atime = QDateTime(), + const QDateTime &mtime = QDateTime(), const QDateTime &ctime = QDateTime()); + + /** + * Here's another way of writing a file into an archive: + * Call prepareWriting(), then call writeData() + * as many times as wanted then call finishWriting( totalSize ). + * For tar.gz files, you need to know the size before hand, it is needed in the header! + * For zip files, size isn't used. + * + * This method also allows some file metadata to be + * set. However, depending on the archive type not all metadata might be + * regarded. + * @param name the name of the file + * @param user the user that owns the file + * @param group the group that owns the file + * @param size the size of the file + * @param perm permissions of the file + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + */ + bool prepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, + mode_t perm = 0100644, const QDateTime &atime = QDateTime(), + const QDateTime &mtime = QDateTime(), const QDateTime &ctime = QDateTime()); + + /** + * Write data into the current file - to be called after calling prepareWriting + */ + virtual bool writeData(const char *data, qint64 size); + + /** + * Call finishWriting after writing the data. + * @param size the size of the file + * @see prepareWriting() + */ + bool finishWriting(qint64 size); + +protected: + /** + * Opens an archive for reading or writing. + * Called by open. + * @param mode may be QIODevice::ReadOnly or QIODevice::WriteOnly + */ + virtual bool openArchive(QIODevice::OpenMode mode) = 0; + + /** + * Closes the archive. + * Called by close. + */ + virtual bool closeArchive() = 0; + + /** + * Sets error description + * @param errorStr error description + * @since 5.29 + */ + void setErrorString(const QString &errorStr); + + /** + * Retrieves or create the root directory. + * The default implementation assumes that openArchive() did the parsing, + * so it creates a dummy rootdir if none was set (write mode, or no '/' in the archive). + * Reimplement this to provide parsing/listing on demand. + * @return the root directory + */ + virtual KArchiveDirectory *rootDir(); + + /** + * Write a directory to the archive. + * This virtual method must be implemented by subclasses. + * + * Depending on the archive type not all metadata might be used. + * + * @param name the name of the directory + * @param user the user that owns the directory + * @param group the group that owns the directory + * @param perm permissions of the directory. Use 040755 if you don't have any other information. + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + * @see writeDir + */ + virtual bool doWriteDir(const QString &name, const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) = 0; + + /** + * Writes a symbolic link to the archive. + * This virtual method must be implemented by subclasses. + * + * @param name name of symbolic link + * @param target target of symbolic link + * @param user the user that owns the directory + * @param group the group that owns the directory + * @param perm permissions of the directory + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + * @see writeSymLink + */ + virtual bool doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) = 0; + + /** + * This virtual method must be implemented by subclasses. + * + * Depending on the archive type not all metadata might be used. + * + * @param name the name of the file + * @param user the user that owns the file + * @param group the group that owns the file + * @param size the size of the file + * @param perm permissions of the file. Use 0100644 if you don't have any more specific permissions to set. + * @param atime time the file was last accessed + * @param mtime modification time of the file + * @param ctime time of last status change + * @see prepareWriting + */ + virtual bool doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) = 0; + + /** + * Called after writing the data. + * This virtual method must be implemented by subclasses. + * + * @param size the size of the file + * @see finishWriting() + */ + virtual bool doFinishWriting(qint64 size) = 0; + + /** + * Ensures that @p path exists, create otherwise. + * This handles e.g. tar files missing directory entries, like mico-2.3.0.tar.gz :) + * @param path the path of the directory + * @return the directory with the given @p path + */ + KArchiveDirectory *findOrCreate(const QString &path); + + /** + * Can be reimplemented in order to change the creation of the device + * (when using the fileName constructor). By default this method uses + * QSaveFile when saving, and a simple QFile on reading. + * This method is called by open(). + */ + virtual bool createDevice(QIODevice::OpenMode mode); + + /** + * Can be called by derived classes in order to set the underlying device. + * Note that KArchive will -not- own the device, it must be deleted by the derived class. + */ + void setDevice(QIODevice *dev); + + /** + * Derived classes call setRootDir from openArchive, + * to set the root directory after parsing an existing archive. + */ + void setRootDir(KArchiveDirectory *rootDir); + +protected: + virtual void virtual_hook(int id, void *data); +private: + friend class KArchivePrivate; + KArchivePrivate *const d; +}; + +// for source compat +#include "karchivefile.h" +#include "karchivedirectory.h" + +#endif diff --git a/src/karchive/src/karchive_export.h b/src/karchive/src/karchive_export.h new file mode 100644 index 0000000000..0c0705d298 --- /dev/null +++ b/src/karchive/src/karchive_export.h @@ -0,0 +1,122 @@ + +#ifndef KARCHIVE_EXPORT_H +#define KARCHIVE_EXPORT_H + +#ifdef KARCHIVE_STATIC_DEFINE +# define KARCHIVE_EXPORT +# define KARCHIVE_NO_EXPORT +#else +# ifndef KARCHIVE_EXPORT +# ifdef KF5Archive_EXPORTS + /* We are building this library */ +# define KARCHIVE_EXPORT __attribute__((visibility("default"))) +# else + /* We are using this library */ +# define KARCHIVE_EXPORT __attribute__((visibility("default"))) +# endif +# endif + +# ifndef KARCHIVE_NO_EXPORT +# define KARCHIVE_NO_EXPORT __attribute__((visibility("hidden"))) +# endif +#endif + +#ifndef KARCHIVE_DECL_DEPRECATED +# define KARCHIVE_DECL_DEPRECATED __attribute__ ((__deprecated__)) +#endif + +#ifndef KARCHIVE_DECL_DEPRECATED_EXPORT +# define KARCHIVE_DECL_DEPRECATED_EXPORT KARCHIVE_EXPORT KARCHIVE_DECL_DEPRECATED +#endif + +#ifndef KARCHIVE_DECL_DEPRECATED_NO_EXPORT +# define KARCHIVE_DECL_DEPRECATED_NO_EXPORT KARCHIVE_NO_EXPORT KARCHIVE_DECL_DEPRECATED +#endif + +#if 0 /* DEFINE_NO_DEPRECATED */ +# ifndef KARCHIVE_NO_DEPRECATED +# define KARCHIVE_NO_DEPRECATED +# endif +#endif + +#define KARCHIVE_DECL_DEPRECATED_TEXT(text) __attribute__ ((__deprecated__(text))) + +#define ECM_GENERATEEXPORTHEADER_VERSION_VALUE(major, minor, patch) ((major<<16)|(minor<<8)|(patch)) + +/* Take any defaults from group settings */ +#if !defined(KARCHIVE_NO_DEPRECATED) && !defined(KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT) +# ifdef KF_NO_DEPRECATED +# define KARCHIVE_NO_DEPRECATED +# elif defined(KF_DISABLE_DEPRECATED_BEFORE_AND_AT) +# define KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT KF_DISABLE_DEPRECATED_BEFORE_AND_AT +# endif +#endif +#if !defined(KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT) && defined(KF_DISABLE_DEPRECATED_BEFORE_AND_AT) +# define KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT KF_DISABLE_DEPRECATED_BEFORE_AND_AT +#endif + +#if !defined(KARCHIVE_NO_DEPRECATED_WARNINGS) && !defined(KARCHIVE_DEPRECATED_WARNINGS_SINCE) +# ifdef KF_NO_DEPRECATED_WARNINGS +# define KARCHIVE_NO_DEPRECATED_WARNINGS +# elif defined(KF_DEPRECATED_WARNINGS_SINCE) +# define KARCHIVE_DEPRECATED_WARNINGS_SINCE KF_DEPRECATED_WARNINGS_SINCE +# endif +#endif +#if !defined(KARCHIVE_DEPRECATED_WARNINGS_SINCE) && defined(KF_DEPRECATED_WARNINGS_SINCE) +# define KARCHIVE_DEPRECATED_WARNINGS_SINCE KF_DEPRECATED_WARNINGS_SINCE +#endif + +#if defined(KARCHIVE_NO_DEPRECATED) +# undef KARCHIVE_DEPRECATED +# define KARCHIVE_DEPRECATED_EXPORT KARCHIVE_EXPORT +# define KARCHIVE_DEPRECATED_NO_EXPORT KARCHIVE_NO_EXPORT +#elif defined(KARCHIVE_NO_DEPRECATED_WARNINGS) +# define KARCHIVE_DEPRECATED +# define KARCHIVE_DEPRECATED_EXPORT KARCHIVE_EXPORT +# define KARCHIVE_DEPRECATED_NO_EXPORT KARCHIVE_NO_EXPORT +#else +# define KARCHIVE_DEPRECATED KARCHIVE_DECL_DEPRECATED +# define KARCHIVE_DEPRECATED_EXPORT KARCHIVE_DECL_DEPRECATED_EXPORT +# define KARCHIVE_DEPRECATED_NO_EXPORT KARCHIVE_DECL_DEPRECATED_NO_EXPORT +#endif + +/* No deprecated API had been removed from build */ +#define KARCHIVE_EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 + +#define KARCHIVE_BUILD_DEPRECATED_SINCE(major, minor) 1 + +#ifdef KARCHIVE_NO_DEPRECATED +# define KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT 0x54c00 +#endif +#ifdef KARCHIVE_NO_DEPRECATED_WARNINGS +# define KARCHIVE_DEPRECATED_WARNINGS_SINCE 0 +#endif + +#ifndef KARCHIVE_DEPRECATED_WARNINGS_SINCE +# ifdef KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT +# define KARCHIVE_DEPRECATED_WARNINGS_SINCE KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT +# else +# define KARCHIVE_DEPRECATED_WARNINGS_SINCE 0x54c00 +# endif +#endif + +#ifndef KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT +# define KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT 0 +#endif + +#ifdef KARCHIVE_DEPRECATED +# define KARCHIVE_ENABLE_DEPRECATED_SINCE(major, minor) (ECM_GENERATEEXPORTHEADER_VERSION_VALUE(major, minor, 0) > KARCHIVE_DISABLE_DEPRECATED_BEFORE_AND_AT) +#else +# define KARCHIVE_ENABLE_DEPRECATED_SINCE(major, minor) 0 +#endif + +#if KARCHIVE_DEPRECATED_WARNINGS_SINCE >= 0x50000 +# define KARCHIVE_DEPRECATED_VERSION_5_0(text) KARCHIVE_DECL_DEPRECATED_TEXT(text) +#else +# define KARCHIVE_DEPRECATED_VERSION_5_0(text) +#endif +#define KARCHIVE_DEPRECATED_VERSION_5(minor, text) KARCHIVE_DEPRECATED_VERSION_5_##minor(text) +#define KARCHIVE_DEPRECATED_VERSION(major, minor, text) KARCHIVE_DEPRECATED_VERSION_##major(minor, "Since "#major"."#minor". " text) +#define KARCHIVE_DEPRECATED_VERSION_BELATED(major, minor, textmajor, textminor, text) KARCHIVE_DEPRECATED_VERSION_##major(minor, "Since "#textmajor"."#textminor". " text) + +#endif /* KARCHIVE_EXPORT_H */ diff --git a/src/karchive/src/karchive_p.h b/src/karchive/src/karchive_p.h new file mode 100644 index 0000000000..d4fefb1e66 --- /dev/null +++ b/src/karchive/src/karchive_p.h @@ -0,0 +1,60 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KARCHIVE_P_H +#define KARCHIVE_P_H + +#include "karchive.h" + +#include + +class KArchivePrivate +{ + Q_DECLARE_TR_FUNCTIONS(KArchivePrivate) + +public: + KArchivePrivate(KArchive *parent) + : q(parent) + , rootDir(nullptr) + , saveFile(nullptr) + , dev(nullptr) + , fileName() + , mode(QIODevice::NotOpen) + , deviceOwned(false) + { + } + ~KArchivePrivate() + { + delete saveFile; + delete rootDir; + } + + KArchivePrivate(const KArchivePrivate &) = delete; + KArchivePrivate &operator=(const KArchivePrivate &) = delete; + + static bool hasRootDir(KArchive *archive) + { + return archive->d->rootDir; + } + + void abortWriting(); + + static QDateTime time_tToDateTime(uint time_t); + + KArchiveDirectory *findOrCreate(const QString &path, int recursionCounter); + + KArchive *q; + KArchiveDirectory *rootDir; + QSaveFile *saveFile; + QIODevice *dev; + QString fileName; + QIODevice::OpenMode mode; + bool deviceOwned; // if true, we (KArchive) own dev and must delete it + QString errorStr{tr("Unknown error")}; +}; + +#endif // KARCHIVE_P_H diff --git a/src/karchive/src/karchivedirectory.h b/src/karchive/src/karchivedirectory.h new file mode 100644 index 0000000000..9fe023ec95 --- /dev/null +++ b/src/karchive/src/karchivedirectory.h @@ -0,0 +1,122 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + Moved from ktar.h by Roberto Teixeira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KARCHIVEDIRECTORY_H +#define KARCHIVEDIRECTORY_H + +#include +#include + +#include +#include +#include + +#include + +class KArchiveDirectoryPrivate; +class KArchiveFile; +/** + * @class KArchiveDirectory karchivedirectory.h KArchiveDirectory + * + * Represents a directory entry in a KArchive. + * @short A directory in an archive. + * + * @see KArchive + * @see KArchiveFile + */ +class KARCHIVE_EXPORT KArchiveDirectory : public KArchiveEntry +{ +public: + /** + * Creates a new directory entry. + * @param archive the entries archive + * @param name the name of the entry + * @param access the permissions in unix format + * @param date the date (in seconds since 1970) + * @param user the user that owns the entry + * @param group the group that owns the entry + * @param symlink the symlink, or QString() + */ + KArchiveDirectory(KArchive *archive, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, + const QString &symlink); + + virtual ~KArchiveDirectory(); + + /** + * Returns a list of sub-entries. + * Note that the list is not sorted, it's even in random order (due to using a hashtable). + * Use sort() on the result to sort the list by filename. + * + * @return the names of all entries in this directory (filenames, no path). + */ + QStringList entries() const; + + /** + * Returns the entry in the archive with the given name. + * The entry could be a file or a directory, use isFile() to find out which one it is. + * @param name may be "test1", "mydir/test3", "mydir/mysubdir/test3", etc. + * @return a pointer to the entry in the directory, or a null pointer if there is no such entry. + */ + const KArchiveEntry *entry(const QString &name) const; + + /** + * Returns the file entry in the archive with the given name. + * If the entry exists and is a file, a KArchiveFile is returned. + * Otherwise, a null pointer is returned. + * This is a convenience method for entry(), when we know the entry is expected to be a file. + * + * @param name may be "test1", "mydir/test3", "mydir/mysubdir/test3", etc. + * @return a pointer to the file entry in the directory, or a null pointer if there is no such file entry. + * @since 5.3 + */ + const KArchiveFile *file(const QString &name) const; + + /** + * @internal + * Adds a new entry to the directory. + * Note: this can delete the entry if another one with the same name is already present + */ + void addEntry(KArchiveEntry *); // KF6 TODO: return bool + + /** + * @internal + * Adds a new entry to the directory. + * @return whether the entry was added or not. Non added entries are deleted + */ + bool addEntryV2(KArchiveEntry *); // KF6 TODO: merge with the one above + + /** + * @internal + * Removes an entry from the directory. + */ + void removeEntry(KArchiveEntry *); // KF6 TODO: return bool since it can fail + + /** + * Checks whether this entry is a directory. + * @return true, since this entry is a directory + */ + bool isDirectory() const override; + + /** + * Extracts all entries in this archive directory to the directory + * @p dest. + * @param dest the directory to extract to + * @param recursive if set to true, subdirectories are extracted as well + * @return true on success, false if the directory (dest + '/' + name()) couldn't be created + */ + bool copyTo(const QString &dest, bool recursive = true) const; + +protected: + void virtual_hook(int id, void *data) override; +private: + friend class KArchiveDirectoryPrivate; + KArchiveDirectoryPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/karchiveentry.h b/src/karchive/src/karchiveentry.h new file mode 100644 index 0000000000..722eb93132 --- /dev/null +++ b/src/karchive/src/karchiveentry.h @@ -0,0 +1,108 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + Moved from ktar.h by Roberto Teixeira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KARCHIVEENTRY_H +#define KARCHIVEENTRY_H + +#include +#include + +#include + +#ifdef Q_OS_WIN +#include // mode_t +#endif + +class KArchiveDirectory; +class KArchiveFile; + +class KArchiveEntryPrivate; +/** + * @class KArchiveEntry karchiveentry.h KArchiveEntry + * + * A base class for entries in an KArchive. + * @short Base class for the archive-file's directory structure. + * + * @see KArchiveFile + * @see KArchiveDirectory + */ +class KARCHIVE_EXPORT KArchiveEntry +{ +public: + /** + * Creates a new entry. + * @param archive the entries archive + * @param name the name of the entry + * @param access the permissions in unix format + * @param date the date (in seconds since 1970) + * @param user the user that owns the entry + * @param group the group that owns the entry + * @param symlink the symlink, or QString() + */ + KArchiveEntry(KArchive *archive, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, + const QString &symlink); + + virtual ~KArchiveEntry(); + + /** + * Creation date of the file. + * @return the creation date + */ + QDateTime date() const; + + /** + * Name of the file without path. + * @return the file name without path + */ + QString name() const; + /** + * The permissions and mode flags as returned by the stat() function + * in st_mode. + * @return the permissions + */ + mode_t permissions() const; + /** + * User who created the file. + * @return the owner of the file + */ + QString user() const; + /** + * Group of the user who created the file. + * @return the group of the file + */ + QString group() const; + + /** + * Symlink if there is one. + * @return the symlink, or QString() + */ + QString symLinkTarget() const; + + /** + * Checks whether the entry is a file. + * @return true if this entry is a file + */ + virtual bool isFile() const; + + /** + * Checks whether the entry is a directory. + * @return true if this entry is a directory + */ + virtual bool isDirectory() const; + +protected: + KArchive *archive() const; + +protected: + virtual void virtual_hook(int id, void *data); +private: + KArchiveEntryPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/karchivefile.h b/src/karchive/src/karchivefile.h new file mode 100644 index 0000000000..c9638d3704 --- /dev/null +++ b/src/karchive/src/karchivefile.h @@ -0,0 +1,102 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + Moved from ktar.h by Roberto Teixeira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KARCHIVEFILE_H +#define KARCHIVEFILE_H + +#include + +class KArchiveFilePrivate; +/** + * @class KArchiveFile karchivefile.h KArchiveFile + * + * Represents a file entry in a KArchive. + * @short A file in an archive. + * + * @see KArchive + * @see KArchiveDirectory + */ +class KARCHIVE_EXPORT KArchiveFile : public KArchiveEntry +{ +public: + /** + * Creates a new file entry. Do not call this, KArchive takes care of it. + * @param archive the entries archive + * @param name the name of the entry + * @param access the permissions in unix format + * @param date the date (in seconds since 1970) + * @param user the user that owns the entry + * @param group the group that owns the entry + * @param symlink the symlink, or QString() + * @param pos the position of the file in the directory + * @param size the size of the file + */ + KArchiveFile(KArchive *archive, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, const QString &symlink, + qint64 pos, qint64 size); + + /** + * Destructor. Do not call this, KArchive takes care of it. + */ + virtual ~KArchiveFile(); + + /** + * Position of the data in the [uncompressed] archive. + * @return the position of the file + */ + qint64 position() const; + /** + * Size of the data. + * @return the size of the file + */ + qint64 size() const; + /** + * Set size of data, usually after writing the file. + * @param s the new size of the file + */ + void setSize(qint64 s); + + /** + * Returns the data of the file. + * Call data() with care (only once per file), this data isn't cached. + * @return the content of this file. + */ + virtual QByteArray data() const; + + /** + * This method returns QIODevice (internal class: KLimitedIODevice) + * on top of the underlying QIODevice. This is obviously for reading only. + * + * WARNING: Note that the ownership of the device is being transferred to the caller, + * who will have to delete it. + * + * The returned device auto-opens (in readonly mode), no need to open it. + * @return the QIODevice of the file + */ + virtual QIODevice *createDevice() const; + + /** + * Checks whether this entry is a file. + * @return true, since this entry is a file + */ + bool isFile() const override; + + /** + * Extracts the file to the directory @p dest + * @param dest the directory to extract to + * @return true on success, false if the file (dest + '/' + name()) couldn't be created + */ + bool copyTo(const QString &dest) const; + +protected: + void virtual_hook(int id, void *data) override; +private: + KArchiveFilePrivate *const d; +}; + +#endif diff --git a/src/karchive/src/kbzip2filter.cpp b/src/karchive/src/kbzip2filter.cpp new file mode 100644 index 0000000000..3fcefd8597 --- /dev/null +++ b/src/karchive/src/kbzip2filter.cpp @@ -0,0 +1,185 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kbzip2filter.h" +#include "loggingcategory.h" + + +#if HAVE_BZIP2_SUPPORT + +// we don't need that +#define BZ_NO_STDIO +extern "C" { +#include +} + +#if NEED_BZ2_PREFIX +#define bzDecompressInit(x,y,z) BZ2_bzDecompressInit(x,y,z) +#define bzDecompressEnd(x) BZ2_bzDecompressEnd(x) +#define bzCompressEnd(x) BZ2_bzCompressEnd(x) +#define bzDecompress(x) BZ2_bzDecompress(x) +#define bzCompress(x,y) BZ2_bzCompress(x, y) +#define bzCompressInit(x,y,z,a) BZ2_bzCompressInit(x, y, z, a); +#endif + +#include + +#include + +// For docu on this, see /usr/doc/bzip2-0.9.5d/bzip2-0.9.5d/manual_3.html + +class Q_DECL_HIDDEN KBzip2Filter::Private +{ +public: + Private() + : isInitialized(false) + { + memset(&zStream, 0, sizeof (zStream)); + mode = 0; + } + + bz_stream zStream; + int mode; + bool isInitialized; +}; + +KBzip2Filter::KBzip2Filter() + : d(new Private) +{ +} + +KBzip2Filter::~KBzip2Filter() +{ + delete d; +} + +bool KBzip2Filter::init(int mode) +{ + if (d->isInitialized) { + terminate(); + } + + d->zStream.next_in = nullptr; + d->zStream.avail_in = 0; + if (mode == QIODevice::ReadOnly) { + const int result = bzDecompressInit(&d->zStream, 0, 0); + if (result != BZ_OK) { + //qCDebug(KArchiveLog) << "bzDecompressInit returned " << result; + return false; + } + } else if (mode == QIODevice::WriteOnly) { + const int result = bzCompressInit(&d->zStream, 5, 0, 0); + if (result != BZ_OK) { + //qCDebug(KArchiveLog) << "bzDecompressInit returned " << result; + return false; + } + } else { + //qCWarning(KArchiveLog) << "Unsupported mode " << mode << ". Only QIODevice::ReadOnly and QIODevice::WriteOnly supported"; + return false; + } + d->mode = mode; + d->isInitialized = true; + return true; +} + +int KBzip2Filter::mode() const +{ + return d->mode; +} + +bool KBzip2Filter::terminate() +{ + if (d->mode == QIODevice::ReadOnly) { + const int result = bzDecompressEnd(&d->zStream); + if (result != BZ_OK) { + //qCDebug(KArchiveLog) << "bzDecompressEnd returned " << result; + return false; + } + } else if (d->mode == QIODevice::WriteOnly) { + const int result = bzCompressEnd(&d->zStream); + if (result != BZ_OK) { + //qCDebug(KArchiveLog) << "bzCompressEnd returned " << result; + return false; + } + } else { + //qCWarning(KArchiveLog) << "Unsupported mode " << d->mode << ". Only QIODevice::ReadOnly and QIODevice::WriteOnly supported"; + return false; + } + d->isInitialized = false; + return true; +} + +void KBzip2Filter::reset() +{ + // bzip2 doesn't seem to have a reset call... + terminate(); + init(d->mode); +} + +void KBzip2Filter::setOutBuffer(char *data, uint maxlen) +{ + d->zStream.avail_out = maxlen; + d->zStream.next_out = data; +} + +void KBzip2Filter::setInBuffer(const char *data, unsigned int size) +{ + d->zStream.avail_in = size; + d->zStream.next_in = const_cast(data); +} + +int KBzip2Filter::inBufferAvailable() const +{ + return d->zStream.avail_in; +} + +int KBzip2Filter::outBufferAvailable() const +{ + return d->zStream.avail_out; +} + +KBzip2Filter::Result KBzip2Filter::uncompress() +{ + //qCDebug(KArchiveLog) << "Calling bzDecompress with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); + int result = bzDecompress(&d->zStream); + if (result < BZ_OK) { + qCWarning(KArchiveLog) << "bzDecompress returned" << result; + } + + switch (result) { + case BZ_OK: + return KFilterBase::Ok; + case BZ_STREAM_END: + return KFilterBase::End; + default: + return KFilterBase::Error; + } +} + +KBzip2Filter::Result KBzip2Filter::compress(bool finish) +{ + //qCDebug(KArchiveLog) << "Calling bzCompress with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); + int result = bzCompress(&d->zStream, finish ? BZ_FINISH : BZ_RUN); + + switch (result) { + case BZ_OK: + case BZ_FLUSH_OK: + case BZ_RUN_OK: + case BZ_FINISH_OK: + return KFilterBase::Ok; + break; + case BZ_STREAM_END: + //qCDebug(KArchiveLog) << " bzCompress returned " << result; + return KFilterBase::End; + break; + default: + //qCDebug(KArchiveLog) << " bzCompress returned " << result; + return KFilterBase::Error; + break; + } +} + +#endif /* HAVE_BZIP2_SUPPORT */ diff --git a/src/karchive/src/kbzip2filter.h b/src/karchive/src/kbzip2filter.h new file mode 100644 index 0000000000..dd94649bd6 --- /dev/null +++ b/src/karchive/src/kbzip2filter.h @@ -0,0 +1,51 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef __kbzip2filter__h +#define __kbzip2filter__h + +#include + +#if HAVE_BZIP2_SUPPORT + +#include "kfilterbase.h" + +/** + * Internal class used by KFilterDev + * @internal + */ +class KBzip2Filter : public KFilterBase +{ +public: + KBzip2Filter(); + virtual ~KBzip2Filter(); + + bool init(int) override; + int mode() const override; + bool terminate() override; + void reset() override; + bool readHeader() override + { + return true; // bzip2 handles it by itself ! Cool ! + } + bool writeHeader(const QByteArray &) override + { + return true; + } + void setOutBuffer(char *data, uint maxlen) override; + void setInBuffer(const char *data, uint size) override; + int inBufferAvailable() const override; + int outBufferAvailable() const override; + Result uncompress() override; + Result compress(bool finish) override; +private: + class Private; + Private *const d; +}; + +#endif + +#endif diff --git a/src/karchive/src/kcompressiondevice.cpp b/src/karchive/src/kcompressiondevice.cpp new file mode 100644 index 0000000000..cff635c83a --- /dev/null +++ b/src/karchive/src/kcompressiondevice.cpp @@ -0,0 +1,418 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + SPDX-FileCopyrightText: 2011 Mario Bensi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kcompressiondevice.h" +#include "kcompressiondevice_p.h" +#include "loggingcategory.h" +#include +#include "kfilterbase.h" +#include +#include // for EOF +#include +#include + +#include "kgzipfilter.h" +#include "knonefilter.h" + +#if HAVE_BZIP2_SUPPORT +#include "kbzip2filter.h" +#endif +#if HAVE_XZ_SUPPORT +#include "kxzfilter.h" +#endif + +#include + +class KCompressionDevicePrivate +{ +public: + KCompressionDevicePrivate(KCompressionDevice *q) + : bNeedHeader(true) + , bSkipHeaders(false) + , bOpenedUnderlyingDevice(false) + , type(KCompressionDevice::None) + , errorCode(QFileDevice::NoError) + , deviceReadPos(0) + , q(q) + { + } + + void propagateErrorCode(); + + bool bNeedHeader; + bool bSkipHeaders; + bool bOpenedUnderlyingDevice; + QByteArray buffer; // Used as 'input buffer' when reading, as 'output buffer' when writing + QByteArray origFileName; + KFilterBase::Result result; + KFilterBase *filter; + KCompressionDevice::CompressionType type; + QFileDevice::FileError errorCode; + qint64 deviceReadPos; + KCompressionDevice *q; +}; + +void KCompressionDevicePrivate::propagateErrorCode() +{ + QIODevice *dev = filter->device(); + if (QFileDevice *fileDev = qobject_cast(dev)) { + if (fileDev->error() != QFileDevice::NoError) { + errorCode = fileDev->error(); + q->setErrorString(dev->errorString()); + } + } + // ... we have no generic way to propagate errors from other kinds of iodevices. Sucks, heh? :( +} + +KFilterBase *KCompressionDevice::filterForCompressionType(KCompressionDevice::CompressionType type) +{ + switch (type) { + case KCompressionDevice::GZip: + return new KGzipFilter; + case KCompressionDevice::BZip2: +#if HAVE_BZIP2_SUPPORT + return new KBzip2Filter; +#else + return nullptr; +#endif + case KCompressionDevice::Xz: +#if HAVE_XZ_SUPPORT + return new KXzFilter; +#else + return nullptr; +#endif + case KCompressionDevice::None: + return new KNoneFilter; + } + return nullptr; +} + +KCompressionDevice::KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type) + : d(new KCompressionDevicePrivate(this)) +{ + assert(inputDevice); + d->filter = filterForCompressionType(type); + if (d->filter) { + d->type = type; + d->filter->setDevice(inputDevice, autoDeleteInputDevice); + } +} + +KCompressionDevice::KCompressionDevice(const QString &fileName, CompressionType type) + : d(new KCompressionDevicePrivate(this)) +{ + QFile *f = new QFile(fileName); + d->filter = filterForCompressionType(type); + if (d->filter) { + d->type = type; + d->filter->setDevice(f, true); + } else { + delete f; + } +} + +KCompressionDevice::~KCompressionDevice() +{ + if (isOpen()) { + close(); + } + delete d->filter; + delete d; +} + +KCompressionDevice::CompressionType KCompressionDevice::compressionType() const +{ + return d->type; +} + +bool KCompressionDevice::open(QIODevice::OpenMode mode) +{ + if (isOpen()) { + //qCWarning(KArchiveLog) << "KCompressionDevice::open: device is already open"; + return true; // QFile returns false, but well, the device -is- open... + } + if (!d->filter) { + return false; + } + d->bOpenedUnderlyingDevice = false; + //qCDebug(KArchiveLog) << mode; + if (mode == QIODevice::ReadOnly) { + d->buffer.resize(0); + } else { + d->buffer.resize(BUFFER_SIZE); + d->filter->setOutBuffer(d->buffer.data(), d->buffer.size()); + } + if (!d->filter->device()->isOpen()) { + if (!d->filter->device()->open(mode)) { + //qCWarning(KArchiveLog) << "KCompressionDevice::open: Couldn't open underlying device"; + d->propagateErrorCode(); + return false; + } + d->bOpenedUnderlyingDevice = true; + } + d->bNeedHeader = !d->bSkipHeaders; + d->filter->setFilterFlags(d->bSkipHeaders ? KFilterBase::NoHeaders : KFilterBase::WithHeaders); + if (!d->filter->init(mode)) { + return false; + } + d->result = KFilterBase::Ok; + setOpenMode(mode); + return true; +} + +void KCompressionDevice::close() +{ + if (!isOpen()) { + return; + } + if (d->filter->mode() == QIODevice::WriteOnly && d->errorCode == QFileDevice::NoError) { + write(nullptr, 0); // finish writing + } + //qCDebug(KArchiveLog) << "Calling terminate()."; + + if (!d->filter->terminate()) { + //qCWarning(KArchiveLog) << "KCompressionDevice::close: terminate returned an error"; + d->errorCode = QFileDevice::UnspecifiedError; + } + if (d->bOpenedUnderlyingDevice) { + QIODevice *dev = d->filter->device(); + dev->close(); + d->propagateErrorCode(); + } + setOpenMode(QIODevice::NotOpen); +} + +QFileDevice::FileError KCompressionDevice::error() const +{ + return d->errorCode; +} + +bool KCompressionDevice::seek(qint64 pos) +{ + if (d->deviceReadPos == pos) { + return QIODevice::seek(pos); + } + + //qCDebug(KArchiveLog) << "seek(" << pos << ") called, current pos=" << QIODevice::pos(); + + Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly); + + if (pos == 0) { + if (!QIODevice::seek(pos)) + return false; + + // We can forget about the cached data + d->bNeedHeader = !d->bSkipHeaders; + d->result = KFilterBase::Ok; + d->filter->setInBuffer(nullptr, 0); + d->filter->reset(); + d->deviceReadPos = 0; + return d->filter->device()->reset(); + } + + qint64 bytesToRead; + if (d->deviceReadPos < pos) { // we can start from here + bytesToRead = pos - d->deviceReadPos; + // Since we're going to do a read() below + // we need to reset the internal QIODevice pos to the real position we are + // so that after read() we are indeed pointing to the pos seek + // asked us to be in + if (!QIODevice::seek(d->deviceReadPos)) { + return false; + } + } else { + // we have to start from 0 ! Ugly and slow, but better than the previous + // solution (KTarGz was allocating everything into memory) + if (!seek(0)) { // recursive + return false; + } + bytesToRead = pos; + } + + //qCDebug(KArchiveLog) << "reading " << bytesToRead << " dummy bytes"; + QByteArray dummy(qMin(bytesToRead, qint64(SEEK_BUFFER_SIZE)), 0); + while (bytesToRead > 0) { + const qint64 bytesToReadThisTime = qMin(bytesToRead, qint64(dummy.size())); + const bool result = (read(dummy.data(), bytesToReadThisTime) == bytesToReadThisTime); + if (!result) { + return false; + } + bytesToRead -= bytesToReadThisTime; + } + return true; +} + +bool KCompressionDevice::atEnd() const +{ + return (d->type == KCompressionDevice::None || d->result == KFilterBase::End) + && QIODevice::atEnd() // take QIODevice's internal buffer into account + && d->filter->device()->atEnd(); +} + +qint64 KCompressionDevice::readData(char *data, qint64 maxlen) +{ + Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly); + //qCDebug(KArchiveLog) << "maxlen=" << maxlen; + KFilterBase *filter = d->filter; + + uint dataReceived = 0; + + // We came to the end of the stream + if (d->result == KFilterBase::End) { + return dataReceived; + } + + // If we had an error, return -1. + if (d->result != KFilterBase::Ok) { + return -1; + } + + qint64 availOut = maxlen; + filter->setOutBuffer(data, maxlen); + + while (dataReceived < maxlen) { + if (filter->inBufferEmpty()) { + // Not sure about the best size to set there. + // For sure, it should be bigger than the header size (see comment in readHeader) + d->buffer.resize(BUFFER_SIZE); + // Request data from underlying device + int size = filter->device()->read(d->buffer.data(), + d->buffer.size()); + //qCDebug(KArchiveLog) << "got" << size << "bytes from device"; + if (size) { + filter->setInBuffer(d->buffer.data(), size); + } else { + // Not enough data available in underlying device for now + break; + } + } + if (d->bNeedHeader) { + (void) filter->readHeader(); + d->bNeedHeader = false; + } + + d->result = filter->uncompress(); + + if (d->result == KFilterBase::Error) { + //qCWarning(KArchiveLog) << "KCompressionDevice: Error when uncompressing data"; + break; + } + + // We got that much data since the last time we went here + uint outReceived = availOut - filter->outBufferAvailable(); + //qCDebug(KArchiveLog) << "avail_out = " << filter->outBufferAvailable() << " result=" << d->result << " outReceived=" << outReceived; + if (availOut < uint(filter->outBufferAvailable())) { + //qCWarning(KArchiveLog) << " last availOut " << availOut << " smaller than new avail_out=" << filter->outBufferAvailable() << " !"; + } + + dataReceived += outReceived; + data += outReceived; + availOut = maxlen - dataReceived; + if (d->result == KFilterBase::End) { + // We're actually at the end, no more data to check + if (filter->device()->atEnd()) { + break; + } + + // Still not done, re-init and try again + filter->init(filter->mode()); + } + filter->setOutBuffer(data, availOut); + } + + d->deviceReadPos += dataReceived; + return dataReceived; +} + +qint64 KCompressionDevice::writeData(const char *data /*0 to finish*/, qint64 len) +{ + KFilterBase *filter = d->filter; + Q_ASSERT(filter->mode() == QIODevice::WriteOnly); + // If we had an error, return 0. + if (d->result != KFilterBase::Ok) { + return 0; + } + + bool finish = (data == nullptr); + if (!finish) { + filter->setInBuffer(data, len); + if (d->bNeedHeader) { + (void)filter->writeHeader(d->origFileName); + d->bNeedHeader = false; + } + } + + uint dataWritten = 0; + uint availIn = len; + while (dataWritten < len || finish) { + + d->result = filter->compress(finish); + + if (d->result == KFilterBase::Error) { + //qCWarning(KArchiveLog) << "KCompressionDevice: Error when compressing data"; + // What to do ? + break; + } + + // Wrote everything ? + if (filter->inBufferEmpty() || (d->result == KFilterBase::End)) { + // We got that much data since the last time we went here + uint wrote = availIn - filter->inBufferAvailable(); + + //qCDebug(KArchiveLog) << " Wrote everything for now. avail_in=" << filter->inBufferAvailable() << "result=" << d->result << "wrote=" << wrote; + + // Move on in the input buffer + data += wrote; + dataWritten += wrote; + + availIn = len - dataWritten; + //qCDebug(KArchiveLog) << " availIn=" << availIn << "dataWritten=" << dataWritten << "pos=" << pos(); + if (availIn > 0) { + filter->setInBuffer(data, availIn); + } + } + + if (filter->outBufferFull() || (d->result == KFilterBase::End) || finish) { + //qCDebug(KArchiveLog) << " writing to underlying. avail_out=" << filter->outBufferAvailable(); + int towrite = d->buffer.size() - filter->outBufferAvailable(); + if (towrite > 0) { + // Write compressed data to underlying device + int size = filter->device()->write(d->buffer.data(), towrite); + if (size != towrite) { + //qCWarning(KArchiveLog) << "KCompressionDevice::write. Could only write " << size << " out of " << towrite << " bytes"; + d->errorCode = QFileDevice::WriteError; + setErrorString(tr("Could not write. Partition full?")); + return 0; // indicate an error + } + //qCDebug(KArchiveLog) << " wrote " << size << " bytes"; + } + if (d->result == KFilterBase::End) { + Q_ASSERT(finish); // hopefully we don't get end before finishing + break; + } + d->buffer.resize(BUFFER_SIZE); + filter->setOutBuffer(d->buffer.data(), d->buffer.size()); + } + } + + return dataWritten; +} + +void KCompressionDevice::setOrigFileName(const QByteArray &fileName) +{ + d->origFileName = fileName; +} + +void KCompressionDevice::setSkipHeaders() +{ + d->bSkipHeaders = true; +} + +KFilterBase *KCompressionDevice::filterBase() +{ + return d->filter; +} diff --git a/src/karchive/src/kcompressiondevice.h b/src/karchive/src/kcompressiondevice.h new file mode 100644 index 0000000000..1007d28f0d --- /dev/null +++ b/src/karchive/src/kcompressiondevice.h @@ -0,0 +1,128 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + SPDX-FileCopyrightText: 2011 Mario Bensi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef __kcompressiondevice_h +#define __kcompressiondevice_h + +#include +#include +#include +#include +#include +class KCompressionDevicePrivate; + +class KFilterBase; + +/** + * @class KCompressionDevice kcompressiondevice.h KCompressionDevice + * + * A class for reading and writing compressed data onto a device + * (e.g. file, but other usages are possible, like a buffer or a socket). + * + * Use this class to read/write compressed files. + */ + +class KARCHIVE_EXPORT KCompressionDevice : public QIODevice // KF6 TODO: consider inheriting from QFileDevice, so apps can use error() generically ? +{ + Q_OBJECT +public: + enum CompressionType { + GZip, + BZip2, + Xz, + None + }; + + /** + * Constructs a KCompressionDevice for a given CompressionType (e.g. GZip, BZip2 etc.). + * @param inputDevice input device. + * @param autoDeleteInputDevice if true, @p inputDevice will be deleted automatically + * @param type the CompressionType to use. + */ + KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type); + + /** + * Constructs a KCompressionDevice for a given CompressionType (e.g. GZip, BZip2 etc.). + * @param fileName the name of the file to filter. + * @param type the CompressionType to use. + */ + KCompressionDevice(const QString &fileName, CompressionType type); + + /** + * Destructs the KCompressionDevice. + * Calls close() if the filter device is still open. + */ + virtual ~KCompressionDevice(); + + /** + * The compression actually used by this device. + * If the support for the compression requested in the constructor + * is not available, then the device will use None. + */ + CompressionType compressionType() const; + + /** + * Open for reading or writing. + */ + bool open(QIODevice::OpenMode mode) override; + + /** + * Close after reading or writing. + */ + void close() override; + + /** + * For writing gzip compressed files only: + * set the name of the original file, to be used in the gzip header. + * @param fileName the name of the original file + */ + void setOrigFileName(const QByteArray &fileName); + + /** + * Call this let this device skip the gzip headers when reading/writing. + * This way KCompressionDevice (with gzip filter) can be used as a direct wrapper + * around zlib - this is used by KZip. + */ + void setSkipHeaders(); + + /** + * That one can be quite slow, when going back. Use with care. + */ + bool seek(qint64) override; + + bool atEnd() const override; + + /** + * Call this to create the appropriate filter for the CompressionType + * named @p type. + * @param type the type of the compression filter + * @return the filter for the @p type, or 0 if not found + */ + static KFilterBase *filterForCompressionType(CompressionType type); + + /** + * Returns the error code from the last failing operation. + * This is especially useful after calling close(), which unfortunately returns void + * (see https://bugreports.qt.io/browse/QTBUG-70033), to see if the flushing done by close + * was able to write all the data to disk. + */ + QFileDevice::FileError error() const; + +protected: + friend class K7Zip; + + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + + KFilterBase *filterBase(); +private: + friend KCompressionDevicePrivate; + KCompressionDevicePrivate *const d; +}; + +Q_DECLARE_METATYPE(KCompressionDevice::CompressionType) + +#endif diff --git a/src/karchive/src/kcompressiondevice_p.h b/src/karchive/src/kcompressiondevice_p.h new file mode 100644 index 0000000000..2a54d32587 --- /dev/null +++ b/src/karchive/src/kcompressiondevice_p.h @@ -0,0 +1,13 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + SPDX-FileCopyrightText: 2011 Mario Bensi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef __kcompressiondevice_p_h +#define __kcompressiondevice_p_h + +#define BUFFER_SIZE 8*1024 +#define SEEK_BUFFER_SIZE 3*BUFFER_SIZE + +#endif diff --git a/src/karchive/src/kfilterbase.cpp b/src/karchive/src/kfilterbase.cpp new file mode 100644 index 0000000000..03e39821ac --- /dev/null +++ b/src/karchive/src/kfilterbase.cpp @@ -0,0 +1,80 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kfilterbase.h" + +#include + +class KFilterBasePrivate +{ +public: + KFilterBasePrivate() + : m_flags(KFilterBase::WithHeaders) + , m_dev(nullptr) + , m_bAutoDel(false) + {} + KFilterBase::FilterFlags m_flags; + QIODevice *m_dev; + bool m_bAutoDel; +}; + +KFilterBase::KFilterBase() + : d(new KFilterBasePrivate) +{ +} + +KFilterBase::~KFilterBase() +{ + if (d->m_bAutoDel) { + delete d->m_dev; + } + delete d; +} + +void KFilterBase::setDevice(QIODevice *dev, bool autodelete) +{ + d->m_dev = dev; + d->m_bAutoDel = autodelete; +} + +QIODevice *KFilterBase::device() +{ + return d->m_dev; +} + +bool KFilterBase::inBufferEmpty() const +{ + return inBufferAvailable() == 0; +} + +bool KFilterBase::outBufferFull() const +{ + return outBufferAvailable() == 0; +} + +bool KFilterBase::terminate() +{ + return true; +} + +void KFilterBase::reset() +{ +} + +void KFilterBase::setFilterFlags(FilterFlags flags) +{ + d->m_flags = flags; +} + +KFilterBase::FilterFlags KFilterBase::filterFlags() const +{ + return d->m_flags; +} + +void KFilterBase::virtual_hook(int, void *) +{ + /*BASE::virtual_hook( id, data );*/ +} diff --git a/src/karchive/src/kfilterbase.h b/src/karchive/src/kfilterbase.h new file mode 100644 index 0000000000..b2fa1a2718 --- /dev/null +++ b/src/karchive/src/kfilterbase.h @@ -0,0 +1,108 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef __kfilterbase__h +#define __kfilterbase__h + +#include + +#include +#include +class KFilterBasePrivate; + +class QIODevice; + +/** + * @class KFilterBase kfilterbase.h KFilterBase + * + * This is the base class for compression filters + * such as gzip and bzip2. It's pretty much internal. + * Don't use directly, use KFilterDev instead. + * @internal + */ +class KARCHIVE_EXPORT KFilterBase +{ +public: + KFilterBase(); + virtual ~KFilterBase(); + + /** + * Sets the device on which the filter will work + * @param dev the device on which the filter will work + * @param autodelete if true, @p dev is deleted when the filter is deleted + */ + void setDevice(QIODevice *dev, bool autodelete = false); + // Note that this isn't in the constructor, because of KLibFactory::create, + // but it should be called before using the filterbase ! + + /** + * Returns the device on which the filter will work. + * @returns the device on which the filter will work + */ + QIODevice *device(); + /** \internal */ + virtual bool init(int mode) = 0; + /** \internal */ + virtual int mode() const = 0; + /** \internal */ + virtual bool terminate(); + /** \internal */ + virtual void reset(); + /** \internal */ + virtual bool readHeader() = 0; + /** \internal */ + virtual bool writeHeader(const QByteArray &filename) = 0; + /** \internal */ + virtual void setOutBuffer(char *data, uint maxlen) = 0; + /** \internal */ + virtual void setInBuffer(const char *data, uint size) = 0; + /** \internal */ + virtual bool inBufferEmpty() const; + /** \internal */ + virtual int inBufferAvailable() const = 0; + /** \internal */ + virtual bool outBufferFull() const; + /** \internal */ + virtual int outBufferAvailable() const = 0; + + /** \internal */ + enum Result { + Ok, + End, + Error + }; + /** \internal */ + virtual Result uncompress() = 0; + /** \internal */ + virtual Result compress(bool finish) = 0; + + /** + * \internal + * \since 4.3 + */ + enum FilterFlags { + NoHeaders = 0, + WithHeaders = 1, + ZlibHeaders = 2 // only use for gzip compression + }; + /** + * \internal + * \since 4.3 + */ + void setFilterFlags(FilterFlags flags); + FilterFlags filterFlags() const; + +protected: + /** Virtual hook, used to add new "virtual" functions while maintaining + binary compatibility. Unused in this class. + */ + virtual void virtual_hook(int id, void *data); +private: + Q_DISABLE_COPY(KFilterBase) + KFilterBasePrivate *const d; +}; + +#endif diff --git a/src/karchive/src/kfilterdev.cpp b/src/karchive/src/kfilterdev.cpp new file mode 100644 index 0000000000..71b34dd7f7 --- /dev/null +++ b/src/karchive/src/kfilterdev.cpp @@ -0,0 +1,88 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000, 2006 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kfilterdev.h" +#include "loggingcategory.h" +#include +#include + +#include + +static KCompressionDevice::CompressionType findCompressionByFileName(const QString &fileName) +{ + if (fileName.endsWith(QLatin1String(".gz"), Qt::CaseInsensitive)) { + return KCompressionDevice::GZip; + } +#if HAVE_BZIP2_SUPPORT + if (fileName.endsWith(QLatin1String(".bz2"), Qt::CaseInsensitive)) { + return KCompressionDevice::BZip2; + } +#endif +#if HAVE_XZ_SUPPORT + if (fileName.endsWith(QLatin1String(".lzma"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".xz"), Qt::CaseInsensitive)) { + return KCompressionDevice::Xz; + } +#endif + else { + // not a warning, since this is called often with other mimetypes (see #88574)... + // maybe we can avoid that though? + //qCDebug(KArchiveLog) << "findCompressionByFileName : no compression found for " << fileName; + } + + return KCompressionDevice::None; +} + +KFilterDev::KFilterDev(const QString &fileName) + : KCompressionDevice(fileName, findCompressionByFileName(fileName)) +{ +} + +KCompressionDevice::CompressionType KFilterDev::compressionTypeForMimeType(const QString &mimeType) +{ + if (mimeType == QLatin1String("application/x-gzip")) { + return KCompressionDevice::GZip; + } +#if HAVE_BZIP2_SUPPORT + if (mimeType == QLatin1String("application/x-bzip") + || mimeType == QLatin1String("application/x-bzip2") // old name, kept for compatibility + ) { + return KCompressionDevice::BZip2; + } +#endif +#if HAVE_XZ_SUPPORT + if (mimeType == QLatin1String("application/x-lzma") // legacy name, still used + || mimeType == QLatin1String("application/x-xz") // current naming + ) { + return KCompressionDevice::Xz; + } +#endif + QMimeDatabase db; + const QMimeType mime = db.mimeTypeForName(mimeType); + if (mime.isValid()) { + if (mime.inherits(QStringLiteral("application/x-gzip"))) { + return KCompressionDevice::GZip; + } +#if HAVE_BZIP2_SUPPORT + if (mime.inherits(QStringLiteral("application/x-bzip"))) { + return KCompressionDevice::BZip2; + } +#endif +#if HAVE_XZ_SUPPORT + if (mime.inherits(QStringLiteral("application/x-lzma"))) { + return KCompressionDevice::Xz; + } + + if (mime.inherits(QStringLiteral("application/x-xz"))) { + return KCompressionDevice::Xz; + } +#endif + } + + // not a warning, since this is called often with other mimetypes (see #88574)... + // maybe we can avoid that though? + //qCDebug(KArchiveLog) << "no compression found for" << mimeType; + return KCompressionDevice::None; +} diff --git a/src/karchive/src/kfilterdev.h b/src/karchive/src/kfilterdev.h new file mode 100644 index 0000000000..b0e4fa7352 --- /dev/null +++ b/src/karchive/src/kfilterdev.h @@ -0,0 +1,147 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef __kfilterdev_h +#define __kfilterdev_h + +#include +#include +#include + +class QFile; +class KFilterBase; + +/** + * @class KFilterDev kfilterdev.h KFilterDev + * + * A class for reading and writing compressed data onto a device + * (e.g. file, but other usages are possible, like a buffer or a socket). + * + * To simply read/write compressed files, see deviceForFile. + * + * KFilterDev adds MIME type support to KCompressionDevice, and also + * provides compatibility methods for KDE 4 code. + * + * @author David Faure + */ +class KARCHIVE_EXPORT KFilterDev : public KCompressionDevice +{ + Q_OBJECT +public: + /** + * @since 5.0 + * Constructs a KFilterDev for a given FileName. + * @param fileName the name of the file to filter. + */ + KFilterDev(const QString &fileName); + + /** + * Returns the compression type for the given mimetype, if possible. Otherwise returns None. + * This handles simple cases like application/x-gzip, but also application/x-compressed-tar, and inheritance. + */ + static CompressionType compressionTypeForMimeType(const QString &mimetype); + +#if KARCHIVE_ENABLE_DEPRECATED_SINCE(5, 0) + /** + * @deprecated Since 5.0, use the constructor instead (if mimetype is empty), or KCompressionDevice (if + * the mimetype is known). + * + * Use: + * KFilterDev dev(fileName) + * instead of: + * QIODevice * dev = KFilterDev::deviceForFile(fileName); + * + * If the mimetype was specified explicitly, use: + * KCompressionDevice dev(fileName, KCompressionDevice::GZip); + * instead of: + * QIODevice * dev = KFilterDev::deviceForFile(fileName, "application/gzip"); + * + * Creates an i/o device that is able to read from @p fileName, + * whether it's compressed or not. Available compression filters + * (gzip/bzip2 etc.) will automatically be used. + * + * The compression filter to be used is determined from the @p fileName + * if @p mimetype is empty. Pass "application/x-gzip" or "application/x-bzip" + * to force the corresponding decompression filter, if available. + * + * Warning: application/x-bzip may not be available. + * In that case a QFile opened on the compressed data will be returned ! + * Use KFilterBase::findFilterByMimeType and code similar to what + * deviceForFile is doing, to better control what's happening. + * + * The returned QIODevice has to be deleted after using. + * + * @param fileName the name of the file to filter + * @param mimetype the mime type of the file to filter, or QString() if unknown + * @param forceFilter if true, the function will either find a compression filter, or return 0. + * If false, it will always return a QIODevice. If no + * filter is available it will return a simple QFile. + * This can be useful if the file is usable without a filter. + * @return if a filter has been found, the KCompressionDevice for the filter. If the + * filter does not exist, the return value depends on @p forceFilter. + * The returned KCompressionDevice has to be deleted after using. + */ + KARCHIVE_DEPRECATED_VERSION(5, 0, "See API docs") + static KCompressionDevice *deviceForFile(const QString &fileName, + const QString &mimetype = QString(), + bool forceFilter = false) + { + KCompressionDevice *device; + if (mimetype.isEmpty()) { + device = new KFilterDev(fileName); + } else { + device = new KCompressionDevice(fileName, compressionTypeForMimeType(mimetype)); + } + if (device->compressionType() == KCompressionDevice::None && forceFilter) { + delete device; + return nullptr; + } else { + return device; + } + } +#endif + +#if KARCHIVE_ENABLE_DEPRECATED_SINCE(5, 0) + /** + * @deprecated Since 5.0, use KCompressionDevice + * + * Use: + * KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(mimeType); + * KCompressionDevice flt(&file, false, type); + * instead of: + * QIODevice *flt = KFilterDev::device(&file, mimeType, false); + * + * Creates an i/o device that is able to read from the QIODevice @p inDevice, + * whether the data is compressed or not. Available compression filters + * (gzip/bzip2 etc.) will automatically be used. + * + * The compression filter to be used is determined @p mimetype . + * Pass "application/x-gzip" or "application/x-bzip" + * to use the corresponding decompression filter. + * + * Warning: application/x-bzip may not be available. + * In that case 0 will be returned ! + * + * The returned QIODevice has to be deleted after using. + * @param inDevice input device. Won't be deleted if @p autoDeleteInDevice = false + * @param mimetype the mime type for the filter + * @param autoDeleteInDevice if true, @p inDevice will be deleted automatically + * @return a KCompressionDevice that filters the original stream. Must be deleted after using + */ + KARCHIVE_DEPRECATED_VERSION(5, 0, "See API docs") + static KCompressionDevice *device(QIODevice *inDevice, const QString &mimetype, + bool autoDeleteInDevice = true) + { + if (inDevice == nullptr) { + return nullptr; + } + CompressionType type = compressionTypeForMimeType(mimetype); + KCompressionDevice *device = new KCompressionDevice(inDevice, autoDeleteInDevice, type); + return device; + } +#endif +}; + +#endif diff --git a/src/karchive/src/kgzipfilter.cpp b/src/karchive/src/kgzipfilter.cpp new file mode 100644 index 0000000000..12cedc8ad6 --- /dev/null +++ b/src/karchive/src/kgzipfilter.cpp @@ -0,0 +1,377 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kgzipfilter.h" +#include "loggingcategory.h" + +#include + +#if defined(Q_OS_WIN) && defined(Q_CC_MSVC) +#include "QtZlib/zlib.h" +#else +#include +#endif +#include +#include + +#ifdef Z_PREFIX +#undef compress +#undef uncompress +#endif + +/* gzip flag byte */ +#define ORIG_NAME 0x08 /* bit 3 set: original file name present */ + +// #define DEBUG_GZIP + +class Q_DECL_HIDDEN KGzipFilter::Private +{ +public: + Private() + : headerWritten(false) + , footerWritten(false) + , compressed(false) + , mode(0) + , crc(0) + , isInitialized(false) + { + zStream.zalloc = static_cast(nullptr); + zStream.zfree = static_cast(nullptr); + zStream.opaque = static_cast(nullptr); + } + + z_stream zStream; + bool headerWritten; + bool footerWritten; + bool compressed; + int mode; + ulong crc; + bool isInitialized; +}; + +KGzipFilter::KGzipFilter() + : d(new Private) +{ +} + +KGzipFilter::~KGzipFilter() +{ + delete d; +} + +bool KGzipFilter::init(int mode) +{ + switch (filterFlags()) { + case NoHeaders: + return init(mode, RawDeflate); + case WithHeaders: + return init(mode, GZipHeader); + case ZlibHeaders: + return init(mode, ZlibHeader); + } + return false; +} + +bool KGzipFilter::init(int mode, Flag flag) +{ + if (d->isInitialized) { + terminate(); + } + d->zStream.next_in = Z_NULL; + d->zStream.avail_in = 0; + if (mode == QIODevice::ReadOnly) { + const int windowBits = (flag == RawDeflate) + ? -MAX_WBITS /*no zlib header*/ + : (flag == GZipHeader) ? + MAX_WBITS + 32 /* auto-detect and eat gzip header */ + : MAX_WBITS /*zlib header*/; + const int result = inflateInit2(&d->zStream, windowBits); + if (result != Z_OK) { + //qCDebug(KArchiveLog) << "inflateInit2 returned " << result; + return false; + } + } else if (mode == QIODevice::WriteOnly) { + int result = deflateInit2(&d->zStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); // same here + if (result != Z_OK) { + //qCDebug(KArchiveLog) << "deflateInit returned " << result; + return false; + } + } else { + //qCWarning(KArchiveLog) << "KGzipFilter: Unsupported mode " << mode << ". Only QIODevice::ReadOnly and QIODevice::WriteOnly supported"; + return false; + } + d->mode = mode; + d->compressed = true; + d->headerWritten = false; + d->footerWritten = false; + d->isInitialized = true; + return true; +} + +int KGzipFilter::mode() const +{ + return d->mode; +} + +bool KGzipFilter::terminate() +{ + if (d->mode == QIODevice::ReadOnly) { + int result = inflateEnd(&d->zStream); + if (result != Z_OK) { + //qCDebug(KArchiveLog) << "inflateEnd returned " << result; + return false; + } + } else if (d->mode == QIODevice::WriteOnly) { + int result = deflateEnd(&d->zStream); + if (result != Z_OK) { + //qCDebug(KArchiveLog) << "deflateEnd returned " << result; + return false; + } + } + d->isInitialized = false; + return true; +} + +void KGzipFilter::reset() +{ + if (d->mode == QIODevice::ReadOnly) { + int result = inflateReset(&d->zStream); + if (result != Z_OK) { + //qCDebug(KArchiveLog) << "inflateReset returned " << result; + // TODO return false + } + } else if (d->mode == QIODevice::WriteOnly) { + int result = deflateReset(&d->zStream); + if (result != Z_OK) { + //qCDebug(KArchiveLog) << "deflateReset returned " << result; + // TODO return false + } + d->headerWritten = false; + d->footerWritten = false; + } +} + +bool KGzipFilter::readHeader() +{ + // We now rely on zlib to read the full header (see the MAX_WBITS + 32 in init). + // We just use this method to check if the data is actually compressed. + +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << "avail=" << d->zStream.avail_in; +#endif + // Assume not compressed until we see a gzip header + d->compressed = false; + const Bytef *p = d->zStream.next_in; + int i = d->zStream.avail_in; + if ((i -= 10) < 0) { + return false; // Need at least 10 bytes + } +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << "first byte is " << QString::number(*p, 16); +#endif + if (*p++ != 0x1f) { + return false; // GZip magic + } +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << "second byte is " << QString::number(*p, 16); +#endif + if (*p++ != 0x8b) { + return false; + } + + d->compressed = true; +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << "header OK"; +#endif + return true; +} + +/* Output a 16 bit value, lsb first */ +#define put_short(w) \ + *p++ = uchar((w) & 0xff); \ + *p++ = uchar(ushort(w) >> 8); + +/* Output a 32 bit value to the bit stream, lsb first */ +#define put_long(n) \ + put_short((n) & 0xffff); \ + put_short((ulong(n)) >> 16); + +bool KGzipFilter::writeHeader(const QByteArray &fileName) +{ + Bytef *p = d->zStream.next_out; + int i = d->zStream.avail_out; + *p++ = 0x1f; + *p++ = 0x8b; + *p++ = Z_DEFLATED; + *p++ = ORIG_NAME; + put_long(time(nullptr)); // Modification time (in unix format) + *p++ = 0; // Extra flags (2=max compress, 4=fastest compress) + *p++ = 3; // Unix + + uint len = fileName.length(); + for (uint j = 0; j < len; ++j) { + *p++ = fileName[j]; + } + *p++ = 0; + int headerSize = p - d->zStream.next_out; + i -= headerSize; + Q_ASSERT(i > 0); + d->crc = crc32(0L, nullptr, 0); + d->zStream.next_out = p; + d->zStream.avail_out = i; + d->headerWritten = true; + return true; +} + +void KGzipFilter::writeFooter() +{ + Q_ASSERT(d->headerWritten); + Q_ASSERT(!d->footerWritten); + Bytef *p = d->zStream.next_out; + int i = d->zStream.avail_out; + //qCDebug(KArchiveLog) << "avail_out=" << i << "writing CRC=" << QString::number(d->crc, 16) << "at p=" << p; + put_long(d->crc); + //qCDebug(KArchiveLog) << "writing totalin=" << d->zStream.total_in << "at p=" << p; + put_long(d->zStream.total_in); + i -= p - d->zStream.next_out; + d->zStream.next_out = p; + d->zStream.avail_out = i; + d->footerWritten = true; +} + +void KGzipFilter::setOutBuffer(char *data, uint maxlen) +{ + d->zStream.avail_out = maxlen; + d->zStream.next_out = reinterpret_cast(data); +} +void KGzipFilter::setInBuffer(const char *data, uint size) +{ +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << "avail_in=" << size; +#endif + d->zStream.avail_in = size; + d->zStream.next_in = reinterpret_cast(const_cast(data)); +} +int KGzipFilter::inBufferAvailable() const +{ + return d->zStream.avail_in; +} +int KGzipFilter::outBufferAvailable() const +{ + return d->zStream.avail_out; +} + +KGzipFilter::Result KGzipFilter::uncompress_noop() +{ + // I'm not sure we really need support for that (uncompressed streams), + // but why not, it can't hurt to have it. One case I can think of is someone + // naming a tar file "blah.tar.gz" :-) + if (d->zStream.avail_in > 0) { + int n = (d->zStream.avail_in < d->zStream.avail_out) ? d->zStream.avail_in : d->zStream.avail_out; + memcpy(d->zStream.next_out, d->zStream.next_in, n); + d->zStream.avail_out -= n; + d->zStream.next_in += n; + d->zStream.avail_in -= n; + return KFilterBase::Ok; + } else { + return KFilterBase::End; + } +} + +KGzipFilter::Result KGzipFilter::uncompress() +{ +#ifndef NDEBUG + if (d->mode == 0) { + //qCWarning(KArchiveLog) << "mode==0; KGzipFilter::init was not called!"; + return KFilterBase::Error; + } else if (d->mode == QIODevice::WriteOnly) { + //qCWarning(KArchiveLog) << "uncompress called but the filter was opened for writing!"; + return KFilterBase::Error; + } + Q_ASSERT(d->mode == QIODevice::ReadOnly); +#endif + + if (!d->compressed) { + return uncompress_noop(); + } + +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << "Calling inflate with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); + qCDebug(KArchiveLog) << " next_in=" << d->zStream.next_in; +#endif + + while (d->zStream.avail_in > 0) { + int result = inflate(&d->zStream, Z_SYNC_FLUSH); + +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << " -> inflate returned " << result; + qCDebug(KArchiveLog) << " now avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); + qCDebug(KArchiveLog) << " next_in=" << d->zStream.next_in; +#endif + + if (result == Z_OK) { + return KFilterBase::Ok; + } + + // We can't handle any other results + if (result != Z_STREAM_END) { + return KFilterBase::Error; + } + + // It really was the end + if (d->zStream.avail_in == 0) { + return KFilterBase::End; + } + + // Store before resetting + Bytef *data = d->zStream.next_in; // This is increased appropriately by zlib beforehand + uInt size = d->zStream.avail_in; + + // Reset the stream, if that fails we assume we're at the end + if (!init(d->mode)) { + return KFilterBase::End; + } + + // Reset the data to where we left off + d->zStream.next_in = data; + d->zStream.avail_in = size; + } + + return KFilterBase::End; +} + +KGzipFilter::Result KGzipFilter::compress(bool finish) +{ + Q_ASSERT(d->compressed); + Q_ASSERT(d->mode == QIODevice::WriteOnly); + + const Bytef *p = d->zStream.next_in; + ulong len = d->zStream.avail_in; +#ifdef DEBUG_GZIP + qCDebug(KArchiveLog) << " calling deflate with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); +#endif + const int result = deflate(&d->zStream, finish ? Z_FINISH : Z_NO_FLUSH); + if (result != Z_OK && result != Z_STREAM_END) { + //qCDebug(KArchiveLog) << " deflate returned " << result; + } + if (d->headerWritten) { + //qCDebug(KArchiveLog) << "Computing CRC for the next " << len - d->zStream.avail_in << " bytes"; + d->crc = crc32(d->crc, p, len - d->zStream.avail_in); + } + KGzipFilter::Result callerResult = result == Z_OK ? KFilterBase::Ok : (Z_STREAM_END ? KFilterBase::End : KFilterBase::Error); + + if (result == Z_STREAM_END && d->headerWritten && !d->footerWritten) { + if (d->zStream.avail_out >= 8 /*footer size*/) { + //qCDebug(KArchiveLog) << "finished, write footer"; + writeFooter(); + } else { + // No room to write the footer (#157706/#188415), we'll have to do it on the next pass. + //qCDebug(KArchiveLog) << "finished, but no room for footer yet"; + callerResult = KFilterBase::Ok; + } + } + return callerResult; +} diff --git a/src/karchive/src/kgzipfilter.h b/src/karchive/src/kgzipfilter.h new file mode 100644 index 0000000000..80908a8178 --- /dev/null +++ b/src/karchive/src/kgzipfilter.h @@ -0,0 +1,59 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000, 2009 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef __kgzipfilter__h +#define __kgzipfilter__h + +#include "kfilterbase.h" + +/** + * Internal class used by KFilterDev + * + * This header is not installed. + * + * @internal + */ +class KGzipFilter : public KFilterBase +{ +public: + KGzipFilter(); + virtual ~KGzipFilter(); + + bool init(int mode) override; + + // The top of zlib.h explains it: there are three cases. + // - Raw deflate, no header (e.g. inside a ZIP file) + // - Thin zlib header (1) (which is normally what HTTP calls "deflate" (2)) + // - Gzip header, implemented here by readHeader + // + // (1) as written out by compress()/compress2() + // (2) see http://www.zlib.net/zlib_faq.html#faq39 + enum Flag { + RawDeflate = 0, // raw deflate data + ZlibHeader = 1, // zlib headers (HTTP deflate) + GZipHeader = 2 + }; + bool init(int mode, Flag flag); // for direct users of KGzipFilter + int mode() const override; + bool terminate() override; + void reset() override; + bool readHeader() override; // this is about the GZIP header + bool writeHeader(const QByteArray &fileName) override; + void writeFooter(); + void setOutBuffer(char *data, uint maxlen) override; + void setInBuffer(const char *data, uint size) override; + int inBufferAvailable() const override; + int outBufferAvailable() const override; + Result uncompress() override; + Result compress(bool finish) override; + +private: + Result uncompress_noop(); + class Private; + Private *const d; +}; + +#endif diff --git a/src/karchive/src/klimitediodevice.cpp b/src/karchive/src/klimitediodevice.cpp new file mode 100644 index 0000000000..bd7074db76 --- /dev/null +++ b/src/karchive/src/klimitediodevice.cpp @@ -0,0 +1,71 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2001, 2002, 2007 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "klimitediodevice_p.h" +#include "loggingcategory.h" + +KLimitedIODevice::KLimitedIODevice(QIODevice *dev, qint64 start, qint64 length) + : m_dev(dev) + , m_start(start) + , m_length(length) +{ + //qCDebug(KArchiveLog) << "start=" << start << "length=" << length; + open(QIODevice::ReadOnly); //krazy:exclude=syscalls +} + +bool KLimitedIODevice::open(QIODevice::OpenMode m) +{ + //qCDebug(KArchiveLog) << "m=" << m; + if (m & QIODevice::ReadOnly) { + /*bool ok = false; + if ( m_dev->isOpen() ) + ok = ( m_dev->mode() == QIODevice::ReadOnly ); + else + ok = m_dev->open( m ); + if ( ok )*/ + m_dev->seek(m_start); // No concurrent access ! + } else { + //qCWarning(KArchiveLog) << "KLimitedIODevice::open only supports QIODevice::ReadOnly!"; + } + setOpenMode(QIODevice::ReadOnly); + return true; +} + +void KLimitedIODevice::close() +{ +} + +qint64 KLimitedIODevice::size() const +{ + return m_length; +} + +qint64 KLimitedIODevice::readData(char *data, qint64 maxlen) +{ + maxlen = qMin(maxlen, m_length - pos()); // Apply upper limit + return m_dev->read(data, maxlen); +} + +bool KLimitedIODevice::seek(qint64 pos) +{ + Q_ASSERT(pos <= m_length); + pos = qMin(pos, m_length); // Apply upper limit + bool ret = m_dev->seek(m_start + pos); + if (ret) { + QIODevice::seek(pos); + } + return ret; +} + +qint64 KLimitedIODevice::bytesAvailable() const +{ + return QIODevice::bytesAvailable(); +} + +bool KLimitedIODevice::isSequential() const +{ + return m_dev->isSequential(); +} diff --git a/src/karchive/src/klimitediodevice_p.h b/src/karchive/src/klimitediodevice_p.h new file mode 100644 index 0000000000..75fd2047bc --- /dev/null +++ b/src/karchive/src/klimitediodevice_p.h @@ -0,0 +1,56 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2001, 2002, 2007 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef klimitediodevice_p_h +#define klimitediodevice_p_h + +#include +#include +/** + * A readonly device that reads from an underlying device + * from a given point to another (e.g. to give access to a single + * file inside an archive). + * @author David Faure + * @internal - used by KArchive + */ +class KLimitedIODevice : public QIODevice +{ + Q_OBJECT +public: + /** + * Creates a new KLimitedIODevice. + * @param dev the underlying device, opened or not + * This device itself auto-opens (in readonly mode), no need to open it. + * @param start where to start reading (position in bytes) + * @param length the length of the data to read (in bytes) + */ + KLimitedIODevice(QIODevice *dev, qint64 start, qint64 length); + virtual ~KLimitedIODevice() + { + } + + bool isSequential() const override; + + bool open(QIODevice::OpenMode m) override; + void close() override; + + qint64 size() const override; + + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *, qint64) override { + return -1; // unsupported + } + + //virtual qint64 pos() const { return m_dev->pos() - m_start; } + bool seek(qint64 pos) override; + qint64 bytesAvailable() const override; +private: + QIODevice *m_dev; + qint64 m_start; + qint64 m_length; +}; + +#endif diff --git a/src/karchive/src/knonefilter.cpp b/src/karchive/src/knonefilter.cpp new file mode 100644 index 0000000000..b0fd8328df --- /dev/null +++ b/src/karchive/src/knonefilter.cpp @@ -0,0 +1,127 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2011 Mario Bensi + + Based on kbzip2filter: + SPDX-FileCopyrightText: 2000, 2009 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "knonefilter.h" + +#include + +class Q_DECL_HIDDEN KNoneFilter::Private +{ +public: + Private() + : mode(0) + , avail_out(0) + , avail_in(0) + , next_in(nullptr) + , next_out(nullptr) + { + } + + int mode; + int avail_out; + int avail_in; + const char *next_in; + char *next_out; +}; + +KNoneFilter::KNoneFilter() + : d(new Private) +{ +} + +KNoneFilter::~KNoneFilter() +{ + delete d; +} + +bool KNoneFilter::init(int mode) +{ + d->mode = mode; + return true; +} + +int KNoneFilter::mode() const +{ + return d->mode; +} + +bool KNoneFilter::terminate() +{ + return true; +} + +void KNoneFilter::reset() +{ +} + +bool KNoneFilter::readHeader() +{ + return true; +} + +bool KNoneFilter::writeHeader(const QByteArray & /*fileName*/) +{ + return true; +} + +void KNoneFilter::setOutBuffer(char *data, uint maxlen) +{ + d->avail_out = maxlen; + d->next_out = data; +} + +void KNoneFilter::setInBuffer(const char *data, uint size) +{ + d->next_in = data; + d->avail_in = size; +} + +int KNoneFilter::inBufferAvailable() const +{ + return d->avail_in; +} + +int KNoneFilter::outBufferAvailable() const +{ + return d->avail_out; +} + +KNoneFilter::Result KNoneFilter::uncompress() +{ +#ifndef NDEBUG + if (d->mode != QIODevice::ReadOnly) { + return KFilterBase::Error; + } +#endif + return copyData(); +} + +KNoneFilter::Result KNoneFilter::compress(bool finish) +{ + Q_ASSERT(d->mode == QIODevice::WriteOnly); + Q_UNUSED(finish); + + return copyData(); +} + +KNoneFilter::Result KNoneFilter::copyData() +{ + Q_ASSERT(d->avail_out > 0); + if (d->avail_in > 0) { + const int n = qMin(d->avail_in, d->avail_out); + memcpy(d->next_out, d->next_in, n); + d->avail_out -= n; + d->next_in += n; + d->next_out += n; + d->avail_in -= n; + return KFilterBase::Ok; + } else { + return KFilterBase::End; + } +} diff --git a/src/karchive/src/knonefilter.h b/src/karchive/src/knonefilter.h new file mode 100644 index 0000000000..1570b29c5d --- /dev/null +++ b/src/karchive/src/knonefilter.h @@ -0,0 +1,48 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2011 Mario Bensi + + Based on kbzip2filter: + SPDX-FileCopyrightText: 2000, 2009 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef __knonefilter__h +#define __knonefilter__h + +#include "kfilterbase.h" + +/** + * Internal class used by KFilterDev + * + * This header is not installed. + * + * @internal + */ +class KNoneFilter : public KFilterBase +{ +public: + KNoneFilter(); + virtual ~KNoneFilter(); + + bool init(int mode) override; + int mode() const override; + bool terminate() override; + void reset() override; + bool readHeader() override; // this is about the GZIP header + bool writeHeader(const QByteArray &fileName) override; + void setOutBuffer(char *data, uint maxlen) override; + void setInBuffer(const char *data, uint size) override; + int inBufferAvailable() const override; + int outBufferAvailable() const override; + Result uncompress() override; + Result compress(bool finish) override; + +private: + Result copyData(); + + class Private; + Private *const d; +}; + +#endif diff --git a/src/karchive/src/krcc.cpp b/src/karchive/src/krcc.cpp new file mode 100644 index 0000000000..d41b591007 --- /dev/null +++ b/src/karchive/src/krcc.cpp @@ -0,0 +1,161 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2014 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "krcc.h" +#include "karchive_p.h" +#include "loggingcategory.h" + +#include +#include +#include +#include +#include +#include +#include + +class Q_DECL_HIDDEN KRcc::KRccPrivate +{ +public: + KRccPrivate() + { + } + void createEntries(const QDir &dir, KArchiveDirectory *parentDir, KRcc *q); + + QString m_prefix; // '/' + uuid +}; + +/** + * A KRccFileEntry represents a file in a rcc archive. + */ +class KARCHIVE_EXPORT KRccFileEntry : public KArchiveFile +{ +public: + KRccFileEntry(KArchive *archive, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, qint64 size, const QString &resourcePath) + : KArchiveFile(archive, name, access, date, user, group, QString(), 0, size) + , m_resourcePath(resourcePath) + { + } + + QByteArray data() const override + { + QFile f(m_resourcePath); + if (f.open(QIODevice::ReadOnly)) { + return f.readAll(); + } + qCWarning(KArchiveLog) << "Couldn't open" << m_resourcePath; + return QByteArray(); + } + QIODevice *createDevice() const override + { + return new QFile(m_resourcePath); + } +private: + QString m_resourcePath; +}; + +KRcc::KRcc(const QString &filename) + : KArchive(filename) + , d(new KRccPrivate) +{ +} + +KRcc::~KRcc() +{ + if (isOpen()) { + close(); + } + delete d; +} + +bool KRcc::doPrepareWriting(const QString &, const QString &, const QString &, + qint64, mode_t, const QDateTime &, const QDateTime &, const QDateTime &) +{ + setErrorString(tr("Cannot write to RCC file")); + qCWarning(KArchiveLog) << "doPrepareWriting not implemented for KRcc"; + return false; +} + +bool KRcc::doFinishWriting(qint64) +{ + setErrorString(tr("Cannot write to RCC file")); + qCWarning(KArchiveLog) << "doFinishWriting not implemented for KRcc"; + return false; +} + +bool KRcc::doWriteDir(const QString &, const QString &, const QString &, + mode_t, const QDateTime &, const QDateTime &, const QDateTime &) +{ + setErrorString(tr("Cannot write to RCC file")); + qCWarning(KArchiveLog) << "doWriteDir not implemented for KRcc"; + return false; +} + +bool KRcc::doWriteSymLink(const QString &, const QString &, const QString &, + const QString &, mode_t, const QDateTime &, const QDateTime &, const QDateTime &) +{ + setErrorString(tr("Cannot write to RCC file")); + qCWarning(KArchiveLog) << "doWriteSymLink not implemented for KRcc"; + return false; +} + +bool KRcc::openArchive(QIODevice::OpenMode mode) +{ + // Open archive + + if (mode == QIODevice::WriteOnly) { + return true; + } + if (mode != QIODevice::ReadOnly && mode != QIODevice::ReadWrite) { + setErrorString(tr("Unsupported mode %1").arg(mode)); + return false; + } + + QUuid uuid = QUuid::createUuid(); + d->m_prefix = QLatin1Char('/') + uuid.toString(); + if (!QResource::registerResource(fileName(), d->m_prefix)) { + setErrorString( + tr("Failed to register resource %1 under prefix %2") + .arg(fileName(), d->m_prefix)); + return false; + } + + QDir dir(QLatin1Char(':') + d->m_prefix); + d->createEntries(dir, rootDir(), this); + return true; +} + +void KRcc::KRccPrivate::createEntries(const QDir &dir, KArchiveDirectory *parentDir, KRcc *q) +{ + for (const QString &fileName : dir.entryList()) { + const QString entryPath = dir.path() + QLatin1Char('/') + fileName; + const QFileInfo info(entryPath); + if (info.isFile()) { + KArchiveEntry *entry = new KRccFileEntry(q, fileName, 0444, info.lastModified(), + parentDir->user(), parentDir->group(), info.size(), entryPath); + parentDir->addEntry(entry); + } else { + KArchiveDirectory *entry = new KArchiveDirectory(q, fileName, 0555, info.lastModified(), + parentDir->user(), parentDir->group(), /*symlink*/ QString()); + if (parentDir->addEntryV2(entry)) { + createEntries(QDir(entryPath), entry, q); + } + } + } +} + +bool KRcc::closeArchive() +{ + // Close the archive + QResource::unregisterResource(fileName(), d->m_prefix); + // ignore errors + return true; +} + +void KRcc::virtual_hook(int id, void *data) +{ + KArchive::virtual_hook(id, data); +} diff --git a/src/karchive/src/krcc.h b/src/karchive/src/krcc.h new file mode 100644 index 0000000000..66efa6e09d --- /dev/null +++ b/src/karchive/src/krcc.h @@ -0,0 +1,84 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2014 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KRCC_H +#define KRCC_H + +#include + +/** + * KRcc is a class for reading dynamic binary resources created by Qt's rcc tool + * from a .qrc file and the files it points to. + * + * Writing is not supported. + * @short A class for reading rcc resources. + * @since 5.3 + */ +class KARCHIVE_EXPORT KRcc : public KArchive +{ + Q_DECLARE_TR_FUNCTIONS(KRcc) + +public: + /** + * Creates an instance that operates on the given filename. + * + * @param filename is a local path (e.g. "/home/holger/myfile.rcc") + */ + KRcc(const QString &filename); + + /** + * If the rcc file is still opened, then it will be + * closed automatically by the destructor. + */ + virtual ~KRcc(); + +protected: + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doPrepareWriting(const QString &name, const QString &user, const QString &group, qint64 size, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doFinishWriting(qint64 size) override; + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doWriteDir(const QString &name, const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + + /* + * Writing is not supported by this class, will always fail. + * @return always false + */ + bool doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + + /** + * Registers the .rcc resource in the QResource system under a unique identifier, + * then lists that, and creates the KArchiveFile/KArchiveDirectory entries. + */ + bool openArchive(QIODevice::OpenMode mode) override; + /** + * Unregisters the .rcc resource from the QResource system. + */ + bool closeArchive() override; + +protected: + void virtual_hook(int id, void *data) override; +private: + class KRccPrivate; + KRccPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/ktar.cpp b/src/karchive/src/ktar.cpp new file mode 100644 index 0000000000..e3e84ec717 --- /dev/null +++ b/src/karchive/src/ktar.cpp @@ -0,0 +1,940 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "ktar.h" +#include "karchive_p.h" +#include "loggingcategory.h" + +#include // strtol +#include + +#include +#include +#include +#include +#include + +#include +#include + +//////////////////////////////////////////////////////////////////////// +/////////////////////////// KTar /////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +// Mime types of known filters +static const char application_gzip[] = "application/x-gzip"; +static const char application_bzip[] = "application/x-bzip"; +static const char application_lzma[] = "application/x-lzma"; +static const char application_xz[] = "application/x-xz"; + +class Q_DECL_HIDDEN KTar::KTarPrivate +{ +public: + KTarPrivate(KTar *parent) + : q(parent) + , tarEnd(0) + , tmpFile(nullptr) + , compressionDevice(nullptr) + { + } + + KTar *q; + QStringList dirList; + qint64 tarEnd; + QTemporaryFile *tmpFile; + QString mimetype; + QByteArray origFileName; + KCompressionDevice *compressionDevice; + + bool fillTempFile(const QString &fileName); + bool writeBackTempFile(const QString &fileName); + void fillBuffer(char *buffer, const char *mode, qint64 size, const QDateTime &mtime, + char typeflag, const char *uname, const char *gname); + void writeLonglink(char *buffer, const QByteArray &name, char typeflag, + const char *uname, const char *gname); + qint64 readRawHeader(char *buffer); + bool readLonglink(char *buffer, QByteArray &longlink); + qint64 readHeader(char *buffer, QString &name, QString &symlink); +}; + +KTar::KTar(const QString &fileName, const QString &_mimetype) + : KArchive(fileName) + , d(new KTarPrivate(this)) +{ + d->mimetype = _mimetype; +} + +KTar::KTar(QIODevice *dev) + : KArchive(dev) + , d(new KTarPrivate(this)) +{ +} + +// Only called when a filename was given +bool KTar::createDevice(QIODevice::OpenMode mode) +{ + if (d->mimetype.isEmpty()) { + // Find out mimetype manually + + QMimeDatabase db; + QMimeType mime; + if (mode != QIODevice::WriteOnly && QFile::exists(fileName())) { + // Give priority to file contents: if someone renames a .tar.bz2 to .tar.gz, + // we can still do the right thing here. + QFile f(fileName()); + if (f.open(QIODevice::ReadOnly)) { + mime = db.mimeTypeForData(&f); + } + if (!mime.isValid()) { + // Unable to determine mimetype from contents, get it from file name + mime = db.mimeTypeForFile(fileName(), QMimeDatabase::MatchExtension); + } + } else { + mime = db.mimeTypeForFile(fileName(), QMimeDatabase::MatchExtension); + } + + //qCDebug(KArchiveLog) << mode << mime->name(); + + if (mime.inherits(QStringLiteral("application/x-compressed-tar")) || mime.inherits(QString::fromLatin1(application_gzip))) { + // gzipped tar file (with possibly invalid file name), ask for gzip filter + d->mimetype = QString::fromLatin1(application_gzip); + } else if (mime.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mime.inherits(QString::fromLatin1(application_bzip))) { + // bzipped2 tar file (with possibly invalid file name), ask for bz2 filter + d->mimetype = QString::fromLatin1(application_bzip); + } else if (mime.inherits(QStringLiteral("application/x-lzma-compressed-tar")) || mime.inherits(QString::fromLatin1(application_lzma))) { + // lzma compressed tar file (with possibly invalid file name), ask for xz filter + d->mimetype = QString::fromLatin1(application_lzma); + } else if (mime.inherits(QStringLiteral("application/x-xz-compressed-tar")) || mime.inherits(QString::fromLatin1(application_xz))) { + // xz compressed tar file (with possibly invalid name), ask for xz filter + d->mimetype = QString::fromLatin1(application_xz); + } + } + + if (d->mimetype == QLatin1String("application/x-tar")) { + return KArchive::createDevice(mode); + } else if (mode == QIODevice::WriteOnly) { + if (!KArchive::createDevice(mode)) { + return false; + } + if (!d->mimetype.isEmpty()) { + // Create a compression filter on top of the QSaveFile device that KArchive created. + //qCDebug(KArchiveLog) << "creating KFilterDev for" << d->mimetype; + KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(d->mimetype); + d->compressionDevice = new KCompressionDevice(device(), false, type); + setDevice(d->compressionDevice); + } + return true; + } else { + // The compression filters are very slow with random access. + // So instead of applying the filter to the device, + // the file is completely extracted instead, + // and we work on the extracted tar file. + // This improves the extraction speed by the tar ioslave dramatically, + // if the archive file contains many files. + // This is because the tar ioslave extracts one file after the other and normally + // has to walk through the decompression filter each time. + // Which is in fact nearly as slow as a complete decompression for each file. + + Q_ASSERT(!d->tmpFile); + d->tmpFile = new QTemporaryFile(); + d->tmpFile->setFileTemplate(QDir::tempPath() + QStringLiteral("/") + QLatin1String("ktar-XXXXXX.tar")); + d->tmpFile->open(); + //qCDebug(KArchiveLog) << "creating tempfile:" << d->tmpFile->fileName(); + + setDevice(d->tmpFile); + return true; + } +} + +KTar::~KTar() +{ + // mjarrett: Closes to prevent ~KArchive from aborting w/o device + if (isOpen()) { + close(); + } + + delete d->tmpFile; + delete d->compressionDevice; + delete d; +} + +void KTar::setOrigFileName(const QByteArray &fileName) +{ + if (!isOpen() || !(mode() & QIODevice::WriteOnly)) { + //qCWarning(KArchiveLog) << "KTar::setOrigFileName: File must be opened for writing first.\n"; + return; + } + d->origFileName = fileName; +} + +qint64 KTar::KTarPrivate::readRawHeader(char *buffer) +{ + // Read header + qint64 n = q->device()->read(buffer, 0x200); + // we need to test if there is a prefix value because the file name can be null + // and the prefix can have a value and in this case we don't reset n. + if (n == 0x200 && (buffer[0] != 0 || buffer[0x159] != 0)) { + // Make sure this is actually a tar header + if (strncmp(buffer + 257, "ustar", 5)) { + // The magic isn't there (broken/old tars), but maybe a correct checksum? + + int check = 0; + for (uint j = 0; j < 0x200; ++j) { + check += static_cast(buffer[j]); + } + + // adjust checksum to count the checksum fields as blanks + for (uint j = 0; j < 8 /*size of the checksum field including the \0 and the space*/; j++) { + check -= static_cast(buffer[148 + j]); + } + check += 8 * ' '; + + QByteArray s = QByteArray::number(check, 8); // octal + + // only compare those of the 6 checksum digits that mean something, + // because the other digits are filled with all sorts of different chars by different tars ... + // Some tars right-justify the checksum so it could start in one of three places - we have to check each. + if (strncmp(buffer + 148 + 6 - s.length(), s.data(), s.length()) + && strncmp(buffer + 148 + 7 - s.length(), s.data(), s.length()) + && strncmp(buffer + 148 + 8 - s.length(), s.data(), s.length())) { + /*qCWarning(KArchiveLog) << "KTar: invalid TAR file. Header is:" << QByteArray( buffer+257, 5 ) + << "instead of ustar. Reading from wrong pos in file?" + << "checksum=" << QByteArray( buffer + 148 + 6 - s.length(), s.length() );*/ + return -1; + } + }/*end if*/ + } else { + // reset to 0 if 0x200 because logical end of archive has been reached + if (n == 0x200) { + n = 0; + } + }/*end if*/ + return n; +} + +bool KTar::KTarPrivate::readLonglink(char *buffer, QByteArray &longlink) +{ + qint64 n = 0; + //qCDebug(KArchiveLog) << "reading longlink from pos " << q->device()->pos(); + QIODevice *dev = q->device(); + // read size of longlink from size field in header + // size is in bytes including the trailing null (which we ignore) + qint64 size = QByteArray(buffer + 0x7c, 12).trimmed().toLongLong(nullptr, 8 /*octal*/); + + size--; // ignore trailing null + if (size > std::numeric_limits::max() - 32) { // QByteArray can't really be INT_MAX big, it's max size is something between INT_MAX - 32 and INT_MAX depending the platform so just be safe + qCWarning(KArchiveLog) << "Failed to allocate memory for longlink of size" << size; + return false; + } + if (size < 0) { + qCWarning(KArchiveLog) << "Invalid longlink size" << size; + return false; + } + longlink.resize(size); + qint64 offset = 0; + while (size > 0) { + int chunksize = qMin(size, 0x200LL); + n = dev->read(longlink.data() + offset, chunksize); + if (n == -1) { + return false; + } + size -= chunksize; + offset += 0x200; + }/*wend*/ + // jump over the rest + const int skip = 0x200 - (n % 0x200); + if (skip <= 0x200) { + if (dev->read(buffer, skip) != skip) { + return false; + } + } + longlink.truncate(qstrlen(longlink.constData())); + return true; +} + +qint64 KTar::KTarPrivate::readHeader(char *buffer, QString &name, QString &symlink) +{ + name.truncate(0); + symlink.truncate(0); + while (true) { + qint64 n = readRawHeader(buffer); + if (n != 0x200) { + return n; + } + + // is it a longlink? + if (strcmp(buffer, "././@LongLink") == 0) { + char typeflag = buffer[0x9c]; + QByteArray longlink; + if (readLonglink(buffer, longlink)) { + switch (typeflag) { + case 'L': + name = QFile::decodeName(longlink.constData()); + break; + case 'K': + symlink = QFile::decodeName(longlink.constData()); + break; + }/*end switch*/ + } + } else { + break; + }/*end if*/ + }/*wend*/ + + // if not result of longlink, read names directly from the header + if (name.isEmpty()) + // there are names that are exactly 100 bytes long + // and neither longlink nor \0 terminated (bug:101472) + { + name = QFile::decodeName(QByteArray(buffer, qstrnlen(buffer, 100))); + } + if (symlink.isEmpty()) { + char *symlinkBuffer = buffer + 0x9d /*?*/; + symlink = QFile::decodeName(QByteArray(symlinkBuffer, qstrnlen(symlinkBuffer, 100))); + } + + return 0x200; +} + +/* + * If we have created a temporary file, we have + * to decompress the original file now and write + * the contents to the temporary file. + */ +bool KTar::KTarPrivate::fillTempFile(const QString &fileName) +{ + if (! tmpFile) { + return true; + } + + //qCDebug(KArchiveLog) << "filling tmpFile of mimetype" << mimetype; + + KCompressionDevice::CompressionType compressionType = KFilterDev::compressionTypeForMimeType(mimetype); + KCompressionDevice filterDev(fileName, compressionType); + + QFile *file = tmpFile; + Q_ASSERT(file->isOpen()); + Q_ASSERT(file->openMode() & QIODevice::WriteOnly); + file->seek(0); + QByteArray buffer; + buffer.resize(8 * 1024); + if (! filterDev.open(QIODevice::ReadOnly)) { + q->setErrorString( + tr("File %1 does not exist") + .arg(fileName)); + return false; + } + qint64 len = -1; + while (!filterDev.atEnd() && len != 0) { + len = filterDev.read(buffer.data(), buffer.size()); + if (len < 0) { // corrupted archive + q->setErrorString(tr("Archive %1 is corrupt").arg(fileName)); + return false; + } + if (file->write(buffer.data(), len) != len) { // disk full + q->setErrorString(tr("Disk full")); + return false; + } + } + filterDev.close(); + + file->flush(); + file->seek(0); + Q_ASSERT(file->isOpen()); + Q_ASSERT(file->openMode() & QIODevice::ReadOnly); + + //qCDebug(KArchiveLog) << "filling tmpFile finished."; + return true; +} + +bool KTar::openArchive(QIODevice::OpenMode mode) +{ + + if (!(mode & QIODevice::ReadOnly)) { + return true; + } + + if (!d->fillTempFile(fileName())) { + return false; + } + + // We'll use the permission and user/group of d->rootDir + // for any directory we emulate (see findOrCreate) + //struct stat buf; + //stat( fileName(), &buf ); + + d->dirList.clear(); + QIODevice *dev = device(); + + if (!dev) { + setErrorString(tr("Could not get underlying device")); + qCWarning(KArchiveLog) << "Could not get underlying device"; + return false; + } + + // read dir information + char buffer[0x200]; + bool ende = false; + do { + QString name; + QString symlink; + + // Read header + qint64 n = d->readHeader(buffer, name, symlink); + if (n < 0) { + setErrorString(tr("Could not read tar header")); + return false; + } + if (n == 0x200) { + bool isdir = false; + + if (name.isEmpty()) { + continue; + } + if (name.endsWith(QLatin1Char('/'))) { + isdir = true; + name.truncate(name.length() - 1); + } + + QByteArray prefix = QByteArray(buffer + 0x159, 155); + if (prefix[0] != '\0') { + name = (QString::fromLatin1(prefix.constData()) + QLatin1Char('/') + name); + } + + int pos = name.lastIndexOf(QLatin1Char('/')); + QString nm = (pos == -1) ? name : name.mid(pos + 1); + + // read access + buffer[0x6b] = 0; + char *dummy; + const char *p = buffer + 0x64; + while (*p == ' ') { + ++p; + } + int access = strtol(p, &dummy, 8); + + // read user and group + const int maxUserGroupLength = 32; + const char *userStart = buffer + 0x109; + const int userLen = qstrnlen(userStart, maxUserGroupLength); + const QString user = QString::fromLocal8Bit(userStart, userLen); + const char *groupStart = buffer + 0x129; + const int groupLen = qstrnlen(groupStart, maxUserGroupLength); + const QString group = QString::fromLocal8Bit(groupStart, groupLen); + + // read time + buffer[0x93] = 0; + p = buffer + 0x88; + while (*p == ' ') { + ++p; + } + uint time = strtol(p, &dummy, 8); + + // read type flag + char typeflag = buffer[0x9c]; + // '0' for files, '1' hard link, '2' symlink, '5' for directory + // (and 'L' for longlink fileNames, 'K' for longlink symlink targets) + // 'D' for GNU tar extension DUMPDIR, 'x' for Extended header referring + // to the next file in the archive and 'g' for Global extended header + + if (typeflag == '5') { + isdir = true; + } + + bool isDumpDir = false; + if (typeflag == 'D') { + isdir = false; + isDumpDir = true; + } + //qCDebug(KArchiveLog) << nm << "isdir=" << isdir << "pos=" << dev->pos() << "typeflag=" << typeflag << " islink=" << ( typeflag == '1' || typeflag == '2' ); + + if (typeflag == 'x' || typeflag == 'g') { // pax extended header, or pax global extended header + // Skip it for now. TODO: implement reading of extended header, as per http://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html + (void)dev->read(buffer, 0x200); + continue; + } + + if (isdir) { + access |= S_IFDIR; // f*cking broken tar files + } + + KArchiveEntry *e; + if (isdir) { + //qCDebug(KArchiveLog) << "directory" << nm; + e = new KArchiveDirectory(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink); + } else { + // read size + QByteArray sizeBuffer(buffer + 0x7c, 12); + qint64 size = sizeBuffer.trimmed().toLongLong(nullptr, 8 /*octal*/); + //qCDebug(KArchiveLog) << "sizeBuffer='" << sizeBuffer << "' -> size=" << size; + + // for isDumpDir we will skip the additional info about that dirs contents + if (isDumpDir) { + //qCDebug(KArchiveLog) << nm << "isDumpDir"; + e = new KArchiveDirectory(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink); + } else { + + // Let's hack around hard links. Our classes don't support that, so make them symlinks + if (typeflag == '1') { + //qCDebug(KArchiveLog) << "Hard link, setting size to 0 instead of" << size; + size = 0; // no contents + } + + //qCDebug(KArchiveLog) << "file" << nm << "size=" << size; + e = new KArchiveFile(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink, + dev->pos(), size); + } + + // Skip contents + align bytes + qint64 rest = size % 0x200; + qint64 skip = size + (rest ? 0x200 - rest : 0); + //qCDebug(KArchiveLog) << "pos()=" << dev->pos() << "rest=" << rest << "skipping" << skip; + if (! dev->seek(dev->pos() + skip)) { + //qCWarning(KArchiveLog) << "skipping" << skip << "failed"; + } + } + + if (pos == -1) { + if (nm == QLatin1String(".")) { // special case + if (isdir) { + if (KArchivePrivate::hasRootDir(this)) { + qWarning() << "Broken tar file has two root dir entries"; + delete e; + } else { + setRootDir(static_cast(e)); + } + } else { + delete e; + } + } else { + rootDir()->addEntry(e); + } + } else { + // In some tar files we can find dir/./file => call cleanPath + QString path = QDir::cleanPath(name.left(pos)); + // Ensure container directory exists, create otherwise + KArchiveDirectory *d = findOrCreate(path); + if (d) { + d->addEntry(e); + } else { + delete e; + return false; + } + } + } else { + //qCDebug(KArchiveLog) << "Terminating. Read " << n << " bytes, first one is " << buffer[0]; + d->tarEnd = dev->pos() - n; // Remember end of archive + ende = true; + } + } while (!ende); + return true; +} + +/* + * Writes back the changes of the temporary file + * to the original file. + * Must only be called if in write mode, not in read mode + */ +bool KTar::KTarPrivate::writeBackTempFile(const QString &fileName) +{ + if (!tmpFile) { + return true; + } + + //qCDebug(KArchiveLog) << "Write temporary file to compressed file" << fileName << mimetype; + + bool forced = false; + if (QLatin1String(application_gzip) == mimetype || QLatin1String(application_bzip) == mimetype || + QLatin1String(application_lzma) == mimetype || QLatin1String(application_xz) == mimetype) { + forced = true; + } + + // #### TODO this should use QSaveFile to avoid problems on disk full + // (KArchive uses QSaveFile by default, but the temp-uncompressed-file trick + // circumvents that). + + KFilterDev dev(fileName); + QFile *file = tmpFile; + if (!dev.open(QIODevice::WriteOnly)) { + file->close(); + q->setErrorString(tr("Failed to write back temp file: %1").arg(dev.errorString())); + return false; + } + if (forced) { + dev.setOrigFileName(origFileName); + } + file->seek(0); + QByteArray buffer; + buffer.resize(8 * 1024); + qint64 len; + while (!file->atEnd()) { + len = file->read(buffer.data(), buffer.size()); + dev.write(buffer.data(), len); // TODO error checking + } + file->close(); + dev.close(); + + //qCDebug(KArchiveLog) << "Write temporary file to compressed file done."; + return true; +} + +bool KTar::closeArchive() +{ + d->dirList.clear(); + + bool ok = true; + + // If we are in readwrite mode and had created + // a temporary tar file, we have to write + // back the changes to the original file + if (d->tmpFile && (mode() & QIODevice::WriteOnly)) { + ok = d->writeBackTempFile(fileName()); + delete d->tmpFile; + d->tmpFile = nullptr; + setDevice(nullptr); + } + + return ok; +} + +bool KTar::doFinishWriting(qint64 size) +{ + // Write alignment + int rest = size % 0x200; + if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + d->tarEnd = device()->pos() + (rest ? 0x200 - rest : 0); // Record our new end of archive + } + if (rest) { + char buffer[0x201]; + for (uint i = 0; i < 0x200; ++i) { + buffer[i] = 0; + } + qint64 nwritten = device()->write(buffer, 0x200 - rest); + const bool ok = nwritten == 0x200 - rest; + + if (!ok) { + setErrorString( + tr("Couldn't write alignment: %1") + .arg(device()->errorString())); + } + + return ok; + } + return true; +} + +/*** Some help from the tar sources +struct posix_header +{ byte offset + char name[100]; * 0 * 0x0 + char mode[8]; * 100 * 0x64 + char uid[8]; * 108 * 0x6c + char gid[8]; * 116 * 0x74 + char size[12]; * 124 * 0x7c + char mtime[12]; * 136 * 0x88 + char chksum[8]; * 148 * 0x94 + char typeflag; * 156 * 0x9c + char linkname[100]; * 157 * 0x9d + char magic[6]; * 257 * 0x101 + char version[2]; * 263 * 0x107 + char uname[32]; * 265 * 0x109 + char gname[32]; * 297 * 0x129 + char devmajor[8]; * 329 * 0x149 + char devminor[8]; * 337 * ... + char prefix[155]; * 345 * + * 500 * +}; +*/ + +void KTar::KTarPrivate::fillBuffer(char *buffer, + const char *mode, qint64 size, const QDateTime &mtime, char typeflag, + const char *uname, const char *gname) +{ + // mode (as in stpos()) + assert(strlen(mode) == 6); + memcpy(buffer + 0x64, mode, 6); + buffer[0x6a] = ' '; + buffer[0x6b] = '\0'; + + // dummy uid + strcpy(buffer + 0x6c, " 765 "); // 501 in decimal + // dummy gid + strcpy(buffer + 0x74, " 144 "); // 100 in decimal + + // size + QByteArray s = QByteArray::number(size, 8); // octal + s = s.rightJustified(11, '0'); + memcpy(buffer + 0x7c, s.data(), 11); + buffer[0x87] = ' '; // space-terminate (no null after) + + // modification time + const QDateTime modificationTime = mtime.isValid() ? mtime : QDateTime::currentDateTime(); + s = QByteArray::number(static_cast(modificationTime.toMSecsSinceEpoch() / 1000), 8); // octal + s = s.rightJustified(11, '0'); + memcpy(buffer + 0x88, s.data(), 11); + buffer[0x93] = ' '; // space-terminate (no null after) -- well current tar writes a null byte + + // spaces, replaced by the check sum later + buffer[0x94] = 0x20; + buffer[0x95] = 0x20; + buffer[0x96] = 0x20; + buffer[0x97] = 0x20; + buffer[0x98] = 0x20; + buffer[0x99] = 0x20; + + /* From the tar sources : + Fill in the checksum field. It's formatted differently from the + other fields: it has [6] digits, a null, then a space -- rather than + digits, a space, then a null. */ + + buffer[0x9a] = '\0'; + buffer[0x9b] = ' '; + + // type flag (dir, file, link) + buffer[0x9c] = typeflag; + + // magic + version + strcpy(buffer + 0x101, "ustar"); + strcpy(buffer + 0x107, "00"); + + // user + strcpy(buffer + 0x109, uname); + // group + strcpy(buffer + 0x129, gname); + + // Header check sum + int check = 32; + for (uint j = 0; j < 0x200; ++j) { + check += static_cast(buffer[j]); + } + s = QByteArray::number(check, 8); // octal + s = s.rightJustified(6, '0'); + memcpy(buffer + 0x94, s.constData(), 6); +} + +void KTar::KTarPrivate::writeLonglink(char *buffer, const QByteArray &name, char typeflag, + const char *uname, const char *gname) +{ + strcpy(buffer, "././@LongLink"); + qint64 namelen = name.length() + 1; + fillBuffer(buffer, " 0", namelen, QDateTime(), typeflag, uname, gname); + q->device()->write(buffer, 0x200); // TODO error checking + qint64 offset = 0; + while (namelen > 0) { + int chunksize = qMin(namelen, 0x200LL); + memcpy(buffer, name.data() + offset, chunksize); + // write long name + q->device()->write(buffer, 0x200); // TODO error checking + // not even needed to reclear the buffer, tar doesn't do it + namelen -= chunksize; + offset += 0x200; + }/*wend*/ +} + +bool KTar::doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, mode_t perm, + const QDateTime & /*atime*/, const QDateTime &mtime, const QDateTime & /*ctime*/) +{ + if (!isOpen()) { + setErrorString(tr("Application error: TAR file must be open before being written into")); + qCWarning(KArchiveLog) << "doPrepareWriting failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { + setErrorString(tr("Application error: attempted to write into non-writable 7-Zip file")); + qCWarning(KArchiveLog) << "doPrepareWriting failed: !(mode() & QIODevice::WriteOnly)"; + return false; + } + + // In some tar files we can find dir/./file => call cleanPath + QString fileName(QDir::cleanPath(name)); + + /* + // Create toplevel dirs + // Commented out by David since it's not necessary, and if anybody thinks it is, + // he needs to implement a findOrCreate equivalent in writeDir. + // But as KTar and the "tar" program both handle tar files without + // dir entries, there's really no need for that + QString tmp ( fileName ); + int i = tmp.lastIndexOf( '/' ); + if ( i != -1 ) + { + QString d = tmp.left( i + 1 ); // contains trailing slash + if ( !m_dirList.contains( d ) ) + { + tmp = tmp.mid( i + 1 ); + writeDir( d, user, group ); // WARNING : this one doesn't create its toplevel dirs + } + } + */ + + char buffer[0x201]; + memset(buffer, 0, 0x200); + if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read + } + + // provide converted stuff we need later on + const QByteArray encodedFileName = QFile::encodeName(fileName); + const QByteArray uname = user.toLocal8Bit(); + const QByteArray gname = group.toLocal8Bit(); + + // If more than 100 bytes, we need to use the LongLink trick + if (encodedFileName.length() > 99) { + d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData()); + } + + // Write (potentially truncated) name + strncpy(buffer, encodedFileName.constData(), 99); + buffer[99] = 0; + // zero out the rest (except for what gets filled anyways) + memset(buffer + 0x9d, 0, 0x200 - 0x9d); + + QByteArray permstr = QByteArray::number(static_cast(perm), 8); + permstr = permstr.rightJustified(6, '0'); + d->fillBuffer(buffer, permstr.constData(), size, mtime, 0x30, uname.constData(), gname.constData()); + + // Write header + if (device()->write(buffer, 0x200) != 0x200) { + setErrorString( + tr("Failed to write header: %1") + .arg(device()->errorString())); + return false; + } else { + return true; + } +} + +bool KTar::doWriteDir(const QString &name, const QString &user, + const QString &group, mode_t perm, + const QDateTime & /*atime*/, const QDateTime &mtime, const QDateTime & /*ctime*/) +{ + if (!isOpen()) { + setErrorString(tr("Application error: TAR file must be open before being written into")); + qCWarning(KArchiveLog) << "doWriteDir failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { + setErrorString(tr("Application error: attempted to write into non-writable TAR file")); + qCWarning(KArchiveLog) << "doWriteDir failed: !(mode() & QIODevice::WriteOnly)"; + return false; + } + + // In some tar files we can find dir/./ => call cleanPath + QString dirName(QDir::cleanPath(name)); + + // Need trailing '/' + if (!dirName.endsWith(QLatin1Char('/'))) { + dirName += QLatin1Char('/'); + } + + if (d->dirList.contains(dirName)) { + return true; // already there + } + + char buffer[0x201]; + memset(buffer, 0, 0x200); + if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read + } + + // provide converted stuff we need lateron + QByteArray encodedDirname = QFile::encodeName(dirName); + QByteArray uname = user.toLocal8Bit(); + QByteArray gname = group.toLocal8Bit(); + + // If more than 100 bytes, we need to use the LongLink trick + if (encodedDirname.length() > 99) { + d->writeLonglink(buffer, encodedDirname, 'L', uname.constData(), gname.constData()); + } + + // Write (potentially truncated) name + strncpy(buffer, encodedDirname.constData(), 99); + buffer[99] = 0; + // zero out the rest (except for what gets filled anyways) + memset(buffer + 0x9d, 0, 0x200 - 0x9d); + + QByteArray permstr = QByteArray::number(static_cast(perm), 8); + permstr = permstr.rightJustified(6, ' '); + d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x35, uname.constData(), gname.constData()); + + // Write header + device()->write(buffer, 0x200); + if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + d->tarEnd = device()->pos(); + } + + d->dirList.append(dirName); // contains trailing slash + return true; // TODO if wanted, better error control +} + +bool KTar::doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime & /*atime*/, const QDateTime &mtime, const QDateTime & /*ctime*/) +{ + if (!isOpen()) { + setErrorString(tr("Application error: TAR file must be open before being written into")); + qCWarning(KArchiveLog) << "doWriteSymLink failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { + setErrorString(tr("Application error: attempted to write into non-writable TAR file")); + qCWarning(KArchiveLog) << "doWriteSymLink failed: !(mode() & QIODevice::WriteOnly)"; + return false; + } + + // In some tar files we can find dir/./file => call cleanPath + QString fileName(QDir::cleanPath(name)); + + char buffer[0x201]; + memset(buffer, 0, 0x200); + if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read + } + + // provide converted stuff we need lateron + QByteArray encodedFileName = QFile::encodeName(fileName); + QByteArray encodedTarget = QFile::encodeName(target); + QByteArray uname = user.toLocal8Bit(); + QByteArray gname = group.toLocal8Bit(); + + // If more than 100 bytes, we need to use the LongLink trick + if (encodedTarget.length() > 99) { + d->writeLonglink(buffer, encodedTarget, 'K', uname.constData(), gname.constData()); + } + if (encodedFileName.length() > 99) { + d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData()); + } + + // Write (potentially truncated) name + strncpy(buffer, encodedFileName.constData(), 99); + buffer[99] = 0; + // Write (potentially truncated) symlink target + strncpy(buffer + 0x9d, encodedTarget.constData(), 99); + buffer[0x9d + 99] = 0; + // zero out the rest + memset(buffer + 0x9d + 100, 0, 0x200 - 100 - 0x9d); + + QByteArray permstr = QByteArray::number(static_cast(perm), 8); + permstr = permstr.rightJustified(6, ' '); + d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x32, uname.constData(), gname.constData()); + + // Write header + bool retval = device()->write(buffer, 0x200) == 0x200; + if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + d->tarEnd = device()->pos(); + } + return retval; +} + +void KTar::virtual_hook(int id, void *data) +{ + KArchive::virtual_hook(id, data); +} diff --git a/src/karchive/src/ktar.h b/src/karchive/src/ktar.h new file mode 100644 index 0000000000..77f6014c73 --- /dev/null +++ b/src/karchive/src/ktar.h @@ -0,0 +1,101 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000-2005 David Faure + SPDX-FileCopyrightText: 2003 Leo Savernik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KTAR_H +#define KTAR_H + +#include + +/** + * @class KTar ktar.h KTar + * + * A class for reading / writing (optionally compressed) tar archives. + * + * KTar allows you to read and write tar archives, including those + * that are compressed using gzip, bzip2 or xz. + * + * @author Torben Weis , David Faure + */ +class KARCHIVE_EXPORT KTar : public KArchive +{ + Q_DECLARE_TR_FUNCTIONS(KTar) + +public: + /** + * Creates an instance that operates on the given filename + * using the compression filter associated to given mimetype. + * + * @param filename is a local path (e.g. "/home/weis/myfile.tgz") + * @param mimetype "application/x-gzip", "application/x-bzip" or + * "application/x-xz" + * Do not use application/x-compressed-tar or similar - you only need to + * specify the compression layer ! If the mimetype is omitted, it + * will be determined from the filename. + */ + explicit KTar(const QString &filename, + const QString &mimetype = QString()); + + /** + * Creates an instance that operates on the given device. + * The device can be compressed (KFilterDev) or not (QFile, etc.). + * @warning Do not assume that giving a QFile here will decompress the file, + * in case it's compressed! + * @param dev the device to read from. If the source is compressed, the + * QIODevice must take care of decompression + */ + explicit KTar(QIODevice *dev); + + /** + * If the tar ball is still opened, then it will be + * closed automatically by the destructor. + */ + virtual ~KTar(); + + /** + * Special function for setting the "original file name" in the gzip header, + * when writing a tar.gz file. It appears when using in the "file" command, + * for instance. Should only be called if the underlying device is a KFilterDev! + * @param fileName the original file name + */ + void setOrigFileName(const QByteArray &fileName); + +protected: + + /// Reimplemented from KArchive + bool doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doWriteDir(const QString &name, const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doFinishWriting(qint64 size) override; + + /** + * Opens the archive for reading. + * Parses the directory listing of the archive + * and creates the KArchiveDirectory/KArchiveFile entries. + * @param mode the mode of the file + */ + bool openArchive(QIODevice::OpenMode mode) override; + bool closeArchive() override; + + bool createDevice(QIODevice::OpenMode mode) override; + +private: + +protected: + void virtual_hook(int id, void *data) override; +private: + class KTarPrivate; + KTarPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/kxzfilter.cpp b/src/karchive/src/kxzfilter.cpp new file mode 100644 index 0000000000..943c0f6285 --- /dev/null +++ b/src/karchive/src/kxzfilter.cpp @@ -0,0 +1,281 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2007-2008 Per Øyvind Karlsen + + Based on kbzip2filter: + SPDX-FileCopyrightText: 2000-2005 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kxzfilter.h" +#include "loggingcategory.h" + + +#if HAVE_XZ_SUPPORT +extern "C" { +#include +} + +#include + +#include + +class Q_DECL_HIDDEN KXzFilter::Private +{ +public: + Private() + : isInitialized(false) + { + memset(&zStream, 0, sizeof (zStream)); + mode = 0; + } + + lzma_stream zStream; + int mode; + bool isInitialized; + KXzFilter::Flag flag; +}; + +KXzFilter::KXzFilter() + : d(new Private) +{ +} + +KXzFilter::~KXzFilter() +{ + delete d; +} + +bool KXzFilter::init(int mode) +{ + QVector props; + return init(mode, AUTO, props); +} + +static void freeFilters(lzma_filter filters[]) +{ + for (int i = 0; filters[i].id != LZMA_VLI_UNKNOWN; i++) { + free(filters[i].options); + } +} + +bool KXzFilter::init(int mode, Flag flag, const QVector &properties) +{ + if (d->isInitialized) { + terminate(); + } + + d->flag = flag; + lzma_ret result; + d->zStream.next_in = nullptr; + d->zStream.avail_in = 0; + if (mode == QIODevice::ReadOnly) { + // TODO when we can depend on Qt 5.12 Use a QScopeGuard to call freeFilters + lzma_filter filters[5]; + filters[0].id = LZMA_VLI_UNKNOWN; + + switch (flag) { + case AUTO: + /* We set the memlimit for decompression to 100MiB which should be + * more than enough to be sufficient for level 9 which requires 65 MiB. + */ + result = lzma_auto_decoder(&d->zStream, 100 << 20, 0); + if (result != LZMA_OK) { + qCWarning(KArchiveLog) << "lzma_auto_decoder returned" << result; + return false; + } + break; + case LZMA: { + filters[0].id = LZMA_FILTER_LZMA1; + filters[0].options = nullptr; + filters[1].id = LZMA_VLI_UNKNOWN; + filters[1].options = nullptr; + + Q_ASSERT(properties.size() == 5); + unsigned char props[5]; + for (int i = 0; i < properties.size(); ++i) { + props[i] = properties[i]; + } + + result = lzma_properties_decode(&filters[0], nullptr, props, sizeof (props)); + if (result != LZMA_OK) { + qCWarning(KArchiveLog) << "lzma_properties_decode returned" << result; + freeFilters(filters); + return false; + } + break; + } + case LZMA2: { + filters[0].id = LZMA_FILTER_LZMA2; + filters[0].options = nullptr; + filters[1].id = LZMA_VLI_UNKNOWN; + filters[1].options = nullptr; + + Q_ASSERT(properties.size() == 1); + unsigned char props[1]; + props[0] = properties[0]; + + result = lzma_properties_decode(&filters[0], nullptr, props, sizeof (props)); + if (result != LZMA_OK) { + qCWarning(KArchiveLog) << "lzma_properties_decode returned" << result; + freeFilters(filters); + return false; + } + break; + } + case BCJ: { + filters[0].id = LZMA_FILTER_X86; + filters[0].options = nullptr; + + unsigned char props[5] = { 0x5d, 0x00, 0x00, 0x08, 0x00 } + ; + filters[1].id = LZMA_FILTER_LZMA1; + filters[1].options = nullptr; + result = lzma_properties_decode(&filters[1], nullptr, props, sizeof (props)); + if (result != LZMA_OK) { + qCWarning(KArchiveLog) << "lzma_properties_decode1 returned" << result; + freeFilters(filters); + return false; + } + + filters[2].id = LZMA_VLI_UNKNOWN; + filters[2].options = nullptr; + + break; + } + case POWERPC: + case IA64: + case ARM: + case ARMTHUMB: + case SPARC: + //qCDebug(KArchiveLog) << "flag" << flag << "props size" << properties.size(); + break; + } + + if (flag != AUTO) { + result = lzma_raw_decoder(&d->zStream, filters); + if (result != LZMA_OK) { + qCWarning(KArchiveLog) << "lzma_raw_decoder returned" << result; + freeFilters(filters); + return false; + } + } + freeFilters(filters); + + } else if (mode == QIODevice::WriteOnly) { + if (flag == AUTO) { + result = lzma_easy_encoder(&d->zStream, LZMA_PRESET_DEFAULT, LZMA_CHECK_CRC32); + } else { + lzma_filter filters[5]; + if (flag == LZMA2) { + lzma_options_lzma lzma_opt; + lzma_lzma_preset(&lzma_opt, LZMA_PRESET_DEFAULT); + + filters[0].id = LZMA_FILTER_LZMA2; + filters[0].options = &lzma_opt; + filters[1].id = LZMA_VLI_UNKNOWN; + filters[1].options = nullptr; + } + result = lzma_raw_encoder(&d->zStream, filters); + } + if (result != LZMA_OK) { + qCWarning(KArchiveLog) << "lzma_easy_encoder returned" << result; + return false; + } + } else { + //qCWarning(KArchiveLog) << "Unsupported mode " << mode << ". Only QIODevice::ReadOnly and QIODevice::WriteOnly supported"; + return false; + } + d->mode = mode; + d->isInitialized = true; + return true; +} + +int KXzFilter::mode() const +{ + return d->mode; +} + +bool KXzFilter::terminate() +{ + if (d->mode == QIODevice::ReadOnly || d->mode == QIODevice::WriteOnly) { + lzma_end(&d->zStream); + } else { + //qCWarning(KArchiveLog) << "Unsupported mode " << d->mode << ". Only QIODevice::ReadOnly and QIODevice::WriteOnly supported"; + return false; + } + d->isInitialized = false; + return true; +} + +void KXzFilter::reset() +{ + //qCDebug(KArchiveLog) << "KXzFilter::reset"; + // liblzma doesn't have a reset call... + terminate(); + init(d->mode); +} + +void KXzFilter::setOutBuffer(char *data, uint maxlen) +{ + d->zStream.avail_out = maxlen; + d->zStream.next_out = (uint8_t *)data; +} + +void KXzFilter::setInBuffer(const char *data, unsigned int size) +{ + d->zStream.avail_in = size; + d->zStream.next_in = (uint8_t *)const_cast(data); +} + +int KXzFilter::inBufferAvailable() const +{ + return d->zStream.avail_in; +} + +int KXzFilter::outBufferAvailable() const +{ + return d->zStream.avail_out; +} + +KXzFilter::Result KXzFilter::uncompress() +{ + //qCDebug(KArchiveLog) << "Calling lzma_code with avail_in=" << inBufferAvailable() << " avail_out =" << outBufferAvailable(); + lzma_ret result; + result = lzma_code(&d->zStream, LZMA_RUN); + + /*if (result != LZMA_OK) { + qCDebug(KArchiveLog) << "lzma_code returned " << result; + //qCDebug(KArchiveLog) << "KXzFilter::uncompress " << ( result == LZMA_STREAM_END ? KFilterBase::End : KFilterBase::Error ); + }*/ + + switch (result) { + case LZMA_OK: + return KFilterBase::Ok; + case LZMA_STREAM_END: + return KFilterBase::End; + default: + return KFilterBase::Error; + } +} + +KXzFilter::Result KXzFilter::compress(bool finish) +{ + //qCDebug(KArchiveLog) << "Calling lzma_code with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); + lzma_ret result = lzma_code(&d->zStream, finish ? LZMA_FINISH : LZMA_RUN); + switch (result) { + case LZMA_OK: + return KFilterBase::Ok; + break; + case LZMA_STREAM_END: + //qCDebug(KArchiveLog) << " lzma_code returned " << result; + return KFilterBase::End; + break; + default: + //qCDebug(KArchiveLog) << " lzma_code returned " << result; + return KFilterBase::Error; + break; + } +} + +#endif /* HAVE_XZ_SUPPORT */ diff --git a/src/karchive/src/kxzfilter.h b/src/karchive/src/kxzfilter.h new file mode 100644 index 0000000000..e5b318c570 --- /dev/null +++ b/src/karchive/src/kxzfilter.h @@ -0,0 +1,68 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2007-2008 Per Øyvind Karlsen + + Based on kbzip2filter: + SPDX-FileCopyrightText: 2000 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KXZFILTER_H +#define KXZFILTER_H + +#include + +#if HAVE_XZ_SUPPORT + +#include "kfilterbase.h" + +/** + * Internal class used by KFilterDev + * @internal + */ +class KXzFilter : public KFilterBase +{ +public: + KXzFilter(); + virtual ~KXzFilter(); + + bool init(int) override; + + enum Flag { + AUTO = 0, + LZMA = 1, + LZMA2 = 2, + BCJ = 3, //X86 + POWERPC = 4, + IA64 = 5, + ARM = 6, + ARMTHUMB = 7, + SPARC = 8 + }; + + virtual bool init(int, Flag flag, const QVector &props); + int mode() const override; + bool terminate() override; + void reset() override; + bool readHeader() override + { + return true; // lzma handles it by itself ! Cool ! + } + bool writeHeader(const QByteArray &) override + { + return true; + } + void setOutBuffer(char *data, uint maxlen) override; + void setInBuffer(const char *data, uint size) override; + int inBufferAvailable() const override; + int outBufferAvailable() const override; + Result uncompress() override; + Result compress(bool finish) override; +private: + class Private; + Private *const d; +}; + +#endif + +#endif // KXZFILTER_H diff --git a/src/karchive/src/kzip.cpp b/src/karchive/src/kzip.cpp new file mode 100644 index 0000000000..56ad70fba4 --- /dev/null +++ b/src/karchive/src/kzip.cpp @@ -0,0 +1,1470 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2000 David Faure + SPDX-FileCopyrightText: 2002 Holger Schroeder + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kzip.h" +#include "karchive_p.h" +#include "kfilterdev.h" +#include "klimitediodevice_p.h" +#include "loggingcategory.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#if defined(Q_OS_WIN) && defined(Q_CC_MSVC) +#include "QtZlib/zlib.h" +#else +#include +#endif +#include + +#ifndef QT_STAT_LNK +# define QT_STAT_LNK 0120000 +#endif // QT_STAT_LNK + +#ifdef Z_PREFIX +#undef crc32 +constexpr auto crc32 = z_crc32; +#endif + +static const int max_path_len = 4095; // maximum number of character a path may contain + +static void transformToMsDos(const QDateTime &_dt, char *buffer) +{ + const QDateTime dt = _dt.isValid() ? _dt : QDateTime::currentDateTime(); + const quint16 time = + (dt.time().hour() << 11) // 5 bit hour + | (dt.time().minute() << 5) // 6 bit minute + | (dt.time().second() >> 1); // 5 bit double seconds + + buffer[0] = char(time); + buffer[1] = char(time >> 8); + + const quint16 date = + ((dt.date().year() - 1980) << 9) // 7 bit year 1980-based + | (dt.date().month() << 5) // 4 bit month + | (dt.date().day()); // 5 bit day + + buffer[2] = char(date); + buffer[3] = char(date >> 8); +} + +static uint transformFromMsDos(const char *buffer) +{ + quint16 time = (uchar)buffer[0] | ((uchar)buffer[1] << 8); + int h = time >> 11; + int m = (time & 0x7ff) >> 5; + int s = (time & 0x1f) * 2; + QTime qt(h, m, s); + + quint16 date = (uchar)buffer[2] | ((uchar)buffer[3] << 8); + int y = (date >> 9) + 1980; + int o = (date & 0x1ff) >> 5; + int d = (date & 0x1f); + QDate qd(y, o, d); + + QDateTime dt(qd, qt); + return dt.toSecsSinceEpoch(); +} + +// == parsing routines for zip headers + +/** all relevant information about parsing file information */ +struct ParseFileInfo +{ + // file related info + mode_t perm; // permissions of this file + // TODO: use quint32 instead of a uint? + uint atime; // last access time (UNIX format) + uint mtime; // modification time (UNIX format) + uint ctime; // creation time (UNIX format) + int uid; // user id (-1 if not specified) + int gid; // group id (-1 if not specified) + QByteArray guessed_symlink; // guessed symlink target + int extralen; // length of extra field + + // parsing related info + bool exttimestamp_seen; // true if extended timestamp extra field + // has been parsed + bool newinfounix_seen; // true if Info-ZIP Unix New extra field has + // been parsed + + ParseFileInfo() + : perm(0100644) + , uid(-1) + , gid(-1) + , extralen(0) + , exttimestamp_seen(false) + , newinfounix_seen(false) + { + ctime = mtime = atime = time(nullptr); + } +}; + +/** updates the parse information with the given extended timestamp extra field. + * @param buffer start content of buffer known to contain an extended + * timestamp extra field (without magic & size) + * @param size size of field content (must not count magic and size entries) + * @param islocal true if this is a local field, false if central + * @param pfi ParseFileInfo object to be updated + * @return true if processing was successful + */ +static bool parseExtTimestamp(const char *buffer, int size, bool islocal, + ParseFileInfo &pfi) +{ + if (size < 1) { + //qCDebug(KArchiveLog) << "premature end of extended timestamp (#1)"; + return false; + }/*end if*/ + int flags = *buffer; // read flags + buffer += 1; + size -= 1; + + if (flags & 1) { // contains modification time + if (size < 4) { + //qCDebug(KArchiveLog) << "premature end of extended timestamp (#2)"; + return false; + }/*end if*/ + pfi.mtime = uint((uchar)buffer[0] | (uchar)buffer[1] << 8 + | (uchar)buffer[2] << 16 | (uchar)buffer[3] << 24); + buffer += 4; + size -= 4; + }/*end if*/ + // central extended field cannot contain more than the modification time + // even if other flags are set + if (!islocal) { + pfi.exttimestamp_seen = true; + return true; + }/*end if*/ + + if (flags & 2) { // contains last access time + if (size < 4) { + //qCDebug(KArchiveLog) << "premature end of extended timestamp (#3)"; + return true; + }/*end if*/ + pfi.atime = uint((uchar)buffer[0] | (uchar)buffer[1] << 8 + | (uchar)buffer[2] << 16 | (uchar)buffer[3] << 24); + buffer += 4; + size -= 4; + }/*end if*/ + + if (flags & 4) { // contains creation time + if (size < 4) { + //qCDebug(KArchiveLog) << "premature end of extended timestamp (#4)"; + return true; + }/*end if*/ + pfi.ctime = uint((uchar)buffer[0] | (uchar)buffer[1] << 8 + | (uchar)buffer[2] << 16 | (uchar)buffer[3] << 24); + buffer += 4; + }/*end if*/ + + pfi.exttimestamp_seen = true; + return true; +} + +/** updates the parse information with the given Info-ZIP Unix old extra field. + * @param buffer start of content of buffer known to contain an Info-ZIP + * Unix old extra field (without magic & size) + * @param size size of field content (must not count magic and size entries) + * @param islocal true if this is a local field, false if central + * @param pfi ParseFileInfo object to be updated + * @return true if processing was successful + */ +static bool parseInfoZipUnixOld(const char *buffer, int size, bool islocal, + ParseFileInfo &pfi) +{ + // spec mandates to omit this field if one of the newer fields are available + if (pfi.exttimestamp_seen || pfi.newinfounix_seen) { + return true; + } + + if (size < 8) { + //qCDebug(KArchiveLog) << "premature end of Info-ZIP unix extra field old"; + return false; + } + + pfi.atime = uint((uchar)buffer[0] | (uchar)buffer[1] << 8 + | (uchar)buffer[2] << 16 | (uchar)buffer[3] << 24); + buffer += 4; + pfi.mtime = uint((uchar)buffer[0] | (uchar)buffer[1] << 8 + | (uchar)buffer[2] << 16 | (uchar)buffer[3] << 24); + buffer += 4; + if (islocal && size >= 12) { + pfi.uid = (uchar)buffer[0] | (uchar)buffer[1] << 8; + buffer += 2; + pfi.gid = (uchar)buffer[0] | (uchar)buffer[1] << 8; + buffer += 2; + }/*end if*/ + return true; +} + +#if 0 // not needed yet +/** updates the parse information with the given Info-ZIP Unix new extra field. + * @param buffer start of content of buffer known to contain an Info-ZIP + * Unix new extra field (without magic & size) + * @param size size of field content (must not count magic and size entries) + * @param islocal true if this is a local field, false if central + * @param pfi ParseFileInfo object to be updated + * @return true if processing was successful + */ +static bool parseInfoZipUnixNew(const char *buffer, int size, bool islocal, + ParseFileInfo &pfi) +{ + if (!islocal) { // contains nothing in central field + pfi.newinfounix = true; + return true; + } + + if (size < 4) { + qCDebug(KArchiveLog) << "premature end of Info-ZIP unix extra field new"; + return false; + } + + pfi.uid = (uchar)buffer[0] | (uchar)buffer[1] << 8; + buffer += 2; + pfi.gid = (uchar)buffer[0] | (uchar)buffer[1] << 8; + buffer += 2; + + pfi.newinfounix = true; + return true; +} +#endif + +/** + * parses the extra field + * @param buffer start of buffer where the extra field is to be found + * @param size size of the extra field + * @param islocal true if this is part of a local header, false if of central + * @param pfi ParseFileInfo object which to write the results into + * @return true if parsing was successful + */ +static bool parseExtraField(const char *buffer, int size, bool islocal, + ParseFileInfo &pfi) +{ + // extra field in central directory doesn't contain useful data, so we + // don't bother parsing it + if (!islocal) { + return true; + } + + while (size >= 4) { // as long as a potential extra field can be read + int magic = (uchar)buffer[0] | (uchar)buffer[1] << 8; + buffer += 2; + int fieldsize = (uchar)buffer[0] | (uchar)buffer[1] << 8; + buffer += 2; + size -= 4; + + if (fieldsize > size) { + //qCDebug(KArchiveLog) << "fieldsize: " << fieldsize << " size: " << size; + //qCDebug(KArchiveLog) << "premature end of extra fields reached"; + break; + } + + switch (magic) { + case 0x5455: // extended timestamp + if (!parseExtTimestamp(buffer, fieldsize, islocal, pfi)) { + return false; + } + break; + case 0x5855: // old Info-ZIP unix extra field + if (!parseInfoZipUnixOld(buffer, fieldsize, islocal, pfi)) { + return false; + } + break; +#if 0 // not needed yet + case 0x7855: // new Info-ZIP unix extra field + if (!parseInfoZipUnixNew(buffer, fieldsize, islocal, pfi)) { + return false; + } + break; +#endif + default: + /* ignore everything else */ + ; + }/*end switch*/ + + buffer += fieldsize; + size -= fieldsize; + }/*wend*/ + return true; +} + +/** + * Checks if a token for a central or local header has been found and resets + * the device to the begin of the token. If a token for the data descriptor is + * found it is assumed there is a central or local header token starting right + * behind the data descriptor, and the device is set accordingly to the begin + * of that token. + * To be called when a 'P' has been found. + * @param buffer start of buffer with the 3 bytes behind 'P' + * @param dev device that is read from + * @param dataDescriptor only search for data descriptor + * @return true if a local or central header begin is or could be reached + */ +static bool handlePossibleHeaderBegin(const char *buffer, QIODevice *dev, bool dataDescriptor) +{ + // we have to detect three magic tokens here: + // PK34 for the next local header in case there is no data descriptor + // PK12 for the central header in case there is no data descriptor + // PK78 for the data descriptor in case it is following the compressed data + // TODO: optimize using 32bit const data for comparison instead of byte-wise, + // given we run at least on 32bit CPUs + + if (buffer[0] == 'K') { + if (buffer[1] == 7 && buffer[2] == 8) { + // data descriptor token found + dev->seek(dev->pos() + 12); // skip the 'data_descriptor' + return true; + } + + if (!dataDescriptor && ((buffer[1] == 1 && buffer[2] == 2) + || (buffer[1] == 3 && buffer[2] == 4))) { + // central/local header token found + dev->seek(dev->pos() - 4); + // go back 4 bytes, so that the magic bytes can be found + // in the next cycle... + return true; + } + } + return false; +} + +/** + * Reads the device forwards from the current pos until a token for a central or + * local header has been found or is to be assumed. + * @param dev device that is read from + * @return true if a local or central header token could be reached, false on error + */ +static bool seekToNextHeaderToken(QIODevice *dev, bool dataDescriptor) +{ + bool headerTokenFound = false; + char buffer[3]; + + while (!headerTokenFound) { + int n = dev->read(buffer, 1); + if (n < 1) { + //qCWarning(KArchiveLog) << "Invalid ZIP file. Unexpected end of file. (#2)"; + return false; + } + + if (buffer[0] != 'P') { + continue; + } + + n = dev->read(buffer, 3); + if (n < 3) { + //qCWarning(KArchiveLog) << "Invalid ZIP file. Unexpected end of file. (#3)"; + return false; + } + + if (handlePossibleHeaderBegin(buffer, dev, dataDescriptor)) { + headerTokenFound = true; + } else { + for (int i = 0; i < 3; ++i) { + if (buffer[i] == 'P') { + // We have another P character so we must go back a little to check if it is a magic + dev->seek(dev->pos() - 3 + i); + break; + } + } + } + } + return true; +} + +//////////////////////////////////////////////////////////////////////// +/////////////////////////// KZip /////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class Q_DECL_HIDDEN KZip::KZipPrivate +{ +public: + KZipPrivate() + : m_crc(0) + , m_currentFile(nullptr) + , m_currentDev(nullptr) + , m_compression(8) + , m_extraField(KZip::NoExtraField) + , m_offset(0) + { + } + + unsigned long m_crc; // checksum + KZipFileEntry *m_currentFile; // file currently being written + QIODevice *m_currentDev; // filterdev used to write to the above file + QList m_fileList; // flat list of all files, for the index (saves a recursive method ;) + int m_compression; + KZip::ExtraField m_extraField; + // m_offset holds the offset of the place in the zip, + // where new data can be appended. after openarchive it points to 0, when in + // writeonly mode, or it points to the beginning of the central directory. + // each call to writefile updates this value. + quint64 m_offset; +}; + +KZip::KZip(const QString &fileName) + : KArchive(fileName) + , d(new KZipPrivate) +{ +} + +KZip::KZip(QIODevice *dev) + : KArchive(dev) + , d(new KZipPrivate) +{ +} + +KZip::~KZip() +{ + //qCDebug(KArchiveLog) << this; + if (isOpen()) { + close(); + } + delete d; +} + +bool KZip::openArchive(QIODevice::OpenMode mode) +{ + //qCDebug(KArchiveLog); + d->m_fileList.clear(); + + if (mode == QIODevice::WriteOnly) { + return true; + } + + char buffer[47]; + + // Check that it's a valid ZIP file + // KArchive::open() opened the underlying device already. + + quint64 offset = 0; // holds offset, where we read + // contains information gathered from the local file headers + QHash pfi_map; + + QIODevice *dev = device(); + + // We set a bool for knowing if we are allowed to skip the start of the file + bool startOfFile = true; + + for (;;) { // repeat until 'end of entries' signature is reached + //qCDebug(KArchiveLog) << "loop starts"; + //qCDebug(KArchiveLog) << "dev->pos() now : " << dev->pos(); + int n = dev->read(buffer, 4); + + if (n < 4) { + setErrorString(tr("Invalid ZIP file. Unexpected end of file. (Error code: %1)").arg(1)); + return false; + } + + if (!memcmp(buffer, "PK\5\6", 4)) { // 'end of entries' + //qCDebug(KArchiveLog) << "PK56 found end of archive"; + startOfFile = false; + break; + } + + if (!memcmp(buffer, "PK\3\4", 4)) { // local file header + //qCDebug(KArchiveLog) << "PK34 found local file header"; + startOfFile = false; + // can this fail ??? + dev->seek(dev->pos() + 2); // skip 'version needed to extract' + + // read static header stuff + n = dev->read(buffer, 24); + if (n < 24) { + setErrorString(tr("Invalid ZIP file. Unexpected end of file. (Error code: %1)").arg(4)); + return false; + } + + int gpf = (uchar)buffer[0]; // "general purpose flag" not "general protection fault" ;-) + int compression_mode = (uchar)buffer[2] | (uchar)buffer[3] << 8; + uint mtime = transformFromMsDos(buffer + 4); + + const qint64 compr_size = uint(uchar(buffer[12])) | uint(uchar(buffer[13])) << 8 | + uint(uchar(buffer[14])) << 16 | uint(uchar(buffer[15])) << 24; + const qint64 uncomp_size = uint(uchar(buffer[16])) | uint(uchar(buffer[17])) << 8 | + uint(uchar(buffer[18])) << 16 | uint(uchar(buffer[19])) << 24; + const int namelen = uint(uchar(buffer[20])) | uint(uchar(buffer[21])) << 8; + const int extralen = uint(uchar(buffer[22])) | uint(uchar(buffer[23])) << 8; + + /* + qCDebug(KArchiveLog) << "general purpose bit flag: " << gpf; + qCDebug(KArchiveLog) << "compressed size: " << compr_size; + qCDebug(KArchiveLog) << "uncompressed size: " << uncomp_size; + qCDebug(KArchiveLog) << "namelen: " << namelen; + qCDebug(KArchiveLog) << "extralen: " << extralen; + qCDebug(KArchiveLog) << "archive size: " << dev->size(); + */ + + // read fileName + if (namelen <= 0) { + setErrorString(tr("Invalid ZIP file. Negative name length")); + return false; + } + QByteArray fileName = dev->read(namelen); + if (fileName.size() < namelen) { + setErrorString(tr("Invalid ZIP file. Name not completely read (#2)")); + return false; + } + + ParseFileInfo pfi; + pfi.mtime = mtime; + + // read and parse the beginning of the extra field, + // skip rest of extra field in case it is too long + unsigned int extraFieldEnd = dev->pos() + extralen; + pfi.extralen = extralen; + int handledextralen = qMin(extralen, (int)sizeof buffer); + + //if (handledextralen) + // qCDebug(KArchiveLog) << "handledextralen: " << handledextralen; + + n = dev->read(buffer, handledextralen); + // no error msg necessary as we deliberately truncate the extra field + if (!parseExtraField(buffer, n, true, pfi)) { + setErrorString(tr("Invalid ZIP File. Broken ExtraField.")); + return false; + } + + // jump to end of extra field + dev->seek(extraFieldEnd); + + // we have to take care of the 'general purpose bit flag'. + // if bit 3 is set, the header doesn't contain the length of + // the file and we look for the signature 'PK\7\8'. + if (gpf & 8) { + // here we have to read through the compressed data to find + // the next PKxx + if (!seekToNextHeaderToken(dev, true)) { + setErrorString(tr("Could not seek to next header token")); + return false; + } + } else { + // here we skip the compressed data and jump to the next header + //qCDebug(KArchiveLog) << "general purpose bit flag indicates, that local file header contains valid size"; + bool foundSignature = false; + // check if this could be a symbolic link + if (compression_mode == NoCompression + && uncomp_size <= max_path_len + && uncomp_size > 0) { + // read content and store it + // If it's not a symlink, then we'll just discard the data for now. + pfi.guessed_symlink = dev->read(uncomp_size); + if (pfi.guessed_symlink.size() < uncomp_size) { + setErrorString(tr("Invalid ZIP file. Unexpected end of file. (#5)")); + return false; + } + } else { + if (compr_size > dev->size()) { + // here we cannot trust the compressed size, so scan through the compressed + // data to find the next header + if (!seekToNextHeaderToken(dev, false)) { + setErrorString(tr("Could not seek to next header token")); + return false; + } + foundSignature = true; + } else { +// qCDebug(KArchiveLog) << "before interesting dev->pos(): " << dev->pos(); + const bool success = dev->seek(dev->pos() + compr_size); + if (!success) { + setErrorString(tr("Could not seek to file compressed size")); + return false; + } + /* qCDebug(KArchiveLog) << "after interesting dev->pos(): " << dev->pos(); + if (success) + qCDebug(KArchiveLog) << "dev->at was successful... "; + else + qCDebug(KArchiveLog) << "dev->at failed... ";*/ + } + + } + // test for optional data descriptor + if (!foundSignature) { +// qCDebug(KArchiveLog) << "Testing for optional data descriptor"; + // read static data descriptor + n = dev->read(buffer, 4); + if (n < 4) { + setErrorString(tr("Invalid ZIP file. Unexpected end of file. (#1)")); + return false; + } + + if (buffer[0] != 'P' || !handlePossibleHeaderBegin(buffer + 1, dev, false)) { + // assume data descriptor without signature + dev->seek(dev->pos() + 8); // skip rest of the 'data_descriptor' + } + } + +// not needed any more + /* // here we calculate the length of the file in the zip + // with headers and jump to the next header. + uint skip = compr_size + namelen + extralen; + offset += 30 + skip;*/ + } + pfi_map.insert(fileName, pfi); + } else if (!memcmp(buffer, "PK\1\2", 4)) { // central block + //qCDebug(KArchiveLog) << "PK12 found central block"; + startOfFile = false; + + // so we reached the central header at the end of the zip file + // here we get all interesting data out of the central header + // of a file + offset = dev->pos() - 4; + + //set offset for appending new files + if (d->m_offset == 0) { + d->m_offset = offset; + } + + n = dev->read(buffer + 4, 42); + if (n < 42) { + setErrorString(tr( + "Invalid ZIP file, central entry too short " + "(not long enough for valid entry)")); + return false; + } + + //int gpf = (uchar)buffer[9] << 8 | (uchar)buffer[10]; + //qCDebug(KArchiveLog) << "general purpose flag=" << gpf; + // length of the fileName (well, pathname indeed) + int namelen = (uchar)buffer[29] << 8 | (uchar)buffer[28]; + if (namelen <= 0) { + setErrorString(tr("Invalid ZIP file, file path name length smaller or equal to zero")); + return false; + } + QByteArray bufferName = dev->read(namelen); + if (bufferName.size() < namelen) { + //qCWarning(KArchiveLog) << "Invalid ZIP file. Name not completely read"; + } + + ParseFileInfo pfi = pfi_map.value(bufferName, ParseFileInfo()); + + QString name(QFile::decodeName(bufferName)); + + //qCDebug(KArchiveLog) << "name: " << name; + // only in central header ! see below. + // length of extra attributes + int extralen = (uchar)buffer[31] << 8 | (uchar)buffer[30]; + // length of comment for this file + int commlen = (uchar)buffer[33] << 8 | (uchar)buffer[32]; + // compression method of this file + int cmethod = (uchar)buffer[11] << 8 | (uchar)buffer[10]; + + //qCDebug(KArchiveLog) << "cmethod: " << cmethod; + //qCDebug(KArchiveLog) << "extralen: " << extralen; + + // crc32 of the file + uint crc32 = (uchar)buffer[19] << 24 | (uchar)buffer[18] << 16 | + (uchar)buffer[17] << 8 | (uchar)buffer[16]; + + // uncompressed file size + uint ucsize = (uchar)buffer[27] << 24 | (uchar)buffer[26] << 16 | + (uchar)buffer[25] << 8 | (uchar)buffer[24]; + // compressed file size + uint csize = (uchar)buffer[23] << 24 | (uchar)buffer[22] << 16 | + (uchar)buffer[21] << 8 | (uchar)buffer[20]; + + // offset of local header + uint localheaderoffset = (uchar)buffer[45] << 24 | (uchar)buffer[44] << 16 | + (uchar)buffer[43] << 8 | (uchar)buffer[42]; + + // some clever people use different extra field lengths + // in the central header and in the local header... funny. + // so we need to get the localextralen to calculate the offset + // from localheaderstart to dataoffset + int localextralen = pfi.extralen; // FIXME: this will not work if + // no local header exists + + //qCDebug(KArchiveLog) << "localextralen: " << localextralen; + + // offset, where the real data for uncompression starts + uint dataoffset = localheaderoffset + 30 + localextralen + namelen; //comment only in central header + + //qCDebug(KArchiveLog) << "esize: " << esize; + //qCDebug(KArchiveLog) << "eoffset: " << eoffset; + //qCDebug(KArchiveLog) << "csize: " << csize; + + int os_madeby = (uchar)buffer[5]; + bool isdir = false; + int access = 0100644; + + if (os_madeby == 3) { // good ole unix + access = (uchar)buffer[40] | (uchar)buffer[41] << 8; + } + + QString entryName; + + if (name.endsWith(QLatin1Char('/'))) { // Entries with a trailing slash are directories + isdir = true; + name = name.left(name.length() - 1); + if (os_madeby != 3) { + access = S_IFDIR | 0755; + } else { + access |= S_IFDIR | 0700; + } + } + + int pos = name.lastIndexOf(QLatin1Char('/')); + if (pos == -1) { + entryName = name; + } else { + entryName = name.mid(pos + 1); + } + if (entryName.isEmpty()) { + setErrorString(tr("Invalid ZIP file, found empty entry name")); + return false; + } + + KArchiveEntry *entry; + if (isdir) { + QString path = QDir::cleanPath(name); + const KArchiveEntry *ent = rootDir()->entry(path); + if (ent && ent->isDirectory()) { + //qCDebug(KArchiveLog) << "Directory already exists, NOT going to add it again"; + entry = nullptr; + } else { + QDateTime mtime = KArchivePrivate::time_tToDateTime(pfi.mtime); + entry = new KArchiveDirectory(this, entryName, access, mtime, rootDir()->user(), rootDir()->group(), QString()); + //qCDebug(KArchiveLog) << "KArchiveDirectory created, entryName= " << entryName << ", name=" << name; + } + } else { + QString symlink; + if ((access & QT_STAT_MASK) == QT_STAT_LNK) { + symlink = QFile::decodeName(pfi.guessed_symlink); + } + QDateTime mtime = KArchivePrivate::time_tToDateTime(pfi.mtime); + entry = new KZipFileEntry(this, entryName, access, mtime, + rootDir()->user(), rootDir()->group(), + symlink, name, dataoffset, + ucsize, cmethod, csize); + static_cast(entry)->setHeaderStart(localheaderoffset); + static_cast(entry)->setCRC32(crc32); + //qCDebug(KArchiveLog) << "KZipFileEntry created, entryName= " << entryName << ", name=" << name; + d->m_fileList.append(static_cast(entry)); + } + + if (entry) { + if (pos == -1) { + rootDir()->addEntry(entry); + } else { + // In some tar files we can find dir/./file => call cleanPath + QString path = QDir::cleanPath(name.left(pos)); + // Ensure container directory exists, create otherwise + KArchiveDirectory *tdir = findOrCreate(path); + if (tdir) { + tdir->addEntry(entry); + } else { + setErrorString(tr("File %1 is in folder %2, but %3 is actually a file.").arg(entryName, path, path)); + delete entry; + return false; + } + } + } + + //calculate offset to next entry + offset += 46 + commlen + extralen + namelen; + const bool b = dev->seek(offset); + if (!b) { + setErrorString(tr("Could not seek to next entry")); + return false; + } + } else if (startOfFile) { + // The file does not start with any ZIP header (e.g. self-extractable ZIP files) + // Therefore we need to find the first PK\003\004 (local header) + //qCDebug(KArchiveLog) << "Try to skip start of file"; + startOfFile = false; + bool foundSignature = false; + + while (!foundSignature) { + n = dev->read(buffer, 1); + if (n < 1) { + setErrorString(tr("Invalid ZIP file. Unexpected end of file.")); + return false; + } + + if (buffer[0] != 'P') { + continue; + } + + n = dev->read(buffer, 3); + if (n < 3) { + setErrorString(tr("Invalid ZIP file. Unexpected end of file.")); + return false; + } + + // We have to detect the magic token for a local header: PK\003\004 + /* + * Note: we do not need to check the other magics, if the ZIP file has no + * local header, then it has not any files! + */ + if (buffer[0] == 'K' && buffer[1] == 3 && buffer[2] == 4) { + foundSignature = true; + dev->seek(dev->pos() - 4); // go back 4 bytes, so that the magic bytes can be found... + } else { + for (int i = 0; i < 3; ++i) { + if (buffer[i] == 'P') { + // We have another P character so we must go back a little to check if it is a magic + dev->seek(dev->pos() - 3 + i); + break; + } + } + } + } + } else { + setErrorString( + tr("Invalid ZIP file. Unrecognized header at offset %1") + .arg(dev->pos() - 4)); + return false; + } + } + //qCDebug(KArchiveLog) << "*** done *** "; + return true; +} + +bool KZip::closeArchive() +{ + if (!(mode() & QIODevice::WriteOnly)) { + //qCDebug(KArchiveLog) << "readonly"; + return true; + } + + //ReadWrite or WriteOnly + //write all central dir file entries + + // to be written at the end of the file... + char buffer[22]; // first used for 12, then for 22 at the end + uLong crc = crc32(0L, nullptr, 0); + + qint64 centraldiroffset = device()->pos(); + //qCDebug(KArchiveLog) << "closearchive: centraldiroffset: " << centraldiroffset; + qint64 atbackup = centraldiroffset; + QMutableListIterator it(d->m_fileList); + + while (it.hasNext()) { + //set crc and compressed size in each local file header + it.next(); + if (!device()->seek(it.value()->headerStart() + 14)) { + setErrorString( + tr("Could not seek to next file header: %1") + .arg(device()->errorString())); + return false; + } + //qCDebug(KArchiveLog) << "closearchive setcrcandcsize: fileName:" + // << it.current()->path() + // << "encoding:" << it.current()->encoding(); + + uLong mycrc = it.value()->crc32(); + buffer[0] = char(mycrc); // crc checksum, at headerStart+14 + buffer[1] = char(mycrc >> 8); + buffer[2] = char(mycrc >> 16); + buffer[3] = char(mycrc >> 24); + + int mysize1 = it.value()->compressedSize(); + buffer[4] = char(mysize1); // compressed file size, at headerStart+18 + buffer[5] = char(mysize1 >> 8); + buffer[6] = char(mysize1 >> 16); + buffer[7] = char(mysize1 >> 24); + + int myusize = it.value()->size(); + buffer[8] = char(myusize); // uncompressed file size, at headerStart+22 + buffer[9] = char(myusize >> 8); + buffer[10] = char(myusize >> 16); + buffer[11] = char(myusize >> 24); + + if (device()->write(buffer, 12) != 12) { + setErrorString( + tr("Could not write file header: %1") + .arg(device()->errorString())); + return false; + } + } + device()->seek(atbackup); + + it.toFront(); + while (it.hasNext()) { + it.next(); + //qCDebug(KArchiveLog) << "fileName:" << it.current()->path() + // << "encoding:" << it.current()->encoding(); + + QByteArray path = QFile::encodeName(it.value()->path()); + + const int extra_field_len = (d->m_extraField == ModificationTime) ? 9 : 0; + const int bufferSize = extra_field_len + path.length() + 46; + char *buffer = new char[bufferSize]; + + memset(buffer, 0, 46); // zero is a nice default for most header fields + + const char head[] = { + 'P', 'K', 1, 2, // central file header signature + 0x14, 3, // version made by (3 == UNIX) + 0x14, 0 // version needed to extract + }; + + // I do not know why memcpy is not working here + //memcpy(buffer, head, sizeof(head)); + memmove(buffer, head, sizeof (head)); + + buffer[10] = char(it.value()->encoding()); // compression method + buffer[11] = char(it.value()->encoding() >> 8); + + transformToMsDos(it.value()->date(), &buffer[12]); + + uLong mycrc = it.value()->crc32(); + buffer[16] = char(mycrc); // crc checksum + buffer[17] = char(mycrc >> 8); + buffer[18] = char(mycrc >> 16); + buffer[19] = char(mycrc >> 24); + + int mysize1 = it.value()->compressedSize(); + buffer[20] = char(mysize1); // compressed file size + buffer[21] = char(mysize1 >> 8); + buffer[22] = char(mysize1 >> 16); + buffer[23] = char(mysize1 >> 24); + + int mysize = it.value()->size(); + buffer[24] = char(mysize); // uncompressed file size + buffer[25] = char(mysize >> 8); + buffer[26] = char(mysize >> 16); + buffer[27] = char(mysize >> 24); + + buffer[28] = char(path.length()); // fileName length + buffer[29] = char(path.length() >> 8); + + buffer[30] = char(extra_field_len); + buffer[31] = char(extra_field_len >> 8); + + buffer[40] = char(it.value()->permissions()); + buffer[41] = char(it.value()->permissions() >> 8); + + int myhst = it.value()->headerStart(); + buffer[42] = char(myhst); //relative offset of local header + buffer[43] = char(myhst >> 8); + buffer[44] = char(myhst >> 16); + buffer[45] = char(myhst >> 24); + + // file name + strncpy(buffer + 46, path.constData(), path.length()); + //qCDebug(KArchiveLog) << "closearchive length to write: " << bufferSize; + + // extra field + if (d->m_extraField == ModificationTime) { + char *extfield = buffer + 46 + path.length(); + // "Extended timestamp" header (0x5455) + extfield[0] = 'U'; + extfield[1] = 'T'; + extfield[2] = 5; // data size + extfield[3] = 0; + extfield[4] = 1 | 2 | 4; // specify flags from local field + // (unless I misread the spec) + // provide only modification time + unsigned long time = (unsigned long)it.value()->date().toSecsSinceEpoch(); + extfield[5] = char(time); + extfield[6] = char(time >> 8); + extfield[7] = char(time >> 16); + extfield[8] = char(time >> 24); + } + + crc = crc32(crc, (Bytef *)buffer, bufferSize); + bool ok = (device()->write(buffer, bufferSize) == bufferSize); + delete[] buffer; + if (!ok) { + setErrorString( + tr("Could not write file header: %1") + .arg(device()->errorString())); + return false; + } + } + qint64 centraldirendoffset = device()->pos(); + //qCDebug(KArchiveLog) << "closearchive: centraldirendoffset: " << centraldirendoffset; + //qCDebug(KArchiveLog) << "closearchive: device()->pos(): " << device()->pos(); + + //write end of central dir record. + buffer[0] = 'P'; //end of central dir signature + buffer[1] = 'K'; + buffer[2] = 5; + buffer[3] = 6; + + buffer[4] = 0; // number of this disk + buffer[5] = 0; + + buffer[6] = 0; // number of disk with start of central dir + buffer[7] = 0; + + int count = d->m_fileList.count(); + //qCDebug(KArchiveLog) << "number of files (count): " << count; + + buffer[8] = char(count); // total number of entries in central dir of + buffer[9] = char(count >> 8); // this disk + + buffer[10] = buffer[8]; // total number of entries in the central dir + buffer[11] = buffer[9]; + + int cdsize = centraldirendoffset - centraldiroffset; + buffer[12] = char(cdsize); // size of the central dir + buffer[13] = char(cdsize >> 8); + buffer[14] = char(cdsize >> 16); + buffer[15] = char(cdsize >> 24); + + //qCDebug(KArchiveLog) << "end : centraldiroffset: " << centraldiroffset; + //qCDebug(KArchiveLog) << "end : centraldirsize: " << cdsize; + + buffer[16] = char(centraldiroffset); // central dir offset + buffer[17] = char(centraldiroffset >> 8); + buffer[18] = char(centraldiroffset >> 16); + buffer[19] = char(centraldiroffset >> 24); + + buffer[20] = 0; //zipfile comment length + buffer[21] = 0; + + if (device()->write(buffer, 22) != 22) { + setErrorString( + tr("Could not write central dir record: %1") + .arg(device()->errorString())); + return false; + } + + return true; +} + +bool KZip::doWriteDir(const QString &name, const QString &user, const QString &group, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) +{ + // Zip files have no explicit directories, they are implicitly created during extraction time + // when file entries have paths in them. + // However, to support empty directories, we must create a dummy file entry which ends with '/'. + QString dirName = name; + if (!name.endsWith(QLatin1Char('/'))) { + dirName = dirName.append(QLatin1Char('/')); + } + return writeFile(dirName, QByteArray(), perm, user, group, atime, mtime, ctime); +} + +bool KZip::doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 /*size*/, mode_t perm, + const QDateTime &accessTime, const QDateTime &modificationTime, const QDateTime &creationTime) +{ + //qCDebug(KArchiveLog); + if (!isOpen()) { + setErrorString(tr("Application error: ZIP file must be open before being written into")); + qCWarning(KArchiveLog) << "doPrepareWriting failed: !isOpen()"; + return false; + } + + if (!(mode() & QIODevice::WriteOnly)) { // accept WriteOnly and ReadWrite + setErrorString(tr("Application error: attempted to write into non-writable ZIP file")); + qCWarning(KArchiveLog) << "doPrepareWriting failed: !(mode() & QIODevice::WriteOnly)"; + return false; + } + + if (!device()) { + setErrorString(tr("Cannot create a device. Disk full?")); + return false; + } + + // set right offset in zip. + if (!device()->seek(d->m_offset)) { + setErrorString(tr("Cannot seek in ZIP file. Disk full?")); + return false; + } + + uint atime = accessTime.toSecsSinceEpoch(); + uint mtime = modificationTime.toSecsSinceEpoch(); + uint ctime = creationTime.toSecsSinceEpoch(); + + // Find or create parent dir + KArchiveDirectory *parentDir = rootDir(); + QString fileName(name); + int i = name.lastIndexOf(QLatin1Char('/')); + if (i != -1) { + QString dir = name.left(i); + fileName = name.mid(i + 1); + //qCDebug(KArchiveLog) << "ensuring" << dir << "exists. fileName=" << fileName; + parentDir = findOrCreate(dir); + } + + // delete entries in the filelist with the same fileName as the one we want + // to save, so that we don't have duplicate file entries when viewing the zip + // with konqi... + // CAUTION: the old file itself is still in the zip and won't be removed !!! + QMutableListIterator it(d->m_fileList); + //qCDebug(KArchiveLog) << "fileName to write: " << name; + while (it.hasNext()) { + it.next(); + //qCDebug(KArchiveLog) << "prepfileName: " << it.current()->path(); + if (name == it.value()->path()) { + // also remove from the parentDir + parentDir->removeEntry(it.value()); + //qCDebug(KArchiveLog) << "removing following entry: " << it.current()->path(); + delete it.value(); + it.remove(); + } + + } + + // construct a KZipFileEntry and add it to list + KZipFileEntry *e = new KZipFileEntry(this, fileName, perm, modificationTime, user, group, QString(), + name, device()->pos() + 30 + name.length(), // start + 0 /*size unknown yet*/, d->m_compression, 0 /*csize unknown yet*/); + e->setHeaderStart(device()->pos()); + //qCDebug(KArchiveLog) << "wrote file start: " << e->position() << " name: " << name; + if (!parentDir->addEntryV2(e)) { + return false; + } + + d->m_currentFile = e; + d->m_fileList.append(e); + + int extra_field_len = 0; + if (d->m_extraField == ModificationTime) { + extra_field_len = 17; // value also used in finishWriting() + } + + // write out zip header + QByteArray encodedName = QFile::encodeName(name); + int bufferSize = extra_field_len + encodedName.length() + 30; + //qCDebug(KArchiveLog) << "bufferSize=" << bufferSize; + char *buffer = new char[bufferSize]; + + buffer[0] = 'P'; //local file header signature + buffer[1] = 'K'; + buffer[2] = 3; + buffer[3] = 4; + + buffer[4] = 0x14; // version needed to extract + buffer[5] = 0; + + buffer[6] = 0; // general purpose bit flag + buffer[7] = 0; + + buffer[8] = char(e->encoding()); // compression method + buffer[9] = char(e->encoding() >> 8); + + transformToMsDos(e->date(), &buffer[10]); + + buffer[14] = 'C'; //dummy crc + buffer[15] = 'R'; + buffer[16] = 'C'; + buffer[17] = 'q'; + + buffer[18] = 'C'; //compressed file size + buffer[19] = 'S'; + buffer[20] = 'I'; + buffer[21] = 'Z'; + + buffer[22] = 'U'; //uncompressed file size + buffer[23] = 'S'; + buffer[24] = 'I'; + buffer[25] = 'Z'; + + buffer[26] = (uchar)(encodedName.length()); //fileName length + buffer[27] = (uchar)(encodedName.length() >> 8); + + buffer[28] = (uchar)(extra_field_len); // extra field length + buffer[29] = (uchar)(extra_field_len >> 8); + + // file name + strncpy(buffer + 30, encodedName.constData(), encodedName.length()); + + // extra field + if (d->m_extraField == ModificationTime) { + char *extfield = buffer + 30 + encodedName.length(); + // "Extended timestamp" header (0x5455) + extfield[0] = 'U'; + extfield[1] = 'T'; + extfield[2] = 13; // data size + extfield[3] = 0; + extfield[4] = 1 | 2 | 4; // contains mtime, atime, ctime + + extfield[5] = char(mtime); + extfield[6] = char(mtime >> 8); + extfield[7] = char(mtime >> 16); + extfield[8] = char(mtime >> 24); + + extfield[9] = char(atime); + extfield[10] = char(atime >> 8); + extfield[11] = char(atime >> 16); + extfield[12] = char(atime >> 24); + + extfield[13] = char(ctime); + extfield[14] = char(ctime >> 8); + extfield[15] = char(ctime >> 16); + extfield[16] = char(ctime >> 24); + } + + // Write header + bool b = (device()->write(buffer, bufferSize) == bufferSize); + d->m_crc = 0; + delete[] buffer; + + if (!b) { + setErrorString(tr("Could not write to the archive. Disk full?")); + return false; + } + + // Prepare device for writing the data + // Either device() if no compression, or a KFilterDev to compress + if (d->m_compression == 0) { + d->m_currentDev = device(); + return true; + } + + auto compressionDevice = new KCompressionDevice(device(), false, KCompressionDevice::GZip); + d->m_currentDev = compressionDevice; + compressionDevice->setSkipHeaders(); // Just zlib, not gzip + + b = d->m_currentDev->open(QIODevice::WriteOnly); + Q_ASSERT(b); + + if (!b) { + setErrorString( + tr("Could not open compression device: %1") + .arg(d->m_currentDev->errorString())); + } + + return b; +} + +bool KZip::doFinishWriting(qint64 size) +{ + if (d->m_currentFile->encoding() == 8) { + // Finish + (void)d->m_currentDev->write(nullptr, 0); + delete d->m_currentDev; + } + // If 0, d->m_currentDev was device() - don't delete ;) + d->m_currentDev = nullptr; + + Q_ASSERT(d->m_currentFile); + //qCDebug(KArchiveLog) << "fileName: " << d->m_currentFile->path(); + //qCDebug(KArchiveLog) << "getpos (at): " << device()->pos(); + d->m_currentFile->setSize(size); + int extra_field_len = 0; + if (d->m_extraField == ModificationTime) { + extra_field_len = 17; // value also used in finishWriting() + } + + const QByteArray encodedName = QFile::encodeName(d->m_currentFile->path()); + int csize = device()->pos() - + d->m_currentFile->headerStart() - 30 - + encodedName.length() - extra_field_len; + d->m_currentFile->setCompressedSize(csize); + //qCDebug(KArchiveLog) << "usize: " << d->m_currentFile->size(); + //qCDebug(KArchiveLog) << "csize: " << d->m_currentFile->compressedSize(); + //qCDebug(KArchiveLog) << "headerstart: " << d->m_currentFile->headerStart(); + + //qCDebug(KArchiveLog) << "crc: " << d->m_crc; + d->m_currentFile->setCRC32(d->m_crc); + + d->m_currentFile = nullptr; + + // update saved offset for appending new files + d->m_offset = device()->pos(); + return true; +} + +bool KZip::doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) +{ + // reassure that symlink flag is set, otherwise strange things happen on + // extraction + perm |= QT_STAT_LNK; + Compression c = compression(); + setCompression(NoCompression); // link targets are never compressed + + if (!doPrepareWriting(name, user, group, 0, perm, atime, mtime, ctime)) { + setCompression(c); + return false; + } + + QByteArray symlink_target = QFile::encodeName(target); + if (!writeData(symlink_target.constData(), symlink_target.length())) { + setCompression(c); + return false; + } + + if (!finishWriting(symlink_target.length())) { + setCompression(c); + return false; + } + + setCompression(c); + return true; +} + +void KZip::virtual_hook(int id, void *data) +{ + KArchive::virtual_hook(id, data); +} + +bool KZip::writeData(const char *data, qint64 size) +{ + Q_ASSERT(d->m_currentFile); + Q_ASSERT(d->m_currentDev); + if (!d->m_currentFile || !d->m_currentDev) { + setErrorString(tr("No file or device")); + return false; + } + + // crc to be calculated over uncompressed stuff... + // and they didn't mention it in their docs... + d->m_crc = crc32(d->m_crc, (const Bytef *) data, size); + + qint64 written = d->m_currentDev->write(data, size); + //qCDebug(KArchiveLog) << "wrote" << size << "bytes."; + const bool ok = written == size; + + if (!ok) { + setErrorString( + tr("Error writing data: %1") + .arg(d->m_currentDev->errorString())); + } + + return ok; +} + +void KZip::setCompression(Compression c) +{ + d->m_compression = (c == NoCompression) ? 0 : 8; +} + +KZip::Compression KZip::compression() const +{ + return (d->m_compression == 8) ? DeflateCompression : NoCompression; +} + +void KZip::setExtraField(ExtraField ef) +{ + d->m_extraField = ef; +} + +KZip::ExtraField KZip::extraField() const +{ + return d->m_extraField; +} + +//////////////////////////////////////////////////////////////////////// +////////////////////// KZipFileEntry//////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +class Q_DECL_HIDDEN KZipFileEntry::KZipFileEntryPrivate +{ +public: + KZipFileEntryPrivate() + : crc(0) + , compressedSize(0) + , headerStart(0) + , encoding(0) + { + } + unsigned long crc; + qint64 compressedSize; + qint64 headerStart; + int encoding; + QString path; +}; + +KZipFileEntry::KZipFileEntry(KZip *zip, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, const QString &symlink, + const QString &path, qint64 start, qint64 uncompressedSize, + int encoding, qint64 compressedSize) + : KArchiveFile(zip, name, access, date, user, group, symlink, start, uncompressedSize) + , d(new KZipFileEntryPrivate) +{ + d->path = path; + d->encoding = encoding; + d->compressedSize = compressedSize; +} + +KZipFileEntry::~KZipFileEntry() +{ + delete d; +} + +int KZipFileEntry::encoding() const +{ + return d->encoding; +} + +qint64 KZipFileEntry::compressedSize() const +{ + return d->compressedSize; +} + +void KZipFileEntry::setCompressedSize(qint64 compressedSize) +{ + d->compressedSize = compressedSize; +} + +void KZipFileEntry::setHeaderStart(qint64 headerstart) +{ + d->headerStart = headerstart; +} + +qint64 KZipFileEntry::headerStart() const +{ + return d->headerStart; +} + +unsigned long KZipFileEntry::crc32() const +{ + return d->crc; +} + +void KZipFileEntry::setCRC32(unsigned long crc32) +{ + d->crc = crc32; +} + +const QString &KZipFileEntry::path() const +{ + return d->path; +} + +QByteArray KZipFileEntry::data() const +{ + QIODevice *dev = createDevice(); + QByteArray arr; + if (dev) { + arr = dev->readAll(); + delete dev; + } + return arr; +} + +QIODevice *KZipFileEntry::createDevice() const +{ + //qCDebug(KArchiveLog) << "creating iodevice limited to pos=" << position() << ", csize=" << compressedSize(); + // Limit the reading to the appropriate part of the underlying device (e.g. file) + KLimitedIODevice *limitedDev = new KLimitedIODevice(archive()->device(), position(), compressedSize()); + if (encoding() == 0 || compressedSize() == 0) { // no compression (or even no data) + return limitedDev; + } + + if (encoding() == 8) { + // On top of that, create a device that uncompresses the zlib data + KCompressionDevice *filterDev = new KCompressionDevice(limitedDev, true, KCompressionDevice::GZip); + + if (!filterDev) { + return nullptr; // ouch + } + filterDev->setSkipHeaders(); // Just zlib, not gzip + bool b = filterDev->open(QIODevice::ReadOnly); + Q_UNUSED(b); + Q_ASSERT(b); + return filterDev; + } + + qCCritical(KArchiveLog) << "This zip file contains files compressed with method" + << encoding() << ", this method is currently not supported by KZip," + << "please use a command-line tool to handle this file."; + delete limitedDev; + return nullptr; +} diff --git a/src/karchive/src/kzip.h b/src/karchive/src/kzip.h new file mode 100644 index 0000000000..85566040aa --- /dev/null +++ b/src/karchive/src/kzip.h @@ -0,0 +1,160 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2002 Holger Schroeder + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef KZIP_H +#define KZIP_H + +#include + +#include "kzipfileentry.h" // for source compat + +class KZipFileEntry; +/** + * @class KZip zip.h KZip + * + * A class for reading / writing zip archives. + * + * You can use it in QIODevice::ReadOnly or in QIODevice::WriteOnly mode, and it + * behaves just as expected. + * It can also be used in QIODevice::ReadWrite mode, in this case one can + * append files to an existing zip archive. When you append new files, which + * are not yet in the zip, it works as expected, i.e. the files are appended at the end. + * When you append a file, which is already in the file, the reference to the + * old file is dropped and the new one is added to the zip - but the + * old data from the file itself is not deleted, it is still in the + * zipfile. So when you want to have a small and garbage-free zipfile, + * just read the contents of the appended zip file and write it to a new one + * in QIODevice::WriteOnly mode. This is especially important when you don't want + * to leak information of how intermediate versions of files in the zip + * were looking. + * + * For more information on the zip fileformat go to + * http://www.pkware.com/products/enterprise/white_papers/appnote.html + * @author Holger Schroeder + */ +class KARCHIVE_EXPORT KZip : public KArchive +{ + Q_DECLARE_TR_FUNCTIONS(KZip) + +public: + /** + * Creates an instance that operates on the given filename. + * using the compression filter associated to given mimetype. + * + * @param filename is a local path (e.g. "/home/holger/myfile.zip") + */ + KZip(const QString &filename); + + /** + * Creates an instance that operates on the given device. + * The device can be compressed (KFilterDev) or not (QFile, etc.). + * @warning Do not assume that giving a QFile here will decompress the file, + * in case it's compressed! + * @param dev the device to access + */ + KZip(QIODevice *dev); + + /** + * If the zip file is still opened, then it will be + * closed automatically by the destructor. + */ + virtual ~KZip(); + + /** + * Describes the contents of the "extra field" for a given file in the Zip archive. + */ + enum ExtraField { + NoExtraField = 0, ///< No extra field + ModificationTime = 1, ///< Modification time ("extended timestamp" header) + DefaultExtraField = 1 // alias of ModificationTime + }; + + /** + * Call this before writeFile or prepareWriting, to define what the next + * file to be written should have in its extra field. + * @param ef the type of "extra field" + * @see extraField() + */ + void setExtraField(ExtraField ef); + + /** + * The current type of "extra field" that will be used for new files. + * @return the current type of "extra field" + * @see setExtraField() + */ + ExtraField extraField() const; + + /** + * Describes the compression type for a given file in the Zip archive. + */ + enum Compression { + NoCompression = 0, ///< Uncompressed. + DeflateCompression = 1 ///< Deflate compression method. + }; + + /** + * Call this before writeFile or prepareWriting, to define whether the next + * files to be written should be compressed or not. + * @param c the new compression mode + * @see compression() + */ + void setCompression(Compression c); + + /** + * The current compression mode that will be used for new files. + * @return the current compression mode + * @see setCompression() + */ + Compression compression() const; + + /** + * Write data to a file that has been created using prepareWriting(). + * @param data a pointer to the data + * @param size the size of the chunk + * @return true if successful, false otherwise + */ + bool writeData(const char *data, qint64 size) override; + +protected: + /// Reimplemented from KArchive + bool doWriteSymLink(const QString &name, const QString &target, + const QString &user, const QString &group, + mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + /// Reimplemented from KArchive + bool doPrepareWriting(const QString &name, const QString &user, + const QString &group, qint64 size, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &creationTime) override; + + /** + * Write data to a file that has been created using prepareWriting(). + * @param size the size of the file + * @return true if successful, false otherwise + */ + bool doFinishWriting(qint64 size) override; + + /** + * Opens the archive for reading. + * Parses the directory listing of the archive + * and creates the KArchiveDirectory/KArchiveFile entries. + * @param mode the mode of the file + */ + bool openArchive(QIODevice::OpenMode mode) override; + + /// Closes the archive + bool closeArchive() override; + + /// Reimplemented from KArchive + bool doWriteDir(const QString &name, const QString &user, const QString &group, mode_t perm, + const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override; + +protected: + void virtual_hook(int id, void *data) override; + +private: + class KZipPrivate; + KZipPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/kzipfileentry.h b/src/karchive/src/kzipfileentry.h new file mode 100644 index 0000000000..d02d34a675 --- /dev/null +++ b/src/karchive/src/kzipfileentry.h @@ -0,0 +1,71 @@ +/* This file is part of the KDE libraries + SPDX-FileCopyrightText: 2002 Holger Schroeder + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KZIPFILEENTRY_H +#define KZIPFILEENTRY_H + +#include "karchive.h" + +class KZip; +/** + * @class KZipFileEntry kzipfileentry.h KZipFileEntry + * + * A KZipFileEntry represents a file in a zip archive. + */ +class KARCHIVE_EXPORT KZipFileEntry : public KArchiveFile +{ +public: + /** + * Creates a new zip file entry. Do not call this, KZip takes care of it. + */ + KZipFileEntry(KZip *zip, const QString &name, int access, const QDateTime &date, + const QString &user, const QString &group, const QString &symlink, + const QString &path, qint64 start, qint64 uncompressedSize, + int encoding, qint64 compressedSize); + + /** + * Destructor. Do not call this. + */ + ~KZipFileEntry(); + + int encoding() const; + qint64 compressedSize() const; + + /// Only used when writing + void setCompressedSize(qint64 compressedSize); + + /// Header start: only used when writing + void setHeaderStart(qint64 headerstart); + qint64 headerStart() const; + + /// CRC: only used when writing + unsigned long crc32() const; + void setCRC32(unsigned long crc32); + + /// Name with complete path - KArchiveFile::name() is the filename only (no path) + const QString &path() const; + + /** + * @return the content of this file. + * Call data() with care (only once per file), this data isn't cached. + */ + QByteArray data() const override; + + /** + * This method returns a QIODevice to read the file contents. + * This is obviously for reading only. + * Note that the ownership of the device is being transferred to the caller, + * who will have to delete it. + * The returned device auto-opens (in readonly mode), no need to open it. + */ + QIODevice *createDevice() const override; + +private: + class KZipFileEntryPrivate; + KZipFileEntryPrivate *const d; +}; + +#endif diff --git a/src/karchive/src/loggingcategory.cpp b/src/karchive/src/loggingcategory.cpp new file mode 100644 index 0000000000..c9c0b066e3 --- /dev/null +++ b/src/karchive/src/loggingcategory.cpp @@ -0,0 +1,11 @@ +// This file was generated by ecm_qt_declare_logging_category(): DO NOT EDIT! + +#include "loggingcategory.h" + + +#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) +Q_LOGGING_CATEGORY(KArchiveLog, "kf.archive", QtWarningMsg) +#else +Q_LOGGING_CATEGORY(KArchiveLog, "kf.archive") +#endif + diff --git a/src/karchive/src/loggingcategory.h b/src/karchive/src/loggingcategory.h new file mode 100644 index 0000000000..43325aa7a2 --- /dev/null +++ b/src/karchive/src/loggingcategory.h @@ -0,0 +1,11 @@ +// This file was generated by ecm_qt_declare_logging_category(): DO NOT EDIT! + +#ifndef ECM_QLOGGINGCATEGORY_KARCHIVELOG_LOGGINGCATEGORY_H +#define ECM_QLOGGINGCATEGORY_KARCHIVELOG_LOGGINGCATEGORY_H + +#include + +Q_DECLARE_LOGGING_CATEGORY(KArchiveLog) + + +#endif diff --git a/src/plugins/plugins.qbs b/src/plugins/plugins.qbs index 59d0296866..d10ccae73a 100644 --- a/src/plugins/plugins.qbs +++ b/src/plugins/plugins.qbs @@ -13,6 +13,7 @@ Project { "lua", "python", "replicaisland", + "rpmap", "tbin", "tengine" ] diff --git a/src/plugins/rpmap/plugin.json b/src/plugins/rpmap/plugin.json new file mode 100644 index 0000000000..5e0f4eb8f6 --- /dev/null +++ b/src/plugins/rpmap/plugin.json @@ -0,0 +1 @@ +{ "defaultEnable": false } diff --git a/src/plugins/rpmap/rpmap.qbs b/src/plugins/rpmap/rpmap.qbs new file mode 100644 index 0000000000..2aa38e719f --- /dev/null +++ b/src/plugins/rpmap/rpmap.qbs @@ -0,0 +1,18 @@ +import qbs 1.0 + +TiledPlugin { + condition: (Qt.core.versionMajor > 5 || Qt.core.versionMinor >= 12) && !qbs.toolchain.contains("msvc") + + Depends { name: "Qt.core" } + Depends { name: "karchive" } + + cpp.defines: base.concat(["RPMAP_LIBRARY"]) + cpp.dynamicLibraries: base.concat(["z"]) + + files: [ + "rpmap_global.h", + "rpmapplugin.cpp", + "rpmapplugin.h", + "plugin.json", + ] +} diff --git a/src/plugins/rpmap/rpmap_global.h b/src/plugins/rpmap/rpmap_global.h new file mode 100644 index 0000000000..9c7e45109e --- /dev/null +++ b/src/plugins/rpmap/rpmap_global.h @@ -0,0 +1,29 @@ +/* + * RpMap Tiled Plugin + * Copyright 2020, Christof Petig + * + * This file is part of Tiled. + * + * 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 2 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 . + */ + +#pragma once + +#include + +#if defined(RPMAP_LIBRARY) +# define RPMAPSHARED_EXPORT Q_DECL_EXPORT +#else +# define RPMAPSHARED_EXPORT Q_DECL_IMPORT +#endif diff --git a/src/plugins/rpmap/rpmapplugin.cpp b/src/plugins/rpmap/rpmapplugin.cpp new file mode 100644 index 0000000000..2a28b1815b --- /dev/null +++ b/src/plugins/rpmap/rpmapplugin.cpp @@ -0,0 +1,450 @@ +/* + * RpMap Tiled Plugin + * Copyright 2020, Christof Petig + * + * This file is part of Tiled. + * + * 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 2 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 . + */ + +#include "rpmapplugin.h" + +#include "gidmapper.h" +#include "map.h" +#include "mapobject.h" +#include "savefile.h" +#include "tile.h" +#include "tiled.h" +#include "tilelayer.h" +#include "tileset.h" +#include "objectgroup.h" + +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) +#include +#endif +#include +#include +#include +#include +#include + +#include + +using namespace Tiled; + +namespace RpMap { + +RpMapPlugin::RpMapPlugin() +{ +} + +#if 0 // not implemented for now +std::unique_ptr RpMapPlugin::read(const QString &fileName) +{ + // the problem is to either reference an already loaded tileset (how) or to import the included image resources + KZip archive(fileName); + if (archive.open(QIODevice::ReadOnly)) { + const KArchiveDirectory *dir = archive.directory(); + + const KArchiveEntry *e = dir->entry("content.xml"); + if (!e) { + //qDebug() << "File not found!"; + return nullptr; + } + const KArchiveFile *f = static_cast(e); + QByteArray arr(f->data()); + //qDebug() << arr; // the file contents + + // To avoid reading everything into memory in one go, we can use createDevice() instead +#if 0 + QIODevice *dev = f->createDevice(); + while (!dev->atEnd()) { + qDebug() << dev->readLine(); + } + delete dev; +#endif + } + return nullptr; +} + +bool RpMapPlugin::supportsFile(const QString &fileName) const +{ + return fileName.endsWith(QLatin1String(".rpmap"), Qt::CaseInsensitive); +} +#endif + +QString RpMapPlugin::nameFilter() const +{ + return tr("RpTool MapTool files (*.rpmap)"); +} + +QString RpMapPlugin::shortName() const +{ + return QStringLiteral("rpmap"); +} + +QString RpMapPlugin::errorString() const +{ + return mError; +} + +static void writeEntry(QXmlStreamWriter &writer, QString const &key, QString const& value) +{ + writer.writeStartElement(QStringLiteral("entry")); + writer.writeTextElement(QStringLiteral("string"), key); + writer.writeTextElement(QStringLiteral("string"), value); + writer.writeEndElement(); +} + +static void writeGUID(QXmlStreamWriter &writer, QString const &key, QUuid const& id) +{ + writer.writeStartElement(key); + writer.writeTextElement(QStringLiteral("baGUID"), id.toRfc4122().toBase64()); + writer.writeEndElement(); +} + +static void writeTile(QXmlStreamWriter &writer, int x, int y, QString const& name, int facing, QString const& md5, bool flipx, bool flipy) +{ + writer.writeStartElement(QStringLiteral("entry")); + writeGUID(writer, QStringLiteral("net.rptools.maptool.model.GUID"), QUuid::createUuid()); + writer.writeStartElement(QStringLiteral("net.rptools.maptool.model.Token")); + writer.writeStartElement(QStringLiteral("id")); writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../net.rptools.maptool.model.GUID")); writer.writeEndElement(); + //writeGUID(writer, QStringLiteral("exposedAreaGUID"), QUuid::createUuid()); + writer.writeStartElement(QStringLiteral("imageAssetMap")); + writer.writeStartElement(QStringLiteral("entry")); + writer.writeEmptyElement(QStringLiteral("null")); + writer.writeStartElement(QStringLiteral("net.rptools.lib.MD5Key")); + writer.writeTextElement(QStringLiteral("id"), md5); + writer.writeEndElement(); // MD5Key + writer.writeEndElement(); // entry + writer.writeEndElement(); // imageAssetMap + writer.writeTextElement(QStringLiteral("x"), QString::number(x)); + writer.writeTextElement(QStringLiteral("y"), QString::number(y)); + writer.writeTextElement(QStringLiteral("z"), QString::number(1)); + writer.writeTextElement(QStringLiteral("anchorX"), QString::number(0)); + writer.writeTextElement(QStringLiteral("anchorY"), QString::number(0)); + writer.writeTextElement(QStringLiteral("snapToScale"), QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("width"), QString::number(300)); + writer.writeTextElement(QStringLiteral("height"), QString::number(300)); + writer.writeTextElement(QStringLiteral("isoWidth"), QString::number(0)); + writer.writeTextElement(QStringLiteral("isoHeight"), QString::number(0)); + writer.writeTextElement(QStringLiteral("scaleX"), QString::number(1.0)); + writer.writeTextElement(QStringLiteral("scaleY"), QString::number(1.0)); + + writer.writeStartElement(QStringLiteral("sizeMap")); + writer.writeStartElement(QStringLiteral("entry")); + writer.writeTextElement(QStringLiteral("java-class"), QStringLiteral("net.rptools.maptool.model.SquareGrid")); + writeGUID(writer, QStringLiteral("net.rptools.maptool.model.GUID"), QUuid::createUuid()); + writer.writeEndElement(); // entry + writer.writeEndElement(); // sizeMap + + writer.writeTextElement(QStringLiteral("snapToGrid"), QStringLiteral("true")); + writer.writeTextElement(QStringLiteral("isVisible"), QStringLiteral("true")); + writer.writeTextElement(QStringLiteral("visibleOnlyToOwner"), QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("vblColorSensitivity"), QString::number(-1)); + writer.writeTextElement(QStringLiteral("alwaysVisibleTolerance"), QString::number(2)); + writer.writeTextElement(QStringLiteral("isAlwaysVisible"), QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("name"), name); + writer.writeTextElement(QStringLiteral("ownerType"), QString::number(0)); + writer.writeTextElement(QStringLiteral("tokenShape"), QStringLiteral("TOP_DOWN")); + writer.writeTextElement(QStringLiteral("tokenType"), QStringLiteral("NPC")); + writer.writeTextElement(QStringLiteral("layer"), QStringLiteral("BACKGROUND")); + writer.writeTextElement(QStringLiteral("propertyType"), QStringLiteral("Basic")); + writer.writeTextElement(QStringLiteral("facing"), QString::number(facing)); + writer.writeTextElement(QStringLiteral("tokenOpacity"), QString::number(1.0)); + writer.writeTextElement(QStringLiteral("terrainModifier"), QString::number(0.0)); + writer.writeTextElement(QStringLiteral("terrainModifierOperation"), QStringLiteral("NONE")); + writer.writeStartElement(QStringLiteral("terrainModifiersIgnored")); + writer.writeTextElement(QStringLiteral("net.rptools.maptool.model.Token_-TerrainModifierOperation"), QStringLiteral("NONE")); + writer.writeEndElement(); // terrainModifiersIgnored + writer.writeTextElement(QStringLiteral("isFlippedX"), flipx ? QStringLiteral("true") : QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("isFlippedY"), flipy ? QStringLiteral("true") : QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("sightType"), QStringLiteral("Normal")); + writer.writeTextElement(QStringLiteral("hasSight"), QStringLiteral("false")); + writer.writeEmptyElement(QStringLiteral("state")); + writer.writeStartElement(QStringLiteral("propertyMapCI")); + writer.writeEmptyElement(QStringLiteral("store")); + writer.writeEndElement(); // propertyMapCI + writer.writeEmptyElement(QStringLiteral("macroPropertiesMap")); + writer.writeEmptyElement(QStringLiteral("speechMap")); + + writer.writeEndElement(); // net.rptools.maptool.model.Token + writer.writeEndElement(); // entry +} + +static void writeCellShape(QXmlStreamWriter &writer, Tiled::Map const* map) +{ + writer.writeStartElement(QStringLiteral("cellShape")); + writer.writeStartElement(QStringLiteral("curves")); + writer.writeStartElement(QStringLiteral("sun.awt.geom.Order0")); + writer.writeTextElement(QStringLiteral("direction"), QStringLiteral("1")); + writer.writeTextElement(QStringLiteral("x"), QStringLiteral("0.0")); + writer.writeTextElement(QStringLiteral("y"), QStringLiteral("0.0")); + writer.writeEndElement(); // Order0 + writer.writeStartElement(QStringLiteral("sun.awt.geom.Order1")); + writer.writeTextElement(QStringLiteral("direction"), QStringLiteral("1")); + writer.writeTextElement(QStringLiteral("x0"), QStringLiteral("0.0")); + writer.writeTextElement(QStringLiteral("y0"), QStringLiteral("0.0")); + writer.writeTextElement(QStringLiteral("x1"), QStringLiteral("0.0")); + writer.writeTextElement(QStringLiteral("y1"), QString::number(map->tileHeight())); + writer.writeTextElement(QStringLiteral("xmin"), QStringLiteral("0.0")); + writer.writeTextElement(QStringLiteral("xmax"), QStringLiteral("0.0")); + writer.writeEndElement(); // Order1 + writer.writeStartElement(QStringLiteral("sun.awt.geom.Order1")); + writer.writeTextElement(QStringLiteral("direction"), QStringLiteral("-1")); + writer.writeTextElement(QStringLiteral("x0"), QString::number(map->tileWidth())); + writer.writeTextElement(QStringLiteral("y0"), QStringLiteral("0.0")); + writer.writeTextElement(QStringLiteral("x1"), QString::number(map->tileWidth())); + writer.writeTextElement(QStringLiteral("y1"), QString::number(map->tileHeight())); + writer.writeTextElement(QStringLiteral("xmin"), QString::number(map->tileWidth())); + writer.writeTextElement(QStringLiteral("xmax"), QString::number(map->tileWidth())); + writer.writeEndElement(); // Order1 + writer.writeEndElement(); // curves + writer.writeEndElement(); // cellShape +} + +static void writeGrid(QXmlStreamWriter &writer, Tiled::Map const* map) +{ + writer.writeStartElement(QStringLiteral("grid")); + writer.writeAttribute(QStringLiteral("class"), QStringLiteral("net.rptools.maptool.model.SquareGrid")); + writer.writeTextElement(QStringLiteral("offsetX"), QString::number(0)); + writer.writeTextElement(QStringLiteral("offsetY"), QString::number(0)); + writer.writeTextElement(QStringLiteral("size"), QString::number(std::max(map->tileWidth(),map->tileHeight()))); + writer.writeStartElement(QStringLiteral("zone")); writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../..")); writer.writeEndElement(); + writeCellShape(writer, map); + writer.writeEndElement(); // grid +} + +static void writeClass(QXmlStreamWriter &writer, QString const &name, QString const& type) +{ + writer.writeStartElement(name); + writer.writeAttribute(QStringLiteral("class"), type); + writer.writeEndElement(); +} + +void RpMapPlugin::writeTokenMap(QXmlStreamWriter &writer, Tiled::Map const* map) +{ + const int tileWidth = map->tileWidth(); + const int tileHeight = map->tileHeight(); +// const QColor backgroundColor = map->backgroundColor(); + writer.writeStartElement(QStringLiteral("tokenMap")); + + for (Layer *layer : map->layers()) { + if (TileLayer *tileLayer = layer->asTileLayer()) { + for (int y = 0; y < tileLayer->height(); ++y) { + for (int x = 0; x < tileLayer->width(); ++x) { + Cell t = tileLayer->cellAt(x, y); + if (t.isEmpty()) + continue; + + static const uint16_t rotation[8] = { 270, 270, 270, 90, 0, 0, 180, 180 }; + // in addition to rotation + static const bool flip_horiz[8] = { false, false, true, false, true, false, false, true }; + static const bool flip_vert[8] = { false, true, false, false, false, false, false, false }; + + uint8_t rot_index = (t.flippedVertically() ? 1 : 0) | (t.flippedHorizontally() ? 2 : 0) | (t.flippedAntiDiagonally() ? 4 : 0); + //int tileid= t.tileId(); + Tile const* tile = t.tile(); + QUrl tileurl = tile->imageSource(); + if (tileurl.isLocalFile()) { + QString tilepath = tileurl.toLocalFile(); + auto it = filename2md5.find(tilepath); + if (it == filename2md5.end()) { + QFile file(tilepath); + if (file.open(QIODevice::ReadOnly)) { + QByteArray image = file.readAll(); + QByteArray md5bin = QCryptographicHash::hash(image, QCryptographicHash::Md5); + QString md5string = md5bin.toHex(); + it = filename2md5.insert(tilepath, md5string); + // remember the first element (tile) referencing this file + first_used_md5.push_back(number_of_tiles); + } + else + continue; + } + assert(it != filename2md5.end()); + QString md5 = it.value(); + writeTile(writer, x*tileWidth, y*tileHeight, QStringLiteral("token"), rotation[rot_index], md5, flip_horiz[rot_index], flip_vert[rot_index]); + ++number_of_tiles; + } + } + } + break; // only output first layer (for now) + } + } + //writeTile(writer, 400, 300, QStringLiteral("token"), 180); + //writeTile(writer, 400, 600, QStringLiteral("token2"), 0); + + writer.writeEndElement(); // tokenMap +} + +void RpMapPlugin::writeTokenOrderedList(QXmlStreamWriter &writer) +{ + writer.writeStartElement(QStringLiteral("tokenOrderedList")); + writer.writeAttribute(QStringLiteral("class"), QStringLiteral("linked-list")); + + writer.writeStartElement(QStringLiteral("net.rptools.maptool.model.Token")); + writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../tokenMap/entry/net.rptools.maptool.model.Token")); + writer.writeEndElement(); // Token + for (uint32_t i = 1; i < number_of_tiles; ++i) { + writer.writeStartElement(QStringLiteral("net.rptools.maptool.model.Token")); + writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../tokenMap/entry[") + +QString::number(i+1) + +QStringLiteral("]/net.rptools.maptool.model.Token")); + writer.writeEndElement(); // Token + } + + writer.writeEndElement(); // tokenOrderedList +} + +static void writeZone2(QXmlStreamWriter &writer) +{ + writer.writeStartElement(QStringLiteral("initiativeList")); + writer.writeEmptyElement(QStringLiteral("tokens")); + writer.writeTextElement(QStringLiteral("current"), QString::number(-1)); + writer.writeTextElement(QStringLiteral("round"), QString::number(-1)); + writer.writeStartElement(QStringLiteral("zoneId")); writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../id")); writer.writeEndElement(); + writer.writeTextElement(QStringLiteral("fullUpdate"), QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("hideNPC"), QStringLiteral("false")); + writer.writeEndElement(); // initiativeList + + writer.writeStartElement(QStringLiteral("exposedArea")); + writer.writeEmptyElement(QStringLiteral("curves")); + writer.writeEndElement(); // exposedArea + writer.writeTextElement(QStringLiteral("hasFog"), QStringLiteral("false")); + + writer.writeStartElement(QStringLiteral("fogPaint")); + writer.writeAttribute(QStringLiteral("class"), QStringLiteral("net.rptools.maptool.model.drawing.DrawableColorPaint")); + writer.writeTextElement(QStringLiteral("color"), QString::number(-16777216)); + writer.writeEndElement(); // fogPaint + + writer.writeStartElement(QStringLiteral("topology")); + writer.writeStartElement(QStringLiteral("curves")); + writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../exposedArea/curves")); + writer.writeEndElement(); // curves + writer.writeEndElement(); // topology + writer.writeStartElement(QStringLiteral("topologyTerrain")); + writer.writeStartElement(QStringLiteral("curves")); + writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../exposedArea/curves")); + writer.writeEndElement(); // curves + writer.writeEndElement(); // topologyTerrain + + writer.writeStartElement(QStringLiteral("backgroundPaint")); + writer.writeAttribute(QStringLiteral("class"), QStringLiteral("net.rptools.maptool.model.drawing.DrawableColorPaint")); + writer.writeTextElement(QStringLiteral("color"), QString::number(-16777216)); + writer.writeEndElement(); // backgroundPaint + + writer.writeStartElement(QStringLiteral("boardPosition")); + writer.writeTextElement(QStringLiteral("x"), QString::number(0)); + writer.writeTextElement(QStringLiteral("y"), QString::number(0)); + writer.writeEndElement(); // boardPosition + + writer.writeTextElement(QStringLiteral("drawBoard"), QStringLiteral("true")); + writer.writeTextElement(QStringLiteral("boardChanged"), QStringLiteral("false")); + writer.writeTextElement(QStringLiteral("name"), QStringLiteral("Tiled export")); + writer.writeTextElement(QStringLiteral("isVisible"), QStringLiteral("true")); + writer.writeTextElement(QStringLiteral("visionType"), QStringLiteral("OFF")); + writer.writeTextElement(QStringLiteral("tokenSelection"), QStringLiteral("ALL")); + writer.writeTextElement(QStringLiteral("height"), QString::number(0)); + writer.writeTextElement(QStringLiteral("width"), QString::number(0)); +} + +void RpMapPlugin::writeMap(QXmlStreamWriter &writer, Tiled::Map const* map) +{ + writer.writeStartElement(QStringLiteral("zone")); + writer.writeTextElement(QStringLiteral("creationTime"), QString::number(QDateTime::currentMSecsSinceEpoch())); + writeGUID(writer, QStringLiteral("id"), QUuid::createUuid()); + writeGrid(writer, map); + writer.writeTextElement(QStringLiteral("gridColor"), QString::number(-16777216)); + writer.writeTextElement(QStringLiteral("imageScaleX"), QString::number(1.0)); + writer.writeTextElement(QStringLiteral("imageScaleY"), QString::number(1.0)); + writer.writeTextElement(QStringLiteral("tokenVisionDistance"), QString::number(1000)); + writer.writeTextElement(QStringLiteral("unitsPerCell"), QString::number(5.0)); + writer.writeTextElement(QStringLiteral("aStarRounding"), QStringLiteral("NONE")); + writer.writeTextElement(QStringLiteral("topologyMode"), QStringLiteral("COMBINED")); + writeClass(writer, QStringLiteral("drawables"), QStringLiteral("linked-list")); + writeClass(writer, QStringLiteral("gmDrawables"), QStringLiteral("linked-list")); + writeClass(writer, QStringLiteral("objectDrawables"), QStringLiteral("linked-list")); + writeClass(writer, QStringLiteral("backgroundDrawables"), QStringLiteral("linked-list")); + writeClass(writer, QStringLiteral("labels"), QStringLiteral("linked-hash-map")); + writeTokenMap(writer, map); + writer.writeStartElement(QStringLiteral("exposedAreaMeta")); + writer.writeEndElement(); // exposedAreaMeta + writeTokenOrderedList(writer); + writeZone2(writer); // some arbitrary data + writer.writeEndElement(); // zone + + writer.writeStartElement(QStringLiteral("assetMap")); + for (auto i: first_used_md5) { + writer.writeStartElement(QStringLiteral("entry")); + writer.writeStartElement(QStringLiteral("net.rptools.lib.MD5Key")); + QString item; + if (i > 0) + item = QStringLiteral("[") + QString::number(i+1) + QStringLiteral("]"); + writer.writeAttribute(QStringLiteral("reference"), QStringLiteral("../../../zone/tokenMap/entry") + +item + +QStringLiteral("/net.rptools.maptool.model.Token/imageAssetMap/entry/net.rptools.lib.MD5Key")); + writer.writeEndElement(); // net.rptools.lib.MD5Key + writer.writeEmptyElement(QStringLiteral("null")); + writer.writeEndElement(); // entry + } + writer.writeEndElement(); // assetMap +} + +bool RpMapPlugin::write(const Tiled::Map *map, const QString &fileName, Options options) +{ + filename2md5.clear(); + first_used_md5.clear(); + number_of_tiles = 0; + + Q_UNUSED(options) + KZip archive(fileName); + if (archive.open(QIODevice::WriteOnly)) + { + { + QByteArray properties; + QXmlStreamWriter writer(&properties); + writer.setAutoFormatting(true); + writer.setAutoFormattingIndent(1); + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("map")); + writeEntry(writer, QStringLiteral("campaignVersion"), QStringLiteral("1.4.1")); + writeEntry(writer, QStringLiteral("version"), QStringLiteral("1.7.0")); + writer.writeEndElement(); + writer.writeEndDocument(); + archive.writeFile(QStringLiteral("properties.xml"), properties); + } + { + QByteArray content; + QXmlStreamWriter writer(&content); + writer.setAutoFormatting(true); + writer.setAutoFormattingIndent(1); + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("net.rptools.maptool.util.PersistenceUtil_-PersistedMap")); + writeMap(writer, map); + writer.writeEndElement(); // PersistedMap + writer.writeEndDocument(); + archive.writeFile(QStringLiteral("content.xml"), content); + } + archive.close(); + return true; + } + return false; +} + +} // namespace RpMap diff --git a/src/plugins/rpmap/rpmapplugin.h b/src/plugins/rpmap/rpmapplugin.h new file mode 100644 index 0000000000..2a52df7eca --- /dev/null +++ b/src/plugins/rpmap/rpmapplugin.h @@ -0,0 +1,64 @@ +/* + * RpMap Tiled Plugin + * Copyright 2020, Christof Petig + * + * This file is part of Tiled. + * + * 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 2 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 . + */ + +#pragma once + +#include "rpmap_global.h" + +#include "mapformat.h" + +#include +#include +#include + +namespace RpMap { + +class RPMAPSHARED_EXPORT RpMapPlugin : public Tiled::WritableMapFormat +{ + Q_OBJECT + Q_INTERFACES(Tiled::MapFormat) + Q_PLUGIN_METADATA(IID "org.mapeditor.MapFormat" FILE "plugin.json") + +public: + RpMapPlugin(); + +#if 0 + std::unique_ptr read(const QString &fileName) override; + bool supportsFile(const QString &fileName) const override; +#endif + + bool write(const Tiled::Map *map, const QString &fileName, Options options) override; + QString nameFilter() const override; + QString shortName() const override; + QString errorString() const override; + +private: + QString mError; + + QMap filename2md5; + QVector first_used_md5; + uint32_t number_of_tiles; + + void writeTokenMap(QXmlStreamWriter &writer, Tiled::Map const* map); + void writeTokenOrderedList(QXmlStreamWriter &writer); + void writeMap(QXmlStreamWriter &writer, Tiled::Map const* map); +}; + +} // namespace RpMap diff --git a/tiled.qbs b/tiled.qbs index 2a6b447f57..a376ac302b 100644 --- a/tiled.qbs +++ b/tiled.qbs @@ -23,6 +23,7 @@ Project { "docs", "src/libtiled", "src/libtiledquick", + "src/karchive", "src/plugins", "src/qtpropertybrowser", "src/qtsingleapplication",