Skip to content

Commit

Permalink
add static transformer directly to jma
Browse files Browse the repository at this point in the history
  • Loading branch information
wagyourtail committed Aug 6, 2023
1 parent 007ee1e commit ffeec81
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 32 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ tasks.jar {
manifest {
attributes(
"Premain-Class" to "xyz.wagyourtail.unimined.jarmodagent.JarModAgent",
"Main-Class" to "xyz.wagyourtail.unimined.jarmodagent.JarModAgent",
"Implementation-Version" to project.version,
)
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1G
version=0.1.2
version=0.1.3
maven_group=xyz.wagyourtail.unimined
archives_base_name=jarmod-agent
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@

import xyz.wagyourtail.unimined.jarmodagent.transformer.JarModder;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class JarModAgent {
public static final boolean DEBUG = Boolean.getBoolean("jma.debug");
Expand All @@ -31,6 +43,17 @@ public class JarModAgent {
*/
public static final String MODS_FOLDER = "jma.modsFolder";

/**
* Disable the mods folder. this will prevent the mods folder from being searched and appended to the priority classpath.
*/
public static final String DISABLE_MODS_FOLDER = "jma.disableModsFolder";

/**
* Load the mods folder to the system classloader. this will load all classes in the mods folder to the system classloader
* this should be set to false if another thing is loading the mods folder to a classloader
*/
public static final String DISABLE_INSERT_INTO_SYSTEM_CL = "jma.disableInsertIntoSystemCL";

public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, ClassNotFoundException {
premain(agentArgs, inst);
}
Expand All @@ -39,7 +62,7 @@ public static void premain(String agentArgs, Instrumentation instrumentation) th
System.out.println("[JarModAgent] Starting agent");
System.out.println("[JarModAgent] Version: " + VERSION);
JarModder jarModder = new JarModder(instrumentation);
jarModder.registerTransforms();
jarModder.registerTransforms(new File[0]);
instrumentation.addTransformer(jarModder);
System.out.println("[JarModAgent] Agent started");

Expand All @@ -66,4 +89,57 @@ public static void premain(String agentArgs, Instrumentation instrumentation) th
}
}

/**
* this endpoint is for statically transforming the jar file.
* this is used for the gradle plugin, so I don't have to include lenni/classtransform in unimined.
* args:
* 0: path to input jar
* 1: classpath, File.pathSeparator separated
* 2: output jar path
* @param args
*/
public static void main(String[] args) throws IOException, IllegalClassFormatException {
System.setProperty(DISABLE_MODS_FOLDER, "true");
System.setProperty(DISABLE_INSERT_INTO_SYSTEM_CL, "true");
JarModder jarModder = new JarModder(null);
jarModder.registerTransforms(new File[] {
new File(args[0])
});
String[] classpath = args[1].split(File.pathSeparator);
URL[] urls = new URL[classpath.length + 1];
for (int i = 0; i < classpath.length; i++) {
urls[i] = new File(classpath[i]).toURI().toURL();
}
urls[urls.length - 1] = new File(args[0]).toURI().toURL();
URLClassLoader loader = new URLClassLoader(urls, null);
Set<String> targets = jarModder.getTargetClasses();
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(Paths.get(args[2]), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(Paths.get(args[0])))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
zos.putNextEntry(entry);
if (entry.isDirectory()) {
zos.closeEntry();
continue;
}
// in case transforms itself :concern:
if (entry.getName().endsWith(".class")) {
String className = entry.getName().substring(0, entry.getName().length() - 6);
if (targets.contains(JarModder.dot(className))) continue;
}
zos.write(JarModder.readAllBytes(zis));
zos.closeEntry();
}
}
for (String targetClass : targets) {
zos.putNextEntry(new ZipEntry(targetClass.replace('.', '/') + ".class"));
zos.write(jarModder.transform(loader, targetClass.replace('.', '/'), null, null, JarModder.readAllBytes(Objects.requireNonNull(loader.getResourceAsStream(targetClass.replace('.', '/') + ".class")))));
zos.closeEntry();
}
} catch (Exception e) {
Files.delete(Paths.get(args[2]));
throw e;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package xyz.wagyourtail.unimined.jarmodagent.transformer;

import net.lenni0451.classtransform.utils.tree.BasicClassProvider;
import net.lenni0451.classtransform.utils.tree.IClassProvider;

import java.io.InputStream;
import java.util.Map;
import java.util.function.Supplier;

public class ClassProviderWithFallback implements IClassProvider {
PriorityClasspath priorityClasspath;
ClassLoader fallback;
BasicClassProvider fallback2 = new BasicClassProvider();


public ClassProviderWithFallback(PriorityClasspath priorityClasspath, ClassLoader fallback) {
this.priorityClasspath = priorityClasspath;
this.fallback = fallback;
}

public void setFallback(ClassLoader fallback) {
this.fallback = fallback;
}

@Override
public byte[] getClass(String name) {
InputStream is = priorityClasspath.getResourceAsStream(name.replace('.', '/') + ".class");
if (is == null && fallback != null) {
is = fallback.getResourceAsStream(name.replace('.', '/') + ".class");
}
if (is == null) {
return fallback2.getClass(name);
}
return JarModder.readAllBytes(is);
}

public Map<String, Supplier<byte[]>> getAllClasses() {
throw new UnsupportedOperationException("Cannot use wildcards yet");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
public class JarModder implements ClassFileTransformer {

private final String[] transformers;
private final PriorityClasspath priorityClasspath;
private final ClassProviderWithFallback classProvider;
private final Instrumentation instrumentation;
private final File modsFolder;
private final TransformerManager transformerManager;
Expand All @@ -37,7 +37,7 @@ public JarModder(Instrumentation instrumentation) {
transformers = Optional.ofNullable(System.getProperty(JarModAgent.TRANSFORMERS))
.map(it -> it.split(File.pathSeparator))
.orElse(new String[0]);
priorityClasspath = new PriorityClasspath(Optional.ofNullable(System.getProperty(JarModAgent.PRIORITY_CLASSPATH))
classProvider = new ClassProviderWithFallback(new PriorityClasspath(Optional.ofNullable(System.getProperty(JarModAgent.PRIORITY_CLASSPATH))
.map(it -> Arrays.stream(
it.split(File.pathSeparator)).map(e -> {
try {
Expand All @@ -46,34 +46,41 @@ public JarModder(Instrumentation instrumentation) {
throw new RuntimeException(ex);
}
}).toArray(URL[]::new))
.orElse(new URL[0]));
modsFolder = Optional.ofNullable(System.getProperty(JarModAgent.MODS_FOLDER)).map(File::new).orElse(null);
.orElse(new URL[0])), null);

transformerManager = new TransformerManager(priorityClasspath);
if (!Boolean.getBoolean(JarModAgent.DISABLE_MODS_FOLDER)) {
modsFolder = Optional.ofNullable(System.getProperty(JarModAgent.MODS_FOLDER)).map(File::new).orElse(new File("mods"));
} else {
modsFolder = null;
}

transformerManager = new TransformerManager(classProvider);
debug("Args: ");
debug(" Transformers: " + Arrays.toString(transformers));
debug(" Priority classpath: " + Arrays.toString(priorityClasspath.getURLs()));
debug(" Priority classpath: " + Arrays.toString(classProvider.priorityClasspath.getURLs()));
debug(" Mods folder: " + modsFolder);

}

public void registerTransforms() throws IOException {
TransformerListBuilder builder = new TransformerListBuilder(priorityClasspath);
public void registerTransforms(File[] extra) throws IOException {
TransformerListBuilder builder = new TransformerListBuilder(classProvider.priorityClasspath);
System.out.println("[JarModAgent] Registering transforms");
debug("Transformers: " + Arrays.toString(transformers));
if (modsFolder != null)
boostrapModsFolder(builder);
for (File file : extra) {
boostrapModJar(file, builder);
}
for (String transformer : transformers) {
builder.addTransformer(transformer);
}
if (modsFolder != null) {
boostrapModsFolder();
}
System.out.println("[JarModAgent] Building transform list");
transformerList = builder.build(transformerManager, priorityClasspath);
transformerList = builder.build(transformerManager, classProvider);
debug("Transformer list: " + transformerList);
System.out.println("[JarModAgent] Building transform list done");
System.out.println("[JarModAgent] Building transform list done, " + transformerList.size() + " classes targeted");
}

public void boostrapModsFolder() {
private void boostrapModsFolder(TransformerListBuilder builder) {
System.out.println("[JarModAgent] Bootstrapping mods folder");
File[] files = modsFolder.listFiles();
if (files == null) {
Expand All @@ -85,31 +92,38 @@ public void boostrapModsFolder() {
for (File file : files) {
if (file.isFile()) {
try {
file = file.getCanonicalFile();
JarFile jf = new JarFile(file);
System.out.println("[JarModAgent] Bootstrapping " + file.getName());
priorityClasspath.addURL(file.toURI().toURL());
//TODO: maybe not to bootstrap classloader?
instrumentation.appendToSystemClassLoaderSearch(jf);
// get transforms from manifest
String transforms = jf.getManifest().getMainAttributes().getValue("JarModAgent-Transforms");
if (transforms != null) {
for (String transformer : transforms.split(",")) {
transformerManager.addTransformer(transformer);
j++;
}
}
j += boostrapModJar(file, builder);
i++;
} catch (IOException e) {
System.out.println("[JarModAgent] Failed to bootstrap " + file.getName());
System.out.println("[JarModAgent] " + e.getMessage());
continue;
}
}
}
System.out.println("[JarModAgent] Bootstrapped " + i + " mods, with " + j + " transforms");
}

private int boostrapModJar(File file, TransformerListBuilder builder) throws IOException {
int j = 0;
file = file.getCanonicalFile();
JarFile jf = new JarFile(file);
System.out.println("[JarModAgent] Bootstrapping " + file.getName());
classProvider.priorityClasspath.addURL(file.toURI().toURL());
//TODO: maybe not to bootstrap classloader?
if (!Boolean.getBoolean(JarModAgent.DISABLE_INSERT_INTO_SYSTEM_CL)) {
instrumentation.appendToSystemClassLoaderSearch(jf);
}
// get transforms from manifest
String transforms = jf.getManifest().getMainAttributes().getValue("JarModAgent-Transforms");
if (transforms != null) {
for (String transformer : transforms.split(",")) {
builder.addTransformer(transformer);
j++;
}
}
return j;
}

public static String dot(String className) {
return className.replace('/', '.');
}
Expand Down Expand Up @@ -215,10 +229,11 @@ private String calculateHash(URL url) {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
classProvider.setFallback(loader);
Map<String, URL> urls = enumerationToList(loader.getResources(
className + ".class")).stream().collect(
Collectors.toMap(URL::toString, Function.identity()));
Map<String, URL> priorityUrls = enumerationToList(priorityClasspath.getResources(
Map<String, URL> priorityUrls = enumerationToList(classProvider.priorityClasspath.getResources(
className + ".class")).stream().collect(
Collectors.toMap(URL::toString, Function.identity()));
if (urls.size() > 1) {
Expand Down Expand Up @@ -320,6 +335,11 @@ public byte[] patch(String className, byte[] base) throws IOException {
return out;
}


public Set<String> getTargetClasses() {
return transformerList.keySet();
}

public static byte[] readAllBytes(InputStream is) {
byte[] buffer = new byte[8192];
int bytesRead;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
throw new UnsupportedOperationException("Cannot load classes from the priority classpath directly.");
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public void addTransformer(String transformer) throws IOException {
JarModder.debug("Found " + transformerUrls.size() + " transformers at " + transformer);
}

// { [targetClass] -> { [transformListUri] -> transform[] }
public Map<String, Map<String, List<String>>> build(TransformerManager transformerManager, IClassProvider classProvider) {
Map<String, Map<String, List<String>>> transformerMap = new HashMap<>();
for (URL url : transformerUrls) {
Expand Down

0 comments on commit ffeec81

Please sign in to comment.