This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 0924f91d5 FastDateParser.readObject(ObjectInputStream) now validates 
the same (#1692)
0924f91d5 is described below

commit 0924f91d57d8932084c44fa7a957318a57b7aa40
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Jun 6 09:53:58 2026 -0400

    FastDateParser.readObject(ObjectInputStream) now validates the same (#1692)
    
    constructor invariants
    
    Add org.apache.commons.lang3.SerializationUtils.requireNonNull(T,
    String)
---
 src/main/java/org/apache/commons/lang3/Range.java  |  12 +-
 .../apache/commons/lang3/SerializationUtils.java   | 140 +++++++++++---------
 .../apache/commons/lang3/time/FastDateParser.java  |  18 +--
 .../lang3/time/FastDateParserReadObjectTest.java   | 143 +++++++++++++++++++++
 4 files changed, 230 insertions(+), 83 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/Range.java 
b/src/main/java/org/apache/commons/lang3/Range.java
index 0d2165de8..0d6bb2680 100644
--- a/src/main/java/org/apache/commons/lang3/Range.java
+++ b/src/main/java/org/apache/commons/lang3/Range.java
@@ -550,15 +550,9 @@ private void readObject(final ObjectInputStream in) throws 
IOException, ClassNot
         if (hashCode != hash(minimum, maximum)) {
             throw new InvalidObjectException("Range hashCode does not match 
minimum/maximum.");
         }
-        if (maximum == null) {
-            throw new InvalidObjectException("maximum null");
-        }
-        if (minimum == null) {
-            throw new InvalidObjectException("minimum null");
-        }
-        if (comparator == null) {
-            throw new InvalidObjectException("comparator null");
-        }
+        SerializationUtils.requireNonNull(maximum, "maximum null");
+        SerializationUtils.requireNonNull(minimum, "minimum null");
+        SerializationUtils.requireNonNull(comparator, "comparator null");
         if (comparator.compare(minimum, maximum) > 0) {
             throw new InvalidObjectException("Range minimum is greater than 
maximum under the comparator.");
         }
diff --git a/src/main/java/org/apache/commons/lang3/SerializationUtils.java 
b/src/main/java/org/apache/commons/lang3/SerializationUtils.java
index 124fcb0fc..4b1fea491 100644
--- a/src/main/java/org/apache/commons/lang3/SerializationUtils.java
+++ b/src/main/java/org/apache/commons/lang3/SerializationUtils.java
@@ -14,12 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.commons.lang3;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InvalidObjectException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.ObjectStreamClass;
@@ -48,31 +50,29 @@
  * </p>
  *
  * @see org.apache.commons.io.serialization.ValidatingObjectInputStream
+ * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
  * @since 1.0
  */
 public class SerializationUtils {
 
     /**
-     * Custom specialization of the standard JDK {@link ObjectInputStream}
-     * that uses a custom  {@link ClassLoader} to resolve a class.
-     * If the specified {@link ClassLoader} is not able to resolve the class,
-     * the context classloader of the current thread will be used.
-     * This way, the standard deserialization work also in web-application
-     * containers and application servers, no matter in which of the
-     * {@link ClassLoader} the particular class that encapsulates
+     * Custom specialization of the standard JDK {@link ObjectInputStream} 
that uses a custom {@link ClassLoader} to resolve a class. If the specified
+     * {@link ClassLoader} is not able to resolve the class, the context 
classloader of the current thread will be used. This way, the standard 
deserialization
+     * work also in web-application containers and application servers, no 
matter in which of the {@link ClassLoader} the particular class that 
encapsulates
      * serialization/deserialization lives.
      *
-     * <p>For more in-depth information about the problem for which this
-     * class here is a workaround, see the JIRA issue LANG-626.</p>
+     * <p>
+     * For more in-depth information about the problem for which this class 
here is a workaround, see the JIRA issue LANG-626.
+     * </p>
      */
-     static final class ClassLoaderAwareObjectInputStream extends 
ObjectInputStream {
+    static final class ClassLoaderAwareObjectInputStream extends 
ObjectInputStream {
 
         private final ClassLoader classLoader;
 
         /**
          * Constructs a new instance.
          *
-         * @param in The {@link InputStream}.
+         * @param in          The {@link InputStream}.
          * @param classLoader classloader to use
          * @throws IOException if an I/O error occurs while reading stream 
header.
          * @see java.io.ObjectInputStream
@@ -83,12 +83,11 @@ static final class ClassLoaderAwareObjectInputStream 
extends ObjectInputStream {
         }
 
         /**
-         * Overridden version that uses the parameterized {@link ClassLoader} 
or the {@link ClassLoader}
-         * of the current {@link Thread} to resolve the class.
+         * Overridden version that uses the parameterized {@link ClassLoader} 
or the {@link ClassLoader} of the current {@link Thread} to resolve the class.
          *
          * @param desc An instance of class {@link ObjectStreamClass}.
          * @return A {@link Class} object corresponding to {@code desc}.
-         * @throws IOException Any of the usual Input/Output exceptions.
+         * @throws IOException            Any of the usual Input/Output 
exceptions.
          * @throws ClassNotFoundException If class of a serialized object 
cannot be found.
          */
         @Override
@@ -108,22 +107,21 @@ protected Class<?> resolveClass(final ObjectStreamClass 
desc) throws IOException
                 }
             }
         }
-
     }
 
     /**
      * Deep clones an {@link Object} using serialization.
      *
-     * <p>This is many times slower than writing clone methods by hand
-     * on all objects in your object graph. However, for complex object
-     * graphs, or for those that don't support deep cloning this can
-     * be a simple alternative implementation. Of course all the objects
-     * must be {@link Serializable}.</p>
+     * <p>
+     * This is many times slower than writing clone methods by hand on all 
objects in your object graph. However, for complex object graphs, or for those 
that
+     * don't support deep cloning this can be a simple alternative 
implementation. Of course all the objects must be {@link Serializable}.
+     * </p>
      *
-     * @param <T> the type of the object involved.
-     * @param object  the {@link Serializable} object to clone.
+     * @param <T>    the type of the object involved.
+     * @param object the {@link Serializable} object to clone.
      * @return the cloned object.
      * @throws SerializationException (runtime) if the serialization fails.
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
      */
     public static <T extends Serializable> T clone(final T object) {
         if (object == null) {
@@ -135,7 +133,6 @@ public static <T extends Serializable> T clone(final T 
object) {
             // When we serialize and deserialize an object, it is reasonable 
to assume the deserialized object is of the
             // same type as the original serialized object
             return (T) in.readObject();
-
         } catch (final ClassNotFoundException | IOException ex) {
             throw new SerializationException(String.format("%s while reading 
cloned object data", ex.getClass().getSimpleName()), ex);
         }
@@ -145,22 +142,22 @@ public static <T extends Serializable> T clone(final T 
object) {
      * Deserializes a single {@link Object} from an array of bytes.
      *
      * <p>
-     * If the call site incorrectly types the return value, a {@link 
ClassCastException} is thrown from the call site.
-     * Without Generics in this declaration, the call site must type cast and 
can cause the same ClassCastException.
-     * Note that in both cases, the ClassCastException is in the call site, 
not in this method.
+     * If the call site incorrectly types the return value, a {@link 
ClassCastException} is thrown from the call site. Without Generics in this 
declaration, the
+     * call site must type cast and can cause the same ClassCastException. 
Note that in both cases, the ClassCastException is in the call site, not in this
+     * method.
      * </p>
      * <p>
      * If you want to secure deserialization with a whitelist or blacklist, 
please use Apache Commons IO's
      * {@link org.apache.commons.io.serialization.ValidatingObjectInputStream 
ValidatingObjectInputStream}.
      * </p>
      *
-     * @param <T>  the object type to be deserialized.
-     * @param objectData
-     *            the serialized object, must not be null.
+     * @param <T>        the object type to be deserialized.
+     * @param objectData the serialized object, must not be null.
      * @return the deserialized object.
-     * @throws NullPointerException if {@code objectData} is {@code null}.
+     * @throws NullPointerException   if {@code objectData} is {@code null}.
      * @throws SerializationException (runtime) if the serialization fails.
      * @see org.apache.commons.io.serialization.ValidatingObjectInputStream
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
      */
     public static <T> T deserialize(final byte[] objectData) {
         Objects.requireNonNull(objectData, "objectData");
@@ -171,19 +168,18 @@ public static <T> T deserialize(final byte[] objectData) {
      * Deserializes an {@link Object} from the specified stream.
      *
      * <p>
-     * The stream will be closed once the object is written. This avoids the 
need for a finally clause, and maybe also
-     * exception handling, in the application code.
+     * The stream will be closed once the object is written. This avoids the 
need for a finally clause, and maybe also exception handling, in the application
+     * code.
      * </p>
      *
      * <p>
-     * The stream passed in is not buffered internally within this method. 
This is the responsibility of your
-     * application if desired.
+     * The stream passed in is not buffered internally within this method. 
This is the responsibility of your application if desired.
      * </p>
      *
      * <p>
-     * If the call site incorrectly types the return value, a {@link 
ClassCastException} is thrown from the call site.
-     * Without Generics in this declaration, the call site must type cast and 
can cause the same ClassCastException.
-     * Note that in both cases, the ClassCastException is in the call site, 
not in this method.
+     * If the call site incorrectly types the return value, a {@link 
ClassCastException} is thrown from the call site. Without Generics in this 
declaration, the
+     * call site must type cast and can cause the same ClassCastException. 
Note that in both cases, the ClassCastException is in the call site, not in this
+     * method.
      * </p>
      *
      * <p>
@@ -191,12 +187,13 @@ public static <T> T deserialize(final byte[] objectData) {
      * {@link org.apache.commons.io.serialization.ValidatingObjectInputStream 
ValidatingObjectInputStream}.
      * </p>
      *
-     * @param <T>  the object type to be deserialized.
+     * @param <T>         the object type to be deserialized.
      * @param inputStream the serialized object input stream, must not be null.
      * @return the deserialized object.
-     * @throws NullPointerException if {@code inputStream} is {@code null}.
+     * @throws NullPointerException   if {@code inputStream} is {@code null}.
      * @throws SerializationException (runtime) if the serialization fails.
      * @see org.apache.commons.io.serialization.ValidatingObjectInputStream
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
      */
     @SuppressWarnings("resource") // inputStream is managed by the caller
     public static <T> T deserialize(final InputStream inputStream) {
@@ -211,14 +208,32 @@ public static <T> T deserialize(final InputStream 
inputStream) {
     }
 
     /**
-     * Performs a serialization roundtrip. Serializes and deserializes the 
given object, great for testing objects that
-     * implement {@link Serializable}.
+     * Checks that the specified object reference is not {@code null} and 
throws a customized {@link InvalidObjectException} if it is. This method is 
designed
+     * primarily for doing state validation in {@link Serializable} class's 
{@code readObject(ObjectInputStream)} methods.
+     *
+     * @param obj     the object reference to check for nullity.
+     * @param message detail message to be used in the event that a {@link 
InvalidObjectException} is thrown.
+     * @param <T>     the type of the reference.
+     * @return {@code obj} if not {@code null}.
+     * @throws InvalidObjectException if {@code obj} is {@code null}.
+     * @see Serializable
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
+     * @since 3.21.0
+     */
+    public static <T> T requireNonNull(final T obj, final String message) 
throws InvalidObjectException {
+        if (obj == null) {
+            throw new InvalidObjectException(message);
+        }
+        return obj;
+    }
+
+    /**
+     * Performs a serialization roundtrip. Serializes and deserializes the 
given object, great for testing objects that implement {@link Serializable}.
      *
-     * @param <T>
-     *           the type of the object involved.
-     * @param obj
-     *            the object to roundtrip.
+     * @param <T> the type of the object involved.
+     * @param obj the object to roundtrip.
      * @return the serialized and deserialized object.
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
      * @since 3.3
      */
     @SuppressWarnings("unchecked") // OK, because we serialized a type `T`
@@ -227,12 +242,12 @@ public static <T extends Serializable> T roundtrip(final 
T obj) {
     }
 
     /**
-     * Serializes an {@link Object} to a byte array for
-     * storage/serialization.
+     * Serializes an {@link Object} to a byte array for storage/serialization.
      *
-     * @param obj  the object to serialize to bytes.
+     * @param obj the object to serialize to bytes.
      * @return a byte[] with the converted Serializable.
      * @throws SerializationException (runtime) if the serialization fails.
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
      */
     public static byte[] serialize(final Serializable obj) {
         final ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
@@ -243,17 +258,20 @@ public static byte[] serialize(final Serializable obj) {
     /**
      * Serializes an {@link Object} to the specified stream.
      *
-     * <p>The stream will be closed once the object is written.
-     * This avoids the need for a finally clause, and maybe also exception
-     * handling, in the application code.</p>
+     * <p>
+     * The stream will be closed once the object is written. This avoids the 
need for a finally clause, and maybe also exception handling, in the application
+     * code.
+     * </p>
      *
-     * <p>The stream passed in is not buffered internally within this method.
-     * This is the responsibility of your application if desired.</p>
+     * <p>
+     * The stream passed in is not buffered internally within this method. 
This is the responsibility of your application if desired.
+     * </p>
      *
-     * @param obj  the object to serialize to bytes, may be null.
-     * @param outputStream  the stream to write to, must not be null.
-     * @throws NullPointerException if {@code outputStream} is {@code null}.
+     * @param obj          the object to serialize to bytes, may be null.
+     * @param outputStream the stream to write to, must not be null.
+     * @throws NullPointerException   if {@code outputStream} is {@code null}.
      * @throws SerializationException (runtime) if the serialization fails.
+     * @see <a 
href="https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/";>Java 
Object Serialization Specification</a>
      */
     @SuppressWarnings("resource") // outputStream is managed by the caller
     public static void serialize(final Serializable obj, final OutputStream 
outputStream) {
@@ -266,11 +284,12 @@ public static void serialize(final Serializable obj, 
final OutputStream outputSt
     }
 
     /**
-     * SerializationUtils instances should NOT be constructed in standard 
programming.
-     * Instead, the class should be used as {@code 
SerializationUtils.clone(object)}.
+     * SerializationUtils instances should NOT be constructed in standard 
programming. Instead, the class should be used as
+     * {@code SerializationUtils.clone(object)}.
      *
-     * <p>This constructor is public to permit tools that require a JavaBean 
instance
-     * to operate.</p>
+     * <p>
+     * This constructor is public to permit tools that require a JavaBean 
instance to operate.
+     * </p>
      *
      * @since 2.0
      * @deprecated TODO Make private in 4.0.
@@ -279,5 +298,4 @@ public static void serialize(final Serializable obj, final 
OutputStream outputSt
     public SerializationUtils() {
         // empty
     }
-
 }
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java 
b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
index 02da1a4a0..21454f375 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
@@ -46,6 +46,7 @@
 
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.LocaleUtils;
+import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.StringUtils;
 
 /**
@@ -105,7 +106,6 @@ private static final class CaseInsensitiveTextStrategy 
extends PatternStrategy {
         CaseInsensitiveTextStrategy(final int field, final Calendar 
definingCalendar, final Locale locale) {
             this.field = field;
             this.locale = LocaleUtils.toLocale(locale);
-
             final StringBuilder regex = new StringBuilder();
             regex.append("((?iu)");
             lKeyValues = appendDisplayNames(definingCalendar, locale, field, 
regex);
@@ -219,7 +219,7 @@ static Strategy getStrategy(final int tokenLen) {
             case 3:
                 return ISO_8601_3_STRATEGY;
             default:
-                throw new IllegalArgumentException("invalid number of X");
+                throw new IllegalArgumentException("Invalid number of X");
             }
         }
 
@@ -280,7 +280,6 @@ int modify(final FastDateParser parser, final int iValue) {
         boolean parse(final FastDateParser parser, final Calendar calendar, 
final String source, final ParsePosition pos, final int maxWidth) {
             int idx = pos.getIndex();
             int last = source.length();
-
             if (maxWidth == 0) {
                 // if no maxWidth, strip leading white space
                 for (; idx < last; ++idx) {
@@ -296,22 +295,18 @@ boolean parse(final FastDateParser parser, final Calendar 
calendar, final String
                     last = end;
                 }
             }
-
             for (; idx < last; ++idx) {
                 final char c = source.charAt(idx);
                 if (!Character.isDigit(c)) {
                     break;
                 }
             }
-
             if (pos.getIndex() == idx) {
                 pos.setErrorIndex(idx);
                 return false;
             }
-
             final int value = 
Integer.parseInt(source.substring(pos.getIndex(), idx));
             pos.setIndex(idx);
-
             calendar.set(field, modify(parser, value));
             return true;
         }
@@ -458,7 +453,6 @@ private StrategyAndWidth letterPattern(final char c) {
 
         private StrategyAndWidth literal() {
             boolean activeQuote = false;
-
             final StringBuilder sb = new StringBuilder();
             while (currentIdx < pattern.length()) {
                 final char c = pattern.charAt(currentIdx);
@@ -540,12 +534,9 @@ static boolean skipTimeZone(final String tzId) {
          */
         TimeZoneStrategy(final Locale locale) {
             this.locale = LocaleUtils.toLocale(locale);
-
             final StringBuilder sb = new StringBuilder();
             sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION);
-
             final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
-
             // Order is undefined.
             // TODO Use of getZoneStrings() is discouraged per its Javadoc.
             final String[][] zones = 
DateFormatSymbols.getInstance(locale).getZoneStrings();
@@ -1107,8 +1098,9 @@ public Object parseObject(final String source, final 
ParsePosition pos) {
      */
     private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
         in.defaultReadObject();
-        final Calendar definingCalendar = Calendar.getInstance(timeZone, 
locale);
-        init(definingCalendar);
+        SerializationUtils.requireNonNull(pattern, "pattern null");
+        SerializationUtils.requireNonNull(timeZone, "timeZone null");
+        init(Calendar.getInstance(timeZone, locale));
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/lang3/time/FastDateParserReadObjectTest.java 
b/src/test/java/org/apache/commons/lang3/time/FastDateParserReadObjectTest.java
new file mode 100644
index 000000000..70510f421
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/time/FastDateParserReadObjectTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.lang3.time;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.Serializable;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that a deserialized {@link FastDateParser} rejects null {@code 
pattern} and null {@code timeZone} fields.
+ *
+ * <p>
+ * The two null-checks were introduced in {@link 
FastDateParser#readObject(ObjectInputStream)}:
+ * </p>
+ * <ul>
+ * <li>{@code if (pattern == null) throw new InvalidObjectException("pattern 
null");}</li>
+ * <li>{@code if (timeZone == null) throw new InvalidObjectException("timeZone 
null");}</li>
+ * </ul>
+ *
+ * <p>
+ * Because neither null value can reach {@code readObject} through the normal 
public API, the tests forge a malicious serialization stream. A
+ * {@link FastDateParserForge} helper carries the same non-transient field set 
as {@link FastDateParser} (same names, same types, same {@code 
serialVersionUID})
+ * but allows null values. A custom {@link ObjectOutputStream} sub-class 
rewrites the class-descriptor name to {@code FastDateParser} so the stream is 
accepted
+ * by {@link ObjectInputStream} as a {@link FastDateParser} payload; {@code 
defaultReadObject} then assigns the forged values to the actual
+ * {@link FastDateParser} fields, triggering the null checks.
+ * </p>
+ */
+class FastDateParserReadObjectTest {
+
+    /**
+     * Forge carrier: same non-transient fields as {@link FastDateParser}, in 
the same alphabetical order used by Java default serialization ({@code century},
+     * {@code locale}, {@code pattern}, {@code startYear}, {@code timeZone}), 
with the same {@code serialVersionUID}. Allows null for {@code pattern} and
+     * {@code timeZone}.
+     */
+    private static final class FastDateParserForge implements Serializable {
+
+        /** Must match {@link FastDateParser#serialVersionUID}. */
+        private static final long serialVersionUID = 3L;
+        // Fields must match FastDateParser's non-transient fields by name and 
type.
+        private final int century;
+        private final Locale locale;
+        private final String pattern;
+        private final int startYear;
+        private final TimeZone timeZone;
+
+        FastDateParserForge(final String pattern, final TimeZone timeZone, 
final Locale locale, final int century, final int startYear) {
+            this.pattern = pattern;
+            this.timeZone = timeZone;
+            this.locale = locale;
+            this.century = century;
+            this.startYear = startYear;
+        }
+    }
+
+    /**
+     * Deserializes {@code bytes} and returns the resulting object.
+     *
+     * @param bytes serialized form
+     * @return the deserialized object
+     * @throws IOException            if an I/O error occurs
+     * @throws ClassNotFoundException if the class of the serialized object 
cannot be found
+     */
+    private static Object deserialize(final byte[] bytes) throws IOException, 
ClassNotFoundException {
+        try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(bytes))) {
+            return ois.readObject();
+        }
+    }
+
+    /**
+     * Serializes a {@link FastDateParserForge} but rewrites the class 
descriptor so that the resulting stream is treated as a {@link FastDateParser} 
during
+     * deserialization.
+     *
+     * @param forge the forge instance to serialize
+     * @return a byte array whose class descriptor names {@link FastDateParser}
+     * @throws IOException if an I/O error occurs
+     */
+    private static byte[] forgeStream(final FastDateParserForge forge) throws 
IOException {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
+
+            @Override
+            protected void writeClassDescriptor(final ObjectStreamClass desc) 
throws IOException {
+                if 
(desc.getName().equals(FastDateParserForge.class.getName())) {
+                    // Spoof the class descriptor so the stream deserializes 
as FastDateParser.
+                    
super.writeClassDescriptor(ObjectStreamClass.lookup(FastDateParser.class));
+                } else {
+                    super.writeClassDescriptor(desc);
+                }
+            }
+        }) {
+            oos.writeObject(forge);
+        }
+        return baos.toByteArray();
+    }
+
+    /**
+     * Tests that a forged stream whose {@code pattern} field is {@code null} 
is rejected with {@link InvalidObjectException}.
+     */
+    @Test
+    void testNullPatternRejected() throws IOException {
+        final FastDateParserForge forge = new FastDateParserForge(null, // 
pattern = null (the evil value under test)
+                TimeZone.getTimeZone("GMT"), Locale.US, 1900, 0);
+        final byte[] forgedBytes = forgeStream(forge);
+        assertThrows(InvalidObjectException.class, () -> 
deserialize(forgedBytes), "A null pattern must be rejected with 
InvalidObjectException");
+    }
+
+    /**
+     * Tests that a forged stream whose {@code timeZone} field is {@code null} 
is rejected with {@link InvalidObjectException}.
+     */
+    @Test
+    void testNullTimeZoneRejected() throws IOException {
+        final FastDateParserForge forge = new 
FastDateParserForge("yyyy-MM-dd", null, // timeZone = null (the evil value 
under test)
+                Locale.US, 1900, 0);
+        final byte[] forgedBytes = forgeStream(forge);
+        assertThrows(InvalidObjectException.class, () -> 
deserialize(forgedBytes), "A null timeZone must be rejected with 
InvalidObjectException");
+    }
+}

Reply via email to