Refactor based on objdiff v3.0.0-alpha.1

This commit is contained in:
Luke Street
2025-03-02 22:55:54 -07:00
parent 0791f6a148
commit 4050ec4d95
33 changed files with 3439 additions and 6399 deletions
+2 -1
View File
@@ -27,7 +27,8 @@
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": false
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
+2 -2
View File
@@ -10,13 +10,13 @@ Open the project in Visual Studio Code.
- Install dependencies:
```bash
npm install
pnpm install
```
- Start dev server using `Ctrl+Shift+B` or by running:
```bash
npm run watch
pnpm watch
```
- Run the extension in debug mode using `F5`.
+6
View File
@@ -27,6 +27,12 @@
"recommended": true,
"a11y": {
"all": false
},
"suspicious": {
"noExplicitAny": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
}
-3916
View File
File diff suppressed because it is too large Load Diff
+39 -18
View File
@@ -1,9 +1,9 @@
{
"name": "objdiff",
"displayName": "objdiff",
"description": "objdiff",
"description": "A local diffing tool for decompilation projects",
"publisher": "decomp-dev",
"version": "0.0.2",
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://github.com/encounter/objdiff-code"
@@ -20,13 +20,15 @@
"@protobuf-ts/runtime": "^2.9.4",
"@vscode/codicons": "^0.0.36",
"clsx": "^2.1.1",
"core-js": "^3.39.0",
"memoize-one": "^6.0.0",
"objdiff-wasm": "^3.0.0-alpha.1",
"picomatch": "^4.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-tooltip": "^5.28.0",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"shescape": "^2.1.1",
"zustand": "^5.0.2"
},
"devDependencies": {
@@ -35,7 +37,6 @@
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-type-check": "^1.1.0",
"@rsbuild/plugin-typed-css-modules": "^1.0.2",
"@types/core-js": "^2.5.8",
"@types/node": "^22.10.2",
"@types/picomatch": "^3.0.1",
"@types/react": "^18.3.1",
@@ -95,23 +96,31 @@
}
],
"configuration": [
{
"title": "Extension",
"properties": {
"objdiff.binaryPath": {
"type": "string",
"description": "Path to the objdiff-cli binary",
"scope": "machine"
}
}
},
{
"title": "General",
"properties": {
"objdiff.relaxRelocDiffs": {
"type": "boolean",
"description": "Ignores differences in relocation targets. (Address, name, etc)",
"default": false
"objdiff.functionRelocDiffs": {
"type": "string",
"description": "How relocation targets will be diffed in the function view.",
"default": "name_address",
"enum": [
"none",
"name_address",
"data_value",
"all"
],
"enumItemLabels": [
"None",
"Name or address",
"Data value",
"Name or address, data value"
],
"enumDescriptions": [
null,
null,
null,
null
]
},
"objdiff.spaceBetweenArgs": {
"type": "boolean",
@@ -122,6 +131,11 @@
"type": "boolean",
"description": "Combines data sections with equal names.",
"default": false
},
"objdiff.combineTextSections": {
"type": "boolean",
"description": "Combines all text sections into one.",
"default": false
}
}
},
@@ -324,5 +338,12 @@
"contents": "Loading..."
}
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"core-js",
"esbuild"
]
}
}
+2388
View File
File diff suppressed because it is too large Load Diff
+18 -6
View File
@@ -1,5 +1,6 @@
import fs from 'node:fs';
import type { ServerResponse } from 'node:http';
import path from 'node:path';
import { type RequestHandler, defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
@@ -92,15 +93,27 @@ export default defineConfig({
},
});
const PROJECT_ROOT = '../prime';
// Mock API middleware for development.
const apiMiddleware: RequestHandler = (req, res, next) => {
if (req.method === 'GET' && req.url === '/api/project') {
return sendFile(res, '../prime/objdiff.json', 'application/json');
if (!req.url || !req.headers.host || req.method !== 'GET') {
return next();
}
if (req.method === 'GET' && req.url === '/api/diff') {
return sendFile(res, '../prime/diff.binpb', 'application/octet-stream');
const url = new URL(req.url, `http://${req.headers.host}`);
if (!url) {
return next();
}
next();
if (url.pathname === '/api/get') {
const file = url.searchParams.get('path');
if (file) {
const filepath = path.join(PROJECT_ROOT, file);
if (filepath.startsWith(PROJECT_ROOT)) {
return sendFile(res, filepath, 'application/octet-stream');
}
}
}
return next();
};
// Send a file as a response.
@@ -115,7 +128,6 @@ function sendFile(
throw err;
}
let statusCode = 500;
// biome-ignore lint/suspicious/noExplicitAny: Node error
if ((err as any).code === 'ENOENT') {
statusCode = 404;
}
-230
View File
@@ -1,230 +0,0 @@
{
"properties": [
{
"id": "relaxRelocDiffs",
"type": "boolean",
"default": false,
"name": "Relax relocation diffs",
"description": "Ignores differences in relocation targets. (Address, name, etc)"
},
{
"id": "spaceBetweenArgs",
"type": "boolean",
"default": true,
"name": "Space between args",
"description": "Adds a space between arguments in the diff output."
},
{
"id": "combineDataSections",
"type": "boolean",
"default": false,
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "arm.archVersion",
"type": "choice",
"default": "auto",
"name": "Architecture version",
"description": "ARM architecture version to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
"default": false,
"name": "Unified syntax",
"description": "Disassemble as unified assembly language (UAL)."
},
{
"id": "arm.avRegisters",
"type": "boolean",
"default": false,
"name": "Use A/V registers",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
},
{
"id": "arm.r9Usage",
"type": "choice",
"default": "generalPurpose",
"name": "Display R9 as",
"items": [
{
"value": "generalPurpose",
"name": "R9 or V6",
"description": "Use R9 as a general-purpose register."
},
{
"value": "sb",
"name": "SB (static base)",
"description": "Used for position-independent data (PID)."
},
{
"value": "tr",
"name": "TR (TLS register)",
"description": "Used for thread-local storage."
}
]
},
{
"id": "arm.slUsage",
"type": "boolean",
"default": false,
"name": "Display R10 as SL",
"description": "Used for explicit stack limits."
},
{
"id": "arm.fpUsage",
"type": "boolean",
"default": false,
"name": "Display R11 as FP",
"description": "Used for frame pointers."
},
{
"id": "arm.ipUsage",
"type": "boolean",
"default": false,
"name": "Display R12 as IP",
"description": "Used for interworking and long branches."
},
{
"id": "mips.abi",
"type": "choice",
"default": "auto",
"name": "ABI",
"description": "MIPS ABI to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "o32",
"name": "O32"
},
{
"value": "n32",
"name": "N32"
},
{
"value": "n64",
"name": "N64"
}
]
},
{
"id": "mips.instrCategory",
"type": "choice",
"default": "auto",
"name": "Instruction category",
"description": "MIPS instruction category to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "cpu",
"name": "CPU"
},
{
"value": "rsp",
"name": "RSP (N64)"
},
{
"value": "r3000gte",
"name": "R3000 GTE (PS1)"
},
{
"value": "r4000allegrex",
"name": "R4000 ALLEGREX (PSP)"
},
{
"value": "r5900",
"name": "R5900 EE (PS2)"
}
]
},
{
"id": "x86.formatter",
"type": "choice",
"default": "intel",
"name": "Format",
"description": "x86 disassembly syntax.",
"items": [
{
"value": "intel",
"name": "Intel"
},
{
"value": "gas",
"name": "AT&T"
},
{
"value": "nasm",
"name": "NASM"
},
{
"value": "masm",
"name": "MASM"
}
]
}
],
"groups": [
{
"id": "general",
"name": "General",
"properties": [
"relaxRelocDiffs",
"spaceBetweenArgs",
"combineDataSections"
]
},
{
"id": "arm",
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
"arm.slUsage",
"arm.fpUsage",
"arm.ipUsage"
]
},
{
"id": "mips",
"name": "MIPS",
"properties": [
"mips.abi",
"mips.instrCategory"
]
},
{
"id": "x86",
"name": "x86",
"properties": [
"x86.formatter"
]
}
]
}
+1 -1
View File
@@ -257,7 +257,7 @@ export type ConfigSchema = {
groups: ConfigGroup[];
};
import configSchema from './config-schema.json';
import configSchema from 'objdiff-wasm/dist/config-schema.json';
export const CONFIG_SCHEMA = configSchema as ConfigSchema;
export function getPropertyValue(
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -5,12 +5,27 @@ import type {
Unit,
} from './config';
export type WebviewProps = {
extensionVersion: string;
resourceRoot: string;
};
export type BuildStatus = {
success: boolean;
cmdline: string;
stdout: string;
stderr: string;
};
export type StateMessage = {
type: 'state';
buildRunning?: boolean;
configProperties?: ConfigProperties;
currentUnit?: Unit | null;
data?: ArrayBuffer | null;
leftStatus?: BuildStatus | null;
rightStatus?: BuildStatus | null;
leftObject?: ArrayBuffer | null;
rightObject?: ArrayBuffer | null;
projectConfig?: ProjectConfig | null;
};
+23 -7
View File
@@ -1,6 +1,10 @@
import * as vscode from 'vscode';
import type { Unit } from '../shared/config';
import type { InboundMessage, OutboundMessage } from '../shared/messages';
import type {
InboundMessage,
OutboundMessage,
WebviewProps,
} from '../shared/messages';
import { Workspace } from './workspace';
export function activate(context: vscode.ExtensionContext) {
@@ -42,7 +46,6 @@ export function activate(context: vscode.ExtensionContext) {
workspace = new Workspace(
chan,
vscode.workspace.workspaceFolders[0],
storageUri,
deferredCurrentUnit,
);
workspace.onDidChangeProjectConfig(
@@ -61,7 +64,8 @@ export function activate(context: vscode.ExtensionContext) {
if (!unit) {
sendMessage({
type: 'state',
data: null,
leftObject: null,
rightObject: null,
currentUnit: null,
});
}
@@ -73,7 +77,8 @@ export function activate(context: vscode.ExtensionContext) {
(data) => {
sendMessage({
type: 'state',
data: data?.buffer || null,
leftObject: data?.leftObject?.buffer || null,
rightObject: data?.rightObject?.buffer || null,
currentUnit: workspace?.currentUnit || null,
});
},
@@ -244,8 +249,17 @@ export function activate(context: vscode.ExtensionContext) {
// For development, allow static assets (production will be inlined)
html = html.replaceAll(/"\/static\/(.*?)"/g, (_, p) => {
const assetUri = vscode.Uri.joinPath(webviewRoot, 'static', p);
return `"${view.webview.asWebviewUri(assetUri)}"`;
return JSON.stringify(view.webview.asWebviewUri(assetUri).toString());
});
// Inject extension information into the webview
const props: WebviewProps = {
extensionVersion: context.extension.packageJSON.version,
resourceRoot: `${view.webview.asWebviewUri(webviewRoot).toString()}/`,
};
html = html.replace(
'<head>',
`<head><script>window.webviewProps=${JSON.stringify(props)}</script>`,
);
view.webview.options = {
localResourceRoots: [context.extensionUri],
enableScripts: true,
@@ -257,7 +271,8 @@ export function activate(context: vscode.ExtensionContext) {
buildRunning: workspace?.buildRunning || false,
configProperties: workspace?.configProperties || {},
currentUnit: workspace?.currentUnit || null,
data: workspace?.cachedData?.buffer || null,
leftObject: workspace?.cachedData?.leftObject?.buffer || null,
rightObject: workspace?.cachedData?.rightObject?.buffer || null,
projectConfig: workspace?.projectConfig || null,
} as InboundMessage);
view.webview.onDidReceiveMessage(
@@ -269,7 +284,8 @@ export function activate(context: vscode.ExtensionContext) {
buildRunning: workspace?.buildRunning || false,
configProperties: workspace?.configProperties || {},
currentUnit: workspace?.currentUnit || null,
data: workspace?.cachedData?.buffer || null,
leftObject: workspace?.cachedData?.leftObject?.buffer || null,
rightObject: workspace?.cachedData?.rightObject?.buffer || null,
projectConfig: workspace?.projectConfig || null,
} as InboundMessage);
chan.info('Webview ready');
+129
View File
@@ -0,0 +1,129 @@
import * as vscode from 'vscode';
export type Task = {
type: string;
command: string;
args: string[];
};
export interface TaskExecutor {
run(task: Task): Promise<TaskResult>;
}
export interface TaskResult {
code: number;
stdout?: string;
stderr?: string;
startTime: number;
}
interface RunningTask {
execution: vscode.TaskExecution;
resolve: (result: TaskResult) => void;
reject: (reason?: any) => void;
startTime: number;
}
export class VscodeTaskExecutor implements TaskExecutor, vscode.Disposable {
private runningTasks: RunningTask[] = [];
private disposables: vscode.Disposable[] = [];
private taskId = 0;
constructor(private workspaceFolder: vscode.WorkspaceFolder) {
vscode.tasks.onDidEndTaskProcess(
(e) => {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
const index = this.runningTasks.findIndex(
(task) =>
task.execution.task.definition.id ===
e.execution.task.definition.id,
);
if (index !== -1) {
const task = this.runningTasks[index];
this.runningTasks.splice(index, 1);
task.resolve({
code: e.exitCode ?? -1,
startTime: task.startTime,
});
}
},
null,
this.disposables,
);
}
async run(task: Task): Promise<TaskResult> {
const { type, command, args } = task;
const startTime = performance.now();
const vscodeTask = new vscode.Task(
{
type: 'objdiff',
taskType: type,
id: this.taskId++,
},
this.workspaceFolder,
'objdiff',
'objdiff',
new vscode.ShellExecution(command, args),
);
vscodeTask.presentationOptions.reveal = vscode.TaskRevealKind.Silent;
const execution = await vscode.tasks.executeTask(vscodeTask);
return new Promise((resolve, reject) => {
this.runningTasks.push({
execution,
resolve,
reject,
startTime,
});
});
}
dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
}
}
export class DirectTaskExecutor implements TaskExecutor {
constructor(private workspaceFolder: vscode.WorkspaceFolder) {}
async run(task: Task): Promise<TaskResult> {
const { command, args } = task;
const startTime = performance.now();
return new Promise((resolve, reject) => {
const { execFile } = require('node:child_process');
execFile(
command,
args,
{
cwd: this.workspaceFolder.uri.fsPath,
encoding: 'utf8',
},
(error: any, stdout: string, stderr: string) => {
if (error) {
if (stdout || stderr) {
resolve({
code: error.code ?? -1,
stdout,
stderr,
startTime,
});
} else {
reject(error);
}
} else {
resolve({
code: 0,
stdout,
stderr,
startTime,
});
}
},
);
});
}
}
+101 -159
View File
@@ -1,4 +1,5 @@
import * as picomatch from 'picomatch';
import { Shescape } from 'shescape';
import * as vscode from 'vscode';
import {
CONFIG_FILENAME,
@@ -6,13 +7,23 @@ import {
type ConfigPropertyValue,
type ProjectConfig,
type Unit,
getModifiedConfigProperties,
resolveProjectConfig,
} from '../shared/config';
import {
DirectTaskExecutor,
type Task,
type TaskResult,
VscodeTaskExecutor,
} from './util';
export type BuildData = {
leftObject: Uint8Array | null;
rightObject: Uint8Array | null;
};
export class Workspace extends vscode.Disposable {
public buildRunning = false;
public cachedData: Uint8Array | null = null;
public cachedData: BuildData | null = null;
public configProperties: ConfigProperties = {};
public currentUnit?: Unit;
public projectConfig?: ProjectConfig;
@@ -22,19 +33,22 @@ export class Workspace extends vscode.Disposable {
public onDidChangeConfigProperties: vscode.Event<ConfigProperties>;
public onDidChangeCurrentUnit: vscode.Event<Unit | undefined>;
public onDidChangeBuildRunning: vscode.Event<boolean>;
public onDidChangeData: vscode.Event<Uint8Array | null>;
public onDidChangeData: vscode.Event<BuildData | null>;
private subscriptions: vscode.Disposable[] = [];
private wwSubscriptions: vscode.Disposable[] = [];
private pathMatcher?: picomatch.Matcher;
private buildTaskExecutor: VscodeTaskExecutor;
private directTaskExecutor: DirectTaskExecutor;
private didChangeBuildRunningEmitter = new vscode.EventEmitter<boolean>();
private didChangeConfigPropertiesEmitter =
new vscode.EventEmitter<ConfigProperties>();
private didChangeCurrentUnitEmitter = new vscode.EventEmitter<
Unit | undefined
>();
private didChangeDataEmitter = new vscode.EventEmitter<Uint8Array | null>();
private didChangeDataEmitter = new vscode.EventEmitter<BuildData | null>();
private didChangeProjectConfigEmitter = new vscode.EventEmitter<
ProjectConfig | undefined
>();
@@ -42,7 +56,6 @@ export class Workspace extends vscode.Disposable {
constructor(
public readonly chan: vscode.LogOutputChannel,
public readonly workspaceFolder: vscode.WorkspaceFolder,
private readonly storageUri: vscode.Uri,
public deferredCurrentUnit?: string,
) {
super(() => {
@@ -55,11 +68,9 @@ export class Workspace extends vscode.Disposable {
this.onDidChangeData = this.didChangeDataEmitter.event;
this.onDidChangeProjectConfig = this.didChangeProjectConfigEmitter.event;
vscode.tasks.onDidEndTaskProcess(
this.onDidEndTaskProcess,
this,
this.subscriptions,
);
this.buildTaskExecutor = new VscodeTaskExecutor(workspaceFolder);
this.directTaskExecutor = new DirectTaskExecutor(workspaceFolder);
vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration,
this,
@@ -90,7 +101,6 @@ export class Workspace extends vscode.Disposable {
chan.info(`Initialized workspace: ${this.workspaceFolder.uri.toString()}`);
}
// biome-ignore lint/suspicious/noExplicitAny: pass through message args
showError(message: string, ...args: any[]) {
this.chan.error(message, ...args);
vscode.window
@@ -104,7 +114,6 @@ export class Workspace extends vscode.Disposable {
});
}
// biome-ignore lint/suspicious/noExplicitAny: pass through message args
showWarning(message: string, ...args: any[]) {
this.chan.warn(message, ...args);
vscode.window.showWarningMessage(`objdiff: ${message}`);
@@ -262,7 +271,7 @@ export class Workspace extends vscode.Disposable {
return true;
}
tryBuild() {
async tryBuild() {
if (this.buildRunning) {
return;
}
@@ -283,145 +292,101 @@ export class Workspace extends vscode.Disposable {
const basePath =
this.currentUnit.base_path &&
vscode.Uri.joinPath(this.workspaceFolder.uri, this.currentUnit.base_path);
this.chan.info('Diffing', targetPath?.toString(), basePath?.toString());
if (!targetPath && !basePath) {
this.showWarning('No target or base path');
return;
}
const buildCmd = this.projectConfig.custom_make || 'make';
const buildArgs = this.projectConfig.custom_args || [];
const hash = cyrb53(this.workspaceFolder.uri.toString());
const outputUri = vscode.Uri.joinPath(
this.storageUri,
`diff_${hash}.binpb`,
);
const args = [];
if (this.currentUnit.target_path && this.projectConfig.build_target) {
if (args.length) {
args.push('&&');
}
args.push(buildCmd, ...buildArgs, this.currentUnit.target_path);
}
if (
this.currentUnit.base_path &&
(this.projectConfig.build_base ||
this.projectConfig.build_base === undefined)
) {
if (args.length) {
args.push('&&');
}
args.push(buildCmd, ...buildArgs, this.currentUnit.base_path);
}
if (args.length) {
args.push('&&');
}
const binaryPath = this.configProperties.binaryPath as string | undefined;
if (!binaryPath) {
vscode.window
.showWarningMessage('objdiff.binaryPath not set', {
title: 'Open settings',
})
.then((item) => {
if (item) {
vscode.commands.executeCommand(
'workbench.action.openSettings',
'objdiff.binaryPath',
);
}
});
return;
}
args.push(binaryPath, 'diff');
if (targetPath) {
args.push('-1', targetPath.fsPath);
}
if (basePath) {
args.push('-2', basePath.fsPath);
}
args.push('--format', 'proto', '-o', outputUri.fsPath);
const configProperties = getModifiedConfigProperties(this.configProperties);
for (const key in configProperties) {
args.push('-c', `${key}=${configProperties[key]}`);
}
const startTime = performance.now();
const task = new vscode.Task(
{
type: 'objdiff',
taskType: 'build',
startTime,
},
this.workspaceFolder,
'objdiff',
'objdiff',
new vscode.ShellExecution(args[0], args.slice(1)),
);
task.presentationOptions.reveal = vscode.TaskRevealKind.Silent;
this.buildRunning = true;
this.didChangeBuildRunningEmitter.fire(true);
vscode.tasks.executeTask(task).then(
({ task, terminate: _ }) => {
const curTime = performance.now();
this.chan.info(
'Diff task started in',
curTime - task.definition.startTime,
);
},
(reason) => {
this.showError('Failed to start diff task', reason);
},
);
}
private async onDidEndTaskProcess(e: vscode.TaskProcessEndEvent) {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
try {
const endTime = performance.now();
this.chan.info(
'Task ended',
e.exitCode,
endTime - e.execution.task.definition.startTime,
);
const proc = e.execution.task.execution as vscode.ProcessExecution;
const outputFile = proc.args[proc.args.indexOf('-o') + 1];
const outputUri = vscode.Uri.file(outputFile);
if (e.exitCode === 0) {
const data = await vscode.workspace.fs.readFile(outputUri);
this.chan.info(
'Read output file',
outputFile,
'with size',
data.byteLength,
);
this.cachedData = data;
this.didChangeDataEmitter.fire(data);
} else {
this.showError(`Build failed with code ${e.exitCode}`);
}
let exists = false;
try {
const stat = await vscode.workspace.fs.stat(outputUri);
exists = stat.type === vscode.FileType.File;
} catch (reason) {
if (
reason instanceof vscode.FileSystemError &&
reason.code !== 'FileNotFound'
) {
throw reason;
const buildCmd = this.projectConfig.custom_make || 'make';
const buildArgs = this.projectConfig.custom_args || [];
// Build target object
if (this.currentUnit.target_path && this.projectConfig.build_target) {
const result = await this.runTask({
type: 'objdiff',
command: buildCmd,
args: [...buildArgs, this.currentUnit.target_path],
});
if (result.code !== 0) {
this.showError(`Target build failed with code ${result.code}`);
return;
}
}
if (exists) {
await vscode.workspace.fs.delete(outputUri);
// Build base object
if (
this.currentUnit.base_path &&
(this.projectConfig.build_base ||
this.projectConfig.build_base === undefined)
) {
const result = await this.runTask({
type: 'objdiff',
command: buildCmd,
args: [...buildArgs, this.currentUnit.base_path],
});
if (result.code !== 0) {
this.showError(`Base build failed with code ${result.code}`);
return;
}
}
// Read target object
let targetData: Uint8Array | null = null;
if (targetPath) {
targetData = await vscode.workspace.fs.readFile(targetPath);
}
// Read base object
let baseData: Uint8Array | null = null;
if (basePath) {
baseData = await vscode.workspace.fs.readFile(basePath);
}
this.cachedData = { leftObject: targetData, rightObject: baseData };
this.didChangeDataEmitter.fire(this.cachedData);
} catch (reason) {
this.showError('Failed to process build result', reason);
this.showError('Failed to execute build', reason);
} finally {
this.buildRunning = false;
this.didChangeBuildRunningEmitter.fire(false);
}
}
private async runTask(task: Task): Promise<TaskResult> {
this.logTask(task);
const result = await this.directTaskExecutor.run(task);
this.logTaskResult(task, result);
return result;
}
private logTask(task: Task) {
const command = new Shescape({ flagProtection: false, shell: true })
.quoteAll([task.command, ...task.args])
.join(' ');
this.chan.info('Executing', command);
}
private logTaskResult(task: Task, result: TaskResult) {
if (result.code === 0) {
const endTime = performance.now();
const elapsed = (endTime - result.startTime).toFixed(0);
this.chan.info(`Command ${task.command} succeeded in ${elapsed}ms`);
return;
}
this.chan.error(
[
`Command ${task.command} failed with code ${result.code}`,
result.stdout,
result.stderr,
]
.filter(Boolean)
.join('\n'),
);
}
private async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) {
if (!e.affectsConfiguration('objdiff')) {
return;
@@ -455,6 +420,7 @@ export class Workspace extends vscode.Disposable {
private disposeImpl() {
this.chan.info('Disposing workspace');
this.buildTaskExecutor.dispose();
this.projectConfigWatcher.dispose();
for (const sub of this.wwSubscriptions) {
sub.dispose();
@@ -468,27 +434,3 @@ export class Workspace extends vscode.Disposable {
this.subscriptions = [];
}
}
/**
* cyrb53 (c) 2018 bryc (github.com/bryc)
* License: Public domain (or MIT if needed). Attribution appreciated.
* A fast and simple 53-bit string hash function with decent collision resistance.
* Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
*/
function cyrb53(str: string, seed = 0) {
let h1 = 0xdeadbeef ^ seed;
let h2 = 0x41c6ce57 ^ seed;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return (
(h2 >>> 0).toString(16).padStart(8, '0') +
(h1 >>> 0).toString(16).padStart(8, '0')
);
}
+4 -4
View File
@@ -3,12 +3,13 @@ import { CONFIG_SCHEMA } from './shared/config';
const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));
const extensionConfig = packageJson.contributes.configuration.find(
// biome-ignore lint/suspicious/noExplicitAny: ignore
(config: any) => config.title === 'Extension',
);
const categories = [extensionConfig];
const categories: any[] = [];
if (extensionConfig) {
categories.push(extensionConfig);
}
for (const group of CONFIG_SCHEMA.groups) {
// biome-ignore lint/suspicious/noExplicitAny: ignore
const category: any = {
title: group.name,
properties: {},
@@ -18,7 +19,6 @@ for (const group of CONFIG_SCHEMA.groups) {
if (!property) {
continue;
}
// biome-ignore lint/suspicious/noExplicitAny: ignore
const config: any = {
type: property.type === 'boolean' ? 'boolean' : 'string',
description: property.description,
+7
View File
@@ -26,6 +26,7 @@
--color-red: #f85149;
--color-blue: #add8e6;
--color-muted: var(--vscode-disabledForeground, rgba(204, 204, 204, 0.5));
--color-bright: light-dark(black, white);
--panel-background: var(--vscode-panel-background, #181818);
--panel-separator: var(--vscode-menu-separatorBackground, #454545);
@@ -71,6 +72,12 @@
--checkbox-border-color: var(--vscode-settings-checkboxBorder, #3c3c3c);
color-scheme: light dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
+24 -28
View File
@@ -1,12 +1,7 @@
import './App.css';
import type { diff, display } from 'objdiff-wasm';
import { useShallow } from 'zustand/react/shallow';
import { SectionKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
ObjectDiff,
SymbolDiff,
} from '../shared/gen/diff_pb';
import FunctionView from './FunctionView';
import SettingsView from './SettingsView';
import SymbolsView from './SymbolsView';
@@ -15,39 +10,38 @@ import { useAppStore, useExtensionStore } from './state';
import type { SymbolRefByName } from './state';
const findSymbol = (
obj: ObjectDiff | undefined,
obj: diff.ObjectDiff | undefined,
symbolRef: SymbolRefByName | null,
): SymbolDiff | null => {
): display.SectionDisplaySymbol | null => {
if (!obj || !symbolRef) {
return null;
}
for (const section of obj.sections) {
if (section.name === symbolRef.section_name) {
if (section.kind === SectionKind.SECTION_TEXT) {
for (const diff of section.symbols) {
const symbol = diff.symbol as DiffSymbol;
if (symbol.name === symbolRef.symbol_name) {
return diff;
}
}
}
}
const idx = obj.findSymbol(
symbolRef.symbolName,
symbolRef.sectionName ?? undefined,
);
if (idx !== undefined) {
return {
symbol: idx,
isMappingSymbol: false,
};
}
return null;
};
const App = () => {
const { buildRunning, diff, config, ready } = useExtensionStore(
const { buildRunning, result, config, ready } = useExtensionStore(
useShallow((state) => ({
buildRunning: state.buildRunning,
diff: state.diff,
result: state.result,
config: state.projectConfig,
ready: state.ready,
})),
);
const { selectedSymbolRef, currentView } = useAppStore(
const { leftSymbolRef, rightSymbolRef, currentView } = useAppStore(
useShallow((state) => ({
selectedSymbolRef: state.selectedSymbol,
leftSymbolRef: state.leftSymbol,
rightSymbolRef: state.rightSymbol,
currentView: state.currentView,
})),
);
@@ -57,13 +51,15 @@ const App = () => {
return <div className="loading-root" />;
}
if (diff) {
const leftSymbol = findSymbol(diff.left, selectedSymbolRef);
const rightSymbol = findSymbol(diff.right, selectedSymbolRef);
if (result) {
const leftSymbol = findSymbol(result.left, leftSymbolRef);
const rightSymbol = findSymbol(result.right, rightSymbolRef);
if (leftSymbol || rightSymbol) {
return <FunctionView left={leftSymbol} right={rightSymbol} />;
return (
<FunctionView diff={result} left={leftSymbol} right={rightSymbol} />
);
}
return <SymbolsView diff={diff} />;
return <SymbolsView diff={result} />;
}
switch (currentView) {
+13 -11
View File
@@ -27,23 +27,25 @@
color: white;
background-color: #aa8b00;
}
.line-number {
color: var(--line-number-foreground);
}
.diff_any {
.diff-any {
background-color: rgba(255, 255, 255, 0.02);
}
.diff_change {
.segment-dim {
color: var(--line-number-foreground);
}
.segment-bright {
color: var(--color-bright);
}
.segment-replace {
color: var(--color-blue);
}
.diff_add {
color: var(--color-green);
}
.diff_remove {
.segment-delete {
color: var(--color-red);
}
.symbol {
color: light-dark(black, white);
.segment-insert {
color: var(--color-green);
}
.rotation0 {
+194 -108
View File
@@ -3,25 +3,25 @@ import headerStyles from './Header.module.css';
import clsx from 'clsx';
import memoizeOne from 'memoize-one';
import { type diff, display } from 'objdiff-wasm';
import { memo, useMemo } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList, areEqual } from 'react-window';
import type { ListChildComponentProps } from 'react-window';
import { useShallow } from 'zustand/react/shallow';
import { DiffKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
InstructionDiff,
SymbolDiff,
} from '../shared/gen/diff_pb';
import { displayDiff } from './diff';
import TooltipShared from './TooltipShared';
import {
type HighlightState,
highlightColumn,
highlightMatches,
updateHighlight,
} from './highlight';
import { runBuild, useAppStore, useExtensionStore } from './state';
import {
buildDiffConfig,
runBuild,
useAppStore,
useExtensionStore,
} from './state';
import { percentClass, useFontSize } from './util';
const ROTATION_CLASSES = [
@@ -37,100 +37,118 @@ const ROTATION_CLASSES = [
];
const AsmCell = ({
insDiff,
obj,
config,
symbol,
row,
column,
highlight: highlightState,
setHighlight,
}: {
insDiff: InstructionDiff | undefined;
symbol: DiffSymbol | undefined;
obj: diff.ObjectDiff | undefined;
config: diff.DiffConfig;
symbol: display.SectionDisplaySymbol | null;
row: number;
column: number;
highlight: HighlightState;
setHighlight: (highlight: HighlightState) => void;
}) => {
if (!insDiff || !symbol) {
if (!obj || !symbol) {
return <div className={styles.instructionCell} />;
}
const highlight = highlightColumn(highlightState, column);
const out: React.ReactNode[] = [];
const insRow = display.displayInstructionRow(obj, symbol, row, config);
let index = 0;
displayDiff(insDiff, symbol.address, (t) => {
for (const segment of insRow.segments) {
let className: string | undefined;
if (t.diff_index != null) {
className = ROTATION_CLASSES[t.diff_index % ROTATION_CLASSES.length];
switch (segment.color.tag) {
case 'normal':
break;
case 'dim':
className = styles.segmentDim;
break;
case 'bright':
className = styles.segmentBright;
break;
case 'replace':
className = styles.segmentReplace;
break;
case 'delete':
className = styles.segmentDelete;
break;
case 'insert':
className = styles.segmentInsert;
break;
case 'rotating':
className =
ROTATION_CLASSES[segment.color.val % ROTATION_CLASSES.length];
break;
default:
console.warn('Unknown color type', segment.color);
break;
}
const t = segment.text;
let text = '';
let postText = ''; // unhighlightable text after the token
let padTo = 0;
let isToken = false;
switch (t.type) {
switch (t.tag) {
case 'basic':
text = t.text;
break;
case 'basic_color':
text = t.text;
className = ROTATION_CLASSES[t.index % ROTATION_CLASSES.length];
text = t.val;
break;
case 'line':
text = (t.line_number || 0).toString(10);
className = styles.lineNumber;
padTo = 5;
text = t.val.toString(10);
break;
case 'address':
text = (t.address || 0).toString(16);
text = t.val.toString(16);
postText = ':';
padTo = 5;
isToken = true;
break;
case 'opcode':
text = t.mnemonic;
padTo = 8;
text = t.val.mnemonic;
isToken = true;
if (insDiff.diff_kind === DiffKind.DIFF_OP_MISMATCH) {
className = styles.diff_change;
}
break;
case 'argument': {
const value = t.value.value;
switch (value.oneofKind) {
case 'signed':
if (value.signed < 0) {
text = `-0x${(-value.signed).toString(16)}`;
} else {
text = `0x${value.signed.toString(16)}`;
}
break;
case 'unsigned':
text = `0x${value.unsigned.toString(16)}`;
break;
case 'opaque':
text = value.opaque;
break;
case 'signed':
if (t.val < 0) {
text = `-0x${(-t.val).toString(16)}`;
} else {
text = `0x${t.val.toString(16)}`;
}
isToken = true;
break;
}
case 'branch_dest':
text = (t.address || 0).toString(16);
case 'unsigned':
text = `0x${t.val.toString(16)}`;
isToken = true;
break;
case 'symbol': {
const symbol = t.target.symbol as DiffSymbol;
text = symbol.demangled_name || symbol.name;
if (t.diff_index == null) {
className = styles.symbol;
case 'opaque':
text = t.val;
isToken = true;
break;
case 'branch-dest':
text = t.val.toString(16);
isToken = true;
break;
case 'symbol':
text = t.val.demangledName || t.val.name;
isToken = true;
break;
case 'addend':
if (t.val < 0) {
text = `-0x${(-t.val).toString(16)}`;
} else {
text = `+0x${t.val.toString(16)}`;
}
isToken = true;
break;
}
case 'spacing':
text = ' '.repeat(t.count);
text = ' '.repeat(t.val);
break;
case 'eol':
continue;
default:
console.warn('Unknown text type', t);
return null;
break;
}
out.push(
<span
@@ -151,39 +169,55 @@ const AsmCell = ({
);
index++;
if (postText) {
out.push(<span key={index}>{postText}</span>);
out.push(
<span key={index} className={className}>
{postText}
</span>,
);
index++;
}
if (padTo > text.length + postText.length) {
const spacing = ' '.repeat(padTo - text.length - postText.length);
if (segment.padTo > text.length + postText.length) {
const spacing = ' '.repeat(segment.padTo - text.length - postText.length);
out.push(<span key={index}>{spacing}</span>);
index++;
}
});
}
const classes = [styles.instructionCell];
if (insDiff.diff_kind) {
classes.push(styles.diff_any);
if (insRow.diffKind !== 'none') {
classes.push(styles.diffAny);
}
switch (insDiff.diff_kind) {
case DiffKind.DIFF_DELETE:
classes.push(styles.diff_remove);
break;
case DiffKind.DIFF_INSERT:
classes.push(styles.diff_add);
break;
case DiffKind.DIFF_REPLACE:
classes.push(styles.diff_change);
break;
if (!out.length) {
return <div className={clsx(classes)} />;
}
return <div className={clsx(classes)}>{out}</div>;
const tooltipContent: InstructionTooltipContent = { column, row };
return (
<div
className={clsx(classes)}
data-tooltip-id="instruction-tooltip"
data-tooltip-content={JSON.stringify(tooltipContent)}
>
{out}
</div>
);
};
type InstructionTooltipContent = {
column: number;
row: number;
};
type ItemData = {
itemCount: number;
left: SymbolDiff | null;
right: SymbolDiff | null;
symbolName: string;
result: diff.DiffResult;
config: diff.DiffConfig;
matchPercent?: number;
left: display.SectionDisplaySymbol | null;
leftSymbol: display.SymbolDisplay | null;
right: display.SectionDisplaySymbol | null;
rightSymbol: display.SymbolDisplay | null;
highlight: HighlightState;
setHighlight: (highlight: HighlightState) => void;
};
@@ -192,10 +226,8 @@ const AsmRow = memo(
({
index,
style,
data: { left, right, highlight, setHighlight },
data: { result, config, left, right, highlight, setHighlight },
}: ListChildComponentProps<ItemData>) => {
const leftIns = left?.instructions[index];
const rightIns = right?.instructions[index];
return (
<div
className={styles.instructionRow}
@@ -212,15 +244,19 @@ const AsmRow = memo(
}}
>
<AsmCell
insDiff={leftIns}
symbol={left?.symbol}
obj={result.left}
config={config}
symbol={left}
row={index}
column={0}
highlight={highlight}
setHighlight={setHighlight}
/>
<AsmCell
insDiff={rightIns}
symbol={right?.symbol}
obj={result.right}
config={config}
symbol={right}
row={index}
column={1}
highlight={highlight}
setHighlight={setHighlight}
@@ -233,23 +269,41 @@ const AsmRow = memo(
const createItemData = memoizeOne(
(
left: SymbolDiff | null,
right: SymbolDiff | null,
result: diff.DiffResult,
left: display.SectionDisplaySymbol | null,
right: display.SectionDisplaySymbol | null,
highlight: HighlightState,
setHighlight: (highlight: HighlightState) => void,
): ItemData => {
const leftSymbol = left ? display.displaySymbol(result.left!, left) : null;
const rightSymbol = right
? display.displaySymbol(result.right!, right)
: null;
const itemCount = Math.max(
left?.instructions.length || 0,
right?.instructions.length || 0,
leftSymbol?.rowCount || 0,
rightSymbol?.rowCount || 0,
);
return { itemCount, left, right, highlight, setHighlight };
const symbolName = leftSymbol?.name || rightSymbol?.name || '';
const config = buildDiffConfig(null);
return {
itemCount,
symbolName,
result,
config,
left,
leftSymbol,
right,
rightSymbol,
highlight,
setHighlight,
};
},
);
const SymbolLabel = ({
symbol,
}: {
symbol: SymbolDiff | null;
symbol: display.SymbolDisplay | null;
}) => {
if (!symbol) {
return (
@@ -258,21 +312,26 @@ const SymbolLabel = ({
</span>
);
}
const demangledName = symbol.symbol?.demangled_name || symbol.symbol?.name;
const displayName = symbol.demangledName || symbol.name;
return (
<span
className={clsx(headerStyles.label, headerStyles.emphasized)}
title={demangledName}
title={displayName}
>
{demangledName}
{displayName}
</span>
);
};
const FunctionView = ({
diff,
left,
right,
}: { left: SymbolDiff | null; right: SymbolDiff | null }) => {
}: {
diff: diff.DiffResult;
left: display.SectionDisplaySymbol | null;
right: display.SectionDisplaySymbol | null;
}) => {
const { buildRunning, currentUnit, lastBuilt } = useExtensionStore(
useShallow((state) => ({
buildRunning: state.buildRunning,
@@ -291,29 +350,27 @@ const FunctionView = ({
})),
);
const symbolName = left?.symbol?.name || right?.symbol?.name || '';
const itemData = createItemData(diff, left, right, highlight, setHighlight);
const initialScrollOffset = useMemo(
() =>
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
symbolName
itemData.symbolName
] || 0,
[currentUnitName, symbolName],
[currentUnitName, itemData.symbolName],
);
const itemSize = useFontSize() * 1.33;
const itemData = createItemData(left, right, highlight, setHighlight);
const matchPercent = right?.match_percent;
return (
<>
<div className={headerStyles.header}>
<div className={headerStyles.column}>
<div className={headerStyles.row}>
<button title="Back" onClick={() => setSelectedSymbol(null)}>
<button title="Back" onClick={() => setSelectedSymbol(null, null)}>
<span className="codicon codicon-chevron-left" />
</button>
</div>
<div className={headerStyles.row}>
<SymbolLabel symbol={left} />
<SymbolLabel symbol={itemData.leftSymbol} />
</div>
</div>
<div className={headerStyles.column}>
@@ -332,20 +389,20 @@ const FunctionView = ({
)}
</div>
<div className={headerStyles.row}>
{matchPercent !== undefined && (
{itemData.matchPercent !== undefined && (
<>
<span
className={clsx(
headerStyles.label,
percentClass(matchPercent),
percentClass(itemData.matchPercent),
)}
>
{Math.floor(matchPercent).toFixed(0)}%
{Math.floor(itemData.matchPercent).toFixed(0)}%
</span>
{' | '}
</>
)}
<SymbolLabel symbol={right} />
<SymbolLabel symbol={itemData.rightSymbol} />
</div>
</div>
</div>
@@ -362,7 +419,7 @@ const FunctionView = ({
onScroll={(e) => {
setSymbolScrollOffset(
currentUnitName,
symbolName,
itemData.symbolName,
e.scrollOffset,
);
}}
@@ -373,6 +430,35 @@ const FunctionView = ({
)}
</AutoSizer>
</div>
<TooltipShared
id="instruction-tooltip"
callback={(content) => {
const data: InstructionTooltipContent = JSON.parse(content);
let obj: diff.ObjectDiff | undefined;
let symbol: display.SectionDisplaySymbol | undefined;
switch (data.column) {
case 0:
obj = diff.left;
symbol = itemData.left ?? undefined;
break;
case 1:
obj = diff.right;
symbol = itemData.right ?? undefined;
break;
default:
break;
}
if (!obj || !symbol) {
return null;
}
return display.instructionHover(
obj,
symbol,
data.row,
itemData.config,
);
}}
/>
</>
);
};
+1 -1
View File
@@ -31,7 +31,7 @@
}
.emphasized {
color: light-dark(black, white);
color: var(--color-bright);
}
.missing {

Some files were not shown because too many files have changed in this diff Show More