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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"comlink": "^4.4.2",
"elkjs": "^0.10.0",
"html-to-image": "1.11.11",
"java-remapper": "^1.0.6",
"queueable": "^5.3.2",
"svelte-modals": "^2.0.1",
"tailwind-merge": "^3.3.1",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/components/dialog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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";
Expand All @@ -12,6 +13,7 @@ export {
ClearDialog,
DeleteDialog,
LoadExternalDialog,
LoadMappings,
ScriptDeleteDialog,
ScriptDialog,
ScriptLoadDialog,
Expand Down
91 changes: 91 additions & 0 deletions src/lib/components/dialog/load_mappings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<script lang="ts">
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "$lib/components/ui/dialog";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import type { ModalProps } from "svelte-modals";
import type { EventHandler } from "$lib/event";
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
import { MappingFormat } from "java-remapper";
import { cn } from "../utils";
import Badge from "../ui/badge/badge.svelte";

interface Props extends ModalProps {
detectedFormat: MappingFormat | null;
content: string;
handler: EventHandler;
}

const displayNames: Record<keyof typeof MappingFormat, string> = {
SRG_XSRG: "SRG/XSRG",
CSRG_TSRG: "CSRG/TSRG",
TSRG2: "TSRG v2",
PG: "ProGuard",
TINY1: "Tiny v1",
TINY2: "Tiny v2",
};
const formats = Object.keys(MappingFormat) as (keyof typeof MappingFormat)[];

let { isOpen, close, detectedFormat, content, handler }: Props = $props();

let value = $state(detectedFormat?.toString() ?? "");
let invalid = $state(false);
const load = async () => {
if (!value || !(value.trim().length > 0)) {
invalid = true;
return;
}

isOpen = false;

await handler.applyMappings(content, value as MappingFormat);
};

const triggerContent = $derived(
value && value.trim().length > 0
? displayNames[value as keyof typeof MappingFormat]
: "Select a mapping format..."
);
</script>

<Dialog bind:open={isOpen} onOpenChangeComplete={(open) => open || close()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Load Mappings</DialogTitle>
<DialogDescription>Please select from the dropdown the mappings format.</DialogDescription>
</DialogHeader>
<div class="grid grid-cols-6 items-center gap-4">
<Label for="format" class="text-right">Format</Label>

<Select type="single" bind:value onValueChange={() => (invalid = false)}>
<SelectTrigger
id="format"
class={cn("col-span-5 w-full", !invalid || "border-destructive ring-offset-destructive")}
>
{triggerContent}
</SelectTrigger>
<SelectContent>
{#each formats as format}
<SelectItem defaultSelected value={format}>
<div class="flex flex-row items-center justify-center gap-2">
{displayNames[format]}
{#if detectedFormat === format}
<Badge variant="outline">Detected</Badge>
{/if}
</div>
</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<DialogFooter>
<Button type="submit" onclick={load}>Import</Button>
</DialogFooter>
</DialogContent>
</Dialog>
3 changes: 3 additions & 0 deletions src/lib/components/menu/menu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@
<MenubarItem onclick={() => modals.open(LoadExternalDialog, { handler })} class="justify-between">
Add from URL
</MenubarItem>
<MenubarItem onclick={() => handler.loadMappings(entries)} disabled={entries.length === 0}>
Open Mappings
</MenubarItem>
<MenubarItem disabled={entries.length === 0} onclick={() => modals.open(ClearDialog, { handler })}>
Clear all
</MenubarItem>
Expand Down
50 changes: 50 additions & 0 deletions src/lib/components/ui/badge/badge.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";

export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});

export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>

<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/components/utils.js";

let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>

<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>
2 changes: 2 additions & 0 deletions src/lib/components/ui/badge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
110 changes: 108 additions & 2 deletions src/lib/event/handler.ts
Original file line number Diff line number Diff line change
@@ -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 "java-remapper";
import {
load as loadScript,
type ProtoScript,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -129,6 +146,95 @@ export default {
});
}
},
async loadMappings(entries: Entry[]): Promise<void> {
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<void> {
try {
const parsed = parseMappings(format, content);

await recordProgress("applying mappings", null, async (task) => {
let completed = 0;
const oldToNewMap = new Map<string, Entry>(); // Track old → new mapping

for (const clazz of parsed.classes) {
const data = await mapClass(clazz, parsed);

if (data.mapped) {
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`);

await analyzeBackground();

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<void> {
if (!files) {
files = await readFiles("", true);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/event/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Disassembler } from "$lib/disasm";
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";
Expand All @@ -9,6 +10,8 @@ type Awaitable<T> = T | PromiseLike<T>;

export interface EventHandler {
load(): Awaitable<void>;
loadMappings(entries: Entry[]): Awaitable<void>;
applyMappings(content: string, format: MappingFormat): Awaitable<void>;
add(files?: File[]): Awaitable<void>;
addRemote(url: string): Awaitable<void>;
clear(): Awaitable<void>;
Expand Down
16 changes: 16 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@ export const readFiles = (pattern: string, multiple: boolean): Promise<File[]> =
});
};

export const fileToString = (file: File): Promise<string> => {
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";
Expand Down
Loading