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

net.dongliu.cute.http.internal.AsyncInflater Maven / Gradle / Ivy

The newest version!
package net.dongliu.cute.http.internal;

import net.dongliu.commons.io.Buffers;

import java.nio.ByteBuffer;
import java.util.function.Consumer;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import static java.lang.Byte.toUnsignedInt;
import static java.lang.Byte.toUnsignedLong;

/**
 * Copy from netty JdkZlibDecoder.
 */
public class AsyncInflater {
    private static final int FHCRC = 0x02;
    private static final int FEXTRA = 0x04;
    private static final int FNAME = 0x08;
    private static final int FCOMMENT = 0x10;
    private static final int FRESERVED = 0xE0;

    private Inflater inflater;
    //    private final byte[] dictionary;

    // GZIP related
    private final CRC32 crc;

    private enum GzipState {
        HEADER_START,
        HEADER_END,
        FLG_READ,
        XLEN_READ,
        SKIP_FNAME,
        SKIP_COMMENT,
        PROCESS_FHCRC,
        FOOTER_START,
    }

    private GzipState gzipState = GzipState.HEADER_START;
    private int flags = -1;
    private int xlen = -1;

    private volatile boolean finished;

    private boolean decideZlibOrNone;

    public static final int GZIP = 1;
    public static final int ZLIB = 2;

    public AsyncInflater(int wrapper) {
        switch (wrapper) {
            case GZIP:
                inflater = new Inflater(true);
                crc = new CRC32();
                break;
            case ZLIB:
                inflater = new Inflater();
                crc = null;
                break;
            default:
                throw new IllegalArgumentException("unknown inflate type:" + wrapper);
        }
    }

    public boolean finished() {
        return finished;
    }

    /**
     * Decode piece of data.
     *
     * @param in       the input data
     * @param consumer notified when new decompressed ByteBuffer is filled.
     */
    public void decode(ByteBuffer in, Consumer consumer) {
        if (finished) {
            // Skip data received after finished.
            in.position(in.limit());
            return;
        }

        if (in.remaining() <= 0) {
            return;
        }

        if (decideZlibOrNone) {
            // First two bytes are needed to decide if it's a ZLIB stream.
            if (in.remaining() < 2) {
                return;
            }
            boolean nowrap = !looksLikeZlib(in.getShort(0));
            inflater = new Inflater(nowrap);
            decideZlibOrNone = false;
        }

        if (crc != null) {
            switch (gzipState) {
                case FOOTER_START:
                    if (readGZIPFooter(in)) {
                        finished = true;
                    }
                    return;
                default:
                    if (gzipState != GzipState.HEADER_END) {
                        if (!readGZIPHeader(in)) {
                            return;
                        }
                    }
            }
        }

        int readableBytes = in.remaining();
        if (in.hasArray()) {
            inflater.setInput(in.array(), in.arrayOffset() + in.position(), in.remaining());
        } else {
            byte[] array = new byte[in.remaining()];
            in.duplicate().get(array);
            inflater.setInput(array);
        }

        int maxOutputLength = inflater.getRemaining() << 1;
        ByteBuffer decompressed = ByteBuffer.allocate(maxOutputLength);
        try {
            boolean readFooter = false;
            byte[] outArray = decompressed.array();
            while (!inflater.needsInput()) {
                int outIndex = decompressed.arrayOffset() + decompressed.position();
                int length = outArray.length - outIndex;

                if (length == 0) {
                    // completely filled the buffer allocate a new one and start to fill it
                    consumer.accept(decompressed.flip());
                    decompressed = ByteBuffer.allocate(maxOutputLength);
                    outArray = decompressed.array();
                    continue;
                }

                int outputLength = inflater.inflate(outArray, outIndex, length);
                if (outputLength > 0) {
                    Buffers.advance(decompressed, outputLength);
                    if (crc != null) {
                        crc.update(outArray, outIndex, outputLength);
                    }
                } else {
                    if (inflater.needsDictionary()) {
                        throw new RuntimeException("no dictionary was specified");
                        //                        inflater.setDictionary(dictionary);
                    }
                }

                if (inflater.finished()) {
                    if (crc == null) {
                        finished = true; // Do not decode anymore.
                    } else {
                        readFooter = true;
                    }
                    break;
                }
            }

            Buffers.advance(in, readableBytes - inflater.getRemaining());

            if (readFooter) {
                gzipState = GzipState.FOOTER_START;
                if (readGZIPFooter(in)) {
                    finished = true;
                }
            }
        } catch (DataFormatException e) {
            throw new RuntimeException("decompression failure", e);
        } finally {
            decompressed.flip();
            if (decompressed.limit() > 0) {
                consumer.accept(decompressed);
            }
        }
    }

    /**
     * Called when input data finished.
     */
    public void onFinish() {
        if (inflater != null) {
            inflater.end();
        }
    }

    private boolean readGZIPHeader(ByteBuffer in) {
        switch (gzipState) {
            case HEADER_START:
                if (in.remaining() < 10) {
                    return false;
                }
                // read magic numbers
                int magic0 = in.get();
                int magic1 = in.get();

                if (magic0 != 31) {
                    throw new RuntimeException("Input is not in the GZIP format");
                }
                crc.update(magic0);
                crc.update(magic1);

                int method = toUnsignedInt(in.get());
                if (method != Deflater.DEFLATED) {
                    throw new RuntimeException("Unsupported compression method "
                            + method + " in the GZIP header");
                }
                crc.update(method);

                flags = toUnsignedInt(in.get());
                crc.update(flags);

                if ((flags & FRESERVED) != 0) {
                    throw new RuntimeException(
                            "Reserved flags are set in the GZIP header");
                }

                // mtime (int)
                crc.update(in.get());
                crc.update(in.get());
                crc.update(in.get());
                crc.update(in.get());

                crc.update(toUnsignedInt(in.get())); // extra flags
                crc.update(toUnsignedInt(in.get())); // operating system

                gzipState = GzipState.FLG_READ;
            case FLG_READ:
                if ((flags & FEXTRA) != 0) {
                    if (in.remaining() < 2) {
                        return false;
                    }
                    int xlen1 = toUnsignedInt(in.get());
                    int xlen2 = toUnsignedInt(in.get());
                    crc.update(xlen1);
                    crc.update(xlen2);

                    xlen |= xlen1 << 8 | xlen2;
                }
                gzipState = GzipState.XLEN_READ;
            case XLEN_READ:
                if (xlen != -1) {
                    if (in.remaining() < xlen) {
                        return false;
                    }
                    byte[] xtra = new byte[xlen];
                    in.get(xtra);
                    crc.update(xtra);
                }
                gzipState = GzipState.SKIP_FNAME;
            case SKIP_FNAME:
                if ((flags & FNAME) != 0) {
                    if (in.remaining() <= 0) {
                        return false;
                    }
                    do {
                        int b = toUnsignedInt(in.get());
                        crc.update(b);
                        if (b == 0x00) {
                            break;
                        }
                    } while (in.remaining() > 0);
                }
                gzipState = GzipState.SKIP_COMMENT;
            case SKIP_COMMENT:
                if ((flags & FCOMMENT) != 0) {
                    if (in.remaining() <= 0) {
                        return false;
                    }
                    do {
                        int b = toUnsignedInt(in.get());
                        crc.update(b);
                        if (b == 0x00) {
                            break;
                        }
                    } while (in.remaining() > 0);
                }
                gzipState = GzipState.PROCESS_FHCRC;
            case PROCESS_FHCRC:
                if ((flags & FHCRC) != 0) {
                    if (in.remaining() < 4) {
                        return false;
                    }
                    verifyCrc(in);
                }
                crc.reset();
                gzipState = GzipState.HEADER_END;
            case HEADER_END:
                return true;
            default:
                throw new IllegalStateException();
        }
    }

    private boolean readGZIPFooter(ByteBuffer buf) {
        if (buf.remaining() < 8) {
            return false;
        }

        verifyCrc(buf);

        // read ISIZE and verify
        int dataLength = 0;
        for (int i = 0; i < 4; ++i) {
            dataLength |= toUnsignedInt(buf.get()) << i * 8;
        }
        int readLength = inflater.getTotalOut();
        if (dataLength != readLength) {
            throw new RuntimeException(
                    "Number of bytes mismatch. Expected: " + dataLength + ", Got: " + readLength);
        }
        return true;
    }

    private void verifyCrc(ByteBuffer in) {
        long crcValue = 0;
        for (int i = 0; i < 4; ++i) {
            crcValue |= toUnsignedLong(in.get()) << i * 8;
        }
        long readCrc = crc.getValue();
        if (crcValue != readCrc) {
            throw new RuntimeException("CRC value missmatch. Expected: " + crcValue + ", Got: " + readCrc);
        }
    }

    /*
     * Returns true if the cmf_flg parameter (think: first two bytes of a zlib stream)
     * indicates that this is a zlib stream.
     * 

* You can lookup the details in the ZLIB RFC: * RFC 1950. */ private static boolean looksLikeZlib(short cmf_flg) { return (cmf_flg & 0x7800) == 0x7800 && cmf_flg % 31 == 0; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy