This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git

commit d106a40cae3db465cbfb3d22f05dbb96cdd90153
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 18 05:09:57 2026 -0700

    Limit Base58 decoding input length
    
    Add a configurable 8192-byte default limit and reject oversized input
    before
    buffering or conversion. Cover stream boundaries and overflow checks,
    update
    large-input tests, and document the limit and configuration options.
---
 src/changes/changes.xml                            |   1 +
 .../org/apache/commons/codec/binary/Base58.java    |  72 +++++++-
 .../commons/codec/binary/Base58InputStream.java    |  35 ++--
 .../commons/codec/binary/Base58OutputStream.java   |  41 ++---
 .../codec/binary/Base58MaxDecodeLengthTest.java    | 181 +++++++++++++++++++++
 .../apache/commons/codec/binary/Base58Test.java    |   6 +-
 6 files changed, 276 insertions(+), 60 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 76545139..9a311b3b 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -55,6 +55,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Throw 
DecoderException instead of IllegalArgumentException in 
RFC1522Codec.decodeText(String).</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Fix Blake3 KDF 
example and clarify finalization semantics.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Base32 and 
Base64 STRICT decoding now require the encoder's canonical alphabet, padding, 
and line separators, and validate streams through EOF. Use LENIENT to retain 
permissive decoding.</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Limit Base58 
decoding to 8192 encoded bytes by default, checking cumulative stream input 
before buffering. Use Base58.Builder.setMaxDecodeLength(int) for larger trusted 
input. Encoding remains unlimited.</action>
       <!-- ADD -->
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add and use 
PhoneticEngine.Builder and deprecate old constructors.</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
BeiderMorseEncoder.Builder and deprecate old constructor.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/Base58.java 
b/src/main/java/org/apache/commons/codec/binary/Base58.java
index 126a9b64..acbeebf1 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58.java
@@ -28,8 +28,12 @@ import java.util.function.BiConsumer;
  * commonly used in Bitcoin and other blockchain systems.
  * </p>
  * <p>
- * This implementation accumulates data internally until EOF is signaled, at 
which point the entire input is converted using BigInteger arithmetic. This is
- * necessary because Base58 encoding/decoding requires access to the complete 
data to properly handle leading zeros.
+ * Encoding and decoding produce results when EOF is signaled.
+ * </p>
+ * <p>
+ * Decoding rejects input longer than a configurable maximum ({@link 
#DEFAULT_MAX_DECODE_LENGTH} encoded bytes by default, see
+ * {@link Builder#setMaxDecodeLength(int)}). Encoding is not limited: callers 
should bound untrusted binary input before encoding. Encoded output
+ * can exceed the default decode limit; raise the limit explicitly when 
decoding larger trusted values.
  * </p>
  * <p>
  * This class is thread-safe for read operations but the Context object used 
during encoding/decoding should not be shared between threads.
@@ -57,6 +61,8 @@ public class Base58 extends BaseNCodec {
      */
     public static class Builder extends AbstractBuilder<Base58, Builder> {
 
+        private int maxDecodeLength = DEFAULT_MAX_DECODE_LENGTH;
+
         /**
          * Constructs a new Base58 builder.
          */
@@ -75,6 +81,10 @@ public class Base58 extends BaseNCodec {
             return new Base58(this);
         }
 
+        int getMaxDecodeLength() {
+            return maxDecodeLength;
+        }
+
         /**
          * Sets the encode table and derives the matching decode table.
          *
@@ -87,12 +97,41 @@ public class Base58 extends BaseNCodec {
             super.setDecodeTableRaw(toDecodeTable(encodeTable));
             return super.setEncodeTable(encodeTable);
         }
+
+        /**
+         * Sets the maximum number of encoded bytes accepted by a single 
decode operation.
+         * <p>
+         * Defaults to {@link Base58#DEFAULT_MAX_DECODE_LENGTH}. Pass {@link 
Integer#MAX_VALUE} to effectively disable the limit for trusted input.
+         * </p>
+         *
+         * @param maxDecodeLength The maximum accepted encoded input length; 
must be positive.
+         * @return {@code this} instance.
+         * @throws IllegalArgumentException if maxDecodeLength is not positive.
+         * @since 1.23.0
+         */
+        public Builder setMaxDecodeLength(final int maxDecodeLength) {
+            if (maxDecodeLength <= 0) {
+                throw new IllegalArgumentException("maxDecodeLength must be 
positive.");
+            }
+            this.maxDecodeLength = maxDecodeLength;
+            return this;
+        }
     }
     private static final BigInteger BASE = BigInteger.valueOf(58);
 
     private static final int DECODING_TABLE_LENGTH = 256;
     private static final int ENCODING_TABLE_LENGTH = 58;
 
+    /**
+     * The default maximum number of encoded bytes accepted by a single decode 
operation: {@value}.
+     * <p>
+     * Use {@link Builder#setMaxDecodeLength(int)} to raise (or effectively 
disable) the limit for trusted input.
+     * </p>
+     *
+     * @since 1.23.0
+     */
+    public static final int DEFAULT_MAX_DECODE_LENGTH = 8192;
+
     /**
      * Base58 alphabet: 
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
      * (excludes: 0, I, O, l).
@@ -179,6 +218,11 @@ public class Base58 extends BaseNCodec {
         return calculateDecodeTable(table);
     }
 
+    /**
+     * The maximum number of encoded bytes accepted by a single decode 
operation.
+     */
+    private final int maxDecodeLength;
+
     /**
      * Constructs a Base58 codec used for encoding and decoding.
      */
@@ -193,6 +237,14 @@ public class Base58 extends BaseNCodec {
      */
     public Base58(final Builder builder) {
         super(builder);
+        this.maxDecodeLength = builder.getMaxDecodeLength();
+    }
+
+    private void checkDecodeLength(final int length, final int 
accumulatedLength) {
+        if (length > maxDecodeLength - accumulatedLength) {
+            throw new IllegalArgumentException("Base58 input exceeds the 
maximum decode length of " + maxDecodeLength +
+                    " bytes; use Base58.Builder.setMaxDecodeLength(int) to 
raise the limit.");
+        }
     }
 
     private void code(final byte[] array, final int offset, final int length, 
final Context context, final BiConsumer<byte[], Context> consumer) {
@@ -221,8 +273,8 @@ public class Base58 extends BaseNCodec {
      * <p>
      * Uses 32-bit word arithmetic ({@code int[]} with {@code long} carry) to 
convert the Base58 string to binary data, avoiding {@link BigInteger} and its
      * per-digit object allocation. Each Base58 digit is processed 
left-to-right using Horner's scheme: {@code value = value * 58 + digit}. An
-     * {@code wordsStart} cursor tracks the leftmost word that contains data, 
so the inner loop only touches the active portion of the work buffer — the 
active
-     * range grows by at most one word per four digits (since log(58)/log(256) 
&lt; 1), keeping total work well below {@code inputLength²}.
+     * {@code wordsStart} cursor tracks the leftmost word that contains data, 
so the inner loop only touches the active portion of the work buffer. The active
+     * range grows linearly with the number of digits, so total conversion 
work is quadratic in the input length.
      * </p>
      * <p>
      * At each word position the carry satisfies {@code carry &le; 57 + 58 
&times; (2³²&minus;1) &lt; 2⁴⁰}, which fits in a Java {@code long}.
@@ -233,9 +285,10 @@ public class Base58 extends BaseNCodec {
      *
      * @param base58  The Base58 encoded data.
      * @param context The context for this decoding operation.
-     * @throws IllegalArgumentException if the Base58 data contains invalid 
characters.
+     * @throws IllegalArgumentException if the Base58 data contains invalid 
characters or is longer than the configured maximum decode length.
      */
     private void convertFromBase58(final byte[] base58, final Context context) 
{
+        checkDecodeLength(base58.length, 0);
         final int zero = encodeTable[0] & 0xff;
         // Count leading Base58 "zero" characters; each represents a leading 
zero byte in the output.
         int leadingZeros = 0;
@@ -245,8 +298,10 @@ public class Base58 extends BaseNCodec {
             }
             leadingZeros++;
         }
-        // Instead of using BigInteger instances, we use a 32-bit word array.
-        // This provides ~4x speedup and avoids per-digit object allocation.
+        // Horner's scheme uses 32-bit words and a long carry, avoiding 
per-digit BigInteger allocation.
+        // wordsStart tracks the active word range, which grows linearly with 
the number of digits.
+        // Traversing this range for each digit makes conversion quadratic in 
the input length.
+        // At each word, carry <= 57 + 58 * (2^32 - 1) < 2^40, which fits in a 
long.
         //
         // Work buffer of 32-bit words, big-endian, right-aligned.
         // Upper bound on decoded bytes is base58.length, so 
(base58.length+3)/4 words suffice.
@@ -331,6 +386,9 @@ public class Base58 extends BaseNCodec {
      */
     @Override
     void decode(final byte[] array, final int offset, final int length, final 
Context context) {
+        if (!context.eof && length > 0) {
+            checkDecodeLength(length, context.buffer != null ? 
context.buffer.length : 0);
+        }
         code(array, offset, length, context, this::convertFromBase58);
     }
 
diff --git 
a/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java 
b/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
index e6988214..1df56ea0 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
@@ -20,29 +20,18 @@ package org.apache.commons.codec.binary;
 import java.io.InputStream;
 
 /**
- * Provides Base58 decoding in a streaming fashion (unlimited size). When 
encoding the default lineLength is 76 characters and the default lineEnding is 
CRLF,
- * but these can be overridden by using the appropriate constructor.
- * <p>
- * The default behavior of the Base58InputStream is to DECODE, whereas the 
default behavior of the Base58OutputStream is to ENCODE, but this behavior can 
be
- * overridden by using a different constructor.
- * </p>
- * <p>
- * Since this class operates directly on byte streams, and not character 
streams, it is hard-coded to only encode/decode character encodings which are
- * compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, 
etc).
- * </p>
- * <p>
- * You can set the decoding behavior when the input bytes contain leftover 
trailing bits that cannot be created by a valid encoding. These can be bits 
that are
- * unused from the final character or entire characters. The default mode is 
lenient decoding.
- * </p>
- * <ul>
- * <li>Lenient: Any trailing bits are composed into 8-bit bytes where 
possible. The remainder are discarded.</li>
- * <li>Strict: The decoding will throw an {@link IllegalArgumentException} if 
trailing bits are not part of a valid encoding. Any unused bits from the final
- * character must be zero. Impossible counts of entire final characters are 
not allowed.</li>
- * </ul>
- * <p>
- * When strict decoding is enabled it is expected that the decoded bytes will 
be re-encoded to a byte array that matches the original, i.e. no changes occur 
on
- * the final character. This requires that the input bytes use the same 
padding and alphabet as the encoder.
- * </p>
+ * Provides Base58 decoding through a stream interface.
+ *
+ * <p>The default behavior of Base58InputStream is to decode, and the default 
behavior of Base58OutputStream is to encode. The builder can select either
+ * behavior with {@code setEncode(boolean)}.</p>
+ *
+ * <p>Results are available only after EOF. Decoding accepts at most
+ * {@link Base58#DEFAULT_MAX_DECODE_LENGTH} encoded bytes by default and 
throws {@link java.io.IOException} when an input chunk would exceed the 
cumulative
+ * limit. To configure the limit, pass a codec built with {@link 
Base58.Builder#setMaxDecodeLength(int)} to the stream builder's
+ * {@code setBaseNCodec(Base58)} method.</p>
+ *
+ * <p>Encoding has no input limit. Callers should bound untrusted binary input 
before encoding, and explicitly raise the decode limit when decoding larger
+ * trusted values. Encoded output can exceed the default decode limit.</p>
  *
  * @see Base58
  * @see <a 
href="https://datatracker.ietf.org/doc/html/draft-msporny-base58-03";>The Base58 
Encoding Scheme draft-msporny-base58-03</a>
diff --git 
a/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java 
b/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
index bca12916..2d8d28e2 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
@@ -20,33 +20,20 @@ package org.apache.commons.codec.binary;
 import java.io.OutputStream;
 
 /**
- * Provides Base58 encoding in a streaming fashion (unlimited size). When 
encoding the default lineLength is 76 characters and the default lineEnding is 
CRLF,
- * but these can be overridden by using the appropriate constructor.
- * <p>
- * The default behavior of the Base58OutputStream is to ENCODE, whereas the 
default behavior of the Base58InputStream is to DECODE. But this behavior can be
- * overridden by using a different constructor.
- * </p>
- * <p>
- * Since this class operates directly on byte streams, and not character 
streams, it is hard-coded to only encode/decode character encodings which are
- * compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, 
etc).
- * </p>
- * <p>
- * <strong>Note:</strong> It is mandatory to close the stream after the last 
byte has been written to it, otherwise the final padding will be omitted and the
- * resulting data will be incomplete/inconsistent.
- * </p>
- * <p>
- * You can set the decoding behavior when the input bytes contain leftover 
trailing bits that cannot be created by a valid encoding. These can be bits 
that are
- * unused from the final character or entire characters. The default mode is 
lenient decoding.
- * </p>
- * <ul>
- * <li>Lenient: Any trailing bits are composed into 8-bit bytes where 
possible. The remainder are discarded.</li>
- * <li>Strict: The decoding will throw an {@link IllegalArgumentException} if 
trailing bits are not part of a valid encoding. Any unused bits from the final
- * character must be zero. Impossible counts of entire final characters are 
not allowed.</li>
- * </ul>
- * <p>
- * When strict decoding is enabled it is expected that the decoded bytes will 
be re-encoded to a byte array that matches the original, i.e. no changes occur 
on
- * the final character. This requires that the input bytes use the same 
padding and alphabet as the encoder.
- * </p>
+ * Provides Base58 encoding through a stream interface.
+ *
+ * <p>The default behavior of Base58InputStream is to decode, and the default 
behavior of Base58OutputStream is to encode. The builder can select either
+ * behavior with {@code setEncode(boolean)}.</p>
+ *
+ * <p>Results are available only after EOF. Decoding accepts at most
+ * {@link Base58#DEFAULT_MAX_DECODE_LENGTH} encoded bytes by default and 
throws {@link java.io.IOException} when an input chunk would exceed the 
cumulative
+ * limit. To configure the limit, pass a codec built with {@link 
Base58.Builder#setMaxDecodeLength(int)} to the stream builder's
+ * {@code setBaseNCodec(Base58)} method.</p>
+ *
+ * <p>Encoding has no input limit. Callers should bound untrusted binary input 
before encoding, and explicitly raise the decode limit when decoding larger
+ * trusted values. Encoded output can exceed the default decode limit.</p>
+ *
+ * <p>Close the output stream or call {@link #eof()} after the last write to 
complete conversion.</p>
  *
  * @see Base58
  * @see <a 
href="https://datatracker.ietf.org/doc/html/draft-msporny-base58-03";>The Base58 
Encoding Scheme draft-msporny-base58-03</a>
diff --git 
a/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java 
b/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java
new file mode 100644
index 00000000..149c55a8
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      https://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.
+ */
+
+package org.apache.commons.codec.binary;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests the maximum decode length limit that bounds the quadratic-cost Base58 
decode against oversized untrusted input.
+ */
+class Base58MaxDecodeLengthTest {
+
+    private static byte[] ones(final int length) {
+        final byte[] bytes = new byte[length];
+        // '1' encodes a leading zero byte, so decoding stays cheap regardless 
of length.
+        Arrays.fill(bytes, (byte) '1');
+        return bytes;
+    }
+
+    @Test
+    void testConfiguredLimit() {
+        final Base58 small = Base58.builder().setMaxDecodeLength(10).get();
+        assertThrows(IllegalArgumentException.class, () -> 
small.decode(ones(11)));
+        assertEquals(10, small.decode(ones(10)).length);
+        final Base58 unlimited = 
Base58.builder().setMaxDecodeLength(Integer.MAX_VALUE).get();
+        assertEquals(Base58.DEFAULT_MAX_DECODE_LENGTH + 1, 
unlimited.decode(ones(Base58.DEFAULT_MAX_DECODE_LENGTH + 1)).length);
+    }
+
+    @Test
+    void testDecodeAtLimitAccepted() {
+        assertEquals(Base58.DEFAULT_MAX_DECODE_LENGTH, new 
Base58().decode(ones(Base58.DEFAULT_MAX_DECODE_LENGTH)).length);
+    }
+
+    @Test
+    void testDecodeOverLimitRejected() {
+        assertThrows(IllegalArgumentException.class, () -> new 
Base58().decode(ones(Base58.DEFAULT_MAX_DECODE_LENGTH + 1)));
+    }
+
+    @Test
+    void testDefaultStreamsRejectOversizedInput() throws IOException {
+        final byte[] encoded = ones(Base58.DEFAULT_MAX_DECODE_LENGTH + 1);
+        try (Base58InputStream stream = new Base58InputStream(new 
ByteArrayInputStream(encoded))) {
+            assertThrows(IOException.class, stream::read);
+        }
+        try (Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(new 
ByteArrayOutputStream()).setEncode(false).get()) {
+            assertThrows(IOException.class, () -> stream.write(encoded));
+        }
+    }
+
+    @Test
+    void testEncodingMayExceedDecodeLimit() throws IOException {
+        final byte[] input = new byte[Base58.DEFAULT_MAX_DECODE_LENGTH + 1];
+        final Base58 codec = new Base58();
+        final byte[] encoded = codec.encode(input);
+        assertEquals(input.length, encoded.length);
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode(encoded));
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream();
+        try (Base58OutputStream stream = new Base58OutputStream(sink)) {
+            stream.write(input);
+        }
+        assertArrayEquals(encoded, sink.toByteArray());
+        try (Base58InputStream stream = 
Base58InputStream.builder().setByteArray(input).setEncode(true).get()) {
+            assertArrayEquals(encoded, IOUtils.toByteArray(stream));
+        }
+        final ByteArrayOutputStream decoded = new ByteArrayOutputStream();
+        try (Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(decoded).setEncode(false)
+                
.setBaseNCodec(Base58.builder().setMaxDecodeLength(encoded.length).get()).get())
 {
+            stream.write(encoded);
+        }
+        assertArrayEquals(input, decoded.toByteArray());
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 10, Base58.DEFAULT_MAX_DECODE_LENGTH, 
Base58.DEFAULT_MAX_DECODE_LENGTH + 1 })
+    void testInputStreamAtConfiguredLimit(final int limit) throws IOException {
+        try (Base58InputStream stream = 
Base58InputStream.builder().setByteArray(ones(limit)).setBaseNCodec(Base58.builder().setMaxDecodeLength(limit).get())
+                .get()) {
+            assertArrayEquals(new byte[limit], IOUtils.toByteArray(stream));
+        }
+    }
+
+    @Test
+    void testInputStreamRejectsBeforeEof() throws IOException {
+        final ByteArrayInputStream source = new 
ByteArrayInputStream(ones(100)) {
+
+            @Override
+            public synchronized int read(final byte[] bytes, final int offset, 
final int length) {
+                return super.read(bytes, offset, Math.min(length, 3));
+            }
+        };
+        try (Base58InputStream stream = 
Base58InputStream.builder().setInputStream(source).setBaseNCodec(Base58.builder().setMaxDecodeLength(10).get()).get())
 {
+            final IOException exception = assertThrows(IOException.class, 
stream::read);
+            assertTrue(exception.getCause() instanceof 
IllegalArgumentException);
+            assertEquals(88, source.available());
+        }
+    }
+
+    @Test
+    void testInvalidLimitRejected() {
+        assertThrows(IllegalArgumentException.class, () -> 
Base58.builder().setMaxDecodeLength(0));
+        assertThrows(IllegalArgumentException.class, () -> 
Base58.builder().setMaxDecodeLength(-1));
+    }
+
+    @Test
+    void testNonzeroDigitsAtLimit() {
+        final Base58 codec = new Base58();
+        final byte[] encoded = new byte[Base58.DEFAULT_MAX_DECODE_LENGTH];
+        Arrays.fill(encoded, (byte) 'z');
+        assertArrayEquals(encoded, codec.encode(codec.decode(encoded)));
+        final byte[] over = Arrays.copyOf(encoded, encoded.length + 1);
+        over[encoded.length] = 'z';
+        assertThrows(IllegalArgumentException.class, () -> codec.decode(over));
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode(StringUtils.newStringUsAscii(over)));
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode((Object) over));
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 1, 3, 10 })
+    void testOutputStreamRejectsCrossingChunk(final int chunkSize) throws 
IOException {
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream();
+        try (Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(sink).setEncode(false)
+                
.setBaseNCodec(Base58.builder().setMaxDecodeLength(10).get()).get()) {
+            int written = 0;
+            while (written < 10) {
+                final int length = Math.min(chunkSize, 10 - written);
+                stream.write(ones(length));
+                written += length;
+            }
+            final IOException exception = assertThrows(IOException.class, () 
-> stream.write('1'));
+            assertTrue(exception.getCause() instanceof 
IllegalArgumentException);
+            assertEquals(0, sink.size());
+        }
+        assertArrayEquals(new byte[10], sink.toByteArray());
+    }
+
+    @Test
+    void testRejectsBeforeBuffering() {
+        final Base58 codec = Base58.builder().setMaxDecodeLength(10).get();
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        codec.decode(ones(8), 0, 8, context);
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode(ones(3), 0, 3, context));
+        assertArrayEquals(ones(8), context.buffer);
+        // The cumulative check must not wrap around when adding the chunk 
length.
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode(ones(1), 0, Integer.MAX_VALUE, context));
+        assertArrayEquals(ones(8), context.buffer);
+    }
+
+    @Test
+    void testRoundTripUnaffected() {
+        final Base58 codec = new Base58();
+        final byte[] data = { 0, 0, 1, 2, 3, 4, 5, -1, 127 };
+        assertArrayEquals(data, codec.decode(codec.encode(data)));
+    }
+}
diff --git a/src/test/java/org/apache/commons/codec/binary/Base58Test.java 
b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
index 4ddc138f..15c6c170 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base58Test.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
@@ -132,7 +132,7 @@ public class Base58Test {
     @ValueSource(ints = { 20_000, 40_000, 80_000, 160_000, 320_000 })
     void testDecodeLargeInput(final int n) {
         // any valid non-'1' Base58 char
-        new Base58().decode(ArrayFill.fill(new byte[n], (byte) 'z'));
+        Base58.builder().setMaxDecodeLength(n).get().decode(ArrayFill.fill(new 
byte[n], (byte) 'z'));
     }
 
     /**
@@ -166,7 +166,7 @@ public class Base58Test {
             final byte[] data = new byte[random.nextInt(BOUND) + 1];
             Arrays.fill(data, (byte) i);
             final byte[] enc = new Base58().encode(data);
-            final byte[] dec = new Base58().decode(enc);
+            final byte[] dec = Base58.builder().setMaxDecodeLength(BOUND * 
2).get().decode(enc);
             assertArrayEqualsAt(data, dec, i);
         }
     }
@@ -177,7 +177,7 @@ public class Base58Test {
             final byte[] data = new byte[random.nextInt(BOUND) + 1];
             random.nextBytes(data);
             final byte[] enc = new Base58().encode(data);
-            final byte[] dec = new Base58().decode(enc);
+            final byte[] dec = Base58.builder().setMaxDecodeLength(BOUND * 
2).get().decode(enc);
             assertArrayEqualsAt(data, dec, i);
         }
     }

Reply via email to