You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by se...@apache.org on 2013/03/20 01:46:21 UTC

svn commit: r1458597 - in /commons/proper/fileupload/trunk/src: main/java/org/apache/commons/fileupload/util/mime/Base64Decoder.java test/java/org/apache/commons/fileupload/util/mime/Base64DecoderTestCase.java

Author: sebb
Date: Wed Mar 20 00:46:21 2013
New Revision: 1458597

URL: http://svn.apache.org/r1458597
Log:
Simplify and improve Base64 decoding:
- drop whitespace handling
- check for invalid bytes
Adjust test cases accordingly

Modified:
    commons/proper/fileupload/trunk/src/main/java/org/apache/commons/fileupload/util/mime/Base64Decoder.java
    commons/proper/fileupload/trunk/src/test/java/org/apache/commons/fileupload/util/mime/Base64DecoderTestCase.java

Modified: commons/proper/fileupload/trunk/src/main/java/org/apache/commons/fileupload/util/mime/Base64Decoder.java
URL: http://svn.apache.org/viewvc/commons/proper/fileupload/trunk/src/main/java/org/apache/commons/fileupload/util/mime/Base64Decoder.java?rev=1458597&r1=1458596&r2=1458597&view=diff
==============================================================================
--- commons/proper/fileupload/trunk/src/main/java/org/apache/commons/fileupload/util/mime/Base64Decoder.java (original)
+++ commons/proper/fileupload/trunk/src/main/java/org/apache/commons/fileupload/util/mime/Base64Decoder.java Wed Mar 20 00:46:21 2013
@@ -25,6 +25,21 @@ import java.io.OutputStream;
 final class Base64Decoder {
 
     /**
+     * Decoding table value for invalid bytes.
+     */
+    private static final int INVALID_BYTE = -1; // must be outside range 0-63
+
+    /**
+     * Mask to treat byte as unsigned integer.
+     */
+    private static final int MASK_BYTE_UNSIGNED = 0xFF;
+
+    /**
+     * Number of bytes per encoded chunk - 4 6bit bytes produce 3 8bit bytes on output.
+     */
+    private static final int INPUT_BYTES_PER_CHUNK = 4;
+
+    /**
      * Set up the encoding table.
      */
     private static final byte[] ENCODING_TABLE = {
@@ -54,6 +69,11 @@ final class Base64Decoder {
     private static final byte[] DECODING_TABLE = new byte[Byte.MAX_VALUE - Byte.MIN_VALUE + 1];
 
     static {
+        // Initialise as all invalid characters
+        for (int i = 0; i < DECODING_TABLE.length; i++) {
+            DECODING_TABLE[i] = INVALID_BYTE;
+        }
+        // set up valid characters
         for (int i = 0; i < ENCODING_TABLE.length; i++) {
             DECODING_TABLE[ENCODING_TABLE[i]] = (byte) i;
         }
@@ -67,17 +87,6 @@ final class Base64Decoder {
     }
 
     /**
-     * Checks if the input char must be skipped from the decode.
-     * The method skips whitespace characters LF, CR, horizontal tab and space.
-     *
-     * @param c the char to be checked.
-     * @return true, if the input char has to be skipped, false otherwise.
-     */
-    private static boolean ignore(char c) {
-        return (c == '\n' || c == '\r' || c == '\t' || c == ' ');
-    }
-
-    /**
      * Decode the base 64 encoded byte data writing it to the given output stream,
      * whitespace characters will be ignored.
      *
@@ -87,89 +96,46 @@ final class Base64Decoder {
      * @return the number of bytes produced.
      */
     public static int decode(byte[] data, OutputStream out) throws IOException {
-        byte    b1, b2, b3, b4;
         int        outLen = 0;
+        byte [] cache = new byte[INPUT_BYTES_PER_CHUNK];
+        int cachedBytes = 0;
 
-        if (data.length == 0) {
-            return outLen;
-        }
-
-        int        end = data.length;
-
-        while (end > 0) {
-            if (!ignore((char) data[end - 1])) {
+        for (byte b : data) {
+            if (b == PADDING) { // Padding means end of input
                 break;
             }
+            final byte d = DECODING_TABLE[MASK_BYTE_UNSIGNED & b];
+            if (d == INVALID_BYTE) {
+                throw new IOException("Invalid Base64 byte: " + b);
+            }
+            cache[cachedBytes++] = d;
+            if (cachedBytes == INPUT_BYTES_PER_CHUNK) {
+                // Convert 4 6-bit bytes to 3 8-bit bytes
+                // CHECKSTYLE IGNORE MagicNumber FOR NEXT 3 LINES
+                out.write((cache[0] << 2) | (cache[1] >> 4)); // 6 bits of b1 plus 2 bits of b2
+                out.write((cache[1] << 4) | (cache[2] >> 2)); // 4 bits of b2 plus 4 bits of b3
+                out.write((cache[2] << 6) | cache[3]);        // 2 bits of b3 plus 6 bits of b4
 
-            end--;
-        }
-
-        int  i = 0;
-        // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE
-        int  finish = end - 4; // last set of 4 bytes might include padding
-
-        while (i < finish) {
-            while ((i < finish) && ignore((char) data[i])) {
-                i++;
-            }
-
-            b1 = DECODING_TABLE[data[i++]];
-
-            while ((i < finish) && ignore((char) data[i])) {
-                i++;
-            }
-
-            b2 = DECODING_TABLE[data[i++]];
-
-            while ((i < finish) && ignore((char) data[i])) {
-                i++;
-            }
-
-            b3 = DECODING_TABLE[data[i++]];
-
-            while ((i < finish) && ignore((char) data[i])) {
-                i++;
+                // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE
+                outLen += 3;
+                cachedBytes = 0;
             }
-
-            b4 = DECODING_TABLE[data[i++]];
-
-            // Convert 4 6-bit bytes to 3 8-bit bytes
-            // CHECKSTYLE IGNORE MagicNumber FOR NEXT 3 LINES
-            out.write((b1 << 2) | (b2 >> 4)); // 6 bits of b1 plus 2 bits of b2
-            out.write((b2 << 4) | (b3 >> 2)); // 4 bits of b2 plus 4 bits of b3
-            out.write((b3 << 6) | b4);        // 2 bits of b3 plus 6 bits of b4
-
-            // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE
-            outLen += 3;
         }
-
-        // Get the last 4 bytes; only last two can be padding
-        b1 = DECODING_TABLE[data[i++]];
-        b2 = DECODING_TABLE[data[i++]];
-
-        // always write the first byte
-        // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE
-        out.write((b1 << 2) | (b2 >> 4)); // 6 bits of b1 plus 2 bits of b2
-        outLen++;
-
-        byte p1 = data[i++];
-        byte p2 = data[i++];
-
-        b3 = DECODING_TABLE[p1]; // may be needed later
-
-        if (p1 != PADDING) { // Nothing more to do if p1 == PADDING
-            // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE
-            out.write((b2 << 4) | (b3 >> 2)); // 4 bits of b2 plus 4 bits of b3
-            outLen++; 
-            if (p2 != PADDING) { // Nothing more to do if p2 == PADDING
-                b4 = DECODING_TABLE[p2];
-                // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE
-                out.write((b3 << 6) | b4);        // 2 bits of b3 plus 6 bits of b4
+        // CHECKSTYLE IGNORE MagicNumber FOR NEXT 2 LINES
+        if (cachedBytes >= 2) {
+            out.write((cache[0] << 2) | (cache[1] >> 4)); // 6 bits of b1 plus 2 bits of b2
+            outLen++;
+            // CHECKSTYLE IGNORE MagicNumber FOR NEXT 2 LINES
+            if (cachedBytes >= 3) {
+                out.write((cache[1] << 4) | (cache[2] >> 2)); // 4 bits of b2 plus 4 bits of b3
                 outLen++;
+                // CHECKSTYLE IGNORE MagicNumber FOR NEXT 2 LINES
+                if (cachedBytes >= 4) {
+                    out.write((cache[2] << 6) | cache[3]);        // 2 bits of b3 plus 6 bits of b4
+                    outLen++;
+                }
             }
         }
-
         return outLen;
     }
-
 }

Modified: commons/proper/fileupload/trunk/src/test/java/org/apache/commons/fileupload/util/mime/Base64DecoderTestCase.java
URL: http://svn.apache.org/viewvc/commons/proper/fileupload/trunk/src/test/java/org/apache/commons/fileupload/util/mime/Base64DecoderTestCase.java?rev=1458597&r1=1458596&r2=1458597&view=diff
==============================================================================
--- commons/proper/fileupload/trunk/src/test/java/org/apache/commons/fileupload/util/mime/Base64DecoderTestCase.java (original)
+++ commons/proper/fileupload/trunk/src/test/java/org/apache/commons/fileupload/util/mime/Base64DecoderTestCase.java Wed Mar 20 00:46:21 2013
@@ -19,6 +19,7 @@ package org.apache.commons.fileupload.ut
 import static org.junit.Assert.assertArrayEquals;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 
 import org.junit.Test;
 
@@ -55,15 +56,15 @@ public final class Base64DecoderTestCase
     }
 
     /**
-     * Test our decode with a pad character in the middle.
+     * Test our decode with pad character in the middle.
+     * Returns data up to pad character.
      *
-     * Contrary to Apache Commons-Codec implementation, doesn't halt decode and return what we've got so far.
      *
      * @throws Exception if any error occurs while decoding the input string.
      */
     @Test
     public void decodeWithInnerPad() throws Exception {
-        assertEncoded("Hello World\0Hello World", "SGVsbG8gV29ybGQ=SGVsbG8gV29ybGQ=");
+        assertEncoded("Hello World", "SGVsbG8gV29ybGQ=SGVsbG8gV29ybGQ=");
     }
 
     private static void assertEncoded(String clearText, String encoded) throws Exception {
@@ -78,16 +79,13 @@ public final class Base64DecoderTestCase
     }
 
     /**
-     * Throws ArrayIndexOutOfBoundsException on some non-BASE64 bytes.
-     *
-     * This is fixed in Apache Commons-Codec.
+     * Throws IOException for non-BASE64 bytes.
      *
      * @throws Exception
-     * @see <a href="https://issues.apache.org/jira/browse/CODEC-68">CODEC-68</a>
      */
-    @Test(expected = ArrayIndexOutOfBoundsException.class)
+    @Test(expected = IOException.class)
     public void nonBase64Bytes() throws Exception {
-        final byte[] x = new byte[]{'n', 'A', '=', '=', (byte) 0x9c};
+        final byte[] x = new byte[]{'n', 'A', (byte) 0x9c};
         Base64Decoder.decode(x, new ByteArrayOutputStream());
     }