divconq.ctp.stream.UngzipStream.java Source code

Java tutorial

Introduction

Here is the source code for divconq.ctp.stream.UngzipStream.java

Source

/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2014 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.ctp.stream;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.util.ArrayList;
import java.util.List;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import org.apache.commons.compress.compressors.gzip.GzipUtils;

import divconq.ctp.f.FileDescriptor;
import divconq.hub.Hub;
import divconq.lang.op.OperationContext;
import divconq.script.StackEntry;
import divconq.util.FileUtil;
import divconq.util.StringUtil;
import divconq.xml.XElement;

public class UngzipStream extends BaseStream implements IStreamSource {
    protected static final int FHCRC = 0x02;
    protected static final int FEXTRA = 0x04;
    protected static final int FNAME = 0x08;
    protected static final int FCOMMENT = 0x10;
    protected static final int FRESERVED = 0xE0;

    private enum GzipState {
        HEADER_START, FLG_READ, XLEN_READ, SKIP_FNAME, SKIP_COMMENT, PROCESS_FHCRC, PRROCESS_CONTENT, PROCESS_FOOTER, DONE
    }

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

    protected Inflater inflater = null;
    protected byte[] dictionary = null;
    protected CRC32 crc = new CRC32();

    protected List<ByteBuf> outlist = new ArrayList<>();

    protected String nameHint = null;
    //protected String ourpath = null;
    //protected long ourmod = 0;
    //protected byte ourperm = 0;
    protected FileDescriptor ufile = null;
    protected boolean eofsent = false;

    protected ByteBuf remnant = null;

    public UngzipStream() {
        this.inflater = new Inflater(true);
        this.crc = new CRC32();
    }

    public UngzipStream(byte[] dictionary) {
        this();
        this.dictionary = dictionary;
    }

    @Override
    public void init(StackEntry stack, XElement el) {
        this.nameHint = stack.stringFromElement(el, "NameHint");
    }

    @Override
    public void close() {
        Inflater inf = this.inflater;

        if (inf != null)
            inf.end();

        this.inflater = null;

        ByteBuf rem = this.remnant;

        if (rem != null) {
            rem.release();

            this.remnant = null;
        }

        // not truly thread safe, consider
        for (ByteBuf bb : this.outlist)
            bb.release();

        this.outlist.clear();

        super.close();
    }

    // make sure we don't return without first releasing the file reference content
    @Override
    public ReturnOption handle(FileDescriptor file, ByteBuf data) {
        if (file == FileDescriptor.FINAL)
            return this.downstream.handle(file, data);

        if (this.ufile == null)
            this.initializeFileValues(file);

        // inflate the payload into 1 or more outgoing buffers set in a queue
        ByteBuf in = data;

        if (in != null) {
            ByteBuf rem = this.remnant;

            ByteBuf src = ((rem != null) && rem.isReadable()) ? Unpooled.copiedBuffer(rem, in) : in;

            this.inflate(src);

            // if there are any unread bytes here we need to store them and combine with the next "handle"
            // this would be rare since the header and footer are small, but it is possible and should be handled
            // file content has its own "in progress" buffer so no need to worry about that
            this.remnant = src.isReadable() ? src.copy() : null; // TODO wrap or slice here? we need copy above

            if (in != null)
                in.release();

            if (rem != null)
                rem.release();

            if (OperationContext.get().getTaskRun().isKilled())
                return ReturnOption.DONE;
        }

        // write all buffers in the queue
        while (this.outlist.size() > 0) {
            ReturnOption ret = this.nextMessage();

            if (ret != ReturnOption.CONTINUE)
                return ret;
        }

        // if we reached done and we wrote all the buffers, then send the EOF marker if not already
        if ((this.gzipState == GzipState.DONE) && !this.eofsent)
            return this.nextMessage();

        // otherwise we need more data
        return ReturnOption.CONTINUE;
    }

    // TODO return null if not EOF and if no bytes to send - there is no point in sending messages
    // downstream without content or EOF
    public ReturnOption nextMessage() {
        ByteBuf out = null;

        if (this.outlist.size() > 0)
            out = this.outlist.remove(0);

        boolean eof = (this.outlist.size() == 0) && (this.gzipState == GzipState.DONE);

        this.ufile.setEof(eof);

        // set it on but never off
        if (eof)
            this.eofsent = true;

        return this.downstream.handle(this.ufile, out);
    }

    public void initializeFileValues(FileDescriptor src) {
        this.ufile = new FileDescriptor();

        if (StringUtil.isNotEmpty(this.nameHint))
            this.ufile.setPath("/" + this.nameHint);
        else if (src.hasPath())
            this.ufile.setPath("/" + GzipUtils.getUncompressedFilename(src.path().getFileName()));
        else
            this.ufile.setPath("/" + FileUtil.randomFilename("bin"));

        this.ufile.setModTime(src.getModTime());
        this.ufile.setPermissions(src.getPermissions());
    }

    // return true when completely done
    protected void inflate(ByteBuf in) {
        switch (this.gzipState) {
        case HEADER_START:
            if (in.readableBytes() < 10)
                return;

            // read magic numbers
            int magic0 = in.readByte();
            int magic1 = in.readByte();

            if (magic0 != 31) {
                OperationContext.get().getTaskRun().kill("Input is not in the GZIP format");
                return;
            }

            this.crc.update(magic0);
            this.crc.update(magic1);

            int method = in.readUnsignedByte();

            if (method != Deflater.DEFLATED) {
                OperationContext.get().getTaskRun()
                        .kill("Unsupported compression method " + method + " in the GZIP header");
                return;
            }

            this.crc.update(method);

            this.flags = in.readUnsignedByte();
            this.crc.update(this.flags);

            if ((this.flags & FRESERVED) != 0) {
                OperationContext.get().getTaskRun().kill("Reserved flags are set in the GZIP header");
                return;
            }

            // mtime (int)
            this.crc.update(in.readByte());
            this.crc.update(in.readByte());
            this.crc.update(in.readByte());
            this.crc.update(in.readByte());

            this.crc.update(in.readUnsignedByte()); // extra flags
            this.crc.update(in.readUnsignedByte()); // operating system

            this.gzipState = GzipState.FLG_READ;
        case FLG_READ:
            if ((this.flags & FEXTRA) != 0) {
                if (in.readableBytes() < 2)
                    return;

                int xlen1 = in.readUnsignedByte();
                int xlen2 = in.readUnsignedByte();

                this.crc.update(xlen1);
                this.crc.update(xlen2);

                this.xlen |= xlen1 << 8 | xlen2;
            }

            this.gzipState = GzipState.XLEN_READ;
        case XLEN_READ:
            if (this.xlen != -1) {
                if (in.readableBytes() < xlen)
                    return;

                byte[] xtra = new byte[xlen];
                in.readBytes(xtra);
                this.crc.update(xtra);
            }

            this.gzipState = GzipState.SKIP_FNAME;
        case SKIP_FNAME:
            if ((this.flags & FNAME) != 0) {
                boolean gotend = false;

                while (in.isReadable()) {
                    int b = in.readUnsignedByte();
                    this.crc.update(b);

                    if (b == 0x00) {
                        gotend = true;
                        break;
                    }
                }

                if (!gotend)
                    return;
            }

            this.gzipState = GzipState.SKIP_COMMENT;
        case SKIP_COMMENT:
            if ((this.flags & FCOMMENT) != 0) {
                boolean gotend = false;

                while (in.isReadable()) {
                    int b = in.readUnsignedByte();
                    this.crc.update(b);

                    if (b == 0x00) {
                        gotend = true;
                        break;
                    }
                }

                if (!gotend)
                    return;
            }

            this.gzipState = GzipState.PROCESS_FHCRC;
        case PROCESS_FHCRC:
            if ((this.flags & FHCRC) != 0) {
                if (in.readableBytes() < 4)
                    return;

                long crcValue = 0;

                for (int i = 0; i < 4; ++i)
                    crcValue |= (long) in.readUnsignedByte() << i * 8;

                long readCrc = crc.getValue();

                if (crcValue != readCrc) {
                    OperationContext.get().getTaskRun()
                            .kill("CRC value missmatch. Expected: " + crcValue + ", Got: " + readCrc);
                    return;
                }
            }

            this.crc.reset();

            this.gzipState = GzipState.PRROCESS_CONTENT;
        case PRROCESS_CONTENT:
            int readableBytes = in.readableBytes();

            if (readableBytes < 1)
                return;

            if (in.hasArray()) {
                this.inflater.setInput(in.array(), in.arrayOffset() + in.readerIndex(), readableBytes);
            } else {
                byte[] array = new byte[readableBytes];
                in.getBytes(in.readerIndex(), array);
                this.inflater.setInput(array);
            }

            int maxOutputLength = this.inflater.getRemaining() << 1;
            ByteBuf decompressed = Hub.instance.getBufferAllocator().heapBuffer(maxOutputLength);

            boolean readFooter = false;
            byte[] outArray = decompressed.array();

            try {
                while (!this.inflater.needsInput()) {
                    int writerIndex = decompressed.writerIndex();
                    int outIndex = decompressed.arrayOffset() + writerIndex;
                    int length = decompressed.writableBytes();

                    if (length == 0) {
                        // completely filled the buffer allocate a new one and start to fill it
                        this.outlist.add(decompressed);
                        decompressed = Hub.instance.getBufferAllocator().heapBuffer(maxOutputLength);
                        outArray = decompressed.array();
                        continue;
                    }

                    int outputLength = this.inflater.inflate(outArray, outIndex, length);

                    if (outputLength > 0) {
                        decompressed.writerIndex(writerIndex + outputLength);

                        this.crc.update(outArray, outIndex, outputLength);
                    } else {
                        if (this.inflater.needsDictionary()) {
                            if (this.dictionary == null) {
                                OperationContext.get().getTaskRun().kill(
                                        "decompression failure, unable to set dictionary as non was specified");
                                return;
                            }

                            this.inflater.setDictionary(this.dictionary);
                        }
                    }

                    if (this.inflater.finished()) {
                        readFooter = true;
                        break;
                    }
                }

                in.skipBytes(readableBytes - this.inflater.getRemaining());
            } catch (DataFormatException x) {
                OperationContext.get().getTaskRun().kill("decompression failure: " + x);
                return;
            } finally {
                if (decompressed.isReadable()) {
                    this.outlist.add(decompressed);
                } else {
                    decompressed.release();
                }
            }

            if (!readFooter)
                break;

            this.gzipState = GzipState.PROCESS_FOOTER;
        case PROCESS_FOOTER:
            if (in.readableBytes() < 8)
                return;

            long crcValue = 0;

            for (int i = 0; i < 4; ++i)
                crcValue |= (long) in.readUnsignedByte() << i * 8;

            long readCrc = this.crc.getValue();

            if (crcValue != readCrc) {
                OperationContext.get().getTaskRun()
                        .kill("CRC value missmatch. Expected: " + crcValue + ", Got: " + readCrc);
                return;
            }

            // read ISIZE and verify
            int dataLength = 0;

            for (int i = 0; i < 4; ++i)
                dataLength |= in.readUnsignedByte() << i * 8;

            int readLength = this.inflater.getTotalOut();

            if (dataLength != readLength) {
                OperationContext.get().getTaskRun()
                        .kill("Number of bytes mismatch. Expected: " + dataLength + ", Got: " + readLength);
                return;
            }

            this.gzipState = GzipState.DONE;
        case DONE:
            break;
        }
    }

    // TODO if there is more from us then handle that before going upstream 

    @Override
    public void read() {
        // write all buffers in the queue
        while (this.outlist.size() > 0) {
            if (this.nextMessage() != ReturnOption.CONTINUE)
                return;
        }

        // if we reached done and we wrote all the buffers, then send the EOF marker if not already
        if ((this.gzipState == GzipState.DONE) && !this.eofsent) {
            if (this.nextMessage() != ReturnOption.CONTINUE)
                return;
        }

        this.upstream.read();
    }
}