001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.io.serialization;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InvalidClassException;
024import java.io.ObjectInputStream;
025import java.io.ObjectStreamClass;
026import java.util.regex.Pattern;
027
028import org.apache.commons.io.build.AbstractStreamBuilder;
029
030/**
031 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
032 *
033 * <p>
034 * Various accept/reject methods allow for specifying which classes can be deserialized.
035 * </p>
036 * <h2>Reading safely</h2>
037 * <p>
038 * Here is the only way to safely read a HashMap of String keys and Integer values:
039 * </p>
040 *
041 * <pre>{@code
042 * // Defining Object fixture
043 * final HashMap<String, Integer> map1 = new HashMap<>();
044 * map1.put("1", 1);
045 * // Writing serialized fixture
046 * final byte[] byteArray;
047 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
048 *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
049 *     oos.writeObject(map1);
050 *     oos.flush();
051 *     byteArray = baos.toByteArray();
052 * }
053 * // Reading
054 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
055 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
056 *             .accept(HashMap.class, Number.class, Integer.class)
057 *             .setInputStream(bais)
058 *             .get()) {
059 *     // String.class is automatically accepted
060 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
061 *     assertEquals(map1, map2);
062 * }
063 * // Reusing a configuration
064 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
065 *     .accept(HashMap.class, Number.class, Integer.class);
066 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
067 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
068 *             .setPredicate(predicate)
069 *             .setInputStream(bais)
070 *             .get()) {
071 *     // String.class is automatically accepted
072 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
073 *     assertEquals(map1, map2);
074 * }
075 * }</pre>
076 * <p>
077 * Design inspired by a <a href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
078 * </p>
079 *
080 * @since 2.5
081 */
082public class ValidatingObjectInputStream extends ObjectInputStream {
083
084    // @formatter:off
085    /**
086     * Builds a new {@link ValidatingObjectInputStream}.
087     *
088     * <h2>Using NIO</h2>
089     * <pre>{@code
090     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
091     *   .setPath(Paths.get("MyFile.ser"))
092     *   .get();}
093     * </pre>
094     * <h2>Using IO</h2>
095     * <pre>{@code
096     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
097     *   .setFile(new File("MyFile.ser"))
098     *   .get();}
099     * </pre>
100     *
101     * @see #get()
102     * @since 2.18.0
103     */
104    // @formatter:on
105    public static class Builder extends AbstractStreamBuilder<ValidatingObjectInputStream, Builder> {
106
107        private ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate();
108
109        /**
110         * Constructs a new instance.
111         *
112         * @deprecated Use {@link #builder()}.
113         */
114        @Deprecated
115        public Builder() {
116            // empty
117        }
118
119        /**
120         * Accepts the specified classes for deserialization, unless they are otherwise rejected.
121         *
122         * @param classes Classes to accept
123         * @return this object
124         * @since 2.18.0
125         */
126        public Builder accept(final Class<?>... classes) {
127            predicate.accept(classes);
128            return this;
129        }
130
131        /**
132         * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
133         *
134         * @param matcher a class name matcher to <em>accept</em> objects.
135         * @return this instance.
136         * @since 2.18.0
137         */
138        public Builder accept(final ClassNameMatcher matcher) {
139            predicate.accept(matcher);
140            return this;
141        }
142
143        /**
144         * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
145         *
146         * @param pattern a Pattern for compiled regular expression.
147         * @return this instance.
148         * @since 2.18.0
149         */
150        public Builder accept(final Pattern pattern) {
151            predicate.accept(pattern);
152            return this;
153        }
154
155        /**
156         * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
157         *
158         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
159         *                 FilenameUtils.wildcardMatch}
160         * @return this instance.
161         * @since 2.18.0
162         */
163        public Builder accept(final String... patterns) {
164            predicate.accept(patterns);
165            return this;
166        }
167
168        @Override
169        public ValidatingObjectInputStream get() throws IOException {
170            return new ValidatingObjectInputStream(getInputStream(), predicate);
171        }
172
173        /**
174         * Gets the predicate.
175         *
176         * @return the predicate.
177         * @since 2.18.0
178         */
179        public ObjectStreamClassPredicate getPredicate() {
180            return predicate;
181        }
182
183        /**
184         * Rejects the specified classes for deserialization, even if they are otherwise accepted.
185         *
186         * @param classes Classes to reject
187         * @return this instance.
188         * @since 2.18.0
189         */
190        public Builder reject(final Class<?>... classes) {
191            predicate.reject(classes);
192            return this;
193        }
194
195        /**
196         * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
197         *
198         * @param matcher the matcher to use
199         * @return this instance.
200         * @since 2.18.0
201         */
202        public Builder reject(final ClassNameMatcher matcher) {
203            predicate.reject(matcher);
204            return this;
205        }
206
207        /**
208         * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
209         *
210         * @param pattern standard Java regexp
211         * @return this instance.
212         * @since 2.18.0
213         */
214        public Builder reject(final Pattern pattern) {
215            predicate.reject(pattern);
216            return this;
217        }
218
219        /**
220         * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
221         *
222         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
223         *                 FilenameUtils.wildcardMatch}
224         * @return this instance.
225         * @since 2.18.0
226         */
227        public Builder reject(final String... patterns) {
228            predicate.reject(patterns);
229            return this;
230        }
231
232        /**
233         * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate.
234         *
235         * @param predicate the predicate.
236         * @return this instance.
237         * @since 2.18.0
238         */
239        public Builder setPredicate(final ObjectStreamClassPredicate predicate) {
240            this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate();
241            return this;
242        }
243
244    }
245
246    /**
247     * Constructs a new {@link Builder}.
248     *
249     * @return a new {@link Builder}.
250     * @since 2.18.0
251     */
252    public static Builder builder() {
253        return new Builder();
254    }
255
256    private final ObjectStreamClassPredicate predicate;
257
258    /**
259     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
260     * deserialized, as by default no classes are accepted.
261     *
262     * @param input an input stream
263     * @throws IOException if an I/O error occurs while reading stream header
264     * @deprecated Use {@link #builder()}.
265     */
266    @Deprecated
267    public ValidatingObjectInputStream(final InputStream input) throws IOException {
268        this(input, new ObjectStreamClassPredicate());
269    }
270
271    /**
272     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
273     * deserialized, as by default no classes are accepted.
274     *
275     * @param input     an input stream.
276     * @param predicate how to accept and reject classes.
277     * @throws IOException if an I/O error occurs while reading stream header.
278     */
279    private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException {
280        super(input);
281        this.predicate = predicate;
282    }
283
284    /**
285     * Accepts the specified classes for deserialization, unless they are otherwise rejected.
286     * <p>
287     * The reject list takes precedence over the accept list.
288     * </p>
289     *
290     * @param classes Classes to accept
291     * @return this instance.
292     */
293    public ValidatingObjectInputStream accept(final Class<?>... classes) {
294        predicate.accept(classes);
295        return this;
296    }
297
298    /**
299     * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
300     * <p>
301     * The reject list takes precedence over the accept list.
302     * </p>
303     *
304     * @param matcher a class name matcher to <em>accept</em> objects.
305     * @return this instance.
306     */
307    public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
308        predicate.accept(matcher);
309        return this;
310    }
311
312    /**
313     * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
314     * <p>
315     * The reject list takes precedence over the accept list.
316     * </p>
317     *
318     * @param pattern a Pattern for compiled regular expression.
319     * @return this instance.
320     */
321    public ValidatingObjectInputStream accept(final Pattern pattern) {
322        predicate.accept(pattern);
323        return this;
324    }
325
326    /**
327     * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
328     * <p>
329     * The reject list takes precedence over the accept list.
330     * </p>
331     *
332     * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
333     *                 FilenameUtils.wildcardMatch}.
334     * @return this instance.
335     */
336    public ValidatingObjectInputStream accept(final String... patterns) {
337        predicate.accept(patterns);
338        return this;
339    }
340
341    /**
342     * Checks that the class name conforms to requirements.
343     * <p>
344     * The reject list takes precedence over the accept list.
345     * </p>
346     *
347     * @param name The class name to test.
348     * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
349     */
350    private void checkClassName(final String name) throws InvalidClassException {
351        if (!predicate.test(name)) {
352            invalidClassNameFound(name);
353        }
354    }
355
356    /**
357     * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
358     * names.
359     *
360     * @param className name of the invalid class.
361     * @throws InvalidClassException Thrown with a message containing the class name.
362     */
363    protected void invalidClassNameFound(final String className) throws InvalidClassException {
364        throw new InvalidClassException("Class name not accepted: " + className);
365    }
366
367    /**
368     * Delegates to {@link #readObject()} and casts to the generic {@code T}.
369     *
370     * @param <T> The return type.
371     * @return Result from {@link #readObject()}.
372     * @throws ClassNotFoundException Thrown by {@link #readObject()}.
373     * @throws IOException            Thrown by {@link #readObject()}.
374     * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
375     * @since 2.18.0
376     */
377    @SuppressWarnings("unchecked")
378    public <T> T readObjectCast() throws ClassNotFoundException, IOException {
379        return (T) super.readObject();
380    }
381
382    /**
383     * Rejects the specified classes for deserialization, even if they are otherwise accepted.
384     * <p>
385     * The reject list takes precedence over the accept list.
386     * </p>
387     *
388     * @param classes Classes to reject.
389     * @return this instance.
390     */
391    public ValidatingObjectInputStream reject(final Class<?>... classes) {
392        predicate.reject(classes);
393        return this;
394    }
395
396    /**
397     * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
398     * <p>
399     * The reject list takes precedence over the accept list.
400     * </p>
401     *
402     * @param matcher a class name matcher to <em>reject</em> objects.
403     * @return this instance.
404     */
405    public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
406        predicate.reject(matcher);
407        return this;
408    }
409
410    /**
411     * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
412     * <p>
413     * The reject list takes precedence over the accept list.
414     * </p>
415     *
416     * @param pattern a Pattern for compiled regular expression.
417     * @return this instance.
418     */
419    public ValidatingObjectInputStream reject(final Pattern pattern) {
420        predicate.reject(pattern);
421        return this;
422    }
423
424    /**
425     * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
426     * <p>
427     * The reject list takes precedence over the accept list.
428     * </p>
429     *
430     * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
431     *                 FilenameUtils.wildcardMatch}
432     * @return this instance.
433     */
434    public ValidatingObjectInputStream reject(final String... patterns) {
435        predicate.reject(patterns);
436        return this;
437    }
438
439    @Override
440    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
441        checkClassName(osc.getName());
442        return super.resolveClass(osc);
443    }
444}