File indexing completed on 2024-05-12 05:50:21

0001 /*
0002     SPDX-FileCopyrightText: 2009 Harald Hvaal <haraldhv@stud.ntnu.no>
0003     SPDX-FileCopyrightText: 2009-2011 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0004     SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "cliplugin.h"
0010 #include "ark_debug.h"
0011 
0012 #include <KLocalizedString>
0013 #include <KPluginFactory>
0014 
0015 #include <QDateTime>
0016 #include <QDir>
0017 #include <QRegularExpression>
0018 #include <QTemporaryDir>
0019 
0020 using namespace Kerfuffle;
0021 
0022 K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_clizip.json")
0023 
0024 CliPlugin::CliPlugin(QObject *parent, const QVariantList &args)
0025     : CliInterface(parent, args)
0026     , m_parseState(ParseStateHeader)
0027     , m_linesComment(0)
0028 {
0029     qCDebug(ARK) << "Loaded cli_zip plugin";
0030     setupCliProperties();
0031 }
0032 
0033 CliPlugin::~CliPlugin()
0034 {
0035 }
0036 
0037 void CliPlugin::resetParsing()
0038 {
0039     m_parseState = ParseStateHeader;
0040     m_tempComment.clear();
0041     m_comment.clear();
0042 }
0043 
0044 // #208091: infozip applies special meanings to some characters, so we
0045 //          need to escape them with backslashes.see match.c in
0046 //          infozip's source code
0047 QString CliPlugin::escapeFileName(const QString &fileName) const
0048 {
0049     const QString escapedCharacters(QStringLiteral("[]*?^-\\!"));
0050 
0051     QString quoted;
0052     const int len = fileName.length();
0053     const QLatin1Char backslash('\\');
0054     quoted.reserve(len * 2);
0055 
0056     for (int i = 0; i < len; ++i) {
0057         if (escapedCharacters.contains(fileName.at(i))) {
0058             quoted.append(backslash);
0059         }
0060 
0061         quoted.append(fileName.at(i));
0062     }
0063 
0064     return quoted;
0065 }
0066 
0067 void CliPlugin::setupCliProperties()
0068 {
0069     qCDebug(ARK) << "Setting up parameters...";
0070 
0071     m_cliProps->setProperty("captureProgress", false);
0072 
0073     m_cliProps->setProperty("addProgram", QStringLiteral("zip"));
0074     m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("-r")}));
0075 
0076     m_cliProps->setProperty("deleteProgram", QStringLiteral("zip"));
0077     m_cliProps->setProperty("deleteSwitch", QStringLiteral("-d"));
0078 
0079     m_cliProps->setProperty("extractProgram", QStringLiteral("unzip"));
0080     m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-j")});
0081 
0082     m_cliProps->setProperty("listProgram", QStringLiteral("zipinfo"));
0083     m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z")});
0084 
0085     m_cliProps->setProperty("testProgram", QStringLiteral("unzip"));
0086     m_cliProps->setProperty("testSwitch", QStringLiteral("-t"));
0087 
0088     m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-P$Password")});
0089 
0090     m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-$CompressionLevel"));
0091     m_cliProps->setProperty("compressionMethodSwitch",
0092                             QHash<QString, QVariant>{{QStringLiteral("application/zip"), QStringLiteral("-Z$CompressionMethod")},
0093                                                      {QStringLiteral("application/x-java-archive"), QStringLiteral("-Z$CompressionMethod")}});
0094     m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek"));
0095 
0096     m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^No errors detected in compressed data of ")});
0097     m_cliProps->setProperty("fileExistsFileNameRegExp",
0098                             QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")});
0099     m_cliProps->setProperty("fileExistsInput",
0100                             QStringList{
0101                                 QStringLiteral("y"), // Overwrite
0102                                 QStringLiteral("n"), // Skip
0103                                 QStringLiteral("A"), // Overwrite all
0104                                 QStringLiteral("N"), // Autoskip
0105                             });
0106     m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("unsupported compression method")});
0107 }
0108 
0109 bool CliPlugin::readListLine(const QString &line)
0110 {
0111     static const QRegularExpression entryPattern(
0112         QStringLiteral("^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$"));
0113 
0114     // RegExp to identify the line preceding comments.
0115     const QRegularExpression commentPattern(QStringLiteral("^Archive:  .*$"));
0116     // RegExp to identify the line following comments.
0117     const QRegularExpression commentEndPattern(QStringLiteral("^Zip file size: .*$"));
0118 
0119     switch (m_parseState) {
0120     case ParseStateHeader:
0121         if (commentPattern.match(line).hasMatch()) {
0122             m_parseState = ParseStateComment;
0123         } else if (commentEndPattern.match(line).hasMatch()) {
0124             m_parseState = ParseStateEntry;
0125         }
0126         break;
0127     case ParseStateComment:
0128         if (commentEndPattern.match(line).hasMatch()) {
0129             m_parseState = ParseStateEntry;
0130             if (!m_tempComment.trimmed().isEmpty()) {
0131                 m_comment = m_tempComment.trimmed();
0132                 m_linesComment = m_comment.count(QLatin1Char('\n')) + 1;
0133                 qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines";
0134             }
0135         } else {
0136             m_tempComment.append(line + QLatin1Char('\n'));
0137         }
0138         break;
0139     case ParseStateEntry:
0140         QRegularExpressionMatch rxMatch = entryPattern.match(line);
0141         if (rxMatch.hasMatch()) {
0142             Archive::Entry *e = new Archive::Entry(this);
0143             e->setProperty("permissions", rxMatch.captured(1));
0144 
0145             // #280354: infozip may not show the right attributes for a given directory, so an entry
0146             //          ending with '/' is actually more reliable than 'd' bein in the attributes.
0147             e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/')));
0148 
0149             e->setProperty("size", rxMatch.captured(4));
0150             QString status = rxMatch.captured(5);
0151             if (status[0].isUpper()) {
0152                 e->setProperty("isPasswordProtected", true);
0153             }
0154             e->setProperty("compressedSize", rxMatch.captured(6).toInt());
0155             e->setProperty("method", rxMatch.captured(7));
0156 
0157             QString method = convertCompressionMethod(rxMatch.captured(7));
0158             Q_EMIT compressionMethodFound(method);
0159 
0160             const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")),
0161                                QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss")));
0162             e->setProperty("timestamp", ts);
0163 
0164             e->setProperty("fullPath", rxMatch.captured(10));
0165             Q_EMIT entry(e);
0166         }
0167         break;
0168     }
0169 
0170     return true;
0171 }
0172 
0173 bool CliPlugin::readExtractLine(const QString &line)
0174 {
0175     const QRegularExpression rxUnsupCompMethod(QStringLiteral("unsupported compression method (\\d+)"));
0176     const QRegularExpression rxUnsupEncMethod(QStringLiteral("need PK compat. v\\d\\.\\d \\(can do v\\d\\.\\d\\)"));
0177     const QRegularExpression rxBadCRC(QStringLiteral("bad CRC"));
0178 
0179     QRegularExpressionMatch unsupCompMethodMatch = rxUnsupCompMethod.match(line);
0180     if (unsupCompMethodMatch.hasMatch()) {
0181         Q_EMIT error(i18n("Extraction failed due to unsupported compression method (%1).", unsupCompMethodMatch.captured(1)));
0182         return false;
0183     }
0184 
0185     if (rxUnsupEncMethod.match(line).hasMatch()) {
0186         Q_EMIT error(i18n("Extraction failed due to unsupported encryption method."));
0187         return false;
0188     }
0189 
0190     if (rxBadCRC.match(line).hasMatch()) {
0191         Q_EMIT error(i18n("Extraction failed due to one or more corrupt files. Any extracted files may be damaged."));
0192         return false;
0193     }
0194 
0195     return true;
0196 }
0197 
0198 bool CliPlugin::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
0199 {
0200     qCDebug(ARK) << "Moving" << files.count() << "file(s) to destination:" << destination;
0201 
0202     m_oldWorkingDir = QDir::currentPath();
0203     m_tempWorkingDir.reset(new QTemporaryDir());
0204     m_tempAddDir.reset(new QTemporaryDir());
0205     QDir::setCurrent(m_tempWorkingDir->path());
0206     m_passedFiles = files;
0207     m_passedDestination = destination;
0208     m_passedOptions = options;
0209 
0210     m_subOperation = Extract;
0211     connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving);
0212 
0213     return extractFiles(files, QDir::currentPath(), ExtractionOptions());
0214 }
0215 
0216 int CliPlugin::moveRequiredSignals() const
0217 {
0218     return 4;
0219 }
0220 
0221 void CliPlugin::continueMoving(bool result)
0222 {
0223     if (!result) {
0224         finishMoving(false);
0225         return;
0226     }
0227 
0228     switch (m_subOperation) {
0229     case Extract:
0230         m_subOperation = Delete;
0231         if (!deleteFiles(m_passedFiles)) {
0232             finishMoving(false);
0233         }
0234         break;
0235     case Delete:
0236         m_subOperation = Add;
0237         if (!setMovingAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) {
0238             finishMoving(false);
0239         }
0240         break;
0241     case Add:
0242         finishMoving(true);
0243         break;
0244     default:
0245         Q_ASSERT(false);
0246     }
0247 }
0248 
0249 bool CliPlugin::setMovingAddedFiles()
0250 {
0251     m_passedFiles = entriesWithoutChildren(m_passedFiles);
0252     // If there are more files being moved than 1, we have destination as a destination folder,
0253     // otherwise it's new entry full path.
0254     if (m_passedFiles.count() > 1) {
0255         return setAddedFiles();
0256     }
0257 
0258     QDir::setCurrent(m_tempAddDir->path());
0259     const Archive::Entry *file = m_passedFiles.at(0);
0260     const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash);
0261     const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + m_passedDestination->name();
0262     if (!QFile::rename(oldPath, newPath)) {
0263         return false;
0264     }
0265     m_tempAddedFiles << new Archive::Entry(nullptr, m_passedDestination->name());
0266 
0267     // We have to exclude file name from destination path in order to pass it to addFiles method.
0268     const QString destinationPath = m_passedDestination->fullPath();
0269     const int slashCount = destinationPath.count(QLatin1Char('/'));
0270     if (slashCount > 1 || (slashCount == 1 && !destinationPath.endsWith(QLatin1Char('/')))) {
0271         int destinationLength = destinationPath.length();
0272         bool iteratedChar = false;
0273         do {
0274             destinationLength--;
0275             if (destinationPath.at(destinationLength) != QLatin1Char('/')) {
0276                 iteratedChar = true;
0277             }
0278         } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/')));
0279         m_passedDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1));
0280     } else {
0281         // ...unless the destination path is already a single folder, e.g. "dir/", or a file, e.g. "foo.txt".
0282         // In this case we're going to add to the root, so we just need to set a null destination.
0283         m_passedDestination = nullptr;
0284     }
0285 
0286     return true;
0287 }
0288 
0289 void CliPlugin::finishMoving(bool result)
0290 {
0291     disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving);
0292     Q_EMIT progress(1.0);
0293     Q_EMIT finished(result);
0294     cleanUp();
0295 }
0296 
0297 QString CliPlugin::convertCompressionMethod(const QString &method)
0298 {
0299     if (method == QLatin1String("stor")) {
0300         return QStringLiteral("Store");
0301     } else if (method.startsWith(QLatin1String("def"))) {
0302         return QStringLiteral("Deflate");
0303     } else if (method == QLatin1String("d64N")) {
0304         return QStringLiteral("Deflate64");
0305     } else if (method == QLatin1String("bzp2")) {
0306         return QStringLiteral("BZip2");
0307     } else if (method == QLatin1String("lzma")) {
0308         return QStringLiteral("LZMA");
0309     } else if (method == QLatin1String("ppmd")) {
0310         return QStringLiteral("PPMd");
0311     } else if (method == QLatin1String("u095")) {
0312         return QStringLiteral("XZ");
0313     } else if (method == QLatin1String("u099")) {
0314         Q_EMIT encryptionMethodFound(QStringLiteral("AES"));
0315         return i18nc("referred to compression method", "unknown");
0316     }
0317     return method;
0318 }
0319 
0320 bool CliPlugin::isPasswordPrompt(const QString &line)
0321 {
0322     return line.endsWith(QLatin1String(" password: "));
0323 }
0324 
0325 bool CliPlugin::isWrongPasswordMsg(const QString &line)
0326 {
0327     return line.endsWith(QLatin1String("incorrect password"));
0328 }
0329 
0330 bool CliPlugin::isCorruptArchiveMsg(const QString &line)
0331 {
0332     return (line.contains(QLatin1String("End-of-central-directory signature not found"))
0333             || line.contains(QLatin1String("didn't find end-of-central-dir signature at end of central dir")));
0334 }
0335 
0336 bool CliPlugin::isDiskFullMsg(const QString &line)
0337 {
0338     return (line.contains(QLatin1String("No space left on device")) || line.contains(QLatin1String("write error (disk full?)")));
0339 }
0340 
0341 bool CliPlugin::isFileExistsMsg(const QString &line)
0342 {
0343     return (line.startsWith(QLatin1String("replace ")) && line.endsWith(QLatin1String("? [y]es, [n]o, [A]ll, [N]one, [r]ename: ")));
0344 }
0345 
0346 bool CliPlugin::isFileExistsFileName(const QString &line)
0347 {
0348     return (line.startsWith(QLatin1String("replace ")) && line.endsWith(QLatin1String("? [y]es, [n]o, [A]ll, [N]one, [r]ename: ")));
0349 }
0350 
0351 #include "cliplugin.moc"
0352 #include "moc_cliplugin.cpp"