// 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.

#ifndef BAZEL_SRC_TOOLS_SINGLEJAR_INPUT_JAR_SCAN_ENTRIES_TEST_H_
#define BAZEL_SRC_TOOLS_SINGLEJAR_INPUT_JAR_SCAN_ENTRIES_TEST_H_ 1

#include <errno.h>
#ifndef _WIN32
#include <unistd.h>
#endif
#include <memory>
#include <string>

#include "src/tools/singlejar/input_jar.h"
#include "src/tools/singlejar/mapped_file.h"
#include "src/tools/singlejar/test_util.h"
#include "googletest/include/gtest/gtest.h"

static const char kJar[] = "jar.jar";
static const char kXXXX[] = "4GB-1file";
static const char kEmpty[] = "empty";
static const char kRes1[] = "res1";
static const char kRes2[] = "res2";
static const char kHuge[] = "4GB+1file";
static const int32_t res1_size = 123;
static const int32_t res2_size = 456;
static const int64_t huge_size = 0x100000001L;
static const int64_t kHugeOffset = 0x100000001L;

/* Verifies that InputJar can handle zip/jar files created by a given creator.
 * This includes basic directory scan, handling huge (>4GB) zip files and huge
 * entries in them, and handling zip files with "huge" (>64K) number of entries.
 * A creator is passed as a typed parameter.
 */
template <class ZipCreator>
class InputJarScanEntries : public testing::Test {
 public:
  static void SetUpTestCase() { ZipCreator::SetUpTestCase(); }

  static void TearDownTestCase() { ZipCreator::TearDownTestCase(); }

  static void CreateBasicJar() {
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kRes1, res1_size));
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kRes2, res2_size));
    unlink(kJar);
    ASSERT_EQ(0, ZipCreator::Jar(true, kJar, kRes1, kRes2, nullptr));
    unlink(kRes1);
    unlink(kRes2);
  }

  static void CreateJarWithHugeUncompressed() {
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kHuge, huge_size));
    unlink(kJar);
    ASSERT_EQ(0, ZipCreator::Jar(true, kJar, kHuge, nullptr));
    unlink(kHuge);
  }

  static void CreateJarWithZip64Entries() {
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kXXXX, 0xFFFFFFFF));
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kHuge, huge_size));
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kEmpty, 0));
    ASSERT_TRUE(singlejar_test_util::AllocateFile(kRes1, res1_size));
    ASSERT_EQ(
        0, ZipCreator::Jar(false, kJar, kXXXX, kHuge, kEmpty, kRes1, nullptr));
    unlink(kXXXX);
    unlink(kHuge);
    unlink(kEmpty);
    unlink(kRes1);
  }

  static void CreateJarWithLotsOfEntries() {
    unlink(kJar);
    // Create 256 directories with 256 files in each one,
    // make an archive from them
    for (int dir = 0; dir < 256; ++dir) {
      char dirname[10];
      snprintf(dirname, sizeof(dirname), "dir%d", dir);
#ifdef _WIN32
      ASSERT_EQ(0, mkdir(dirname));
#else
      ASSERT_EQ(0, mkdir(dirname, 0777));
#endif
      for (int file = 0; file < 256; ++file) {
        char filepath[20];
        snprintf(filepath, sizeof(filepath), "%s/%d", dirname, file);
        ASSERT_TRUE(singlejar_test_util::AllocateFile(filepath, 1));
      }
    }
    ASSERT_EQ(0, ZipCreator::Jar(false, kJar, "dir*", nullptr));
    for (int dir = 0; dir < 256; ++dir) {
      char rmdircmd[100];
#ifdef _WIN32
      snprintf(rmdircmd, sizeof(rmdircmd), "rmdir /S /Q dir%d", dir);
#else
      snprintf(rmdircmd, sizeof(rmdircmd), "rm dir%d/* && rmdir dir%d", dir,
               dir);
#endif
      ASSERT_EQ(0, system(rmdircmd));
    }
  }

  void SetUp() override { input_jar_.reset(new InputJar); }

  static void SmogCheck(const CDH *cdh, const LH *lh) {
    ASSERT_TRUE(cdh->is()) << "No expected tag in the Central Directory Entry.";
    ASSERT_NE(nullptr, lh) << "No local header.";
    ASSERT_TRUE(lh->is()) << "No expected tag in the Local Header.";
    EXPECT_EQ(lh->file_name_string(), cdh->file_name_string());
    if (!cdh->no_size_in_local_header()) {
      EXPECT_EQ(lh->compressed_file_size(), cdh->compressed_file_size())
          << "Entry: " << lh->file_name_string();
      EXPECT_EQ(lh->uncompressed_file_size(), cdh->uncompressed_file_size())
          << "Entry: " << cdh->file_name_string();
    }
  }

  std::unique_ptr<InputJar> input_jar_;
};

TYPED_TEST_SUITE_P(InputJarScanEntries);

TYPED_TEST_P(InputJarScanEntries, OpenClose) {
  ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
  this->CreateBasicJar();
  singlejar_test_util::LsZip(kJar);
  ASSERT_TRUE(this->input_jar_->Open(kJar));
  EXPECT_GE(this->input_jar_->fd(), 0);
  this->input_jar_->Close();
  EXPECT_LT(this->input_jar_->fd(), 0);
}

/*
 * Check that the jar has the expected entries, they have expected
 * sizes, and that we can access both central directory entries and
 * local headers.
 */
TYPED_TEST_P(InputJarScanEntries, Basic) {
  ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
  this->CreateBasicJar();
  ASSERT_TRUE(this->input_jar_->Open(kJar));
  const LH *lh;
  const CDH *cdh;
  int file_count = 0;
  bool res1_present = false;
  bool res2_present = false;
  for (int entry_count = 0; (cdh = this->input_jar_->NextEntry(&lh));
       ++entry_count) {
    this->SmogCheck(cdh, lh);
    if ('/' != lh->file_name()[lh->file_name_length() - 1]) {
      ++file_count;
      if (cdh->file_name_is(kRes1)) {
        EXPECT_EQ(res1_size, cdh->uncompressed_file_size());
        res1_present = true;
      } else if (cdh->file_name_is(kRes2)) {
        EXPECT_EQ(res2_size, cdh->uncompressed_file_size());
        res2_present = true;
      }
    }
  }

  this->input_jar_->Close();
  unlink(kJar);
  EXPECT_TRUE(res1_present) << "Jar file " << kJar << " lacks expected '"
                            << kRes1 << "' file.";
  EXPECT_TRUE(res2_present) << "Jar file " << kJar << " lacks expected '"
                            << kRes2 << "' file.";
}

/*
 * Check we can handle >4GB jar with >4GB entry in it.
 */
TYPED_TEST_P(InputJarScanEntries, HugeUncompressed) {
  ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
  this->CreateJarWithHugeUncompressed();
  singlejar_test_util::LsZip(kJar);
  ASSERT_TRUE(this->input_jar_->Open(kJar));
  const LH *lh;
  const CDH *cdh;
  bool huge_file_present = false;

  while ((cdh = this->input_jar_->NextEntry(&lh))) {
    this->SmogCheck(cdh, lh);
    if (cdh->file_name_is(kHuge)) {
      EXPECT_EQ(huge_size, cdh->uncompressed_file_size())
          << "Entry: " << cdh->file_name_string();
      huge_file_present = true;
    }
  }
  this->input_jar_->Close();
  unlink(kJar);
  EXPECT_TRUE(huge_file_present) << "Jar file " << kJar << " lacks expected '"
                                 << kHuge << "' file.";
}

/*
 * Check we can handle >4GB jar with huge and small entries and huge and
 * small offsets in the central directory.
 */
TYPED_TEST_P(InputJarScanEntries, TestZip64) {
  ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
  this->CreateJarWithZip64Entries();
  singlejar_test_util::LsZip(kJar);
  ASSERT_TRUE(this->input_jar_->Open(kJar));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = this->input_jar_->NextEntry(&lh))) {
    this->SmogCheck(cdh, lh);

    if (cdh->file_name_is(kXXXX)) {
      EXPECT_EQ(0xFFFFFFFF, cdh->uncompressed_file_size());
      EXPECT_EQ(0xFFFFFFFF, cdh->compressed_file_size());
    } else if (cdh->file_name_is(kHuge)) {
      EXPECT_EQ(huge_size, cdh->uncompressed_file_size());
      EXPECT_EQ(huge_size, cdh->compressed_file_size());
      EXPECT_LT(kHugeOffset, cdh->local_header_offset());
    } else if (cdh->file_name_is(kEmpty)) {
      EXPECT_EQ(0, cdh->uncompressed_file_size());
      EXPECT_EQ(0, cdh->compressed_file_size());
      EXPECT_EQ(0, lh->compressed_file_size());
      EXPECT_LT(kHugeOffset, cdh->local_header_offset());
    } else if (cdh->file_name_is(kRes1)) {
      EXPECT_EQ(res1_size, cdh->uncompressed_file_size());
      EXPECT_LT(kHugeOffset, cdh->local_header_offset());
    }
  }
  this->input_jar_->Close();
  unlink(kJar);
}

/*
 * Check we can handle >64K entries.
 */
TYPED_TEST_P(InputJarScanEntries, LotsOfEntries) {
  ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
  this->CreateJarWithLotsOfEntries();
#if !defined(__APPLE__) && !defined(_WIN32)
  const char kTailUnzip[] = "unzip -v jar.jar | tail";
  ASSERT_EQ(0, system(kTailUnzip)) << "Failed command: " << kTailUnzip;
#endif
  ASSERT_TRUE(this->input_jar_->Open(kJar));
  const LH *lh;
  const CDH *cdh;
  int entry_count = 0;
  int file_count = 0;
  int dir_count = 0;
  while ((cdh = this->input_jar_->NextEntry(&lh))) {
    this->SmogCheck(cdh, lh);
    ++entry_count;
    if (cdh->file_name()[cdh->file_name_length() - 1] == '/') {
      ++dir_count;
    } else {
      ++file_count;
    }
  }
  this->input_jar_->Close();
  unlink(kJar);

  /* We cannot compare to the exact number because JDK's jar
   * adds META-INF/ and META-INF/MANIFEST.MF.
   */
  EXPECT_LE(256 * 257, entry_count);
  EXPECT_LE(256, dir_count);
  EXPECT_LE(256 * 256, file_count);
}

TYPED_TEST_P(InputJarScanEntries, BasicInMemory) {
  ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
  this->CreateBasicJar();
  MappedFile mapped_file;
  ASSERT_TRUE(mapped_file.Open(kJar));
  ASSERT_TRUE(this->input_jar_->Open(
      kJar, const_cast<unsigned char *>(mapped_file.start()),
      mapped_file.size()));
  const LH *lh;
  const CDH *cdh;
  int file_count = 0;
  bool res1_present = false;
  bool res2_present = false;
  while ((cdh = this->input_jar_->NextEntry(&lh))) {
    this->SmogCheck(cdh, lh);
    if ('/' != lh->file_name()[lh->file_name_length() - 1]) {
      ++file_count;
      if (cdh->file_name_is(kRes1)) {
        EXPECT_EQ(res1_size, cdh->uncompressed_file_size());
        res1_present = true;
      } else if (cdh->file_name_is(kRes2)) {
        EXPECT_EQ(res2_size, cdh->uncompressed_file_size());
        res2_present = true;
      }
    }
  }

  this->input_jar_->Close();
  mapped_file.Close();
  unlink(kJar);
  EXPECT_TRUE(res1_present)
      << "Jar file " << kJar << " lacks expected '" << kRes1 << "' file.";
  EXPECT_TRUE(res2_present)
      << "Jar file " << kJar << " lacks expected '" << kRes2 << "' file.";
}

REGISTER_TYPED_TEST_SUITE_P(InputJarScanEntries, OpenClose, Basic,
                            HugeUncompressed, TestZip64, LotsOfEntries,
                            BasicInMemory);

#endif  // BAZEL_SRC_TOOLS_SINGLEJAR_INPUT_JAR_SCAN_ENTRIES_TEST_H_
