Ignore `Multi-Release:` attributes in input jars

PiperOrigin-RevId: 436332447
diff --git a/src/tools/singlejar/BUILD b/src/tools/singlejar/BUILD
index 39ff13a..625f006 100644
--- a/src/tools/singlejar/BUILD
+++ b/src/tools/singlejar/BUILD
@@ -117,11 +117,15 @@
         ":zip_headers",
         ":zlib_interface",
     ],
+    data = [
+        "data/multi_release.jar",
+    ],
     # Requires at least 5 GiB of memory
     exec_compatible_with = ["//:highcpu_machine"],
     deps = [
         ":combiners",
         ":input_jar",
+        ":test_util",
         "//third_party/zlib",
         "@com_google_googletest//:gtest_main",
     ],
diff --git a/src/tools/singlejar/combiners.cc b/src/tools/singlejar/combiners.cc
index fa10f76..ec84c27 100644
--- a/src/tools/singlejar/combiners.cc
+++ b/src/tools/singlejar/combiners.cc
@@ -179,3 +179,44 @@
 bool PropertyCombiner::Merge(const CDH * /*cdh*/, const LH * /*lh*/) {
   return false;  // This should not be called.
 }
+
+ManifestCombiner::~ManifestCombiner() {}
+
+static const char *MULTI_RELEASE = "Multi-Release: true";
+static const size_t MULTI_RELEASE_LENGTH = strlen(MULTI_RELEASE);
+
+static const char *MULTI_RELEASE_PREFIX = "Multi-Release: ";
+static const size_t MULTI_RELEASE_PREFIX_LENGTH = strlen(MULTI_RELEASE_PREFIX);
+
+void ManifestCombiner::AppendLine(const std::string &line) {
+  if (line.find(MULTI_RELEASE_PREFIX, 0, MULTI_RELEASE_PREFIX_LENGTH) !=
+      std::string::npos) {
+    if (line.find("true", MULTI_RELEASE_PREFIX_LENGTH) != std::string::npos) {
+      multi_release_ = true;
+    } else if (line.find("false", MULTI_RELEASE_PREFIX_LENGTH) !=
+               std::string::npos) {
+      multi_release_ = false;
+    }
+    return;
+  }
+  concatenator_->Append(line);
+  if (line[line.size() - 1] != '\n') {
+    concatenator_->Append("\r\n");
+  }
+}
+
+bool ManifestCombiner::Merge(const CDH *cdh, const LH *lh) {
+  // Ignore Multi-Release attributes in inputs: we write the manifest first,
+  // before inputs are processed, so we reply on  deploy_manifest_lines to
+  // create Multi-Release jars instead of doing it automatically based on
+  // the inputs.
+  return true;
+}
+
+void *ManifestCombiner::OutputEntry(bool compress) {
+  if (multi_release_) {
+    concatenator_->Append(MULTI_RELEASE);
+  }
+  concatenator_->Append("\r\n");
+  return concatenator_->OutputEntry(compress);
+}
diff --git a/src/tools/singlejar/combiners.h b/src/tools/singlejar/combiners.h
index 28dbbad..a8e372e 100644
--- a/src/tools/singlejar/combiners.h
+++ b/src/tools/singlejar/combiners.h
@@ -136,4 +136,28 @@
   }
 };
 
+// Combines the contents of the multiple manifests.
+class ManifestCombiner : public Combiner {
+ public:
+  ManifestCombiner(const std::string &filename)
+      : filename_(filename), multi_release_(false) {
+    concatenator_.reset(new Concatenator(filename_, false));
+  }
+  ~ManifestCombiner() override;
+
+  void AppendLine(const std::string &line);
+
+  bool Merge(const CDH *cdh, const LH *lh) override;
+
+  void *OutputEntry(bool compress) override;
+
+  const std::string filename() const { return filename_; }
+
+ private:
+  std::unique_ptr<Concatenator> concatenator_;
+  const std::string filename_;
+  bool multi_release_;
+  std::unique_ptr<Inflater> inflater_;
+};
+
 #endif  //  SRC_TOOLS_SINGLEJAR_COMBINERS_H_
diff --git a/src/tools/singlejar/combiners_test.cc b/src/tools/singlejar/combiners_test.cc
index 6efde7b..d5df6e5 100644
--- a/src/tools/singlejar/combiners_test.cc
+++ b/src/tools/singlejar/combiners_test.cc
@@ -15,22 +15,30 @@
 #include "src/tools/singlejar/combiners.h"
 
 #include "src/tools/singlejar/input_jar.h"
+#include "src/tools/singlejar/test_util.h"
 #include "src/tools/singlejar/zip_headers.h"
 #include "src/tools/singlejar/zlib_interface.h"
 #include "googletest/include/gtest/gtest.h"
 
 namespace {
 
+using bazel::tools::cpp::runfiles::Runfiles;
+
 static const char kTag1Contents[] = "<tag1>Contents1</tag1>";
 static const char kTag2Contents[] = "<tag2>Contents2</tag2>";
 static const char kCombinedXmlContents[] =
     "<toplevel>\n<tag1>Contents1</tag1><tag2>Contents2</tag2></toplevel>\n";
 static const char kConcatenatedContents[] =
     "<tag1>Contents1</tag1>\n<tag2>Contents2</tag2>";
+const char kCombinedManifestContents[] = "Multi-Release: true\r\n";
+const char kCombinedManifestContentsDisabled[] = "\r\n";
 const uint8_t kPoison = 0xFA;
 
 // A test fixture is used because test case setup is needed.
 class CombinersTest : public ::testing::Test {
+ public:
+  void SetUp() override { runfiles.reset(Runfiles::CreateForTest()); }
+
  protected:
   static void SetUpTestCase() {
     ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR")));
@@ -51,6 +59,8 @@
     }
     return true;
   }
+
+  std::unique_ptr<Runfiles> runfiles;
 };
 
 // Test Concatenator.
@@ -246,4 +256,120 @@
   free(reinterpret_cast<void *>(entry));
 }
 
+// Test ManifestCombiner.
+TEST_F(CombinersTest, ManifestCombiner) {
+  InputJar input_jar;
+  ManifestCombiner manifest_combiner("META-INF/MANIFEST.MF");
+  ASSERT_TRUE(
+      input_jar.Open(runfiles
+                         ->Rlocation("io_bazel/src/tools/"
+                                     "singlejar/data/multi_release.jar")
+                         .c_str()));
+  const LH *lh;
+  const CDH *cdh;
+  while ((cdh = input_jar.NextEntry(&lh))) {
+    if (cdh->file_name_is("META-INF/MANIFEST.MF")) {
+      ASSERT_TRUE(manifest_combiner.Merge(cdh, lh));
+    }
+  }
+
+  // check that Multi-Release is de-duped, e.g. if present both in deps and
+  // deploy_manifest_lines
+  manifest_combiner.AppendLine("Multi-Release: true");
+
+  // Create output, verify Local Header contents.
+  LH *entry = reinterpret_cast<LH *>(manifest_combiner.OutputEntry(true));
+  EXPECT_TRUE(entry->is());
+  EXPECT_EQ(20, entry->version());
+  EXPECT_EQ(Z_NO_COMPRESSION, entry->compression_method());
+  uint64_t original_size = entry->uncompressed_file_size();
+  uint64_t compressed_size = entry->compressed_file_size();
+  EXPECT_EQ(strlen(kCombinedManifestContents), original_size);
+  EXPECT_LE(compressed_size, original_size);
+  EXPECT_TRUE(entry->file_name_is("META-INF/MANIFEST.MF"));
+  EXPECT_EQ(0, entry->extra_fields_length());
+
+  // Check contents.
+  EXPECT_EQ(
+      kCombinedManifestContents,
+      std::string(reinterpret_cast<char *>(entry->data()), original_size));
+  free(reinterpret_cast<void *>(entry));
+}
+
+// Test ManifestCombiner.
+TEST_F(CombinersTest, ManifestCombinerJarOnly) {
+  InputJar input_jar;
+  ManifestCombiner manifest_combiner("META-INF/MANIFEST.MF");
+  ASSERT_TRUE(
+      input_jar.Open(runfiles
+                         ->Rlocation("io_bazel/src/tools/"
+                                     "singlejar/data/multi_release.jar")
+                         .c_str()));
+  const LH *lh;
+  const CDH *cdh;
+  while ((cdh = input_jar.NextEntry(&lh))) {
+    if (cdh->file_name_is("META-INF/MANIFEST.MF")) {
+      ASSERT_TRUE(manifest_combiner.Merge(cdh, lh));
+    }
+  }
+
+  // check that Multi-Release is ignored if present only in deps
+
+  // Create output, verify Local Header contents.
+  LH *entry = reinterpret_cast<LH *>(manifest_combiner.OutputEntry(true));
+  EXPECT_TRUE(entry->is());
+  EXPECT_EQ(20, entry->version());
+  EXPECT_EQ(Z_NO_COMPRESSION, entry->compression_method());
+  uint64_t original_size = entry->uncompressed_file_size();
+  uint64_t compressed_size = entry->compressed_file_size();
+  EXPECT_EQ(strlen(kCombinedManifestContentsDisabled), original_size);
+  EXPECT_LE(compressed_size, original_size);
+  EXPECT_TRUE(entry->file_name_is("META-INF/MANIFEST.MF"));
+  EXPECT_EQ(0, entry->extra_fields_length());
+
+  // Check contents.
+  EXPECT_EQ(
+      kCombinedManifestContentsDisabled,
+      std::string(reinterpret_cast<char *>(entry->data()), original_size));
+  free(reinterpret_cast<void *>(entry));
+}
+
+TEST_F(CombinersTest, ManifestCombinerFalse) {
+  InputJar input_jar;
+  ManifestCombiner manifest_combiner("META-INF/MANIFEST.MF");
+  ASSERT_TRUE(
+      input_jar.Open(runfiles
+                         ->Rlocation("io_bazel/src/tools/"
+                                     "singlejar/data/multi_release.jar")
+                         .c_str()));
+  const LH *lh;
+  const CDH *cdh;
+  while ((cdh = input_jar.NextEntry(&lh))) {
+    if (cdh->file_name_is("META-INF/MANIFEST.MF")) {
+      ASSERT_TRUE(manifest_combiner.Merge(cdh, lh));
+    }
+  }
+
+  // check that deploy_manifest_lines can disable the setting in input jars
+  manifest_combiner.AppendLine("Multi-Release: false");
+
+  // Create output, verify Local Header contents.
+  LH *entry = reinterpret_cast<LH *>(manifest_combiner.OutputEntry(true));
+  EXPECT_TRUE(entry->is());
+  EXPECT_EQ(20, entry->version());
+  EXPECT_EQ(Z_NO_COMPRESSION, entry->compression_method());
+  uint64_t original_size = entry->uncompressed_file_size();
+  uint64_t compressed_size = entry->compressed_file_size();
+  EXPECT_EQ(strlen(kCombinedManifestContentsDisabled), original_size);
+  EXPECT_LE(compressed_size, original_size);
+  EXPECT_TRUE(entry->file_name_is("META-INF/MANIFEST.MF"));
+  EXPECT_EQ(0, entry->extra_fields_length());
+
+  // Check contents.
+  EXPECT_EQ(
+      kCombinedManifestContentsDisabled,
+      std::string(reinterpret_cast<char *>(entry->data()), original_size));
+  free(reinterpret_cast<void *>(entry));
+}
+
 }  // anonymous namespace
diff --git a/src/tools/singlejar/data/multi_release.jar b/src/tools/singlejar/data/multi_release.jar
new file mode 100644
index 0000000..6976b3e
--- /dev/null
+++ b/src/tools/singlejar/data/multi_release.jar
Binary files differ
diff --git a/src/tools/singlejar/output_jar.cc b/src/tools/singlejar/output_jar.cc
index 147fcce..2b03d736 100644
--- a/src/tools/singlejar/output_jar.cc
+++ b/src/tools/singlejar/output_jar.cc
@@ -72,9 +72,8 @@
   known_members_.emplace(manifest_.filename(), EntryInfo{&manifest_});
   known_members_.emplace(protobuf_meta_handler_.filename(),
                          EntryInfo{&protobuf_meta_handler_});
-  manifest_.Append(
-      "Manifest-Version: 1.0\r\n"
-      "Created-By: singlejar\r\n");
+  manifest_.AppendLine("Manifest-Version: 1.0");
+  manifest_.AppendLine("Created-By: singlejar");
 }
 
 static std::string Basename(const std::string &path) {
@@ -127,9 +126,7 @@
 
   if (!options_->main_class.empty()) {
     build_properties_.AddProperty("main.class", options_->main_class);
-    manifest_.Append("Main-Class: ");
-    manifest_.Append(options_->main_class);
-    manifest_.Append("\r\n");
+    manifest_.AppendLine("Main-Class: " + options_->main_class);
   }
 
   // Copy CDS archive file (.jsa) if it is set.
@@ -139,10 +136,7 @@
 
   for (auto &manifest_line : options_->manifest_lines) {
     if (!manifest_line.empty()) {
-      manifest_.Append(manifest_line);
-      if (manifest_line[manifest_line.size() - 1] != '\n') {
-        manifest_.Append("\r\n");
-      }
+      manifest_.AppendLine(manifest_line);
     }
   }
 
@@ -206,7 +200,6 @@
   // First, write a directory entry for the META-INF, followed by the manifest
   // file, followed by the build properties file.
   WriteMetaInf();
-  manifest_.Append("\r\n");
   WriteEntry(manifest_.OutputEntry(compress));
   if (!options_->exclude_build_data) {
     WriteEntry(build_properties_.OutputEntry(compress));
@@ -1056,8 +1049,7 @@
   snprintf( cds_manifest_attr, sizeof(cds_manifest_attr),
     "Jsa-Offset: %ld", (long)aligned_offset); // NOLINT(runtime/int,
                                               // google-runtime-int)
-  manifest_.Append(cds_manifest_attr);
-  manifest_.Append("\r\n");
+  manifest_.AppendLine(cds_manifest_attr);
 
   // Add to build_properties
   build_properties_.AddProperty("cds.archive",
diff --git a/src/tools/singlejar/output_jar.h b/src/tools/singlejar/output_jar.h
index a7ceb35..21a93fa 100644
--- a/src/tools/singlejar/output_jar.h
+++ b/src/tools/singlejar/output_jar.h
@@ -128,7 +128,7 @@
   Concatenator spring_handlers_;
   Concatenator spring_schemas_;
   Concatenator protobuf_meta_handler_;
-  Concatenator manifest_;
+  ManifestCombiner manifest_;
   PropertyCombiner build_properties_;
   NullCombiner null_combiner_;
   std::vector<std::unique_ptr<Concatenator> > service_handlers_;