Skip to content

Commit e8c2d47

Browse files
committed
feat(web): support download button for some packers
1 parent 9dc2c46 commit e8c2d47

File tree

6 files changed

+107
-39
lines changed

6 files changed

+107
-39
lines changed

web/bun.lockb

640 Bytes
Binary file not shown.

web/package.json

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,42 +15,42 @@
1515
},
1616
"devDependencies": {
1717
"@biomejs/biome": "2.1.4",
18-
"@react-router/dev": "^7.9.1",
19-
"@types/node": "^24.5.2",
20-
"@types/react": "^19.1.13",
18+
"@react-router/dev": "^7.9.5",
19+
"@types/node": "^24.10.0",
20+
"@types/react": "^19.2.2",
2121
"@types/react-copy-to-clipboard": "^5.0.7",
22-
"@types/react-dom": "^19.1.9",
22+
"@types/react-dom": "^19.2.2",
2323
"@types/react-syntax-highlighter": "^15.5.13",
24-
"@vitejs/plugin-react": "^5.0.3",
25-
"rimraf": "^6.0.1",
26-
"tailwindcss": "^4.1.13",
27-
"typescript": "^5.9.2",
28-
"vite": "^7.1.6",
24+
"@vitejs/plugin-react": "^5.1.0",
25+
"rimraf": "^6.1.0",
26+
"tailwindcss": "^4.1.16",
27+
"typescript": "^5.9.3",
28+
"vite": "^7.1.12",
2929
"vite-bundle-visualizer": "^1.2.1"
3030
},
3131
"dependencies": {
3232
"@hookform/resolvers": "^5.2.2",
33-
"@tailwindcss/vite": "^4.1.13",
34-
"@tanstack/react-query": "^5.89.0",
33+
"@tailwindcss/vite": "^4.1.16",
34+
"@tanstack/react-query": "^5.90.6",
3535
"class-variance-authority": "^0.7.1",
3636
"clsx": "^2.1.1",
37-
"framer-motion": "^12.23.16",
38-
"i18next": "^25.5.2",
37+
"framer-motion": "^12.23.24",
38+
"i18next": "^25.6.0",
3939
"lucide-react": "^0.539.0",
40-
"motion": "^12.23.16",
40+
"motion": "^12.23.24",
4141
"radix-ui": "^1.4.3",
42-
"react": "^19.1.1",
42+
"react": "^19.2.0",
4343
"react-copy-to-clipboard": "^5.1.0",
44-
"react-dom": "^19.1.1",
45-
"react-hook-form": "^7.62.0",
46-
"react-i18next": "^15.7.3",
47-
"react-router": "^7.9.1",
48-
"react-router-dom": "^7.9.1",
44+
"react-dom": "^19.2.0",
45+
"react-hook-form": "^7.66.0",
46+
"react-i18next": "^15.7.4",
47+
"react-router": "^7.9.5",
48+
"react-router-dom": "^7.9.5",
4949
"react-syntax-highlighter": "^15.6.6",
5050
"sonner": "^2.0.7",
5151
"tailwind-merge": "^3.3.1",
52-
"tw-animate-css": "^1.3.8",
53-
"yup": "^1.7.0"
52+
"tw-animate-css": "^1.4.0",
53+
"yup": "^1.7.1"
5454
},
5555
"trustedDependencies": [
5656
"@biomejs/biome"

web/src/components/memshell/results/multi-packer.tsx

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,76 @@
1+
import { DownloadIcon } from "lucide-react";
12
import { useEffect, useState } from "react";
23
import { useTranslation } from "react-i18next";
34
import CodeViewer from "@/components/code-viewer";
5+
import { Button } from "@/components/ui/button";
46
import {
57
Select,
68
SelectContent,
79
SelectItem,
810
SelectTrigger,
911
SelectValue,
1012
} from "@/components/ui/select";
13+
import { base64ToBytes, downloadBytes, downloadContent } from "@/lib/utils";
1114

1215
export function MultiPackResult({
1316
allPackResults,
1417
packMethod,
18+
shellClassName,
1519
height = 350,
1620
}: Readonly<{
1721
allPackResults: object | undefined;
1822
packMethod: string;
23+
shellClassName?: string;
1924
height?: number;
2025
}>) {
2126
const showCode = packMethod === "JSP";
2227
const { t } = useTranslation();
2328
const packMethods = Object.keys(allPackResults ?? {});
29+
2430
const [selectedMethod, setSelectedMethod] = useState(packMethods[0]);
2531
const [packResult, setPackResult] = useState(
2632
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
2733
);
2834

2935
useEffect(() => {
30-
const methods = Object.keys(allPackResults ?? {});
31-
const firstMethod = methods[0];
32-
setSelectedMethod(firstMethod);
33-
setPackResult(
34-
allPackResults?.[firstMethod as keyof typeof allPackResults] ?? "",
35-
);
36-
}, [allPackResults]);
36+
const newPackMethods = Object.keys(allPackResults ?? {});
37+
if (!newPackMethods.includes(selectedMethod)) {
38+
const newSelectedMethod = newPackMethods[0];
39+
setSelectedMethod(newSelectedMethod);
40+
setPackResult(
41+
allPackResults?.[newSelectedMethod as keyof typeof allPackResults] ??
42+
"",
43+
);
44+
} else {
45+
setPackResult(
46+
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
47+
);
48+
}
49+
}, [allPackResults, selectedMethod]);
50+
51+
const handleDownload = () => {
52+
const fileName =
53+
shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
54+
if (packMethod === "JSP") {
55+
const fileExtension = selectedMethod.includes("JSPX") ? ".jspx" : ".jsp";
56+
const content = new Blob([packResult], { type: "text/plain" });
57+
return downloadContent(content, fileName, fileExtension);
58+
} else if (
59+
packMethod === "JavaDeserialize" ||
60+
packMethod.includes("Hessian")
61+
) {
62+
const content = new Blob([base64ToBytes(packResult)], {
63+
type: "application/octet-stream",
64+
});
65+
return downloadContent(content, fileName, ".data");
66+
} else if (packMethod === "Base64") {
67+
const base64Content =
68+
allPackResults?.[
69+
Object.keys(allPackResults)[0] as keyof typeof allPackResults
70+
] ?? "";
71+
return downloadBytes(base64Content, shellClassName);
72+
}
73+
};
3774

3875
return (
3976
<CodeViewer
@@ -66,6 +103,22 @@ export function MultiPackResult({
66103
<span className="text-muted-foreground">({packResult?.length})</span>
67104
</div>
68105
}
106+
button={
107+
packMethod === "JSP" ||
108+
packMethod === "Base64" ||
109+
packMethod === "JavaDeserialize" ||
110+
packMethod.includes("Hessian") ? (
111+
<Button
112+
variant="ghost"
113+
size="icon"
114+
type="button"
115+
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
116+
onClick={handleDownload}
117+
>
118+
<DownloadIcon className="h-4 w-4" />
119+
</Button>
120+
) : null
121+
}
69122
wrapLongLines={!showCode}
70123
showLineNumbers={showCode}
71124
language={showCode ? "java" : "text"}

web/src/components/memshell/results/result-component.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function ResultComponent({
2525
<MultiPackResult
2626
allPackResults={allPackResults}
2727
packMethod={packMethod}
28+
shellClassName={generateResult?.injectorClassName}
2829
/>
2930
);
3031
}

web/src/components/probeshell/shell-result.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default function ShellResult({
4040
{allPackResults && (
4141
<MultiPackResult
4242
allPackResults={allPackResults}
43+
shellClassName={generateResult?.shellClassName}
4344
packMethod={packMethod}
4445
height={height}
4546
/>

web/src/lib/utils.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,38 @@ export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs));
66
}
77

8-
export function downloadBytes(
9-
base64String: string,
10-
className?: string,
11-
jarName?: string,
8+
export function downloadContent(
9+
content: Blob,
10+
fileName: string,
11+
fileExtension: string,
1212
) {
13+
const link = document.createElement("a");
14+
link.href = URL.createObjectURL(content);
15+
link.download = `${fileName}${fileExtension}`;
16+
document.body.appendChild(link);
17+
link.click();
18+
document.body.removeChild(link);
19+
}
20+
21+
export function base64ToBytes(base64String: string) {
1322
const byteCharacters = atob(base64String);
1423
const byteNumbers = new Array(byteCharacters.length);
1524
for (let i = 0; i < byteCharacters.length; i++) {
1625
byteNumbers[i] = byteCharacters.charCodeAt(i);
1726
}
18-
const byteArray = new Uint8Array(byteNumbers);
27+
return new Uint8Array(byteNumbers);
28+
}
1929

20-
// Create a Blob from the byte array
30+
export function downloadBytes(
31+
base64String: string,
32+
className?: string,
33+
jarName?: string,
34+
) {
35+
const byteArray = base64ToBytes(base64String);
2136
const blob = new Blob([byteArray], {
2237
type: className ? "application/java-vm" : "application/java-archive",
2338
});
2439

25-
// Create a download link
2640
const link = document.createElement("a");
2741
link.href = window.URL.createObjectURL(blob);
2842
link.download = className
@@ -39,15 +53,14 @@ export function formatBytes(bytes: number) {
3953
if (Number.isNaN(bytes) || !Number.isFinite(bytes)) return "N/A";
4054

4155
const k = 1024;
42-
const sizes = ["Bytes", "KB", "MB"]; // Add more units like GB, TB if needed
56+
const sizes = ["Bytes", "KB", "MB"];
4357

4458
if (bytes < k) {
45-
return `${bytes.toFixed(0)} ${sizes[0]}`; // Bytes, no decimal
59+
return `${bytes.toFixed(0)} ${sizes[0]}`;
4660
}
4761

4862
const i = Math.floor(Math.log(bytes) / Math.log(k));
4963

50-
// Prefer MB if KB value is 1000 or more, or if it's already in MB (i >= 2)
5164
if (i >= 2 || bytes / k ** 1 >= 1000) {
5265
const mbValue = Number.parseFloat((bytes / k ** 2).toFixed(2));
5366
return `${mbValue} ${sizes[2]}`;

0 commit comments

Comments
 (0)