MetadataListReader.java

/*
 * Copyright 2021 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.emoji2.text;

import android.content.res.AssetManager;

import androidx.annotation.AnyThread;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.emoji2.text.flatbuffer.MetadataList;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * Reads the emoji metadata from a given InputStream or ByteBuffer.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@AnyThread
@RequiresApi(19)
class MetadataListReader {

    /**
     * Meta tag for emoji metadata. This string is used by the font update script to insert the
     * emoji meta into the font. This meta table contains the list of all emojis which are stored in
     * binary format using FlatBuffers. This flat list is later converted by the system into a trie.
     * {@code int} representation for "Emji"
     *
     * @see MetadataRepo
     */
    private static final int EMJI_TAG = 'E' << 24 | 'm' << 16 | 'j' << 8 | 'i';

    /**
     * Deprecated meta tag name. Do not use, kept for compatibility reasons, will be removed soon.
     */
    private static final int EMJI_TAG_DEPRECATED = 'e' << 24 | 'm' << 16 | 'j' << 8 | 'i';

    /**
     * The name of the meta table in the font. int representation for "meta"
     */
    private static final int META_TABLE_NAME = 'm' << 24 | 'e' << 16 | 't' << 8 | 'a';

    /**
     * Construct MetadataList from an input stream. Does not close the given InputStream, therefore
     * it is caller's responsibility to properly close the stream.
     *
     * @param inputStream InputStream to read emoji metadata from
     */
    static MetadataList read(InputStream inputStream) throws IOException {
        final OpenTypeReader openTypeReader = new InputStreamOpenTypeReader(inputStream);
        final OffsetInfo offsetInfo = findOffsetInfo(openTypeReader);
        // skip to where metadata is
        openTypeReader.skip((int) (offsetInfo.getStartOffset() - openTypeReader.getPosition()));
        // allocate a ByteBuffer and read into it since FlatBuffers can read only from a ByteBuffer
        final ByteBuffer buffer = ByteBuffer.allocate((int) offsetInfo.getLength());
        final int numRead = inputStream.read(buffer.array());
        if (numRead != offsetInfo.getLength()) {
            throw new IOException("Needed " + offsetInfo.getLength() + " bytes, got " + numRead);
        }

        return MetadataList.getRootAsMetadataList(buffer);
    }

    /**
     * Construct MetadataList from a byte buffer.
     *
     * @param byteBuffer ByteBuffer to read emoji metadata from
     */
    static MetadataList read(final ByteBuffer byteBuffer) throws IOException {
        final ByteBuffer newBuffer = byteBuffer.duplicate();
        final OpenTypeReader reader = new ByteBufferReader(newBuffer);
        final OffsetInfo offsetInfo = findOffsetInfo(reader);
        // skip to where metadata is
        newBuffer.position((int) offsetInfo.getStartOffset());
        return MetadataList.getRootAsMetadataList(newBuffer);
    }

    /**
     * Construct MetadataList from an asset.
     *
     * @param assetManager AssetManager instance
     * @param assetPath asset manager path of the file that the Typeface and metadata will be
     *                  created from
     */
    static MetadataList read(AssetManager assetManager, String assetPath)
            throws IOException {
        try (InputStream inputStream = assetManager.open(assetPath)) {
            return read(inputStream);
        }
    }

    /**
     * Finds the start offset and length of the emoji metadata in the font.
     *
     * @return OffsetInfo which contains start offset and length of the emoji metadata in the font
     *
     * @throws IOException
     */
    private static OffsetInfo findOffsetInfo(OpenTypeReader reader) throws IOException {
        // skip sfnt version
        reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
        // start of Table Count
        final int tableCount = reader.readUnsignedShort();
        if (tableCount > 100) {
            //something is wrong quit
            throw new IOException("Cannot read metadata.");
        }
        //skip to beginning of tables data
        reader.skip(OpenTypeReader.UINT16_BYTE_COUNT * 3);

        long metaOffset = -1;
        for (int i = 0; i < tableCount; i++) {
            final int tag = reader.readTag();
            // skip checksum
            reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
            final long offset = reader.readUnsignedInt();
            // skip mLength
            reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
            if (META_TABLE_NAME == tag) {
                metaOffset = offset;
                break;
            }
        }

        if (metaOffset != -1) {
            // skip to the beginning of meta tables.
            reader.skip((int) (metaOffset - reader.getPosition()));
            // skip minorVersion, majorVersion, flags, reserved,
            reader.skip(
                    OpenTypeReader.UINT16_BYTE_COUNT * 2 + OpenTypeReader.UINT32_BYTE_COUNT * 2);
            final long mapsCount = reader.readUnsignedInt();
            for (int i = 0; i < mapsCount; i++) {
                final int tag = reader.readTag();
                final long dataOffset = reader.readUnsignedInt();
                final long dataLength = reader.readUnsignedInt();
                if (EMJI_TAG == tag || EMJI_TAG_DEPRECATED == tag) {
                    return new OffsetInfo(dataOffset + metaOffset, dataLength);
                }
            }
        }

        throw new IOException("Cannot read metadata.");
    }

    /**
     * Start offset and length of the emoji metadata in the font.
     */
    private static class OffsetInfo {
        private final long mStartOffset;
        private final long mLength;

        OffsetInfo(long startOffset, long length) {
            mStartOffset = startOffset;
            mLength = length;
        }

        long getStartOffset() {
            return mStartOffset;
        }

        long getLength() {
            return mLength;
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static int toUnsignedShort(final short value) {
        return value & 0xFFFF;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static long toUnsignedInt(final int value) {
        return value & 0xFFFFFFFFL;
    }

    private interface OpenTypeReader {
        int UINT16_BYTE_COUNT = 2;
        int UINT32_BYTE_COUNT = 4;

        /**
         * Reads an {@code OpenType uint16}.
         *
         * @throws IOException
         */
        int readUnsignedShort() throws IOException;

        /**
         * Reads an {@code OpenType uint32}.
         *
         * @throws IOException
         */
        long readUnsignedInt() throws IOException;

        /**
         * Reads an {@code OpenType Tag}.
         *
         * @throws IOException
         */
        int readTag() throws IOException;

        /**
         * Skip the given amount of numOfBytes
         *
         * @throws IOException
         */
        void skip(int numOfBytes) throws IOException;

        /**
         * @return the position of the reader
         */
        long getPosition();
    }

    /**
     * Reads {@code OpenType} data from an {@link InputStream}.
     */
    private static class InputStreamOpenTypeReader implements OpenTypeReader {

        private final @NonNull byte[] mByteArray;
        private final @NonNull ByteBuffer mByteBuffer;
        private final @NonNull InputStream mInputStream;
        private long mPosition = 0;

        /**
         * Constructs the reader with the given InputStream. Does not close the InputStream, it is
         * caller's responsibility to close it.
         *
         * @param inputStream InputStream to read from
         */
        InputStreamOpenTypeReader(@NonNull final InputStream inputStream) {
            mInputStream = inputStream;
            mByteArray = new byte[UINT32_BYTE_COUNT];
            mByteBuffer = ByteBuffer.wrap(mByteArray);
            mByteBuffer.order(ByteOrder.BIG_ENDIAN);
        }

        @Override
        public int readUnsignedShort() throws IOException {
            mByteBuffer.position(0);
            read(UINT16_BYTE_COUNT);
            return toUnsignedShort(mByteBuffer.getShort());
        }

        @Override
        public long readUnsignedInt() throws IOException {
            mByteBuffer.position(0);
            read(UINT32_BYTE_COUNT);
            return toUnsignedInt(mByteBuffer.getInt());
        }

        @Override
        public int readTag() throws IOException {
            mByteBuffer.position(0);
            read(UINT32_BYTE_COUNT);
            return mByteBuffer.getInt();
        }

        @Override
        public void skip(int numOfBytes) throws IOException {
            while (numOfBytes > 0) {
                int skipped = (int) mInputStream.skip(numOfBytes);
                if (skipped < 1) {
                    throw new IOException("Skip didn't move at least 1 byte forward");
                }
                numOfBytes -= skipped;
                mPosition += skipped;
            }
        }

        @Override
        public long getPosition() {
            return mPosition;
        }

        private void read(@IntRange(from = 0, to = UINT32_BYTE_COUNT) final int numOfBytes)
                throws IOException {
            if (mInputStream.read(mByteArray, 0, numOfBytes) != numOfBytes) {
                throw new IOException("read failed");
            }
            mPosition += numOfBytes;
        }
    }

    /**
     * Reads OpenType data from a ByteBuffer.
     */
    private static class ByteBufferReader implements OpenTypeReader {

        private final @NonNull ByteBuffer mByteBuffer;

        /**
         * Constructs the reader with the given ByteBuffer.
         *
         * @param byteBuffer ByteBuffer to read from
         */
        ByteBufferReader(@NonNull final ByteBuffer byteBuffer) {
            mByteBuffer = byteBuffer;
            mByteBuffer.order(ByteOrder.BIG_ENDIAN);
        }

        @Override
        public int readUnsignedShort() throws IOException {
            return toUnsignedShort(mByteBuffer.getShort());
        }

        @Override
        public long readUnsignedInt() throws IOException {
            return toUnsignedInt(mByteBuffer.getInt());
        }

        @Override
        public int readTag() throws IOException {
            return mByteBuffer.getInt();
        }

        @Override
        public void skip(final int numOfBytes) throws IOException {
            mByteBuffer.position(mByteBuffer.position() + numOfBytes);
        }

        @Override
        public long getPosition() {
            return mByteBuffer.position();
        }
    }

    private MetadataListReader() {
    }
}