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