JPEGSegmentImageInputStreamTest.java

/*
 * Copyright (c) 2012, 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.imageio.plugins.jpeg;

import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;

import org.junit.jupiter.api.Test;

import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.*;

/**
 * JPEGSegmentImageInputStreamTest
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: JPEGSegmentImageInputStreamTest.java,v 1.0 30.01.12 22:14 haraldk Exp$
 */
public class JPEGSegmentImageInputStreamTest {
    static {
        IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
        ImageIO.setUseCache(false);
    }

    private URL getClassLoaderResource(final String pName) {
        return getClass().getResource(pName);
    }

    @Test
    public void testCreateNull() {
        assertThrows(IllegalArgumentException.class, () -> new JPEGSegmentImageInputStream(null));
    }

    @Test
    public void testStreamNonJPEG() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[] {42, 42, 0, 0, 77, 99})));
        assertThrows(IIOException.class, () -> stream.read());
    }

    @Test
    public void testStreamNonJPEGArray() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[] {42, 42, 0, 0, 77, 99})));
        assertThrows(IIOException.class, () -> stream.readFully(new byte[1]));
    }

    @Test
    public void testStreamEmpty() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[0])));
        assertThrows(IIOException.class, () -> stream.read());
    }

    @Test
    public void testStreamEmptyArray() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[0])));
        assertThrows(IIOException.class, () -> stream.readFully(new byte[1]));
    }

    @Test
    public void testStreamRealData() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
        assertEquals(JPEG.SOI, stream.readUnsignedShort());
        assertEquals(JPEG.DQT, stream.readUnsignedShort());
    }

    @Test
    public void testStreamRealDataArray() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
        byte[] bytes = new byte[20];

        // NOTE: read(byte[], int, int) must always read len bytes (or until EOF), due to known bug in Sun code
        assertEquals(20, stream.read(bytes, 0, 20));

        assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xDB, 0x0, 0x43, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, bytes);
    }

    @Test
    public void testStreamRealDataLength() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-adobe-rgb.jpg")));

        long length = 0;
        while (stream.read() != -1) {
            length++;
        }

        assertThat(length, lessThanOrEqualTo(10203L)); // In no case should length increase

        assertEquals(9607L, length); // May change, if more chunks are passed to reader...
    }

    @Test
    public void testAppSegmentsFiltering() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg")));
        List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);

        assertEquals(2, appSegments.size());

        assertEquals(JPEG.APP1, appSegments.get(0).marker());
        assertEquals("Exif", appSegments.get(0).identifier());

        assertEquals(JPEG.APP14, appSegments.get(1).marker());
        assertEquals("Adobe", appSegments.get(1).identifier());

        // And thus, no JFIF, no XMP, no ICC_PROFILE or other segments
    }

    @Test
    public void testEOFSOSSegmentBug() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/eof-sos-segment-bug.jpg")));

        long length = 0;
        while (stream.read() != -1) {
            length++;
        }

        assertEquals(9281L, length); // Sanity check: same as file size, except..?
    }

    @Test
    public void testReadPaddedSegmentsBug() throws IOException {
        ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg")));

        List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
        assertEquals(1, appSegments.size());

        assertEquals(JPEG.APP1, appSegments.get(0).marker());
        assertEquals("Exif", appSegments.get(0).identifier());

        stream.seek(0L);

        long length = 0;
        while (stream.read() != -1) {
            length++;
        }

        assertEquals(1061L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment
    }

    @Test
    public void testEOFExceptionInSegmentParsingShouldNotCreateBadState2() throws IOException {
        ImageInputStream iis = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/51432b02-02a8-11e7-9203-b42b1c43c0c3.jpg")));

        byte[] buffer = new byte[4096];

        // NOTE: This is a simulation of how the native parts of com.sun...JPEGImageReader would read the image...
        assertEquals(2, iis.read(buffer, 0, buffer.length));
        assertEquals(2, iis.getStreamPosition());

        iis.seek(2000); // Just a random position beyond EOF
        assertEquals(2000, iis.getStreamPosition());

        // So far, so good (but stream position is now really beyond EOF)...

        // This however, will blow up with an EOFException internally (but we'll return -1 to be good)
        assertEquals(-1, iis.read(buffer, 0, buffer.length));
        assertEquals(-1, iis.read());
        assertEquals(2000, iis.getStreamPosition());

        // Again, should just continue returning -1 for ever
        assertEquals(-1, iis.read());
        assertEquals(-1, iis.read(buffer, 0, buffer.length));
        assertEquals(2000, iis.getStreamPosition());
    }

    @Test
    public void testEOFExceptionInSegmentParsingShouldNotCreateBadState() throws IOException {
        ImageInputStream iis = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/broken-no-sof-ascii-transfer-mode.jpg")));

        byte[] buffer = new byte[4096];

        // NOTE: This is a simulation of how the native parts of com.sun...JPEGImageReader would read the image...
        assertEquals(2, iis.read(buffer, 0, buffer.length));
        assertEquals(2, iis.getStreamPosition());

        iis.seek(0x2012); // bad segment length, should have been 0x0012, not 0x2012
        assertEquals(0x2012, iis.getStreamPosition());

        // So far, so good (but stream position is now really beyond EOF)...

        // This however, will blow up with an EOFException internally (but we'll return -1 to be good)
        assertEquals(-1, iis.read(buffer, 0, buffer.length));
        assertEquals(-1, iis.read());
        assertEquals(0x2012, iis.getStreamPosition());

        // Again, should just continue returning -1 for ever
        assertEquals(-1, iis.read(buffer, 0, buffer.length));
        assertEquals(-1, iis.read());
        assertEquals(0x2012, iis.getStreamPosition());
    }


    @Test
    public void testInfiniteLoopCorrupt() throws IOException {
        assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
            try (ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/110115680-6d6dce80-7d84-11eb-99df-4cb21df3b09f.jpeg")))) {
                long length = 0;
                while (stream.read() != -1) {
                    length++;
                }

                assertEquals(25504L, length); // Sanity check: same as file size, except..?
            }

            try (ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/110115680-6d6dce80-7d84-11eb-99df-4cb21df3b09f.jpeg")))) {
                long length = 0;
                byte[] buffer = new byte[1024];
                int read;
                while ((read = stream.read(buffer)) != -1) {
                    length += read;
                }

                assertEquals(25504L, length); // Sanity check: same as file size, except..?
            }
        });
    }
}