diff --git a/web/bun.lockb b/web/bun.lockb index 9ad30ac2..9b05fd07 100755 Binary files a/web/bun.lockb and b/web/bun.lockb differ diff --git a/web/package.json b/web/package.json index cf8fa112..ca1f9282 100644 --- a/web/package.json +++ b/web/package.json @@ -15,42 +15,42 @@ }, "devDependencies": { "@biomejs/biome": "2.1.4", - "@react-router/dev": "^7.9.1", - "@types/node": "^24.5.2", - "@types/react": "^19.1.13", + "@react-router/dev": "^7.9.5", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", "@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", - "@vitejs/plugin-react": "^5.0.3", - "rimraf": "^6.0.1", - "tailwindcss": "^4.1.13", - "typescript": "^5.9.2", - "vite": "^7.1.6", + "@vitejs/plugin-react": "^5.1.0", + "rimraf": "^6.1.0", + "tailwindcss": "^4.1.16", + "typescript": "^5.9.3", + "vite": "^7.1.12", "vite-bundle-visualizer": "^1.2.1" }, "dependencies": { "@hookform/resolvers": "^5.2.2", - "@tailwindcss/vite": "^4.1.13", - "@tanstack/react-query": "^5.89.0", + "@tailwindcss/vite": "^4.1.16", + "@tanstack/react-query": "^5.90.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "framer-motion": "^12.23.16", - "i18next": "^25.5.2", + "framer-motion": "^12.23.24", + "i18next": "^25.6.0", "lucide-react": "^0.539.0", - "motion": "^12.23.16", + "motion": "^12.23.24", "radix-ui": "^1.4.3", - "react": "^19.1.1", + "react": "^19.2.0", "react-copy-to-clipboard": "^5.1.0", - "react-dom": "^19.1.1", - "react-hook-form": "^7.62.0", - "react-i18next": "^15.7.3", - "react-router": "^7.9.1", - "react-router-dom": "^7.9.1", + "react-dom": "^19.2.0", + "react-hook-form": "^7.66.0", + "react-i18next": "^15.7.4", + "react-router": "^7.9.5", + "react-router-dom": "^7.9.5", "react-syntax-highlighter": "^15.6.6", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tw-animate-css": "^1.3.8", - "yup": "^1.7.0" + "tw-animate-css": "^1.4.0", + "yup": "^1.7.1" }, "trustedDependencies": [ "@biomejs/biome" diff --git a/web/src/components/memshell/results/multi-packer.tsx b/web/src/components/memshell/results/multi-packer.tsx index 7500b8dc..fdcc2bfb 100644 --- a/web/src/components/memshell/results/multi-packer.tsx +++ b/web/src/components/memshell/results/multi-packer.tsx @@ -1,6 +1,8 @@ +import { DownloadIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import CodeViewer from "@/components/code-viewer"; +import { Button } from "@/components/ui/button"; import { Select, SelectContent, @@ -8,32 +10,67 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { base64ToBytes, downloadBytes, downloadContent } from "@/lib/utils"; export function MultiPackResult({ allPackResults, packMethod, + shellClassName, height = 350, }: Readonly<{ allPackResults: object | undefined; packMethod: string; + shellClassName?: string; height?: number; }>) { const showCode = packMethod === "JSP"; const { t } = useTranslation(); const packMethods = Object.keys(allPackResults ?? {}); + const [selectedMethod, setSelectedMethod] = useState(packMethods[0]); const [packResult, setPackResult] = useState( allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "", ); useEffect(() => { - const methods = Object.keys(allPackResults ?? {}); - const firstMethod = methods[0]; - setSelectedMethod(firstMethod); - setPackResult( - allPackResults?.[firstMethod as keyof typeof allPackResults] ?? "", - ); - }, [allPackResults]); + const newPackMethods = Object.keys(allPackResults ?? {}); + if (!newPackMethods.includes(selectedMethod)) { + const newSelectedMethod = newPackMethods[0]; + setSelectedMethod(newSelectedMethod); + setPackResult( + allPackResults?.[newSelectedMethod as keyof typeof 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 ( ({packResult?.length}) } + button={ + packMethod === "JSP" || + packMethod === "Base64" || + packMethod === "JavaDeserialize" || + packMethod.includes("Hessian") ? ( + + ) : null + } wrapLongLines={!showCode} showLineNumbers={showCode} language={showCode ? "java" : "text"} diff --git a/web/src/components/memshell/results/result-component.tsx b/web/src/components/memshell/results/result-component.tsx index d957f3f5..bbdab73c 100644 --- a/web/src/components/memshell/results/result-component.tsx +++ b/web/src/components/memshell/results/result-component.tsx @@ -25,6 +25,7 @@ export function ResultComponent({ ); } diff --git a/web/src/components/probeshell/shell-result.tsx b/web/src/components/probeshell/shell-result.tsx index 72a30669..f91ffeaf 100644 --- a/web/src/components/probeshell/shell-result.tsx +++ b/web/src/components/probeshell/shell-result.tsx @@ -40,6 +40,7 @@ export default function ShellResult({ {allPackResults && ( diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index bc8b4687..ae962470 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -5,24 +5,38 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function downloadBytes( - base64String: string, - className?: string, - jarName?: string, +export function downloadContent( + content: Blob, + fileName: 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 byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; 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], { type: className ? "application/java-vm" : "application/java-archive", }); - // Create a download link const link = document.createElement("a"); link.href = window.URL.createObjectURL(blob); link.download = className @@ -39,15 +53,14 @@ export function formatBytes(bytes: number) { if (Number.isNaN(bytes) || !Number.isFinite(bytes)) return "N/A"; const k = 1024; - const sizes = ["Bytes", "KB", "MB"]; // Add more units like GB, TB if needed + const sizes = ["Bytes", "KB", "MB"]; 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)); - // 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) { const mbValue = Number.parseFloat((bytes / k ** 2).toFixed(2)); return `${mbValue} ${sizes[2]}`;