diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index cd35aa07..292e2fe8 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1102 +1,1102 @@ /* * 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 #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_testResult(true) { //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::supportsParameter(CliInterfaceParameters param) { if (m_param.isEmpty()) cacheParameterList(); bool hasParam = false; if (m_param.contains(param)) { QVariant var = m_param.value(param); switch (var.type()) { case QVariant::StringList: hasParam = !var.toStringList().isEmpty(); break; case QVariant::String: hasParam = !var.toString().isEmpty(); break; case QVariant::Bool: hasParam = var.toBool(); break; default: break; } } return hasParam; } 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("$MultiThreadingSwitch")) { QString multiThreadingSwitch = m_param.value(MultiThreadingSwitch).toString(); bool multiThreading = options.value(QLatin1String("MultiThreadingEnabled")).toBool(); QString theReplacement; if (multiThreading == true) { theReplacement = multiThreadingSwitch; } 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("$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; } - fixFileNameEncoding(files); + fixFileNameEncoding(destinationDirectory); return true; } -void CliInterface::fixFileNameEncoding(const QList & files) +void CliInterface::fixFileNameEncoding(const QString & destinationDirectory) { - for (int i = 0; i < files.count(); ++i) { - QFile file(files.at(i).toString()); + QDir destDir(destinationDirectory); + destDir.setFilter(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System); - if (file.exists()) { - QString encodingCorrectedString = autoConvertEncoding(file.fileName()); + QStringList list = destDir.entryList(); + for (int i = 0; i < list.size(); ++i) { + QString encodingCorrectedString = autoConvertEncoding(list.at(i)); - if (file.fileName() != encodingCorrectedString) { - kDebug(1601) << "Renaming" << file.fileName() << "to" << encodingCorrectedString; - file.rename(encodingCorrectedString); - } + if (list.at(i) != encodingCorrectedString) { + kDebug(1601) << "Renaming" << list.at(i) << "to" << encodingCorrectedString; + QFile::rename(list.at(i), encodingCorrectedString); } } } 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 theReplacement = compressionLevelSwitches.at(options.value(QLatin1String("CompressionLevel"), 2).toInt()); 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("$MultiThreadingSwitch")) { QString multiThreadingSwitch = m_param.value(MultiThreadingSwitch).toString(); bool multiThreading = options.value(QLatin1String("MultiThreadingEnabled")).toBool(); QString theReplacement; if (multiThreading == true) { theReplacement = multiThreadingSwitch; } 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("$EncryptHeaderSwitch")) { QString encryptHeaderSwitch = m_param.value(EncryptHeaderSwitch).toString(); bool encryptHeader = options.value(QLatin1String("EncryptHeaderEnabled")).toBool(); QString theReplacement; if (encryptHeader == true) { theReplacement = encryptHeaderSwitch; } 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("$EncryptionMethodSwitches")) { QStringList encryptionMethodSwitches = m_param.value(EncryptionMethodSwitches).toStringList(); QString encryptionMethod = options.value(QLatin1String("EncryptionMethod")).toString(); QString theReplacement; if (encryptionMethod == "AES256") { theReplacement = encryptionMethodSwitches.at(0); } if (encryptionMethod == "ZipCrypto") { theReplacement = encryptionMethodSwitches.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("$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::testFiles(const QList & files, TestOptions options) { Q_UNUSED(options) kDebug(); cacheParameterList(); m_testResult = true; m_operationMode = Test; //start preparing the argument list QStringList args = m_param.value(TestArgs).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("$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("$Files")) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { args.insert(i + j, escapeFileName(files.at(j).toString())); ++i; } --i; } } if (!runProcess(m_param.value(TestProgram).toString(), args)) { failOperation(); return false; } return m_testResult; } 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_operationMode == Test) && 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!"; setPassword(QString()); 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; } if (m_operationMode == Test) { 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, TestFailedPatterns) && checkForErrorMessage(line, WrongPasswordPatterns)) { m_testResult = false; error(i18n("Integrity check failed: Either the archive is broken or the password is incorrect.")); failOperation(); return; } if (checkForErrorMessage(line, TestFailedPatterns)) { m_testResult = false; error(i18n("Integrity check failed: The archive is broken.")); failOperation(); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { kDebug() << "Wrong password!"; error(i18n("Incorrect password.")); failOperation(); 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) { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString & rawPattern, m_param.value(parameterIndex).toStringList()) { if (QRegExp(rawPattern).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 } QString CliInterface::autoConvertEncoding( const QString & fileName ) { QByteArray result( fileName.toLatin1() ); QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); KEncodingProber prober(KEncodingProber::CentralEuropean); KEncodingProber prober2(KEncodingProber::Universal); prober.feed(result); prober2.feed(result); QByteArray refinedEncoding; // fileName is probably in UTF-8 already. if (prober2.confidence() > 0.49) { refinedEncoding = prober2.encoding(); } else { refinedEncoding = prober.encoding(); } kDebug() << "KEncodingProber detected encodings: " << refinedEncoding << "for: " << fileName; // Workaround for CP850 support (which is frequently attributed to CP1251 by KEncodingProber instead) if (refinedEncoding == "windows-1251") { kDebug() << "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 kDebug() << "fileName" << fileName; kDebug() << "toLatin: " << fileName.toLatin1(); // Check for case CP850 (Windows XP & Windows7) QString checkString = QTextCodec::codecForName("CP850")->toUnicode(fileName.toLatin1()); kDebug() << "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"; kDebug() << "RefinedEncoding: " << refinedEncoding; } } } else if (refinedEncoding == "IBM855") { refinedEncoding = "IBM 850"; kDebug() << "Setting refinedEncoding to " << refinedEncoding; } else if (refinedEncoding == "IBM866") { refinedEncoding = "IBM 866"; kDebug() << "Setting refinedEncoding to " << refinedEncoding; } QTextCodec * codec = QTextCodec::codecForName(refinedEncoding); if (!codec) { kDebug() << "codecForName returned null, using codec ISO-8859-1 instead"; codec = QTextCodec::codecForName("ISO-8859-1"); } QString refinedString = codec->toUnicode(fileName.toLatin1()); return ( refinedString != fileName ) ? refinedString : fileName; } } #include "cliinterface.moc" diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 1a0ad2cf..04f27313 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,408 +1,408 @@ /* * 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, /** * QStringList * The arguments that are passed to the program above for * setting the compression level. * First entry is "Store" * Second entry is "Fast" * Third entry is "Normal" * Fourth entry is "Good" * Fifth entry is "Maximum" */ CompressionLevelSwitches, /** * QString * This is a string that allows to enable Multithreading for * compression and decompression. This only has some effect for some * cases. */ MultiThreadingSwitch, /** * QStringList * The arguments that are passed to the program above for * setting the compression level. * First entry is "AES256" * Second entry is "ZipCrypto" */ EncryptionMethodSwitches, /** * QString * This is a string that allows to enable encrypting headers for * compression. This only has some effect when supported. */ EncryptHeaderSwitch, /** * QString * The name to the program that will handle testing in this * archive format (eg "rar"). Will be searched for in PATH */ TestProgram, /** * QStringList * The arguments that are passed to the program above for * testing the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ TestArgs, /** * QStringList * Patterns that indicate a fail during testing */ TestFailedPatterns }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: enum OperationMode { List, Copy, Add, Delete, Test }; 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 bool testFiles(const QList & files, TestOptions options = TestOptions()); 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); /** * Checks whether the implementing plugin supports the provided option */ bool supportsParameter( CliInterfaceParameters param ); /** * Does encoding detection for a filename using * KEncodingProber and filename specific criteria and * returns the encoding-corrected string */ static QString autoConvertEncoding( const QString & fileName ); protected: - static void fixFileNameEncoding(const QList & files); + static void fixFileNameEncoding(const QString & destinationDirectory); 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; bool m_testResult; private slots: void readStdout(bool handleAll = false); void processFinished(int exitCode, QProcess::ExitStatus exitStatus); }; } #endif /* CLIINTERFACE_H */