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

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 <QDateTime>
0013 #include <QDir>
0014 #include <QRegularExpression>
0015 
0016 #include <KLocalizedString>
0017 #include <KPluginFactory>
0018 
0019 using namespace Kerfuffle;
0020 
0021 K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_cli7z.json")
0022 
0023 CliPlugin::CliPlugin(QObject *parent, const QVariantList &args)
0024     : CliInterface(parent, args)
0025     , m_archiveType(ArchiveType7z)
0026     , m_parseState(ParseStateTitle)
0027     , m_binaryVariant(Undefined)
0028     , m_linesComment(0)
0029     , m_isFirstInformationEntry(true)
0030 {
0031     qCDebug(ARK) << "Loaded cli_7z plugin";
0032 
0033     setupCliProperties();
0034 }
0035 
0036 CliPlugin::~CliPlugin()
0037 {
0038 }
0039 
0040 void CliPlugin::resetParsing()
0041 {
0042     m_parseState = ParseStateTitle;
0043     m_comment.clear();
0044     m_numberOfVolumes = 0;
0045 }
0046 
0047 void CliPlugin::setupCliProperties()
0048 {
0049     qCDebug(ARK) << "Setting up parameters...";
0050 
0051     if (m_binaryVariant == Undefined) {
0052         qCDebug(ARK) << "Checking 7z variant...";
0053         QProcess process;
0054         process.setProgram(QStringLiteral("7z"));
0055         process.start();
0056         process.waitForFinished(500);
0057         const QString output = QString::fromUtf8(process.readAllStandardOutput());
0058         if (output.contains(QLatin1String("p7zip"))) {
0059             qCDebug(ARK) << "Detected p7zip variant.";
0060             m_binaryVariant = P7zip;
0061         } else if (output.contains(QLatin1String("7-Zip"))) {
0062             qCDebug(ARK) << "Detected upstream 7-Zip variant.";
0063             m_binaryVariant = Upstream7zip;
0064         }
0065     }
0066 
0067     m_cliProps->setProperty("captureProgress", false);
0068 
0069     m_cliProps->setProperty("addProgram", QStringLiteral("7z"));
0070     QStringList addSwitch = {QStringLiteral("a")};
0071     if (m_binaryVariant == P7zip) {
0072         addSwitch << QStringLiteral("-l");
0073     }
0074     m_cliProps->setProperty("addSwitch", addSwitch);
0075 
0076     m_cliProps->setProperty("deleteProgram", QStringLiteral("7z"));
0077     m_cliProps->setProperty("deleteSwitch", QStringLiteral("d"));
0078 
0079     m_cliProps->setProperty("extractProgram", QStringLiteral("7z"));
0080     m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x")});
0081     m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e")});
0082 
0083     m_cliProps->setProperty("listProgram", QStringLiteral("7z"));
0084     m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("l"), QStringLiteral("-slt")});
0085 
0086     m_cliProps->setProperty("moveProgram", QStringLiteral("7z"));
0087     m_cliProps->setProperty("moveSwitch", QStringLiteral("rn"));
0088 
0089     m_cliProps->setProperty("testProgram", QStringLiteral("7z"));
0090     m_cliProps->setProperty("testSwitch", QStringLiteral("t"));
0091 
0092     m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")});
0093     m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-p$Password"), QStringLiteral("-mhe=on")});
0094     m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-mx=$CompressionLevel"));
0095     m_cliProps->setProperty("compressionMethodSwitch",
0096                             QHash<QString, QVariant>{{QStringLiteral("application/x-7z-compressed"), QStringLiteral("-m0=$CompressionMethod")},
0097                                                      {QStringLiteral("application/zip"), QStringLiteral("-mm=$CompressionMethod")}});
0098     m_cliProps->setProperty("encryptionMethodSwitch",
0099                             QHash<QString, QVariant>{{QStringLiteral("application/x-7z-compressed"), QString()},
0100                                                      {QStringLiteral("application/zip"), QStringLiteral("-mem=$EncryptionMethod")}});
0101     m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek"));
0102     m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^Everything is Ok$")});
0103     m_cliProps->setProperty("fileExistsFileNameRegExp", QStringList{QStringLiteral("^file \\./(.*)$"), QStringLiteral("^  Path:     \\./(.*)$")});
0104     m_cliProps->setProperty("fileExistsInput",
0105                             QStringList{
0106                                 QStringLiteral("Y"), // Overwrite
0107                                 QStringLiteral("N"), // Skip
0108                                 QStringLiteral("A"), // Overwrite all
0109                                 QStringLiteral("S"), // Autoskip
0110                                 QStringLiteral("Q"), // Cancel
0111                             });
0112     m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("$Suffix.001")});
0113 }
0114 
0115 void CliPlugin::fixDirectoryFullName()
0116 {
0117     if (m_currentArchiveEntry->isDir()) {
0118         const QString directoryName = m_currentArchiveEntry->fullPath();
0119         if (!directoryName.endsWith(QLatin1Char('/'))) {
0120             m_currentArchiveEntry->setProperty("fullPath", QString(directoryName + QLatin1Char('/')));
0121         }
0122     }
0123 }
0124 
0125 bool CliPlugin::readListLine(const QString &line)
0126 {
0127     static const QLatin1String archiveInfoDelimiter1("--"); // 7z 9.13+
0128     static const QLatin1String archiveInfoDelimiter2("----"); // 7z 9.04
0129     static const QLatin1String entryInfoDelimiter("----------");
0130 
0131     if (line.startsWith(QLatin1String("Open ERROR: Can not open the file as [7z] archive"))) {
0132         Q_EMIT error(i18n("Listing the archive failed."));
0133         return false;
0134     }
0135 
0136     const QRegularExpression rxVersionLine(QStringLiteral("^p7zip Version ([\\d\\.]+) .*$"));
0137     const QRegularExpression rxVersionLine7z(QStringLiteral("^7-Zip \\(\\w\\) ([\\d\\.]+) .*$"));
0138     QRegularExpressionMatch matchVersion;
0139 
0140     switch (m_parseState) {
0141     case ParseStateTitle:
0142         matchVersion = rxVersionLine.match(line);
0143         if (matchVersion.hasMatch()) {
0144             m_parseState = ParseStateHeader;
0145             const QString p7zipVersion = matchVersion.captured(1);
0146             qCDebug(ARK) << "p7zip version" << p7zipVersion << "detected";
0147             break;
0148         }
0149         matchVersion = rxVersionLine7z.match(line);
0150         if (matchVersion.hasMatch()) {
0151             m_parseState = ParseStateHeader;
0152             const QString l7zipVersion = matchVersion.captured(1);
0153             qCDebug(ARK) << "7zip version" << l7zipVersion << "detected";
0154             break;
0155         }
0156         break;
0157 
0158     case ParseStateHeader:
0159         if (line.startsWith(QLatin1String("Listing archive:"))) {
0160             qCDebug(ARK) << "Archive name: " << line.right(line.size() - 16).trimmed();
0161         } else if ((line == archiveInfoDelimiter1) || (line == archiveInfoDelimiter2)) {
0162             m_parseState = ParseStateArchiveInformation;
0163         } else if (line.contains(QLatin1String("Error: "))) {
0164             qCWarning(ARK) << line.mid(7);
0165         }
0166         break;
0167 
0168     case ParseStateArchiveInformation:
0169         if (line == entryInfoDelimiter) {
0170             m_parseState = ParseStateEntryInformation;
0171 
0172         } else if (line.startsWith(QLatin1String("Type = "))) {
0173             const QString type = line.mid(7).trimmed();
0174             qCDebug(ARK) << "Archive type: " << type;
0175 
0176             if (type == QLatin1String("7z")) {
0177                 m_archiveType = ArchiveType7z;
0178             } else if (type == QLatin1String("bzip2")) {
0179                 m_archiveType = ArchiveTypeBZip2;
0180             } else if (type == QLatin1String("gzip")) {
0181                 m_archiveType = ArchiveTypeGZip;
0182             } else if (type == QLatin1String("xz")) {
0183                 m_archiveType = ArchiveTypeXz;
0184             } else if (type == QLatin1String("tar")) {
0185                 m_archiveType = ArchiveTypeTar;
0186             } else if (type == QLatin1String("zip")) {
0187                 m_archiveType = ArchiveTypeZip;
0188             } else if (type == QLatin1String("Rar")) {
0189                 m_archiveType = ArchiveTypeRar;
0190             } else if (type == QLatin1String("Split")) {
0191                 setMultiVolume(true);
0192             } else {
0193                 // Should not happen
0194                 qCWarning(ARK) << "Unsupported archive type";
0195                 return false;
0196             }
0197 
0198         } else if (line.startsWith(QLatin1String("Volumes = "))) {
0199             m_numberOfVolumes = line.section(QLatin1Char('='), 1).trimmed().toInt();
0200 
0201         } else if (line.startsWith(QLatin1String("Method = "))) {
0202             QStringList methods = line.section(QLatin1Char('='), 1).trimmed().split(QLatin1Char(' '), Qt::SkipEmptyParts);
0203             handleMethods(methods);
0204 
0205         } else if (line.startsWith(QLatin1String("Comment = "))) {
0206             m_parseState = ParseStateComment;
0207             m_comment.append(line.section(QLatin1Char('='), 1) + QLatin1Char('\n'));
0208         }
0209         break;
0210 
0211     case ParseStateComment:
0212         if (line == entryInfoDelimiter) {
0213             m_parseState = ParseStateEntryInformation;
0214             if (!m_comment.trimmed().isEmpty()) {
0215                 m_comment = m_comment.trimmed();
0216                 m_linesComment = m_comment.count(QLatin1Char('\n')) + 1;
0217                 qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines";
0218             }
0219         } else {
0220             m_comment.append(line + QLatin1Char('\n'));
0221         }
0222         break;
0223 
0224     case ParseStateEntryInformation:
0225         if (m_isFirstInformationEntry) {
0226             m_isFirstInformationEntry = false;
0227             m_currentArchiveEntry = new Archive::Entry(this);
0228             m_currentArchiveEntry->compressedSizeIsSet = false;
0229         }
0230         if (line.startsWith(QLatin1String("Path = "))) {
0231             const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed());
0232             m_currentArchiveEntry->setProperty("fullPath", entryFilename);
0233 
0234         } else if (line.startsWith(QLatin1String("Size = "))) {
0235             m_currentArchiveEntry->setProperty("size", line.mid(7).trimmed());
0236 
0237         } else if (line.startsWith(QLatin1String("Packed Size = "))) {
0238             // #236696: 7z files only show a single Packed Size value
0239             //          corresponding to the whole archive.
0240             if (m_archiveType != ArchiveType7z) {
0241                 m_currentArchiveEntry->compressedSizeIsSet = true;
0242                 m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed());
0243             }
0244 
0245         } else if (line.startsWith(QLatin1String("Modified = "))) {
0246             m_currentArchiveEntry->setProperty("timestamp", QDateTime::fromString(line.mid(11).trimmed(), QStringLiteral("yyyy-MM-dd hh:mm:ss")));
0247 
0248         } else if (line.startsWith(QLatin1String("Folder = "))) {
0249             const QString isDirectoryStr = line.mid(9).trimmed();
0250             Q_ASSERT(isDirectoryStr == QLatin1String("+") || isDirectoryStr == QStringLiteral("-"));
0251             const bool isDirectory = isDirectoryStr.startsWith(QLatin1Char('+'));
0252             m_currentArchiveEntry->setProperty("isDirectory", isDirectory);
0253             fixDirectoryFullName();
0254 
0255         } else if (line.startsWith(QLatin1String("Attributes = "))) {
0256             const QString attributes = line.mid(13).trimmed();
0257             if (attributes.contains(QLatin1Char('D'))) {
0258                 m_currentArchiveEntry->setProperty("isDirectory", true);
0259                 fixDirectoryFullName();
0260             }
0261 
0262             if (attributes.contains(QLatin1Char('_'))) {
0263                 // Unix attributes
0264                 m_currentArchiveEntry->setProperty("permissions", attributes.mid(attributes.indexOf(QLatin1Char(' ')) + 1));
0265             } else {
0266                 // FAT attributes
0267                 m_currentArchiveEntry->setProperty("permissions", attributes);
0268             }
0269 
0270         } else if (line.startsWith(QLatin1String("CRC = "))) {
0271             m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed());
0272 
0273         } else if (line.startsWith(QLatin1String("Method = "))) {
0274             m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed());
0275 
0276             // For zip archives we need to check method for each entry.
0277             if (m_archiveType == ArchiveTypeZip) {
0278                 QStringList methods = line.section(QLatin1Char('='), 1).trimmed().split(QLatin1Char(' '), Qt::SkipEmptyParts);
0279                 handleMethods(methods);
0280             }
0281 
0282         } else if (line.startsWith(QLatin1String("Encrypted = ")) && line.size() >= 13) {
0283             m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+'));
0284 
0285         } else if (line.startsWith(QLatin1String("Block = ")) || line.startsWith(QLatin1String("Version = "))) {
0286             m_isFirstInformationEntry = true;
0287             if (!m_currentArchiveEntry->fullPath().isEmpty()) {
0288                 Q_EMIT entry(m_currentArchiveEntry);
0289             } else {
0290                 delete m_currentArchiveEntry;
0291             }
0292             m_currentArchiveEntry = nullptr;
0293         }
0294         break;
0295     }
0296 
0297     return true;
0298 }
0299 
0300 bool CliPlugin::readExtractLine(const QString &line)
0301 {
0302     if (line.startsWith(QLatin1String("ERROR: E_FAIL"))) {
0303         Q_EMIT error(i18n("Extraction failed due to an unknown error."));
0304         return false;
0305     }
0306 
0307     if (line.startsWith(QLatin1String("ERROR: CRC Failed")) || line.startsWith(QLatin1String("ERROR: Headers Error"))) {
0308         Q_EMIT error(i18n("Extraction failed due to one or more corrupt files. Any extracted files may be damaged."));
0309         return false;
0310     }
0311 
0312     return true;
0313 }
0314 
0315 bool CliPlugin::readDeleteLine(const QString &line)
0316 {
0317     if (line.startsWith(QLatin1String("Error: ")) && line.endsWith(QLatin1String(" is not supported archive"))) {
0318         Q_EMIT error(i18n("Delete operation failed. Try upgrading 7z or disabling the 7z plugin in the configuration dialog."));
0319         return false;
0320     }
0321 
0322     return true;
0323 }
0324 
0325 void CliPlugin::handleMethods(const QStringList &methods)
0326 {
0327     for (const QString &method : methods) {
0328         QRegularExpression rxEncMethod(QStringLiteral("^(7zAES|AES-128|AES-192|AES-256|ZipCrypto)$"));
0329         if (rxEncMethod.match(method).hasMatch()) {
0330             QRegularExpression rxAESMethods(QStringLiteral("^(AES-128|AES-192|AES-256)$"));
0331             if (rxAESMethods.match(method).hasMatch()) {
0332                 // Remove dash for AES methods.
0333                 Q_EMIT encryptionMethodFound(QString(method).remove(QLatin1Char('-')));
0334             } else {
0335                 Q_EMIT encryptionMethodFound(method);
0336             }
0337             continue;
0338         }
0339 
0340         // LZMA methods are output with some trailing numbers by 7z representing dictionary/block sizes.
0341         // We are not interested in these, so remove them.
0342         if (method.startsWith(QLatin1String("LZMA2"))) {
0343             Q_EMIT compressionMethodFound(method.left(5));
0344         } else if (method.startsWith(QLatin1String("LZMA"))) {
0345             Q_EMIT compressionMethodFound(method.left(4));
0346         } else if (method == QLatin1String("xz")) {
0347             Q_EMIT compressionMethodFound(method.toUpper());
0348         } else {
0349             Q_EMIT compressionMethodFound(method);
0350         }
0351     }
0352 }
0353 
0354 bool CliPlugin::isPasswordPrompt(const QString &line)
0355 {
0356     return line.startsWith(QLatin1String("Enter password"));
0357 }
0358 
0359 bool CliPlugin::isWrongPasswordMsg(const QString &line)
0360 {
0361     return line.contains(QLatin1String("Wrong password"));
0362 }
0363 
0364 bool CliPlugin::isCorruptArchiveMsg(const QString &line)
0365 {
0366     return (line == QLatin1String("Unexpected end of archive") || line == QLatin1String("Headers Error"));
0367 }
0368 
0369 bool CliPlugin::isDiskFullMsg(const QString &line)
0370 {
0371     return line.contains(QLatin1String("No space left on device"));
0372 }
0373 
0374 bool CliPlugin::isFileExistsMsg(const QString &line)
0375 {
0376     return (line == QLatin1String("(Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? ")
0377             || line == QLatin1String("? (Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? "));
0378 }
0379 
0380 bool CliPlugin::isFileExistsFileName(const QString &line)
0381 {
0382     return (line.startsWith(QLatin1String("file ./")) || line.startsWith(QLatin1String("  Path:     ./")));
0383 }
0384 
0385 #include "cliplugin.moc"
0386 #include "moc_cliplugin.cpp"