Coverage Report

Created: 2026-02-14 08:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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) const
Unexecuted 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}) const
Unexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned short>(unsigned short) const
Unexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<Archive::ZipCompressionMethod>(Archive::ZipCompressionMethod) const
Unexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedTime>(AK::DOSPackedTime) const
Unexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedDate>(AK::DOSPackedDate) const
Unexecuted instantiation: auto Archive::CentralDirectoryRecord::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<unsigned int>(unsigned int) const
Unexecuted 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) const
Unexecuted instantiation: auto Archive::LocalFileHeader::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedTime>(AK::DOSPackedTime) const
Unexecuted instantiation: auto Archive::LocalFileHeader::write(AK::Stream&) const::{lambda(auto:1)#1}::operator()<AK::DOSPackedDate>(AK::DOSPackedDate) const
Unexecuted 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
}