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"