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.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
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
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
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
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 }