| /* | |
| * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package java.io; | |
| import java.lang.reflect.Field; | |
| import java.lang.reflect.Member; | |
| import java.lang.reflect.Method; | |
| import java.lang.reflect.Proxy; | |
| import java.util.Arrays; | |
| import static jdk.internal.event.SerializationMisdeclarationEvent.*; | |
| import static java.lang.reflect.Modifier.*; | |
| final class SerializationMisdeclarationChecker { | |
| private static final String SUID_NAME = "serialVersionUID"; | |
| private static final String SERIAL_PERSISTENT_FIELDS_NAME = "serialPersistentFields"; | |
| private static final String WRITE_OBJECT_NAME = "writeObject"; | |
| private static final String READ_OBJECT_NAME = "readObject"; | |
| private static final String READ_OBJECT_NO_DATA_NAME = "readObjectNoData"; | |
| private static final String WRITE_REPLACE_NAME = "writeReplace"; | |
| private static final String READ_RESOLVE_NAME = "readResolve"; | |
| private static final Class<?>[] WRITE_OBJECT_PARAM_TYPES = {ObjectOutputStream.class}; | |
| private static final Class<?>[] READ_OBJECT_PARAM_TYPES = {ObjectInputStream.class}; | |
| /* | |
| * The sharing of a single Class<?>[] instance here is just to avoid wasting | |
| * space, and should not be considered as a conceptual sharing of types. | |
| */ | |
| private static final Class<?>[] READ_OBJECT_NO_DATA_PARAM_TYPES = {}; | |
| private static final Class<?>[] WRITE_REPLACE_PARAM_TYPES = READ_OBJECT_NO_DATA_PARAM_TYPES; | |
| private static final Class<?>[] READ_RESOLVE_PARAM_TYPES = READ_OBJECT_NO_DATA_PARAM_TYPES; | |
| static void checkMisdeclarations(Class<?> cl) { | |
| checkSerialVersionUID(cl); | |
| checkSerialPersistentFields(cl); | |
| checkPrivateMethod(cl, WRITE_OBJECT_NAME, | |
| WRITE_OBJECT_PARAM_TYPES, Void.TYPE); | |
| checkPrivateMethod(cl, READ_OBJECT_NAME, | |
| READ_OBJECT_PARAM_TYPES, Void.TYPE); | |
| checkPrivateMethod(cl, READ_OBJECT_NO_DATA_NAME, | |
| READ_OBJECT_NO_DATA_PARAM_TYPES, Void.TYPE); | |
| checkAccessibleMethod(cl, WRITE_REPLACE_NAME, | |
| WRITE_REPLACE_PARAM_TYPES, Object.class); | |
| checkAccessibleMethod(cl, READ_RESOLVE_NAME, | |
| READ_RESOLVE_PARAM_TYPES, Object.class); | |
| } | |
| private static void checkSerialVersionUID(Class<?> cl) { | |
| Field f = declaredField(cl, SUID_NAME); | |
| if (f == null) { | |
| if (isOrdinaryClass(cl)) { | |
| commitEvent(cl, SUID_NAME + " should be declared explicitly" + | |
| " as a private static final long field"); | |
| } | |
| return; | |
| } | |
| if (cl.isEnum()) { | |
| commitEvent(cl, SUID_NAME + " should not be declared in an enum class"); | |
| } | |
| if (!isPrivate(f)) { | |
| commitEvent(cl, SUID_NAME + " should be private"); | |
| } | |
| if (!isStatic(f)) { | |
| commitEvent(cl, SUID_NAME + " must be static"); | |
| } | |
| if (!isFinal(f)) { | |
| commitEvent(cl, SUID_NAME + " must be final"); | |
| } | |
| if (f.getType() != Long.TYPE) { | |
| commitEvent(cl, SUID_NAME + " must be of type long"); | |
| } | |
| } | |
| private static void checkSerialPersistentFields(Class<?> cl) { | |
| Field f = declaredField(cl, SERIAL_PERSISTENT_FIELDS_NAME); | |
| if (f == null) { | |
| return; | |
| } | |
| if (cl.isRecord()) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + | |
| " should not be declared in a record class"); | |
| } else if (cl.isEnum()) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + | |
| " should not be declared in an enum class"); | |
| } | |
| if (!isPrivate(f)) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be private"); | |
| } | |
| if (!isStatic(f)) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be static"); | |
| } | |
| if (!isFinal(f)) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " must be final"); | |
| } | |
| if (f.getType() != ObjectStreamField[].class) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + | |
| " should be of type ObjectStreamField[]"); | |
| } | |
| if (!isStatic(f)) { | |
| return; | |
| } | |
| f.setAccessible(true); | |
| Object spf = objectFromStatic(f); | |
| if (spf == null) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + " should be non-null"); | |
| return; | |
| } | |
| if (!(spf instanceof ObjectStreamField[])) { | |
| commitEvent(cl, SERIAL_PERSISTENT_FIELDS_NAME + | |
| " must be an instance of ObjectStreamField[]"); | |
| } | |
| } | |
| private static void checkPrivateMethod(Class<?> cl, | |
| String name, Class<?>[] paramTypes, Class<?> retType) { | |
| for (Method m : cl.getDeclaredMethods()) { | |
| if (m.getName().equals(name)) { | |
| checkPrivateMethod(cl, m, paramTypes, retType); | |
| } | |
| } | |
| } | |
| private static void checkPrivateMethod(Class<?> cl, | |
| Method m, Class<?>[] paramTypes, Class<?> retType) { | |
| if (cl.isEnum()) { | |
| commitEvent(cl, "method " + m + " should not be declared in an enum class"); | |
| } else if (cl.isRecord()) { | |
| commitEvent(cl, "method " + m + " should not be declared in a record class"); | |
| } | |
| if (!isPrivate(m)) { | |
| commitEvent(cl, "method " + m + " must be private"); | |
| } | |
| if (isStatic(m)) { | |
| commitEvent(cl, "method " + m + " must be non-static"); | |
| } | |
| if (m.getReturnType() != retType) { | |
| commitEvent(cl, "method " + m + " must have return type " + retType); | |
| } | |
| if (!Arrays.equals(m.getParameterTypes(), paramTypes)) { | |
| commitEvent(cl, "method " + m + " must have parameter types " + Arrays.toString(paramTypes)); | |
| } | |
| } | |
| private static void checkAccessibleMethod(Class<?> cl, | |
| String name, Class<?>[] paramTypes, Class<?> retType) { | |
| for (Class<?> superCl = cl; superCl != null; superCl = superCl.getSuperclass()) { | |
| for (Method m : superCl.getDeclaredMethods()) { | |
| if (m.getName().equals(name)) { | |
| checkAccessibleMethod(cl, superCl, m, paramTypes, retType); | |
| } | |
| } | |
| } | |
| } | |
| private static void checkAccessibleMethod(Class<?> cl, | |
| Class<?> superCl, Method m, Class<?>[] paramTypes, Class<?> retType) { | |
| if (superCl.isEnum()) { | |
| commitEvent(cl, "method " + m + " should not be declared in an enum class"); | |
| } | |
| if (isAbstract(m)) { | |
| commitEvent(cl, "method " + m + " must be non-abstract"); | |
| } | |
| if (isStatic(m)) { | |
| commitEvent(cl, "method " + m + " must be non-static"); | |
| } | |
| if (m.getReturnType() != retType) { | |
| commitEvent(cl, "method " + m + " must have return type " + retType); | |
| } | |
| if (!Arrays.equals(m.getParameterTypes(), paramTypes)) { | |
| commitEvent(cl, "method " + m + " must have parameter types " + Arrays.toString(paramTypes)); | |
| } | |
| if (isPrivate(m) && cl != superCl | |
| || isPackageProtected(m) && !isSamePackage(cl, superCl)) { | |
| commitEvent(cl, "method " + m + " is not accessible"); | |
| } | |
| } | |
| private static boolean isSamePackage(Class<?> cl0, Class<?> cl1) { | |
| return cl0.getClassLoader() == cl1.getClassLoader() | |
| && cl0.getPackageName().equals(cl1.getPackageName()); | |
| } | |
| private static boolean isOrdinaryClass(Class<?> cl) { | |
| /* class Enum and class Record are not considered ordinary classes */ | |
| return !(cl.isRecord() || cl.isEnum() || cl.isArray() | |
| || Enum.class == cl || Record.class == cl | |
| || Proxy.isProxyClass(cl)); | |
| } | |
| private static boolean isPrivate(Member m) { | |
| return (m.getModifiers() & PRIVATE) != 0; | |
| } | |
| private static boolean isPackageProtected(Member m) { | |
| return (m.getModifiers() & (PRIVATE | PROTECTED | PUBLIC)) == 0; | |
| } | |
| private static boolean isAbstract(Member m) { | |
| return (m.getModifiers() & ABSTRACT) != 0; | |
| } | |
| private static boolean isFinal(Member m) { | |
| return (m.getModifiers() & FINAL) != 0; | |
| } | |
| private static boolean isStatic(Member m) { | |
| return (m.getModifiers() & STATIC) != 0; | |
| } | |
| private static Field declaredField(Class<?> cl, String name) { | |
| try { | |
| return cl.getDeclaredField(name); | |
| } catch (NoSuchFieldException ignored) { | |
| } | |
| return null; | |
| } | |
| private static Object objectFromStatic(Field f) { | |
| try { | |
| return f.get(null); | |
| } catch (IllegalAccessException ignored) { | |
| } | |
| return null; | |
| } | |
| private static void commitEvent(Class<?> cl, String msg) { | |
| commit(timestamp(), cl, msg); | |
| } | |
| } |