Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 28 additions & 32 deletions src/ui/JarDecompilerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alert, Button, Form, message, Modal, Popconfirm, Space } from "antd";
import { Alert, Button, Flex, Form, message, Modal, Popconfirm, Progress } from "antd";
import { JavaOutlined } from '@ant-design/icons';
import { BehaviorSubject } from "rxjs";
import { useObservable } from "../utils/UseObservable";
Expand Down Expand Up @@ -29,8 +29,8 @@ export const JarDecompilerModal = () => {
const task = decompileEntireJar(jar.jar, {
threads: decompilerThreads.value,
splits: decompilerSplits.value,
logger(className) {
progressSubject.next(className);
logger(progress, current, total) {
progressSubject.next([progress, current, total]);
},
});

Expand All @@ -49,17 +49,13 @@ export const JarDecompilerModal = () => {
taskSubject.next(undefined);
progressSubject.next(undefined);
});
progressSubject.next("Decompiling...");
};

const clearCache = (all: boolean) => {
const clearCache = () => {
if (!jar) return;

deleteCache(all ? null : jar.jar.name)
.finally(() => messageApi.open({ type: "success", content: "Cache deleted" }));
deleteCache().then(c => messageApi.open({ type: "success", content: `Deleted ${c} clasess from cache.` }));
};


return (
<Modal
title="Decompile Entire JAR"
Expand All @@ -72,7 +68,7 @@ export const JarDecompilerModal = () => {
{modalCtx}
<Alert
type="warning"
message="Decompiling the entire JAR will use large amount of resources and may crash the browser."
title="Decompiling the entire JAR will use large amount of resources and may crash the browser."
description="If the browser crashed, simply reopen the page and you can continue decompiling the rest of the classes by opening this menu again."
/>
<br />
Expand All @@ -81,32 +77,29 @@ export const JarDecompilerModal = () => {
<NumberOption setting={decompilerThreads} title="Worker Threads" min={1} max={MAX_THREADS} />
<NumberOption testid="jar-decompiler-splits" setting={decompilerSplits} title="Worker Splits" min={1} />
<Form.Item label="Cache">
<Space>
<Popconfirm title="Are you sure?" onConfirm={() => clearCache(false)}>
<Button color="danger" variant="outlined">Clear Current</Button>
</Popconfirm>
<Popconfirm title="Are you sure?" onConfirm={() => clearCache(true)}>
<Button color="danger" variant="outlined">Clear ALL</Button>
</Popconfirm>
</Space>
<Popconfirm title="Are you sure? This will also delete cache for all versions." onConfirm={clearCache}>
<Button color="danger" variant="outlined">Clear</Button>
</Popconfirm>
</Form.Item>
</Form>

</Modal>
);
};

const progressSubject = new BehaviorSubject<string | undefined>(undefined);
const progressSubject = new BehaviorSubject<[string, number, number] | undefined>(undefined);
const taskSubject = new BehaviorSubject<DecompileEntireJarTask | undefined>(undefined);

export const JarDecompilerProgressModal = () => {
const progress = useObservable(progressSubject);
const [text, current, total] = useObservable(progressSubject) ?? [];
const task = useObservable(taskSubject);

const percent = (current ?? 0) / (total ?? 1) * 100;

return (
<Modal
title="Decompiling JAR..."
open={progress ? true : false}
open={text ? true : false}
closable={false}
keyboard={false}
mask={{ closable: false }}
Expand All @@ -120,17 +113,20 @@ export const JarDecompilerProgressModal = () => {
<OkBtn />
)}
>
<div data-testid="jar-decompiler-progress" style={{
fontFamily: "monospace",
padding: "10px 0",
overflow: "hidden",
textOverflow: "ellipsis",
wordBreak: "break-all",
whiteSpace: "nowrap",
width: "100%"
}}>
{progress}
</div>
<Flex vertical>
<div data-testid="jar-decompiler-progress" style={{
fontFamily: "monospace",
fontSize: "small",
overflow: "hidden",
textOverflow: "ellipsis",
wordBreak: "break-all",
whiteSpace: "nowrap",
width: "100%"
}}>
{text}
</div>
<Progress percent={percent} format={() => `${current}/${total}`} />
</Flex>
</Modal>
);
};
61 changes: 37 additions & 24 deletions src/workers/decompile/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ export async function setOptions(options: vf.Options) {
await Promise.all(workers.map(w => w.setOptions(options, sab)));
}

export async function deleteCache(jarName: string | null) {
export async function deleteCache(): Promise<number> {
const worker = await findWorker();
await worker.clear(jarName);
return await worker.clear();
}

export type DecompileEntireJarOptions = {
threads?: number,
splits?: number,
logger?: (className: string) => void,
logger?: (className: string, current: number, total: number) => void,
};

export type DecompileEntireJarTask = {
Expand All @@ -79,74 +79,87 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions
const state = new Uint32Array(sab);
state[0] = 0;

const optThreads = Math.min(options?.threads ?? MAX_THREADS, MAX_THREADS);
const optSplits = options?.splits ?? 100;
const optLogger = options?.logger ? Comlink.proxy(options.logger) : null;

const classNames = new DecompileJar(jar).classes
.filter(n => !n.includes("$"));

const dJar = new DecompileJar(jar);
return {
async start() {
try {
const classNames = dJar.classes.filter(n => !n.includes("$"));
options?.logger?.("Decompiling...", 0, classNames.length)

const optThreads = Math.min(options?.threads ?? MAX_THREADS, MAX_THREADS);
const optSplits = options?.splits ?? 100;

let current = 0;
const optLogger = options?.logger ? Comlink.proxy((i: number) => {
options.logger!(classNames[i], ++current, classNames.length);
}) : undefined;

await ensureWorkers(optThreads);
const result = await Promise.all(workers
.slice(0, optThreads)
const result = await Promise.all((workers
.slice(0, optThreads))
.map(w => w.decompileMany(jar.name, jar.blob, classNames, sab, optSplits, optLogger)));
const total = result.reduce((acc, n) => acc + n, 0);
return total;
} finally {
// kill all workers
setRuntime(preferWasmRuntime);
}
},
stop() {
Atomics.store(state, 0, classNames.length);
Atomics.store(state, 0, dJar.classes.length);
},
};
}

export async function decompileClass(className: string, jar: Jar): Promise<DecompileResult> {
className = className.replace(".class", "");
const entry = jar.entries[`${className}.class`];

if (!jar.entries[`${className}.class`]) return {
owner: jar.name,
if (!entry) return {
className,
checksum: 0,
source: `// Class not found: ${className}`,
tokens: [],
language: "java",
};

const jarClasses = new DecompileJar(jar).classes;
const classData: DecompileData = {};
const data = await jar.entries[`${className}.class`].bytes();
classData[className] = data;
classData[className] = {
checksum: entry.crc32,
data: await entry.bytes(),
};

for (const classFile of jarClasses) {
if (!classFile.startsWith(`${className}\$`)) {
continue;
}

const data = await jar.entries[`${classFile}.class`].bytes();
classData[classFile] = data;
const entry = jar.entries[`${classFile}.class`];
classData[classFile] = {
checksum: entry.crc32,
data: await entry.bytes(),
};
}

const worker = await findWorker();
return await worker.decompile(jar.name, jarClasses, className, classData);
return await worker.decompile(jarClasses, className, classData);
}

export async function getClassBytecode(className: string, jar: Jar): Promise<DecompileResult> {
className = className.replace(".class", "");
const entry = jar.entries[`${className}.class`];

if (!jar.entries[`${className}.class`]) return {
owner: jar.name,
if (!entry) return {
className,
checksum: 0,
source: `// Class not found: ${className}`,
tokens: [],
language: "bytecode",
};

const classData: ArrayBufferLike[] = [];
const data = await jar.entries[`${className}.class`].bytes();
const data = await entry.bytes();
classData.push(data.buffer);

const jarClasses = new DecompileJar(jar).classes;
Expand All @@ -160,5 +173,5 @@ export async function getClassBytecode(className: string, jar: Jar): Promise<Dec
}

const worker = await findWorker();
return await worker.getClassBytecode(jar.name, className, classData);
return await worker.getClassBytecode(className, entry.crc32, classData);
}
19 changes: 13 additions & 6 deletions src/workers/decompile/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import type { Token } from "../../logic/Tokens";
import type { Jar } from "../../utils/Jar";

export type DecompileResult = {
owner: string;
className: string;
checksum: number;
source: string;
tokens: Token[];
language: 'java' | 'bytecode';
};

export type DecompileOption = { key: string, value: string; };

export type DecompileLogger = (className: string) => void;

export type DecompileData = Record<string, Uint8Array | Promise<Uint8Array>>;
export type DecompileData = {
[className: string]: undefined | {
checksum: number;
data: Uint8Array | Promise<Uint8Array>;
};
};

export class DecompileJar {
jar: Jar;
Expand All @@ -22,8 +25,12 @@ export class DecompileJar {
constructor(jar: Jar) {
this.jar = jar;
this.proxy = new Proxy({}, {
get(_, className: string) {
return jar.entries[className + ".class"]?.bytes();
get(_, className: string): DecompileData[""] {
const entry = jar.entries[className + ".class"];
if (entry) return {
checksum: entry.crc32,
data: entry.bytes()
};
}
});
}
Expand Down
Loading