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.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.util.Arrays;
30  import java.util.Random;
31  import java.util.stream.Stream;
32  
33  import org.apache.commons.codec.CodecPolicy;
34  import org.apache.commons.codec.binary.BaseNCodec.Context;
35  import org.junit.jupiter.api.Test;
36  import org.junit.jupiter.params.ParameterizedTest;
37  import org.junit.jupiter.params.provider.MethodSource;
38  
39  /** Tests canonical decoding across array and stream boundaries. */
40  class BaseNStrictDecodingTest {
41  
42      private static final Base64 BASE64 = Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
43      private static final Base32 BASE32 = Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
44  
45      private static void assertCanonicalOrRejected(final BaseNCodec codec, final byte[] encoded) {
46          final byte[] decoded;
47          try {
48              decoded = codec.decode(encoded);
49          } catch (final IllegalArgumentException expected) {
50              return;
51          }
52          assertArrayEquals(encoded, codec.encode(decoded));
53      }
54  
55      private static void assertInvalid(final BaseNCodec codec, final String... inputs) {
56          for (final String input : inputs) {
57              assertThrows(IllegalArgumentException.class, () -> codec.decode(input), input);
58          }
59      }
60  
61      private static Stream<BaseNCodec> codecs() {
62          final byte[] custom64 = StringUtils.getBytesUsAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
63          // A custom alphabet uses the encoder's unpadded form, even when it is not the built-in URL-safe alphabet.
64          custom64[0] = '!';
65          final byte[] custom32 = StringUtils.getBytesUsAscii("abcdefghijklmnopqrstuvwxyz234567");
66          return Stream.of(BASE64, BASE32,
67                  Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setUrlSafe(true).get(),
68                  Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setEncodeTable(custom64).get(),
69                  Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setEncodeTable(custom32).get(),
70                  Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setHexEncodeTable(true).get(),
71                  Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setPadding((byte) '.').get(),
72                  Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setPadding((byte) '.').get(),
73                  Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(9).setLineSeparator((byte) '!', (byte) '?').get(),
74                  Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(17).setLineSeparator((byte) '!', (byte) '?').get(),
75                  Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setUrlSafe(true).setLineLength(8).get(),
76                  Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(16).get(),
77                  Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(1).get(),
78                  Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(8).setLineSeparator(new byte[0]).get());
79      }
80  
81      private static byte[] decodeInParts(final BaseNCodec codec, final byte[] encoded, final int split) {
82          final Context context = new Context();
83          codec.decode(encoded, 0, split, context);
84          codec.decode(encoded, split, encoded.length - split, context);
85          codec.decode(encoded, 0, BaseNCodec.EOF, context);
86          final byte[] result = new byte[codec.available(context)];
87          codec.readResults(result, 0, result.length, context);
88          return result;
89      }
90  
91      private static InputStream oneByteAtATime(final byte[] encoded) {
92          return new ByteArrayInputStream(encoded) {
93              @Override
94              public synchronized int read(final byte[] buffer, final int offset, final int length) {
95                  return super.read(buffer, offset, Math.min(1, length));
96              }
97          };
98      }
99  
100     @ParameterizedTest
101     @MethodSource("codecs")
102     void testCanonicalRoundTripAtEverySplit(final BaseNCodec codec) {
103         final Random random = new Random(87654);
104         for (int length = 0; length <= 32; length++) {
105             final byte[] input = new byte[length];
106             random.nextBytes(input);
107             final byte[] encoded = codec.encode(input);
108             assertArrayEquals(input, codec.decode(encoded));
109             for (int split = 0; split <= encoded.length; split++) {
110                 assertArrayEquals(input, decodeInParts(codec, encoded, split));
111             }
112         }
113     }
114 
115     @ParameterizedTest
116     @MethodSource("codecs")
117     void testCanonicalStreams(final BaseNCodec codec) throws IOException {
118         final byte[] input = new byte[67];
119         new Random(23456).nextBytes(input);
120         final byte[] encoded = codec.encode(input);
121         final ByteArrayOutputStream decoded = new ByteArrayOutputStream();
122         try (InputStream stream = new BaseNCodecInputStream<>(oneByteAtATime(encoded), codec, false)) {
123             for (int value; (value = stream.read()) != -1;) {
124                 decoded.write(value);
125             }
126         }
127         assertArrayEquals(input, decoded.toByteArray());
128         decoded.reset();
129         try (BaseNCodecOutputStream<?, ?, ?> stream = new BaseNCodecOutputStream<>(decoded, codec, false)) {
130             for (final byte value : encoded) {
131                 stream.write(value & 0xff);
132             }
133             stream.flush();
134         }
135         assertArrayEquals(input, decoded.toByteArray());
136     }
137 
138     @Test
139     void testGarbageWhitespaceAndAlphabetAliases() {
140         assertInvalid(BASE64, "QU JD", "QU$JD", "QU\u001cJD", "QQ==JUNK", "QQ==\n", "QUJ-", "QUJ_", "QR==", "QUJ=");
141         assertInvalid(BASE32, "M Y======", "MY!======", "MY======JUNK", "my======", "MY======\n", "MZ======");
142         final Base64 url = Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setUrlSafe(true).get();
143         assertInvalid(url, "QQ==", "QUJ+", "QUJ/");
144         assertArrayEquals(new byte[] {65}, url.decode("QQ"));
145     }
146 
147     @Test
148     void testLenientBehaviorIsUnchanged() {
149         assertArrayEquals(new byte[] {65}, new Base64().decode("Q $Q==JUNK"));
150         assertArrayEquals(new byte[] {102}, new Base32().decode("m !y======JUNK"));
151         assertArrayEquals(new Base64().decode("QUJ+"), new Base64().decode("QUJ-"));
152     }
153 
154     @Test
155     void testLineSeparators() {
156         final Base64 codec = Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(8).get();
157         assertEquals("ABC", StringUtils.newStringUsAscii(codec.decode("QUJD\r\n")));
158         assertEquals("ABCDEF", StringUtils.newStringUsAscii(codec.decode("QUJDREVG\r\n")));
159         assertInvalid(codec, "QUJD", "QUJD\r", "QUJD\n", "QUJD\rX", "\r\n", "QUJD\r\nREVG\r\n", "QUJDREVGQUJD\r\n",
160                 "QUJDREVG\r\n\r\n", "QQ==\r\nQUJD\r\n", "QUJDREVG=", "QUJDREVG\rX");
161     }
162 
163     @ParameterizedTest
164     @MethodSource("codecs")
165     void testMutationsAreCanonicalOrRejected(final BaseNCodec codec) {
166         final byte[] input = new byte[7];
167         new Random(34567).nextBytes(input);
168         final byte[] encoded = codec.encode(input);
169         for (int pos = 0; pos < encoded.length; pos++) {
170             final byte[] deleted = new byte[encoded.length - 1];
171             System.arraycopy(encoded, 0, deleted, 0, pos);
172             System.arraycopy(encoded, pos + 1, deleted, pos, encoded.length - pos - 1);
173             assertCanonicalOrRejected(codec, deleted);
174             final byte[] changed = encoded.clone();
175             for (int value = 0; value <= 255; value++) {
176                 changed[pos] = (byte) value;
177                 assertCanonicalOrRejected(codec, changed);
178             }
179         }
180         for (int pos = 0; pos <= encoded.length; pos++) {
181             final byte[] inserted = new byte[encoded.length + 1];
182             System.arraycopy(encoded, 0, inserted, 0, pos);
183             System.arraycopy(encoded, pos, inserted, pos + 1, encoded.length - pos);
184             for (int value = 0; value <= 255; value++) {
185                 inserted[pos] = (byte) value;
186                 assertCanonicalOrRejected(codec, inserted);
187             }
188         }
189     }
190 
191     @Test
192     void testPadding() {
193         assertInvalid(BASE64, "QQ", "QQ=", "QQ===", "QQ=====", "=", "====", "QUJD=", "Q===", "QQ==QQ==");
194         assertInvalid(BASE32, "MY", "MY=", "MY=====", "MY=======", "=", "========", "MZXW6YTB=", "M=======", "MY======MY======");
195     }
196 
197     @Test
198     void testPostPaddingDataAtEverySplit() {
199         for (final BaseNCodec codec : Arrays.asList(BASE64, BASE32)) {
200             final byte[] encoded = StringUtils.getBytesUsAscii(codec.encodeToString(new byte[] {65}) + "JUNK");
201             for (int split = 0; split <= encoded.length; split++) {
202                 final int boundary = split;
203                 assertThrows(IllegalArgumentException.class, () -> decodeInParts(codec, encoded, boundary));
204             }
205             final IOException exception = assertThrows(IOException.class, () -> {
206                 try (InputStream stream = new BaseNCodecInputStream<>(oneByteAtATime(encoded), codec, false)) {
207                     while (stream.read() != -1) {
208                         // Consume the complete stream to validate its suffix.
209                     }
210                 }
211             });
212             assertTrue(exception.getCause() instanceof IllegalArgumentException);
213             assertThrows(IOException.class, () -> {
214                 try (BaseNCodecOutputStream<?, ?, ?> stream = new BaseNCodecOutputStream<>(new ByteArrayOutputStream(), codec, false)) {
215                     for (final byte value : encoded) {
216                         stream.write(value & 0xff);
217                     }
218                 }
219             });
220         }
221     }
222 
223     @Test
224     void testTruncatedStreamsFailAtEof() {
225         for (final BaseNCodec codec : Arrays.asList(BASE64, BASE32)) {
226             final byte[] encoded = codec.encode(new byte[] {65});
227             final byte[] truncated = Arrays.copyOf(encoded, encoded.length - 1);
228             assertThrows(IOException.class, () -> {
229                 try (InputStream stream = new BaseNCodecInputStream<>(oneByteAtATime(truncated), codec, false)) {
230                     while (stream.read() != -1) {
231                         // Consume the stream to validate missing padding at EOF.
232                     }
233                 }
234             });
235             assertThrows(IOException.class, () -> {
236                 try (BaseNCodecOutputStream<?, ?, ?> stream = new BaseNCodecOutputStream<>(new ByteArrayOutputStream(), codec, false)) {
237                     stream.write(truncated);
238                 }
239             });
240         }
241     }
242 }