View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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   * Tests that Base58 chunk-by-chunk accumulation (the streaming path used by {@link Base58InputStream} and {@link Base58OutputStream}) produces the same
38   * results as one-shot coding, guarding the amortized-growth accumulation logic.
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 }