1 /*
2 SPDX-FileCopyrightText: 2007 Tobias Koenig <tokoe@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "unrar.h"
8
9 #include <QEventLoop>
10 #include <QFile>
11 #include <QFileInfo>
12 #include <QGlobalStatic>
13 #include <QTemporaryDir>
14
15 #include <QLoggingCategory>
16 #if defined(WITH_KPTY)
17 #include <KPty/kptydevice.h>
18 #include <KPty/kptyprocess.h>
19 #endif
20
21 #include "debug_comicbook.h"
22
23 #include <QRegularExpression>
24 #include <QStandardPaths>
25 #include <memory>
26
27 struct UnrarHelper {
28 UnrarHelper();
29 ~UnrarHelper();
30
31 UnrarHelper(const UnrarHelper &) = delete;
32 UnrarHelper &operator=(const UnrarHelper &) = delete;
33
34 UnrarFlavour *kind;
35 QString unrarPath;
36 QString lsarPath;
37 };
38
Q_GLOBAL_STATIC(UnrarHelper,helper)39 Q_GLOBAL_STATIC(UnrarHelper, helper)
40
41 static UnrarFlavour *detectUnrar(const QString &unrarPath, const QString &versionCommand)
42 {
43 UnrarFlavour *kind = nullptr;
44 QProcess proc;
45 proc.start(unrarPath, QStringList() << versionCommand);
46 bool ok = proc.waitForFinished(-1);
47 Q_UNUSED(ok)
48 const QRegularExpression regex(QStringLiteral("[\r\n]"));
49 const QStringList lines = QString::fromLocal8Bit(proc.readAllStandardOutput()).split(regex, QString::SkipEmptyParts);
50 if (!lines.isEmpty()) {
51 if (lines.first().startsWith(QLatin1String("UNRAR ")))
52 kind = new NonFreeUnrarFlavour();
53 else if (lines.first().startsWith(QLatin1String("RAR ")))
54 kind = new NonFreeUnrarFlavour();
55 else if (lines.first().startsWith(QLatin1String("unrar ")))
56 kind = new FreeUnrarFlavour();
57 else if (lines.first().startsWith(QLatin1String("v")))
58 kind = new UnarFlavour();
59 }
60 return kind;
61 }
62
UnrarHelper()63 UnrarHelper::UnrarHelper()
64 : kind(nullptr)
65 {
66 QString path = QStandardPaths::findExecutable(QStringLiteral("lsar"));
67
68 if (!path.isEmpty()) {
69 lsarPath = path;
70 }
71
72 path = QStandardPaths::findExecutable(QStringLiteral("unrar-nonfree"));
73
74 if (path.isEmpty())
75 path = QStandardPaths::findExecutable(QStringLiteral("unrar"));
76 if (path.isEmpty())
77 path = QStandardPaths::findExecutable(QStringLiteral("rar"));
78 if (path.isEmpty())
79 path = QStandardPaths::findExecutable(QStringLiteral("unar"));
80
81 if (!path.isEmpty())
82 kind = detectUnrar(path, QStringLiteral("--version"));
83
84 if (!kind)
85 kind = detectUnrar(path, QStringLiteral("-v"));
86
87 if (!kind) {
88 // no luck, print that
89 qWarning() << "Neither unrar nor unarchiver were found.";
90 } else {
91 unrarPath = path;
92 qCDebug(OkularComicbookDebug) << "detected:" << path << "(" << kind->name() << ")";
93 }
94 }
95
~UnrarHelper()96 UnrarHelper::~UnrarHelper()
97 {
98 delete kind;
99 }
100
Unrar()101 Unrar::Unrar()
102 : QObject(nullptr)
103 , mLoop(nullptr)
104 , mTempDir(nullptr)
105 {
106 }
107
~Unrar()108 Unrar::~Unrar()
109 {
110 delete mTempDir;
111 }
112
open(const QString & fileName)113 bool Unrar::open(const QString &fileName)
114 {
115 if (!isSuitableVersionAvailable())
116 return false;
117
118 delete mTempDir;
119 mTempDir = new QTemporaryDir();
120
121 mFileName = fileName;
122
123 /**
124 * Extract the archive to a temporary directory
125 */
126 mStdOutData.clear();
127 mStdErrData.clear();
128
129 const int ret = startSyncProcess(helper->kind->processOpenArchiveArgs(mFileName, mTempDir->path()));
130 bool ok = ret == 0;
131
132 return ok;
133 }
134
list()135 QStringList Unrar::list()
136 {
137 mStdOutData.clear();
138 mStdErrData.clear();
139
140 if (!isSuitableVersionAvailable())
141 return QStringList();
142
143 startSyncProcess(helper->kind->processListArgs(mFileName));
144
145 const QRegularExpression regex(QStringLiteral("[\r\n]"));
146 QStringList listFiles = helper->kind->processListing(QString::fromLocal8Bit(mStdOutData).split(regex, QString::SkipEmptyParts));
147
148 QString subDir;
149
150 if (listFiles.last().endsWith(QLatin1Char('/')) && helper->kind->name() == QLatin1String("unar")) {
151 // Subfolder detected. The unarchiver is unable to extract all files into a single folder
152 subDir = listFiles.last();
153 listFiles.removeLast();
154 }
155
156 QStringList newList;
157 for (const QString &f : qAsConst(listFiles)) {
158 // Extract all the files to mTempDir regardless of their path inside the archive
159 // This will break if ever an arvhice with two files with the same name in different subfolders
160 QFileInfo fi(f);
161 if (QFile::exists(mTempDir->path() + QLatin1Char('/') + subDir + fi.fileName())) {
162 newList.append(subDir + fi.fileName());
163 }
164 }
165 return newList;
166 }
167
contentOf(const QString & fileName) const168 QByteArray Unrar::contentOf(const QString &fileName) const
169 {
170 if (!isSuitableVersionAvailable())
171 return QByteArray();
172
173 QFile file(mTempDir->path() + QLatin1Char('/') + fileName);
174 if (!file.open(QIODevice::ReadOnly))
175 return QByteArray();
176
177 return file.readAll();
178 }
179
createDevice(const QString & fileName) const180 QIODevice *Unrar::createDevice(const QString &fileName) const
181 {
182 if (!isSuitableVersionAvailable())
183 return nullptr;
184
185 std::unique_ptr<QFile> file(new QFile(mTempDir->path() + QLatin1Char('/') + fileName));
186 if (!file->open(QIODevice::ReadOnly))
187 return nullptr;
188
189 return file.release();
190 }
191
isAvailable()192 bool Unrar::isAvailable()
193 {
194 return helper->kind;
195 }
196
isSuitableVersionAvailable()197 bool Unrar::isSuitableVersionAvailable()
198 {
199 if (!isAvailable())
200 return false;
201
202 if (dynamic_cast<NonFreeUnrarFlavour *>(helper->kind) || dynamic_cast<UnarFlavour *>(helper->kind))
203 return true;
204 else
205 return false;
206 }
207
readFromStdout()208 void Unrar::readFromStdout()
209 {
210 if (!mProcess)
211 return;
212
213 mStdOutData += mProcess->readAllStandardOutput();
214 }
215
readFromStderr()216 void Unrar::readFromStderr()
217 {
218 if (!mProcess)
219 return;
220
221 mStdErrData += mProcess->readAllStandardError();
222 if (!mStdErrData.isEmpty()) {
223 mProcess->kill();
224 return;
225 }
226 }
227
finished(int exitCode,QProcess::ExitStatus exitStatus)228 void Unrar::finished(int exitCode, QProcess::ExitStatus exitStatus)
229 {
230 Q_UNUSED(exitCode)
231 if (mLoop) {
232 mLoop->exit(exitStatus == QProcess::CrashExit ? 1 : 0);
233 }
234 }
235
startSyncProcess(const ProcessArgs & args)236 int Unrar::startSyncProcess(const ProcessArgs &args)
237 {
238 int ret = 0;
239
240 #if !defined(WITH_KPTY)
241 mProcess = new QProcess(this);
242 connect(mProcess, &QProcess::readyReadStandardOutput, this, &Unrar::readFromStdout);
243 connect(mProcess, &QProcess::readyReadStandardError, this, &Unrar::readFromStderr);
244 connect(mProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &Unrar::finished);
245
246 #else
247 mProcess = new KPtyProcess(this);
248 mProcess->setOutputChannelMode(KProcess::SeparateChannels);
249 connect(mProcess, &KPtyProcess::readyReadStandardOutput, this, &Unrar::readFromStdout);
250 connect(mProcess, &KPtyProcess::readyReadStandardError, this, &Unrar::readFromStderr);
251 connect(mProcess, static_cast<void (KPtyProcess::*)(int, QProcess::ExitStatus)>(&KPtyProcess::finished), this, &Unrar::finished);
252
253 #endif
254
255 #if !defined(WITH_KPTY)
256 if (helper->kind->name() == QLatin1String("unar") && args.useLsar) {
257 mProcess->start(helper->lsarPath, args.appArgs, QIODevice::ReadWrite | QIODevice::Unbuffered);
258 } else {
259 mProcess->start(helper->unrarPath, args.appArgs, QIODevice::ReadWrite | QIODevice::Unbuffered);
260 }
261
262 ret = mProcess->waitForFinished(-1) ? 0 : 1;
263 #else
264 if (helper->kind->name() == QLatin1String("unar") && args.useLsar) {
265 mProcess->setProgram(helper->lsarPath, args.appArgs);
266 } else {
267 mProcess->setProgram(helper->unrarPath, args.appArgs);
268 }
269
270 mProcess->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered);
271 mProcess->start();
272 QEventLoop loop;
273 mLoop = &loop;
274 ret = loop.exec(QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents);
275 mLoop = nullptr;
276 #endif
277
278 delete mProcess;
279 mProcess = nullptr;
280
281 return ret;
282 }
283
writeToProcess(const QByteArray & data)284 void Unrar::writeToProcess(const QByteArray &data)
285 {
286 if (!mProcess || data.isNull())
287 return;
288
289 #if !defined(WITH_KPTY)
290 mProcess->write(data);
291 #else
292 mProcess->pty()->write(data);
293 #endif
294 }
295