Slightly relax Java Poison Pill on prerelease versions (-rc1, -dev, etc).

Before this, the poison pill intended to require exact match including suffix if there were suffixes on either gencode or runtime.

After this change:
- If gencode is suffixed, it must be exact match to runtime (including suffix).

- If runtime is suffixed and gencode is not suffixed, the gencode must be a strictly lower major.minor.point (4.32.1-rc1 runtime must be on <= 4.32.0 gencode). We log a warning once if this allowed inequality state happens.

This change also fixes a bug where a lower minor version was allowed if it did have the same suffix (4.33.0-rc1 runtime accidentally accepted 4.32.0-rc1 gencode while still rejecting 4.32.0).

PiperOrigin-RevId: 813286207
diff --git a/java/core/src/main/java/com/google/protobuf/RuntimeVersion.java b/java/core/src/main/java/com/google/protobuf/RuntimeVersion.java
index e6cbd81..f9bba40 100644
--- a/java/core/src/main/java/com/google/protobuf/RuntimeVersion.java
+++ b/java/core/src/main/java/com/google/protobuf/RuntimeVersion.java
@@ -46,6 +46,9 @@
   @SuppressWarnings("NonFinalStaticField")
   static int minorWarningLoggedCount = 0;
 
+  @SuppressWarnings("NonFinalStaticField")
+  static boolean preleaseRuntimeWarningLogged = false;
+
   private static final String VERSION_STRING = versionString(MAJOR, MINOR, PATCH, SUFFIX);
   private static final Logger logger = Logger.getLogger(RuntimeVersion.class.getName());
 
@@ -117,6 +120,26 @@
 
     String gencodeVersionString = null;
 
+    if (!SUFFIX.isEmpty() && !preleaseRuntimeWarningLogged) {
+      if (gencodeVersionString == null) {
+        gencodeVersionString = versionString(major, minor, patch, suffix);
+      }
+      logger.warning(
+          String.format(
+              Locale.US,
+              " Protobuf prelease version %s in use. This is not recommended for "
+                  + "production use.\n"
+                  + " You can ignore this message if you are deliberately testing a prerelease."
+                  + " Otherwise you should switch to a non-prerelease Protobuf version.",
+              VERSION_STRING));
+      preleaseRuntimeWarningLogged = true;
+    }
+
+    // Exact match is always good.
+    if (major == MAJOR && minor == MINOR && patch == PATCH && suffix.equals(SUFFIX)) {
+      return;
+    }
+
     // Check that runtime major version is the same as the gencode major version.
     if (major != MAJOR) {
       if (major == MAJOR - 1 && majorWarningLoggedCount < MAX_WARNING_COUNT) {
@@ -158,8 +181,15 @@
               VERSION_STRING));
     }
 
-    // Check that runtime version suffix is the same as the gencode version suffix.
-    if (!suffix.equals(SUFFIX)) {
+    // If neither gencode or runtime has a suffix we're done.
+    if (suffix.isEmpty() && SUFFIX.isEmpty()) {
+      return;
+    }
+
+    // If gencode has any suffix, we only support exact matching runtime including suffix. Exact
+    // match including suffix was already checked above so if we get this far and the gencode has a
+    // suffix it is a disallowed combination.
+    if (!suffix.isEmpty()) {
       if (gencodeVersionString == null) {
         gencodeVersionString = versionString(major, minor, patch, suffix);
       }
@@ -167,7 +197,28 @@
           String.format(
               Locale.US,
               "Detected mismatched Protobuf Gencode/Runtime version suffixes when loading %s:"
-                  + " gencode %s, runtime %s. Version suffixes must be the same.",
+                  + " gencode %s, runtime %s. Prerelease gencode must be used with the same"
+                  + " runtime.",
+              location,
+              gencodeVersionString,
+              VERSION_STRING));
+    }
+
+    // Here we know the runtime version is suffixed and the gencode version is not. If the
+    // major.minor.patch is exact match, its an illegal combination (eg 4.32.0-rc1 runtime with
+    // 4.32.0 gencode). If major.minor.patch is not an exact match, then the gencode is a lower
+    // version (e.g 4.32.0-rc1 runtime with 4.31.0 gencode) which is allowed.
+    if (major == MAJOR && minor == MINOR && patch == PATCH) {
+      if (gencodeVersionString == null) {
+        gencodeVersionString = versionString(major, minor, patch, suffix);
+      }
+      throw new ProtobufRuntimeVersionException(
+          String.format(
+              Locale.US,
+              "Detected mismatched Protobuf Gencode/Runtime version suffixes when loading %s:"
+                  + " gencode %s, runtime %s. Prelease runtimes must only be used with exact match"
+                  + " gencode (including suffix) or non-prerelease gencode versions of a"
+                  + " lower version.",
               location,
               gencodeVersionString,
               VERSION_STRING));
diff --git a/java/core/src/test/java/com/google/protobuf/RuntimeVersionTest.java b/java/core/src/test/java/com/google/protobuf/RuntimeVersionTest.java
index 0f5edbf..521694b 100644
--- a/java/core/src/test/java/com/google/protobuf/RuntimeVersionTest.java
+++ b/java/core/src/test/java/com/google/protobuf/RuntimeVersionTest.java
@@ -77,18 +77,6 @@
   }
 
   @Test
-  public void versionValidation_newerRuntimeVersionAllowed() {
-    int gencodeMinor = RuntimeVersion.MINOR - 1;
-    RuntimeVersion.validateProtobufGencodeVersion(
-        RuntimeVersion.DOMAIN,
-        RuntimeVersion.MAJOR,
-        gencodeMinor,
-        RuntimeVersion.PATCH,
-        RuntimeVersion.SUFFIX,
-        "dummy");
-  }
-
-  @Test
   public void versionValidation_olderRuntimeVersionDisallowed() {
     int gencodeMinor = RuntimeVersion.MINOR + 1;
     RuntimeVersion.ProtobufRuntimeVersionException thrown =
@@ -147,7 +135,106 @@
   }
 
   @Test
+  public void versionValidation_suffixedRuntime_logsWarning() {
+    // We can only execute this test if the test runtime does have a suffix (which is nearly always
+    // for our OSS continuous tests).
+    if (RuntimeVersion.SUFFIX.isEmpty()) {
+      return;
+    }
+
+    // Suffixed runtimes only log the message once. To force the warning to be logged for
+    // this test unrelated to test order, flip the bool back to false if it had been flipped by
+    // another test to ensure the intended log can be observed.
+    RuntimeVersion.preleaseRuntimeWarningLogged = false;
+
+    TestUtil.TestLogHandler logHandler = new TestUtil.TestLogHandler();
+    Logger logger = Logger.getLogger(RuntimeVersion.class.getName());
+    logger.addHandler(logHandler);
+    RuntimeVersion.validateProtobufGencodeVersion(
+        RuntimeVersion.DOMAIN,
+        RuntimeVersion.MAJOR,
+        RuntimeVersion.MINOR,
+        RuntimeVersion.PATCH,
+        RuntimeVersion.SUFFIX,
+        "dummy");
+    assertThat(logHandler.getStoredLogRecords()).hasSize(1);
+    assertThat(logHandler.getStoredLogRecords().get(0).getMessage())
+        .contains("You can ignore this message if you are deliberately testing a prerelease.");
+  }
+
+  @Test
+  public void versionValidation_suffixedRuntime_sameSuffixLowerMinorDisallowed() {
+    // We can only execute this test if the test runtime does have a suffix (which is nearly always
+    // for our OSS continuous tests).
+    if (RuntimeVersion.SUFFIX.isEmpty()) {
+      return;
+    }
+    RuntimeVersion.ProtobufRuntimeVersionException thrown =
+        assertThrows(
+            RuntimeVersion.ProtobufRuntimeVersionException.class,
+            () ->
+                RuntimeVersion.validateProtobufGencodeVersion(
+                    RuntimeVersion.DOMAIN,
+                    RuntimeVersion.MAJOR,
+                    RuntimeVersion.MINOR - 1,
+                    RuntimeVersion.PATCH,
+                    RuntimeVersion.SUFFIX,
+                    "testing.Foo"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "Detected mismatched Protobuf Gencode/Runtime version suffixes when loading"
+                + " testing.Foo");
+  }
+
+  @Test
+  public void versionValidation_suffixedRuntime_sameNumbersNoSuffixDisallowed() {
+    // We can only execute this test if the test runtime does have a suffix (which is nearly always
+    // for our OSS continuous tests).
+    if (RuntimeVersion.SUFFIX.isEmpty()) {
+      return;
+    }
+    RuntimeVersion.ProtobufRuntimeVersionException thrown =
+        assertThrows(
+            RuntimeVersion.ProtobufRuntimeVersionException.class,
+            () ->
+                RuntimeVersion.validateProtobufGencodeVersion(
+                    RuntimeVersion.DOMAIN,
+                    RuntimeVersion.MAJOR,
+                    RuntimeVersion.MINOR,
+                    RuntimeVersion.PATCH,
+                    "",
+                    "testing.Foo"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(
+            "Detected mismatched Protobuf Gencode/Runtime version suffixes when loading"
+                + " testing.Foo");
+  }
+
+  @Test
+  public void versionValidation_suffixedRuntime_allowedLowerVersionWarns() {
+    // We can only execute this test if the runtime does have a suffix (which is nearly always for
+    // our OSS continuous tests).
+    if (RuntimeVersion.SUFFIX.isEmpty()) {
+      return;
+    }
+    RuntimeVersion.validateProtobufGencodeVersion(
+        RuntimeVersion.DOMAIN,
+        RuntimeVersion.MAJOR,
+        RuntimeVersion.MINOR - 1,
+        RuntimeVersion.PATCH,
+        "",
+        "testing.Foo");
+  }
+
+  @Test
   public void versionValidation_gencodeOneMajorVersionOlderWarning() {
+    // Hack: if this is a suffixed runtime it may log the prerelease warning here if this
+    // is the first test to run. Force the bool to true to avoid the warning happening during
+    // this test only if it was the first one run.
+    RuntimeVersion.preleaseRuntimeWarningLogged = true;
+
     TestUtil.TestLogHandler logHandler = new TestUtil.TestLogHandler();
     Logger logger = Logger.getLogger(RuntimeVersion.class.getName());
     logger.addHandler(logHandler);
@@ -156,7 +243,7 @@
         RuntimeVersion.MAJOR - 1,
         RuntimeVersion.MINOR,
         RuntimeVersion.PATCH,
-        RuntimeVersion.SUFFIX,
+        "",
         "dummy");
     assertThat(logHandler.getStoredLogRecords()).hasSize(1);
     assertThat(logHandler.getStoredLogRecords().get(0).getMessage())