unzip.cpp 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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/Directory.h>
  13. #include <LibCore/File.h>
  14. #include <LibCore/MappedFile.h>
  15. #include <LibCore/System.h>
  16. #include <LibCrypto/Checksum/CRC32.h>
  17. #include <sys/stat.h>
  18. static bool unpack_zip_member(Archive::ZipMember zip_member, bool quiet)
  19. {
  20. if (zip_member.is_directory) {
  21. if (mkdir(zip_member.name.characters(), 0755) < 0) {
  22. perror("mkdir");
  23. return false;
  24. }
  25. if (!quiet)
  26. outln(" extracting: {}", zip_member.name);
  27. return true;
  28. }
  29. MUST(Core::Directory::create(LexicalPath(zip_member.name).parent(), Core::Directory::CreateDirectories::Yes));
  30. auto new_file = Core::File::construct(zip_member.name);
  31. if (!new_file->open(Core::OpenMode::WriteOnly)) {
  32. warnln("Can't write file {}: {}", zip_member.name, new_file->error_string());
  33. return false;
  34. }
  35. if (!quiet)
  36. outln(" extracting: {}", zip_member.name);
  37. Crypto::Checksum::CRC32 checksum;
  38. switch (zip_member.compression_method) {
  39. case Archive::ZipCompressionMethod::Store: {
  40. if (!new_file->write(zip_member.compressed_data.data(), zip_member.compressed_data.size())) {
  41. warnln("Can't write file contents in {}: {}", zip_member.name, new_file->error_string());
  42. return false;
  43. }
  44. checksum.update({ zip_member.compressed_data.data(), zip_member.compressed_data.size() });
  45. break;
  46. }
  47. case Archive::ZipCompressionMethod::Deflate: {
  48. auto decompressed_data = Compress::DeflateDecompressor::decompress_all(zip_member.compressed_data);
  49. if (decompressed_data.is_error()) {
  50. warnln("Failed decompressing file {}: {}", zip_member.name, decompressed_data.error());
  51. return false;
  52. }
  53. if (decompressed_data.value().size() != zip_member.uncompressed_size) {
  54. warnln("Failed decompressing file {}", zip_member.name);
  55. return false;
  56. }
  57. if (!new_file->write(decompressed_data.value().data(), decompressed_data.value().size())) {
  58. warnln("Can't write file contents in {}: {}", zip_member.name, new_file->error_string());
  59. return false;
  60. }
  61. checksum.update({ decompressed_data.value().data(), decompressed_data.value().size() });
  62. break;
  63. }
  64. default:
  65. VERIFY_NOT_REACHED();
  66. }
  67. if (!new_file->close()) {
  68. warnln("Can't close file {}: {}", zip_member.name, new_file->error_string());
  69. return false;
  70. }
  71. if (checksum.digest() != zip_member.crc32) {
  72. warnln("Failed decompressing file {}: CRC32 mismatch", zip_member.name);
  73. MUST(Core::File::remove(zip_member.name, Core::File::RecursionMode::Disallowed));
  74. return false;
  75. }
  76. return true;
  77. }
  78. ErrorOr<int> serenity_main(Main::Arguments arguments)
  79. {
  80. StringView zip_file_path;
  81. bool quiet { false };
  82. StringView output_directory_path;
  83. Vector<StringView> file_filters;
  84. Core::ArgsParser args_parser;
  85. args_parser.add_option(output_directory_path, "Directory to receive the archive content", "output-directory", 'd', "path");
  86. args_parser.add_option(quiet, "Be less verbose", "quiet", 'q');
  87. args_parser.add_positional_argument(zip_file_path, "File to unzip", "path", Core::ArgsParser::Required::Yes);
  88. args_parser.add_positional_argument(file_filters, "Files or filters in the archive to extract", "files", Core::ArgsParser::Required::No);
  89. args_parser.parse(arguments);
  90. struct stat st = TRY(Core::System::stat(zip_file_path));
  91. // FIXME: Map file chunk-by-chunk once we have mmap() with offset.
  92. // This will require mapping some parts then unmapping them repeatedly,
  93. // but it would be significantly faster and less syscall heavy than seek()/read() at every read.
  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. TRY(Core::Directory::create(output_directory_path, Core::Directory::CreateDirectories::Yes));
  109. TRY(Core::System::chdir(output_directory_path));
  110. }
  111. auto success = zip_file->for_each_member([&](auto zip_member) {
  112. bool keep_file = false;
  113. if (!file_filters.is_empty()) {
  114. for (auto& filter : file_filters) {
  115. // Convert underscore wildcards (usual unzip convention) to question marks (as used by StringUtils)
  116. auto string_filter = filter.replace("_"sv, "?"sv, ReplaceMode::All);
  117. if (zip_member.name.matches(string_filter, CaseSensitivity::CaseSensitive)) {
  118. keep_file = true;
  119. break;
  120. }
  121. }
  122. } else {
  123. keep_file = true;
  124. }
  125. if (keep_file) {
  126. if (!unpack_zip_member(zip_member, quiet))
  127. return IterationDecision::Break;
  128. }
  129. return IterationDecision::Continue;
  130. });
  131. return success ? 0 : 1;
  132. }