Serenity Operating System
at master 184 lines 6.8 kB view raw
1/* 2 * Copyright (c) 2020, Andrés Vieira <anvieiravazquez@gmail.com> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/Assertions.h> 8#include <AK/DOSPackedTime.h> 9#include <AK/NumberFormat.h> 10#include <AK/StringUtils.h> 11#include <LibArchive/Zip.h> 12#include <LibCompress/Deflate.h> 13#include <LibCore/ArgsParser.h> 14#include <LibCore/DeprecatedFile.h> 15#include <LibCore/Directory.h> 16#include <LibCore/MappedFile.h> 17#include <LibCore/System.h> 18#include <LibCrypto/Checksum/CRC32.h> 19#include <sys/stat.h> 20 21static ErrorOr<void> adjust_modification_time(Archive::ZipMember const& zip_member) 22{ 23 auto time = time_from_packed_dos(zip_member.modification_date, zip_member.modification_time); 24 auto seconds = static_cast<time_t>(time.to_seconds()); 25 struct utimbuf buf { 26 .actime = seconds, 27 .modtime = seconds 28 }; 29 30 return Core::System::utime(zip_member.name, buf); 31} 32 33static bool unpack_zip_member(Archive::ZipMember zip_member, bool quiet) 34{ 35 if (zip_member.is_directory) { 36 if (auto maybe_error = Core::System::mkdir(zip_member.name, 0755); maybe_error.is_error()) { 37 warnln("Failed to create directory '{}': {}", zip_member.name, maybe_error.error()); 38 return false; 39 } 40 if (!quiet) 41 outln(" extracting: {}", zip_member.name); 42 return true; 43 } 44 MUST(Core::Directory::create(LexicalPath(zip_member.name.to_deprecated_string()).parent(), Core::Directory::CreateDirectories::Yes)); 45 auto new_file = Core::DeprecatedFile::construct(zip_member.name.to_deprecated_string()); 46 if (!new_file->open(Core::OpenMode::WriteOnly)) { 47 warnln("Can't write file {}: {}", zip_member.name, new_file->error_string()); 48 return false; 49 } 50 51 if (!quiet) 52 outln(" extracting: {}", zip_member.name); 53 54 Crypto::Checksum::CRC32 checksum; 55 switch (zip_member.compression_method) { 56 case Archive::ZipCompressionMethod::Store: { 57 if (!new_file->write(zip_member.compressed_data.data(), zip_member.compressed_data.size())) { 58 warnln("Can't write file contents in {}: {}", zip_member.name, new_file->error_string()); 59 return false; 60 } 61 checksum.update({ zip_member.compressed_data.data(), zip_member.compressed_data.size() }); 62 break; 63 } 64 case Archive::ZipCompressionMethod::Deflate: { 65 auto decompressed_data = Compress::DeflateDecompressor::decompress_all(zip_member.compressed_data); 66 if (decompressed_data.is_error()) { 67 warnln("Failed decompressing file {}: {}", zip_member.name, decompressed_data.error()); 68 return false; 69 } 70 if (decompressed_data.value().size() != zip_member.uncompressed_size) { 71 warnln("Failed decompressing file {}", zip_member.name); 72 return false; 73 } 74 if (!new_file->write(decompressed_data.value().data(), decompressed_data.value().size())) { 75 warnln("Can't write file contents in {}: {}", zip_member.name, new_file->error_string()); 76 return false; 77 } 78 checksum.update({ decompressed_data.value().data(), decompressed_data.value().size() }); 79 break; 80 } 81 default: 82 VERIFY_NOT_REACHED(); 83 } 84 85 if (adjust_modification_time(zip_member).is_error()) { 86 warnln("Failed setting modification_time for file {}", zip_member.name); 87 return false; 88 } 89 90 if (!new_file->close()) { 91 warnln("Can't close file {}: {}", zip_member.name, new_file->error_string()); 92 return false; 93 } 94 95 if (checksum.digest() != zip_member.crc32) { 96 warnln("Failed decompressing file {}: CRC32 mismatch", zip_member.name); 97 MUST(Core::DeprecatedFile::remove(zip_member.name, Core::DeprecatedFile::RecursionMode::Disallowed)); 98 return false; 99 } 100 101 return true; 102} 103 104ErrorOr<int> serenity_main(Main::Arguments arguments) 105{ 106 StringView zip_file_path; 107 bool quiet { false }; 108 StringView output_directory_path; 109 Vector<StringView> file_filters; 110 111 Core::ArgsParser args_parser; 112 args_parser.add_option(output_directory_path, "Directory to receive the archive content", "output-directory", 'd', "path"); 113 args_parser.add_option(quiet, "Be less verbose", "quiet", 'q'); 114 args_parser.add_positional_argument(zip_file_path, "File to unzip", "path", Core::ArgsParser::Required::Yes); 115 args_parser.add_positional_argument(file_filters, "Files or filters in the archive to extract", "files", Core::ArgsParser::Required::No); 116 args_parser.parse(arguments); 117 118 struct stat st = TRY(Core::System::stat(zip_file_path)); 119 120 // FIXME: Map file chunk-by-chunk once we have mmap() with offset. 121 // This will require mapping some parts then unmapping them repeatedly, 122 // but it would be significantly faster and less syscall heavy than seek()/read() at every read. 123 RefPtr<Core::MappedFile> mapped_file; 124 ReadonlyBytes input_bytes; 125 if (st.st_size > 0) { 126 mapped_file = TRY(Core::MappedFile::map(zip_file_path)); 127 input_bytes = mapped_file->bytes(); 128 } 129 130 if (!quiet) 131 warnln("Archive: {}", zip_file_path); 132 133 auto zip_file = Archive::Zip::try_create(input_bytes); 134 if (!zip_file.has_value()) { 135 warnln("Invalid zip file {}", zip_file_path); 136 return 1; 137 } 138 139 if (!output_directory_path.is_null()) { 140 TRY(Core::Directory::create(output_directory_path, Core::Directory::CreateDirectories::Yes)); 141 TRY(Core::System::chdir(output_directory_path)); 142 } 143 144 Vector<Archive::ZipMember> zip_directories; 145 146 auto success = TRY(zip_file->for_each_member([&](auto zip_member) { 147 bool keep_file = false; 148 149 if (!file_filters.is_empty()) { 150 for (auto& filter : file_filters) { 151 // Convert underscore wildcards (usual unzip convention) to question marks (as used by StringUtils) 152 auto string_filter = filter.replace("_"sv, "?"sv, ReplaceMode::All); 153 if (zip_member.name.bytes_as_string_view().matches(string_filter, CaseSensitivity::CaseSensitive)) { 154 keep_file = true; 155 break; 156 } 157 } 158 } else { 159 keep_file = true; 160 } 161 162 if (keep_file) { 163 if (!unpack_zip_member(zip_member, quiet)) 164 return IterationDecision::Break; 165 if (zip_member.is_directory) 166 zip_directories.append(zip_member); 167 } 168 169 return IterationDecision::Continue; 170 })); 171 172 if (!success) { 173 return 1; 174 } 175 176 for (auto& directory : zip_directories) { 177 if (adjust_modification_time(directory).is_error()) { 178 warnln("Failed setting modification time for directory {}", directory.name); 179 return 1; 180 } 181 } 182 183 return success ? 0 : 1; 184}