MythTV master
unzip2.cpp
Go to the documentation of this file.
1/*
2 * Class UnZip
3 *
4 * Copyright (c) David Hampton 2021
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21#include "unzip2.h"
22
23// libmythbase headers
24#include "mythdate.h"
25#include "mythlogging.h"
26
27static constexpr uint32_t ZIP_ATTR_FILE_TYPE_MASK { 0xFE000000 };
28static constexpr uint32_t ZIP_ATTR_FILE_TYPE_SYMLINK { 0xA0000000 };
29//static constexpr uint32_t ZIP_ATTR_FILE_TYPE_NORMAL { 0x80000000 };
30
31static constexpr uint32_t ZIP_ATTR_USER_PERM_MASK { 0x01C00000 };
32static constexpr uint32_t ZIP_ATTR_GROUP_PERM_MASK { 0x03800000 };
33static constexpr uint32_t ZIP_ATTR_OTHER_PERM_MASK { 0x00700000 };
34static constexpr uint8_t ZIP_ATTR_USER_PERM_SHIFT { 22 };
35static constexpr uint8_t ZIP_ATTR_GROUP_PERM_SHIFT { 19 };
36static constexpr uint8_t ZIP_ATTR_OTHER_PERM_SHIFT { 16 };
37
38UnZip::UnZip(QString zipFileName)
39 : m_zipFileName(std::move(zipFileName))
40{
41 int err { ZIP_ER_OK };
42 m_zip = zip_open(qPrintable(m_zipFileName), 0, &err);
43 if (m_zip != nullptr)
44 return;
45
46 LOG(VB_GENERAL, LOG_ERR,
47 QString("UnZip: Unable to open zip file %1, error %2")
48 .arg(m_zipFileName).arg(err));
49}
50
52{
53 int err = zip_close(m_zip);
54 if (err == 0)
55 return;
56
57 LOG(VB_GENERAL, LOG_DEBUG,
58 QString("UnZip: Error closing zip file %1, error %2")
59 .arg(m_zipFileName).arg(err));
60}
61
63{
64 zip_stat_init(&entry.m_stats);
65 if (-1 == zip_stat_index(m_zip, entry.m_index, 0, &entry.m_stats))
66 {
67 LOG(VB_GENERAL, LOG_ERR,
68 QString("UnZip: Can't get info for index %1 in %2")
69 .arg(entry.m_index).arg(m_zipFileName));
70 return false;
71 }
72 if ((entry.m_stats.valid & kSTATS_REQUIRED) != kSTATS_REQUIRED)
73 {
74 LOG(VB_GENERAL, LOG_ERR,
75 QString("UnZip: Invalid status for index %1 in %2")
76 .arg(entry.m_index).arg(m_zipFileName));
77 return false;
78 }
79 return true;
80}
81
83{
84 zip_uint8_t opsys {ZIP_OPSYS_UNIX};// NOLINT(readability-uppercase-literal-suffix)
85
86 entry.m_attributes = 0;
87 zip_file_get_external_attributes(m_zip, entry.m_index, 0, &opsys,
88 &entry.m_attributes);
89}
90
92{
93 QDir dir = entry.m_fi.absoluteDir();
94 if (dir.exists())
95 return true;
96 if (dir.mkpath(dir.absolutePath()))
97 return true;
98 LOG(VB_GENERAL, LOG_ERR,
99 QString("UnZip: Failed to create directory %1")
100 .arg(dir.absolutePath()));
101 return false;
102}
103
104// Validate that the filename is beneath the extraction directrory.
105// This prevents a zip from overwriting arbitrary files by using names
106// with a sequence of ".." directories.
107bool UnZip::zipValidateFilename(const QFileInfo& fi)
108{
109 if (fi.absoluteFilePath().startsWith(m_outDir.absolutePath()))
110 return true;
111 LOG(VB_GENERAL, LOG_ERR,
112 QString("UnZip: Attempt to write outside destination directory. File: %1")
113 .arg(QString(fi.fileName())));
114 return false;
115}
116
117// Would be nice if Qt provided a unix perm to Qt perm conversion.
118QFileDevice::Permissions UnZip::zipToQtPerms(const zipEntry& entry)
119{
120 QFileDevice::Permissions qt_perms;
121 zip_uint32_t attrs = entry.m_attributes;
122
124 int32_t group = (attrs & ZIP_ATTR_GROUP_PERM_MASK) >> ZIP_ATTR_GROUP_PERM_SHIFT;
125 int32_t other = (attrs & ZIP_ATTR_OTHER_PERM_MASK) >> ZIP_ATTR_OTHER_PERM_SHIFT;
126
127 if (user & 4)
128 qt_perms |= (QFileDevice::ReadOwner | QFileDevice::ReadUser);
129 if (user & 2)
130 qt_perms |= (QFileDevice::WriteOwner | QFileDevice::WriteUser);
131 if (user & 1)
132 qt_perms |= (QFileDevice::ExeOwner | QFileDevice::ExeUser);
133 if (group & 4)
134 qt_perms |= QFileDevice::ReadGroup;
135 if (group & 2)
136 qt_perms |= QFileDevice::WriteGroup;
137 if (group & 1)
138 qt_perms |= QFileDevice::ExeGroup;
139 if (other & 4)
140 qt_perms |= QFileDevice::ReadOther;
141 if (other & 2)
142 qt_perms |= QFileDevice::WriteOther;
143 if (other & 1)
144 qt_perms |= QFileDevice::ExeOther;
145 return qt_perms;
146}
147
148void UnZip::zipSetFileAttributes(const zipEntry& entry, QFile& outfile)
149{
150 // Set times
151 auto dateTime = MythDate::fromSecsSinceEpoch(entry.m_stats.mtime);
152
153 outfile.setFileTime(dateTime, QFileDevice::FileAccessTime);
154 outfile.setFileTime(dateTime, QFileDevice::FileBirthTime);
155 outfile.setFileTime(dateTime, QFileDevice::FileMetadataChangeTime);
156 outfile.setFileTime(dateTime, QFileDevice::FileModificationTime);
157
158 if (entry.m_attributes == 0)
159 return;
160 outfile.setPermissions(zipToQtPerms(entry));
161}
162
164{
165 zip_file_t *infile = zip_fopen_index(m_zip, entry.m_index, 0);
166 if (infile == nullptr)
167 {
168 LOG(VB_GENERAL, LOG_ERR,
169 QString("UnZip: Can't open index %1 in %2")
170 .arg(entry.m_index).arg(m_zipFileName));
171 return false;
172 }
173
174 int64_t readLen {0};
175 static constexpr int BLOCK_SIZE { 4096 };
176 QByteArray data; data.resize(BLOCK_SIZE);
177 readLen = zip_fread(infile, data.data(), BLOCK_SIZE);
178 if (readLen < 1)
179 {
180 LOG(VB_GENERAL, LOG_ERR,
181 QString("UnZip: Invalid symlink name for index %1 in %2")
182 .arg(entry.m_index).arg(m_zipFileName));
183 return false;
184 }
185 data.resize(readLen);
186
187 auto target = QFileInfo(entry.m_fi.absolutePath() + "/" + data);
188 if (!zipValidateFilename(target))
189 return false;
190 if (!QFile::link(target.absoluteFilePath(), entry.m_fi.absoluteFilePath()))
191 {
192 LOG(VB_GENERAL, LOG_ERR,
193 QString("UnZip: Failed to create symlink from %1 to %2")
194 .arg(entry.m_fi.absoluteFilePath(),
195 target.absoluteFilePath()));
196 return false;
197 }
198 return true;
199}
200
202{
203 zip_file_t *infile = zip_fopen_index(m_zip, entry.m_index, 0);
204 if (infile == nullptr)
205 {
206 LOG(VB_GENERAL, LOG_ERR,
207 QString("UnZip: Can't open file at index %1 in %2")
208 .arg(entry.m_index).arg(m_zipFileName));
209 return false;
210 }
211
212 auto outfile = QFile(entry.m_fi.absoluteFilePath());
213 if (!outfile.open(QIODevice::Truncate|QIODevice::WriteOnly))
214 {
215 LOG(VB_GENERAL, LOG_ERR,
216 QString("UnZip: Failed to open output file %1")
217 .arg(entry.m_fi.absoluteFilePath()));
218 return false;
219 }
220
221 int64_t readLen {0};
222 uint64_t bytesRead {0};
223 uint64_t bytesWritten {0};
224 static constexpr int BLOCK_SIZE { 4096 };
225 QByteArray data; data.resize(BLOCK_SIZE);
226 while ((readLen = zip_fread(infile, data.data(), BLOCK_SIZE)) > 0)
227 {
228 bytesRead += readLen;
229 int64_t writeLen = outfile.write(data.data(), readLen);
230 if (writeLen < 0)
231 {
232 LOG(VB_GENERAL, LOG_ERR,
233 QString("UnZip: Failed to write %1/%2 bytes to output file %3")
234 .arg(writeLen).arg(readLen).arg(entry.m_fi.absoluteFilePath()));
235 return false;
236 }
237 bytesWritten += writeLen;
238 }
239
240 if ((entry.m_stats.size != bytesRead) ||
241 (entry.m_stats.size != bytesWritten))
242 {
243 LOG(VB_GENERAL, LOG_ERR,
244 QString("UnZip: Failed to copy file %1. Read %2 and wrote %3 of %4.")
245 .arg(entry.m_fi.fileName()).arg(bytesRead).arg(bytesWritten)
246 .arg(entry.m_stats.size));
247 return false;
248 }
249
250 outfile.flush();
251 zipSetFileAttributes(entry, outfile);
252 outfile.close();
253 if (zip_fclose(infile) == -1)
254 {
255 LOG(VB_GENERAL, LOG_ERR,
256 QString("UnZip: Failed to close file at index %1 in %2")
257 .arg(entry.m_index).arg(entry.m_fi.fileName()));
258 return false;
259 }
260
261 return true;
262}
263
264bool UnZip::extractFile(const QString& outDirName)
265{
266 if (!isValid())
267 return false;
268
269 m_outDir = QDir(outDirName);
270 if (!m_outDir.exists())
271 {
272 LOG(VB_GENERAL, LOG_ERR,
273 QString("UnZip: Target directory %1 doesn't exist")
274 .arg(outDirName));
275 return false;
276 }
277
278 m_zipFileCount = zip_get_num_entries(m_zip, 0);
279 if (m_zipFileCount < 1)
280 {
281 LOG(VB_GENERAL, LOG_ERR,
282 QString("UnZip: Zip archive %1 is empty")
283 .arg(m_zipFileName));
284 return false;
285 }
286
287 bool ok { true };
288 for (auto index = 0; ok && (index < m_zipFileCount); index++)
289 {
290 zipEntry entry;
291 entry.m_index = index;
292 if (!getEntryStats(entry))
293 return false;
294 if (entry.m_stats.encryption_method > 0)
295 {
296 LOG(VB_GENERAL, LOG_WARNING,
297 QString("UnZip: Skipping encryped file %1 in %2")
298 .arg(entry.m_index).arg(m_zipFileName));
299 continue;
300 }
301 getEntryAttrs(entry);
302
303 entry.m_fi = QFileInfo(outDirName + '/' + entry.m_stats.name);
304 ok = zipValidateFilename(entry.m_fi);
305 if (ok)
306 ok = zipCreateDirectory(entry);
307 if (ok && (entry.m_stats.size > 0))
308 {
310 {
312 ok = zipCreateSymlink(entry);
313 break;
314 default:
315 ok = zipWriteOneFile(entry);
316 break;
317 }
318 }
319 }
320
321 return ok;
322}
zip_t * m_zip
Definition: unzip2.h:62
static QFileDevice::Permissions zipToQtPerms(const zipEntry &entry)
Definition: unzip2.cpp:118
UnZip(QString zipFile)
Definition: unzip2.cpp:38
bool zipCreateSymlink(const zipEntry &entry)
Definition: unzip2.cpp:163
static bool zipCreateDirectory(const zipEntry &entry)
Definition: unzip2.cpp:91
zip_int64_t m_zipFileCount
Definition: unzip2.h:63
bool zipWriteOneFile(const zipEntry &entry)
Definition: unzip2.cpp:201
QDir m_outDir
Definition: unzip2.h:58
bool getEntryStats(zipEntry &entry)
Definition: unzip2.cpp:62
bool extractFile(const QString &outDir)
Definition: unzip2.cpp:264
QString m_zipFileName
Definition: unzip2.h:61
static void zipSetFileAttributes(const zipEntry &entry, QFile &outfile)
Definition: unzip2.cpp:148
~UnZip()
Definition: unzip2.cpp:51
bool isValid()
Definition: unzip2.h:48
bool zipValidateFilename(const QFileInfo &fi)
Definition: unzip2.cpp:107
static constexpr uint64_t kSTATS_REQUIRED
Definition: unzip2.h:32
void getEntryAttrs(zipEntry &entry)
Definition: unzip2.cpp:82
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MBASE_PUBLIC QDateTime fromSecsSinceEpoch(int64_t seconds)
This function takes the number of seconds since the start of the epoch and returns a QDateTime with t...
Definition: mythdate.cpp:81
STL namespace.
zip_stat_t m_stats
Definition: unzip2.h:37
zip_uint32_t m_attributes
Definition: unzip2.h:38
QFileInfo m_fi
Definition: unzip2.h:39
static constexpr uint8_t ZIP_ATTR_OTHER_PERM_SHIFT
Definition: unzip2.cpp:36
static constexpr uint32_t ZIP_ATTR_USER_PERM_MASK
Definition: unzip2.cpp:31
static constexpr uint32_t ZIP_ATTR_FILE_TYPE_MASK
Definition: unzip2.cpp:27
static constexpr uint32_t ZIP_ATTR_GROUP_PERM_MASK
Definition: unzip2.cpp:32
static constexpr uint32_t ZIP_ATTR_FILE_TYPE_SYMLINK
Definition: unzip2.cpp:28
static constexpr uint8_t ZIP_ATTR_USER_PERM_SHIFT
Definition: unzip2.cpp:34
static constexpr uint8_t ZIP_ATTR_GROUP_PERM_SHIFT
Definition: unzip2.cpp:35
static constexpr uint32_t ZIP_ATTR_OTHER_PERM_MASK
Definition: unzip2.cpp:33