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:
@@ -17,6 +17,7 @@ ENV VITE_APP_API_URL=${CONTEXT_PATH} \
|
|||||||
VITE_APP_BASE_PATH=${ROUTE_ROOT_PATH}/ui
|
VITE_APP_BASE_PATH=${ROUTE_ROOT_PATH}/ui
|
||||||
|
|
||||||
COPY --from=source /usr/src/web/package.json /usr/src/web/bun.lock /usr/src/web/source.config.ts /usr/src/web/
|
COPY --from=source /usr/src/web/package.json /usr/src/web/bun.lock /usr/src/web/source.config.ts /usr/src/web/
|
||||||
|
COPY --from=source /usr/src/web/vendor/cfr /usr/src/web/vendor/cfr
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import materialDark from "react-syntax-highlighter/dist/esm/styles/prism/materia
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Button, type buttonVariants } from "@/components/ui/button";
|
import { Button, type buttonVariants } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
SyntaxHighlighter.registerLanguage("java", java);
|
SyntaxHighlighter.registerLanguage("java", java);
|
||||||
@@ -18,8 +24,16 @@ interface CopyButtonProps extends React.ComponentProps<"button"> {
|
|||||||
src?: string;
|
src?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CopyOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function CopyButton({
|
export function CopyButton({
|
||||||
value,
|
value,
|
||||||
|
className,
|
||||||
|
...buttonProps
|
||||||
}: Readonly<CopyButtonProps & VariantProps<typeof buttonVariants>>) {
|
}: Readonly<CopyButtonProps & VariantProps<typeof buttonVariants>>) {
|
||||||
const [hasCopied, setHasCopied] = useState(false);
|
const [hasCopied, setHasCopied] = useState(false);
|
||||||
const { t } = useTranslation(["common"]);
|
const { t } = useTranslation(["common"]);
|
||||||
@@ -43,11 +57,12 @@ export function CopyButton({
|
|||||||
return (
|
return (
|
||||||
<CopyToClipboard.CopyToClipboard text={value} onCopy={handleCopy}>
|
<CopyToClipboard.CopyToClipboard text={value} onCopy={handleCopy}>
|
||||||
<Button
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
className={cn("h-7 w-7 [&_svg]:h-4 [&_svg]:w-4", className)}
|
||||||
disabled={hasCopied}
|
disabled={hasCopied || buttonProps.disabled}
|
||||||
>
|
>
|
||||||
{hasCopied ? <Check /> : <Copy />}
|
{hasCopied ? <Check /> : <Copy />}
|
||||||
</Button>
|
</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({
|
export default function CodeViewer({
|
||||||
code,
|
code,
|
||||||
header,
|
header,
|
||||||
@@ -63,6 +134,9 @@ export default function CodeViewer({
|
|||||||
height,
|
height,
|
||||||
showLineNumbers = true,
|
showLineNumbers = true,
|
||||||
wrapLongLines = true,
|
wrapLongLines = true,
|
||||||
|
copyLabel,
|
||||||
|
copyDisabled = false,
|
||||||
|
copyOptions,
|
||||||
}: Readonly<CodeViewerProps>) {
|
}: Readonly<CodeViewerProps>) {
|
||||||
const lineProps: lineTagPropsFunction | HTMLProps<HTMLElement> | undefined = wrapLongLines
|
const lineProps: lineTagPropsFunction | HTMLProps<HTMLElement> | undefined = wrapLongLines
|
||||||
? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } }
|
? { style: { overflowWrap: "break-word", whiteSpace: "pre-wrap" } }
|
||||||
@@ -75,7 +149,24 @@ export default function CodeViewer({
|
|||||||
{header}
|
{header}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{button}
|
{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>
|
</div>
|
||||||
<div className="wrap-all relative overflow-hidden text-xs">
|
<div className="wrap-all relative overflow-hidden text-xs">
|
||||||
@@ -110,4 +201,7 @@ interface CodeViewerProps {
|
|||||||
showLineNumbers?: boolean;
|
showLineNumbers?: boolean;
|
||||||
wrapLongLines?: boolean;
|
wrapLongLines?: boolean;
|
||||||
lineProps?: (lineNumber: number) => React.HTMLProps<HTMLElement>;
|
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 { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import DecompiledCodeViewer from "@/components/memshell/decompiled-code-viewer";
|
||||||
import { QuickUsage } from "@/components/memshell/quick-usage";
|
import { QuickUsage } from "@/components/memshell/quick-usage";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { useDecompiledSources } from "@/lib/decompile/use-decompiled-sources";
|
||||||
import { downloadBytes } from "@/lib/utils";
|
import { downloadBytes } from "@/lib/utils";
|
||||||
|
|
||||||
import CodeViewer from "../code-viewer";
|
|
||||||
import { BasicInfo } from "./results/basic-info";
|
import { BasicInfo } from "./results/basic-info";
|
||||||
import { ResultComponent } from "./results/result-component";
|
import { ResultComponent } from "./results/result-component";
|
||||||
|
|
||||||
@@ -23,10 +24,24 @@ export default function ShellResult({
|
|||||||
generateResult?: MemShellResult;
|
generateResult?: MemShellResult;
|
||||||
}>) {
|
}>) {
|
||||||
const { t } = useTranslation(["common", "memshell"]);
|
const { t } = useTranslation(["common", "memshell"]);
|
||||||
|
const { sources, isDecompiling, error } = useDecompiledSources(generateResult);
|
||||||
|
|
||||||
if (!generateResult) {
|
if (!generateResult) {
|
||||||
return <QuickUsage />;
|
return <QuickUsage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shellClassName = generateResult.shellClassName;
|
||||||
|
const shellBytesBase64 = generateResult.shellBytesBase64Str;
|
||||||
|
const injectorClassName = generateResult.injectorClassName;
|
||||||
|
const injectorBytesBase64 = generateResult.injectorBytesBase64Str;
|
||||||
|
|
||||||
const height = 800;
|
const height = 800;
|
||||||
|
const sourcePlaceholder = isDecompiling
|
||||||
|
? `// ${t("common:decompiling")}`
|
||||||
|
: error
|
||||||
|
? `// ${t("common:decompileFailed", { error })}`
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="packResult">
|
<Tabs defaultValue="packResult">
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
@@ -42,61 +57,84 @@ export default function ShellResult({
|
|||||||
generateResult={generateResult}
|
generateResult={generateResult}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="shell" className="mt-4">
|
<TabsContent value="shell" className="mt-4" keepMounted>
|
||||||
<CodeViewer
|
<DecompiledCodeViewer
|
||||||
showLineNumbers={false}
|
copyLabel={t("common:copy")}
|
||||||
header={<div className="truncate text-xs">{generateResult?.shellClassName}</div>}
|
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={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
||||||
|
aria-label={t("common:download")}
|
||||||
|
title={t("common:download")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!generateResult?.shellBytesBase64Str) {
|
if (!shellBytesBase64) {
|
||||||
toast.warning(t("memshell:tips.shellBytesEmpty"));
|
toast.warning(t("memshell:tips.shellBytesEmpty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadBytes(generateResult?.shellBytesBase64Str, generateResult?.shellClassName);
|
downloadBytes(shellBytesBase64, shellClassName);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon className="h-4 w-4" />
|
<DownloadIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
wrapLongLines={true}
|
|
||||||
height={height}
|
height={height}
|
||||||
code={generateResult?.shellBytesBase64Str ?? ""}
|
lines={sources.shell?.lines ?? null}
|
||||||
language="text"
|
placeholder={sourcePlaceholder}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="injector" className="mt-4">
|
<TabsContent value="injector" className="mt-4" keepMounted>
|
||||||
<CodeViewer
|
<DecompiledCodeViewer
|
||||||
showLineNumbers={false}
|
copyLabel={t("common:copy")}
|
||||||
wrapLongLines={true}
|
copyOptions={[
|
||||||
header={<div className="text-xs">{generateResult?.injectorClassName}</div>}
|
{
|
||||||
|
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={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
||||||
|
aria-label={t("common:download")}
|
||||||
|
title={t("common:download")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!generateResult?.injectorBytesBase64Str) {
|
if (!injectorBytesBase64) {
|
||||||
toast.warning(t("memshell:tips.shellBytesEmpty"));
|
toast.warning(t("memshell:tips.shellBytesEmpty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadBytes(
|
downloadBytes(injectorBytesBase64, injectorClassName);
|
||||||
generateResult?.injectorBytesBase64Str,
|
|
||||||
generateResult?.injectorClassName,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon className="h-4 w-4" />
|
<DownloadIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
height={height}
|
height={height}
|
||||||
code={generateResult?.injectorBytesBase64Str ?? ""}
|
lines={sources.injector?.lines ?? null}
|
||||||
language="text"
|
placeholder={sourcePlaceholder}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</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 };
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"byPassJavaModule": "BypassModule",
|
"byPassJavaModule": "BypassModule",
|
||||||
"byPassJavaModule.description": "JDK 9+ module system strictly restricts reflective defineClass calls. When enabled, automatically inserts Unsafe-based module bypass code into injector",
|
"byPassJavaModule.description": "JDK 9+ module system strictly restricts reflective defineClass calls. When enabled, automatically inserts Unsafe-based module bypass code into injector",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"copy": "Copy",
|
||||||
"copyLabelSuccess": "Copy {{label}} successfully",
|
"copyLabelSuccess": "Copy {{label}} successfully",
|
||||||
"copySuccess": "Copy successfully",
|
"copySuccess": "Copy successfully",
|
||||||
"debug": "Debug Mode",
|
"debug": "Debug Mode",
|
||||||
@@ -56,5 +57,8 @@
|
|||||||
"commandTemplate": "Command Template",
|
"commandTemplate": "Command Template",
|
||||||
"commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1",
|
"commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1",
|
||||||
"commandTemplate.description": "Use {command} as placeholder",
|
"commandTemplate.description": "Use {command} as placeholder",
|
||||||
"targetJdkVersion": "JRE Version"
|
"targetJdkVersion": "JRE Version",
|
||||||
|
"decompile": "Decompile",
|
||||||
|
"decompiling": "Decompiling...",
|
||||||
|
"decompileFailed": "Failed to decompile source: {{error}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"byPassJavaModule": "绕过模块限制",
|
"byPassJavaModule": "绕过模块限制",
|
||||||
"byPassJavaModule.description": "JDK 9+ 模块化系统严格限制反射调用 defineClass。开启后会在注入器中自动插入使用 Unsafe 绕过模块限制的代码",
|
"byPassJavaModule.description": "JDK 9+ 模块化系统严格限制反射调用 defineClass。开启后会在注入器中自动插入使用 Unsafe 绕过模块限制的代码",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
|
"copy": "复制",
|
||||||
"copyLabelSuccess": "复制 {{label}} 成功",
|
"copyLabelSuccess": "复制 {{label}} 成功",
|
||||||
"copySuccess": "复制成功",
|
"copySuccess": "复制成功",
|
||||||
"debug": "调试模式",
|
"debug": "调试模式",
|
||||||
@@ -56,5 +57,8 @@
|
|||||||
"commandTemplate": "命令模板",
|
"commandTemplate": "命令模板",
|
||||||
"commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1",
|
"commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1",
|
||||||
"commandTemplate.description": "使用 {command} 作为占位符",
|
"commandTemplate.description": "使用 {command} 作为占位符",
|
||||||
"targetJdkVersion": "JRE 版本"
|
"targetJdkVersion": "JRE 版本",
|
||||||
|
"decompile": "反编译",
|
||||||
|
"decompiling": "正在反编译...",
|
||||||
|
"decompileFailed": "反编译源码失败:{{error}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,5 +57,7 @@
|
|||||||
"tips.try-to-use-shell": "Try to use the memory shell",
|
"tips.try-to-use-shell": "Try to use the memory shell",
|
||||||
"tips.download-jar": "Download the jar file and upload it to the public network server, so that it can be accessed through the http link to download",
|
"tips.download-jar": "Download the jar file and upload it to the public network server, so that it can be accessed through the http link to download",
|
||||||
"tips.load-jar-with-scriptenginemanager": "Load the jar file with javax.script.ScriptEngineManager to implement injection",
|
"tips.load-jar-with-scriptenginemanager": "Load the jar file with javax.script.ScriptEngineManager to implement injection",
|
||||||
"tips.trigger-injector-class-loading": "Trigger the injector class loading with RCE vulnerability"
|
"tips.trigger-injector-class-loading": "Trigger the injector class loading with RCE vulnerability",
|
||||||
|
"copySource": "Copy Source",
|
||||||
|
"copyBase64": "Copy Base64"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,5 +57,7 @@
|
|||||||
"tips.try-to-use-shell": "尝试利用内存马",
|
"tips.try-to-use-shell": "尝试利用内存马",
|
||||||
"tips.download-jar": "下载 jar 包并上传至公网服务器,使其能通过 http 链接访问下载",
|
"tips.download-jar": "下载 jar 包并上传至公网服务器,使其能通过 http 链接访问下载",
|
||||||
"tips.load-jar-with-scriptenginemanager": "通过 RCE 漏洞使用 javax.script.ScriptEngineManager 加载 jar 包实现注入",
|
"tips.load-jar-with-scriptenginemanager": "通过 RCE 漏洞使用 javax.script.ScriptEngineManager 加载 jar 包实现注入",
|
||||||
"tips.trigger-injector-class-loading": "通过 RCE 漏洞触发注入器类加载"
|
"tips.trigger-injector-class-loading": "通过 RCE 漏洞触发注入器类加载",
|
||||||
|
"copySource": "复制源码",
|
||||||
|
"copyBase64": "复制 Base64"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import "./prism-worker-guard";
|
||||||
|
import { decompile } from "@run-slicer/cfr";
|
||||||
|
import Prism from "prismjs";
|
||||||
|
import "prismjs/components/prism-clike";
|
||||||
|
import "prismjs/components/prism-java";
|
||||||
|
|
||||||
|
/** [tokenType, text] segments of one rendered line */
|
||||||
|
export type TokenLine = Array<[string, string]>;
|
||||||
|
|
||||||
|
export interface DecompiledClass {
|
||||||
|
source: string;
|
||||||
|
lines: TokenLine[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecompileRequest {
|
||||||
|
id: number;
|
||||||
|
shellClassName: string;
|
||||||
|
shellBytesBase64: string;
|
||||||
|
injectorClassName: string;
|
||||||
|
injectorBytesBase64: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecompileResponse {
|
||||||
|
id: number;
|
||||||
|
shell?: DecompiledClass;
|
||||||
|
injector?: DecompiledClass;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToBytes(base64String: string) {
|
||||||
|
const byteCharacters = atob(base64String);
|
||||||
|
const bytes = new Uint8Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
bytes[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJvmClassName(className: string) {
|
||||||
|
return className.replaceAll(".", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeToLines(code: string): TokenLine[] {
|
||||||
|
const lines: TokenLine[] = [[]];
|
||||||
|
const push = (type: string, text: string) => {
|
||||||
|
const parts = text.split("\n");
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (i > 0) lines.push([]);
|
||||||
|
if (parts[i]) lines[lines.length - 1].push([type, parts[i]]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const walk = (token: string | Prism.Token, inheritedType: string) => {
|
||||||
|
if (typeof token === "string") {
|
||||||
|
push(inheritedType, token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { type, content } = token;
|
||||||
|
if (typeof content === "string") {
|
||||||
|
push(type, content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const child of Array.isArray(content) ? content : [content]) {
|
||||||
|
walk(child, type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const token of Prism.tokenize(code, Prism.languages.java)) {
|
||||||
|
walk(token, "plain");
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decompileClass(
|
||||||
|
jvmClassName: string,
|
||||||
|
classes: Map<string, Uint8Array>,
|
||||||
|
): Promise<DecompiledClass> {
|
||||||
|
const source = await decompile(jvmClassName, { source: (name) => classes.get(name) ?? null });
|
||||||
|
return { source, lines: tokenizeToLines(source) };
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async (event: MessageEvent<DecompileRequest>) => {
|
||||||
|
const { id, shellClassName, shellBytesBase64, injectorClassName, injectorBytesBase64 } =
|
||||||
|
event.data;
|
||||||
|
try {
|
||||||
|
const classes = new Map<string, Uint8Array>([
|
||||||
|
[toJvmClassName(shellClassName), base64ToBytes(shellBytesBase64)],
|
||||||
|
[toJvmClassName(injectorClassName), base64ToBytes(injectorBytesBase64)],
|
||||||
|
]);
|
||||||
|
const shell = await decompileClass(toJvmClassName(shellClassName), classes);
|
||||||
|
const injector = await decompileClass(toJvmClassName(injectorClassName), classes);
|
||||||
|
self.postMessage({ id, shell, injector } satisfies DecompileResponse);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
id,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
} satisfies DecompileResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* prismjs auto-registers a `message` listener when loaded inside a Web Worker
|
||||||
|
* (it expects JSON strings and calls JSON.parse on evt.data). Our worker
|
||||||
|
* exchanges structured-clone objects, which crashes that handler with an
|
||||||
|
* uncaught SyntaxError. Setting `self.Prism.disableWorkerMessageHandler`
|
||||||
|
* before prism-core evaluates suppresses the registration.
|
||||||
|
*
|
||||||
|
* Imported for side effects before any prismjs import — ES module evaluation
|
||||||
|
* order guarantees this runs first.
|
||||||
|
*/
|
||||||
|
(globalThis as unknown as { Prism: { disableWorkerMessageHandler: boolean } }).Prism = {
|
||||||
|
disableWorkerMessageHandler: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import type { DecompiledClass, DecompileRequest, DecompileResponse } from "./decompile.worker";
|
||||||
|
import type { MemShellResult } from "@/types/memshell";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export interface DecompiledSources {
|
||||||
|
shell: DecompiledClass | null;
|
||||||
|
injector: DecompiledClass | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DecompileState {
|
||||||
|
sources: DecompiledSources;
|
||||||
|
isDecompiling: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IDLE: DecompileState = {
|
||||||
|
sources: { shell: null, injector: null },
|
||||||
|
isDecompiling: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CACHE_LIMIT = 8;
|
||||||
|
const cache = new Map<string, DecompiledSources>();
|
||||||
|
|
||||||
|
function cacheGet(key: string) {
|
||||||
|
const value = cache.get(key);
|
||||||
|
if (value) {
|
||||||
|
cache.delete(key);
|
||||||
|
cache.set(key, value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheSet(key: string, value: DecompiledSources) {
|
||||||
|
cache.set(key, value);
|
||||||
|
if (cache.size > CACHE_LIMIT) {
|
||||||
|
const oldest = cache.keys().next().value;
|
||||||
|
if (oldest !== undefined) cache.delete(oldest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** FNV-1a over both base64 payloads; cheap identity for one generation. */
|
||||||
|
function cacheKey(shellBytes: string, injectorBytes: string) {
|
||||||
|
let hash = 0x811c9dc5;
|
||||||
|
for (const str of [shellBytes, injectorBytes]) {
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash ^= str.charCodeAt(i);
|
||||||
|
hash = Math.imul(hash, 0x01000193);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${(hash >>> 0).toString(36)}:${shellBytes.length}:${injectorBytes.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let worker: Worker | null = null;
|
||||||
|
let nextId = 0;
|
||||||
|
const pending = new Map<
|
||||||
|
number,
|
||||||
|
{ resolve: (value: DecompiledSources) => void; reject: (reason: Error) => void }
|
||||||
|
>();
|
||||||
|
|
||||||
|
function getWorker() {
|
||||||
|
if (!worker) {
|
||||||
|
worker = new Worker(new URL("./decompile.worker.ts", import.meta.url), { type: "module" });
|
||||||
|
worker.onmessage = (event: MessageEvent<DecompileResponse>) => {
|
||||||
|
const { id, shell, injector, error } = event.data;
|
||||||
|
const entry = pending.get(id);
|
||||||
|
if (!entry) return;
|
||||||
|
pending.delete(id);
|
||||||
|
if (error || !shell || !injector) {
|
||||||
|
entry.reject(new Error(error ?? "decompile failed"));
|
||||||
|
} else {
|
||||||
|
entry.resolve({ shell, injector });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
worker.onerror = (event) => {
|
||||||
|
const reason = new Error(event.message || "decompile worker failed");
|
||||||
|
for (const entry of pending.values()) entry.reject(reason);
|
||||||
|
pending.clear();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decompileInWorker(request: Omit<DecompileRequest, "id">) {
|
||||||
|
return new Promise<DecompiledSources>((resolve, reject) => {
|
||||||
|
const id = nextId++;
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
getWorker().postMessage({ ...request, id } satisfies DecompileRequest);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDecompiledSources(generateResult: MemShellResult | undefined) {
|
||||||
|
const shellClassName = generateResult?.shellClassName;
|
||||||
|
const shellBytesBase64 = generateResult?.shellBytesBase64Str;
|
||||||
|
const injectorClassName = generateResult?.injectorClassName;
|
||||||
|
const injectorBytesBase64 = generateResult?.injectorBytesBase64Str;
|
||||||
|
|
||||||
|
const [state, setState] = useState<DecompileState>(IDLE);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shellClassName || !shellBytesBase64 || !injectorClassName || !injectorBytesBase64) {
|
||||||
|
setState(IDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = cacheKey(shellBytesBase64, injectorBytesBase64);
|
||||||
|
const cached = cacheGet(key);
|
||||||
|
if (cached) {
|
||||||
|
setState({ sources: cached, isDecompiling: false, error: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setState({ sources: { shell: null, injector: null }, isDecompiling: true, error: null });
|
||||||
|
|
||||||
|
decompileInWorker({
|
||||||
|
shellClassName,
|
||||||
|
shellBytesBase64,
|
||||||
|
injectorClassName,
|
||||||
|
injectorBytesBase64,
|
||||||
|
})
|
||||||
|
.then((sources) => {
|
||||||
|
cacheSet(key, sources);
|
||||||
|
if (!cancelled) setState({ sources, isDecompiling: false, error: null });
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setState({
|
||||||
|
sources: { shell: null, injector: null },
|
||||||
|
isDecompiling: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [injectorBytesBase64, injectorClassName, shellBytesBase64, shellClassName]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"@orama/stopwords": "^3.1.18",
|
"@orama/stopwords": "^3.1.18",
|
||||||
"@orama/tokenizers": "^3.1.18",
|
"@orama/tokenizers": "^3.1.18",
|
||||||
"@react-router/node": "^7.15.1",
|
"@react-router/node": "^7.15.1",
|
||||||
|
"@run-slicer/cfr": "file:./vendor/cfr",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -452,6 +453,8 @@
|
|||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
||||||
|
|
||||||
|
"@run-slicer/cfr": ["@run-slicer/cfr@file:./vendor/cfr", {}],
|
||||||
|
|
||||||
"@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/primitive": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ=="],
|
"@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/primitive": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ=="],
|
||||||
|
|
||||||
"@shikijs/engine-javascript": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ=="],
|
"@shikijs/engine-javascript": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ=="],
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@orama/stopwords": "^3.1.18",
|
"@orama/stopwords": "^3.1.18",
|
||||||
"@orama/tokenizers": "^3.1.18",
|
"@orama/tokenizers": "^3.1.18",
|
||||||
"@react-router/node": "^7.15.1",
|
"@react-router/node": "^7.15.1",
|
||||||
|
"@run-slicer/cfr": "file:./vendor/cfr",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
declare module "@run-slicer/cfr" {
|
||||||
|
export type Options = Record<string, string>;
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
source?: (name: string) => Uint8Array | null;
|
||||||
|
options?: Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decompile(name: string, config?: Config): Promise<string>;
|
||||||
|
}
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
const isNode =
|
||||||
|
typeof process !== "undefined" &&
|
||||||
|
process?.versions?.node != null;
|
||||||
|
const wasmPath = async () => {
|
||||||
|
const url = new URL("./cfr.wasm", import.meta.url).href;
|
||||||
|
return isNode ? await import("node:url").then((mod) => mod.fileURLToPath(url)) : url;
|
||||||
|
};
|
||||||
|
|
||||||
|
let decompileFunc = null;
|
||||||
|
export const decompile = async (name, options) => {
|
||||||
|
if (!decompileFunc) {
|
||||||
|
const { load } = await import("./cfr.wasm-runtime.js");
|
||||||
|
const { exports } = await load(await wasmPath(), { noAutoImports: true });
|
||||||
|
|
||||||
|
decompileFunc = exports.decompile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decompileFunc(name, options);
|
||||||
|
};
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
+30
File diff suppressed because one or more lines are too long
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@run-slicer/cfr",
|
||||||
|
"version": "0.1.6-0.152",
|
||||||
|
"description": "A JavaScript port of the CFR decompiler (https://github.com/leibnitz27/cfr).",
|
||||||
|
"type": "module",
|
||||||
|
"main": "cfr.js",
|
||||||
|
"module": "cfr.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./cfr.d.ts",
|
||||||
|
"import": "./cfr.js",
|
||||||
|
"default": "./cfr.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": "cfr.d.ts",
|
||||||
|
"keywords": [
|
||||||
|
"decompiler",
|
||||||
|
"java",
|
||||||
|
"decompilation",
|
||||||
|
"cfr"
|
||||||
|
],
|
||||||
|
"author": "run-slicer",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user