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

0001 /*
0002     SPDX-FileCopyrightText: 2011 Luke Shumaker <lukeshu@sbcglobal.net>
0003     SPDX-FileCopyrightText: 2016 Elvis Angelaccio <elvis.angelaccio@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "cliplugin.h"
0009 #include "ark_debug.h"
0010 #include "queries.h"
0011 
0012 #include <QJsonArray>
0013 #include <QJsonParseError>
0014 
0015 #include <KLocalizedString>
0016 #include <KPluginFactory>
0017 
0018 #ifndef Q_OS_WIN
0019 #include <KPtyProcess>
0020 #endif
0021 
0022 using namespace Kerfuffle;
0023 
0024 K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_cliunarchiver.json")
0025 
0026 CliPlugin::CliPlugin(QObject *parent, const QVariantList &args)
0027     : CliInterface(parent, args)
0028 {
0029     qCDebug(ARK) << "Loaded cli_unarchiver plugin";
0030     setupCliProperties();
0031 }
0032 
0033 CliPlugin::~CliPlugin()
0034 {
0035 }
0036 
0037 bool CliPlugin::list()
0038 {
0039     resetParsing();
0040     m_operationMode = List;
0041 
0042     return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password()));
0043 }
0044 
0045 bool CliPlugin::extractFiles(const QVector<Archive::Entry *> &files, const QString &destinationDirectory, const ExtractionOptions &options)
0046 {
0047     ExtractionOptions newOptions = options;
0048 
0049     // unar has the following limitations:
0050     // 1. creates an empty file upon entering a wrong password.
0051     // 2. detects that the stdout has been redirected and blocks the stdin.
0052     //    This prevents Ark from executing unar's overwrite queries.
0053     // To prevent both, we always extract to a temporary directory
0054     // and then we move the files to the intended destination.
0055 
0056     qCDebug(ARK) << "Enabling extraction to temporary directory.";
0057     newOptions.setAlwaysUseTempDir(true);
0058 
0059     return CliInterface::extractFiles(files, destinationDirectory, newOptions);
0060 }
0061 
0062 void CliPlugin::resetParsing()
0063 {
0064     m_jsonOutput.clear();
0065     m_numberOfVolumes = 0;
0066 }
0067 
0068 void CliPlugin::setupCliProperties()
0069 {
0070     m_cliProps->setProperty("captureProgress", false);
0071 
0072     m_cliProps->setProperty("extractProgram", QStringLiteral("unar"));
0073     m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("-D")});
0074     m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-D")});
0075 
0076     m_cliProps->setProperty("listProgram", QStringLiteral("lsar"));
0077     m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-json")});
0078 
0079     m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-password"), QStringLiteral("$Password")});
0080 }
0081 
0082 bool CliPlugin::readListLine(const QString &line)
0083 {
0084     const QRegularExpression rx(QStringLiteral("Failed! \\((.+)\\)$"));
0085 
0086     if (rx.match(line).hasMatch()) {
0087         Q_EMIT error(i18n("Listing the archive failed."));
0088         return false;
0089     }
0090 
0091     return true;
0092 }
0093 
0094 bool CliPlugin::readExtractLine(const QString &line)
0095 {
0096     const QRegularExpression rx(QStringLiteral("Failed! \\((.+)\\)$"));
0097 
0098     if (rx.match(line).hasMatch()) {
0099         Q_EMIT error(i18n("Extraction failed."));
0100         return false;
0101     }
0102 
0103     return true;
0104 }
0105 
0106 void CliPlugin::setJsonOutput(const QString &jsonOutput)
0107 {
0108     m_jsonOutput = jsonOutput;
0109     readJsonOutput();
0110 }
0111 
0112 void CliPlugin::readStdout(bool handleAll)
0113 {
0114     if (!handleAll) {
0115         CliInterface::readStdout(false);
0116         return;
0117     }
0118 
0119     // We are ready to read the json output.
0120     readJsonOutput();
0121 }
0122 
0123 bool CliPlugin::handleLine(const QString &line)
0124 {
0125     // Collect the json output line by line.
0126     if (m_operationMode == List) {
0127         // #372210: lsar can generate huge JSONs for big archives.
0128         // We can at least catch a bad_alloc here in order to not crash.
0129         try {
0130             m_jsonOutput += line + QLatin1Char('\n');
0131         } catch (const std::bad_alloc &) {
0132             m_jsonOutput.clear();
0133             Q_EMIT error(i18n("Not enough memory for loading the archive."));
0134             return false;
0135         }
0136     }
0137 
0138     if (m_operationMode == List) {
0139         // This can only be an header-encrypted archive.
0140         if (isPasswordPrompt(line)) {
0141             qCDebug(ARK) << "Detected header-encrypted RAR archive";
0142 
0143             Kerfuffle::PasswordNeededQuery query(filename());
0144             Q_EMIT userQuery(&query);
0145             query.waitForResponse();
0146 
0147             if (query.responseCancelled()) {
0148                 Q_EMIT cancelled();
0149                 // Process is gone, so we emit finished() manually and we return true.
0150                 Q_EMIT finished(false);
0151                 return true;
0152             }
0153 
0154             setPassword(query.password());
0155             CliPlugin::list();
0156         }
0157     }
0158 
0159     return true;
0160 }
0161 
0162 void CliPlugin::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
0163 {
0164     qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus;
0165 
0166     if (m_process) {
0167         // handle all the remaining data in the process
0168         readStdout(true);
0169 
0170         delete m_process;
0171         m_process = nullptr;
0172     }
0173 
0174     // #193908 - #222392
0175     // Don't Q_EMIT finished() if the job was killed quietly.
0176     if (m_abortingOperation) {
0177         return;
0178     }
0179 
0180     if (!password().isEmpty()) {
0181         // lsar -json exits with error code 1 if the archive is header-encrypted and the password is wrong.
0182         if (exitCode == 1) {
0183             qCWarning(ARK) << "Wrong password, list() aborted";
0184             Q_EMIT error(i18n("Wrong password."));
0185             Q_EMIT finished(false);
0186             setPassword(QString());
0187             return;
0188         }
0189     }
0190 
0191     // lsar -json exits with error code 2 if the archive is header-encrypted and no password is given as argument.
0192     // At this point we are asking a password to the user and we are going to list() again after we get one.
0193     // This means that we cannot Q_EMIT finished here.
0194     if (exitCode == 2) {
0195         return;
0196     }
0197 
0198     Q_EMIT finished(true);
0199 }
0200 
0201 void CliPlugin::readJsonOutput()
0202 {
0203     QJsonParseError error;
0204     QJsonDocument jsonDoc = QJsonDocument::fromJson(m_jsonOutput.toUtf8(), &error);
0205 
0206     if (error.error != QJsonParseError::NoError) {
0207         qCDebug(ARK) << "Could not parse json output:" << error.errorString();
0208         return;
0209     }
0210 
0211     const QJsonObject json = jsonDoc.object();
0212 
0213     const QJsonObject properties = json.value(QStringLiteral("lsarProperties")).toObject();
0214     const QJsonArray volumes = properties.value(QStringLiteral("XADVolumes")).toArray();
0215     if (volumes.count() > 1) {
0216         qCDebug(ARK) << "Detected multivolume archive";
0217         m_numberOfVolumes = volumes.count();
0218         setMultiVolume(true);
0219     }
0220 
0221     QString formatName = json.value(QStringLiteral("lsarFormatName")).toString();
0222     if (formatName == QLatin1String("RAR")) {
0223         Q_EMIT compressionMethodFound(QStringLiteral("RAR4"));
0224     } else if (formatName == QLatin1String("RAR 5")) {
0225         Q_EMIT compressionMethodFound(QStringLiteral("RAR5"));
0226     }
0227     const QJsonArray entries = json.value(QStringLiteral("lsarContents")).toArray();
0228 
0229     for (const QJsonValue &value : entries) {
0230         const QJsonObject currentEntryJson = value.toObject();
0231 
0232         Archive::Entry *currentEntry = new Archive::Entry(this);
0233 
0234         QString filename = currentEntryJson.value(QStringLiteral("XADFileName")).toString();
0235 
0236         currentEntry->setProperty("isDirectory", !currentEntryJson.value(QStringLiteral("XADIsDirectory")).isUndefined());
0237         if (currentEntry->isDir()) {
0238             filename += QLatin1Char('/');
0239         }
0240 
0241         currentEntry->setProperty("fullPath", filename);
0242 
0243         // FIXME: archives created from OSX (i.e. with the __MACOSX folder) list each entry twice, the 2nd time with size 0
0244         currentEntry->setProperty("size", currentEntryJson.value(QStringLiteral("XADFileSize")));
0245         currentEntry->setProperty("compressedSize", currentEntryJson.value(QStringLiteral("XADCompressedSize")));
0246         currentEntry->setProperty("timestamp", currentEntryJson.value(QStringLiteral("XADLastModificationDate")).toVariant());
0247         currentEntry->setProperty("size", currentEntryJson.value(QStringLiteral("XADFileSize")));
0248         const bool isPasswordProtected = (currentEntryJson.value(QStringLiteral("XADIsEncrypted")).toInt() == 1);
0249         currentEntry->setProperty("isPasswordProtected", isPasswordProtected);
0250         if (isPasswordProtected) {
0251             formatName == QLatin1String("RAR 5") ? Q_EMIT encryptionMethodFound(QStringLiteral("AES256"))
0252                                                  : Q_EMIT encryptionMethodFound(QStringLiteral("AES128"));
0253         }
0254         // TODO: missing fields
0255 
0256         // FIXME: currently with multivolume archives we emit the same entry multiple times because they occur multiple times in the CLI output...
0257         // This breaks at least the numberOfEntries() method, and possibly other things.
0258         Q_EMIT entry(currentEntry);
0259     }
0260 }
0261 
0262 bool CliPlugin::isPasswordPrompt(const QString &line)
0263 {
0264     return (line == QLatin1String("This archive requires a password to unpack. Use the -p option to provide one."));
0265 }
0266 
0267 #include "cliplugin.moc"
0268 #include "moc_cliplugin.cpp"