blob: e93661441a8f2f910e0f7a2c509320b14b47de54 [file] [log] [blame]
Elliott Hughes55fd2932017-05-28 22:59:04 -07001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include <errno.h>
18#include <error.h>
19#include <fcntl.h>
Elliott Hughes5f8b3092019-04-08 12:39:20 -070020#include <fnmatch.h>
Elliott Hughes55fd2932017-05-28 22:59:04 -070021#include <getopt.h>
22#include <inttypes.h>
23#include <stdio.h>
24#include <stdlib.h>
25#include <sys/stat.h>
26#include <sys/types.h>
27#include <time.h>
28#include <unistd.h>
29
30#include <set>
31#include <string>
32
33#include <android-base/file.h>
34#include <android-base/strings.h>
35#include <ziparchive/zip_archive.h>
36
37enum OverwriteMode {
38 kAlways,
39 kNever,
40 kPrompt,
41};
42
Elliott Hughes26724132019-10-25 09:57:58 -070043static bool is_unzip;
Elliott Hughes55fd2932017-05-28 22:59:04 -070044static OverwriteMode overwrite_mode = kPrompt;
Elliott Hughes26724132019-10-25 09:57:58 -070045static bool flag_1 = false;
Elliott Hughes55fd2932017-05-28 22:59:04 -070046static const char* flag_d = nullptr;
47static bool flag_l = false;
48static bool flag_p = false;
49static bool flag_q = false;
50static bool flag_v = false;
Elliott Hughes26724132019-10-25 09:57:58 -070051static bool flag_x = false;
Elliott Hughes55fd2932017-05-28 22:59:04 -070052static const char* archive_name = nullptr;
53static std::set<std::string> includes;
54static std::set<std::string> excludes;
55static uint64_t total_uncompressed_length = 0;
56static uint64_t total_compressed_length = 0;
57static size_t file_count = 0;
58
Elliott Hughes5f8b3092019-04-08 12:39:20 -070059static bool ShouldInclude(const std::string& name) {
60 // Explicitly excluded?
61 if (!excludes.empty()) {
62 for (const auto& exclude : excludes) {
63 if (!fnmatch(exclude.c_str(), name.c_str(), 0)) return false;
64 }
65 }
66
67 // Implicitly included?
68 if (includes.empty()) return true;
69
70 // Explicitly included?
71 for (const auto& include : includes) {
72 if (!fnmatch(include.c_str(), name.c_str(), 0)) return true;
73 }
Elliott Hughes55fd2932017-05-28 22:59:04 -070074 return false;
75}
76
77static bool MakeDirectoryHierarchy(const std::string& path) {
78 // stat rather than lstat because a symbolic link to a directory is fine too.
79 struct stat sb;
80 if (stat(path.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) return true;
81
82 // Ensure the parent directories exist first.
83 if (!MakeDirectoryHierarchy(android::base::Dirname(path))) return false;
84
85 // Then try to create this directory.
86 return (mkdir(path.c_str(), 0777) != -1);
87}
88
89static int CompressionRatio(int64_t uncompressed, int64_t compressed) {
90 if (uncompressed == 0) return 0;
Andreas Gampe964b95c2019-04-05 13:48:02 -070091 return static_cast<int>((100LL * (uncompressed - compressed)) / uncompressed);
Elliott Hughes55fd2932017-05-28 22:59:04 -070092}
93
Elliott Hughes26724132019-10-25 09:57:58 -070094static void MaybeShowHeader(ZipArchiveHandle zah) {
95 if (is_unzip) {
96 // unzip has three formats.
97 if (!flag_q) printf("Archive: %s\n", archive_name);
98 if (flag_v) {
99 printf(
100 " Length Method Size Cmpr Date Time CRC-32 Name\n"
101 "-------- ------ ------- ---- ---------- ----- -------- ----\n");
102 } else if (flag_l) {
103 printf(
104 " Length Date Time Name\n"
105 "--------- ---------- ----- ----\n");
106 }
107 } else {
108 // zipinfo.
109 if (!flag_1 && includes.empty() && excludes.empty()) {
110 ZipArchiveInfo info{GetArchiveInfo(zah)};
111 printf("Archive: %s\n", archive_name);
112 printf("Zip file size: %" PRId64 " bytes, number of entries: %zu\n", info.archive_size,
113 info.entry_count);
114 }
Elliott Hughes55fd2932017-05-28 22:59:04 -0700115 }
116}
117
118static void MaybeShowFooter() {
Elliott Hughes26724132019-10-25 09:57:58 -0700119 if (is_unzip) {
120 if (flag_v) {
121 printf(
122 "-------- ------- --- -------\n"
123 "%8" PRId64 " %8" PRId64 " %3d%% %zu file%s\n",
124 total_uncompressed_length, total_compressed_length,
125 CompressionRatio(total_uncompressed_length, total_compressed_length), file_count,
126 (file_count == 1) ? "" : "s");
127 } else if (flag_l) {
128 printf(
129 "--------- -------\n"
130 "%9" PRId64 " %zu file%s\n",
131 total_uncompressed_length, file_count, (file_count == 1) ? "" : "s");
132 }
133 } else {
134 if (!flag_1 && includes.empty() && excludes.empty()) {
135 printf("%zu files, %" PRId64 " bytes uncompressed, %" PRId64 " bytes compressed: %3d%%\n",
136 file_count, total_uncompressed_length, total_compressed_length,
137 CompressionRatio(total_uncompressed_length, total_compressed_length));
138 }
Elliott Hughes55fd2932017-05-28 22:59:04 -0700139 }
140}
141
142static bool PromptOverwrite(const std::string& dst) {
143 // TODO: [r]ename not implemented because it doesn't seem useful.
144 printf("replace %s? [y]es, [n]o, [A]ll, [N]one: ", dst.c_str());
145 fflush(stdout);
146 while (true) {
147 char* line = nullptr;
148 size_t n;
149 if (getline(&line, &n, stdin) == -1) {
150 error(1, 0, "(EOF/read error; assuming [N]one...)");
151 overwrite_mode = kNever;
152 return false;
153 }
154 if (n == 0) continue;
155 char cmd = line[0];
156 free(line);
157 switch (cmd) {
158 case 'y':
159 return true;
160 case 'n':
161 return false;
162 case 'A':
163 overwrite_mode = kAlways;
164 return true;
165 case 'N':
166 overwrite_mode = kNever;
167 return false;
168 }
169 }
170}
171
172static void ExtractToPipe(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) {
173 // We need to extract to memory because ExtractEntryToFile insists on
174 // being able to seek and truncate, and you can't do that with stdout.
175 uint8_t* buffer = new uint8_t[entry.uncompressed_length];
176 int err = ExtractToMemory(zah, &entry, buffer, entry.uncompressed_length);
177 if (err < 0) {
178 error(1, 0, "failed to extract %s: %s", name.c_str(), ErrorCodeString(err));
179 }
180 if (!android::base::WriteFully(1, buffer, entry.uncompressed_length)) {
181 error(1, errno, "failed to write %s to stdout", name.c_str());
182 }
183 delete[] buffer;
184}
185
186static void ExtractOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) {
187 // Bad filename?
188 if (android::base::StartsWith(name, "/") || android::base::StartsWith(name, "../") ||
189 name.find("/../") != std::string::npos) {
190 error(1, 0, "bad filename %s", name.c_str());
191 }
192
193 // Where are we actually extracting to (for human-readable output)?
194 std::string dst;
195 if (flag_d) {
196 dst = flag_d;
197 if (!android::base::EndsWith(dst, "/")) dst += '/';
198 }
199 dst += name;
200
201 // Ensure the directory hierarchy exists.
202 if (!MakeDirectoryHierarchy(android::base::Dirname(name))) {
203 error(1, errno, "couldn't create directory hierarchy for %s", dst.c_str());
204 }
205
206 // An entry in a zip file can just be a directory itself.
207 if (android::base::EndsWith(name, "/")) {
208 if (mkdir(name.c_str(), entry.unix_mode) == -1) {
209 // If the directory already exists, that's fine.
210 if (errno == EEXIST) {
211 struct stat sb;
212 if (stat(name.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) return;
213 }
214 error(1, errno, "couldn't extract directory %s", dst.c_str());
215 }
216 return;
217 }
218
219 // Create the file.
220 int fd = open(name.c_str(), O_CREAT | O_WRONLY | O_CLOEXEC | O_EXCL, entry.unix_mode);
221 if (fd == -1 && errno == EEXIST) {
222 if (overwrite_mode == kNever) return;
223 if (overwrite_mode == kPrompt && !PromptOverwrite(dst)) return;
224 // Either overwrite_mode is kAlways or the user consented to this specific case.
225 fd = open(name.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC | O_TRUNC, entry.unix_mode);
226 }
227 if (fd == -1) error(1, errno, "couldn't create file %s", dst.c_str());
228
229 // Actually extract into the file.
230 if (!flag_q) printf(" inflating: %s\n", dst.c_str());
231 int err = ExtractEntryToFile(zah, &entry, fd);
232 if (err < 0) error(1, 0, "failed to extract %s: %s", dst.c_str(), ErrorCodeString(err));
233 close(fd);
234}
235
236static void ListOne(const ZipEntry& entry, const std::string& name) {
237 tm t = entry.GetModificationTime();
238 char time[32];
239 snprintf(time, sizeof(time), "%04d-%02d-%02d %02d:%02d", t.tm_year + 1900, t.tm_mon + 1,
240 t.tm_mday, t.tm_hour, t.tm_min);
241 if (flag_v) {
242 printf("%8d %s %7d %3d%% %s %08x %s\n", entry.uncompressed_length,
243 (entry.method == kCompressStored) ? "Stored" : "Defl:N", entry.compressed_length,
244 CompressionRatio(entry.uncompressed_length, entry.compressed_length), time, entry.crc32,
245 name.c_str());
246 } else {
247 printf("%9d %s %s\n", entry.uncompressed_length, time, name.c_str());
248 }
249}
250
Elliott Hughes26724132019-10-25 09:57:58 -0700251static void InfoOne(const ZipEntry& entry, const std::string& name) {
252 if (flag_1) {
253 // "android-ndk-r19b/sources/android/NOTICE"
254 printf("%s\n", name.c_str());
255 return;
256 }
257
258 int version = entry.version_made_by & 0xff;
259 int os = (entry.version_made_by >> 8) & 0xff;
260
261 // TODO: Support suid/sgid? Non-Unix host file system attributes?
262 char mode[] = "??????????";
263 if (os == 3) {
264 mode[0] = S_ISDIR(entry.unix_mode) ? 'd' : (S_ISREG(entry.unix_mode) ? '-' : '?');
265 mode[1] = entry.unix_mode & S_IRUSR ? 'r' : '-';
266 mode[2] = entry.unix_mode & S_IWUSR ? 'w' : '-';
267 mode[3] = entry.unix_mode & S_IXUSR ? 'x' : '-';
268 mode[4] = entry.unix_mode & S_IRGRP ? 'r' : '-';
269 mode[5] = entry.unix_mode & S_IWGRP ? 'w' : '-';
270 mode[6] = entry.unix_mode & S_IXGRP ? 'x' : '-';
271 mode[7] = entry.unix_mode & S_IROTH ? 'r' : '-';
272 mode[8] = entry.unix_mode & S_IWOTH ? 'w' : '-';
273 mode[9] = entry.unix_mode & S_IXOTH ? 'x' : '-';
274 }
275
276 // TODO: zipinfo (unlike unzip) sometimes uses time zone?
277 // TODO: this uses 4-digit years because we're not barbarians unless interoperability forces it.
278 tm t = entry.GetModificationTime();
279 char time[32];
280 snprintf(time, sizeof(time), "%04d-%02d-%02d %02d:%02d", t.tm_year + 1900, t.tm_mon + 1,
281 t.tm_mday, t.tm_hour, t.tm_min);
282
283 // "-rw-r--r-- 3.0 unx 577 t- defX 19-Feb-12 16:09 android-ndk-r19b/sources/android/NOTICE"
284 printf("%s %2d.%d %s %8d %c%c %s %s %s\n", mode, version / 10, version % 10,
285 os == 3 ? "unx" : "???", entry.uncompressed_length, entry.is_text ? 't' : 'b',
286 entry.has_data_descriptor ? 'X' : 'x', entry.method == kCompressStored ? "stor" : "defX",
287 time, name.c_str());
288}
289
Elliott Hughes55fd2932017-05-28 22:59:04 -0700290static void ProcessOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) {
Elliott Hughes26724132019-10-25 09:57:58 -0700291 if (is_unzip) {
292 if (flag_l || flag_v) {
293 // -l or -lv or -lq or -v.
294 ListOne(entry, name);
Elliott Hughes55fd2932017-05-28 22:59:04 -0700295 } else {
Elliott Hughes26724132019-10-25 09:57:58 -0700296 // Actually extract.
297 if (flag_p) {
298 ExtractToPipe(zah, entry, name);
299 } else {
300 ExtractOne(zah, entry, name);
301 }
Elliott Hughes55fd2932017-05-28 22:59:04 -0700302 }
Elliott Hughes26724132019-10-25 09:57:58 -0700303 } else {
304 // zipinfo or zipinfo -1.
305 InfoOne(entry, name);
Elliott Hughes55fd2932017-05-28 22:59:04 -0700306 }
307 total_uncompressed_length += entry.uncompressed_length;
308 total_compressed_length += entry.compressed_length;
309 ++file_count;
310}
311
312static void ProcessAll(ZipArchiveHandle zah) {
Elliott Hughes26724132019-10-25 09:57:58 -0700313 MaybeShowHeader(zah);
Elliott Hughes55fd2932017-05-28 22:59:04 -0700314
315 // libziparchive iteration order doesn't match the central directory.
316 // We could sort, but that would cost extra and wouldn't match either.
317 void* cookie;
Elliott Hughesa22ac0f2019-05-08 10:44:06 -0700318 int err = StartIteration(zah, &cookie);
Elliott Hughes55fd2932017-05-28 22:59:04 -0700319 if (err != 0) {
320 error(1, 0, "couldn't iterate %s: %s", archive_name, ErrorCodeString(err));
321 }
322
323 ZipEntry entry;
Elliott Hughese06a8082019-05-22 18:56:41 -0700324 std::string name;
325 while ((err = Next(cookie, &entry, &name)) >= 0) {
Elliott Hughes5f8b3092019-04-08 12:39:20 -0700326 if (ShouldInclude(name)) ProcessOne(zah, entry, name);
Elliott Hughes55fd2932017-05-28 22:59:04 -0700327 }
328
329 if (err < -1) error(1, 0, "failed iterating %s: %s", archive_name, ErrorCodeString(err));
330 EndIteration(cookie);
331
332 MaybeShowFooter();
333}
334
335static void ShowHelp(bool full) {
Elliott Hughes26724132019-10-25 09:57:58 -0700336 if (is_unzip) {
337 fprintf(full ? stdout : stderr, "usage: unzip [-d DIR] [-lnopqv] ZIP [FILE...] [-x FILE...]\n");
338 if (!full) exit(EXIT_FAILURE);
Elliott Hughes55fd2932017-05-28 22:59:04 -0700339
Elliott Hughes26724132019-10-25 09:57:58 -0700340 printf(
341 "\n"
342 "Extract FILEs from ZIP archive. Default is all files. Both the include and\n"
343 "exclude (-x) lists use shell glob patterns.\n"
344 "\n"
345 "-d DIR Extract into DIR\n"
346 "-l List contents (-lq excludes archive name, -lv is verbose)\n"
347 "-n Never overwrite files (default: prompt)\n"
348 "-o Always overwrite files\n"
349 "-p Pipe to stdout\n"
350 "-q Quiet\n"
351 "-v List contents verbosely\n"
352 "-x FILE Exclude files\n");
353 } else {
354 fprintf(full ? stdout : stderr, "usage: zipinfo [-1] ZIP [FILE...] [-x FILE...]\n");
355 if (!full) exit(EXIT_FAILURE);
356
357 printf(
358 "\n"
359 "Show information about FILEs from ZIP archive. Default is all files.\n"
360 "Both the include and exclude (-x) lists use shell glob patterns.\n"
361 "\n"
362 "-1 Show filenames only, one per line\n"
363 "-x FILE Exclude files\n");
364 }
Elliott Hughes55fd2932017-05-28 22:59:04 -0700365 exit(EXIT_SUCCESS);
366}
367
Elliott Hughes26724132019-10-25 09:57:58 -0700368static void HandleCommonOption(int opt) {
369 switch (opt) {
370 case 'h':
371 ShowHelp(true);
372 break;
373 case 'x':
374 flag_x = true;
375 break;
376 case 1:
377 // -x swallows all following arguments, so we use '-' in the getopt
378 // string and collect files here.
379 if (!archive_name) {
380 archive_name = optarg;
381 } else if (flag_x) {
382 excludes.insert(optarg);
383 } else {
384 includes.insert(optarg);
385 }
386 break;
387 default:
388 ShowHelp(false);
389 break;
390 }
391}
392
Elliott Hughes55fd2932017-05-28 22:59:04 -0700393int main(int argc, char* argv[]) {
394 static struct option opts[] = {
395 {"help", no_argument, 0, 'h'},
396 };
Elliott Hughes26724132019-10-25 09:57:58 -0700397
398 is_unzip = !strcmp(basename(argv[0]), "unzip");
399 if (is_unzip) {
400 int opt;
401 while ((opt = getopt_long(argc, argv, "-d:hlnopqvx", opts, nullptr)) != -1) {
402 switch (opt) {
403 case 'd':
404 flag_d = optarg;
405 break;
406 case 'l':
407 flag_l = true;
408 break;
409 case 'n':
410 overwrite_mode = kNever;
411 break;
412 case 'o':
413 overwrite_mode = kAlways;
414 break;
415 case 'p':
416 flag_p = flag_q = true;
417 break;
418 case 'q':
419 flag_q = true;
420 break;
421 case 'v':
422 flag_v = true;
423 break;
424 default:
425 HandleCommonOption(opt);
426 break;
427 }
428 }
429 } else {
430 int opt;
431 while ((opt = getopt_long(argc, argv, "-1hx", opts, nullptr)) != -1) {
432 switch (opt) {
433 case '1':
434 flag_1 = true;
435 break;
436 default:
437 HandleCommonOption(opt);
438 break;
439 }
Elliott Hughes55fd2932017-05-28 22:59:04 -0700440 }
441 }
442
443 if (!archive_name) error(1, 0, "missing archive filename");
444
445 // We can't support "-" to unzip from stdin because libziparchive relies on mmap.
446 ZipArchiveHandle zah;
447 int32_t err;
448 if ((err = OpenArchive(archive_name, &zah)) != 0) {
449 error(1, 0, "couldn't open %s: %s", archive_name, ErrorCodeString(err));
450 }
451
452 // Implement -d by changing into that directory.
453 // We'll create implicit directories based on paths in the zip file, but we
454 // require that the -d directory already exists.
455 if (flag_d && chdir(flag_d) == -1) error(1, errno, "couldn't chdir to %s", flag_d);
456
457 ProcessAll(zah);
458
459 CloseArchive(zah);
460 return 0;
461}