diff --git a/src/app/debug/dll-viewer/page-client.tsx b/src/app/debug/dll-viewer/page-client.tsx
new file mode 100644
index 000000000..57ec26edc
--- /dev/null
+++ b/src/app/debug/dll-viewer/page-client.tsx
@@ -0,0 +1,570 @@
+"use client";
+
+import { ChangeEvent, useCallback, useMemo, useState } from "react";
+
+import {
+ parsePortableExecutable,
+ type PeDataDirectory,
+ type PeExport,
+ type PeImportLibrary,
+ type PeSection,
+ type PortableExecutableView,
+} from "@/client/dll-viewer/pe-parser";
+import { toErrorMessage } from "@/client/utils/diagnostics";
+import { useTranslation } from "@/i18n";
+
+type ParseStage = "idle" | "parsing" | "ready" | "error";
+
+function formatBytes(value: number): string {
+ if (value < 1024) return `${value} B`;
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
+ return `${(value / (1024 * 1024)).toFixed(2)} MB`;
+}
+
+function hex(value: number): string {
+ return `0x${value.toString(16).toUpperCase()}`;
+}
+
+function truncateJson(value: string): string {
+ const maxLength = 60_000;
+ if (value.length <= maxLength) return value;
+ return `${value.slice(0, maxLength)}\n...`;
+}
+
+function FieldGrid({ fields }: { fields: Array<[string, string]> }) {
+ return (
+
+ {fields.map(([label, value]) => (
+
+
- {label}
+ - {value}
+
+ ))}
+
+ );
+}
+
+function FlagList({ values }: { values: string[] }) {
+ if (values.length === 0) return -;
+ return (
+
+ {values.map((value) => (
+ {value}
+ ))}
+
+ );
+}
+
+function SectionsTable({ labels, sections }: { labels: ReturnType; sections: PeSection[] }) {
+ return (
+
+ {labels.dllViewerSections}
+
+
+
+
+ | {labels.dllViewerName} |
+ {labels.dllViewerVirtualAddress} |
+ {labels.dllViewerVirtualSize} |
+ {labels.dllViewerRawPointer} |
+ {labels.dllViewerRawSize} |
+ {labels.dllViewerCharacteristics} |
+
+
+
+ {sections.map((section) => (
+
+ | {section.name} |
+ {hex(section.virtualAddress)} |
+ {formatBytes(section.virtualSize)} |
+ {hex(section.rawPointer)} |
+ {formatBytes(section.rawSize)} |
+
+
+ |
+
+ ))}
+
+
+
+
+ );
+}
+
+function DirectoriesTable({
+ directories,
+ labels,
+}: {
+ directories: PeDataDirectory[];
+ labels: ReturnType;
+}) {
+ return (
+
+ {labels.dllViewerDataDirectories}
+
+
+
+
+ | {labels.dllViewerName} |
+ {labels.dllViewerRva} |
+ {labels.dllViewerSize} |
+
+
+
+ {directories.map((directory) => (
+
+ | {directory.name} |
+ {hex(directory.rva)} |
+ {formatBytes(directory.size)} |
+
+ ))}
+
+
+
+
+ );
+}
+
+function ExportsTable({ labels, exports }: { labels: ReturnType; exports: PeExport[] }) {
+ const visibleExports = exports.slice(0, 240);
+ return (
+ <>
+ {exports.length > visibleExports.length ? (
+ {labels.dllViewerShowingFirstExports.replace("{count}", String(visibleExports.length))}
+ ) : null}
+
+
+
+
+ | {labels.dllViewerName} |
+ {labels.dllViewerOrdinal} |
+ {labels.dllViewerRva} |
+ {labels.dllViewerForwardedTo} |
+
+
+
+ {visibleExports.map((exported) => (
+
+ | {exported.name} |
+ {exported.ordinal} |
+ {hex(exported.rva)} |
+ {exported.forwardedTo ?? "-"} |
+
+ ))}
+
+
+
+ >
+ );
+}
+
+function ImportsView({ imports, labels }: { imports: PeImportLibrary[]; labels: ReturnType }) {
+ if (imports.length === 0) {
+ return {labels.dllViewerNoImports}
;
+ }
+
+ return (
+
+ {imports.map((library) => {
+ const visibleFunctions = library.functions.slice(0, 80);
+ return (
+
+
+ {library.name}
+ {labels.dllViewerFunctions.replace("{count}", String(library.functions.length))}
+
+
+ {visibleFunctions.map((imported) => (
+
+ {imported.name}
+
+ ))}
+
+ {library.functions.length > visibleFunctions.length ? (
+ {labels.dllViewerShowingFirstImports.replace("{count}", String(visibleFunctions.length))}
+ ) : null}
+
+ );
+ })}
+
+ );
+}
+
+function useDllViewerLabels() {
+ const { t } = useTranslation();
+ return t.debug;
+}
+
+function ParsedDllView({ parsed }: { parsed: PortableExecutableView }) {
+ const labels = useDllViewerLabels();
+ const summaryFields = useMemo>(
+ () => [
+ [labels.dllViewerFile, `${parsed.fileName} · ${formatBytes(parsed.fileSize)}`],
+ [labels.dllViewerFormat, parsed.optionalHeader.format],
+ [labels.dllViewerMachine, parsed.coffHeader.machine],
+ [labels.dllViewerSubsystem, parsed.optionalHeader.subsystem],
+ [labels.dllViewerEntryPoint, hex(parsed.optionalHeader.entryPoint)],
+ [labels.dllViewerImageBase, parsed.optionalHeader.imageBase],
+ [labels.dllViewerTimestamp, parsed.coffHeader.timestamp],
+ [labels.dllViewerPeOffset, hex(parsed.dosHeader.peHeaderOffset)],
+ [labels.dllViewerSectionAlignment, formatBytes(parsed.optionalHeader.sectionAlignment)],
+ [labels.dllViewerFileAlignment, formatBytes(parsed.optionalHeader.fileAlignment)],
+ [labels.dllViewerSizeOfImage, formatBytes(parsed.optionalHeader.sizeOfImage)],
+ [labels.dllViewerChecksum, hex(parsed.optionalHeader.checksum)],
+ ],
+ [labels, parsed],
+ );
+
+ return (
+
+
+ {labels.dllViewerSummary}
+
+
+ {labels.dllViewerCharacteristics}
+
+
+
+
+
+ {labels.dllViewerExports}
+ {parsed.exports ? (
+ <>
+
+
+ >
+ ) : (
+ {labels.dllViewerNoExports}
+ )}
+
+
+
+ {labels.dllViewerImports}
+
+
+
+
+
+
+ {parsed.warnings.length > 0 ? (
+
+ {labels.dllViewerWarnings}
+
+ {parsed.warnings.map((warning) => (
+ - {warning}
+ ))}
+
+
+ ) : null}
+
+
+
+ {labels.dllViewerRawJson}
+ {truncateJson(JSON.stringify(parsed, null, 2))}
+
+
+
+ );
+}
+
+export function DllViewerPageClient() {
+ const labels = useDllViewerLabels();
+ const [stage, setStage] = useState("idle");
+ const [parsed, setParsed] = useState(null);
+ const [error, setError] = useState(null);
+
+ const statusLabel = {
+ idle: labels.dllViewerStatusIdle,
+ parsing: labels.dllViewerStatusParsing,
+ ready: labels.dllViewerStatusReady,
+ error: labels.dllViewerStatusError,
+ }[stage];
+
+ const handleFile = useCallback(
+ async (event: ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ setStage("parsing");
+ setError(null);
+ setParsed(null);
+
+ try {
+ const buffer = await file.arrayBuffer();
+ setParsed(parsePortableExecutable(buffer, file.name));
+ setStage("ready");
+ } catch (parseError) {
+ setError(toErrorMessage(parseError));
+ setStage("error");
+ }
+ },
+ [],
+ );
+
+ return (
+
+
+
+
+ {labels.dllViewerStatus}
+ {statusLabel}
+
+
+ {error ? (
+
+ {labels.dllViewerError}
+ {error}
+
+ ) : null}
+
+ {parsed ? : {labels.dllViewerNoResult}
}
+
+
+
+ );
+}
diff --git a/src/app/debug/dll-viewer/page.tsx b/src/app/debug/dll-viewer/page.tsx
new file mode 100644
index 000000000..fe6e647ae
--- /dev/null
+++ b/src/app/debug/dll-viewer/page.tsx
@@ -0,0 +1,11 @@
+import { Suspense } from "react";
+
+import { DllViewerPageClient } from "./page-client";
+
+export default function DllViewerPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/client/dll-viewer/__tests__/pe-parser.test.ts b/src/client/dll-viewer/__tests__/pe-parser.test.ts
new file mode 100644
index 000000000..e2e9ba94c
--- /dev/null
+++ b/src/client/dll-viewer/__tests__/pe-parser.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it } from "vitest";
+
+import { parsePortableExecutable } from "../pe-parser";
+
+function writeAscii(bytes: Uint8Array, offset: number, value: string): void {
+ for (let index = 0; index < value.length; index += 1) {
+ bytes[offset + index] = value.charCodeAt(index);
+ }
+}
+
+function buildPeFixture(): ArrayBuffer {
+ const buffer = new ArrayBuffer(1024);
+ const bytes = new Uint8Array(buffer);
+ const view = new DataView(buffer);
+ const peOffset = 0x80;
+ const coffOffset = peOffset + 4;
+ const optionalOffset = coffOffset + 20;
+ const sectionTableOffset = optionalOffset + 0xf0;
+
+ writeAscii(bytes, 0, "MZ");
+ view.setUint32(0x3c, peOffset, true);
+ writeAscii(bytes, peOffset, "PE\0\0");
+ view.setUint16(coffOffset, 0x8664, true);
+ view.setUint16(coffOffset + 2, 1, true);
+ view.setUint16(coffOffset + 16, 0xf0, true);
+ view.setUint16(coffOffset + 18, 0x2022, true);
+
+ view.setUint16(optionalOffset, 0x20b, true);
+ view.setUint32(optionalOffset + 16, 0x1010, true);
+ view.setUint32(optionalOffset + 24, 0x400000, true);
+ view.setUint32(optionalOffset + 32, 0x1000, true);
+ view.setUint32(optionalOffset + 36, 0x200, true);
+ view.setUint32(optionalOffset + 56, 0x3000, true);
+ view.setUint32(optionalOffset + 60, 0x200, true);
+ view.setUint16(optionalOffset + 68, 3, true);
+ view.setUint16(optionalOffset + 70, 0x0140, true);
+ view.setUint32(optionalOffset + 108, 16, true);
+ view.setUint32(optionalOffset + 112, 0x1100, true);
+ view.setUint32(optionalOffset + 116, 0x60, true);
+ view.setUint32(optionalOffset + 120, 0x1180, true);
+ view.setUint32(optionalOffset + 124, 0x80, true);
+
+ writeAscii(bytes, sectionTableOffset, ".rdata");
+ view.setUint32(sectionTableOffset + 8, 0x1000, true);
+ view.setUint32(sectionTableOffset + 12, 0x1000, true);
+ view.setUint32(sectionTableOffset + 16, 0x200, true);
+ view.setUint32(sectionTableOffset + 20, 0x200, true);
+ view.setUint32(sectionTableOffset + 36, 0x40000040, true);
+
+ const exportOffset = 0x300;
+ view.setUint32(exportOffset + 12, 0x1140, true);
+ view.setUint32(exportOffset + 16, 1, true);
+ view.setUint32(exportOffset + 20, 1, true);
+ view.setUint32(exportOffset + 24, 1, true);
+ view.setUint32(exportOffset + 28, 0x1150, true);
+ view.setUint32(exportOffset + 32, 0x1154, true);
+ view.setUint32(exportOffset + 36, 0x1158, true);
+ writeAscii(bytes, 0x340, "fixture.dll\0");
+ view.setUint32(0x350, 0x1010, true);
+ view.setUint32(0x354, 0x1160, true);
+ view.setUint16(0x358, 0, true);
+ writeAscii(bytes, 0x360, "FixtureExport\0");
+
+ const importOffset = 0x380;
+ view.setUint32(importOffset, 0x11d0, true);
+ view.setUint32(importOffset + 12, 0x11c0, true);
+ view.setUint32(importOffset + 16, 0x11e0, true);
+ writeAscii(bytes, 0x3c0, "KERNEL32.dll\0");
+ view.setUint32(0x3d0, 0x11f0, true);
+ view.setUint32(0x3d4, 0, true);
+ view.setUint16(0x3f0, 7, true);
+ writeAscii(bytes, 0x3f2, "CreateFileW\0");
+
+ return buffer;
+}
+
+describe("parsePortableExecutable", () => {
+ it("parses PE headers, sections, exports, and imports", () => {
+ const parsed = parsePortableExecutable(buildPeFixture(), "fixture.dll");
+
+ expect(parsed.coffHeader.machine).toBe("x86-64");
+ expect(parsed.coffHeader.characteristics).toContain("DLL");
+ expect(parsed.optionalHeader.format).toBe("PE32+");
+ expect(parsed.optionalHeader.subsystem).toBe("Windows Console");
+ expect(parsed.sections[0]).toMatchObject({ name: ".rdata", virtualAddress: 0x1000 });
+ expect(parsed.exports?.dllName).toBe("fixture.dll");
+ expect(parsed.exports?.namedExports[0]).toMatchObject({ name: "FixtureExport", ordinal: 1, rva: 0x1010 });
+ expect(parsed.imports[0]).toMatchObject({ name: "KERNEL32.dll" });
+ expect(parsed.imports[0].functions[0]).toMatchObject({ name: "CreateFileW", hint: 7 });
+ });
+
+ it("rejects non-PE input", () => {
+ expect(() => parsePortableExecutable(new Uint8Array([1, 2, 3]).buffer, "raw.bin")).toThrow(/missing MZ/);
+ });
+});
diff --git a/src/client/dll-viewer/pe-parser.ts b/src/client/dll-viewer/pe-parser.ts
new file mode 100644
index 000000000..d730947c0
--- /dev/null
+++ b/src/client/dll-viewer/pe-parser.ts
@@ -0,0 +1,476 @@
+export type PeDataDirectory = {
+ name: string;
+ rva: number;
+ size: number;
+};
+
+export type PeSection = {
+ name: string;
+ virtualAddress: number;
+ virtualSize: number;
+ rawPointer: number;
+ rawSize: number;
+ characteristics: string[];
+};
+
+export type PeExport = {
+ name: string;
+ ordinal: number;
+ rva: number;
+ forwardedTo?: string;
+};
+
+export type PeImport = {
+ name: string;
+ hint?: number;
+ ordinal?: number;
+};
+
+export type PeImportLibrary = {
+ name: string;
+ functions: PeImport[];
+};
+
+export type PortableExecutableView = {
+ fileName: string;
+ fileSize: number;
+ dosHeader: {
+ peHeaderOffset: number;
+ stubText: string;
+ };
+ coffHeader: {
+ machine: string;
+ machineValue: number;
+ sectionCount: number;
+ timestamp: string;
+ characteristics: string[];
+ };
+ optionalHeader: {
+ format: string;
+ entryPoint: number;
+ imageBase: string;
+ subsystem: string;
+ sectionAlignment: number;
+ fileAlignment: number;
+ sizeOfImage: number;
+ checksum: number;
+ dllCharacteristics: string[];
+ };
+ dataDirectories: PeDataDirectory[];
+ sections: PeSection[];
+ exports: {
+ dllName: string;
+ ordinalBase: number;
+ functionCount: number;
+ namedExports: PeExport[];
+ } | null;
+ imports: PeImportLibrary[];
+ warnings: string[];
+};
+
+const DATA_DIRECTORY_NAMES = [
+ "Export Table",
+ "Import Table",
+ "Resource Table",
+ "Exception Table",
+ "Certificate Table",
+ "Base Relocation Table",
+ "Debug",
+ "Architecture",
+ "Global Ptr",
+ "TLS Table",
+ "Load Config Table",
+ "Bound Import",
+ "Import Address Table",
+ "Delay Import Descriptor",
+ "CLR Runtime Header",
+ "Reserved",
+];
+
+const MACHINE_NAMES = new Map([
+ [0x014c, "x86"],
+ [0x01c0, "ARM"],
+ [0x01c4, "ARMv7"],
+ [0x0200, "Intel Itanium"],
+ [0x8664, "x86-64"],
+ [0xaa64, "ARM64"],
+]);
+
+const SUBSYSTEM_NAMES = new Map([
+ [1, "Native"],
+ [2, "Windows GUI"],
+ [3, "Windows Console"],
+ [5, "OS/2 Console"],
+ [7, "POSIX Console"],
+ [9, "Windows CE GUI"],
+ [10, "EFI Application"],
+ [11, "EFI Boot Service Driver"],
+ [12, "EFI Runtime Driver"],
+ [13, "EFI ROM"],
+ [14, "Xbox"],
+ [16, "Windows Boot Application"],
+]);
+
+const COFF_CHARACTERISTICS: Array<[number, string]> = [
+ [0x0002, "Executable"],
+ [0x0020, "Large address aware"],
+ [0x0100, "32-bit machine"],
+ [0x0200, "Debug symbols stripped"],
+ [0x2000, "DLL"],
+ [0x4000, "Uniprocessor only"],
+];
+
+const DLL_CHARACTERISTICS: Array<[number, string]> = [
+ [0x0020, "High entropy VA"],
+ [0x0040, "Dynamic base"],
+ [0x0080, "Force integrity"],
+ [0x0100, "NX compatible"],
+ [0x0200, "No isolation"],
+ [0x0400, "No SEH"],
+ [0x0800, "No bind"],
+ [0x1000, "AppContainer"],
+ [0x2000, "WDM driver"],
+ [0x4000, "Guard CF"],
+ [0x8000, "Terminal Server aware"],
+];
+
+const SECTION_CHARACTERISTICS: Array<[number, string]> = [
+ [0x00000020, "Code"],
+ [0x00000040, "Initialized data"],
+ [0x00000080, "Uninitialized data"],
+ [0x02000000, "Discardable"],
+ [0x04000000, "Not cached"],
+ [0x08000000, "Not paged"],
+ [0x10000000, "Shared"],
+ [0x20000000, "Execute"],
+ [0x40000000, "Read"],
+ [0x80000000, "Write"],
+];
+
+function flagsFrom(value: number, definitions: Array<[number, string]>): string[] {
+ return definitions.filter(([flag]) => (value & flag) !== 0).map(([, label]) => label);
+}
+
+function hex(value: number): string {
+ return `0x${value.toString(16).toUpperCase()}`;
+}
+
+class PeReader {
+ readonly data: DataView;
+ readonly bytes: Uint8Array;
+ readonly warnings: string[] = [];
+
+ constructor(buffer: ArrayBuffer) {
+ this.bytes = new Uint8Array(buffer);
+ this.data = new DataView(buffer);
+ }
+
+ has(offset: number, length: number): boolean {
+ return Number.isInteger(offset) && offset >= 0 && offset + length <= this.data.byteLength;
+ }
+
+ u16(offset: number): number {
+ if (!this.has(offset, 2)) throw new Error(`PE read out of bounds at ${hex(offset)}`);
+ return this.data.getUint16(offset, true);
+ }
+
+ u32(offset: number): number {
+ if (!this.has(offset, 4)) throw new Error(`PE read out of bounds at ${hex(offset)}`);
+ return this.data.getUint32(offset, true);
+ }
+
+ u64(offset: number): bigint {
+ const low = BigInt(this.u32(offset));
+ const high = BigInt(this.u32(offset + 4));
+ return (high << 32n) | low;
+ }
+
+ ascii(offset: number, length: number): string {
+ if (!this.has(offset, length)) return "";
+ let result = "";
+ for (let index = 0; index < length; index += 1) {
+ const byte = this.bytes[offset + index];
+ if (byte === 0) break;
+ if (byte >= 32 && byte <= 126) result += String.fromCharCode(byte);
+ }
+ return result;
+ }
+
+ cString(offset: number, maxLength = 4096): string {
+ if (!this.has(offset, 1)) return "";
+ let result = "";
+ for (let index = offset; index < this.bytes.length && index < offset + maxLength; index += 1) {
+ const byte = this.bytes[index];
+ if (byte === 0) break;
+ if (byte >= 32 && byte <= 126) result += String.fromCharCode(byte);
+ }
+ return result;
+ }
+}
+
+function rvaToOffset(rva: number, sections: PeSection[], sizeOfHeaders: number): number | null {
+ if (rva > 0 && rva < sizeOfHeaders) return rva;
+
+ for (const section of sections) {
+ const sectionSize = Math.max(section.virtualSize, section.rawSize);
+ if (rva >= section.virtualAddress && rva < section.virtualAddress + sectionSize) {
+ return section.rawPointer + (rva - section.virtualAddress);
+ }
+ }
+
+ return null;
+}
+
+function readTimestamp(seconds: number): string {
+ if (seconds === 0) return "0";
+ return new Date(seconds * 1000).toISOString();
+}
+
+function readDataDirectories(reader: PeReader, start: number, count: number): PeDataDirectory[] {
+ const directories: PeDataDirectory[] = [];
+ const safeCount = Math.min(count, DATA_DIRECTORY_NAMES.length);
+
+ for (let index = 0; index < safeCount; index += 1) {
+ const offset = start + index * 8;
+ if (!reader.has(offset, 8)) break;
+
+ const rva = reader.u32(offset);
+ const size = reader.u32(offset + 4);
+ if (rva !== 0 || size !== 0) {
+ directories.push({ name: DATA_DIRECTORY_NAMES[index], rva, size });
+ }
+ }
+
+ return directories;
+}
+
+function readSections(reader: PeReader, sectionTableOffset: number, count: number): PeSection[] {
+ const sections: PeSection[] = [];
+
+ for (let index = 0; index < count; index += 1) {
+ const offset = sectionTableOffset + index * 40;
+ if (!reader.has(offset, 40)) {
+ reader.warnings.push(`Section table truncated before section ${index + 1}.`);
+ break;
+ }
+
+ sections.push({
+ name: reader.ascii(offset, 8) || `section_${index + 1}`,
+ virtualSize: reader.u32(offset + 8),
+ virtualAddress: reader.u32(offset + 12),
+ rawSize: reader.u32(offset + 16),
+ rawPointer: reader.u32(offset + 20),
+ characteristics: flagsFrom(reader.u32(offset + 36), SECTION_CHARACTERISTICS),
+ });
+ }
+
+ return sections;
+}
+
+function findDirectory(directories: PeDataDirectory[], name: string): PeDataDirectory | undefined {
+ return directories.find((directory) => directory.name === name && directory.rva !== 0 && directory.size !== 0);
+}
+
+function readExports(
+ reader: PeReader,
+ directories: PeDataDirectory[],
+ sections: PeSection[],
+ sizeOfHeaders: number,
+): PortableExecutableView["exports"] {
+ const directory = findDirectory(directories, "Export Table");
+ if (!directory) return null;
+
+ const directoryOffset = rvaToOffset(directory.rva, sections, sizeOfHeaders);
+ if (directoryOffset == null || !reader.has(directoryOffset, 40)) {
+ reader.warnings.push("Export table points outside the mapped sections.");
+ return null;
+ }
+
+ const dllNameOffset = rvaToOffset(reader.u32(directoryOffset + 12), sections, sizeOfHeaders);
+ const ordinalBase = reader.u32(directoryOffset + 16);
+ const functionCount = reader.u32(directoryOffset + 20);
+ const nameCount = reader.u32(directoryOffset + 24);
+ const functionsOffset = rvaToOffset(reader.u32(directoryOffset + 28), sections, sizeOfHeaders);
+ const namesOffset = rvaToOffset(reader.u32(directoryOffset + 32), sections, sizeOfHeaders);
+ const ordinalsOffset = rvaToOffset(reader.u32(directoryOffset + 36), sections, sizeOfHeaders);
+ const namedExports: PeExport[] = [];
+
+ if (functionsOffset == null || namesOffset == null || ordinalsOffset == null) {
+ reader.warnings.push("Export name arrays point outside the mapped sections.");
+ } else {
+ const safeNameCount = Math.min(nameCount, 5000);
+ for (let index = 0; index < safeNameCount; index += 1) {
+ const nameRva = reader.u32(namesOffset + index * 4);
+ const nameOffset = rvaToOffset(nameRva, sections, sizeOfHeaders);
+ const ordinalIndex = reader.u16(ordinalsOffset + index * 2);
+ const functionRva = reader.u32(functionsOffset + ordinalIndex * 4);
+ const forwardedTo =
+ functionRva >= directory.rva && functionRva < directory.rva + directory.size
+ ? reader.cString(rvaToOffset(functionRva, sections, sizeOfHeaders) ?? -1)
+ : undefined;
+
+ namedExports.push({
+ name: nameOffset == null ? `(name at ${hex(nameRva)})` : reader.cString(nameOffset),
+ ordinal: ordinalBase + ordinalIndex,
+ rva: functionRva,
+ ...(forwardedTo ? { forwardedTo } : {}),
+ });
+ }
+
+ if (nameCount > safeNameCount) {
+ reader.warnings.push(`Export list truncated to ${safeNameCount} names.`);
+ }
+ }
+
+ return {
+ dllName: dllNameOffset == null ? "" : reader.cString(dllNameOffset),
+ ordinalBase,
+ functionCount,
+ namedExports,
+ };
+}
+
+function readImports(
+ reader: PeReader,
+ directories: PeDataDirectory[],
+ sections: PeSection[],
+ sizeOfHeaders: number,
+ isPe32Plus: boolean,
+): PeImportLibrary[] {
+ const directory = findDirectory(directories, "Import Table");
+ if (!directory) return [];
+
+ const importOffset = rvaToOffset(directory.rva, sections, sizeOfHeaders);
+ if (importOffset == null) {
+ reader.warnings.push("Import table points outside the mapped sections.");
+ return [];
+ }
+
+ const libraries: PeImportLibrary[] = [];
+ for (let descriptor = 0; descriptor < 256; descriptor += 1) {
+ const offset = importOffset + descriptor * 20;
+ if (!reader.has(offset, 20)) break;
+
+ const originalFirstThunk = reader.u32(offset);
+ const nameRva = reader.u32(offset + 12);
+ const firstThunk = reader.u32(offset + 16);
+ if (originalFirstThunk === 0 && nameRva === 0 && firstThunk === 0) break;
+
+ const nameOffset = rvaToOffset(nameRva, sections, sizeOfHeaders);
+ const thunkOffset = rvaToOffset(originalFirstThunk || firstThunk, sections, sizeOfHeaders);
+ const functions: PeImport[] = [];
+
+ if (thunkOffset == null) {
+ reader.warnings.push(`Import thunk table for descriptor ${descriptor + 1} points outside mapped sections.`);
+ } else {
+ const thunkSize = isPe32Plus ? 8 : 4;
+ const ordinalMask = isPe32Plus ? 0x8000000000000000n : 0x80000000n;
+ const valueMask = isPe32Plus ? 0x7fffffffffffffffn : 0x7fffffffn;
+
+ for (let index = 0; index < 2000; index += 1) {
+ const thunkEntryOffset = thunkOffset + index * thunkSize;
+ if (!reader.has(thunkEntryOffset, thunkSize)) break;
+
+ const thunkValue = isPe32Plus ? reader.u64(thunkEntryOffset) : BigInt(reader.u32(thunkEntryOffset));
+ if (thunkValue === 0n) break;
+
+ if ((thunkValue & ordinalMask) !== 0n) {
+ functions.push({ name: `#${Number(thunkValue & 0xffffn)}`, ordinal: Number(thunkValue & 0xffffn) });
+ continue;
+ }
+
+ const hintNameRva = Number(thunkValue & valueMask);
+ const hintNameOffset = rvaToOffset(hintNameRva, sections, sizeOfHeaders);
+ if (hintNameOffset == null || !reader.has(hintNameOffset, 2)) {
+ functions.push({ name: `(import at ${hex(hintNameRva)})` });
+ continue;
+ }
+
+ functions.push({
+ hint: reader.u16(hintNameOffset),
+ name: reader.cString(hintNameOffset + 2),
+ });
+ }
+ }
+
+ libraries.push({
+ name: nameOffset == null ? `(library at ${hex(nameRva)})` : reader.cString(nameOffset),
+ functions,
+ });
+ }
+
+ return libraries;
+}
+
+export function parsePortableExecutable(buffer: ArrayBuffer, fileName: string): PortableExecutableView {
+ const reader = new PeReader(buffer);
+
+ if (reader.ascii(0, 2) !== "MZ") {
+ throw new Error("Not a PE file: missing MZ DOS header.");
+ }
+
+ const peHeaderOffset = reader.u32(0x3c);
+ if (reader.ascii(peHeaderOffset, 4) !== "PE") {
+ throw new Error("Not a PE file: missing PE signature.");
+ }
+
+ const coffOffset = peHeaderOffset + 4;
+ const machineValue = reader.u16(coffOffset);
+ const sectionCount = reader.u16(coffOffset + 2);
+ const timestamp = reader.u32(coffOffset + 4);
+ const optionalHeaderSize = reader.u16(coffOffset + 16);
+ const coffCharacteristics = reader.u16(coffOffset + 18);
+ const optionalOffset = coffOffset + 20;
+ const optionalMagic = reader.u16(optionalOffset);
+ const isPe32Plus = optionalMagic === 0x20b;
+
+ if (optionalMagic !== 0x10b && optionalMagic !== 0x20b) {
+ throw new Error(`Unsupported PE optional header magic ${hex(optionalMagic)}.`);
+ }
+
+ const entryPoint = reader.u32(optionalOffset + 16);
+ const imageBase = isPe32Plus ? reader.u64(optionalOffset + 24).toString() : String(reader.u32(optionalOffset + 28));
+ const sectionAlignment = reader.u32(optionalOffset + 32);
+ const fileAlignment = reader.u32(optionalOffset + 36);
+ const sizeOfImage = reader.u32(optionalOffset + 56);
+ const sizeOfHeaders = reader.u32(optionalOffset + 60);
+ const checksum = reader.u32(optionalOffset + 64);
+ const subsystemValue = reader.u16(optionalOffset + 68);
+ const dllCharacteristicsValue = reader.u16(optionalOffset + 70);
+ const directoryCountOffset = optionalOffset + (isPe32Plus ? 108 : 92);
+ const dataDirectoryOffset = optionalOffset + (isPe32Plus ? 112 : 96);
+ const dataDirectories = readDataDirectories(reader, dataDirectoryOffset, reader.u32(directoryCountOffset));
+ const sections = readSections(reader, optionalOffset + optionalHeaderSize, sectionCount);
+
+ return {
+ fileName,
+ fileSize: buffer.byteLength,
+ dosHeader: {
+ peHeaderOffset,
+ stubText: reader.ascii(0x40, Math.max(0, peHeaderOffset - 0x40)),
+ },
+ coffHeader: {
+ machine: MACHINE_NAMES.get(machineValue) ?? `Unknown (${hex(machineValue)})`,
+ machineValue,
+ sectionCount,
+ timestamp: readTimestamp(timestamp),
+ characteristics: flagsFrom(coffCharacteristics, COFF_CHARACTERISTICS),
+ },
+ optionalHeader: {
+ format: isPe32Plus ? "PE32+" : "PE32",
+ entryPoint,
+ imageBase,
+ subsystem: SUBSYSTEM_NAMES.get(subsystemValue) ?? `Unknown (${subsystemValue})`,
+ sectionAlignment,
+ fileAlignment,
+ sizeOfImage,
+ checksum,
+ dllCharacteristics: flagsFrom(dllCharacteristicsValue, DLL_CHARACTERISTICS),
+ },
+ dataDirectories,
+ sections,
+ exports: readExports(reader, dataDirectories, sections, sizeOfHeaders),
+ imports: readImports(reader, dataDirectories, sections, sizeOfHeaders, isPe32Plus),
+ warnings: reader.warnings,
+ };
+}
diff --git a/src/i18n/locales/en-tail.ts b/src/i18n/locales/en-tail.ts
index 5c32457ec..215c0be65 100644
--- a/src/i18n/locales/en-tail.ts
+++ b/src/i18n/locales/en-tail.ts
@@ -140,6 +140,53 @@ export const enTail: TailTranslationDictionarySections = {
officeWasmPocShowingFirstRows: "Showing the first 40 rows and 18 columns.",
officeWasmPocShapes: "shapes",
officeWasmPocTextRuns: "text blocks",
+ dllViewerTitle: "DLL Viewer",
+ dllViewerDescription: "Upload a Windows DLL, EXE, SYS or OCX file to inspect its PE headers, sections, imports, and exports in-browser.",
+ dllViewerSelectFile: "Select binary",
+ dllViewerStatus: "Status",
+ dllViewerStatusIdle: "Idle",
+ dllViewerStatusParsing: "Parsing",
+ dllViewerStatusReady: "Ready",
+ dllViewerStatusError: "Error",
+ dllViewerError: "Parse error",
+ dllViewerNoResult: "No DLL parsed yet.",
+ dllViewerSummary: "Summary",
+ dllViewerFile: "File",
+ dllViewerFormat: "Format",
+ dllViewerMachine: "Machine",
+ dllViewerSubsystem: "Subsystem",
+ dllViewerEntryPoint: "Entry point",
+ dllViewerImageBase: "Image base",
+ dllViewerTimestamp: "Timestamp",
+ dllViewerPeOffset: "PE header offset",
+ dllViewerSectionAlignment: "Section alignment",
+ dllViewerFileAlignment: "File alignment",
+ dllViewerSizeOfImage: "Image size",
+ dllViewerChecksum: "Checksum",
+ dllViewerCharacteristics: "Characteristics",
+ dllViewerExports: "Exports",
+ dllViewerImports: "Imports",
+ dllViewerSections: "Sections",
+ dllViewerDataDirectories: "Data directories",
+ dllViewerWarnings: "Warnings",
+ dllViewerRawJson: "Raw JSON",
+ dllViewerDllName: "DLL name",
+ dllViewerOrdinalBase: "Ordinal base",
+ dllViewerFunctionCount: "Function count",
+ dllViewerNoExports: "No export table found.",
+ dllViewerNoImports: "No import table found.",
+ dllViewerShowingFirstExports: "Showing the first {count} exports.",
+ dllViewerShowingFirstImports: "Showing the first {count} imports.",
+ dllViewerFunctions: "{count} functions",
+ dllViewerName: "Name",
+ dllViewerVirtualAddress: "Virtual address",
+ dllViewerVirtualSize: "Virtual size",
+ dllViewerRawPointer: "Raw pointer",
+ dllViewerRawSize: "Raw size",
+ dllViewerRva: "RVA",
+ dllViewerSize: "Size",
+ dllViewerOrdinal: "Ordinal",
+ dllViewerForwardedTo: "Forwarded to",
},
// UI components
diff --git a/src/i18n/locales/zh-tail.ts b/src/i18n/locales/zh-tail.ts
index 13e4eddd1..1879cc08a 100644
--- a/src/i18n/locales/zh-tail.ts
+++ b/src/i18n/locales/zh-tail.ts
@@ -137,6 +137,53 @@ export const zhTail: TailTranslationDictionarySections = {
officeWasmPocShowingFirstRows: "显示前 40 行、18 列。",
officeWasmPocShapes: "个形状",
officeWasmPocTextRuns: "个文本块",
+ dllViewerTitle: "DLL 查看器",
+ dllViewerDescription: "上传 Windows DLL、EXE、SYS 或 OCX 文件,在浏览器内查看 PE 头、区段、导入表和导出表。",
+ dllViewerSelectFile: "选择二进制文件",
+ dllViewerStatus: "状态",
+ dllViewerStatusIdle: "空闲",
+ dllViewerStatusParsing: "解析中",
+ dllViewerStatusReady: "已完成",
+ dllViewerStatusError: "错误",
+ dllViewerError: "解析错误",
+ dllViewerNoResult: "尚未解析 DLL。",
+ dllViewerSummary: "摘要",
+ dllViewerFile: "文件",
+ dllViewerFormat: "格式",
+ dllViewerMachine: "机器类型",
+ dllViewerSubsystem: "子系统",
+ dllViewerEntryPoint: "入口点",
+ dllViewerImageBase: "镜像基址",
+ dllViewerTimestamp: "时间戳",
+ dllViewerPeOffset: "PE 头偏移",
+ dllViewerSectionAlignment: "区段对齐",
+ dllViewerFileAlignment: "文件对齐",
+ dllViewerSizeOfImage: "镜像大小",
+ dllViewerChecksum: "校验和",
+ dllViewerCharacteristics: "特征",
+ dllViewerExports: "导出表",
+ dllViewerImports: "导入表",
+ dllViewerSections: "区段",
+ dllViewerDataDirectories: "数据目录",
+ dllViewerWarnings: "警告",
+ dllViewerRawJson: "原始 JSON",
+ dllViewerDllName: "DLL 名称",
+ dllViewerOrdinalBase: "序号基数",
+ dllViewerFunctionCount: "函数数量",
+ dllViewerNoExports: "未找到导出表。",
+ dllViewerNoImports: "未找到导入表。",
+ dllViewerShowingFirstExports: "显示前 {count} 个导出。",
+ dllViewerShowingFirstImports: "显示前 {count} 个导入。",
+ dllViewerFunctions: "{count} 个函数",
+ dllViewerName: "名称",
+ dllViewerVirtualAddress: "虚拟地址",
+ dllViewerVirtualSize: "虚拟大小",
+ dllViewerRawPointer: "原始偏移",
+ dllViewerRawSize: "原始大小",
+ dllViewerRva: "RVA",
+ dllViewerSize: "大小",
+ dllViewerOrdinal: "序号",
+ dllViewerForwardedTo: "转发到",
},
ui: {
diff --git a/src/i18n/types-tail.ts b/src/i18n/types-tail.ts
index b0992fc00..986fd8f36 100644
--- a/src/i18n/types-tail.ts
+++ b/src/i18n/types-tail.ts
@@ -138,6 +138,53 @@ export interface TailTranslationDictionarySections {
officeWasmPocShowingFirstRows: string;
officeWasmPocShapes: string;
officeWasmPocTextRuns: string;
+ dllViewerTitle: string;
+ dllViewerDescription: string;
+ dllViewerSelectFile: string;
+ dllViewerStatus: string;
+ dllViewerStatusIdle: string;
+ dllViewerStatusParsing: string;
+ dllViewerStatusReady: string;
+ dllViewerStatusError: string;
+ dllViewerError: string;
+ dllViewerNoResult: string;
+ dllViewerSummary: string;
+ dllViewerFile: string;
+ dllViewerFormat: string;
+ dllViewerMachine: string;
+ dllViewerSubsystem: string;
+ dllViewerEntryPoint: string;
+ dllViewerImageBase: string;
+ dllViewerTimestamp: string;
+ dllViewerPeOffset: string;
+ dllViewerSectionAlignment: string;
+ dllViewerFileAlignment: string;
+ dllViewerSizeOfImage: string;
+ dllViewerChecksum: string;
+ dllViewerCharacteristics: string;
+ dllViewerExports: string;
+ dllViewerImports: string;
+ dllViewerSections: string;
+ dllViewerDataDirectories: string;
+ dllViewerWarnings: string;
+ dllViewerRawJson: string;
+ dllViewerDllName: string;
+ dllViewerOrdinalBase: string;
+ dllViewerFunctionCount: string;
+ dllViewerNoExports: string;
+ dllViewerNoImports: string;
+ dllViewerShowingFirstExports: string;
+ dllViewerShowingFirstImports: string;
+ dllViewerFunctions: string;
+ dllViewerName: string;
+ dllViewerVirtualAddress: string;
+ dllViewerVirtualSize: string;
+ dllViewerRawPointer: string;
+ dllViewerRawSize: string;
+ dllViewerRva: string;
+ dllViewerSize: string;
+ dllViewerOrdinal: string;
+ dllViewerForwardedTo: string;
};
// UI components