InputStream.java

// Copyright (c) ZeroC, Inc.

package com.zeroc.Ice;

import java.io.IOException;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Interface to read sequence of bytes encoded using the Slice encoding and recreate the corresponding
 * Slice types.
 *
 * @see OutputStream
 */
public final class InputStream {
    /**
     * This constructor uses the communicator's default encoding version.
     *
     * @param communicator The communicator to use when initializing the stream.
     * @param data The byte array containing encoded Slice types.
     */
    public InputStream(Communicator communicator, byte[] data) {
        this(
            communicator.getInstance(),
            communicator.getInstance().defaultsAndOverrides().defaultEncoding,
            new Buffer(data));
    }

    /**
     * This constructor uses the communicator's default encoding version.
     *
     * @param communicator The communicator to use when initializing the stream.
     * @param buf The byte buffer containing encoded Slice types.
     */
    public InputStream(Communicator communicator, ByteBuffer buf) {
        this(
            communicator.getInstance(),
            communicator.getInstance().defaultsAndOverrides().defaultEncoding,
            new Buffer(buf));
    }

    /**
     * This constructor uses the given communicator and encoding version.
     *
     * @param communicator The communicator to use when initializing the stream.
     * @param encoding The desired encoding version.
     * @param data The byte array containing encoded Slice types.
     */
    public InputStream(Communicator communicator, EncodingVersion encoding, byte[] data) {
        this(communicator.getInstance(), encoding, new Buffer(data));
    }

    /**
     * This constructor uses the given communicator and encoding version.
     *
     * @param communicator The communicator to use when initializing the stream.
     * @param encoding The desired encoding version.
     * @param buf The byte buffer containing encoded Slice types.
     */
    public InputStream(
            Communicator communicator, EncodingVersion encoding, ByteBuffer buf) {
        this(communicator.getInstance(), encoding, new Buffer(buf));
    }

    /** Constructs an InputStream with an empty buffer. */
    InputStream(Instance instance, EncodingVersion encoding, boolean direct) {
        // Create an empty non-direct buffer.
        this(instance, encoding, new Buffer(direct));
    }

    InputStream(Instance instance, EncodingVersion encoding, Buffer buf, boolean adopt) {
        this(instance, encoding, new Buffer(buf, adopt));
    }

    /** The primary constructor called by all other constructors. */
    private InputStream(Instance instance, EncodingVersion encoding, Buffer buf) {
        _instance = instance;
        _encoding = encoding;
        _buf = buf;

        // Everything below is cached from instance.
        _classGraphDepthMax = _instance.classGraphDepthMax();
    }

    /**
     * Resets this stream. This method allows the stream to be reused, to avoid creating unnecessary
     * garbage.
     */
    public void reset() {
        _buf.reset();
        clear();
    }

    /**
     * Releases any data retained by encapsulations. The {@link #reset} method internally calls
     * <code>clear</code>.
     */
    public void clear() {
        if (_encapsStack != null) {
            assert (_encapsStack.next == null);
            _encapsStack.next = _encapsCache;
            _encapsCache = _encapsStack;
            _encapsCache.reset();
            _encapsStack = null;
        }

        _startSeq = -1;
    }

    Instance instance() {
        return _instance;
    }

    /**
     * Swaps the contents of one stream with another.
     *
     * @param other The other stream.
     */
    public void swap(InputStream other) {
        assert (_instance == other._instance);

        Buffer tmpBuf = other._buf;
        other._buf = _buf;
        _buf = tmpBuf;

        EncodingVersion tmpEncoding = other._encoding;
        other._encoding = _encoding;
        _encoding = tmpEncoding;

        int tmpStartSeq = other._startSeq;
        other._startSeq = _startSeq;
        _startSeq = tmpStartSeq;

        int tmpMinSeqSize = other._minSeqSize;
        other._minSeqSize = _minSeqSize;
        _minSeqSize = tmpMinSeqSize;

        // Swap is never called for streams that have encapsulations being read. However,
        // encapsulations might still be set in case unmarshaling failed. We just reset the
        // encapsulations if there are still some set.
        resetEncapsulation();
        other.resetEncapsulation();
    }

    private void resetEncapsulation() {
        _encapsStack = null;
    }

    /**
     * Resizes the stream to a new size.
     *
     * @param sz The new size.
     */
    void resize(int sz) {
        _buf.resize(sz, true);
        _buf.position(sz);
    }

    Buffer getBuffer() {
        return _buf;
    }

    /** Marks the start of a class instance. */
    public void startValue() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        _encapsStack.decoder.startInstance(SliceType.ValueSlice);
    }

    /**
     * Marks the end of a class instance.
     *
     * @return An object that encapsulates the unknown slice data.
     */
    public SlicedData endValue() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        return _encapsStack.decoder.endInstance();
    }

    /** Marks the start of a user exception. */
    public void startException() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        _encapsStack.decoder.startInstance(SliceType.ExceptionSlice);
    }

    /**
     * Marks the end of a user exception.
     *
     * @return An object that encapsulates the unknown slice data.
     */
    public SlicedData endException() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        return _encapsStack.decoder.endInstance();
    }

    /**
     * Reads the start of an encapsulation.
     *
     * @return The encoding version used by the encapsulation.
     */
    public EncodingVersion startEncapsulation() {
        Encaps curr = _encapsCache;
        if (curr != null) {
            curr.reset();
            _encapsCache = _encapsCache.next;
        } else {
            curr = new Encaps();
        }
        curr.next = _encapsStack;
        _encapsStack = curr;

        _encapsStack.start = _buf.b.position();

        //
        // I don't use readSize() for encapsulations, because when creating an encapsulation,
        // I must know in advance how many bytes the size information will require in the data
        // stream. If I use an Int, it is always 4 bytes. For readSize(), it could be 1 or 5 bytes.
        //
        int sz = readInt();
        if (sz < 6) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }
        if (sz - 4 > _buf.b.remaining()) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }
        _encapsStack.sz = sz;

        EncodingVersion encoding = EncodingVersion.ice_read(this);
        Protocol.checkSupportedEncoding(encoding); // Make sure the encoding is supported.
        _encapsStack.setEncoding(encoding);

        return encoding;
    }

    /** Ends the current encapsulation. */
    public void endEncapsulation() {
        assert (_encapsStack != null);

        if (!_encapsStack.encoding_1_0) {
            skipOptionals();
            if (_buf.b.position() != _encapsStack.start + _encapsStack.sz) {
                throw new MarshalException("Failed to unmarshal encapsulation.");
            }
        } else if (_buf.b.position() != _encapsStack.start + _encapsStack.sz) {
            if (_buf.b.position() + 1 != _encapsStack.start + _encapsStack.sz) {
                throw new MarshalException("Failed to unmarshal encapsulation.");
            }

            //
            // Ice version < 3.3 had a bug where user exceptions with
            // class members could be encoded with a trailing byte
            // when dispatched with AMD. So we tolerate an extra byte
            // in the encapsulation.
            //
            try {
                _buf.b.get();
            } catch (BufferUnderflowException ex) {
                throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
            }
        }

        Encaps curr = _encapsStack;
        _encapsStack = curr.next;
        curr.next = _encapsCache;
        _encapsCache = curr;
        _encapsCache.reset();
    }

    /**
     * Skips an empty encapsulation.
     *
     * @return The encapsulation's encoding version.
     */
    public EncodingVersion skipEmptyEncapsulation() {
        int sz = readInt();
        if (sz < 6) {
            throw new MarshalException(sz + " is not a valid encapsulation size.");
        }
        if (sz - 4 > _buf.b.remaining()) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }

        EncodingVersion encoding = EncodingVersion.ice_read(this);
        Protocol.checkSupportedEncoding(encoding); // Make sure the encoding is supported.

        if (encoding.equals(Util.Encoding_1_0)) {
            if (sz != 6) {
                throw new MarshalException(
                    sz + "is not a valid encapsulation size for a 1.0 empty encapsulation.");
            }
        } else {
            //
            // Skip the optional content of the encapsulation if we are expecting an empty
            // encapsulation.
            //
            _buf.position(_buf.b.position() + sz - 6);
        }
        return encoding;
    }

    /**
     * Returns a blob of bytes representing an encapsulation. The encapsulation's encoding version
     * is returned in the argument.
     *
     * @param encoding The encapsulation's encoding version.
     * @return The encoded encapsulation.
     */
    public byte[] readEncapsulation(EncodingVersion encoding) {
        int sz = readInt();
        if (sz < 6) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }

        if (sz - 4 > _buf.b.remaining()) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }

        if (encoding != null) {
            encoding.ice_readMembers(this);
            _buf.position(_buf.b.position() - 6);
        } else {
            _buf.position(_buf.b.position() - 4);
        }

        byte[] v = new byte[sz];
        try {
            _buf.b.get(v);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Determines the current encoding version.
     *
     * @return The encoding version.
     */
    public EncodingVersion getEncoding() {
        return _encapsStack != null ? _encapsStack.encoding : _encoding;
    }

    /**
     * Determines the size of the current encapsulation, excluding the encapsulation header.
     *
     * @return The size of the encapsulated data.
     */
    public int getEncapsulationSize() {
        assert (_encapsStack != null);
        return _encapsStack.sz - 6;
    }

    /**
     * Skips over an encapsulation.
     *
     * @return The encoding version of the skipped encapsulation.
     */
    public EncodingVersion skipEncapsulation() {
        int sz = readInt();
        if (sz < 6) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }
        EncodingVersion encoding = EncodingVersion.ice_read(this);
        try {
            _buf.position(_buf.b.position() + sz - 6);
        } catch (IllegalArgumentException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
        return encoding;
    }

    /**
     * Reads the start of a value or exception slice.
     */
    public void startSlice() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        _encapsStack.decoder.startSlice();
    }

    /** Indicates that the end of a value or exception slice has been reached. */
    public void endSlice() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        _encapsStack.decoder.endSlice();
    }

    /** Skips over a value or exception slice. */
    public void skipSlice() {
        assert (_encapsStack != null && _encapsStack.decoder != null);
        _encapsStack.decoder.skipSlice();
    }

    /**
     * Indicates that unmarshaling is complete, except for any class instances. The application must
     * call this method only if the stream actually contains class instances. Calling <code>
     * readPendingValues</code> triggers the calls to consumers provided with {@link #readValue} to
     * inform the application that unmarshaling of an instance is complete.
     */
    public void readPendingValues() {
        if (_encapsStack != null && _encapsStack.decoder != null) {
            _encapsStack.decoder.readPendingValues();
        } else if (_encapsStack != null
            ? _encapsStack.encoding_1_0
            : _encoding.equals(Util.Encoding_1_0)) {
            //
            // If using the 1.0 encoding and no instances were read, we still read an empty sequence
            // of pending instances if requested (i.e.: if this is called).
            //
            // This is required by the 1.0 encoding, even if no instances are written we do marshal
            // an empty sequence if marshaled data types use classes.
            //
            skipSize();
        }
    }

    /**
     * Extracts a size from the stream.
     *
     * @return The extracted size.
     */
    public int readSize() {
        try {
            byte b = _buf.b.get();
            if (b == -1) {
                int v = _buf.b.getInt();
                if (v < 0) {
                    throw new MarshalException(END_OF_BUFFER_MESSAGE);
                }
                return v;
            } else {
                return b < 0 ? b + 256 : b;
            }
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Reads and validates a sequence size.
     *
     * @param minSize The minimum size required by the sequence type.
     * @return The extracted size.
     */
    public int readAndCheckSeqSize(int minSize) {
        int sz = readSize();

        if (sz == 0) {
            return sz;
        }

        //
        // The _startSeq variable points to the start of the sequence for which
        // we expect to read at least _minSeqSize bytes from the stream.
        //
        // If not initialized or if we already read more data than _minSeqSize, we reset _startSeq
        // and _minSeqSize for this sequence (possibly a top-level sequence or enclosed sequence it
        // doesn't really matter).
        //
        // Otherwise, we are reading an enclosed sequence and we have to bump _minSeqSize by the
        // minimum size that this sequence will require on the stream.
        //
        // The goal of this check is to ensure that when we start un-marshaling a new sequence, we
        // check the minimal size of this new sequence against the estimated remaining buffer size.
        // This estimation is based on the minimum size of the enclosing sequences, it's
        // _minSeqSize.
        //
        if (_startSeq == -1 || _buf.b.position() > (_startSeq + _minSeqSize)) {
            _startSeq = _buf.b.position();
            _minSeqSize = sz * minSize;
        } else {
            _minSeqSize += sz * minSize;
        }

        //
        // If there isn't enough data to read on the stream for the sequence (and possibly enclosed
        // sequences), something is wrong with the marshaled data: it's claiming having more data
        // that what is possible to read.
        //
        if (_startSeq + _minSeqSize > _buf.size()) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }

        return sz;
    }

    /**
     * Reads a blob of bytes from the stream.
     *
     * @param sz The number of bytes to read.
     * @return The requested bytes as a byte array.
     */
    public byte[] readBlob(int sz) {
        if (_buf.b.remaining() < sz) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }
        byte[] v = new byte[sz];
        try {
            _buf.b.get(v);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Determine if an optional value is available for reading.
     *
     * @param tag The tag associated with the value.
     * @param expectedFormat The optional format for the value.
     * @return True if the value is present, false otherwise.
     */
    public boolean readOptional(int tag, OptionalFormat expectedFormat) {
        assert (_encapsStack != null);
        if (_encapsStack.decoder != null) {
            return _encapsStack.decoder.readOptional(tag, expectedFormat);
        } else {
            return readOptImpl(tag, expectedFormat);
        }
    }

    /**
     * Extracts a byte value from the stream.
     *
     * @return The extracted byte.
     */
    public byte readByte() {
        try {
            return _buf.b.get();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional byte value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<Byte> readByte(int tag) {
        if (readOptional(tag, OptionalFormat.F1)) {
            return Optional.of(readByte());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a sequence of byte values from the stream.
     *
     * @return The extracted byte sequence.
     */
    public byte[] readByteSeq() {
        try {
            final int sz = readAndCheckSeqSize(1);
            byte[] v = new byte[sz];
            _buf.b.get(v);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional byte sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<byte[]> readByteSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            return Optional.of(readByteSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Returns a byte buffer representing a sequence of bytes. This method does not copy the data.
     *
     * @return A byte buffer "slice" of the internal buffer.
     */
    public ByteBuffer readByteBuffer() {
        try {
            final int sz = readAndCheckSeqSize(1);
            ByteBuffer v = _buf.b.slice();
            // Cast to java.nio.Buffer to avoid incompatible covariant
            // return type used in Java 9 java.nio.ByteBuffer
            ((java.nio.Buffer) v).limit(sz);
            _buf.position(_buf.b.position() + sz);
            return v.asReadOnlyBuffer();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts a serializable Java object from the stream.
     *
     * @param <T> The serializable type.
     * @param cl The class for the serializable type.
     * @return The deserialized Java object.
     */
    public <T extends Serializable> T readSerializable(Class<T> cl) {
        int sz = readAndCheckSeqSize(1);
        if (sz == 0) {
            return null;
        }
        ObjectInputStreamWrapper in = null;
        try {
            var w = new InputStreamWrapper(sz, _buf.b);
            in = new ObjectInputStreamWrapper(_instance, w);
            return cl.cast(in.readObject());
        } catch (LocalException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new MarshalException("cannot deserialize object", ex);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ex) {
                    throw new MarshalException("cannot deserialize object", ex);
                }
            }
        }
    }

    /**
     * Extracts a optional serializable Java object from the stream.
     *
     * @param <T> The serializable type.
     * @param tag The numeric tag associated with the value.
     * @param cl The class for the serializable type.
     * @return The optional value (if any).
     */
    public <T extends Serializable> Optional<T> readSerializable(
            int tag, Class<T> cl) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            return Optional.of(readSerializable(cl));
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a boolean value from the stream.
     *
     * @return The extracted boolean.
     */
    public boolean readBool() {
        try {
            return _buf.b.get() == 1;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional boolean value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<Boolean> readBool(int tag) {
        if (readOptional(tag, OptionalFormat.F1)) {
            return Optional.of(readBool());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a sequence of boolean values from the stream.
     *
     * @return The extracted boolean sequence.
     */
    public boolean[] readBoolSeq() {
        try {
            final int sz = readAndCheckSeqSize(1);
            boolean[] v = new boolean[sz];
            for (int i = 0; i < sz; i++) {
                v[i] = _buf.b.get() == 1;
            }
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional boolean sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<boolean[]> readBoolSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            return Optional.of(readBoolSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a short value from the stream.
     *
     * @return The extracted short.
     */
    public short readShort() {
        try {
            return _buf.b.getShort();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional short value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<Short> readShort(int tag) {
        if (readOptional(tag, OptionalFormat.F2)) {
            return Optional.of(readShort());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a sequence of short values from the stream.
     *
     * @return The extracted short sequence.
     */
    public short[] readShortSeq() {
        try {
            final int sz = readAndCheckSeqSize(2);
            short[] v = new short[sz];
            ShortBuffer shortBuf = _buf.b.asShortBuffer();
            shortBuf.get(v);
            _buf.position(_buf.b.position() + sz * 2);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional short sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<short[]> readShortSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            skipSize();
            return Optional.of(readShortSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Returns a short buffer representing a sequence of shorts. This method does not copy the data.
     *
     * @return A short buffer "slice" of the internal buffer.
     */
    public ShortBuffer readShortBuffer() {
        try {
            final int sz = readAndCheckSeqSize(2);
            ShortBuffer shortBuf = _buf.b.asShortBuffer();
            ShortBuffer v = shortBuf.slice();
            // Cast to java.nio.Buffer to avoid incompatible covariant
            // return type used in Java 9 java.nio.ShortBuffer
            ((java.nio.Buffer) v).limit(sz);
            _buf.position(_buf.b.position() + sz * 2);
            return v.asReadOnlyBuffer();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an int value from the stream.
     *
     * @return The extracted int.
     */
    public int readInt() {
        try {
            return _buf.b.getInt();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional int value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public OptionalInt readInt(int tag) {
        if (readOptional(tag, OptionalFormat.F4)) {
            return OptionalInt.of(readInt());
        } else {
            return OptionalInt.empty();
        }
    }

    /**
     * Extracts a sequence of int values from the stream.
     *
     * @return The extracted int sequence.
     */
    public int[] readIntSeq() {
        try {
            final int sz = readAndCheckSeqSize(4);
            int[] v = new int[sz];
            IntBuffer intBuf = _buf.b.asIntBuffer();
            intBuf.get(v);
            _buf.position(_buf.b.position() + sz * 4);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional int sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<int[]> readIntSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            skipSize();
            return Optional.of(readIntSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Returns an int buffer representing a sequence of ints. This method does not copy the data.
     *
     * @return An int buffer "slice" of the internal buffer.
     */
    public IntBuffer readIntBuffer() {
        try {
            final int sz = readAndCheckSeqSize(4);
            IntBuffer intBuf = _buf.b.asIntBuffer();
            IntBuffer v = intBuf.slice();
            // Cast to java.nio.Buffer to avoid incompatible covariant
            // return type used in Java 9 java.nio.IntBuffer
            ((java.nio.Buffer) v).limit(sz);
            _buf.position(_buf.b.position() + sz * 4);
            return v.asReadOnlyBuffer();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts a long value from the stream.
     *
     * @return The extracted long.
     */
    public long readLong() {
        try {
            return _buf.b.getLong();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional long value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public OptionalLong readLong(int tag) {
        if (readOptional(tag, OptionalFormat.F8)) {
            return OptionalLong.of(readLong());
        } else {
            return OptionalLong.empty();
        }
    }

    /**
     * Extracts a sequence of long values from the stream.
     *
     * @return The extracted long sequence.
     */
    public long[] readLongSeq() {
        try {
            final int sz = readAndCheckSeqSize(8);
            long[] v = new long[sz];
            LongBuffer longBuf = _buf.b.asLongBuffer();
            longBuf.get(v);
            _buf.position(_buf.b.position() + sz * 8);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional long sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<long[]> readLongSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            skipSize();
            return Optional.of(readLongSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Returns a long buffer representing a sequence of longs. This method does not copy the data.
     *
     * @return A long buffer "slice" of the internal buffer.
     */
    public LongBuffer readLongBuffer() {
        try {
            final int sz = readAndCheckSeqSize(8);
            LongBuffer longBuf = _buf.b.asLongBuffer();
            LongBuffer v = longBuf.slice();
            // Cast to java.nio.Buffer to avoid incompatible covariant
            // return type used in Java 9 java.nio.LongBuffer
            ((java.nio.Buffer) v).limit(sz);
            _buf.position(_buf.b.position() + sz * 8);
            return v.asReadOnlyBuffer();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts a float value from the stream.
     *
     * @return The extracted float.
     */
    public float readFloat() {
        try {
            return _buf.b.getFloat();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional float value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<Float> readFloat(int tag) {
        if (readOptional(tag, OptionalFormat.F4)) {
            return Optional.of(readFloat());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a sequence of float values from the stream.
     *
     * @return The extracted float sequence.
     */
    public float[] readFloatSeq() {
        try {
            final int sz = readAndCheckSeqSize(4);
            float[] v = new float[sz];
            FloatBuffer floatBuf = _buf.b.asFloatBuffer();
            floatBuf.get(v);
            _buf.position(_buf.b.position() + sz * 4);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional float sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<float[]> readFloatSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            skipSize();
            return Optional.of(readFloatSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Returns a float buffer representing a sequence of floats. This method does not copy the data.
     *
     * @return A float buffer "slice" of the internal buffer.
     */
    public FloatBuffer readFloatBuffer() {
        try {
            final int sz = readAndCheckSeqSize(4);
            FloatBuffer floatBuf = _buf.b.asFloatBuffer();
            FloatBuffer v = floatBuf.slice();
            // Cast to java.nio.Buffer to avoid incompatible covariant
            // return type used in Java 9 java.nio.FloatBuffer
            ((java.nio.Buffer) v).limit(sz);
            _buf.position(_buf.b.position() + sz * 4);
            return v.asReadOnlyBuffer();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts a double value from the stream.
     *
     * @return The extracted double.
     */
    public double readDouble() {
        try {
            return _buf.b.getDouble();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional double value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public OptionalDouble readDouble(int tag) {
        if (readOptional(tag, OptionalFormat.F8)) {
            return OptionalDouble.of(readDouble());
        } else {
            return OptionalDouble.empty();
        }
    }

    /**
     * Extracts a sequence of double values from the stream.
     *
     * @return The extracted double sequence.
     */
    public double[] readDoubleSeq() {
        try {
            final int sz = readAndCheckSeqSize(8);
            double[] v = new double[sz];
            DoubleBuffer doubleBuf = _buf.b.asDoubleBuffer();
            doubleBuf.get(v);
            _buf.position(_buf.b.position() + sz * 8);
            return v;
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    /**
     * Extracts an optional double sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<double[]> readDoubleSeq(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            skipSize();
            return Optional.of(readDoubleSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Returns a double buffer representing a sequence of doubles. This method does not copy the
     * data.
     *
     * @return A double buffer "slice" of the internal buffer.
     */
    public DoubleBuffer readDoubleBuffer() {
        try {
            final int sz = readAndCheckSeqSize(8);
            DoubleBuffer doubleBuf = _buf.b.asDoubleBuffer();
            DoubleBuffer v = doubleBuf.slice();
            // Cast to java.nio.Buffer to avoid incompatible covariant
            // return type used in Java 9 java.nio.DoubleBuffer
            ((java.nio.Buffer) v).limit(sz);
            _buf.position(_buf.b.position() + sz * 8);
            return v.asReadOnlyBuffer();
        } catch (BufferUnderflowException ex) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
        }
    }

    static final Charset _utf8 = Charset.forName("UTF8");

    /**
     * Extracts a string from the stream.
     *
     * @return The extracted string.
     */
    public String readString() {
        final int len = readSize();

        if (len == 0) {
            return "";
        } else {
            //
            // Check the buffer has enough bytes to read.
            //
            if (_buf.b.remaining() < len) {
                throw new MarshalException(END_OF_BUFFER_MESSAGE);
            }

            try {
                //
                // We reuse the _stringBytes array to avoid creating excessive garbage.
                //
                if (_stringBytes == null || len > _stringBytes.length) {
                    _stringBytes = new byte[len];
                }
                if (_stringChars == null || len > _stringChars.length) {
                    _stringChars = new char[len];
                }
                _buf.b.get(_stringBytes, 0, len);

                //
                // It's more efficient to construct a string using a
                // character array instead of a byte array, because
                // byte arrays require conversion.
                //
                for (int i = 0; i < len; i++) {
                    if (_stringBytes[i] < 0) {
                        //
                        // Multi-byte character found - we must use
                        // conversion.
                        //
                        // TODO: If the string contains garbage bytes
                        // that won't correctly decode as UTF, the behavior of this constructor is
                        // undefined. It would be better to explicitly decode using
                        // java.nio.charset.CharsetDecoder and to
                        // throw MarshalException if the string won't
                        // decode.
                        //
                        return new String(_stringBytes, 0, len, "UTF8");
                    } else {
                        _stringChars[i] = (char) _stringBytes[i];
                    }
                }
                return new String(_stringChars, 0, len);
            } catch (UnsupportedEncodingException ex) {
                assert false;
                return "";
            } catch (BufferUnderflowException ex) {
                throw new MarshalException(END_OF_BUFFER_MESSAGE, ex);
            }
        }
    }

    /**
     * Extracts an optional string value from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<String> readString(int tag) {
        if (readOptional(tag, OptionalFormat.VSize)) {
            return Optional.of(readString());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a sequence of string values from the stream.
     *
     * @return The extracted string sequence.
     */
    public String[] readStringSeq() {
        final int sz = readAndCheckSeqSize(1);
        String[] v = new String[sz];
        for (int i = 0; i < sz; i++) {
            v[i] = readString();
        }
        return v;
    }

    /**
     * Extracts an optional string sequence from the stream.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<String[]> readStringSeq(int tag) {
        if (readOptional(tag, OptionalFormat.FSize)) {
            skip(4);
            return Optional.of(readStringSeq());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts a proxy from the stream. The stream must have been initialized with a communicator.
     *
     * @return The extracted proxy.
     */
    public ObjectPrx readProxy() {
        var ident = Identity.ice_read(this);
        if (ident.name.isEmpty()) {
            return null;
        } else {
            var ref = _instance.referenceFactory().create(ident, this);
            return new _ObjectPrxI(ref);
        }
    }

    /**
     * Extracts a proxy from the stream. The stream must have been initialized with a communicator.
     *
     * @param <T> the proxy type
     * @param cast the uncheckedCast function to call on the unmarshaled proxy to obtain the correct
     *     proxy type
     * @return The extracted proxy.
     */
    public <T extends ObjectPrx> T readProxy(Function<ObjectPrx, T> cast) {
        return cast.apply(readProxy());
    }

    /**
     * Extracts an optional proxy from the stream. The stream must have been initialized with a
     * communicator.
     *
     * @param tag The numeric tag associated with the value.
     * @return The optional value (if any).
     */
    public Optional<ObjectPrx> readProxy(int tag) {
        if (readOptional(tag, OptionalFormat.FSize)) {
            skip(4);
            return Optional.of(readProxy());
        } else {
            return Optional.empty();
        }
    }

    /**
     * Extracts an optional proxy from the stream. The stream must have been initialized with a
     * communicator.
     *
     * @param <T> The proxy type.
     * @param tag The numeric tag associated with the value.
     * @param cast The uncheckedCast function to call on the unmarshaled proxy to obtain the correct
     *     proxy type.
     * @return The optional value (if any).
     */
    public <T extends ObjectPrx> Optional<T> readProxy(
            int tag, Function<ObjectPrx, T> cast) {
        if (readOptional(tag, OptionalFormat.FSize)) {
            skip(4);
            return Optional.of(readProxy(cast));
        } else {
            return Optional.empty();
        }
    }

    /**
     * Read an enumerated value.
     *
     * @param maxValue The maximum enumerator value in the definition.
     * @return The enumerator.
     */
    public int readEnum(int maxValue) {
        if (getEncoding().equals(Util.Encoding_1_0)) {
            if (maxValue < 127) {
                return readByte();
            } else if (maxValue < 32767) {
                return readShort();
            } else {
                return readInt();
            }
        } else {
            return readSize();
        }
    }

    /**
     * Extracts a Slice value from the stream.
     *
     * @param <T> The value type.
     * @param cb The consumer to notify when the extracted instance is available. The stream
     *     extracts Slice values in stages. The Ice run time calls accept on the consumer when the
     *     corresponding instance has been fully unmarshaled.
     * @param cls The type of the Ice.Value to unmarshal.
     */
    public <T extends Value> void readValue(Consumer<T> cb, Class<T> cls) {
        initEncaps();
        _encapsStack.decoder.readValue(
            v -> {
                if (v == null || cls.isInstance(v)) {
                    cb.accept(cls.cast(v));
                } else {
                    Ex.throwUOE(cls, v);
                }
            });
    }

    /**
     * Extracts a Slice value from the stream.
     *
     * @param cb The consumer to notify when the extracted instance is available. The stream
     *     extracts Slice values in stages. The Ice run time calls accept on the consumer when the
     *     corresponding instance has been fully unmarshaled.
     */
    public void readValue(Consumer<Value> cb) {
        readValue(cb, Value.class);
    }

    /**
     * Extracts a user exception from the stream and throws it.
     *
     * @throws UserException The user exception that was unmarshaled.
     */
    public void throwException() throws UserException {
        initEncaps();
        _encapsStack.decoder.throwException();
    }

    private boolean readOptImpl(int readTag, OptionalFormat expectedFormat) {
        if (isEncoding_1_0()) {
            return false; // Optional members aren't supported with the 1.0 encoding.
        }

        while (true) {
            if (_buf.b.position() >= _encapsStack.start + _encapsStack.sz) {
                return false; // End of encapsulation also indicates end of optionals.
            }

            final byte b = readByte();
            final int v = b < 0 ? b + 256 : b;
            if (v == Protocol.OPTIONAL_END_MARKER) {
                _buf.position(_buf.b.position() - 1); // Rewind.
                return false;
            }

            OptionalFormat format = OptionalFormat.valueOf(v & 0x07); // First 3 bits.
            int tag = v >> 3;
            if (tag == 30) {
                tag = readSize();
            }

            if (tag > readTag) {
                int offset = tag < 30 ? 1 : (tag < 255 ? 2 : 6); // Rewind
                _buf.position(_buf.b.position() - offset);
                return false; // No optional data members with the requested tag.
            } else if (tag < readTag) {
                skipOptional(format); // Skip optional data members
            } else {
                if (format != expectedFormat) {
                    throw new MarshalException(
                        "invalid optional data member `" + tag + "': unexpected format");
                }
                return true;
            }
        }
    }

    private void skipOptional(OptionalFormat format) {
        switch (format) {
            case F1 -> skip(1);
            case F2 -> skip(2);
            case F4 -> skip(4);
            case F8 -> skip(8);
            case Size -> skipSize();
            case VSize -> skip(readSize());
            case FSize -> skip(readInt());
            case Class -> throw new MarshalException("cannot skip an optional class");
        }
    }

    private void skipOptionals() {
        //
        // Skip remaining un-read optional members.
        //
        while (true) {
            if (_buf.b.position() >= _encapsStack.start + _encapsStack.sz) {
                return; // End of encapsulation also indicates end of optionals.
            }

            final byte b = readByte();
            final int v = b < 0 ? b + 256 : b;
            if (v == Protocol.OPTIONAL_END_MARKER) {
                return;
            }

            OptionalFormat format = OptionalFormat.valueOf(v & 0x07); // Read first 3 bits.
            if ((v >> 3) == 30) {
                skipSize();
            }
            skipOptional(format);
        }
    }

    /**
     * Skip the given number of bytes.
     *
     * @param size The number of bytes to skip.
     */
    public void skip(int size) {
        if (size < 0 || size > _buf.b.remaining()) {
            throw new MarshalException(END_OF_BUFFER_MESSAGE);
        }
        _buf.position(_buf.b.position() + size);
    }

    /** Skip over a size value. */
    public void skipSize() {
        byte b = readByte();
        if (b == -1) {
            skip(4);
        }
    }

    /**
     * Determines the current position in the stream.
     *
     * @return The current position.
     */
    public int pos() {
        return _buf.b.position();
    }

    /**
     * Sets the current position in the stream.
     *
     * @param n The new position.
     */
    public void pos(int n) {
        _buf.position(n);
    }

    /**
     * Determines the current size of the stream.
     *
     * @return The current size.
     */
    public int size() {
        return _buf.size();
    }

    /**
     * Determines whether the stream is empty.
     *
     * @return True if the internal buffer has no data, false otherwise.
     */
    public boolean isEmpty() {
        return _buf.empty();
    }

    private UserException createUserException(String id) {
        return (UserException) _instance.sliceLoader().newInstance(id);
    }

    private final Instance _instance;
    private Buffer _buf;
    private byte[] _stringBytes; // Reusable array for reading strings.
    private char[] _stringChars; // Reusable array for reading strings.

    private enum SliceType {
        NoSlice,
        ValueSlice,
        ExceptionSlice
    }

    private abstract static class EncapsDecoder {

        protected class PatchEntry {
            public PatchEntry(Consumer<Value> cb, int classGraphDepth) {
                this.cb = cb;
                this.classGraphDepth = classGraphDepth;
            }

            public Consumer<Value> cb;
            public int classGraphDepth;
        }

        EncapsDecoder(InputStream stream, int classGraphDepthMax, SliceLoader sliceLoader) {
            _stream = stream;
            _classGraphDepthMax = classGraphDepthMax;
            _classGraphDepth = 0;
            _sliceLoader = sliceLoader;
            _typeIdIndex = 0;
            _unmarshaledMap = new TreeMap<>();
        }

        abstract void readValue(Consumer<Value> cb);

        abstract void throwException() throws UserException;

        abstract void startInstance(SliceType type);

        abstract SlicedData endInstance();

        abstract void startSlice();

        abstract void endSlice();

        abstract void skipSlice();

        boolean readOptional(int tag, OptionalFormat format) {
            return false;
        }

        void readPendingValues() {}

        protected String readTypeId(boolean isIndex) {
            // Lazy initialization
            if (_typeIdMap == null) {
                _typeIdMap = new TreeMap<>();
            }

            if (isIndex) {
                int index = _stream.readSize();
                String typeId = _typeIdMap.get(index);
                if (typeId == null) {
                    throw new MarshalException(END_OF_BUFFER_MESSAGE);
                }
                return typeId;
            } else {
                String typeId = _stream.readString();
                _typeIdMap.put(++_typeIdIndex, typeId);
                return typeId;
            }
        }

        protected Value newInstance(String typeId) {
            return (Value) _sliceLoader.newInstance(typeId);
        }

        protected void addPatchEntry(int index, Consumer<Value> cb) {
            assert (index > 0);

            //
            // Check if we have already unmarshaled the instance. If that's the case, just invoke
            // the callback and we're done.
            //
            Value obj = _unmarshaledMap.get(index);
            if (obj != null) {
                cb.accept(obj);
                return;
            }

            if (_patchMap == null) { // Lazy initialization
                _patchMap = new TreeMap<>();
            }

            //
            // Add patch entry if the instance isn't unmarshaled yet, the callback will be called
            // when the instance is unmarshaled.
            //
            LinkedList<PatchEntry> l = _patchMap.get(index);
            if (l == null) {
                //
                // We have no outstanding instances to be patched for this index, so make a new
                // entry in the patch map.
                //
                l = new LinkedList<>();
                _patchMap.put(index, l);
            }

            //
            // Append a patch entry for this instance.
            //
            l.add(new PatchEntry(cb, _classGraphDepth));
        }

        protected void unmarshal(int index, Value v) {
            //
            // Add the instance to the map of unmarshaled instances, this must be done before
            // reading the instances (for circular references).
            //
            _unmarshaledMap.put(index, v);

            //
            // Read the instance.
            //
            v._iceRead(_stream);

            if (_patchMap != null) {
                //
                // Patch all instances now that the instance is unmarshaled.
                //
                LinkedList<PatchEntry> l = _patchMap.get(index);
                if (l != null) {
                    assert (!l.isEmpty());

                    //
                    // Patch all pointers that refer to the instance.
                    //
                    for (PatchEntry entry : l) {
                        entry.cb.accept(v);
                    }

                    //
                    // Clear out the patch map for that index -- there is nothing left to patch for
                    // that index for the time being.
                    //
                    _patchMap.remove(index);
                }
            }

            if ((_patchMap == null || _patchMap.isEmpty()) && _valueList == null) {
                v.ice_postUnmarshal();
            } else {
                // Lazy initialization
                if (_valueList == null) {
                    _valueList = new ArrayList<>();
                }
                _valueList.add(v);

                if (_patchMap == null || _patchMap.isEmpty()) {
                    // Iterate over the instance list and invoke ice_postUnmarshal on each instance.
                    // We must do this after all instances have been unmarshaled in order to ensure
                    // that any instance data members have been properly patched.
                    for (Value p : _valueList) {
                        p.ice_postUnmarshal();
                    }
                    _valueList.clear();
                }
            }
        }

        protected final InputStream _stream;
        protected final int _classGraphDepthMax;
        protected int _classGraphDepth;
        protected final SliceLoader _sliceLoader;

        //
        // Encapsulation attributes for value unmarshaling.
        //
        protected TreeMap<Integer, LinkedList<PatchEntry>> _patchMap;
        private TreeMap<Integer, Value> _unmarshaledMap;
        private TreeMap<Integer, String> _typeIdMap;
        private int _typeIdIndex;
        private List<Value> _valueList;
    }

    private static final class EncapsDecoder10 extends EncapsDecoder {
        EncapsDecoder10(
                InputStream stream,
                int classGraphDepthMax,
                SliceLoader sliceLoader) {
            super(stream, classGraphDepthMax, sliceLoader);
            _sliceType = SliceType.NoSlice;
        }

        @Override
        void readValue(Consumer<Value> cb) {
            //
            // Object references are encoded as a negative integer in 1.0.
            //
            int index = _stream.readInt();
            if (index > 0) {
                throw new MarshalException("invalid object id");
            }
            index = -index;

            if (index == 0) {
                cb.accept(null);
            } else {
                addPatchEntry(index, cb);
            }
        }

        @Override
        void throwException() throws UserException {
            assert (_sliceType == SliceType.NoSlice);

            //
            // User exception with the 1.0 encoding start with a boolean flag
            // that indicates whether or not the exception has classes.
            //
            // This allows reading the pending instances even if some part of the exception was
            // sliced.
            //
            boolean usesClasses = _stream.readBool();

            _sliceType = SliceType.ExceptionSlice;
            _skipFirstSlice = false;

            //
            // Read the first slice header.
            //
            startSlice();
            final String mostDerivedId = _typeId;
            while (true) {
                UserException userEx = _stream.createUserException(_typeId);

                //
                // We found the exception.
                //
                if (userEx != null) {
                    userEx._read(_stream);
                    if (usesClasses) {
                        readPendingValues();
                    }
                    throw userEx;

                    // Never reached.
                }

                //
                // Slice off what we don't understand.
                //
                skipSlice();
                try {
                    startSlice();
                } catch (MarshalException ex) {
                    //
                    // An oversight in the 1.0 encoding means there is no marker to indicate the
                    // last slice of an exception. As a result, we just try to read the
                    // next type ID, which raises MarshalException when the input buffer underflows.
                    throw new MarshalException("unknown exception type '" + mostDerivedId + "'");
                }
            }
        }

        @Override
        void startInstance(SliceType sliceType) {
            assert (_sliceType == sliceType);
            _skipFirstSlice = true;
        }

        @Override
        SlicedData endInstance() {
            //
            // Read the Ice::Object slice.
            //
            if (_sliceType == SliceType.ValueSlice) {
                startSlice();
                int sz = _stream.readSize(); // For compatibility with the old AFM.
                if (sz != 0) {
                    throw new MarshalException("invalid Object slice");
                }
                endSlice();
            }

            _sliceType = SliceType.NoSlice;
            return null;
        }

        @Override
        void startSlice() {
            //
            // If first slice, don't read the header, it was already read in
            // readInstance or throwException to find the factory.
            //
            if (_skipFirstSlice) {
                _skipFirstSlice = false;
                return;
            }

            // For class instances, first read the type ID boolean which indicates whether or not
            // the type ID is encoded as a string or as an index.
            //
            // For exceptions, the type ID is always encoded as a string.
            if (_sliceType == SliceType.ValueSlice) {
                boolean isIndex = _stream.readBool();
                _typeId = readTypeId(isIndex);
            } else {
                _typeId = _stream.readString();
            }

            _sliceSize = _stream.readInt();
            if (_sliceSize < 4) {
                throw new MarshalException(END_OF_BUFFER_MESSAGE);
            }
        }

        @Override
        void endSlice() {}

        @Override
        void skipSlice() {
            _stream.traceSkipSlice(_typeId, _sliceType);
            assert (_sliceSize >= 4);
            _stream.skip(_sliceSize - 4);
        }

        @Override
        void readPendingValues() {
            int num;
            do {
                num = _stream.readSize();
                for (int k = num; k > 0; k--) {
                    readInstance();
                }
            } while (num > 0);

            if (_patchMap != null && !_patchMap.isEmpty()) {
                //
                // If any entries remain in the patch map, the sender has sent an index for an
                // object, but failed to supply the object.
                //
                throw new MarshalException("index for class received, but no instance");
            }
        }

        private void readInstance() {
            int index = _stream.readInt();

            if (index <= 0) {
                throw new MarshalException("invalid object id");
            }

            _sliceType = SliceType.ValueSlice;
            _skipFirstSlice = false;

            //
            // Read the first slice header.
            //
            startSlice();
            final String mostDerivedId = _typeId;
            Value v = null;
            while (true) {
                // For the 1.0 encoding, the type ID for the base Object class marks the last slice.
                if (_typeId.equals(Value.ice_staticId())) {
                    throw new MarshalException(
                        "The Slice loader did not find a class for type ID '" + mostDerivedId + "'.");
                }

                v = newInstance(_typeId);

                //
                // We found a factory, we get out of this loop.
                //
                if (v != null) {
                    break;
                }

                //
                // Slice off what we don't understand.
                //
                skipSlice();
                startSlice(); // Read next Slice header for next iteration.
            }

            //
            // Compute the biggest class graph depth of this object. To compute this, we get the
            // class graph depth of each ancestor from the patch map and keep the biggest one.
            //
            _classGraphDepth = 0;
            var l = _patchMap != null ? _patchMap.get(index) : null;
            if (l != null) {
                assert (!l.isEmpty());
                for (PatchEntry entry : l) {
                    if (entry.classGraphDepth > _classGraphDepth) {
                        _classGraphDepth = entry.classGraphDepth;
                    }
                }
            }

            if (++_classGraphDepth > _classGraphDepthMax) {
                throw new MarshalException("maximum class graph depth reached");
            }

            //
            // Unmarshal the instance and add it to the map of unmarshaled instances.
            //
            unmarshal(index, v);
        }

        // Value/exception attributes
        private SliceType _sliceType;
        private boolean _skipFirstSlice;

        // Slice attributes
        private int _sliceSize;
        private String _typeId;
    }

    private static class EncapsDecoder11 extends EncapsDecoder {
        EncapsDecoder11(
                InputStream stream,
                int classGraphDepthMax,
                SliceLoader sliceLoader) {
            super(stream, classGraphDepthMax, sliceLoader);
            _current = null;
            _valueIdIndex = 1;
        }

        @Override
        void readValue(Consumer<Value> cb) {
            int index = _stream.readSize();
            if (index < 0) {
                throw new MarshalException("invalid object id");
            } else if (index == 0) {
                cb.accept(null);
            } else if (_current != null && (_current.sliceFlags & Protocol.FLAG_HAS_INDIRECTION_TABLE) != 0) {
                //
                // When reading a class instance within a slice and there's an indirect instance
                // table, always read an indirect reference
                // that points to an instance from the indirect instance table
                // marshaled at the end of the Slice.
                //
                // Maintain a list of indirect references. Note that the
                // indirect index starts at 1, so we decrement it by one to
                // derive an index into the indirection table that we'll read
                // at the end of the slice.
                //

                // Lazy initialization
                if (_current.indirectPatchList == null) {
                    _current.indirectPatchList = new ArrayDeque<>();
                }
                IndirectPatchEntry e = new IndirectPatchEntry();
                e.index = index - 1;
                e.cb = cb;
                _current.indirectPatchList.push(e);
            } else {
                readInstance(index, cb);
            }
        }

        @Override
        void throwException() throws UserException {
            assert (_current == null);

            push(SliceType.ExceptionSlice);

            //
            // Read the first slice header.
            //
            startSlice();
            final String mostDerivedId = _current.typeId;
            while (true) {
                UserException userEx = _stream.createUserException(_current.typeId);

                //
                // We found the exception.
                //
                if (userEx != null) {
                    userEx._read(_stream);
                    throw userEx;

                    // Never reached.
                }

                //
                // Slice off what we don't understand.
                //
                skipSlice();

                if ((_current.sliceFlags & Protocol.FLAG_IS_LAST_SLICE) != 0) {
                    throw new MarshalException(
                        "cannot unmarshal user exception with type ID '" + mostDerivedId + "'");
                }

                startSlice();
            }
        }

        @Override
        void startInstance(SliceType sliceType) {
            assert (_current.sliceType == sliceType);
            _current.skipFirstSlice = true;
        }

        @Override
        SlicedData endInstance() {
            SlicedData slicedData = readSlicedData();
            if (_current.slices != null) {
                _current.slices.clear();
            }
            if (_current.indirectionTables != null) {
                _current.indirectionTables.clear();
            }
            _current = _current.previous;
            return slicedData;
        }

        @Override
        void startSlice() {
            // If first slice, don't read the header, it was already read in
            // readInstance or throwException to find the factory.
            if (_current.skipFirstSlice) {
                _current.skipFirstSlice = false;
                return;
            }

            _current.sliceFlags = _stream.readByte();

            // Read the type ID, for value slices the type ID is encoded as a string or as an index,
            // for exceptions it's always encoded as a string.
            if (_current.sliceType == SliceType.ValueSlice) {
                // Must be checked 1st!
                if ((_current.sliceFlags & Protocol.FLAG_HAS_TYPE_ID_COMPACT) == Protocol.FLAG_HAS_TYPE_ID_COMPACT) {
                    _current.compactId = _stream.readSize();
                    _current.typeId = String.valueOf(_current.compactId);
                } else if (
                    (_current.sliceFlags & (Protocol.FLAG_HAS_TYPE_ID_INDEX | Protocol.FLAG_HAS_TYPE_ID_STRING)) != 0) {
                    _current.typeId =
                        readTypeId(
                            (_current.sliceFlags & Protocol.FLAG_HAS_TYPE_ID_INDEX) != 0);
                    _current.compactId = -1;
                } else {
                    // Only the most derived slice encodes the type ID for the compact format.
                    _current.typeId = "";
                    _current.compactId = -1;
                }
            } else {
                _current.typeId = _stream.readString();
                _current.compactId = -1;
            }

            // Read the slice size if necessary.
            if ((_current.sliceFlags & Protocol.FLAG_HAS_SLICE_SIZE) != 0) {
                _current.sliceSize = _stream.readInt();
                if (_current.sliceSize < 4) {
                    throw new MarshalException(END_OF_BUFFER_MESSAGE);
                }
            } else {
                _current.sliceSize = 0;
            }
        }

        @Override
        void endSlice() {
            if ((_current.sliceFlags & Protocol.FLAG_HAS_OPTIONAL_MEMBERS) != 0) {
                _stream.skipOptionals();
            }

            //
            // Read the indirection table if one is present and transform the
            // indirect patch list into patch entries with direct references.
            //
            if ((_current.sliceFlags & Protocol.FLAG_HAS_INDIRECTION_TABLE) != 0) {
                //
                // The table is written as a sequence<size> to conserve space.
                //
                int[] indirectionTable = new int[_stream.readAndCheckSeqSize(1)];
                for (int i = 0; i < indirectionTable.length; i++) {
                    indirectionTable[i] = readInstance(_stream.readSize(), null);
                }

                //
                // Sanity checks. If there are optional members, it's possible that not all instance
                // references were read if they are from unknown optional data members.
                //
                if (indirectionTable.length == 0) {
                    throw new MarshalException("empty indirection table");
                }
                if ((_current.indirectPatchList == null || _current.indirectPatchList.isEmpty())
                    && (_current.sliceFlags & Protocol.FLAG_HAS_OPTIONAL_MEMBERS) == 0) {
                    throw new MarshalException("no references to indirection table");
                }

                //
                // Convert indirect references into direct references.
                //
                if (_current.indirectPatchList != null) {
                    for (IndirectPatchEntry e : _current.indirectPatchList) {
                        assert (e.index >= 0);
                        if (e.index >= indirectionTable.length) {
                            throw new MarshalException("indirection out of range");
                        }
                        addPatchEntry(indirectionTable[e.index], e.cb);
                    }
                    _current.indirectPatchList.clear();
                }
            }
        }

        @Override
        void skipSlice() {
            _stream.traceSkipSlice(_current.typeId, _current.sliceType);

            int start = _stream.pos();

            if ((_current.sliceFlags & Protocol.FLAG_HAS_SLICE_SIZE) != 0) {
                assert (_current.sliceSize >= 4);
                _stream.skip(_current.sliceSize - 4);
            } else {
                if (_current.sliceType == SliceType.ValueSlice) {
                    throw new MarshalException(
                        "The Slice loader did not find a class for type ID '"
                            + _current.typeId
                            + "' and compact format prevents slicing.");
                } else {
                    throw new MarshalException(
                        "The Slice loader did not find a user exception class for type ID '"
                            + _current.typeId
                            + "' and compact format prevents slicing.");
                }
            }

            //
            // Preserve this slice if unmarshaling a value in Slice format. Exception slices are not
            // preserved.
            //
            if (_current.sliceType == SliceType.ValueSlice) {
                boolean hasOptionalMembers =
                    (_current.sliceFlags & Protocol.FLAG_HAS_OPTIONAL_MEMBERS) != 0;

                Buffer buffer = _stream.getBuffer();
                final int end = buffer.b.position();
                int dataEnd = end;
                if (hasOptionalMembers) {
                    //
                    // Don't include the optional member end marker. It will be re-written by
                    // endSlice when the sliced data is re-written.
                    //
                    --dataEnd;
                }
                var bytes = new byte[dataEnd - start];
                buffer.position(start);
                buffer.b.get(bytes);
                buffer.position(end);

                var info =
                    new SliceInfo(
                        _current.compactId == -1 ? _current.typeId : "",
                        _current.compactId,
                        bytes,
                        hasOptionalMembers,
                        (_current.sliceFlags & Protocol.FLAG_IS_LAST_SLICE) != 0);

                // Lazy initialization
                if (_current.slices == null) {
                    _current.slices = new ArrayList<>();
                }
                _current.slices.add(info);
            }

            // Lazy initialization
            if (_current.indirectionTables == null) {
                _current.indirectionTables = new ArrayList<>();
            }

            //
            // Read the indirect instance table. We read the instances or their IDs if the instance
            // is a reference to an already unmarshaled instance.
            //
            if ((_current.sliceFlags & Protocol.FLAG_HAS_INDIRECTION_TABLE) != 0) {
                int[] indirectionTable = new int[_stream.readAndCheckSeqSize(1)];
                for (int i = 0; i < indirectionTable.length; i++) {
                    indirectionTable[i] = readInstance(_stream.readSize(), null);
                }
                _current.indirectionTables.add(indirectionTable);
            } else {
                _current.indirectionTables.add(null);
            }
        }

        @Override
        boolean readOptional(int readTag, OptionalFormat expectedFormat) {
            if (_current == null) {
                return _stream.readOptImpl(readTag, expectedFormat);
            } else if ((_current.sliceFlags & Protocol.FLAG_HAS_OPTIONAL_MEMBERS) != 0) {
                return _stream.readOptImpl(readTag, expectedFormat);
            }
            return false;
        }

        private int readInstance(int index, Consumer<Value> cb) {
            assert (index > 0);

            if (index > 1) {
                if (cb != null) {
                    addPatchEntry(index, cb);
                }
                return index;
            }

            push(SliceType.ValueSlice);

            //
            // Get the instance ID before we start reading slices. If some slices are skipped, the
            // indirect instance table is still read and might read other instances.
            //
            index = ++_valueIdIndex;

            //
            // Read the first slice header.
            //
            startSlice();
            final String mostDerivedId = _current.typeId;
            Value v = null;
            while (true) {
                if (!_current.typeId.isEmpty()) { // we can read an empty typeId with the compact format
                    v = newInstance(_current.typeId);
                    if (v != null) {
                        break;
                    }
                }

                // Slice off what we don't understand.
                skipSlice();

                //
                // If this is the last slice, keep the instance as an opaque
                // UnknownSlicedValue object.
                //
                if ((_current.sliceFlags & Protocol.FLAG_IS_LAST_SLICE) != 0) {
                    //
                    // Provide a factory with an opportunity to supply the instance. We pass the
                    // "::Ice::Object" ID to indicate that this is the last chance to preserve the
                    // instance.
                    //
                    v = newInstance(Value.ice_staticId());
                    if (v == null) {
                        v = new UnknownSlicedValue(mostDerivedId);
                    }

                    break;
                }

                startSlice(); // Read next Slice header for next iteration.
            }

            if (++_classGraphDepth > _classGraphDepthMax) {
                throw new MarshalException("maximum class graph depth reached");
            }

            //
            // Unmarshal the instance.
            //
            unmarshal(index, v);

            --_classGraphDepth;

            if (_current == null && _patchMap != null && !_patchMap.isEmpty()) {
                //
                // If any entries remain in the patch map, the sender has sent an index for an
                // instance, but failed to supply the instance.
                //
                throw new MarshalException("index for class received, but no instance");
            }

            if (cb != null) {
                cb.accept(v);
            }

            return index;
        }

        private SlicedData readSlicedData() {
            // No preserved slices.
            if (_current.slices == null) {
                return null;
            }

            // The _indirectionTables member holds the indirection table for each slice in _slices.
            assert (_current.slices.size() == _current.indirectionTables.size());
            for (int n = 0; n < _current.slices.size(); n++) {
                // We use the "instances" list in SliceInfo to hold references to the target instances.
                // Note that the instances might not have been read yet in the case of a circular reference
                // to an enclosing instance.
                final int[] table = _current.indirectionTables.get(n);
                SliceInfo info = _current.slices.get(n);
                info.instances = new Value[table != null ? table.length : 0];
                for (int j = 0; j < info.instances.length; j++) {
                    final int k = j;
                    addPatchEntry(table[j], v -> info.instances[k] = v);
                }
            }

            SliceInfo[] arr = new SliceInfo[_current.slices.size()];
            _current.slices.toArray(arr);
            return new SlicedData(arr);
        }

        private void push(SliceType sliceType) {
            if (_current == null) {
                _current = new InstanceData(null);
            } else {
                _current = _current.next == null ? new InstanceData(_current) : _current.next;
            }
            _current.sliceType = sliceType;
            _current.skipFirstSlice = false;
        }

        private static final class IndirectPatchEntry {
            int index;
            Consumer<Value> cb;
        }

        private static final class InstanceData {
            InstanceData(InstanceData previous) {
                if (previous != null) {
                    previous.next = this;
                }
                this.previous = previous;
                this.next = null;
            }

            // Instance attributes
            SliceType sliceType;
            boolean skipFirstSlice;
            List<SliceInfo> slices; // Preserved slices.
            List<int[]> indirectionTables;

            // Slice attributes
            byte sliceFlags;
            int sliceSize;
            String typeId;
            int compactId;
            Deque<IndirectPatchEntry> indirectPatchList;

            final InstanceData previous;
            InstanceData next;
        }

        private InstanceData _current;
        private int _valueIdIndex; // The ID of the next instance to unmarshal.
    }

    private static final class Encaps {
        void reset() {
            decoder = null;
        }

        void setEncoding(EncodingVersion encoding) {
            this.encoding = encoding;
            encoding_1_0 = encoding.equals(Util.Encoding_1_0);
        }

        int start;
        int sz;
        EncodingVersion encoding;
        boolean encoding_1_0;

        EncapsDecoder decoder;

        Encaps next;
    }

    //
    // The encoding version to use when there's no encapsulation to read from. This is for example
    // used to read message headers.
    //
    private EncodingVersion _encoding;

    private boolean isEncoding_1_0() {
        return _encapsStack != null
            ? _encapsStack.encoding_1_0
            : _encoding.equals(Util.Encoding_1_0);
    }

    private Encaps _encapsStack;
    private Encaps _encapsCache;

    private void initEncaps() {
        // Lazy initialization
        if (_encapsStack == null) {
            _encapsStack = _encapsCache;
            if (_encapsStack != null) {
                _encapsCache = _encapsCache.next;
            } else {
                _encapsStack = new Encaps();
            }
            _encapsStack.setEncoding(_encoding);
            _encapsStack.sz = _buf.b.limit();
        }

        // Lazy initialization.
        if (_encapsStack.decoder == null) {
            if (_encapsStack.encoding_1_0) {
                _encapsStack.decoder =
                    new EncapsDecoder10(
                        this, _classGraphDepthMax, _instance.sliceLoader());
            } else {
                _encapsStack.decoder =
                    new EncapsDecoder11(
                        this, _classGraphDepthMax, _instance.sliceLoader());
            }
        }
    }

    private void traceSkipSlice(String typeId, SliceType sliceType) {
        if (_instance.traceLevels().slicing > 0) {
            TraceUtil.traceSlicing(
                sliceType == SliceType.ExceptionSlice ? "exception" : "object",
                typeId,
                "Slicing",
                _instance.initializationData().logger);
        }
    }

    /**
     * We need to override the resolveClass method of ObjectInputStream so that we can use the same
     * class-lookup mechanism as elsewhere in the Ice run time.
     */
    private class ObjectInputStreamWrapper extends java.io.ObjectInputStream {
        public ObjectInputStreamWrapper(Instance instance, java.io.InputStream in)
            throws IOException {
            super(in);
            _instance = instance;
        }

        @Override
        protected Class<?> resolveClass(ObjectStreamClass cls)
            throws IOException, ClassNotFoundException {

            try {
                Class<?> c = _instance.findClass(cls.getName());
                if (c != null) {
                    return c;
                }
                throw new ClassNotFoundException("unable to resolve class" + cls.getName());
            } catch (Exception ex) {
                throw new ClassNotFoundException("unable to resolve class " + cls.getName(), ex);
            }
        }

        private final Instance _instance;
    }

    private final int _classGraphDepthMax;

    private int _startSeq = -1;
    private int _minSeqSize;

    private static final String END_OF_BUFFER_MESSAGE =
        "Attempting to unmarshal past the end of the buffer.";
}