You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2024/01/09 15:26:06 UTC

(groovy) 01/01: GROOVY-11272: Generate `serialVersionUID` for each closure

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

sunlan pushed a commit to branch GROOVY-11272
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit c48126fb432ab4525ed089ff04669e22320f7336
Author: Daniel Sun <su...@apache.org>
AuthorDate: Tue Jan 9 23:11:06 2024 +0800

    GROOVY-11272: Generate `serialVersionUID` for each closure
---
 .../groovy/classgen/asm/ClosureWriter.java         | 31 ++++++++
 .../classgen/asm/sc/StaticTypesLambdaWriter.java   |  6 --
 src/test/groovy/bugs/Groovy11272.groovy            | 90 ++++++++++++++++++++++
 3 files changed, 121 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
index eb9e2fc613..42c2d206de 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
@@ -41,6 +41,9 @@ import org.codehaus.groovy.ast.tools.GenericsUtils;
 import org.codehaus.groovy.classgen.AsmClassGenerator;
 import org.objectweb.asm.MethodVisitor;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -48,8 +51,10 @@ import java.util.Map;
 
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
 import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod;
+import static org.codehaus.groovy.ast.ClassHelper.long_TYPE;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorSuperX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.fieldX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getGetterName;
@@ -257,9 +262,35 @@ public class ClosureWriter {
 
         correctAccessedVariable(answer, expression);
 
+        addSerialVersionUIDField(answer);
+
         return answer;
     }
 
+    protected void addSerialVersionUIDField(final ClassNode classNode) {
+        // just to hash the full class name for better performance.
+        // The full spec for `serialVersionUID` is here:
+        //      https://docs.oracle.com/en/java/javase/21/docs/specs/serialization/class.html#stream-unique-identifiers
+        // As we could see, it's too complex for closures.
+        long serialVersionUID = hash(classNode.getName());
+        classNode.addFieldFirst("serialVersionUID", ACC_PRIVATE | ACC_STATIC | ACC_FINAL, long_TYPE, constX(serialVersionUID, true));
+    }
+
+    private static long hash(String str) {
+        final MessageDigest md;
+        try {
+            md = MessageDigest.getInstance("SHA");
+        } catch (NoSuchAlgorithmException e) {
+            throw new GroovyBugError("Failed to find SHA", e);
+        }
+        final byte[] hashBytes = md.digest(str.getBytes(StandardCharsets.UTF_8));
+        long hash = 0;
+        for (int i = Math.min(hashBytes.length, 7); i >= 0; i--) {
+            hash = (hash << 8) | (hashBytes[i] & 0xFF);
+        }
+        return hash;
+    }
+
     protected ConstructorNode addConstructor(final ClosureExpression expression, final Parameter[] localVariableParams, final InnerClassNode answer, final BlockStatement block) {
         Parameter[] params = new Parameter[2 + localVariableParams.length];
         params[0] = new Parameter(ClassHelper.OBJECT_TYPE, OUTER_INSTANCE);
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
index 8841d1ce77..843c834f16 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesLambdaWriter.java
@@ -56,11 +56,9 @@ import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.SERIALIZABLE_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.SERIALIZEDLAMBDA_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE;
-import static org.codehaus.groovy.ast.ClassHelper.long_TYPE;
 import static org.codehaus.groovy.ast.tools.ClosureUtils.getParametersSafe;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
@@ -263,10 +261,6 @@ public class StaticTypesLambdaWriter extends LambdaWriter implements AbstractFun
         return enclosingClass.getName() + "$" + controller.getContext().getNextLambdaInnerName(outermostClass, enclosingClass, controller.getMethodNode());
     }
 
-    private static void addSerialVersionUIDField(final ClassNode lambdaClass) {
-        lambdaClass.addFieldFirst("serialVersionUID", ACC_PRIVATE | ACC_STATIC | ACC_FINAL, long_TYPE, constX(-1L, true));
-    }
-
     private MethodNode addSyntheticLambdaMethodNode(final LambdaExpression expression, final ClassNode lambdaClass, final MethodNode abstractMethod) {
         Parameter[] parametersWithExactType = createParametersWithExactType(expression, abstractMethod);
         Parameter[] localVariableParameters = getLambdaSharedVariables(expression);
diff --git a/src/test/groovy/bugs/Groovy11272.groovy b/src/test/groovy/bugs/Groovy11272.groovy
new file mode 100644
index 0000000000..9753241a7e
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy11272.groovy
@@ -0,0 +1,90 @@
+/*
+ *  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
+ *
+ *    http://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 bugs
+
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+final class Groovy11272 {
+    @Test
+    void testSerialVersionUID_1() {
+        assertScript '''
+            def c = {->}
+            def serialVersionUIDField = c.class.getDeclaredField('serialVersionUID')
+            assert null != serialVersionUIDField
+        '''
+    }
+
+    @Test
+    void testSerialVersionUID_2() {
+        assertScript '''
+            class C {
+                static test() {
+                    def out = new ByteArrayOutputStream()
+                    out.withObjectOutputStream {
+                        def c = {p -> "Hello, $p"}
+                        it.writeObject(c)
+                    }
+                    out.toByteArray()
+                }
+            }
+            def serializedClosureBytes = C.test()
+            new ByteArrayInputStream(serializedClosureBytes).withObjectInputStream(this.class.classLoader) {
+                def c = it.readObject()
+                assert c.call('Daniel') == 'Hello, Daniel'
+            }
+        '''
+    }
+
+    @Test
+    void testSerialVersionUID_1_CS() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def test() {
+                def c = {->}
+                def serialVersionUIDField = c.class.getDeclaredField('serialVersionUID')
+                assert null != serialVersionUIDField
+            }
+            test()
+        '''
+    }
+
+    @Test
+    void testSerialVersionUID_2_CS() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            class C {
+                static test() {
+                    def out = new ByteArrayOutputStream()
+                    out.withObjectOutputStream {
+                        def c = {p -> "Hello, $p"}
+                        it.writeObject(c)
+                    }
+                    out.toByteArray()
+                }
+            }
+            def serializedClosureBytes = C.test()
+            new ByteArrayInputStream(serializedClosureBytes).withObjectInputStream(this.class.classLoader) {
+                def c = it.readObject()
+                assert c.call('Daniel') == 'Hello, Daniel'
+            }
+        '''
+    }
+}