finish replacing Email's base64 implementation with android-common

Change-Id: I19adbbb884311d70073e9f7a961aa6808ac0dfb4
This commit is contained in:
Doug Zongker 2010-02-10 12:00:05 -08:00
parent a8d44824c3
commit ba714999f2
8 changed files with 77 additions and 1278 deletions

View File

@ -1,796 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.email.codec.binary;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
/**
* Provides Base64 encoding and decoding as defined by RFC 2045.
*
* <p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @author Apache Software Foundation
* @since 1.0-dev
* @version $Id$
*/
/* package */ class Base64 {
/**
* Chunk size per RFC 2045 section 6.8.
*
* <p>
* The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
* equal signs.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
*/
static final int CHUNK_SIZE = 76;
/**
* Chunk separator per RFC 2045 section 2.1.
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
*/
static final byte[] CHUNK_SEPARATOR = {'\r','\n'};
/**
* This array is a lookup table that translates 6-bit positive integer
* index values into their "Base64 Alphabet" equivalents as specified
* in Table 1 of RFC 2045.
*
* Thanks to "commons" project in ws.apache.org for this code.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
*/
private static final byte[] intToBase64 = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};
/**
* Byte used to pad output.
*/
private static final byte PAD = '=';
/**
* This array is a lookup table that translates unicode characters
* drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045)
* into their 6-bit positive integer equivalents. Characters that
* are not in the Base64 alphabet but fall within the bounds of the
* array are translated to -1.
*
* Thanks to "commons" project in ws.apache.org for this code.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
*/
private static final byte[] base64ToInt = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
};
/** Mask used to extract 6 bits, used when encoding */
private static final int MASK_6BITS = 0x3f;
/** Mask used to extract 8 bits, used in decoding base64 bytes */
private static final int MASK_8BITS = 0xff;
// The static final fields above are used for the original static byte[] methods on Base64.
// The private member fields below are used with the new streaming approach, which requires
// some state be preserved between calls of encode() and decode().
/**
* Line length for encoding. Not used when decoding. A value of zero or less implies
* no chunking of the base64 encoded data.
*/
private final int lineLength;
/**
* Line separator for encoding. Not used when decoding. Only used if lineLength > 0.
*/
private final byte[] lineSeparator;
/**
* Convenience variable to help us determine when our buffer is going to run out of
* room and needs resizing. <code>decodeSize = 3 + lineSeparator.length;</code>
*/
private final int decodeSize;
/**
* Convenience variable to help us determine when our buffer is going to run out of
* room and needs resizing. <code>encodeSize = 4 + lineSeparator.length;</code>
*/
private final int encodeSize;
/**
* Buffer for streaming.
*/
private byte[] buf;
/**
* Position where next character should be written in the buffer.
*/
private int pos;
/**
* Position where next character should be read from the buffer.
*/
private int readPos;
/**
* Variable tracks how many characters have been written to the current line.
* Only used when encoding. We use it to make sure each encoded line never
* goes beyond lineLength (if lineLength > 0).
*/
private int currentLinePos;
/**
* Writes to the buffer only occur after every 3 reads when encoding, an
* every 4 reads when decoding. This variable helps track that.
*/
private int modulus;
/**
* Boolean flag to indicate the EOF has been reached. Once EOF has been
* reached, this Base64 object becomes useless, and must be thrown away.
*/
private boolean eof;
/**
* Place holder for the 3 bytes we're dealing with for our base64 logic.
* Bitwise operations store and extract the base64 encoding or decoding from
* this variable.
*/
private int x;
/**
* Default constructor: lineLength is 76, and the lineSeparator is CRLF
* when encoding, and all forms can be decoded.
*/
public Base64() {
this(CHUNK_SIZE, CHUNK_SEPARATOR);
}
/**
* <p>
* Consumer can use this constructor to choose a different lineLength
* when encoding (lineSeparator is still CRLF). All forms of data can
* be decoded.
* </p><p>
* Note: lineLengths that aren't multiples of 4 will still essentially
* end up being multiples of 4 in the encoded data.
* </p>
*
* @param lineLength each line of encoded data will be at most this long
* (rounded up to nearest multiple of 4).
* If lineLength <= 0, then the output will not be divided into lines (chunks).
* Ignored when decoding.
*/
public Base64(int lineLength) {
this(lineLength, CHUNK_SEPARATOR);
}
/**
* <p>
* Consumer can use this constructor to choose a different lineLength
* and lineSeparator when encoding. All forms of data can
* be decoded.
* </p><p>
* Note: lineLengths that aren't multiples of 4 will still essentially
* end up being multiples of 4 in the encoded data.
* </p>
* @param lineLength Each line of encoded data will be at most this long
* (rounded up to nearest multiple of 4). Ignored when decoding.
* If <= 0, then output will not be divided into lines (chunks).
* @param lineSeparator Each line of encoded data will end with this
* sequence of bytes.
* If lineLength <= 0, then the lineSeparator is not used.
* @throws IllegalArgumentException The provided lineSeparator included
* some base64 characters. That's not going to work!
*/
public Base64(int lineLength, byte[] lineSeparator) {
this.lineLength = lineLength;
this.lineSeparator = new byte[lineSeparator.length];
System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
if (lineLength > 0) {
this.encodeSize = 4 + lineSeparator.length;
} else {
this.encodeSize = 4;
}
this.decodeSize = encodeSize - 1;
if (containsBase64Byte(lineSeparator)) {
String sep;
try {
sep = new String(lineSeparator, "UTF-8");
} catch (UnsupportedEncodingException uee) {
sep = new String(lineSeparator);
}
throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]");
}
}
/**
* Returns true if this Base64 object has buffered data for reading.
*
* @return true if there is Base64 object still available for reading.
*/
boolean hasData() { return buf != null; }
/**
* Returns the amount of buffered data available for reading.
*
* @return The amount of buffered data available for reading.
*/
int avail() { return buf != null ? pos - readPos : 0; }
/** Doubles our buffer. */
private void resizeBuf() {
if (buf == null) {
buf = new byte[8192];
pos = 0;
readPos = 0;
} else {
byte[] b = new byte[buf.length * 2];
System.arraycopy(buf, 0, b, 0, buf.length);
buf = b;
}
}
/**
* Extracts buffered data into the provided byte[] array, starting
* at position bPos, up to a maximum of bAvail bytes. Returns how
* many bytes were actually extracted.
*
* @param b byte[] array to extract the buffered data into.
* @param bPos position in byte[] array to start extraction at.
* @param bAvail amount of bytes we're allowed to extract. We may extract
* fewer (if fewer are available).
* @return The number of bytes successfully extracted into the provided
* byte[] array.
*/
int readResults(byte[] b, int bPos, int bAvail) {
if (buf != null) {
int len = Math.min(avail(), bAvail);
if (buf != b) {
System.arraycopy(buf, readPos, b, bPos, len);
readPos += len;
if (readPos >= pos) {
buf = null;
}
} else {
// Re-using the original consumer's output array is only
// allowed for one round.
buf = null;
}
return len;
} else {
return eof ? -1 : 0;
}
}
/**
* Small optimization where we try to buffer directly to the consumer's
* output array for one round (if consumer calls this method first!) instead
* of starting our own buffer.
*
* @param out byte[] array to buffer directly to.
* @param outPos Position to start buffering into.
* @param outAvail Amount of bytes available for direct buffering.
*/
void setInitialBuffer(byte[] out, int outPos, int outAvail) {
// We can re-use consumer's original output array under
// special circumstances, saving on some System.arraycopy().
if (out != null && out.length == outAvail) {
buf = out;
pos = outPos;
readPos = outPos;
}
}
/**
* <p>
* Encodes all of the provided data, starting at inPos, for inAvail bytes.
* Must be called at least twice: once with the data to encode, and once
* with inAvail set to "-1" to alert encoder that EOF has been reached,
* so flush last remaining bytes (if not multiple of 3).
* </p><p>
* Thanks to "commons" project in ws.apache.org for the bitwise operations,
* and general approach.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
*
* @param in byte[] array of binary data to base64 encode.
* @param inPos Position to start reading data from.
* @param inAvail Amount of bytes available from input for encoding.
*/
void encode(byte[] in, int inPos, int inAvail) {
if (eof) {
return;
}
// inAvail < 0 is how we're informed of EOF in the underlying data we're
// encoding.
if (inAvail < 0) {
eof = true;
if (buf == null || buf.length - pos < encodeSize) {
resizeBuf();
}
switch (modulus) {
case 1:
buf[pos++] = intToBase64[(x >> 2) & MASK_6BITS];
buf[pos++] = intToBase64[(x << 4) & MASK_6BITS];
buf[pos++] = PAD;
buf[pos++] = PAD;
break;
case 2:
buf[pos++] = intToBase64[(x >> 10) & MASK_6BITS];
buf[pos++] = intToBase64[(x >> 4) & MASK_6BITS];
buf[pos++] = intToBase64[(x << 2) & MASK_6BITS];
buf[pos++] = PAD;
break;
}
if (lineLength > 0) {
System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
pos += lineSeparator.length;
}
} else {
for (int i = 0; i < inAvail; i++) {
if (buf == null || buf.length - pos < encodeSize) {
resizeBuf();
}
modulus = (++modulus) % 3;
int b = in[inPos++];
if (b < 0) { b += 256; }
x = (x << 8) + b;
if (0 == modulus) {
buf[pos++] = intToBase64[(x >> 18) & MASK_6BITS];
buf[pos++] = intToBase64[(x >> 12) & MASK_6BITS];
buf[pos++] = intToBase64[(x >> 6) & MASK_6BITS];
buf[pos++] = intToBase64[x & MASK_6BITS];
currentLinePos += 4;
if (lineLength > 0 && lineLength <= currentLinePos) {
System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
pos += lineSeparator.length;
currentLinePos = 0;
}
}
}
}
}
/**
* <p>
* Decodes all of the provided data, starting at inPos, for inAvail bytes.
* Should be called at least twice: once with the data to decode, and once
* with inAvail set to "-1" to alert decoder that EOF has been reached.
* The "-1" call is not necessary when decoding, but it doesn't hurt, either.
* </p><p>
* Ignores all non-base64 characters. This is how chunked (e.g. 76 character)
* data is handled, since CR and LF are silently ignored, but has implications
* for other bytes, too. This method subscribes to the garbage-in, garbage-out
* philosophy: it will not check the provided data for validity.
* </p><p>
* Thanks to "commons" project in ws.apache.org for the bitwise operations,
* and general approach.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
* @param in byte[] array of ascii data to base64 decode.
* @param inPos Position to start reading data from.
* @param inAvail Amount of bytes available from input for encoding.
*/
void decode(byte[] in, int inPos, int inAvail) {
if (eof) {
return;
}
if (inAvail < 0) {
eof = true;
}
for (int i = 0; i < inAvail; i++) {
if (buf == null || buf.length - pos < decodeSize) {
resizeBuf();
}
byte b = in[inPos++];
if (b == PAD) {
x = x << 6;
switch (modulus) {
case 2:
x = x << 6;
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
break;
case 3:
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
buf[pos++] = (byte) ((x >> 8) & MASK_8BITS);
break;
}
// WE'RE DONE!!!!
eof = true;
return;
} else {
if (b >= 0 && b < base64ToInt.length) {
int result = base64ToInt[b];
if (result >= 0) {
modulus = (++modulus) % 4;
x = (x << 6) + result;
if (modulus == 0) {
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
buf[pos++] = (byte) ((x >> 8) & MASK_8BITS);
buf[pos++] = (byte) (x & MASK_8BITS);
}
}
}
}
}
}
/**
* Returns whether or not the <code>octet</code> is in the base 64 alphabet.
*
* @param octet
* The value to test
* @return <code>true</code> if the value is defined in the the base 64 alphabet, <code>false</code> otherwise.
*/
public static boolean isBase64(byte octet) {
return octet == PAD || (octet >= 0 && octet < base64ToInt.length && base64ToInt[octet] != -1);
}
/**
* Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
* Currently the method treats whitespace as valid.
*
* @param arrayOctet
* byte array to test
* @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is
* empty; false, otherwise
*/
public static boolean isArrayByteBase64(byte[] arrayOctet) {
for (int i = 0; i < arrayOctet.length; i++) {
if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
return false;
}
}
return true;
}
/*
* Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
*
* @param arrayOctet
* byte array to test
* @return <code>true</code> if any byte is a valid character in the Base64 alphabet; false herwise
*/
private static boolean containsBase64Byte(byte[] arrayOctet) {
for (int i = 0; i < arrayOctet.length; i++) {
if (isBase64(arrayOctet[i])) {
return true;
}
}
return false;
}
/**
* Encodes binary data using the base64 algorithm but does not chunk the output.
*
* @param binaryData
* binary data to encode
* @return Base64 characters
*/
public static byte[] encodeBase64(byte[] binaryData) {
return encodeBase64(binaryData, false);
}
/**
* Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks
*
* @param binaryData
* binary data to encode
* @return Base64 characters chunked in 76 character blocks
*/
public static byte[] encodeBase64Chunked(byte[] binaryData) {
return encodeBase64(binaryData, true);
}
/**
* Decodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
* Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[].
*
* @param pObject
* Object to decode
* @return An object (of type byte[]) containing the binary data which corresponds to the byte[] supplied.
* @throws DecoderException
* if the parameter supplied is not of type byte[]
*/
public Object decode(Object pObject) throws DecoderException {
if (!(pObject instanceof byte[])) {
throw new DecoderException("Parameter supplied to Base64 decode is not a byte[]");
}
return decode((byte[]) pObject);
}
/**
* Decodes a byte[] containing containing characters in the Base64 alphabet.
*
* @param pArray
* A byte array containing Base64 character data
* @return a byte array containing binary data
*/
public byte[] decode(byte[] pArray) {
return decodeBase64(pArray);
}
/**
* Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
*
* @param binaryData
* Array containing binary data to encode.
* @param isChunked
* if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
* @return Base64-encoded data.
* @throws IllegalArgumentException
* Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
*/
public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
if (binaryData == null || binaryData.length == 0) {
return binaryData;
}
Base64 b64 = isChunked ? new Base64() : new Base64(0);
long len = (binaryData.length * 4) / 3;
long mod = len % 4;
if (mod != 0) {
len += 4 - mod;
}
// If chunked, add space for one CHUNK_SEPARATOR per chunk. (Technically, these are chunk
// terminators, because even a single chunk message has one.)
//
// User length Encoded length Rounded up by 4 Num chunks Final buf len
// 56 74 76 1 78
// 57 76 76 1 78
// 58 77 80 2 84
// 59 78 80 2 84
//
// Or...
// Rounded up size: 4...76 Chunks: 1
// Rounded up size: 80..152 Chunks: 2
// Rounded up size: 156..228 Chunks: 3 ...etc...
if (isChunked) {
len += ((len + CHUNK_SIZE - 1) / CHUNK_SIZE) * CHUNK_SEPARATOR.length;
}
if (len > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Input array too big, output array would be bigger than Integer.MAX_VALUE=" + Integer.MAX_VALUE);
}
byte[] buf = new byte[(int) len];
b64.setInitialBuffer(buf, 0, buf.length);
b64.encode(binaryData, 0, binaryData.length);
b64.encode(binaryData, 0, -1); // Notify encoder of EOF.
// Encoder might have resized, even though it was unnecessary.
if (b64.buf != buf) {
b64.readResults(buf, 0, buf.length);
}
return buf;
}
/**
* Decodes Base64 data into octets
*
* @param base64Data Byte array containing Base64 data
* @return Array containing decoded data.
*/
public static byte[] decodeBase64(byte[] base64Data) {
if (base64Data == null || base64Data.length == 0) {
return base64Data;
}
Base64 b64 = new Base64();
long len = (base64Data.length * 3) / 4;
byte[] buf = new byte[(int) len];
b64.setInitialBuffer(buf, 0, buf.length);
b64.decode(base64Data, 0, base64Data.length);
b64.decode(base64Data, 0, -1); // Notify decoder of EOF.
// We have no idea what the line-length was, so we
// cannot know how much of our array wasn't used.
byte[] result = new byte[b64.pos];
b64.readResults(result, 0, result.length);
return result;
}
/**
* Discards any whitespace from a base-64 encoded block.
*
* @param data
* The base-64 encoded data to discard the whitespace from.
* @return The data, less whitespace (see RFC 2045).
* @deprecated This method is no longer needed
*/
static byte[] discardWhitespace(byte[] data) {
byte groomedData[] = new byte[data.length];
int bytesCopied = 0;
for (int i = 0; i < data.length; i++) {
switch (data[i]) {
case ' ' :
case '\n' :
case '\r' :
case '\t' :
break;
default :
groomedData[bytesCopied++] = data[i];
}
}
byte packedData[] = new byte[bytesCopied];
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
return packedData;
}
/**
* Check if a byte value is whitespace or not.
*
* @param byteToCheck the byte to check
* @return true if byte is whitespace, false otherwise
*/
private static boolean isWhiteSpace(byte byteToCheck){
switch (byteToCheck) {
case ' ' :
case '\n' :
case '\r' :
case '\t' :
return true;
default :
return false;
}
}
/**
* Discards any characters outside of the base64 alphabet, per the requirements on page 25 of RFC 2045 - "Any
* characters outside of the base64 alphabet are to be ignored in base64 encoded data."
*
* @param data
* The base-64 encoded data to groom
* @return The data, less non-base64 characters (see RFC 2045).
*/
static byte[] discardNonBase64(byte[] data) {
byte groomedData[] = new byte[data.length];
int bytesCopied = 0;
for (int i = 0; i < data.length; i++) {
if (isBase64(data[i])) {
groomedData[bytesCopied++] = data[i];
}
}
byte packedData[] = new byte[bytesCopied];
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
return packedData;
}
// Implementation of the Encoder Interface
/**
* Encodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
* Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[].
*
* @param pObject
* Object to encode
* @return An object (of type byte[]) containing the base64 encoded data which corresponds to the byte[] supplied.
* @throws EncoderException
* if the parameter supplied is not of type byte[]
*/
public Object encode(Object pObject) throws EncoderException {
if (!(pObject instanceof byte[])) {
throw new EncoderException("Parameter supplied to Base64 encode is not a byte[]");
}
return encode((byte[]) pObject);
}
/**
* Encodes a byte[] containing binary data, into a byte[] containing characters in the Base64 alphabet.
*
* @param pArray
* a byte array containing binary data
* @return A byte array containing only Base64 character data
*/
public byte[] encode(byte[] pArray) {
return encodeBase64(pArray, false);
}
// Implementation of integer encoding used for crypto
/**
* Decode a byte64-encoded integer according to crypto
* standards such as W3C's XML-Signature
*
* @param pArray a byte array containing base64 character data
* @return A BigInteger
*/
public static BigInteger decodeInteger(byte[] pArray) {
return new BigInteger(1, decodeBase64(pArray));
}
/**
* Encode to a byte64-encoded integer according to crypto
* standards such as W3C's XML-Signature
*
* @param bigInt a BigInteger
* @return A byte array containing base64 character data
* @throws NullPointerException if null is passed in
*/
public static byte[] encodeInteger(BigInteger bigInt) {
if(bigInt == null) {
throw new NullPointerException("encodeInteger called with null parameter");
}
return encodeBase64(toIntegerBytes(bigInt), false);
}
/**
* Returns a byte-array representation of a <code>BigInteger</code>
* without sign bit.
*
* @param bigInt <code>BigInteger</code> to be converted
* @return a byte array representation of the BigInteger parameter
*/
static byte[] toIntegerBytes(BigInteger bigInt) {
int bitlen = bigInt.bitLength();
// round bitlen
bitlen = ((bitlen + 7) >> 3) << 3;
byte[] bigBytes = bigInt.toByteArray();
if(((bigInt.bitLength() % 8) != 0) &&
(((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
return bigBytes;
}
// set up params for copying everything but sign bit
int startSrc = 0;
int len = bigBytes.length;
// if bigInt is exactly byte-aligned, just skip signbit in copy
if((bigInt.bitLength() % 8) == 0) {
startSrc = 1;
len--;
}
int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
byte[] resizedBytes = new byte[bitlen / 8];
System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
return resizedBytes;
}
}

View File

@ -1,179 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.email.codec.binary;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Provides Base64 encoding and decoding in a streaming fashion (unlimited size).
* When encoding the default lineLength is 76 characters and the default
* lineEnding is CRLF, but these can be overridden by using the appropriate
* constructor.
* <p>
* The default behaviour of the Base64OutputStream is to ENCODE, whereas the
* default behaviour of the Base64InputStream is to DECODE. But this behaviour
* can be overridden by using a different constructor.
* </p><p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
*
* @author Apache Software Foundation
* @version $Id $
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @since 1.0-dev
*/
public class Base64OutputStream extends FilterOutputStream {
private final boolean doEncode;
private final Base64 base64;
private final byte[] singleByte = new byte[1];
/**
* Creates a Base64OutputStream such that all data written is Base64-encoded
* to the original provided OutputStream.
*
* @param out OutputStream to wrap.
*/
public Base64OutputStream(OutputStream out) {
this(out, true);
}
/**
* Creates a Base64OutputStream such that all data written is either
* Base64-encoded or Base64-decoded to the original provided OutputStream.
*
* @param out OutputStream to wrap.
* @param doEncode true if we should encode all data written to us,
* false if we should decode.
*/
public Base64OutputStream(OutputStream out, boolean doEncode) {
super(out);
this.doEncode = doEncode;
this.base64 = new Base64();
}
/**
* Creates a Base64OutputStream such that all data written is either
* Base64-encoded or Base64-decoded to the original provided OutputStream.
*
* @param out OutputStream to wrap.
* @param doEncode true if we should encode all data written to us,
* false if we should decode.
* @param lineLength If doEncode is true, each line of encoded
* data will contain lineLength characters.
* If lineLength <=0, the encoded data is not divided into lines.
* If doEncode is false, lineLength is ignored.
* @param lineSeparator If doEncode is true, each line of encoded
* data will be terminated with this byte sequence (e.g. \r\n).
* If lineLength <= 0, the lineSeparator is not used.
* If doEncode is false lineSeparator is ignored.
*/
public Base64OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) {
super(out);
this.doEncode = doEncode;
this.base64 = new Base64(lineLength, lineSeparator);
}
/**
* Writes the specified <code>byte</code> to this output stream.
*/
public void write(int i) throws IOException {
singleByte[0] = (byte) i;
write(singleByte, 0, 1);
}
/**
* Writes <code>len</code> bytes from the specified
* <code>b</code> array starting at <code>offset</code> to
* this output stream.
*
* @param b source byte array
* @param offset where to start reading the bytes
* @param len maximum number of bytes to write
*
* @throws IOException if an I/O error occurs.
* @throws NullPointerException if the byte array parameter is null
* @throws IndexOutOfBoundsException if offset, len or buffer size are invalid
*/
public void write(byte b[], int offset, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (offset < 0 || len < 0 || offset + len < 0) {
throw new IndexOutOfBoundsException();
} else if (offset > b.length || offset + len > b.length) {
throw new IndexOutOfBoundsException();
} else if (len > 0) {
if (doEncode) {
base64.encode(b, offset, len);
} else {
base64.decode(b, offset, len);
}
flush(false);
}
}
/**
* Flushes this output stream and forces any buffered output bytes
* to be written out to the stream. If propogate is true, the wrapped
* stream will also be flushed.
*
* @param propogate boolean flag to indicate whether the wrapped
* OutputStream should also be flushed.
* @throws IOException if an I/O error occurs.
*/
private void flush(boolean propogate) throws IOException {
int avail = base64.avail();
if (avail > 0) {
byte[] buf = new byte[avail];
int c = base64.readResults(buf, 0, avail);
if (c > 0) {
out.write(buf, 0, c);
}
}
if (propogate) {
out.flush();
}
}
/**
* Flushes this output stream and forces any buffered output bytes
* to be written out to the stream.
*
* @throws IOException if an I/O error occurs.
*/
public void flush() throws IOException {
flush(true);
}
/**
* Closes this output stream, flushing any remaining bytes that must be encoded. The
* underlying stream is flushed but not closed.
*/
public void close() throws IOException {
// Notify encoder of EOF (-1).
if (doEncode) {
base64.encode(singleByte, 0, -1);
} else {
base64.decode(singleByte, 0, -1);
}
flush();
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright 2001-2004 The Apache Software Foundation.
*
* 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.email.codec.binary;
/**
* Thrown when a Decoder has encountered a failure condition during a decode.
*
* @author Apache Software Foundation
* @version $Id: DecoderException.java,v 1.9 2004/02/29 04:08:31 tobrien Exp $
*/
public class DecoderException extends Exception {
/**
* Creates a DecoderException
*
* @param pMessage A message with meaning to a human
*/
public DecoderException(String pMessage) {
super(pMessage);
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright 2001-2004 The Apache Software Foundation.
*
* 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.email.codec.binary;
/**
* Thrown when there is a failure condition during the encoding process. This
* exception is thrown when an Encoder encounters a encoding specific exception
* such as invalid data, inability to calculate a checksum, characters outside of the
* expected range.
*
* @author Apache Software Foundation
* @version $Id: EncoderException.java,v 1.10 2004/02/29 04:08:31 tobrien Exp $
*/
public class EncoderException extends Exception {
/**
* Creates a new instance of this exception with an useful message.
*
* @param pMessage a useful message relating to the encoder specific error.
*/
public EncoderException(String pMessage) {
super(pMessage);
}
}

View File

@ -16,6 +16,17 @@
package com.android.email.mail.internet;
import com.android.common.Base64;
import com.android.common.Base64OutputStream;
import com.android.email.Email;
import com.android.email.mail.Body;
import com.android.email.mail.MessagingException;
import org.apache.commons.io.IOUtils;
import android.util.Config;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -24,16 +35,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
import android.util.Config;
import android.util.Log;
import com.android.email.Email;
import com.android.email.codec.binary.Base64OutputStream;
import com.android.email.mail.Body;
import com.android.email.mail.MessagingException;
/**
* A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
* the user to write to the temp file. After the write the body is available via getInputStream
@ -82,7 +83,8 @@ public class BinaryTempFileBody implements Body {
public void writeTo(OutputStream out) throws IOException, MessagingException {
InputStream in = getInputStream();
Base64OutputStream base64Out = new Base64OutputStream(out);
Base64OutputStream base64Out = new Base64OutputStream(
out, Base64.CRLF | Base64.NO_CLOSE);
IOUtils.copy(in, base64Out);
base64Out.close();
mFile.delete();

View File

@ -16,21 +16,22 @@
package com.android.email.mail.store;
import com.android.common.Base64;
import com.android.common.Base64OutputStream;
import com.android.email.Email;
import com.android.email.Utility;
import com.android.email.codec.binary.Base64OutputStream;
import com.android.email.mail.Address;
import com.android.email.mail.Body;
import com.android.email.mail.FetchProfile;
import com.android.email.mail.Flag;
import com.android.email.mail.Folder;
import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.Message;
import com.android.email.mail.MessageRetrievalListener;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Part;
import com.android.email.mail.Store;
import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.Store.PersistentDataCallbacks;
import com.android.email.mail.Store;
import com.android.email.mail.internet.MimeBodyPart;
import com.android.email.mail.internet.MimeHeader;
import com.android.email.mail.internet.MimeMessage;
@ -69,7 +70,7 @@ import java.util.UUID;
public class LocalStore extends Store implements PersistentDataCallbacks {
/**
* History of database revisions.
*
*
* db version Shipped in Notes
* ---------- ---------- -----
* 18 pre-1.0 Development versions. No upgrade path.
@ -82,9 +83,9 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
* columns to message table.
* 24 - Added x_headers to messages table.
*/
private static final int DB_VERSION = 24;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN };
private String mPath;
@ -123,13 +124,13 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null);
int oldVersion = mDb.getVersion();
/*
* TODO we should have more sophisticated way to upgrade database.
*/
if (oldVersion != DB_VERSION) {
if (Email.LOGD) {
Log.v(Email.LOG_TAG, String.format("Upgrading database from %d to %d",
Log.v(Email.LOG_TAG, String.format("Upgrading database from %d to %d",
oldVersion, DB_VERSION));
}
if (oldVersion < 18) {
@ -157,7 +158,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
mDb.execSQL("DROP TABLE IF EXISTS pending_commands");
mDb.execSQL("CREATE TABLE pending_commands " +
"(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)");
addRemoteStoreDataTable();
addFolderDeleteTrigger();
@ -170,14 +171,14 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
if (oldVersion < 19) {
/**
* Upgrade 18 to 19: add message_id to messages table
*/
*/
mDb.execSQL("ALTER TABLE messages ADD COLUMN message_id TEXT;");
mDb.setVersion(19);
}
if (oldVersion < 20) {
/**
* Upgrade 19 to 20: add content_id to attachments table
*/
*/
mDb.execSQL("ALTER TABLE attachments ADD COLUMN content_id TEXT;");
mDb.setVersion(20);
}
@ -235,7 +236,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
mAttachmentsDir.mkdirs();
}
}
/**
* Common code to add the remote_store_data table
*/
@ -246,7 +247,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
"UNIQUE (folder_id, data_key) ON CONFLICT REPLACE" +
")");
}
/**
* Common code to add folder delete trigger
*/
@ -255,19 +256,19 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
mDb.execSQL("CREATE TRIGGER delete_folder "
+ "BEFORE DELETE ON folders "
+ "BEGIN "
+ "DELETE FROM messages WHERE old.id = folder_id; "
+ "DELETE FROM remote_store_data WHERE old.id = folder_id; "
+ "DELETE FROM messages WHERE old.id = folder_id; "
+ "DELETE FROM remote_store_data WHERE old.id = folder_id; "
+ "END;");
}
/**
* When upgrading from 22 to 23, we have to move any flags "X_DOWNLOADED_FULL" or
* "X_DOWNLOADED_PARTIAL" or "DELETED" from the old string-based storage to their own columns.
*
*
* Note: Caller should open a db transaction around this
*/
private void migrateMessageFlags() {
Cursor cursor = mDb.query("messages",
Cursor cursor = mDb.query("messages",
new String[] { "id", "flags" },
null, null, null, null, null);
try {
@ -431,7 +432,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
/**
* Set the visible limit for all folders in a given store.
*
*
* @param visibleLimit the value to write to all folders. -1 may also be used as a marker.
*/
public void resetVisibleLimits(int visibleLimit) {
@ -509,7 +510,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
return sb.toString();
}
}
/**
* LocalStore-only function to get the callbacks API
*/
@ -524,7 +525,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
public void setPersistentString(String key, String value) {
setPersistentString(-1, key, value);
}
/**
* Common implementation of getPersistentString
* @param folderId The id of the associated folder, or -1 for "store" values
@ -583,7 +584,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
public long getId() {
return mFolderId;
}
/**
* This is just used by the internal callers
*/
@ -592,7 +593,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
@Override
public void open(OpenMode mode, PersistentDataCallbacks callbacks)
public void open(OpenMode mode, PersistentDataCallbacks callbacks)
throws MessagingException {
if (isOpen()) {
return;
@ -674,7 +675,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
/**
* Return number of messages based on the state of the flags.
*
*
* @param setFlags The flags that should be set for a message to be selected (null ok)
* @param clearFlags The flags that should be clear for a message to be selected (null ok)
* @return The number of messages matching the desired flag states.
@ -685,7 +686,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM messages WHERE ");
buildFlagPredicates(sql, setFlags, clearFlags);
sql.append("messages.folder_id = ?");
open(OpenMode.READ_WRITE);
Cursor cursor = null;
try {
@ -782,7 +783,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
MimeMultipart mp = new MimeMultipart();
mp.setSubType("mixed");
localMessage.setBody(mp);
// If fetching the body, retrieve html & plaintext from DB.
// If fetching structure, simply build placeholders for them.
if (fp.contains(FetchProfile.Item.BODY)) {
@ -885,7 +886,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
/**
* The columns to select when calling populateMessageFromGetMessageCursor()
*/
private final String POPULATE_MESSAGE_SELECT_COLUMNS =
private final String POPULATE_MESSAGE_SELECT_COLUMNS =
"subject, sender_list, date, uid, flags, id, to_list, cc_list, " +
"bcc_list, reply_to_list, attachment_count, internal_date, message_id, " +
"store_flag_1, store_flag_2, flag_downloaded_full, flag_downloaded_partial, " +
@ -893,7 +894,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
/**
* Populate a message from a cursor with the following columns:
*
*
* 0 subject
* 1 from address
* 2 date (long)
@ -965,7 +966,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
try {
cursor = mDb.rawQuery(
"SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS +
" FROM messages" +
" FROM messages" +
" WHERE uid = ? AND folder_id = ?",
new String[] {
message.getUid(), Long.toString(mFolderId)
@ -992,7 +993,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
cursor = mDb.rawQuery(
"SELECT " + POPULATE_MESSAGE_SELECT_COLUMNS +
" FROM messages" +
" WHERE folder_id = ?",
" WHERE folder_id = ?",
new String[] {
Long.toString(mFolderId)
});
@ -1025,10 +1026,10 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
return messages.toArray(new Message[] {});
}
/**
* Return a set of messages based on the state of the flags.
*
*
* @param setFlags The flags that should be set for a message to be selected (null ok)
* @param clearFlags The flags that should be clear for a message to be selected (null ok)
* @param listener
@ -1036,7 +1037,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
* @throws MessagingException
*/
@Override
public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags,
public Message[] getMessages(Flag[] setFlags, Flag[] clearFlags,
MessageRetrievalListener listener) throws MessagingException {
// Generate WHERE clause based on flags observed
StringBuilder sql = new StringBuilder(
@ -1045,10 +1046,10 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
" WHERE ");
buildFlagPredicates(sql, setFlags, clearFlags);
sql.append("folder_id = ?");
open(OpenMode.READ_WRITE);
ArrayList<Message> messages = new ArrayList<Message>();
Cursor cursor = null;
try {
cursor = mDb.rawQuery(
@ -1070,7 +1071,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
return messages.toArray(new Message[] {});
}
/*
* Build SQL where predicates expression from set and clear flag arrays.
*/
@ -1203,9 +1204,9 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
cv.put("message_id", ((MimeMessage)message).getMessageId());
cv.put("store_flag_1", makeFlagNumeric(message, Flag.X_STORE_1));
cv.put("store_flag_2", makeFlagNumeric(message, Flag.X_STORE_2));
cv.put("flag_downloaded_full",
cv.put("flag_downloaded_full",
makeFlagNumeric(message, Flag.X_DOWNLOADED_FULL));
cv.put("flag_downloaded_partial",
cv.put("flag_downloaded_partial",
makeFlagNumeric(message, Flag.X_DOWNLOADED_PARTIAL));
cv.put("flag_deleted", makeFlagNumeric(message, Flag.DELETED));
cv.put("x_headers", ((MimeMessage) message).getExtendedHeaders());
@ -1291,7 +1292,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
makeFlagNumeric(message, Flag.X_DOWNLOADED_PARTIAL),
makeFlagNumeric(message, Flag.DELETED),
message.getExtendedHeaders(),
message.mId
});
@ -1520,7 +1521,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
}
}
/**
* Support for local persistence for our remote stores.
* Will open the folder if necessary.
@ -1539,12 +1540,12 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
/**
* Transactionally combine a key/value and a complete message flags flip. Used
* Transactionally combine a key/value and a complete message flags flip. Used
* for setting sync bits in messages.
*
*
* Note: Not all flags are supported here and can only be changed with Message.setFlag().
* For example, Flag.DELETED has side effects (removes attachments).
*
*
* @param key
* @param value
* @param setFlags
@ -1558,7 +1559,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
if (key != null) {
setPersistentString(key, value);
}
// take care of flags
ContentValues cv = new ContentValues();
if (setFlags != null) {
@ -1591,14 +1592,14 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
}
}
mDb.update("messages", cv,
mDb.update("messages", cv,
"folder_id = ?", new String[] { Long.toString(mFolderId) });
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
}
@Override
@ -1725,8 +1726,8 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
StringBuilder sb = null;
boolean nonEmpty = false;
for (Flag flag : Flag.values()) {
if (flag != Flag.X_STORE_1 && flag != Flag.X_STORE_2 &&
flag != Flag.X_DOWNLOADED_FULL && flag != Flag.X_DOWNLOADED_PARTIAL &&
if (flag != Flag.X_STORE_1 && flag != Flag.X_STORE_2 &&
flag != Flag.X_DOWNLOADED_FULL && flag != Flag.X_DOWNLOADED_PARTIAL &&
flag != Flag.DELETED &&
message.isSet(flag)) {
if (sb == null) {
@ -1741,7 +1742,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
return (sb == null) ? null : sb.toString();
}
/**
* Convert flags to numeric form (0 or 1) for database storage.
* @param message The message containing the flag of interest
@ -1805,7 +1806,8 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
public void writeTo(OutputStream out) throws IOException, MessagingException {
InputStream in = getInputStream();
Base64OutputStream base64Out = new Base64OutputStream(out);
Base64OutputStream base64Out = new Base64OutputStream(
out, Base64.CRLF | Base64.NO_CLOSE);
IOUtils.copy(in, base64Out);
base64Out.close();
}
@ -1814,7 +1816,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
return mUri;
}
}
/**
* LocalStore does not have SettingActivity.
*/

View File

@ -17,7 +17,7 @@
package com.android.email.mail.transport;
import com.android.common.Base64;
import com.android.email.codec.binary.Base64OutputStream;
import com.android.common.Base64OutputStream;
import com.android.email.mail.Address;
import com.android.email.mail.MessagingException;
import com.android.email.mail.internet.MimeUtility;
@ -225,10 +225,18 @@ public class Rfc822Output {
inStream = context.getContentResolver().openInputStream(fileUri);
// switch to output stream for base64 text output
writer.flush();
Base64OutputStream base64Out = new Base64OutputStream(out);
Base64OutputStream base64Out = new Base64OutputStream(
out, Base64.CRLF | Base64.NO_CLOSE);
// copy base64 data and close up
IOUtils.copy(inStream, base64Out);
base64Out.close();
// The old Base64OutputStream wrote an extra CRLF after
// the output. It's not required by the base-64 spec; not
// sure if it's required by RFC 822 or not.
out.write('\r');
out.write('\n');
out.flush();
}
catch (FileNotFoundException fnfe) {
// Ignore this - empty file is OK

View File

@ -1,162 +0,0 @@
/*
* Copyright (C) 2008 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.email.codec.binary;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.TestCase;
/**
* A series of tests of the Base64 encoder.
*/
@SmallTest
public class Base64Test extends TestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
/**
* Looking for issues with line length and trailing zeros. The code we're modeling is
* in mail.internet.TextBody:
* byte[] bytes = mBody.getBytes("UTF-8");
* out.write(Base64.encodeBase64Chunked(bytes));
*/
public void testLineLength54() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(54));
checkBase64Structure(out, 1);
}
public void testLineLength55() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(55));
checkBase64Structure(out, 1);
}
public void testLineLength56() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(56));
checkBase64Structure(out, 1);
}
public void testLineLength57() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(57));
checkBase64Structure(out, 1);
}
public void testLineLength58() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(58));
checkBase64Structure(out, 2);
}
public void testLineLength59() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(59));
checkBase64Structure(out, 2);
}
/**
* Repeat the above tests with 2x line lengths
*/
public void testLineLength111() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(111));
checkBase64Structure(out, 2);
}
public void testLineLength112() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(112));
checkBase64Structure(out, 2);
}
public void testLineLength113() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(113));
checkBase64Structure(out, 2);
}
public void testLineLength114() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(114));
checkBase64Structure(out, 2);
}
public void testLineLength115() {
byte[] out = Base64.encodeBase64Chunked(getByteArray(115));
checkBase64Structure(out, 3);
}
/**
* Validate that base64 output is structurally sound. Does not independently confirm
* that the actual encoding is valid.
*/
private void checkBase64Structure(byte[] buffer, int expectedChunks) {
// outer loop - divide into chunks
int chunkCount = 0;
int chunkStart;
int nextChunkStart = 0;
int limit = buffer.length;
while (nextChunkStart < limit) {
chunkStart = -1;
int chunkEnd;
for (chunkEnd = nextChunkStart; chunkEnd < limit; ++chunkEnd) {
assertFalse("nulls in chunk", buffer[chunkEnd] == 0);
if (buffer[chunkEnd] == '\r') {
assertTrue(buffer[chunkEnd+1] == '\n');
chunkStart = nextChunkStart;
break;
}
if (chunkEnd == limit) {
chunkStart = nextChunkStart;
break;
}
}
chunkCount++;
nextChunkStart = chunkEnd + 2;
assertTrue("chunk not found", chunkStart >= 0);
// At this point we have a single chunk from chunkStart to chunkEnd
// And we can analyze it for structural correctness
int chunkLen = chunkEnd - chunkStart;
// Max chunk length
assertTrue("chunk length <= 76", chunkLen <= 76);
// Multiple of 4 (every 3 bytes of source -> 4 bytes of output)
assertEquals("chunk length mod 4", 0, chunkLen % 4);
// 0, 1 or 2 '=' at the end
boolean lastEquals1 = buffer[chunkEnd-1] == '=';
boolean lastEquals2 = buffer[chunkEnd-2] == '=';
boolean lastEquals3 = buffer[chunkEnd-3] == '=';
assertTrue("trailing equals",
(!lastEquals1 && !lastEquals2) || // 0
(lastEquals1 && !lastEquals2) || // or 1
(lastEquals1 && lastEquals2)); // or 2
}
assertEquals("total chunk count", expectedChunks, chunkCount);
}
/**
* Generate a test sequence of a given length.
*/
private byte[] getByteArray(int size) {
byte[] result = new byte[size];
byte fillChar = '1';
for (int i = 0; i < size; ++i) {
result[i] = fillChar++;
if (fillChar > '9') {
fillChar = '0';
}
}
return result;
}
}