diff options
| author | Adam Lesinski <adamlesinski@google.com> | 2015-10-05 18:16:18 -0700 |
|---|---|---|
| committer | Adam Lesinski <adamlesinski@google.com> | 2015-10-15 16:27:44 -0700 |
| commit | ad4ad8cfc8c1eada8356d554eeb0639ee48ef00d (patch) | |
| tree | a05500fa9edfa366c0bda041e4f7a6c53e94c7a2 | |
| parent | ca0d66d597dde0a76adc3f5da15aedff4b6cb70b (diff) | |
Implement ZipWriter for quickly writing ZipFiles.
The ZipWriter implementation exposes a stateful interface that allows
bytes of data to be streamed in as they arrive. ZipEntries can be
compressed and/or aligned on a 32-bit boundary for mmapping at runtime.
Change-Id: I43ac9e661aa5022f00d9e12b247c4314d61c441c
| -rw-r--r-- | include/ziparchive/zip_writer.h | 141 | ||||
| -rw-r--r-- | libziparchive/Android.mk | 22 | ||||
| -rw-r--r-- | libziparchive/zip_archive.cc | 156 | ||||
| -rw-r--r-- | libziparchive/zip_archive_common.h | 179 | ||||
| -rw-r--r-- | libziparchive/zip_writer.cc | 262 | ||||
| -rw-r--r-- | libziparchive/zip_writer_test.cc | 140 |
6 files changed, 738 insertions, 162 deletions
diff --git a/include/ziparchive/zip_writer.h b/include/ziparchive/zip_writer.h new file mode 100644 index 0000000000..9ee4abec29 --- /dev/null +++ b/include/ziparchive/zip_writer.h @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBZIPARCHIVE_ZIPWRITER_H_ +#define LIBZIPARCHIVE_ZIPWRITER_H_ + +#include "base/macros.h" +#include <utils/Compat.h> + +#include <cstdio> +#include <string> +#include <ctime> +#include <vector> + +/** + * Writes a Zip file via a stateful interface. + * + * Example: + * + * FILE* file = fopen("path/to/zip.zip", "wb"); + * + * ZipWriter writer(file); + * + * writer.StartEntry("test.txt", ZipWriter::kCompress | ZipWriter::kAlign); + * writer.WriteBytes(buffer, bufferLen); + * writer.WriteBytes(buffer2, bufferLen2); + * writer.FinishEntry(); + * + * writer.StartEntry("empty.txt", 0); + * writer.FinishEntry(); + * + * writer.Finish(); + * + * fclose(file); + */ +class ZipWriter { +public: + enum { + /** + * Flag to compress the zip entry using deflate. + */ + kCompress = 0x01, + + /** + * Flag to align the zip entry data on a 32bit boundary. Useful for + * mmapping the data at runtime. + */ + kAlign32 = 0x02, + }; + + static const char* ErrorCodeString(int32_t error_code); + + /** + * Create a ZipWriter that will write into a FILE stream. The file should be opened with + * open mode of "wb" or "w+b". ZipWriter does not take ownership of the file stream. The + * caller is responsible for closing the file. + */ + explicit ZipWriter(FILE* f); + + // Move constructor. + ZipWriter(ZipWriter&& zipWriter); + + // Move assignment. + ZipWriter& operator=(ZipWriter&& zipWriter); + + /** + * Starts a new zip entry with the given path and flags. + * Flags can be a bitwise OR of ZipWriter::kCompress and ZipWriter::kAlign. + * Subsequent calls to WriteBytes(const void*, size_t) will add data to this entry. + * Returns 0 on success, and an error value < 0 on failure. + */ + int32_t StartEntry(const char* path, size_t flags); + + /** + * Same as StartEntry(const char*, size_t), but sets a last modified time for the entry. + */ + int32_t StartEntryWithTime(const char* path, size_t flags, time_t time); + + /** + * Writes bytes to the zip file for the previously started zip entry. + * Returns 0 on success, and an error value < 0 on failure. + */ + int32_t WriteBytes(const void* data, size_t len); + + /** + * Finish a zip entry started with StartEntry(const char*, size_t) or + * StartEntryWithTime(const char*, size_t, time_t). This must be called before + * any new zip entries are started, or before Finish() is called. + * Returns 0 on success, and an error value < 0 on failure. + */ + int32_t FinishEntry(); + + /** + * Writes the Central Directory Headers and flushes the zip file stream. + * Returns 0 on success, and an error value < 0 on failure. + */ + int32_t Finish(); + +private: + DISALLOW_COPY_AND_ASSIGN(ZipWriter); + + int32_t HandleError(int32_t error_code); + + struct FileInfo { + std::string path; + uint16_t compression_method; + uint32_t crc32; + uint32_t compressed_size; + uint32_t uncompressed_size; + uint16_t last_mod_time; + uint16_t last_mod_date; + uint32_t local_file_header_offset; + }; + + enum class State { + kWritingZip, + kWritingEntry, + kDone, + kError, + }; + + FILE* file_; + off64_t current_offset_; + State state_; + std::vector<FileInfo> files_; +}; + +#endif /* LIBZIPARCHIVE_ZIPWRITER_H_ */ diff --git a/libziparchive/Android.mk b/libziparchive/Android.mk index 608ff1cc0c..8ff94d4122 100644 --- a/libziparchive/Android.mk +++ b/libziparchive/Android.mk @@ -15,7 +15,12 @@ LOCAL_PATH := $(call my-dir) -source_files := zip_archive.cc +source_files := zip_archive.cc zip_writer.cc +test_files := zip_archive_test.cc zip_writer_test.cc entry_name_utils_test.cc + +# Incorrectly warns when C++11 empty brace {} initializer is used. +# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61489 +common_cpp_flags := -Wno-missing-field-initializers include $(CLEAR_VARS) LOCAL_CPP_EXTENSION := .cc @@ -24,7 +29,7 @@ LOCAL_STATIC_LIBRARIES := libz LOCAL_SHARED_LIBRARIES := libutils libbase LOCAL_MODULE:= libziparchive LOCAL_CFLAGS := -Werror -Wall -LOCAL_CPPFLAGS := -Wold-style-cast +LOCAL_CPPFLAGS := -Wold-style-cast $(common_cpp_flags) include $(BUILD_STATIC_LIBRARY) include $(CLEAR_VARS) @@ -34,6 +39,8 @@ LOCAL_STATIC_LIBRARIES := libz libutils libbase LOCAL_MODULE:= libziparchive-host LOCAL_CFLAGS := -Werror LOCAL_CFLAGS_windows := -mno-ms-bitfields +LOCAL_CPPFLAGS := $(common_cpp_flags) + LOCAL_MULTILIB := both LOCAL_MODULE_HOST_OS := darwin linux windows include $(BUILD_HOST_STATIC_LIBRARY) @@ -45,6 +52,7 @@ LOCAL_STATIC_LIBRARIES := libutils LOCAL_SHARED_LIBRARIES := libz-host liblog libbase LOCAL_MODULE:= libziparchive-host LOCAL_CFLAGS := -Werror +LOCAL_CPPFLAGS := $(common_cpp_flags) LOCAL_MULTILIB := both include $(BUILD_HOST_SHARED_LIBRARY) @@ -53,7 +61,8 @@ include $(CLEAR_VARS) LOCAL_MODULE := ziparchive-tests LOCAL_CPP_EXTENSION := .cc LOCAL_CFLAGS := -Werror -LOCAL_SRC_FILES := zip_archive_test.cc entry_name_utils_test.cc +LOCAL_CPPFLAGS := $(common_cpp_flags) +LOCAL_SRC_FILES := $(test_files) LOCAL_SHARED_LIBRARIES := liblog libbase LOCAL_STATIC_LIBRARIES := libziparchive libz libutils include $(BUILD_NATIVE_TEST) @@ -61,10 +70,9 @@ include $(BUILD_NATIVE_TEST) include $(CLEAR_VARS) LOCAL_MODULE := ziparchive-tests-host LOCAL_CPP_EXTENSION := .cc -LOCAL_CFLAGS += \ - -Werror \ - -Wno-unnamed-type-template-args -LOCAL_SRC_FILES := zip_archive_test.cc entry_name_utils_test.cc +LOCAL_CFLAGS := -Werror +LOCAL_CPPFLAGS := -Wno-unnamed-type-template-args $(common_cpp_flags) +LOCAL_SRC_FILES := $(test_files) LOCAL_SHARED_LIBRARIES := libziparchive-host liblog libbase LOCAL_STATIC_LIBRARIES := \ libz \ diff --git a/libziparchive/zip_archive.cc b/libziparchive/zip_archive.cc index 3716343aee..f1e13a7bc2 100644 --- a/libziparchive/zip_archive.cc +++ b/libziparchive/zip_archive.cc @@ -39,6 +39,7 @@ #include "zlib.h" #include "entry_name_utils-inl.h" +#include "zip_archive_common.h" #include "ziparchive/zip_archive.h" using android::base::get_unaligned; @@ -49,161 +50,6 @@ using android::base::get_unaligned; #define O_BINARY 0 #endif -// The "end of central directory" (EOCD) record. Each archive -// contains exactly once such record which appears at the end of -// the archive. It contains archive wide information like the -// number of entries in the archive and the offset to the central -// directory of the offset. -struct EocdRecord { - static const uint32_t kSignature = 0x06054b50; - - // End of central directory signature, should always be - // |kSignature|. - uint32_t eocd_signature; - // The number of the current "disk", i.e, the "disk" that this - // central directory is on. - // - // This implementation assumes that each archive spans a single - // disk only. i.e, that disk_num == 1. - uint16_t disk_num; - // The disk where the central directory starts. - // - // This implementation assumes that each archive spans a single - // disk only. i.e, that cd_start_disk == 1. - uint16_t cd_start_disk; - // The number of central directory records on this disk. - // - // This implementation assumes that each archive spans a single - // disk only. i.e, that num_records_on_disk == num_records. - uint16_t num_records_on_disk; - // The total number of central directory records. - uint16_t num_records; - // The size of the central directory (in bytes). - uint32_t cd_size; - // The offset of the start of the central directory, relative - // to the start of the file. - uint32_t cd_start_offset; - // Length of the central directory comment. - uint16_t comment_length; - private: - EocdRecord() = default; - DISALLOW_COPY_AND_ASSIGN(EocdRecord); -} __attribute__((packed)); - -// A structure representing the fixed length fields for a single -// record in the central directory of the archive. In addition to -// the fixed length fields listed here, each central directory -// record contains a variable length "file_name" and "extra_field" -// whose lengths are given by |file_name_length| and |extra_field_length| -// respectively. -struct CentralDirectoryRecord { - static const uint32_t kSignature = 0x02014b50; - - // The start of record signature. Must be |kSignature|. - uint32_t record_signature; - // Tool version. Ignored by this implementation. - uint16_t version_made_by; - // Tool version. Ignored by this implementation. - uint16_t version_needed; - // The "general purpose bit flags" for this entry. The only - // flag value that we currently check for is the "data descriptor" - // flag. - uint16_t gpb_flags; - // The compression method for this entry, one of |kCompressStored| - // and |kCompressDeflated|. - uint16_t compression_method; - // The file modification time and date for this entry. - uint16_t last_mod_time; - uint16_t last_mod_date; - // The CRC-32 checksum for this entry. - uint32_t crc32; - // The compressed size (in bytes) of this entry. - uint32_t compressed_size; - // The uncompressed size (in bytes) of this entry. - uint32_t uncompressed_size; - // The length of the entry file name in bytes. The file name - // will appear immediately after this record. - uint16_t file_name_length; - // The length of the extra field info (in bytes). This data - // will appear immediately after the entry file name. - uint16_t extra_field_length; - // The length of the entry comment (in bytes). This data will - // appear immediately after the extra field. - uint16_t comment_length; - // The start disk for this entry. Ignored by this implementation). - uint16_t file_start_disk; - // File attributes. Ignored by this implementation. - uint16_t internal_file_attributes; - // File attributes. Ignored by this implementation. - uint32_t external_file_attributes; - // The offset to the local file header for this entry, from the - // beginning of this archive. - uint32_t local_file_header_offset; - private: - CentralDirectoryRecord() = default; - DISALLOW_COPY_AND_ASSIGN(CentralDirectoryRecord); -} __attribute__((packed)); - -// The local file header for a given entry. This duplicates information -// present in the central directory of the archive. It is an error for -// the information here to be different from the central directory -// information for a given entry. -struct LocalFileHeader { - static const uint32_t kSignature = 0x04034b50; - - // The local file header signature, must be |kSignature|. - uint32_t lfh_signature; - // Tool version. Ignored by this implementation. - uint16_t version_needed; - // The "general purpose bit flags" for this entry. The only - // flag value that we currently check for is the "data descriptor" - // flag. - uint16_t gpb_flags; - // The compression method for this entry, one of |kCompressStored| - // and |kCompressDeflated|. - uint16_t compression_method; - // The file modification time and date for this entry. - uint16_t last_mod_time; - uint16_t last_mod_date; - // The CRC-32 checksum for this entry. - uint32_t crc32; - // The compressed size (in bytes) of this entry. - uint32_t compressed_size; - // The uncompressed size (in bytes) of this entry. - uint32_t uncompressed_size; - // The length of the entry file name in bytes. The file name - // will appear immediately after this record. - uint16_t file_name_length; - // The length of the extra field info (in bytes). This data - // will appear immediately after the entry file name. - uint16_t extra_field_length; - private: - LocalFileHeader() = default; - DISALLOW_COPY_AND_ASSIGN(LocalFileHeader); -} __attribute__((packed)); - -struct DataDescriptor { - // The *optional* data descriptor start signature. - static const uint32_t kOptSignature = 0x08074b50; - - // CRC-32 checksum of the entry. - uint32_t crc32; - // Compressed size of the entry. - uint32_t compressed_size; - // Uncompressed size of the entry. - uint32_t uncompressed_size; - private: - DataDescriptor() = default; - DISALLOW_COPY_AND_ASSIGN(DataDescriptor); -} __attribute__((packed)); - - -static const uint32_t kGPBDDFlagMask = 0x0008; // mask value that signifies that the entry has a DD - -// The maximum size of a central directory or a file -// comment in bytes. -static const uint32_t kMaxCommentLen = 65535; - // The maximum number of bytes to scan backwards for the EOCD start. static const uint32_t kMaxEOCDSearch = kMaxCommentLen + sizeof(EocdRecord); diff --git a/libziparchive/zip_archive_common.h b/libziparchive/zip_archive_common.h new file mode 100644 index 0000000000..7f20d51c64 --- /dev/null +++ b/libziparchive/zip_archive_common.h @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBZIPARCHIVE_ZIPARCHIVECOMMON_H_ +#define LIBZIPARCHIVE_ZIPARCHIVECOMMON_H_ + +#include "base/macros.h" + +#include <inttypes.h> + +// The "end of central directory" (EOCD) record. Each archive +// contains exactly once such record which appears at the end of +// the archive. It contains archive wide information like the +// number of entries in the archive and the offset to the central +// directory of the offset. +struct EocdRecord { + static const uint32_t kSignature = 0x06054b50; + + // End of central directory signature, should always be + // |kSignature|. + uint32_t eocd_signature; + // The number of the current "disk", i.e, the "disk" that this + // central directory is on. + // + // This implementation assumes that each archive spans a single + // disk only. i.e, that disk_num == 1. + uint16_t disk_num; + // The disk where the central directory starts. + // + // This implementation assumes that each archive spans a single + // disk only. i.e, that cd_start_disk == 1. + uint16_t cd_start_disk; + // The number of central directory records on this disk. + // + // This implementation assumes that each archive spans a single + // disk only. i.e, that num_records_on_disk == num_records. + uint16_t num_records_on_disk; + // The total number of central directory records. + uint16_t num_records; + // The size of the central directory (in bytes). + uint32_t cd_size; + // The offset of the start of the central directory, relative + // to the start of the file. + uint32_t cd_start_offset; + // Length of the central directory comment. + uint16_t comment_length; + private: + EocdRecord() = default; + DISALLOW_COPY_AND_ASSIGN(EocdRecord); +} __attribute__((packed)); + +// A structure representing the fixed length fields for a single +// record in the central directory of the archive. In addition to +// the fixed length fields listed here, each central directory +// record contains a variable length "file_name" and "extra_field" +// whose lengths are given by |file_name_length| and |extra_field_length| +// respectively. +struct CentralDirectoryRecord { + static const uint32_t kSignature = 0x02014b50; + + // The start of record signature. Must be |kSignature|. + uint32_t record_signature; + // Tool version. Ignored by this implementation. + uint16_t version_made_by; + // Tool version. Ignored by this implementation. + uint16_t version_needed; + // The "general purpose bit flags" for this entry. The only + // flag value that we currently check for is the "data descriptor" + // flag. + uint16_t gpb_flags; + // The compression method for this entry, one of |kCompressStored| + // and |kCompressDeflated|. + uint16_t compression_method; + // The file modification time and date for this entry. + uint16_t last_mod_time; + uint16_t last_mod_date; + // The CRC-32 checksum for this entry. + uint32_t crc32; + // The compressed size (in bytes) of this entry. + uint32_t compressed_size; + // The uncompressed size (in bytes) of this entry. + uint32_t uncompressed_size; + // The length of the entry file name in bytes. The file name + // will appear immediately after this record. + uint16_t file_name_length; + // The length of the extra field info (in bytes). This data + // will appear immediately after the entry file name. + uint16_t extra_field_length; + // The length of the entry comment (in bytes). This data will + // appear immediately after the extra field. + uint16_t comment_length; + // The start disk for this entry. Ignored by this implementation). + uint16_t file_start_disk; + // File attributes. Ignored by this implementation. + uint16_t internal_file_attributes; + // File attributes. Ignored by this implementation. + uint32_t external_file_attributes; + // The offset to the local file header for this entry, from the + // beginning of this archive. + uint32_t local_file_header_offset; + private: + CentralDirectoryRecord() = default; + DISALLOW_COPY_AND_ASSIGN(CentralDirectoryRecord); +} __attribute__((packed)); + +// The local file header for a given entry. This duplicates information +// present in the central directory of the archive. It is an error for +// the information here to be different from the central directory +// information for a given entry. +struct LocalFileHeader { + static const uint32_t kSignature = 0x04034b50; + + // The local file header signature, must be |kSignature|. + uint32_t lfh_signature; + // Tool version. Ignored by this implementation. + uint16_t version_needed; + // The "general purpose bit flags" for this entry. The only + // flag value that we currently check for is the "data descriptor" + // flag. + uint16_t gpb_flags; + // The compression method for this entry, one of |kCompressStored| + // and |kCompressDeflated|. + uint16_t compression_method; + // The file modification time and date for this entry. + uint16_t last_mod_time; + uint16_t last_mod_date; + // The CRC-32 checksum for this entry. + uint32_t crc32; + // The compressed size (in bytes) of this entry. + uint32_t compressed_size; + // The uncompressed size (in bytes) of this entry. + uint32_t uncompressed_size; + // The length of the entry file name in bytes. The file name + // will appear immediately after this record. + uint16_t file_name_length; + // The length of the extra field info (in bytes). This data + // will appear immediately after the entry file name. + uint16_t extra_field_length; + private: + LocalFileHeader() = default; + DISALLOW_COPY_AND_ASSIGN(LocalFileHeader); +} __attribute__((packed)); + +struct DataDescriptor { + // The *optional* data descriptor start signature. + static const uint32_t kOptSignature = 0x08074b50; + + // CRC-32 checksum of the entry. + uint32_t crc32; + // Compressed size of the entry. + uint32_t compressed_size; + // Uncompressed size of the entry. + uint32_t uncompressed_size; + private: + DataDescriptor() = default; + DISALLOW_COPY_AND_ASSIGN(DataDescriptor); +} __attribute__((packed)); + +// mask value that signifies that the entry has a DD +static const uint32_t kGPBDDFlagMask = 0x0008; + +// The maximum size of a central directory or a file +// comment in bytes. +static const uint32_t kMaxCommentLen = 65535; + +#endif /* LIBZIPARCHIVE_ZIPARCHIVECOMMON_H_ */ diff --git a/libziparchive/zip_writer.cc b/libziparchive/zip_writer.cc new file mode 100644 index 0000000000..de75d1edeb --- /dev/null +++ b/libziparchive/zip_writer.cc @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "entry_name_utils-inl.h" +#include "zip_archive_common.h" +#include "ziparchive/zip_writer.h" + +#include <cassert> +#include <cstdio> +#include <memory> +#include <zlib.h> + +/* Zip compression methods we support */ +enum { + kCompressStored = 0, // no compression + kCompressDeflated = 8, // standard deflate +}; + +// No error, operation completed successfully. +static const int32_t kNoError = 0; + +// The ZipWriter is in a bad state. +static const int32_t kInvalidState = -1; + +// There was an IO error while writing to disk. +static const int32_t kIoError = -2; + +// The zip entry name was invalid. +static const int32_t kInvalidEntryName = -3; + +static const char* sErrorCodes[] = { + "Invalid state", + "IO error", + "Invalid entry name", +}; + +const char* ZipWriter::ErrorCodeString(int32_t error_code) { + if (error_code < 0 && (-error_code) < static_cast<int32_t>(arraysize(sErrorCodes))) { + return sErrorCodes[-error_code]; + } + return nullptr; +} + +ZipWriter::ZipWriter(FILE* f) : file_(f), current_offset_(0), state_(State::kWritingZip) { +} + +ZipWriter::ZipWriter(ZipWriter&& writer) : file_(writer.file_), + current_offset_(writer.current_offset_), + state_(writer.state_), + files_(std::move(writer.files_)) { + writer.file_ = nullptr; + writer.state_ = State::kError; +} + +ZipWriter& ZipWriter::operator=(ZipWriter&& writer) { + file_ = writer.file_; + current_offset_ = writer.current_offset_; + state_ = writer.state_; + files_ = std::move(writer.files_); + writer.file_ = nullptr; + writer.state_ = State::kError; + return *this; +} + +int32_t ZipWriter::HandleError(int32_t error_code) { + state_ = State::kError; + return error_code; +} + +int32_t ZipWriter::StartEntry(const char* path, size_t flags) { + return StartEntryWithTime(path, flags, time_t()); +} + +static void ExtractTimeAndDate(time_t when, uint16_t* out_time, uint16_t* out_date) { + /* round up to an even number of seconds */ + when = static_cast<time_t>((static_cast<unsigned long>(when) + 1) & (~1)); + + struct tm* ptm; +#if !defined(_WIN32) + struct tm tm_result; + ptm = localtime_r(&when, &tm_result); +#else + ptm = localtime(&when); +#endif + + int year = ptm->tm_year; + if (year < 80) { + year = 80; + } + + *out_date = (year - 80) << 9 | (ptm->tm_mon + 1) << 5 | ptm->tm_mday; + *out_time = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1; +} + +int32_t ZipWriter::StartEntryWithTime(const char* path, size_t flags, time_t time) { + if (state_ != State::kWritingZip) { + return kInvalidState; + } + + FileInfo fileInfo = {}; + fileInfo.path = std::string(path); + fileInfo.local_file_header_offset = current_offset_; + + if (!IsValidEntryName(reinterpret_cast<const uint8_t*>(fileInfo.path.data()), + fileInfo.path.size())) { + return kInvalidEntryName; + } + + LocalFileHeader header = {}; + header.lfh_signature = LocalFileHeader::kSignature; + + // Set this flag to denote that a DataDescriptor struct will appear after the data, + // containing the crc and size fields. + header.gpb_flags |= kGPBDDFlagMask; + + // For now, ignore the ZipWriter::kCompress flag. + fileInfo.compression_method = kCompressStored; + header.compression_method = fileInfo.compression_method; + + ExtractTimeAndDate(time, &fileInfo.last_mod_time, &fileInfo.last_mod_date); + header.last_mod_time = fileInfo.last_mod_time; + header.last_mod_date = fileInfo.last_mod_date; + + header.file_name_length = fileInfo.path.size(); + + off64_t offset = current_offset_ + sizeof(header) + fileInfo.path.size(); + if ((flags & ZipWriter::kAlign32) && (offset & 0x03)) { + // Pad the extra field so the data will be aligned. + uint16_t padding = 4 - (offset % 4); + header.extra_field_length = padding; + offset += padding; + } + + if (fwrite(&header, sizeof(header), 1, file_) != 1) { + return HandleError(kIoError); + } + + if (fwrite(path, sizeof(*path), fileInfo.path.size(), file_) != fileInfo.path.size()) { + return HandleError(kIoError); + } + + if (fwrite("\0\0\0", 1, header.extra_field_length, file_) != header.extra_field_length) { + return HandleError(kIoError); + } + + files_.emplace_back(std::move(fileInfo)); + + current_offset_ = offset; + state_ = State::kWritingEntry; + return kNoError; +} + +int32_t ZipWriter::WriteBytes(const void* data, size_t len) { + if (state_ != State::kWritingEntry) { + return HandleError(kInvalidState); + } + + FileInfo& currentFile = files_.back(); + if (currentFile.compression_method & kCompressDeflated) { + // TODO(adamlesinski): Implement compression using zlib deflate. + assert(false); + } else { + if (fwrite(data, 1, len, file_) != len) { + return HandleError(kIoError); + } + currentFile.crc32 = crc32(currentFile.crc32, reinterpret_cast<const Bytef*>(data), len); + currentFile.compressed_size += len; + current_offset_ += len; + } + + currentFile.uncompressed_size += len; + return kNoError; +} + +int32_t ZipWriter::FinishEntry() { + if (state_ != State::kWritingEntry) { + return kInvalidState; + } + + const uint32_t sig = DataDescriptor::kOptSignature; + if (fwrite(&sig, sizeof(sig), 1, file_) != 1) { + state_ = State::kError; + return kIoError; + } + + FileInfo& currentFile = files_.back(); + DataDescriptor dd = {}; + dd.crc32 = currentFile.crc32; + dd.compressed_size = currentFile.compressed_size; + dd.uncompressed_size = currentFile.uncompressed_size; + if (fwrite(&dd, sizeof(dd), 1, file_) != 1) { + return HandleError(kIoError); + } + + current_offset_ += sizeof(DataDescriptor::kOptSignature) + sizeof(dd); + state_ = State::kWritingZip; + return kNoError; +} + +int32_t ZipWriter::Finish() { + if (state_ != State::kWritingZip) { + return kInvalidState; + } + + off64_t startOfCdr = current_offset_; + for (FileInfo& file : files_) { + CentralDirectoryRecord cdr = {}; + cdr.record_signature = CentralDirectoryRecord::kSignature; + cdr.gpb_flags |= kGPBDDFlagMask; + cdr.compression_method = file.compression_method; + cdr.last_mod_time = file.last_mod_time; + cdr.last_mod_date = file.last_mod_date; + cdr.crc32 = file.crc32; + cdr.compressed_size = file.compressed_size; + cdr.uncompressed_size = file.uncompressed_size; + cdr.file_name_length = file.path.size(); + cdr.local_file_header_offset = file.local_file_header_offset; + if (fwrite(&cdr, sizeof(cdr), 1, file_) != 1) { + return HandleError(kIoError); + } + + if (fwrite(file.path.data(), 1, file.path.size(), file_) != file.path.size()) { + return HandleError(kIoError); + } + + current_offset_ += sizeof(cdr) + file.path.size(); + } + + EocdRecord er = {}; + er.eocd_signature = EocdRecord::kSignature; + er.disk_num = 1; + er.cd_start_disk = 1; + er.num_records_on_disk = files_.size(); + er.num_records = files_.size(); + er.cd_size = current_offset_ - startOfCdr; + er.cd_start_offset = startOfCdr; + + if (fwrite(&er, sizeof(er), 1, file_) != 1) { + return HandleError(kIoError); + } + + if (fflush(file_) != 0) { + return HandleError(kIoError); + } + + current_offset_ += sizeof(er); + state_ = State::kDone; + return kNoError; +} diff --git a/libziparchive/zip_writer_test.cc b/libziparchive/zip_writer_test.cc new file mode 100644 index 0000000000..5269730d77 --- /dev/null +++ b/libziparchive/zip_writer_test.cc @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ziparchive/zip_archive.h" +#include "ziparchive/zip_writer.h" + +#include <base/test_utils.h> +#include <gtest/gtest.h> +#include <memory> + +struct zipwriter : public ::testing::Test { + TemporaryFile* temp_file_; + int fd_; + FILE* file_; + + void SetUp() override { + temp_file_ = new TemporaryFile(); + fd_ = temp_file_->fd; + file_ = fdopen(fd_, "w"); + ASSERT_NE(file_, nullptr); + } + + void TearDown() override { + fclose(file_); + delete temp_file_; + } +}; + +TEST_F(zipwriter, WriteUncompressedZipWithOneFile) { + ZipWriter writer(file_); + + const char* expected = "hello"; + + ASSERT_EQ(writer.StartEntry("file.txt", 0), 0); + ASSERT_EQ(writer.WriteBytes("he", 2), 0); + ASSERT_EQ(writer.WriteBytes("llo", 3), 0); + ASSERT_EQ(writer.FinishEntry(), 0); + ASSERT_EQ(writer.Finish(), 0); + + ASSERT_GE(lseek(fd_, 0, SEEK_SET), 0); + + ZipArchiveHandle handle; + ASSERT_EQ(OpenArchiveFd(fd_, "temp", &handle, false), 0); + + ZipEntry data; + ASSERT_EQ(FindEntry(handle, ZipString("file.txt"), &data), 0); + EXPECT_EQ(data.compressed_length, strlen(expected)); + EXPECT_EQ(data.uncompressed_length, strlen(expected)); + EXPECT_EQ(data.method, kCompressStored); + + char buffer[6]; + EXPECT_EQ(ExtractToMemory(handle, &data, reinterpret_cast<uint8_t*>(&buffer), sizeof(buffer)), + 0); + buffer[5] = 0; + + EXPECT_STREQ(expected, buffer); + + CloseArchive(handle); +} + +TEST_F(zipwriter, WriteUncompressedZipWithMultipleFiles) { + ZipWriter writer(file_); + + ASSERT_EQ(writer.StartEntry("file.txt", 0), 0); + ASSERT_EQ(writer.WriteBytes("he", 2), 0); + ASSERT_EQ(writer.FinishEntry(), 0); + + ASSERT_EQ(writer.StartEntry("file/file.txt", 0), 0); + ASSERT_EQ(writer.WriteBytes("llo", 3), 0); + ASSERT_EQ(writer.FinishEntry(), 0); + + ASSERT_EQ(writer.StartEntry("file/file2.txt", 0), 0); + ASSERT_EQ(writer.FinishEntry(), 0); + + ASSERT_EQ(writer.Finish(), 0); + + ASSERT_GE(lseek(fd_, 0, SEEK_SET), 0); + + ZipArchiveHandle handle; + ASSERT_EQ(OpenArchiveFd(fd_, "temp", &handle, false), 0); + + char buffer[4]; + ZipEntry data; + + ASSERT_EQ(FindEntry(handle, ZipString("file.txt"), &data), 0); + EXPECT_EQ(data.method, kCompressStored); + EXPECT_EQ(data.compressed_length, 2u); + EXPECT_EQ(data.uncompressed_length, 2u); + ASSERT_EQ(ExtractToMemory(handle, &data, reinterpret_cast<uint8_t*>(buffer), arraysize(buffer)), + 0); + buffer[2] = 0; + EXPECT_STREQ("he", buffer); + + ASSERT_EQ(FindEntry(handle, ZipString("file/file.txt"), &data), 0); + EXPECT_EQ(data.method, kCompressStored); + EXPECT_EQ(data.compressed_length, 3u); + EXPECT_EQ(data.uncompressed_length, 3u); + ASSERT_EQ(ExtractToMemory(handle, &data, reinterpret_cast<uint8_t*>(buffer), arraysize(buffer)), + 0); + buffer[3] = 0; + EXPECT_STREQ("llo", buffer); + + ASSERT_EQ(FindEntry(handle, ZipString("file/file2.txt"), &data), 0); + EXPECT_EQ(data.method, kCompressStored); + EXPECT_EQ(data.compressed_length, 0u); + EXPECT_EQ(data.uncompressed_length, 0u); + + CloseArchive(handle); +} + +TEST_F(zipwriter, WriteUncompressedZipWithAlignedFile) { + ZipWriter writer(file_); + + ASSERT_EQ(writer.StartEntry("align.txt", ZipWriter::kAlign32), 0); + ASSERT_EQ(writer.WriteBytes("he", 2), 0); + ASSERT_EQ(writer.FinishEntry(), 0); + ASSERT_EQ(writer.Finish(), 0); + + ASSERT_GE(lseek(fd_, 0, SEEK_SET), 0); + + ZipArchiveHandle handle; + ASSERT_EQ(OpenArchiveFd(fd_, "temp", &handle, false), 0); + + ZipEntry data; + ASSERT_EQ(FindEntry(handle, ZipString("align.txt"), &data), 0); + EXPECT_EQ(data.offset & 0x03, 0); +} |
