ByteOrderedDataInputStream.java

/*
 * Copyright 2020 The Android Open Source Project
 *
 * 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 androidx.camera.core.impl.utils;

import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;

/**
 * An input stream to parse EXIF data area, which can be written in either little or big endian
 * order.
 */
// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}
final class ByteOrderedDataInputStream extends InputStream implements DataInput {
    private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
    private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;

    private final DataInputStream mDataInputStream;
    private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final int mLength;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
            int mPosition;

    ByteOrderedDataInputStream(InputStream in) throws IOException {
        this(in, ByteOrder.BIG_ENDIAN);
    }

    ByteOrderedDataInputStream(InputStream in, ByteOrder byteOrder) throws IOException {
        mDataInputStream = new DataInputStream(in);
        mLength = mDataInputStream.available();
        mPosition = 0;
        // TODO (b/142218289): Need to handle case where input stream does not support mark
        mDataInputStream.mark(mLength);
        mByteOrder = byteOrder;
    }

    ByteOrderedDataInputStream(byte[] bytes) throws IOException {
        this(new ByteArrayInputStream(bytes));
    }

    public void setByteOrder(ByteOrder byteOrder) {
        mByteOrder = byteOrder;
    }

    public void seek(long byteCount) throws IOException {
        if (mPosition > byteCount) {
            mPosition = 0;
            mDataInputStream.reset();
            // TODO (b/142218289): Need to handle case where input stream does not support mark
            mDataInputStream.mark(mLength);
        } else {
            byteCount -= mPosition;
        }

        if (skipBytes((int) byteCount) != (int) byteCount) {
            throw new IOException("Couldn't seek up to the byteCount");
        }
    }

    public int peek() {
        return mPosition;
    }

    @Override
    public int available() throws IOException {
        return mDataInputStream.available();
    }

    @Override
    public int read() throws IOException {
        ++mPosition;
        return mDataInputStream.read();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int bytesRead = mDataInputStream.read(b, off, len);
        mPosition += bytesRead;
        return bytesRead;
    }

    @Override
    public int readUnsignedByte() throws IOException {
        ++mPosition;
        return mDataInputStream.readUnsignedByte();
    }

    @Override
    public String readLine() {
        throw new UnsupportedOperationException("readLine() not implemented.");
    }

    @Override
    public boolean readBoolean() throws IOException {
        ++mPosition;
        return mDataInputStream.readBoolean();
    }

    @Override
    public char readChar() throws IOException {
        mPosition += 2;
        return mDataInputStream.readChar();
    }

    @Override
    public String readUTF() throws IOException {
        mPosition += 2;
        return mDataInputStream.readUTF();
    }

    @Override
    public void readFully(byte[] buffer, int offset, int length) throws IOException {
        mPosition += length;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        if (mDataInputStream.read(buffer, offset, length) != length) {
            throw new IOException("Couldn't read up to the length of buffer");
        }
    }

    @Override
    public void readFully(byte[] buffer) throws IOException {
        mPosition += buffer.length;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) {
            throw new IOException("Couldn't read up to the length of buffer");
        }
    }

    @Override
    public byte readByte() throws IOException {
        ++mPosition;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        int ch = mDataInputStream.read();
        if (ch < 0) {
            throw new EOFException();
        }
        return (byte) ch;
    }

    @Override
    public short readShort() throws IOException {
        mPosition += 2;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        int ch1 = mDataInputStream.read();
        int ch2 = mDataInputStream.read();
        if ((ch1 | ch2) < 0) {
            throw new EOFException();
        }
        if (mByteOrder == LITTLE_ENDIAN) {
            return (short) ((ch2 << 8) + ch1);
        } else if (mByteOrder == BIG_ENDIAN) {
            return (short) ((ch1 << 8) + ch2);
        }
        throw new IOException("Invalid byte order: " + mByteOrder);
    }

    @Override
    public int readInt() throws IOException {
        mPosition += 4;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        int ch1 = mDataInputStream.read();
        int ch2 = mDataInputStream.read();
        int ch3 = mDataInputStream.read();
        int ch4 = mDataInputStream.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0) {
            throw new EOFException();
        }
        if (mByteOrder == LITTLE_ENDIAN) {
            return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
        } else if (mByteOrder == BIG_ENDIAN) {
            return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
        }
        throw new IOException("Invalid byte order: " + mByteOrder);
    }

    @Override
    public int skipBytes(int byteCount) throws IOException {
        int totalSkip = Math.min(byteCount, mLength - mPosition);
        int skipped = 0;
        while (skipped < totalSkip) {
            skipped += mDataInputStream.skipBytes(totalSkip - skipped);
        }
        mPosition += skipped;
        return skipped;
    }

    @Override
    public int readUnsignedShort() throws IOException {
        mPosition += 2;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        int ch1 = mDataInputStream.read();
        int ch2 = mDataInputStream.read();
        if ((ch1 | ch2) < 0) {
            throw new EOFException();
        }
        if (mByteOrder == LITTLE_ENDIAN) {
            return ((ch2 << 8) + ch1);
        } else if (mByteOrder == BIG_ENDIAN) {
            return ((ch1 << 8) + ch2);
        }
        throw new IOException("Invalid byte order: " + mByteOrder);
    }

    public long readUnsignedInt() throws IOException {
        return readInt() & 0xffffffffL;
    }

    @Override
    public long readLong() throws IOException {
        mPosition += 8;
        if (mPosition > mLength) {
            throw new EOFException();
        }
        int ch1 = mDataInputStream.read();
        int ch2 = mDataInputStream.read();
        int ch3 = mDataInputStream.read();
        int ch4 = mDataInputStream.read();
        int ch5 = mDataInputStream.read();
        int ch6 = mDataInputStream.read();
        int ch7 = mDataInputStream.read();
        int ch8 = mDataInputStream.read();
        if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
            throw new EOFException();
        }
        if (mByteOrder == LITTLE_ENDIAN) {
            return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
                    + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
                    + ((long) ch2 << 8) + (long) ch1);
        } else if (mByteOrder == BIG_ENDIAN) {
            return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
                    + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
                    + ((long) ch7 << 8) + (long) ch8);
        }
        throw new IOException("Invalid byte order: " + mByteOrder);
    }

    @Override
    public float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }

    @Override
    public double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }

    @Override
    public void mark(int readlimit) {
        synchronized (mDataInputStream) {
            mDataInputStream.mark(readlimit);
        }
    }

    public int getLength() {
        return mLength;
    }
}