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 ®istryKey, 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