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"