For backward compatibility this method just delegates to {@link #decodeChunk(byte[], int, byte[], int, int)}, + * ignoring the {@code inEnd} parameter. Subclasses should override it and consider the {@code inEnd} parameter. + */ + public void decodeChunk(byte[] in, int inPos, int inEnd, byte[] out, int outPos, int outEnd) + throws LZFException { + decodeChunk(in, inPos, out, outPos, outEnd); + } + /** * @return If positive number, number of bytes skipped; if -1, end-of-stream was * reached; otherwise, amount of content diff --git a/src/main/java/com/ning/compress/lzf/LZFUncompressor.java b/src/main/java/com/ning/compress/lzf/LZFUncompressor.java index 0cd6a44..878d420 100644 --- a/src/main/java/com/ning/compress/lzf/LZFUncompressor.java +++ b/src/main/java/com/ning/compress/lzf/LZFUncompressor.java @@ -327,7 +327,7 @@ private final void _uncompress(byte[] src, int srcOffset, int len) throws IOExce if (_decodeBuffer == null) { _decodeBuffer = _recycler.allocDecodeBuffer(LZFChunk.MAX_CHUNK_LEN); } - _decoder.decodeChunk(src, srcOffset, _decodeBuffer, 0, _uncompressedLength); + _decoder.decodeChunk(src, srcOffset, srcOffset + len, _decodeBuffer, 0, _uncompressedLength); _handler.handleData(_decodeBuffer, 0, _uncompressedLength); } diff --git a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java index 9a0018e..8472cf2 100644 --- a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java +++ b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java @@ -63,19 +63,27 @@ public final int decodeChunk(final InputStream is, final byte[] inputBuffer, fin // compressed readFully(is, true, inputBuffer, 0, 2+compLen); // first 2 bytes are uncompressed length int uncompLen = uint16(inputBuffer, 0); - decodeChunk(inputBuffer, 2, outputBuffer, 0, uncompLen); + decodeChunk(inputBuffer, 2, 2 + compLen, outputBuffer, 0, uncompLen); return uncompLen; } - + + @Override + public void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd) throws LZFException { + decodeChunk(in, inPos, in.length, out, outPos, outEnd); + } + @Override - public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd) + public final void decodeChunk(byte[] in, int inPos, int inEnd, byte[] out, int outPos, int outEnd) throws LZFException { // Sanity checks; otherwise if any of the arguments are invalid `Unsafe` might corrupt memory - checkArrayIndices(in, inPos, in.length); + checkArrayIndices(in, inPos, inEnd); checkArrayIndices(out, outPos, outEnd); + final int outPosStart = outPos; + // We need to take care of end condition, leave last 32 bytes out + final int inputEnd32 = inEnd - 32; final int outputEnd8 = outEnd - 8; final int outputEnd32 = outEnd - 32; @@ -83,7 +91,7 @@ public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int do { int ctrl = in[inPos++] & 255; while (ctrl < LZFChunk.MAX_LITERAL) { // literal run(s) - if (outPos > outputEnd32) { + if (outPos > outputEnd32 || inPos > inputEnd32) { System.arraycopy(in, inPos, out, outPos, ctrl+1); } else { copyUpTo32(in, inPos, out, outPos, ctrl); @@ -103,6 +111,9 @@ public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int if (len < 7) { ctrl -= in[inPos++] & 255; if (ctrl < -7 && outPos < outputEnd8) { // non-overlapping? can use efficient bulk copy + if (outPos + ctrl < outPosStart) { + throw new LZFException("Invalid back reference"); + } final long rawOffset = BYTE_ARRAY_OFFSET + outPos; unsafe.putLong(out, rawOffset, unsafe.getLong(out, rawOffset + ctrl)); // moveLong(out, outPos, outEnd, ctrl); @@ -122,6 +133,9 @@ public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int continue; } // but non-overlapping is simple + if (outPos + ctrl < outPosStart) { + throw new LZFException("Invalid back reference"); + } if (len <= 32) { copyUpTo32(out, outPos+ctrl, outPos, len-1); outPos += len; @@ -132,6 +146,9 @@ public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int } while (outPos < outEnd); // sanity check to guard against corrupt data: + if (inPos != inEnd) { + throw new LZFException("Corrupt data: unexpected input amount was consumed"); + } if (outPos != outEnd) { throw new LZFException("Corrupt data: overrun in decompress, input offset "+inPos+", output offset "+outPos); } @@ -170,7 +187,7 @@ public int skipOrDecodeChunk(final InputStream is, final byte[] inputBuffer, } // otherwise, read and uncompress the chunk normally readFully(is, true, inputBuffer, 2, compLen); // first 2 bytes are uncompressed length - decodeChunk(inputBuffer, 2, outputBuffer, 0, uncompLen); + decodeChunk(inputBuffer, 2, 2 + compLen, outputBuffer, 0, uncompLen); return -(uncompLen+1); } diff --git a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoder.java b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoder.java index 6655023..6574454 100644 --- a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoder.java +++ b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoder.java @@ -73,7 +73,7 @@ static void _checkArrayIndices(byte[] array, int start, int end) { static void _checkOutputLength(int inputLen, int outputLen) { int maxEncoded = inputLen + ((inputLen + 31) >> 5); - if (maxEncoded > outputLen) { + if (maxEncoded < 0 || maxEncoded > outputLen) { throw new IllegalArgumentException("Output length " + outputLen + " is too small for input length " + inputLen); } } @@ -81,6 +81,10 @@ static void _checkOutputLength(int inputLen, int outputLen) { final static int _copyPartialLiterals(byte[] in, int inPos, byte[] out, int outPos, int literals) { + if (out.length - outPos < literals + 1) { + throw new IllegalArgumentException("Not enough space in output array"); + } + out[outPos++] = (byte) (literals-1); // Here use of Unsafe is clear win: @@ -104,10 +108,8 @@ final static int _copyPartialLiterals(byte[] in, int inPos, byte[] out, int outP rawOutPtr += 8; } int left = (literals & 7); - if (left > 4) { - unsafe.putLong(out, rawOutPtr, unsafe.getLong(in, rawInPtr)); - } else { - unsafe.putInt(out, rawOutPtr, unsafe.getInt(in, rawInPtr)); + if (left > 0) { + System.arraycopy(in, (int) (rawInPtr - BYTE_ARRAY_OFFSET), out, (int) (rawOutPtr - BYTE_ARRAY_OFFSET), left); } return outPos+literals; @@ -150,6 +152,10 @@ final static int _copyLongLiterals(byte[] in, int inPos, byte[] out, int outPos, final static int _copyFullLiterals(byte[] in, int inPos, byte[] out, int outPos) { + if (out.length - outPos < 32 + 1) { + throw new IllegalArgumentException("Not enough space in output array"); + } + // literals == 32 out[outPos++] = (byte) 31; diff --git a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderBE.java b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderBE.java index 4b1ef00..1fed50c 100644 --- a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderBE.java +++ b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderBE.java @@ -52,7 +52,7 @@ protected int tryCompress(byte[] in, int inPos, int inEnd, byte[] out, int outPo if ((ref >= inPos) // can't refer forward (i.e. leftovers) || (ref < firstPos) // or to previous block || (off = inPos - ref) > MAX_OFF - || ((seen << 8) != (_getInt(in, ref-1) << 8))) { + || ((seen << 8) != _getShifted3Bytes(in, ref))) { ++inPos; ++literals; if (literals == LZFChunk.MAX_LITERAL) { @@ -85,6 +85,7 @@ protected int tryCompress(byte[] in, int inPos, int inEnd, byte[] out, int outPo hashTable[hash(seen)] = inPos; ++inPos; } + assert inPos <= inEnd + TAIL_LENGTH; // try offlining the tail return _handleTail(in, inPos, inEnd+TAIL_LENGTH, out, outPos, literals); } @@ -93,6 +94,19 @@ private final static int _getInt(final byte[] in, final int inPos) { return unsafe.getInt(in, BYTE_ARRAY_OFFSET + inPos); } + /** + * Reads 3 bytes, shifted to the left by 8. + */ + private static int _getShifted3Bytes(byte[] in, int inPos) { + // For inPos 0 have to read bytes manually to avoid Unsafe out-of-bounds access at `inPos - 1` + // But for higher inPos values can use Unsafe to read as int and discard first byte + if (inPos == 0) { + return ((in[0] & 0xFF) << 24) | ((in[1] & 0xFF) << 16) | ((in[2] & 0xFF) << 8); + } else { + return _getInt(in, inPos - 1) << 8; + } + } + /* /////////////////////////////////////////////////////////////////////// // Methods for finding length of a back-reference diff --git a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderLE.java b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderLE.java index 30f44bd..7874a44 100644 --- a/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderLE.java +++ b/src/main/java/com/ning/compress/lzf/impl/UnsafeChunkEncoderLE.java @@ -52,7 +52,7 @@ protected int tryCompress(byte[] in, int inPos, int inEnd, byte[] out, int outPo if ((ref >= inPos) // can't refer forward (i.e. leftovers) || (ref < firstPos) // or to previous block || (off = inPos - ref) > MAX_OFF - || ((seen << 8) != (_getInt(in, ref-1) << 8))) { + || ((seen << 8) != _getShifted3Bytes(in, ref))) { ++inPos; ++literals; if (literals == LZFChunk.MAX_LITERAL) { @@ -85,6 +85,7 @@ protected int tryCompress(byte[] in, int inPos, int inEnd, byte[] out, int outPo hashTable[hash(seen)] = inPos; ++inPos; } + assert inPos <= inEnd + TAIL_LENGTH; // try offlining the tail return _handleTail(in, inPos, inEnd+TAIL_LENGTH, out, outPos, literals); } @@ -93,6 +94,19 @@ private final static int _getInt(final byte[] in, final int inPos) { return Integer.reverseBytes(unsafe.getInt(in, BYTE_ARRAY_OFFSET + inPos)); } + /** + * Reads 3 bytes, shifted to the left by 8. + */ + private static int _getShifted3Bytes(byte[] in, int inPos) { + // For inPos 0 have to read bytes manually to avoid Unsafe out-of-bounds access at `inPos - 1` + // But for higher inPos values can use Unsafe to read as int and discard first byte + if (inPos == 0) { + return ((in[0] & 0xFF) << 24) | ((in[1] & 0xFF) << 16) | ((in[2] & 0xFF) << 8); + } else { + return _getInt(in, inPos - 1) << 8; + } + } + /* /////////////////////////////////////////////////////////////////////// // Methods for finding length of a back-reference diff --git a/src/main/java/com/ning/compress/lzf/impl/VanillaChunkDecoder.java b/src/main/java/com/ning/compress/lzf/impl/VanillaChunkDecoder.java index 3528c49..a6a600c 100644 --- a/src/main/java/com/ning/compress/lzf/impl/VanillaChunkDecoder.java +++ b/src/main/java/com/ning/compress/lzf/impl/VanillaChunkDecoder.java @@ -37,12 +37,17 @@ public final int decodeChunk(final InputStream is, final byte[] inputBuffer, fin // compressed readFully(is, true, inputBuffer, 0, 2+compLen); // first 2 bytes are uncompressed length int uncompLen = uint16(inputBuffer, 0); - decodeChunk(inputBuffer, 2, outputBuffer, 0, uncompLen); + decodeChunk(inputBuffer, 2, 2 + compLen, outputBuffer, 0, uncompLen); return uncompLen; } - + + @Override + public void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd) throws LZFException { + decodeChunk(in, inPos, in.length, out, outPos, outEnd); + } + @Override - public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd) + public final void decodeChunk(byte[] in, int inPos, int inEnd, byte[] out, int outPos, int outEnd) throws LZFException { do { @@ -190,6 +195,9 @@ public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int } while (outPos < outEnd); // sanity check to guard against corrupt data: + if (inPos != inEnd) { + throw new LZFException("Corrupt data: unexpected input amount was consumed"); + } if (outPos != outEnd) { throw new LZFException("Corrupt data: overrun in decompress, input offset "+inPos+", output offset "+outPos); } @@ -228,7 +236,7 @@ public int skipOrDecodeChunk(final InputStream is, final byte[] inputBuffer, } // otherwise, read and uncompress the chunk normally readFully(is, true, inputBuffer, 2, compLen); // first 2 bytes are uncompressed length - decodeChunk(inputBuffer, 2, outputBuffer, 0, uncompLen); + decodeChunk(inputBuffer, 2, 2 + compLen, outputBuffer, 0, uncompLen); return -(uncompLen+1); } diff --git a/src/main/java/com/ning/compress/lzf/impl/VanillaChunkEncoder.java b/src/main/java/com/ning/compress/lzf/impl/VanillaChunkEncoder.java index 4a817f7..ebfeced 100644 --- a/src/main/java/com/ning/compress/lzf/impl/VanillaChunkEncoder.java +++ b/src/main/java/com/ning/compress/lzf/impl/VanillaChunkEncoder.java @@ -130,8 +130,9 @@ protected int tryCompress(byte[] in, int inPos, int inEnd, byte[] out, int outPo hashTable[hash(seen)] = inPos; ++inPos; } + assert inPos <= inEnd + TAIL_LENGTH; // try offlining the tail - return _handleTail(in, inPos, inEnd+4, out, outPos, literals); + return _handleTail(in, inPos, inEnd+TAIL_LENGTH, out, outPos, literals); } private final int _handleTail(byte[] in, int inPos, int inEnd, byte[] out, int outPos, diff --git a/src/test/java/com/ning/compress/lzf/LZFEncoderTest.java b/src/test/java/com/ning/compress/lzf/LZFEncoderTest.java index 5945a6c..568b190 100644 --- a/src/test/java/com/ning/compress/lzf/LZFEncoderTest.java +++ b/src/test/java/com/ning/compress/lzf/LZFEncoderTest.java @@ -182,10 +182,10 @@ private void _testUnsafeValidation(UnsafeChunkEncoder encoder) { assertThrows(NullPointerException.class, () -> encoder.tryCompress(null, goodStart, goodEnd, array, goodStart)); assertThrows(NullPointerException.class, () -> encoder.tryCompress(array, goodStart, goodEnd, null, goodStart)); assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, -1, goodEnd, array, goodStart)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, 12, goodEnd, array, goodStart)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, 1, array, goodStart)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, 12, array, goodStart)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, array.length + 1, goodEnd, array, goodStart)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, goodStart - 1, array, goodStart)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, array.length + 1, array, goodStart)); assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, goodEnd, array, -1)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, goodEnd, array, 12)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> encoder.tryCompress(array, goodStart, goodEnd, array, array.length + 1)); } } diff --git a/src/test/java/com/ning/compress/lzf/TestFuzzUnsafeLZF.java b/src/test/java/com/ning/compress/lzf/TestFuzzUnsafeLZF.java new file mode 100644 index 0000000..91b2453 --- /dev/null +++ b/src/test/java/com/ning/compress/lzf/TestFuzzUnsafeLZF.java @@ -0,0 +1,225 @@ +package com.ning.compress.lzf; + +import com.code_intelligence.jazzer.junit.FuzzTest; +import com.code_intelligence.jazzer.mutation.annotation.InRange; +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.WithLength; +import com.ning.compress.BufferRecycler; +import com.ning.compress.lzf.impl.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Fuzzing test using Jazzer (https://github.com/CodeIntelligenceTesting/jazzer/) for + * LZF decoder and encoder which uses {@link sun.misc.Unsafe}. + * + *
By default the tests are run in 'regression mode' where no fuzzing is performed. + * To run in 'fuzzing mode' set the environment variable {@code JAZZER_FUZZ=1}, see + * also the {@code pom.xml} of this project. + * + *
See the Jazzer README for more information. + */ +public class TestFuzzUnsafeLZF { + /* + * Important: + * These fuzz test methods all have to be listed separately in the `pom.xml` to + * support running them in fuzzing mode, see https://github.com/CodeIntelligenceTesting/jazzer/issues/599 + */ + + @FuzzTest(maxDuration = "30s") + @Retention(RetentionPolicy.RUNTIME) + @interface LZFFuzzTest { + } + + // This fuzz test performs decoding twice and verifies that the result is the same (either same decoded value or both exception) + @LZFFuzzTest + void decode(byte @NotNull @WithLength(min = 0, max = 32767) [] input, byte @NotNull [] suffix, @InRange(min = 0, max = 32767) int outputSize) { + byte[] output = new byte[outputSize]; + UnsafeChunkDecoder decoder = new UnsafeChunkDecoder(); + + byte[] input1 = input.clone(); + + // For the second decoding, append a suffix which should be ignored + byte[] input2 = new byte[input.length + suffix.length]; + System.arraycopy(input, 0, input2, 0, input.length); + // Append suffix + System.arraycopy(suffix, 0, input2, input.length, suffix.length); + + byte[] decoded1 = null; + try { + int decodedLen = decoder.decode(input1, 0, input.length, output); + decoded1 = Arrays.copyOf(output, decodedLen); + } catch (LZFException | ArrayIndexOutOfBoundsException ignored) { + } + + // Repeat decoding, this time with (ignored) suffix and prefilled output + // Should lead to same decoded result + Arrays.fill(output, (byte) 0xFF); + byte[] decoded2 = null; + try { + int decodedLen = decoder.decode(input2, 0, input.length, output); + decoded2 = Arrays.copyOf(output, decodedLen); + } catch (LZFException | ArrayIndexOutOfBoundsException ignored) { + } + + assertArrayEquals(decoded1, decoded2); + + // Compare with result of vanilla decoder + byte[] decodedVanilla = null; + try { + int decodedLen = new VanillaChunkDecoder().decode(input, output); + decodedVanilla = Arrays.copyOf(output, decodedLen); + } catch (Exception ignored) { + } + assertArrayEquals(decodedVanilla, decoded1); + + } + + @LZFFuzzTest + // `boolean dummy` parameter is as workaround for https://github.com/CodeIntelligenceTesting/jazzer/issues/1022 + void roundtrip(byte @NotNull @WithLength(min = 1, max = 32767) [] input, boolean dummy) throws LZFException { + UnsafeChunkDecoder decoder = new UnsafeChunkDecoder(); + try (UnsafeChunkEncoder encoder = UnsafeChunkEncoders.createEncoder(input.length, new BufferRecycler())) { + byte[] decoded = decoder.decode(LZFEncoder.encode(encoder, input.clone(), input.length)); + assertArrayEquals(input, decoded); + } + } + + + // Note: These encoder fuzz tests only cover the encoder implementation matching the platform endianness; + // don't cover the other endianness here because that could lead to failures simply due to endianness + // mismatch, and not due to an actual bug in the implementation + + @LZFFuzzTest + void encode(byte @NotNull @WithLength(min = 1, max = 32767) [] input, byte @NotNull [] suffix) { + byte[] input1 = input.clone(); + + // For the second encoding, append a suffix which should be ignored + byte[] input2 = new byte[input.length + suffix.length]; + System.arraycopy(input, 0, input2, 0, input.length); + // Append suffix + System.arraycopy(suffix, 0, input2, input.length, suffix.length); + + byte[] encoded1; + try (UnsafeChunkEncoder encoder = UnsafeChunkEncoders.createEncoder(input.length, new BufferRecycler())) { + encoded1 = LZFEncoder.encode(encoder, input1, input.length); + } + + byte[] encoded2; + try (UnsafeChunkEncoder encoder = UnsafeChunkEncoders.createEncoder(input.length, new BufferRecycler())) { + encoded2 = LZFEncoder.encode(encoder, input2, input.length); + } + assertArrayEquals(encoded1, encoded2); + + // Compare with result of vanilla encoder + byte[] encodedVanilla; + try (VanillaChunkEncoder encoder = new VanillaChunkEncoder(input.length, new BufferRecycler())) { + encodedVanilla = LZFEncoder.encode(encoder, input, input.length); + } + assertArrayEquals(encodedVanilla, encoded1); + } + + @LZFFuzzTest + void encodeAppend(byte @NotNull @WithLength(min = 1, max = 32767) [] input, @InRange(min = 0, max = 32767) int outputSize) { + byte[] output = new byte[outputSize]; + // Prefill output; should have no effect on encoded result + Arrays.fill(output, (byte) 0xFF); + int encodedLen; + try (UnsafeChunkEncoder encoder = UnsafeChunkEncoders.createEncoder(input.length, new BufferRecycler())) { + encodedLen = LZFEncoder.appendEncoded(encoder, input.clone(), 0, input.length, output, 0); + } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException ignored) { + // Skip comparison with vanilla encoder + return; + } + + byte[] encodedUnsafe = Arrays.copyOf(output, encodedLen); + + // Compare with result of vanilla encoder + Arrays.fill(output, (byte) 0); + try (VanillaChunkEncoder encoder = new VanillaChunkEncoder(input.length, new BufferRecycler())) { + encodedLen = LZFEncoder.appendEncoded(encoder, input, 0, input.length, output, 0); + } + // TODO: VanillaChunkEncoder performs out-of-bounds array index whereas UnsafeChunkEncoder does not (not sure which one is correct) + // Why do they even have different `_handleTail` implementations, UnsafeChunkEncoder is not using Unsafe there? + catch (ArrayIndexOutOfBoundsException ignored) { + return; + } + byte[] encodedVanilla = Arrays.copyOf(output, encodedLen); + assertArrayEquals(encodedVanilla, encodedUnsafe); + } + + /// Note: Also cover LZFInputStream and LZFOutputStream because they in parts use methods of the decoder and encoder + /// which are otherwise not reachable + + @LZFFuzzTest + void inputStreamRead(byte @NotNull @WithLength(min = 0, max = 32767) [] input, @InRange(min = 1, max = 32767) int readBufferSize) throws IOException { + UnsafeChunkDecoder decoder = new UnsafeChunkDecoder(); + try (LZFInputStream inputStream = new LZFInputStream(decoder, new ByteArrayInputStream(input), new BufferRecycler(), false)) { + byte[] readBuffer = new byte[readBufferSize]; + while (inputStream.read(readBuffer) != -1) { + // Do nothing, just consume the data + } + } catch (LZFException | ArrayIndexOutOfBoundsException ignored) { + } + // TODO: This IndexOutOfBoundsException occurs because LZFInputStream makes an invalid call to ByteArrayInputStream + // The reason seems to be that `_inputBuffer` is only MAX_CHUNK_LEN large, but should be `2 + MAX_CHUNK_LEN` to + // account for first two bytes encoding the length? (might affect more places in code) + catch (IndexOutOfBoundsException ignored) { + } + } + + @LZFFuzzTest + void inputStreamSkip(byte @NotNull @WithLength(min = 0, max = 32767) [] input, @InRange(min = 1, max = 32767) int skipCount) throws IOException { + UnsafeChunkDecoder decoder = new UnsafeChunkDecoder(); + try (LZFInputStream inputStream = new LZFInputStream(decoder, new ByteArrayInputStream(input), new BufferRecycler(), false)) { + while (inputStream.skip(skipCount) > 0) { + // Do nothing, just consume the data + } + } catch (LZFException ignored) { + } + // TODO: This IndexOutOfBoundsException occurs because LZFInputStream makes an invalid call to ByteArrayInputStream + // The reason seems to be that `_inputBuffer` is only MAX_CHUNK_LEN large, but should be `2 + MAX_CHUNK_LEN` to + // account for first two bytes encoding the length? (might affect more places in code) + catch (IndexOutOfBoundsException ignored) { + } + } + + private static class NullOutputStream extends OutputStream { + public static final OutputStream INSTANCE = new NullOutputStream(); + + private NullOutputStream() { + } + + @Override + public void write(int b) { + // Do nothing + } + + @Override + public void write(byte[] b, int off, int len) { + // Do nothing + } + } + + @LZFFuzzTest + // Generates multiple arrays and writes them separately + void outputStream(byte @NotNull @WithLength(min = 1, max = 10) [] @NotNull @WithLength(min = 1) [] arrays, @InRange(min = 1, max = 32767) int bufferSize) throws IOException { + int totalLength = Stream.of(arrays).mapToInt(a -> a.length).sum(); + + UnsafeChunkEncoder encoder = UnsafeChunkEncoders.createEncoder(totalLength, new BufferRecycler()); + try (LZFOutputStream outputStream = new LZFOutputStream(encoder, NullOutputStream.INSTANCE, bufferSize, null)) { + for (byte[] array : arrays) { + outputStream.write(array); + } + } + } +} diff --git a/src/test/java/com/ning/compress/lzf/TestLZFDecoder.java b/src/test/java/com/ning/compress/lzf/TestLZFDecoder.java index 4553847..fb7dd0e 100644 --- a/src/test/java/com/ning/compress/lzf/TestLZFDecoder.java +++ b/src/test/java/com/ning/compress/lzf/TestLZFDecoder.java @@ -38,13 +38,16 @@ public void testUnsafeValidation() { byte[] array = new byte[10]; int goodStart = 2; int goodEnd = 5; - assertThrows(NullPointerException.class, () -> decoder.decodeChunk(null, goodStart, array, goodStart, goodEnd)); - assertThrows(NullPointerException.class, () -> decoder.decodeChunk(array, goodStart, null, goodStart, goodEnd)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, -1, array, goodStart, goodEnd)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, 12, array, goodStart, goodEnd)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, array, -1, goodEnd)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, array, goodStart, 1)); - assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, array, goodStart, 12)); + assertThrows(NullPointerException.class, () -> decoder.decodeChunk(null, goodStart, goodEnd, array, goodStart, goodEnd)); + assertThrows(NullPointerException.class, () -> decoder.decodeChunk(array, goodStart, goodEnd, null, goodStart, goodEnd)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, -1, goodEnd, array, goodStart, goodEnd)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, goodStart - 1, array, goodStart, goodEnd)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, -1, array, goodStart, goodEnd)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, array.length + 1, array, goodStart, goodEnd)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, goodEnd, array, -1, goodEnd)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, goodEnd, array, goodStart, goodStart - 1)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, goodEnd, array, goodStart, -1)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> decoder.decodeChunk(array, goodStart, goodEnd, array, goodStart, array.length + 1)); } /*