From f6aa810f8b2f73f32ffec51ea024828114942b1b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 7 Aug 2019 16:04:46 +0200 Subject: [PATCH] #489 Fix for Unexpected End of PackBits Stream for padded streams. --- .../twelvemonkeys/io/enc/PackBitsDecoder.java | 396 ++++++++---------- 1 file changed, 186 insertions(+), 210 deletions(-) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java index 82b71ebc..37d0bae2 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java @@ -1,210 +1,186 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.twelvemonkeys.io.enc; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Decoder implementation for Apple PackBits run-length encoding. - *

- * From Wikipedia, the free encyclopedia
- * PackBits is a fast, simple compression scheme for run-length encoding of - * data. - *

- * Apple introduced the PackBits format with the release of MacPaint on the - * Macintosh computer. This compression scheme is one of the types of - * compression that can be used in TIFF-files. - *

- * A PackBits data stream consists of packets of one byte of header followed by - * data. The header is a signed byte; the data can be signed, unsigned, or - * packed (such as MacPaint pixels). - *

- * - * - * - *
Header byteData
0 to 127 1 + n literal bytes of data
0 to -127 One byte of data, repeated 1 - n times in - * the decompressed output
-128 No operation
- *

- * Note that interpreting 0 as positive or negative makes no difference in the - * output. Runs of two bytes adjacent to non-runs are typically written as - * literal data. - *

- * See Understanding PackBits - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java#1 $ - */ -public final class PackBitsDecoder implements Decoder { - // TODO: Look at ICNSImageReader#unpackbits... What is this weirdness? - - private final boolean disableNoOp; - private final byte[] sample; - - private int leftOfRun; - private boolean splitRun; - private boolean reachedEOF; - - /** Creates a {@code PackBitsDecoder}. */ - public PackBitsDecoder() { - this(1, false); - } - - /** - * Creates a {@code PackBitsDecoder}, with optional compatibility mode. - *

- * As some implementations of PackBits-like encoders treat {@code -128} as length of - * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. - * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. - * - * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op - */ - public PackBitsDecoder(final boolean disableNoOp) { - this(1, disableNoOp); - } - - /** - * Creates a {@code PackBitsDecoder}, with optional compatibility mode. - *

- * As some implementations of PackBits-like encoders treat {@code -128} as length of - * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. - * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. - * - * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op - */ - public PackBitsDecoder(int sampleSize, final boolean disableNoOp) { - this.sample = new byte[sampleSize]; - this.disableNoOp = disableNoOp; - } - - /** - * Decodes bytes from the given input stream, to the given buffer. - * - * @param stream the stream to decode from - * @param buffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long - * @return The number of bytes decoded - * - * @throws java.io.IOException - */ - public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { - if (reachedEOF) { - return -1; - } - - // TODO: Don't decode more than single runs, because some writers add pad bytes inside the stream... - while (buffer.hasRemaining()) { - int n; - - if (splitRun) { - // Continue run - n = leftOfRun; - splitRun = false; - } - else { - // Start new run - int b = stream.read(); - if (b < 0) { - reachedEOF = true; - break; - } - n = (byte) b; - } - - // Split run at or before max - if (n >= 0 && n + 1 > buffer.remaining()) { - leftOfRun = n; - splitRun = true; - break; - } - else if (n < 0 && -n + 1 > buffer.remaining()) { - leftOfRun = n; - splitRun = true; - break; - } - - try { - if (n >= 0) { - // Copy next n + 1 bytes literally - readFully(stream, buffer, sample.length * (n + 1)); - } - // Allow -128 for compatibility, see above - else if (disableNoOp || n != -128) { - // Replicate the next byte -n + 1 times - for (int s = 0; s < sample.length; s++) { - sample[s] = readByte(stream); - } - - for (int i = -n + 1; i > 0; i--) { - buffer.put(sample); - } - } - // else NOOP (-128) - } - catch (IndexOutOfBoundsException e) { - throw new DecodeException("Error in PackBits decompression, data seems corrupt", e); - } - } - - return buffer.position(); - } - - static byte readByte(final InputStream pStream) throws IOException { - int read = pStream.read(); - - if (read < 0) { - throw new EOFException("Unexpected end of PackBits stream"); - } - - return (byte) read; - } - - static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException { - if (pLength < 0) { - throw new IndexOutOfBoundsException(String.format("Negative length: %d", pLength)); - } - - int total = 0; - - while (total < pLength) { - int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + total, pLength - total); - - if (count < 0) { - throw new EOFException("Unexpected end of PackBits stream"); - } - - total += count; - } - - pBuffer.position(pBuffer.position() + total); - } -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Decoder implementation for Apple PackBits run-length encoding. + *

+ * From Wikipedia, the free encyclopedia
+ * PackBits is a fast, simple compression scheme for run-length encoding of + * data. + *

+ * Apple introduced the PackBits format with the release of MacPaint on the + * Macintosh computer. This compression scheme is one of the types of + * compression that can be used in TIFF-files. + *

+ * A PackBits data stream consists of packets of one byte of header followed by + * data. The header is a signed byte; the data can be signed, unsigned, or + * packed (such as MacPaint pixels). + *

+ * + * + * + *
Header byteData
0 to 127 1 + n literal bytes of data
0 to -127 One byte of data, repeated 1 - n times in + * the decompressed output
-128 No operation
+ *

+ * Note that interpreting 0 as positive or negative makes no difference in the + * output. Runs of two bytes adjacent to non-runs are typically written as + * literal data. + *

+ * See Understanding PackBits + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java#1 $ + */ +public final class PackBitsDecoder implements Decoder { + // TODO: Look at ICNSImageReader#unpackbits... What is this weirdness? + + private final boolean disableNoOp; + private final byte[] sample; + + private boolean reachedEOF; + + /** Creates a {@code PackBitsDecoder}. */ + public PackBitsDecoder() { + this(1, false); + } + + /** + * Creates a {@code PackBitsDecoder}, with optional compatibility mode. + *

+ * As some implementations of PackBits-like encoders treat {@code -128} as length of + * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. + * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. + * + * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op + */ + public PackBitsDecoder(final boolean disableNoOp) { + this(1, disableNoOp); + } + + /** + * Creates a {@code PackBitsDecoder}, with optional compatibility mode. + *

+ * As some implementations of PackBits-like encoders treat {@code -128} as length of + * a compressed run, instead of a no-op, it's possible to disable no-ops for compatibility. + * Should be used with caution, even though, most known encoders never write no-ops in the compressed streams. + * + * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op + */ + public PackBitsDecoder(int sampleSize, final boolean disableNoOp) { + this.sample = new byte[sampleSize]; + this.disableNoOp = disableNoOp; + } + + /** + * Decodes bytes from the given input stream, to the given buffer. + * + * @param stream the stream to decode from + * @param buffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long + * @return The number of bytes decoded + * + * @throws java.io.IOException if a problem occurs during decoding. + */ + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { + if (reachedEOF) { + return -1; + } + + // NOTE: We don't decode more than single runs, because some writers add pad bytes inside the stream... + // Start new run + int b = stream.read(); + if (b < 0) { + reachedEOF = true; + return 0; + } + + int n = (byte) b; + + try { + if (n >= 0) { + // Copy next n + 1 bytes literally + readFully(stream, buffer, sample.length * (n + 1)); + } + // Allow -128 for compatibility, see above + else if (disableNoOp || n != -128) { + // Replicate the next byte -n + 1 times + for (int s = 0; s < sample.length; s++) { + sample[s] = readByte(stream); + } + + for (int i = -n + 1; i > 0; i--) { + buffer.put(sample); + } + } + // else NOOP (-128) + } + catch (IndexOutOfBoundsException e) { + throw new DecodeException("Error in PackBits decompression, data seems corrupt", e); + } + + return buffer.position(); + } + + static byte readByte(final InputStream pStream) throws IOException { + int read = pStream.read(); + + if (read < 0) { + throw new EOFException("Unexpected end of PackBits stream"); + } + + return (byte) read; + } + + static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException { + if (pLength < 0) { + throw new IndexOutOfBoundsException(String.format("Negative length: %d", pLength)); + } + + int total = 0; + + while (total < pLength) { + int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + total, pLength - total); + + if (count < 0) { + throw new EOFException("Unexpected end of PackBits stream"); + } + + total += count; + } + + pBuffer.position(pBuffer.position() + total); + } +}