finish replacing Email's base64 implementation with android-common
Change-Id: I19adbbb884311d70073e9f7a961aa6808ac0dfb4
This commit is contained in:
parent
a8d44824c3
commit
ba714999f2
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue