| // Copyright 2017-2020 The Verible Authors. | |
| // | |
| // 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 "common/util/simple_zip.h" | |
| #include <zlib.h> | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <cstdio> | |
| #include <memory> | |
| #include <string> | |
| #include "absl/strings/string_view.h" | |
| #include "third_party/portable_endian/portable_endian.h" | |
| namespace verible { | |
| namespace zip { | |
| ByteSource MemoryByteSource(absl::string_view input) { | |
| auto is_called = std::make_shared<bool>(false); | |
| return [is_called, input]() -> absl::string_view { | |
| if (!*is_called) { | |
| *is_called = true; | |
| return input; | |
| } | |
| return {}; | |
| }; | |
| } | |
| ByteSource FileByteSource(const char *filename) { | |
| FILE *f = fopen(filename, "rb"); | |
| if (!f) return nullptr; | |
| struct State { | |
| State(FILE *f) : file(f) {} | |
| ~State() { fclose(file); } | |
| FILE *file; | |
| char buffer[65536]; | |
| }; | |
| auto state = std::make_shared<State>(f); // capture req. copy-constructable | |
| return [state]() -> absl::string_view { | |
| size_t r = fread(state->buffer, 1, sizeof(State::buffer), state->file); | |
| return {state->buffer, r}; | |
| }; | |
| } | |
| // Header writer, taking care of little-endianness writing of fields. | |
| namespace { | |
| class HeaderWriter { | |
| public: | |
| HeaderWriter(char *buffer) : begin_(buffer), pos_(buffer) {} | |
| HeaderWriter &AddInt16(uint16_t value) { | |
| uint16_t le = htole16(value); // NOP on most platforms | |
| memcpy(pos_, &le, 2); | |
| pos_ += 2; | |
| return *this; | |
| } | |
| HeaderWriter &AddInt32(uint32_t value) { | |
| uint32_t le = htole32(value); // NOP on most platforms | |
| memcpy(pos_, &le, 4); | |
| pos_ += 4; | |
| return *this; | |
| } | |
| HeaderWriter &AddLiteral(absl::string_view str) { | |
| memcpy(pos_, str.data(), str.size()); | |
| pos_ += str.size(); | |
| return *this; | |
| } | |
| bool Write(const ByteSink &out) { | |
| return out({begin_, static_cast<size_t>(pos_ - begin_)}); | |
| } | |
| void Append(std::string *buffer) { buffer->append(begin_, pos_ - begin_); } | |
| private: | |
| const char *begin_; | |
| char *pos_; | |
| }; | |
| } // namespace | |
| struct Encoder::Impl { | |
| static constexpr int16_t kPkZipVersion = 20; // 2.0, pretty basic. | |
| struct CompressResult { | |
| uint32_t input_crc; | |
| size_t input_size; | |
| size_t output_size; | |
| }; | |
| Impl(int compression_level, ByteSink out) | |
| : compression_level_(std::clamp(compression_level, 0, 9)), | |
| delegate_write_(std::move(out)), | |
| out_([this](absl::string_view s) { | |
| output_file_offset_ += s.size(); // Keep track of offsets. | |
| return delegate_write_(s); | |
| }) {} | |
| bool AddFile(absl::string_view filename, | |
| const ByteSource &content_generator) { | |
| if (is_finished_) return false; // Can't add more files. | |
| if (!content_generator) return false; | |
| ++file_count_; | |
| const size_t start_offset = output_file_offset_; | |
| const uint16_t mod_time = 0; // TODO: accept time_t and convert ? | |
| const uint16_t mod_date = 0; | |
| bool success = // Assemble local file header | |
| HeaderWriter(scratch_space_) | |
| .AddLiteral("PK\x03\x04") | |
| .AddInt16(kPkZipVersion) // Minimum version needed | |
| .AddInt16(0x08) // Flags. Sizes and CRC in data descriptor. | |
| .AddInt16(compression_level_ == 0 ? 0 : 8) | |
| .AddInt16(mod_time) | |
| .AddInt16(mod_date) | |
| .AddInt32(0) // CRC32. Known later. | |
| .AddInt32(0) // Compressed size: known later. | |
| .AddInt32(0) // Uncompressed size: known later. | |
| .AddInt16(filename.length()) | |
| .AddInt16(0) // Extra field length | |
| .AddLiteral(filename) | |
| .Write(out_); | |
| if (!success) return false; | |
| // Data output | |
| const CompressResult compress_result = | |
| compression_level_ == 0 ? CopyDataToOutput(content_generator) | |
| : CompressDataToOutput(content_generator); | |
| success = // Assemble Data Descriptor after file with known CRC and size. | |
| HeaderWriter(scratch_space_) | |
| .AddInt32(compress_result.input_crc) | |
| .AddInt32(compress_result.output_size) | |
| .AddInt32(compress_result.input_size) | |
| .Write(out_); | |
| // Append directory file header entry to be written in Finish() | |
| HeaderWriter(scratch_space_) | |
| .AddLiteral("PK\x01\x02") | |
| .AddInt16(kPkZipVersion) // Our Version | |
| .AddInt16(kPkZipVersion) // Readable by version | |
| .AddInt16(0x08) // Flag | |
| .AddInt16(compression_level_ == 0 ? 0 : 8) | |
| .AddInt16(mod_time) | |
| .AddInt16(mod_date) | |
| .AddInt32(compress_result.input_crc) | |
| .AddInt32(compress_result.output_size) | |
| .AddInt32(compress_result.input_size) | |
| .AddInt16(filename.length()) | |
| .AddInt16(0) // Extra field length | |
| .AddInt16(0) // file comment length | |
| .AddInt16(0) // disk number | |
| .AddInt16(0) // Internal file attr | |
| .AddInt32(0) // External file attr | |
| .AddInt32(start_offset) | |
| .AddLiteral(filename) | |
| .Append(¢ral_dir_data_); | |
| return success; | |
| } | |
| bool Finish() { | |
| if (is_finished_) return false; | |
| is_finished_ = true; | |
| const size_t start_offset = output_file_offset_; | |
| if (!out_({central_dir_data_.data(), central_dir_data_.size()})) { | |
| return false; | |
| } | |
| // End of central directory record | |
| constexpr absl::string_view comment("Created with Verible simple zip"); | |
| return HeaderWriter(scratch_space_) | |
| .AddLiteral("PK\x05\x06") // End of central directory signature | |
| .AddInt16(0) // our disk number | |
| .AddInt16(0) // disk where it all starts | |
| .AddInt16(file_count_) // Number of directory records on this disk | |
| .AddInt16(file_count_) // ... and overall | |
| .AddInt32(central_dir_data_.size()) | |
| .AddInt32(start_offset) | |
| .AddInt16(comment.length()) | |
| .AddLiteral(comment) | |
| .Write(out_); | |
| } | |
| CompressResult CopyDataToOutput(const ByteSource &generator) { | |
| uint32_t crc = 0; | |
| size_t processed_size = 0; | |
| absl::string_view chunk; | |
| while (!(chunk = generator()).empty()) { | |
| crc = crc32(crc, (const uint8_t *)chunk.data(), chunk.size()); | |
| processed_size += chunk.size(); | |
| out_(chunk); | |
| } | |
| return {crc, processed_size, processed_size}; | |
| } | |
| CompressResult CompressDataToOutput(const ByteSource &generator) { | |
| uint32_t crc = 0; | |
| absl::string_view chunk; | |
| z_stream stream; | |
| memset(&stream, 0x00, sizeof(stream)); | |
| // Need negative window bits to tell zlib not to create a header. | |
| deflateInit2(&stream, compression_level_, Z_DEFLATED, -15 /*window bits*/, | |
| 9 /* memlevel*/, 0); | |
| const size_t kScratchSize = sizeof(scratch_space_); | |
| do { | |
| chunk = generator(); | |
| const int flush_setting = chunk.empty() ? Z_FINISH : Z_NO_FLUSH; | |
| if (!chunk.empty()) | |
| crc = crc32(crc, (const uint8_t *)chunk.data(), chunk.size()); | |
| stream.avail_in = chunk.size(); | |
| stream.next_in = (uint8_t *)chunk.data(); | |
| do { | |
| stream.avail_out = kScratchSize; | |
| stream.next_out = (uint8_t *)scratch_space_; | |
| deflate(&stream, flush_setting); | |
| const size_t output_size = kScratchSize - stream.avail_out; | |
| if (output_size) out_({scratch_space_, output_size}); | |
| } while (stream.avail_out == 0); | |
| } while (!chunk.empty()); | |
| CompressResult result = {crc, stream.total_in, stream.total_out}; | |
| deflateEnd(&stream); | |
| return result; | |
| } | |
| const int compression_level_; | |
| const ByteSink delegate_write_; | |
| const ByteSink out_; | |
| size_t file_count_ = 0; | |
| size_t output_file_offset_ = 0; | |
| std::string central_dir_data_; | |
| bool is_finished_ = false; | |
| char scratch_space_[1 << 20]; // to assemble headers and compression data | |
| }; | |
| Encoder::Encoder(int compression_level, ByteSink out) | |
| : impl_(new Impl(compression_level, std::move(out))) {} | |
| Encoder::~Encoder() { Finish(); } | |
| bool Encoder::AddFile(absl::string_view filename, | |
| const ByteSource &content_generator) { | |
| return impl_->AddFile(filename, content_generator); | |
| } | |
| bool Encoder::Finish() { return impl_->Finish(); } | |
| } // namespace zip | |
| } // namespace verible |