From f2981db834ce7ca09806b5636bf221bab6857bc1 Mon Sep 17 00:00:00 2001 From: Dani-error Date: Thu, 24 Jul 2025 18:44:10 +0100 Subject: [PATCH 1/6] feat: some mapping loading --- src/lib/components/dialog/index.ts | 2 + .../components/dialog/load_mappings.svelte | 92 ++++++++++ src/lib/components/menu/menu.svelte | 3 + src/lib/components/ui/badge/badge.svelte | 50 ++++++ src/lib/components/ui/badge/index.ts | 2 + src/lib/event/handler.ts | 89 +++++++++- src/lib/event/index.ts | 3 + src/lib/mapping/index.ts | 96 +++++++++++ .../parsers/abstract-mapping-parser.ts | 7 + src/lib/mapping/parsers/csrg-tsrg-parser.ts | 100 +++++++++++ src/lib/mapping/parsers/index.ts | 6 + src/lib/mapping/parsers/proguard-parser.ts | 129 ++++++++++++++ src/lib/mapping/parsers/srg-xsrg-parser.ts | 82 +++++++++ src/lib/mapping/parsers/tiny-parser.ts | 87 ++++++++++ src/lib/mapping/parsers/tiny-v2-parser.ts | 160 ++++++++++++++++++ src/lib/mapping/parsers/tsrg2-parser.ts | 115 +++++++++++++ src/lib/mapping/types.ts | 29 ++++ src/lib/mapping/util.ts | 98 +++++++++++ src/lib/utils.ts | 16 ++ src/lib/workspace/index.ts | 34 +++- 20 files changed, 1197 insertions(+), 3 deletions(-) create mode 100644 src/lib/components/dialog/load_mappings.svelte create mode 100644 src/lib/components/ui/badge/badge.svelte create mode 100644 src/lib/components/ui/badge/index.ts create mode 100644 src/lib/mapping/index.ts create mode 100644 src/lib/mapping/parsers/abstract-mapping-parser.ts create mode 100644 src/lib/mapping/parsers/csrg-tsrg-parser.ts create mode 100644 src/lib/mapping/parsers/index.ts create mode 100644 src/lib/mapping/parsers/proguard-parser.ts create mode 100644 src/lib/mapping/parsers/srg-xsrg-parser.ts create mode 100644 src/lib/mapping/parsers/tiny-parser.ts create mode 100644 src/lib/mapping/parsers/tiny-v2-parser.ts create mode 100644 src/lib/mapping/parsers/tsrg2-parser.ts create mode 100644 src/lib/mapping/types.ts create mode 100644 src/lib/mapping/util.ts diff --git a/src/lib/components/dialog/index.ts b/src/lib/components/dialog/index.ts index 0299024..80de4ed 100644 --- a/src/lib/components/dialog/index.ts +++ b/src/lib/components/dialog/index.ts @@ -6,6 +6,7 @@ import ScriptDialog from "./script.svelte"; import ScriptDeleteDialog from "./script_delete.svelte"; import ScriptLoadDialog from "./script_load.svelte"; import ScriptLoadShareDialog from "./script_load_share.svelte"; +import LoadMappings from "./load_mappings.svelte"; export { AboutDialog, @@ -16,4 +17,5 @@ export { ScriptDialog, ScriptLoadDialog, ScriptLoadShareDialog, + LoadMappings }; diff --git a/src/lib/components/dialog/load_mappings.svelte b/src/lib/components/dialog/load_mappings.svelte new file mode 100644 index 0000000..91c4f9f --- /dev/null +++ b/src/lib/components/dialog/load_mappings.svelte @@ -0,0 +1,92 @@ + + + open || close()}> + + + Load Mappings + Please select from the dropdown the mappings format. + +
+ + + +
+ + + +
+
diff --git a/src/lib/components/menu/menu.svelte b/src/lib/components/menu/menu.svelte index 3a7b9bd..032e51d 100644 --- a/src/lib/components/menu/menu.svelte +++ b/src/lib/components/menu/menu.svelte @@ -183,6 +183,9 @@ modals.open(LoadExternalDialog, { handler })} class="justify-between"> Add from URL + handler.loadMappings(entries)} disabled={entries.length === 0}> + Open Mappings + modals.open(ClearDialog, { handler })}> Clear all diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..f71bd00 --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/event/handler.ts b/src/lib/event/handler.ts index 3cd2f39..58c820a 100644 --- a/src/lib/event/handler.ts +++ b/src/lib/event/handler.ts @@ -1,5 +1,7 @@ +import { LoadMappings } from "$lib/components/dialog"; import { disassembleEntry, type Disassembler } from "$lib/disasm"; import { error } from "$lib/log"; +import { detectMappingFormat, MappingFormat, parseMappings } from "$lib/mapping"; import { load as loadScript, type ProtoScript, @@ -31,25 +33,40 @@ import { recordTimed, remove as removeTask, } from "$lib/task"; -import { chunk, distribute, downloadBlob, partition, readFiles, timestampFile, truncate } from "$lib/utils"; +import { + chunk, + distribute, + downloadBlob, + fileToString, + partition, + readFiles, + timestampFile, + truncate, +} from "$lib/utils"; import { type ClassEntry, clear as clearWs, + entries, type Entry, EntryType, loadFile, loadRemote, type LoadResult, loadZip, + mapClass, + type MapClassResult, + MAPPINGS_EXTENSIONS, readDeferred, remove as removeWs, ZIP_EXTENSIONS, } from "$lib/workspace"; +import { analyzeBackground } from "$lib/workspace/analysis"; import { type Data, download } from "$lib/workspace/data"; import { Channel } from "queueable"; +import { modals } from "svelte-modals"; import { toast } from "svelte-sonner"; import { get } from "svelte/store"; -import type { EventHandler } from "./"; +import { type EventHandler } from "./"; // one hell of a file that responds to basically all essential actions as signalled by the UI @@ -129,6 +146,74 @@ export default { }); } }, + async loadMappings(entries: Entry[]): Promise { + if (entries.length === 0) return; + + const files = await readFiles( + Array.from(MAPPINGS_EXTENSIONS.values()) + .map((e) => `.${e}`) + .join(","), + false + ); + if (files.length === 0) { + return; + } + + const file = files[0]; + + try { + const content = await fileToString(file); + + const detectedFormat = detectMappingFormat(content); + + modals.open(LoadMappings, { detectedFormat, handler: this, content }); + } catch (e) { + error(`failed to read mappings file ${file.name}`, e); + toast.error("Error occurred", { + description: `Could not read mappings file ${file.name}, check the console.`, + }); + } + }, + async applyMappings(content: string, format: MappingFormat): Promise { + try { + const parsed = parseMappings(format, content); + + recordProgress("applying mappings", null, async (task) => { + let completed = 0; + parsed.classes.forEach(async (clazz) => { + const data = await mapClass(clazz, parsed); + + if (data.mapped) { + + entries.update(($entries) => { + $entries.delete(data.oldName); + $entries.set(data.entry?.name + ".class", data.entry!!); + return $entries; + }); + completed++; + task.desc.set( + `${parsed.classes.length} entries (${parsed.classes.length - completed} remaining)` + ); + task.progress?.set((completed / parsed.classes.length) * 100); + } + }); + + task.desc.set(`${parsed.classes.length} entries`); + }).then(() => { + + analyzeBackground().then(); + + toast.success("Mapped classes", { + description: `Mapped ${parsed.classes.length} ${parsed.classes.length === 1 ? "entry" : "entries"}.`, + }); + }); + } catch (e) { + error(`failed to apply mappings`, e); + toast.error("Error occurred", { + description: `Could not apply mappings file, check the console.`, + }); + } + }, async add(files?: File[]): Promise { if (!files) { files = await readFiles("", true); diff --git a/src/lib/event/index.ts b/src/lib/event/index.ts index 4f20ce4..7abb291 100644 --- a/src/lib/event/index.ts +++ b/src/lib/event/index.ts @@ -4,11 +4,14 @@ import type { Tab, TabDefinition, TabPosition, TabType } from "$lib/tab"; import type { Entry } from "$lib/workspace"; import { writable } from "svelte/store"; import defaultHandler from "./handler"; +import type { MappingFormat } from "$lib/mapping"; type Awaitable = T | PromiseLike; export interface EventHandler { load(): Awaitable; + loadMappings(entries: Entry[]): Awaitable; + applyMappings(content: string, format: MappingFormat): Awaitable; add(files?: File[]): Awaitable; addRemote(url: string): Awaitable; clear(): Awaitable; diff --git a/src/lib/mapping/index.ts b/src/lib/mapping/index.ts new file mode 100644 index 0000000..be6d962 --- /dev/null +++ b/src/lib/mapping/index.ts @@ -0,0 +1,96 @@ +import { CSRGTSRGParser, ProguardParser, SRGXSRGParser, TinyParser, TinyV2Parser, TSRG2Parser } from "./parsers"; +import type { MappingSet } from "./types"; +import { stripComment } from "./util"; + +export enum MappingFormat { + SRG_XSRG = "SRG_XSRG", + CSRG_TSRG = "CSRG_TSRG", + TSRG2 = "TSRG2", + PG = "PG", + TINY1 = "TINY1", + TINY2 = "TINY2", +} + +export function parseMappings(format: MappingFormat, content: string): MappingSet { + const linesRaw = content.split(/\r?\n/); + const lines = linesRaw.filter((l) => l.trim().length > 0); + + if (lines.length === 0) { + return { classes: [] }; + } + + // Find first non-empty non-comment line + let firstLine = ""; + for (const l of lines) { + const stripped = stripComment(l); + if (stripped.length > 0) { + firstLine = stripped; + break; + } + } + + // Use filters to clean lines for some loaders + const filtered = lines.map(stripComment).filter((l) => l.length > 0); + + switch (format) { + case MappingFormat.SRG_XSRG: + return new SRGXSRGParser().parse(filtered); + case MappingFormat.CSRG_TSRG: + return new CSRGTSRGParser().parse(filtered); + case MappingFormat.TSRG2: + return new TSRG2Parser().parse(filtered); + case MappingFormat.PG: + return new ProguardParser().parse(filtered); + case MappingFormat.TINY1: + return new TinyParser().parse(filtered); + case MappingFormat.TINY2: + return new TinyV2Parser().parse(filtered); + default: + throw new Error(`Unknown mapping format: ${format}`); + } +} + +/** + * Detects the mapping format of a Java mapping file based on its header and content. + */ +export function detectMappingFormat(content: string): MappingFormat | null { + const lines = content.split(/\r?\n/); + if (lines.length === 0) return null; + + // Skip empty and comment-only lines + let i = 0; + let firstLine = lines[i] ?? ""; + while (i < lines.length && stripComment(firstLine).length === 0) { + i++; + firstLine = lines[i] ?? ""; + } + + const test = firstLine.split(" ")[0]!!; + + if (["PK:", "CL:", "FD:", "MD:"].includes(test)) return MappingFormat.SRG_XSRG; + if (firstLine.includes(" -> ")) return MappingFormat.PG; + if (firstLine.startsWith("v1\t")) return MappingFormat.TINY1; + if (firstLine.startsWith("tiny\t")) return MappingFormat.TINY2; + if (firstLine.startsWith("tsrg2 ")) return MappingFormat.TSRG2; + + // Check if it follows CSRG/TSRG style structure + const nonCommentLines = lines.map(stripComment).filter((l) => l.trim().length > 0); + + if ( + nonCommentLines.length > 0 && + nonCommentLines.every((line) => { + const parts = line.trim().split(/\s+/); + + return ( + parts.length === 2 || // class rename + parts.length === 3 || // field rename + (parts.length === 4 && /^\(.*\).*$/.test(parts[2]!!)) // method with descriptor + ); + }) + ) { + return MappingFormat.CSRG_TSRG; + } + + // Default fallback to old TSRG/CSRG + return null; +} diff --git a/src/lib/mapping/parsers/abstract-mapping-parser.ts b/src/lib/mapping/parsers/abstract-mapping-parser.ts new file mode 100644 index 0000000..f591fc3 --- /dev/null +++ b/src/lib/mapping/parsers/abstract-mapping-parser.ts @@ -0,0 +1,7 @@ +import type { MappingSet } from "../types"; + +export abstract class AbstractMappingsParser { + + abstract parse(content: string[]): MappingSet; + +} \ No newline at end of file diff --git a/src/lib/mapping/parsers/csrg-tsrg-parser.ts b/src/lib/mapping/parsers/csrg-tsrg-parser.ts new file mode 100644 index 0000000..0271de0 --- /dev/null +++ b/src/lib/mapping/parsers/csrg-tsrg-parser.ts @@ -0,0 +1,100 @@ +import { AbstractMappingsParser } from "./abstract-mapping-parser"; +import type { MappingSet, ClassMapping, FieldMapping, MethodMapping } from "../types"; + +/** + * Parses CSRG format mappings. + */ +export class CSRGTSRGParser extends AbstractMappingsParser { + parse(content: string[]): MappingSet { + const classes: Map = new Map(); + + // First pass: packages and classes (lines without leading tab) + for (const l of content.filter((l) => !l.startsWith("\t"))) { + const pts = l.split(" "); + if (pts.length !== 2) continue; + if (pts[0]!!.endsWith("/")) { + // package line, ignoring + continue; + } else { + classes.set(pts[0]!!, { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[1]!!, + fields: [], + methods: [], + }); + } + } + + let currentClass: ClassMapping | null = null; + + for (const line of content) { + const pts = line.split(" "); + + if (line.startsWith("\t")) { + if (!currentClass) + throw new Error("Invalid TSRG line, missing class: " + line); + // Remove leading tab from first part + pts[0] = pts[0]!!.substring(1); + if (pts.length === 2) { + // field mapping + const field: FieldMapping = { + obfuscatedName: pts[0], + deobfuscatedName: pts[1]!!, + }; + currentClass.fields.push(field); + } else if (pts.length === 3) { + // method mapping + const method: MethodMapping = { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[2]!!, + descriptor: pts[1], + }; + currentClass.methods.push(method); + } else { + throw new Error("Invalid TSRG line, too many parts: " + line); + } + } else { + if (pts.length === 2) { + if (!pts[0]!!.endsWith("/")) { + currentClass = classes.get(pts[0]!!) || null; + } + } else if (pts.length === 3) { + // field on class + const clazz = classes.get(pts[0]!!) ?? { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[0]!!, + fields: [], + methods: [], + }; + if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); + const field: FieldMapping = { + obfuscatedName: pts[1]!!, + deobfuscatedName: pts[2]!!, + }; + clazz.fields.push(field); + } else if (pts.length === 4) { + // method on class + const clazz = classes.get(pts[0]!!) ?? { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[0]!!, + fields: [], + methods: [], + }; + if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); + const method: MethodMapping = { + obfuscatedName: pts[2]!!, + deobfuscatedName: pts[3]!!, + descriptor: pts[1], + }; + clazz.methods.push(method); + } else { + throw new Error("Invalid CSRG line, too many parts: " + line); + } + } + } + + return { + classes: Array.from(classes.values()), + }; + } +} \ No newline at end of file diff --git a/src/lib/mapping/parsers/index.ts b/src/lib/mapping/parsers/index.ts new file mode 100644 index 0000000..0e9175c --- /dev/null +++ b/src/lib/mapping/parsers/index.ts @@ -0,0 +1,6 @@ +export { SRGXSRGParser } from "./srg-xsrg-parser"; +export { CSRGTSRGParser } from "./csrg-tsrg-parser"; +export { TSRG2Parser } from "./tsrg2-parser"; +export { ProguardParser } from "./proguard-parser"; +export { TinyParser } from "./tiny-parser"; +export { TinyV2Parser } from './tiny-v2-parser' \ No newline at end of file diff --git a/src/lib/mapping/parsers/proguard-parser.ts b/src/lib/mapping/parsers/proguard-parser.ts new file mode 100644 index 0000000..24902c0 --- /dev/null +++ b/src/lib/mapping/parsers/proguard-parser.ts @@ -0,0 +1,129 @@ +import { toDesc } from "../util"; +import type { + MappingSet, + ClassMapping, + FieldMapping, + MethodMapping, +} from "../types"; +import { AbstractMappingsParser } from "./abstract-mapping-parser"; + +/** + * ProGuard parser. + * Does NOT support parameter or local variable names. + */ +export class ProguardParser extends AbstractMappingsParser { + parse(content: string[]): MappingSet { + const classes: Map = new Map(); + + + + let currentClass: ClassMapping | null = null; + + for (let line of content) { + + if (!line.startsWith(" ") && line.endsWith(":")) { + // Class declaration line + // e.g. some/package/ClassName -> new/package/NewClassName: + const parts = line.split(" -> "); + if (parts.length !== 2) + throw new Error("Invalid Proguard class line: " + line); + + const deobfName = parts[0]; + const obfName = parts[1]!!.slice(0, -1); // remove trailing ':' + + currentClass = { + obfuscatedName: obfName!!, + deobfuscatedName: deobfName!!, + fields: [], + methods: [], + }; + classes.set(obfName!!, currentClass); + } else if (line.includes("(") && line.includes(")")) { + // Method line + if (!currentClass) + throw new Error("Invalid Proguard line, missing class: " + line); + + line = line.trim(); + + let startLine = 0; + let endLine = 0; + + // Check if line has line number ranges like: 10:15 returnType methodName(...) + if (line.indexOf(":") !== -1) { + const firstColon = line.indexOf(":"); + const secondColon = line.indexOf(":", firstColon + 1); + if (secondColon !== -1) { + startLine = parseInt(line.substring(0, firstColon)); + endLine = parseInt(line.substring(firstColon + 1, secondColon)); + line = line.substring(secondColon + 1).trim(); + } + } + + // Split into parts around " -> " + const arrowIndex = line.indexOf(" -> "); + if (arrowIndex === -1) + throw new Error("Invalid Proguard method line (missing ->): " + line); + + const obfName = line.substring(arrowIndex + 4).trim(); + + // Extract return type, method name and args + const retTypeAndName = line.substring(0, arrowIndex).trim(); + // retType is the first word before space + const firstSpaceIdx = retTypeAndName.indexOf(" "); + if (firstSpaceIdx === -1) + throw new Error("Invalid Proguard method line (no space): " + line); + + const retType = retTypeAndName.substring(0, firstSpaceIdx).trim(); + const rest = retTypeAndName.substring(firstSpaceIdx + 1).trim(); + + // The method name is from after return type up to '(' + const parenStart = rest.indexOf("("); + if (parenStart === -1) + throw new Error("Invalid Proguard method line (no '('): " + line); + + const methodName = rest.substring(0, parenStart).trim(); + + // Parse args inside parentheses + const argsStr = rest.substring(parenStart + 1, rest.indexOf(")")); + const args = argsStr.length > 0 ? argsStr.split(",").map((a) => a.trim()) : []; + + // Build method descriptor string + let desc = "("; + for (const arg of args) { + if (arg.length === 0) break; + desc += toDesc(arg); + } + desc += ")" + toDesc(retType); + + const method: MethodMapping = { + obfuscatedName: obfName, + deobfuscatedName: methodName, + descriptor: desc, + }; + + currentClass.methods.push(method); + } else { + // Field line + if (!currentClass) + throw new Error("Invalid Proguard line, missing class: " + line); + + // e.g. int oldField -> newField + const parts = line.trim().split(" "); + if (parts.length < 4 || parts[2] !== "->") + throw new Error("Invalid Proguard field line: " + line); + + const field: FieldMapping = { + obfuscatedName: parts[3]!!, + deobfuscatedName: parts[1]!!, + descriptor: toDesc(parts[0]!!), + }; + currentClass.fields.push(field); + } + } + + return { + classes: Array.from(classes.values()), + }; + } + +} diff --git a/src/lib/mapping/parsers/srg-xsrg-parser.ts b/src/lib/mapping/parsers/srg-xsrg-parser.ts new file mode 100644 index 0000000..4c13593 --- /dev/null +++ b/src/lib/mapping/parsers/srg-xsrg-parser.ts @@ -0,0 +1,82 @@ +import { putOrGetClass, rsplit } from "../util"; +import type { + MappingSet, + ClassMapping, + FieldMapping, + MethodMapping, +} from "../types"; +import { AbstractMappingsParser } from "./abstract-mapping-parser"; + +/** + * SRG/XSRG parser. + * Does NOT support parameters or local variables (not in SRG). + */ +export class SRGXSRGParser extends AbstractMappingsParser { + parse(content: string[]): MappingSet { + const classes: Map = new Map(); + + for (const line of content) { + const pts = line.split(" "); + switch (pts[0]) { + case "PK:": + // Package mapping ignored + break; + case "CL:": { + const obf = pts[1]; + const deobf = pts[2]; + classes.set(obf!!, { + obfuscatedName: obf!!, + deobfuscatedName: deobf!!, + fields: [], + methods: [], + }); + break; + } + case "FD:": { + if (pts.length === 5) { + // XSRG format: FD: OrigClass/OrigField OrigDesc NewClass/NewField NewDesc + const [leftClass, leftField] = rsplit(pts[1], "/", 1); + const [_, rightField] = rsplit(pts[3], "/", 1); + const clazz = putOrGetClass(leftClass!!, classes); + const field: FieldMapping = { + obfuscatedName: leftField!!, + deobfuscatedName: rightField!!, + descriptor: pts[2], + }; + clazz.fields.push(field); + } else { + // SRG format: FD: OrigClass/OrigField NewClass/NewField + const [leftClass, leftField] = rsplit(pts[1], "/", 1); + const [_, rightField] = rsplit(pts[2], "/", 1); + const clazz = putOrGetClass(leftClass!!, classes); + const field: FieldMapping = { + obfuscatedName: leftField!!, + deobfuscatedName: rightField!!, + }; + clazz.fields.push(field); + } + break; + } + case "MD:": { + // MD: OrigClass/OrigMethod OrigDesc NewClass/NewMethod NewDesc + const [leftClass, leftMethod] = rsplit(pts[1], "/", 1); + const [_, rightMethod] = rsplit(pts[3], "/", 1); + const clazz = putOrGetClass(leftClass!!, classes); + const method: MethodMapping = { + obfuscatedName: leftMethod!!, + deobfuscatedName: rightMethod!!, + descriptor: pts[2], + }; + clazz.methods.push(method); + break; + } + default: + throw new Error("Invalid SRG file, Unknown type: " + line); + } + } + + return { + classes: Array.from(classes.values()), + }; + } +} diff --git a/src/lib/mapping/parsers/tiny-parser.ts b/src/lib/mapping/parsers/tiny-parser.ts new file mode 100644 index 0000000..caa25dd --- /dev/null +++ b/src/lib/mapping/parsers/tiny-parser.ts @@ -0,0 +1,87 @@ +import type { + MappingSet, + ClassMapping, +} from "../types"; +import { putOrGetClass } from "../util"; +import { AbstractMappingsParser } from "./abstract-mapping-parser"; + +/** + * Tiny v1 parser. + * Limited support, no parameters or locals. + * Only basic classes, fields, methods. + */ +export class TinyParser extends AbstractMappingsParser { + parse(content: string[]): MappingSet { + if (content.length === 0) { + throw new Error("Empty Tiny V1 lines"); + } + + const header = content[0]!!.split("\t"); + if (header.length < 3) { + throw new Error("Invalid Tiny v1 Header: " + content[0]); + } + + const nameCount = header.length - 1; + + // Map of obfuscated class name -> ClassMapping + const classes = new Map(); + + const classMappings: ClassMapping[] = []; + + function duplicate(value: string, count: number): string[] { + return Array(count).fill(value); + } + + for (let i = 1; i < content.length; i++) { + const line = content[i]!!.split("\t"); + if (line[0]!!.startsWith("#")) { + // Comment line, skip + continue; + } + + switch (line[0]) { + case "CLASS": + if (line.length !== nameCount + 1) { + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); + } + putOrGetClass(line.slice(1), classes, classMappings); + break; + + case "FIELD": + if (line.length !== nameCount + 3) { + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); + } + { + const cls = classes.get(line[1]!!) ?? putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); + cls.fields.push({ + obfuscatedName: line[3]!!, + deobfuscatedName: line[4] ?? line[3]!!, + descriptor: line[2], + }); + } + break; + + case "METHOD": + if (line.length !== nameCount + 3) { + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]!!}`); + } + { + const cls = classes.get(line[1]!!) ?? putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); + cls.methods.push({ + descriptor: line[2], + obfuscatedName: line[3]!!, + deobfuscatedName: line[4] ?? line[3]!!, + }); + } + break; + + default: + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); + } + } + + return { + classes: classMappings, + }; + } +} diff --git a/src/lib/mapping/parsers/tiny-v2-parser.ts b/src/lib/mapping/parsers/tiny-v2-parser.ts new file mode 100644 index 0000000..3d70885 --- /dev/null +++ b/src/lib/mapping/parsers/tiny-v2-parser.ts @@ -0,0 +1,160 @@ +import { putOrGetClass, tiny2Exception, unescapeTinyString } from "../util"; +import type { + MappingSet, + ClassMapping, + MethodMapping, + FieldMapping, + ParameterMapping, +} from "../types"; +import { AbstractMappingsParser } from "./abstract-mapping-parser"; + +export enum TinyV2State { + ROOT, + CLASS, + FIELD, + METHOD, + PARAMETER, +} + + +/** + * Tiny v2 parser with parameters & local variables support. + */ +export class TinyV2Parser extends AbstractMappingsParser { + parse(content: string[]): MappingSet { + if (content.length === 0) { + throw new Error("Empty Tiny V2 lines"); + } + + const header = content[0]!!.split("\t"); + if (header.length < 5) { + throw new Error("Invalid Tiny v2 Header: " + content[0]); + } + + const major = Number(header[1]); + const minor = Number(header[2]); + if (major !== 2 || minor !== 0) { + throw new Error("Unsupported Tiny v2 version: " + content[0]); + } + + const nameCount = header.length - 3; + + const classes = new Map(); + const classMappings: ClassMapping[] = []; + + const properties = new Map(); + let escaped = false; + let start = 1; + + // Parse properties at the beginning (empty first column lines) + for (; start < content.length; start++) { + const line = content[start]!!.split("\t"); + if (line[0] !== "") break; + + const key = line[1]!!; + const value = line.length < 3 ? null : escaped ? unescapeTinyString(line[2]!!) : line[2]; + properties.set(key, value!!); + + if (key === "escaped-names") escaped = true; + } + + // State stacks and current elements + const stack: TinyV2State[] = []; + let cls: ClassMapping | null = null; + let field: FieldMapping | null = null; + let method: MethodMapping | null = null; + let param: ParameterMapping | null = null; + + for (let x = start; x < content.length; x++) { + let line = content[x]!!; + + // Count leading tabs to determine depth + let newdepth = 0; + while (line.charAt(newdepth) === "\t") newdepth++; + if (newdepth !== 0) line = line.substring(newdepth); + + // Pop stack until depth matches + while (stack.length !== newdepth) { + const popped = stack.pop(); + switch (popped) { + case TinyV2State.CLASS: + cls = null; + break; + case TinyV2State.FIELD: + field = null; + break; + case TinyV2State.METHOD: + method = null; + break; + case TinyV2State.PARAMETER: + param = null; + break; + } + } + + const parts = line.split("\t"); + if (escaped) { + for (let y = 1; y < parts.length; y++) { + parts[y] = unescapeTinyString(parts[y]!!); + } + } + + switch (parts[0]) { + case "c": // Class + if (stack.length === 0) { + if (parts.length !== nameCount + 1) throw tiny2Exception(x, line); + cls = putOrGetClass(parts.slice(1), classes, classMappings); + stack.push(TinyV2State.CLASS); + } + break; + + case "f": // Field + if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) throw tiny2Exception(x, line); + if (!cls) throw tiny2Exception(x, line); + field = { + obfuscatedName: parts[2]!!, + deobfuscatedName: parts[3]!!, + descriptor: parts[1], + }; + cls.fields.push(field); + stack.push(TinyV2State.FIELD); + break; + + case "m": // Method + if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) throw tiny2Exception(x, line); + if (!cls) throw tiny2Exception(x, line); + method = { + obfuscatedName: parts[2]!!, + deobfuscatedName: parts[3]!!, + descriptor: parts[1], + parameters: [], + }; + cls.methods.push(method); + stack.push(TinyV2State.METHOD); + break; + + case "p": // Parameter + if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.METHOD) throw tiny2Exception(x, line); + if (!method) throw tiny2Exception(x, line); + const index = Number(parts[1]); + const paramNames = parts.slice(2); + param = { + index, + obfuscatedName: paramNames[0] ?? "", + deobfuscatedName: paramNames[1] ?? paramNames[0] ?? "", + }; + method.parameters?.push(param); + stack.push(TinyV2State.PARAMETER); + break; + + case "v": // Local variable (ignored) + break; + + default: + throw tiny2Exception(x, line); + } + } + + return { classes: classMappings }; + } +} diff --git a/src/lib/mapping/parsers/tsrg2-parser.ts b/src/lib/mapping/parsers/tsrg2-parser.ts new file mode 100644 index 0000000..191ab37 --- /dev/null +++ b/src/lib/mapping/parsers/tsrg2-parser.ts @@ -0,0 +1,115 @@ +import { AbstractMappingsParser } from "./abstract-mapping-parser"; +import type { MappingSet, ClassMapping, MethodMapping } from "../types"; +import { putOrGetClass, swapFirst } from "../util"; + +/** + * Parses TSRG2 format mappings. + */ +export class TSRG2Parser extends AbstractMappingsParser { + parse(content: string[]): MappingSet { + if (content.length === 0) { + throw new Error("Empty TSRG2 lines"); + } + + const header = content[0]!!.split(" "); + if (header.length < 3) { + throw new Error("Invalid TSRG v2 Header: " + content[0]); + } + const nameCount = header.length - 1; // number of names per line after first + + // The first line lists the "name keys" like ["left", "right", "intermediary"] + // We won't use those names explicitly here, but we parse lines accordingly + content = content.slice(1); + + // Internal structures to hold mappings + const classes: Map = new Map(); + + let currentClass: ClassMapping | null = null; + let currentMethod: MethodMapping | null = null; + + for (const line of content) { + if (line.length < 2) { + throw new Error("Invalid TSRG v2 line, too short: " + line); + } + + const pts = line.split(" "); + + if (line.charAt(0) !== "\t") { + // Classes or Packages are not tabbed + if (pts.length !== nameCount) { + throw new Error("Invalid TSRG v2 line: " + line); + } + + // Class line: add class mapping + currentClass = putOrGetClass(pts, classes); + currentMethod = null; + } else if (line.charAt(1) === "\t") { + // Parameter or static marker line, two tabs at start + if (currentMethod === null) { + throw new Error("Invalid TSRG v2 line, missing method: " + line); + } + // Strip two tabs from start + pts[0] = pts[0]!!.substring(2); + + if (pts.length === nameCount + 1) { + // Parameter line: first is index, rest are names + const paramIndex = parseInt(pts[0], 10); + if (isNaN(paramIndex)) { + throw new Error("Invalid TSRG v2 parameter index: " + pts[0]); + } + const paramNames = pts.slice(1); + if (!currentMethod.parameters) currentMethod.parameters = []; + currentMethod.parameters.push({ + index: paramIndex, + obfuscatedName: paramNames[0]!!, + deobfuscatedName: (paramNames[1] ?? paramNames[0])!!, + }); + } else { + throw new Error("Invalid TSRG v2 line, too many parts: " + line); + } + } else { + // One tab at start: field or method line + if (currentClass === null) { + throw new Error("Invalid TSRG v2 line, missing class: " + line); + } + // Strip one tab + pts[0] = pts[0]!!.substring(1); + + if (pts.length === nameCount) { + // Field without descriptor + currentClass.fields.push({ + obfuscatedName: pts[0], + deobfuscatedName: pts[1] ?? pts[0], + }); + currentMethod = null; + } else if (pts.length === 1 + nameCount) { + // Field with descriptor or method + swapFirst(pts); + if (pts[0].startsWith("(")) { + // Method line: pts[0] = descriptor, rest = names + currentMethod = { + descriptor: pts[0], + obfuscatedName: pts[1]!!, + deobfuscatedName: pts[2] ?? pts[1]!!, + }; + currentClass.methods.push(currentMethod); + } else { + // Field with descriptor + currentMethod = null; + currentClass.fields.push({ + obfuscatedName: pts[1]!!, + deobfuscatedName: pts[2] ?? pts[1]!!, + descriptor: pts[0], + }); + } + } else { + throw new Error("Invalid TSRG v2 line, too many parts: " + line); + } + } + } + + return { + classes: Array.from(classes.values()), + }; + } +} \ No newline at end of file diff --git a/src/lib/mapping/types.ts b/src/lib/mapping/types.ts new file mode 100644 index 0000000..724f7e5 --- /dev/null +++ b/src/lib/mapping/types.ts @@ -0,0 +1,29 @@ +export interface MappingSet { + classes: ClassMapping[]; +} + +export interface ClassMapping { + obfuscatedName: string; + deobfuscatedName: string; + fields: FieldMapping[]; + methods: MethodMapping[]; +} + +export interface FieldMapping { + obfuscatedName: string; + deobfuscatedName: string; + descriptor?: string; +} + +export interface MethodMapping { + obfuscatedName: string; + deobfuscatedName: string; + descriptor?: string; + parameters?: ParameterMapping[]; +} + +export interface ParameterMapping { + index: number; + obfuscatedName: string; + deobfuscatedName: string; +} \ No newline at end of file diff --git a/src/lib/mapping/util.ts b/src/lib/mapping/util.ts new file mode 100644 index 0000000..293bab1 --- /dev/null +++ b/src/lib/mapping/util.ts @@ -0,0 +1,98 @@ +import type { ClassMapping } from "./types"; + +export const stripComment = (line: string): string => { + const idx = line.indexOf("#"); + if (idx === 0) return ""; + if (idx !== -1) line = line.substring(0, idx - 1); + let end = line.length; + while (end > 1 && line.charAt(end - 1) === " ") end--; + return end === 0 ? "" : line.substring(0, end); +}; + +export const rsplit = (str: string | undefined, chr: string, count: number): string[] => { + const parts: string[] = []; + let remainder = str ?? ""; + while (count > 0) { + const idx = remainder.lastIndexOf(chr); + if (idx === -1) break; + parts.push(remainder.substring(idx + 1)); + remainder = remainder.substring(0, idx); + count--; + } + parts.push(remainder); + parts.reverse(); + return parts; +}; + +export const toDesc = (type: string): string => { + if (type.endsWith("[]")) { + return "[" + toDesc(type.substring(0, type.length - 2)); + } + switch (type) { + case "int": + return "I"; + case "void": + return "V"; + case "boolean": + return "Z"; + case "byte": + return "B"; + case "char": + return "C"; + case "short": + return "S"; + case "double": + return "D"; + case "float": + return "F"; + case "long": + return "J"; + default: + // Object type: use 'L' + internal name + ';' + // If it contains dots, convert them to slashes + const internalName = type.replace(/\./g, "/"); + return `L${internalName};`; + } +}; + +export const swapFirst = (arr: string[]) => { + if (arr.length >= 2) { + const tmp = arr[0]!!; + arr[0] = arr[1]!!; + arr[1] = tmp; + } +}; + +export const unescapeTinyString = (value: string): string => { + return value + .replace(/\\\\/g, "\\") + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\r") + .replace(/\\t/g, "\t") + .replace(/\\0/g, "\0"); +}; + +export const tiny2Exception = (line: number, data: string): Error => { + return new Error(`Invalid Tiny v2 line: #${line}: ${data}`); +}; + +export const putOrGetClass = ( + names: string | string[], + classes: Map, + classMappings?: ClassMapping[] +): ClassMapping => { + const [obfName, deobfName] = typeof names === 'string' ? [names, names] : [names[0]!, names[1] ?? names[0]!]; + + if (!classes.has(obfName)) { + const cls: ClassMapping = { + obfuscatedName: obfName, + deobfuscatedName: deobfName, + fields: [], + methods: [], + }; + classes.set(obfName, cls); + classMappings?.push(cls); + } + + return classes.get(obfName)!; +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index dbb88b8..e6c6955 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -186,6 +186,22 @@ export const readFiles = (pattern: string, multiple: boolean): Promise = }); }; +export const fileToString = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + resolve(reader.result as string); + }; + + reader.onerror = () => { + reject(reader.error); + }; + + reader.readAsText(file); // reads file as string (UTF-8 by default) + }); +} + export const downloadUrl = (name: string, url: string) => { const link = document.createElement("a"); link.style.display = "none"; diff --git a/src/lib/workspace/index.ts b/src/lib/workspace/index.ts index 2b6da34..6184467 100644 --- a/src/lib/workspace/index.ts +++ b/src/lib/workspace/index.ts @@ -1,9 +1,10 @@ import { warn } from "$lib/log"; +import type { ClassMapping, MappingSet } from "$lib/mapping/types"; import { analysisBackground } from "$lib/state"; import { record } from "$lib/task"; import { prettyMethodDesc } from "$lib/utils"; import type { Member, Node } from "@run-slicer/asm"; -import type { UTF8Entry } from "@run-slicer/asm/pool"; +import { type UTF8Entry } from "@run-slicer/asm/pool"; import type { Zip } from "@run-slicer/zip"; import { derived, get, writable } from "svelte/store"; import { AnalysisState, analyze, analyzeBackground, analyzeSchedule } from "./analysis"; @@ -129,7 +130,14 @@ export interface LoadResult { created: boolean; } +export interface MapClassResult { + mapped: boolean; + oldName: string; + entry?: ClassEntry; +} + export const ZIP_EXTENSIONS = new Set(["zip", "jar", "apk", "war", "ear", "jmod"]); +export const MAPPINGS_EXTENSIONS = new Set(["srg", "csrg", "tsrg", "tiny", "txt", "xsrg"]); const load0 = async (entries: Map, d: Data, parent?: Entry): Promise => { const name = parent ? `${parent.name}/${d.name}` : d.name; @@ -233,6 +241,30 @@ export const clear = () => { }); }; +export const mapClass = async (clazz: ClassMapping, mappings: MappingSet): Promise => { + const entries0 = get(entries); + + const fullName = clazz.obfuscatedName + ".class"; + + const entry = entries0.get(fullName) as ClassEntry; + if (!entry) return { mapped: false, oldName: fullName, entry: undefined }; + + const parsedNames = parseName(clazz.deobfuscatedName + ".class"); + + // TODO: change name, interfaces, superclasses, fields, methods, methods' parameters,... + + return { + mapped: true, + oldName: fullName, + entry: { + ...entry, + ...parsedNames, + parent: undefined, + state: AnalysisState.NONE, + } as ClassEntry, + }; +}; + // PWA file handler // @ts-ignore - experimental APIs if (window.launchQueue) { From 3ef8dbaff2564e51f07c66ab46cf21bb19ac6868 Mon Sep 17 00:00:00 2001 From: Dani-error Date: Sun, 27 Jul 2025 16:00:08 +0100 Subject: [PATCH 2/6] feat: working remapping --- package.json | 1 + src/lib/mapping/index.ts | 2 +- .../parsers/abstract-mapping-parser.ts | 2 +- src/lib/mapping/parsers/csrg-tsrg-parser.ts | 2 +- src/lib/mapping/parsers/proguard-parser.ts | 2 +- src/lib/mapping/parsers/srg-xsrg-parser.ts | 2 +- src/lib/mapping/parsers/tiny-parser.ts | 2 +- src/lib/mapping/parsers/tiny-v2-parser.ts | 2 +- src/lib/mapping/parsers/tsrg2-parser.ts | 2 +- src/lib/mapping/types.ts | 29 -------------- src/lib/mapping/util.ts | 2 +- src/lib/workspace/index.ts | 15 ++++--- vite.config.ts | 40 +++++++++++++++++-- 13 files changed, 57 insertions(+), 46 deletions(-) delete mode 100644 src/lib/mapping/types.ts diff --git a/package.json b/package.json index 08e08e8..19a985a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "comlink": "^4.4.2", "elkjs": "^0.10.0", "html-to-image": "1.11.11", + "java-remapper": "^1.0.3", "queueable": "^5.3.2", "svelte-modals": "^2.0.1", "tailwind-merge": "^3.3.1", diff --git a/src/lib/mapping/index.ts b/src/lib/mapping/index.ts index be6d962..efae3bf 100644 --- a/src/lib/mapping/index.ts +++ b/src/lib/mapping/index.ts @@ -1,5 +1,5 @@ import { CSRGTSRGParser, ProguardParser, SRGXSRGParser, TinyParser, TinyV2Parser, TSRG2Parser } from "./parsers"; -import type { MappingSet } from "./types"; +import type { MappingSet } from "java-remapper"; import { stripComment } from "./util"; export enum MappingFormat { diff --git a/src/lib/mapping/parsers/abstract-mapping-parser.ts b/src/lib/mapping/parsers/abstract-mapping-parser.ts index f591fc3..21ac94e 100644 --- a/src/lib/mapping/parsers/abstract-mapping-parser.ts +++ b/src/lib/mapping/parsers/abstract-mapping-parser.ts @@ -1,4 +1,4 @@ -import type { MappingSet } from "../types"; +import type { MappingSet } from "java-remapper"; export abstract class AbstractMappingsParser { diff --git a/src/lib/mapping/parsers/csrg-tsrg-parser.ts b/src/lib/mapping/parsers/csrg-tsrg-parser.ts index 0271de0..ca129df 100644 --- a/src/lib/mapping/parsers/csrg-tsrg-parser.ts +++ b/src/lib/mapping/parsers/csrg-tsrg-parser.ts @@ -1,5 +1,5 @@ import { AbstractMappingsParser } from "./abstract-mapping-parser"; -import type { MappingSet, ClassMapping, FieldMapping, MethodMapping } from "../types"; +import type { MappingSet, ClassMapping, FieldMapping, MethodMapping } from "java-remapper"; /** * Parses CSRG format mappings. diff --git a/src/lib/mapping/parsers/proguard-parser.ts b/src/lib/mapping/parsers/proguard-parser.ts index 24902c0..dbd5082 100644 --- a/src/lib/mapping/parsers/proguard-parser.ts +++ b/src/lib/mapping/parsers/proguard-parser.ts @@ -4,7 +4,7 @@ import type { ClassMapping, FieldMapping, MethodMapping, -} from "../types"; +} from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; /** diff --git a/src/lib/mapping/parsers/srg-xsrg-parser.ts b/src/lib/mapping/parsers/srg-xsrg-parser.ts index 4c13593..ea06a05 100644 --- a/src/lib/mapping/parsers/srg-xsrg-parser.ts +++ b/src/lib/mapping/parsers/srg-xsrg-parser.ts @@ -4,7 +4,7 @@ import type { ClassMapping, FieldMapping, MethodMapping, -} from "../types"; +} from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; /** diff --git a/src/lib/mapping/parsers/tiny-parser.ts b/src/lib/mapping/parsers/tiny-parser.ts index caa25dd..b807718 100644 --- a/src/lib/mapping/parsers/tiny-parser.ts +++ b/src/lib/mapping/parsers/tiny-parser.ts @@ -1,7 +1,7 @@ import type { MappingSet, ClassMapping, -} from "../types"; +} from "java-remapper"; import { putOrGetClass } from "../util"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; diff --git a/src/lib/mapping/parsers/tiny-v2-parser.ts b/src/lib/mapping/parsers/tiny-v2-parser.ts index 3d70885..f950436 100644 --- a/src/lib/mapping/parsers/tiny-v2-parser.ts +++ b/src/lib/mapping/parsers/tiny-v2-parser.ts @@ -5,7 +5,7 @@ import type { MethodMapping, FieldMapping, ParameterMapping, -} from "../types"; +} from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; export enum TinyV2State { diff --git a/src/lib/mapping/parsers/tsrg2-parser.ts b/src/lib/mapping/parsers/tsrg2-parser.ts index 191ab37..5a88d9a 100644 --- a/src/lib/mapping/parsers/tsrg2-parser.ts +++ b/src/lib/mapping/parsers/tsrg2-parser.ts @@ -1,5 +1,5 @@ import { AbstractMappingsParser } from "./abstract-mapping-parser"; -import type { MappingSet, ClassMapping, MethodMapping } from "../types"; +import type { MappingSet, ClassMapping, MethodMapping } from "java-remapper"; import { putOrGetClass, swapFirst } from "../util"; /** diff --git a/src/lib/mapping/types.ts b/src/lib/mapping/types.ts deleted file mode 100644 index 724f7e5..0000000 --- a/src/lib/mapping/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface MappingSet { - classes: ClassMapping[]; -} - -export interface ClassMapping { - obfuscatedName: string; - deobfuscatedName: string; - fields: FieldMapping[]; - methods: MethodMapping[]; -} - -export interface FieldMapping { - obfuscatedName: string; - deobfuscatedName: string; - descriptor?: string; -} - -export interface MethodMapping { - obfuscatedName: string; - deobfuscatedName: string; - descriptor?: string; - parameters?: ParameterMapping[]; -} - -export interface ParameterMapping { - index: number; - obfuscatedName: string; - deobfuscatedName: string; -} \ No newline at end of file diff --git a/src/lib/mapping/util.ts b/src/lib/mapping/util.ts index 293bab1..d2617f1 100644 --- a/src/lib/mapping/util.ts +++ b/src/lib/mapping/util.ts @@ -1,4 +1,4 @@ -import type { ClassMapping } from "./types"; +import type { ClassMapping } from "java-remapper"; export const stripComment = (line: string): string => { const idx = line.indexOf("#"); diff --git a/src/lib/workspace/index.ts b/src/lib/workspace/index.ts index 6184467..f411625 100644 --- a/src/lib/workspace/index.ts +++ b/src/lib/workspace/index.ts @@ -1,9 +1,9 @@ import { warn } from "$lib/log"; -import type { ClassMapping, MappingSet } from "$lib/mapping/types"; +import { remap, type ClassMapping, type MappingSet } from "java-remapper"; import { analysisBackground } from "$lib/state"; import { record } from "$lib/task"; import { prettyMethodDesc } from "$lib/utils"; -import type { Member, Node } from "@run-slicer/asm"; +import { read, type Member, type Node } from "@run-slicer/asm"; import { type UTF8Entry } from "@run-slicer/asm/pool"; import type { Zip } from "@run-slicer/zip"; import { derived, get, writable } from "svelte/store"; @@ -251,7 +251,10 @@ export const mapClass = async (clazz: ClassMapping, mappings: MappingSet): Promi const parsedNames = parseName(clazz.deobfuscatedName + ".class"); - // TODO: change name, interfaces, superclasses, fields, methods, methods' parameters,... + const entryBytes = await entry.data.bytes() + const remappedData = await remap(entryBytes, mappings) + const newData = memoryData(parsedNames.name, remappedData) + const newNode = read(remappedData) return { mapped: true, @@ -259,10 +262,12 @@ export const mapClass = async (clazz: ClassMapping, mappings: MappingSet): Promi entry: { ...entry, ...parsedNames, + node: newNode, parent: undefined, state: AnalysisState.NONE, - } as ClassEntry, - }; + data: newData + } as ClassEntry + } }; // PWA file handler diff --git a/vite.config.ts b/vite.config.ts index f33236f..85e303f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,45 @@ import { svelte } from "@sveltejs/vite-plugin-svelte"; import tailwindcss from "@tailwindcss/vite"; -import { resolve } from "path"; -import { defineConfig } from "vite"; +import { readFileSync } from "fs"; +import { basename, join, resolve } from "path"; +import { defineConfig, PluginOption } from "vite"; + +const wasmMiddleware: (files: { module: string; file: string }[]) => PluginOption = (files) => { + return { + name: "wasm-middleware", + configureServer(server) { + server.middlewares.use((req, res, next) => { + files.forEach((file) => { + if (req.url && req.url.endsWith(file.file)) { + const wasmPath = join(__dirname, "node_modules/" + file.module, basename(req.url)); + const wasmFile = readFileSync(wasmPath); + res.setHeader("Content-Type", "application/wasm"); + res.end(wasmFile); + return; + } + }); + next(); + }); + }, + }; +}; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [tailwindcss(), svelte()], + plugins: [ + tailwindcss(), + svelte(), + wasmMiddleware([ + { + file: "java-remapper.wasm", + module: "java-remapper", + }, + { + file: "jasm.wasm", + module: "@run-slicer/jasm" + } + ]), + ], build: { sourcemap: "hidden", }, From f407a0a5743e81da6dd89c6ad55adc189a39fb5d Mon Sep 17 00:00:00 2001 From: Dani-error Date: Sun, 27 Jul 2025 16:30:57 +0100 Subject: [PATCH 3/6] feat: mappings loading --- src/lib/components/dialog/index.ts | 4 +- .../components/dialog/load_mappings.svelte | 7 +- src/lib/event/handler.ts | 53 ++-- src/lib/event/index.ts | 2 +- src/lib/mapping/index.ts | 2 +- .../parsers/abstract-mapping-parser.ts | 4 +- src/lib/mapping/parsers/csrg-tsrg-parser.ts | 175 ++++++------ src/lib/mapping/parsers/index.ts | 6 +- src/lib/mapping/parsers/proguard-parser.ts | 216 +++++++------- src/lib/mapping/parsers/srg-xsrg-parser.ts | 135 +++++---- src/lib/mapping/parsers/tiny-parser.ts | 133 ++++----- src/lib/mapping/parsers/tiny-v2-parser.ts | 270 +++++++++--------- src/lib/mapping/parsers/tsrg2-parser.ts | 196 ++++++------- src/lib/mapping/util.ts | 30 +- src/lib/utils.ts | 22 +- src/lib/workspace/index.ts | 18 +- vite.config.ts | 4 +- 17 files changed, 635 insertions(+), 642 deletions(-) diff --git a/src/lib/components/dialog/index.ts b/src/lib/components/dialog/index.ts index 80de4ed..f735e22 100644 --- a/src/lib/components/dialog/index.ts +++ b/src/lib/components/dialog/index.ts @@ -2,20 +2,20 @@ import AboutDialog from "./about.svelte"; import ClearDialog from "./clear.svelte"; import DeleteDialog from "./delete.svelte"; import LoadExternalDialog from "./load_external.svelte"; +import LoadMappings from "./load_mappings.svelte"; import ScriptDialog from "./script.svelte"; import ScriptDeleteDialog from "./script_delete.svelte"; import ScriptLoadDialog from "./script_load.svelte"; import ScriptLoadShareDialog from "./script_load_share.svelte"; -import LoadMappings from "./load_mappings.svelte"; export { AboutDialog, ClearDialog, DeleteDialog, LoadExternalDialog, + LoadMappings, ScriptDeleteDialog, ScriptDialog, ScriptLoadDialog, ScriptLoadShareDialog, - LoadMappings }; diff --git a/src/lib/components/dialog/load_mappings.svelte b/src/lib/components/dialog/load_mappings.svelte index 91c4f9f..ced7851 100644 --- a/src/lib/components/dialog/load_mappings.svelte +++ b/src/lib/components/dialog/load_mappings.svelte @@ -18,7 +18,7 @@ interface Props extends ModalProps { detectedFormat: MappingFormat | null; - content: string + content: string; handler: EventHandler; } @@ -44,7 +44,7 @@ isOpen = false; - await handler.applyMappings(content, value as MappingFormat) + await handler.applyMappings(content, value as MappingFormat); }; const triggerContent = $derived( @@ -52,7 +52,6 @@ ? displayNames[value as keyof typeof MappingFormat] : "Select a mapping format..." ); - open || close()}> @@ -74,7 +73,7 @@ {#each formats as format} -
+
{displayNames[format]} {#if detectedFormat === format} Detected diff --git a/src/lib/event/handler.ts b/src/lib/event/handler.ts index 58c820a..8d7a0fe 100644 --- a/src/lib/event/handler.ts +++ b/src/lib/event/handler.ts @@ -178,33 +178,54 @@ export default { try { const parsed = parseMappings(format, content); - recordProgress("applying mappings", null, async (task) => { + await recordProgress("applying mappings", null, async (task) => { let completed = 0; - parsed.classes.forEach(async (clazz) => { + const oldToNewMap = new Map(); // Track old → new mapping + + for (const clazz of parsed.classes) { const data = await mapClass(clazz, parsed); if (data.mapped) { - - entries.update(($entries) => { - $entries.delete(data.oldName); - $entries.set(data.entry?.name + ".class", data.entry!!); - return $entries; - }); - completed++; - task.desc.set( - `${parsed.classes.length} entries (${parsed.classes.length - completed} remaining)` - ); - task.progress?.set((completed / parsed.classes.length) * 100); + oldToNewMap.set(data.oldName, data.entry!!); } + + completed++; + task.desc.set(`${parsed.classes.length} entries (${parsed.classes.length - completed} remaining)`); + task.progress?.set((completed / parsed.classes.length) * 100); + } + + entries.update(($entries) => { + oldToNewMap.forEach((value, key) => { + $entries.delete(key); + $entries.set(value.name, value); + }); + return $entries; + }); + + tabs.update(($tabs) => { + oldToNewMap.forEach((newEntry, oldName) => { + for (const [key, tab] of $tabs.entries()) { + if (tab.entry && tab.entry.name === oldName) { + $tabs.set(key, { + ...tab, + id: `${TabType.CODE}:${newEntry.name}`, + name: newEntry.shortName, + entry: newEntry, + }); + } + } + }); + return $tabs; }); task.desc.set(`${parsed.classes.length} entries`); - }).then(() => { - analyzeBackground().then(); + await analyzeBackground(); toast.success("Mapped classes", { - description: `Mapped ${parsed.classes.length} ${parsed.classes.length === 1 ? "entry" : "entries"}.`, + description: `Mapped ${parsed.classes.length} ${ + parsed.classes.length === 1 ? "entry" : "entries" + }.`, }); }); } catch (e) { diff --git a/src/lib/event/index.ts b/src/lib/event/index.ts index 7abb291..a5ef51b 100644 --- a/src/lib/event/index.ts +++ b/src/lib/event/index.ts @@ -1,10 +1,10 @@ import type { Disassembler } from "$lib/disasm"; +import type { MappingFormat } from "$lib/mapping"; import type { ProtoScript } from "$lib/script"; import type { Tab, TabDefinition, TabPosition, TabType } from "$lib/tab"; import type { Entry } from "$lib/workspace"; import { writable } from "svelte/store"; import defaultHandler from "./handler"; -import type { MappingFormat } from "$lib/mapping"; type Awaitable = T | PromiseLike; diff --git a/src/lib/mapping/index.ts b/src/lib/mapping/index.ts index efae3bf..af18bb2 100644 --- a/src/lib/mapping/index.ts +++ b/src/lib/mapping/index.ts @@ -1,5 +1,5 @@ -import { CSRGTSRGParser, ProguardParser, SRGXSRGParser, TinyParser, TinyV2Parser, TSRG2Parser } from "./parsers"; import type { MappingSet } from "java-remapper"; +import { CSRGTSRGParser, ProguardParser, SRGXSRGParser, TinyParser, TinyV2Parser, TSRG2Parser } from "./parsers"; import { stripComment } from "./util"; export enum MappingFormat { diff --git a/src/lib/mapping/parsers/abstract-mapping-parser.ts b/src/lib/mapping/parsers/abstract-mapping-parser.ts index 21ac94e..eb28382 100644 --- a/src/lib/mapping/parsers/abstract-mapping-parser.ts +++ b/src/lib/mapping/parsers/abstract-mapping-parser.ts @@ -1,7 +1,5 @@ import type { MappingSet } from "java-remapper"; export abstract class AbstractMappingsParser { - abstract parse(content: string[]): MappingSet; - -} \ No newline at end of file +} diff --git a/src/lib/mapping/parsers/csrg-tsrg-parser.ts b/src/lib/mapping/parsers/csrg-tsrg-parser.ts index ca129df..866c752 100644 --- a/src/lib/mapping/parsers/csrg-tsrg-parser.ts +++ b/src/lib/mapping/parsers/csrg-tsrg-parser.ts @@ -1,100 +1,99 @@ +import type { ClassMapping, FieldMapping, MappingSet, MethodMapping } from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; -import type { MappingSet, ClassMapping, FieldMapping, MethodMapping } from "java-remapper"; /** * Parses CSRG format mappings. */ export class CSRGTSRGParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - const classes: Map = new Map(); + parse(content: string[]): MappingSet { + const classes: Map = new Map(); - // First pass: packages and classes (lines without leading tab) - for (const l of content.filter((l) => !l.startsWith("\t"))) { - const pts = l.split(" "); - if (pts.length !== 2) continue; - if (pts[0]!!.endsWith("/")) { - // package line, ignoring - continue; - } else { - classes.set(pts[0]!!, { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[1]!!, - fields: [], - methods: [], - }); - } - } + // First pass: packages and classes (lines without leading tab) + for (const l of content.filter((l) => !l.startsWith("\t"))) { + const pts = l.split(" "); + if (pts.length !== 2) continue; + if (pts[0]!!.endsWith("/")) { + // package line, ignoring + continue; + } else { + classes.set(pts[0]!!, { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[1]!!, + fields: [], + methods: [], + }); + } + } - let currentClass: ClassMapping | null = null; + let currentClass: ClassMapping | null = null; - for (const line of content) { - const pts = line.split(" "); + for (const line of content) { + const pts = line.split(" "); - if (line.startsWith("\t")) { - if (!currentClass) - throw new Error("Invalid TSRG line, missing class: " + line); - // Remove leading tab from first part - pts[0] = pts[0]!!.substring(1); - if (pts.length === 2) { - // field mapping - const field: FieldMapping = { - obfuscatedName: pts[0], - deobfuscatedName: pts[1]!!, - }; - currentClass.fields.push(field); - } else if (pts.length === 3) { - // method mapping - const method: MethodMapping = { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[2]!!, - descriptor: pts[1], - }; - currentClass.methods.push(method); - } else { - throw new Error("Invalid TSRG line, too many parts: " + line); - } - } else { - if (pts.length === 2) { - if (!pts[0]!!.endsWith("/")) { - currentClass = classes.get(pts[0]!!) || null; - } - } else if (pts.length === 3) { - // field on class - const clazz = classes.get(pts[0]!!) ?? { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[0]!!, - fields: [], - methods: [], - }; - if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); - const field: FieldMapping = { - obfuscatedName: pts[1]!!, - deobfuscatedName: pts[2]!!, - }; - clazz.fields.push(field); - } else if (pts.length === 4) { - // method on class - const clazz = classes.get(pts[0]!!) ?? { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[0]!!, - fields: [], - methods: [], - }; - if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); - const method: MethodMapping = { - obfuscatedName: pts[2]!!, - deobfuscatedName: pts[3]!!, - descriptor: pts[1], - }; - clazz.methods.push(method); - } else { - throw new Error("Invalid CSRG line, too many parts: " + line); + if (line.startsWith("\t")) { + if (!currentClass) throw new Error("Invalid TSRG line, missing class: " + line); + // Remove leading tab from first part + pts[0] = pts[0]!!.substring(1); + if (pts.length === 2) { + // field mapping + const field: FieldMapping = { + obfuscatedName: pts[0], + deobfuscatedName: pts[1]!!, + }; + currentClass.fields.push(field); + } else if (pts.length === 3) { + // method mapping + const method: MethodMapping = { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[2]!!, + descriptor: pts[1], + }; + currentClass.methods.push(method); + } else { + throw new Error("Invalid TSRG line, too many parts: " + line); + } + } else { + if (pts.length === 2) { + if (!pts[0]!!.endsWith("/")) { + currentClass = classes.get(pts[0]!!) || null; + } + } else if (pts.length === 3) { + // field on class + const clazz = classes.get(pts[0]!!) ?? { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[0]!!, + fields: [], + methods: [], + }; + if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); + const field: FieldMapping = { + obfuscatedName: pts[1]!!, + deobfuscatedName: pts[2]!!, + }; + clazz.fields.push(field); + } else if (pts.length === 4) { + // method on class + const clazz = classes.get(pts[0]!!) ?? { + obfuscatedName: pts[0]!!, + deobfuscatedName: pts[0]!!, + fields: [], + methods: [], + }; + if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); + const method: MethodMapping = { + obfuscatedName: pts[2]!!, + deobfuscatedName: pts[3]!!, + descriptor: pts[1], + }; + clazz.methods.push(method); + } else { + throw new Error("Invalid CSRG line, too many parts: " + line); + } + } } - } - } - return { - classes: Array.from(classes.values()), - }; - } -} \ No newline at end of file + return { + classes: Array.from(classes.values()), + }; + } +} diff --git a/src/lib/mapping/parsers/index.ts b/src/lib/mapping/parsers/index.ts index 0e9175c..d98aeaa 100644 --- a/src/lib/mapping/parsers/index.ts +++ b/src/lib/mapping/parsers/index.ts @@ -1,6 +1,6 @@ -export { SRGXSRGParser } from "./srg-xsrg-parser"; export { CSRGTSRGParser } from "./csrg-tsrg-parser"; -export { TSRG2Parser } from "./tsrg2-parser"; export { ProguardParser } from "./proguard-parser"; +export { SRGXSRGParser } from "./srg-xsrg-parser"; export { TinyParser } from "./tiny-parser"; -export { TinyV2Parser } from './tiny-v2-parser' \ No newline at end of file +export { TinyV2Parser } from "./tiny-v2-parser"; +export { TSRG2Parser } from "./tsrg2-parser"; diff --git a/src/lib/mapping/parsers/proguard-parser.ts b/src/lib/mapping/parsers/proguard-parser.ts index dbd5082..98d04d8 100644 --- a/src/lib/mapping/parsers/proguard-parser.ts +++ b/src/lib/mapping/parsers/proguard-parser.ts @@ -1,10 +1,5 @@ +import type { ClassMapping, FieldMapping, MappingSet, MethodMapping } from "java-remapper"; import { toDesc } from "../util"; -import type { - MappingSet, - ClassMapping, - FieldMapping, - MethodMapping, -} from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; /** @@ -12,118 +7,107 @@ import { AbstractMappingsParser } from "./abstract-mapping-parser"; * Does NOT support parameter or local variable names. */ export class ProguardParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - const classes: Map = new Map(); - - - - let currentClass: ClassMapping | null = null; - - for (let line of content) { - - if (!line.startsWith(" ") && line.endsWith(":")) { - // Class declaration line - // e.g. some/package/ClassName -> new/package/NewClassName: - const parts = line.split(" -> "); - if (parts.length !== 2) - throw new Error("Invalid Proguard class line: " + line); - - const deobfName = parts[0]; - const obfName = parts[1]!!.slice(0, -1); // remove trailing ':' - - currentClass = { - obfuscatedName: obfName!!, - deobfuscatedName: deobfName!!, - fields: [], - methods: [], - }; - classes.set(obfName!!, currentClass); - } else if (line.includes("(") && line.includes(")")) { - // Method line - if (!currentClass) - throw new Error("Invalid Proguard line, missing class: " + line); - - line = line.trim(); - - let startLine = 0; - let endLine = 0; - - // Check if line has line number ranges like: 10:15 returnType methodName(...) - if (line.indexOf(":") !== -1) { - const firstColon = line.indexOf(":"); - const secondColon = line.indexOf(":", firstColon + 1); - if (secondColon !== -1) { - startLine = parseInt(line.substring(0, firstColon)); - endLine = parseInt(line.substring(firstColon + 1, secondColon)); - line = line.substring(secondColon + 1).trim(); - } + parse(content: string[]): MappingSet { + const classes: Map = new Map(); + + let currentClass: ClassMapping | null = null; + + for (let line of content) { + if (!line.startsWith(" ") && line.endsWith(":")) { + // Class declaration line + // e.g. some/package/ClassName -> new/package/NewClassName: + const parts = line.split(" -> "); + if (parts.length !== 2) throw new Error("Invalid Proguard class line: " + line); + + const deobfName = parts[0]; + const obfName = parts[1]!!.slice(0, -1); // remove trailing ':' + + currentClass = { + obfuscatedName: obfName!!, + deobfuscatedName: deobfName!!, + fields: [], + methods: [], + }; + classes.set(obfName!!, currentClass); + } else if (line.includes("(") && line.includes(")")) { + // Method line + if (!currentClass) throw new Error("Invalid Proguard line, missing class: " + line); + + line = line.trim(); + + let startLine = 0; + let endLine = 0; + + // Check if line has line number ranges like: 10:15 returnType methodName(...) + if (line.indexOf(":") !== -1) { + const firstColon = line.indexOf(":"); + const secondColon = line.indexOf(":", firstColon + 1); + if (secondColon !== -1) { + startLine = parseInt(line.substring(0, firstColon)); + endLine = parseInt(line.substring(firstColon + 1, secondColon)); + line = line.substring(secondColon + 1).trim(); + } + } + + // Split into parts around " -> " + const arrowIndex = line.indexOf(" -> "); + if (arrowIndex === -1) throw new Error("Invalid Proguard method line (missing ->): " + line); + + const obfName = line.substring(arrowIndex + 4).trim(); + + // Extract return type, method name and args + const retTypeAndName = line.substring(0, arrowIndex).trim(); + // retType is the first word before space + const firstSpaceIdx = retTypeAndName.indexOf(" "); + if (firstSpaceIdx === -1) throw new Error("Invalid Proguard method line (no space): " + line); + + const retType = retTypeAndName.substring(0, firstSpaceIdx).trim(); + const rest = retTypeAndName.substring(firstSpaceIdx + 1).trim(); + + // The method name is from after return type up to '(' + const parenStart = rest.indexOf("("); + if (parenStart === -1) throw new Error("Invalid Proguard method line (no '('): " + line); + + const methodName = rest.substring(0, parenStart).trim(); + + // Parse args inside parentheses + const argsStr = rest.substring(parenStart + 1, rest.indexOf(")")); + const args = argsStr.length > 0 ? argsStr.split(",").map((a) => a.trim()) : []; + + // Build method descriptor string + let desc = "("; + for (const arg of args) { + if (arg.length === 0) break; + desc += toDesc(arg); + } + desc += ")" + toDesc(retType); + + const method: MethodMapping = { + obfuscatedName: obfName, + deobfuscatedName: methodName, + descriptor: desc, + }; + + currentClass.methods.push(method); + } else { + // Field line + if (!currentClass) throw new Error("Invalid Proguard line, missing class: " + line); + + // e.g. int oldField -> newField + const parts = line.trim().split(" "); + if (parts.length < 4 || parts[2] !== "->") throw new Error("Invalid Proguard field line: " + line); + + const field: FieldMapping = { + obfuscatedName: parts[3]!!, + deobfuscatedName: parts[1]!!, + descriptor: toDesc(parts[0]!!), + }; + currentClass.fields.push(field); + } } - // Split into parts around " -> " - const arrowIndex = line.indexOf(" -> "); - if (arrowIndex === -1) - throw new Error("Invalid Proguard method line (missing ->): " + line); - - const obfName = line.substring(arrowIndex + 4).trim(); - - // Extract return type, method name and args - const retTypeAndName = line.substring(0, arrowIndex).trim(); - // retType is the first word before space - const firstSpaceIdx = retTypeAndName.indexOf(" "); - if (firstSpaceIdx === -1) - throw new Error("Invalid Proguard method line (no space): " + line); - - const retType = retTypeAndName.substring(0, firstSpaceIdx).trim(); - const rest = retTypeAndName.substring(firstSpaceIdx + 1).trim(); - - // The method name is from after return type up to '(' - const parenStart = rest.indexOf("("); - if (parenStart === -1) - throw new Error("Invalid Proguard method line (no '('): " + line); - - const methodName = rest.substring(0, parenStart).trim(); - - // Parse args inside parentheses - const argsStr = rest.substring(parenStart + 1, rest.indexOf(")")); - const args = argsStr.length > 0 ? argsStr.split(",").map((a) => a.trim()) : []; - - // Build method descriptor string - let desc = "("; - for (const arg of args) { - if (arg.length === 0) break; - desc += toDesc(arg); - } - desc += ")" + toDesc(retType); - - const method: MethodMapping = { - obfuscatedName: obfName, - deobfuscatedName: methodName, - descriptor: desc, - }; - - currentClass.methods.push(method); - } else { - // Field line - if (!currentClass) - throw new Error("Invalid Proguard line, missing class: " + line); - - // e.g. int oldField -> newField - const parts = line.trim().split(" "); - if (parts.length < 4 || parts[2] !== "->") - throw new Error("Invalid Proguard field line: " + line); - - const field: FieldMapping = { - obfuscatedName: parts[3]!!, - deobfuscatedName: parts[1]!!, - descriptor: toDesc(parts[0]!!), + return { + classes: Array.from(classes.values()), }; - currentClass.fields.push(field); - } } - - return { - classes: Array.from(classes.values()), - }; - } - } diff --git a/src/lib/mapping/parsers/srg-xsrg-parser.ts b/src/lib/mapping/parsers/srg-xsrg-parser.ts index ea06a05..7aaa8a2 100644 --- a/src/lib/mapping/parsers/srg-xsrg-parser.ts +++ b/src/lib/mapping/parsers/srg-xsrg-parser.ts @@ -1,10 +1,5 @@ +import type { ClassMapping, FieldMapping, MappingSet, MethodMapping } from "java-remapper"; import { putOrGetClass, rsplit } from "../util"; -import type { - MappingSet, - ClassMapping, - FieldMapping, - MethodMapping, -} from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; /** @@ -12,71 +7,71 @@ import { AbstractMappingsParser } from "./abstract-mapping-parser"; * Does NOT support parameters or local variables (not in SRG). */ export class SRGXSRGParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - const classes: Map = new Map(); + parse(content: string[]): MappingSet { + const classes: Map = new Map(); - for (const line of content) { - const pts = line.split(" "); - switch (pts[0]) { - case "PK:": - // Package mapping ignored - break; - case "CL:": { - const obf = pts[1]; - const deobf = pts[2]; - classes.set(obf!!, { - obfuscatedName: obf!!, - deobfuscatedName: deobf!!, - fields: [], - methods: [], - }); - break; + for (const line of content) { + const pts = line.split(" "); + switch (pts[0]) { + case "PK:": + // Package mapping ignored + break; + case "CL:": { + const obf = pts[1]; + const deobf = pts[2]; + classes.set(obf!!, { + obfuscatedName: obf!!, + deobfuscatedName: deobf!!, + fields: [], + methods: [], + }); + break; + } + case "FD:": { + if (pts.length === 5) { + // XSRG format: FD: OrigClass/OrigField OrigDesc NewClass/NewField NewDesc + const [leftClass, leftField] = rsplit(pts[1], "/", 1); + const [_, rightField] = rsplit(pts[3], "/", 1); + const clazz = putOrGetClass(leftClass!!, classes); + const field: FieldMapping = { + obfuscatedName: leftField!!, + deobfuscatedName: rightField!!, + descriptor: pts[2], + }; + clazz.fields.push(field); + } else { + // SRG format: FD: OrigClass/OrigField NewClass/NewField + const [leftClass, leftField] = rsplit(pts[1], "/", 1); + const [_, rightField] = rsplit(pts[2], "/", 1); + const clazz = putOrGetClass(leftClass!!, classes); + const field: FieldMapping = { + obfuscatedName: leftField!!, + deobfuscatedName: rightField!!, + }; + clazz.fields.push(field); + } + break; + } + case "MD:": { + // MD: OrigClass/OrigMethod OrigDesc NewClass/NewMethod NewDesc + const [leftClass, leftMethod] = rsplit(pts[1], "/", 1); + const [_, rightMethod] = rsplit(pts[3], "/", 1); + const clazz = putOrGetClass(leftClass!!, classes); + const method: MethodMapping = { + obfuscatedName: leftMethod!!, + deobfuscatedName: rightMethod!!, + descriptor: pts[2], + }; + clazz.methods.push(method); + break; + } + default: + throw new Error("Invalid SRG file, Unknown type: " + line); + } } - case "FD:": { - if (pts.length === 5) { - // XSRG format: FD: OrigClass/OrigField OrigDesc NewClass/NewField NewDesc - const [leftClass, leftField] = rsplit(pts[1], "/", 1); - const [_, rightField] = rsplit(pts[3], "/", 1); - const clazz = putOrGetClass(leftClass!!, classes); - const field: FieldMapping = { - obfuscatedName: leftField!!, - deobfuscatedName: rightField!!, - descriptor: pts[2], - }; - clazz.fields.push(field); - } else { - // SRG format: FD: OrigClass/OrigField NewClass/NewField - const [leftClass, leftField] = rsplit(pts[1], "/", 1); - const [_, rightField] = rsplit(pts[2], "/", 1); - const clazz = putOrGetClass(leftClass!!, classes); - const field: FieldMapping = { - obfuscatedName: leftField!!, - deobfuscatedName: rightField!!, - }; - clazz.fields.push(field); - } - break; - } - case "MD:": { - // MD: OrigClass/OrigMethod OrigDesc NewClass/NewMethod NewDesc - const [leftClass, leftMethod] = rsplit(pts[1], "/", 1); - const [_, rightMethod] = rsplit(pts[3], "/", 1); - const clazz = putOrGetClass(leftClass!!, classes); - const method: MethodMapping = { - obfuscatedName: leftMethod!!, - deobfuscatedName: rightMethod!!, - descriptor: pts[2], - }; - clazz.methods.push(method); - break; - } - default: - throw new Error("Invalid SRG file, Unknown type: " + line); - } - } - return { - classes: Array.from(classes.values()), - }; - } + return { + classes: Array.from(classes.values()), + }; + } } diff --git a/src/lib/mapping/parsers/tiny-parser.ts b/src/lib/mapping/parsers/tiny-parser.ts index b807718..b4560d7 100644 --- a/src/lib/mapping/parsers/tiny-parser.ts +++ b/src/lib/mapping/parsers/tiny-parser.ts @@ -1,7 +1,4 @@ -import type { - MappingSet, - ClassMapping, -} from "java-remapper"; +import type { ClassMapping, MappingSet } from "java-remapper"; import { putOrGetClass } from "../util"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; @@ -11,77 +8,81 @@ import { AbstractMappingsParser } from "./abstract-mapping-parser"; * Only basic classes, fields, methods. */ export class TinyParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - if (content.length === 0) { - throw new Error("Empty Tiny V1 lines"); - } + parse(content: string[]): MappingSet { + if (content.length === 0) { + throw new Error("Empty Tiny V1 lines"); + } - const header = content[0]!!.split("\t"); - if (header.length < 3) { - throw new Error("Invalid Tiny v1 Header: " + content[0]); - } + const header = content[0]!!.split("\t"); + if (header.length < 3) { + throw new Error("Invalid Tiny v1 Header: " + content[0]); + } - const nameCount = header.length - 1; + const nameCount = header.length - 1; - // Map of obfuscated class name -> ClassMapping - const classes = new Map(); + // Map of obfuscated class name -> ClassMapping + const classes = new Map(); - const classMappings: ClassMapping[] = []; + const classMappings: ClassMapping[] = []; - function duplicate(value: string, count: number): string[] { - return Array(count).fill(value); - } + function duplicate(value: string, count: number): string[] { + return Array(count).fill(value); + } - for (let i = 1; i < content.length; i++) { - const line = content[i]!!.split("\t"); - if (line[0]!!.startsWith("#")) { - // Comment line, skip - continue; - } + for (let i = 1; i < content.length; i++) { + const line = content[i]!!.split("\t"); + if (line[0]!!.startsWith("#")) { + // Comment line, skip + continue; + } - switch (line[0]) { - case "CLASS": - if (line.length !== nameCount + 1) { - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); - } - putOrGetClass(line.slice(1), classes, classMappings); - break; + switch (line[0]) { + case "CLASS": + if (line.length !== nameCount + 1) { + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); + } + putOrGetClass(line.slice(1), classes, classMappings); + break; - case "FIELD": - if (line.length !== nameCount + 3) { - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); - } - { - const cls = classes.get(line[1]!!) ?? putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); - cls.fields.push({ - obfuscatedName: line[3]!!, - deobfuscatedName: line[4] ?? line[3]!!, - descriptor: line[2], - }); - } - break; + case "FIELD": + if (line.length !== nameCount + 3) { + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); + } + { + const cls = + classes.get(line[1]!!) ?? + putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); + cls.fields.push({ + obfuscatedName: line[3]!!, + deobfuscatedName: line[4] ?? line[3]!!, + descriptor: line[2], + }); + } + break; - case "METHOD": - if (line.length !== nameCount + 3) { - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]!!}`); - } - { - const cls = classes.get(line[1]!!) ?? putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); - cls.methods.push({ - descriptor: line[2], - obfuscatedName: line[3]!!, - deobfuscatedName: line[4] ?? line[3]!!, - }); - } - break; + case "METHOD": + if (line.length !== nameCount + 3) { + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]!!}`); + } + { + const cls = + classes.get(line[1]!!) ?? + putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); + cls.methods.push({ + descriptor: line[2], + obfuscatedName: line[3]!!, + deobfuscatedName: line[4] ?? line[3]!!, + }); + } + break; - default: - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); - } - } + default: + throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); + } + } - return { - classes: classMappings, - }; - } + return { + classes: classMappings, + }; + } } diff --git a/src/lib/mapping/parsers/tiny-v2-parser.ts b/src/lib/mapping/parsers/tiny-v2-parser.ts index f950436..f21be17 100644 --- a/src/lib/mapping/parsers/tiny-v2-parser.ts +++ b/src/lib/mapping/parsers/tiny-v2-parser.ts @@ -1,160 +1,156 @@ +import type { ClassMapping, FieldMapping, MappingSet, MethodMapping, ParameterMapping } from "java-remapper"; import { putOrGetClass, tiny2Exception, unescapeTinyString } from "../util"; -import type { - MappingSet, - ClassMapping, - MethodMapping, - FieldMapping, - ParameterMapping, -} from "java-remapper"; import { AbstractMappingsParser } from "./abstract-mapping-parser"; export enum TinyV2State { - ROOT, - CLASS, - FIELD, - METHOD, - PARAMETER, + ROOT, + CLASS, + FIELD, + METHOD, + PARAMETER, } - /** * Tiny v2 parser with parameters & local variables support. */ export class TinyV2Parser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - if (content.length === 0) { - throw new Error("Empty Tiny V2 lines"); - } - - const header = content[0]!!.split("\t"); - if (header.length < 5) { - throw new Error("Invalid Tiny v2 Header: " + content[0]); - } + parse(content: string[]): MappingSet { + if (content.length === 0) { + throw new Error("Empty Tiny V2 lines"); + } - const major = Number(header[1]); - const minor = Number(header[2]); - if (major !== 2 || minor !== 0) { - throw new Error("Unsupported Tiny v2 version: " + content[0]); - } + const header = content[0]!!.split("\t"); + if (header.length < 5) { + throw new Error("Invalid Tiny v2 Header: " + content[0]); + } - const nameCount = header.length - 3; + const major = Number(header[1]); + const minor = Number(header[2]); + if (major !== 2 || minor !== 0) { + throw new Error("Unsupported Tiny v2 version: " + content[0]); + } - const classes = new Map(); - const classMappings: ClassMapping[] = []; + const nameCount = header.length - 3; - const properties = new Map(); - let escaped = false; - let start = 1; + const classes = new Map(); + const classMappings: ClassMapping[] = []; - // Parse properties at the beginning (empty first column lines) - for (; start < content.length; start++) { - const line = content[start]!!.split("\t"); - if (line[0] !== "") break; + const properties = new Map(); + let escaped = false; + let start = 1; - const key = line[1]!!; - const value = line.length < 3 ? null : escaped ? unescapeTinyString(line[2]!!) : line[2]; - properties.set(key, value!!); + // Parse properties at the beginning (empty first column lines) + for (; start < content.length; start++) { + const line = content[start]!!.split("\t"); + if (line[0] !== "") break; - if (key === "escaped-names") escaped = true; - } + const key = line[1]!!; + const value = line.length < 3 ? null : escaped ? unescapeTinyString(line[2]!!) : line[2]; + properties.set(key, value!!); - // State stacks and current elements - const stack: TinyV2State[] = []; - let cls: ClassMapping | null = null; - let field: FieldMapping | null = null; - let method: MethodMapping | null = null; - let param: ParameterMapping | null = null; - - for (let x = start; x < content.length; x++) { - let line = content[x]!!; - - // Count leading tabs to determine depth - let newdepth = 0; - while (line.charAt(newdepth) === "\t") newdepth++; - if (newdepth !== 0) line = line.substring(newdepth); - - // Pop stack until depth matches - while (stack.length !== newdepth) { - const popped = stack.pop(); - switch (popped) { - case TinyV2State.CLASS: - cls = null; - break; - case TinyV2State.FIELD: - field = null; - break; - case TinyV2State.METHOD: - method = null; - break; - case TinyV2State.PARAMETER: - param = null; - break; + if (key === "escaped-names") escaped = true; } - } - const parts = line.split("\t"); - if (escaped) { - for (let y = 1; y < parts.length; y++) { - parts[y] = unescapeTinyString(parts[y]!!); + // State stacks and current elements + const stack: TinyV2State[] = []; + let cls: ClassMapping | null = null; + let field: FieldMapping | null = null; + let method: MethodMapping | null = null; + let param: ParameterMapping | null = null; + + for (let x = start; x < content.length; x++) { + let line = content[x]!!; + + // Count leading tabs to determine depth + let newdepth = 0; + while (line.charAt(newdepth) === "\t") newdepth++; + if (newdepth !== 0) line = line.substring(newdepth); + + // Pop stack until depth matches + while (stack.length !== newdepth) { + const popped = stack.pop(); + switch (popped) { + case TinyV2State.CLASS: + cls = null; + break; + case TinyV2State.FIELD: + field = null; + break; + case TinyV2State.METHOD: + method = null; + break; + case TinyV2State.PARAMETER: + param = null; + break; + } + } + + const parts = line.split("\t"); + if (escaped) { + for (let y = 1; y < parts.length; y++) { + parts[y] = unescapeTinyString(parts[y]!!); + } + } + + switch (parts[0]) { + case "c": // Class + if (stack.length === 0) { + if (parts.length !== nameCount + 1) throw tiny2Exception(x, line); + cls = putOrGetClass(parts.slice(1), classes, classMappings); + stack.push(TinyV2State.CLASS); + } + break; + + case "f": // Field + if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) + throw tiny2Exception(x, line); + if (!cls) throw tiny2Exception(x, line); + field = { + obfuscatedName: parts[2]!!, + deobfuscatedName: parts[3]!!, + descriptor: parts[1], + }; + cls.fields.push(field); + stack.push(TinyV2State.FIELD); + break; + + case "m": // Method + if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) + throw tiny2Exception(x, line); + if (!cls) throw tiny2Exception(x, line); + method = { + obfuscatedName: parts[2]!!, + deobfuscatedName: parts[3]!!, + descriptor: parts[1], + parameters: [], + }; + cls.methods.push(method); + stack.push(TinyV2State.METHOD); + break; + + case "p": // Parameter + if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.METHOD) + throw tiny2Exception(x, line); + if (!method) throw tiny2Exception(x, line); + const index = Number(parts[1]); + const paramNames = parts.slice(2); + param = { + index, + obfuscatedName: paramNames[0] ?? "", + deobfuscatedName: paramNames[1] ?? paramNames[0] ?? "", + }; + method.parameters?.push(param); + stack.push(TinyV2State.PARAMETER); + break; + + case "v": // Local variable (ignored) + break; + + default: + throw tiny2Exception(x, line); + } } - } - - switch (parts[0]) { - case "c": // Class - if (stack.length === 0) { - if (parts.length !== nameCount + 1) throw tiny2Exception(x, line); - cls = putOrGetClass(parts.slice(1), classes, classMappings); - stack.push(TinyV2State.CLASS); - } - break; - - case "f": // Field - if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) throw tiny2Exception(x, line); - if (!cls) throw tiny2Exception(x, line); - field = { - obfuscatedName: parts[2]!!, - deobfuscatedName: parts[3]!!, - descriptor: parts[1], - }; - cls.fields.push(field); - stack.push(TinyV2State.FIELD); - break; - - case "m": // Method - if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) throw tiny2Exception(x, line); - if (!cls) throw tiny2Exception(x, line); - method = { - obfuscatedName: parts[2]!!, - deobfuscatedName: parts[3]!!, - descriptor: parts[1], - parameters: [], - }; - cls.methods.push(method); - stack.push(TinyV2State.METHOD); - break; - - case "p": // Parameter - if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.METHOD) throw tiny2Exception(x, line); - if (!method) throw tiny2Exception(x, line); - const index = Number(parts[1]); - const paramNames = parts.slice(2); - param = { - index, - obfuscatedName: paramNames[0] ?? "", - deobfuscatedName: paramNames[1] ?? paramNames[0] ?? "", - }; - method.parameters?.push(param); - stack.push(TinyV2State.PARAMETER); - break; - - case "v": // Local variable (ignored) - break; - - default: - throw tiny2Exception(x, line); - } - } - return { classes: classMappings }; - } + return { classes: classMappings }; + } } diff --git a/src/lib/mapping/parsers/tsrg2-parser.ts b/src/lib/mapping/parsers/tsrg2-parser.ts index 5a88d9a..4d9a0c8 100644 --- a/src/lib/mapping/parsers/tsrg2-parser.ts +++ b/src/lib/mapping/parsers/tsrg2-parser.ts @@ -1,115 +1,115 @@ -import { AbstractMappingsParser } from "./abstract-mapping-parser"; -import type { MappingSet, ClassMapping, MethodMapping } from "java-remapper"; +import type { ClassMapping, MappingSet, MethodMapping } from "java-remapper"; import { putOrGetClass, swapFirst } from "../util"; +import { AbstractMappingsParser } from "./abstract-mapping-parser"; /** * Parses TSRG2 format mappings. */ export class TSRG2Parser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - if (content.length === 0) { - throw new Error("Empty TSRG2 lines"); - } + parse(content: string[]): MappingSet { + if (content.length === 0) { + throw new Error("Empty TSRG2 lines"); + } - const header = content[0]!!.split(" "); - if (header.length < 3) { - throw new Error("Invalid TSRG v2 Header: " + content[0]); - } - const nameCount = header.length - 1; // number of names per line after first + const header = content[0]!!.split(" "); + if (header.length < 3) { + throw new Error("Invalid TSRG v2 Header: " + content[0]); + } + const nameCount = header.length - 1; // number of names per line after first - // The first line lists the "name keys" like ["left", "right", "intermediary"] - // We won't use those names explicitly here, but we parse lines accordingly - content = content.slice(1); + // The first line lists the "name keys" like ["left", "right", "intermediary"] + // We won't use those names explicitly here, but we parse lines accordingly + content = content.slice(1); - // Internal structures to hold mappings - const classes: Map = new Map(); + // Internal structures to hold mappings + const classes: Map = new Map(); - let currentClass: ClassMapping | null = null; - let currentMethod: MethodMapping | null = null; + let currentClass: ClassMapping | null = null; + let currentMethod: MethodMapping | null = null; - for (const line of content) { - if (line.length < 2) { - throw new Error("Invalid TSRG v2 line, too short: " + line); - } + for (const line of content) { + if (line.length < 2) { + throw new Error("Invalid TSRG v2 line, too short: " + line); + } - const pts = line.split(" "); + const pts = line.split(" "); - if (line.charAt(0) !== "\t") { - // Classes or Packages are not tabbed - if (pts.length !== nameCount) { - throw new Error("Invalid TSRG v2 line: " + line); - } - - // Class line: add class mapping - currentClass = putOrGetClass(pts, classes); - currentMethod = null; - } else if (line.charAt(1) === "\t") { - // Parameter or static marker line, two tabs at start - if (currentMethod === null) { - throw new Error("Invalid TSRG v2 line, missing method: " + line); - } - // Strip two tabs from start - pts[0] = pts[0]!!.substring(2); + if (line.charAt(0) !== "\t") { + // Classes or Packages are not tabbed + if (pts.length !== nameCount) { + throw new Error("Invalid TSRG v2 line: " + line); + } - if (pts.length === nameCount + 1) { - // Parameter line: first is index, rest are names - const paramIndex = parseInt(pts[0], 10); - if (isNaN(paramIndex)) { - throw new Error("Invalid TSRG v2 parameter index: " + pts[0]); - } - const paramNames = pts.slice(1); - if (!currentMethod.parameters) currentMethod.parameters = []; - currentMethod.parameters.push({ - index: paramIndex, - obfuscatedName: paramNames[0]!!, - deobfuscatedName: (paramNames[1] ?? paramNames[0])!!, - }); - } else { - throw new Error("Invalid TSRG v2 line, too many parts: " + line); - } - } else { - // One tab at start: field or method line - if (currentClass === null) { - throw new Error("Invalid TSRG v2 line, missing class: " + line); - } - // Strip one tab - pts[0] = pts[0]!!.substring(1); + // Class line: add class mapping + currentClass = putOrGetClass(pts, classes); + currentMethod = null; + } else if (line.charAt(1) === "\t") { + // Parameter or static marker line, two tabs at start + if (currentMethod === null) { + throw new Error("Invalid TSRG v2 line, missing method: " + line); + } + // Strip two tabs from start + pts[0] = pts[0]!!.substring(2); - if (pts.length === nameCount) { - // Field without descriptor - currentClass.fields.push({ - obfuscatedName: pts[0], - deobfuscatedName: pts[1] ?? pts[0], - }); - currentMethod = null; - } else if (pts.length === 1 + nameCount) { - // Field with descriptor or method - swapFirst(pts); - if (pts[0].startsWith("(")) { - // Method line: pts[0] = descriptor, rest = names - currentMethod = { - descriptor: pts[0], - obfuscatedName: pts[1]!!, - deobfuscatedName: pts[2] ?? pts[1]!!, - }; - currentClass.methods.push(currentMethod); - } else { - // Field with descriptor - currentMethod = null; - currentClass.fields.push({ - obfuscatedName: pts[1]!!, - deobfuscatedName: pts[2] ?? pts[1]!!, - descriptor: pts[0], - }); - } - } else { - throw new Error("Invalid TSRG v2 line, too many parts: " + line); + if (pts.length === nameCount + 1) { + // Parameter line: first is index, rest are names + const paramIndex = parseInt(pts[0], 10); + if (isNaN(paramIndex)) { + throw new Error("Invalid TSRG v2 parameter index: " + pts[0]); + } + const paramNames = pts.slice(1); + if (!currentMethod.parameters) currentMethod.parameters = []; + currentMethod.parameters.push({ + index: paramIndex, + obfuscatedName: paramNames[0]!!, + deobfuscatedName: (paramNames[1] ?? paramNames[0])!!, + }); + } else { + throw new Error("Invalid TSRG v2 line, too many parts: " + line); + } + } else { + // One tab at start: field or method line + if (currentClass === null) { + throw new Error("Invalid TSRG v2 line, missing class: " + line); + } + // Strip one tab + pts[0] = pts[0]!!.substring(1); + + if (pts.length === nameCount) { + // Field without descriptor + currentClass.fields.push({ + obfuscatedName: pts[0], + deobfuscatedName: pts[1] ?? pts[0], + }); + currentMethod = null; + } else if (pts.length === 1 + nameCount) { + // Field with descriptor or method + swapFirst(pts); + if (pts[0].startsWith("(")) { + // Method line: pts[0] = descriptor, rest = names + currentMethod = { + descriptor: pts[0], + obfuscatedName: pts[1]!!, + deobfuscatedName: pts[2] ?? pts[1]!!, + }; + currentClass.methods.push(currentMethod); + } else { + // Field with descriptor + currentMethod = null; + currentClass.fields.push({ + obfuscatedName: pts[1]!!, + deobfuscatedName: pts[2] ?? pts[1]!!, + descriptor: pts[0], + }); + } + } else { + throw new Error("Invalid TSRG v2 line, too many parts: " + line); + } + } } - } - } - return { - classes: Array.from(classes.values()), - }; - } -} \ No newline at end of file + return { + classes: Array.from(classes.values()), + }; + } +} diff --git a/src/lib/mapping/util.ts b/src/lib/mapping/util.ts index d2617f1..a57236a 100644 --- a/src/lib/mapping/util.ts +++ b/src/lib/mapping/util.ts @@ -77,22 +77,22 @@ export const tiny2Exception = (line: number, data: string): Error => { }; export const putOrGetClass = ( - names: string | string[], - classes: Map, - classMappings?: ClassMapping[] + names: string | string[], + classes: Map, + classMappings?: ClassMapping[] ): ClassMapping => { - const [obfName, deobfName] = typeof names === 'string' ? [names, names] : [names[0]!, names[1] ?? names[0]!]; + const [obfName, deobfName] = typeof names === "string" ? [names, names] : [names[0]!, names[1] ?? names[0]!]; - if (!classes.has(obfName)) { - const cls: ClassMapping = { - obfuscatedName: obfName, - deobfuscatedName: deobfName, - fields: [], - methods: [], - }; - classes.set(obfName, cls); - classMappings?.push(cls); - } + if (!classes.has(obfName)) { + const cls: ClassMapping = { + obfuscatedName: obfName, + deobfuscatedName: deobfName, + fields: [], + methods: [], + }; + classes.set(obfName, cls); + classMappings?.push(cls); + } - return classes.get(obfName)!; + return classes.get(obfName)!; }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e3d1527..8dbfd2b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -188,20 +188,20 @@ export const readFiles = (pattern: string, multiple: boolean): Promise = }; export const fileToString = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); - reader.onload = () => { - resolve(reader.result as string); - }; + reader.onload = () => { + resolve(reader.result as string); + }; - reader.onerror = () => { - reject(reader.error); - }; + reader.onerror = () => { + reject(reader.error); + }; - reader.readAsText(file); // reads file as string (UTF-8 by default) - }); -} + reader.readAsText(file); // reads file as string (UTF-8 by default) + }); +}; export const downloadUrl = (name: string, url: string) => { const link = document.createElement("a"); diff --git a/src/lib/workspace/index.ts b/src/lib/workspace/index.ts index f411625..dccf2ee 100644 --- a/src/lib/workspace/index.ts +++ b/src/lib/workspace/index.ts @@ -1,15 +1,15 @@ import { warn } from "$lib/log"; -import { remap, type ClassMapping, type MappingSet } from "java-remapper"; import { analysisBackground } from "$lib/state"; import { record } from "$lib/task"; import { prettyMethodDesc } from "$lib/utils"; import { read, type Member, type Node } from "@run-slicer/asm"; import { type UTF8Entry } from "@run-slicer/asm/pool"; import type { Zip } from "@run-slicer/zip"; +import { remap, type ClassMapping, type MappingSet } from "java-remapper"; import { derived, get, writable } from "svelte/store"; import { AnalysisState, analyze, analyzeBackground, analyzeSchedule } from "./analysis"; import { transform } from "./analysis/transform"; -import { type Data, fileData, memoryData, type Named, parseName, zipData } from "./data"; +import { fileData, memoryData, parseName, zipData, type Data, type Named } from "./data"; import { archiveDecoder } from "./encoding"; export const enum EntryType { @@ -251,10 +251,10 @@ export const mapClass = async (clazz: ClassMapping, mappings: MappingSet): Promi const parsedNames = parseName(clazz.deobfuscatedName + ".class"); - const entryBytes = await entry.data.bytes() - const remappedData = await remap(entryBytes, mappings) - const newData = memoryData(parsedNames.name, remappedData) - const newNode = read(remappedData) + const entryBytes = await entry.data.bytes(); + const remappedData = await remap(entryBytes, mappings); + const newData = memoryData(parsedNames.name, remappedData); + const newNode = read(remappedData); return { mapped: true, @@ -265,9 +265,9 @@ export const mapClass = async (clazz: ClassMapping, mappings: MappingSet): Promi node: newNode, parent: undefined, state: AnalysisState.NONE, - data: newData - } as ClassEntry - } + data: newData, + } as ClassEntry, + }; }; // PWA file handler diff --git a/vite.config.ts b/vite.config.ts index 85e303f..96aa59c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -36,8 +36,8 @@ export default defineConfig({ }, { file: "jasm.wasm", - module: "@run-slicer/jasm" - } + module: "@run-slicer/jasm", + }, ]), ], build: { From 8565eb9a8b7ca69d8b336a06e8186ee304f057fb Mon Sep 17 00:00:00 2001 From: Dani-error Date: Mon, 28 Jul 2025 00:02:42 +0100 Subject: [PATCH 4/6] chore: add mapping loading feature --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a3fc8ea..2b1063e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ A modern Java reverse engineering tool for the web. - simple hexadecimal viewer for binary files - a simple JS scripting API for doing various things - multi-pane workspace for viewing multiple files at once +- supports loading different mapping formats for remapping + - Tiny v1, Tiny v2, SRG/XSRC, CSRG/TSRG, TSRG v2, ProGuard - [shadcn/ui](https://ui.shadcn.com/) design and theming support ## Installation From aac0a5a7eaffa3d81fae0241b8b978adbfcfac49 Mon Sep 17 00:00:00 2001 From: Dani-error Date: Mon, 28 Jul 2025 09:37:47 +0100 Subject: [PATCH 5/6] bump: java-remapper (v1.0.4) --- package.json | 2 +- .../components/dialog/load_mappings.svelte | 2 +- src/lib/event/handler.ts | 2 +- src/lib/event/index.ts | 2 +- src/lib/mapping/index.ts | 96 ----------- .../parsers/abstract-mapping-parser.ts | 5 - src/lib/mapping/parsers/csrg-tsrg-parser.ts | 99 ----------- src/lib/mapping/parsers/index.ts | 6 - src/lib/mapping/parsers/proguard-parser.ts | 113 ------------- src/lib/mapping/parsers/srg-xsrg-parser.ts | 77 --------- src/lib/mapping/parsers/tiny-parser.ts | 88 ---------- src/lib/mapping/parsers/tiny-v2-parser.ts | 156 ------------------ src/lib/mapping/parsers/tsrg2-parser.ts | 115 ------------- src/lib/mapping/util.ts | 98 ----------- 14 files changed, 4 insertions(+), 857 deletions(-) delete mode 100644 src/lib/mapping/index.ts delete mode 100644 src/lib/mapping/parsers/abstract-mapping-parser.ts delete mode 100644 src/lib/mapping/parsers/csrg-tsrg-parser.ts delete mode 100644 src/lib/mapping/parsers/index.ts delete mode 100644 src/lib/mapping/parsers/proguard-parser.ts delete mode 100644 src/lib/mapping/parsers/srg-xsrg-parser.ts delete mode 100644 src/lib/mapping/parsers/tiny-parser.ts delete mode 100644 src/lib/mapping/parsers/tiny-v2-parser.ts delete mode 100644 src/lib/mapping/parsers/tsrg2-parser.ts delete mode 100644 src/lib/mapping/util.ts diff --git a/package.json b/package.json index 19a985a..a5df22e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "comlink": "^4.4.2", "elkjs": "^0.10.0", "html-to-image": "1.11.11", - "java-remapper": "^1.0.3", + "java-remapper": "^1.0.4", "queueable": "^5.3.2", "svelte-modals": "^2.0.1", "tailwind-merge": "^3.3.1", diff --git a/src/lib/components/dialog/load_mappings.svelte b/src/lib/components/dialog/load_mappings.svelte index ced7851..15c5df1 100644 --- a/src/lib/components/dialog/load_mappings.svelte +++ b/src/lib/components/dialog/load_mappings.svelte @@ -12,7 +12,7 @@ import type { ModalProps } from "svelte-modals"; import type { EventHandler } from "$lib/event"; import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select"; - import { MappingFormat } from "$lib/mapping"; + import { MappingFormat } from "java-remapper"; import { cn } from "../utils"; import Badge from "../ui/badge/badge.svelte"; diff --git a/src/lib/event/handler.ts b/src/lib/event/handler.ts index 8d7a0fe..e7cb71a 100644 --- a/src/lib/event/handler.ts +++ b/src/lib/event/handler.ts @@ -1,7 +1,7 @@ import { LoadMappings } from "$lib/components/dialog"; import { disassembleEntry, type Disassembler } from "$lib/disasm"; import { error } from "$lib/log"; -import { detectMappingFormat, MappingFormat, parseMappings } from "$lib/mapping"; +import { detectMappingFormat, MappingFormat, parseMappings } from "java-remapper"; import { load as loadScript, type ProtoScript, diff --git a/src/lib/event/index.ts b/src/lib/event/index.ts index a5ef51b..55b83d7 100644 --- a/src/lib/event/index.ts +++ b/src/lib/event/index.ts @@ -1,5 +1,5 @@ import type { Disassembler } from "$lib/disasm"; -import type { MappingFormat } from "$lib/mapping"; +import type { MappingFormat } from "java-remapper"; import type { ProtoScript } from "$lib/script"; import type { Tab, TabDefinition, TabPosition, TabType } from "$lib/tab"; import type { Entry } from "$lib/workspace"; diff --git a/src/lib/mapping/index.ts b/src/lib/mapping/index.ts deleted file mode 100644 index af18bb2..0000000 --- a/src/lib/mapping/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { MappingSet } from "java-remapper"; -import { CSRGTSRGParser, ProguardParser, SRGXSRGParser, TinyParser, TinyV2Parser, TSRG2Parser } from "./parsers"; -import { stripComment } from "./util"; - -export enum MappingFormat { - SRG_XSRG = "SRG_XSRG", - CSRG_TSRG = "CSRG_TSRG", - TSRG2 = "TSRG2", - PG = "PG", - TINY1 = "TINY1", - TINY2 = "TINY2", -} - -export function parseMappings(format: MappingFormat, content: string): MappingSet { - const linesRaw = content.split(/\r?\n/); - const lines = linesRaw.filter((l) => l.trim().length > 0); - - if (lines.length === 0) { - return { classes: [] }; - } - - // Find first non-empty non-comment line - let firstLine = ""; - for (const l of lines) { - const stripped = stripComment(l); - if (stripped.length > 0) { - firstLine = stripped; - break; - } - } - - // Use filters to clean lines for some loaders - const filtered = lines.map(stripComment).filter((l) => l.length > 0); - - switch (format) { - case MappingFormat.SRG_XSRG: - return new SRGXSRGParser().parse(filtered); - case MappingFormat.CSRG_TSRG: - return new CSRGTSRGParser().parse(filtered); - case MappingFormat.TSRG2: - return new TSRG2Parser().parse(filtered); - case MappingFormat.PG: - return new ProguardParser().parse(filtered); - case MappingFormat.TINY1: - return new TinyParser().parse(filtered); - case MappingFormat.TINY2: - return new TinyV2Parser().parse(filtered); - default: - throw new Error(`Unknown mapping format: ${format}`); - } -} - -/** - * Detects the mapping format of a Java mapping file based on its header and content. - */ -export function detectMappingFormat(content: string): MappingFormat | null { - const lines = content.split(/\r?\n/); - if (lines.length === 0) return null; - - // Skip empty and comment-only lines - let i = 0; - let firstLine = lines[i] ?? ""; - while (i < lines.length && stripComment(firstLine).length === 0) { - i++; - firstLine = lines[i] ?? ""; - } - - const test = firstLine.split(" ")[0]!!; - - if (["PK:", "CL:", "FD:", "MD:"].includes(test)) return MappingFormat.SRG_XSRG; - if (firstLine.includes(" -> ")) return MappingFormat.PG; - if (firstLine.startsWith("v1\t")) return MappingFormat.TINY1; - if (firstLine.startsWith("tiny\t")) return MappingFormat.TINY2; - if (firstLine.startsWith("tsrg2 ")) return MappingFormat.TSRG2; - - // Check if it follows CSRG/TSRG style structure - const nonCommentLines = lines.map(stripComment).filter((l) => l.trim().length > 0); - - if ( - nonCommentLines.length > 0 && - nonCommentLines.every((line) => { - const parts = line.trim().split(/\s+/); - - return ( - parts.length === 2 || // class rename - parts.length === 3 || // field rename - (parts.length === 4 && /^\(.*\).*$/.test(parts[2]!!)) // method with descriptor - ); - }) - ) { - return MappingFormat.CSRG_TSRG; - } - - // Default fallback to old TSRG/CSRG - return null; -} diff --git a/src/lib/mapping/parsers/abstract-mapping-parser.ts b/src/lib/mapping/parsers/abstract-mapping-parser.ts deleted file mode 100644 index eb28382..0000000 --- a/src/lib/mapping/parsers/abstract-mapping-parser.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { MappingSet } from "java-remapper"; - -export abstract class AbstractMappingsParser { - abstract parse(content: string[]): MappingSet; -} diff --git a/src/lib/mapping/parsers/csrg-tsrg-parser.ts b/src/lib/mapping/parsers/csrg-tsrg-parser.ts deleted file mode 100644 index 866c752..0000000 --- a/src/lib/mapping/parsers/csrg-tsrg-parser.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { ClassMapping, FieldMapping, MappingSet, MethodMapping } from "java-remapper"; -import { AbstractMappingsParser } from "./abstract-mapping-parser"; - -/** - * Parses CSRG format mappings. - */ -export class CSRGTSRGParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - const classes: Map = new Map(); - - // First pass: packages and classes (lines without leading tab) - for (const l of content.filter((l) => !l.startsWith("\t"))) { - const pts = l.split(" "); - if (pts.length !== 2) continue; - if (pts[0]!!.endsWith("/")) { - // package line, ignoring - continue; - } else { - classes.set(pts[0]!!, { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[1]!!, - fields: [], - methods: [], - }); - } - } - - let currentClass: ClassMapping | null = null; - - for (const line of content) { - const pts = line.split(" "); - - if (line.startsWith("\t")) { - if (!currentClass) throw new Error("Invalid TSRG line, missing class: " + line); - // Remove leading tab from first part - pts[0] = pts[0]!!.substring(1); - if (pts.length === 2) { - // field mapping - const field: FieldMapping = { - obfuscatedName: pts[0], - deobfuscatedName: pts[1]!!, - }; - currentClass.fields.push(field); - } else if (pts.length === 3) { - // method mapping - const method: MethodMapping = { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[2]!!, - descriptor: pts[1], - }; - currentClass.methods.push(method); - } else { - throw new Error("Invalid TSRG line, too many parts: " + line); - } - } else { - if (pts.length === 2) { - if (!pts[0]!!.endsWith("/")) { - currentClass = classes.get(pts[0]!!) || null; - } - } else if (pts.length === 3) { - // field on class - const clazz = classes.get(pts[0]!!) ?? { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[0]!!, - fields: [], - methods: [], - }; - if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); - const field: FieldMapping = { - obfuscatedName: pts[1]!!, - deobfuscatedName: pts[2]!!, - }; - clazz.fields.push(field); - } else if (pts.length === 4) { - // method on class - const clazz = classes.get(pts[0]!!) ?? { - obfuscatedName: pts[0]!!, - deobfuscatedName: pts[0]!!, - fields: [], - methods: [], - }; - if (!classes.has(pts[0]!!)) classes.set(pts[0]!!, clazz); - const method: MethodMapping = { - obfuscatedName: pts[2]!!, - deobfuscatedName: pts[3]!!, - descriptor: pts[1], - }; - clazz.methods.push(method); - } else { - throw new Error("Invalid CSRG line, too many parts: " + line); - } - } - } - - return { - classes: Array.from(classes.values()), - }; - } -} diff --git a/src/lib/mapping/parsers/index.ts b/src/lib/mapping/parsers/index.ts deleted file mode 100644 index d98aeaa..0000000 --- a/src/lib/mapping/parsers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { CSRGTSRGParser } from "./csrg-tsrg-parser"; -export { ProguardParser } from "./proguard-parser"; -export { SRGXSRGParser } from "./srg-xsrg-parser"; -export { TinyParser } from "./tiny-parser"; -export { TinyV2Parser } from "./tiny-v2-parser"; -export { TSRG2Parser } from "./tsrg2-parser"; diff --git a/src/lib/mapping/parsers/proguard-parser.ts b/src/lib/mapping/parsers/proguard-parser.ts deleted file mode 100644 index 98d04d8..0000000 --- a/src/lib/mapping/parsers/proguard-parser.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { ClassMapping, FieldMapping, MappingSet, MethodMapping } from "java-remapper"; -import { toDesc } from "../util"; -import { AbstractMappingsParser } from "./abstract-mapping-parser"; - -/** - * ProGuard parser. - * Does NOT support parameter or local variable names. - */ -export class ProguardParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - const classes: Map = new Map(); - - let currentClass: ClassMapping | null = null; - - for (let line of content) { - if (!line.startsWith(" ") && line.endsWith(":")) { - // Class declaration line - // e.g. some/package/ClassName -> new/package/NewClassName: - const parts = line.split(" -> "); - if (parts.length !== 2) throw new Error("Invalid Proguard class line: " + line); - - const deobfName = parts[0]; - const obfName = parts[1]!!.slice(0, -1); // remove trailing ':' - - currentClass = { - obfuscatedName: obfName!!, - deobfuscatedName: deobfName!!, - fields: [], - methods: [], - }; - classes.set(obfName!!, currentClass); - } else if (line.includes("(") && line.includes(")")) { - // Method line - if (!currentClass) throw new Error("Invalid Proguard line, missing class: " + line); - - line = line.trim(); - - let startLine = 0; - let endLine = 0; - - // Check if line has line number ranges like: 10:15 returnType methodName(...) - if (line.indexOf(":") !== -1) { - const firstColon = line.indexOf(":"); - const secondColon = line.indexOf(":", firstColon + 1); - if (secondColon !== -1) { - startLine = parseInt(line.substring(0, firstColon)); - endLine = parseInt(line.substring(firstColon + 1, secondColon)); - line = line.substring(secondColon + 1).trim(); - } - } - - // Split into parts around " -> " - const arrowIndex = line.indexOf(" -> "); - if (arrowIndex === -1) throw new Error("Invalid Proguard method line (missing ->): " + line); - - const obfName = line.substring(arrowIndex + 4).trim(); - - // Extract return type, method name and args - const retTypeAndName = line.substring(0, arrowIndex).trim(); - // retType is the first word before space - const firstSpaceIdx = retTypeAndName.indexOf(" "); - if (firstSpaceIdx === -1) throw new Error("Invalid Proguard method line (no space): " + line); - - const retType = retTypeAndName.substring(0, firstSpaceIdx).trim(); - const rest = retTypeAndName.substring(firstSpaceIdx + 1).trim(); - - // The method name is from after return type up to '(' - const parenStart = rest.indexOf("("); - if (parenStart === -1) throw new Error("Invalid Proguard method line (no '('): " + line); - - const methodName = rest.substring(0, parenStart).trim(); - - // Parse args inside parentheses - const argsStr = rest.substring(parenStart + 1, rest.indexOf(")")); - const args = argsStr.length > 0 ? argsStr.split(",").map((a) => a.trim()) : []; - - // Build method descriptor string - let desc = "("; - for (const arg of args) { - if (arg.length === 0) break; - desc += toDesc(arg); - } - desc += ")" + toDesc(retType); - - const method: MethodMapping = { - obfuscatedName: obfName, - deobfuscatedName: methodName, - descriptor: desc, - }; - - currentClass.methods.push(method); - } else { - // Field line - if (!currentClass) throw new Error("Invalid Proguard line, missing class: " + line); - - // e.g. int oldField -> newField - const parts = line.trim().split(" "); - if (parts.length < 4 || parts[2] !== "->") throw new Error("Invalid Proguard field line: " + line); - - const field: FieldMapping = { - obfuscatedName: parts[3]!!, - deobfuscatedName: parts[1]!!, - descriptor: toDesc(parts[0]!!), - }; - currentClass.fields.push(field); - } - } - - return { - classes: Array.from(classes.values()), - }; - } -} diff --git a/src/lib/mapping/parsers/srg-xsrg-parser.ts b/src/lib/mapping/parsers/srg-xsrg-parser.ts deleted file mode 100644 index 7aaa8a2..0000000 --- a/src/lib/mapping/parsers/srg-xsrg-parser.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ClassMapping, FieldMapping, MappingSet, MethodMapping } from "java-remapper"; -import { putOrGetClass, rsplit } from "../util"; -import { AbstractMappingsParser } from "./abstract-mapping-parser"; - -/** - * SRG/XSRG parser. - * Does NOT support parameters or local variables (not in SRG). - */ -export class SRGXSRGParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - const classes: Map = new Map(); - - for (const line of content) { - const pts = line.split(" "); - switch (pts[0]) { - case "PK:": - // Package mapping ignored - break; - case "CL:": { - const obf = pts[1]; - const deobf = pts[2]; - classes.set(obf!!, { - obfuscatedName: obf!!, - deobfuscatedName: deobf!!, - fields: [], - methods: [], - }); - break; - } - case "FD:": { - if (pts.length === 5) { - // XSRG format: FD: OrigClass/OrigField OrigDesc NewClass/NewField NewDesc - const [leftClass, leftField] = rsplit(pts[1], "/", 1); - const [_, rightField] = rsplit(pts[3], "/", 1); - const clazz = putOrGetClass(leftClass!!, classes); - const field: FieldMapping = { - obfuscatedName: leftField!!, - deobfuscatedName: rightField!!, - descriptor: pts[2], - }; - clazz.fields.push(field); - } else { - // SRG format: FD: OrigClass/OrigField NewClass/NewField - const [leftClass, leftField] = rsplit(pts[1], "/", 1); - const [_, rightField] = rsplit(pts[2], "/", 1); - const clazz = putOrGetClass(leftClass!!, classes); - const field: FieldMapping = { - obfuscatedName: leftField!!, - deobfuscatedName: rightField!!, - }; - clazz.fields.push(field); - } - break; - } - case "MD:": { - // MD: OrigClass/OrigMethod OrigDesc NewClass/NewMethod NewDesc - const [leftClass, leftMethod] = rsplit(pts[1], "/", 1); - const [_, rightMethod] = rsplit(pts[3], "/", 1); - const clazz = putOrGetClass(leftClass!!, classes); - const method: MethodMapping = { - obfuscatedName: leftMethod!!, - deobfuscatedName: rightMethod!!, - descriptor: pts[2], - }; - clazz.methods.push(method); - break; - } - default: - throw new Error("Invalid SRG file, Unknown type: " + line); - } - } - - return { - classes: Array.from(classes.values()), - }; - } -} diff --git a/src/lib/mapping/parsers/tiny-parser.ts b/src/lib/mapping/parsers/tiny-parser.ts deleted file mode 100644 index b4560d7..0000000 --- a/src/lib/mapping/parsers/tiny-parser.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ClassMapping, MappingSet } from "java-remapper"; -import { putOrGetClass } from "../util"; -import { AbstractMappingsParser } from "./abstract-mapping-parser"; - -/** - * Tiny v1 parser. - * Limited support, no parameters or locals. - * Only basic classes, fields, methods. - */ -export class TinyParser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - if (content.length === 0) { - throw new Error("Empty Tiny V1 lines"); - } - - const header = content[0]!!.split("\t"); - if (header.length < 3) { - throw new Error("Invalid Tiny v1 Header: " + content[0]); - } - - const nameCount = header.length - 1; - - // Map of obfuscated class name -> ClassMapping - const classes = new Map(); - - const classMappings: ClassMapping[] = []; - - function duplicate(value: string, count: number): string[] { - return Array(count).fill(value); - } - - for (let i = 1; i < content.length; i++) { - const line = content[i]!!.split("\t"); - if (line[0]!!.startsWith("#")) { - // Comment line, skip - continue; - } - - switch (line[0]) { - case "CLASS": - if (line.length !== nameCount + 1) { - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); - } - putOrGetClass(line.slice(1), classes, classMappings); - break; - - case "FIELD": - if (line.length !== nameCount + 3) { - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); - } - { - const cls = - classes.get(line[1]!!) ?? - putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); - cls.fields.push({ - obfuscatedName: line[3]!!, - deobfuscatedName: line[4] ?? line[3]!!, - descriptor: line[2], - }); - } - break; - - case "METHOD": - if (line.length !== nameCount + 3) { - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]!!}`); - } - { - const cls = - classes.get(line[1]!!) ?? - putOrGetClass(duplicate(line[1]!!, nameCount), classes, classMappings); - cls.methods.push({ - descriptor: line[2], - obfuscatedName: line[3]!!, - deobfuscatedName: line[4] ?? line[3]!!, - }); - } - break; - - default: - throw new Error(`Invalid Tiny v1 line #${i}: ${content[i]}`); - } - } - - return { - classes: classMappings, - }; - } -} diff --git a/src/lib/mapping/parsers/tiny-v2-parser.ts b/src/lib/mapping/parsers/tiny-v2-parser.ts deleted file mode 100644 index f21be17..0000000 --- a/src/lib/mapping/parsers/tiny-v2-parser.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { ClassMapping, FieldMapping, MappingSet, MethodMapping, ParameterMapping } from "java-remapper"; -import { putOrGetClass, tiny2Exception, unescapeTinyString } from "../util"; -import { AbstractMappingsParser } from "./abstract-mapping-parser"; - -export enum TinyV2State { - ROOT, - CLASS, - FIELD, - METHOD, - PARAMETER, -} - -/** - * Tiny v2 parser with parameters & local variables support. - */ -export class TinyV2Parser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - if (content.length === 0) { - throw new Error("Empty Tiny V2 lines"); - } - - const header = content[0]!!.split("\t"); - if (header.length < 5) { - throw new Error("Invalid Tiny v2 Header: " + content[0]); - } - - const major = Number(header[1]); - const minor = Number(header[2]); - if (major !== 2 || minor !== 0) { - throw new Error("Unsupported Tiny v2 version: " + content[0]); - } - - const nameCount = header.length - 3; - - const classes = new Map(); - const classMappings: ClassMapping[] = []; - - const properties = new Map(); - let escaped = false; - let start = 1; - - // Parse properties at the beginning (empty first column lines) - for (; start < content.length; start++) { - const line = content[start]!!.split("\t"); - if (line[0] !== "") break; - - const key = line[1]!!; - const value = line.length < 3 ? null : escaped ? unescapeTinyString(line[2]!!) : line[2]; - properties.set(key, value!!); - - if (key === "escaped-names") escaped = true; - } - - // State stacks and current elements - const stack: TinyV2State[] = []; - let cls: ClassMapping | null = null; - let field: FieldMapping | null = null; - let method: MethodMapping | null = null; - let param: ParameterMapping | null = null; - - for (let x = start; x < content.length; x++) { - let line = content[x]!!; - - // Count leading tabs to determine depth - let newdepth = 0; - while (line.charAt(newdepth) === "\t") newdepth++; - if (newdepth !== 0) line = line.substring(newdepth); - - // Pop stack until depth matches - while (stack.length !== newdepth) { - const popped = stack.pop(); - switch (popped) { - case TinyV2State.CLASS: - cls = null; - break; - case TinyV2State.FIELD: - field = null; - break; - case TinyV2State.METHOD: - method = null; - break; - case TinyV2State.PARAMETER: - param = null; - break; - } - } - - const parts = line.split("\t"); - if (escaped) { - for (let y = 1; y < parts.length; y++) { - parts[y] = unescapeTinyString(parts[y]!!); - } - } - - switch (parts[0]) { - case "c": // Class - if (stack.length === 0) { - if (parts.length !== nameCount + 1) throw tiny2Exception(x, line); - cls = putOrGetClass(parts.slice(1), classes, classMappings); - stack.push(TinyV2State.CLASS); - } - break; - - case "f": // Field - if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) - throw tiny2Exception(x, line); - if (!cls) throw tiny2Exception(x, line); - field = { - obfuscatedName: parts[2]!!, - deobfuscatedName: parts[3]!!, - descriptor: parts[1], - }; - cls.fields.push(field); - stack.push(TinyV2State.FIELD); - break; - - case "m": // Method - if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.CLASS) - throw tiny2Exception(x, line); - if (!cls) throw tiny2Exception(x, line); - method = { - obfuscatedName: parts[2]!!, - deobfuscatedName: parts[3]!!, - descriptor: parts[1], - parameters: [], - }; - cls.methods.push(method); - stack.push(TinyV2State.METHOD); - break; - - case "p": // Parameter - if (parts.length !== nameCount + 2 || stack[stack.length - 1] !== TinyV2State.METHOD) - throw tiny2Exception(x, line); - if (!method) throw tiny2Exception(x, line); - const index = Number(parts[1]); - const paramNames = parts.slice(2); - param = { - index, - obfuscatedName: paramNames[0] ?? "", - deobfuscatedName: paramNames[1] ?? paramNames[0] ?? "", - }; - method.parameters?.push(param); - stack.push(TinyV2State.PARAMETER); - break; - - case "v": // Local variable (ignored) - break; - - default: - throw tiny2Exception(x, line); - } - } - - return { classes: classMappings }; - } -} diff --git a/src/lib/mapping/parsers/tsrg2-parser.ts b/src/lib/mapping/parsers/tsrg2-parser.ts deleted file mode 100644 index 4d9a0c8..0000000 --- a/src/lib/mapping/parsers/tsrg2-parser.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { ClassMapping, MappingSet, MethodMapping } from "java-remapper"; -import { putOrGetClass, swapFirst } from "../util"; -import { AbstractMappingsParser } from "./abstract-mapping-parser"; - -/** - * Parses TSRG2 format mappings. - */ -export class TSRG2Parser extends AbstractMappingsParser { - parse(content: string[]): MappingSet { - if (content.length === 0) { - throw new Error("Empty TSRG2 lines"); - } - - const header = content[0]!!.split(" "); - if (header.length < 3) { - throw new Error("Invalid TSRG v2 Header: " + content[0]); - } - const nameCount = header.length - 1; // number of names per line after first - - // The first line lists the "name keys" like ["left", "right", "intermediary"] - // We won't use those names explicitly here, but we parse lines accordingly - content = content.slice(1); - - // Internal structures to hold mappings - const classes: Map = new Map(); - - let currentClass: ClassMapping | null = null; - let currentMethod: MethodMapping | null = null; - - for (const line of content) { - if (line.length < 2) { - throw new Error("Invalid TSRG v2 line, too short: " + line); - } - - const pts = line.split(" "); - - if (line.charAt(0) !== "\t") { - // Classes or Packages are not tabbed - if (pts.length !== nameCount) { - throw new Error("Invalid TSRG v2 line: " + line); - } - - // Class line: add class mapping - currentClass = putOrGetClass(pts, classes); - currentMethod = null; - } else if (line.charAt(1) === "\t") { - // Parameter or static marker line, two tabs at start - if (currentMethod === null) { - throw new Error("Invalid TSRG v2 line, missing method: " + line); - } - // Strip two tabs from start - pts[0] = pts[0]!!.substring(2); - - if (pts.length === nameCount + 1) { - // Parameter line: first is index, rest are names - const paramIndex = parseInt(pts[0], 10); - if (isNaN(paramIndex)) { - throw new Error("Invalid TSRG v2 parameter index: " + pts[0]); - } - const paramNames = pts.slice(1); - if (!currentMethod.parameters) currentMethod.parameters = []; - currentMethod.parameters.push({ - index: paramIndex, - obfuscatedName: paramNames[0]!!, - deobfuscatedName: (paramNames[1] ?? paramNames[0])!!, - }); - } else { - throw new Error("Invalid TSRG v2 line, too many parts: " + line); - } - } else { - // One tab at start: field or method line - if (currentClass === null) { - throw new Error("Invalid TSRG v2 line, missing class: " + line); - } - // Strip one tab - pts[0] = pts[0]!!.substring(1); - - if (pts.length === nameCount) { - // Field without descriptor - currentClass.fields.push({ - obfuscatedName: pts[0], - deobfuscatedName: pts[1] ?? pts[0], - }); - currentMethod = null; - } else if (pts.length === 1 + nameCount) { - // Field with descriptor or method - swapFirst(pts); - if (pts[0].startsWith("(")) { - // Method line: pts[0] = descriptor, rest = names - currentMethod = { - descriptor: pts[0], - obfuscatedName: pts[1]!!, - deobfuscatedName: pts[2] ?? pts[1]!!, - }; - currentClass.methods.push(currentMethod); - } else { - // Field with descriptor - currentMethod = null; - currentClass.fields.push({ - obfuscatedName: pts[1]!!, - deobfuscatedName: pts[2] ?? pts[1]!!, - descriptor: pts[0], - }); - } - } else { - throw new Error("Invalid TSRG v2 line, too many parts: " + line); - } - } - } - - return { - classes: Array.from(classes.values()), - }; - } -} diff --git a/src/lib/mapping/util.ts b/src/lib/mapping/util.ts deleted file mode 100644 index a57236a..0000000 --- a/src/lib/mapping/util.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { ClassMapping } from "java-remapper"; - -export const stripComment = (line: string): string => { - const idx = line.indexOf("#"); - if (idx === 0) return ""; - if (idx !== -1) line = line.substring(0, idx - 1); - let end = line.length; - while (end > 1 && line.charAt(end - 1) === " ") end--; - return end === 0 ? "" : line.substring(0, end); -}; - -export const rsplit = (str: string | undefined, chr: string, count: number): string[] => { - const parts: string[] = []; - let remainder = str ?? ""; - while (count > 0) { - const idx = remainder.lastIndexOf(chr); - if (idx === -1) break; - parts.push(remainder.substring(idx + 1)); - remainder = remainder.substring(0, idx); - count--; - } - parts.push(remainder); - parts.reverse(); - return parts; -}; - -export const toDesc = (type: string): string => { - if (type.endsWith("[]")) { - return "[" + toDesc(type.substring(0, type.length - 2)); - } - switch (type) { - case "int": - return "I"; - case "void": - return "V"; - case "boolean": - return "Z"; - case "byte": - return "B"; - case "char": - return "C"; - case "short": - return "S"; - case "double": - return "D"; - case "float": - return "F"; - case "long": - return "J"; - default: - // Object type: use 'L' + internal name + ';' - // If it contains dots, convert them to slashes - const internalName = type.replace(/\./g, "/"); - return `L${internalName};`; - } -}; - -export const swapFirst = (arr: string[]) => { - if (arr.length >= 2) { - const tmp = arr[0]!!; - arr[0] = arr[1]!!; - arr[1] = tmp; - } -}; - -export const unescapeTinyString = (value: string): string => { - return value - .replace(/\\\\/g, "\\") - .replace(/\\n/g, "\n") - .replace(/\\r/g, "\r") - .replace(/\\t/g, "\t") - .replace(/\\0/g, "\0"); -}; - -export const tiny2Exception = (line: number, data: string): Error => { - return new Error(`Invalid Tiny v2 line: #${line}: ${data}`); -}; - -export const putOrGetClass = ( - names: string | string[], - classes: Map, - classMappings?: ClassMapping[] -): ClassMapping => { - const [obfName, deobfName] = typeof names === "string" ? [names, names] : [names[0]!, names[1] ?? names[0]!]; - - if (!classes.has(obfName)) { - const cls: ClassMapping = { - obfuscatedName: obfName, - deobfuscatedName: deobfName, - fields: [], - methods: [], - }; - classes.set(obfName, cls); - classMappings?.push(cls); - } - - return classes.get(obfName)!; -}; From 6b0c17b0a00300ea1d0f90c1bd7ac28ee5a3f691 Mon Sep 17 00:00:00 2001 From: Dani-error Date: Mon, 28 Jul 2025 16:47:45 +0100 Subject: [PATCH 6/6] bump: again java remapper --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5df22e..dbee4aa 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "comlink": "^4.4.2", "elkjs": "^0.10.0", "html-to-image": "1.11.11", - "java-remapper": "^1.0.4", + "java-remapper": "^1.0.6", "queueable": "^5.3.2", "svelte-modals": "^2.0.1", "tailwind-merge": "^3.3.1",