diff --git a/ui/.gitignore b/ui/.gitignore index f4116f2d78..d2f6414102 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -24,4 +24,6 @@ dist-ssr *.sw? data.json -gsa.wasm \ No newline at end of file +gsa.wasm + +coverage/ \ No newline at end of file diff --git a/ui/common.ts b/ui/common.ts index 6d057c55e8..d617bbc8dd 100644 --- a/ui/common.ts +++ b/ui/common.ts @@ -3,6 +3,7 @@ import {codecovVitePlugin} from "@codecov/vite-plugin"; import * as path from "node:path"; import react from "@vitejs/plugin-react-swc"; import {execSync} from "node:child_process"; +import type { InlineConfig } from 'vitest'; export function getSha(): string | undefined { const envs = process.env; @@ -72,3 +73,22 @@ export function build(dir: string): BuildOptions { }, } } + +export function testConfig(): InlineConfig { + return { + coverage: { + provider: "v8", + enabled: true, + exclude: [ + "node_modules", + "dist", + "coverage", + "vite.config.ts", + "vite.config-explorer.ts", + "common.ts", + "src/tool/wasm_exec.js" + ] + } + } +} + diff --git a/ui/package.json b/ui/package.json index d4f7395a53..5a0884cdb3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,8 @@ "dev:explorer": "vite -c vite.config-explorer.ts", "generate": "typia generate --input src/schema --output src/generated --project tsconfig.json", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "test": "vitest" + "test:ui": "vitest -c vite.config.ts", + "test:explorer": "vitest -c vite.config-explorer.ts" }, "dependencies": { "@emotion/react": "^11.11.4", @@ -39,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^7.10.0", "@typescript-eslint/parser": "^7.10.0", "@vitejs/plugin-react-swc": "^3.7.0", + "@vitest/coverage-v8": "^1.6.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.2", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index d2aa60beb1..9d75fa33b5 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: '@vitejs/plugin-react-swc': specifier: ^3.7.0 version: 3.7.0(vite@5.2.11(@types/node@20.12.12)(lightningcss@1.25.1)(sass@1.77.2)(terser@5.31.0)) + '@vitest/coverage-v8': + specifier: ^1.6.0 + version: 1.6.0(vitest@1.6.0(@types/node@20.12.12)(jsdom@24.1.0)(lightningcss@1.25.1)(sass@1.77.2)(terser@5.31.0)) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -123,6 +126,10 @@ importers: packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@babel/code-frame@7.24.6': resolution: {integrity: sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==} engines: {node: '>=6.9.0'} @@ -143,6 +150,11 @@ packages: resolution: {integrity: sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==} engines: {node: '>=6.9.0'} + '@babel/parser@7.24.6': + resolution: {integrity: sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.24.6': resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==} engines: {node: '>=6.9.0'} @@ -151,6 +163,9 @@ packages: resolution: {integrity: sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@codecov/bundler-plugin-core@0.0.1-beta.8': resolution: {integrity: sha512-3yxNa4N+pZxqU60XQxJ10sDKUWcDf/YaQSvODrgxLfKG/peZhSdbd92uRc7PYi73szu7lr1CVZIvVerod9C4Uw==} engines: {node: '>=18.0.0'} @@ -397,6 +412,10 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -808,6 +827,11 @@ packages: peerDependencies: vite: ^4 || ^5 + '@vitest/coverage-v8@1.6.0': + resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} + peerDependencies: + vitest: 1.6.0 + '@vitest/expect@1.6.0': resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} @@ -1551,6 +1575,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} engines: {node: '>=12'} @@ -1751,6 +1778,22 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -1903,6 +1946,13 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magicast@0.3.4: + resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -2444,6 +2494,10 @@ packages: engines: {node: '>=10'} hasBin: true + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2748,6 +2802,11 @@ packages: snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + '@babel/code-frame@7.24.6': dependencies: '@babel/highlight': 7.24.6 @@ -2768,6 +2827,10 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.1 + '@babel/parser@7.24.6': + dependencies: + '@babel/types': 7.24.6 + '@babel/runtime@7.24.6': dependencies: regenerator-runtime: 0.14.1 @@ -2778,6 +2841,8 @@ snapshots: '@babel/helper-validator-identifier': 7.24.6 to-fast-properties: 2.0.0 + '@bcoe/v8-coverage@0.2.3': {} + '@codecov/bundler-plugin-core@0.0.1-beta.8': dependencies: chalk: 4.1.2 @@ -2989,6 +3054,8 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@istanbuljs/schema@0.1.3': {} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 @@ -3371,6 +3438,25 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.12.12)(jsdom@24.1.0)(lightningcss@1.25.1)(sass@1.77.2)(terser@5.31.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.7 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + test-exclude: 6.0.0 + vitest: 1.6.0(@types/node@20.12.12)(jsdom@24.1.0)(lightningcss@1.25.1)(sass@1.77.2)(terser@5.31.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@1.6.0': dependencies: '@vitest/spy': 1.6.0 @@ -4302,6 +4388,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + html-minifier-terser@6.1.0: dependencies: camel-case: 4.1.2 @@ -4501,6 +4589,27 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.4: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterator.prototype@1.1.2: dependencies: define-properties: 1.2.1 @@ -4664,6 +4773,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + magicast@0.3.4: + dependencies: + '@babel/parser': 7.24.6 + '@babel/types': 7.24.6 + source-map-js: 1.2.0 + + make-dir@4.0.0: + dependencies: + semver: 7.6.2 + mdn-data@2.0.14: {} merge-stream@2.0.0: {} @@ -5260,6 +5379,12 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + text-table@0.2.0: {} throttle-debounce@3.0.1: {} diff --git a/ui/src/explorer/app.tsx b/ui/src/explorer/app.tsx index 161880d715..ae32b85d9f 100644 --- a/ui/src/explorer/app.tsx +++ b/ui/src/explorer/app.tsx @@ -1,7 +1,7 @@ import React, {ReactNode, useEffect, useMemo} from "react"; import {useAsync} from "react-use"; import gsa from "../../gsa.wasm?init"; -import {Entry} from "../tool/entry.ts"; +import {createEntry} from "../tool/entry.ts"; import {Dialog, DialogContent, DialogContentText, DialogTitle} from "@mui/material"; import {FileSelector} from "./file_selector.tsx"; import TreeMap from "../TreeMap.tsx"; @@ -51,7 +51,7 @@ export const App: React.FC = () => { return null } - return new Entry(result) + return createEntry(result) }, [result]) useEffect(() => { diff --git a/ui/src/main.tsx b/ui/src/main.tsx index a3721923ce..0f114381e4 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -3,10 +3,10 @@ import ReactDOM from 'react-dom/client' import TreeMap from './TreeMap.tsx' import {loadDataFromEmbed} from "./tool/utils.ts"; -import {Entry} from "./tool/entry.ts"; +import {createEntry} from "./tool/entry.ts"; ReactDOM.createRoot(document.getElementById('root')!).render( - + , ) diff --git a/ui/src/tool/aligner.ts b/ui/src/tool/aligner.ts new file mode 100644 index 0000000000..ccfdf8009b --- /dev/null +++ b/ui/src/tool/aligner.ts @@ -0,0 +1,23 @@ +import {max} from "d3-array"; + +export class aligner { + private pre: string[] = []; + private post: string[] = []; + + public add(pre: string, post: string): aligner { + this.pre.push(pre); + this.post.push(post); + return this; + } + + public toString(): string { + // determine the maximum length of the pre-strings + const maxPreLength = max(this.pre, (d) => d.length) ?? 0; + let ret = ""; + for (let i = 0; i < this.pre.length; i++) { + ret += this.pre[i].padEnd(maxPreLength + 1) + this.post[i] + "\n"; + } + ret = ret.trimEnd(); + return ret; + } +} \ No newline at end of file diff --git a/ui/src/tool/entry.ts b/ui/src/tool/entry.ts index 9376be9db2..92b30d5eb7 100644 --- a/ui/src/tool/entry.ts +++ b/ui/src/tool/entry.ts @@ -3,286 +3,376 @@ import { FileSymbol, Package, Result, - Section, - isFile, - isPackage, - isResult, - isSection, - isSymbol + Section } from "../generated/schema.ts"; import {orderedID} from "./id.ts"; import {formatBytes, title} from "./utils.ts"; -import {max} from "d3-array"; - -type Candidate = Section | File | Package | Result | FileSymbol; - -export type EntryType = "section" | "file" | "package" | "result" | "symbol" | "disasm" | "unknown" | "container"; - -export class Entry { - private readonly type: EntryType; - private readonly data?: Candidate; - private readonly size: number; - private readonly name: string; - private readonly children: Entry[] = []; - private readonly uid = orderedID(); - explain: string = ""; // should only be used by the container type - - constructor(data: Candidate) - constructor(name: string, size: number, type: EntryType, children?: Entry[]) - constructor(data_or_name: Candidate | string, size?: number, type?: EntryType, children: Entry[] = []) { - if (typeof data_or_name === "string") { - this.type = type!; - this.size = size!; - this.name = data_or_name; - this.children = children; - return - } +import {aligner} from "./aligner.ts"; + +type EntryType = "section" | "file" | "package" | "result" | "symbol" | "disasm" | "unknown" | "container"; + +type EntryChildren = { + "section": never[], + "file": never[], + "package": EntryLike<"package" | "symbol" | "disasm" | "file">[], + "result": EntryLike<"section" | "container" | "unknown">[], + "symbol": never[], + "disasm": never[], + "unknown": never[], + "container": EntryLike<"package" | "disasm" | "section">[] +} - this.type = Entry.checkType(data_or_name); - this.data = data_or_name; - this.size = Entry.loadSize(data_or_name); - this.name = Entry.candidateName(data_or_name, this.type); - this.children = Entry.childrenFromData(data_or_name, this.type); - } - - static childrenFromData(data: Candidate, type: EntryType): Entry[] { - switch (type) { - case "section": - case "file": - case "symbol": - return []; // no children for section or file - case "package": - return childrenFromPackage(data as Package); - case "result": - return childrenFromResult(data as Result); - default: - throw new Error(`Unknown type: ${type} in childrenFromData`); - } +export interface EntryLike { + toString(): string; + + getSize(): number; + + getType(): T; + + getName(): string; + + getChildren(): EntryChildren[T] + + getID(): number; +} + +class BaseImpl { + private readonly id = orderedID() + + getID(): number { + return this.id; } +} - static candidateName(candidate: Candidate, type: EntryType): string { - switch (type) { - case "section": - case "result": - case "package": - case "symbol": - return (
candidate).name; +export class SectionImpl extends BaseImpl implements EntryLike<"section"> { + constructor(private readonly data: Section) { + super(); + } - case "file": - return (candidate).file_path.split("/").pop()!; + getChildren(): EntryChildren["section"] { + return []; + } - default: - throw new Error(`Unknown type: ${type} in candidateName`); - } + getName(): string { + return this.data.name; } - static loadSize(data: Candidate): number { - switch (true) { - case isSection(data): - return data.file_size - data.known_size; - default: - return data.size; - } + getSize(): number { + return this.data.file_size - this.data.known_size; } - static checkType(candidate: Candidate): EntryType { - switch (true) { - case isSection(candidate): - return "section"; - case isFile(candidate): - return "file"; - case isPackage(candidate): - return "package"; - case isResult(candidate): - return "result"; - case isSymbol(candidate): - return "symbol"; - default: - throw new Error(`Unknown type in checkType`); - } + getType(): "section" { + return "section"; } - public toString(): string { + toString(): string { const align = new aligner(); + align.add("Section:", this.data.name) + .add("Size:", formatBytes(this.getSize())) + .add("File Size:", formatBytes(this.data.file_size)) + .add("Known size:", formatBytes(this.data.known_size)) + .add("Unknown size:", formatBytes(this.getSize())) + .add("Offset:", `0x${this.data.offset.toString(16)} - 0x${this.data.end.toString(16)}`) + .add("Address:", `0x${this.data.addr.toString(16)} - 0x${this.data.addr_end.toString(16)}`) + .add("Memory:", this.data.only_in_memory.toString()) + .add("Debug:", this.data.debug.toString()); + return align.toString(); + } +} - function assertTyp(_c?: Candidate): asserts _c is T { - } +export class FileImpl extends BaseImpl implements EntryLike<"file"> { + constructor(private readonly data: File) { + super(); + } - switch (this.type) { - case "section": - assertTyp
(this.data); - align.add("Section:", this.name); - align.add("Size:", formatBytes(this.size)); - align.add("File Size:", formatBytes(this.data.file_size)); - align.add("Known size:", formatBytes(this.data.known_size)); - align.add("Unknown size:", formatBytes(this.data.size - this.data.known_size)); - align.add("Offset:", `0x${this.data.offset.toString(16)} - 0x${this.data.end.toString(16)}`); - align.add("Address:", `0x${this.data.addr.toString(16)} - 0x${this.data.addr_end.toString(16)}`); - align.add("Memory:", this.data.only_in_memory.toString()); - align.add("Debug:", this.data.debug.toString()); - return align.toString(); - - case "file": - assertTyp(this.data); - align.add("File:", this.data.file_path); - align.add("Path:", this.data.file_path); - align.add("Size:", formatBytes(this.data.size)); - align.add("Pcln Size:", formatBytes(this.data.pcln_size)); - return align.toString(); - - case "package": - assertTyp(this.data); - align.add("Package:", this.data.name); - align.add("Type:", this.data.type); - align.add("Size:", formatBytes(this.data.size)); - return align.toString(); - - case "result": - assertTyp(this.data); - align.add("Result:", this.data.name); - align.add("Size:", formatBytes(this.data.size)); - return align.toString(); - - case "disasm": { - align.add("Disasm:", this.name); - align.add("Size:", formatBytes(this.size)); - let ret = align.toString(); - ret += "\n\n" + - "This size was not accurate." + - "The real size determined by disassembling can be larger."; - return ret; - } + getChildren(): EntryChildren["file"] { + return []; + } - case "symbol": { - assertTyp(this.data); - align.add("Symbol:", this.data.name); - align.add("Size:", formatBytes(this.size)); - align.add("Address:", `0x${this.data.addr.toString(16)}`); - align.add("Type:", this.data.type); - return align.toString(); - } + getName(): string { + return this.data.file_path.split("/").pop()!; + } - case "unknown": { - align.add("Size:", formatBytes(this.size)); - let ret = align.toString(); - ret += "\n\n" + - "The unknown part in the binary.\n" + - "Can be ELF Header, Program Header, align offset...\n" + - "We just don't know."; - return ret; - } + getSize(): number { + return this.data.size; + } - case "container": { - let ret = this.explain + "\n" - align.add("Size:", formatBytes(this.size)); - ret += "\n" + align.toString(); - return ret; - } + getType(): "file" { + return "file"; + } + + toString(): string { + const align = new aligner(); + align.add("File:", this.data.file_path) + .add("Path:", this.data.file_path) + .add("Size:", formatBytes(this.data.size)) + .add("Pcln Size:", formatBytes(this.data.pcln_size)); + return align.toString(); + } +} + +export class PackageImpl extends BaseImpl implements EntryLike<"package"> { + private readonly children: EntryChildren["package"]; + + constructor(private readonly data: Package) { + super(); + + const children: EntryChildren["package"] = []; + for (const file of data.files) { + children.push(new FileImpl(file)); + } + for (const subPackage of Object.values(data.subPackages)) { + children.push(new PackageImpl(subPackage)); } + + for (const s of data.symbols) { + children.push(new SymbolImpl(s)); + } + + const leftSize = data.size - children.reduce((acc, child) => acc + child.getSize(), 0); + if (leftSize > 0) { + const name = `${data.name} Disasm` + children.push(new DisasmImpl(name, leftSize)); + } + + this.children = children; } - public getSize(): number { - return this.size; + getChildren(): EntryChildren["package"] { + return this.children; } - public getType(): EntryType { - return this.type; + getName(): string { + return this.data.name; } - public getName(): string { + getSize(): number { + return this.data.size; + } + + getType(): "package" { + return "package"; + } + + toString(): string { + const align = new aligner(); + align.add("Package:", this.data.name) + .add("Type:", this.data.type) + .add("Size:", formatBytes(this.data.size)); + return align.toString(); + } +} + +export class DisasmImpl extends BaseImpl implements EntryLike<"disasm"> { + constructor(private readonly name: string, private readonly size: number) { + super(); + } + + getChildren(): EntryChildren["disasm"] { + return []; + } + + getName(): string { return this.name; } - public getChildren(): Entry[] { - return this.children; + getSize(): number { + return this.size; } - public getID(): number { - return this.uid; + getType(): "disasm" { + return "disasm"; + } + + toString(): string { + const align = new aligner(); + align.add("Disasm:", this.name) + .add("Size:", formatBytes(this.size)); + let ret = align.toString(); + ret += "\n\n" + + "This size was not accurate." + + "The real size determined by disassembling can be larger."; + return ret; } } -function childrenFromPackage(pkg: Package): Entry[] { - const children: Entry[] = []; - for (const file of pkg.files) { - children.push(new Entry(file)); +export class SymbolImpl extends BaseImpl implements EntryLike<"symbol"> { + constructor(private readonly data: FileSymbol) { + super(); } - for (const subPackage of Object.values(pkg.subPackages)) { - children.push(new Entry(subPackage)); + + getChildren(): EntryChildren["symbol"] { + return []; + } + + getName(): string { + return this.data.name; } - for (const s of pkg.symbols) { - children.push(new Entry(s)); + getSize(): number { + return this.data.size; } - const leftSize = pkg.size - children.reduce((acc, child) => acc + child.getSize(), 0); - if (leftSize > 0) { - const name = `${pkg.name} Disasm` - children.push(new Entry(name, leftSize, "disasm")); + getType(): "symbol" { + return "symbol"; } - return children; + toString(): string { + const align = new aligner(); + align.add("Symbol:", this.data.name) + .add("Size:", formatBytes(this.data.size)) + .add("Address:", `0x${this.data.addr.toString(16)}`) + .add("Type:", this.data.type); + return align.toString(); + } } -function childrenFromResult(result: Result): Entry[] { - const children: Entry[] = []; +export class ContainerImpl extends BaseImpl implements EntryLike<"container"> { + constructor(private readonly name: string, + private readonly size: number, + private readonly children: EntryChildren["container"], + private readonly explain: string = "") { + super(); + } - const sectionContainerChildren: Entry[] = [] - for (const section of result.sections) { - sectionContainerChildren.push(new Entry(section)); + getChildren(): EntryChildren["container"] { + return this.children; } - const sectionContainerSize = sectionContainerChildren.reduce((acc, child) => acc + child.getSize(), 0); - const sectionContainer = new Entry("Unknown Sections Size", sectionContainerSize, "container", sectionContainerChildren); - sectionContainer.explain = "The unknown size of the sections in the binary." - children.push(sectionContainer); - const typedPackages: Record = {}; - for (const pkg of Object.values(result.packages)) { - if (typedPackages[pkg.type] == null) { - typedPackages[pkg.type] = []; - } - typedPackages[pkg.type].push(pkg); + getName(): string { + return this.name; } - const typedPackagesChildren: Entry[] = [] - for (const [type, packages] of Object.entries(typedPackages)) { - const packageContainerChildren: Entry[] = []; - for (const pkg of packages) { - packageContainerChildren.push(new Entry(pkg)); - } - const packageContainerSize = packageContainerChildren.reduce((acc, child) => acc + child.getSize(), 0); - const packageContainer = new Entry(`${title(type)} Packages Size`, packageContainerSize, "container", packageContainerChildren); - packageContainer.explain = `The size of the ${type} packages in the binary.` - typedPackagesChildren.push(packageContainer); + + getSize(): number { + return this.size; } - children.push(...typedPackagesChildren); - const leftSize = result.size - children.reduce((acc, child) => acc + child.getSize(), 0); - if (leftSize > 0) { - const name = `Unknown` - children.push(new Entry(name, leftSize, "unknown")); + getType(): "container" { + return "container"; } - return children; + toString(): string { + let ret = this.explain + "\n" + const align = new aligner(); + align.add("Size:", formatBytes(this.size)); + ret += "\n" + align.toString(); + return ret; + } } -class aligner { - private pre: string[] = []; - private post: string[] = []; +export class UnknownImpl extends BaseImpl implements EntryLike<"unknown"> { + constructor(private readonly size: number) { + super(); + } - public add(pre: string, post: string): void { - this.pre.push(pre); - this.post.push(post); + getChildren(): EntryChildren["unknown"] { + return []; } - public toString(): string { - // determine the maximum length of the pre-strings - const maxPreLength = max(this.pre, (d) => d.length) ?? 0; - let ret = ""; - for (let i = 0; i < this.pre.length; i++) { - ret += this.pre[i].padEnd(maxPreLength + 1) + this.post[i] + "\n"; - } - ret = ret.trimEnd(); + getName(): string { + return "Unknown"; + } + + getSize(): number { + return this.size; + } + + getType(): "unknown" { + return "unknown"; + } + + toString(): string { + const align = new aligner(); + align.add("Size:", formatBytes(this.size)); + let ret = align.toString(); + ret += "\n\n" + + "The unknown part in the binary.\n" + + "Can be ELF Header, Program Header, align offset...\n" + + "We just don't know."; return ret; } } + +export class ResultImpl extends BaseImpl implements EntryLike<"result"> { + private readonly children: EntryChildren["result"]; + + constructor(private readonly data: Result) { + super(); + + const children: EntryChildren["result"] = []; + + const sectionContainerChildren: EntryLike<"section">[] = [] + for (const section of data.sections) { + sectionContainerChildren.push(new SectionImpl(section)); + } + const sectionContainerSize = sectionContainerChildren.reduce((acc, child) => acc + child.getSize(), 0); + const sectionContainer = new ContainerImpl( + "Unknown Sections Size", + sectionContainerSize, + sectionContainerChildren, + "The unknown size of the sections in the binary."); + children.push(sectionContainer); + + const typedPackages: Record = {}; + for (const pkg of Object.values(data.packages)) { + if (typedPackages[pkg.type] == null) { + typedPackages[pkg.type] = []; + } + typedPackages[pkg.type].push(pkg); + } + const typedPackagesChildren: EntryLike<"container">[] = []; + for (const [type, packages] of Object.entries(typedPackages)) { + const packageContainerChildren: EntryLike<"package" | "disasm">[] = []; + for (const pkg of packages) { + packageContainerChildren.push(new PackageImpl(pkg)); + } + const packageContainerSize = packageContainerChildren.reduce((acc, child) => acc + child.getSize(), 0); + const packageContainer = new ContainerImpl( + `${title(type)} Packages Size`, + packageContainerSize, + packageContainerChildren, + `The size of the ${type} packages in the binary.` + ) + + typedPackagesChildren.push(packageContainer); + } + children.push(...typedPackagesChildren); + + const leftSize = data.size - children.reduce((acc, child) => acc + child.getSize(), 0); + if (leftSize > 0) { + children.push(new UnknownImpl(leftSize)); + } + + this.children = children; + } + + getChildren(): EntryChildren["result"] { + return this.children; + } + + getName(): string { + return this.data.name; + } + + getSize(): number { + return this.data.size; + } + + getType(): "result" { + return "result"; + } + + toString(): string { + const align = new aligner(); + align.add("Result:", this.data.name) + .add("Size:", formatBytes(this.data.size)); + return align.toString(); + } +} + +export type Entry = EntryLike; + +export function createEntry(data: Result): Entry { + return new ResultImpl(data); +} \ No newline at end of file diff --git a/ui/vite.config-explorer.ts b/ui/vite.config-explorer.ts index ec871f35e9..b558e1b649 100644 --- a/ui/vite.config-explorer.ts +++ b/ui/vite.config-explorer.ts @@ -1,5 +1,5 @@ -import {defineConfig} from 'vite' -import {build, codecov, commonPlugin, getVersionTag} from "./common"; +import {defineConfig} from 'vitest/config'; +import {build, codecov, commonPlugin, getVersionTag, testConfig} from "./common"; import {createHtmlPlugin} from "vite-plugin-html"; export default defineConfig({ @@ -22,5 +22,6 @@ export default defineConfig({ watch: { usePolling: true, }, - } + }, + test: testConfig(), }) diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 1434003e0e..1f61d7eb5e 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -1,7 +1,7 @@ -import {defineConfig} from 'vite' +import {defineConfig} from 'vitest/config'; import {viteSingleFile} from "vite-plugin-singlefile" import * as fs from "node:fs" -import {build, codecov, commonPlugin, getVersionTag} from "./common"; +import {build, codecov, commonPlugin, getVersionTag, testConfig} from "./common"; import {createHtmlPlugin} from "vite-plugin-html"; @@ -52,5 +52,6 @@ export default defineConfig({ codecov("gsa-ui"), ], clearScreen: false, - build: build("webui") + build: build("webui"), + test: testConfig(), })