/src/serenity/Userland/Libraries/LibArchive/Zip.h
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> |
3 | | * Copyright (c) 2022-2025, the SerenityOS developers. |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #pragma once |
9 | | |
10 | | #include <AK/Array.h> |
11 | | #include <AK/DOSPackedTime.h> |
12 | | #include <AK/Function.h> |
13 | | #include <AK/IterationDecision.h> |
14 | | #include <AK/NonnullOwnPtr.h> |
15 | | #include <AK/Stream.h> |
16 | | #include <AK/String.h> |
17 | | #include <AK/Vector.h> |
18 | | #include <LibArchive/Statistics.h> |
19 | | #include <LibCore/DateTime.h> |
20 | | #include <string.h> |
21 | | |
22 | | namespace Archive { |
23 | | |
24 | | template<size_t fields_size, class T> |
25 | | static bool read_helper(ReadonlyBytes buffer, T* self) |
26 | 15.2k | { |
27 | 15.2k | if (buffer.size() < T::signature.size() + fields_size) |
28 | 50 | return false; |
29 | 15.1k | if (buffer.slice(0, T::signature.size()) != T::signature) |
30 | 31 | return false; |
31 | 15.1k | memcpy(self, buffer.data() + T::signature.size(), fields_size); |
32 | 15.1k | return true; |
33 | 15.1k | } Unexecuted instantiation: FuzzZip.cpp:bool Archive::read_helper<18ul, Archive::EndOfCentralDirectory>(AK::Span<unsigned char const>, Archive::EndOfCentralDirectory*) Unexecuted instantiation: FuzzZip.cpp:bool Archive::read_helper<42ul, Archive::CentralDirectoryRecord>(AK::Span<unsigned char const>, Archive::CentralDirectoryRecord*) Unexecuted instantiation: FuzzZip.cpp:bool Archive::read_helper<26ul, Archive::LocalFileHeader>(AK::Span<unsigned char const>, Archive::LocalFileHeader*) Zip.cpp:bool Archive::read_helper<18ul, Archive::EndOfCentralDirectory>(AK::Span<unsigned char const>, Archive::EndOfCentralDirectory*) Line | Count | Source | 26 | 803 | { | 27 | 803 | if (buffer.size() < T::signature.size() + fields_size) | 28 | 19 | return false; | 29 | 784 | if (buffer.slice(0, T::signature.size()) != T::signature) | 30 | 0 | return false; | 31 | 784 | memcpy(self, buffer.data() + T::signature.size(), fields_size); | 32 | 784 | return true; | 33 | 784 | } |
Zip.cpp:bool Archive::read_helper<42ul, Archive::CentralDirectoryRecord>(AK::Span<unsigned char const>, Archive::CentralDirectoryRecord*) Line | Count | Source | 26 | 7.30k | { | 27 | 7.30k | if (buffer.size() < T::signature.size() + fields_size) | 28 | 22 | return false; | 29 | 7.27k | if (buffer.slice(0, T::signature.size()) != T::signature) | 30 | 16 | return false; | 31 | 7.26k | memcpy(self, buffer.data() + T::signature.size(), fields_size); | 32 | 7.26k | return true; | 33 | 7.27k | } |
Zip.cpp:bool Archive::read_helper<26ul, Archive::LocalFileHeader>(AK::Span<unsigned char const>, Archive::LocalFileHeader*) Line | Count | Source | 26 | 7.13k | { | 27 | 7.13k | if (buffer.size() < T::signature.size() + fields_size) | 28 | 9 | return false; | 29 | 7.12k | if (buffer.slice(0, T::signature.size()) != T::signature) | 30 | 15 | return false; | 31 | 7.10k | memcpy(self, buffer.data() + T::signature.size(), fields_size); | 32 | 7.10k | return true; | 33 | 7.12k | } |
|
34 | | |
35 | | // NOTE: Due to the format of zip files compression is streamed and decompression is random access. |
36 | | // Zip format specification: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT |
37 | | |
38 | | static constexpr auto signature_length = 4; |
39 | | |
40 | | struct [[gnu::packed]] EndOfCentralDirectory { |
41 | | static constexpr Array<u8, signature_length> signature = { 0x50, 0x4b, 0x05, 0x06 }; // 'PK\x05\x06' |
42 | | |
43 | | u16 disk_number; |
44 | | u16 central_directory_start_disk; |
45 | | u16 disk_records_count; |
46 | | u16 total_records_count; |
47 | | u32 central_directory_size; |
48 | | u32 central_directory_offset; |
49 | | u16 comment_length; |
50 | | u8 const* comment; |
51 | | |
52 | | bool read(ReadonlyBytes buffer) |
53 | 803 | { |
54 | 803 | constexpr auto fields_size = sizeof(EndOfCentralDirectory) - (sizeof(u8*) * 1); |
55 | 803 | if (!read_helper<fields_size>(buffer, this)) |
56 | 19 | return false; |
57 | 784 | if (buffer.size() < signature.size() + fields_size + comment_length) |
58 | 25 | return false; |
59 | 759 | comment = buffer.data() + signature.size() + fields_size; |
60 | 759 | return true; |
61 | 784 | } |
62 | | |
63 | | ErrorOr<void> write(Stream& stream) const |
64 | 0 | { |
65 | 0 | auto write_value = [&stream](auto value) { |
66 | 0 | return stream.write_until_depleted({ &value, sizeof(value) }); |
67 | 0 | }; Unexecuted instantiation: auto Archive::EndOfCentralDirectory::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned short>(unsigned short) constUnexecuted instantiation: auto Archive::EndOfCentralDirectory::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned int>(unsigned int) const |
68 | |
|
69 | 0 | TRY(stream.write_until_depleted(signature)); |
70 | 0 | TRY(write_value(disk_number)); |
71 | 0 | TRY(write_value(central_directory_start_disk)); |
72 | 0 | TRY(write_value(disk_records_count)); |
73 | 0 | TRY(write_value(total_records_count)); |
74 | 0 | TRY(write_value(central_directory_size)); |
75 | 0 | TRY(write_value(central_directory_offset)); |
76 | 0 | TRY(write_value(comment_length)); |
77 | 0 | if (comment_length > 0) |
78 | 0 | TRY(stream.write_until_depleted({ comment, comment_length })); |
79 | 0 | return {}; |
80 | 0 | } |
81 | | }; |
82 | | |
83 | | enum class ZipCompressionMethod : u16 { |
84 | | Store = 0, |
85 | | Shrink = 1, |
86 | | Reduce1 = 2, |
87 | | Reduce2 = 3, |
88 | | Reduce3 = 4, |
89 | | Reduce4 = 5, |
90 | | Implode = 6, |
91 | | Reserved = 7, |
92 | | Deflate = 8 |
93 | | }; |
94 | | |
95 | | union ZipGeneralPurposeFlags { |
96 | | u16 flags; |
97 | | struct { |
98 | | u16 encrypted : 1; |
99 | | u16 compression_options : 2; |
100 | | u16 data_descriptor : 1; |
101 | | u16 enhanced_deflation : 1; |
102 | | u16 compressed_patched_data : 1; |
103 | | u16 strong_encryption : 1; |
104 | | u16 : 4; |
105 | | u16 language_encoding : 1; |
106 | | u16 : 1; |
107 | | u16 masked_data_values : 1; |
108 | | u16 : 2; |
109 | | }; |
110 | | }; |
111 | | static_assert(sizeof(ZipGeneralPurposeFlags) == sizeof(u16)); |
112 | | |
113 | | enum class ZipMadeBy : u8 { |
114 | | MSDOS = 0, |
115 | | Unix = 3, |
116 | | }; |
117 | | |
118 | | struct [[gnu::packed]] CentralDirectoryRecord { |
119 | | static constexpr Array<u8, signature_length> signature = { 0x50, 0x4b, 0x01, 0x02 }; // 'PK\x01\x02' |
120 | | |
121 | | struct { |
122 | | u8 version; |
123 | | ZipMadeBy made_by; |
124 | | } made_by_version; |
125 | | u16 minimum_version; |
126 | | ZipGeneralPurposeFlags general_purpose_flags; |
127 | | ZipCompressionMethod compression_method; |
128 | | DOSPackedTime modification_time; |
129 | | DOSPackedDate modification_date; |
130 | | u32 crc32; |
131 | | u32 compressed_size; |
132 | | u32 uncompressed_size; |
133 | | u16 name_length; |
134 | | u16 extra_data_length; |
135 | | u16 comment_length; |
136 | | u16 start_disk; |
137 | | u16 internal_attributes; |
138 | | struct { |
139 | | u16 msdos; |
140 | | u16 unix; |
141 | | } external_attributes; |
142 | | u32 local_file_header_offset; |
143 | | u8 const* name; |
144 | | u8 const* extra_data; |
145 | | u8 const* comment; |
146 | | |
147 | | bool read(ReadonlyBytes buffer) |
148 | 7.30k | { |
149 | 7.30k | constexpr auto fields_size = sizeof(CentralDirectoryRecord) - (sizeof(u8*) * 3); |
150 | 7.30k | if (!read_helper<fields_size>(buffer, this)) |
151 | 38 | return false; |
152 | 7.26k | if (buffer.size() < size()) |
153 | 18 | return false; |
154 | 7.24k | name = buffer.data() + signature.size() + fields_size; |
155 | 7.24k | extra_data = name + name_length; |
156 | 7.24k | comment = extra_data + extra_data_length; |
157 | 7.24k | return true; |
158 | 7.26k | } |
159 | | |
160 | | ErrorOr<void> write(Stream& stream) const |
161 | 0 | { |
162 | 0 | auto write_value = [&stream](auto value) { |
163 | 0 | return stream.write_until_depleted({ &value, sizeof(value) }); |
164 | 0 | }; Unexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<Archive::CentralDirectoryRecord::{unnamed type#1}>(Archive::CentralDirectoryRecord::{unnamed type#1}) constUnexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned short>(unsigned short) constUnexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<Archive::ZipCompressionMethod>(Archive::ZipCompressionMethod) constUnexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedTime>(AK::DOSPackedTime) constUnexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedDate>(AK::DOSPackedDate) constUnexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned int>(unsigned int) constUnexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<Archive::CentralDirectoryRecord::{unnamed type#2}>(Archive::CentralDirectoryRecord::{unnamed type#2}) const |
165 | |
|
166 | 0 | TRY(stream.write_until_depleted(signature)); |
167 | 0 | TRY(write_value(made_by_version)); |
168 | 0 | TRY(write_value(minimum_version)); |
169 | 0 | TRY(write_value(general_purpose_flags.flags)); |
170 | 0 | TRY(write_value(compression_method)); |
171 | 0 | TRY(write_value(modification_time)); |
172 | 0 | TRY(write_value(modification_date)); |
173 | 0 | TRY(write_value(crc32)); |
174 | 0 | TRY(write_value(compressed_size)); |
175 | 0 | TRY(write_value(uncompressed_size)); |
176 | 0 | TRY(write_value(name_length)); |
177 | 0 | TRY(write_value(extra_data_length)); |
178 | 0 | TRY(write_value(comment_length)); |
179 | 0 | TRY(write_value(start_disk)); |
180 | 0 | TRY(write_value(internal_attributes)); |
181 | 0 | TRY(write_value(external_attributes)); |
182 | 0 | TRY(write_value(local_file_header_offset)); |
183 | 0 | if (name_length > 0) |
184 | 0 | TRY(stream.write_until_depleted({ name, name_length })); |
185 | 0 | if (extra_data_length > 0) |
186 | 0 | TRY(stream.write_until_depleted({ extra_data, extra_data_length })); |
187 | 0 | if (comment_length > 0) |
188 | 0 | TRY(stream.write_until_depleted({ comment, comment_length })); |
189 | 0 | return {}; |
190 | 0 | } |
191 | | |
192 | | [[nodiscard]] size_t size() const |
193 | 14.2k | { |
194 | 14.2k | return signature.size() + (sizeof(CentralDirectoryRecord) - (sizeof(u8*) * 3)) + name_length + extra_data_length + comment_length; |
195 | 14.2k | } |
196 | | }; |
197 | | static constexpr u16 zip_directory_msdos_attribute = 1 << 4; |
198 | | |
199 | | struct [[gnu::packed]] LocalFileHeader { |
200 | | static constexpr Array<u8, signature_length> signature = { 0x50, 0x4b, 0x03, 0x04 }; // 'PK\x03\x04' |
201 | | |
202 | | u16 minimum_version; |
203 | | ZipGeneralPurposeFlags general_purpose_flags; |
204 | | u16 compression_method; |
205 | | DOSPackedTime modification_time; |
206 | | DOSPackedDate modification_date; |
207 | | u32 crc32; |
208 | | u32 compressed_size; |
209 | | u32 uncompressed_size; |
210 | | u16 name_length; |
211 | | u16 extra_data_length; |
212 | | u8 const* name; |
213 | | u8 const* extra_data; |
214 | | u8 const* compressed_data; |
215 | | |
216 | | bool read(ReadonlyBytes buffer) |
217 | 7.13k | { |
218 | 7.13k | constexpr auto fields_size = sizeof(LocalFileHeader) - (sizeof(u8*) * 3); |
219 | 7.13k | if (!read_helper<fields_size>(buffer, this)) |
220 | 24 | return false; |
221 | 7.10k | if (buffer.size() < signature.size() + fields_size + name_length + extra_data_length + compressed_size) |
222 | 29 | return false; |
223 | 7.07k | name = buffer.data() + signature.size() + fields_size; |
224 | 7.07k | extra_data = name + name_length; |
225 | 7.07k | compressed_data = extra_data + extra_data_length; |
226 | 7.07k | return true; |
227 | 7.10k | } |
228 | | |
229 | | ErrorOr<void> write(Stream& stream) const |
230 | 0 | { |
231 | 0 | auto write_value = [&stream](auto value) { |
232 | 0 | return stream.write_until_depleted({ &value, sizeof(value) }); |
233 | 0 | }; Unexecuted instantiation: auto Archive::LocalFileHeader::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned short>(unsigned short) constUnexecuted instantiation: auto Archive::LocalFileHeader::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedTime>(AK::DOSPackedTime) constUnexecuted instantiation: auto Archive::LocalFileHeader::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedDate>(AK::DOSPackedDate) constUnexecuted instantiation: auto Archive::LocalFileHeader::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned int>(unsigned int) const |
234 | |
|
235 | 0 | TRY(stream.write_until_depleted(signature)); |
236 | 0 | TRY(write_value(minimum_version)); |
237 | 0 | TRY(write_value(general_purpose_flags.flags)); |
238 | 0 | TRY(write_value(compression_method)); |
239 | 0 | TRY(write_value(modification_time)); |
240 | 0 | TRY(write_value(modification_date)); |
241 | 0 | TRY(write_value(crc32)); |
242 | 0 | TRY(write_value(compressed_size)); |
243 | 0 | TRY(write_value(uncompressed_size)); |
244 | 0 | TRY(write_value(name_length)); |
245 | 0 | TRY(write_value(extra_data_length)); |
246 | 0 | if (name_length > 0) |
247 | 0 | TRY(stream.write_until_depleted({ name, name_length })); |
248 | 0 | if (extra_data_length > 0) |
249 | 0 | TRY(stream.write_until_depleted({ extra_data, extra_data_length })); |
250 | 0 | if (compressed_size > 0) |
251 | 0 | TRY(stream.write_until_depleted({ compressed_data, compressed_size })); |
252 | 0 | return {}; |
253 | 0 | } |
254 | | }; |
255 | | |
256 | | struct ZipMember { |
257 | | String name; |
258 | | ReadonlyBytes compressed_data; // TODO: maybe the decompression/compression should be handled by LibArchive instead of the user? |
259 | | ZipCompressionMethod compression_method; |
260 | | u32 uncompressed_size; |
261 | | u32 crc32; |
262 | | bool is_directory; |
263 | | DOSPackedTime modification_time; |
264 | | DOSPackedDate modification_date; |
265 | | Optional<mode_t> mode; |
266 | | }; |
267 | | |
268 | | class Zip { |
269 | | public: |
270 | | static Optional<Zip> try_create(ReadonlyBytes buffer); |
271 | | ErrorOr<bool> for_each_member(Function<ErrorOr<IterationDecision>(ZipMember const&)>) const; |
272 | | ErrorOr<Statistics> calculate_statistics() const; |
273 | | |
274 | | private: |
275 | | static bool find_end_of_central_directory_offset(ReadonlyBytes, size_t& offset); |
276 | | |
277 | | Zip(u16 member_count, size_t members_start_offset, ReadonlyBytes input_data) |
278 | 386 | : m_member_count { member_count } |
279 | 386 | , m_members_start_offset { members_start_offset } |
280 | 386 | , m_input_data { input_data } |
281 | 386 | { |
282 | 386 | } |
283 | | u16 m_member_count { 0 }; |
284 | | size_t m_members_start_offset { 0 }; |
285 | | ReadonlyBytes m_input_data; |
286 | | }; |
287 | | |
288 | | class ZipOutputStream { |
289 | | public: |
290 | | struct MemberInformation { |
291 | | float compression_ratio; |
292 | | size_t compressed_size; |
293 | | }; |
294 | | |
295 | | ZipOutputStream(NonnullOwnPtr<Stream>); |
296 | | |
297 | | ErrorOr<void> add_member(ZipMember const&); |
298 | | ErrorOr<MemberInformation> add_member_from_stream(StringView, Stream&, Optional<Core::DateTime> const& = {}, Optional<mode_t> mode = {}); |
299 | | |
300 | | // NOTE: This does not add any of the files within the directory, |
301 | | // it just adds an entry for it. |
302 | | ErrorOr<void> add_directory(StringView, Optional<Core::DateTime> const& = {}, Optional<mode_t> mode = {}); |
303 | | |
304 | | ErrorOr<void> finish(); |
305 | | |
306 | | private: |
307 | | NonnullOwnPtr<Stream> m_stream; |
308 | | Vector<ZipMember> m_members; |
309 | | |
310 | | bool m_finished { false }; |
311 | | }; |
312 | | |
313 | | } |