Serenity Operating System
1/*
2 * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <LibArchive/Zip.h>
9
10namespace Archive {
11
12bool Zip::find_end_of_central_directory_offset(ReadonlyBytes buffer, size_t& offset)
13{
14 for (size_t backwards_offset = 0; backwards_offset <= UINT16_MAX; backwards_offset++) // the file may have a trailing comment of an arbitrary 16 bit length
15 {
16 if (buffer.size() < (sizeof(EndOfCentralDirectory) - sizeof(u8*)) + backwards_offset)
17 return false;
18
19 auto const signature_offset = (buffer.size() - (sizeof(EndOfCentralDirectory) - sizeof(u8*)) - backwards_offset);
20 if (auto signature = ReadonlyBytes { buffer.data() + signature_offset, EndOfCentralDirectory::signature.size() };
21 signature == EndOfCentralDirectory::signature) {
22 offset = signature_offset;
23 return true;
24 }
25 }
26 return false;
27}
28
29Optional<Zip> Zip::try_create(ReadonlyBytes buffer)
30{
31 size_t end_of_central_directory_offset;
32 if (!find_end_of_central_directory_offset(buffer, end_of_central_directory_offset))
33 return {};
34
35 EndOfCentralDirectory end_of_central_directory {};
36 if (!end_of_central_directory.read(buffer.slice(end_of_central_directory_offset)))
37 return {};
38
39 if (end_of_central_directory.disk_number != 0 || end_of_central_directory.central_directory_start_disk != 0 || end_of_central_directory.disk_records_count != end_of_central_directory.total_records_count)
40 return {}; // TODO: support multi-volume zip archives
41
42 size_t member_offset = end_of_central_directory.central_directory_offset;
43 for (size_t i = 0; i < end_of_central_directory.total_records_count; i++) {
44 CentralDirectoryRecord central_directory_record {};
45 if (member_offset > buffer.size())
46 return {};
47 if (!central_directory_record.read(buffer.slice(member_offset)))
48 return {};
49 if (central_directory_record.general_purpose_flags.encrypted)
50 return {}; // TODO: support encrypted zip members
51 if (central_directory_record.general_purpose_flags.data_descriptor)
52 return {}; // TODO: support zip data descriptors
53 if (central_directory_record.compression_method != ZipCompressionMethod::Store && central_directory_record.compression_method != ZipCompressionMethod::Deflate)
54 return {}; // TODO: support obsolete zip compression methods
55 if (central_directory_record.compression_method == ZipCompressionMethod::Store && central_directory_record.uncompressed_size != central_directory_record.compressed_size)
56 return {};
57 if (central_directory_record.start_disk != 0)
58 return {}; // TODO: support multi-volume zip archives
59 if (memchr(central_directory_record.name, 0, central_directory_record.name_length) != nullptr)
60 return {};
61 LocalFileHeader local_file_header {};
62 if (central_directory_record.local_file_header_offset > buffer.size())
63 return {};
64 if (!local_file_header.read(buffer.slice(central_directory_record.local_file_header_offset)))
65 return {};
66 if (buffer.size() - (local_file_header.compressed_data - buffer.data()) < central_directory_record.compressed_size)
67 return {};
68 member_offset += central_directory_record.size();
69 }
70
71 return Zip {
72 end_of_central_directory.total_records_count,
73 end_of_central_directory.central_directory_offset,
74 buffer,
75 };
76}
77
78ErrorOr<bool> Zip::for_each_member(Function<IterationDecision(ZipMember const&)> callback)
79{
80 size_t member_offset = m_members_start_offset;
81 for (size_t i = 0; i < m_member_count; i++) {
82 CentralDirectoryRecord central_directory_record {};
83 VERIFY(central_directory_record.read(m_input_data.slice(member_offset)));
84 LocalFileHeader local_file_header {};
85 VERIFY(local_file_header.read(m_input_data.slice(central_directory_record.local_file_header_offset)));
86
87 ZipMember member;
88 member.name = TRY(String::from_utf8({ central_directory_record.name, central_directory_record.name_length }));
89 member.compressed_data = { local_file_header.compressed_data, central_directory_record.compressed_size };
90 member.compression_method = central_directory_record.compression_method;
91 member.uncompressed_size = central_directory_record.uncompressed_size;
92 member.crc32 = central_directory_record.crc32;
93 member.modification_time = central_directory_record.modification_time;
94 member.modification_date = central_directory_record.modification_date;
95 member.is_directory = central_directory_record.external_attributes & zip_directory_external_attribute || member.name.bytes_as_string_view().ends_with('/'); // FIXME: better directory detection
96
97 if (callback(member) == IterationDecision::Break)
98 return false;
99
100 member_offset += central_directory_record.size();
101 }
102 return true;
103}
104
105ZipOutputStream::ZipOutputStream(NonnullOwnPtr<Stream> stream)
106 : m_stream(move(stream))
107{
108}
109
110static u16 minimum_version_needed(ZipCompressionMethod method)
111{
112 // Deflate was added in PKZip 2.0
113 return method == ZipCompressionMethod::Deflate ? 20 : 10;
114}
115
116ErrorOr<void> ZipOutputStream::add_member(ZipMember const& member)
117{
118 VERIFY(!m_finished);
119 VERIFY(member.name.bytes_as_string_view().length() <= UINT16_MAX);
120 VERIFY(member.compressed_data.size() <= UINT32_MAX);
121 TRY(m_members.try_append(member));
122
123 LocalFileHeader local_file_header {
124 .minimum_version = minimum_version_needed(member.compression_method),
125 .general_purpose_flags = { .flags = 0 },
126 .compression_method = static_cast<u16>(member.compression_method),
127 .modification_time = member.modification_time,
128 .modification_date = member.modification_date,
129 .crc32 = member.crc32,
130 .compressed_size = static_cast<u32>(member.compressed_data.size()),
131 .uncompressed_size = member.uncompressed_size,
132 .name_length = static_cast<u16>(member.name.bytes_as_string_view().length()),
133 .extra_data_length = 0,
134 .name = reinterpret_cast<u8 const*>(member.name.bytes_as_string_view().characters_without_null_termination()),
135 .extra_data = nullptr,
136 .compressed_data = member.compressed_data.data(),
137 };
138 return local_file_header.write(*m_stream);
139}
140
141ErrorOr<void> ZipOutputStream::finish()
142{
143 VERIFY(!m_finished);
144 m_finished = true;
145
146 auto file_header_offset = 0u;
147 auto central_directory_size = 0u;
148 for (ZipMember const& member : m_members) {
149 auto zip_version = minimum_version_needed(member.compression_method);
150 CentralDirectoryRecord central_directory_record {
151 .made_by_version = zip_version,
152 .minimum_version = zip_version,
153 .general_purpose_flags = { .flags = 0 },
154 .compression_method = member.compression_method,
155 .modification_time = member.modification_time,
156 .modification_date = member.modification_date,
157 .crc32 = member.crc32,
158 .compressed_size = static_cast<u32>(member.compressed_data.size()),
159 .uncompressed_size = member.uncompressed_size,
160 .name_length = static_cast<u16>(member.name.bytes_as_string_view().length()),
161 .extra_data_length = 0,
162 .comment_length = 0,
163 .start_disk = 0,
164 .internal_attributes = 0,
165 .external_attributes = member.is_directory ? zip_directory_external_attribute : 0,
166 .local_file_header_offset = file_header_offset, // FIXME: we assume the wrapped output stream was never written to before us
167 .name = reinterpret_cast<u8 const*>(member.name.bytes_as_string_view().characters_without_null_termination()),
168 .extra_data = nullptr,
169 .comment = nullptr,
170 };
171 file_header_offset += sizeof(LocalFileHeader::signature) + (sizeof(LocalFileHeader) - (sizeof(u8*) * 3)) + member.name.bytes_as_string_view().length() + member.compressed_data.size();
172 TRY(central_directory_record.write(*m_stream));
173 central_directory_size += central_directory_record.size();
174 }
175
176 EndOfCentralDirectory end_of_central_directory {
177 .disk_number = 0,
178 .central_directory_start_disk = 0,
179 .disk_records_count = static_cast<u16>(m_members.size()),
180 .total_records_count = static_cast<u16>(m_members.size()),
181 .central_directory_size = central_directory_size,
182 .central_directory_offset = file_header_offset,
183 .comment_length = 0,
184 .comment = nullptr,
185 };
186 return end_of_central_directory.write(*m_stream);
187}
188
189}