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


The following commit(s) were added to refs/heads/master by this push:
     new 7d5d6481 Grow Base58 accumulation buffers geometrically within the 
configured input limits and validate actual accumulated length.
7d5d6481 is described below

commit 7d5d64814621ae23501cd16631d057dd57b475bc
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 18 05:35:31 2026 -0700

    Grow Base58 accumulation buffers geometrically within the configured
    input limits and validate actual accumulated length.
---
 src/changes/changes.xml                            |   3 +-
 .../org/apache/commons/codec/binary/Base58.java    |  90 +++++++--
 .../commons/codec/binary/Base58InputStream.java    |  22 ++-
 .../commons/codec/binary/Base58OutputStream.java   |  26 ++-
 .../binary/Base58ChunkedAccumulationTest.java      | 208 +++++++++++++++++++++
 .../codec/binary/Base58MaxDecodeLengthTest.java    |   6 +-
 .../apache/commons/codec/binary/Base58Test.java    |   4 +-
 7 files changed, 320 insertions(+), 39 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9a311b3b..8299dffd 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -55,7 +55,8 @@ 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>
+      <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 defaults to a configurable 8192-byte binary input limit through 
Base58.Builder.setMaxEncodeLength(int).</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Grow Base58 
accumulation buffers geometrically within the configured input limits and 
validate actual accumulated length.</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 acbeebf1..e6b2cb0a 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58.java
@@ -32,8 +32,9 @@ import java.util.function.BiConsumer;
  * </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.
+ * {@link Builder#setMaxDecodeLength(int)}). Encoding rejects binary input 
longer than {@link #DEFAULT_MAX_ENCODE_LENGTH} bytes by default; configure it 
with
+ * {@link Builder#setMaxEncodeLength(int)}. These limits apply to the total 
input across all chunks in an operation. Memory usage is proportional to the
+ * accumulated input and conversion output. Encoded output can exceed the 
decode limit; configure both limits appropriately for 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.
@@ -62,6 +63,7 @@ public class Base58 extends BaseNCodec {
     public static class Builder extends AbstractBuilder<Base58, Builder> {
 
         private int maxDecodeLength = DEFAULT_MAX_DECODE_LENGTH;
+        private int maxEncodeLength = DEFAULT_MAX_ENCODE_LENGTH;
 
         /**
          * Constructs a new Base58 builder.
@@ -85,6 +87,10 @@ public class Base58 extends BaseNCodec {
             return maxDecodeLength;
         }
 
+        int getMaxEncodeLength() {
+            return maxEncodeLength;
+        }
+
         /**
          * Sets the encode table and derives the matching decode table.
          *
@@ -116,6 +122,26 @@ public class Base58 extends BaseNCodec {
             this.maxDecodeLength = maxDecodeLength;
             return this;
         }
+
+        /**
+         * Sets the maximum number of binary bytes accepted by a single encode 
operation.
+         * <p>
+         * Defaults to {@link Base58#DEFAULT_MAX_ENCODE_LENGTH}. Pass {@link 
Integer#MAX_VALUE} to effectively disable the limit for trusted input.
+         * </p>
+         *
+         * @param maxEncodeLength The maximum accepted binary input length; 
must be positive.
+         * @return {@code this} instance.
+         * @throws IllegalArgumentException if maxEncodeLength is not positive.
+         * @since 1.23.0
+         */
+        public Builder setMaxEncodeLength(final int maxEncodeLength) {
+            if (maxEncodeLength <= 0) {
+                throw new IllegalArgumentException("maxEncodeLength must be 
positive.");
+            }
+            this.maxEncodeLength = maxEncodeLength;
+            return this;
+        }
+
     }
     private static final BigInteger BASE = BigInteger.valueOf(58);
 
@@ -132,6 +158,16 @@ public class Base58 extends BaseNCodec {
      */
     public static final int DEFAULT_MAX_DECODE_LENGTH = 8192;
 
+    /**
+     * The default maximum number of binary bytes accepted by a single encode 
operation: {@value}.
+     * <p>
+     * Use {@link Builder#setMaxEncodeLength(int)} to raise (or effectively 
disable) the limit for trusted input.
+     * </p>
+     *
+     * @since 1.23.0
+     */
+    public static final int DEFAULT_MAX_ENCODE_LENGTH = 8192;
+
     /**
      * Base58 alphabet: 
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
      * (excludes: 0, I, O, l).
@@ -223,6 +259,11 @@ public class Base58 extends BaseNCodec {
      */
     private final int maxDecodeLength;
 
+    /**
+     * The maximum number of binary bytes accepted by a single encode 
operation.
+     */
+    private final int maxEncodeLength;
+
     /**
      * Constructs a Base58 codec used for encoding and decoding.
      */
@@ -238,34 +279,46 @@ public class Base58 extends BaseNCodec {
     public Base58(final Builder builder) {
         super(builder);
         this.maxDecodeLength = builder.getMaxDecodeLength();
+        this.maxEncodeLength = builder.getMaxEncodeLength();
     }
 
-    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 checkLength(final int length, final int accumulatedLength, 
final int maximum, final String operation) {
+        if (length > maximum - accumulatedLength) {
+            throw new IllegalArgumentException("Base58 input exceeds the 
maximum " + operation + " length of " + maximum + " bytes.");
         }
     }
 
-    private void code(final byte[] array, final int offset, final int length, 
final Context context, final BiConsumer<byte[], Context> consumer) {
+    private void code(final byte[] array, final int offset, final int length, 
final Context context, final int maximum, final String operation,
+            final BiConsumer<byte[], Context> consumer) {
         if (context.eof) {
             return;
         }
+        // Base58 needs the complete input before it can convert, so input is 
accumulated in context.buffer. The number of accumulated bytes
+        // is tracked in context.ibitWorkArea (otherwise unused by this codec) 
so the buffer can grow geometrically; reallocating an
+        // exact-size buffer per chunk would copy the whole accumulation on 
every chunk, making streaming quadratic in the input length.
         if (length < 0) {
             context.eof = true;
-            final byte[] accumulate = context.buffer = context.buffer != null 
? context.buffer : EMPTY_BYTE_ARRAY;
+            final byte[] accumulate = context.buffer = context.buffer == null 
? EMPTY_BYTE_ARRAY :
+                    context.buffer.length == context.ibitWorkArea ? 
context.buffer : Arrays.copyOf(context.buffer, context.ibitWorkArea);
             if (accumulate.length > 0) {
                 consumer.accept(accumulate, context);
             }
             return;
         }
-        final byte[] accumulate = context.buffer = context.buffer != null ? 
context.buffer : EMPTY_BYTE_ARRAY;
-        final byte[] newAccumulated = new byte[accumulate.length + length];
-        if (accumulate.length > 0) {
-            System.arraycopy(accumulate, 0, newAccumulated, 0, 
accumulate.length);
+        final int accumulated = context.ibitWorkArea;
+        checkLength(length, accumulated, maximum, operation);
+        if (length > Integer.MAX_VALUE - 8 - accumulated) {
+            throw new IllegalArgumentException("Base58 input too large to 
accumulate: " + ((long) accumulated + length) + " bytes.");
+        }
+        final int required = accumulated + length;
+        byte[] buffer = context.buffer != null ? context.buffer : 
EMPTY_BYTE_ARRAY;
+        if (required > buffer.length) {
+            // Grow geometrically to amortize copying across chunks.
+            buffer = Arrays.copyOf(buffer, (int) Math.min(Math.max((long) 
buffer.length * 2, required), Math.min(maximum, Integer.MAX_VALUE - 8L)));
         }
-        System.arraycopy(array, offset, newAccumulated, accumulate.length, 
length);
-        context.buffer = newAccumulated;
+        System.arraycopy(array, offset, buffer, accumulated, length);
+        context.buffer = buffer;
+        context.ibitWorkArea = required;
     }
 
     /**
@@ -288,7 +341,7 @@ public class Base58 extends BaseNCodec {
      * @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);
+        checkLength(base58.length, 0, maxDecodeLength, "decode");
         final int zero = encodeTable[0] & 0xff;
         // Count leading Base58 "zero" characters; each represents a leading 
zero byte in the output.
         int leadingZeros = 0;
@@ -386,10 +439,7 @@ 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);
+        code(array, offset, length, context, maxDecodeLength, "decode", 
this::convertFromBase58);
     }
 
     /**
@@ -405,7 +455,7 @@ public class Base58 extends BaseNCodec {
      */
     @Override
     void encode(final byte[] array, final int offset, final int length, final 
Context context) {
-        code(array, offset, length, context, this::convertToBase58);
+        code(array, offset, length, context, maxEncodeLength, "encode", 
this::convertToBase58);
     }
 
     /**
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 1df56ea0..2e0672b1 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
@@ -22,16 +22,26 @@ import java.io.InputStream;
 /**
  * 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>
+ * 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
+ * <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>
+ * {@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>
+ * Encoding accepts at most {@link Base58#DEFAULT_MAX_ENCODE_LENGTH} binary 
bytes by default and throws {@link java.io.IOException} when an input chunk 
would
+ * exceed that cumulative limit. Configure it with {@link 
Base58.Builder#setMaxEncodeLength(int)} on the codec passed to {@code 
setBaseNCodec(Base58)}.
+ * </p>
+ * <p>
+ * The complete input is retained until EOF. Memory usage is proportional to 
the accumulated input and conversion output. Configure both input limits
+ * appropriately for larger trusted values; encoded output can exceed the 
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 2d8d28e2..8c7a5dc0 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
@@ -22,18 +22,30 @@ import java.io.OutputStream;
 /**
  * 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>
+ * 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
+ * <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>
+ * {@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>
+ * Encoding accepts at most {@link Base58#DEFAULT_MAX_ENCODE_LENGTH} binary 
bytes by default and throws {@link java.io.IOException} when an input chunk 
would
+ * exceed that cumulative limit. Configure it with {@link 
Base58.Builder#setMaxEncodeLength(int)} on the codec passed to {@code 
setBaseNCodec(Base58)}.
+ * </p>
+ * <p>
+ * The complete input is retained until EOF. Memory usage is proportional to 
the accumulated input and conversion output. Configure both input limits
+ * appropriately for larger trusted values; encoded output can exceed the 
decode limit.
+ * </p>
  *
- * <p>Close the output stream or call {@link #eof()} after the last write to 
complete conversion.</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/Base58ChunkedAccumulationTest.java
 
b/src/test/java/org/apache/commons/codec/binary/Base58ChunkedAccumulationTest.java
new file mode 100644
index 00000000..7674c625
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/codec/binary/Base58ChunkedAccumulationTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.assertSame;
+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 that Base58 chunk-by-chunk accumulation (the streaming path used by 
{@link Base58InputStream} and {@link Base58OutputStream}) produces the same
+ * results as one-shot coding, guarding the amortized-growth accumulation 
logic.
+ */
+class Base58ChunkedAccumulationTest {
+
+    private static byte[] drain(final Base58 codec, final BaseNCodec.Context 
context) {
+        final byte[] out = new byte[context.pos];
+        codec.readResults(out, 0, out.length, context);
+        return out;
+    }
+
+    private static byte[] newData() {
+        final byte[] data = new byte[1000];
+        for (int i = 0; i < data.length; i++) {
+            data[i] = (byte) (i * 31 + 7);
+        }
+        return data;
+    }
+
+    @Test
+    void testChunkedDecodeMatchesOneShot() {
+        final byte[] data = newData();
+        final Base58 codec = new Base58();
+        final byte[] encoded = codec.encode(data);
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        for (int i = 0; i < encoded.length; i += 7) {
+            codec.decode(encoded, i, Math.min(7, encoded.length - i), context);
+        }
+        codec.decode(encoded, 0, -1, context);
+        assertArrayEquals(data, drain(codec, context));
+    }
+
+    @Test
+    void testChunkedEncodeMatchesOneShot() {
+        final byte[] data = newData();
+        final Base58 codec = new Base58();
+        final byte[] expected = codec.encode(data);
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        for (int i = 0; i < data.length; i += 11) {
+            codec.encode(data, i, Math.min(11, data.length - i), context);
+        }
+        codec.encode(data, 0, -1, context);
+        assertArrayEquals(expected, drain(codec, context));
+    }
+
+    @Test
+    void testEmptyInput() {
+        final Base58 codec = new Base58();
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        codec.decode(new byte[0], 0, 0, context);
+        codec.decode(new byte[0], 0, -1, context);
+        assertArrayEquals(new byte[0], drain(codec, context));
+    }
+
+    @Test
+    void testEncodeLimits() {
+        assertThrows(IllegalArgumentException.class, () -> 
Base58.builder().setMaxEncodeLength(0));
+        assertThrows(IllegalArgumentException.class, () -> 
Base58.builder().setMaxEncodeLength(-1));
+        final byte[] input = new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH + 1];
+        assertThrows(IllegalArgumentException.class, () -> new 
Base58().encode(input));
+        assertEquals(Base58.DEFAULT_MAX_ENCODE_LENGTH, new Base58().encode(new 
byte[Base58.DEFAULT_MAX_ENCODE_LENGTH]).length);
+        assertEquals(input.length, 
Base58.builder().setMaxEncodeLength(Integer.MAX_VALUE).get().encode(input).length);
+        final Base58 codec = Base58.builder().setMaxEncodeLength(10).get();
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        codec.encode(new byte[8], 0, 8, context);
+        final byte[] buffer = context.buffer;
+        assertThrows(IllegalArgumentException.class, () -> codec.encode(input, 
0, Integer.MAX_VALUE, context));
+        assertSame(buffer, context.buffer);
+        assertEquals(8, context.ibitWorkArea);
+    }
+
+    @Test
+    void testEncodingInputStreamRejectsBeforeEof() throws IOException {
+        final ByteArrayInputStream source = new ByteArrayInputStream(new 
byte[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).setEncode(true)
+                
.setBaseNCodec(Base58.builder().setMaxEncodeLength(10).get()).get()) {
+            assertThrows(IOException.class, stream::read);
+            assertEquals(88, source.available());
+        }
+    }
+
+    @Test
+    void testEncodingOutputStreamRejectsCrossingChunk() throws IOException {
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream();
+        try (Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(sink)
+                
.setBaseNCodec(Base58.builder().setMaxEncodeLength(10).get()).get()) {
+            for (int i = 0; i < 10; i++) {
+                stream.write(0);
+            }
+            assertThrows(IOException.class, () -> stream.write(0));
+            assertEquals(0, sink.size());
+        }
+        assertArrayEquals(new Base58().encode(new byte[10]), 
sink.toByteArray());
+    }
+
+    @Test
+    void testEncodingStreamsAtDefaultLimit() throws IOException {
+        final byte[] input = new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH];
+        final byte[] expected = new Base58().encode(input);
+        try (Base58InputStream stream = 
Base58InputStream.builder().setByteArray(input).setEncode(true).get()) {
+            assertArrayEquals(expected, IOUtils.toByteArray(stream));
+        }
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream();
+        try (Base58OutputStream stream = new Base58OutputStream(sink)) {
+            stream.write(input);
+        }
+        assertArrayEquals(expected, sink.toByteArray());
+    }
+
+    @Test
+    void testEncodingStreamsRejectDefaultLimit() throws IOException {
+        final byte[] input = new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH + 1];
+        try (Base58InputStream stream = 
Base58InputStream.builder().setByteArray(input).setEncode(true).get()) {
+            assertThrows(IOException.class, stream::read);
+        }
+        try (Base58OutputStream stream = new Base58OutputStream(new 
ByteArrayOutputStream())) {
+            assertThrows(IOException.class, () -> stream.write(input));
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { false, true })
+    void testGeometricGrowth(final boolean encode) throws IOException {
+        final Base58 codec = new Base58();
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        final byte[] input = { '1' };
+        int copied = 0;
+        for (int i = 0; i < 1000; i++) {
+            final byte[] previous = context.buffer;
+            BaseNCodec.code(encode, codec, input, 0, 1, context);
+            if (previous != null && previous != context.buffer) {
+                copied += previous.length;
+            }
+        }
+        assertTrue(copied < 2000, "Accumulation copies must grow linearly");
+        assertEquals(1000, context.ibitWorkArea);
+        final byte[] all = new byte[1000];
+        Arrays.fill(all, (byte) '1');
+        BaseNCodec.code(encode, codec, input, 0, -1, context);
+        assertArrayEquals(encode ? codec.encode(all) : codec.decode(all), 
drain(codec, context));
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { false, true })
+    void testIrregularChunksAtLimit(final boolean encode) throws IOException {
+        final Base58 codec = 
Base58.builder().setMaxDecodeLength(10).setMaxEncodeLength(10).get();
+        final byte[] input = new byte[10];
+        Arrays.fill(input, (byte) 'z');
+        final byte[] expected = encode ? codec.encode(input) : 
codec.decode(input);
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        int offset = 0;
+        for (final int length : new int[] { 6, 1, 1, 2 }) {
+            BaseNCodec.code(encode, codec, input, offset, length, context);
+            offset += length;
+            assertEquals(offset, context.ibitWorkArea);
+            assertTrue(context.buffer.length <= 10);
+            assertEquals(0, context.pos);
+        }
+        final byte[] buffer = context.buffer;
+        final IOException exception = assertThrows(IOException.class, () -> 
BaseNCodec.code(encode, codec, input, 0, 1, context));
+        assertTrue(exception.getCause() instanceof IllegalArgumentException);
+        assertSame(buffer, context.buffer);
+        assertEquals(10, context.ibitWorkArea);
+        BaseNCodec.code(encode, codec, input, 0, -1, context);
+        assertArrayEquals(expected, drain(codec, context));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java 
b/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java
index 149c55a8..c0efd523 100644
--- 
a/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java
+++ 
b/src/test/java/org/apache/commons/codec/binary/Base58MaxDecodeLengthTest.java
@@ -77,16 +77,16 @@ class Base58MaxDecodeLengthTest {
     @Test
     void testEncodingMayExceedDecodeLimit() throws IOException {
         final byte[] input = new byte[Base58.DEFAULT_MAX_DECODE_LENGTH + 1];
-        final Base58 codec = new Base58();
+        final Base58 codec = 
Base58.builder().setMaxEncodeLength(input.length).get();
         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)) {
+        try (Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(sink).setBaseNCodec(codec).get()) {
             stream.write(input);
         }
         assertArrayEquals(encoded, sink.toByteArray());
-        try (Base58InputStream stream = 
Base58InputStream.builder().setByteArray(input).setEncode(true).get()) {
+        try (Base58InputStream stream = 
Base58InputStream.builder().setByteArray(input).setEncode(true).setBaseNCodec(codec).get())
 {
             assertArrayEquals(encoded, IOUtils.toByteArray(stream));
         }
         final ByteArrayOutputStream decoded = new ByteArrayOutputStream();
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 15c6c170..5d7b3f6c 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base58Test.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
@@ -165,7 +165,7 @@ public class Base58Test {
         for (int i = 1; i < 5; i++) {
             final byte[] data = new byte[random.nextInt(BOUND) + 1];
             Arrays.fill(data, (byte) i);
-            final byte[] enc = new Base58().encode(data);
+            final byte[] enc = 
Base58.builder().setMaxEncodeLength(BOUND).get().encode(data);
             final byte[] dec = Base58.builder().setMaxDecodeLength(BOUND * 
2).get().decode(enc);
             assertArrayEqualsAt(data, dec, i);
         }
@@ -176,7 +176,7 @@ public class Base58Test {
         for (int i = 1; i < 5; i++) {
             final byte[] data = new byte[random.nextInt(BOUND) + 1];
             random.nextBytes(data);
-            final byte[] enc = new Base58().encode(data);
+            final byte[] enc = 
Base58.builder().setMaxEncodeLength(BOUND).get().encode(data);
             final byte[] dec = Base58.builder().setMaxDecodeLength(BOUND * 
2).get().decode(enc);
             assertArrayEqualsAt(data, dec, i);
         }

Reply via email to