diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 7b843f46..58f848b9 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,727 +1,752 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } } void CliInterface::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); Q_ASSERT(m_param.contains(PreservePathSwitch)); Q_ASSERT(m_param.contains(FileExistsExpression)); Q_ASSERT(m_param.contains(FileExistsInput)); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); } bool CliInterface::list() { cacheParameterList(); m_operationMode = List; QStringList args = m_param.value(ListArgs).toStringList(); substituteListVariables(args); if (!runProcess(m_param.value(ListProgram).toString(), args)) { failOperation(); return false; } return true; } bool CliInterface::copyFiles(const QList & files, const QString & destinationDirectory, ExtractionOptions options) { kDebug(); cacheParameterList(); m_operationMode = Copy; //start preparing the argument list QStringList args = m_param.value(ExtractArgs).toStringList(); //now replace the various elements in the list for (int i = 0; i < args.size(); ++i) { QString argument = args.at(i); kDebug() << "Processing argument " << argument; if (argument == QLatin1String( "$Archive" )) { args[i] = filename(); } if (argument == QLatin1String( "$PreservePathSwitch" )) { QStringList replacementFlags = m_param.value(PreservePathSwitch).toStringList(); Q_ASSERT(replacementFlags.size() == 2); bool preservePaths = options.value(QLatin1String( "PreservePaths" )).toBool(); QString theReplacement; if (preservePaths) { theReplacement = replacementFlags.at(0); } else { theReplacement = replacementFlags.at(1); } if (theReplacement.isEmpty()) { args.removeAt(i); --i; //decrement to compensate for the variable we removed } else { //but in this case we don't have to decrement, we just //replace it args[i] = theReplacement; } } if (argument == QLatin1String( "$PasswordSwitch" )) { //if the PasswordSwitch argument has been added, we at least //assume that the format of the switch has been added as well Q_ASSERT(m_param.contains(PasswordSwitch)); //we will decrement i afterwards args.removeAt(i); //if we get a hint about this being a password protected archive, ask about //the password in advance. if ((options.value(QLatin1String("PasswordProtectedHint")).toBool()) && (password().isEmpty())) { kDebug() << "Password hint enabled, querying user"; Kerfuffle::PasswordNeededQuery query(filename()); userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { failOperation(); return false; } setPassword(query.password()); } QString pass = password(); if (!pass.isEmpty()) { QStringList theSwitch = m_param.value(PasswordSwitch).toStringList(); for (int j = 0; j < theSwitch.size(); ++j) { //get the argument part QString newArg = theSwitch.at(j); //substitute the $Path newArg.replace(QLatin1String( "$Password" ), pass); //put it in the arg list args.insert(i + j, newArg); ++i; } } --i; //decrement to compensate for the variable we replaced } if (argument == QLatin1String( "$RootNodeSwitch" )) { //if the RootNodeSwitch argument has been added, we at least //assume that the format of the switch has been added as well Q_ASSERT(m_param.contains(RootNodeSwitch)); //we will decrement i afterwards args.removeAt(i); QString rootNode; if (options.contains(QLatin1String( "RootNode" ))) { rootNode = options.value(QLatin1String( "RootNode" )).toString(); kDebug() << "Set root node " << rootNode; } if (!rootNode.isEmpty()) { QStringList theSwitch = m_param.value(RootNodeSwitch).toStringList(); for (int j = 0; j < theSwitch.size(); ++j) { //get the argument part QString newArg = theSwitch.at(j); //substitute the $Path newArg.replace(QLatin1String( "$Path" ), rootNode); //put it in the arg list args.insert(i + j, newArg); ++i; } } --i; //decrement to compensate for the variable we replaced } if (argument == QLatin1String( "$Files" )) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { args.insert(i + j, escapeFileName(files.at(j).toString())); ++i; } --i; } } kDebug() << "Setting current dir to " << destinationDirectory; QDir::setCurrent(destinationDirectory); if (!runProcess(m_param.value(ExtractProgram).toString(), args)) { failOperation(); return false; } return true; } bool CliInterface::addFiles(const QStringList & files, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QString globalWorkDir = options.value(QLatin1String( "GlobalWorkDir" )).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { kDebug() << "GlobalWorkDir is set, changing dir to " << globalWorkDir; QDir::setCurrent(globalWorkDir); } //start preparing the argument list QStringList args = m_param.value(AddArgs).toStringList(); //now replace the various elements in the list for (int i = 0; i < args.size(); ++i) { const QString argument = args.at(i); kDebug() << "Processing argument " << argument; + if (argument == QLatin1String( "$CompressionLevelSwitch" )) { + QStringList compressionLevelSwitches = m_param.value(CompressionLevelSwitches).toStringList(); + QString compressionLevel = options.value(QLatin1String( "CompressionLevel")).toString(); + + QString theReplacement; + if (compressionLevel == "Store") { + theReplacement = compressionLevelSwitches.at(0); + } + if (compressionLevel == "Normal") { + theReplacement = compressionLevelSwitches.at(1); + } + if (compressionLevel == "Maximum") { + theReplacement = compressionLevelSwitches.at(2); + } + + if (theReplacement.isEmpty()) { + args.removeAt(i); + --i; //decrement to compensate for the variable we removed + } else { + //but in this case we don't have to decrement, we just + //replace it + args[i] = theReplacement; + } + } + if (argument == QLatin1String( "$Archive" )) { args[i] = filename(); } if (argument == QLatin1String( "$Files" )) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically // TODO: this kind of call should be moved upwards in the // class hierarchy to avoid code duplication const QString relativeName = workDir.relativeFilePath(files.at(j)); args.insert(i + j, relativeName); ++i; } --i; } } if (!runProcess(m_param.value(AddProgram).toString(), args)) { failOperation(); return false; } return true; } bool CliInterface::deleteFiles(const QList & files) { cacheParameterList(); m_operationMode = Delete; //start preparing the argument list QStringList args = m_param.value(DeleteArgs).toStringList(); //now replace the various elements in the list for (int i = 0; i < args.size(); ++i) { QString argument = args.at(i); kDebug() << "Processing argument " << argument; if (argument == QLatin1String( "$Archive" )) { args[i] = filename(); } else if (argument == QLatin1String( "$Files" )) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { args.insert(i + j, escapeFileName(files.at(j).toString())); ++i; } --i; } } m_removedFiles = files; if (!runProcess(m_param.value(DeleteProgram).toString(), args)) { failOperation(); return false; } return true; } bool CliInterface::runProcess(const QString& programName, const QStringList& arguments) { const QString programPath(KStandardDirs::findExe(programName)); if (programPath.isEmpty()) { error(i18nc("@info", "Failed to locate program %1 in PATH.", programName)); return false; } kDebug() << "Executing" << programPath << arguments; if (m_process) { m_process->waitForFinished(); delete m_process; } #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, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection); connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(processFinished(int,QProcess::ExitStatus)), Qt::DirectConnection); m_stdOutData.clear(); m_process->start(); #ifdef Q_OS_WIN bool ret = m_process->waitForFinished(-1); #else QEventLoop loop; bool ret = (loop.exec(QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents) == 0); #endif delete m_process; m_process = 0; return ret; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitCode) Q_UNUSED(exitStatus) kDebug(); //if the m_process pointer is gone, then there is nothing to worry //about here if (!m_process) { return; } if (m_operationMode == Delete) { foreach(const QVariant& v, m_removedFiles) { entryRemoved(v.toString()); } } //handle all the remaining data in the process readStdout(true); progress(1.0); if (m_operationMode == Add) { list(); return; } //and we're finished finished(true); } void CliInterface::failOperation() { kDebug(); doKill(); finished(false); } 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 Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } //if the process is still not finished (m_process is appearantly not //set to NULL if here), then the operation should definitely not be in //the main thread as this would freeze everything. assert this. Q_ASSERT(QThread::currentThread() != QApplication::instance()->thread()); 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. bool foundErrorMessage = (checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns) || checkForErrorMessage(QLatin1String( lines.last() ), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForFileExistsMessage(QLatin1String( lines.last() ))); if (foundErrorMessage) { handleAll = true; } //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()) { handleLine(QString::fromLocal8Bit(line)); } } } void CliInterface::handleLine(const QString& line) { if ((m_operationMode == Copy || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos != -1 && pos > 1) { int percentage = line.mid(pos - 2, 2).toInt(); progress(float(percentage) / 100); return; } } if (m_operationMode == Copy) { if (checkForPasswordPromptMessage(line)) { kDebug() << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { failOperation(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { kDebug() << "Wrong password!"; error(i18n("Incorrect password.")); failOperation(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { kDebug() << "Error in extraction!!"; error(i18n("Extraction failed because of an unexpected error.")); failOperation(); return; } if (handleFileExistsMessage(line)) { return; } } if (m_operationMode == List) { if (checkForPasswordPromptMessage(line)) { kDebug() << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { failOperation(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { kDebug() << "Wrong password!"; error(i18n("Incorrect password.")); failOperation(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { kDebug() << "Error in extraction!!"; error(i18n("Extraction failed because of an unexpected error.")); failOperation(); return; } if (handleFileExistsMessage(line)) { return; } readListLine(line); return; } } bool CliInterface::checkForPasswordPromptMessage(const QString& line) { const QString passwordPromptPattern(m_param.value(PasswordPromptPattern).toString()); if (passwordPromptPattern.isEmpty()) return false; if (m_passwordPromptPattern.isEmpty()) { m_passwordPromptPattern.setPattern(m_param.value(PasswordPromptPattern).toString()); } if (m_passwordPromptPattern.indexIn(line) != -1) { return true; } return false; } bool CliInterface::checkForFileExistsMessage(const QString& line) { if (m_existsPattern.isEmpty()) { m_existsPattern.setPattern(m_param.value(FileExistsExpression).toString()); } if (m_existsPattern.indexIn(line) != -1) { kDebug() << "Detected file existing!! Filename " << m_existsPattern.cap(1); return true; } return false; } bool CliInterface::handleFileExistsMessage(const QString& line) { if (!checkForFileExistsMessage(line)) { return false; } const QString filename = m_existsPattern.cap(1); Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + filename); query.setNoRenameMode(true); userQuery(&query); kDebug() << "Waiting response"; query.waitForResponse(); kDebug() << "Finished response"; QString responseToProcess; const QStringList choices = m_param.value(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()) { 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::checkForErrorMessage(const QString& line, int parameterIndex) { static QHash > patternCache; QList patterns; if (patternCache.contains(parameterIndex)) { patterns = patternCache.value(parameterIndex); } else { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString& rawPattern, m_param.value(parameterIndex).toStringList()) { patterns << QRegExp(rawPattern); } patternCache[parameterIndex] = patterns; } foreach(const QRegExp& pattern, patterns) { if (pattern.indexIn(line) != -1) { return true; } } return false; } bool CliInterface::doKill() { if (m_process) { // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); } return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } void CliInterface::substituteListVariables(QStringList& params) { for (int i = 0; i < params.size(); ++i) { const QString parameter = params.at(i); if (parameter == QLatin1String( "$Archive" )) { params[i] = filename(); } } } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); kDebug() << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } } #include "cliinterface.moc" diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 2d4187cd..91ce6aba 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,339 +1,348 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" #include "kerfuffle_export.h" #include #include class KProcess; class KPtyProcess; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QString * The name to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, ///////////////[ EXTRACT ]///////////// /** * QString * The name to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $RootNodeSwitch - the internal work dir in the archive (for example * when the user has dragged a folder from the archive and wants it * extracted relative to it) * $PasswordSwitch - the switch setting the password. Note that this * will not be inserted unless the listing function has emitted an * entry with the IsPasswordProtected property set to true. */ ExtractArgs, /** * Bool (default false) * When passing directories to the extract program, do not * include trailing slashes * e.g. if the user selected "foo/" and "foo/bar" in the gui, the * paths "foo" and "foo/bar" will be sent to the program. */ NoTrailingSlashes, /** * QStringList * This should be a qstringlist with either two elements. The first * string is what PreservePathSwitch in the ExtractArgs will be replaced * with if PreservePath is True/enabled. The second is for the disabled * case. An empty string means that the argument will not be used in * that case. * Example: for rar, "x" means extract with full paths, and "e" means * extract without full paths. in this case we will use the stringlist * ("x", "e"). Or, for another format that might use the switch * "--extractFull" for preservePaths, and nothing otherwise: we use the * stringlist ("--extractFull", "") */ PreservePathSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Path will be * substituted for the path string. * Example: ("--internalPath=$Path) * or ("--path", "$Path") */ RootNodeSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QString * This is a regexp, defining how to recognize a "File already exists" * prompt when extracting. It should have one captured string, which is * the filename of the file/folder that already exists. */ FileExistsExpression, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, ///////////////[ DELETE ]///////////// /** * QString * The name to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QString * The name to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ - AddArgs + AddArgs, + /** + * QStringList + * The arguments that are passed to the program above for + * setting the compression level. + * First entry is "Store" + * Second entry is "Normal" + * Third entry is "Maximum" + */ + CompressionLevelSwitches }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: enum OperationMode { List, Copy, Add, Delete }; OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual bool list(); virtual bool copyFiles(const QList & files, const QString & destinationDirectory, ExtractionOptions options); virtual bool addFiles(const QStringList & files, const CompressionOptions& options); virtual bool deleteFiles(const QList & files); virtual ParameterList parameterList() const = 0; virtual bool readListLine(const QString &line) = 0; bool doKill(); bool doSuspend(); bool doResume(); /** * Returns the list of characters which are preceded by a * backslash when a file name in an archive is passed to * a program. * * @see setEscapedCharacters(). */ QString escapedCharacters(); /** * Sets which characters will be preceded by a backslash when * a file name in an archive is passed to a program. * * @see escapedCharacters(). */ void setEscapedCharacters(const QString& characters); private: void substituteListVariables(QStringList& params); void cacheParameterList(); /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); bool checkForFileExistsMessage(const QString& line); bool handleFileExistsMessage(const QString& filename); bool checkForErrorMessage(const QString& line, int parameterIndex); void handleLine(const QString& line); void failOperation(); /** * Run @p programName with the given @p arguments. * The method waits until @p programName is finished to exit. * * @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 ran correctly, * @c false otherwise. */ bool runProcess(const QString& programName, const QStringList& arguments); /** * 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; /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); QByteArray m_stdOutData; QRegExp m_existsPattern; QRegExp m_passwordPromptPattern; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif ParameterList m_param; QVariantList m_removedFiles; private slots: void readStdout(bool handleAll = false); void processFinished(int exitCode, QProcess::ExitStatus exitStatus); }; } #endif /* CLIINTERFACE_H */ diff --git a/part/part.cpp b/part/part.cpp index 245a4ee3..876f950e 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,904 +1,905 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterface.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/archive.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY(Factory, registerPlugin();) K_EXPORT_PLUGIN(Factory("ark")) namespace Ark { Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_model(new ArchiveModel(this)), m_splitter(0), m_previewDir(0), m_busy(false), m_jobTracker(0) { Q_UNUSED(args) setComponentData(Factory::componentData(), false); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); setWidget(m_splitter); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); m_splitter->addWidget(m_view); m_splitter->addWidget(m_infoPanel); QList splitterSizes = ArkSettings::splitterSizes(); if (splitterSizes.isEmpty()) { splitterSizes.append(200); splitterSizes.append(100); } m_splitter->setSizes(splitterSizes); setupView(); setupActions(); connect(m_model, SIGNAL(loadingStarted()), this, SLOT(slotLoadingStarted())); connect(m_model, SIGNAL(loadingFinished(KJob*)), this, SLOT(slotLoadingFinished(KJob*))); connect(m_model, SIGNAL(droppedFiles(QStringList,QString)), this, SLOT(slotAddFiles(QStringList,QString))); connect(m_model, SIGNAL(error(QString,QString)), this, SLOT(slotError(QString,QString))); connect(this, SIGNAL(busy()), this, SLOT(setBusyGui())); connect(this, SIGNAL(ready()), this, SLOT(setReadyGui())); connect(this, SIGNAL(completed()), this, SLOT(setFileNameFromArchive())); m_statusBarExtension = new KParts::StatusBarExtension(this); new DndExtractAdaptor(this); QDBusConnection::sessionBus().registerObject(QLatin1String( "/DndExtract" ), this); setXMLFile(QLatin1String( "ark_part.rc" )); } Part::~Part() { updateSplitterSizes(); m_extractFilesAction->menu()->deleteLater(); delete m_previewDir; m_previewDir = 0; } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, SIGNAL(result(KJob*)), this, SIGNAL(ready())); } // TODO: One should construct a KUrl out of localPath in order to be able to handle // non-local destinations (ie. trash:/ or a remote location) // See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { kDebug() << "Extract to " << localPath; if (!m_model) { return; } if (m_view->selectionModel()->selectedRows().count() != 1) { m_view->selectionModel()->setCurrentIndex(m_view->currentIndex(), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } if (m_view->selectionModel()->selectedRows().count() != 1) { return; } QVariant internalRoot; kDebug() << "valid " << m_view->currentIndex().parent().isValid(); if (m_view->currentIndex().parent().isValid()) { internalRoot = m_model->entryForIndex(m_view->currentIndex().parent()).value(InternalID); } if (internalRoot.isNull()) { //we have the special case valid parent, but the parent does not //actually correspond to an item in the archive, but an automatically //created folder. for now, we will just use the filename of the node //instead, but for plugins that rely on a non-filename value as the //InternalId, this WILL break things. TODO find a solution internalRoot = m_model->entryForIndex(m_view->currentIndex().parent()).value(FileName); } QList files = selectedFilesWithChildren(); if (files.isEmpty()) { return; } kDebug() << "selected files are " << files; Kerfuffle::ExtractionOptions options; options[QLatin1String( "PreservePaths" )] = true; if (!internalRoot.isNull()) { options[QLatin1String("RootNode")] = internalRoot; } ExtractJob *job = m_model->extractFiles(files, localPath, options); registerJob(job); connect(job, SIGNAL(result(KJob*)), this, SLOT(slotExtractionDone(KJob*))); job->start(); } void Part::setupView() { m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(updateActions())); connect(m_view->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(selectionChanged())); //TODO: fix an actual eventhandler connect(m_view, SIGNAL(itemTriggered(QModelIndex)), this, SLOT(slotPreview(QModelIndex))); connect(m_model, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(adjustColumns())); } void Part::setupActions() { KToggleAction *showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QLatin1String( "show-infopanel" ), showInfoPanelAction); showInfoPanelAction->setChecked(m_splitter->sizes().at(1) > 0); connect(showInfoPanelAction, SIGNAL(triggered(bool)), this, SLOT(slotToggleInfoPanel(bool))); m_saveAsAction = KStandardAction::saveAs(this, SLOT(slotSaveAs()), actionCollection()); m_previewAction = actionCollection()->addAction(QLatin1String( "preview" )); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(KIcon( QLatin1String( "document-preview-archive" ))); m_previewAction->setStatusTip(i18n("Click to preview the selected file")); connect(m_previewAction, SIGNAL(triggered(bool)), this, SLOT(slotPreview())); m_extractFilesAction = actionCollection()->addAction(QLatin1String( "extract" )); m_extractFilesAction->setText(i18n("E&xtract")); m_extractFilesAction->setIcon(KIcon( QLatin1String( "archive-extract" ))); m_extractFilesAction->setStatusTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); m_extractFilesAction->setShortcut(QKeySequence( QLatin1String( "Ctrl+E" ) )); connect(m_extractFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotExtractFiles())); m_addFilesAction = actionCollection()->addAction(QLatin1String( "add" )); m_addFilesAction->setIcon(KIcon( QLatin1String( "archive-insert" ))); m_addFilesAction->setText(i18n("Add &File...")); m_addFilesAction->setStatusTip(i18n("Click to add files to the archive")); connect(m_addFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotAddFiles())); m_addDirAction = actionCollection()->addAction(QLatin1String( "add-dir" )); m_addDirAction->setIcon(KIcon( QLatin1String( "archive-insert-directory" ))); m_addDirAction->setText(i18n("Add Fo&lder...")); m_addDirAction->setStatusTip(i18n("Click to add a folder to the archive")); connect(m_addDirAction, SIGNAL(triggered(bool)), this, SLOT(slotAddDir())); m_deleteFilesAction = actionCollection()->addAction(QLatin1String( "delete" )); m_deleteFilesAction->setIcon(KIcon( QLatin1String( "archive-remove" ))); m_deleteFilesAction->setText(i18n("De&lete")); m_deleteFilesAction->setShortcut(Qt::Key_Delete); m_deleteFilesAction->setStatusTip(i18n("Click to delete the selected files")); connect(m_deleteFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotDeleteFiles())); updateActions(); } void Part::updateActions() { bool isWritable = m_model->archive() && (!m_model->archive()->isReadOnly()); m_previewAction->setEnabled(!isBusy() && (m_view->selectionModel()->selectedRows().count() == 1) && isPreviewable(m_view->selectionModel()->currentIndex())); m_extractFilesAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_addFilesAction->setEnabled(!isBusy() && isWritable); m_addDirAction->setEnabled(!isBusy() && isWritable); m_deleteFilesAction->setEnabled(!isBusy() && (m_view->selectionModel()->selectedRows().count() > 0) && isWritable); QMenu *menu = m_extractFilesAction->menu(); if (!menu) { menu = new QMenu; m_extractFilesAction->setMenu(menu); connect(menu, SIGNAL(triggered(QAction*)), this, SLOT(slotQuickExtractFiles(QAction*))); // Remember to keep this action's properties as similar to // m_extractFilesAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(m_extractFilesAction->icon()); extractTo->setStatusTip(m_extractFilesAction->statusTip()); connect(extractTo, SIGNAL(triggered(bool)), SLOT(slotExtractFiles())); menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(KIcon( QLatin1String( "archive-extract" ))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KGlobal::config(), "DirSelect Dialog"); const QStringList dirHistory = conf.readPathEntry("History Items", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const KUrl dirUrl(dirHistory.at(i)); QAction *newAction = menu->addAction(dirUrl.pathOrUrl()); newAction->setData(dirUrl.pathOrUrl()); } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { kDebug() << "Extract to " << triggeredAction->data().toString(); const QString userDestination = triggeredAction->data().toString(); QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } Kerfuffle::ExtractionOptions options; options[QLatin1String( "PreservePaths" )] = true; QList files = selectedFiles(); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, SIGNAL(result(KJob*)), this, SLOT(slotExtractionDone(KJob*))); job->start(); } } bool Part::isPreviewable(const QModelIndex& index) const { return index.isValid() && (!m_model->entryForIndex(index)[ IsDirectory ].toBool()); } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } KAboutData* Part::createAboutData() { return new KAboutData("ark", 0, ki18n("ArkPart"), "3.0"); } bool Part::openFile() { const QString localFile(localFilePath()); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QLatin1String("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { KMessageBox::error(NULL, i18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { int overwrite = KMessageBox::questionYesNo(NULL, i18nc("@info", "The archive %1 already exists. Would you like to open it instead?", localFile), i18nc("@title:window", "File Exists"), KGuiItem(i18n("Open File")), KStandardGuiItem::cancel()); if (overwrite == KMessageBox::No) { return false; } } } else { if (!localFileInfo.exists()) { KMessageBox::sorry(NULL, i18nc("@info", "The archive %1 was not found.", localFile), i18nc("@title:window", "Error Opening Archive")); return false; } } Kerfuffle::Archive *archive = Kerfuffle::factory(localFile); if ((!archive) || ((creatingNewArchive) && (archive->isReadOnly()))) { QStringList mimeTypeList; QHash mimeTypes; if (creatingNewArchive) { mimeTypeList = Kerfuffle::supportedWriteMimeTypes(); } else { mimeTypeList = Kerfuffle::supportedMimeTypes(); } foreach(const QString& mime, mimeTypeList) { KMimeType::Ptr mimePtr(KMimeType::mimeType(mime)); if (mimePtr) { // Key = "application/zip", Value = "Zip Archive" mimeTypes[mime] = mimePtr->comment(); } } QStringList mimeComments(mimeTypes.values()); mimeComments.sort(); bool ok; QString item; if (creatingNewArchive) { item = KInputDialog::getItem(i18nc("@title:window", "Invalid Archive Type"), i18nc("@info", "Ark cannot create archives of the type you have chosen.Please choose another archive type below."), mimeComments, 0, false, &ok); } else { item = KInputDialog::getItem(i18nc("@title:window", "Unable to Determine Archive Type"), i18nc("@info", "Ark was unable to determine the archive type of the filename.Please choose the correct archive type below."), mimeComments, 0, false, &ok); } if ((!ok) || (item.isEmpty())) { return false; } archive = Kerfuffle::factory(localFile, mimeTypes.key(item)); } if (!archive) { KMessageBox::sorry(NULL, i18nc("@info", "Ark was not able to open the archive %1. No plugin capable of handling the file was found.", localFile), i18nc("@title:window", "Error Opening Archive")); return false; } KJob *job = m_model->setArchive(archive); registerJob(job); job->start(); m_infoPanel->setIndex(QModelIndex()); if (archive != 0 && arguments().metaData()[QLatin1String( "showExtractDialog" )] == QLatin1String( "true" )) { QTimer::singleShot(0, this, SLOT(slotExtractFiles())); } return (archive != 0); } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } void Part::slotLoadingStarted() { } void Part::slotLoadingFinished(KJob *job) { kDebug(); if (job->error()) { if (arguments().metaData()[QLatin1String( "createNewArchive" )] != QLatin1String( "true" )) { KMessageBox::sorry(NULL, i18nc("@info", "Loading the archive %1 failed with the following error: %2", localFilePath(), job->errorText()), i18nc("@title:window", "Error Opening Archive")); } } m_view->sortByColumn(0, Qt::AscendingOrder); m_view->expandToDepth(0); // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); } void Part::setReadyGui() { kDebug(); QApplication::restoreOverrideCursor(); m_busy = false; m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { kDebug(); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotPreview() { slotPreview(m_view->selectionModel()->currentIndex()); } void Part::slotPreview(const QModelIndex & index) { if (!m_previewDir) { m_previewDir = new KTempDir(); } if (!isPreviewable(index)) { return; } const ArchiveEntry& entry = m_model->entryForIndex(index); if (!entry.isEmpty()) { Kerfuffle::ExtractionOptions options; options[QLatin1String( "PreservePaths" )] = true; ExtractJob *job = m_model->extractFile(entry[ InternalID ], m_previewDir->name(), options); registerJob(job); connect(job, SIGNAL(result(KJob*)), this, SLOT(slotPreviewExtracted(KJob*))); job->start(); } } void Part::slotPreviewExtracted(KJob *job) { // FIXME: the error checking here isn't really working // if there's an error or an overwrite dialog, // the preview dialog will be launched anyway if (!job->error()) { const ArchiveEntry& entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); QString fullName = m_previewDir->name() + QLatin1Char('/') + entry[FileName].toString(); // 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. fullName.remove(QLatin1String("../")); ArkViewer::view(fullName, widget()); } else { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractFiles() { if (!m_model) { return; } QWeakPointer dialog = new Kerfuffle::ExtractionDialog; if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QFileInfo(m_model->archive()->fileName()).path()); if (dialog.data()->exec()) { //this is done to update the quick extract menu updateActions(); QVariantList files; //if the user has chosen to extract only selected entries, fetch these //from the listview if (!dialog.data()->extractAllFiles()) { files = selectedFilesWithChildren(); } kDebug() << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QLatin1String("PreservePaths")] = true; } options[QLatin1String("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().pathOrUrl(); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, SIGNAL(result(KJob*)), this, SLOT(slotExtractionDone(KJob*))); job->start(); } delete dialog.data(); } QList Part::selectedFilesWithChildren() { Q_ASSERT(m_model); QModelIndexList toIterate = m_view->selectionModel()->selectedRows(); for (int i = 0; i < toIterate.size(); ++i) { QModelIndex index = toIterate.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!toIterate.contains(child)) { toIterate << child; } } } QVariantList ret; foreach(const QModelIndex & index, toIterate) { const ArchiveEntry& entry = m_model->entryForIndex(index); if (entry.contains(InternalID)) { ret << entry[ InternalID ]; } } return ret; } QList Part::selectedFiles() { QStringList toSort; foreach(const QModelIndex & index, m_view->selectionModel()->selectedRows()) { const ArchiveEntry& entry = m_model->entryForIndex(index); toSort << entry[ InternalID ].toString(); } toSort.sort(); QVariantList ret; foreach(const QString &i, toSort) { ret << i; } return ret; } void Part::slotExtractionDone(KJob* job) { kDebug(); if (job->error()) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QLatin1String("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { KUrl destinationDirectory(extractJob->destinationDirectory()); destinationDirectory.cleanPath(); KRun::runUrl(destinationDirectory, QLatin1String("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { kDebug(); m_view->header()->setResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) { if (filesToAdd.isEmpty()) { return; } kDebug() << "Adding " << filesToAdd << " to " << path; kDebug() << "Warning, for now the path argument is not implemented"; QStringList cleanFilesToAdd(filesToAdd); for (int i = 0; i < cleanFilesToAdd.size(); ++i) { QString& file = cleanFilesToAdd[i]; if (QFileInfo(file).isDir()) { if (!file.endsWith(QLatin1Char( '/' ))) { file += QLatin1Char( '/' ); } } } CompressionOptions options; QString firstPath = cleanFilesToAdd.first(); if (firstPath.right(1) == QLatin1String( "/" )) { firstPath.chop(1); } firstPath = QFileInfo(firstPath).dir().absolutePath(); kDebug() << "Detected relative path to be " << firstPath; options[QLatin1String( "GlobalWorkDir" )] = firstPath; + options[QLatin1String( "CompressionLevel") ] = "Maximum"; AddJob *job = m_model->addFiles(cleanFilesToAdd, options); if (!job) { return; } connect(job, SIGNAL(result(KJob*)), this, SLOT(slotAddFilesDone(KJob*))); registerJob(job); job->start(); } void Part::slotAddFiles() { kDebug(); // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. const QStringList filesToAdd = KFileDialog::getOpenFileNames(KUrl("kfiledialog:///ArkAddFiles"), QString(), widget()->parentWidget(), i18nc("@title:window", "Add Files")); slotAddFiles(filesToAdd); } void Part::slotAddDir() { kDebug(); const QString dirToAdd = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///ArkAddFiles"), widget(), i18nc("@title:window", "Add Folder")); if (!dirToAdd.isEmpty()) { slotAddFiles(QStringList() << dirToAdd); } } void Part::slotAddFilesDone(KJob* job) { kDebug(); if (job->error()) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFilesDone(KJob* job) { kDebug(); if (job->error()) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFiles() { kDebug(); const int reallyDelete = KMessageBox::questionYesNo(NULL, i18n("Deleting these files is not undoable. Are you sure you want to do this?"), i18nc("@title:window", "Delete files"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(selectedFilesWithChildren()); connect(job, SIGNAL(result(KJob*)), this, SLOT(slotDeleteFilesDone(KJob*))); registerJob(job); job->start(); } void Part::slotToggleInfoPanel(bool visible) { QList splitterSizes; if (visible) { splitterSizes = ArkSettings::splitterSizesWithBothWidgets(); } else { splitterSizes = m_splitter->sizes(); ArkSettings::setSplitterSizesWithBothWidgets(splitterSizes); splitterSizes[1] = 0; } m_splitter->setSizes(splitterSizes); updateSplitterSizes(); } void Part::updateSplitterSizes() { ArkSettings::setSplitterSizes(m_splitter->sizes()); ArkSettings::self()->writeConfig(); } void Part::slotSaveAs() { KUrl saveUrl = KFileDialog::getSaveUrl(KUrl(QLatin1String( "kfiledialog:///ArkSaveAs/" ) + url().fileName()), QString(), widget()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { if (KIO::NetAccess::exists(saveUrl, KIO::NetAccess::DestinationSide, widget())) { int overwrite = KMessageBox::warningContinueCancel(widget(), i18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } KUrl srcUrl = KUrl::fromPath(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), i18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); if (!KIO::NetAccess::synchronousRun(copyJob, widget())) { KMessageBox::error(widget(), i18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.pathOrUrl())); } } } } // namespace Ark diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index 2e2b6cc3..9a6472b9 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,180 +1,181 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include #include using namespace Kerfuffle; CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_archiveType(ArchiveType7z) , m_state(ReadStateHeader) { } CliPlugin::~CliPlugin() { } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { //p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[AddProgram] = QLatin1String( "7z" ); p[ListArgs] = QStringList() << QLatin1String( "l" ) << QLatin1String( "-slt" ) << QLatin1String( "$Archive" ); p[ExtractArgs] = QStringList() << QLatin1String( "$PreservePathSwitch" ) << QLatin1String( "$PasswordSwitch" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[PreservePathSwitch] = QStringList() << QLatin1String( "x" ) << QLatin1String( "e" ); p[PasswordSwitch] = QStringList() << QLatin1String( "-p$Password" ); p[FileExistsExpression] = QLatin1String( "already exists. Overwrite with" ); p[WrongPasswordPatterns] = QStringList() << QLatin1String( "Wrong password" ); - p[AddArgs] = QStringList() << QLatin1String( "a" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); + p[AddArgs] = QStringList() << QLatin1String( "a" ) << QLatin1String( "$CompressionLevelSwitch" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[DeleteArgs] = QStringList() << QLatin1String( "d" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[FileExistsInput] = QStringList() << QLatin1String( "Y" ) //overwrite << QLatin1String( "N" )//skip << QLatin1String( "A" ) //overwrite all << QLatin1String( "S" ) //autoskip << QLatin1String( "Q" ) //cancel ; + p[CompressionLevelSwitches] = QStringList() << QLatin1String( "-mx=0" ) << QLatin1String( "-mx=5" ) << QLatin1String("-mx=9" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) :"); } return p; } bool CliPlugin::readListLine(const QString& line) { static const QLatin1String archiveInfoDelimiter1("--"); // 7z 9.13+ static const QLatin1String archiveInfoDelimiter2("----"); // 7z 9.04 static const QLatin1String entryInfoDelimiter("----------"); switch (m_state) { case ReadStateHeader: if (line.startsWith(QLatin1String("Listing archive:"))) { kDebug() << "Archive name: " << line.right(line.size() - 16).trimmed(); } else if ((line == archiveInfoDelimiter1) || (line == archiveInfoDelimiter2)) { m_state = ReadStateArchiveInformation; } else if (line.contains(QLatin1String( "Error:" ))) { kDebug() << line.mid(6); } break; case ReadStateArchiveInformation: if (line == entryInfoDelimiter) { m_state = ReadStateEntryInformation; } else if (line.startsWith(QLatin1String("Type ="))) { const QString type = line.mid(7).trimmed(); kDebug() << "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("Tar")) { m_archiveType = ArchiveTypeTar; } else if (type == QLatin1String("Zip")) { m_archiveType = ArchiveTypeZip; } else { // Should not happen kWarning() << "Unsupported archive type"; return false; } } break; case ReadStateEntryInformation: if (line.startsWith(QLatin1String("Path ="))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(6).trimmed()); m_currentArchiveEntry.clear(); m_currentArchiveEntry[FileName] = entryFilename; m_currentArchiveEntry[InternalID] = entryFilename; } else if (line.startsWith(QLatin1String("Size = "))) { m_currentArchiveEntry[ 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[CompressedSize] = line.mid(14).trimmed(); } } else if (line.startsWith(QLatin1String("Modified = "))) { m_currentArchiveEntry[ Timestamp ] = QDateTime::fromString(line.mid(11).trimmed(), QLatin1String( "yyyy-MM-dd hh:mm:ss" )); } else if (line.startsWith(QLatin1String("Attributes = "))) { const QString attributes = line.mid(13).trimmed(); const bool isDirectory = attributes.startsWith(QLatin1Char( 'D' )); m_currentArchiveEntry[ IsDirectory ] = isDirectory; if (isDirectory) { const QString directoryName = m_currentArchiveEntry[FileName].toString(); if (!directoryName.endsWith(QLatin1Char( '/' ))) { const bool isPasswordProtected = (line.at(12) == QLatin1Char( '+' )); m_currentArchiveEntry[FileName] = m_currentArchiveEntry[InternalID] = QString(directoryName + QLatin1Char( '/' )); m_currentArchiveEntry[ IsPasswordProtected ] = isPasswordProtected; } } m_currentArchiveEntry[ Permissions ] = attributes.mid(1); } else if (line.startsWith(QLatin1String("CRC = "))) { m_currentArchiveEntry[ CRC ] = line.mid(6).trimmed(); } else if (line.startsWith(QLatin1String("Method = "))) { m_currentArchiveEntry[ Method ] = line.mid(9).trimmed(); } else if (line.startsWith(QLatin1String("Encrypted = ")) && line.size() >= 13) { m_currentArchiveEntry[ IsPasswordProtected ] = (line.at(12) == QLatin1Char( '+' )); } else if (line.startsWith(QLatin1String("Block = "))) { if (m_currentArchiveEntry.contains(FileName)) { entry(m_currentArchiveEntry); } } break; } return true; } KERFUFFLE_EXPORT_PLUGIN(CliPlugin) #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 16179b8c..8064613a 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,268 +1,270 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include #include using namespace Kerfuffle; CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateColumnDescription1) , m_isPasswordProtected(false) , m_remainingIgnoredSubHeaderLines(0) , m_isUnrarFree(false) { } CliPlugin::~CliPlugin() { } // #272281: the proprietary unrar program does not like trailing '/'s // in directories passed to it when extracting only part of // the files in an archive. QString CliPlugin::escapeFileName(const QString &fileName) const { if (fileName.endsWith(QLatin1Char('/'))) { return fileName.left(fileName.length() - 1); } return fileName; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = QLatin1String( "unrar" ); p[DeleteProgram] = p[AddProgram] = QLatin1String( "rar" ); p[ListArgs] = QStringList() << QLatin1String( "vt" ) << QLatin1String( "-c-" ) << QLatin1String( "-v" ) << QLatin1String( "$Archive" ); p[ExtractArgs] = QStringList() << QLatin1String( "-kb" ) << QLatin1String( "-p-" ) << QLatin1String( "$PreservePathSwitch" ) << QLatin1String( "$PasswordSwitch" ) << QLatin1String( "$RootNodeSwitch" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[PreservePathSwitch] = QStringList() << QLatin1String( "x" ) << QLatin1String( "e" ); p[RootNodeSwitch] = QStringList() << QLatin1String( "-ap$Path" ); p[PasswordSwitch] = QStringList() << QLatin1String( "-p$Password" ); p[DeleteArgs] = QStringList() << QLatin1String( "d" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[FileExistsExpression] = QLatin1String( "^(.+) already exists. Overwrite it" ); p[FileExistsInput] = QStringList() << QLatin1String( "Y" ) //overwrite << QLatin1String( "N" ) //skip << QLatin1String( "A" ) //overwrite all << QLatin1String( "E" ) //autoskip << QLatin1String( "Q" ) //cancel ; - p[AddArgs] = QStringList() << QLatin1String( "a" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); + p[AddArgs] = QStringList() << QLatin1String( "a" ) << QLatin1String( "$CompressionLevelSwitch" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); + + p[CompressionLevelSwitches] = QStringList() << QLatin1String( "-m0" ) << QLatin1String( "-m3" ) << QLatin1String("-m5" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); p[WrongPasswordPatterns] = QStringList() << QLatin1String("password incorrect") << QLatin1String("wrong password"); p[ExtractionFailedPatterns] = QStringList() << QLatin1String( "CRC failed" ) << QLatin1String( "Cannot find volume" ); } return p; } bool CliPlugin::readListLine(const QString &line) { static const QLatin1String headerString("----------------------"); static const QLatin1String subHeaderString("Data header type: "); static const QLatin1String columnDescription1String(" Size Packed Ratio Date Time Attr CRC Meth Ver"); static const QLatin1String columnDescription2String(" Host OS Solid Old"); // Only present in unrar-nonfree switch (m_parseState) { case ParseStateColumnDescription1: if (line.startsWith(columnDescription1String)) { m_parseState = ParseStateColumnDescription2; } break; case ParseStateColumnDescription2: // #243273: We need a way to differentiate unrar and unrar-free, // as their output for the "vt" option is different. // Currently, we differ them by checking if "vt" produces // two lines of column names before the header string, as // only unrar does that (unrar-free always outputs one line // for column names regardless of how verbose we tell it to // be). if (line.startsWith(columnDescription2String)) { m_parseState = ParseStateHeader; } else if (line.startsWith(headerString)) { m_parseState = ParseStateEntryFileName; m_isUnrarFree = true; } break; case ParseStateHeader: if (line.startsWith(headerString)) { m_parseState = ParseStateEntryFileName; } break; case ParseStateEntryFileName: if (m_remainingIgnoredSubHeaderLines > 0) { --m_remainingIgnoredSubHeaderLines; return true; } // #242071: The RAR file format has the concept of subheaders, such as // CMT for comments and STM for NTFS streams (?). // Since the format is undocumented, we cannot do much, and // ignoring them seems harmless (at least 7zip and WinRAR do // notes show them either). if (line.startsWith(subHeaderString)) { // subHeaderString's length is 18 const QString subHeaderType(line.mid(18)); // XXX: If we ever support archive comments, this code must // be changed, because the comments will be shown after // a CMT subheader and will have an arbitrary number of lines if (subHeaderType == QLatin1String("STM")) { m_remainingIgnoredSubHeaderLines = 4; } else { m_remainingIgnoredSubHeaderLines = 3; } kDebug() << "Found a subheader of type" << subHeaderType; kDebug() << "The next" << m_remainingIgnoredSubHeaderLines << "lines will be ignored"; return true; } else if (line.startsWith(headerString)) { m_parseState = ParseStateHeader; return true; } m_isPasswordProtected = (line.at(0) == QLatin1Char( '*' )); // Start from 1 because the first character is either ' ' or '*' m_entryFileName = QDir::fromNativeSeparators(line.mid(1)); m_parseState = ParseStateEntryDetails; break; case ParseStateEntryIgnoredDetails: m_parseState = ParseStateEntryFileName; break; case ParseStateEntryDetails: if (line.startsWith(headerString)) { m_parseState = ParseStateHeader; return true; } const QStringList details = line.split(QLatin1Char( ' ' ), QString::SkipEmptyParts); QDateTime ts(QDate::fromString(details.at(3), QLatin1String("dd-MM-yy")), QTime::fromString(details.at(4), QLatin1String("hh:mm"))); // unrar outputs dates with a 2-digit year but QDate takes it as 19?? // let's take 1950 is cut-off; similar to KDateTime if (ts.date().year() < 1950) { ts = ts.addYears(100); } bool isDirectory = ((details.at(5).at(0) == QLatin1Char( 'd' )) || (details.at(5).at(1) == QLatin1Char( 'D' ))); if (isDirectory && !m_entryFileName.endsWith(QLatin1Char( '/' ))) { m_entryFileName += QLatin1Char( '/' ); } // 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 = details.at(2); if ((compressionRatio == QLatin1String("<--")) || (compressionRatio == QLatin1String("<->")) || (compressionRatio == QLatin1String("-->"))) { compressionRatio = QLatin1Char( '0' ); } else { compressionRatio.chop(1); // Remove the '%' } // TODO: // - Permissions differ depending on the system the entry was added // to the archive. // - unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). ArchiveEntry e; e[FileName] = m_entryFileName; e[InternalID] = m_entryFileName; e[Size] = details.at(0); e[CompressedSize] = details.at(1); e[Ratio] = compressionRatio; e[Timestamp] = ts; e[IsDirectory] = isDirectory; e[Permissions] = details.at(5); e[CRC] = details.at(6); e[Method] = details.at(7); e[Version] = details.at(8); e[IsPasswordProtected] = m_isPasswordProtected; kDebug() << "Added entry: " << e; entry(e); // #243273: unrar-free does not output the third file entry line, // skip directly to parsing a new entry. if (m_isUnrarFree) { m_parseState = ParseStateEntryFileName; } else { m_parseState = ParseStateEntryIgnoredDetails; } break; } return true; } KERFUFFLE_EXPORT_PLUGIN(CliPlugin) #include "cliplugin.moc" diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index 1f5518ea..dec7d740 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,217 +1,218 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cliplugin.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_status(Header) { } CliPlugin::~CliPlugin() { } // #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(QLatin1String("[]*?^-\\!")); 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; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = false; p[ListProgram] = QLatin1String( "zipinfo" ); p[ExtractProgram] = QLatin1String( "unzip" ); p[DeleteProgram] = p[AddProgram] = QLatin1String( "zip" ); p[ListArgs] = QStringList() << QLatin1String( "-l" ) << QLatin1String( "-T" ) << QLatin1String( "$Archive" ); p[ExtractArgs] = QStringList() << QLatin1String( "$PreservePathSwitch" ) << QLatin1String( "$PasswordSwitch" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[PreservePathSwitch] = QStringList() << QLatin1String( "" ) << QLatin1String( "-j" ); p[PasswordSwitch] = QStringList() << QLatin1String( "-P$Password" ); p[DeleteArgs] = QStringList() << QLatin1String( "-d" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); p[FileExistsExpression] = QLatin1String( "^replace (.+)\\?" ); p[FileExistsInput] = QStringList() << QLatin1String( "y" ) //overwrite << QLatin1String( "n" ) //skip << QLatin1String( "A" ) //overwrite all << QLatin1String( "N" ) //autoskip ; - p[AddArgs] = QStringList() << QLatin1String( "-r" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); + p[AddArgs] = QStringList() << QLatin1String( "$CompressionLevelSwitch" ) << QLatin1String( "-r" ) << QLatin1String( "$Archive" ) << QLatin1String( "$Files" ); + p[CompressionLevelSwitches] = QStringList() << QLatin1String( "-0" ) << QLatin1String( "-6" ) << QLatin1String("-9" ); p[PasswordPromptPattern] = QLatin1String(" password: "); p[WrongPasswordPatterns] = QStringList() << QLatin1String( "incorrect password" ); //p[ExtractionFailedPatterns] = QStringList() << "CRC failed"; } return p; } QString CliPlugin::autoConvertEncoding( const QString & fileName ) { QByteArray result( fileName.toLatin1() ); QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); KEncodingProber prober(KEncodingProber::CentralEuropean); prober.feed(result); QByteArray refinedEncoding = prober.encoding(); qDebug() << "KEncodingProber detected encoding: " << refinedEncoding << "for: " << fileName; // Workaround for CP850 support (which is frequently attributed to CP1251 by KEncodingProber instead) if (refinedEncoding == "windows-1251") { qDebug() << "Language: " << KGlobal::locale()->language(); if ( KGlobal::locale()->language() == "de" ) { // In case the user's language is German we refine the detection of KEncodingProber // by assuming that in a german environment the usage of serbian / macedonian letters // and special characters is less likely to happen than the usage of umlauts qDebug() << "fileName" << fileName; qDebug() << "toLatin: " << fileName.toLatin1(); // Check for case CP850 (Windows XP & Windows7) QString checkString = QTextCodec::codecForName("CP850")->toUnicode(fileName.toLatin1()); qDebug() << "String converted to CP850: " << checkString; if ( checkString.contains("ä") || // Equals lower quotation mark in CP1251 - unlikely to be used in filenames checkString.contains("ö") || // Equals quotation mark in CP1251 - unlikely to be used in filenames checkString.contains("Ö") || // Equals TM symbol - unlikely to be used in filenames checkString.contains("ü") || // Overlaps with "Gje" in the Macedonian alphabet checkString.contains("Ä") || // Overlaps with "Tshe" in the Serbian, Bosnian and Montenegrin alphabet checkString.contains("Ü") || // Overlaps with "Lje" in the Serbian and Montenegrin alphabet checkString.contains("ß") ) // Overlaps with "Be" in the cyrillic alphabet { refinedEncoding = "CP850"; qDebug() << "RefinedEncoding: " << refinedEncoding; } } } QString refinedString = QTextCodec::codecForName(refinedEncoding )->toUnicode(fileName.toLatin1()); return ( refinedString != fileName ) ? refinedString : fileName; } bool CliPlugin::readListLine(const QString &line) { static const QRegExp entryPattern(QLatin1String( "^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$") ); switch (m_status) { case Header: m_status = Entry; break; case Entry: if (entryPattern.indexIn(line) != -1) { ArchiveEntry e; e[Permissions] = entryPattern.cap(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[IsDirectory] = entryPattern.cap(10).endsWith(QLatin1Char('/')); e[Size] = entryPattern.cap(4).toInt(); QString status = entryPattern.cap(5); if (status[0].isUpper()) { e[IsPasswordProtected] = true; } e[CompressedSize] = entryPattern.cap(6).toInt(); const QDateTime ts(QDate::fromString(entryPattern.cap(8), QLatin1String( "yyyyMMdd" )), QTime::fromString(entryPattern.cap(9), QLatin1String( "hhmmss" ))); e[Timestamp] = ts; e[InternalID] = entryPattern.cap(10); QString resultString; resultString = entryPattern.cap(10); // Adjust encoding for zip files since zip doesn't handle it e[FileName] = autoConvertEncoding( resultString ); entry(e); } break; } return true; } bool CliPlugin::copyFiles(const QList & files, const QString & destinationDirectory, ExtractionOptions options) { bool saveReturn = CliInterface::copyFiles(files, destinationDirectory, options); // Rename unzipped files to the encoding-corrected files. for (int j = 0; j < files.count(); ++j) { QFile file( files.at(j).toString() ); if ( file.exists() ) { QString encodingCorrectedString = autoConvertEncoding( file.fileName() ); if ( file.fileName() != encodingCorrectedString ) { file.rename( encodingCorrectedString ); } } } return saveReturn; } KERFUFFLE_EXPORT_PLUGIN(CliPlugin)