1 /* 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 <fcntl.h> 19 #include <fnmatch.h> 20 #include <getopt.h> 21 #include <inttypes.h> 22 #include <libgen.h> 23 #include <stdarg.h> 24 #include <stdio.h> 25 #include <stdlib.h> 26 #include <sys/stat.h> 27 #include <sys/types.h> 28 #include <time.h> 29 #include <unistd.h> 30 31 #include <set> 32 #include <string> 33 34 #include <android-base/file.h> 35 #include <android-base/strings.h> 36 #include <ziparchive/zip_archive.h> 37 #include <zlib.h> 38 39 using android::base::EndsWith; 40 using android::base::StartsWith; 41 42 enum OverwriteMode { 43 kAlways, 44 kNever, 45 kPrompt, 46 }; 47 48 enum Role { 49 kUnzip, 50 kZipinfo, 51 }; 52 53 static Role role; 54 static OverwriteMode overwrite_mode = kPrompt; 55 static bool flag_1 = false; 56 static std::string flag_d; 57 static bool flag_j = false; 58 static bool flag_l = false; 59 static bool flag_p = false; 60 static bool flag_q = false; 61 static bool flag_t = false; 62 static bool flag_v = false; 63 static bool flag_x = false; 64 static const char* archive_name = nullptr; 65 static std::set<std::string> includes; 66 static std::set<std::string> excludes; 67 static uint64_t total_uncompressed_length = 0; 68 static uint64_t total_compressed_length = 0; 69 static size_t file_count = 0; 70 static size_t bad_crc_count = 0; 71 72 static const char* g_progname; 73 static int g_exit_code = 0; 74 75 static void die(int error, const char* fmt, ...) { 76 va_list ap; 77 78 va_start(ap, fmt); 79 fprintf(stderr, "%s: ", g_progname); 80 vfprintf(stderr, fmt, ap); 81 if (error != 0) fprintf(stderr, ": %s", strerror(error)); 82 fprintf(stderr, "\n"); 83 va_end(ap); 84 exit(1); 85 } 86 87 static bool ShouldInclude(const std::string& name) { 88 // Explicitly excluded? 89 if (!excludes.empty()) { 90 for (const auto& exclude : excludes) { 91 if (!fnmatch(exclude.c_str(), name.c_str(), 0)) return false; 92 } 93 } 94 95 // Implicitly included? 96 if (includes.empty()) return true; 97 98 // Explicitly included? 99 for (const auto& include : includes) { 100 if (!fnmatch(include.c_str(), name.c_str(), 0)) return true; 101 } 102 return false; 103 } 104 105 static bool MakeDirectoryHierarchy(const std::string& path) { 106 // stat rather than lstat because a symbolic link to a directory is fine too. 107 struct stat sb; 108 if (stat(path.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) return true; 109 110 // Ensure the parent directories exist first. 111 if (!MakeDirectoryHierarchy(android::base::Dirname(path))) return false; 112 113 // Then try to create this directory. 114 return (mkdir(path.c_str(), 0777) != -1); 115 } 116 117 static float CompressionRatio(int64_t uncompressed, int64_t compressed) { 118 if (uncompressed == 0) return 0; 119 return static_cast<float>(100LL * (uncompressed - compressed)) / 120 static_cast<float>(uncompressed); 121 } 122 123 static void MaybeShowHeader(ZipArchiveHandle zah) { 124 if (role == kUnzip) { 125 // unzip has three formats. 126 if (!flag_q) printf("Archive: %s\n", archive_name); 127 if (flag_v) { 128 printf( 129 " Length Method Size Cmpr Date Time CRC-32 Name\n" 130 "-------- ------ ------- ---- ---------- ----- -------- ----\n"); 131 } else if (flag_l) { 132 printf( 133 " Length Date Time Name\n" 134 "--------- ---------- ----- ----\n"); 135 } 136 } else { 137 // zipinfo. 138 if (!flag_1 && includes.empty() && excludes.empty()) { 139 ZipArchiveInfo info{GetArchiveInfo(zah)}; 140 printf("Archive: %s\n", archive_name); 141 printf("Zip file size: %" PRId64 " bytes, number of entries: %" PRIu64 "\n", 142 info.archive_size, info.entry_count); 143 } 144 } 145 } 146 147 static void MaybeShowFooter() { 148 if (role == kUnzip) { 149 if (flag_v) { 150 printf( 151 "-------- ------- --- -------\n" 152 "%8" PRId64 " %8" PRId64 " %3.0f%% %zu file%s\n", 153 total_uncompressed_length, total_compressed_length, 154 CompressionRatio(total_uncompressed_length, total_compressed_length), file_count, 155 (file_count == 1) ? "" : "s"); 156 } else if (flag_l) { 157 printf( 158 "--------- -------\n" 159 "%9" PRId64 " %zu file%s\n", 160 total_uncompressed_length, file_count, (file_count == 1) ? "" : "s"); 161 } else if (flag_t) { 162 if (bad_crc_count != 0) { 163 printf("At least one error was detected in %s.\n", archive_name); 164 } else { 165 printf("No errors detected in "); 166 if (includes.empty() && excludes.empty()) { 167 printf("compressed data of %s.\n", archive_name); 168 } else { 169 printf("%s for the %zu file%s tested.\n", archive_name, 170 file_count, file_count == 1 ? "" : "s"); 171 } 172 } 173 } 174 } else { 175 if (!flag_1 && includes.empty() && excludes.empty()) { 176 printf("%zu files, %" PRId64 " bytes uncompressed, %" PRId64 " bytes compressed: %.1f%%\n", 177 file_count, total_uncompressed_length, total_compressed_length, 178 CompressionRatio(total_uncompressed_length, total_compressed_length)); 179 } 180 } 181 } 182 183 static bool PromptOverwrite(const std::string& dst) { 184 // TODO: [r]ename not implemented because it doesn't seem useful. 185 printf("replace %s? [y]es, [n]o, [A]ll, [N]one: ", dst.c_str()); 186 fflush(stdout); 187 while (true) { 188 char* line = nullptr; 189 size_t n; 190 if (getline(&line, &n, stdin) == -1) { 191 die(0, "(EOF/read error; assuming [N]one...)"); 192 overwrite_mode = kNever; 193 return false; 194 } 195 if (n == 0) continue; 196 char cmd = line[0]; 197 free(line); 198 switch (cmd) { 199 case 'y': 200 return true; 201 case 'n': 202 return false; 203 case 'A': 204 overwrite_mode = kAlways; 205 return true; 206 case 'N': 207 overwrite_mode = kNever; 208 return false; 209 } 210 } 211 } 212 213 class TestWriter : public zip_archive::Writer { 214 public: 215 bool Append(uint8_t* buf, size_t size) { 216 crc = static_cast<uint32_t>(crc32(crc, reinterpret_cast<const Bytef*>(buf), 217 static_cast<uInt>(size))); 218 return true; 219 } 220 uint32_t crc = 0; 221 }; 222 223 static void TestOne(ZipArchiveHandle zah, const ZipEntry64& entry, const std::string& name) { 224 if (!flag_q) printf(" testing: %-24s ", name.c_str()); 225 TestWriter writer; 226 int err = ExtractToWriter(zah, &entry, &writer); 227 if (err < 0) { 228 die(0, "failed to extract %s: %s", name.c_str(), ErrorCodeString(err)); 229 } 230 if (writer.crc == entry.crc32) { 231 if (!flag_q) printf("OK\n"); 232 } else { 233 if (flag_q) printf("%-23s ", name.c_str()); 234 printf("bad CRC %08" PRIx32 " (should be %08" PRIx32 ")\n", writer.crc, entry.crc32); 235 bad_crc_count++; 236 g_exit_code = 2; 237 } 238 } 239 240 static void ExtractToPipe(ZipArchiveHandle zah, const ZipEntry64& entry, const std::string& name) { 241 // We need to extract to memory because ExtractEntryToFile insists on 242 // being able to seek and truncate, and you can't do that with stdout. 243 if (entry.uncompressed_length > SIZE_MAX) { 244 die(0, "entry size %" PRIu64 " is too large to extract.", entry.uncompressed_length); 245 } 246 auto uncompressed_length = static_cast<size_t>(entry.uncompressed_length); 247 uint8_t* buffer = new uint8_t[uncompressed_length]; 248 int err = ExtractToMemory(zah, &entry, buffer, uncompressed_length); 249 if (err < 0) { 250 die(0, "failed to extract %s: %s", name.c_str(), ErrorCodeString(err)); 251 } 252 if (!android::base::WriteFully(1, buffer, uncompressed_length)) { 253 die(errno, "failed to write %s to stdout", name.c_str()); 254 } 255 delete[] buffer; 256 } 257 258 static void ExtractOne(ZipArchiveHandle zah, const ZipEntry64& entry, std::string name) { 259 // Bad filename? 260 if (StartsWith(name, "/") || StartsWith(name, "../") || name.find("/../") != std::string::npos) { 261 die(0, "bad filename %s", name.c_str()); 262 } 263 264 // Junk the path if we were asked to. 265 if (flag_j) name = android::base::Basename(name); 266 267 // Where are we actually extracting to (for human-readable output)? 268 // flag_d is the empty string if -d wasn't used, or has a trailing '/' 269 // otherwise. 270 std::string dst = flag_d + name; 271 272 // Ensure the directory hierarchy exists. 273 if (!MakeDirectoryHierarchy(android::base::Dirname(name))) { 274 die(errno, "couldn't create directory hierarchy for %s", dst.c_str()); 275 } 276 277 // An entry in a zip file can just be a directory itself. 278 if (EndsWith(name, "/")) { 279 if (mkdir(name.c_str(), entry.unix_mode) == -1) { 280 // If the directory already exists, that's fine. 281 if (errno == EEXIST) { 282 struct stat sb; 283 if (stat(name.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) return; 284 } 285 die(errno, "couldn't extract directory %s", dst.c_str()); 286 } 287 return; 288 } 289 290 // Create the file. 291 int fd = open(name.c_str(), O_CREAT | O_WRONLY | O_CLOEXEC | O_EXCL, entry.unix_mode); 292 if (fd == -1 && errno == EEXIST) { 293 if (overwrite_mode == kNever) return; 294 if (overwrite_mode == kPrompt && !PromptOverwrite(dst)) return; 295 // Either overwrite_mode is kAlways or the user consented to this specific case. 296 fd = open(name.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC | O_TRUNC, entry.unix_mode); 297 } 298 if (fd == -1) die(errno, "couldn't create file %s", dst.c_str()); 299 300 // Actually extract into the file. 301 if (!flag_q) printf(" inflating: %s\n", dst.c_str()); 302 int err = ExtractEntryToFile(zah, &entry, fd); 303 if (err < 0) die(0, "failed to extract %s: %s", dst.c_str(), ErrorCodeString(err)); 304 close(fd); 305 } 306 307 static void ListOne(const ZipEntry64& entry, const std::string& name) { 308 tm t = entry.GetModificationTime(); 309 char time[32]; 310 snprintf(time, sizeof(time), "%04d-%02d-%02d %02d:%02d", t.tm_year + 1900, t.tm_mon + 1, 311 t.tm_mday, t.tm_hour, t.tm_min); 312 if (flag_v) { 313 printf("%8" PRIu64 " %s %8" PRIu64 " %3.0f%% %s %08x %s\n", entry.uncompressed_length, 314 (entry.method == kCompressStored) ? "Stored" : "Defl:N", entry.compressed_length, 315 CompressionRatio(entry.uncompressed_length, entry.compressed_length), time, entry.crc32, 316 name.c_str()); 317 } else { 318 printf("%9" PRIu64 " %s %s\n", entry.uncompressed_length, time, name.c_str()); 319 } 320 } 321 322 static void InfoOne(const ZipEntry64& entry, const std::string& name) { 323 if (flag_1) { 324 // "android-ndk-r19b/sources/android/NOTICE" 325 printf("%s\n", name.c_str()); 326 return; 327 } 328 329 int version = entry.version_made_by & 0xff; 330 int os = (entry.version_made_by >> 8) & 0xff; 331 332 // TODO: Support suid/sgid? Non-Unix/non-FAT host file system attributes? 333 const char* src_fs = "???"; 334 char mode[] = "??? "; 335 if (os == 0) { 336 src_fs = "fat"; 337 // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants 338 int attrs = entry.external_file_attributes & 0xff; 339 mode[0] = (attrs & 0x10) ? 'd' : '-'; 340 mode[1] = 'r'; 341 mode[2] = (attrs & 0x01) ? '-' : 'w'; 342 // The man page also mentions ".btm", but that seems to be obsolete? 343 mode[3] = EndsWith(name, ".exe") || EndsWith(name, ".com") || EndsWith(name, ".bat") || 344 EndsWith(name, ".cmd") 345 ? 'x' 346 : '-'; 347 mode[4] = (attrs & 0x20) ? 'a' : '-'; 348 mode[5] = (attrs & 0x02) ? 'h' : '-'; 349 mode[6] = (attrs & 0x04) ? 's' : '-'; 350 } else if (os == 3) { 351 src_fs = "unx"; 352 mode[0] = S_ISDIR(entry.unix_mode) ? 'd' : (S_ISREG(entry.unix_mode) ? '-' : '?'); 353 mode[1] = entry.unix_mode & S_IRUSR ? 'r' : '-'; 354 mode[2] = entry.unix_mode & S_IWUSR ? 'w' : '-'; 355 mode[3] = entry.unix_mode & S_IXUSR ? 'x' : '-'; 356 mode[4] = entry.unix_mode & S_IRGRP ? 'r' : '-'; 357 mode[5] = entry.unix_mode & S_IWGRP ? 'w' : '-'; 358 mode[6] = entry.unix_mode & S_IXGRP ? 'x' : '-'; 359 mode[7] = entry.unix_mode & S_IROTH ? 'r' : '-'; 360 mode[8] = entry.unix_mode & S_IWOTH ? 'w' : '-'; 361 mode[9] = entry.unix_mode & S_IXOTH ? 'x' : '-'; 362 } 363 364 char method[5] = "stor"; 365 if (entry.method == kCompressDeflated) { 366 snprintf(method, sizeof(method), "def%c", "NXFS"[(entry.gpbf >> 1) & 0x3]); 367 } 368 369 // TODO: zipinfo (unlike unzip) sometimes uses time zone? 370 // TODO: this uses 4-digit years because we're not barbarians unless interoperability forces it. 371 tm t = entry.GetModificationTime(); 372 char time[32]; 373 snprintf(time, sizeof(time), "%04d-%02d-%02d %02d:%02d", t.tm_year + 1900, t.tm_mon + 1, 374 t.tm_mday, t.tm_hour, t.tm_min); 375 376 // "-rw-r--r-- 3.0 unx 577 t- defX 19-Feb-12 16:09 android-ndk-r19b/sources/android/NOTICE" 377 printf("%s %2d.%d %s %8" PRIu64 " %c%c %s %s %s\n", mode, version / 10, version % 10, src_fs, 378 entry.uncompressed_length, entry.is_text ? 't' : 'b', 379 entry.has_data_descriptor ? 'X' : 'x', method, time, name.c_str()); 380 } 381 382 static void ProcessOne(ZipArchiveHandle zah, const ZipEntry64& entry, const std::string& name) { 383 if (role == kUnzip) { 384 if (flag_t) { 385 // -t. 386 TestOne(zah, entry, name); 387 } else if (flag_l || flag_v) { 388 // -l or -lv or -lq or -v. 389 ListOne(entry, name); 390 } else { 391 // Actually extract. 392 if (flag_p) { 393 ExtractToPipe(zah, entry, name); 394 } else { 395 ExtractOne(zah, entry, name); 396 } 397 } 398 } else { 399 // zipinfo or zipinfo -1. 400 InfoOne(entry, name); 401 } 402 total_uncompressed_length += entry.uncompressed_length; 403 total_compressed_length += entry.compressed_length; 404 ++file_count; 405 } 406 407 static void ProcessAll(ZipArchiveHandle zah) { 408 MaybeShowHeader(zah); 409 410 // libziparchive iteration order doesn't match the central directory. 411 // We could sort, but that would cost extra and wouldn't match either. 412 void* cookie; 413 int err = StartIteration(zah, &cookie); 414 if (err != 0) { 415 die(0, "couldn't iterate %s: %s", archive_name, ErrorCodeString(err)); 416 } 417 418 ZipEntry64 entry; 419 std::string name; 420 while ((err = Next(cookie, &entry, &name)) >= 0) { 421 if (ShouldInclude(name)) ProcessOne(zah, entry, name); 422 } 423 424 if (err < -1) die(0, "failed iterating %s: %s", archive_name, ErrorCodeString(err)); 425 EndIteration(cookie); 426 427 MaybeShowFooter(); 428 } 429 430 static void ShowHelp(bool full) { 431 if (role == kUnzip) { 432 fprintf(full ? stdout : stderr, "usage: unzip [-d DIR] [-lnopqv] ZIP [FILE...] [-x FILE...]\n"); 433 if (!full) exit(EXIT_FAILURE); 434 435 printf( 436 "\n" 437 "Extract FILEs from ZIP archive. Default is all files. Both the include and\n" 438 "exclude (-x) lists use shell glob patterns.\n" 439 "\n" 440 "-d DIR Extract into DIR\n" 441 "-j Junk (ignore) file paths\n" 442 "-l List contents (-lq excludes archive name, -lv is verbose)\n" 443 "-n Never overwrite files (default: prompt)\n" 444 "-o Always overwrite files\n" 445 "-p Pipe to stdout\n" 446 "-q Quiet\n" 447 "-t Test compressed data (do not extract)\n" 448 "-v List contents verbosely\n" 449 "-x FILE Exclude files\n"); 450 } else { 451 fprintf(full ? stdout : stderr, "usage: zipinfo [-1] ZIP [FILE...] [-x FILE...]\n"); 452 if (!full) exit(EXIT_FAILURE); 453 454 printf( 455 "\n" 456 "Show information about FILEs from ZIP archive. Default is all files.\n" 457 "Both the include and exclude (-x) lists use shell glob patterns.\n" 458 "\n" 459 "-1 Show filenames only, one per line\n" 460 "-x FILE Exclude files\n"); 461 } 462 exit(EXIT_SUCCESS); 463 } 464 465 static void HandleCommonOption(int opt) { 466 switch (opt) { 467 case 'h': 468 ShowHelp(true); 469 break; 470 case 'x': 471 flag_x = true; 472 break; 473 case 1: 474 // -x swallows all following arguments, so we use '-' in the getopt 475 // string and collect files here. 476 if (!archive_name) { 477 archive_name = optarg; 478 } else if (flag_x) { 479 excludes.insert(optarg); 480 } else { 481 includes.insert(optarg); 482 } 483 break; 484 default: 485 ShowHelp(false); 486 break; 487 } 488 } 489 490 int main(int argc, char* argv[]) { 491 // Who am I, and what am I doing? 492 g_progname = basename(argv[0]); 493 if (!strcmp(g_progname, "ziptool") && argc > 1) return main(argc - 1, argv + 1); 494 if (!strcmp(g_progname, "unzip")) { 495 role = kUnzip; 496 } else if (!strcmp(g_progname, "zipinfo")) { 497 role = kZipinfo; 498 } else { 499 die(0, "run as ziptool with unzip or zipinfo as the first argument, or symlink"); 500 } 501 502 static const struct option opts[] = { 503 {"help", no_argument, 0, 'h'}, 504 {}, 505 }; 506 507 if (role == kUnzip) { 508 // `unzip -Z` is "zipinfo mode", so in that case just restart... 509 if (argc > 1 && !strcmp(argv[1], "-Z")) { 510 argv[1] = const_cast<char*>("zipinfo"); 511 return main(argc - 1, argv + 1); 512 } 513 514 int opt; 515 while ((opt = getopt_long(argc, argv, "-Dd:hjlnopqtvx", opts, nullptr)) != -1) { 516 switch (opt) { 517 case 'D': 518 // Undocumented and ignored, since we never use the times from the zip 519 // file when creating files or directories. Moreover, libziparchive 520 // only looks at the DOS last modified date anyway. There's no code to 521 // use the GMT modification/access times in the extra field at the 522 // moment. 523 break; 524 case 'd': 525 flag_d = optarg; 526 if (!EndsWith(flag_d, "/")) flag_d += '/'; 527 break; 528 case 'j': 529 flag_j = true; 530 break; 531 case 'l': 532 flag_l = true; 533 break; 534 case 'n': 535 overwrite_mode = kNever; 536 break; 537 case 'o': 538 overwrite_mode = kAlways; 539 break; 540 case 'p': 541 flag_p = flag_q = true; 542 break; 543 case 'q': 544 flag_q = true; 545 break; 546 case 't': 547 flag_t = true; 548 break; 549 case 'v': 550 flag_v = true; 551 break; 552 default: 553 HandleCommonOption(opt); 554 break; 555 } 556 } 557 } else { 558 int opt; 559 while ((opt = getopt_long(argc, argv, "-1hx", opts, nullptr)) != -1) { 560 switch (opt) { 561 case '1': 562 flag_1 = true; 563 break; 564 default: 565 HandleCommonOption(opt); 566 break; 567 } 568 } 569 } 570 571 if (!archive_name) die(0, "missing archive filename"); 572 573 // We can't support "-" to unzip from stdin because libziparchive relies on mmap. 574 ZipArchiveHandle zah; 575 int32_t err; 576 if ((err = OpenArchive(archive_name, &zah)) != 0) { 577 die(0, "couldn't open %s: %s", archive_name, ErrorCodeString(err)); 578 } 579 580 // Implement -d by changing into that directory. 581 // We'll create implicit directories based on paths in the zip file, and we'll create 582 // the -d directory itself, but we require that *parents* of the -d directory already exists. 583 // This is pretty arbitrary, but it's the behavior of the original unzip. 584 if (!flag_d.empty()) { 585 if (mkdir(flag_d.c_str(), 0777) == -1 && errno != EEXIST) { 586 die(errno, "couldn't created %s", flag_d.c_str()); 587 } 588 if (chdir(flag_d.c_str()) == -1) { 589 die(errno, "couldn't chdir to %s", flag_d.c_str()); 590 } 591 } 592 593 ProcessAll(zah); 594 595 CloseArchive(zah); 596 return g_exit_code; 597 } 598