diff --git a/README.md b/README.md index 15e3507..d090d3b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ For use of the actual CLI, grab a build from GitHub Packages and run it with the Usage: poke [-hV] [--[no-]optimize] [--[no-]verify] [-p=] A Java library for performing bytecode normalization and generic deobfuscation. - The class file to be analyzed. - The analyzed class file destination. + The class/JAR file to be analyzed. + The analyzed class/JAR file destination. -h, --help Show this help message and exit. --[no-]optimize Performs optimizations. -p, --passes= The amount of optimization passes. diff --git a/cli/src/main/java/run/slicer/poke/cli/Main.java b/cli/src/main/java/run/slicer/poke/cli/Main.java index 3a40f25..cfe726f 100644 --- a/cli/src/main/java/run/slicer/poke/cli/Main.java +++ b/cli/src/main/java/run/slicer/poke/cli/Main.java @@ -2,11 +2,20 @@ import picocli.CommandLine; import run.slicer.poke.Analyzer; +import run.slicer.poke.Entry; +import java.io.DataInputStream; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Collections; import java.util.concurrent.Callable; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; @CommandLine.Command( name = "poke", @@ -15,10 +24,10 @@ description = "A Java library for performing bytecode normalization and generic deobfuscation." ) public final class Main implements Callable { - @CommandLine.Parameters(index = "0", description = "The class file to be analyzed.") + @CommandLine.Parameters(index = "0", description = "The class/JAR file to be analyzed.") private Path input; - @CommandLine.Parameters(index = "1", description = "The analyzed class file destination.") + @CommandLine.Parameters(index = "1", description = "The analyzed class/JAR file destination.") private Path output; @CommandLine.Option(names = {"-p", "--passes"}, description = "The amount of optimization passes.", defaultValue = "1") @@ -32,18 +41,49 @@ public final class Main implements Callable { @Override public Integer call() throws Exception { + final Analyzer analyzer = Analyzer.builder() + .passes(this.passes) + .optimize(this.optimize) + .verify(this.verify) + .build(); + + boolean isClass = false; + try (final var dis = new DataInputStream(Files.newInputStream(this.input))) { + isClass = dis.readInt() == 0xcafebabe; // class file magic + } catch (IOException ignored) { + } + Files.createDirectories(this.output.getParent()); + if (isClass) { + Files.write( + this.output, analyzer.analyze(Files.readAllBytes(this.input)), + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE + ); + } else { + try (final var zf = new ZipFile(this.input.toFile())) { + final var results = analyzer.analyze( + zf.stream() + .filter(e -> !e.isDirectory() && e.getName().endsWith(".class")) + .map(e -> new ZipEntryImpl(zf, e)) + .toList() + ); + + final var entries = results.stream().collect(Collectors.toMap(Entry::name, Function.identity())); + try (final var zos = new ZipOutputStream(Files.newOutputStream(this.output))) { + for (final ZipEntry entry : Collections.list(zf.entries())) { + // TODO: copy entry metadata? + zos.putNextEntry(new ZipEntry(entry.getName())); + + if (!entry.isDirectory()) { + final Entry pokeEntry = entries.get(entry.getName()); + zos.write(pokeEntry != null ? pokeEntry.data() : zf.getInputStream(entry).readAllBytes()); + } - Files.write( - this.output, - Analyzer.builder() - .passes(this.passes) - .optimize(this.optimize) - .verify(this.verify) - .build() - .analyze(Files.readAllBytes(this.input)), - StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE - ); + zos.closeEntry(); + } + } + } + } return 0; } diff --git a/cli/src/main/java/run/slicer/poke/cli/ZipEntryImpl.java b/cli/src/main/java/run/slicer/poke/cli/ZipEntryImpl.java new file mode 100644 index 0000000..b7cf634 --- /dev/null +++ b/cli/src/main/java/run/slicer/poke/cli/ZipEntryImpl.java @@ -0,0 +1,24 @@ +package run.slicer.poke.cli; + +import run.slicer.poke.Entry; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +record ZipEntryImpl(ZipFile file, ZipEntry entry) implements Entry { + @Override + public String name() { + return entry.getName(); + } + + @Override + public byte[] data() { + try { + return file.getInputStream(entry).readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/cli/src/main/java/run/slicer/poke/cli/package-info.java b/cli/src/main/java/run/slicer/poke/cli/package-info.java new file mode 100644 index 0000000..e5cd40a --- /dev/null +++ b/cli/src/main/java/run/slicer/poke/cli/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package run.slicer.poke.cli; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/run/slicer/poke/Analyzer.java b/core/src/main/java/run/slicer/poke/Analyzer.java index b800abb..4c464f6 100644 --- a/core/src/main/java/run/slicer/poke/Analyzer.java +++ b/core/src/main/java/run/slicer/poke/Analyzer.java @@ -1,11 +1,21 @@ package run.slicer.poke; +import java.util.List; + public interface Analyzer { static Builder builder() { return new AnalyzerImpl.Builder(); } - byte[] analyze(byte[] b); + List analyze(Iterable entries); + + default byte[] analyze(byte[] b) { + return this.analyze(Entry.of(null, b)).getFirst().data(); + } + + default List analyze(Entry... entries) { + return this.analyze(List.of(entries)); + } interface Builder { Builder passes(int passes); diff --git a/core/src/main/java/run/slicer/poke/AnalyzerImpl.java b/core/src/main/java/run/slicer/poke/AnalyzerImpl.java index 7127482..194fe21 100644 --- a/core/src/main/java/run/slicer/poke/AnalyzerImpl.java +++ b/core/src/main/java/run/slicer/poke/AnalyzerImpl.java @@ -1,6 +1,7 @@ package run.slicer.poke; -import proguard.*; +import proguard.AppView; +import proguard.Configuration; import proguard.classfile.ClassPool; import proguard.classfile.ProgramClass; import proguard.classfile.io.ProgramClassReader; @@ -15,15 +16,23 @@ import proguard.preverify.SubroutineInliner; import java.io.*; +import java.util.HashMap; import java.util.List; +import java.util.Map; record AnalyzerImpl(Configuration config) implements Analyzer { @Override - public byte[] analyze(byte[] b) { - final var clazz = new ProgramClass(); - clazz.accept(new ProgramClassReader(new DataInputStream(new ByteArrayInputStream(b)))); + public List analyze(Iterable entries) { + final Map classes = new HashMap<>(); + for (final Entry entry : entries) { + final var clazz = new ProgramClass(); + classes.put(entry, clazz); - final var view = new AppView(new ClassPool(clazz), new ClassPool()); + clazz.accept(new ProgramClassReader(new DataInputStream(new ByteArrayInputStream(entry.data())))); + } + + final var pool = new ClassPool(classes.values()); + final var view = new AppView(pool, new ClassPool()); final boolean willOptimize = config.optimize && config.optimizationPasses > 0; if (config.preverify || willOptimize) { @@ -37,7 +46,7 @@ public byte[] analyze(byte[] b) { new PrimitiveArrayConstantIntroducer().execute(view); this.optimize(view); new LineNumberLinearizer().execute(view); - clazz.accept(new PrimitiveArrayConstantReplacer()); + pool.classesAccept(new PrimitiveArrayConstantReplacer()); } if (config.preverify) { new Preverifier(config).execute(view); @@ -47,10 +56,15 @@ public byte[] analyze(byte[] b) { new LineNumberTrimmer().execute(view); } - final var output = new ByteArrayOutputStream(); - clazz.accept(new ProgramClassWriter(new DataOutputStream(output))); + return classes.entrySet() + .stream() + .map(e -> { + final var output = new ByteArrayOutputStream(); + e.getValue().accept(new ProgramClassWriter(new DataOutputStream(output))); - return output.toByteArray(); + return e.getKey().withData(output.toByteArray()); + }) + .toList(); } private void optimize(AppView view) { diff --git a/core/src/main/java/run/slicer/poke/Entry.java b/core/src/main/java/run/slicer/poke/Entry.java new file mode 100644 index 0000000..a4df64e --- /dev/null +++ b/core/src/main/java/run/slicer/poke/Entry.java @@ -0,0 +1,22 @@ +package run.slicer.poke; + +import org.jspecify.annotations.Nullable; + +public interface Entry { + static Entry of(@Nullable String name, byte[] data) { + return new EntryImpl(name, data); + } + + @Nullable + String name(); + + default Entry withName(String name) { + return new EntryImpl(name, this.data()); + } + + byte[] data(); + + default Entry withData(byte[] data) { + return new EntryImpl(this.name(), data); + } +} diff --git a/core/src/main/java/run/slicer/poke/EntryImpl.java b/core/src/main/java/run/slicer/poke/EntryImpl.java new file mode 100644 index 0000000..b1c3ecf --- /dev/null +++ b/core/src/main/java/run/slicer/poke/EntryImpl.java @@ -0,0 +1,6 @@ +package run.slicer.poke; + +import org.jspecify.annotations.Nullable; + +public record EntryImpl(@Nullable String name, byte[] data) implements Entry { +} diff --git a/core/src/main/java/run/slicer/poke/package-info.java b/core/src/main/java/run/slicer/poke/package-info.java new file mode 100644 index 0000000..fbefc0e --- /dev/null +++ b/core/src/main/java/run/slicer/poke/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package run.slicer.poke; + +import org.jspecify.annotations.NullMarked;