I made Recaf.
I can use Recaf, it will decompile it well and I can save modifications and export program without issues.
The title and later description seems to imply the code is obfuscated. Please do not rely on recompilation features when dealing with obfuscated code. It rarely ever works and often has unintended side-effects.
Use the assembler for situations like this.
Even by editing bytecode, it's not an issue
Recaf self-advertises itself as a bytecode editor, and using it is explained in the documentation. Did you read the documentation?
It prompts you to do so when first running the application.
but doing that it recompiles the whole jar
No it does not. When you make changes to a class, only that class gets updated within the in-memory model. When you export your changes Recaf will repackage the contents of the model into a JAR file again.
If you are using the current main releases 2.X, the JAR will be written with compression enabled for all entries.
If you are using the preview 3.X releases then you can toggle the use of compression in the config window. Upcoming work supports matching the compression settings per-entry of what they originally were marked as.
If I open the jar with WinRar, those class appears as empty folders due to obfuscation.
Assuming you mean the original input JAR here, this is because the JVM allows class files in zip files to end with a /
character. Most tools will thus interpret it as a directory.
Using a standard ZIP archive tool to work around this is going to be tricky unless you normalize all the paths first. If this is the only ZIP file trick in the sample you can use the following to patch the JAR file:
File output = new File("output.jar");
File input = new File("input.jar");
try(ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(output))) {
try (ZipFile zipIn = new ZipFile(input)) {
// Copy zip entries of input to output
Enumeration<? extends ZipEntry> entries = zipIn.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.endsWith(".class/")) {
// Fix the zip entry names of classes ending with slashes
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
zipIn.getInputStream(entry).transferTo(tmp);
// New blank entry with the fixed name, then write the original class data
ZipEntry normalizedClassEntry = new ZipEntry(entryName.substring(0, entryName.length() - 1));
zos.putNextEntry(normalizedClassEntry);
zos.write(tmp.toByteArray());
zos.closeEntry();
} else if (!entry.isDirectory()) {
// Copy any non-directory content as-is
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
zipIn.getInputStream(entry).transferTo(tmp);
zos.putNextEntry(entry);
zos.write(tmp.toByteArray());
zos.closeEntry();
}
}
} finally {
zos.finish();
zos.close();
}
}