mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
feat: support auto decompiling with cfr
This commit is contained in:
@@ -10,6 +10,12 @@ import materialDark from "react-syntax-highlighter/dist/esm/styles/prism/materia
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button, type buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
SyntaxHighlighter.registerLanguage("java", java);
|
||||
@@ -18,8 +24,16 @@ interface CopyButtonProps extends React.ComponentProps<"button"> {
|
||||
src?: string;
|
||||
}
|
||||
|
||||
export interface CopyOption {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function CopyButton({
|
||||
value,
|
||||
className,
|
||||
...buttonProps
|
||||
}: Readonly<CopyButtonProps & VariantProps<typeof buttonVariants>>) {
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
const { t } = useTranslation(["common"]);
|
||||
@@ -43,11 +57,12 @@ export function CopyButton({
|
||||
return (
|
||||
<CopyToClipboard.CopyToClipboard text={value} onCopy={handleCopy}>
|
||||
<Button
|
||||
{...buttonProps}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
||||
disabled={hasCopied}
|
||||
className={cn("h-7 w-7 [&_svg]:h-4 [&_svg]:w-4", className)}
|
||||
disabled={hasCopied || buttonProps.disabled}
|
||||
>
|
||||
{hasCopied ? <Check /> : <Copy />}
|
||||
</Button>
|
||||
@@ -55,6 +70,62 @@ export function CopyButton({
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyMenuButton({
|
||||
options,
|
||||
className,
|
||||
...buttonProps
|
||||
}: Readonly<
|
||||
{ options: CopyOption[] } & React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>
|
||||
>) {
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [hasCopied]);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
if (!hasCopied) {
|
||||
setHasCopied(true);
|
||||
toast.success(t("copySuccess"), { duration: 1000 });
|
||||
}
|
||||
}, [hasCopied, t]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
{...buttonProps}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
className={cn("h-7 w-7 [&_svg]:h-4 [&_svg]:w-4", className)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{hasCopied ? <Check /> : <Copy />}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{options.map((option) => (
|
||||
<CopyToClipboard.CopyToClipboard
|
||||
key={option.label}
|
||||
text={option.value}
|
||||
onCopy={handleCopy}
|
||||
>
|
||||
<DropdownMenuItem disabled={option.disabled}>{option.label}</DropdownMenuItem>
|
||||
</CopyToClipboard.CopyToClipboard>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CodeViewer({
|
||||
code,
|
||||
header,
|
||||
@@ -63,6 +134,9 @@ export default function CodeViewer({
|
||||
height,
|
||||
showLineNumbers = true,
|
||||
wrapLongLines = true,
|
||||
copyLabel,
|
||||
copyDisabled = false,
|
||||
copyOptions,
|
||||
}: Readonly<CodeViewerProps>) {
|
||||
const lineProps: lineTagPropsFunction | HTMLProps<HTMLElement> | undefined = wrapLongLines
|
||||
? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } }
|
||||
@@ -75,7 +149,24 @@ export default function CodeViewer({
|
||||
{header}
|
||||
<div className="flex items-center gap-2">
|
||||
{button}
|
||||
<CopyButton value={code} variant="ghost" size="sm" />
|
||||
{copyOptions ? (
|
||||
<CopyMenuButton
|
||||
options={copyOptions}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={copyLabel}
|
||||
title={copyLabel}
|
||||
/>
|
||||
) : (
|
||||
<CopyButton
|
||||
value={code}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={copyLabel}
|
||||
title={copyLabel}
|
||||
disabled={copyDisabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="wrap-all relative overflow-hidden text-xs">
|
||||
@@ -110,4 +201,7 @@ interface CodeViewerProps {
|
||||
showLineNumbers?: boolean;
|
||||
wrapLongLines?: boolean;
|
||||
lineProps?: (lineNumber: number) => React.HTMLProps<HTMLElement>;
|
||||
copyLabel?: string;
|
||||
copyDisabled?: boolean;
|
||||
copyOptions?: CopyOption[];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = MenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = MenuPrimitive.Trigger;
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props &
|
||||
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"relative isolate z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({ className, ...props }: MenuPrimitive.Item.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger };
|
||||
Reference in New Issue
Block a user