feat(web): support download button for some packers

This commit is contained in:
ReaJason
2025-11-20 00:36:25 +08:00
parent 761ec7a9e7
commit 2620a30a4e
6 changed files with 107 additions and 39 deletions
BIN
View File
Binary file not shown.
+22 -22
View File
@@ -15,42 +15,42 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.1.4", "@biomejs/biome": "2.1.4",
"@react-router/dev": "^7.9.1", "@react-router/dev": "^7.9.5",
"@types/node": "^24.5.2", "@types/node": "^24.10.0",
"@types/react": "^19.1.13", "@types/react": "^19.2.2",
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.2.2",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^5.0.3", "@vitejs/plugin-react": "^5.1.0",
"rimraf": "^6.0.1", "rimraf": "^6.1.0",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.16",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vite": "^7.1.6", "vite": "^7.1.12",
"vite-bundle-visualizer": "^1.2.1" "vite-bundle-visualizer": "^1.2.1"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.16",
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.90.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.16", "framer-motion": "^12.23.24",
"i18next": "^25.5.2", "i18next": "^25.6.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"motion": "^12.23.16", "motion": "^12.23.24",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.1.1", "react": "^19.2.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^19.1.1", "react-dom": "^19.2.0",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.66.0",
"react-i18next": "^15.7.3", "react-i18next": "^15.7.4",
"react-router": "^7.9.1", "react-router": "^7.9.5",
"react-router-dom": "^7.9.1", "react-router-dom": "^7.9.5",
"react-syntax-highlighter": "^15.6.6", "react-syntax-highlighter": "^15.6.6",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.4.0",
"yup": "^1.7.0" "yup": "^1.7.1"
}, },
"trustedDependencies": [ "trustedDependencies": [
"@biomejs/biome" "@biomejs/biome"
@@ -1,6 +1,8 @@
import { DownloadIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CodeViewer from "@/components/code-viewer"; import CodeViewer from "@/components/code-viewer";
import { Button } from "@/components/ui/button";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -8,32 +10,67 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { base64ToBytes, downloadBytes, downloadContent } from "@/lib/utils";
export function MultiPackResult({ export function MultiPackResult({
allPackResults, allPackResults,
packMethod, packMethod,
shellClassName,
height = 350, height = 350,
}: Readonly<{ }: Readonly<{
allPackResults: object | undefined; allPackResults: object | undefined;
packMethod: string; packMethod: string;
shellClassName?: string;
height?: number; height?: number;
}>) { }>) {
const showCode = packMethod === "JSP"; const showCode = packMethod === "JSP";
const { t } = useTranslation(); const { t } = useTranslation();
const packMethods = Object.keys(allPackResults ?? {}); const packMethods = Object.keys(allPackResults ?? {});
const [selectedMethod, setSelectedMethod] = useState(packMethods[0]); const [selectedMethod, setSelectedMethod] = useState(packMethods[0]);
const [packResult, setPackResult] = useState( const [packResult, setPackResult] = useState(
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "", allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
); );
useEffect(() => { useEffect(() => {
const methods = Object.keys(allPackResults ?? {}); const newPackMethods = Object.keys(allPackResults ?? {});
const firstMethod = methods[0]; if (!newPackMethods.includes(selectedMethod)) {
setSelectedMethod(firstMethod); const newSelectedMethod = newPackMethods[0];
setPackResult( setSelectedMethod(newSelectedMethod);
allPackResults?.[firstMethod as keyof typeof allPackResults] ?? "", setPackResult(
); allPackResults?.[newSelectedMethod as keyof typeof allPackResults] ??
}, [allPackResults]); "",
);
} else {
setPackResult(
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
);
}
}, [allPackResults, selectedMethod]);
const handleDownload = () => {
const fileName =
shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
if (packMethod === "JSP") {
const fileExtension = selectedMethod.includes("JSPX") ? ".jspx" : ".jsp";
const content = new Blob([packResult], { type: "text/plain" });
return downloadContent(content, fileName, fileExtension);
} else if (
packMethod === "JavaDeserialize" ||
packMethod.includes("Hessian")
) {
const content = new Blob([base64ToBytes(packResult)], {
type: "application/octet-stream",
});
return downloadContent(content, fileName, ".data");
} else if (packMethod === "Base64") {
const base64Content =
allPackResults?.[
Object.keys(allPackResults)[0] as keyof typeof allPackResults
] ?? "";
return downloadBytes(base64Content, shellClassName);
}
};
return ( return (
<CodeViewer <CodeViewer
@@ -66,6 +103,22 @@ export function MultiPackResult({
<span className="text-muted-foreground">({packResult?.length})</span> <span className="text-muted-foreground">({packResult?.length})</span>
</div> </div>
} }
button={
packMethod === "JSP" ||
packMethod === "Base64" ||
packMethod === "JavaDeserialize" ||
packMethod.includes("Hessian") ? (
<Button
variant="ghost"
size="icon"
type="button"
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
onClick={handleDownload}
>
<DownloadIcon className="h-4 w-4" />
</Button>
) : null
}
wrapLongLines={!showCode} wrapLongLines={!showCode}
showLineNumbers={showCode} showLineNumbers={showCode}
language={showCode ? "java" : "text"} language={showCode ? "java" : "text"}
@@ -25,6 +25,7 @@ export function ResultComponent({
<MultiPackResult <MultiPackResult
allPackResults={allPackResults} allPackResults={allPackResults}
packMethod={packMethod} packMethod={packMethod}
shellClassName={generateResult?.injectorClassName}
/> />
); );
} }
@@ -40,6 +40,7 @@ export default function ShellResult({
{allPackResults && ( {allPackResults && (
<MultiPackResult <MultiPackResult
allPackResults={allPackResults} allPackResults={allPackResults}
shellClassName={generateResult?.shellClassName}
packMethod={packMethod} packMethod={packMethod}
height={height} height={height}
/> />
+23 -10
View File
@@ -5,24 +5,38 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export function downloadBytes( export function downloadContent(
base64String: string, content: Blob,
className?: string, fileName: string,
jarName?: string, fileExtension: string,
) { ) {
const link = document.createElement("a");
link.href = URL.createObjectURL(content);
link.download = `${fileName}${fileExtension}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
export function base64ToBytes(base64String: string) {
const byteCharacters = atob(base64String); const byteCharacters = atob(base64String);
const byteNumbers = new Array(byteCharacters.length); const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) { for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i); byteNumbers[i] = byteCharacters.charCodeAt(i);
} }
const byteArray = new Uint8Array(byteNumbers); return new Uint8Array(byteNumbers);
}
// Create a Blob from the byte array export function downloadBytes(
base64String: string,
className?: string,
jarName?: string,
) {
const byteArray = base64ToBytes(base64String);
const blob = new Blob([byteArray], { const blob = new Blob([byteArray], {
type: className ? "application/java-vm" : "application/java-archive", type: className ? "application/java-vm" : "application/java-archive",
}); });
// Create a download link
const link = document.createElement("a"); const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob); link.href = window.URL.createObjectURL(blob);
link.download = className link.download = className
@@ -39,15 +53,14 @@ export function formatBytes(bytes: number) {
if (Number.isNaN(bytes) || !Number.isFinite(bytes)) return "N/A"; if (Number.isNaN(bytes) || !Number.isFinite(bytes)) return "N/A";
const k = 1024; const k = 1024;
const sizes = ["Bytes", "KB", "MB"]; // Add more units like GB, TB if needed const sizes = ["Bytes", "KB", "MB"];
if (bytes < k) { if (bytes < k) {
return `${bytes.toFixed(0)} ${sizes[0]}`; // Bytes, no decimal return `${bytes.toFixed(0)} ${sizes[0]}`;
} }
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
// Prefer MB if KB value is 1000 or more, or if it's already in MB (i >= 2)
if (i >= 2 || bytes / k ** 1 >= 1000) { if (i >= 2 || bytes / k ** 1 >= 1000) {
const mbValue = Number.parseFloat((bytes / k ** 2).toFixed(2)); const mbValue = Number.parseFloat((bytes / k ** 2).toFixed(2));
return `${mbValue} ${sizes[2]}`; return `${mbValue} ${sizes[2]}`;