unzip.cpp 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /*
  2. * Copyright (c) 2020, Andrés Vieira <anvieiravazquez@gmail.com>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Assertions.h>
  7. #include <AK/NumberFormat.h>
  8. #include <AK/StringUtils.h>
  9. #include <LibArchive/Zip.h>
  10. #include <LibCompress/Deflate.h>
  11. #include <LibCore/ArgsParser.h>
  12. #include <LibCore/File.h>
  13. #include <LibCore/MappedFile.h>
  14. #include <LibCore/System.h>
  15. #include <sys/stat.h>
  16. #include <unistd.h>
  17. static bool unpack_zip_member(Archive::ZipMember zip_member, bool quiet)
  18. {
  19. if (zip_member.is_directory) {
  20. if (mkdir(zip_member.name.characters(), 0755) < 0) {
  21. perror("mkdir");
  22. return false;
  23. }
  24. if (!quiet)
  25. outln(" extracting: {}", zip_member.name);
  26. return true;
  27. }
  28. auto new_file = Core::File::construct(zip_member.name);
  29. if (!new_file->open(Core::OpenMode::WriteOnly)) {
  30. warnln("Can't write file {}: {}", zip_member.name, new_file->error_string());
  31. return false;
  32. }
  33. if (!quiet)
  34. outln(" extracting: {}", zip_member.name);
  35. // TODO: verify CRC32s match!
  36. switch (zip_member.compression_method) {
  37. case Archive::ZipCompressionMethod::Store: {
  38. if (!new_file->write(zip_member.compressed_data.data(), zip_member.compressed_data.size())) {
  39. warnln("Can't write file contents in {}: {}", zip_member.name, new_file->error_string());
  40. return false;
  41. }
  42. break;
  43. }
  44. case Archive::ZipCompressionMethod::Deflate: {
  45. auto decompressed_data = Compress::DeflateDecompressor::decompress_all(zip_member.compressed_data);
  46. if (!decompressed_data.has_value()) {
  47. warnln("Failed decompressing file {}", zip_member.name);
  48. return false;
  49. }
  50. if (decompressed_data.value().size() != zip_member.uncompressed_size) {
  51. warnln("Failed decompressing file {}", zip_member.name);
  52. return false;
  53. }
  54. if (!new_file->write(decompressed_data.value().data(), decompressed_data.value().size())) {
  55. warnln("Can't write file contents in {}: {}", zip_member.name, new_file->error_string());
  56. return false;
  57. }
  58. break;
  59. }
  60. default:
  61. VERIFY_NOT_REACHED();
  62. }
  63. if (!new_file->close()) {
  64. warnln("Can't close file {}: {}", zip_member.name, new_file->error_string());
  65. return false;
  66. }
  67. return true;
  68. }
  69. ErrorOr<int> serenity_main(Main::Arguments arguments)
  70. {
  71. const char* path;
  72. int map_size_limit = 32 * MiB;
  73. bool quiet { false };
  74. String output_directory_path;
  75. Vector<StringView> file_filters;
  76. Core::ArgsParser args_parser;
  77. args_parser.add_option(map_size_limit, "Maximum chunk size to map", "map-size-limit", 0, "size");
  78. args_parser.add_option(output_directory_path, "Directory to receive the archive content", "output-directory", 'd', "path");
  79. args_parser.add_option(quiet, "Be less verbose", "quiet", 'q');
  80. args_parser.add_positional_argument(path, "File to unzip", "path", Core::ArgsParser::Required::Yes);
  81. args_parser.add_positional_argument(file_filters, "Files or filters in the archive to extract", "files", Core::ArgsParser::Required::No);
  82. args_parser.parse(arguments);
  83. String zip_file_path { path };
  84. struct stat st = TRY(Core::System::stat(zip_file_path));
  85. // FIXME: Map file chunk-by-chunk once we have mmap() with offset.
  86. // This will require mapping some parts then unmapping them repeatedly,
  87. // but it would be significantly faster and less syscall heavy than seek()/read() at every read.
  88. if (st.st_size >= map_size_limit) {
  89. warnln("unzip warning: Refusing to map file since it is larger than {}, pass '--map-size-limit {}' to get around this",
  90. human_readable_size(map_size_limit),
  91. round_up_to_power_of_two(st.st_size, 16));
  92. return 1;
  93. }
  94. RefPtr<Core::MappedFile> mapped_file;
  95. ReadonlyBytes input_bytes;
  96. if (st.st_size > 0) {
  97. mapped_file = TRY(Core::MappedFile::map(zip_file_path));
  98. input_bytes = mapped_file->bytes();
  99. }
  100. if (!quiet)
  101. warnln("Archive: {}", zip_file_path);
  102. auto zip_file = Archive::Zip::try_create(input_bytes);
  103. if (!zip_file.has_value()) {
  104. warnln("Invalid zip file {}", zip_file_path);
  105. return 1;
  106. }
  107. if (!output_directory_path.is_null()) {
  108. auto mkdir_error = Core::System::mkdir(output_directory_path, 0755);
  109. if (mkdir_error.is_error() && mkdir_error.error().code() != EEXIST)
  110. return mkdir_error.release_error();
  111. TRY(Core::System::chdir(output_directory_path));
  112. }
  113. auto success = zip_file->for_each_member([&](auto zip_member) {
  114. bool keep_file = false;
  115. if (!file_filters.is_empty()) {
  116. for (auto& filter : file_filters) {
  117. // Convert underscore wildcards (usual unzip convention) to question marks (as used by StringUtils)
  118. auto string_filter = filter.replace("_", "?", true);
  119. if (zip_member.name.matches(string_filter, CaseSensitivity::CaseSensitive)) {
  120. keep_file = true;
  121. break;
  122. }
  123. }
  124. } else {
  125. keep_file = true;
  126. }
  127. if (keep_file) {
  128. if (!unpack_zip_member(zip_member, quiet))
  129. return IterationDecision::Break;
  130. }
  131. return IterationDecision::Continue;
  132. });
  133. return success ? 0 : 1;
  134. }