feat: support auto decompiling with cfr

This commit is contained in:
ReaJason
2026-08-30 12:30:58 +08:00
parent f239dfb6df
commit 3c91ba50e9
19 changed files with 692 additions and 29 deletions
@@ -0,0 +1,123 @@
import type { TokenLine } from "@/lib/decompile/decompile.worker";
import { memo, type ReactNode } from "react";
import { CopyMenuButton, type CopyOption } from "@/components/code-viewer";
/** material-dark palette (react-syntax-highlighter prism theme) */
const TOKEN_COLORS: Record<string, string> = {
atrule: "#c792ea",
boolean: "#c792ea",
builtin: "#ffcb6b",
char: "#80cbc4",
"class-name": "#f2ff00",
comment: "#616161",
constant: "#c792ea",
function: "#c792ea",
keyword: "#c792ea",
number: "#fd9170",
operator: "#89ddff",
property: "#80cbc4",
punctuation: "#89ddff",
regex: "#f2ff00",
string: "#a5e844",
variable: "#ff6666",
};
const BACKGROUND = "#2f2f2f";
const PLAIN = "#eee";
const GUTTER_COLOR = "#616161";
const FONT_FAMILY = "Roboto Mono, monospace";
interface DecompiledCodeViewerProps {
lines: TokenLine[] | null;
placeholder: string;
header?: ReactNode;
button?: ReactNode;
height: number;
copyLabel: string;
copyOptions: CopyOption[];
}
export default memo(function DecompiledCodeViewer({
lines,
placeholder,
header,
button,
height,
copyLabel,
copyOptions,
}: Readonly<DecompiledCodeViewerProps>) {
return (
<div className="rounded-lg border">
<div
className={
header
? "flex items-center justify-between border-b p-2"
: "flex items-center justify-end border-b p-2"
}
>
{header}
<div className="flex items-center gap-2">
{button}
<CopyMenuButton
options={copyOptions}
variant="ghost"
size="sm"
aria-label={copyLabel}
title={copyLabel}
/>
</div>
</div>
<div
className="overflow-auto text-xs"
style={{
height,
background: BACKGROUND,
color: PLAIN,
borderRadius: "0 0 var(--radius) var(--radius)",
}}
>
{lines ? (
<div
className="flex"
style={{ fontFamily: FONT_FAMILY, lineHeight: "1.5em", tabSize: 4 }}
>
<div
aria-hidden
className="sticky left-0 shrink-0 pr-4 pl-4 text-right select-none"
style={{ background: BACKGROUND, color: GUTTER_COLOR }}
>
{lines.map((_, index) => (
<div key={index}>{index + 1}</div>
))}
</div>
<pre className="m-0 flex-1 pr-4" style={{ fontFamily: "inherit", tabSize: 4 }}>
{lines.map((line, index) => (
<div key={index} style={{ whiteSpace: "pre" }}>
{line.map(([type, text], segmentIndex) =>
type === "plain" ? (
text
) : (
<span key={segmentIndex} style={{ color: TOKEN_COLORS[type] ?? PLAIN }}>
{text}
</span>
),
)}
{line.length === 0 ? " " : null}
</div>
))}
</pre>
</div>
) : (
<pre
className="m-0 p-4"
style={{ fontFamily: FONT_FAMILY, lineHeight: "1.5em", whiteSpace: "pre-wrap" }}
>
{placeholder}
</pre>
)}
</div>
</div>
);
});
+60 -22
View File
@@ -4,12 +4,13 @@ import { DownloadIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import DecompiledCodeViewer from "@/components/memshell/decompiled-code-viewer";
import { QuickUsage } from "@/components/memshell/quick-usage";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useDecompiledSources } from "@/lib/decompile/use-decompiled-sources";
import { downloadBytes } from "@/lib/utils";
import CodeViewer from "../code-viewer";
import { BasicInfo } from "./results/basic-info";
import { ResultComponent } from "./results/result-component";
@@ -23,10 +24,24 @@ export default function ShellResult({
generateResult?: MemShellResult;
}>) {
const { t } = useTranslation(["common", "memshell"]);
const { sources, isDecompiling, error } = useDecompiledSources(generateResult);
if (!generateResult) {
return <QuickUsage />;
}
const shellClassName = generateResult.shellClassName;
const shellBytesBase64 = generateResult.shellBytesBase64Str;
const injectorClassName = generateResult.injectorClassName;
const injectorBytesBase64 = generateResult.injectorBytesBase64Str;
const height = 800;
const sourcePlaceholder = isDecompiling
? `// ${t("common:decompiling")}`
: error
? `// ${t("common:decompileFailed", { error })}`
: "";
return (
<Tabs defaultValue="packResult">
<TabsList className="grid w-full grid-cols-3">
@@ -42,61 +57,84 @@ export default function ShellResult({
generateResult={generateResult}
/>
</TabsContent>
<TabsContent value="shell" className="mt-4">
<CodeViewer
showLineNumbers={false}
header={<div className="truncate text-xs">{generateResult?.shellClassName}</div>}
<TabsContent value="shell" className="mt-4" keepMounted>
<DecompiledCodeViewer
copyLabel={t("common:copy")}
copyOptions={[
{
label: t("memshell:copySource"),
value: sources.shell?.source ?? "",
disabled: !sources.shell,
},
{
label: t("memshell:copyBase64"),
value: shellBytesBase64 ?? "",
disabled: !shellBytesBase64,
},
]}
header={<div className="truncate text-xs">{shellClassName}</div>}
button={
<Button
variant="ghost"
size="icon"
type="button"
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
aria-label={t("common:download")}
title={t("common:download")}
onClick={() => {
if (!generateResult?.shellBytesBase64Str) {
if (!shellBytesBase64) {
toast.warning(t("memshell:tips.shellBytesEmpty"));
return;
}
downloadBytes(generateResult?.shellBytesBase64Str, generateResult?.shellClassName);
downloadBytes(shellBytesBase64, shellClassName);
}}
>
<DownloadIcon className="h-4 w-4" />
</Button>
}
wrapLongLines={true}
height={height}
code={generateResult?.shellBytesBase64Str ?? ""}
language="text"
lines={sources.shell?.lines ?? null}
placeholder={sourcePlaceholder}
/>
</TabsContent>
<TabsContent value="injector" className="mt-4">
<CodeViewer
showLineNumbers={false}
wrapLongLines={true}
header={<div className="text-xs">{generateResult?.injectorClassName}</div>}
<TabsContent value="injector" className="mt-4" keepMounted>
<DecompiledCodeViewer
copyLabel={t("common:copy")}
copyOptions={[
{
label: t("memshell:copySource"),
value: sources.injector?.source ?? "",
disabled: !sources.injector,
},
{
label: t("memshell:copyBase64"),
value: injectorBytesBase64 ?? "",
disabled: !injectorBytesBase64,
},
]}
header={<div className="truncate text-xs">{injectorClassName}</div>}
button={
<Button
variant="ghost"
size="icon"
type="button"
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
aria-label={t("common:download")}
title={t("common:download")}
onClick={() => {
if (!generateResult?.injectorBytesBase64Str) {
if (!injectorBytesBase64) {
toast.warning(t("memshell:tips.shellBytesEmpty"));
return;
}
downloadBytes(
generateResult?.injectorBytesBase64Str,
generateResult?.injectorClassName,
);
downloadBytes(injectorBytesBase64, injectorClassName);
}}
>
<DownloadIcon className="h-4 w-4" />
</Button>
}
height={height}
code={generateResult?.injectorBytesBase64Str ?? ""}
language="text"
lines={sources.injector?.lines ?? null}
placeholder={sourcePlaceholder}
/>
</TabsContent>
</Tabs>