| // Copyright 2016 The Bazel Authors. All rights reserved. |
| // |
| // 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 <stdio.h> |
| #include <fstream> |
| #include <ios> |
| #include <iostream> |
| #include <memory> |
| #include <sstream> |
| |
| #include "src/tools/singlejar/input_jar.h" |
| #include "src/tools/singlejar/test_util.h" |
| #include "src/tools/singlejar/transient_bytes.h" |
| #include "googletest/include/gtest/gtest.h" |
| |
| #ifdef _MSC_VER |
| #define SINGLEJAR_ALYWAYS_INLINE __forceinline |
| #else |
| #define SINGLEJAR_ALYWAYS_INLINE __attribute__((always_inline)) |
| #endif |
| |
| namespace { |
| const char kStoredJar[] = "stored.zip"; |
| const char kCompressedJar[] = "compressed.zip"; |
| const char kBytesSmall[] = |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789" |
| "0123456789012345678901234567890123456789"; |
| |
| std::ostream &operator<<(std::ostream &out, |
| TransientBytes const &bytes) { |
| struct Sink { |
| void operator()(const void *chunk, uint64_t chunk_size) const { |
| out_.write(reinterpret_cast<const char *>(chunk), chunk_size); |
| } |
| std::ostream &out_; |
| }; |
| Sink sink{out}; |
| bytes.stream_out(sink); |
| return out; |
| } |
| |
| class TransientBytesTest : public ::testing::Test { |
| protected: |
| static void SetUpTestCase() { |
| ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR"))); |
| CreateCompressedJar(); |
| } |
| |
| static void TearDownTestCase() { unlink(kCompressedJar); } |
| |
| void SetUp() override { transient_bytes_.reset(new TransientBytes); } |
| |
| // The value of the byte at a given position in a file created by the |
| // CreateFile method below. |
| static SINGLEJAR_ALYWAYS_INLINE uint8_t file_byte_at(uint64_t offset) { |
| // return offset >> (8 * (offset & 7)); |
| return offset & 255; |
| } |
| |
| // Create file with given name and size and contents. |
| static bool CreateFile(const char *filename, uint64_t size) { |
| FILE *fp = fopen(filename, "wb"); |
| if (fp == nullptr) { |
| perror(filename); |
| return false; |
| } |
| const uint64_t buffer_size = 4096; |
| uint8_t buffer[buffer_size]; |
| uint64_t offset = 0; |
| while (offset < size) { |
| uint64_t offset_end = std::min(size, offset + buffer_size); |
| uint64_t to_write = 0; |
| while (offset < offset_end) { |
| buffer[to_write++] = file_byte_at(offset++); |
| } |
| if (fwrite(buffer, to_write, 1, fp) != 1) { |
| perror(filename); |
| fclose(fp); |
| return false; |
| } |
| } |
| if (0 == fclose(fp)) { |
| return true; |
| } |
| perror(filename); |
| return false; |
| } |
| |
| static void CreateStoredJar() { |
| ASSERT_TRUE(singlejar_test_util::AllocateFile("small1", 100)); |
| ASSERT_TRUE(singlejar_test_util::AllocateFile("huge", 0x100000001)); |
| ASSERT_TRUE(singlejar_test_util::AllocateFile("small2", 100)); |
| unlink(kStoredJar); |
| ASSERT_EQ(0, system("zip -0qm stored.zip small1 huge small2")); |
| #if !defined(__APPLE__) |
| ASSERT_EQ(0, system("unzip -v stored.zip")); |
| #endif |
| } |
| |
| static void CreateCompressedJar() { |
| unlink(kCompressedJar); |
| ASSERT_TRUE(CreateFile("511", 511)); |
| ASSERT_TRUE(CreateFile("huge", 0x100000001)); |
| ASSERT_TRUE(CreateFile("1K", 1024)); |
| ASSERT_EQ(0, system("zip -qm compressed.zip 511 huge 1K")); |
| #if !defined(__APPLE__) |
| ASSERT_EQ(0, system("unzip -v compressed.zip")); |
| #endif |
| } |
| std::unique_ptr<TransientBytes> transient_bytes_; |
| }; |
| |
| TEST_F(TransientBytesTest, AppendBytes) { |
| int const kIter = 10000; |
| transient_bytes_->Append(kBytesSmall); |
| EXPECT_EQ(strlen(kBytesSmall), transient_bytes_->data_size()); |
| std::ostringstream out; |
| out << *transient_bytes_; |
| EXPECT_STREQ(kBytesSmall, out.str().c_str()); |
| out.flush(); |
| |
| for (int i = 1; i < kIter; ++i) { |
| transient_bytes_->Append(kBytesSmall); |
| ASSERT_EQ((i + 1) * strlen(kBytesSmall), transient_bytes_->data_size()); |
| } |
| |
| out << *transient_bytes_; |
| std::string out_string = out.str(); |
| size_t size = strlen(kBytesSmall); |
| for (size_t pos = 0; pos < kIter * size; pos += size) { |
| ASSERT_STREQ(kBytesSmall, out_string.substr(pos, size).c_str()) |
| << (pos / size) << "-th chunk does not match"; |
| } |
| } |
| |
| TEST_F(TransientBytesTest, ReadEntryContents) { |
| ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR"))); |
| CreateStoredJar(); |
| std::unique_ptr<InputJar> input_jar(new InputJar); |
| ASSERT_TRUE(input_jar->Open(kStoredJar)); |
| const LH *lh; |
| const CDH *cdh; |
| while ((cdh = input_jar->NextEntry(&lh))) { |
| transient_bytes_.reset(new TransientBytes); |
| if (!cdh->uncompressed_file_size()) { |
| continue; |
| } |
| ASSERT_EQ(Z_NO_COMPRESSION, lh->compression_method()); |
| transient_bytes_->ReadEntryContents(cdh, lh); |
| ASSERT_EQ(cdh->uncompressed_file_size(), transient_bytes_->data_size()); |
| struct Sink { |
| Sink(const LH *lh) |
| : data_start_(lh->data()), |
| data_(lh->data()), |
| entry_name_(lh->file_name(), lh->file_name_length()) {} |
| void operator()(const void *chunk, uint64_t chunk_size) const { |
| ASSERT_EQ(0, memcmp(chunk, data_, chunk_size)) |
| << "Entry " << entry_name_ << "The chunk [" << data_ - data_start_ |
| << ".." << data_ + chunk_size - data_start_ << ") differs"; |
| data_ += chunk_size; |
| } |
| const uint8_t *data_start_; |
| mutable const uint8_t *data_; |
| std::string entry_name_; |
| }; |
| Sink sink(lh); |
| transient_bytes_->stream_out(sink); |
| } |
| input_jar->Close(); |
| unlink(kStoredJar); |
| } |
| |
| TEST_F(TransientBytesTest, DecompressEntryContents) { |
| std::unique_ptr<InputJar> input_jar(new InputJar); |
| ASSERT_TRUE(input_jar->Open(kCompressedJar)); |
| const LH *lh; |
| const CDH *cdh; |
| std::unique_ptr<Inflater> inflater; |
| while ((cdh = input_jar->NextEntry(&lh))) { |
| transient_bytes_.reset(new TransientBytes); |
| inflater.reset(new Inflater); |
| if (!cdh->uncompressed_file_size()) { |
| continue; |
| } |
| ASSERT_EQ(Z_DEFLATED, lh->compression_method()); |
| transient_bytes_->DecompressEntryContents(cdh, lh, inflater.get()); |
| |
| ASSERT_EQ(cdh->uncompressed_file_size(), transient_bytes_->data_size()); |
| // A sink that verifies decompressed entry contents. |
| struct Sink { |
| Sink(const LH *lh) |
| : offset_(0), entry_name_(lh->file_name(), lh->file_name_length()) {} |
| void operator()(const void *chunk, uint64_t chunk_size) const { |
| for (uint64_t i = 0; i < chunk_size; ++i) { |
| // ASSERT_EQ is quite slow in the non-optimized build, avoid calling |
| // it 4billion files on a 4GB file. |
| if (file_byte_at(offset_ + i) == |
| reinterpret_cast<const uint8_t *>(chunk)[i]) { |
| break; |
| } |
| ASSERT_EQ(file_byte_at(offset_ + i), |
| reinterpret_cast<const uint8_t *>(chunk)[i]) |
| << "Entry " << entry_name_ << ": mismatch at offset " |
| << (offset_ + i); |
| } |
| offset_ += chunk_size; |
| } |
| mutable uint64_t offset_; |
| std::string entry_name_; |
| }; |
| Sink sink(lh); |
| transient_bytes_->stream_out(sink); |
| } |
| input_jar->Close(); |
| } |
| |
| // Verify CompressOut: if compressed size is less than original, it writes out |
| // compressed data. |
| TEST_F(TransientBytesTest, CompressOut) { |
| std::unique_ptr<InputJar> input_jar(new InputJar); |
| ASSERT_TRUE(input_jar->Open(kCompressedJar)); |
| const LH *lh; |
| const CDH *cdh; |
| std::unique_ptr<Inflater> inflater; |
| while ((cdh = input_jar->NextEntry(&lh))) { |
| transient_bytes_.reset(new TransientBytes); |
| inflater.reset(new Inflater); |
| if (!cdh->uncompressed_file_size()) { |
| continue; |
| } |
| ASSERT_EQ(Z_DEFLATED, lh->compression_method()); |
| transient_bytes_->DecompressEntryContents(cdh, lh, inflater.get()); |
| ASSERT_EQ(cdh->uncompressed_file_size(), transient_bytes_->data_size()); |
| // Now let us compress it back. |
| uint8_t *buffer = |
| reinterpret_cast<uint8_t *>(malloc(cdh->uncompressed_file_size())); |
| ASSERT_NE(nullptr, buffer); |
| uint32_t crc32 = 0; |
| uint64_t bytes_written; |
| uint16_t rc = transient_bytes_->CompressOut(buffer, &crc32, &bytes_written); |
| |
| EXPECT_EQ(Z_DEFLATED, rc) << "TransientBytes::Write did not compress " |
| << cdh->file_name_string(); |
| EXPECT_EQ(cdh->crc32(), crc32) |
| << "TransientBytes::Write has wrong crc32 for " |
| << cdh->file_name_string(); |
| |
| // Verify contents. |
| Inflater inf2; |
| inf2.DataToInflate(buffer, 0); // Just to save the position. |
| uint64_t to_inflate = bytes_written; |
| uint64_t position = 0; |
| while (to_inflate > 0) { |
| uint32_t to_inflate_chunk = |
| std::min(to_inflate, static_cast<uint64_t>(0xFFFFFFFF)); |
| inf2.DataToInflate(inf2.next_in(), to_inflate_chunk); |
| to_inflate -= to_inflate_chunk; |
| for (;;) { |
| uint8_t decomp_buf[1024]; |
| int rc = inf2.Inflate(decomp_buf, sizeof(decomp_buf)); |
| ASSERT_TRUE(Z_STREAM_END == rc || Z_OK == rc) |
| << "Decompressiong contents of " << cdh->file_name_string() |
| << " at offset " << position << " returned " << rc; |
| for (uint32_t i = 0; i < sizeof(decomp_buf) - inf2.available_out(); |
| ++i) { |
| if (file_byte_at(position) != decomp_buf[i]) { |
| EXPECT_EQ(file_byte_at(position), decomp_buf[i]) |
| << "Decompressed contents of " << cdh->file_name_string() |
| << " at offset " << position << " is wrong"; |
| } |
| ++position; |
| } |
| if (Z_STREAM_END == rc) { |
| // Input buffer done. |
| break; |
| } else { |
| EXPECT_EQ(0, inf2.available_out()); |
| } |
| } |
| } |
| free(buffer); |
| } |
| input_jar->Close(); |
| } |
| |
| // Verify CompressOut: if compressed size exceeds original, it writes out |
| // original data |
| TEST_F(TransientBytesTest, CompressOutStore) { |
| transient_bytes_->Append("a"); |
| uint8_t buffer[400] = {0xfe, 0xfb}; |
| uint32_t crc32 = 0; |
| uint64_t bytes_written; |
| uint16_t rc = transient_bytes_->CompressOut(buffer, &crc32, &bytes_written); |
| ASSERT_EQ(Z_NO_COMPRESSION, rc); |
| ASSERT_EQ(1, bytes_written); |
| ASSERT_EQ('a', buffer[0]); |
| ASSERT_EQ(0xfb, buffer[1]); |
| ASSERT_EQ(0xE8B7BE43, crc32); |
| } |
| |
| // Verify CompressOut: if there are zero bytes in the buffer, just store. |
| TEST_F(TransientBytesTest, CompressZero) { |
| transient_bytes_->Append(""); |
| uint8_t buffer[400] = {0xfe, 0xfb}; |
| uint32_t crc32 = 0; |
| uint64_t bytes_written; |
| uint16_t rc = transient_bytes_->CompressOut(buffer, &crc32, &bytes_written); |
| ASSERT_EQ(Z_NO_COMPRESSION, rc); |
| ASSERT_EQ(0, bytes_written); |
| ASSERT_EQ(0xfe, buffer[0]); |
| ASSERT_EQ(0xfb, buffer[1]); |
| ASSERT_EQ(0, crc32); |
| } |
| |
| // Verify CopyOut. |
| TEST_F(TransientBytesTest, CopyOut) { |
| transient_bytes_->Append("a"); |
| uint8_t buffer[400] = {0xfe, 0xfb}; |
| uint32_t crc32 = 0; |
| transient_bytes_->CopyOut(buffer, &crc32); |
| ASSERT_EQ('a', buffer[0]); |
| ASSERT_EQ(0xfb, buffer[1]); |
| ASSERT_EQ(0xE8B7BE43, crc32); |
| } |
| |
| } // namespace |