1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.codec.binary;
19
20 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21 import static org.junit.jupiter.api.Assertions.assertEquals;
22 import static org.junit.jupiter.api.Assertions.assertSame;
23 import static org.junit.jupiter.api.Assertions.assertThrows;
24 import static org.junit.jupiter.api.Assertions.assertTrue;
25
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.io.IOException;
29 import java.util.Arrays;
30
31 import org.apache.commons.io.IOUtils;
32 import org.junit.jupiter.api.Test;
33 import org.junit.jupiter.params.ParameterizedTest;
34 import org.junit.jupiter.params.provider.ValueSource;
35
36
37
38
39
40 class Base58ChunkedAccumulationTest {
41
42 private static byte[] drain(final Base58 codec, final BaseNCodec.Context context) {
43 final byte[] out = new byte[context.pos];
44 codec.readResults(out, 0, out.length, context);
45 return out;
46 }
47
48 private static byte[] newData() {
49 final byte[] data = new byte[1000];
50 for (int i = 0; i < data.length; i++) {
51 data[i] = (byte) (i * 31 + 7);
52 }
53 return data;
54 }
55
56 @Test
57 void testChunkedDecodeMatchesOneShot() {
58 final byte[] data = newData();
59 final Base58 codec = new Base58();
60 final byte[] encoded = codec.encode(data);
61 final BaseNCodec.Context context = new BaseNCodec.Context();
62 for (int i = 0; i < encoded.length; i += 7) {
63 codec.decode(encoded, i, Math.min(7, encoded.length - i), context);
64 }
65 codec.decode(encoded, 0, -1, context);
66 assertArrayEquals(data, drain(codec, context));
67 }
68
69 @Test
70 void testChunkedEncodeMatchesOneShot() {
71 final byte[] data = newData();
72 final Base58 codec = new Base58();
73 final byte[] expected = codec.encode(data);
74 final BaseNCodec.Context context = new BaseNCodec.Context();
75 for (int i = 0; i < data.length; i += 11) {
76 codec.encode(data, i, Math.min(11, data.length - i), context);
77 }
78 codec.encode(data, 0, -1, context);
79 assertArrayEquals(expected, drain(codec, context));
80 }
81
82 @Test
83 void testEmptyInput() {
84 final Base58 codec = new Base58();
85 final BaseNCodec.Context context = new BaseNCodec.Context();
86 codec.decode(new byte[0], 0, 0, context);
87 codec.decode(new byte[0], 0, -1, context);
88 assertArrayEquals(new byte[0], drain(codec, context));
89 }
90
91 @Test
92 void testEncodeLimits() {
93 assertThrows(IllegalArgumentException.class, () -> Base58.builder().setMaxEncodeLength(0));
94 assertThrows(IllegalArgumentException.class, () -> Base58.builder().setMaxEncodeLength(-1));
95 final byte[] input = new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH + 1];
96 assertThrows(IllegalArgumentException.class, () -> new Base58().encode(input));
97 assertEquals(Base58.DEFAULT_MAX_ENCODE_LENGTH, new Base58().encode(new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH]).length);
98 assertEquals(input.length, Base58.builder().setMaxEncodeLength(Integer.MAX_VALUE).get().encode(input).length);
99 final Base58 codec = Base58.builder().setMaxEncodeLength(10).get();
100 final BaseNCodec.Context context = new BaseNCodec.Context();
101 codec.encode(new byte[8], 0, 8, context);
102 final byte[] buffer = context.buffer;
103 assertThrows(IllegalArgumentException.class, () -> codec.encode(input, 0, Integer.MAX_VALUE, context));
104 assertSame(buffer, context.buffer);
105 assertEquals(8, context.ibitWorkArea);
106 }
107
108 @Test
109 void testEncodingInputStreamRejectsBeforeEof() throws IOException {
110 final ByteArrayInputStream source = new ByteArrayInputStream(new byte[100]) {
111 @Override
112 public synchronized int read(final byte[] bytes, final int offset, final int length) {
113 return super.read(bytes, offset, Math.min(length, 3));
114 }
115 };
116 try (Base58InputStream stream = Base58InputStream.builder().setInputStream(source).setEncode(true)
117 .setBaseNCodec(Base58.builder().setMaxEncodeLength(10).get()).get()) {
118 assertThrows(IOException.class, stream::read);
119 assertEquals(88, source.available());
120 }
121 }
122
123 @Test
124 void testEncodingOutputStreamRejectsCrossingChunk() throws IOException {
125 final ByteArrayOutputStream sink = new ByteArrayOutputStream();
126 try (Base58OutputStream stream = Base58OutputStream.builder().setOutputStream(sink)
127 .setBaseNCodec(Base58.builder().setMaxEncodeLength(10).get()).get()) {
128 for (int i = 0; i < 10; i++) {
129 stream.write(0);
130 }
131 assertThrows(IOException.class, () -> stream.write(0));
132 assertEquals(0, sink.size());
133 }
134 assertArrayEquals(new Base58().encode(new byte[10]), sink.toByteArray());
135 }
136
137 @Test
138 void testEncodingStreamsAtDefaultLimit() throws IOException {
139 final byte[] input = new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH];
140 final byte[] expected = new Base58().encode(input);
141 try (Base58InputStream stream = Base58InputStream.builder().setByteArray(input).setEncode(true).get()) {
142 assertArrayEquals(expected, IOUtils.toByteArray(stream));
143 }
144 final ByteArrayOutputStream sink = new ByteArrayOutputStream();
145 try (Base58OutputStream stream = new Base58OutputStream(sink)) {
146 stream.write(input);
147 }
148 assertArrayEquals(expected, sink.toByteArray());
149 }
150
151 @Test
152 void testEncodingStreamsRejectDefaultLimit() throws IOException {
153 final byte[] input = new byte[Base58.DEFAULT_MAX_ENCODE_LENGTH + 1];
154 try (Base58InputStream stream = Base58InputStream.builder().setByteArray(input).setEncode(true).get()) {
155 assertThrows(IOException.class, stream::read);
156 }
157 try (Base58OutputStream stream = new Base58OutputStream(new ByteArrayOutputStream())) {
158 assertThrows(IOException.class, () -> stream.write(input));
159 }
160 }
161
162 @ParameterizedTest
163 @ValueSource(booleans = { false, true })
164 void testGeometricGrowth(final boolean encode) throws IOException {
165 final Base58 codec = new Base58();
166 final BaseNCodec.Context context = new BaseNCodec.Context();
167 final byte[] input = { '1' };
168 int copied = 0;
169 for (int i = 0; i < 1000; i++) {
170 final byte[] previous = context.buffer;
171 BaseNCodec.code(encode, codec, input, 0, 1, context);
172 if (previous != null && previous != context.buffer) {
173 copied += previous.length;
174 }
175 }
176 assertTrue(copied < 2000, "Accumulation copies must grow linearly");
177 assertEquals(1000, context.ibitWorkArea);
178 final byte[] all = new byte[1000];
179 Arrays.fill(all, (byte) '1');
180 BaseNCodec.code(encode, codec, input, 0, -1, context);
181 assertArrayEquals(encode ? codec.encode(all) : codec.decode(all), drain(codec, context));
182 }
183
184 @ParameterizedTest
185 @ValueSource(booleans = { false, true })
186 void testIrregularChunksAtLimit(final boolean encode) throws IOException {
187 final Base58 codec = Base58.builder().setMaxDecodeLength(10).setMaxEncodeLength(10).get();
188 final byte[] input = new byte[10];
189 Arrays.fill(input, (byte) 'z');
190 final byte[] expected = encode ? codec.encode(input) : codec.decode(input);
191 final BaseNCodec.Context context = new BaseNCodec.Context();
192 int offset = 0;
193 for (final int length : new int[] { 6, 1, 1, 2 }) {
194 BaseNCodec.code(encode, codec, input, offset, length, context);
195 offset += length;
196 assertEquals(offset, context.ibitWorkArea);
197 assertTrue(context.buffer.length <= 10);
198 assertEquals(0, context.pos);
199 }
200 final byte[] buffer = context.buffer;
201 final IOException exception = assertThrows(IOException.class, () -> BaseNCodec.code(encode, codec, input, 0, 1, context));
202 assertTrue(exception.getCause() instanceof IllegalArgumentException);
203 assertSame(buffer, context.buffer);
204 assertEquals(10, context.ibitWorkArea);
205 BaseNCodec.code(encode, codec, input, 0, -1, context);
206 assertArrayEquals(expected, drain(codec, context));
207 }
208 }