Firstly the classes from the jar need to be loaded. We'll use three methods for this process:
Map<String, ClassNode> loadClasses(File jarFile) throws IOException {
Map<String, ClassNode> classes = new HashMap<String, ClassNode>();
JarFile jar = new JarFile(jarFile);
Stream<JarEntry> str = jar.stream();
str.forEach(z -> readJar(jar, z, classes));
jar.close();
return classes;
}
Map<String, ClassNode> readJar(JarFile jar, JarEntry entry, Map<String, ClassNode> classes) {
String name = entry.getName();
try (InputStream jis = jar.getInputStream(entry)){
if (name.endsWith(".class")) {
byte[] bytes = IOUtils.toByteArray(jis);
String cafebabe = String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]);
if (!cafebabe.toLowerCase().equals("cafebabe")) {
// This class doesn't have a valid magic
return classes;
}
try {
ClassNode cn = getNode(bytes);
classes.put(cn.name, cn);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
ClassNode getNode(byte[] bytes) {
ClassReader cr = new ClassReader(bytes);
ClassNode cn = new ClassNode();
try {
cr.accept(cn, ClassReader.EXPAND_FRAMES);
} catch (Exception e) {
e.printStackTrace();
}
cr = null;
return cn;
}
With these methods loading and changing a jar file becomes a simple matter of changing ClassNodes in a map. In this example we will replace all Strings in the jar with capitalized ones using the Tree API.
File jarFile = new File("sample.jar");
Map<String, ClassNode> nodes = loadClasses(jarFile);
// Iterate ClassNodes
for (ClassNode cn : nodes.values()){
// Iterate methods in class
for (MethodNode mn : cn.methods){
// Iterate instructions in method
for (AbstractInsnNode ain : mn.instructions.toArray()){
// If the instruction is loading a constant value
if (ain.getOpcode() == Opcodes.LDC){
// Cast current instruction to Ldc
// If the constant is a string then capitalize it.
LdcInsnNode ldc = (LdcInsnNode) ain;
if (ldc.cst instanceof String){
ldc.cst = ldc.cst.toString().toUpperCase();
}
}
}
}
}
Now that all of the ClassNode's strings have been modified we need to save the changes. In order to save the changes and have a working output a few things have to be done:
From the last portion above, we'll create three methods.
Usage:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
The methods used:
static Map<String, byte[]> processNodes(Map<String, ClassNode> nodes, Map<String, MappedClass> mappings) {
Map<String, byte[]> out = new HashMap<String, byte[]>();
// Iterate nodes and add them to the map of <Class names , Class bytes>
// Using Compute_Frames ensures that stack-frames will be re-calculated automatically
for (ClassNode cn : nodes.values()) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
out.put(mappings.containsKey(cn.name) ? mappings.get(cn.name).getNewName() : cn.name, cw.toByteArray());
}
return out;
}
static Map<String, byte[]> loadNonClasses(File jarFile) throws IOException {
Map<String, byte[]> entries = new HashMap<String, byte[]>();
ZipInputStream jis = new ZipInputStream(new FileInputStream(jarFile));
ZipEntry entry;
// Iterate all entries
while ((entry = jis.getNextEntry()) != null) {
try {
String name = entry.getName();
if (!name.endsWith(".class") && !entry.isDirectory()) {
// Apache Commons - byte[] toByteArray(InputStream input)
//
// Add each entry to the map <Entry name , Entry bytes>
byte[] bytes = IOUtils.toByteArray(jis);
entries.put(name, bytes);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jis.closeEntry();
}
}
jis.close();
return entries;
}
static void saveAsJar(Map<String, byte[]> outBytes, String fileName) {
try {
// Create jar output stream
JarOutputStream out = new JarOutputStream(new FileOutputStream(fileName));
// For each entry in the map, save the bytes
for (String entry : outBytes.keySet()) {
// Appent class names to class entries
String ext = entry.contains(".") ? "" : ".class";
out.putNextEntry(new ZipEntry(entry + ext));
out.write(outBytes.get(entry));
out.closeEntry();
}
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
That's it. All the changes will be saved to "sample-edit.jar".