1 /****************************************************************************
2 **
3 ** Copyright (C) 2020 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "archive.h"
27 
28 #include "algorithm.h"
29 #include "checkablemessagebox.h"
30 #include "environment.h"
31 #include "mimetypes/mimedatabase.h"
32 #include "qtcassert.h"
33 #include "qtcprocess.h"
34 
35 #include <QDir>
36 #include <QPushButton>
37 #include <QSettings>
38 #include <QTimer>
39 
40 namespace {
41 
42 struct Tool
43 {
44     QString executable;
45     QStringList arguments;
46     QStringList supportedMimeTypes;
47     QStringList additionalSearchDirs;
48     bool nativeWindowsArguments = false;
49 };
50 
additionalInstallDirs(const QString & registryKey,const QString & valueName)51 static QStringList additionalInstallDirs(const QString &registryKey, const QString &valueName)
52 {
53 #if defined(Q_OS_WIN)
54     const QSettings settings64(registryKey, QSettings::Registry64Format);
55     const QSettings settings32(registryKey, QSettings::Registry32Format);
56     return {settings64.value(valueName).toString(), settings32.value(valueName).toString()};
57 #else
58     Q_UNUSED(registryKey)
59     Q_UNUSED(valueName)
60     return {};
61 #endif
62 }
63 
sTools()64 static const QVector<Tool> &sTools()
65 {
66     static QVector<Tool> tools;
67     if (tools.isEmpty()) {
68         if (Utils::HostOsInfo::isWindowsHost()) {
69             tools << Tool{{"powershell"},
70                           {"-command Expand-Archive -Force '%{src}' '%{dest}'"},
71                           {"application/zip"},
72                           {},
73                           true};
74         }
75         tools << Tool{{"unzip"}, {"-o", "%{src}", "-d", "%{dest}"}, {"application/zip"}, {}};
76         tools << Tool{{"7z"},
77                       {"x", "-o%{dest}", "-y", "-bb", "%{src}"},
78                       {"application/zip", "application/x-7z-compressed"},
79                       additionalInstallDirs("HKEY_CURRENT_USER\\Software\\7-Zip", "Path")};
80         tools << Tool{{"tar"},
81                       {"xvf", "%{src}"},
82                       {"application/zip", "application/x-tar", "application/x-7z-compressed"},
83                       {}};
84         tools << Tool{{"tar"}, {"xvzf", "%{src}"}, {"application/x-compressed-tar"}, {}};
85         tools << Tool{{"tar"}, {"xvJf", "%{src}"}, {"application/x-xz-compressed-tar"}, {}};
86         tools << Tool{{"tar"}, {"xvjf", "%{src}"}, {"application/x-bzip-compressed-tar"}, {}};
87         const QStringList additionalCMakeDirs = additionalInstallDirs(
88             "HKEY_LOCAL_MACHINE\\SOFTWARE\\Kitware\\CMake", "InstallDir");
89         tools << Tool{{"cmake"},
90                       {"-E", "tar", "xvf", "%{src}"},
91                       {"application/zip", "application/x-tar", "application/x-7z-compressed"},
92                       additionalCMakeDirs};
93         tools << Tool{{"cmake"},
94                       {"-E", "tar", "xvzf", "%{src}"},
95                       {"application/x-compressed-tar"},
96                       additionalCMakeDirs};
97         tools << Tool{{"cmake"},
98                       {"-E", "tar", "xvJf", "%{src}"},
99                       {"application/x-xz-compressed-tar"},
100                       additionalCMakeDirs};
101         tools << Tool{{"cmake"},
102                       {"-E", "tar", "xvjf", "%{src}"},
103                       {"application/x-bzip-compressed-tar"},
104                       additionalCMakeDirs};
105     }
106     return tools;
107 }
108 
toolsForMimeType(const Utils::MimeType & mimeType)109 static QVector<Tool> toolsForMimeType(const Utils::MimeType &mimeType)
110 {
111     return Utils::filtered(sTools(), [mimeType](const Tool &tool) {
112         return Utils::anyOf(tool.supportedMimeTypes,
113                             [mimeType](const QString &mt) { return mimeType.inherits(mt); });
114     });
115 }
116 
toolsForFilePath(const Utils::FilePath & fp)117 static QVector<Tool> toolsForFilePath(const Utils::FilePath &fp)
118 {
119     return toolsForMimeType(Utils::mimeTypeForFile(fp));
120 }
121 
resolveTool(const Tool & tool)122 static Utils::optional<Tool> resolveTool(const Tool &tool)
123 {
124     const QString executable
125         = Utils::Environment::systemEnvironment()
126               .searchInPath(Utils::HostOsInfo::withExecutableSuffix(tool.executable),
127                             Utils::transform(tool.additionalSearchDirs, &Utils::FilePath::fromString))
128               .toString();
129     Tool resolvedTool = tool;
130     resolvedTool.executable = executable;
131     return executable.isEmpty() ? Utils::nullopt : Utils::make_optional(resolvedTool);
132 }
133 
unzipTool(const Utils::FilePath & src,const Utils::FilePath & dest)134 Utils::optional<Tool> unzipTool(const Utils::FilePath &src, const Utils::FilePath &dest)
135 {
136     const QVector<Tool> tools = toolsForFilePath(src);
137     for (const Tool &tool : tools) {
138         const Utils::optional<Tool> resolvedTool = resolveTool(tool);
139         if (resolvedTool) {
140             Tool result = *resolvedTool;
141             const QString srcStr = src.toString();
142             const QString destStr = dest.toString();
143             result.arguments
144                 = Utils::transform(result.arguments, [srcStr, destStr](const QString &a) {
145                       QString val = a;
146                       return val.replace("%{src}", srcStr).replace("%{dest}", destStr);
147                   });
148             return result;
149         }
150     }
151     return {};
152 }
153 
154 } // namespace
155 
156 namespace Utils {
157 
supportsFile(const FilePath & filePath,QString * reason)158 bool Archive::supportsFile(const FilePath &filePath, QString *reason)
159 {
160     const QVector<Tool> tools = toolsForFilePath(filePath);
161     if (tools.isEmpty()) {
162         if (reason)
163             *reason = tr("File format not supported.");
164         return false;
165     }
166     if (!anyOf(tools, [](const Tool &t) { return resolveTool(t); })) {
167         if (reason)
168             *reason = tr("Could not find any unarchiving executable in PATH (%1).")
169                           .arg(transform<QStringList>(tools, &Tool::executable).join(", "));
170         return false;
171     }
172     return true;
173 }
174 
unarchive(const FilePath & src,const FilePath & dest,QWidget * parent)175 bool Archive::unarchive(const FilePath &src, const FilePath &dest, QWidget *parent)
176 {
177     Archive *archive = unarchive(src, dest);
178     QTC_ASSERT(archive, return false);
179 
180     CheckableMessageBox box(parent);
181     box.setIcon(QMessageBox::Information);
182     box.setWindowTitle(tr("Unarchiving File"));
183     box.setText(tr("Unzipping \"%1\" to \"%2\".").arg(src.toUserOutput(), dest.toUserOutput()));
184     box.setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
185     box.button(QDialogButtonBox::Ok)->setEnabled(false);
186     box.setCheckBoxVisible(false);
187     QObject::connect(archive, &Archive::outputReceived, &box, [&box](const QString &output) {
188         box.setDetailedText(box.detailedText() + output);
189     });
190     bool success = false;
191     QObject::connect(archive, &Archive::finished, [&box, &success](bool ret) {
192         box.button(QDialogButtonBox::Ok)->setEnabled(true);
193         box.button(QDialogButtonBox::Cancel)->setEnabled(false);
194         success = ret;
195     });
196     QObject::connect(&box, &QMessageBox::rejected, archive, &Archive::cancel);
197     box.exec();
198     return success;
199 }
200 
unarchive(const FilePath & src,const FilePath & dest)201 Archive *Archive::unarchive(const FilePath &src, const FilePath &dest)
202 {
203     const Utils::optional<Tool> tool = unzipTool(src, dest);
204     QTC_ASSERT(tool, return nullptr);
205 
206     auto archive = new Archive;
207 
208     const QString workingDirectory = dest.toFileInfo().absoluteFilePath();
209     QDir(workingDirectory).mkpath(".");
210 
211     archive->m_process = new QtcProcess;
212     archive->m_process->setProcessChannelMode(QProcess::MergedChannels);
213     QObject::connect(
214         archive->m_process,
215         &QtcProcess::readyReadStandardOutput,
216         archive,
217         [archive]() {
218             if (!archive->m_process)
219                 return;
220             archive->outputReceived(QString::fromUtf8(archive->m_process->readAllStandardOutput()));
221         },
222         Qt::QueuedConnection);
223     QObject::connect(
224         archive->m_process,
225         &QtcProcess::finished,
226         archive,
227         [archive] {
228             if (!archive->m_process)
229                 return;
230             archive->finished(archive->m_process->result() == QtcProcess::FinishedWithSuccess);
231             archive->m_process->deleteLater();
232             archive->m_process = nullptr;
233             archive->deleteLater();
234         },
235         Qt::QueuedConnection);
236     QObject::connect(
237         archive->m_process,
238         &QtcProcess::errorOccurred,
239         archive,
240         [archive](QProcess::ProcessError) {
241             if (!archive->m_process)
242                 return;
243             archive->outputReceived(tr("Command failed."));
244             archive->finished(false);
245             archive->m_process->deleteLater();
246             archive->m_process = nullptr;
247             archive->deleteLater();
248         },
249         Qt::QueuedConnection);
250 
251     QTimer::singleShot(0, archive, [archive, tool, workingDirectory] {
252         archive->outputReceived(
253             tr("Running %1\nin \"%2\".\n\n", "Running <cmd> in <workingdirectory>")
254                 .arg(CommandLine(tool->executable, tool->arguments).toUserOutput(),
255                      workingDirectory));
256     });
257 
258     CommandLine cmd = tool->nativeWindowsArguments
259         ? CommandLine{FilePath::fromString(tool->executable), tool->arguments[0], CommandLine::Raw}
260         : CommandLine{tool->executable, tool->arguments};
261     archive->m_process->setCommand(cmd);
262     archive->m_process->setWorkingDirectory(workingDirectory);
263     archive->m_process->setOpenMode(QProcess::ReadOnly);
264     archive->m_process->start();
265     return archive;
266 }
267 
cancel()268 void Archive::cancel()
269 {
270     if (m_process)
271         m_process->stopProcess();
272 }
273 
274 } // namespace Utils
275