Skip to content

Commit

Permalink
feat: bulk operation support, ZIP file reading in CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
zlataovce committed Aug 13, 2024
1 parent bab5f3f commit 5156eb9
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 24 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<passes>] <input>
<output>
A Java library for performing bytecode normalization and generic deobfuscation.
<input> The class file to be analyzed.
<output> The analyzed class file destination.
<input> The class/JAR file to be analyzed.
<output> The analyzed class/JAR file destination.
-h, --help Show this help message and exit.
--[no-]optimize Performs optimizations.
-p, --passes=<passes> The amount of optimization passes.
Expand Down
64 changes: 52 additions & 12 deletions cli/src/main/java/run/slicer/poke/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -15,10 +24,10 @@
description = "A Java library for performing bytecode normalization and generic deobfuscation."
)
public final class Main implements Callable<Integer> {
@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")
Expand All @@ -32,18 +41,49 @@ public final class Main implements Callable<Integer> {

@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;
}
Expand Down
24 changes: 24 additions & 0 deletions cli/src/main/java/run/slicer/poke/cli/ZipEntryImpl.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
4 changes: 4 additions & 0 deletions cli/src/main/java/run/slicer/poke/cli/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package run.slicer.poke.cli;

import org.jspecify.annotations.NullMarked;
12 changes: 11 additions & 1 deletion core/src/main/java/run/slicer/poke/Analyzer.java
Original file line number Diff line number Diff line change
@@ -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<? extends Entry> analyze(Iterable<? extends Entry> entries);

default byte[] analyze(byte[] b) {
return this.analyze(Entry.of(null, b)).getFirst().data();
}

default List<? extends Entry> analyze(Entry... entries) {
return this.analyze(List.of(entries));
}

interface Builder {
Builder passes(int passes);
Expand Down
32 changes: 23 additions & 9 deletions core/src/main/java/run/slicer/poke/AnalyzerImpl.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<? extends Entry> analyze(Iterable<? extends Entry> entries) {
final Map<Entry, ProgramClass> 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) {
Expand All @@ -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);
Expand All @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions core/src/main/java/run/slicer/poke/Entry.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 6 additions & 0 deletions core/src/main/java/run/slicer/poke/EntryImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package run.slicer.poke;

import org.jspecify.annotations.Nullable;

public record EntryImpl(@Nullable String name, byte[] data) implements Entry {
}
4 changes: 4 additions & 0 deletions core/src/main/java/run/slicer/poke/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package run.slicer.poke;

import org.jspecify.annotations.NullMarked;

0 comments on commit 5156eb9

Please sign in to comment.