This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
The following commit(s) were added to refs/heads/8.0.x by this push:
new 33964e780e Make the spring security acl jar reproducible
33964e780e is described below
commit 33964e780e1ec44856033e2c42773af20c2e6018
Author: James Daugherty <[email protected]>
AuthorDate: Sat Jul 11 13:20:43 2026 -0400
Make the spring security acl jar reproducible
---
.../transform/DetachedCriteriaTransformer.java | 18 ++++
.../transform/WhereQueryClosureCaptureSpec.groovy | 103 +++++++++++++++++++++
2 files changed, 121 insertions(+)
diff --git
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
index 78ec7c22b6..728508a605 100644
---
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
+++
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
@@ -68,6 +68,7 @@ import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.control.ErrorCollector;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.LocatedMessage;
import org.codehaus.groovy.syntax.Token;
@@ -101,6 +102,7 @@ public class DetachedCriteriaTransformer extends
ClassCodeVisitorSupport {
public static final ConstantExpression WHERE_LAZY = new
ConstantExpression("whereLazy");
private SourceUnit sourceUnit;
+ private boolean appliedWhereTransform;
private static final Set<String> CANDIDATE_METHODS = newSet("where",
"whereLazy", "whereAny", "findAll", "find");
private static final Set<String> SUPPORTED_FUNCTIONS = newSet(
@@ -174,7 +176,22 @@ public class DetachedCriteriaTransformer extends
ClassCodeVisitorSupport {
public void visitClass(ClassNode node) {
try {
this.currentClassNode = node;
+ this.appliedWhereTransform = false;
super.visitClass(node);
+ if (appliedWhereTransform) {
+ // The nested closures generated for association criteria
share the outer
+ // where-closure's VariableScope, which still references every
local variable the
+ // original closure used. Which locals a generated closure
captures then depends on
+ // whether a later transformation happens to recompute the
scopes, making the
+ // compiled closure constructors nondeterministic. Recompute
the scopes now so each
+ // generated closure captures exactly the variables it
references.
+ // A throwaway ErrorCollector is used because the code has
already been scope-checked
+ // once and re-running the visitor must not report duplicate
errors (see
+ // AbstractMethodDecoratingTransformation for the same
pattern).
+ SourceUnit dummySourceUnit = new SourceUnit("dummy", "dummy",
sourceUnit.getConfiguration(),
+ sourceUnit.getClassLoader(), new
ErrorCollector(sourceUnit.getConfiguration()));
+ AstUtils.processVariableScopes(dummySourceUnit, node, null);
+ }
} catch (Exception e) {
logTransformationError(node, e);
} finally {
@@ -627,6 +644,7 @@ public class DetachedCriteriaTransformer extends
ClassCodeVisitorSupport {
if (!newCode.getStatements().isEmpty()) {
closureExpression.putNodeMetaData(TRANSFORMED_MARKER,
Boolean.TRUE);
closureExpression.setCode(newCode);
+ appliedWhereTransform = true;
}
} finally {
this.currentClassNode = previousClassNode;
diff --git
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/transform/WhereQueryClosureCaptureSpec.groovy
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/transform/WhereQueryClosureCaptureSpec.groovy
new file mode 100644
index 0000000000..f57617b856
--- /dev/null
+++
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/transform/WhereQueryClosureCaptureSpec.groovy
@@ -0,0 +1,103 @@
+/*
+ * 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.grails.datastore.gorm.query.transform
+
+import java.lang.reflect.Constructor
+
+import groovy.lang.Reference
+import spock.lang.Specification
+
+/**
+ * The where-query transform generates nested closures for association
criteria. Those closures
+ * must capture exactly the local variables they reference; historically they
shared the outer
+ * where-closure's VariableScope, so their captures depended on whether a
later transformation
+ * happened to recompute the scopes, producing nondeterministic bytecode
between builds.
+ */
+class WhereQueryClosureCaptureSpec extends Specification {
+
+ private static final String SERVICE_SOURCE = '''
+import grails.gorm.DetachedCriteria
+import grails.gorm.annotation.Entity
+
+class BookQueryService {
+ protected DetachedCriteria<Book>
findQueryByBookIdAndAuthorName(Serializable bookId, String authorName) {
+ Book.where {
+ bookId == bookId && author.name == authorName
+ }
+ }
+}
+
+@Entity
+class Book {
+ Long bookId
+ Author author
+}
+
+@Entity
+class Author {
+ String name
+}
+'''
+
+ void "association criteria closures capture only the variables they
reference"() {
+ given: 'a where query with an association criterion referencing one of
two parameters'
+ GroovyClassLoader gcl = new GroovyClassLoader()
+ gcl.parseClass(SERVICE_SOURCE)
+
+ when: 'the generated closure classes for the where query are located'
+ List<Class<?>> queryClosures = findQueryClosures(gcl).sort {
it.name.count('$_closure') }
+ Class<?> outerClosure = queryClosures.first()
+ Class<?> associationClosure = queryClosures.last()
+
+ then: 'the outer closure captures both parameters it references'
+ outerClosure != null
+ capturedReferenceCount(outerClosure) == 2
+
+ and: 'the innermost association closure captures only the parameter it
references'
+ associationClosure.name.count('$_closure') > 1
+ capturedReferenceCount(associationClosure) == 1
+ }
+
+ void "generated closure constructors are identical across compilations"() {
+ when: 'the same source is compiled twice in isolated class loaders'
+ List<String> firstSignatures = closureConstructorSignatures(new
GroovyClassLoader())
+ List<String> secondSignatures = closureConstructorSignatures(new
GroovyClassLoader())
+
+ then:
+ !firstSignatures.empty
+ firstSignatures == secondSignatures
+ }
+
+ private static List<String> closureConstructorSignatures(GroovyClassLoader
gcl) {
+ gcl.parseClass(SERVICE_SOURCE)
+ findQueryClosures(gcl).collect { Class<?> closureClass ->
+ Constructor<?> constructor =
closureClass.declaredConstructors.first()
+ "${closureClass.name.replaceAll(/^.*\$_/,
'')}(${constructor.parameterTypes*.simpleName.join(', ')})".toString()
+ }.sort()
+ }
+
+ private static List<Class<?>> findQueryClosures(GroovyClassLoader gcl) {
+ gcl.loadedClasses.findAll {
it.name.contains('_findQueryByBookIdAndAuthorName_') }
+ }
+
+ private static int capturedReferenceCount(Class<?> closureClass) {
+ Constructor<?> constructor = closureClass.declaredConstructors.first()
+ constructor.parameterTypes.count { it == Reference }
+ }
+}