Serenity Operating System
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}