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

// Must be included before anything else.
#include "src/tools/singlejar/port.h"

#include "src/main/cpp/util/file.h"
#include "src/main/cpp/util/port.h"
#include "src/main/cpp/util/strings.h"
#include "src/tools/singlejar/input_jar.h"
#include "src/tools/singlejar/options.h"
#include "src/tools/singlejar/output_jar.h"
#include "src/tools/singlejar/test_util.h"
#include "googletest/include/gtest/gtest.h"

#if !defined(JAR_TOOL_PATH)
#error "The path to jar tool has to be defined via -DJAR_TOOL_PATH="
#endif

#ifdef _WIN32
#define unlink _unlink
#define CMD_SEPARATOR "&"
#else
#define CMD_SEPARATOR ";"
#endif

namespace {

using bazel::tools::cpp::runfiles::Runfiles;
using singlejar_test_util::CreateTextFile;
using singlejar_test_util::GetEntryContents;
using singlejar_test_util::OutputFilePath;
using singlejar_test_util::RunCommand;
using singlejar_test_util::VerifyZip;

using std::string;

#if !defined(DATA_DIR_TOP)
#define DATA_DIR_TOP
#endif

const char kPathLibData1[] =
    "io_bazel/src/tools/singlejar/libdata1.jar";
const char kPathLibData2[] =
    "io_bazel/src/tools/singlejar/libdata2.jar";

static bool HasSubstr(const string &s, const string &what) {
  return string::npos != s.find(what);
}

static bool EndsWith(const string &s, const string &what) {
  return what.size() <= s.size() && s.substr(s.size() - what.size()) == what;
}

// A subclass of the OutputJar which concatenates the contents of each
// entry in the data/ directory from the input archives.
class CustomOutputJar : public OutputJar {
 public:
  ~CustomOutputJar() override {}
  void ExtraHandler(const std::string & /*input_jar_path*/, const CDH *cdh,
                    const std::string *input_jar_aux_label) override {
    auto file_name = cdh->file_name();
    auto file_name_length = cdh->file_name_length();
    if (file_name_length > 0 && file_name[file_name_length - 1] != '/' &&
        begins_with(file_name, file_name_length, "tools/singlejar/data/")) {
      // The contents of the data/<FILE> on the output is the
      // concatenation of the data/<FILE> files from all inputs.
      std::string metadata_file_path(file_name, file_name_length);
      if (NewEntry(metadata_file_path)) {
        ExtraCombiner(metadata_file_path, new Concatenator(metadata_file_path));
      }
    }
  }
};

class OutputJarSimpleTest : public ::testing::Test {
 protected:
  void SetUp() override { runfiles.reset(Runfiles::CreateForTest()); }

  void CreateOutput(const string &out_path, const std::vector<string> &args) {
    const char *option_list[100] = {"--output", out_path.c_str()};
    int nargs = 2;
    for (auto &arg : args) {
      if (arg.empty()) {
        continue;
      }
      option_list[nargs++] = arg.c_str();
      if (arg.find(' ') == string::npos) {
        fprintf(stderr, " '%s'", arg.c_str());
      } else {
        fprintf(stderr, " %s", arg.c_str());
      }
    }
    fprintf(stderr, "\n");
    options_.ParseCommandLine(nargs, option_list);
    ASSERT_EQ(0, output_jar_.Doit(&options_));
    EXPECT_EQ(0, VerifyZip(out_path));
  }

  string CompressionOptionsTestingJar(const string &compression_option) {
    string cp_res_path =
        CreateTextFile("cp_res", "line1\nline2\nline3\nline4\n");
    string out_path = OutputFilePath("out.jar");
    CreateOutput(
        out_path,
        {compression_option, "--sources",
         runfiles
             ->Rlocation(
                 "io_bazel/src/tools/singlejar/libtest1.jar")
             .c_str(),
         runfiles
             ->Rlocation(
                 "io_bazel/src/tools/singlejar/stored.jar")
             .c_str(),
         "--resources", cp_res_path, "--deploy_manifest_lines",
         "property1: value1", "property2: value2"});
    return out_path;
  }

  OutputJar output_jar_;
  Options options_;
  std::unique_ptr<Runfiles> runfiles;
};

// No inputs at all.
TEST_F(OutputJarSimpleTest, Empty) {
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {});
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  int entry_count = 0;
  const LH *lh;
  const CDH *cdh;
  const uint8_t cafe_extra_field[] = {0xFE, 0xCA, 0, 0};
  while ((cdh = input_jar.NextEntry(&lh))) {
    ++entry_count;
    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();
    }
    // Verify that each entry has a reasonable timestamp.
    EXPECT_EQ(lh->last_mod_file_date(), cdh->last_mod_file_date())
        << "Entry: " << lh->file_name_string();
    EXPECT_EQ(lh->last_mod_file_time(), cdh->last_mod_file_time())
        << "Entry: " << lh->file_name_string();
    uint16_t dos_time = lh->last_mod_file_time();
    uint16_t dos_date = lh->last_mod_file_date();

    // Current time, rounded to even number of seconds because MSDOS timestamp
    // does this, too.
    time_t now = (time(nullptr) + 1) & ~1;
    struct tm tm_now;
    localtime_r(&now, &tm_now);
    char now_time_str[50];
    strftime(now_time_str, sizeof(now_time_str), "%c", &tm_now);

    // Unpack MSDOS file timestamp. See the comment about its format in
    // output_jar.cc.
    struct tm tm;
    tm.tm_sec = (dos_time & 31) << 1;
    tm.tm_min = (dos_time >> 5) & 63;
    tm.tm_hour = (dos_time >> 11) & 31;
    tm.tm_mday = (dos_date & 31);
    tm.tm_mon = ((dos_date >> 5) & 15) - 1;
    tm.tm_year = ((dos_date >> 9) & 127) + 80;
    tm.tm_isdst = tm_now.tm_isdst;
    time_t entry_time = mktime(&tm);
    char entry_time_str[50];
    strftime(entry_time_str, sizeof(entry_time_str), "%c", &tm);

    // Without --normalize option all the entries should have reasonably
    // current timestamp (which we arbitrarily choose to be <5 minutes).
    EXPECT_GE(now, entry_time) << now_time_str << " vs. " << entry_time_str;
    EXPECT_LE(now, entry_time + 300) << now_time_str << " vs. "
                                     << entry_time_str;

    // The first entry should be for the META-INF/ directory, and it should
    // contain a single extra field 0xCAFE. Although
    // https://bugs.openjdk.java.net/browse/JDK-6808540 claims that this extra
    // field is optional, 'file' utility in Linux relies on to distinguish
    // jar from zip.
    if (entry_count == 1) {
      ASSERT_EQ("META-INF/", lh->file_name_string());
      ASSERT_EQ(4, lh->extra_fields_length());
      ASSERT_EQ(0, memcmp(cafe_extra_field, lh->extra_fields(), 4));
      ASSERT_EQ(4, cdh->extra_fields_length());
      ASSERT_EQ(0, memcmp(cafe_extra_field, cdh->extra_fields(), 4));
    }
  }
  input_jar.Close();
  string manifest = GetEntryContents(out_path, "META-INF/MANIFEST.MF");
  EXPECT_EQ(
      "Manifest-Version: 1.0\r\n"
      "Created-By: singlejar\r\n"
      "\r\n",
      manifest);
  string build_properties = GetEntryContents(out_path, "build-data.properties");
  EXPECT_PRED2(HasSubstr, build_properties, "build.target=");
}

// Source jars.
TEST_F(OutputJarSimpleTest, Source) {
  string out_path = OutputFilePath("out.jar");
  CreateOutput(
      out_path,
      {"--sources",
       runfiles
           ->Rlocation(
               "io_bazel/src/tools/singlejar/libtest1.jar")
           .c_str(),
       runfiles
           ->Rlocation(
               "io_bazel/src/tools/singlejar/libtest2.jar")
           .c_str()});
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  int file_count = 0;
  while ((cdh = input_jar.NextEntry(&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();
    }
    if (lh->file_name()[lh->file_name_length() - 1] != '/') {
      ++file_count;
    }
  }
  ASSERT_LE(4, file_count);
  input_jar.Close();
}

// Verify --java_launcher argument
TEST_F(OutputJarSimpleTest, JavaLauncher) {
  string out_path = OutputFilePath("out.jar");
  std::string launcher_path = runfiles->Rlocation(
      "io_bazel/src/tools/singlejar/libtest1.jar");
  CreateOutput(out_path, {"--java_launcher", launcher_path});
  // check that the offset of the first entry equals launcher size.
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path.c_str()));
  const LH *lh;
  const CDH *cdh;
  cdh = input_jar.NextEntry(&lh);
  ASSERT_NE(nullptr, cdh);
  struct stat statbuf;
  ASSERT_EQ(0, stat(launcher_path.c_str(), &statbuf));
  EXPECT_TRUE(cdh->is());
  EXPECT_TRUE(lh->is());
  EXPECT_EQ(static_cast<uint64_t>(statbuf.st_size), cdh->local_header_offset());
  input_jar.Close();
}

// --cds_archive option
TEST_F(OutputJarSimpleTest, CDSArchive) {
  string out_path = OutputFilePath("out.jar");
  string launcher_path = CreateTextFile("launcher", "Dummy");
  string cds_archive_path = CreateTextFile("classes.jsa", "Dummy");
  CreateOutput(out_path, {"--java_launcher", launcher_path,
                          "--cds_archive", cds_archive_path});

  // check META-INF/MANIFEST.MF attribute
  string manifest = GetEntryContents(out_path, "META-INF/MANIFEST.MF");
  size_t pagesize;
#ifndef _WIN32
  pagesize = sysconf(_SC_PAGESIZE);
#else
  SYSTEM_INFO si;
  GetSystemInfo(&si);
  pagesize = si.dwPageSize;
#endif
  char attr[128];
  snprintf(attr, sizeof(attr), "Jsa-Offset: %ld", pagesize);
  EXPECT_PRED2(HasSubstr, manifest, attr);

  // check build-data.properties entry
  string build_properties = GetEntryContents(out_path, "build-data.properties");
  char prop[4096];
  snprintf(prop, sizeof(prop), "\ncds.archive=%s\n",
           cds_archive_path.c_str());
  EXPECT_PRED2(HasSubstr, build_properties, prop);
}

// --main_class option.
TEST_F(OutputJarSimpleTest, MainClass) {
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--main_class", "com.google.my.Main"});
  string manifest = GetEntryContents(out_path, "META-INF/MANIFEST.MF");
  EXPECT_EQ(
      "Manifest-Version: 1.0\r\n"
      "Created-By: singlejar\r\n"
      "Main-Class: com.google.my.Main\r\n"
      "\r\n",
      manifest);
}

// --deploy_manifest_lines option.
TEST_F(OutputJarSimpleTest, DeployManifestLines) {
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path,
               {"--deploy_manifest_lines", "property1: foo", "property2: bar"});
  string manifest = GetEntryContents(out_path, "META-INF/MANIFEST.MF");
  EXPECT_EQ(
      "Manifest-Version: 1.0\r\n"
      "Created-By: singlejar\r\n"
      "property1: foo\r\n"
      "property2: bar\r\n"
      "\r\n",
      manifest);
}

// --extra_build_info option
TEST_F(OutputJarSimpleTest, ExtraBuildInfo) {
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--extra_build_info", "property1=value1",
                          "--extra_build_info", "property2=value2"});
  string build_properties = GetEntryContents(out_path, "build-data.properties");
  EXPECT_PRED2(HasSubstr, build_properties, "\nproperty1=value1\n");
  EXPECT_PRED2(HasSubstr, build_properties, "\nproperty2=value2\n");
}

// --build_info_file and --extra_build_info options.
TEST_F(OutputJarSimpleTest, BuildInfoFile) {
  string build_info_path1 =
      CreateTextFile("buildinfo1", "property11=value11\nproperty12=value12\n");
  string build_info_path2 =
      CreateTextFile("buildinfo2", "property21=value21\nproperty22=value22\n");

  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--build_info_file", build_info_path1,
                          "--extra_build_info", "property=value",
                          "--build_info_file", build_info_path2.c_str()});
  string build_properties = GetEntryContents(out_path, "build-data.properties");
  EXPECT_PRED2(HasSubstr, build_properties, "property11=value11\n");
  EXPECT_PRED2(HasSubstr, build_properties, "property12=value12\n");
  EXPECT_PRED2(HasSubstr, build_properties, "property21=value21\n");
  EXPECT_PRED2(HasSubstr, build_properties, "property22=value22\n");
  EXPECT_PRED2(HasSubstr, build_properties, "property=value\n");
}

// --resources option.
TEST_F(OutputJarSimpleTest, Resources) {
  string res11_path = CreateTextFile("res11", "res11.line1\nres11.line2\n");
  string res11_spec = res11_path + ":res1";

  string res12_path = CreateTextFile("res12", "res12.line1\nres12.line2\n");
  string res12_spec = res12_path + ":res1";

  string res2_path = CreateTextFile("res2", "res2.line1\nres2.line2\n");

  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--resources", res11_spec, res12_spec, res2_path});

  // The output should have 'res1' entry containing the concatenation of the
  // 'res11' and 'res12' files.
  string res1 = GetEntryContents(out_path, "res1");
  EXPECT_EQ("res11.line1\nres11.line2\nres12.line1\nres12.line2\n", res1);

  // The output should have res2 path entry and contents.
  string res2 = GetEntryContents(out_path, res2_path);
  EXPECT_EQ("res2.line1\nres2.line2\n", res2);
}

TEST_F(OutputJarSimpleTest, ResourcesParentDirectories) {
  string res1_path = CreateTextFile("res1", "res1.line1\nres1.line2\n");
  string res2_path = CreateTextFile("res2", "res2.line1\nres2.line2\n");

  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--exclude_build_data", "--resources",
                          res1_path + ":the/resources/res1",
                          res2_path + ":the/resources2/res2"});

  string res1 = GetEntryContents(out_path, "the/resources/res1");
  EXPECT_EQ("res1.line1\nres1.line2\n", res1);

  string res2 = GetEntryContents(out_path, "the/resources2/res2");
  EXPECT_EQ("res2.line1\nres2.line2\n", res2);

  // The output should contain entries for parent directories
  std::vector<string> expected_entries(
      {"META-INF/", "META-INF/MANIFEST.MF", "the/", "the/resources/",
       "the/resources/res1", "the/resources2/", "the/resources2/res2"});
  std::vector<string> jar_entries;
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    jar_entries.push_back(cdh->file_name_string());
  }
  input_jar.Close();
  EXPECT_EQ(expected_entries, jar_entries);
}

TEST_F(OutputJarSimpleTest, ResourcesDirectories) {
  string dir_path = OutputFilePath("resource_dir");
  blaze_util::MakeDirectories(dir_path, 0777);

  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path,
               {"--exclude_build_data", "--resources", dir_path + ":the/dir"});

  // The output should contain entries for the directory
  std::vector<string> expected_entries({
      "META-INF/", "META-INF/MANIFEST.MF", "the/", "the/dir/",
  });
  std::vector<string> jar_entries;
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    jar_entries.push_back(cdh->file_name_string());
  }
  input_jar.Close();
  EXPECT_EQ(expected_entries, jar_entries);
}

// --classpath_resources
TEST_F(OutputJarSimpleTest, ClasspathResources) {
  string res1_path = OutputFilePath("cp_res");
  ASSERT_TRUE(blaze_util::WriteFile("line1\nline2\n", res1_path));
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--classpath_resources", res1_path.c_str()});
  string res = GetEntryContents(out_path, "cp_res");
  EXPECT_EQ("line1\nline2\n", res);
}

// Duplicate entries for --resources or --classpath_resources
TEST_F(OutputJarSimpleTest, DuplicateResources) {
  string cp_res_path = CreateTextFile("cp_res", "line1\nline2\n");

  string res1_path = CreateTextFile("res1", "resline1\nresline2\n");
  string res1_spec = res1_path + ":foo";

  string res2_path = CreateTextFile("res2", "line3\nline4\n");
  string res2_spec = res2_path + ":foo";

  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path,
               {"--warn_duplicate_resources", "--resources", res1_spec,
                res2_spec, "--classpath_resources", cp_res_path, cp_res_path});

  string cp_res = GetEntryContents(out_path, "cp_res");
  EXPECT_EQ("line1\nline2\n", cp_res);

  string foo = GetEntryContents(out_path, "foo");
  EXPECT_EQ("resline1\nresline2\n", foo);
}

// Extra combiners
TEST_F(OutputJarSimpleTest, ExtraCombiners) {
  string resolvedLibDataPath1 = runfiles->Rlocation(kPathLibData1);
  string resolvedLibDataPath2 = runfiles->Rlocation(kPathLibData2);
  string out_path = OutputFilePath("out.jar");
  const char kEntry[] = "tools/singlejar/data/extra_file1";
  output_jar_.ExtraCombiner(kEntry, new Concatenator(kEntry));
  CreateOutput(out_path, {"--sources", resolvedLibDataPath1.c_str(),
                          resolvedLibDataPath2.c_str()});
  string contents1 = GetEntryContents(resolvedLibDataPath1.c_str(), kEntry);
  string contents2 = GetEntryContents(resolvedLibDataPath2.c_str(), kEntry);
  EXPECT_EQ(contents1 + contents2, GetEntryContents(out_path, kEntry));
}

// Test ExtraHandler override.
TEST_F(OutputJarSimpleTest, ExtraHandler) {
  string resolvedLibDataPath1 = runfiles->Rlocation(kPathLibData1);
  string resolvedLibDataPath2 = runfiles->Rlocation(kPathLibData2);
  string out_path = OutputFilePath("out.jar");
  const char kEntry[] = "tools/singlejar/data/extra_file1";
  const char *option_list[] = {"--output", out_path.c_str(), "--sources",
                               resolvedLibDataPath1.c_str(),
                               resolvedLibDataPath2.c_str()};
  CustomOutputJar custom_output_jar;
  options_.ParseCommandLine(arraysize(option_list), option_list);
  ASSERT_EQ(0, custom_output_jar.Doit(&options_));
  EXPECT_EQ(0, VerifyZip(out_path));

  string contents1 = GetEntryContents(resolvedLibDataPath1.c_str(), kEntry);
  string contents2 = GetEntryContents(resolvedLibDataPath2.c_str(), kEntry);
  EXPECT_EQ(contents1 + contents2, GetEntryContents(out_path, kEntry));
}

// --include_headers
TEST_F(OutputJarSimpleTest, IncludeHeaders) {
  string resolvedLibDataPath1 = runfiles->Rlocation(kPathLibData1);
  string out_path = OutputFilePath("out.jar");
  CreateOutput(
      out_path,
      {"--sources",
       runfiles
           ->Rlocation(
               "io_bazel/src/tools/singlejar/libtest1.jar")
           .c_str(),
       resolvedLibDataPath1.c_str(), "--include_prefixes",
       "tools/singlejar/data"});
  std::vector<string> expected_entries(
      {"META-INF/", "META-INF/MANIFEST.MF", "build-data.properties",
       "tools/singlejar/data/", "tools/singlejar/data/extra_file1",
       "tools/singlejar/data/extra_file2"});
  std::vector<string> jar_entries;
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    jar_entries.push_back(cdh->file_name_string());
  }
  input_jar.Close();
  EXPECT_EQ(expected_entries, jar_entries);
}

// --normalize
TEST_F(OutputJarSimpleTest, Normalize) {
  // Creates output jar containing entries from all possible sources:
  //  * archives created by java_library rule, by jar tool, by zip
  //  * resource files
  //  * classpath resource files
  //  *
  string out_path = OutputFilePath("out.jar");
  string testjar_path = OutputFilePath("testinput.jar");
  {
    std::string jar_tool_path = runfiles->Rlocation(JAR_TOOL_PATH);
    string textfile_path = CreateTextFile("jar_testinput.txt", "jar_inputtext");
    string classfile_path = CreateTextFile("JarTestInput.class", "Dummy");
    unlink(testjar_path.c_str());
    ASSERT_EQ(
        0, RunCommand(jar_tool_path.c_str(), "-cf", testjar_path.c_str(),
                      textfile_path.c_str(), classfile_path.c_str(), nullptr));
  }

  string testzip_path = OutputFilePath("testinput.zip");
  {
    string textfile_path = CreateTextFile("zip_testinput.txt", "zip_inputtext");
    string classfile_path = CreateTextFile("ZipTestInput.class", "Dummy");
    unlink(testzip_path.c_str());
    ASSERT_EQ(
        0, RunCommand("zip", "-m", testzip_path.c_str(), textfile_path.c_str(),
                      classfile_path.c_str(), nullptr));
  }

  string resource_path = CreateTextFile("resource", "resource_text");
  string cp_resource_path = CreateTextFile("cp_resource", "cp_resource_text");

  // TODO(asmundak): check the following generated entries, too:
  //  * services
  //  * spring.schemas
  //  * spring.handlers
  //  * protobuf.meta
  //  * extra combiner

  CreateOutput(
      out_path,
      {"--normalize", "--sources",
       runfiles
           ->Rlocation(
               "io_bazel/src/tools/singlejar/libtest1.jar")
           .c_str(),
       testjar_path, testzip_path, "--resources", resource_path,
       "--classpath_resources", cp_resource_path});

  // Scan all entries, verify that *.class entries have timestamp
  // 01/01/2010 00:00:02 and the rest have the timestamp of 01/01/2010 00:00:00.
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    string entry_name = cdh->file_name_string();
    EXPECT_EQ(lh->last_mod_file_date(), cdh->last_mod_file_date())
        << entry_name << " modification date";
    EXPECT_EQ(lh->last_mod_file_time(), cdh->last_mod_file_time())
        << entry_name << " modification time";
    EXPECT_EQ(15393, cdh->last_mod_file_date())
        << entry_name << " modification date should be 01/01/2010";
    auto n = entry_name.size() - strlen(".class");
    if (0 == strcmp(entry_name.c_str() + n, ".class")) {
      EXPECT_EQ(1, cdh->last_mod_file_time())
          << entry_name
          << " modification time for .class entry should be 00:00:02";
    } else {
      EXPECT_EQ(0, cdh->last_mod_file_time())
          << entry_name
          << " modification time for non .class entry should be 00:00:00";
    }
    // Zip creates Unix timestamps, too. Check that normalization removes them.
    ASSERT_EQ(nullptr, cdh->unix_time_extra_field())
        << entry_name << ": CDH should not have Unix Time extra field";
    ASSERT_EQ(nullptr, lh->unix_time_extra_field())
        << entry_name << ": LH should not have Unix Time extra field";
  }
  input_jar.Close();
}

// --add_missing_directories
TEST_F(OutputJarSimpleTest, AddMissingDirectories) {
  string out_path = OutputFilePath("out.jar");
  string testjar_path = OutputFilePath("testinput.jar");

  std::string jar_tool_path = runfiles->Rlocation(JAR_TOOL_PATH);
  string textfile_path =
      CreateTextFile("a/b/jar_testinput.txt", "jar_inputtext");
  string classfile1_path = CreateTextFile("a/c/Foo.class", "Dummy");
  string classfile2_path = CreateTextFile("c/Foo.class", "Dummy");
  string classfile3_path = CreateTextFile("c/Bar.class", "Dummy");
  unlink(testjar_path.c_str());
  ASSERT_EQ(
      0, RunCommand(jar_tool_path.c_str(), "-cf", testjar_path.c_str(),
                    textfile_path.c_str(), classfile1_path.c_str(),
                    classfile2_path.c_str(), classfile3_path.c_str(), nullptr));

  CreateOutput(out_path, {"--normalize", "--add_missing_directories",
                          "--sources", testjar_path});

  // Scan all entries, verify that *.class entries have timestamp
  // 01/01/2010 00:00:02 and the rest have the timestamp of 01/01/2010 00:00:00.
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  bool seen_a = false;
  bool seen_ab = false;
  bool seen_ac = false;
  bool seen_c = false;
  while ((cdh = input_jar.NextEntry(&lh))) {
    string entry_name = cdh->file_name_string();
    EXPECT_EQ(lh->last_mod_file_date(), cdh->last_mod_file_date())
        << entry_name << " modification date";
    EXPECT_EQ(lh->last_mod_file_time(), cdh->last_mod_file_time())
        << entry_name << " modification time";
    EXPECT_EQ(15393, cdh->last_mod_file_date())
        << entry_name << " modification date should be 01/01/2010";
    auto n = entry_name.size() - strlen(".class");
    if (0 == strcmp(entry_name.c_str() + n, ".class")) {
      EXPECT_EQ(1, cdh->last_mod_file_time())
          << entry_name
          << " modification time for .class entry should be 00:00:02";
    } else {
      EXPECT_EQ(0, cdh->last_mod_file_time())
          << entry_name
          << " modification time for non .class entry should be 00:00:00";
    }
    // Zip creates Unix timestamps, too. Check that normalization removes them.
    ASSERT_EQ(nullptr, cdh->unix_time_extra_field())
        << entry_name << ": CDH should not have Unix Time extra field";
    ASSERT_EQ(nullptr, lh->unix_time_extra_field())
        << entry_name << ": LH should not have Unix Time extra field";

    if (EndsWith(entry_name, "/a/")) {
      EXPECT_FALSE(seen_a) << "a/ duplicate";
      seen_a = true;
    } else if (EndsWith(entry_name, "/a/b/")) {
      EXPECT_FALSE(seen_ab) << "a/b/ duplicate";
      seen_ab = true;
    } else if (EndsWith(entry_name, "/a/c/")) {
      EXPECT_FALSE(seen_ac) << "a/c/ duplicate";
      seen_ac = true;
    } else if (EndsWith(entry_name, "/c/")) {
      EXPECT_FALSE(seen_c) << "c/ duplicate";
      seen_c = true;
    }
  }
  EXPECT_TRUE(seen_a) << "a/ entry missing";
  EXPECT_TRUE(seen_ab) << "a/b/ entry missing";
  EXPECT_TRUE(seen_ac) << "a/c/ entry missing";
  EXPECT_TRUE(seen_c) << "c/ entry missing";
  input_jar.Close();
}

// The files names META-INF/services/<something> are concatenated.
// The files named META-INF/spring.handlers are concatenated.
// The files named META-INF/spring.schemas are concatenated.
TEST_F(OutputJarSimpleTest, Services) {
  CreateTextFile("META-INF/services/spi.DateProvider",
                 "my.DateProviderImpl1\n");
  CreateTextFile("META-INF/services/spi.TimeProvider",
                 "my.TimeProviderImpl1\n");
  CreateTextFile("META-INF/spring.handlers", "handler1\n");
  CreateTextFile("META-INF/spring.schemas", "schema1\n");

  // We have to be in the output directory if we want to have entries in the
  // archive to start with META-INF. The resulting zip will contain 4 entries:
  //   META-INF/services/spi.DateProvider
  //   META-INF/services/spi.TimeProvider
  //   META-INF/spring.handlers
  //   META-INF/spring.schemas
  string out_dir = OutputFilePath("");
  ASSERT_EQ(0, RunCommand("cd", out_dir.c_str(), CMD_SEPARATOR, "zip", "-mr",
                          "testinput1.zip", "META-INF", nullptr));
  string zip1_path = OutputFilePath("testinput1.zip");

  // Create the second zip, with 3 files:
  //   META-INF/services/spi.DateProvider.
  //   META-INF/spring.handlers
  //   META-INF/spring.schemas
  CreateTextFile("META-INF/services/spi.DateProvider",
                 "my.DateProviderImpl2\n");
  CreateTextFile("META-INF/spring.handlers", "handler2\n");
  CreateTextFile("META-INF/spring.schemas", "schema2\n");
  ASSERT_EQ(0, RunCommand("cd ", out_dir.c_str(), CMD_SEPARATOR, "zip", "-mr",
                          "testinput2.zip", "META-INF", nullptr));
  string zip2_path = OutputFilePath("testinput2.zip");

  // The output jar should contain two service entries. The contents of the
  // META-INF/services/spi.DateProvider should be the concatenation of the
  // contents of this entry from both archives. And it should also contain
  // spring.handlers and spring.schemas entries.
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--sources", zip1_path, zip2_path});
  EXPECT_EQ("my.DateProviderImpl1\n" "my.DateProviderImpl2\n",
            GetEntryContents(out_path, "META-INF/services/spi.DateProvider"));
  EXPECT_EQ("my.TimeProviderImpl1\n",
            GetEntryContents(out_path, "META-INF/services/spi.TimeProvider"));

  EXPECT_EQ("schema1\n" "schema2\n",
            GetEntryContents(out_path, "META-INF/spring.schemas"));
  EXPECT_EQ("handler1\n" "handler2\n",
            GetEntryContents(out_path, "META-INF/spring.handlers"));
}

// Test that in the absence of the compression option all the plain files in
// the output archive are not compressed but just stored.
TEST_F(OutputJarSimpleTest, NoCompressionOption) {
  string out_path = CompressionOptionsTestingJar("");
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    string entry_name = lh->file_name_string();
    // Each file entry is compressed, each directory entry is uncompressed.
    EXPECT_EQ(lh->compression_method(), cdh->compression_method());
    EXPECT_EQ(Z_NO_COMPRESSION, lh->compression_method())
        << "Entry " << entry_name << " should be stored.";
  }
  input_jar.Close();
}

// Test --compression option. If enabled, all file entries are compressed
// while all directory entries remain uncompressed.
TEST_F(OutputJarSimpleTest, CompressionOption) {
  string out_path = CompressionOptionsTestingJar("--compression");
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    string entry_name = lh->file_name_string();
    // Each file entry is compressed, each directory entry is uncompressed.
    EXPECT_EQ(lh->compression_method(), cdh->compression_method());
    if (lh->file_name()[lh->file_name_length() - 1] != '/') {
      EXPECT_EQ(Z_DEFLATED, lh->compression_method())
          << "File entry " << entry_name << " should be compressed.";
    } else {
      EXPECT_EQ(Z_NO_COMPRESSION, lh->compression_method())
          << "Directory entry " << entry_name << " should be stored.";
    }
  }
  input_jar.Close();
}

// Test --dontchangecompression option. If enabled, existing file entries are
// copied as is, and created entries are compressed.
// Test --compression option. If enabled, all file entries are compressed
// while all directory entries remain uncompressed.
TEST_F(OutputJarSimpleTest, DontChangeCompressionOption) {
  string out_path = CompressionOptionsTestingJar("--dont_change_compression");
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  std::string kStoredEntry = DATA_DIR_TOP "src/tools/singlejar/output_jar.cc";

  while ((cdh = input_jar.NextEntry(&lh))) {
    string entry_name = lh->file_name_string();
    EXPECT_EQ(lh->compression_method(), cdh->compression_method());
    if (lh->file_name()[lh->file_name_length() - 1] != '/') {
      // All created file entries are compressed, and so are all the file
      // entries from the input jar created by the java_library rule. Only
      // the file entries from the 'stored_jar' should be uncompressed, and
      // it contains a single one:
      if (entry_name == kStoredEntry) {
        EXPECT_EQ(Z_NO_COMPRESSION, lh->compression_method())
            << "File entry " << entry_name << " should be stored.";
      } else {
        EXPECT_EQ(Z_DEFLATED, lh->compression_method())
            << "File entry " << entry_name << " should be compressed.";
      }
    } else {
      EXPECT_EQ(Z_NO_COMPRESSION, lh->compression_method())
          << "Directory entry " << entry_name << " should be stored.";
    }
  }
  input_jar.Close();
}

const char kBuildDataFile[] = "build-data.properties";

// Test --exclude_build_data option when none of the source archives contain
// build-data.properties file: no such file in the output archive.
TEST_F(OutputJarSimpleTest, ExcludeBuildData1) {
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--exclude_build_data"});
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    string entry_name = lh->file_name_string();
    EXPECT_NE(kBuildDataFile, lh->file_name_string());
  }
  input_jar.Close();
}

// Test --exclude_build_data option when a source archive contains
// build-data.properties file, it should be then copied to the output.
TEST_F(OutputJarSimpleTest, ExcludeBuildData2) {
  string out_dir = OutputFilePath("");
  string testzip_path = OutputFilePath("testinput.zip");
  string buildprop_path = CreateTextFile(kBuildDataFile, "build: foo");
  unlink(testzip_path.c_str());
  ASSERT_EQ(0, RunCommand("cd ", out_dir.c_str(), CMD_SEPARATOR, "zip", "-m",
                          "testinput.zip", kBuildDataFile, nullptr));
  string out_path = OutputFilePath("out.jar");
  CreateOutput(out_path, {"--exclude_build_data", "--sources", testzip_path});
  EXPECT_EQ("build: foo", GetEntryContents(out_path, kBuildDataFile));
}

// Test that the entries with suffixes in --nocompressed_suffixes are
// not compressed. This applies both to the source archives' entries and
// standalone files.
TEST_F(OutputJarSimpleTest, Nocompress) {
  string res1_path =
      CreateTextFile("resource.foo", "line1\nline2\nline3\nline4\n");
  string res2_path =
      CreateTextFile("resource.bar", "line1\nline2\nline3\nline4\n");
  string out_path = OutputFilePath("out.jar");
  CreateOutput(
      out_path,
      {"--compression", "--sources",
       runfiles
           ->Rlocation(
               "io_bazel/src/tools/singlejar/libtest1.jar")
           .c_str(),
       "--resources", res1_path, res2_path, "--nocompress_suffixes", ".foo",
       ".h"});
  InputJar input_jar;
  ASSERT_TRUE(input_jar.Open(out_path));
  const LH *lh;
  const CDH *cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    const char *entry_name_end = lh->file_name() + lh->file_name_length();
    if (!strncmp(entry_name_end - 4, ".foo", 4) ||
        !strncmp(entry_name_end - 2, ".h", 2)) {
      EXPECT_EQ(Z_NO_COMPRESSION, lh->compression_method())
          << "Expected " << lh->file_name_string() << " uncompressed";
    } else if (!strncmp(entry_name_end - 3, ".cc", 3) ||
               !strncmp(entry_name_end - 4, ".bar", 4)) {
      EXPECT_EQ(Z_DEFLATED, lh->compression_method())
          << "Expected " << lh->file_name_string() << " compressed";
    }
  }
  input_jar.Close();
}

}  // namespace
