diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index c785c857..8d8d0835 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1112 +1,1146 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } m_cliProps = new CliProperties(this, m_metaData, mimetype()); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } int CliInterface::copyRequiredSignals() const { return 2; } bool CliInterface::list() { resetParsing(); m_operationMode = List; m_numberOfEntries = 0; // To compute progress. m_archiveSizeOnDisk = static_cast(QFileInfo(filename()).size()); connect(this, &ReadOnlyArchiveInterface::entry, this, &CliInterface::onEntry); return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); } bool CliInterface::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << "destination directory:" << destinationDirectory; m_operationMode = Extract; m_extractionOptions = options; m_extractedFiles = files; m_extractDestDir = destinationDirectory; if (!m_cliProps->property("passwordSwitch").toStringList().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } QUrl destDir = QUrl(destinationDirectory); m_oldWorkingDirExtraction = QDir::currentPath(); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); const bool useTmpExtractDir = options.isDragAndDropEnabled() || options.alwaysUseTempDir(); if (useTmpExtractDir) { // Create an hidden temp folder in the current directory. m_extractTempDir.reset(new QTemporaryDir(QStringLiteral(".%1-").arg(QCoreApplication::applicationName()))); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } return runProcess(m_cliProps->property("extractProgram").toString(), m_cliProps->extractArgs(filename(), extractFilesList(files), options.preservePaths(), password())); } bool CliInterface::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) m_operationMode = Add; QVector filesToPass = QVector(); // If destination path is specified, we have recreate its structure inside the temp directory // and then place symlinks of targeted files there. const QString destinationPath = (destination == nullptr) ? QString() : destination->fullPath(); qCDebug(ARK) << "Adding" << files.count() << "file(s) to destination:" << destinationPath; if (!destinationPath.isEmpty()) { m_extractTempDir.reset(new QTemporaryDir()); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = nullptr; foreach (Archive::Entry *file, files) { // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory // leaks. if (preservedParent == nullptr) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newFilePath = absoluteDestinationPath + file->fullPath(NoTrailingSlash); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; emit finished(false); return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } return runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->addArgs(filename(), entryFullPaths(filesToPass, NoTrailingSlash), password(), isHeaderEncryptionEnabled(), options.compressionLevel(), options.compressionMethod(), options.encryptionMethod(), options.volumeSize())); } bool CliInterface::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); m_operationMode = Move; m_removedFiles = files; QVector withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); return runProcess(m_cliProps->property("moveProgram").toString(), m_cliProps->moveArgs(filename(), withoutChildren, destination, password())); } bool CliInterface::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempWorkingDir.reset(new QTemporaryDir()); m_tempAddDir.reset(new QTemporaryDir()); QDir::setCurrent(m_tempWorkingDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_numberOfEntries = 0; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } bool CliInterface::deleteFiles(const QVector &files) { m_operationMode = Delete; m_removedFiles = files; return runProcess(m_cliProps->property("deleteProgram").toString(), m_cliProps->deleteArgs(filename(), files, password())); } bool CliInterface::testArchive() { resetParsing(); m_operationMode = Test; return runProcess(m_cliProps->property("testProgram").toString(), m_cliProps->testArgs(filename(), password())); } bool CliInterface::runProcess(const QString& programName, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath = QStandardPaths::findExecutable(programName); if (programPath.isEmpty()) { emit error(xi18nc("@info", "Failed to locate program %1 on disk.", programName)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { readStdout(); }); if (m_operationMode == Extract) { // Extraction jobs need a dedicated post-processing function. connect(m_process, QOverload::of(&QProcess::finished), this, &CliInterface::extractProcessFinished); } else { connect(m_process, QOverload::of(&QProcess::finished), this, &CliInterface::processFinished); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = nullptr; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } foreach (Archive::Entry *e, m_newMovedFiles) { emit entry(e); } m_newMovedFiles.clear(); } if (m_operationMode == Add && !isMultiVolume()) { list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); query.execute(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = nullptr; } // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_extractionOptions.alwaysUseTempDir()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } cleanUpExtracting(); emit finished(false); return; } if (!m_extractionOptions.isDragAndDropEnabled()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_extractionOptions.preservePaths())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_extractionOptions.isDragAndDropEnabled()) { const bool droppedFilesMoved = moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir); if (!droppedFilesMoved) { cleanUpExtracting(); return; } cleanUpExtracting(); } // #395939: make sure we *always* restore the old working dir. restoreWorkingDirExtraction(); emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const QVector &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; foreach (const Archive::Entry *file, files) { QFileInfo relEntry(file->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); query.execute(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { emit cancelled(); emit finished(false); return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); emit finished(false); return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::cleanUpExtracting() { restoreWorkingDirExtraction(); m_extractTempDir.reset(); } void CliInterface::restoreWorkingDirExtraction() { if (m_oldWorkingDirExtraction.isEmpty()) { return; } if (!QDir::setCurrent(m_oldWorkingDirExtraction)) { qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDirExtraction; } else { m_oldWorkingDirExtraction.clear(); } } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); query.execute(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } void CliInterface::setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; foreach (const Archive::Entry* entry, entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; foreach (const Archive::Entry* entry, entryMap) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entry->fullPath().count() - lastFolder.count(); if (entriesWithoutChildren > 1) { charsCount += nameLength; } newPath = destination->fullPath() + entry->fullPath().right(charsCount); } else { if (entriesWithoutChildren > 1) { newPath = destination->fullPath() + entry->name(); } else { // If there is only one passed file in the list, // we have to use destination as newPath. newPath = destination->fullPath(NoTrailingSlash); } if (entry->isDir()) { newPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastFolder = entry->fullPath(); } else { nameLength = 0; lastFolder = QString(); } } Archive::Entry *newEntry = new Archive::Entry(nullptr); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } QStringList CliInterface::extractFilesList(const QVector &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->fullPath(NoTrailingSlash)); } return filesList; } void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } bool CliInterface::passwordQuery() { Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); // There is no process running, so finished() must be emitted manually. emit finished(false); return false; } setPassword(query.password()); return true; } void CliInterface::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); m_tempWorkingDir.reset(); m_tempAddDir.reset(); } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. - bool wrongPasswordMessage = m_cliProps->isWrongPasswordMsg(QLatin1String(lines.last())); + bool wrongPasswordMessage = isWrongPasswordMsg(QLatin1String(lines.last())); bool foundErrorMessage = (wrongPasswordMessage || - m_cliProps->isDiskFullMsg(QLatin1String(lines.last())) || - m_cliProps->isfileExistsMsg(QLatin1String(lines.last()))) || - m_cliProps->isPasswordPrompt(QLatin1String(lines.last())); + isDiskFullMsg(QLatin1String(lines.last())) || + isFileExistsMsg(QLatin1String(lines.last()))) || + isPasswordPrompt(QLatin1String(lines.last())); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { if (!handleLine(QString::fromLocal8Bit(line))) { killProcess(); return; } } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(nullptr, file->name()); } return true; } bool CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Extract || m_operationMode == Add) && m_cliProps->property("captureProgress").toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return true; } } if (m_operationMode == Extract) { - if (m_cliProps->isPasswordPrompt(line)) { + if (isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } - if (m_cliProps->isDiskFullMsg(line)) { + if (isDiskFullMsg(line)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); return false; } - if (m_cliProps->isWrongPasswordMsg(line)) { + if (isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); return false; } if (handleFileExistsMessage(line)) { return true; } return readExtractLine(line); } if (m_operationMode == List) { - if (m_cliProps->isPasswordPrompt(line)) { + if (isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } - if (m_cliProps->isWrongPasswordMsg(line)) { + if (isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); return false; } - if (m_cliProps->isCorruptArchiveMsg(line)) { + if (isCorruptArchiveMsg(line)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); // Special case: corrupt is not a "fatal" error so we return true here. return true; } - if (handleFileExistsMessage(line)) { - return true; - } - return readListLine(line); } if (m_operationMode == Delete) { return readDeleteLine(line); } if (m_operationMode == Test) { - if (m_cliProps->isPasswordPrompt(line)) { + if (isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing this archive.")); return false; } if (m_cliProps->isTestPassedMsg(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return true; } } return true; } bool CliInterface::readDeleteLine(const QString &line) { Q_UNUSED(line); return true; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. - foreach (const QString &pattern, m_cliProps->property("fileExistsFileName").toStringList()) { - const QRegularExpression rxFileNamePattern(pattern); - const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); - - if (rxMatch.hasMatch()) { - m_storedFileName = rxMatch.captured(1); - qCWarning(ARK) << "Detected existing file:" << m_storedFileName; + if (isFileExistsFileName(line)) { + foreach (const QString &pattern, m_cliProps->property("fileExistsFileNameRegExp").toStringList()) { + const QRegularExpression rxFileNamePattern(pattern); + const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); + + if (rxMatch.hasMatch()) { + m_storedFileName = rxMatch.captured(1); + qCWarning(ARK) << "Detected existing file:" << m_storedFileName; + } } } - if (!m_cliProps->isfileExistsMsg(line)) { + if (!isFileExistsMsg(line)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); query.execute(); QString responseToProcess; const QStringList choices = m_cliProps->property("fileExistsInput").toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { emit cancelled(); if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } return pairList; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { m_operationMode = Comment; m_commentTempFile.reset(new QTemporaryFile()); if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile.data()); stream << comment << endl; m_commentTempFile->close(); if (!runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->commentArgs(filename(), m_commentTempFile->fileName()))) { return false; } m_comment = comment; return true; } QString CliInterface::multiVolumeName() const { QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); QString name; foreach (const QString &multiSuffix, m_cliProps->property("multiVolumeSuffix").toStringList()) { QString newSuffix = multiSuffix; newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); name = filename().remove(oldSuffix).append(newSuffix); if (QFileInfo::exists(name)) { break; } } return name; } CliProperties *CliInterface::cliProperties() const { return m_cliProps; } void CliInterface::onEntry(Archive::Entry *archiveEntry) { if (archiveEntry->compressedSizeIsSet) { m_listedSize += archiveEntry->property("compressedSize").toULongLong(); if (m_listedSize <= m_archiveSizeOnDisk) { emit progress(float(m_listedSize)/float(m_archiveSizeOnDisk)); } else { // In case summed compressed size exceeds archive size on disk. emit progress(1); } } } +bool CliInterface::isPasswordPrompt(const QString &line) +{ + Q_UNUSED(line); + return false; +} + +bool CliInterface::isWrongPasswordMsg(const QString &line) +{ + Q_UNUSED(line); + return false; +} + +bool CliInterface::isCorruptArchiveMsg(const QString &line) +{ + Q_UNUSED(line); + return false; +} + +bool CliInterface::isDiskFullMsg(const QString &line) +{ + Q_UNUSED(line); + return false; +} + +bool CliInterface::isFileExistsMsg(const QString &line) +{ + Q_UNUSED(line); + return false; +} + +bool CliInterface::isFileExistsFileName(const QString &line) +{ + Q_UNUSED(line); + return false; +} + } diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 847b90fc..98ff4b97 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,234 +1,240 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" #include "archiveentry.h" #include "cliproperties.h" #include "kerfuffle_export.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; class QTemporaryFile; namespace Kerfuffle { class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: explicit CliInterface(QObject *parent, const QVariantList & args); ~CliInterface() override; int copyRequiredSignals() const override; bool list() override; bool extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) override; bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) override; bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) override; bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) override; bool deleteFiles(const QVector &files) override; bool addComment(const QString &comment) override; bool testArchive() override; virtual void resetParsing() = 0; virtual bool readListLine(const QString &line) = 0; virtual bool readExtractLine(const QString &line) = 0; virtual bool readDeleteLine(const QString &line); + virtual bool isPasswordPrompt(const QString &line); + virtual bool isWrongPasswordMsg(const QString &line); + virtual bool isCorruptArchiveMsg(const QString &line); + virtual bool isDiskFullMsg(const QString &line); + virtual bool isFileExistsMsg(const QString &line); + virtual bool isFileExistsFileName(const QString &line); bool doKill() override; /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); /** * @see ArchiveModel::entryPathsFromDestination */ void setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren); /** * @return The list of selected files to extract. */ QStringList extractFilesList(const QVector &files) const; QString multiVolumeName() const override; CliProperties *cliProperties() const; protected: bool setAddedFiles(); /** * Handles the given @p line. * @return True if the line is ok. False if the line contains/triggers a "fatal" error * or a canceled user query. If false is returned, the caller is supposed to call killProcess(). */ virtual bool handleLine(const QString& line); /** * Run @p programName with the given @p arguments. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ bool runProcess(const QString& programName, const QStringList& arguments); /** * Kill the running process. The finished signal is emitted according to @p emitFinished. */ void killProcess(bool emitFinished = true); /** * Ask the password *before* running any process. * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). */ bool passwordQuery(); void cleanUp(); OperationMode m_operationMode = NoOperation; CliProperties *m_cliProps = nullptr; QString m_oldWorkingDirExtraction; // Used ONLY by extraction jobs. QString m_oldWorkingDir; // Used by copy and move jobs. QScopedPointer m_tempWorkingDir; QScopedPointer m_tempAddDir; OperationMode m_subOperation = NoOperation; QVector m_passedFiles; QVector m_tempAddedFiles; Archive::Entry *m_passedDestination = nullptr; CompressionOptions m_passedOptions; #ifdef Q_OS_WIN KProcess *m_process = nullptr; #else KPtyProcess *m_process = nullptr; #endif bool m_abortingOperation = false; protected Q_SLOTS: virtual void readStdout(bool handleAll = false); private: bool handleFileExistsMessage(const QString& filename); /** * Returns a list of path pairs which will be supplied to rn command. * [ ... ] * Also constructs a list of new entries resulted in moving. * * @param entriesWithoutChildren List of archive entries * @param destination Must be a directory entry if QList contains more that one entry */ QStringList entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination); /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); /** * Moves the dropped @files from the temp dir to the @p finalDest. * @return @c true if the files have been moved, @c false otherwise. */ bool moveDroppedFilesToDest(const QVector &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; void cleanUpExtracting(); void restoreWorkingDirExtraction(); void finishCopying(bool result); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; QVector m_removedFiles; QVector m_newMovedFiles; int m_exitCode = 0; bool m_listEmptyLines = false; QString m_storedFileName; ExtractionOptions m_extractionOptions; QString m_extractDestDir; QScopedPointer m_extractTempDir; QScopedPointer m_commentTempFile; QVector m_extractedFiles; qulonglong m_archiveSizeOnDisk = 0; qulonglong m_listedSize = 0; protected Q_SLOTS: virtual void processFinished(int exitCode, QProcess::ExitStatus exitStatus); private Q_SLOTS: void extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void continueCopying(bool result); void onEntry(Archive::Entry *archiveEntry); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/cliproperties.cpp b/kerfuffle/cliproperties.cpp index 8eb05fe3..1edb33fc 100644 --- a/kerfuffle/cliproperties.cpp +++ b/kerfuffle/cliproperties.cpp @@ -1,363 +1,303 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * 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 "cliproperties.h" #include "ark_debug.h" #include "archiveformat.h" #include "pluginmanager.h" namespace Kerfuffle { CliProperties::CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType) : QObject(parent) , m_mimeType(archiveType) , m_metaData(metaData) { } QStringList CliProperties::addArgs(const QString &archive, const QStringList &files, const QString &password, bool headerEncryption, int compressionLevel, const QString &compressionMethod, const QString &encryptionMethod, ulong volumeSize) { if (!encryptionMethod.isEmpty()) { Q_ASSERT(!password.isEmpty()); } QStringList args; foreach (const QString &s, m_addSwitch) { args << s; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password, headerEncryption); } if (compressionLevel > -1) { args << substituteCompressionLevelSwitch(compressionLevel); } if (!compressionMethod.isEmpty()) { args << substituteCompressionMethodSwitch(compressionMethod); } if (!encryptionMethod.isEmpty()) { args << substituteEncryptionMethodSwitch(encryptionMethod); } if (volumeSize > 0) { args << substituteMultiVolumeSwitch(volumeSize); } args << archive; args << files; args.removeAll(QString()); return args; } QStringList CliProperties::commentArgs(const QString &archive, const QString &commentfile) { QStringList args; foreach (const QString &s, substituteCommentSwitch(commentfile)) { args << s; } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::deleteArgs(const QString &archive, const QVector &files, const QString &password) { QStringList args; args << m_deleteSwitch; if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; foreach (const Archive::Entry *e, files) { args << e->fullPath(NoTrailingSlash); } args.removeAll(QString()); return args; } QStringList CliProperties::extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password) { QStringList args; if (preservePaths && !m_extractSwitch.isEmpty()) { args << m_extractSwitch; } else if (!preservePaths && !m_extractSwitchNoPreserve.isEmpty()) { args << m_extractSwitchNoPreserve; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; args << files; args.removeAll(QString()); return args; } QStringList CliProperties::listArgs(const QString &archive, const QString &password) { QStringList args; foreach (const QString &s, m_listSwitch) { args << s; } const auto encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); if (!password.isEmpty() && encryptionType == Archive::EncryptionType::HeaderEncrypted) { args << substitutePasswordSwitch(password); } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password) { QStringList args; args << m_moveSwitch; if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; if (entries.count() > 1) { foreach (const Archive::Entry *file, entries) { args << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { args << entries.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } args.removeAll(QString()); return args; } QStringList CliProperties::testArgs(const QString &archive, const QString &password) { QStringList args; foreach (const QString &s, m_testSwitch) { args << s; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::substituteCommentSwitch(const QString &commentfile) const { Q_ASSERT(!commentfile.isEmpty()); Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsWriteComment()); QStringList commentSwitches = m_commentSwitch; Q_ASSERT(!commentSwitches.isEmpty()); QMutableListIterator i(commentSwitches); while (i.hasNext()) { i.next(); i.value().replace(QLatin1String("$CommentFile"), commentfile); } return commentSwitches; } QStringList CliProperties::substitutePasswordSwitch(const QString &password, bool headerEnc) const { if (password.isEmpty()) { return QStringList(); } Archive::EncryptionType encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); Q_ASSERT(encryptionType != Archive::EncryptionType::Unencrypted); QStringList passwordSwitch; if (headerEnc) { passwordSwitch = m_passwordSwitchHeaderEnc; } else { passwordSwitch = m_passwordSwitch; } Q_ASSERT(!passwordSwitch.isEmpty()); QMutableListIterator i(passwordSwitch); while (i.hasNext()) { i.next(); i.value().replace(QLatin1String("$Password"), password); } return passwordSwitch; } QString CliProperties::substituteCompressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).maxCompressionLevel() != -1); QString compLevelSwitch = m_compressionLevelSwitch; Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } QString CliProperties::substituteCompressionMethodSwitch(const QString &method) const { if (method.isEmpty()) { return QString(); } Q_ASSERT(!ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().isEmpty()); QString compMethodSwitch = m_compressionMethodSwitch[m_mimeType.name()].toString(); Q_ASSERT(!compMethodSwitch.isEmpty()); QString cliMethod = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().value(method).toString(); compMethodSwitch.replace(QLatin1String("$CompressionMethod"), cliMethod); return compMethodSwitch; } QString CliProperties::substituteEncryptionMethodSwitch(const QString &method) const { if (method.isEmpty()) { return QString(); } const ArchiveFormat format = ArchiveFormat::fromMetadata(m_mimeType, m_metaData); Q_ASSERT(!format.encryptionMethods().isEmpty()); QString encMethodSwitch = m_encryptionMethodSwitch[m_mimeType.name()].toString(); if (encMethodSwitch.isEmpty()) { return QString(); } Q_ASSERT(format.encryptionMethods().contains(method)); encMethodSwitch.replace(QLatin1String("$EncryptionMethod"), method); return encMethodSwitch; } QString CliProperties::substituteMultiVolumeSwitch(ulong volumeSize) const { // The maximum value we allow in the QDoubleSpinBox is 1,000,000MB. Converted to // KB this is 1,024,000,000. if (volumeSize <= 0 || volumeSize > 1024000000) { return QString(); } Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsMultiVolume()); QString multiVolumeSwitch = m_multiVolumeSwitch; Q_ASSERT(!multiVolumeSwitch.isEmpty()); multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize)); return multiVolumeSwitch; } -bool CliProperties::isPasswordPrompt(const QString &line) -{ - foreach(const QString &rx, m_passwordPromptPatterns) { - if (QRegularExpression(rx).match(line).hasMatch()) { - return true; - } - } - return false; -} - -bool CliProperties::isWrongPasswordMsg(const QString &line) -{ - foreach(const QString &rx, m_wrongPasswordPatterns) { - if (QRegularExpression(rx).match(line).hasMatch()) { - return true; - } - } - return false; -} - bool CliProperties::isTestPassedMsg(const QString &line) { foreach(const QString &rx, m_testPassedPatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } -bool CliProperties::isfileExistsMsg(const QString &line) -{ - foreach(const QString &rx, m_fileExistsPatterns) { - if (QRegularExpression(rx).match(line).hasMatch()) { - return true; - } - } - return false; -} - -bool CliProperties::isFileExistsFileName(const QString &line) -{ - foreach(const QString &rx, m_fileExistsFileName) { - if (QRegularExpression(rx).match(line).hasMatch()) { - return true; - } - } - return false; -} - -bool CliProperties::isCorruptArchiveMsg(const QString &line) -{ - foreach(const QString &rx, m_corruptArchivePatterns) { - if (QRegularExpression(rx).match(line).hasMatch()) { - return true; - } - } - return false; -} - -bool CliProperties::isDiskFullMsg(const QString &line) -{ - foreach(const QString &rx, m_diskFullPatterns) { - if (QRegularExpression(rx).match(line).hasMatch()) { - return true; - } - } - return false; -} - } diff --git a/kerfuffle/cliproperties.h b/kerfuffle/cliproperties.h index f8b11843..9a33e36d 100644 --- a/kerfuffle/cliproperties.h +++ b/kerfuffle/cliproperties.h @@ -1,153 +1,137 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * 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. */ #ifndef CLIPROPERTIES_H #define CLIPROPERTIES_H #include "archiveinterface.h" #include "kerfuffle_export.h" #include namespace Kerfuffle { class KERFUFFLE_EXPORT CliProperties: public QObject { Q_OBJECT Q_PROPERTY(QString addProgram MEMBER m_addProgram) Q_PROPERTY(QString deleteProgram MEMBER m_deleteProgram) Q_PROPERTY(QString extractProgram MEMBER m_extractProgram) Q_PROPERTY(QString listProgram MEMBER m_listProgram) Q_PROPERTY(QString moveProgram MEMBER m_moveProgram) Q_PROPERTY(QString testProgram MEMBER m_testProgram) Q_PROPERTY(QStringList addSwitch MEMBER m_addSwitch) Q_PROPERTY(QStringList commentSwitch MEMBER m_commentSwitch) Q_PROPERTY(QString deleteSwitch MEMBER m_deleteSwitch) Q_PROPERTY(QStringList extractSwitch MEMBER m_extractSwitch) Q_PROPERTY(QStringList extractSwitchNoPreserve MEMBER m_extractSwitchNoPreserve) Q_PROPERTY(QStringList listSwitch MEMBER m_listSwitch) Q_PROPERTY(QString moveSwitch MEMBER m_moveSwitch) Q_PROPERTY(QStringList testSwitch MEMBER m_testSwitch) Q_PROPERTY(QStringList passwordSwitch MEMBER m_passwordSwitch) Q_PROPERTY(QStringList passwordSwitchHeaderEnc MEMBER m_passwordSwitchHeaderEnc) Q_PROPERTY(QString compressionLevelSwitch MEMBER m_compressionLevelSwitch) Q_PROPERTY(QHash compressionMethodSwitch MEMBER m_compressionMethodSwitch) Q_PROPERTY(QHash encryptionMethodSwitch MEMBER m_encryptionMethodSwitch) Q_PROPERTY(QString multiVolumeSwitch MEMBER m_multiVolumeSwitch) - Q_PROPERTY(QStringList passwordPromptPatterns MEMBER m_passwordPromptPatterns) - Q_PROPERTY(QStringList wrongPasswordPatterns MEMBER m_wrongPasswordPatterns) Q_PROPERTY(QStringList testPassedPatterns MEMBER m_testPassedPatterns) - Q_PROPERTY(QStringList fileExistsPatterns MEMBER m_fileExistsPatterns) - Q_PROPERTY(QStringList fileExistsFileName MEMBER m_fileExistsFileName) - Q_PROPERTY(QStringList corruptArchivePatterns MEMBER m_corruptArchivePatterns) - Q_PROPERTY(QStringList diskFullPatterns MEMBER m_diskFullPatterns) + Q_PROPERTY(QStringList fileExistsFileNameRegExp MEMBER m_fileExistsFileNameRegExp) Q_PROPERTY(QStringList fileExistsInput MEMBER m_fileExistsInput) Q_PROPERTY(QStringList multiVolumeSuffix MEMBER m_multiVolumeSuffix) Q_PROPERTY(bool captureProgress MEMBER m_captureProgress) public: explicit CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType); QStringList addArgs(const QString &archive, const QStringList &files, const QString &password, bool headerEncryption, int compressionLevel, const QString &compressionMethod, const QString &encryptionMethod, ulong volumeSize); QStringList commentArgs(const QString &archive, const QString &commentfile); QStringList deleteArgs(const QString &archive, const QVector &files, const QString &password); QStringList extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password); QStringList listArgs(const QString &archive, const QString &password); QStringList moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password); QStringList testArgs(const QString &archive, const QString &password); - bool isPasswordPrompt(const QString &line); - bool isWrongPasswordMsg(const QString &line); bool isTestPassedMsg(const QString &line); - bool isfileExistsMsg(const QString &line); - bool isFileExistsFileName(const QString &line); - bool isCorruptArchiveMsg(const QString &line); - bool isDiskFullMsg(const QString &line); private: QStringList substituteCommentSwitch(const QString &commentfile) const; QStringList substitutePasswordSwitch(const QString &password, bool headerEnc = false) const; QString substituteCompressionLevelSwitch(int level) const; QString substituteCompressionMethodSwitch(const QString &method) const; QString substituteEncryptionMethodSwitch(const QString &method) const; QString substituteMultiVolumeSwitch(ulong volumeSize) const; QString m_addProgram; QString m_deleteProgram; QString m_extractProgram; QString m_listProgram; QString m_moveProgram; QString m_testProgram; QStringList m_addSwitch; QStringList m_commentSwitch; QString m_deleteSwitch; QStringList m_extractSwitch; QStringList m_extractSwitchNoPreserve; QStringList m_listSwitch; QString m_moveSwitch; QStringList m_testSwitch; QStringList m_passwordSwitch; QStringList m_passwordSwitchHeaderEnc; QString m_compressionLevelSwitch; QHash m_compressionMethodSwitch; QHash m_encryptionMethodSwitch; QString m_multiVolumeSwitch; - QStringList m_passwordPromptPatterns; - QStringList m_wrongPasswordPatterns; QStringList m_testPassedPatterns; - QStringList m_fileExistsPatterns; - QStringList m_fileExistsFileName; - QStringList m_corruptArchivePatterns; - QStringList m_diskFullPatterns; + QStringList m_fileExistsFileNameRegExp; QStringList m_fileExistsInput; QStringList m_multiVolumeSuffix; bool m_captureProgress = false; QMimeType m_mimeType; KPluginMetaData m_metaData; }; } #endif /* CLIPROPERTIES_H */ diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index db9e3211..f018dc2b 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,854 +1,857 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include #include #include #include namespace Kerfuffle { class Job::Private : public QThread { Q_OBJECT public: Private(Job *job, QObject *parent = nullptr) : QThread(parent) , q(job) { } void run() override; private: Job *q; }; void Job::Private::run() { q->doWork(); } Job::Job(Archive *archive, ReadOnlyArchiveInterface *interface) : KJob() , m_archive(archive) , m_archiveInterface(interface) , d(new Private(this)) { setCapabilities(KJob::Killable); } Job::Job(Archive *archive) : Job(archive, nullptr) {} Job::Job(ReadOnlyArchiveInterface *interface) : Job(nullptr, interface) {} Job::~Job() { if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { // Use the archive interface. if (archive()) { return archive()->interface(); } // Use the interface passed to this job (e.g. JSONArchiveInterface in jobstest.cpp). return m_archiveInterface; } Archive *Job::archive() const { return m_archive; } QString Job::errorString() const { if (!errorText().isEmpty()) { return errorText(); } if (archive()) { if (archive()->error() == NoPlugin) { return i18n("No suitable plugin found. Ark does not seem to support this file type."); } if (archive()->error() == FailedPlugin) { return i18n("Failed to load a suitable plugin. Make sure any executables needed to handle the archive type are installed."); } } return QString(); } void Job::start() { jobTimer.start(); // We have an archive but it's not valid, nothing to do. if (archive() && !archive()->isValid()) { QTimer::singleShot(0, this, [=]() { onFinished(false); }); return; } if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); auto readWriteInterface = qobject_cast(archiveInterface()); if (readWriteInterface) { connect(readWriteInterface, &ReadWriteArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); } } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; if (archive() && !archive()->isValid()) { setError(KJob::UserDefinedError); } if (!d->isInterruptionRequested()) { emitResult(); } } void Job::onUserQuery(Query *query) { if (archiveInterface()->waitForFinishedSignal()) { qCWarning(ARK) << "Plugins run from the main thread should call directly query->execute()"; } emit userQuery(query); } bool Job::doKill() { const bool canKillJob = archiveInterface()->doKill(); if (!d->isRunning()) { return canKillJob; } d->requestInterruption(); if (!canKillJob) { qCDebug(ARK) << "Forcing thread exit in one second..."; d->wait(1000); return true; } d->wait(); return canKillJob; } LoadJob::LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface) : Job(archive, interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "Created job instance"; connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); } LoadJob::LoadJob(Archive *archive) : LoadJob(archive, nullptr) {} LoadJob::LoadJob(ReadOnlyArchiveInterface *interface) : LoadJob(nullptr, interface) {} void LoadJob::doWork() { emit description(this, i18n("Loading archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { // onFinished() needs to be called after onNewEntry(), because the former reads members set in the latter. // So we need to put it in the event queue, just like the single-thread case does by emitting finished(). QTimer::singleShot(0, this, [=]() { onFinished(ret); }); } } void LoadJob::onFinished(bool result) { if (archive() && result) { archive()->setProperty("unpackedSize", extractedFilesSize()); archive()->setProperty("isSingleFolder", isSingleFolderArchive()); const auto name = subfolderName().isEmpty() ? archive()->completeBaseName() : subfolderName(); archive()->setProperty("subfolderName", name); if (isPasswordProtected()) { archive()->setProperty("encryptionType", archive()->password().isEmpty() ? Archive::Encrypted : Archive::HeaderEncrypted); } } Job::onFinished(result); } qlonglong LoadJob::extractedFilesSize() const { return m_extractedFilesSize; } bool LoadJob::isPasswordProtected() const { return m_isPasswordProtected; } bool LoadJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void LoadJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString LoadJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } BatchExtractJob::BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths) : Job(loadJob->archive()) , m_loadJob(loadJob) , m_destination(destination) , m_autoSubfolder(autoSubfolder) , m_preservePaths(preservePaths) { qCDebug(ARK) << "Created job instance"; } void BatchExtractJob::doWork() { connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &BatchExtractJob::onCancelled); if (archiveInterface()->hasBatchExtractionProgress()) { // progress() will be actually emitted by the LoadJob, but the archiveInterface() is the same. connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); } // Forward LoadJob's signals. connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry); connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); m_loadJob->start(); } bool BatchExtractJob::doKill() { if (m_step == Loading) { return m_loadJob->kill(); } return m_extractJob->kill(); } void BatchExtractJob::slotLoadingProgress(double progress) { // Progress from LoadJob counts only for 50% of the BatchExtractJob's duration. m_lastPercentage = static_cast(50.0*progress); setPercent(m_lastPercentage); } void BatchExtractJob::slotExtractProgress(double progress) { // The 2nd 50% of the BatchExtractJob's duration comes from the ExtractJob. setPercent(m_lastPercentage + static_cast(50.0*progress)); } void BatchExtractJob::slotLoadingFinished(KJob *job) { if (job->error()) { // Forward errors as well. onError(job->errorString(), QString()); onFinished(false); return; } // Now we can start extraction. setupDestination(); Kerfuffle::ExtractionOptions options; options.setPreservePaths(m_preservePaths); m_extractJob = archive()->extractFiles({}, m_destination, options); if (m_extractJob) { connect(m_extractJob, &KJob::result, this, &BatchExtractJob::emitResult); connect(m_extractJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); if (archiveInterface()->hasBatchExtractionProgress()) { // The LoadJob is done, change slot and start setting the percentage from m_lastPercentage on. disconnect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotExtractProgress); } m_step = Extracting; m_extractJob->start(); } else { emitResult(); } } void BatchExtractJob::setupDestination() { const bool isSingleFolderRPM = (archive()->isSingleFolder() && (archive()->mimeType().name() == QLatin1String("application/x-rpm"))); if (m_autoSubfolder && (!archive()->isSingleFolder() || isSingleFolderRPM)) { const QDir d(m_destination); QString subfolderName = archive()->subfolderName(); // Special case for single folder RPM archives. // We don't want the autodetected folder to have a meaningless "usr" name. if (isSingleFolderRPM && subfolderName == QStringLiteral("usr")) { qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name"; subfolderName = QFileInfo(archive()->fileName()).completeBaseName(); } if (d.exists(subfolderName)) { subfolderName = KIO::suggestName(QUrl::fromUserInput(m_destination, QString(), QUrl::AssumeLocalFile), subfolderName); } d.mkdir(subfolderName); m_destination += QLatin1Char( '/' ) + subfolderName; } } CreateJob::CreateJob(Archive *archive, const QVector &entries, const CompressionOptions &options) : Job(archive) , m_entries(entries) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void CreateJob::enableEncryption(const QString &password, bool encryptHeader) { archive()->encrypt(password, encryptHeader); } void CreateJob::setMultiVolume(bool isMultiVolume) { archive()->setMultiVolume(isMultiVolume); } void CreateJob::doWork() { m_addJob = archive()->addFiles(m_entries, nullptr, m_options); if (m_addJob) { connect(m_addJob, &KJob::result, this, &CreateJob::emitResult); // Forward description signal from AddJob, we need to change the first argument ('this' needs to be a CreateJob). connect(m_addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair &field1, const QPair &) { emit description(this, title, field1); }); + connect(m_addJob, QOverload::of(&KJob::percent), this, [=](KJob*, unsigned long percent) { + emitPercent(percent, 100); + }); m_addJob->start(); } else { emitResult(); } } bool CreateJob::doKill() { return m_addJob && m_addJob->kill(); } ExtractJob::ExtractJob(const QVector &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()), qMakePair(i18nc("extraction folder", "Destination"), m_destinationDir)); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files." << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { m_tmpExtractDir = new QTemporaryDir(); } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; if (m_passwordProtectedHint) { options.setEncryptedArchiveHint(true); } return options; } QTemporaryDir *TempExtractJob::tempDir() const { return m_tmpExtractDir; } void TempExtractJob::doWork() { // pass 1 to i18np on purpose so this translation may properly be reused. emit description(this, i18np("Extracting one file", "Extracting %1 files", 1)); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString TempExtractJob::extractionDir() const { return m_tmpExtractDir->path(); } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } AddJob::AddJob(const QVector &entries, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void AddJob::doWork() { // Set current dir. const QString globalWorkDir = m_options.globalWorkDir(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // Count total number of entries to be added. uint totalCount = 0; QElapsedTimer timer; timer.start(); foreach (const Archive::Entry* entry, m_entries) { totalCount++; if (QFileInfo(entry->fullPath()).isDir()) { QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); totalCount++; } } } qCDebug(ARK) << "Going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; const QString desc = i18np("Compressing a file", "Compressing %1 files", totalCount); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); // The file paths must be relative to GlobalWorkDir. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options, totalCount); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } MoveJob::MoveJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void MoveJob::doWork() { qCDebug(ARK) << "Going to move" << m_entries.count() << "file(s)"; QString desc = i18np("Moving a file", "Moving %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void MoveJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { Job::onFinished(result); } } CopyJob::CopyJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "Created job instance"; } void CopyJob::doWork() { qCDebug(ARK) << "Going to copy" << m_entries.count() << "file(s)"; QString desc = i18np("Copying a file", "Copying %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void CopyJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { Job::onFinished(result); } } DeleteJob::DeleteJob(const QVector &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { QString desc = i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "Job started"; emit description(this, i18n("Testing archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle #include "jobs.moc" diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index eecd8b5a..a560d129 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,358 +1,383 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "cliinterface.h" #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_cli7z.json") CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_archiveType(ArchiveType7z) , m_parseState(ParseStateTitle) , m_linesComment(0) , m_isFirstInformationEntry(true) { qCDebug(ARK) << "Loaded cli_7z plugin"; setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_comment.clear(); m_numberOfVolumes = 0; } void CliPlugin::setupCliProperties() { qCDebug(ARK) << "Setting up parameters..."; m_cliProps->setProperty("captureProgress", false); m_cliProps->setProperty("addProgram", QStringLiteral("7z")); m_cliProps->setProperty("addSwitch", QStringList{QStringLiteral("a"), QStringLiteral("-l")}); m_cliProps->setProperty("deleteProgram", QStringLiteral("7z")); m_cliProps->setProperty("deleteSwitch", QStringLiteral("d")); m_cliProps->setProperty("extractProgram", QStringLiteral("7z")); m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x")}); m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e")}); m_cliProps->setProperty("listProgram", QStringLiteral("7z")); m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("l"), QStringLiteral("-slt")}); m_cliProps->setProperty("moveProgram", QStringLiteral("7z")); m_cliProps->setProperty("moveSwitch", QStringLiteral("rn")); m_cliProps->setProperty("testProgram", QStringLiteral("7z")); m_cliProps->setProperty("testSwitch", QStringLiteral("t")); m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")}); m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-p$Password"), QStringLiteral("-mhe=on")}); m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-mx=$CompressionLevel")); m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/x-7z-compressed"), QStringLiteral("-m0=$CompressionMethod")}, {QStringLiteral("application/zip"), QStringLiteral("-mm=$CompressionMethod")}}); m_cliProps->setProperty("encryptionMethodSwitch", QHash{{QStringLiteral("application/x-7z-compressed"), QString()}, {QStringLiteral("application/zip"), QStringLiteral("-mem=$EncryptionMethod")}}); m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); - - m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("Enter password \\(will not be echoed\\)")}); - m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("Wrong password")}); m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^Everything is Ok$")}); - m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^\\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $"), - QStringLiteral("^\\? \\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $")}); - m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^file \\./(.*)$"), - QStringLiteral("^ Path: \\./(.*)$")}); + m_cliProps->setProperty("fileExistsFileNameRegExp", QStringList{QStringLiteral("^file \\./(.*)$"), + QStringLiteral("^ Path: \\./(.*)$")}); m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("Y"), //Overwrite QStringLiteral("N"), //Skip QStringLiteral("A"), //Overwrite all QStringLiteral("S"), //Autoskip QStringLiteral("Q")}); //Cancel - m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("Unexpected end of archive"), - QStringLiteral("Headers Error")}); - m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("No space left on device")}); m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("$Suffix.001")}); } void CliPlugin::fixDirectoryFullName() { if (m_currentArchiveEntry->isDir()) { const QString directoryName = m_currentArchiveEntry->fullPath(); if (!directoryName.endsWith(QLatin1Char('/'))) { m_currentArchiveEntry->setProperty("fullPath", QString(directoryName + QLatin1Char('/'))); } } } bool CliPlugin::readListLine(const QString& line) { static const QLatin1String archiveInfoDelimiter1("--"); // 7z 9.13+ static const QLatin1String archiveInfoDelimiter2("----"); // 7z 9.04 static const QLatin1String entryInfoDelimiter("----------"); if (line.startsWith(QLatin1String("Open ERROR: Can not open the file as [7z] archive"))) { emit error(i18n("Listing the archive failed.")); return false; } const QRegularExpression rxVersionLine(QStringLiteral("^p7zip Version ([\\d\\.]+) .*$")); QRegularExpressionMatch matchVersion; switch (m_parseState) { case ParseStateTitle: matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateHeader; const QString p7zipVersion = matchVersion.captured(1); qCDebug(ARK) << "p7zip version" << p7zipVersion << "detected"; } break; case ParseStateHeader: if (line.startsWith(QLatin1String("Listing archive:"))) { qCDebug(ARK) << "Archive name: " << line.right(line.size() - 16).trimmed(); } else if ((line == archiveInfoDelimiter1) || (line == archiveInfoDelimiter2)) { m_parseState = ParseStateArchiveInformation; } else if (line.contains(QLatin1String("Error: "))) { qCWarning(ARK) << line.mid(7); } break; case ParseStateArchiveInformation: if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; } else if (line.startsWith(QLatin1String("Type = "))) { const QString type = line.mid(7).trimmed(); qCDebug(ARK) << "Archive type: " << type; if (type == QLatin1String("7z")) { m_archiveType = ArchiveType7z; } else if (type == QLatin1String("bzip2")) { m_archiveType = ArchiveTypeBZip2; } else if (type == QLatin1String("gzip")) { m_archiveType = ArchiveTypeGZip; } else if (type == QLatin1String("xz")) { m_archiveType = ArchiveTypeXz; } else if (type == QLatin1String("tar")) { m_archiveType = ArchiveTypeTar; } else if (type == QLatin1String("zip")) { m_archiveType = ArchiveTypeZip; } else if (type == QLatin1String("Rar")) { m_archiveType = ArchiveTypeRar; } else if (type == QLatin1String("Split")) { setMultiVolume(true); } else { // Should not happen qCWarning(ARK) << "Unsupported archive type"; return false; } } else if (line.startsWith(QLatin1String("Volumes = "))) { m_numberOfVolumes = line.section(QLatin1Char('='), 1).trimmed().toInt(); } else if (line.startsWith(QLatin1String("Method = "))) { QStringList methods = line.section(QLatin1Char('='), 1).trimmed().split(QLatin1Char(' '), QString::SkipEmptyParts); handleMethods(methods); } else if (line.startsWith(QLatin1String("Comment = "))) { m_parseState = ParseStateComment; m_comment.append(line.section(QLatin1Char('='), 1) + QLatin1Char('\n')); } break; case ParseStateComment: if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; if (!m_comment.trimmed().isEmpty()) { m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } break; case ParseStateEntryInformation: if (m_isFirstInformationEntry) { m_isFirstInformationEntry = false; m_currentArchiveEntry = new Archive::Entry(this); m_currentArchiveEntry->compressedSizeIsSet = false; } if (line.startsWith(QLatin1String("Path = "))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed()); m_currentArchiveEntry->setProperty("fullPath", entryFilename); } else if (line.startsWith(QLatin1String("Size = "))) { m_currentArchiveEntry->setProperty("size", line.mid(7).trimmed()); } else if (line.startsWith(QLatin1String("Packed Size = "))) { // #236696: 7z files only show a single Packed Size value // corresponding to the whole archive. if (m_archiveType != ArchiveType7z) { m_currentArchiveEntry->compressedSizeIsSet = true; m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed()); } } else if (line.startsWith(QLatin1String("Modified = "))) { m_currentArchiveEntry->setProperty("timestamp", QDateTime::fromString(line.mid(11).trimmed(), QStringLiteral("yyyy-MM-dd hh:mm:ss"))); } else if (line.startsWith(QLatin1String("Folder = "))) { const QString isDirectoryStr = line.mid(9).trimmed(); Q_ASSERT(isDirectoryStr == QStringLiteral("+") || isDirectoryStr == QStringLiteral("-")); const bool isDirectory = isDirectoryStr.startsWith(QLatin1Char('+')); m_currentArchiveEntry->setProperty("isDirectory", isDirectory); fixDirectoryFullName(); } else if (line.startsWith(QLatin1String("Attributes = "))) { const QString attributes = line.mid(13).trimmed(); if (attributes.contains(QLatin1Char('D'))) { m_currentArchiveEntry->setProperty("isDirectory", true); fixDirectoryFullName(); } if (attributes.contains(QLatin1Char('_'))) { // Unix attributes m_currentArchiveEntry->setProperty("permissions", attributes.mid(attributes.indexOf(QLatin1Char(' ')) + 1)); } else { // FAT attributes m_currentArchiveEntry->setProperty("permissions", attributes); } } else if (line.startsWith(QLatin1String("CRC = "))) { m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed()); } else if (line.startsWith(QLatin1String("Method = "))) { m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed()); // For zip archives we need to check method for each entry. if (m_archiveType == ArchiveTypeZip) { QStringList methods = line.section(QLatin1Char('='), 1).trimmed().split(QLatin1Char(' '), QString::SkipEmptyParts); handleMethods(methods); } } else if (line.startsWith(QLatin1String("Encrypted = ")) && line.size() >= 13) { m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+')); } else if (line.startsWith(QLatin1String("Block = ")) || line.startsWith(QLatin1String("Version = "))) { m_isFirstInformationEntry = true; if (!m_currentArchiveEntry->fullPath().isEmpty()) { emit entry(m_currentArchiveEntry); } else { delete m_currentArchiveEntry; } m_currentArchiveEntry = nullptr; } break; } return true; } bool CliPlugin::readExtractLine(const QString &line) { if (line.startsWith(QLatin1String("ERROR: E_FAIL"))) { emit error(i18n("Extraction failed due to an unknown error.")); return false; } if (line.startsWith(QLatin1String("ERROR: CRC Failed")) || line.startsWith(QLatin1String("ERROR: Headers Error"))) { emit error(i18n("Extraction failed due to one or more corrupt files. Any extracted files may be damaged.")); return false; } return true; } bool CliPlugin::readDeleteLine(const QString &line) { if (line.startsWith(QLatin1String("Error: ")) && line.endsWith(QLatin1String(" is not supported archive"))) { emit error(i18n("Delete operation failed. Try upgrading p7zip or disabling the p7zip plugin in the configuration dialog.")); return false; } return true; } void CliPlugin::handleMethods(const QStringList &methods) { foreach (const QString &method, methods) { QRegularExpression rxEncMethod(QStringLiteral("^(7zAES|AES-128|AES-192|AES-256|ZipCrypto)$")); if (rxEncMethod.match(method).hasMatch()) { QRegularExpression rxAESMethods(QStringLiteral("^(AES-128|AES-192|AES-256)$")); if (rxAESMethods.match(method).hasMatch()) { // Remove dash for AES methods. emit encryptionMethodFound(QString(method).remove(QLatin1Char('-'))); } else { emit encryptionMethodFound(method); } continue; } // LZMA methods are output with some trailing numbers by 7z representing dictionary/block sizes. // We are not interested in these, so remove them. if (method.startsWith(QLatin1String("LZMA2"))) { emit compressionMethodFound(method.left(5)); } else if (method.startsWith(QLatin1String("LZMA"))) { emit compressionMethodFound(method.left(4)); } else if (method == QLatin1String("xz")) { emit compressionMethodFound(method.toUpper()); } else { emit compressionMethodFound(method); } } } +bool CliPlugin::isPasswordPrompt(const QString &line) +{ + return line.startsWith(QLatin1String("Enter password (will not be echoed):")); +} + +bool CliPlugin::isWrongPasswordMsg(const QString &line) +{ + return line.contains(QLatin1String("Wrong password")); +} + +bool CliPlugin::isCorruptArchiveMsg(const QString &line) +{ + return (line == QLatin1String("Unexpected end of archive") || + line == QLatin1String("Headers Error")); +} + +bool CliPlugin::isDiskFullMsg(const QString &line) +{ + return line.contains(QLatin1String("No space left on device")); +} + +bool CliPlugin::isFileExistsMsg(const QString &line) +{ + return (line == QLatin1String("(Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? ") || + line == QLatin1String("? (Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? ")); +} + +bool CliPlugin::isFileExistsFileName(const QString &line) +{ + return (line.startsWith(QLatin1String("file ./")) || + line.startsWith(QLatin1String(" Path: ./"))); +} + #include "cliplugin.moc" diff --git a/plugins/cli7zplugin/cliplugin.h b/plugins/cli7zplugin/cliplugin.h index 0368a18c..4ab188bb 100644 --- a/plugins/cli7zplugin/cliplugin.h +++ b/plugins/cli7zplugin/cliplugin.h @@ -1,71 +1,77 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList & args); ~CliPlugin() override; void resetParsing() override; bool readListLine(const QString &line) override; bool readExtractLine(const QString &line) override; bool readDeleteLine(const QString &line) override; + bool isPasswordPrompt(const QString &line) override; + bool isWrongPasswordMsg(const QString &line) override; + bool isCorruptArchiveMsg(const QString &line) override; + bool isDiskFullMsg(const QString &line) override; + bool isFileExistsMsg(const QString &line) override; + bool isFileExistsFileName(const QString &line) override; private: enum ArchiveType { ArchiveType7z = 0, ArchiveTypeBZip2, ArchiveTypeGZip, ArchiveTypeXz, ArchiveTypeTar, ArchiveTypeZip, ArchiveTypeRar } m_archiveType; enum ParseState { ParseStateTitle = 0, ParseStateHeader, ParseStateArchiveInformation, ParseStateComment, ParseStateEntryInformation } m_parseState; void setupCliProperties(); void handleMethods(const QStringList &methods); void fixDirectoryFullName(); int m_linesComment; Kerfuffle::Archive::Entry *m_currentArchiveEntry; bool m_isFirstInformationEntry; }; #endif // CLIPLUGIN_H diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 186d3037..f8eec390 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,578 +1,604 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "archiveentry.h" #include #include #include using namespace Kerfuffle; K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_clirar.json") CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isSolid(false) , m_isRAR5(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); setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_unrarVersion.clear(); m_comment.clear(); m_numberOfVolumes = 0; } void CliPlugin::setupCliProperties() { qCDebug(ARK) << "Setting up parameters..."; m_cliProps->setProperty("captureProgress", true); m_cliProps->setProperty("addProgram", QStringLiteral("rar")); m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("a")})); m_cliProps->setProperty("deleteProgram", QStringLiteral("rar")); m_cliProps->setProperty("deleteSwitch", QStringLiteral("d")); m_cliProps->setProperty("extractProgram", QStringLiteral("unrar")); m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x"), QStringLiteral("-kb"), QStringLiteral("-p-")}); m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e"), QStringLiteral("-kb"), QStringLiteral("-p-")}); m_cliProps->setProperty("listProgram", QStringLiteral("unrar")); m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("vt"), QStringLiteral("-v")}); m_cliProps->setProperty("moveProgram", QStringLiteral("rar")); m_cliProps->setProperty("moveSwitch", QStringLiteral("rn")); m_cliProps->setProperty("testProgram", QStringLiteral("unrar")); m_cliProps->setProperty("testSwitch", QStringLiteral("t")); m_cliProps->setProperty("commentSwitch", QStringList{QStringLiteral("c"), QStringLiteral("-z$CommentFile")}); m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")}); m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-hp$Password")}); m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-m$CompressionLevel")); m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/vnd.rar"), QStringLiteral("-ma$CompressionMethod")}, {QStringLiteral("application/x-rar"), QStringLiteral("-ma$CompressionMethod")}}); m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); - m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("Enter password \\(will not be echoed\\) for")}); - m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("password incorrect"), - QStringLiteral("wrong password")}); m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^All OK$")}); - m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $")}); - m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^(.+) already exists. Overwrite it"), // unrar 3 & 4 - QStringLiteral("^Would you like to replace the existing file (.+)$")}); // unrar 5 + m_cliProps->setProperty("fileExistsFileNameRegExp", QStringList{QStringLiteral("^(.+) already exists. Overwrite it"), // unrar 3 & 4 + QStringLiteral("^Would you like to replace the existing file (.+)$")}); // unrar 5 m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("Y"), //Overwrite QStringLiteral("N"), //Skip QStringLiteral("A"), //Overwrite all QStringLiteral("E"), //Autoskip QStringLiteral("Q")}); //Cancel - m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("Unexpected end of archive"), - QStringLiteral("the file header is corrupt")}); - m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("No space left on device")}); // rar will sometimes create multi-volume archives where first volume is // called name.part1.rar and other times name.part01.rar. m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("part01.$Suffix"), QStringLiteral("part1.$Suffix")}); } 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; m_unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << m_unrarVersion << "detected"; if (m_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) { return handleUnrar5Line(line); } else { return handleUnrar4Line(line); } return true; } bool CliPlugin::handleUnrar5Line(const QString &line) { if (line.startsWith(QLatin1String("Cannot find volume "))) { emit error(i18n("Failed to find all archive volumes.")); return false; } // Parses the comment field. if (m_parseState == ParseStateComment) { // "Archive: " is printed after the comment. // FIXME: Comment itself could also contain the searched string. if (line.startsWith(QLatin1String("Archive: "))) { 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 true; } // 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(QLatin1String("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume"))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } if (line.contains(QLatin1String("RAR 4"))) { emit compressionMethodFound(QStringLiteral("RAR4")); } else if (line.contains(QLatin1String("RAR 5"))) { emit compressionMethodFound(QStringLiteral("RAR5")); m_isRAR5 = true; } } return true; } // 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 true; // 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 true; } // 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 true; } return true; } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(this); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); e->setProperty("timestamp", QDateTime::fromString(m_unrar5Details.value(QStringLiteral("mtime")), QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz"))); 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", QString()); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (m_isPasswordProtected) { m_isRAR5 ? emit encryptionMethodFound(QStringLiteral("AES256")) : emit encryptionMethodFound(QStringLiteral("AES128")); } 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); } bool CliPlugin::handleUnrar4Line(const QString &line) { if (line.startsWith(QLatin1String("Cannot find volume "))) { emit error(i18n("Failed to find all archive volumes.")); return false; } // 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) .+$")); // unrar 4 outputs the following string when opening v5 RAR archives. if (line == QLatin1String("Unsupported archive format. Please update RAR to a newer version.")) { emit error(i18n("Your unrar executable is version %1, which is too old to handle this archive. Please update to a more recent version.", m_unrarVersion)); return false; } // unrar 3 reports a non-RAR archive when opening v5 RAR archives. if (line.endsWith(QLatin1String(" is not RAR archive"))) { emit error(i18n("Unrar reported a non-RAR archive. The installed unrar version (%1) is old. Try updating your unrar.", m_unrarVersion)); return false; } // If we reach this point, then we can be sure that it's not a RAR5 // archive, so assume RAR4. emit compressionMethodFound(QStringLiteral("RAR4")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(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 true; } // 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(QLatin1String("--------------------"))) { m_parseState = ParseStateEntryFileName; } else if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; } return true; } // 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 true; } // 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 true; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QLatin1String("-----------------"))) { m_parseState = ParseStateHeader; return true; // 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 emit encryptionMethodFound(QStringLiteral("AES128")); // 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 true; // 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 true; } // 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(QLatin1String("-----------------"))) { m_parseState = ParseStateHeader; return true; } // 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 true; } // 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 true; } 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 true; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return true; } return true; } void CliPlugin::handleUnrar4Entry() { Archive::Entry *e = new Archive::Entry(this); 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); } bool CliPlugin::readExtractLine(const QString &line) { const QRegularExpression rxCRC(QStringLiteral("CRC failed")); if (rxCRC.match(line).hasMatch()) { emit error(i18n("One or more wrong checksums")); return false; } if (line.startsWith(QLatin1String("Cannot find volume "))) { emit error(i18n("Failed to find all archive volumes.")); return false; } return true; } bool CliPlugin::hasBatchExtractionProgress() const { return true; } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } +bool CliPlugin::isPasswordPrompt(const QString &line) +{ + return line.startsWith(QLatin1String("Enter password \\(will not be echoed\\) for")); +} + +bool CliPlugin::isWrongPasswordMsg(const QString &line) +{ + return (line.contains(QLatin1String("password incorrect")) || line.contains(QLatin1String("wrong password"))); +} + +bool CliPlugin::isCorruptArchiveMsg(const QString &line) +{ + return (line == QLatin1String("Unexpected end of archive") || + line.contains(QLatin1String("the file header is corrupt")) || + line.endsWith(QLatin1String("checksum error"))); +} + +bool CliPlugin::isDiskFullMsg(const QString &line) +{ + return line.contains(QLatin1String("No space left on device")); +} + +bool CliPlugin::isFileExistsMsg(const QString &line) +{ + return (line == QLatin1String("[Y]es, [N]o, [A]ll, n[E]ver, [R]ename, [Q]uit ")); +} + +bool CliPlugin::isFileExistsFileName(const QString &line) +{ + return (line.startsWith(QLatin1String("Would you like to replace the existing file ")) || // unrar 5 + line.contains(QLatin1String(" already exists. Overwrite it"))); // unrar 3 & 4 +} + #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.h b/plugins/clirarplugin/cliplugin.h index b9bb003c..95447831 100644 --- a/plugins/clirarplugin/cliplugin.h +++ b/plugins/clirarplugin/cliplugin.h @@ -1,75 +1,81 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * 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. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); ~CliPlugin() override; void resetParsing() override; bool readListLine(const QString &line) override; bool readExtractLine(const QString &line) override; bool hasBatchExtractionProgress() const override; + bool isPasswordPrompt(const QString &line) override; + bool isWrongPasswordMsg(const QString &line) override; + bool isCorruptArchiveMsg(const QString &line) override; + bool isDiskFullMsg(const QString &line) override; + bool isFileExistsMsg(const QString &line) override; + bool isFileExistsFileName(const QString &line) override; private: enum ParseState { ParseStateTitle = 0, ParseStateComment, ParseStateHeader, ParseStateEntryFileName, ParseStateEntryDetails, ParseStateLinkTarget } m_parseState; void setupCliProperties(); bool handleUnrar5Line(const QString &line); void handleUnrar5Entry(); bool handleUnrar4Line(const QString &line); void handleUnrar4Entry(); void ignoreLines(int lines, ParseState nextState); QStringList m_unrar4Details; QHash m_unrar5Details; QString m_unrarVersion; bool m_isUnrar5; bool m_isPasswordProtected; bool m_isSolid; bool m_isRAR5; int m_remainingIgnoreLines; int m_linesComment; }; #endif // CLIPLUGIN_H diff --git a/plugins/cliunarchiverplugin/cliplugin.cpp b/plugins/cliunarchiverplugin/cliplugin.cpp index 4a79d2d2..b337b506 100644 --- a/plugins/cliunarchiverplugin/cliplugin.cpp +++ b/plugins/cliunarchiverplugin/cliplugin.cpp @@ -1,276 +1,279 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Luke Shumaker * Copyright (C) 2016 Elvis Angelaccio * * 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 "queries.h" #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_cliunarchiver.json") CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) : CliInterface(parent, args) { qCDebug(ARK) << "Loaded cli_unarchiver plugin"; setupCliProperties(); } CliPlugin::~CliPlugin() { } bool CliPlugin::list() { resetParsing(); m_operationMode = List; return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); } bool CliPlugin::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { ExtractionOptions newOptions = options; // unar has the following limitations: // 1. creates an empty file upon entering a wrong password. // 2. detects that the stdout has been redirected and blocks the stdin. // This prevents Ark from executing unar's overwrite queries. // To prevent both, we always extract to a temporary directory // and then we move the files to the intended destination. qCDebug(ARK) << "Enabling extraction to temporary directory."; newOptions.setAlwaysUseTempDir(true); return CliInterface::extractFiles(files, destinationDirectory, newOptions); } void CliPlugin::resetParsing() { m_jsonOutput.clear(); m_numberOfVolumes = 0; } void CliPlugin::setupCliProperties() { m_cliProps->setProperty("captureProgress", false); m_cliProps->setProperty("extractProgram", QStringLiteral("unar")); m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("-D")}); m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-D")}); m_cliProps->setProperty("listProgram", QStringLiteral("lsar")); m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-json")}); m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-password"), QStringLiteral("$Password")}); - - m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("This archive requires a password to unpack. Use the -p option to provide one.")}); } bool CliPlugin::readListLine(const QString &line) { const QRegularExpression rx(QStringLiteral("Failed! \\((.+)\\)$")); if (rx.match(line).hasMatch()) { emit error(i18n("Listing the archive failed.")); return false; } return true; } bool CliPlugin::readExtractLine(const QString &line) { const QRegularExpression rx(QStringLiteral("Failed! \\((.+)\\)$")); if (rx.match(line).hasMatch()) { emit error(i18n("Extraction failed.")); return false; } return true; } void CliPlugin::setJsonOutput(const QString &jsonOutput) { m_jsonOutput = jsonOutput; readJsonOutput(); } void CliPlugin::readStdout(bool handleAll) { if (!handleAll) { CliInterface::readStdout(false); return; } // We are ready to read the json output. readJsonOutput(); } bool CliPlugin::handleLine(const QString& line) { // Collect the json output line by line. if (m_operationMode == List) { // #372210: lsar can generate huge JSONs for big archives. // We can at least catch a bad_alloc here in order to not crash. try { m_jsonOutput += line + QLatin1Char('\n'); } catch (const std::bad_alloc&) { m_jsonOutput.clear(); emit error(i18n("Not enough memory for loading the archive.")); return false; } } if (m_operationMode == List) { // This can only be an header-encrypted archive. - if (m_cliProps->isPasswordPrompt(line)) { + if (isPasswordPrompt(line)) { qCDebug(ARK) << "Detected header-encrypted RAR archive"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); // Process is gone, so we emit finished() manually and we return true. emit finished(false); return true; } setPassword(query.password()); CliPlugin::list(); } } return true; } void CliPlugin::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = nullptr; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (!password().isEmpty()) { // lsar -json exits with error code 1 if the archive is header-encrypted and the password is wrong. if (exitCode == 1) { qCWarning(ARK) << "Wrong password, list() aborted"; emit error(i18n("Wrong password.")); emit finished(false); setPassword(QString()); return; } } // lsar -json exits with error code 2 if the archive is header-encrypted and no password is given as argument. // At this point we are asking a password to the user and we are going to list() again after we get one. // This means that we cannot emit finished here. if (exitCode == 2) { return; } emit finished(true); } void CliPlugin::readJsonOutput() { QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(m_jsonOutput.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { qCDebug(ARK) << "Could not parse json output:" << error.errorString(); return; } const QJsonObject json = jsonDoc.object(); const QJsonObject properties = json.value(QStringLiteral("lsarProperties")).toObject(); const QJsonArray volumes = properties.value(QStringLiteral("XADVolumes")).toArray(); if (volumes.count() > 1) { qCDebug(ARK) << "Detected multivolume archive"; m_numberOfVolumes = volumes.count(); setMultiVolume(true); } QString formatName = json.value(QStringLiteral("lsarFormatName")).toString(); if (formatName == QLatin1String("RAR")) { emit compressionMethodFound(QStringLiteral("RAR4")); } else if (formatName == QLatin1String("RAR 5")) { emit compressionMethodFound(QStringLiteral("RAR5")); } const QJsonArray entries = json.value(QStringLiteral("lsarContents")).toArray(); foreach (const QJsonValue& value, entries) { const QJsonObject currentEntryJson = value.toObject(); Archive::Entry *currentEntry = new Archive::Entry(this); QString filename = currentEntryJson.value(QStringLiteral("XADFileName")).toString(); currentEntry->setProperty("isDirectory", !currentEntryJson.value(QStringLiteral("XADIsDirectory")).isUndefined()); if (currentEntry->isDir()) { filename += QLatin1Char('/'); } currentEntry->setProperty("fullPath", filename); // FIXME: archives created from OSX (i.e. with the __MACOSX folder) list each entry twice, the 2nd time with size 0 currentEntry->setProperty("size", currentEntryJson.value(QStringLiteral("XADFileSize"))); currentEntry->setProperty("compressedSize", currentEntryJson.value(QStringLiteral("XADCompressedSize"))); currentEntry->setProperty("timestamp", currentEntryJson.value(QStringLiteral("XADLastModificationDate")).toVariant()); currentEntry->setProperty("size", currentEntryJson.value(QStringLiteral("XADFileSize"))); const bool isPasswordProtected = (currentEntryJson.value(QStringLiteral("XADIsEncrypted")).toInt() == 1); currentEntry->setProperty("isPasswordProtected", isPasswordProtected); if (isPasswordProtected) { formatName == QLatin1String("RAR 5") ? emit encryptionMethodFound(QStringLiteral("AES256")) : emit encryptionMethodFound(QStringLiteral("AES128")); } // TODO: missing fields emit entry(currentEntry); } } +bool CliPlugin::isPasswordPrompt(const QString &line) +{ + return (line == QLatin1String("This archive requires a password to unpack. Use the -p option to provide one.")); +} + #include "cliplugin.moc" diff --git a/plugins/cliunarchiverplugin/cliplugin.h b/plugins/cliunarchiverplugin/cliplugin.h index 89ebda7d..fb1c4935 100644 --- a/plugins/cliunarchiverplugin/cliplugin.h +++ b/plugins/cliunarchiverplugin/cliplugin.h @@ -1,64 +1,65 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Luke Shumaker * Copyright (C) 2016 Elvis Angelaccio * * 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. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); ~CliPlugin() override; bool list() override; bool extractFiles(const QVector &files, const QString &destinationDirectory, const Kerfuffle::ExtractionOptions &options) override; void resetParsing() override; bool readListLine(const QString &line) override; bool readExtractLine(const QString &line) override; + bool isPasswordPrompt(const QString &line) override; /** * Fill the lsar's json output all in once (useful for unit testing). */ void setJsonOutput(const QString &jsonOutput); protected Q_SLOTS: void readStdout(bool handleAll = false) override; protected: bool handleLine(const QString& line) override; private Q_SLOTS: void processFinished(int exitCode, QProcess::ExitStatus exitStatus) override; private: void setupCliProperties(); void readJsonOutput(); QString m_jsonOutput; }; #endif // CLIPLUGIN_H diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index 8661a078..909b43bc 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,338 +1,365 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "cliinterface.h" #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_clizip.json") CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_parseState(ParseStateHeader) , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_zip plugin"; setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateHeader; m_tempComment.clear(); m_comment.clear(); } // #208091: infozip applies special meanings to some characters, so we // need to escape them with backslashes.see match.c in // infozip's source code QString CliPlugin::escapeFileName(const QString &fileName) const { const QString escapedCharacters(QStringLiteral("[]*?^-\\!")); QString quoted; const int len = fileName.length(); const QLatin1Char backslash('\\'); quoted.reserve(len * 2); for (int i = 0; i < len; ++i) { if (escapedCharacters.contains(fileName.at(i))) { quoted.append(backslash); } quoted.append(fileName.at(i)); } return quoted; } void CliPlugin::setupCliProperties() { qCDebug(ARK) << "Setting up parameters..."; m_cliProps->setProperty("captureProgress", false); m_cliProps->setProperty("addProgram", QStringLiteral("zip")); m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("-r")})); m_cliProps->setProperty("deleteProgram", QStringLiteral("zip")); m_cliProps->setProperty("deleteSwitch", QStringLiteral("-d")); m_cliProps->setProperty("extractProgram", QStringLiteral("unzip")); m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-j")}); m_cliProps->setProperty("listProgram", QStringLiteral("zipinfo")); m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z")}); m_cliProps->setProperty("testProgram", QStringLiteral("unzip")); m_cliProps->setProperty("testSwitch", QStringLiteral("-t")); m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-P$Password")}); m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-$CompressionLevel")); m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/zip"), QStringLiteral("-Z$CompressionMethod")}, {QStringLiteral("application/x-java-archive"), QStringLiteral("-Z$CompressionMethod")}}); m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); - m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral(" password: ")}); - m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("incorrect password")}); m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^No errors detected in compressed data of ")}); - m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")}); - m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")}); + m_cliProps->setProperty("fileExistsFileNameRegExp", QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")}); m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("y"), //Overwrite QStringLiteral("n"), //Skip QStringLiteral("A"), //Overwrite all QStringLiteral("N")}); //Autoskip m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("unsupported compression method")}); - m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("End-of-central-directory signature not found"), - QStringLiteral("didn't find end-of-central-dir signature at end of central dir")}); - m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("write error \\(disk full\\?\\)"), - QStringLiteral("No space left on device")}); } bool CliPlugin::readListLine(const QString &line) { static const QRegularExpression entryPattern(QStringLiteral( "^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$") ); // RegExp to identify the line preceding comments. const QRegularExpression commentPattern(QStringLiteral("^Archive: .*$")); // RegExp to identify the line following comments. const QRegularExpression commentEndPattern(QStringLiteral("^Zip file size: .*$")); switch (m_parseState) { case ParseStateHeader: if (commentPattern.match(line).hasMatch()) { m_parseState = ParseStateComment; } else if (commentEndPattern.match(line).hasMatch()){ m_parseState = ParseStateEntry; } break; case ParseStateComment: if (commentEndPattern.match(line).hasMatch()) { m_parseState = ParseStateEntry; if (!m_tempComment.trimmed().isEmpty()) { m_comment = m_tempComment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_tempComment.append(line + QLatin1Char('\n')); } break; case ParseStateEntry: QRegularExpressionMatch rxMatch = entryPattern.match(line); if (rxMatch.hasMatch()) { Archive::Entry *e = new Archive::Entry(this); e->setProperty("permissions", rxMatch.captured(1)); // #280354: infozip may not show the right attributes for a given directory, so an entry // ending with '/' is actually more reliable than 'd' bein in the attributes. e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); e->setProperty("size", rxMatch.captured(4)); QString status = rxMatch.captured(5); if (status[0].isUpper()) { e->setProperty("isPasswordProtected", true); } e->setProperty("compressedSize", rxMatch.captured(6).toInt()); e->setProperty("method", rxMatch.captured(7)); QString method = convertCompressionMethod(rxMatch.captured(7)); emit compressionMethodFound(method); const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); e->setProperty("timestamp", ts); e->setProperty("fullPath", rxMatch.captured(10)); emit entry(e); } break; } return true; } bool CliPlugin::readExtractLine(const QString &line) { const QRegularExpression rxUnsupCompMethod(QStringLiteral("unsupported compression method (\\d+)")); const QRegularExpression rxUnsupEncMethod(QStringLiteral("need PK compat. v\\d\\.\\d \\(can do v\\d\\.\\d\\)")); const QRegularExpression rxBadCRC(QStringLiteral("bad CRC")); QRegularExpressionMatch unsupCompMethodMatch = rxUnsupCompMethod.match(line); if (unsupCompMethodMatch.hasMatch()) { emit error(i18n("Extraction failed due to unsupported compression method (%1).", unsupCompMethodMatch.captured(1))); return false; } if (rxUnsupEncMethod.match(line).hasMatch()) { emit error(i18n("Extraction failed due to unsupported encryption method.")); return false; } if (rxBadCRC.match(line).hasMatch()) { emit error(i18n("Extraction failed due to one or more corrupt files. Any extracted files may be damaged.")); return false; } return true; } bool CliPlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { qCDebug(ARK) << "Moving" << files.count() << "file(s) to destination:" << destination; m_oldWorkingDir = QDir::currentPath(); m_tempWorkingDir.reset(new QTemporaryDir()); m_tempAddDir.reset(new QTemporaryDir()); QDir::setCurrent(m_tempWorkingDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_subOperation = Extract; connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } int CliPlugin::moveRequiredSignals() const { return 4; } void CliPlugin::continueMoving(bool result) { if (!result) { finishMoving(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Delete; if (!deleteFiles(m_passedFiles)) { finishMoving(false); } break; case Delete: m_subOperation = Add; if (!setMovingAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishMoving(false); } break; case Add: finishMoving(true); break; default: Q_ASSERT(false); } } bool CliPlugin::setMovingAddedFiles() { m_passedFiles = entriesWithoutChildren(m_passedFiles); // If there are more files being moved than 1, we have destination as a destination folder, // otherwise it's new entry full path. if (m_passedFiles.count() > 1) { return setAddedFiles(); } QDir::setCurrent(m_tempAddDir->path()); const Archive::Entry *file = m_passedFiles.at(0); const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + m_passedDestination->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(nullptr, m_passedDestination->name()); // We have to exclude file name from destination path in order to pass it to addFiles method. const QString destinationPath = m_passedDestination->fullPath(); const int slashCount = destinationPath.count(QLatin1Char('/')); if (slashCount > 1 || (slashCount == 1 && !destinationPath.endsWith(QLatin1Char('/')))) { int destinationLength = destinationPath.count(); bool iteratedChar = false; do { destinationLength--; if (destinationPath.at(destinationLength) != QLatin1Char('/')) { iteratedChar = true; } } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/'))); m_passedDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1)); } else { // ...unless the destination path is already a single folder, e.g. "dir/", or a file, e.g. "foo.txt". // In this case we're going to add to the root, so we just need to set a null destination. m_passedDestination = nullptr; } return true; } void CliPlugin::finishMoving(bool result) { disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); emit progress(1.0); emit finished(result); cleanUp(); } QString CliPlugin::convertCompressionMethod(const QString &method) { if (method == QLatin1String("stor")) { return QStringLiteral("Store"); } else if (method.startsWith(QLatin1String("def"))) { return QStringLiteral("Deflate"); } else if (method == QLatin1String("d64N")) { return QStringLiteral("Deflate64"); } else if (method == QLatin1String("bzp2")) { return QStringLiteral("BZip2"); } else if (method == QLatin1String("lzma")) { return QStringLiteral("LZMA"); } else if (method == QLatin1String("ppmd")) { return QStringLiteral("PPMd"); } else if (method == QLatin1String("u095")) { return QStringLiteral("XZ"); } else if (method == QLatin1String("u099")) { emit encryptionMethodFound(QStringLiteral("AES")); return i18nc("referred to compression method", "unknown"); } return method; } +bool CliPlugin::isPasswordPrompt(const QString &line) +{ + return line.endsWith(QLatin1String(" password: ")); +} + +bool CliPlugin::isWrongPasswordMsg(const QString &line) +{ + return line.endsWith(QLatin1String("incorrect password")); +} + +bool CliPlugin::isCorruptArchiveMsg(const QString &line) +{ + return (line.contains(QLatin1String("End-of-central-directory signature not found")) || + line.contains(QLatin1String("didn't find end-of-central-dir signature at end of central dir"))); +} + +bool CliPlugin::isDiskFullMsg(const QString &line) +{ + return (line.contains(QLatin1String("No space left on device")) || + line.contains(QLatin1String("write error (disk full?)"))); +} + +bool CliPlugin::isFileExistsMsg(const QString &line) +{ + return (line.startsWith(QLatin1String("replace ")) && + line.endsWith(QLatin1String("? [y]es, [n]o, [A]ll, [N]one, [r]ename: "))); +} + +bool CliPlugin::isFileExistsFileName(const QString &line) +{ + return (line.startsWith(QLatin1String("replace ")) && + line.endsWith(QLatin1String("? [y]es, [n]o, [A]ll, [N]one, [r]ename: "))); +} + #include "cliplugin.moc" diff --git a/plugins/clizipplugin/cliplugin.h b/plugins/clizipplugin/cliplugin.h index 5c5325a2..d9e7832c 100644 --- a/plugins/clizipplugin/cliplugin.h +++ b/plugins/clizipplugin/cliplugin.h @@ -1,64 +1,70 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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. */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "cliinterface.h" using namespace Kerfuffle; class KERFUFFLE_EXPORT CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); ~CliPlugin() override; void resetParsing() override; QString escapeFileName(const QString &fileName) const override; bool readListLine(const QString &line) override; bool readExtractLine(const QString &line) override; + bool isPasswordPrompt(const QString &line) override; + bool isWrongPasswordMsg(const QString &line) override; + bool isCorruptArchiveMsg(const QString &line) override; + bool isDiskFullMsg(const QString &line) override; + bool isFileExistsMsg(const QString &line) override; + bool isFileExistsFileName(const QString &line) override; bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) override; int moveRequiredSignals() const override; private Q_SLOTS: void continueMoving(bool result); private: void setupCliProperties(); bool setMovingAddedFiles(); void finishMoving(bool result); QString convertCompressionMethod(const QString &method); enum ParseState { ParseStateHeader = 0, ParseStateComment, ParseStateEntry } m_parseState; int m_linesComment; QString m_tempComment; }; #endif // CLIPLUGIN_H