Skip to content

Commit f9980f2

Browse files
committed
add (optional) match on class bytes SHA256 digest
1 parent 2f505a7 commit f9980f2

File tree

4 files changed

+117
-39
lines changed

4 files changed

+117
-39
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.gradle.internal.dependencies.patches;
11+
12+
import org.objectweb.asm.ClassVisitor;
13+
import org.objectweb.asm.ClassWriter;
14+
15+
import java.util.Arrays;
16+
import java.util.HexFormat;
17+
import java.util.function.Function;
18+
19+
public record PatcherInfo(String jarEntryName, byte[] classSha256, Function<ClassWriter, ClassVisitor> visitorFactory) {
20+
21+
/**
22+
* Creates a patcher info entry, linking a jar entry path name to a patcher factory (a factory to create an ASM visitor)
23+
* @param jarEntryName the jar entry path, as a string
24+
* @param visitorFactory the factory to create an ASM visitor from a ASM writer
25+
*/
26+
public static PatcherInfo classPatcher(String jarEntryName, Function<ClassWriter, ClassVisitor> visitorFactory) {
27+
return new PatcherInfo(jarEntryName, null, visitorFactory);
28+
}
29+
30+
/**
31+
* Creates a patcher info entry, linking a jar entry path name and its SHA256 digest to a patcher factory (a factory to create an ASM
32+
* visitor)
33+
* @param jarEntryName the jar entry path, as a string
34+
* @param classSha256 the SHA256 digest of the class bytes, as a HEX string
35+
* @param visitorFactory the factory to create an ASM visitor from a ASM writer
36+
*/
37+
public static PatcherInfo classPatcher(String jarEntryName, String classSha256, Function<ClassWriter, ClassVisitor> visitorFactory) {
38+
return new PatcherInfo(jarEntryName, HexFormat.of().parseHex(classSha256), visitorFactory);
39+
}
40+
41+
boolean matches(byte[] otherClassSha256) {
42+
if (this.classSha256 == null) {
43+
return true;
44+
}
45+
return Arrays.equals(this.classSha256, otherClassSha256);
46+
}
47+
}

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/Utils.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,50 @@
1010
package org.elasticsearch.gradle.internal.dependencies.patches;
1111

1212
import org.objectweb.asm.ClassReader;
13-
import org.objectweb.asm.ClassVisitor;
1413
import org.objectweb.asm.ClassWriter;
1514

1615
import java.io.File;
1716
import java.io.FileOutputStream;
1817
import java.io.IOException;
1918
import java.io.InputStream;
19+
import java.security.MessageDigest;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.util.Collection;
2022
import java.util.Enumeration;
21-
import java.util.HashMap;
23+
import java.util.HexFormat;
2224
import java.util.Locale;
23-
import java.util.Map;
2425
import java.util.function.Function;
2526
import java.util.jar.JarEntry;
2627
import java.util.jar.JarFile;
2728
import java.util.jar.JarOutputStream;
29+
import java.util.stream.Collectors;
2830

2931
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
3032
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
3133

3234
public class Utils {
33-
public static void patchJar(File inputFile, File outputFile, Map<String, Function<ClassWriter, ClassVisitor>> patchers) {
34-
var classPatchers = new HashMap<>(patchers);
35+
36+
private static final MessageDigest SHA_256;
37+
38+
static {
39+
try {
40+
SHA_256 = MessageDigest.getInstance("SHA-256");
41+
} catch (NoSuchAlgorithmException e) {
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
/**
47+
* Patches the classes in the input JAR file, using the collection of patchers. If the patcher info specify a SHA256 digest, and
48+
* the class to patch does not match it, an IllegalArgumentException is thrown.
49+
* If the input file does not contain all the classes to patch specified in the patcher info collection, an IllegalArgumentException
50+
* is also thrown.
51+
* @param inputFile the JAR file to patch
52+
* @param outputFile the output (patched) JAR file
53+
* @param patchers list of patcher info (classes to patch (jar entry name + optional SHA256 digest) and ASM visitor to transform them)
54+
*/
55+
public static void patchJar(File inputFile, File outputFile, Collection<PatcherInfo> patchers) {
56+
var classPatchers = patchers.stream().collect(Collectors.toMap(PatcherInfo::jarEntryName, Function.identity()));
3557
try (JarFile jarFile = new JarFile(inputFile); JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputFile))) {
3658
Enumeration<JarEntry> entries = jarFile.entries();
3759
while (entries.hasMoreElements()) {
@@ -40,13 +62,27 @@ public static void patchJar(File inputFile, File outputFile, Map<String, Functio
4062
// Add the entry to the new JAR file
4163
jos.putNextEntry(new JarEntry(entryName));
4264

43-
Function<ClassWriter, ClassVisitor> classPatcher = classPatchers.remove(entryName);
65+
var classPatcher = classPatchers.remove(entryName);
4466
if (classPatcher != null) {
4567
byte[] classToPatch = jarFile.getInputStream(entry).readAllBytes();
68+
var classSha256 = SHA_256.digest(classToPatch);
69+
70+
if (classPatcher.matches(classSha256) == false) {
71+
throw new IllegalArgumentException(
72+
String.format(
73+
Locale.ROOT,
74+
"error patching [%s]: jar entry [%s] digest mismatch (expected: [%s], found: [%s])",
75+
inputFile.getName(),
76+
classPatcher.jarEntryName(),
77+
HexFormat.of().formatHex(classPatcher.classSha256()),
78+
HexFormat.of().formatHex(classSha256)
79+
)
80+
);
81+
}
4682

4783
ClassReader classReader = new ClassReader(classToPatch);
4884
ClassWriter classWriter = new ClassWriter(classReader, COMPUTE_MAXS | COMPUTE_FRAMES);
49-
classReader.accept(classPatcher.apply(classWriter), 0);
85+
classReader.accept(classPatcher.visitorFactory().apply(classWriter), 0);
5086
jos.write(classWriter.toByteArray());
5187
} else {
5288
// Read the entry's data and write it to the new JAR
@@ -66,7 +102,7 @@ public static void patchJar(File inputFile, File outputFile, Map<String, Functio
66102
Locale.ROOT,
67103
"error patching [%s]: the jar does not contain [%s]",
68104
inputFile.getName(),
69-
String.join(", ", patchers.keySet())
105+
String.join(", ", classPatchers.keySet())
70106
)
71107
);
72108
}

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/Awsv2ClassPatcher.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk;
1111

12+
import org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo;
1213
import org.elasticsearch.gradle.internal.dependencies.patches.Utils;
1314
import org.gradle.api.artifacts.transform.CacheableTransform;
1415
import org.gradle.api.artifacts.transform.InputArtifact;
@@ -19,24 +20,25 @@
1920
import org.gradle.api.provider.Provider;
2021
import org.gradle.api.tasks.Classpath;
2122
import org.jetbrains.annotations.NotNull;
22-
import org.objectweb.asm.ClassVisitor;
23-
import org.objectweb.asm.ClassWriter;
2423

2524
import java.io.File;
26-
import java.util.Map;
27-
import java.util.function.Function;
25+
import java.util.List;
2826

29-
import static java.util.Map.entry;
27+
import static org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo.classPatcher;
3028

3129
@CacheableTransform
3230
public abstract class Awsv2ClassPatcher implements TransformAction<TransformParameters.None> {
3331

3432
private static final String JAR_FILE_TO_PATCH = "aws-query-protocol";
3533

36-
private static final Map<String, Function<ClassWriter, ClassVisitor>> CLASS_PATCHERS = Map.ofEntries(
34+
private static final List<PatcherInfo> CLASS_PATCHERS = List.of(
3735
// This patcher is needed because of this AWS bug: https://github.com/aws/aws-sdk-java-v2/issues/5968
3836
// As soon as the bug is resolved and we upgrade our AWS SDK v2 libraries, we can remove this.
39-
entry("software/amazon/awssdk/protocols/query/internal/marshall/ListQueryMarshaller.class", StringFormatInPathResolverPatcher::new)
37+
classPatcher(
38+
"software/amazon/awssdk/protocols/query/internal/marshall/ListQueryMarshaller.class",
39+
"213e84d9a745bdae4b844334d17aecdd6499b36df32aa73f82dc114b35043009",
40+
StringFormatInPathResolverPatcher::new
41+
)
4042
);
4143

4244
@Classpath
@@ -47,13 +49,13 @@ public abstract class Awsv2ClassPatcher implements TransformAction<TransformPara
4749
public void transform(@NotNull TransformOutputs outputs) {
4850
File inputFile = getInputArtifact().get().getAsFile();
4951

50-
if (inputFile.getName().startsWith(JAR_FILE_TO_PATCH) == false) {
51-
System.out.println("Skipping " + inputFile.getName());
52-
outputs.file(getInputArtifact());
53-
} else {
52+
if (inputFile.getName().startsWith(JAR_FILE_TO_PATCH)) {
5453
System.out.println("Patching " + inputFile.getName());
5554
File outputFile = outputs.file(inputFile.getName().replace(".jar", "-patched.jar"));
5655
Utils.patchJar(inputFile, outputFile, CLASS_PATCHERS);
56+
} else {
57+
System.out.println("Skipping " + inputFile.getName());
58+
outputs.file(getInputArtifact());
5759
}
5860
}
5961
}

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/hdfs/HdfsClassPatcher.java

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.gradle.internal.dependencies.patches.hdfs;
1111

12+
import org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo;
1213
import org.elasticsearch.gradle.internal.dependencies.patches.Utils;
1314
import org.gradle.api.artifacts.transform.CacheableTransform;
1415
import org.gradle.api.artifacts.transform.InputArtifact;
@@ -21,41 +22,36 @@
2122
import org.gradle.api.tasks.Input;
2223
import org.gradle.api.tasks.Optional;
2324
import org.jetbrains.annotations.NotNull;
24-
import org.objectweb.asm.ClassVisitor;
25-
import org.objectweb.asm.ClassWriter;
2625

2726
import java.io.File;
28-
import java.util.HashMap;
2927
import java.util.List;
30-
import java.util.Map;
31-
import java.util.function.Function;
3228
import java.util.regex.Pattern;
3329

34-
import static java.util.Map.entry;
30+
import static org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo.classPatcher;
3531

3632
@CacheableTransform
3733
public abstract class HdfsClassPatcher implements TransformAction<HdfsClassPatcher.Parameters> {
3834

39-
record JarPatchers(String artifactTag, Pattern artifactPattern, Map<String, Function<ClassWriter, ClassVisitor>> jarPatchers) {}
35+
record JarPatchers(String artifactTag, Pattern artifactPattern, List<PatcherInfo> jarPatchers) {}
4036

4137
static final List<JarPatchers> allPatchers = List.of(
4238
new JarPatchers(
4339
"hadoop-common",
4440
Pattern.compile("hadoop-common-(?!.*tests)"),
45-
Map.ofEntries(
46-
entry("org/apache/hadoop/util/ShutdownHookManager.class", ShutdownHookManagerPatcher::new),
47-
entry("org/apache/hadoop/util/Shell.class", ShellPatcher::new),
48-
entry("org/apache/hadoop/security/UserGroupInformation.class", SubjectGetSubjectPatcher::new)
41+
List.of(
42+
classPatcher("org/apache/hadoop/util/ShutdownHookManager.class", ShutdownHookManagerPatcher::new),
43+
classPatcher("org/apache/hadoop/util/Shell.class", ShellPatcher::new),
44+
classPatcher("org/apache/hadoop/security/UserGroupInformation.class", SubjectGetSubjectPatcher::new)
4945
)
5046
),
5147
new JarPatchers(
5248
"hadoop-client-api",
5349
Pattern.compile("hadoop-client-api.*"),
54-
Map.ofEntries(
55-
entry("org/apache/hadoop/util/ShutdownHookManager.class", ShutdownHookManagerPatcher::new),
56-
entry("org/apache/hadoop/util/Shell.class", ShellPatcher::new),
57-
entry("org/apache/hadoop/security/UserGroupInformation.class", SubjectGetSubjectPatcher::new),
58-
entry("org/apache/hadoop/security/authentication/client/KerberosAuthenticator.class", SubjectGetSubjectPatcher::new)
50+
List.of(
51+
classPatcher("org/apache/hadoop/util/ShutdownHookManager.class", ShutdownHookManagerPatcher::new),
52+
classPatcher("org/apache/hadoop/util/Shell.class", ShellPatcher::new),
53+
classPatcher("org/apache/hadoop/security/UserGroupInformation.class", SubjectGetSubjectPatcher::new),
54+
classPatcher("org/apache/hadoop/security/authentication/client/KerberosAuthenticator.class", SubjectGetSubjectPatcher::new)
5955
)
6056
)
6157
);
@@ -85,11 +81,8 @@ public void transform(@NotNull TransformOutputs outputs) {
8581
} else {
8682
patchersToApply.forEach(patchers -> {
8783
System.out.println("Patching " + inputFile.getName());
88-
89-
Map<String, Function<ClassWriter, ClassVisitor>> jarPatchers = new HashMap<>(patchers.jarPatchers());
9084
File outputFile = outputs.file(inputFile.getName().replace(".jar", "-patched.jar"));
91-
92-
Utils.patchJar(inputFile, outputFile, jarPatchers);
85+
Utils.patchJar(inputFile, outputFile, patchers.jarPatchers());
9386
});
9487
}
9588
}

0 commit comments

Comments
 (0)