Serenity Operating System
at master 189 lines 8.9 kB view raw
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}