001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package org.apache.commons.codec.binary;
019
020 import static org.junit.Assert.assertArrayEquals;
021 import static org.junit.Assert.assertEquals;
022 import static org.junit.Assert.assertFalse;
023 import static org.junit.Assert.assertNotNull;
024 import static org.junit.Assert.assertTrue;
025 import static org.junit.Assert.fail;
026
027 import java.io.BufferedReader;
028 import java.io.ByteArrayInputStream;
029 import java.io.ByteArrayOutputStream;
030 import java.io.IOException;
031 import java.io.InputStream;
032 import java.io.InputStreamReader;
033 import java.util.Arrays;
034
035 import org.junit.Test;
036
037 /**
038 * @version $Id $
039 * @since 1.4
040 */
041 public class Base64InputStreamTest {
042
043 /**
044 * Decodes to {0, 0, 0, 255, 255, 255}
045 */
046 private static final String ENCODED_B64 = "AAAA////";
047
048 private final static byte[] CRLF = { (byte) '\r', (byte) '\n' };
049
050 private final static byte[] LF = { (byte) '\n' };
051
052 private static final String STRING_FIXTURE = "Hello World";
053
054 /**
055 * Tests the problem reported in CODEC-130. Missing / wrong implementation of skip.
056 */
057 @Test
058 public void testCodec130() throws IOException {
059 final ByteArrayOutputStream bos = new ByteArrayOutputStream();
060 final Base64OutputStream base64os = new Base64OutputStream(bos);
061
062 base64os.write(StringUtils.getBytesUtf8(STRING_FIXTURE));
063 base64os.close();
064
065 final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
066 final Base64InputStream ins = new Base64InputStream(bis);
067
068 // we skip the first character read from the reader
069 ins.skip(1);
070 final byte[] decodedBytes = Base64TestData.streamToBytes(ins, new byte[64]);
071 final String str = StringUtils.newStringUtf8(decodedBytes);
072
073 assertEquals(STRING_FIXTURE.substring(1), str);
074 }
075
076 /**
077 * Tests the bug reported in CODEC-105. Bad interactions with InputStream when reading one byte at a time.
078 */
079 @Test
080 public void testCodec105() throws IOException {
081 final Base64InputStream in = new Base64InputStream(new Codec105ErrorInputStream(), true, 0, null);
082 try {
083 for (int i = 0; i < 5; i++) {
084 in.read();
085 }
086 } finally {
087 in.close();
088 }
089 }
090
091 /**
092 * Test for the CODEC-101 bug: InputStream.read(byte[]) should never return 0 because Java's builtin InputStreamReader hates that.
093 *
094 * @throws Exception
095 * for some failure scenarios.
096 */
097 @Test
098 public void testCodec101() throws Exception {
099 final byte[] codec101 = StringUtils.getBytesUtf8(Base64TestData.CODEC_101_MULTIPLE_OF_3);
100 final ByteArrayInputStream bais = new ByteArrayInputStream(codec101);
101 final Base64InputStream in = new Base64InputStream(bais);
102 final byte[] result = new byte[8192];
103 int c = in.read(result);
104 assertTrue("Codec101: First read successful [c=" + c + "]", c > 0);
105
106 c = in.read(result);
107 assertTrue("Codec101: Second read should report end-of-stream [c=" + c + "]", c < 0);
108 in.close();
109 }
110
111 /**
112 * Another test for the CODEC-101 bug: In commons-codec-1.4 this test shows InputStreamReader explicitly hating an
113 * InputStream.read(byte[]) return of 0:
114 *
115 * java.io.IOException: Underlying input stream returned zero bytes at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:268) at
116 * sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158) at
117 * java.io.InputStreamReader.read(InputStreamReader.java:167) at java.io.BufferedReader.fill(BufferedReader.java:136) at
118 * java.io.BufferedReader.readLine(BufferedReader.java:299) at java.io.BufferedReader.readLine(BufferedReader.java:362) at
119 * org.apache.commons.codec.binary.Base64InputStreamTest.testInputStreamReader(Base64InputStreamTest.java:75)
120 *
121 * But in commons-codec-1.5 it's fixed. :-)
122 *
123 * @throws Exception
124 * for some failure scenarios.
125 */
126 @Test
127 public void testInputStreamReader() throws Exception {
128 final byte[] codec101 = StringUtils.getBytesUtf8(Base64TestData.CODEC_101_MULTIPLE_OF_3);
129 final ByteArrayInputStream bais = new ByteArrayInputStream(codec101);
130 final Base64InputStream in = new Base64InputStream(bais);
131 final InputStreamReader isr = new InputStreamReader(in);
132 final BufferedReader br = new BufferedReader(isr);
133 final String line = br.readLine();
134 assertNotNull("Codec101: InputStreamReader works!", line);
135 br.close();
136 }
137
138 /**
139 * Test the Base64InputStream implementation against the special NPE inducing input identified in the CODEC-98 bug.
140 *
141 * @throws Exception
142 * for some failure scenarios.
143 */
144 @Test
145 public void testCodec98NPE() throws Exception {
146 final byte[] codec98 = StringUtils.getBytesUtf8(Base64TestData.CODEC_98_NPE);
147 final ByteArrayInputStream data = new ByteArrayInputStream(codec98);
148 final Base64InputStream stream = new Base64InputStream(data);
149
150 // This line causes an NPE in commons-codec-1.4.jar:
151 final byte[] decodedBytes = Base64TestData.streamToBytes(stream, new byte[1024]);
152
153 final String decoded = StringUtils.newStringUtf8(decodedBytes);
154 assertEquals("codec-98 NPE Base64InputStream", Base64TestData.CODEC_98_NPE_DECODED, decoded);
155 }
156
157 /**
158 * Tests skipping past the end of a stream.
159 *
160 * @throws Throwable
161 */
162 @Test
163 public void testAvailable() throws Throwable {
164 final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
165 final Base64InputStream b64stream = new Base64InputStream(ins);
166 assertEquals(1, b64stream.available());
167 assertEquals(6, b64stream.skip(10));
168 // End of stream reached
169 assertEquals(0, b64stream.available());
170 assertEquals(-1, b64stream.read());
171 assertEquals(-1, b64stream.read());
172 assertEquals(0, b64stream.available());
173 b64stream.close();
174 }
175
176 /**
177 * Tests the Base64InputStream implementation against empty input.
178 *
179 * @throws Exception
180 * for some failure scenarios.
181 */
182 @Test
183 public void testBase64EmptyInputStreamMimeChuckSize() throws Exception {
184 testBase64EmptyInputStream(BaseNCodec.MIME_CHUNK_SIZE);
185 }
186
187 /**
188 * Tests the Base64InputStream implementation against empty input.
189 *
190 * @throws Exception
191 * for some failure scenarios.
192 */
193 @Test
194 public void testBase64EmptyInputStreamPemChuckSize() throws Exception {
195 testBase64EmptyInputStream(BaseNCodec.PEM_CHUNK_SIZE);
196 }
197
198 private void testBase64EmptyInputStream(final int chuckSize) throws Exception {
199 final byte[] emptyEncoded = new byte[0];
200 final byte[] emptyDecoded = new byte[0];
201 testByteByByte(emptyEncoded, emptyDecoded, chuckSize, CRLF);
202 testByChunk(emptyEncoded, emptyDecoded, chuckSize, CRLF);
203 }
204
205 /**
206 * Tests the Base64InputStream implementation.
207 *
208 * @throws Exception
209 * for some failure scenarios.
210 */
211 @Test
212 public void testBase64InputStreamByChunk() throws Exception {
213 // Hello World test.
214 byte[] encoded = StringUtils.getBytesUtf8("SGVsbG8gV29ybGQ=\r\n");
215 byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
216 testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
217
218 // Single Byte test.
219 encoded = StringUtils.getBytesUtf8("AA==\r\n");
220 decoded = new byte[] { (byte) 0 };
221 testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
222
223 // OpenSSL interop test.
224 encoded = StringUtils.getBytesUtf8(Base64TestData.ENCODED_64_CHARS_PER_LINE);
225 decoded = Base64TestData.DECODED;
226 testByChunk(encoded, decoded, BaseNCodec.PEM_CHUNK_SIZE, LF);
227
228 // Single Line test.
229 final String singleLine = Base64TestData.ENCODED_64_CHARS_PER_LINE.replaceAll("\n", "");
230 encoded = StringUtils.getBytesUtf8(singleLine);
231 decoded = Base64TestData.DECODED;
232 testByChunk(encoded, decoded, 0, LF);
233
234 // test random data of sizes 0 thru 150
235 for (int i = 0; i <= 150; i++) {
236 final byte[][] randomData = Base64TestData.randomData(i, false);
237 encoded = randomData[1];
238 decoded = randomData[0];
239 testByChunk(encoded, decoded, 0, LF);
240 }
241 }
242
243 /**
244 * Tests the Base64InputStream implementation.
245 *
246 * @throws Exception
247 * for some failure scenarios.
248 */
249 @Test
250 public void testBase64InputStreamByteByByte() throws Exception {
251 // Hello World test.
252 byte[] encoded = StringUtils.getBytesUtf8("SGVsbG8gV29ybGQ=\r\n");
253 byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
254 testByteByByte(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
255
256 // Single Byte test.
257 encoded = StringUtils.getBytesUtf8("AA==\r\n");
258 decoded = new byte[] { (byte) 0 };
259 testByteByByte(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
260
261 // OpenSSL interop test.
262 encoded = StringUtils.getBytesUtf8(Base64TestData.ENCODED_64_CHARS_PER_LINE);
263 decoded = Base64TestData.DECODED;
264 testByteByByte(encoded, decoded, BaseNCodec.PEM_CHUNK_SIZE, LF);
265
266 // Single Line test.
267 final String singleLine = Base64TestData.ENCODED_64_CHARS_PER_LINE.replaceAll("\n", "");
268 encoded = StringUtils.getBytesUtf8(singleLine);
269 decoded = Base64TestData.DECODED;
270 testByteByByte(encoded, decoded, 0, LF);
271
272 // test random data of sizes 0 thru 150
273 for (int i = 0; i <= 150; i++) {
274 final byte[][] randomData = Base64TestData.randomData(i, false);
275 encoded = randomData[1];
276 decoded = randomData[0];
277 testByteByByte(encoded, decoded, 0, LF);
278 }
279 }
280
281 /**
282 * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
283 * ---[WRAP-WRAP-WRAP-etc...] --> decoded
284 * <p/>
285 * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base64InputStream wraps itself in encode and decode mode over and over
286 * again.
287 *
288 * @param encoded
289 * base64 encoded data
290 * @param decoded
291 * the data from above, but decoded
292 * @param chunkSize
293 * chunk size (line-length) of the base64 encoded data.
294 * @param seperator
295 * Line separator in the base64 encoded data.
296 * @throws Exception
297 * Usually signifies a bug in the Base64 commons-codec implementation.
298 */
299 private void testByChunk(final byte[] encoded, final byte[] decoded, final int chunkSize, final byte[] seperator) throws Exception {
300
301 // Start with encode.
302 InputStream in;
303 in = new Base64InputStream(new ByteArrayInputStream(decoded), true, chunkSize, seperator);
304 byte[] output = Base64TestData.streamToBytes(in);
305
306 assertEquals("EOF", -1, in.read());
307 assertEquals("Still EOF", -1, in.read());
308 assertTrue("Streaming base64 encode", Arrays.equals(output, encoded));
309
310 in.close();
311
312 // Now let's try decode.
313 in = new Base64InputStream(new ByteArrayInputStream(encoded));
314 output = Base64TestData.streamToBytes(in);
315
316 assertEquals("EOF", -1, in.read());
317 assertEquals("Still EOF", -1, in.read());
318 assertTrue("Streaming base64 decode", Arrays.equals(output, decoded));
319
320 // I always wanted to do this! (wrap encoder with decoder etc etc).
321 in = new ByteArrayInputStream(decoded);
322 for (int i = 0; i < 10; i++) {
323 in = new Base64InputStream(in, true, chunkSize, seperator);
324 in = new Base64InputStream(in, false);
325 }
326 output = Base64TestData.streamToBytes(in);
327
328 assertEquals("EOF", -1, in.read());
329 assertEquals("Still EOF", -1, in.read());
330 assertTrue("Streaming base64 wrap-wrap-wrap!", Arrays.equals(output, decoded));
331 in.close();
332 }
333
334 /**
335 * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
336 * ---[WRAP-WRAP-WRAP-etc...] --> decoded
337 * <p/>
338 * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base64InputStream wraps itself in encode and decode mode over and over
339 * again.
340 *
341 * @param encoded
342 * base64 encoded data
343 * @param decoded
344 * the data from above, but decoded
345 * @param chunkSize
346 * chunk size (line-length) of the base64 encoded data.
347 * @param seperator
348 * Line separator in the base64 encoded data.
349 * @throws Exception
350 * Usually signifies a bug in the Base64 commons-codec implementation.
351 */
352 private void testByteByByte(final byte[] encoded, final byte[] decoded, final int chunkSize, final byte[] seperator) throws Exception {
353
354 // Start with encode.
355 InputStream in;
356 in = new Base64InputStream(new ByteArrayInputStream(decoded), true, chunkSize, seperator);
357 byte[] output = new byte[encoded.length];
358 for (int i = 0; i < output.length; i++) {
359 output[i] = (byte) in.read();
360 }
361
362 assertEquals("EOF", -1, in.read());
363 assertEquals("Still EOF", -1, in.read());
364 assertTrue("Streaming base64 encode", Arrays.equals(output, encoded));
365
366 in.close();
367 // Now let's try decode.
368 in = new Base64InputStream(new ByteArrayInputStream(encoded));
369 output = new byte[decoded.length];
370 for (int i = 0; i < output.length; i++) {
371 output[i] = (byte) in.read();
372 }
373
374 assertEquals("EOF", -1, in.read());
375 assertEquals("Still EOF", -1, in.read());
376 assertTrue("Streaming base64 decode", Arrays.equals(output, decoded));
377
378 in.close();
379
380 // I always wanted to do this! (wrap encoder with decoder etc etc).
381 in = new ByteArrayInputStream(decoded);
382 for (int i = 0; i < 10; i++) {
383 in = new Base64InputStream(in, true, chunkSize, seperator);
384 in = new Base64InputStream(in, false);
385 }
386 output = new byte[decoded.length];
387 for (int i = 0; i < output.length; i++) {
388 output[i] = (byte) in.read();
389 }
390
391 assertEquals("EOF", -1, in.read());
392 assertEquals("Still EOF", -1, in.read());
393 assertTrue("Streaming base64 wrap-wrap-wrap!", Arrays.equals(output, decoded));
394 in.close();
395 }
396
397 /**
398 * Tests markSupported.
399 *
400 * @throws Exception
401 */
402 @Test
403 public void testMarkSupported() throws Exception {
404 final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
405 final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
406 final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
407 // Always returns false for now.
408 assertFalse("Base64InputStream.markSupported() is false", in.markSupported());
409 in.close();
410 }
411
412 /**
413 * Tests read returning 0
414 *
415 * @throws Exception
416 */
417 @Test
418 public void testRead0() throws Exception {
419 final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
420 final byte[] buf = new byte[1024];
421 int bytesRead = 0;
422 final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
423 final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
424 bytesRead = in.read(buf, 0, 0);
425 assertEquals("Base64InputStream.read(buf, 0, 0) returns 0", 0, bytesRead);
426 in.close();
427 }
428
429 /**
430 * Tests read with null.
431 *
432 * @throws Exception
433 * for some failure scenarios.
434 */
435 @Test
436 public void testReadNull() throws Exception {
437 final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
438 final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
439 final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
440 try {
441 in.read(null, 0, 0);
442 fail("Base64InputStream.read(null, 0, 0) to throw a NullPointerException");
443 } catch (final NullPointerException e) {
444 // Expected
445 }
446 in.close();
447 }
448
449 /**
450 * Tests read throwing IndexOutOfBoundsException
451 *
452 * @throws Exception
453 */
454 @Test
455 public void testReadOutOfBounds() throws Exception {
456 final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
457 final byte[] buf = new byte[1024];
458 final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
459 final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
460
461 try {
462 in.read(buf, -1, 0);
463 fail("Expected Base64InputStream.read(buf, -1, 0) to throw IndexOutOfBoundsException");
464 } catch (final IndexOutOfBoundsException e) {
465 // Expected
466 }
467
468 try {
469 in.read(buf, 0, -1);
470 fail("Expected Base64InputStream.read(buf, 0, -1) to throw IndexOutOfBoundsException");
471 } catch (final IndexOutOfBoundsException e) {
472 // Expected
473 }
474
475 try {
476 in.read(buf, buf.length + 1, 0);
477 fail("Base64InputStream.read(buf, buf.length + 1, 0) throws IndexOutOfBoundsException");
478 } catch (final IndexOutOfBoundsException e) {
479 // Expected
480 }
481
482 try {
483 in.read(buf, buf.length - 1, 2);
484 fail("Base64InputStream.read(buf, buf.length - 1, 2) throws IndexOutOfBoundsException");
485 } catch (final IndexOutOfBoundsException e) {
486 // Expected
487 }
488 in.close();
489 }
490
491 /**
492 * Tests skipping number of characters larger than the internal buffer.
493 *
494 * @throws Throwable
495 */
496 @Test
497 public void testSkipBig() throws Throwable {
498 final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
499 final Base64InputStream b64stream = new Base64InputStream(ins);
500 assertEquals(6, b64stream.skip(Integer.MAX_VALUE));
501 // End of stream reached
502 assertEquals(-1, b64stream.read());
503 assertEquals(-1, b64stream.read());
504 b64stream.close();
505 }
506
507 /**
508 * Tests skipping as a noop
509 *
510 * @throws Throwable
511 */
512 @Test
513 public void testSkipNone() throws Throwable {
514 final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
515 final Base64InputStream b64stream = new Base64InputStream(ins);
516 final byte[] actualBytes = new byte[6];
517 assertEquals(0, b64stream.skip(0));
518 b64stream.read(actualBytes, 0, actualBytes.length);
519 assertArrayEquals(actualBytes, new byte[] { 0, 0, 0, (byte) 255, (byte) 255, (byte) 255 });
520 // End of stream reached
521 assertEquals(-1, b64stream.read());
522 b64stream.close();
523 }
524
525 /**
526 * Tests skipping past the end of a stream.
527 *
528 * @throws Throwable
529 */
530 @Test
531 public void testSkipPastEnd() throws Throwable {
532 final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
533 final Base64InputStream b64stream = new Base64InputStream(ins);
534 // due to CODEC-130, skip now skips correctly decoded characters rather than encoded
535 assertEquals(6, b64stream.skip(10));
536 // End of stream reached
537 assertEquals(-1, b64stream.read());
538 assertEquals(-1, b64stream.read());
539 b64stream.close();
540 }
541
542 /**
543 * Tests skipping to the end of a stream.
544 *
545 * @throws Throwable
546 */
547 @Test
548 public void testSkipToEnd() throws Throwable {
549 final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
550 final Base64InputStream b64stream = new Base64InputStream(ins);
551 // due to CODEC-130, skip now skips correctly decoded characters rather than encoded
552 assertEquals(6, b64stream.skip(6));
553 // End of stream reached
554 assertEquals(-1, b64stream.read());
555 assertEquals(-1, b64stream.read());
556 b64stream.close();
557 }
558
559 /**
560 * Tests if negative arguments to skip are handled correctly.
561 *
562 * @throws Throwable
563 */
564 @Test(expected=IllegalArgumentException.class)
565 public void testSkipWrongArgument() throws Throwable {
566 final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
567 final Base64InputStream b64stream = new Base64InputStream(ins);
568 b64stream.skip(-10);
569 b64stream.close();
570 }
571 }