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);
}