All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.moon.core.util.Base64Decoder Maven / Gradle / Ivy

package com.moon.core.util;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;

/**
 * @author moonsky
 */
public final class Base64Decoder extends Base64Coder {

    private final boolean isURL;
    private final boolean isMIME;

    private Base64Decoder(boolean isURL, boolean isMIME) {
        this.isURL = isURL;
        this.isMIME = isMIME;
    }

    static final int[] fromBase64 = new int[256];

    static {
        Arrays.fill(fromBase64, -1);
        for (int i = 0; i < toBase64.length; i++) {
            fromBase64[toBase64[i]] = i;
        }
        fromBase64['='] = -2;
    }

    static final int[] fromBase64URL = new int[256];

    static {
        Arrays.fill(fromBase64URL, -1);
        for (int i = 0; i < toBase64URL.length; i++) {
            fromBase64URL[toBase64URL[i]] = i;
        }
        fromBase64URL['='] = -2;
    }

    static final Base64Decoder RFC4648 = new Base64Decoder(false, false);
    static final Base64Decoder RFC4648_URL_SAFE = new Base64Decoder(true, false);
    static final Base64Decoder RFC2045 = new Base64Decoder(false, true);

    public byte[] decode(byte[] src) {
        byte[] dst = new byte[outLength(src, 0, src.length)];
        int ret = decode0(src, 0, src.length, dst);
        if (ret != dst.length) {
            dst = Arrays.copyOf(dst, ret);
        }
        return dst;
    }

    public byte[] decode(String src) {
        return decode(src.getBytes(StandardCharsets.ISO_8859_1));
    }

    public int decode(byte[] src, byte[] dst) {
        int len = outLength(src, 0, src.length);
        if (dst.length < len) {
            throw new IllegalArgumentException("Output byte array is too small for decoding all input bytes");
        }
        return decode0(src, 0, src.length, dst);
    }

    public ByteBuffer decode(ByteBuffer buffer) {
        int pos0 = buffer.position();
        try {
            byte[] src;
            int sp, sl;
            if (buffer.hasArray()) {
                src = buffer.array();
                sp = buffer.arrayOffset() + buffer.position();
                sl = buffer.arrayOffset() + buffer.limit();
                buffer.position(buffer.limit());
            } else {
                src = new byte[buffer.remaining()];
                buffer.get(src);
                sp = 0;
                sl = src.length;
            }
            byte[] dst = new byte[outLength(src, sp, sl)];
            return ByteBuffer.wrap(dst, 0, decode0(src, sp, sl, dst));
        } catch (IllegalArgumentException iae) {
            buffer.position(pos0);
            throw iae;
        }
    }

    public InputStream wrap(InputStream is) {
        return new DecodeInputStream(Objects.requireNonNull(is), isURL ? fromBase64URL : fromBase64, isMIME);
    }

    private int outLength(byte[] src, int sp, int sl) {
        int[] base64 = isURL ? fromBase64URL : fromBase64;
        int paddings = 0;
        int len = sl - sp;
        if (len == 0) { return 0; }
        if (len < 2) {
            if (isMIME && base64[0] == -1) { return 0; }
            throw new IllegalArgumentException("Input byte[] should at least have 2 bytes for base64 bytes");
        }
        if (isMIME) {
            // scan all bytes to fill out all non-alphabet. a performance
            // trade-off of pre-scan or Arrays.copyOf
            int n = 0;
            while (sp < sl) {
                int b = src[sp++] & 0xff;
                if (b == '=') {
                    len -= (sl - sp + 1);
                    break;
                }
                if ((b = base64[b]) == -1) { n++; }
            }
            len -= n;
        } else {
            if (src[sl - 1] == '=') {
                paddings++;
                if (src[sl - 2] == '=') { paddings++; }
            }
        }
        if (paddings == 0 && (len & 0x3) != 0) { paddings = 4 - (len & 0x3); }
        return 3 * ((len + 3) / 4) - paddings;
    }

    private int decode0(byte[] src, int sp, int sl, byte[] dst) {
        int[] base64 = isURL ? fromBase64URL : fromBase64;
        int dp = 0;
        int bits = 0;
        int shiftto = 18;
        // pos of first byte of 4-byte atom
        while (sp < sl) {
            int b = src[sp++] & 0xff;
            if ((b = base64[b]) < 0) {
                if (b == -2) {
                    // padding byte '='
                    // =     shiftto==18 unnecessary padding
                    // x=    shiftto==12 a dangling single x
                    // x     to be handled together with non-padding case
                    // xx=   shiftto==6&&sp==sl missing last =
                    // xx=y  shiftto==6 last is not =
                    if (shiftto == 6 && (sp == sl || src[sp++] != '=') || shiftto == 18) {
                        throw new IllegalArgumentException("Input byte array has wrong 4-byte ending unit");
                    }
                    break;
                }
                if (isMIME)    // skip if for rfc2045
                { continue; } else {
                    throw new IllegalArgumentException("Illegal base64 character " + Integer.toString(src[sp - 1], 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;
            }
        }
        // reached end of byte array or hit padding '=' characters.
        if (shiftto == 6) {
            dst[dp++] = (byte) (bits >> 16);
        } else if (shiftto == 0) {
            dst[dp++] = (byte) (bits >> 16);
            dst[dp++] = (byte) (bits >> 8);
        } else if (shiftto == 12) {
            // dangling single "x", incorrectly encoded.
            throw new IllegalArgumentException("Last unit does not have enough valid bits");
        }
        // anything left is invalid, if is not MIME.
        // if MIME, ignore all non-base64 character
        while (sp < sl) {
            if (isMIME && base64[src[sp++]] < 0) { continue; }
            throw new IllegalArgumentException("Input byte array has incorrect ending byte at " + sp);
        }
        return dp;
    }

    /**
     * An input stream for decoding Base64 bytes
     */
    private static class DecodeInputStream 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;

        DecodeInputStream(InputStream is, int[] base64, boolean isMIME) {
            this.is = is;
            this.base64 = base64;
            this.isMIME = isMIME;
        }

        private byte[] sbBuf = new byte[1];

        @Override
        public int read() throws IOException {
            return read(sbBuf, 0, 1) == -1 ? -1 : sbBuf[0] & 0xff;
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            if (closed) { throw new IOException("Stream is 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) {
                        if (nextin == 12) { throw new IOException("Base64 stream has one un-decoded dangling byte."); }
                        // treat ending xx/xxx without padding character legal.
                        // same logic as v == '=' below
                        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);
                            }
                        }
                    }
                    if (off == oldOff) { return -1; } else { return off - oldOff; }
                }
                if (v == '=') {                  // padding byte(s)
                    // =     shiftto==18 unnecessary padding
                    // x=    shiftto==12 dangling x, invalid unit
                    // xx=   shiftto==6 && missing last '='
                    // xx=y  or last is not '='
                    if (nextin == 18 || nextin == 12 || nextin == 6 && is.read() != '=') {
                        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;
        }

        @Override
        public int available() throws IOException {
            if (closed) { throw new IOException("Stream is closed"); }
            return is.available();   // TBD:
        }

        @Override
        public void close() throws IOException {
            if (!closed) {
                closed = true;
                is.close();
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy