From 98e183a9f6fac105996bb4eb5e9f7e4c487e9f3a Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Sat, 14 Feb 2026 18:24:42 +0700 Subject: [PATCH 1/4] use checksum as decompile cache key --- src/workers/decompile/client.ts | 39 +++++---- src/workers/decompile/types.ts | 17 +++- src/workers/decompile/worker.ts | 138 +++++++++++++++++--------------- 3 files changed, 109 insertions(+), 85 deletions(-) diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 1370238..028d6c5 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -60,7 +60,7 @@ export async function setOptions(options: vf.Options) { export async function deleteCache(jarName: string | null) { const worker = await findWorker(); - await worker.clear(jarName); + await worker.clear(); } export type DecompileEntireJarOptions = { @@ -90,12 +90,14 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions async start() { try { await ensureWorkers(optThreads); - const result = await Promise.all(workers - .slice(0, optThreads) - .map(w => w.decompileMany(jar.name, jar.blob, classNames, sab, optSplits, optLogger))); + const workers2 = workers.slice(0, optThreads); + await Promise.all(workers2.map(w => w.registerJar(jar.name, jar.blob))); + const result = await Promise.all(workers2 + .map(w => w.decompileMany(jar.name, classNames, sab, optSplits, optLogger))); const total = result.reduce((acc, n) => acc + n, 0); return total; } finally { + // kill all workers setRuntime(preferWasmRuntime); } }, @@ -107,10 +109,11 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions export async function decompileClass(className: string, jar: Jar): Promise { 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", @@ -118,35 +121,41 @@ export async function decompileClass(className: string, jar: Jar): Promise { 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; @@ -160,5 +169,5 @@ export async function getClassBytecode(className: string, jar: Jar): Promise void; -export type DecompileData = Record>; +export type DecompileData = { + [className: string]: undefined | { + checksum: number; + data: Uint8Array | Promise; + }; +}; export class DecompileJar { jar: Jar; @@ -22,8 +27,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() + }; } }); } diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index 65983aa..14cb545 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -25,11 +25,13 @@ export const scheduleClose = () => schedule(async () => close()); const db = new Dexie("decompiler") as Dexie & { options: EntityTable, - results: Table, + results2: Table, }; -db.version(1).stores({ - options: "key, value", - results: "[owner+className+language], source, tokens", +db.version(2).stores({ + options: "key", + results2: "[className+checksum+language]", + // clear old data + results: null, }); let _options: vf.Options | undefined = undefined; @@ -59,14 +61,14 @@ export const setOptions = (options: vf.Options, sab: SharedArrayBuffer) => sched } if (changed || notVisited.size > 0) { - await db.results.clear(); + await db.results2.clear(); } await db.options.clear(); await db.options.bulkAdd(Object.entries(options).map(([k, v]) => ({ key: k, value: v }))); }); -const jars: Record = {}; +const jars: Record = {}; export const registerJar = (jarName: string, blob: Blob | null) => schedule(async () => { if (blob) { jars[jarName] = new DecompileJar(await openJar(jarName, blob)); @@ -78,92 +80,95 @@ export const registerJar = (jarName: string, blob: Blob | null) => schedule(asyn export const loadVFRuntime = (preferWasm: boolean) => schedule(() => vf.loadRuntime(preferWasm)); -export const clear = (jarName: string | null) => schedule(async () => { - if (jarName) { - await db.results.where("owner").equals(jarName).delete(); - } else { - await db.results.clear(); - } +export const clear = () => schedule(async () => { + await db.results2.clear(); }); -export async function decompileMany( +export const decompileMany = ( jarName: string, - blob: Blob, classNames: string[], sab: SharedArrayBuffer, splits: number, logger: DecompileLogger | null -): Promise { - try { - await registerJar(jarName, blob); - const state = new Uint32Array(sab); - - let count = 0; - await schedule(async () => { - while (true) { - const i = Atomics.add(state, 0, splits); - if (i >= classNames.length) break; - - const targetClassNames: string[] = []; - for (let j = 0; j < splits; j++) { - if ((i + j) >= classNames.length) break; - targetClassNames.push(classNames[i + j]); - } +): Promise => schedule(async () => { + const jar = jars[jarName]; + if (!jar) { + console.error(`No jar found for ${jarName}`); + return 0; + } - try { - const result = await _decompile(jarName, null, targetClassNames, null, logger, true); - count += result.length; - } catch (e) { - console.error("Error during decompilation:", e); - } - } - }); + const state = new Uint32Array(sab); - return count; - } finally { - await registerJar(jarName, null); + let count = 0; + while (true) { + const i = Atomics.add(state, 0, splits); + if (i >= classNames.length) break; + + const targetClassNames: string[] = []; + for (let j = 0; j < splits; j++) { + if ((i + j) >= classNames.length) break; + + const className = classNames[i + j]; + const checksum = jar.proxy[className]?.checksum; + if (!checksum) continue; + + const dbCount = await db.results2 + .where("[className+checksum+language]") + .equals([className, checksum, "java"]) + .count(); + if (dbCount === 1) continue; + + targetClassNames.push(className); + } + + try { + const result = await _decompile(jar.classes, targetClassNames, jar.proxy, logger); + count += result.length; + } catch (e) { + console.error("Error during decompilation:", e); + } } -} + + return count; +}); export const decompile = ( - jarName: string, jarClasses: string[], className: string, classData: DecompileData ): Promise => schedule(async () => { try { - const result = await _decompile(jarName, jarClasses, [className], classData, null, false); + const dbResult = await db.results2.get([className, classData[className]?.checksum, "java"]); + if (dbResult) return dbResult; + + const result = await _decompile(jarClasses, [className], classData, null); return result[0]; } catch (e) { console.error(`Error during decompilation of class '${className}':`, e); - return { owner: jarName, className, source: `// Error during decompilation: ${(e as Error).message}`, tokens: [], language: "java" }; + return { + className, + checksum: 0, + source: `// Error during decompilation: ${(e as Error).message}`, + tokens: [], + language: "java" + }; } }); async function _decompile( - jarName: string, - jarClasses: string[] | null, + jarClasses: string[], classNames: string[], - classData: DecompileData | null, + classData: DecompileData, logger: DecompileLogger | null, - skipDb: boolean, ): Promise { - if (!jarClasses) jarClasses = jars[jarName].classes; - if (!classData) classData = jars[jarName].proxy; - - const dbResult = await db.results.bulkGet(classNames.map(n => [jarName, n, "java"] as [string, string, string])); - if (dbResult.every(t => t)) return skipDb ? [] : dbResult as DecompileResult[]; - - const options = await getOptions(); - const allTokens: Record = {}; let currentContent: string | undefined; let currentTokens: Token[] | undefined; const sources = await vf.decompile(classNames, { - source: async (name) => await classData[name] ?? null, + source: async (name) => await classData[name]?.data ?? null, resources: jarClasses, - options, + options: await getOptions(), logger: { writeMessage(level, message, error) { switch (level) { @@ -205,6 +210,7 @@ async function _decompile( const res: DecompileResult[] = []; for (const [className, source] of Object.entries(sources)) { + const checksum = classData[className]?.checksum ?? 0; const tokens = allTokens[source] ?? []; const importRegex = /^\s*import\s+(?!static\b)([^\s;]+)\s*;/gm; @@ -227,25 +233,25 @@ async function _decompile( } tokens.sort((a, b) => a.start - b.start); - res.push({ owner: jarName, className, source, tokens, language: "java" }); + res.push({ className, checksum, source, tokens, language: "java" }); } - await db.results.bulkPut(res); + await db.results2.bulkPut(res); return res; } -export const getClassBytecode = (jarName: string, className: string, classData: ArrayBufferLike[]): Promise => schedule(async () => { - let result = await db.results.get([jarName, className, "bytecode"]); +export const getClassBytecode = (className: string, checksum: number, classData: ArrayBufferLike[]): Promise => schedule(async () => { + let result = await db.results2.get([className, checksum, "bytecode"]); if (result) return result; try { const bytecode = await getBytecode(classData); - result = { owner: jarName, className, source: bytecode, tokens: [], language: "bytecode" }; + result = { className, checksum, source: bytecode, tokens: [], language: "bytecode" }; } catch (e) { console.error(`Error during bytecode retrieval of class '${className}':`, e); - result = { owner: jarName, className, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode" }; + result = { className, checksum, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode" }; } - await db.results.put(result); + await db.results2.put(result); return result; }); From 345da7d5eec448c0d3a5d5be97d6016dcb5f8aaa Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Sat, 14 Feb 2026 23:06:12 +0700 Subject: [PATCH 2/4] remove separate register jar --- src/ui/JarDecompilerModal.tsx | 22 +++++++--------------- src/workers/decompile/client.ts | 26 ++++++++++++-------------- src/workers/decompile/worker.ts | 21 +++++---------------- 3 files changed, 24 insertions(+), 45 deletions(-) diff --git a/src/ui/JarDecompilerModal.tsx b/src/ui/JarDecompilerModal.tsx index ed646e4..4bacd2a 100644 --- a/src/ui/JarDecompilerModal.tsx +++ b/src/ui/JarDecompilerModal.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Form, message, Modal, Popconfirm, Space } from "antd"; +import { Alert, Button, Form, message, Modal, Popconfirm } from "antd"; import { JavaOutlined } from '@ant-design/icons'; import { BehaviorSubject } from "rxjs"; import { useObservable } from "../utils/UseObservable"; @@ -52,14 +52,11 @@ export const JarDecompilerModal = () => { 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 ( { {modalCtx}
@@ -81,14 +78,9 @@ export const JarDecompilerModal = () => { - - clearCache(false)}> - - - clearCache(true)}> - - - + + + diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 028d6c5..944e664 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -58,9 +58,9 @@ 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 { const worker = await findWorker(); - await worker.clear(); + return await worker.clear(); } export type DecompileEntireJarOptions = { @@ -79,21 +79,19 @@ 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 optThreads = Math.min(options?.threads ?? MAX_THREADS, MAX_THREADS); + const optSplits = options?.splits ?? 100; + const optLogger = options?.logger ? Comlink.proxy(options.logger) : null; + await ensureWorkers(optThreads); - const workers2 = workers.slice(0, optThreads); - await Promise.all(workers2.map(w => w.registerJar(jar.name, jar.blob))); - const result = await Promise.all(workers2 - .map(w => w.decompileMany(jar.name, classNames, sab, optSplits, optLogger))); + const classNames = dJar.classes.filter(n => !n.includes("$")); + 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 { @@ -102,7 +100,7 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions } }, stop() { - Atomics.store(state, 0, classNames.length); + Atomics.store(state, 0, dJar.classes.length); }, }; } diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index 14cb545..c8b4c0c 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -68,36 +68,25 @@ export const setOptions = (options: vf.Options, sab: SharedArrayBuffer) => sched await db.options.bulkAdd(Object.entries(options).map(([k, v]) => ({ key: k, value: v }))); }); -const jars: Record = {}; -export const registerJar = (jarName: string, blob: Blob | null) => schedule(async () => { - if (blob) { - jars[jarName] = new DecompileJar(await openJar(jarName, blob)); - } else { - delete jars[jarName]; - } -}); - export const loadVFRuntime = (preferWasm: boolean) => schedule(() => vf.loadRuntime(preferWasm)); -export const clear = () => schedule(async () => { +export const clear = (): Promise => schedule(async () => { + const count = await db.results2.count(); await db.results2.clear(); + return count; }); export const decompileMany = ( jarName: string, + jarBlob: Blob, classNames: string[], sab: SharedArrayBuffer, splits: number, logger: DecompileLogger | null ): Promise => schedule(async () => { - const jar = jars[jarName]; - if (!jar) { - console.error(`No jar found for ${jarName}`); - return 0; - } - const state = new Uint32Array(sab); + const jar = new DecompileJar(await openJar(jarName, jarBlob)); let count = 0; while (true) { From 5e3d4c09ae8642112e1fab2708b7cd7ef18fc7c7 Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Sun, 15 Feb 2026 03:40:30 +0700 Subject: [PATCH 3/4] jar decompiler progress bar --- src/ui/JarDecompilerModal.tsx | 40 ++++++++++++++++++--------------- src/workers/decompile/client.ts | 12 +++++++--- src/workers/decompile/types.ts | 2 -- src/workers/decompile/worker.ts | 38 ++++++++++++++++++++++++------- 4 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/ui/JarDecompilerModal.tsx b/src/ui/JarDecompilerModal.tsx index 4bacd2a..74c16aa 100644 --- a/src/ui/JarDecompilerModal.tsx +++ b/src/ui/JarDecompilerModal.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Form, message, Modal, Popconfirm } 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"; @@ -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]); }, }); @@ -49,7 +49,6 @@ export const JarDecompilerModal = () => { taskSubject.next(undefined); progressSubject.next(undefined); }); - progressSubject.next("Decompiling..."); }; const clearCache = () => { @@ -88,17 +87,19 @@ export const JarDecompilerModal = () => { ); }; -const progressSubject = new BehaviorSubject(undefined); +const progressSubject = new BehaviorSubject<[string, number, number] | undefined>(undefined); const taskSubject = new BehaviorSubject(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 ( { )} > -
- {progress} -
+ +
+ {text} +
+ `${current}/${total}`} /> +
); }; diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 944e664..5284963 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -66,7 +66,7 @@ export async function deleteCache(): Promise { export type DecompileEntireJarOptions = { threads?: number, splits?: number, - logger?: (className: string) => void, + logger?: (className: string, current: number, total: number) => void, }; export type DecompileEntireJarTask = { @@ -83,12 +83,18 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions 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; - const optLogger = options?.logger ? Comlink.proxy(options.logger) : null; + + let current = 0; + const optLogger = options?.logger ? Comlink.proxy((i: number) => { + options.logger!(classNames[i], ++current, classNames.length); + }) : undefined; await ensureWorkers(optThreads); - const classNames = dJar.classes.filter(n => !n.includes("$")); const result = await Promise.all((workers .slice(0, optThreads)) .map(w => w.decompileMany(jar.name, jar.blob, classNames, sab, optSplits, optLogger))); diff --git a/src/workers/decompile/types.ts b/src/workers/decompile/types.ts index a7f21ee..967fd7c 100644 --- a/src/workers/decompile/types.ts +++ b/src/workers/decompile/types.ts @@ -11,8 +11,6 @@ export type DecompileResult = { export type DecompileOption = { key: string, value: string; }; -export type DecompileLogger = (className: string) => void; - export type DecompileData = { [className: string]: undefined | { checksum: number; diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index c8b4c0c..195e663 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -2,7 +2,7 @@ import * as vf from "../../logic/vf"; import Dexie, { type EntityTable, type Table } from "dexie"; import type { Token } from "../../logic/Tokens"; import { getBytecode } from "../JarIndexWorker"; -import { type DecompileResult, type DecompileOption, type DecompileData, DecompileJar, type DecompileLogger } from "./types"; +import { type DecompileResult, type DecompileOption, type DecompileData, DecompileJar } from "./types"; import { openJar } from "../../utils/Jar"; let lastPromise: Promise | undefined = undefined; @@ -83,11 +83,22 @@ export const decompileMany = ( classNames: string[], sab: SharedArrayBuffer, splits: number, - logger: DecompileLogger | null + logger?: (index: number) => Promise | void, ): Promise => schedule(async () => { const state = new Uint32Array(sab); const jar = new DecompileJar(await openJar(jarName, jarBlob)); + let logPromises: Promise[] = []; + let nameLogger; + if (logger) { + const class2index = new Map(classNames.map((v, i) => [v, i] as [string, number])); + nameLogger = (className: string) => { + if (!class2index) return; + const i = class2index.get(className); + if (i) logPromises.push(Promise.resolve(logger!(i))); + }; + } + let count = 0; while (true) { const i = Atomics.add(state, 0, splits); @@ -105,17 +116,23 @@ export const decompileMany = ( .where("[className+checksum+language]") .equals([className, checksum, "java"]) .count(); - if (dbCount === 1) continue; - targetClassNames.push(className); + if (dbCount >= 1) { + nameLogger?.(className); + } else { + targetClassNames.push(className); + } } try { - const result = await _decompile(jar.classes, targetClassNames, jar.proxy, logger); + const result = await _decompile(jar.classes, targetClassNames, jar.proxy, nameLogger); count += result.length; } catch (e) { console.error("Error during decompilation:", e); } + + await Promise.all(logPromises); + logPromises = []; } return count; @@ -130,7 +147,7 @@ export const decompile = ( const dbResult = await db.results2.get([className, classData[className]?.checksum, "java"]); if (dbResult) return dbResult; - const result = await _decompile(jarClasses, [className], classData, null); + const result = await _decompile(jarClasses, [className], classData); return result[0]; } catch (e) { console.error(`Error during decompilation of class '${className}':`, e); @@ -148,11 +165,12 @@ async function _decompile( jarClasses: string[], classNames: string[], classData: DecompileData, - logger: DecompileLogger | null, + logger?: (className: string) => void, ): Promise { const allTokens: Record = {}; let currentContent: string | undefined; let currentTokens: Token[] | undefined; + let currentClassName: string | undefined; const sources = await vf.decompile(classNames, { source: async (name) => await classData[name]?.data ?? null, @@ -166,7 +184,11 @@ async function _decompile( } }, startClass(className) { - if (logger) logger(className); + currentClassName = className; + }, + endClass() { + if (logger && currentClassName) logger(currentClassName); + currentClassName = undefined; }, }, tokenCollector: { From 491cd14966a658dc6533b0a5769aa150eeb2d26a Mon Sep 17 00:00:00 2001 From: Dimas Firmansyah Date: Sun, 15 Feb 2026 21:26:25 +0700 Subject: [PATCH 4/4] remove padding from modal, make the font smaller --- src/ui/JarDecompilerModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/JarDecompilerModal.tsx b/src/ui/JarDecompilerModal.tsx index 74c16aa..4a881e3 100644 --- a/src/ui/JarDecompilerModal.tsx +++ b/src/ui/JarDecompilerModal.tsx @@ -116,7 +116,7 @@ export const JarDecompilerProgressModal = () => {