/* * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FilterOutputStream; import java.io.FilterInputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; import java.security.AccessController; import sun.security.action.GetPropertyAction; /** * This class contains classes and methods for encoding and decoding * byte data using Base64 encoding scheme. The implementation of this * class supports different variants of Base64 as specified in * RFC 4648 and * RFC 2045. * * @author Xueming Shen * @since 1.8 */ public class Base64 { /** * Returns an {@link java.util.Base64.Encoder} that uses the "The * Base64 Alphabet" as specified in Table 1 of the RFC 4648 and * RFC 2045 for encoding operation. The encoder does not add any * line feed (line separator) character. * * @return A Base64 encoder. */ public static Encoder getEncoder() { return Encoder.RFC4648; } /** * Returns an {@link java.util.Base64.Encoder} that uses the "URL and * Filename safe Base64 Alphabet" as specified in Table 2 of the RFC * 4648 for encoding operation. The encoder does not add any line feed * (line separator) character. * * @return A Base64 encoder. */ public static Encoder getUrlEncoder() { return Encoder.RFC4648_URLSAFE; } /** * Returns a MIME type {@link java.util.Base64.Encoder} that uses * the "The Base64 Alphabet" as specified in Table 1 of the RFC 4648 * for encoding operation. The encoded output must be represented in * lines of no more than 76 characters each. * * @return A Base64 encoder. */ public static Encoder getMimeEncoder() { return new Encoder(false, crlf, MIMELINEMAX); } /** * Returns a MIME type {@link java.util.Base64.Encoder} that uses * the "The Base64 Alphabet" as specified in Table 1 of the RFC 2045 * for encoding operation. The encoded output must be represented in * lines of no more than {@code lineLength} characters each and uses * {@code lineSeparator} as the line separator. * * @param lineLength the length of each output line (rounded down * to nearest multiple of 4). If {@code lineLength <= 0} * the output will not be separated in lines * @param lineSeparator the line separator for each output line, not * null * * @return A Base64 encoder. */ public static Encoder getEncoder(int lineLength, byte[] lineSeparator) { Objects.requireNonNull(lineSeparator); return new Encoder(false, lineSeparator, lineLength / 4 * 4); } /** * Returns a {@link java.util.Base64.Decoder} that uses the "The Base64 * Alphabet" as specified in Table 1 of the RFC 4648 and RFC 2045 for * encoding operation. The decoder rejects data that contains characters * outside the base64 alphabet. * * @return A Base64 decoder. */ public static Decoder getDecoder() { return Decoder.RFC4648; } /** * Returns a {@link java.util.Base64.Decoder} that uses the "URL and * Filename safe Base64 Alphabet" as specified in Table 2 of the RFC * 4648 for decoding operation. The decoder rejects data that contains * characters outside the base64 alphabet. * * @return A Base64 decoder. */ public static Decoder getUrlDecoder() { return Decoder.RFC4648_URLSAFE; } /** * Returns a MIME type {@link java.util.Base64.Encoder} that uses the * "The Base64 Alphabet" as specified in Table 1 of the RFC 2045 and * RFC 4648 for decoding operation. All line separators or other * characters not found in the Base64 alphabet table are ignored in * decoding operation. * * @return A Base64 decoder. */ public static Decoder getMimeDecoder() { return Decoder.RFC2045; } /* * This class implements an encoder for encoding byte data using * Base64 encoding scheme as specified in RFC 4648 and RFC 2045. */ public static class Encoder { /** * This array is a lookup table that translates 6-bit positive integer * index values into their "Base64 Alphabet" equivalents as specified * in "Table 1: The Base64 Alphabet" of RFC 2045 (and RFC 4648). */ private static final byte[] toBase64 = { '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', '+', '/' }; /** * It's the lookup table for "URL and Filename safe Base64" as specified * in Table 2 of the RFC 4648, with the '+' and '/' changed to '-' and * '_'. This table is used when BASE64_URL is specified. */ private static final byte[] toBase64URL = { '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 final Encoder RFC4648 = new Encoder(false, null, -1); static final Encoder RFC4648_URLSAFE = new Encoder(true, null, -1); private final byte[] newline; private final int linemax; private final boolean isURL; private Encoder(boolean isURL, byte[] newline, int linemax) { this.isURL = isURL; this.newline = newline; this.linemax = linemax; } /** * Encodes the specified byte array into a newly-allocated byte array * using Base64 encoding scheme. * * @param src the byte array to encode * @return A newly-allocated byte array containing the resulting * encoded bytes. */ public byte[] encode(byte[] src) { int dlen = 4 * ((src.length + 2) / 3); // dst array size if (linemax > 0) // line separators dlen += (dlen - 1) / linemax * newline.length; byte[] dst = new byte[dlen]; int ret = encode0(src, dst); if (ret != dst.length) // TBD: not necessary return Arrays.copyOf(dst, ret); return dst; } /** * Encodes the specified byter array into a String using Base64 * encoding scheme. * * @param src the byte array to encode * @return A String containing the resulting Base64 encoded characters */ public String encodeToString(byte[] src) { return new String(encode(src), StandardCharsets.US_ASCII); } /** * Encodes the remaining bytes of a specified byte buffer into a * newly-allocated ByteBuffer using Base64 encoding scheme. * * Upon return, the source buffer's position will be updated to * its limit; its limit will not have been changed. * * @param buffer the ByteBuffer to encode * @return A newly-allocated byte array containing the encoded bytes. * The buffer's position will be zero and its limit will * be the number of resulting bytes */ public ByteBuffer encode(ByteBuffer buffer) { return ByteBuffer.wrap(encode(getBytes(buffer))); } /** * Encodes the specified input byte array using Base64 encoding scheme, * writing the resulting bytes to the given output byte array. * *

It is the responsibility of the invoker of this methold to make * sure the output byte arry {@code dst} has enough space for encoding * all bytes from the input byte array. An IllegalArgumentException will * be thrown if the output byte array is not big enough, and no byte will * be written to the output byte array. If the {@code dst} is null, this * method returns the length of the byte array needed to encode all * input bytes. * * @param src the byte array to encocde * @param dst the output byte array * @return The number of bytes writeen to the output byte array, * or the length of the byte array needed to encode all * bytes from the input byte array, if the output byte * array {@code dst} is null. * @throws IllegalArgumentException if {@code dst} does not have * enough space for encoding all input bytes. */ public int encode(byte[] src, byte[] dst) { int dlen = 4 * ((src.length + 2) / 3); // dst array size if (linemax > 0) { dlen += (dlen - 1) / linemax * newline.length; } if (dst == null) return dlen; if (dst.length < dlen) throw new IllegalArgumentException( "output byte[] is too small for encoding all input bytes"); return encode0(src, dst); } /** * Encodes as many bytes as possible from the input byte buffter * using Base64 encoding scheme, writing the resulting bytes to * the given output byte buffer. * *

The buffers are read from, and written to, starting at their * current positions. Upon return, the input and output buffers' * positions will be advanced to reflect the bytes read and written, * but their limits will not be modified. * *

The decoding operation will stop and return if either all * remaining bytes in the input buffer have been encoded and * written to the output buffer, or the output buffer has * insufficient space to encode any more input bytes. The * encoding operation can be continued, if there is more bytes * in input buffer to be encoded, by invoking this method again * with an output buffer that has more {@linkplain Buffer#remaining * remaining} bytes.This is typically done by draining any encoded * bytes from the output buffer. The value returned from last * invocation needs to be passed in as the third parameter * {@code bytesOut} if it is to continue an unfinished encoding, * 0 otherwise. * *

Recommended Usage Example *

         *    ByteBuffer bbIn = ...;
         *    ByteBuffer bbOut = ...;
         *    ...
         *    Base64.Encoder enc = Base64.getMimeDecoder();
         *    int bytesOut = enc.encode(bbIn, bbOut, 0);
         *    while (bbIn.hasRemaining()) {
         *        // read bytes out of bbOut, clear more space
         *        ...
         *        bbOut.clear();
         *        bytesOut = enc.encode(bbIn, bbOut, bytesOut);
         *    }
         * 
* * @param src the input byte buffer to encocde * @param dst the output byte buffer * @param bytesOut * the return value of last invocation if this is * to continue an unfinished encoding operation, 0 * otherwise * @return The sum of {@code bytesOut} and the number of * bytes written to the output ByteBuffer during this * invocation. */ public int encode(ByteBuffer src, ByteBuffer dst, int bytesOut) { if (src.hasArray() && dst.hasArray()) return encodeArray(src, dst, bytesOut); return encodeBuffer(src, dst, bytesOut); } private int encodeArray(ByteBuffer src, ByteBuffer dst, int bytesOut) { byte[] base64 = isURL? toBase64URL : toBase64; byte[] sa = src.array(); int sp = src.position(); int sl = src.limit(); byte[] da = dst.array(); int dp = dst.position(); int dp0 = dst.position(); int dl = dst.limit(); int linepos = 0; try { if (linemax > 0 && bytesOut > 0) { linepos = bytesOut % (linemax + newline.length); if (linepos == linemax && sp < sl) { if (dl < dp + newline.length) return bytesOut; for (byte b : newline) da[dp++] = b; linepos = 0; } } int atom = src.remaining() / 3; while (atom-- > 0) { //all 24-bit atoms to Base64 if (dl < dp + 4) return dp - dp0 + bytesOut; int bits = (sa[sp++] & 0xff) << 16 | (sa[sp++] & 0xff) << 8 | (sa[sp++] & 0xff); da[dp++] = base64[(bits >>> 18) & 0x3f]; da[dp++] = base64[(bits >>> 12) & 0x3f]; da[dp++] = base64[(bits >>> 6) & 0x3f]; da[dp++] = base64[bits & 0x3f]; linepos += 4; if (linepos == linemax && sp < sl) { if (dl < dp + newline.length) return dp - dp0 + bytesOut; for (byte b : newline){ da[dp++] = b; } linepos = 0; } } if (sp < sl && dl > dp + 4) { // 1 or 2 leftover bytes int b0 = sa[sp++] & 0xff; da[dp++] = base64[b0 >> 2]; if (sp == sl) { da[dp++] = base64[(b0 << 4) & 0x3f]; da[dp++] = '='; da[dp++] = '='; } else { int b1 = sa[sp++] & 0xff; da[dp++] = base64[(b0 << 4) & 0x3f | (b1 >> 4)]; da[dp++] = base64[(b1 << 2) & 0x3f]; da[dp++] = '='; } } return dp - dp0 + bytesOut; } finally { src.position(sp); dst.position(dp); } } private int encodeBuffer(ByteBuffer src, ByteBuffer dst, int bytesOut) { byte[] base64 = isURL? toBase64URL : toBase64; int atom = src.remaining() / 3; int linepos = 0; int dp0 = dst.position(); if (linemax > 0 && bytesOut > 0) { linepos = bytesOut % (linemax + newline.length); if (linepos == linemax && src.hasRemaining()) { if (dst.remaining() < newline.length) return bytesOut; dst.put(newline); linepos = 0; } } for (int i = 0; i < atom; i++) { //all 24-bit atoms to Base64 if (dst.remaining() < 4) return dst.position() - dp0 + bytesOut; int bits = (src.get() & 0xff) << 16 | (src.get() & 0xff) << 8 | (src.get() & 0xff); dst.put(base64[(bits >>> 18) & 0x3f]); dst.put(base64[(bits >>> 12) & 0x3f]); dst.put(base64[(bits >>> 6) & 0x3f]); dst.put(base64[bits & 0x3f]); linepos += 4; if (linepos == linemax && src.hasRemaining()) { if (dst.remaining() < newline.length) return dst.position() - dp0 + bytesOut; dst.put(newline); linepos = 0; } } int r = src.remaining(); if (r > 0 && dst.remaining() > 3) { // 1 or 2 leftover bytes int b0 = src.get() & 0xff; dst.put(base64[b0 >> 2]); if (r == 1) { dst.put(base64[(b0 << 4) & 0x3f]); dst.put((byte)'='); dst.put((byte)'='); } else { int b1 = src.get() & 0xff; dst.put(base64[(b0 << 4) & 0x3f | (b1 >> 4)]); dst.put(base64[(b1 << 2) & 0x3f]); dst.put((byte)'='); } } return dst.position() - dp0 + bytesOut; } /** * Wraps an output stream for encoding byte data using Base64 encoding * scheme. * *

It is recommended to promptly close the returned output stream after * use, in which it will flush all possible leftover bytes to the underlying * output stream. Closing the returned output stream will close the underlying * output stream. * * @param os the output stream. * @return the output stream for encoding the byte data into * the specified Base64 encoded format */ public OutputStream wrap(OutputStream os) { return new EncOutputStream(os, isURL ? toBase64URL : toBase64, newline, linemax); } private final int encode0(byte[] src, byte[] dst) { byte[] base64 = isURL? toBase64URL : toBase64; int sl = src.length / 3 * 3; int dl = dst.length - 4; int linepos = 0; int nls = 0; for (int sp = 0, dp = 0; sp < sl; ) { //all 24-bit atoms to Base64 int bits = (src[sp++] & 0xff) << 16 | (src[sp++] & 0xff) << 8 | (src[sp++] & 0xff); dst[dp++] = base64[(bits >>> 18) & 0x3f]; dst[dp++] = base64[(bits >>> 12) & 0x3f]; dst[dp++] = base64[(bits >>> 6) & 0x3f]; dst[dp++] = base64[bits & 0x3f]; linepos += 4; if (linepos == linemax && sp < src.length) { for (byte b : newline){ dst[dp++] = b; } linepos = 0; nls += newline.length; } } int dp = src.length / 3 * 4 + nls; if (sl < src.length) { // 1 or 2 leftover bytes int b0 = src[sl++] & 0xff; dst[dp++] = base64[b0 >> 2]; if (sl == src.length) { dst[dp++] = base64[(b0 << 4) & 0x3f]; dst[dp++] = '='; dst[dp++] = '='; } else { int b1 = src[sl++] & 0xff; dst[dp++] = base64[(b0 << 4) & 0x3f | (b1 >> 4)]; dst[dp++] = base64[(b1 << 2) & 0x3f]; dst[dp++] = '='; } } return dp; } } /* * This class implements a decoder for decoding byte data using * Base64 encoding scheme as specified in RFC 4648 and RFC 2045. */ public static class Decoder { /** * Lookup table for decoding 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 encoded to -1. */ private static final int[] fromBase64 = new int[256]; static { Arrays.fill(fromBase64, -1); for (int i = 0; i < Encoder.toBase64.length; i++) fromBase64[Encoder.toBase64[i]] = i; } /** * Lookup table for decoding "URL and Filename safe Base64 Alphabet" * as specified in Table2 of the RFC 4648. */ private static final int[] fromBase64URL = new int[256]; static { Arrays.fill(fromBase64URL, -1); for (int i = 0; i < Encoder.toBase64URL.length; i++) fromBase64URL[Encoder.toBase64URL[i]] = i; } static final Decoder RFC4648 = new Decoder(false, false); static final Decoder RFC4648_URLSAFE = new Decoder(true, false); static final Decoder RFC2045 = new Decoder(false, true); private final boolean isURL; private final boolean isMIME; private Decoder(boolean isURL, boolean isMIME) { this.isURL = isURL; this.isMIME = isMIME; } /** * Decodes the specified byter array into a newly-allocated byte array * using Base64 encoding scheme. * * @param src the byte array to decode * @return A newly-allocated byte array containing the decoded bytes. * @throws IllegalArgumentException if src is not in valid * Base64 scheme */ public byte[] decode(byte[] src) { byte[] dst = new byte[outLength(src)]; int ret = decode0(src, dst); if (ret != dst.length) { //TBD: not necessary if scan mime dst = Arrays.copyOf(dst, ret); } return dst; } /** * Decodes the remaining bytes of a specified byte buffer into a * newly-allocated ByteBuffer using Base64 encoding scheme. * * Upon return, the source buffer's position will be updated to * its limit; its limit will not have been changed. The position * will not be changed if IllegalArgumentException is thrown * during decoding. * * @param src the ByteBuffer to decode * @return A newly-allocated byte array containing the decoded * bytes. The buffer's position will be zero and its limit * will be the number of decoded bytes * @throws IllegalArgumentException if src is not in valid * Base64 scheme. */ public ByteBuffer decode(ByteBuffer src) { int pos = src.position(); try { return ByteBuffer.wrap(decode(getBytes(src))); } catch (IllegalArgumentException iae) { src.position(pos); throw iae; } } /** * Decodes a Base64 encoded String into a newly-allocated byte array * using Base64 encoding scheme. * * @param src the string to decode * * @return A newly-allocated byte array containing the decoded bytes. * * @throws IllegalArgumentException if src is not in valid * Base64 scheme */ public byte[] decode(String src) { // TBD: non-ascii -> '?' -> IAE now. pre-screen for better err msg? return decode(src.getBytes(StandardCharsets.US_ASCII)); } /** * Decodes the specified input byte array using Base64 encoding scheme, * writing the resulting bytes to the given output byte array. * *

It is the responsibility of the invoker of this methold to make * sure the output byte arry {@code dst} has enough space for decoding * all bytes from the input byte array. If the output byte array is not * big enough, an IllegalArgumentException will be thrown and no byte * will be be written to the output byte array. If the {@code dst} is * null, this method returns the length of the byte array needed to * decode all input bytes. * * @param src the byte array to decocde * @param dst the output byte array * @return The number of bytes writeen to the output byte array, * or the length of the byte array needed to decode all * bytes from the input byte array, if the output byte * array {@code dst} is null. * @throws IllegalArgumentException if src is not in valid * Base64 scheme, or {@code dst} does not have enough space * for decoding all input bytes. */ public int decode(byte[] src, byte[] dst) { int dlen = outLength(src); if (dst == null) return dlen; if (dst.length < dlen) throw new IllegalArgumentException( "output byte[] is too small for decoding all input bytes"); return decode0(src, dst); } /** * Decodes as many bytes as possible from the input byte buffter * using Base64 encoding scheme, writing the resulting bytes to * the given output byte buffer. * *

The buffers are read from, and written to, starting at their * current positions. Upon return, the input and output buffers' * positions will be advanced to reflect the bytes read and written, * but their limits will not be modified. * *

The decoding operation will finish and return {@code true} if * either the limit of the input buffer has been reached (no more * byte date to decode) or the Base64 padding character {@code '='} * is met at current position. The decoder will consume as many * padding character as needed to finish the decoding.

* *

The decoding operation will stop and return {@code false} if * the output buffer has insufficient space to decode any more input * bytes. The decoding operation can be resumed by invoking this * method again with an output buffer that has more * {@linkplain Buffer#remaining remaining} bytes. This is typically * done by draining any encoded bytes from the output buffer.

* *

Recommended Usage Example *

         *    ByteBuffer bbIn = ...;
         *    ByteBuffer bbOut = ...;
         *    Base64.Decoder dec = Base64.getDecoder();
         *    ...
         *    while (!dec.decode(bbIn, bbOut)) {
         *        // read bytes out of bbOut, clear bbOut for more space
         *        ...
         *        bbOut.clear();
         *    }
         * 
* * @param src the input byte buffer to decocde * @param dst the output byte buffer * @return true if decoding operation has finished, there is no more * byte in input buffer can be decoded. false, if decoding * operation stops because there is no more space in output * buffer. * @throws IllegalArgumentException if src is not in valid * Base64 scheme. */ public boolean decode(ByteBuffer src, ByteBuffer dst) { if (src.hasArray() && dst.hasArray()) return decodeArray(src, dst); return decodeBuffer(src, dst); } /** * Returns an input stream for decoding Base64 encoded byte stream. * *

Closing the returned input stream will close the underlying * input stream. * * @param is the input stream. * @return the input stream for decoding the specified Base64 * encoded byte stream */ public InputStream wrap(InputStream is) { return new DecInputStream(is, isURL?fromBase64URL:fromBase64, isMIME); } private boolean decodeArray(ByteBuffer src, ByteBuffer dst) { int[] base64 = isURL ? fromBase64URL : fromBase64; int bits = 0; int shiftto = 18; // pos of first byte of 4-byte atom byte[] sa = src.array(); int sp = src.arrayOffset() + src.position(); int sl = src.arrayOffset() + src.limit(); byte[] da = dst.array(); int dp = dst.arrayOffset() + dst.position(); int dl = dst.arrayOffset() + dst.limit(); int mark = sp; boolean padding = false; try { while (sp < sl) { int b = sa[sp++]; if (b == '=') { padding = true; break; } if ((b = base64[b]) == -1) { if (isMIME) // skip if for rfc2045 continue; else throw new IllegalArgumentException( "Illegal character " + Integer.toString(b, 16)); } if (dl < dp + 3) return false; bits |= (b << shiftto); shiftto -= 6; if (shiftto < 0) { da[dp++] = (byte) (bits >> 16); da[dp++] = (byte) (bits >> 8); da[dp++] = (byte) (bits); shiftto = 18; bits = 0; mark = sp; } } if (shiftto == 6) { if (dl - dp < 1) return false; da[dp++] = (byte)(bits >> 16); if (sp < sl && sa[sp] == '=') // consume the second '=' sp++; mark = sp; } else if (shiftto == 0) { if (dl - dp < 2) return false; da[dp++] = (byte)(bits >> 16); da[dp++] = (byte) (bits >> 8); mark = sp; } else if (padding || shiftto != 18) { throw new IllegalArgumentException( "last unit does not have enough valid bits"); } return true; } finally { src.position(mark); dst.position(dp); } } private boolean decodeBuffer(ByteBuffer src, ByteBuffer dst) { int[] base64 = isURL ? fromBase64URL : fromBase64; int bits = 0; int shiftto = 18; int mark = src.position(); boolean padding = false; try { while (src.hasRemaining()) { int b = src.get() & 0xff; if (b == '=') { // padding byte padding = true; mark = src.position(); break; } if ((b = base64[b]) == -1) { if (isMIME) // skip if for rfc2045 continue; else throw new IllegalArgumentException( "Illegal character at " + (src.position() - 1)); } if (dst.remaining() < 3) return false; bits |= (b << shiftto); shiftto -= 6; if (shiftto < 0) { dst.put((byte) (bits >> 16)); dst.put((byte) (bits >> 8)); dst.put((byte) (bits)); shiftto = 18; bits = 0; mark = src.position(); } } if (shiftto == 6) { if (dst.remaining() < 1) return false; dst.put((byte)(bits >> 16)); if (src.hasRemaining() && src.get() == '=') mark = src.position(); } else if (shiftto == 0) { if (dst.remaining() < 2) return false; dst.put((byte)(bits >> 16)); dst.put((byte) (bits >> 8)); } else if (padding || shiftto != 18) { throw new IllegalArgumentException( "last unit does not have enough valid bits"); } return true; } finally { src.position(mark); } } private int outLength(byte[] src) { int[] base64 = isURL ? fromBase64URL : fromBase64; int paddings = 0; int len = src.length; if (len == 0) return 0; if (len < 4) throw new IllegalArgumentException( "input byte[] should at least have 4 bytes for base64 bytes"); if (src[len - 1] == '=') { paddings++; if (src[len - 2] == '=') paddings++; } if (isMIME) { // scan all bytes to fill out all non-alphabet. a performance // trade-off of pre-scan or Arrays.copyOf int sp = 0; int n = 0; while (sp < src.length) { int b = src[sp++]; if (b == '=') break; if ((b = base64[b]) == -1) n++; } len -= n; } return 3 * (len / 4) - paddings; } private int decode0(byte[] src, byte[] dst) { int[] base64 = isURL ? fromBase64URL : fromBase64; int sp = 0, dp = 0; int bits = 0; int shiftto = 18; // pos of first byte of 4-byte atom boolean padding = false; while (sp < src.length) { int b = src[sp++]; if (b == '=') { // padding byte padding = true; break; } if ((b = base64[b]) == -1) { if (isMIME) // skip if for rfc2045 continue; else throw new IllegalArgumentException( "Illegal character " + Integer.toString(b, 16)); } bits |= (b << shiftto); shiftto -= 6; if (shiftto < 0) { dst[dp++] = (byte) (bits >> 16); dst[dp++] = (byte) (bits >> 8); dst[dp++] = (byte) (bits); shiftto = 18; bits = 0; } } // hit padding '=' or reach end if (shiftto == 6) { // xx== dst[dp++] = (byte)(bits >> 16); } else if (shiftto == 0) { // xxx= dst[dp++] = (byte)(bits >> 16); dst[dp++] = (byte) (bits >> 8); } else if (padding || shiftto != 18) { throw new IllegalArgumentException( "last unit does not have enough valid bits"); } return dp; } } private Base64() {} private static final int MIMELINEMAX = 76; private static final byte[] crlf = new byte[] {'\r', '\n'}; private static byte[] getBytes(ByteBuffer bb) { byte[] buf = null; if (bb.hasArray()) { byte[] tmp = bb.array(); if ((tmp.length == bb.capacity()) && (tmp.length == bb.remaining())) { buf = tmp; bb.position(bb.limit()); } } if (buf == null) { buf = new byte[bb.remaining()]; bb.get(buf); } return buf; } /* * An output stream for encoding bytes into the Base64. */ private static class EncOutputStream extends FilterOutputStream { private int leftover = 0; private int b0, b1, b2; private boolean closed = false; private final byte[] base64; // byte->base64 mapping private final byte[] newline; // line separator, if needed private final int linemax; private int linepos = 0; EncOutputStream(OutputStream os, byte[] base64, byte[] newline, int linemax) { super(os); this.base64 = base64; this.newline = newline; this.linemax = linemax; } public void write(int b) throws IOException { byte[] buf = new byte[1]; buf[0] = (byte)(b & 0xff); write(buf, 0, 1); } private final void checkNewline() throws IOException { if (linepos == linemax) { out.write(newline); linepos = 0; } } public void write(byte[] b, int off, int len) throws IOException { if (closed) throw new IOException("stream is closed"); if (off < 0 || len < 0 || off + len > b.length) throw new ArrayIndexOutOfBoundsException(); if (len == 0) return; if (leftover != 0) { if (leftover == 1) { b1 = b[off++] & 0xff; len--; if (len == 0) { leftover++; return; } } b2 = b[off++] & 0xff; len--; checkNewline(); out.write(base64[b0 >> 2]); out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]); out.write(base64[(b1 << 2) & 0x3f | (b2 >> 6)]); out.write(base64[b2 & 0x3f]); linepos += 4; } int nBits24 = len / 3; leftover = len - (nBits24 * 3); while (nBits24-- > 0) { checkNewline(); int b0 = b[off++] & 0xff; int b1 = b[off++] & 0xff; int b2 = b[off++] & 0xff; out.write(base64[b0 >> 2]); out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]); out.write(base64[(b1 << 2) & 0x3f | (b2 >> 6)]); out.write(base64[b2 & 0x3f]); linepos += 4; } if (leftover == 1) { b0 = b[off++] & 0xff; } else if (leftover == 2) { b0 = b[off++] & 0xff; b1 = b[off++] & 0xff; } } void end() throws IOException { if (leftover == 1) { checkNewline(); out.write(base64[b0 >> 2]); out.write(base64[(b0 << 4) & 0x3f]); out.write('='); out.write('='); } else if (leftover == 2) { checkNewline(); out.write(base64[b0 >> 2]); out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]); out.write(base64[(b1 << 2) & 0x3f]); out.write('='); } leftover = 0; } public void close() throws IOException { if (!closed) { end(); closed = true; out.close(); } } } /* * An input stream for decoding Base64 bytes */ private static class DecInputStream extends InputStream { private final InputStream is; private final boolean isMIME; private final int[] base64; // base64 -> byte mapping private int bits = 0; // 24-bit buffer for decoding private int nextin = 18; // next available "off" in "bits" for input; // -> 18, 12, 6, 0 private int nextout = -8; // next available "off" in "bits" for output; // -> 8, 0, -8 (no byte for output) private boolean eof = false; private boolean closed = false; DecInputStream(InputStream is, int[] base64, boolean isMIME) { this.is = is; this.base64 = base64; this.isMIME = isMIME; } private byte[] sbBuf = new byte[1]; public int read() throws IOException { return read(sbBuf, 0, 1) == -1 ? -1 : sbBuf[0] & 0xff; } public int read(byte[] b, int off, int len) throws IOException { if (closed) throw new IOException("Stream closed"); if (eof && nextout < 0) // eof and no leftover return -1; if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException(); int oldOff = off; if (nextout >= 0) { // leftover output byte(s) in bits buf do { if (len == 0) return off - oldOff; b[off++] = (byte)(bits >> nextout); len--; nextout -= 8; } while (nextout >= 0); bits = 0; } while (len > 0) { int v = is.read(); if (v == -1) { eof = true; if (nextin != 18) throw new IOException("base64 stream has un-decoded dangling byte(s)."); if (off == oldOff) return -1; else return off - oldOff; } if (v == '=') { // padding byte(s) if (nextin != 6 && nextin != 0) { throw new IOException("Illegal base64 ending sequence:" + nextin); } b[off++] = (byte)(bits >> (16)); len--; if (nextin == 0) { // only one padding byte if (len == 0) { // no enough output space bits >>= 8; // shift to lowest byte nextout = 0; } else { b[off++] = (byte) (bits >> 8); } } eof = true; break; } if ((v = base64[v]) == -1) { if (isMIME) // skip if for rfc2045 continue; else throw new IOException("Illegal base64 character " + Integer.toString(v, 16)); } bits |= (v << nextin); if (nextin == 0) { nextin = 18; // clear for next nextout = 16; while (nextout >= 0) { b[off++] = (byte)(bits >> nextout); len--; nextout -= 8; if (len == 0 && nextout >= 0) { // don't clean "bits" return off - oldOff; } } bits = 0; } else { nextin -= 6; } } return off - oldOff; } public int available() throws IOException { if (closed) throw new IOException("Stream closed"); return is.available(); // TBD: } public void close() throws IOException { if (!closed) { is.close(); closed = true; } } } }