diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp index a5b5f3b4..c0f2e21d 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -1,117 +1,117 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * 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 AUTHOR ``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 AUTHOR 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. */ #include "jsonarchiveinterface.h" #include #include "kerfuffle/archiveentry.h" JSONArchiveInterface::JSONArchiveInterface(QObject *parent, const QVariantList& args) : Kerfuffle::ReadWriteArchiveInterface(parent, args) { } JSONArchiveInterface::~JSONArchiveInterface() { } bool JSONArchiveInterface::list() { JSONParser::JSONArchive::const_iterator it = m_archive.constBegin(); for (; it != m_archive.constEnd(); ++it) { emit entry(*it); } return true; } bool JSONArchiveInterface::open() { QFile file(filename()); if (!file.exists()) { return false; } if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } m_archive = JSONParser::parse(&file); return !m_archive.isEmpty(); } bool JSONArchiveInterface::addFiles(const QList &files, const Kerfuffle::CompressionOptions& options) { Q_UNUSED(options) foreach (const Kerfuffle::Archive::Entry *entry, files) { const QString &path = entry->property("fullPath").toString(); if (m_archive.contains(path)) { return false; } - Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(NULL); + Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); e->setProperty("fullPath", path); m_archive[path] = e; } return true; } bool JSONArchiveInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(destinationDirectory) Q_UNUSED(options) return true; } bool JSONArchiveInterface::deleteFiles(const QList& files) { foreach (const Kerfuffle::Archive::Entry *file, files) { const QString &fileName = file->property("fullPath").toString(); if (m_archive.contains(fileName)) { m_archive.remove(fileName); emit entryRemoved(fileName); } } return true; } bool JSONArchiveInterface::addComment(const QString& comment) { Q_UNUSED(comment) return true; } bool JSONArchiveInterface::testArchive() { return true; } diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 5a1c5636..b56ec415 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,514 +1,514 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isMultiVolume(false) , m_isSolid(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_comment.clear(); } // #272281: the proprietary unrar program does not like trailing '/'s // in directories passed to it when extracting only part of // the files in an archive. QString CliPlugin::escapeFileName(const QString &fileName) const { if (fileName.endsWith(QLatin1Char('/'))) { return fileName.left(fileName.length() - 1); } return fileName; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral( "unrar" ); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral( "rar" ); p[ListArgs] = QStringList() << QStringLiteral("vt") << QStringLiteral("-v") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral( "-kb" ) << QStringLiteral( "-p-" ) << QStringLiteral( "$PreservePathSwitch" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[PreservePathSwitch] = QStringList() << QStringLiteral( "x" ) << QStringLiteral( "e" ); p[PasswordSwitch] = QStringList() << QStringLiteral( "-p$Password" ); p[PasswordHeaderSwitch] = QStringList() << QStringLiteral("-hp$Password"); p[CompressionLevelSwitch] = QStringLiteral("-m$CompressionLevel"); p[DeleteArgs] = QStringList() << QStringLiteral( "d" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[FileExistsExpression] = QStringList() << QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $"); p[FileExistsFileName] = QStringList() << QStringLiteral("^(.+) already exists. Overwrite it") // unrar 3 & 4 << QStringLiteral("^Would you like to replace the existing file (.+)$"); // unrar 5 p[FileExistsInput] = QStringList() << QStringLiteral( "Y" ) //overwrite << QStringLiteral( "N" ) //skip << QStringLiteral( "A" ) //overwrite all << QStringLiteral( "E" ) //autoskip << QStringLiteral( "Q" ); //cancel p[AddArgs] = QStringList() << QStringLiteral( "a" ) << QStringLiteral( "$Archive" ) << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral( "$Files" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("password incorrect") << QStringLiteral("wrong password"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral( "CRC failed" ) << QStringLiteral( "Cannot find volume" ); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("the file header is corrupt"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); p[CommentArgs] = QStringList() << QStringLiteral("c") << QStringLiteral("$CommentSwitch") << QStringLiteral("$Archive"); p[CommentSwitch] = QStringLiteral("-z$CommentFile"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive"); p[TestPassedPattern] = QStringLiteral("^All OK$"); } return p; } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; QString unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << unrarVersion << "detected"; if (unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { handleUnrar5Line(line); } else { handleUnrar4Line(line); } return true; } void CliPlugin::handleUnrar5Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^Archive: .+$")); if (rxCommentEnd.match(line).hasMatch()) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // "Details: " indicates end of header. if (line.startsWith(QStringLiteral("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume")) && !m_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } } return; } // Parses the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } return; } } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QStringLiteral("")); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } void CliPlugin::handleUnrar4Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume")) && !m_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // Horizontal line indicates end of header. if (line.startsWith(QStringLiteral("--------------------"))) { m_parseState = ParseStateEntryFileName; } return; } // Parses the entry name, which is on the first line of each entry. else if (m_parseState == ParseStateEntryFileName) { // Ignore empty lines. if (line.trimmed().isEmpty()) { return; } // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; return; } // Parses the remainder of the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); return; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return; } } void CliPlugin::handleUnrar4Entry() { - Archive::Entry *e = new Archive::Entry(NULL); + Archive::Entry *e = new Archive::Entry(Q_NULLPTR); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } #include "cliplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 406dd92e..e2db7ced 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,519 +1,519 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * * 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 AUTHOR ``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 AUTHOR 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. */ #include "libarchiveplugin.h" #include "kerfuffle/queries.h" #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_abortOperation(false) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; ArchiveRead arch_reader(archive_read_new()); if (!(arch_reader.data())) { return false; } if (archive_read_support_filter_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch_reader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(i18nc("@info", "Could not open the archive.")); return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(arch_reader.data(), 0); m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!m_abortOperation && (result = archive_read_next_header(arch_reader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(arch_reader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); m_cachedArchiveEntryCount++; archive_read_data_skip(arch_reader.data()); } m_abortOperation = false; if (result != ARCHIVE_EOF) { const QString errorString = QLatin1String(archive_error_string(arch_reader.data())); // FIXME: what about the other archive_error_string() calls? Do they also happen to return empty strings? emit error(errorString.isEmpty() ? i18nc("@info", "Could not read until the end of the archive") : errorString); return false; } return archive_read_close(arch_reader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QList &files, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QList &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString& comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::doKill() { m_abortOperation = true; return true; } bool LibarchivePlugin::copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); bool removeRootNode = options.value(QStringLiteral("RemoveRootNode"), QVariant()).toBool(); // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. QList remainingFiles = files; ArchiveRead arch(archive_read_new()); if (!(arch.data())) { return false; } if (archive_read_support_filter_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { // This error might be shown outside of a running Ark part (e.g. from a batch job). emit error(xi18nc("@info", "Could not open the archive %1." "Check whether you have sufficient permissions.", filename())); return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!m_abortOperation && (archive_read_next_header(arch.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(arch.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } Archive::Entry e(Q_NULLPTR); e.setFullPath(entryName); // Should the entry be extracted? if (extractAll || remainingFiles.contains(&e) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = files.indexOf(&e); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { const QString &rootNode = files.at(index)->rootNode; if (!rootNode.isEmpty()) { //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; const QString truncatedFilename(entryName.remove(0, rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(arch.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, arch.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(xi18nc("@info", "Extraction failed at:%1", entryName)); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } archive_entry_clear(entry); no_entries++; Archive::Entry e(Q_NULLPTR); e.setFullPath(entryName); remainingFiles.removeOne(&e); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(arch.data()); } } // While entries left to read in archive. m_abortOperation = false; qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(arch.data()) == ARCHIVE_OK; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { - Archive::Entry *e = new Archive::Entry(NULL); + Archive::Entry *e = new Archive::Entry(Q_NULLPTR); #ifdef _MSC_VER e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } e->setProperty("timestamp", QDateTime::fromTime_t(archive_entry_mtime(aentry))); emit entry(e); } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } #include "libarchiveplugin.moc"