package org.apache.commons.compress.compressors.lz4;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import org.apache.commons.compress.compressors.CompressorOutputStream;
import org.apache.commons.compress.compressors.lz77support.LZ77Compressor;
import org.apache.commons.compress.compressors.lz77support.Parameters;
import org.apache.commons.compress.utils.ByteUtils;
public class BlockLZ4CompressorOutputStream extends CompressorOutputStream {
private static final int MIN_BACK_REFERENCE_LENGTH = 4;
private static final int MIN_OFFSET_OF_LAST_BACK_REFERENCE = 12;
private final LZ77Compressor compressor;
private final OutputStream os;
private final byte[] oneByte = new byte[1];
private boolean finished = false;
private Deque<Pair> pairs = new LinkedList<>();
private Deque<byte[]> expandedBlocks = new LinkedList<>();
public BlockLZ4CompressorOutputStream(final OutputStream os) throws IOException {
this(os, createParameterBuilder().build());
}
public BlockLZ4CompressorOutputStream(final OutputStream os, Parameters params) throws IOException {
this.os = os;
compressor = new LZ77Compressor(params,
new LZ77Compressor.Callback() {
@Override
public void accept(LZ77Compressor.Block block) throws IOException {
switch (block.getType()) {
case LITERAL:
addLiteralBlock((LZ77Compressor.LiteralBlock) block);
break;
case BACK_REFERENCE:
addBackReference((LZ77Compressor.BackReference) block);
break;
case EOD:
writeFinalLiteralBlock();
break;
}
}
});
}
@Override
public void write(int b) throws IOException {
oneByte[0] = (byte) (b & 0xff);
write(oneByte);
}
@Override
public void write(byte[] data, int off, int len) throws IOException {
compressor.compress(data, off, len);
}
@Override
public void close() throws IOException {
try {
finish();
} finally {
os.close();
}
}
public void finish() throws IOException {
if (!finished) {
compressor.finish();
finished = true;
}
}
public void prefill(byte[] data, int off, int len) {
if (len > 0) {
byte[] b = Arrays.copyOfRange(data, off, off + len);
compressor.prefill(b);
recordLiteral(b);
}
}
private void addLiteralBlock(LZ77Compressor.LiteralBlock block) throws IOException {
Pair last = writeBlocksAndReturnUnfinishedPair(block.getLength());
recordLiteral(last.addLiteral(block));
clearUnusedBlocksAndPairs();
}
private void addBackReference(LZ77Compressor.BackReference block) throws IOException {
Pair last = writeBlocksAndReturnUnfinishedPair(block.getLength());
last.setBackReference(block);
recordBackReference(block);
clearUnusedBlocksAndPairs();
}
private Pair writeBlocksAndReturnUnfinishedPair(int length) throws IOException {
writeWritablePairs(length);
Pair last = pairs.peekLast();
if (last == null || last.hasBackReference()) {
last = new Pair();
pairs.addLast(last);
}
return last;
}
private void recordLiteral(byte[] b) {
expandedBlocks.addFirst(b);
}
private void clearUnusedBlocksAndPairs() {
clearUnusedBlocks();
clearUnusedPairs();
}
private void clearUnusedBlocks() {
int blockLengths = 0;
int blocksToKeep = 0;
for (byte[] b : expandedBlocks) {
blocksToKeep++;
blockLengths += b.length;
if (blockLengths >= BlockLZ4CompressorInputStream.WINDOW_SIZE) {
break;
}
}
final int size = expandedBlocks.size();
for (int i = blocksToKeep; i < size; i++) {
expandedBlocks.removeLast();
}
}
private void recordBackReference(LZ77Compressor.BackReference block) {
expandedBlocks.addFirst(expand(block.getOffset(), block.getLength()));
}
private byte[] expand(final int offset, final int length) {
byte[] expanded = new byte[length];
if (offset == 1) {
byte[] block = expandedBlocks.peekFirst();
byte b = block[block.length - 1];
if (b != 0) {
Arrays.fill(expanded, b);
}
} else {
expandFromList(expanded, offset, length);
}
return expanded;
}
private void expandFromList(final byte[] expanded, int offset, int length) {
int offsetRemaining = offset;
int lengthRemaining = length;
int writeOffset = 0;
while (lengthRemaining > 0) {
byte[] block = null;
int copyLen, copyOffset;
if (offsetRemaining > 0) {
int blockOffset = 0;
for (byte[] b : expandedBlocks) {
if (b.length + blockOffset >= offsetRemaining) {
block = b;
break;
}
blockOffset += b.length;
}
if (block == null) {
throw new IllegalStateException("Failed to find a block containing offset " + offset);
}
copyOffset = blockOffset + block.length - offsetRemaining;
copyLen = Math.min(lengthRemaining, block.length - copyOffset);
} else {
block = expanded;
copyOffset = -offsetRemaining;
copyLen = Math.min(lengthRemaining, writeOffset + offsetRemaining);
}
System.arraycopy(block, copyOffset, expanded, writeOffset, copyLen);
offsetRemaining -= copyLen;
lengthRemaining -= copyLen;
writeOffset += copyLen;
}
}
private void clearUnusedPairs() {
int pairLengths = 0;
int pairsToKeep = 0;
for (Iterator<Pair> it = pairs.descendingIterator(); it.hasNext(); ) {
Pair p = it.next();
pairsToKeep++;
pairLengths += p.length();
if (pairLengths >= BlockLZ4CompressorInputStream.WINDOW_SIZE) {
break;
}
}
final int size = pairs.size();
for (int i = pairsToKeep; i < size; i++) {
Pair p = pairs.peekFirst();
if (p.hasBeenWritten()) {
pairs.removeFirst();
} else {
break;
}
}
}
private void writeFinalLiteralBlock() throws IOException {
rewriteLastPairs();
for (Pair p : pairs) {
if (!p.hasBeenWritten()) {
p.writeTo(os);
}
}
pairs.clear();
}
private void writeWritablePairs(int lengthOfBlocksAfterLastPair) throws IOException {
int unwrittenLength = lengthOfBlocksAfterLastPair;
for (Iterator<Pair> it = pairs.descendingIterator(); it.hasNext(); ) {
Pair p = it.next();
if (p.hasBeenWritten()) {
break;
}
unwrittenLength += p.length();
}
for (Pair p : pairs) {
if (p.hasBeenWritten()) {
continue;
}
unwrittenLength -= p.length();
if (p.canBeWritten(unwrittenLength)) {
p.writeTo(os);
} else {
break;
}
}
}
private void rewriteLastPairs() {
LinkedList<Pair> lastPairs = new LinkedList<>();
LinkedList<Integer> pairLength = new LinkedList<>();
int offset = 0;
for (Iterator<Pair> it = pairs.descendingIterator(); it.hasNext(); ) {
Pair p = it.next();
if (p.hasBeenWritten()) {
break;
}
int len = p.length();
pairLength.addFirst(len);
lastPairs.addFirst(p);
offset += len;
if (offset >= MIN_OFFSET_OF_LAST_BACK_REFERENCE) {
break;
}
}
for (Pair p : lastPairs) {
pairs.remove(p);
}
final int lastPairsSize = lastPairs.size();
int toExpand = 0;
for (int i = 1; i < lastPairsSize; i++) {
toExpand += pairLength.get(i);
}
Pair replacement = new Pair();
if (toExpand > 0) {
replacement.prependLiteral(expand(toExpand, toExpand));
}
Pair splitCandidate = lastPairs.get(0);
int stillNeeded = MIN_OFFSET_OF_LAST_BACK_REFERENCE - toExpand;
int brLen = splitCandidate.hasBackReference() ? splitCandidate.backReferenceLength() : 0;
if (splitCandidate.hasBackReference() && brLen >= MIN_BACK_REFERENCE_LENGTH + stillNeeded) {
replacement.prependLiteral(expand(toExpand + stillNeeded, stillNeeded));
pairs.add(splitCandidate.splitWithNewBackReferenceLengthOf(brLen - stillNeeded));
} else {
if (splitCandidate.hasBackReference()) {
replacement.prependLiteral(expand(toExpand + brLen, brLen));
}
splitCandidate.prependTo(replacement);
}
pairs.add(replacement);
}
public static Parameters.Builder createParameterBuilder() {
int maxLen = BlockLZ4CompressorInputStream.WINDOW_SIZE - 1;
return Parameters.builder(BlockLZ4CompressorInputStream.WINDOW_SIZE)
.withMinBackReferenceLength(MIN_BACK_REFERENCE_LENGTH)
.withMaxBackReferenceLength(maxLen)
.withMaxOffset(maxLen)
.withMaxLiteralLength(maxLen);
}
final static class Pair {
private final Deque<byte[]> literals = new LinkedList<>();
private int brOffset, brLength;
private boolean written;
private void prependLiteral(byte[] data) {
literals.addFirst(data);
}
byte[] addLiteral(LZ77Compressor.LiteralBlock block) {
byte[] copy = Arrays.copyOfRange(block.getData(), block.getOffset(),
block.getOffset() + block.getLength());
literals.add(copy);
return copy;
}
void setBackReference(LZ77Compressor.BackReference block) {
if (hasBackReference()) {
throw new IllegalStateException();
}
brOffset = block.getOffset();
brLength = block.getLength();
}
boolean hasBackReference() {
return brOffset > 0;
}
boolean canBeWritten(int lengthOfBlocksAfterThisPair) {
return hasBackReference()
&& lengthOfBlocksAfterThisPair >= MIN_OFFSET_OF_LAST_BACK_REFERENCE + MIN_BACK_REFERENCE_LENGTH;
}
int length() {
return literalLength() + brLength;
}
private boolean hasBeenWritten() {
return written;
}
void writeTo(OutputStream out) throws IOException {
int litLength = literalLength();
out.write(lengths(litLength, brLength));
if (litLength >= BlockLZ4CompressorInputStream.BACK_REFERENCE_SIZE_MASK) {
writeLength(litLength - BlockLZ4CompressorInputStream.BACK_REFERENCE_SIZE_MASK, out);
}
for (byte[] b : literals) {
out.write(b);
}
if (hasBackReference()) {
ByteUtils.toLittleEndian(out, brOffset, 2);
if (brLength - MIN_BACK_REFERENCE_LENGTH >= BlockLZ4CompressorInputStream.BACK_REFERENCE_SIZE_MASK) {
writeLength(brLength - MIN_BACK_REFERENCE_LENGTH
- BlockLZ4CompressorInputStream.BACK_REFERENCE_SIZE_MASK, out);
}
}
written = true;
}
private int literalLength() {
int length = 0;
for (byte[] b : literals) {
length += b.length;
}
return length;
}
private static int lengths(int litLength, int brLength) {
int l = litLength < 15 ? litLength : 15;
int br = brLength < 4 ? 0 : (brLength < 19 ? brLength - 4 : 15);
return (l << BlockLZ4CompressorInputStream.SIZE_BITS) | br;
}
private static void writeLength(int length, OutputStream out) throws IOException {
while (length >= 255) {
out.write(255);
length -= 255;
}
out.write(length);
}
private int backReferenceLength() {
return brLength;
}
private void prependTo(Pair other) {
Iterator<byte[]> listBackwards = literals.descendingIterator();
while (listBackwards.hasNext()) {
other.prependLiteral(listBackwards.next());
}
}
private Pair splitWithNewBackReferenceLengthOf(int newBackReferenceLength) {
Pair p = new Pair();
p.literals.addAll(literals);
p.brOffset = brOffset;
p.brLength = newBackReferenceLength;
return p;
}
}
}