ValidatingObjectInputStream.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   https://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 org.apache.commons.io.serialization;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.io.build.AbstractStreamBuilder;
import org.apache.commons.io.input.BoundedInputStream;

/**
 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
 *
 * <p>
 * Various accept/reject methods allow for specifying which classes can be deserialized.
 * </p>
 * <h2>Deserializing with class restrictions</h2>
 * <p>
 * This example restricts deserialization to the classes used by a HashMap of String keys and Integer values. Class restrictions alone do not bound
 * resource use; see the resource limits below:
 * </p>
 *
 * <pre>{@code
 * // Defining Object fixture
 * final HashMap<String, Integer> map1 = new HashMap<>();
 * map1.put("1", 1);
 * // Writing serialized fixture
 * final byte[] byteArray;
 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
 *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
 *     oos.writeObject(map1);
 *     oos.flush();
 *     byteArray = baos.toByteArray();
 * }
 * // Deserializing
 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
 *             .accept(HashMap.class, Number.class, Integer.class)
 *             .setInputStream(bais)
 *             .get()) {
 *     // String.class is automatically accepted
 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
 *     assertEquals(map1, map2);
 * }
 * // Reusing a configuration
 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
 *     .accept(HashMap.class, Number.class, Integer.class);
 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
 *             .setPredicate(predicate)
 *             .setInputStream(bais)
 *             .get()) {
 *     // String.class is automatically accepted
 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
 *     assertEquals(map1, map2);
 * }
 * }</pre>
 * <p>
 * This design was inspired by a <a href="https://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
 * </p>
 * <h2>Deserializing with a size boundary</h2>
 * <p>
 * You can further guard your application against untrusted input by limiting how much data to process using a {@link BoundedInputStream}.
 * Note that a byte-count bound alone does not bound allocation, nesting depth, or reference count claimed by the bytes already read; see
 * <em>Deserializing with resource limits</em> below. For example:
 * </p>
 * <pre>{@code
 * // Deserializing with a size limit successfully
 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
 *             .accept(HashMap.class, Number.class, Integer.class)
 *             .setInputStream(BoundedInputStream.builder()
 *                 .setMaxCount(10_000)
 *                 .setOnMaxCount((max, count) -> {
 *                     throw new IllegalArgumentException("Input exceeds limit.");
 *                 })
 *                 .setInputStream(bais)
 *                 .get())
 *             .get()) {
 *     // String.class is automatically accepted
 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
 *     assertEquals(map1, map2);
 * }
 * // Deserializing with a size limit reaching the limit
 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
 *             .accept(HashMap.class, Number.class, Integer.class)
 *             .setInputStream(BoundedInputStream.builder()
 *                 .setMaxCount(10)
 *                 .setOnMaxCount((max, count) -> {
 *                     throw new IllegalArgumentException("Input exceeds limit.");
 *                 })
 *                 .setInputStream(bais)
 *                 .get())
 *             .get()) {
 *     // String.class is automatically accepted
 *     final IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> vois.readObject());
 *     assertEquals("Input exceeds limit.", e.getMessage());
 * }
 * }</pre>
 * <h2>Deserializing with resource limits</h2>
 * <p>
 * Validating class names, or bounding the input byte count as above, does <em>not</em> bound resources that a serialized stream can claim without actually
 * sending the corresponding bytes: a stream of a few dozen bytes can declare a huge array (which is allocated <em>before</em> its contents are read), nest
 * accepted containers deeply enough to exhaust the call stack with a {@link StackOverflowError}, or multiply the object graph through internal references.
 * To also bound these resources, set the <a href="https://openjdk.org/jeps/290">JEP 290</a>-style limits {@link Builder#setMaxDepth(long)},
 * {@link Builder#setMaxArrayLength(long)} and {@link Builder#setMaxRefs(long)}, which this class enforces through the platform {@code ObjectInputFilter}:
 * </p>
 * <pre>{@code
 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
 *             .accept(HashMap.class, Number.class, Integer.class)
 *             .setMaxDepth(100)          // maximum nesting depth of the object graph
 *             .setMaxArrayLength(10_000) // maximum declared array length
 *             .setMaxRefs(10_000)        // maximum number of internal object references
 *             .setInputStream(bais)
 *             .get()) {
 *     // String.class is automatically accepted
 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
 *     assertEquals(map1, map2);
 * }
 * }</pre>
 * <p>
 * For streams that may be crafted by an adversary, combine all three guards: an accept list, a {@link BoundedInputStream} byte bound, and the resource limits
 * above. These limits do not guarantee safe deserialization of arbitrary untrusted data. Existing stream filter rejections are preserved; failures
 * to install the limits are propagated to the caller.
 * </p>
 * @since 2.5
 */
public class ValidatingObjectInputStream extends ObjectInputStream {

    // @formatter:off
    /**
     * Builds a new {@link ValidatingObjectInputStream}.
     *
     * <h2>Using NIO</h2>
     * <pre>{@code
     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
     *   .setPath(Paths.get("MyFile.ser"))
     *   .get();}
     * </pre>
     * <h2>Using IO</h2>
     * <pre>{@code
     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
     *   .setFile(new File("MyFile.ser"))
     *   .get();}
     * </pre>
     *
     * @see #get()
     * @since 2.18.0
     */
    // @formatter:on
    public static class Builder extends AbstractStreamBuilder<ValidatingObjectInputStream, Builder> {

        private ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate();

        private boolean strict;

        private long maxDepth = -1;

        private long maxArrayLength = -1;

        private long maxRefs = -1;

        /**
         * Constructs a new builder of {@link ValidatingObjectInputStream}.
         *
         * @deprecated Use {@link #builder()}.
         */
        @Deprecated
        public Builder() {
            // empty
        }

        /**
         * Accepts the specified classes for deserialization, unless they are otherwise rejected.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * <p>
         * Classes are matched by their exact names. Array classes must be accepted separately from their element classes.
         * For example, accepting {@code Foo.class} does not accept {@code Foo[].class}.
         * </p>
         *
         * @param classes Classes to accept.
         * @return this object.
         * @since 2.18.0
         */
        public Builder accept(final Class<?>... classes) {
            predicate.accept(classes);
            return this;
        }

        /**
         * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * @param matcher A class name matcher to <em>accept</em> objects.
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder accept(final ClassNameMatcher matcher) {
            predicate.accept(matcher);
            return this;
        }

        /**
         * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * <p>
         * The pattern must match the entire class name, as defined by {@link java.util.regex.Matcher#matches()}.
         * For example, {@code foo\.Bar} matches {@code foo.Bar}, but not {@code foo.BarExtra}. Use {@code .*foo\.Bar.*} to match a substring.
         * </p>
         *
         * @param pattern A Pattern for compiled regular expression.
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder accept(final Pattern pattern) {
            predicate.accept(pattern);
            return this;
        }

        /**
         * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * <p>
         * Matching is case-sensitive. The wildcard {@code *} matches zero or more characters and {@code ?} matches one character, including {@code '.'} in both
         * cases. Unlike <a href="https://openjdk.org/jeps/290">JEP 290</a> serialization filter patterns, these patterns do not treat package boundaries
         * specially: {@code com.example.*} matches classes in {@code com.example} and all its subpackages, and {@code **} is equivalent to {@code *}.
         * </p>
         *
         * <p>
         * Use {@link #accept(Pattern)} with a regular expression to match only classes directly in a package.
         * </p>
         *
         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
         *                 FilenameUtils.wildcardMatch}
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder accept(final String... patterns) {
            predicate.accept(patterns);
            return this;
        }

        /**
         * Builds a new {@link ValidatingObjectInputStream}.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         * <p>
         * You must set an aspect that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception.
         * </p>
         * <p>
         * This builder uses the following aspects:
         * </p>
         * <ul>
         * <li>{@link #getInputStream()} gets the target aspect.</li>
         * <li>predicate</li>
         * <li>charsetDecoder</li>
         * <li>writeImmediately</li>
         * </ul>
         *
         * @return A new instance.
         * @throws UnsupportedOperationException if the origin cannot provide a {@link InputStream}.
         * @throws IOException                   if an I/O error occurs converting to an {@link InputStream} using {@link #getInputStream()}.
         * @see #getWriter()
         * @see #getUnchecked()
         */
        @Override
        public ValidatingObjectInputStream get() throws IOException {
            return new ValidatingObjectInputStream(this);
        }

        /**
         * Gets the predicate.
         *
         * @return The predicate.
         * @since 2.18.0
         */
        public ObjectStreamClassPredicate getPredicate() {
            return predicate;
        }

        /**
         * Rejects the specified classes for deserialization, even if they are otherwise accepted.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * <p>
         * Classes are matched by their exact names. Rejecting {@code Foo.class} does not reject {@code Foo[].class}, so an otherwise accepted
         * empty or null-only array can still be deserialized. Element class descriptors encountered while reading array contents are checked separately.
         * To reject the array class itself, also reject {@code Foo[].class}.
         * </p>
         *
         * @param classes Classes to reject.
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder reject(final Class<?>... classes) {
            predicate.reject(classes);
            return this;
        }

        /**
         * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * @param matcher The matcher to use.
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder reject(final ClassNameMatcher matcher) {
            predicate.reject(matcher);
            return this;
        }

        /**
         * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * <p>
         * The pattern must match the entire class name, as defined by {@link java.util.regex.Matcher#matches()}.
         * For example, {@code foo\.Bar} matches {@code foo.Bar}, but not {@code foo.BarExtra}. Use {@code .*foo\.Bar.*} to match a substring.
         * </p>
         *
         * @param pattern standard Java regexp.
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder reject(final Pattern pattern) {
            predicate.reject(pattern);
            return this;
        }

        /**
         * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * <p>Matching is case-sensitive. The wildcard {@code *} matches zero or more characters and {@code ?} matches one character, including
         * {@code '.'} in both cases. Unlike <a href="https://openjdk.org/jeps/290">JEP 290</a> serialization filter patterns, these patterns do not
         * treat package boundaries specially: {@code com.example.*} matches classes in {@code com.example} and all its subpackages, and
         * {@code **} is equivalent to {@code *}.</p>
         *
         * <p>Use {@link #reject(Pattern)} with a regular expression to match only classes directly in a package.</p>
         *
         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
         *                 FilenameUtils.wildcardMatch}
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder reject(final String... patterns) {
            predicate.reject(patterns);
            return this;
        }

        /**
         * Sets the maximum declared length accepted for any array in the stream, like the <a href="https://openjdk.org/jeps/290">JEP 290</a>
         * {@code maxarray} limit.
         * <p>
         * An array's length is declared in the stream and the array is allocated <em>before</em> its contents are read, so without this limit a stream of a
         * few dozen bytes can force a ~2 GiB allocation regardless of any {@link BoundedInputStream} byte bound. Enforced through the platform
         * {@code ObjectInputFilter} (Java 8u121 and up); building throws {@link UnsupportedOperationException} on older JVMs where it cannot be enforced.
         * </p>
         *
         * @param maxArrayLength The maximum declared array length, a negative value disables the limit (the default).
         * @return {@code this} instance.
         * @since 2.23.0
         */
        public Builder setMaxArrayLength(final long maxArrayLength) {
            this.maxArrayLength = maxArrayLength;
            return this;
        }

        /**
         * Sets the maximum nesting depth accepted for the object graph, like the <a href="https://openjdk.org/jeps/290">JEP 290</a> {@code maxdepth} limit.
         * <p>
         * Deserialization is recursive: deeply nested containers exhaust the call stack with a {@link StackOverflowError}, an {@link Error} that typical
         * {@link IOException} handling does not catch. Enforced through the platform {@code ObjectInputFilter} (Java 8u121 and up); building throws
         * {@link UnsupportedOperationException} on older JVMs where it cannot be enforced.
         * </p>
         *
         * @param maxDepth The maximum object graph depth, a negative value disables the limit (the default).
         * @return {@code this} instance.
         * @since 2.23.0
         */
        public Builder setMaxDepth(final long maxDepth) {
            this.maxDepth = maxDepth;
            return this;
        }

        /**
         * Sets the maximum number of internal object references accepted in the stream, like the <a href="https://openjdk.org/jeps/290">JEP 290</a>
         * {@code maxrefs} limit; together with {@link #setMaxArrayLength(long)} this also bounds {@code TC_REFERENCE} back-reference amplification.
         * <p>
         * Enforced through the platform {@code ObjectInputFilter} (Java 8u121 and up); building throws {@link UnsupportedOperationException} on older JVMs
         * where it cannot be enforced. This count includes back-references, rather than only distinct objects.
         * </p>
         *
         * @param maxRefs The maximum number of object references, a negative value disables the limit (the default).
         * @return {@code this} instance.
         * @since 2.23.0
         */
        public Builder setMaxRefs(final long maxRefs) {
            this.maxRefs = maxRefs;
            return this;
        }

        /**
         * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate.
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * @param predicate The predicate.
         * @return {@code this} instance.
         * @since 2.18.0
         */
        public Builder setPredicate(final ObjectStreamClassPredicate predicate) {
            this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate();
            return this;
        }

        /**
         * Sets whether to validate interfaces and runtime-visible annotations recursively.
         * <p>
         * For compatibility with previous versions, this is false by default.
         * </p>
         * <p>
         * Checks:
         * </p>
         * <ol>
         * <li>The type name.</li>
         * <li>The interfaces implemented by the class, recursively.</li>
         * <li>The runtime-visible annotations on this type, recursively.</li>
         * </ol>
         * <p>
         * The reject list takes precedence over the accept list.
         * </p>
         *
         * @param strict If true, checks that all interfaces and annotations for a given type are not rejected and accepted.
         * @return {@code this} instance.
         * @since 2.23.0
         */
        public Builder setStrict(final boolean strict) {
            this.strict = strict;
            return this;
        }

    }

    /**
     * Constructs a new {@link Builder}.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * @return A new {@link Builder}.
     * @since 2.18.0
     */
    public static Builder builder() {
        return new Builder();
    }

    private static Object invokeFilter(final Method method, final Object filter, final Object[] args) throws Throwable {
        try {
            return method.invoke(filter, args);
        } catch (final InvocationTargetException e) {
            throw e.getCause();
        }
    }

    /**
     * Installs resource limits while preserving rejections from the stream's existing filter.
     *
     * @param stream the stream to configure.
     * @param pattern the resource limit pattern.
     * @param loader the loader used to discover the platform filter API.
     * @throws IOException if the filter API cannot be accessed.
     */
    static void setObjectInputFilter(final ObjectInputStream stream, final String pattern, final ClassLoader loader) throws IOException {
        final Class<?> filterClass;
        boolean legacy = false;
        Class<?> discovered;
        try {
            discovered = Class.forName("java.io.ObjectInputFilter", true, loader);
        } catch (final ClassNotFoundException absent) {
            try {
                discovered = Class.forName("sun.misc.ObjectInputFilter", true, loader);
                legacy = true;
            } catch (final ClassNotFoundException unavailable) {
                throw new UnsupportedOperationException("Resource limits require ObjectInputFilter (JEP 290, Java 8u121+)", unavailable);
            }
        }
        filterClass = discovered;
        try {
            final Class<?> configClass = Class.forName(filterClass.getName() + "$Config", true, loader);
            final Object limits = configClass.getMethod("createFilter", String.class).invoke(null, pattern);
            final Object current = legacy
                    ? configClass.getMethod("getObjectInputFilter", ObjectInputStream.class).invoke(null, stream)
                    : ObjectInputStream.class.getMethod("getObjectInputFilter").invoke(stream);
            final Object combined;
            if (current == null) {
                combined = limits;
            } else {
                final Class<?> infoClass = Class.forName(filterClass.getName() + "$FilterInfo", true, loader);
                final Method checkInput = filterClass.getMethod("checkInput", infoClass);
                combined = Proxy.newProxyInstance(filterClass.getClassLoader(), new Class<?>[] {filterClass}, (proxy, method, args) -> {
                    if (method.getDeclaringClass() == Object.class) {
                        switch (method.getName()) {
                        case "equals":
                            return proxy == args[0];
                        case "hashCode":
                            return System.identityHashCode(proxy);
                        default:
                            return "Resource limits combined with existing deserialization filter";
                        }
                    }
                    final Object existingStatus = invokeFilter(checkInput, current, args);
                    if (existingStatus == null || "REJECTED".equals(existingStatus.toString())) {
                        return existingStatus;
                    }
                    final Object limitStatus = invokeFilter(checkInput, limits, args);
                    return "UNDECIDED".equals(limitStatus.toString()) ? existingStatus : limitStatus;
                });
            }
            if (legacy) {
                configClass.getMethod("setObjectInputFilter", ObjectInputStream.class, filterClass).invoke(null, stream, combined);
            } else {
                ObjectInputStream.class.getMethod("setObjectInputFilter", filterClass).invoke(stream, combined);
            }
        } catch (final InvocationTargetException e) {
            final Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new IOException("Cannot install deserialization resource limits", cause);
        } catch (final ReflectiveOperationException e) {
            throw new IOException("Cannot access deserialization filter API", e);
        }
    }

    private final ObjectStreamClassPredicate predicate;
    private final boolean strict;

    /**
     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
     * deserialized, as by default no classes are accepted.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * @param builder     The builder.
     */
    @SuppressWarnings("resource") // caller closes
    private ValidatingObjectInputStream(final Builder builder) throws IOException {
        super(builder.getInputStream());
        this.predicate = builder.predicate;
        this.strict = builder.strict;
        final StringBuilder pattern = new StringBuilder();
        if (builder.maxDepth >= 0) {
            pattern.append("maxdepth=").append(builder.maxDepth).append(';');
        }
        if (builder.maxArrayLength >= 0) {
            pattern.append("maxarray=").append(builder.maxArrayLength).append(';');
        }
        if (builder.maxRefs >= 0) {
            pattern.append("maxrefs=").append(builder.maxRefs).append(';');
        }
        if (pattern.length() != 0) {
            setObjectInputFilter(this, pattern.toString(), ValidatingObjectInputStream.class.getClassLoader());
        }
    }

    /**
     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
     * deserialized, as by default no classes are accepted.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * @param input An input stream.
     * @throws IOException Thrown if an I/O error occurs while reading stream header.
     * @deprecated Use {@link #builder()}.
     */
    @Deprecated
    public ValidatingObjectInputStream(final InputStream input) throws IOException {
        this(builder().setInputStream(input).setPredicate(new ObjectStreamClassPredicate()));
    }

    /**
     * Accepts the specified classes for deserialization, unless they are otherwise rejected.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * <p>
     * Classes are matched by their exact names. Array classes must be accepted separately from their element classes.
     * For example, accepting {@code Foo.class} does not accept {@code Foo[].class}.
     * </p>
     *
     * @param classes Classes to accept.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream accept(final Class<?>... classes) {
        predicate.accept(classes);
        return this;
    }

    /**
     * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * @param matcher A class name matcher to <em>accept</em> objects.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
        predicate.accept(matcher);
        return this;
    }

    /**
     * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * <p>
     * The pattern must match the entire class name, as defined by {@link java.util.regex.Matcher#matches()}.
     * For example, {@code foo\.Bar} matches {@code foo.Bar}, but not {@code foo.BarExtra}. Use {@code .*foo\.Bar.*} to match a substring.
     * </p>
     *
     * @param pattern A Pattern for compiled regular expression.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream accept(final Pattern pattern) {
        predicate.accept(pattern);
        return this;
    }

    /**
     * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * <p>
     * Matching is case-sensitive. The wildcard {@code *} matches zero or more characters and {@code ?} matches one character, including {@code '.'} in both
     * cases. Unlike <a href="https://openjdk.org/jeps/290">JEP 290</a> serialization filter patterns, these patterns do not treat package boundaries specially:
     * {@code com.example.*} matches classes in {@code com.example} and all its subpackages, and {@code **} is equivalent to {@code *}.
     * </p>
     *
     * <p>
     * Use {@link #accept(Pattern)} with a regular expression to match only classes directly in a package.
     * </p>
     *
     * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
     *                 FilenameUtils.wildcardMatch}.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream accept(final String... patterns) {
        predicate.accept(patterns);
        return this;
    }

    /**
     * Checks that the given type can be legally resolved as configured.
     * <p>
     * Checks the type name, its interfaces, and its runtime-visible annotations recursively. Each type is visited once per traversal to avoid
     * annotation cycles.
     * </p>
     *
     * @param result The type to check.
     * @throws InvalidClassException Thrown when a rejected or non-accepted type is found.
     */
    private void check(final Class<?> result) throws InvalidClassException {
        check(result, new HashSet<>());
    }

    /**
     * Checks a type and recursively checks its interfaces and runtime-visible annotations.
     *
     * @param result The type to check.
     * @param visited The types already visited in this traversal.
     * @throws InvalidClassException Thrown when a rejected or non-accepted type is found.
     */
    private void check(final Class<?> result, final Set<Class<?>> visited) throws InvalidClassException {
        if (!visited.add(result)) {
            return;
        }
        checkTypeName(result.getName());
        for (final Class<?> iface : result.getInterfaces()) {
            check(iface, visited);
        }
        for (final Annotation annotation : result.getAnnotations()) {
            check(annotation.annotationType(), visited);
        }
    }

    /**
     * Checks that the type name conforms to requirements.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * @param name The type name to test.
     * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
     */
    private void checkTypeName(final String name) throws InvalidClassException {
        if (!predicate.test(name)) {
            invalidClassNameFound(name);
        }
    }

    /**
     * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
     * names.
     *
     * @param className name of the invalid class.
     * @throws InvalidClassException Thrown with a message containing the class name.
     */
    protected void invalidClassNameFound(final String className) throws InvalidClassException {
        throw new InvalidClassException("Class name not accepted: " + className);
    }

    /**
     * Delegates to {@link #readObject()} and casts to the generic {@code T}.
     *
     * @param <T> The return type.
     * @return Result from {@link #readObject()}.
     * @throws ClassNotFoundException Thrown by {@link #readObject()}.
     * @throws IOException            Thrown by {@link #readObject()}.
     * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
     * @since 2.18.0
     */
    @SuppressWarnings("unchecked")
    public <T> T readObjectCast() throws ClassNotFoundException, IOException {
        return (T) super.readObject();
    }

    /**
     * Rejects the specified classes for deserialization, even if they are otherwise accepted.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * <p>
     * Classes are matched by their exact names. Rejecting {@code Foo.class} does not reject {@code Foo[].class}, so an otherwise accepted
     * empty or null-only array can still be deserialized. Element class descriptors encountered while reading array contents are checked separately.
     * To reject the array class itself, also reject {@code Foo[].class}.
     * </p>
     *
     * @param classes Classes to reject.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream reject(final Class<?>... classes) {
        predicate.reject(classes);
        return this;
    }

    /**
     * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * @param matcher A class name matcher to <em>reject</em> objects.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
        predicate.reject(matcher);
        return this;
    }

    /**
     * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * <p>
     * The pattern must match the entire class name, as defined by {@link java.util.regex.Matcher#matches()}.
     * For example, {@code foo\.Bar} matches {@code foo.Bar}, but not {@code foo.BarExtra}. Use {@code .*foo\.Bar.*} to match a substring.
     * </p>
     *
     * @param pattern A Pattern for compiled regular expression.
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream reject(final Pattern pattern) {
        predicate.reject(pattern);
        return this;
    }

    /**
     * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     *
     * <p>
     * Matching is case-sensitive. The wildcard {@code *} matches zero or more characters and {@code ?} matches one character, including {@code '.'} in both
     * cases. Unlike <a href="https://openjdk.org/jeps/290">JEP 290</a> serialization filter patterns, these patterns do not treat package boundaries specially:
     * {@code com.example.*} matches classes in {@code com.example} and all its subpackages, and {@code **} is equivalent to {@code *}.
     * </p>
     *
     * <p>
     * Use {@link #reject(Pattern)} with a regular expression to match only classes directly in a package.
     * </p>
     *
     * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
     *                 FilenameUtils.wildcardMatch}
     * @return {@code this} instance.
     */
    public ValidatingObjectInputStream reject(final String... patterns) {
        predicate.reject(patterns);
        return this;
    }

    /**
     * Checks that the given object's class name conforms to requirements and if so delegates to the superclass.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     */
    @Override
    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
        checkTypeName(osc.getName());
        // resolveClass() calls Class.forName(String, boolean, ClassLoader) with initialize set to false.
        // The result Class is therefore not initialized.
        final Class<?> result = super.resolveClass(osc);
        if (strict) {
            check(result);
        }
        return result;
    }

    /**
     * Checks that the given names conform to requirements and if so delegates to the superclass.
     * <p>
     * The reject list takes precedence over the accept list.
     * </p>
     * <p>
     * In strict mode, each interface of the resolved proxy class is checked recursively, including its super-interfaces and runtime-visible annotations,
     * as described by {@link Builder#setStrict(boolean)}. The generated proxy class name is not checked. Without strict mode, only the interface names
     * supplied by the stream are checked.
     * </p>
     */
    @Override
    protected Class<?> resolveProxyClass(final String[] interfaces) throws IOException, ClassNotFoundException {
        for (final String interfaceName : interfaces) {
            checkTypeName(interfaceName);
        }
        final Class<?> result = super.resolveProxyClass(interfaces);
        if (strict) {
            // The proxy class itself has a synthetic name; validate its interfaces recursively,
            // exactly like resolveClass() validates a resolved class in strict mode.
            final Set<Class<?>> visited = new HashSet<>();
            for (final Class<?> iface : result.getInterfaces()) {
                check(iface, visited);
            }
        }
        return result;
    }
}