File indexing completed on 2024-05-12 05:50:20
0001 /* 0002 SPDX-FileCopyrightText: 2022 Ilya Pominov <ipominov@astralinux.ru> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "cliplugin.h" 0008 #include "ark_debug.h" 0009 0010 #include <QDateTime> 0011 #include <QFileInfo> 0012 #include <QStringView> 0013 0014 #include <KLocalizedString> 0015 #include <KPluginFactory> 0016 0017 using namespace Kerfuffle; 0018 0019 K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_cliarj.json") 0020 0021 struct ArjFileEntry { 0022 enum EncryptedMethod { 0023 // https://sourceforge.net/p/arj/git/ci/master/tree/defines.h#l104 0024 EncryptedMethodArjOld = 0, 0025 EncryptedMethodArjStd = 1, 0026 EncryptedMethodGost256 = 2, 0027 EncryptedMethodGost256L = 3, 0028 EncryptedMethodGost40bit = 4, 0029 EncryptedMethodUnknown = 16 0030 }; 0031 0032 QString fileName() const 0033 { 0034 return QFileInfo(m_path).fileName(); 0035 } 0036 0037 bool isExecutable() const 0038 { 0039 return m_attributes.contains(QLatin1Char('x')); 0040 } 0041 0042 int m_currentEntryNumber = 0; 0043 QString m_path; 0044 QStringList m_comments; 0045 bool m_commentsEnd = false; 0046 int m_version = 0; 0047 qulonglong m_origSize = 0; 0048 qulonglong m_compressedSize = 0; 0049 double m_ratio = 0.; 0050 QDateTime m_timeStamp; 0051 QString m_attributes; 0052 bool m_encrypted = false; 0053 EncryptedMethod m_encryptedMethod = EncryptedMethodUnknown; 0054 }; 0055 0056 CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) 0057 : CliInterface(parent, args) 0058 { 0059 qCDebug(ARK) << "Loaded cli_arj plugin"; 0060 0061 setupCliProperties(); 0062 } 0063 0064 CliPlugin::~CliPlugin() = default; 0065 0066 bool CliPlugin::addFiles(const QVector<Kerfuffle::Archive::Entry *> &files, 0067 const Kerfuffle::Archive::Entry *destination, 0068 const Kerfuffle::CompressionOptions &options, 0069 uint numberOfEntriesToAdd) 0070 { 0071 auto opt = options; 0072 if (opt.compressionMethod() == QStringLiteral("Standard")) { 0073 opt.setCompressionMethod(QString()); 0074 } else if (opt.compressionMethod() == QStringLiteral("GOST 40-bit")) { 0075 opt.setCompressionMethod(QStringLiteral("!")); 0076 } 0077 0078 return CliInterface::addFiles(files, destination, opt, numberOfEntriesToAdd); 0079 } 0080 0081 bool CliPlugin::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options) 0082 { 0083 Q_UNUSED(options); 0084 0085 m_operationMode = Move; 0086 0087 QVector<Archive::Entry *> withoutChildren = entriesWithoutChildren(files); 0088 m_renamedFiles = files; 0089 setNewMovedFiles(files, destination, withoutChildren.count()); 0090 0091 QStringList args = cliProperties()->moveArgs(filename(), withoutChildren, nullptr, password()); 0092 0093 return runProcess(cliProperties()->property("moveProgram").toString(), args); 0094 } 0095 0096 void CliPlugin::resetParsing() 0097 { 0098 m_comment.clear(); 0099 m_numberOfVolumes = 0; 0100 0101 m_parseState = ParseStateTitle; 0102 m_remainingIgnoreLines = 0; 0103 m_headerComment.clear(); 0104 m_currentParsedFile.reset(new ArjFileEntry()); 0105 m_testPassed = true; 0106 m_renamedFiles.clear(); 0107 } 0108 0109 bool CliPlugin::readListLine(const QString &line) 0110 { 0111 auto res = readLine(line); 0112 if (m_parseState == ParseStateEntryTotal && res) { 0113 m_comment = m_headerComment.join(QLatin1Char('\n')); 0114 } 0115 return res; 0116 } 0117 0118 bool CliPlugin::readExtractLine(const QString &line) 0119 { 0120 Q_UNUSED(line); 0121 0122 return true; 0123 } 0124 0125 bool CliPlugin::isFileExistsMsg(const QString &line) 0126 { 0127 return line.contains(QStringLiteral("is same or newer, Overwrite?")); 0128 } 0129 0130 bool CliPlugin::isFileExistsFileName(const QString &line) 0131 { 0132 return line.contains(QStringLiteral("is same or newer, Overwrite?")); 0133 } 0134 0135 bool CliPlugin::isNewMovedFileNamesMsg(const QString &line) 0136 { 0137 return line.startsWith(QStringLiteral("Current filename:")); 0138 } 0139 0140 bool CliPlugin::handleLine(const QString &line) 0141 { 0142 if (line.contains(QStringLiteral("bad password"))) { 0143 qCWarning(ARK) << "Wrong password!"; 0144 setPassword(QString()); 0145 Q_EMIT error(i18nc("@info", "Extraction failed: Incorrect password")); 0146 return false; 0147 } 0148 0149 if (m_operationMode == Test) { 0150 if (line.startsWith(QStringLiteral("Testing "))) { 0151 auto list = line.split(QStringLiteral("\b\b\b\b\b")); 0152 if (list.isEmpty() || !list.last().startsWith(QStringLiteral("OK"))) { 0153 m_testPassed = false; 0154 } 0155 } 0156 0157 if (line.contains(QStringLiteral("file(s)")) && m_testPassed) { 0158 qCDebug(ARK) << "Test successful"; 0159 Q_EMIT testSuccess(); 0160 } 0161 0162 return true; 0163 } 0164 0165 return CliInterface::handleLine(line); 0166 } 0167 0168 void CliPlugin::processFinished(int exitCode, QProcess::ExitStatus exitStatus) 0169 { 0170 if (m_operationMode == Move && exitCode == 0 && exitStatus == QProcess::NormalExit) { 0171 const QStringList removedFullPaths = entryFullPaths(m_renamedFiles); 0172 for (const QString &fullPath : removedFullPaths) { 0173 Q_EMIT entryRemoved(fullPath); 0174 } 0175 } 0176 CliInterface::processFinished(exitCode, exitStatus); 0177 } 0178 0179 void CliPlugin::setupCliProperties() 0180 { 0181 qCDebug(ARK) << "Setting up parameters..."; 0182 0183 CliProperties *cliProps = cliProperties(); 0184 cliProps->setProperty("captureProgress", true); 0185 0186 cliProps->setProperty("addProgram", QStringLiteral("arj")); 0187 cliProps->setProperty("addSwitch", QStringList{QStringLiteral("a"), QStringLiteral("-r")}); 0188 0189 cliProps->setProperty("deleteProgram", QStringLiteral("arj")); 0190 cliProps->setProperty("deleteSwitch", QStringLiteral("d")); 0191 0192 cliProps->setProperty("extractProgram", QStringLiteral("arj")); 0193 cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x"), QStringLiteral("-p1"), QStringLiteral("-jyc")}); 0194 cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e")}); 0195 0196 cliProps->setProperty("listProgram", QStringLiteral("arj")); 0197 cliProps->setProperty("listSwitch", QStringLiteral("v")); 0198 0199 cliProps->setProperty("moveProgram", QStringLiteral("arj")); 0200 cliProps->setProperty("moveSwitch", QStringLiteral("n")); 0201 0202 cliProps->setProperty("testProgram", QStringLiteral("arj")); 0203 cliProps->setProperty("testSwitch", QStringLiteral("t")); 0204 0205 cliProps->setProperty("passwordSwitch", QStringLiteral("-g$Password")); 0206 0207 cliProps->setProperty("compressionMethodSwitch", 0208 QHash<QString, QVariant>{{QStringLiteral("application/x-arj"), QStringLiteral("-m$CompressionMethod")}, 0209 {QStringLiteral("application/arj"), QStringLiteral("-m$CompressionMethod")}}); 0210 cliProps->setProperty("encryptionMethodSwitch", 0211 QHash<QString, QVariant>{{QStringLiteral("application/x-arj"), QStringLiteral("-hg$EncryptionMethod")}, 0212 {QStringLiteral("application/arj"), QStringLiteral("-hg$EncryptionMethod")}}); 0213 cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); 0214 0215 cliProps->setProperty("fileExistsFileNameRegExp", QStringList{QStringLiteral("^file \\./(.*)$"), QStringLiteral("^ Path: \\./(.*)$")}); 0216 0217 cliProps->setProperty("commentSwitch", QStringList{QStringLiteral("c"), QStringLiteral("-z$CommentFile")}); 0218 0219 cliProps->setProperty("fileExistsInput", 0220 QStringList{ 0221 QStringLiteral("Yes"), // Overwrite 0222 QStringLiteral("No"), // Skip 0223 QStringLiteral("Always"), // Overwrite all 0224 QStringLiteral("Skip"), // Autoskip 0225 QStringLiteral("Quit"), // Cancel 0226 }); 0227 cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("$Suffix.001")}); 0228 } 0229 0230 void CliPlugin::ignoreLines(int lines, ParseState nextState) 0231 { 0232 m_remainingIgnoreLines = lines; 0233 m_parseState = nextState; 0234 } 0235 0236 bool CliPlugin::tryAddCurFileProperties(const QString &line) 0237 { 0238 // 79 is fixed property line size 0239 if (line.size() != 79) { 0240 return false; 0241 } 0242 // Rev/Host OS Original Compressed Ratio DateTime modified Attributes/GUA BPMGS 0243 // ------------ ---------- ---------- ----- ----------------- -------------- ----- 0244 QStringList revHost = line.left(12).trimmed().split(QLatin1Char(' ')); 0245 bool ok; 0246 m_currentParsedFile->m_version = revHost.first().toInt(&ok); 0247 if (!ok) { 0248 return false; 0249 } 0250 0251 m_currentParsedFile->m_origSize = QStringView(line).mid(13, 10).toULongLong(&ok); 0252 if (!ok) { 0253 return false; 0254 } 0255 0256 m_currentParsedFile->m_compressedSize = QStringView(line).mid(24, 10).toULongLong(&ok); 0257 if (!ok) { 0258 return false; 0259 } 0260 0261 m_currentParsedFile->m_ratio = QStringView(line).mid(35, 5).toDouble(&ok); 0262 if (!ok) { 0263 return false; 0264 } 0265 0266 m_currentParsedFile->m_timeStamp = QDateTime::fromString(line.mid(41, 17), QStringLiteral("yy-MM-dd hh:mm:ss")); 0267 if (!m_currentParsedFile->m_timeStamp.isValid()) { 0268 return false; 0269 } 0270 m_currentParsedFile->m_timeStamp = m_currentParsedFile->m_timeStamp.addYears(100); 0271 0272 m_currentParsedFile->m_attributes = line.mid(59, 14); 0273 QChar garble = line.at(77); 0274 if (garble != QLatin1Char(' ')) { 0275 m_currentParsedFile->m_encrypted = true; 0276 m_currentParsedFile->m_encryptedMethod = 0277 garble.isDigit() ? static_cast<ArjFileEntry::EncryptedMethod>(garble.digitValue()) : ArjFileEntry::EncryptedMethodUnknown; 0278 } 0279 return true; 0280 } 0281 0282 bool CliPlugin::tryAddCurFileComment(const QString &line) 0283 { 0284 // There can be only one empty line 0285 if (m_currentParsedFile->m_commentsEnd) { 0286 return false; 0287 } 0288 0289 if (line.isEmpty()) { 0290 m_currentParsedFile->m_commentsEnd = true; 0291 // If there is an empty line there should be comments 0292 return !m_currentParsedFile->m_comments.isEmpty(); 0293 } 0294 0295 // 25 is maximum comment lines count 0296 if (m_currentParsedFile->m_comments.size() == 25) { 0297 return false; 0298 } 0299 0300 m_currentParsedFile->m_comments << line; 0301 return true; 0302 } 0303 0304 void CliPlugin::sendCurFileEntry() 0305 { 0306 Archive::Entry *e = new Archive::Entry(this); 0307 0308 e->setProperty("fullPath", m_currentParsedFile->m_path); 0309 e->setProperty("name", m_currentParsedFile->fileName()); 0310 e->setProperty("permissions", m_currentParsedFile->m_attributes); 0311 e->setProperty("size", m_currentParsedFile->m_origSize); 0312 e->setProperty("compressedSize", m_currentParsedFile->m_compressedSize); 0313 e->setProperty("ratio", QStringLiteral("%1").arg(m_currentParsedFile->m_ratio, 0, 'f', 3)); 0314 e->setProperty("version", QStringLiteral("%1").arg(m_currentParsedFile->m_version)); 0315 e->setProperty("timestamp", m_currentParsedFile->m_timeStamp); 0316 e->setProperty("isDirectory", false); 0317 e->setProperty("isExecutable", m_currentParsedFile->isExecutable()); 0318 e->setProperty("isPasswordProtected", m_currentParsedFile->m_encrypted); 0319 if (m_currentParsedFile->m_encrypted) { 0320 const QMap<ArjFileEntry::EncryptedMethod, QString> methods = { 0321 {ArjFileEntry::EncryptedMethodArjOld, i18n("ARJ old")}, 0322 {ArjFileEntry::EncryptedMethodArjStd, i18n("ARJ")}, 0323 {ArjFileEntry::EncryptedMethodGost256, i18n("GOST256")}, 0324 {ArjFileEntry::EncryptedMethodGost256L, i18n("GOST256L")}, 0325 {ArjFileEntry::EncryptedMethodGost40bit, i18n("GOST 40-bit")}, 0326 }; 0327 e->setProperty("method", methods.value(m_currentParsedFile->m_encryptedMethod, i18n("unknown"))); 0328 } 0329 0330 Q_EMIT entry(e); 0331 } 0332 0333 bool CliPlugin::readLine(const QString &line) 0334 { 0335 // Ignore number of lines corresponding to m_remainingIgnoreLines. 0336 if (m_remainingIgnoreLines > 0) { 0337 --m_remainingIgnoreLines; 0338 return true; 0339 } 0340 0341 switch (m_parseState) { 0342 case ParseStateTitle: 0343 if (line.startsWith(QStringLiteral("ARJ"))) { 0344 // Can be obtained ARJ version 0345 m_parseState = ParseStateProcessing; 0346 return true; 0347 } 0348 return false; 0349 0350 case ParseStateProcessing: 0351 if (line.startsWith(QStringLiteral("Processing archive:"))) { 0352 // Can be obtained archive name 0353 m_parseState = ParseStateArchiveDateTime; 0354 return true; 0355 } 0356 return false; 0357 0358 case ParseStateArchiveDateTime: 0359 if (line.startsWith(QStringLiteral("Archive created:"))) { 0360 // Can be obtained archive created and modified times 0361 m_parseState = ParseStateArchiveComments; 0362 return true; 0363 } 0364 return false; 0365 0366 case ParseStateArchiveComments: 0367 if (line == QStringLiteral("Sequence/Pathname/Comment/Chapters")) { 0368 m_parseState = ParseStateEntryFileHeader; 0369 return true; 0370 } 0371 // 25 is maximum comment lines count 0372 if (m_headerComment.size() == 25) { 0373 return false; 0374 } 0375 m_headerComment << line; 0376 return true; 0377 0378 case ParseStateEntryFileHeader: 0379 if (line.startsWith(QStringLiteral("Rev/Host"))) { 0380 ignoreLines(1, ParseStateEntryFileName); 0381 m_currentParsedFile.reset(new ArjFileEntry()); 0382 return true; 0383 } 0384 return false; 0385 0386 case ParseStateEntryFileName: { 0387 // Send previos file entry 0388 if (m_currentParsedFile->m_currentEntryNumber > 0) { 0389 sendCurFileEntry(); 0390 } 0391 0392 // End file entry 0393 if (line.startsWith(QStringLiteral("------------"))) { 0394 if (m_currentParsedFile->m_currentEntryNumber > 0) { 0395 m_parseState = ParseStateEntryTotal; 0396 return true; 0397 } 0398 return false; 0399 } 0400 0401 // Next file entry 0402 QStringList fNameColumns = line.split(QStringLiteral(") ")); 0403 if (fNameColumns.size() == 2) { 0404 bool bOk = false; 0405 int fileNo = fNameColumns.first().toInt(&bOk); 0406 if (bOk && (fileNo - m_currentParsedFile->m_currentEntryNumber) == 1) { 0407 m_currentParsedFile.reset(new ArjFileEntry()); 0408 m_currentParsedFile->m_currentEntryNumber = fileNo; 0409 m_currentParsedFile->m_path = fNameColumns.last(); 0410 m_parseState = ParseStateEntryFileProperty; 0411 return true; 0412 } 0413 } 0414 return false; 0415 } 0416 0417 case ParseStateEntryFileProperty: 0418 if (tryAddCurFileProperties(line)) { 0419 m_parseState = ParseStateEntryFileDTA; 0420 return true; 0421 } 0422 // If property parsing fails, the line may be a comment 0423 return tryAddCurFileComment(line); 0424 0425 case ParseStateEntryFileDTA: 0426 if (line.mid(35, 5) == QStringLiteral("DTA ")) { 0427 // Can be obtained DTA timestamp 0428 m_parseState = ParseStateEntryFileDTC; 0429 return true; 0430 } 0431 return false; 0432 0433 case ParseStateEntryFileDTC: 0434 if (line.mid(35, 5) == QStringLiteral("DTC ")) { 0435 // Can be obtained DTC timestamp 0436 m_parseState = ParseStateEntryFileName; 0437 return true; 0438 } 0439 return false; 0440 0441 case ParseStateEntryTotal: 0442 if (line.size() == 41) { 0443 // Can be obtained files count, and total sizes 0444 return true; 0445 } 0446 return false; 0447 } 0448 return false; 0449 } 0450 0451 #include "cliplugin.moc" 0452 #include "moc_cliplugin.cpp"