/* * This class is a part of Base64 in Apach commons codec. * * 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.kakao.helper; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.Arrays; /** * * @author MJ */ public class Base64 { private static final int BYTES_PER_UNENCODED_BLOCK = 3; private static final int BYTES_PER_ENCODED_BLOCK = 4; private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; private static final int DEFAULT_BUFFER_SIZE = 8192; private static final int MASK_6BITS = 0x3f; private static final int EOF = -1; private static final int MIME_CHUNK_SIZE = 76; private static final byte[] CHUNK_SEPARATOR = {'\r', '\n'}; private static final Charset UTF_8 = Charset.forName("UTF-8"); private final int unencodedBlockSize; private final int encodedBlockSize; private final int lineLength; private final int chunkSeparatorLength; private static final byte PAD_DEFAULT = '='; private final byte PAD = PAD_DEFAULT; private final byte[] lineSeparator; @SuppressWarnings("unused") private final int decodeSize; private final int encodeSize; private final byte[] encodeTable; private final byte[] decodeTable = DECODE_TABLE; private static final byte[] DECODE_TABLE = { -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, 62, -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, 63, -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 }; private static final byte[] STANDARD_ENCODE_TABLE = { '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', '+', '/' }; private static final byte[] URL_SAFE_ENCODE_TABLE = { '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', '-', '_' }; static class Context { int ibitWorkArea; long lbitWorkArea; byte[] buffer; int pos; int readPos; boolean eof; int currentLinePos; int modulus; Context() { } public String toString() { return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, " + "modulus=%s, pos=%s, readPos=%s]", this.getClass().getSimpleName(), Arrays.toString(buffer), currentLinePos, eof, ibitWorkArea, lbitWorkArea, modulus, pos, readPos); } } private Base64(final boolean urlSafe) { this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe); } private Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) { this.unencodedBlockSize = BYTES_PER_UNENCODED_BLOCK; this.encodedBlockSize = BYTES_PER_ENCODED_BLOCK; this.chunkSeparatorLength = lineSeparator == null ? 0 : lineSeparator.length; final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0; this.lineLength = useChunking ? (lineLength / encodedBlockSize) * encodedBlockSize : 0; if (lineSeparator != null) { if (containsAlphabetOrPad(lineSeparator)) { final String sep = newStringUtf8(lineSeparator); throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]"); } if (lineLength > 0) { this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length; this.lineSeparator = new byte[lineSeparator.length]; System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length); } else { this.encodeSize = BYTES_PER_ENCODED_BLOCK; this.lineSeparator = null; } } else { this.encodeSize = BYTES_PER_ENCODED_BLOCK; this.lineSeparator = null; } this.decodeSize = this.encodeSize - 1; this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; } boolean containsAlphabetOrPad(final byte[] arrayOctet) { if (arrayOctet == null) { return false; } for (final byte element : arrayOctet) { if (PAD == element || isInAlphabet(element)) { return true; } } return false; } public static String encodeBase64URLSafeString(final byte[] binaryData) { return newStringUtf8(encodeBase64(binaryData, false, true)); } private static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) { return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE); } private static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe, final int maxResultSize) { if (binaryData == null || binaryData.length == 0) { return binaryData; } final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe); final long len = b64.getEncodedLength(binaryData); if (len > maxResultSize) { throw new IllegalArgumentException("Input array too big, the output array would be bigger (" + len + ") than the specified maximum size of " + maxResultSize); } return b64.encode(binaryData); } byte[] encode(final byte[] pArray) { if (pArray == null || pArray.length == 0) { return pArray; } final Context context = new Context(); encode(pArray, 0, pArray.length, context); encode(pArray, 0, EOF, context); final byte[] buf = new byte[context.pos - context.readPos]; readResults(buf, 0, buf.length, context); return buf; } void encode(final byte[] in, int inPos, final int inAvail, final Context context) { if (context.eof) { return; } // inAvail < 0 is how we're informed of EOF in the underlying data we're // encoding. if (inAvail < 0) { context.eof = true; if (0 == context.modulus && lineLength == 0) { return; // no leftovers to process and not using chunking } final byte[] buffer = ensureBufferSize(encodeSize, context); final int savedPos = context.pos; switch (context.modulus) { // 0-2 case 0: // nothing to do here break; case 1: // 8 bits = 6 + 2 // top 6 bits: buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS]; // remaining 2: buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS]; // URL-SAFE skips the padding to further reduce size. if (encodeTable == STANDARD_ENCODE_TABLE) { buffer[context.pos++] = PAD; buffer[context.pos++] = PAD; } break; case 2: // 16 bits = 6 + 6 + 4 buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS]; buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS]; buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS]; // URL-SAFE skips the padding to further reduce size. if (encodeTable == STANDARD_ENCODE_TABLE) { buffer[context.pos++] = PAD; } break; default: throw new IllegalStateException("Impossible modulus " + context.modulus); } context.currentLinePos += context.pos - savedPos; // keep track of current line position // if currentPos == 0 we are at the start of a line, so don't add CRLF if (lineLength > 0 && context.currentLinePos > 0) { System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); context.pos += lineSeparator.length; } } else { for (int i = 0; i < inAvail; i++) { final byte[] buffer = ensureBufferSize(encodeSize, context); context.modulus = (context.modulus + 1) % BYTES_PER_UNENCODED_BLOCK; int b = in[inPos++]; if (b < 0) { b += 256; } context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS]; buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS]; buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS]; buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS]; context.currentLinePos += BYTES_PER_ENCODED_BLOCK; if (lineLength > 0 && lineLength <= context.currentLinePos) { System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); context.pos += lineSeparator.length; context.currentLinePos = 0; } } } } } byte[] ensureBufferSize(final int size, final Context context) { if ((context.buffer == null) || (context.buffer.length < context.pos + size)) { return resizeBuffer(context); } return context.buffer; } int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) { if (context.buffer != null) { final int len = Math.min(available(context), bAvail); System.arraycopy(context.buffer, context.readPos, b, bPos, len); context.readPos += len; if (context.readPos >= context.pos) { context.buffer = null; // so hasData() will return false, and this method can return -1 } return len; } return context.eof ? EOF : 0; } int available(final Context context) { // package protected for access from I/O streams return context.buffer != null ? context.pos - context.readPos : 0; } private byte[] resizeBuffer(final Context context) { if (context.buffer == null) { context.buffer = new byte[getDefaultBufferSize()]; context.pos = 0; context.readPos = 0; } else { final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR]; System.arraycopy(context.buffer, 0, b, 0, context.buffer.length); context.buffer = b; } return context.buffer; } int getDefaultBufferSize() { return DEFAULT_BUFFER_SIZE; } long getEncodedLength(final byte[] pArray) { long len = ((pArray.length + unencodedBlockSize - 1) / unencodedBlockSize) * (long) encodedBlockSize; if (lineLength > 0) { len += ((len + lineLength - 1) / lineLength) * chunkSeparatorLength; } return len; } private static String newStringUtf8(final byte[] bytes) { return newString(bytes, UTF_8); } private static String newString(final byte[] bytes, final Charset charset) { try { return bytes == null ? null : new String(bytes, charset.name()); } catch (UnsupportedEncodingException e) { return null; } } boolean isInAlphabet(final byte octet) { return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1; } }