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 *   https://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 builder of {@link ValidatingObjectInputStream}.
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        /**
169         * Builds a new {@link ValidatingObjectInputStream}.
170         * <p>
171         * You must set an aspect that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception.
172         * </p>
173         * <p>
174         * This builder uses the following aspects:
175         * </p>
176         * <ul>
177         * <li>{@link #getInputStream()} gets the target aspect.</li>
178         * <li>predicate</li>
179         * <li>charsetDecoder</li>
180         * <li>writeImmediately</li>
181         * </ul>
182         *
183         * @return a new instance.
184         * @throws UnsupportedOperationException if the origin cannot provide a {@link InputStream}.
185         * @throws IOException                   if an I/O error occurs converting to an {@link InputStream} using {@link #getInputStream()}.
186         * @see #getWriter()
187         * @see #getUnchecked()
188         */
189        @Override
190        public ValidatingObjectInputStream get() throws IOException {
191            return new ValidatingObjectInputStream(this);
192        }
193
194        /**
195         * Gets the predicate.
196         *
197         * @return the predicate.
198         * @since 2.18.0
199         */
200        public ObjectStreamClassPredicate getPredicate() {
201            return predicate;
202        }
203
204        /**
205         * Rejects the specified classes for deserialization, even if they are otherwise accepted.
206         *
207         * @param classes Classes to reject
208         * @return this instance.
209         * @since 2.18.0
210         */
211        public Builder reject(final Class<?>... classes) {
212            predicate.reject(classes);
213            return this;
214        }
215
216        /**
217         * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
218         *
219         * @param matcher the matcher to use
220         * @return this instance.
221         * @since 2.18.0
222         */
223        public Builder reject(final ClassNameMatcher matcher) {
224            predicate.reject(matcher);
225            return this;
226        }
227
228        /**
229         * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
230         *
231         * @param pattern standard Java regexp
232         * @return this instance.
233         * @since 2.18.0
234         */
235        public Builder reject(final Pattern pattern) {
236            predicate.reject(pattern);
237            return this;
238        }
239
240        /**
241         * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
242         *
243         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
244         *                 FilenameUtils.wildcardMatch}
245         * @return this instance.
246         * @since 2.18.0
247         */
248        public Builder reject(final String... patterns) {
249            predicate.reject(patterns);
250            return this;
251        }
252
253        /**
254         * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate.
255         *
256         * @param predicate the predicate.
257         * @return this instance.
258         * @since 2.18.0
259         */
260        public Builder setPredicate(final ObjectStreamClassPredicate predicate) {
261            this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate();
262            return this;
263        }
264
265    }
266
267    /**
268     * Constructs a new {@link Builder}.
269     *
270     * @return a new {@link Builder}.
271     * @since 2.18.0
272     */
273    public static Builder builder() {
274        return new Builder();
275    }
276
277    private final ObjectStreamClassPredicate predicate;
278
279    @SuppressWarnings("resource") // caller closes/
280    private ValidatingObjectInputStream(final Builder builder) throws IOException {
281        this(builder.getInputStream(), builder.predicate);
282    }
283
284    /**
285     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
286     * deserialized, as by default no classes are accepted.
287     *
288     * @param input an input stream
289     * @throws IOException if an I/O error occurs while reading stream header
290     * @deprecated Use {@link #builder()}.
291     */
292    @Deprecated
293    public ValidatingObjectInputStream(final InputStream input) throws IOException {
294        this(input, new ObjectStreamClassPredicate());
295    }
296
297    /**
298     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
299     * deserialized, as by default no classes are accepted.
300     *
301     * @param input     an input stream.
302     * @param predicate how to accept and reject classes.
303     * @throws IOException if an I/O error occurs while reading stream header.
304     */
305    private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException {
306        super(input);
307        this.predicate = predicate;
308    }
309
310    /**
311     * Accepts the specified classes for deserialization, unless they are otherwise rejected.
312     * <p>
313     * The reject list takes precedence over the accept list.
314     * </p>
315     *
316     * @param classes Classes to accept
317     * @return this instance.
318     */
319    public ValidatingObjectInputStream accept(final Class<?>... classes) {
320        predicate.accept(classes);
321        return this;
322    }
323
324    /**
325     * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
326     * <p>
327     * The reject list takes precedence over the accept list.
328     * </p>
329     *
330     * @param matcher a class name matcher to <em>accept</em> objects.
331     * @return this instance.
332     */
333    public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
334        predicate.accept(matcher);
335        return this;
336    }
337
338    /**
339     * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
340     * <p>
341     * The reject list takes precedence over the accept list.
342     * </p>
343     *
344     * @param pattern a Pattern for compiled regular expression.
345     * @return this instance.
346     */
347    public ValidatingObjectInputStream accept(final Pattern pattern) {
348        predicate.accept(pattern);
349        return this;
350    }
351
352    /**
353     * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
354     * <p>
355     * The reject list takes precedence over the accept list.
356     * </p>
357     *
358     * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
359     *                 FilenameUtils.wildcardMatch}.
360     * @return this instance.
361     */
362    public ValidatingObjectInputStream accept(final String... patterns) {
363        predicate.accept(patterns);
364        return this;
365    }
366
367    /**
368     * Checks that the class name conforms to requirements.
369     * <p>
370     * The reject list takes precedence over the accept list.
371     * </p>
372     *
373     * @param name The class name to test.
374     * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
375     */
376    private void checkClassName(final String name) throws InvalidClassException {
377        if (!predicate.test(name)) {
378            invalidClassNameFound(name);
379        }
380    }
381
382    /**
383     * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
384     * names.
385     *
386     * @param className name of the invalid class.
387     * @throws InvalidClassException Thrown with a message containing the class name.
388     */
389    protected void invalidClassNameFound(final String className) throws InvalidClassException {
390        throw new InvalidClassException("Class name not accepted: " + className);
391    }
392
393    /**
394     * Delegates to {@link #readObject()} and casts to the generic {@code T}.
395     *
396     * @param <T> The return type.
397     * @return Result from {@link #readObject()}.
398     * @throws ClassNotFoundException Thrown by {@link #readObject()}.
399     * @throws IOException            Thrown by {@link #readObject()}.
400     * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
401     * @since 2.18.0
402     */
403    @SuppressWarnings("unchecked")
404    public <T> T readObjectCast() throws ClassNotFoundException, IOException {
405        return (T) super.readObject();
406    }
407
408    /**
409     * Rejects the specified classes for deserialization, even if they are otherwise accepted.
410     * <p>
411     * The reject list takes precedence over the accept list.
412     * </p>
413     *
414     * @param classes Classes to reject.
415     * @return this instance.
416     */
417    public ValidatingObjectInputStream reject(final Class<?>... classes) {
418        predicate.reject(classes);
419        return this;
420    }
421
422    /**
423     * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
424     * <p>
425     * The reject list takes precedence over the accept list.
426     * </p>
427     *
428     * @param matcher a class name matcher to <em>reject</em> objects.
429     * @return this instance.
430     */
431    public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
432        predicate.reject(matcher);
433        return this;
434    }
435
436    /**
437     * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
438     * <p>
439     * The reject list takes precedence over the accept list.
440     * </p>
441     *
442     * @param pattern a Pattern for compiled regular expression.
443     * @return this instance.
444     */
445    public ValidatingObjectInputStream reject(final Pattern pattern) {
446        predicate.reject(pattern);
447        return this;
448    }
449
450    /**
451     * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
452     * <p>
453     * The reject list takes precedence over the accept list.
454     * </p>
455     *
456     * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
457     *                 FilenameUtils.wildcardMatch}
458     * @return this instance.
459     */
460    public ValidatingObjectInputStream reject(final String... patterns) {
461        predicate.reject(patterns);
462        return this;
463    }
464
465    @Override
466    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
467        checkClassName(osc.getName());
468        return super.resolveClass(osc);
469    }
470}