File indexing completed on 2024-05-12 05:50:21
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 <KLocalizedString> 0013 #include <KPluginFactory> 0014 0015 #include <QDateTime> 0016 #include <QDir> 0017 #include <QRegularExpression> 0018 #include <QTemporaryDir> 0019 0020 using namespace Kerfuffle; 0021 0022 K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_clizip.json") 0023 0024 CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) 0025 : CliInterface(parent, args) 0026 , m_parseState(ParseStateHeader) 0027 , m_linesComment(0) 0028 { 0029 qCDebug(ARK) << "Loaded cli_zip plugin"; 0030 setupCliProperties(); 0031 } 0032 0033 CliPlugin::~CliPlugin() 0034 { 0035 } 0036 0037 void CliPlugin::resetParsing() 0038 { 0039 m_parseState = ParseStateHeader; 0040 m_tempComment.clear(); 0041 m_comment.clear(); 0042 } 0043 0044 // #208091: infozip applies special meanings to some characters, so we 0045 // need to escape them with backslashes.see match.c in 0046 // infozip's source code 0047 QString CliPlugin::escapeFileName(const QString &fileName) const 0048 { 0049 const QString escapedCharacters(QStringLiteral("[]*?^-\\!")); 0050 0051 QString quoted; 0052 const int len = fileName.length(); 0053 const QLatin1Char backslash('\\'); 0054 quoted.reserve(len * 2); 0055 0056 for (int i = 0; i < len; ++i) { 0057 if (escapedCharacters.contains(fileName.at(i))) { 0058 quoted.append(backslash); 0059 } 0060 0061 quoted.append(fileName.at(i)); 0062 } 0063 0064 return quoted; 0065 } 0066 0067 void CliPlugin::setupCliProperties() 0068 { 0069 qCDebug(ARK) << "Setting up parameters..."; 0070 0071 m_cliProps->setProperty("captureProgress", false); 0072 0073 m_cliProps->setProperty("addProgram", QStringLiteral("zip")); 0074 m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("-r")})); 0075 0076 m_cliProps->setProperty("deleteProgram", QStringLiteral("zip")); 0077 m_cliProps->setProperty("deleteSwitch", QStringLiteral("-d")); 0078 0079 m_cliProps->setProperty("extractProgram", QStringLiteral("unzip")); 0080 m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-j")}); 0081 0082 m_cliProps->setProperty("listProgram", QStringLiteral("zipinfo")); 0083 m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z")}); 0084 0085 m_cliProps->setProperty("testProgram", QStringLiteral("unzip")); 0086 m_cliProps->setProperty("testSwitch", QStringLiteral("-t")); 0087 0088 m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-P$Password")}); 0089 0090 m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-$CompressionLevel")); 0091 m_cliProps->setProperty("compressionMethodSwitch", 0092 QHash<QString, QVariant>{{QStringLiteral("application/zip"), QStringLiteral("-Z$CompressionMethod")}, 0093 {QStringLiteral("application/x-java-archive"), QStringLiteral("-Z$CompressionMethod")}}); 0094 m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); 0095 0096 m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^No errors detected in compressed data of ")}); 0097 m_cliProps->setProperty("fileExistsFileNameRegExp", 0098 QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")}); 0099 m_cliProps->setProperty("fileExistsInput", 0100 QStringList{ 0101 QStringLiteral("y"), // Overwrite 0102 QStringLiteral("n"), // Skip 0103 QStringLiteral("A"), // Overwrite all 0104 QStringLiteral("N"), // Autoskip 0105 }); 0106 m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("unsupported compression method")}); 0107 } 0108 0109 bool CliPlugin::readListLine(const QString &line) 0110 { 0111 static const QRegularExpression entryPattern( 0112 QStringLiteral("^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$")); 0113 0114 // RegExp to identify the line preceding comments. 0115 const QRegularExpression commentPattern(QStringLiteral("^Archive: .*$")); 0116 // RegExp to identify the line following comments. 0117 const QRegularExpression commentEndPattern(QStringLiteral("^Zip file size: .*$")); 0118 0119 switch (m_parseState) { 0120 case ParseStateHeader: 0121 if (commentPattern.match(line).hasMatch()) { 0122 m_parseState = ParseStateComment; 0123 } else if (commentEndPattern.match(line).hasMatch()) { 0124 m_parseState = ParseStateEntry; 0125 } 0126 break; 0127 case ParseStateComment: 0128 if (commentEndPattern.match(line).hasMatch()) { 0129 m_parseState = ParseStateEntry; 0130 if (!m_tempComment.trimmed().isEmpty()) { 0131 m_comment = m_tempComment.trimmed(); 0132 m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; 0133 qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; 0134 } 0135 } else { 0136 m_tempComment.append(line + QLatin1Char('\n')); 0137 } 0138 break; 0139 case ParseStateEntry: 0140 QRegularExpressionMatch rxMatch = entryPattern.match(line); 0141 if (rxMatch.hasMatch()) { 0142 Archive::Entry *e = new Archive::Entry(this); 0143 e->setProperty("permissions", rxMatch.captured(1)); 0144 0145 // #280354: infozip may not show the right attributes for a given directory, so an entry 0146 // ending with '/' is actually more reliable than 'd' bein in the attributes. 0147 e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); 0148 0149 e->setProperty("size", rxMatch.captured(4)); 0150 QString status = rxMatch.captured(5); 0151 if (status[0].isUpper()) { 0152 e->setProperty("isPasswordProtected", true); 0153 } 0154 e->setProperty("compressedSize", rxMatch.captured(6).toInt()); 0155 e->setProperty("method", rxMatch.captured(7)); 0156 0157 QString method = convertCompressionMethod(rxMatch.captured(7)); 0158 Q_EMIT compressionMethodFound(method); 0159 0160 const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), 0161 QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); 0162 e->setProperty("timestamp", ts); 0163 0164 e->setProperty("fullPath", rxMatch.captured(10)); 0165 Q_EMIT entry(e); 0166 } 0167 break; 0168 } 0169 0170 return true; 0171 } 0172 0173 bool CliPlugin::readExtractLine(const QString &line) 0174 { 0175 const QRegularExpression rxUnsupCompMethod(QStringLiteral("unsupported compression method (\\d+)")); 0176 const QRegularExpression rxUnsupEncMethod(QStringLiteral("need PK compat. v\\d\\.\\d \\(can do v\\d\\.\\d\\)")); 0177 const QRegularExpression rxBadCRC(QStringLiteral("bad CRC")); 0178 0179 QRegularExpressionMatch unsupCompMethodMatch = rxUnsupCompMethod.match(line); 0180 if (unsupCompMethodMatch.hasMatch()) { 0181 Q_EMIT error(i18n("Extraction failed due to unsupported compression method (%1).", unsupCompMethodMatch.captured(1))); 0182 return false; 0183 } 0184 0185 if (rxUnsupEncMethod.match(line).hasMatch()) { 0186 Q_EMIT error(i18n("Extraction failed due to unsupported encryption method.")); 0187 return false; 0188 } 0189 0190 if (rxBadCRC.match(line).hasMatch()) { 0191 Q_EMIT error(i18n("Extraction failed due to one or more corrupt files. Any extracted files may be damaged.")); 0192 return false; 0193 } 0194 0195 return true; 0196 } 0197 0198 bool CliPlugin::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options) 0199 { 0200 qCDebug(ARK) << "Moving" << files.count() << "file(s) to destination:" << destination; 0201 0202 m_oldWorkingDir = QDir::currentPath(); 0203 m_tempWorkingDir.reset(new QTemporaryDir()); 0204 m_tempAddDir.reset(new QTemporaryDir()); 0205 QDir::setCurrent(m_tempWorkingDir->path()); 0206 m_passedFiles = files; 0207 m_passedDestination = destination; 0208 m_passedOptions = options; 0209 0210 m_subOperation = Extract; 0211 connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); 0212 0213 return extractFiles(files, QDir::currentPath(), ExtractionOptions()); 0214 } 0215 0216 int CliPlugin::moveRequiredSignals() const 0217 { 0218 return 4; 0219 } 0220 0221 void CliPlugin::continueMoving(bool result) 0222 { 0223 if (!result) { 0224 finishMoving(false); 0225 return; 0226 } 0227 0228 switch (m_subOperation) { 0229 case Extract: 0230 m_subOperation = Delete; 0231 if (!deleteFiles(m_passedFiles)) { 0232 finishMoving(false); 0233 } 0234 break; 0235 case Delete: 0236 m_subOperation = Add; 0237 if (!setMovingAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { 0238 finishMoving(false); 0239 } 0240 break; 0241 case Add: 0242 finishMoving(true); 0243 break; 0244 default: 0245 Q_ASSERT(false); 0246 } 0247 } 0248 0249 bool CliPlugin::setMovingAddedFiles() 0250 { 0251 m_passedFiles = entriesWithoutChildren(m_passedFiles); 0252 // If there are more files being moved than 1, we have destination as a destination folder, 0253 // otherwise it's new entry full path. 0254 if (m_passedFiles.count() > 1) { 0255 return setAddedFiles(); 0256 } 0257 0258 QDir::setCurrent(m_tempAddDir->path()); 0259 const Archive::Entry *file = m_passedFiles.at(0); 0260 const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); 0261 const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + m_passedDestination->name(); 0262 if (!QFile::rename(oldPath, newPath)) { 0263 return false; 0264 } 0265 m_tempAddedFiles << new Archive::Entry(nullptr, m_passedDestination->name()); 0266 0267 // We have to exclude file name from destination path in order to pass it to addFiles method. 0268 const QString destinationPath = m_passedDestination->fullPath(); 0269 const int slashCount = destinationPath.count(QLatin1Char('/')); 0270 if (slashCount > 1 || (slashCount == 1 && !destinationPath.endsWith(QLatin1Char('/')))) { 0271 int destinationLength = destinationPath.length(); 0272 bool iteratedChar = false; 0273 do { 0274 destinationLength--; 0275 if (destinationPath.at(destinationLength) != QLatin1Char('/')) { 0276 iteratedChar = true; 0277 } 0278 } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/'))); 0279 m_passedDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1)); 0280 } else { 0281 // ...unless the destination path is already a single folder, e.g. "dir/", or a file, e.g. "foo.txt". 0282 // In this case we're going to add to the root, so we just need to set a null destination. 0283 m_passedDestination = nullptr; 0284 } 0285 0286 return true; 0287 } 0288 0289 void CliPlugin::finishMoving(bool result) 0290 { 0291 disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); 0292 Q_EMIT progress(1.0); 0293 Q_EMIT finished(result); 0294 cleanUp(); 0295 } 0296 0297 QString CliPlugin::convertCompressionMethod(const QString &method) 0298 { 0299 if (method == QLatin1String("stor")) { 0300 return QStringLiteral("Store"); 0301 } else if (method.startsWith(QLatin1String("def"))) { 0302 return QStringLiteral("Deflate"); 0303 } else if (method == QLatin1String("d64N")) { 0304 return QStringLiteral("Deflate64"); 0305 } else if (method == QLatin1String("bzp2")) { 0306 return QStringLiteral("BZip2"); 0307 } else if (method == QLatin1String("lzma")) { 0308 return QStringLiteral("LZMA"); 0309 } else if (method == QLatin1String("ppmd")) { 0310 return QStringLiteral("PPMd"); 0311 } else if (method == QLatin1String("u095")) { 0312 return QStringLiteral("XZ"); 0313 } else if (method == QLatin1String("u099")) { 0314 Q_EMIT encryptionMethodFound(QStringLiteral("AES")); 0315 return i18nc("referred to compression method", "unknown"); 0316 } 0317 return method; 0318 } 0319 0320 bool CliPlugin::isPasswordPrompt(const QString &line) 0321 { 0322 return line.endsWith(QLatin1String(" password: ")); 0323 } 0324 0325 bool CliPlugin::isWrongPasswordMsg(const QString &line) 0326 { 0327 return line.endsWith(QLatin1String("incorrect password")); 0328 } 0329 0330 bool CliPlugin::isCorruptArchiveMsg(const QString &line) 0331 { 0332 return (line.contains(QLatin1String("End-of-central-directory signature not found")) 0333 || line.contains(QLatin1String("didn't find end-of-central-dir signature at end of central dir"))); 0334 } 0335 0336 bool CliPlugin::isDiskFullMsg(const QString &line) 0337 { 0338 return (line.contains(QLatin1String("No space left on device")) || line.contains(QLatin1String("write error (disk full?)"))); 0339 } 0340 0341 bool CliPlugin::isFileExistsMsg(const QString &line) 0342 { 0343 return (line.startsWith(QLatin1String("replace ")) && line.endsWith(QLatin1String("? [y]es, [n]o, [A]ll, [N]one, [r]ename: "))); 0344 } 0345 0346 bool CliPlugin::isFileExistsFileName(const QString &line) 0347 { 0348 return (line.startsWith(QLatin1String("replace ")) && line.endsWith(QLatin1String("? [y]es, [n]o, [A]ll, [N]one, [r]ename: "))); 0349 } 0350 0351 #include "cliplugin.moc" 0352 #include "moc_cliplugin.cpp"