Merge "add Base64InputStream"
diff --git a/common/java/com/android/common/Base64InputStream.java b/common/java/com/android/common/Base64InputStream.java
new file mode 100644
index 0000000..1969bc4
--- /dev/null
+++ b/common/java/com/android/common/Base64InputStream.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.common;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An OutputStream that does either Base64 encoding or decoding on the
+ * data written to it, writing the resulting data to another
+ * OutputStream.
+ */
+public class Base64InputStream extends FilterInputStream {
+    private final boolean encode;
+    private final Base64.EncoderState estate;
+    private final Base64.DecoderState dstate;
+
+    private static byte[] EMPTY = new byte[0];
+
+    private static final int BUFFER_SIZE = 2048;
+    private boolean eof;
+    private byte[] inputBuffer;
+    private byte[] outputBuffer;
+    private int outputStart;
+    private int outputEnd;
+
+    /**
+     * An InputStream that performs Base64 decoding on the data read
+     * from the wrapped stream.
+     *
+     * @param in the InputStream to read the source data from
+     * @param flags bit flags for controlling the decoder; see the
+     *        constants in {@link Base64}
+     */
+    public Base64InputStream(InputStream out, int flags) {
+        this(out, flags, false);
+    }
+
+    /**
+     * Performs Base64 encoding or decoding on the data read from the
+     * wrapped InputStream.
+     *
+     * @param in the InputStream to read the source data from
+     * @param flags bit flags for controlling the decoder; see the
+     *        constants in {@link Base64}
+     * @param encode true to encode, false to decode
+     */
+    public Base64InputStream(InputStream out, int flags, boolean encode) {
+        super(out);
+        this.encode = encode;
+        eof = false;
+        inputBuffer = new byte[BUFFER_SIZE];
+        if (encode) {
+            // len*8/5+10 is an overestimate of the most bytes the
+            // encoder can produce for len bytes of input.
+            outputBuffer = new byte[BUFFER_SIZE * 8/5 + 10];
+            estate = new Base64.EncoderState(flags, outputBuffer);
+            dstate = null;
+        } else {
+            // len*3/4+10 is an overestimate of the most bytes the
+            // decoder can produce for len bytes of input.
+            outputBuffer = new byte[BUFFER_SIZE * 3/4 + 10];
+            estate = null;
+            dstate = new Base64.DecoderState(flags, outputBuffer);
+        }
+        outputStart = 0;
+        outputEnd = 0;
+    }
+
+    public boolean markSupported() {
+        return false;
+    }
+
+    public void mark(int readlimit) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void reset() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void close() throws IOException {
+        in.close();
+        inputBuffer = null;
+    }
+
+    public int available() {
+        return outputEnd - outputStart;
+    }
+
+    public long skip(long n) throws IOException {
+        if (outputStart >= outputEnd) {
+            refill();
+        }
+        if (outputStart >= outputEnd) {
+            return 0;
+        }
+        long bytes = Math.min(n, outputEnd-outputStart);
+        outputStart += bytes;
+        return bytes;
+    }
+
+    public int read() throws IOException {
+        if (outputStart >= outputEnd) {
+            refill();
+        }
+        if (outputStart >= outputEnd) {
+            return -1;
+        } else {
+            return outputBuffer[outputStart++];
+        }
+    }
+
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (outputStart >= outputEnd) {
+            refill();
+        }
+        if (outputStart >= outputEnd) {
+            return -1;
+        }
+        int bytes = Math.min(len, outputEnd-outputStart);
+        System.arraycopy(outputBuffer, outputStart, b, off, bytes);
+        outputStart += bytes;
+        return bytes;
+    }
+
+    /**
+     * Read data from the input stream into inputBuffer, then
+     * decode/encode it into the empty outputBuffer, and reset the
+     * outputStart and outputEnd pointers.
+     */
+    private void refill() throws IOException {
+        if (eof) return;
+        int bytesRead = in.read(inputBuffer);
+        if (encode) {
+            if (bytesRead == -1) {
+                eof = true;
+                Base64.encodeInternal(EMPTY, 0, 0, estate, true);
+            } else {
+                Base64.encodeInternal(inputBuffer, 0, bytesRead, estate, false);
+            }
+            outputEnd = estate.op;
+        } else {
+            if (bytesRead == -1) {
+                eof = true;
+                Base64.decodeInternal(EMPTY, 0, 0, dstate, true);
+            } else {
+                Base64.decodeInternal(inputBuffer, 0, bytesRead, dstate, false);
+            }
+            outputEnd = dstate.op;
+        }
+        outputStart = 0;
+    }
+}
diff --git a/common/tests/src/com/android/common/Base64Test.java b/common/tests/src/com/android/common/Base64Test.java
index e6b491f..1064625 100644
--- a/common/tests/src/com/android/common/Base64Test.java
+++ b/common/tests/src/com/android/common/Base64Test.java
@@ -18,6 +18,7 @@
 
 import junit.framework.TestCase;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.util.Random;
 
@@ -228,8 +229,13 @@
     /**
      * Tests that Base64.encodeInternal does correct handling of the
      * tail for each call.
+     *
+     * This test is disabled because while it passes if you can get it
+     * to run, android's test infrastructure currently doesn't allow
+     * us to get at package-private members (Base64.EncoderState in
+     * this case).
      */
-    public void testEncodeInternal() throws Exception {
+    public void XXXtestEncodeInternal() throws Exception {
         byte[] input = { (byte) 0x61, (byte) 0x62, (byte) 0x63 };
         byte[] output = new byte[100];
 
@@ -272,6 +278,132 @@
         assertEquals("YQ".getBytes(), 2, state.output, state.op);
     }
 
+    private static final String lipsum =
+            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+            "Quisque congue eleifend odio, eu ornare nulla facilisis eget. " +
+            "Integer eget elit diam, sit amet laoreet nibh. Quisque enim " +
+            "urna, pharetra vitae consequat eget, adipiscing eu ante. " +
+            "Aliquam venenatis arcu nec nibh imperdiet tempor. In id dui " +
+            "eget lorem aliquam rutrum vel vitae eros. In placerat ornare " +
+            "pretium. Curabitur non fringilla mi. Fusce ultricies, turpis " +
+            "eu ultrices suscipit, ligula nisi consectetur eros, dapibus " +
+            "aliquet dui sapien a turpis. Donec ultricies varius ligula, " +
+            "ut hendrerit arcu malesuada at. Praesent sed elit pretium " +
+            "eros luctus gravida. In ac dolor lorem. Cras condimentum " +
+            "convallis elementum. Phasellus vel felis in nulla ultrices " +
+            "venenatis. Nam non tortor non orci convallis convallis. " +
+            "Nam tristique lacinia hendrerit. Pellentesque habitant morbi " +
+            "tristique senectus et netus et malesuada fames ac turpis " +
+            "egestas. Vivamus cursus, nibh eu imperdiet porta, magna " +
+            "ipsum mollis mauris, sit amet fringilla mi nisl eu mi. " +
+            "Phasellus posuere, leo at ultricies vehicula, massa risus " +
+            "volutpat sapien, eu tincidunt diam ipsum eget nulla. Cras " +
+            "molestie dapibus commodo. Ut vel tellus at massa gravida " +
+            "semper non sed orci.";
+
+    public void testInputStream() throws Exception {
+        int[] flagses = { Base64.DEFAULT,
+                          Base64.NO_PADDING,
+                          Base64.NO_WRAP,
+                          Base64.NO_PADDING | Base64.NO_WRAP,
+                          Base64.CRLF,
+                          Base64.WEB_SAFE };
+        int[] writeLengths = { -10, -5, -1, 0, 1, 1, 2, 2, 3, 10, 100 };
+        Random rng = new Random(32176L);
+
+        // Test input needs to be at least 2048 bytes to fill up the
+        // read buffer of Base64InputStream.
+        byte[] plain = (lipsum + lipsum + lipsum + lipsum + lipsum).getBytes();
+
+        for (int flags: flagses) {
+            byte[] encoded = Base64.encode(plain, flags);
+
+            ByteArrayInputStream bais;
+            Base64InputStream b64is;
+            byte[] actual = new byte[plain.length * 2];
+            int ap;
+            int b;
+
+            // ----- test decoding ("encoded" -> "plain") -----
+
+            // read as much as it will give us in one chunk
+            bais = new ByteArrayInputStream(encoded);
+            b64is = new Base64InputStream(bais, flags);
+            ap = 0;
+            while ((b = b64is.read(actual, ap, actual.length-ap)) != -1) {
+                ap += b;
+            }
+            assertEquals(actual, ap, plain);
+
+            // read individual bytes
+            bais = new ByteArrayInputStream(encoded);
+            b64is = new Base64InputStream(bais, flags);
+            ap = 0;
+            while ((b = b64is.read()) != -1) {
+                actual[ap++] = (byte) b;
+            }
+            assertEquals(actual, ap, plain);
+
+            // mix reads of variously-sized arrays with one-byte reads
+            bais = new ByteArrayInputStream(encoded);
+            b64is = new Base64InputStream(bais, flags);
+            ap = 0;
+            readloop: while (true) {
+                int l = writeLengths[rng.nextInt(writeLengths.length)];
+                if (l >= 0) {
+                    b = b64is.read(actual, ap, l);
+                    if (b == -1) break readloop;
+                    ap += b;
+                } else {
+                    for (int i = 0; i < -l; ++i) {
+                        if ((b = b64is.read()) == -1) break readloop;
+                        actual[ap++] = (byte) b;
+                    }
+                }
+            }
+            assertEquals(actual, ap, plain);
+
+            // ----- test encoding ("plain" -> "encoded") -----
+
+            // read as much as it will give us in one chunk
+            bais = new ByteArrayInputStream(plain);
+            b64is = new Base64InputStream(bais, flags, true);
+            ap = 0;
+            while ((b = b64is.read(actual, ap, actual.length-ap)) != -1) {
+                ap += b;
+            }
+            assertEquals(actual, ap, encoded);
+
+            // read individual bytes
+            bais = new ByteArrayInputStream(plain);
+            b64is = new Base64InputStream(bais, flags, true);
+            ap = 0;
+            while ((b = b64is.read()) != -1) {
+                actual[ap++] = (byte) b;
+            }
+            assertEquals(actual, ap, encoded);
+
+            // mix reads of variously-sized arrays with one-byte reads
+            bais = new ByteArrayInputStream(plain);
+            b64is = new Base64InputStream(bais, flags, true);
+            ap = 0;
+            readloop: while (true) {
+                int l = writeLengths[rng.nextInt(writeLengths.length)];
+                if (l >= 0) {
+                    b = b64is.read(actual, ap, l);
+                    if (b == -1) break readloop;
+                    ap += b;
+                } else {
+                    for (int i = 0; i < -l; ++i) {
+                        if ((b = b64is.read()) == -1) break readloop;
+                        actual[ap++] = (byte) b;
+                    }
+                }
+            }
+            assertEquals(actual, ap, encoded);
+        }
+    }
+
     /**
      * Tests that Base64OutputStream produces exactly the same results
      * as calling Base64.encode/.decode on an in-memory array.
@@ -286,125 +418,103 @@
         int[] writeLengths = { -10, -5, -1, 0, 1, 1, 2, 2, 3, 10, 100 };
         Random rng = new Random(32176L);
 
-        // input needs to be at least 1024 bytes to test filling up
-        // the write(int) buffer.
-        byte[] input = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
-                        "Quisque congue eleifend odio, eu ornare nulla facilisis eget. " +
-                        "Integer eget elit diam, sit amet laoreet nibh. Quisque enim " +
-                        "urna, pharetra vitae consequat eget, adipiscing eu ante. " +
-                        "Aliquam venenatis arcu nec nibh imperdiet tempor. In id dui " +
-                        "eget lorem aliquam rutrum vel vitae eros. In placerat ornare " +
-                        "pretium. Curabitur non fringilla mi. Fusce ultricies, turpis " +
-                        "eu ultrices suscipit, ligula nisi consectetur eros, dapibus " +
-                        "aliquet dui sapien a turpis. Donec ultricies varius ligula, " +
-                        "ut hendrerit arcu malesuada at. Praesent sed elit pretium " +
-                        "eros luctus gravida. In ac dolor lorem. Cras condimentum " +
-                        "convallis elementum. Phasellus vel felis in nulla ultrices " +
-                        "venenatis. Nam non tortor non orci convallis convallis. " +
-                        "Nam tristique lacinia hendrerit. Pellentesque habitant morbi " +
-                        "tristique senectus et netus et malesuada fames ac turpis " +
-                        "egestas. Vivamus cursus, nibh eu imperdiet porta, magna " +
-                        "ipsum mollis mauris, sit amet fringilla mi nisl eu mi. " +
-                        "Phasellus posuere, leo at ultricies vehicula, massa risus " +
-                        "volutpat sapien, eu tincidunt diam ipsum eget nulla. Cras " +
-                        "molestie dapibus commodo. Ut vel tellus at massa gravida " +
-                        "semper non sed orci.").getBytes();
+        // Test input needs to be at least 1024 bytes to test filling
+        // up the write(int) buffer of Base64OutputStream.
+        byte[] plain = (lipsum + lipsum).getBytes();
 
-        for (int f = 0; f < flagses.length; ++f) {
-            int flags = flagses[f];
-
-            byte[] expected = Base64.encode(input, flags);
+        for (int flags: flagses) {
+            byte[] encoded = Base64.encode(plain, flags);
 
             ByteArrayOutputStream baos;
             Base64OutputStream b64os;
             byte[] actual;
             int p;
 
-            // ----- test encoding ("input" -> "expected") -----
+            // ----- test encoding ("plain" -> "encoded") -----
 
             // one large write(byte[]) of the whole input
             baos = new ByteArrayOutputStream();
             b64os = new Base64OutputStream(baos, flags);
-            b64os.write(input);
+            b64os.write(plain);
             b64os.close();
             actual = baos.toByteArray();
-            assertEquals(expected, actual);
+            assertEquals(encoded, actual);
 
             // many calls to write(int)
             baos = new ByteArrayOutputStream();
             b64os = new Base64OutputStream(baos, flags);
-            for (int i = 0; i < input.length; ++i) {
-                b64os.write(input[i]);
+            for (int i = 0; i < plain.length; ++i) {
+                b64os.write(plain[i]);
             }
             b64os.close();
             actual = baos.toByteArray();
-            assertEquals(expected, actual);
+            assertEquals(encoded, actual);
 
             // intermixed sequences of write(int) with
             // write(byte[],int,int) of various lengths.
             baos = new ByteArrayOutputStream();
             b64os = new Base64OutputStream(baos, flags);
             p = 0;
-            while (p < input.length) {
+            while (p < plain.length) {
                 int l = writeLengths[rng.nextInt(writeLengths.length)];
-                l = Math.min(l, input.length-p);
+                l = Math.min(l, plain.length-p);
                 if (l >= 0) {
-                    b64os.write(input, p, l);
+                    b64os.write(plain, p, l);
                     p += l;
                 } else {
-                    l = Math.min(-l, input.length-p);
+                    l = Math.min(-l, plain.length-p);
                     for (int i = 0; i < l; ++i) {
-                        b64os.write(input[p+i]);
+                        b64os.write(plain[p+i]);
                     }
                     p += l;
                 }
             }
             b64os.close();
             actual = baos.toByteArray();
-            assertEquals(expected, actual);
+            assertEquals(encoded, actual);
 
-            // ----- test decoding ("expected" -> "input") -----
+            // ----- test decoding ("encoded" -> "plain") -----
 
             // one large write(byte[]) of the whole input
             baos = new ByteArrayOutputStream();
             b64os = new Base64OutputStream(baos, flags, false);
-            b64os.write(expected);
+            b64os.write(encoded);
             b64os.close();
             actual = baos.toByteArray();
-            assertEquals(input, actual);
+            assertEquals(plain, actual);
 
             // many calls to write(int)
             baos = new ByteArrayOutputStream();
             b64os = new Base64OutputStream(baos, flags, false);
-            for (int i = 0; i < expected.length; ++i) {
-                b64os.write(expected[i]);
+            for (int i = 0; i < encoded.length; ++i) {
+                b64os.write(encoded[i]);
             }
             b64os.close();
             actual = baos.toByteArray();
-            assertEquals(input, actual);
+            assertEquals(plain, actual);
 
             // intermixed sequences of write(int) with
             // write(byte[],int,int) of various lengths.
             baos = new ByteArrayOutputStream();
             b64os = new Base64OutputStream(baos, flags, false);
             p = 0;
-            while (p < expected.length) {
+            while (p < encoded.length) {
                 int l = writeLengths[rng.nextInt(writeLengths.length)];
-                l = Math.min(l, expected.length-p);
+                l = Math.min(l, encoded.length-p);
                 if (l >= 0) {
-                    b64os.write(expected, p, l);
+                    b64os.write(encoded, p, l);
                     p += l;
                 } else {
-                    l = Math.min(-l, expected.length-p);
+                    l = Math.min(-l, encoded.length-p);
                     for (int i = 0; i < l; ++i) {
-                        b64os.write(expected[p+i]);
+                        b64os.write(encoded[p+i]);
                     }
                     p += l;
                 }
             }
             b64os.close();
             actual = baos.toByteArray();
-            assertEquals(input, actual);
+            assertEquals(plain, actual);
         }
     }
 }