From 27a31e573fecc2d6f77cd9326571672de639532b Mon Sep 17 00:00:00 2001 From: Doug Zongker Date: Thu, 11 Feb 2010 14:37:17 -0800 Subject: [PATCH] add Base64InputStream Change-Id: I777b54bd6d01c86105b473a6701a06d350cee8d1 --- .../com/android/common/Base64InputStream.java | 169 ++++++++++++++ .../src/com/android/common/Base64Test.java | 214 +++++++++++++----- 2 files changed, 331 insertions(+), 52 deletions(-) create mode 100644 common/java/com/android/common/Base64InputStream.java diff --git a/common/java/com/android/common/Base64InputStream.java b/common/java/com/android/common/Base64InputStream.java new file mode 100644 index 000000000..1969bc495 --- /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 e6b491f7f..1064625f2 100644 --- a/common/tests/src/com/android/common/Base64Test.java +++ b/common/tests/src/com/android/common/Base64Test.java @@ -18,6 +18,7 @@ package com.android.common; import junit.framework.TestCase; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Random; @@ -228,8 +229,13 @@ public class Base64Test extends TestCase { /** * 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 @@ public class Base64Test extends TestCase { 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 @@ public class Base64Test extends TestCase { 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); } } }