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"