1

Currently I am trying to update an old project. The problem is, that in one of my sources (bungeecord) they have changed two fileds (see enum "protocol") from public final to final modifier. To make the project work again I need to access these two fields.

As a reason of this I try to "inject" the project. This works great, so the modifier changes but I am currently not able to save it to the jar file. But this is necessary.

The process of saving works perfectly for the "userconnection" (see enum below). In this case I edit a class modifier.

If you need any more Information please let me know.

When the "injection" (enum: protocol) is done and I check the modifier type of these fileds I see that there have been some changes. But when I restart the system and check the filed modifiers again before the "injection" they are as there were no changes.

public static int inject(InjectionType type) {
    try{

        System.out.println("Starting  injection.");
        System.out.println(type.getInfo());

        ClassPool cp = ClassPool.getDefault();
        CtClass clazz = cp.getCtClass(type.getClazz().getName());

        switch (type) {
            case USERCONNECTION:
                int modifier = UserConnection.class.getModifiers();
                if (!Modifier.isFinal(modifier) && Modifier.isPublic(modifier)) {
                    return -1;
                }
                clazz.setModifiers(Modifier.PUBLIC);
                break;
            case PROTOCOL:
                CtField field = clazz.getField("TO_CLIENT");
                field.setModifiers(Modifier.PUBLIC + Modifier.FINAL);
                field = clazz.getField("TO_SERVER");
                field.setModifiers(Modifier.PUBLIC + Modifier.FINAL);
                break;
            default:
                return -1;  //no data
        }

        ByteArrayOutputStream bout;
        DataOutputStream out = new DataOutputStream(bout = new ByteArrayOutputStream());
        clazz.getClassFile().write(out);

        InputStream[] streams = { new ByteArrayInputStream(bout.toByteArray()) };
        File bungee_file = new File(BungeeCord.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
        updateZipFile(bungee_file, type, streams);
        return 1;
    }catch (Exception e){
        e.printStackTrace();
    }
    return 0;
}

private static void updateZipFile(File zipFile, InjectionType type, InputStream[] ins) throws IOException {
    File tempFile = File.createTempFile(zipFile.getName(), null);
    if (!tempFile.delete()) {
        System.out.println("Warn: Cant delete temp file.");
    }
    if (tempFile.exists()) {
        System.out.println("Warn: Temp target file alredy exist!");
    }
    if (!zipFile.exists()) {
        throw new RuntimeException("Could not rename the file " + zipFile.getAbsolutePath() + " to " + tempFile.getAbsolutePath() + " (Src. not found!)");
    }
    int renameOk = zipFile.renameTo(tempFile) ? 1 : 0;
    if (renameOk == 0) {
        tempFile = new File(zipFile.toString() + ".copy");
        com.google.common.io.Files.copy(zipFile, tempFile);
        renameOk = 2;
        if (zipFile.delete()) {
            System.out.println("Warn: Src file cant delete.");
            renameOk = -1;
        }
    }
    if (renameOk == 0) {
        throw new RuntimeException("Could not rename the file " + zipFile.getAbsolutePath() + " to " + tempFile.getAbsolutePath() + " (Directory read only? (Temp:[R:" + (tempFile.canRead() ? 1 : 0) + ";W:" + (tempFile.canWrite() ? 1 : 0) + ",D:" + (tempFile.canExecute() ? 1 : 0) + "],Src:[R:" + (zipFile.canRead() ? 1 : 0) + ";W:" + (zipFile.canWrite() ? 1 : 0) + ",D:" + (zipFile.canExecute() ? 1 : 0) + "]))");
    }
    if (renameOk != 1) {
        System.out.println("Warn: Cant create temp file. Use .copy file");
    }
    byte[] buf = new byte[Configuration.getLoadingBufferSize()];
    System.out.println("Buffer size: " + buf.length);
    ZipInputStream zin = new ZipInputStream(new FileInputStream(tempFile));
    ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));

    ZipEntry entry = zin.getNextEntry();
    while (entry != null) {
        String path_name = entry.getName().replaceAll("/", "\\.");
        boolean notReplace = true;
        for (String f : type.getNames()) {
            if (f.equals(path_name)) {
                notReplace = false;
                break;
            }
        }
        if (notReplace) {
            out.putNextEntry(new ZipEntry(entry.getName()));
            int len;
            while ((len = zin.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        }
        entry = zin.getNextEntry();
    }
    zin.close();
    for (int i = 0; i < type.getNames().length; i++) {  
        InputStream in = ins[i];
        int index = type.getNames()[i].lastIndexOf('.');
        out.putNextEntry(new ZipEntry(type.getNames()[i].substring(0, index).replaceAll("\\.", "/") + type.getNames()[i].substring(index)));
        int len;
        while ((len = in.read(buf)) > 0) {
            out.write(buf, 0, len);
        }
        out.closeEntry();
        in.close();
    }
    out.close();
    tempFile.delete();
    if (renameOk == -1) {
        System.exit(-1);
    }
}
}

@Getter
public enum InjectionType {
    USERCONNECTION(UserConnection.class, new String[] {"net.md_5.bungee.UserConnection.class"}, "Set modifiers for class UserConnection.class to \"public\""),
    PROTOCOL(Protocol.class, new String[] {"net.md_5.bungee.protocol.Protocol"}, "Set modifiers for class Protocol.class to \"public\"");

    private Class<?> clazz;
    private String[] names;
    private String info;


    InjectionType (Class<?> clazz, String[] names, String info) {
        this.clazz = clazz;
        this.names = names;
        this.info = info;
    }
}
Domi
  • 11
  • 2

1 Answers1

1

When the "injection" (enum: protocol) is done and I check the modifier type of these fileds I see that there have been some changes. But when I restart the system and check the filed modifiers again before the "injection" they are as there were no changes.

What you're trying to do is permanently modify field's access in a jar file using Java reflection. This cannot work as reflection modifies things in runtime only:

Reflection is an API which is used to examine or modify the behavior of methods, classes, interfaces at runtime.

Excerpt taken from this page.

What you need to do is physically edit the jar itself if you want the changes to be permanent. I know you said that you are not able to do that, but as far as I know that is the only possible way. The file itself has to be physically changed if you want the changes to stick after the application has terminated and be applied before the program has started.

Read the official documentation about Java reflection here.

However I don't really understand why is it important that the changes persists after you've restarted the system. The reason you need to change the access is so you can access and perhaps manipulate the class in some way during runtime. What you are doing is correct, one of the more important apsects of reflection is to manipulate data without actually having to modify the physical files themselves and end up using custom distributions.

EDIT: Read this question, it's comments and the accepted answer. They pretty much say the same thing that you can't edit a jar file that is currently being used by JVM, it's locked in a read-only state.

Matthew
  • 1,905
  • 3
  • 19
  • 26
  • Yes this is exactly what I am trying to do. I am not that advanced in this topic, is there a possibility to edit this jar throught my jar file? – Domi May 29 '19 at 07:28
  • As I said, to my knowledge it's not possible if you are using that jar as a dependency as they both load in memory simultaneously and modifying them physically during runtime would have no effect because the code is loaded in memory. – Matthew May 29 '19 at 07:35
  • But can you please explain to me why you would like to do this? You are already modifying the access during runtime anyway so you have access to do with elements whatever you want, why is it important that the changes persist after the application terminates? – Matthew May 29 '19 at 07:36
  • I can not use it like i want. Sure it is modified but I think it isnt in the pluginloader. So if I try to access one of these fields I receive an IllegalAccessException. When I check the fields at this time I also get the result I want to have (public final). So I thought it might be a way edit and load the jar already edited. – Domi May 29 '19 at 07:48
  • What do you mean by `pluginloader`, are you using the jar as a dependency of sort? As soon as you are able to reference it's fields from your code you are using it as a dependency hence you can't **permanently** modify it during runtime. – Matthew May 29 '19 at 09:02
  • My jar is only an additional plugin to the jar I try to edit (BungeeCord.jar). So this jar starts up and loads after that mine. But the not modified src must be in some case loaded/in memory because I get this exception even if i edit the fields. I will try again today may I find a solution, but currently I have no idea where to start. – Domi May 29 '19 at 11:00
  • I think you should re-read my answer and the conversation we had in this comment section, particularly the part where I say that it's **not possible** because this is how JVM works. I have edited my answer to provide an additional reference to another relevant answer. – Matthew May 29 '19 at 12:25