/*
* Copyright (C) 2015 SoftIndex LLC.
*
* Licensed 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 io.datakernel.stream.processor;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.bytebuf.ByteBufPool;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.exception.ParseException;
import io.datakernel.jmx.ValueStats;
import io.datakernel.stream.AbstractStreamTransformer_1_1;
import io.datakernel.stream.StreamDataReceiver;
import net.jpountz.lz4.LZ4Exception;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;
import net.jpountz.util.SafeUtils;
import net.jpountz.xxhash.StreamingXXHash32;
import net.jpountz.xxhash.XXHashFactory;
import static io.datakernel.stream.processor.StreamLZ4Compressor.*;
import static java.lang.Math.min;
import static java.lang.String.format;
public final class StreamLZ4Decompressor extends AbstractStreamTransformer_1_1<ByteBuf, ByteBuf> {
private final static class Header {
private int originalLen;
private int compressedLen;
private int compressionMethod;
private int check;
private boolean finished;
}
private final LZ4FastDecompressor decompressor;
private final StreamingXXHash32 checksum;
private InputConsumer inputConsumer;
private OutputProducer outputProducer;
private final Header header = new Header();
public interface Inspector extends AbstractStreamTransformer_1_1.Inspector {
void onInputBuf(StreamLZ4Decompressor self, ByteBuf buf);
void onOutputBuf(StreamLZ4Decompressor self, ByteBuf buf);
}
public static class JmxInspector extends AbstractStreamTransformer_1_1.JmxInspector implements Inspector {
private static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE;
private final ValueStats bytesIn = ValueStats.create(SMOOTHING_WINDOW);
private final ValueStats bytesOut = ValueStats.create(SMOOTHING_WINDOW);
@Override
public void onInputBuf(StreamLZ4Decompressor self, ByteBuf buf) {
bytesIn.recordValue(buf.readRemaining());
}
@Override
public void onOutputBuf(StreamLZ4Decompressor self, ByteBuf buf) {
bytesOut.recordValue(buf.readRemaining());
}
}
private final class InputConsumer extends AbstractInputConsumer {
@Override
protected void onUpstreamEndOfStream() {
outputProducer.sendEndOfStream();
}
@Override
public StreamDataReceiver<ByteBuf> getDataReceiver() {
return outputProducer;
}
}
private final class OutputProducer extends AbstractOutputProducer implements StreamDataReceiver<ByteBuf> {
private static final int INITIAL_BUFFER_SIZE = 256;
private final LZ4FastDecompressor decompressor;
private final StreamingXXHash32 checksum;
private final ByteBuf headerBuf = ByteBuf.wrapForWriting(new byte[HEADER_LENGTH]);
private ByteBuf inputBuf;
private long inputStreamPosition;
private final Inspector inspector = (Inspector) StreamLZ4Decompressor.this.inspector;
private OutputProducer(LZ4FastDecompressor decompressor, StreamingXXHash32 checksum) {
this.decompressor = decompressor;
this.checksum = checksum;
this.inputBuf = ByteBufPool.allocate(INITIAL_BUFFER_SIZE);
}
@Override
protected void onDownstreamSuspended() {
inputConsumer.suspend();
}
@Override
protected void onDownstreamResumed() {
inputConsumer.resume();
}
@Override
public void onData(ByteBuf buf) {
if (inspector != null) inspector.onInputBuf(StreamLZ4Decompressor.this, buf);
try {
if (header.finished) {
throw new ParseException(format("Unexpected byteBuf after LZ4 EOS packet %s : %s", this, buf));
}
consumeInputByteBuffer(buf);
} catch (ParseException e) {
inputConsumer.closeWithError(e);
} finally {
buf.recycle();
}
}
private void consumeInputByteBuffer(ByteBuf buf) throws ParseException {
while (buf.canRead() && getProducerStatus().isOpen()) {
if (isReadingHeader()) {
if (headerBuf.writePosition() == 0 && buf.readRemaining() >= HEADER_LENGTH) {
readHeader(header, buf.array(), buf.readPosition());
buf.moveReadPosition(HEADER_LENGTH);
headerBuf.writePosition(HEADER_LENGTH);
} else {
buf.drainTo(headerBuf, min(headerBuf.writeRemaining(), buf.readRemaining()));
if (isReadingHeader()) break;
readHeader(header, headerBuf.array(), 0);
}
assert !isReadingHeader();
}
if (header.finished) {
inputStreamPosition += HEADER_LENGTH; // end-of-stream block size
break;
}
// read message body:
assert !isReadingHeader();
ByteBuf outputBuf;
if (!inputBuf.canRead() && buf.readRemaining() >= header.compressedLen) {
outputBuf = readBody(decompressor, checksum, header, buf.array(), buf.readPosition());
buf.moveReadPosition(header.compressedLen);
} else {
inputBuf = ByteBufPool.ensureTailRemaining(inputBuf, header.compressedLen);
int remainingToProcessBytes = header.compressedLen - inputBuf.readRemaining();
int size = min(remainingToProcessBytes, buf.readRemaining());
buf.drainTo(inputBuf, size);
if (inputBuf.readRemaining() < header.compressedLen) break;
outputBuf = readBody(decompressor, checksum, header, inputBuf.array(), 0);
}
inputStreamPosition += HEADER_LENGTH + header.compressedLen;
if (inspector != null) inspector.onOutputBuf(StreamLZ4Decompressor.this, outputBuf);
inputBuf.rewind();
headerBuf.rewind();
downstreamDataReceiver.onData(outputBuf);
assert isReadingHeader();
}
}
private boolean isReadingHeader() {
return headerBuf.canWrite(); // while reading header we need to fill all 21 bytes with data
}
@Override
protected void doCleanup() {
if (inputBuf != null) {
inputBuf.recycle();
inputBuf = null;
}
}
}
// region creators
private StreamLZ4Decompressor(Eventloop eventloop, LZ4FastDecompressor decompressor, StreamingXXHash32 checksum) {
super(eventloop);
this.decompressor = decompressor;
this.checksum = checksum;
recreate();
}
private void recreate() {
this.outputProducer = new OutputProducer(decompressor, checksum);
this.inputConsumer = new InputConsumer();
}
@Override
protected AbstractInputConsumer getInputImpl() {
return inputConsumer;
}
@Override
protected AbstractOutputProducer getOutputImpl() {
return outputProducer;
}
public static StreamLZ4Decompressor create(Eventloop eventloop, LZ4FastDecompressor decompressor,
StreamingXXHash32 checksum) {
return new StreamLZ4Decompressor(eventloop, decompressor, checksum);
}
public static StreamLZ4Decompressor create(Eventloop eventloop) {
return new StreamLZ4Decompressor(eventloop, LZ4Factory.fastestInstance().fastDecompressor(),
XXHashFactory.fastestInstance().newStreamingHash32(DEFAULT_SEED));
}
public StreamLZ4Decompressor withInspector(Inspector inspector) {
super.inspector = inspector;
recreate();
return this;
}
// endregion
private static ByteBuf readBody(LZ4FastDecompressor decompressor, StreamingXXHash32 checksum, Header header,
byte[] bytes, int off) throws ParseException {
ByteBuf outputBuf = ByteBufPool.allocate(header.originalLen);
outputBuf.writePosition(header.originalLen);
switch (header.compressionMethod) {
case COMPRESSION_METHOD_RAW:
System.arraycopy(bytes, off, outputBuf.array(), 0, header.originalLen);
break;
case COMPRESSION_METHOD_LZ4:
try {
int compressedLen2 = decompressor.decompress(bytes, off, outputBuf.array(), 0, header.originalLen);
if (header.compressedLen != compressedLen2) {
throw new ParseException("Stream is corrupted");
}
} catch (LZ4Exception e) {
throw new ParseException("Stream is corrupted", e);
}
break;
default:
throw new AssertionError();
}
checksum.reset();
checksum.update(outputBuf.array(), 0, header.originalLen);
if (checksum.getValue() != header.check) {
throw new ParseException("Stream is corrupted");
}
return outputBuf;
}
private static void readHeader(Header header, byte[] buf, int off) throws ParseException {
for (int i = 0; i < MAGIC_LENGTH; ++i) {
if (buf[off + i] != MAGIC[i]) {
throw new ParseException("Stream is corrupted");
}
}
int token = buf[off + MAGIC_LENGTH] & 0xFF;
header.compressionMethod = token & 0xF0;
int compressionLevel = COMPRESSION_LEVEL_BASE + (token & 0x0F);
if (header.compressionMethod != COMPRESSION_METHOD_RAW && header.compressionMethod != COMPRESSION_METHOD_LZ4) {
throw new ParseException("Stream is corrupted");
}
header.compressedLen = SafeUtils.readIntLE(buf, off + MAGIC_LENGTH + 1);
header.originalLen = SafeUtils.readIntLE(buf, off + MAGIC_LENGTH + 5);
header.check = SafeUtils.readIntLE(buf, off + MAGIC_LENGTH + 9);
if (header.originalLen > 1 << compressionLevel
|| (header.originalLen < 0 || header.compressedLen < 0)
|| (header.originalLen == 0 && header.compressedLen != 0)
|| (header.originalLen != 0 && header.compressedLen == 0)
|| (header.compressionMethod == COMPRESSION_METHOD_RAW && header.originalLen != header.compressedLen)) {
throw new ParseException("Stream is corrupted");
}
if (header.originalLen == 0) {
if (header.check != 0) {
throw new ParseException("Stream is corrupted");
}
header.finished = true;
}
}
public long getInputStreamPosition() {
return outputProducer.inputStreamPosition;
}
}