Base64.java

/***************************************************************************
   Copyright 2008 Emily Estes

   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 net.metanotion.util;


import java.nio.charset.StandardCharsets;

/** This class provides to convert between an array of bytes and an ASCII character set
Base64 encoded representation of the byte array. */
public final class Base64 {
	/** This is used for padding at the end of an encoded sequence. Ascii code for '=' */
	private static final byte BASE64_PADDING = 61;
	private static final int OCTET_SECOND = 8;
	private static final int OCTET_THIRD = 16;
	private static final int SEXTET_SECOND = 6;
	private static final int SEXTET_THIRD = 12;
	private static final int SEXTET_FOURTH = 18;
	private static final int FIRST_BYTE = 0x000000FF;
	private static final int SECOND_BYTE = 0x0000FF00;
	private static final int THIRD_BYTE = 0x00FF0000;
	private static final int FIRST_6BIT = 0x0000003F;

	private static final byte[] b64_tbl = {
		65,  66,  67,   68,  69,  70,  71,  72,
		73,  74,  75,   76,  77,  78,  79,  80,
		81,  82,  83,   84,  85,  86,  87,  88,
		89,  90,  97,   98,  99, 100, 101, 102,
		103, 104, 105, 106, 107, 108, 109, 110,
		111, 112, 113, 114, 115, 116, 117, 118,
		119, 120, 121, 122,  48,  49,  50,  51,
		52,   53,  54,  55,  56,  57,  43,  47 };

	private static final int[] b64_inv = {
		-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, -2, -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, -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, -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, -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, -1, -1, -1, -1 };

	/** Convert a byte array to a base64 encoded string representation.
		@param data The byte array to convert.
		@return A String with the base64 encoded data using the "US-ASCII" encoding.
	*/
	public static String encode(final byte[] data) {
		final byte[] outAscii = new byte[Base64.encodeLength(data.length)];
		Base64.encode(data, 0, data.length, outAscii, 0);
		return new String(outAscii, StandardCharsets.US_ASCII);
	}

	/** Convert a String containing base64 encoded data into a byte array containing the decoded data.
		@param data ASCII character set string of base64 encoded data.
		@return the byte array represented by the string.
	*/
	public static byte[] decode(final String data) {
		if(data == null) { return null; }
		final byte[] s = data.getBytes(StandardCharsets.US_ASCII);
		final byte[] out = new byte[Base64.decodeLength(s, 0, s.length)];
		Base64.decode(s, 0, s.length, out, 0);
		return out;
	}

	/** Given a byte array representation of base64 encoded data in US-ASCII character set
		calculate how long the decoded byte array representation will be. This is a VERY low level
		function and does not verify the string is in fact properly encoded.
		@param b The byte array we're going to decode.
		@param off The offset into the byte array to start.
		@param len The length of the base64 representation in the byte array.
		@return The length in bytes of the decoded string.
	*/
	public static int decodeLength(final byte[] b, final int off, final int len) {
		final int x = Math.min(b.length, off + len) - off;
		if(x < 0) { throw new IllegalArgumentException(); }
		int y = (x * 3) / 4;
		if(b[(off + x) - 1] == BASE64_PADDING) { y--; }
		if(b[(off + x) - 2] == BASE64_PADDING) { y--; }
		return y;
	}

	/** Calculate how many bytes will be required to encode a byte array of a given length in base64.
		@param length The size of the byte array to encode.
		@return The size in bytes required to hold a base64 representation of the byte array.
	*/
	public static int encodeLength(final int length) {
		if(length < 0) { throw new IllegalArgumentException(); }
		final int x=length * 4;
		final int y=x / 3;
		if((x%3)==1) { return y + 3; }
		if((x%3)==2) { return y + 2; }
		return y;
	}

	/** Decode a base64 representation in a byte array into a buffer.
		@param src The byte array containing a base64 representation to decode.
		@param srcoff The offset into src to start decoding.
		@param srclen The number of bytes in src to decode.
		@param dest The buffer to decode src into.
		@param destoff The offset in the buffer to start storing the decoded representation.
		@return length The number of bytes successfully decoded into dest.
	*/
	public static int decode(final byte[] src, final int srcoff, final int srclen, final byte[] dest, final int destoff) {
		final int[] a = new int[4];
		int chars=0;

		int length = 0;
		final int len = Math.min(srcoff + srclen, src.length);
		for(int pos=srcoff;pos < len; pos++) {
			if(b64_inv[src[pos]] != -1) {
				a[chars] = b64_inv[src[pos]];
				chars++;
				if(chars==4) {
					// decode
					if(a[2] == -2) {
						final int val = (a[0] << 2) + (a[1] >> 4);
						dest[length] = (byte) (FIRST_BYTE & val);
						length++;
						break;
					} else if(a[3] == -2) {
						final int val = (a[0] << 10) + (a[1] << 4) + (a[2] >> 2);
						dest[length] = (byte) (FIRST_BYTE & (val >>> OCTET_SECOND));
						length++;
						dest[length] = (byte) (FIRST_BYTE & val);
						length++;
						break;
					} else {
						final int val = (a[0] << SEXTET_FOURTH) + (a[1] << SEXTET_THIRD) + (a[2] << SEXTET_SECOND) + (a[3]);
						dest[length] = (byte) (FIRST_BYTE & (val >>> OCTET_THIRD));
						length++;
						dest[length] = (byte) (FIRST_BYTE & (val >>> OCTET_SECOND));
						length++;
						dest[length] = (byte) (FIRST_BYTE & val);
						length++;
					}
					chars = 0;
				}
			}
		}
		return length;
	}

	/** Encode a byte array into a base64 representation in a buffer.
		@param src The byte array to encode
		@param srcoff The offset into src to start decoding.
		@param srclen The number of bytes in src to decode.
		@param dest The buffer to encode src into.
		@param destoff The offset in the buffer to start storing the encoded representation.
		@return length The number of bytes representing the encoding in dest.
	*/
	public static int encode(final byte[] src, final int srcoff, final int srclen, final byte[] dest, final int destoff) {
		int outpos=destoff;

		final int length = Math.min(srcoff + srclen, src.length);
		for(int pos=srcoff; pos<length; pos+=3,outpos+=4) {
			final int a = src[pos];
			final int b = ((pos + 1) < length) ? src[pos + 1] : 0;
			final int c = ((pos + 2) < length) ? src[pos + 2] : 0;

			final int val = (THIRD_BYTE & (a << OCTET_THIRD)) + (SECOND_BYTE & (b << OCTET_SECOND)) + (FIRST_BYTE & c);
			final int oa = (FIRST_6BIT & (val >>> SEXTET_FOURTH));
			final int ob = (FIRST_6BIT & (val >>> SEXTET_THIRD));
			final int oc = (FIRST_6BIT & (val >>> SEXTET_SECOND));
			final int od = (FIRST_6BIT & val);

			dest[outpos + 0] = b64_tbl[oa];
			dest[outpos + 1] = b64_tbl[ob];
			if(length >= (pos + 3)) {
				dest[outpos + 2] = b64_tbl[oc];
				dest[outpos + 3] = b64_tbl[od];
			} else if((length - pos) == 2) {
				dest[outpos + 2] = b64_tbl[oc];
				dest[outpos + 3] = BASE64_PADDING;
			} else {
				dest[outpos + 2] = BASE64_PADDING;
				dest[outpos + 3] = BASE64_PADDING;
			}
		}
		return outpos - destoff;
	}
}