fix(java): reject decoder push length exceeding input buffer capacity

DecoderJNI.Wrapper.push(int length) validated only length < 0. nativePush() stored input_length without an upper bound, and DecoderHandle did not retain the input buffer's allocation size, so new DecoderJNI.Wrapper(1).push(32) made BrotliDecoderDecompressStream read past the one-byte input allocation (heap out-of-bounds read).

Add an upper-bound check in Wrapper.push(), and as defense in depth store the allocation size in DecoderHandle and reject oversized input_length in nativePush(). Add DecoderJNITest covering rejection and a valid round trip.
diff --git a/java/org/brotli/wrapper/dec/BUILD.bazel b/java/org/brotli/wrapper/dec/BUILD.bazel
index e4edcf9..db7b341 100644
--- a/java/org/brotli/wrapper/dec/BUILD.bazel
+++ b/java/org/brotli/wrapper/dec/BUILD.bazel
@@ -104,3 +104,16 @@
     test_class = "org.brotli.wrapper.dec.CornerCasesTest",
     runtime_deps = [":test_lib"],
 )
+
+java_test(
+    name = "DecoderJNITest",
+    size = "large",
+    data = [
+        ":brotli_jni",  # Bazel JNI workaround
+    ],
+    jvm_flags = [
+        "-DBROTLI_JNI_LIBRARY=$(location :brotli_jni)",
+    ],
+    test_class = "org.brotli.wrapper.dec.DecoderJNITest",
+    runtime_deps = [":test_lib"],
+)
diff --git a/java/org/brotli/wrapper/dec/DecoderJNI.java b/java/org/brotli/wrapper/dec/DecoderJNI.java
index 726a6d6..01e07ba 100644
--- a/java/org/brotli/wrapper/dec/DecoderJNI.java
+++ b/java/org/brotli/wrapper/dec/DecoderJNI.java
@@ -58,6 +58,9 @@
       if (length < 0) {
         throw new IllegalArgumentException("negative block length");
       }
+      if (length > inputBuffer.capacity()) {
+        throw new IllegalArgumentException("block length exceeds input buffer capacity");
+      }
       if (context[0] == 0) {
         throw new IllegalStateException("brotli decoder is already destroyed");
       }
diff --git a/java/org/brotli/wrapper/dec/DecoderJNITest.java b/java/org/brotli/wrapper/dec/DecoderJNITest.java
new file mode 100644
index 0000000..99faf8f
--- /dev/null
+++ b/java/org/brotli/wrapper/dec/DecoderJNITest.java
@@ -0,0 +1,102 @@
+/* Copyright 2025 Google Inc. All Rights Reserved.
+
+   Distributed under MIT license.
+   See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
+*/
+
+package org.brotli.wrapper.dec;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.brotli.integration.BrotliJniTestBase;
+import org.brotli.wrapper.enc.Encoder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link org.brotli.wrapper.dec.DecoderJNI}. */
+@RunWith(JUnit4.class)
+public class DecoderJNITest extends BrotliJniTestBase {
+
+  @Test
+  public void testPushLengthExceedingInputBufferIsRejected() throws IOException {
+    DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(1);
+    try {
+      decoder.getInputBuffer().put(0, (byte) 0);
+      try {
+        decoder.push(2);
+        fail("push(length > inputBufferSize) must be rejected");
+      } catch (IllegalArgumentException expected) {
+        // Expected: oversized length would read past the input buffer.
+      }
+    } finally {
+      decoder.destroy();
+    }
+  }
+
+  @Test
+  public void testPushNegativeLengthIsRejected() throws IOException {
+    DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(4);
+    try {
+      try {
+        decoder.push(-1);
+        fail("push(negative) must be rejected");
+      } catch (IllegalArgumentException expected) {
+        // Expected.
+      }
+    } finally {
+      decoder.destroy();
+    }
+  }
+
+  @Test
+  public void testPushLengthEqualToInputBufferRoundTrips() throws IOException {
+    byte[] data = new byte[64];
+    for (int i = 0; i < data.length; i++) {
+      data[i] = (byte) (i * 7);
+    }
+    byte[] compressed = Encoder.compress(data);
+
+    DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(compressed.length);
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    try {
+      decoder.getInputBuffer().put(compressed);
+      decoder.push(compressed.length);
+      int iterations = 0;
+      while (decoder.getStatus() != DecoderJNI.Status.DONE) {
+        if (++iterations > 1000) {
+          fail("decoder did not finish within iteration bound");
+        }
+        switch (decoder.getStatus()) {
+          case OK:
+            decoder.push(0);
+            break;
+
+          case NEEDS_MORE_OUTPUT:
+            ByteBuffer buffer = decoder.pull();
+            byte[] chunk = new byte[buffer.remaining()];
+            buffer.get(chunk);
+            output.write(chunk, 0, chunk.length);
+            break;
+
+          case NEEDS_MORE_INPUT:
+            decoder.push(0);
+            if (decoder.getStatus() == DecoderJNI.Status.NEEDS_MORE_INPUT) {
+              fail("unexpected truncated stream");
+            }
+            break;
+
+          default:
+            fail("unexpected decoder status: " + decoder.getStatus());
+        }
+      }
+    } finally {
+      decoder.destroy();
+    }
+    assertArrayEquals(data, output.toByteArray());
+  }
+}
diff --git a/java/org/brotli/wrapper/dec/decoder_jni.cc b/java/org/brotli/wrapper/dec/decoder_jni.cc
index a92d34c..1004bab 100644
--- a/java/org/brotli/wrapper/dec/decoder_jni.cc
+++ b/java/org/brotli/wrapper/dec/decoder_jni.cc
@@ -24,6 +24,7 @@
   size_t dictionary_count;
 
   uint8_t* input_start;
+  size_t input_size;
   size_t input_offset;
   size_t input_length;
 } DecoderHandle;
@@ -67,6 +68,7 @@
     handle->dictionary_count = 0;
     handle->input_offset = 0;
     handle->input_length = 0;
+    handle->input_size = 0;
     handle->input_start = nullptr;
 
     if (input_size == 0) {
@@ -74,6 +76,9 @@
     } else {
       handle->input_start = new (std::nothrow) uint8_t[input_size];
       ok = !!handle->input_start;
+      if (ok) {
+        handle->input_size = input_size;
+      }
     }
   }
 
@@ -125,6 +130,11 @@
   env->functions->SetLongArrayRegion(env, ctx, 0, 3, context);
 
   if (input_length != 0) {
+    /* Reject input length that exceeds the allocated input buffer. */
+    if (input_length < 0 ||
+        static_cast<size_t>(input_length) > handle->input_size) {
+      return;
+    }
     /* Still have unconsumed data. Workflow is broken. */
     if (handle->input_offset < handle->input_length) {
       return;