feat/ida66 #3

Merged
rov merged 6 commits from feat/ida66 into main 2026-06-10 11:57:14 -03:00
23 changed files with 2562 additions and 140 deletions

16
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "decompile-ai", "name": "decompile-ai",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^3.0.0",
"@fontsource/inter": "^5.2.8", "@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
@ -317,6 +318,21 @@
"commander": "~14.0.0" "commander": "~14.0.0"
} }
}, },
"node_modules/@dagrejs/dagre": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.0.0.tgz",
"integrity": "sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==",
"license": "MIT",
"dependencies": {
"@dagrejs/graphlib": "4.0.1"
}
},
"node_modules/@dagrejs/graphlib": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.1.tgz",
"integrity": "sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==",
"license": "MIT"
},
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",

View File

@ -12,6 +12,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^3.0.0",
"@fontsource/inter": "^5.2.8", "@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",

View File

@ -19,9 +19,14 @@ import type {
import type { import type {
AnalysisResponse, AnalysisResponse,
DataLabelResponse,
EnumResponse,
ImportResponse,
ListFunctionsParams, ListFunctionsParams,
PageFunctionSummaryResponse, PageFunctionSummaryResponse,
SegmentResponse, SegmentResponse,
StringResponse,
StructResponse,
} from "../api.schemas"; } from "../api.schemas";
import { customInstance } from "../../api"; import { customInstance } from "../../api";
@ -248,6 +253,242 @@ export function useGetAnalysis<TData = Awaited<ReturnType<typeof getAnalysis>>,
return { ...query, queryKey: queryOptions.queryKey }; return { ...query, queryKey: queryOptions.queryKey };
} }
/**
* Retrieve all struct/union definitions identified during a static analysis.
* @summary List structures in an analysis
*/
export const listStructures = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<StructResponse[]>(
{
url: `/analysis/${encodeURIComponent(String(analysisId))}/structures`,
method: "GET",
signal,
},
options
);
};
export const getListStructuresQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/structures`] as const;
};
export const getListStructuresQueryOptions = <
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListStructuresQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listStructures>>> = ({ signal }) =>
listStructures(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListStructuresQueryResult = NonNullable<Awaited<ReturnType<typeof listStructures>>>;
export type ListStructuresQueryError = unknown;
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listStructures>>,
TError,
Awaited<ReturnType<typeof listStructures>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listStructures>>,
TError,
Awaited<ReturnType<typeof listStructures>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List structures in an analysis
*/
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListStructuresQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* Retrieve all strings extracted during a static analysis.
* @summary List strings in an analysis
*/
export const listStrings = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<StringResponse[]>(
{ url: `/analysis/${encodeURIComponent(String(analysisId))}/strings`, method: "GET", signal },
options
);
};
export const getListStringsQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/strings`] as const;
};
export const getListStringsQueryOptions = <
TData = Awaited<ReturnType<typeof listStrings>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListStringsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listStrings>>> = ({ signal }) =>
listStrings(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListStringsQueryResult = NonNullable<Awaited<ReturnType<typeof listStrings>>>;
export type ListStringsQueryError = unknown;
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listStrings>>,
TError,
Awaited<ReturnType<typeof listStrings>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listStrings>>,
TError,
Awaited<ReturnType<typeof listStrings>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List strings in an analysis
*/
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListStringsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/** /**
* Retrieve memory segments (code, data, etc.) identified during a static analysis. * Retrieve memory segments (code, data, etc.) identified during a static analysis.
* @summary List segments in an analysis * @summary List segments in an analysis
@ -358,6 +599,242 @@ export function useListSegments<TData = Awaited<ReturnType<typeof listSegments>>
return { ...query, queryKey: queryOptions.queryKey }; return { ...query, queryKey: queryOptions.queryKey };
} }
/**
* Retrieve all imported DLL functions identified during a static analysis.
* @summary List imports in an analysis
*/
export const listImports = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<ImportResponse[]>(
{ url: `/analysis/${encodeURIComponent(String(analysisId))}/imports`, method: "GET", signal },
options
);
};
export const getListImportsQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/imports`] as const;
};
export const getListImportsQueryOptions = <
TData = Awaited<ReturnType<typeof listImports>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListImportsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listImports>>> = ({ signal }) =>
listImports(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListImportsQueryResult = NonNullable<Awaited<ReturnType<typeof listImports>>>;
export type ListImportsQueryError = unknown;
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listImports>>,
TError,
Awaited<ReturnType<typeof listImports>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listImports>>,
TError,
Awaited<ReturnType<typeof listImports>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List imports in an analysis
*/
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListImportsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* Retrieve all named data labels (global variables) identified during a static analysis.
* @summary List global variables in an analysis
*/
export const listGlobalVars = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<DataLabelResponse[]>(
{
url: `/analysis/${encodeURIComponent(String(analysisId))}/global-vars`,
method: "GET",
signal,
},
options
);
};
export const getListGlobalVarsQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/global-vars`] as const;
};
export const getListGlobalVarsQueryOptions = <
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListGlobalVarsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listGlobalVars>>> = ({ signal }) =>
listGlobalVars(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListGlobalVarsQueryResult = NonNullable<Awaited<ReturnType<typeof listGlobalVars>>>;
export type ListGlobalVarsQueryError = unknown;
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listGlobalVars>>,
TError,
Awaited<ReturnType<typeof listGlobalVars>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listGlobalVars>>,
TError,
Awaited<ReturnType<typeof listGlobalVars>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List global variables in an analysis
*/
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListGlobalVarsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/** /**
* Retrieve paginated list of functions extracted during a static analysis. * Retrieve paginated list of functions extracted during a static analysis.
* @summary List functions in an analysis * @summary List functions in an analysis
@ -490,3 +967,113 @@ export function useListFunctions<
return { ...query, queryKey: queryOptions.queryKey }; return { ...query, queryKey: queryOptions.queryKey };
} }
/**
* Retrieve all enum definitions identified during a static analysis.
* @summary List enums in an analysis
*/
export const listEnums = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<EnumResponse[]>(
{ url: `/analysis/${encodeURIComponent(String(analysisId))}/enums`, method: "GET", signal },
options
);
};
export const getListEnumsQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/enums`] as const;
};
export const getListEnumsQueryOptions = <
TData = Awaited<ReturnType<typeof listEnums>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListEnumsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listEnums>>> = ({ signal }) =>
listEnums(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListEnumsQueryResult = NonNullable<Awaited<ReturnType<typeof listEnums>>>;
export type ListEnumsQueryError = unknown;
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
Awaited<ReturnType<typeof listEnums>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
Awaited<ReturnType<typeof listEnums>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List enums in an analysis
*/
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListEnumsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@ -104,6 +104,27 @@ export interface JobListResponse {
failed?: number; failed?: number;
} }
export interface CfgBlockResponse {
id?: number;
start?: string;
end?: string;
type?: number;
succs?: number[];
preds?: number[];
}
export interface CommentResponse {
address?: string;
text?: string;
repeatable?: boolean;
}
export interface FrameMemberResponse {
offset?: number;
size?: number;
name?: string;
}
export interface LabelResponse { export interface LabelResponse {
name?: string; name?: string;
address?: string; address?: string;
@ -123,6 +144,9 @@ export interface FunctionDetailResponse {
callerCount?: number; callerCount?: number;
calleeCount?: number; calleeCount?: number;
labels?: LabelResponse[]; labels?: LabelResponse[];
stackFrame?: FrameMemberResponse[];
comments?: CommentResponse[];
cfgBlocks?: CfgBlockResponse[];
} }
export interface XrefResponse { export interface XrefResponse {
@ -135,6 +159,12 @@ export interface XrefResponse {
address?: string; address?: string;
} }
export interface EngineResponse {
name?: string;
label?: string;
available?: boolean;
}
export interface MessageResponse { export interface MessageResponse {
id?: string; id?: string;
sessionId?: string; sessionId?: string;
@ -156,6 +186,29 @@ export interface AnalysisResponse {
updatedAt?: string; updatedAt?: string;
} }
export interface StructMemberResponse {
offset?: number;
size?: number;
name?: string;
type?: string;
}
export interface StructResponse {
id?: string;
name?: string;
size?: number;
isUnion?: boolean;
members?: StructMemberResponse[];
}
export interface StringResponse {
id?: string;
address?: string;
value?: string;
encoding?: string;
referencedBy?: string;
}
export interface SegmentResponse { export interface SegmentResponse {
id?: string; id?: string;
name?: string; name?: string;
@ -163,6 +216,29 @@ export interface SegmentResponse {
endAddress?: string; endAddress?: string;
} }
export interface ImportResponse {
id?: string;
dll?: string;
name?: string;
address?: string;
ordinal?: number;
}
export interface DataXrefResponse {
function?: string;
address?: string;
type?: string;
}
export interface DataLabelResponse {
id?: string;
name?: string;
address?: string;
segment?: string;
size?: number;
dataXrefs?: DataXrefResponse[];
}
export interface Pageable { export interface Pageable {
/** @minimum 0 */ /** @minimum 0 */
page?: number; page?: number;
@ -209,6 +285,18 @@ export interface PageFunctionSummaryResponse {
empty?: boolean; empty?: boolean;
} }
export interface EnumConstResponse {
name?: string;
value?: number;
}
export interface EnumResponse {
id?: string;
name?: string;
width?: number;
constants?: EnumConstResponse[];
}
export type GetWorkspacesParams = { export type GetWorkspacesParams = {
/** /**
* Filter workspaces whose name contains this value (case-insensitive) * Filter workspaces whose name contains this value (case-insensitive)

View File

@ -0,0 +1,120 @@
/**
* Generated by orval v8.15.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import { useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type { EngineResponse } from "../api.schemas";
import { customInstance } from "../../api";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Retrieve all registered static analysis engines with their availability status.
* @summary List available analysis engines
*/
export const listEngines = (
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<EngineResponse[]>({ url: `/engines`, method: "GET", signal }, options);
};
export const getListEnginesQueryKey = () => {
return [`/engines`] as const;
};
export const getListEnginesQueryOptions = <
TData = Awaited<ReturnType<typeof listEngines>>,
TError = unknown,
>(options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListEnginesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listEngines>>> = ({ signal }) =>
listEngines(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listEngines>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListEnginesQueryResult = NonNullable<Awaited<ReturnType<typeof listEngines>>>;
export type ListEnginesQueryError = unknown;
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listEngines>>,
TError,
Awaited<ReturnType<typeof listEngines>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listEngines>>,
TError,
Awaited<ReturnType<typeof listEngines>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List available analysis engines
*/
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListEnginesQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@ -36,7 +36,7 @@ export const ConfirmDialog = ({
<DialogHeader> <DialogHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{variant === "destructive" && ( {variant === "destructive" && (
<div className="bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full"> <div className="bg-destructive/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full">
<AlertTriangle className="text-destructive h-5 w-5" /> <AlertTriangle className="text-destructive h-5 w-5" />
</div> </div>
)} )}

View File

@ -0,0 +1,57 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2 } from "lucide-react";
import type { EngineResponse } from "@/api/generated/api.schemas";
import { useMemo } from "react";
interface EngineSelectProps {
engines: EngineResponse[];
value?: string;
onChange: (engine: string) => void;
placeholder?: string;
isLoading?: boolean;
disabled?: boolean;
}
export const EngineSelect = ({
engines,
value,
onChange,
placeholder = "Select engine",
isLoading = false,
disabled = false,
}: EngineSelectProps) => {
const available = useMemo(() => engines.filter((e) => e.available), [engines]);
return (
<Select value={value} onValueChange={onChange} disabled={disabled || isLoading}>
<SelectTrigger className="w-full">
{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground">Loading engines...</span>
</div>
) : (
<SelectValue placeholder={placeholder} />
)}
</SelectTrigger>
<SelectContent>
{available.length === 0 && !isLoading && (
<div className="text-muted-foreground px-2 py-4 text-center text-sm">
No engines available
</div>
)}
{available.map((engine) => (
<SelectItem key={engine.name} value={engine.name}>
{engine.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@ -109,7 +109,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground relative flex w-full cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-3 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:cursor-default data-[disabled]:opacity-50 data-[state=checked]:pl-8",
className className
)} )}
{...props} {...props}

View File

@ -0,0 +1,96 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { EngineSelect } from "@/components/EngineSelect";
import { AlertTriangle } from "lucide-react";
import type { EngineResponse } from "@/api/generated/api.schemas";
interface AnalyzeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
isReanalyze: boolean;
engines: EngineResponse[];
enginesLoading: boolean;
onConfirm: (engine: string) => void;
}
export const AnalyzeDialog = ({
open,
onOpenChange,
isReanalyze,
engines,
enginesLoading,
onConfirm,
}: AnalyzeDialogProps) => {
const [selectedEngine, setSelectedEngine] = useState<string>();
const handleConfirm = () => {
if (!selectedEngine) return;
onConfirm(selectedEngine);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
{isReanalyze && (
<div className="flex items-center gap-3">
<div className="bg-destructive/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full">
<AlertTriangle className="text-destructive h-5 w-5" />
</div>
<div>
<DialogTitle>Re-analyze binary</DialogTitle>
<DialogDescription className="mt-1">
This will replace all current analysis data. Function names, comments, and
decompiled code will be lost.
</DialogDescription>
</div>
</div>
)}
{!isReanalyze && (
<div>
<DialogTitle>Analyze binary</DialogTitle>
<DialogDescription className="mt-1">
Select the analysis engine to use.
</DialogDescription>
</div>
)}
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Analysis Engine</label>
<EngineSelect
engines={engines}
value={selectedEngine}
onChange={setSelectedEngine}
isLoading={enginesLoading}
placeholder="Select an engine"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant={isReanalyze ? "destructive" : "default"}
onClick={handleConfirm}
disabled={!selectedEngine}
>
{isReanalyze ? "Re-analyze" : "Analyze"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,354 @@
import { useMemo, useRef, useState, useCallback, useEffect } from "react";
import { Graph } from "@dagrejs/graphlib";
import { layout } from "@dagrejs/dagre";
import type { CfgBlockResponse } from "@/api/generated/api.schemas";
interface CfgGraphProps {
blocks: CfgBlockResponse[];
assembly?: string;
onHighlightRange: (start: string, end: string) => void;
}
const NODE_W = 220;
const HEADER_H = 18;
const LINE_H = 13;
const FOOTER_H = 11;
const PAD_V = 12;
const GRAPH_PAD = 32;
const BLOCK_DEFS: Record<number, { stroke: string; fill: string; label: string }> = {
0: { stroke: "#2563eb", fill: "#bfdbfe", label: "Normal" },
1: { stroke: "#ea580c", fill: "#fed7aa", label: "Indirect Jump" },
2: { stroke: "#dc2626", fill: "#fecaca", label: "Return" },
3: { stroke: "#dc2626", fill: "#fecaca", label: "Cond Return" },
4: { stroke: "#4b5563", fill: "#e5e7eb", label: "No-return" },
5: { stroke: "#4b5563", fill: "#e5e7eb", label: "Ext No-return" },
6: { stroke: "#7c3aed", fill: "#ddd6fe", label: "External" },
7: { stroke: "#dc2626", fill: "#fecaca", label: "Error" },
};
const DEFAULT_DEF = { stroke: "#6b7280", fill: "#f3f4f6", label: "Unknown" };
interface AsmLine {
address: string;
text: string;
}
function parseAssembly(assembly?: string): AsmLine[] {
if (!assembly) return [];
return assembly
.split(/\\n/)
.map((line) => {
const m = line.trim().match(/^(0x[0-9a-fA-F]+)\s{2,}(.+)?$/);
return m ? { address: m[1], text: m[2]?.trim() ?? "" } : null;
})
.filter((l): l is AsmLine => l !== null);
}
function addressNum(addr: string) {
return parseInt(addr, 16);
}
export function CfgGraph({ blocks, assembly, onHighlightRange }: CfgGraphProps) {
const svgRef = useRef<SVGSVGElement>(null);
const isDragging = useRef(false);
const hasDragged = useRef(false);
const dragStart = useRef({ x: 0, y: 0 });
const dragLast = useRef({ x: 0, y: 0 });
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
const { svgWidth, svgHeight, nodes, edges } = useMemo(() => {
const allLines = parseAssembly(assembly);
const blockLines = new Map<number, AsmLine[]>();
for (const b of blocks) {
const matched = allLines.filter((l) => {
const a = addressNum(l.address);
const s = addressNum(b.start ?? "");
const e = addressNum(b.end ?? "");
if (isNaN(a) || isNaN(s) || isNaN(e)) return false;
return a >= s && a <= e;
});
blockLines.set(b.id ?? -1, matched);
}
const g = new Graph<object, { width: number; height: number }>();
g.setGraph({ rankdir: "TB", nodesep: 30, ranksep: 50 });
g.setDefaultEdgeLabel(() => ({}));
for (const b of blocks) {
const lines = blockLines.get(b.id ?? -1) ?? [];
const h = HEADER_H + lines.length * LINE_H + FOOTER_H + PAD_V;
g.setNode(String(b.id), { width: NODE_W, height: h });
}
for (const b of blocks) {
for (const t of b.succs ?? []) {
g.setEdge(String(b.id), String(t));
}
}
layout(g);
const nodeList: Array<{
block: CfgBlockResponse;
lines: AsmLine[];
x: number;
y: number;
width: number;
height: number;
}> = [];
for (const b of blocks) {
const n = g.node(String(b.id));
if (n) {
nodeList.push({
block: b,
lines: blockLines.get(b.id ?? -1) ?? [],
x: n.x,
y: n.y,
width: n.width,
height: n.height,
});
}
}
const edgeList: Array<{
points: { x: number; y: number }[];
}> = [];
for (const b of blocks) {
for (const t of b.succs ?? []) {
const e = g.edge(String(b.id), String(t)) as unknown as {
points?: { x: number; y: number }[];
};
if (e?.points?.length) {
edgeList.push({ points: e.points });
}
}
}
let maxX = 0;
let maxY = 0;
for (const n of nodeList) {
maxX = Math.max(maxX, n.x + n.width / 2);
maxY = Math.max(maxY, n.y + n.height / 2);
}
return {
svgWidth: maxX + GRAPH_PAD,
svgHeight: maxY + GRAPH_PAD,
nodes: nodeList,
edges: edgeList,
};
}, [blocks, assembly]);
// Center the graph in the container on mount / resize
const centerGraph = useCallback(() => {
const svg = svgRef.current;
if (!svg) return;
const rect = svg.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
setTransform({
x: (rect.width - svgWidth) / 2,
y: (rect.height - svgHeight) / 2,
scale: 1,
});
}, [svgWidth, svgHeight]);
useEffect(() => {
centerGraph();
}, [centerGraph]);
// Also center when the window resizes
useEffect(() => {
window.addEventListener("resize", centerGraph);
return () => window.removeEventListener("resize", centerGraph);
}, [centerGraph]);
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault();
const svg = svgRef.current;
if (!svg) return;
const rect = svg.getBoundingClientRect();
const cx = e.clientX - rect.left;
const cy = e.clientY - rect.top;
const delta = e.deltaY > 0 ? 0.9 : 1.1;
setTransform((prev) => {
const newScale = Math.max(0.3, Math.min(3, prev.scale * delta));
const newX = cx - (cx - prev.x) * (newScale / prev.scale);
const newY = cy - (cy - prev.y) * (newScale / prev.scale);
return { x: newX, y: newY, scale: newScale };
});
}, []);
const handlePointerDown = useCallback((e: React.PointerEvent) => {
hasDragged.current = false;
isDragging.current = true;
dragStart.current = { x: e.clientX, y: e.clientY };
dragLast.current = { x: e.clientX, y: e.clientY };
(e.target as Element).setPointerCapture?.(e.pointerId);
}, []);
const handlePointerMove = useCallback((e: React.PointerEvent) => {
if (!isDragging.current) return;
const dx = e.clientX - dragLast.current.x;
const dy = e.clientY - dragLast.current.y;
const totalDx = e.clientX - dragStart.current.x;
const totalDy = e.clientY - dragStart.current.y;
if (Math.abs(totalDx) > 3 || Math.abs(totalDy) > 3) {
hasDragged.current = true;
}
dragLast.current = { x: e.clientX, y: e.clientY };
setTransform((prev) => ({
...prev,
x: prev.x + dx,
y: prev.y + dy,
}));
}, []);
const handlePointerUp = useCallback(
(e: React.PointerEvent) => {
isDragging.current = false;
(e.target as Element).releasePointerCapture?.(e.pointerId);
if (hasDragged.current || e.button !== 0) return;
const nodeEl = (e.target as Element).closest("[data-cfg-node]") as HTMLElement | null;
if (nodeEl) {
const id = nodeEl.getAttribute("data-cfg-node");
const block = blocks.find((b) => String(b.id) === id);
if (block?.start && block?.end) {
onHighlightRange(block.start, block.end);
}
}
},
[blocks, onHighlightRange]
);
if (blocks.length === 0) {
return (
<div className="text-muted-foreground py-8 text-center text-xs">No control flow blocks</div>
);
}
return (
<div className="flex h-full flex-col gap-3">
<div className="min-h-0 flex-1 overflow-hidden rounded border">
<svg
ref={svgRef}
width="100%"
height="100%"
className="block cursor-grab select-none"
onContextMenu={(e) => e.preventDefault()}
onWheel={handleWheel}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
>
<defs>
<marker
id="cfg-arrow"
viewBox="0 0 10 10"
refX={10}
refY={5}
markerWidth={8}
markerHeight={8}
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#9ca3af" />
</marker>
</defs>
<g transform={`translate(${transform.x} ${transform.y}) scale(${transform.scale})`}>
{/* Padding background (invisible, just to show padding area) */}
<rect x={0} y={0} width={svgWidth} height={svgHeight} fill="transparent" />
{/* Edges */}
{edges.map((edge, ei) => {
const d = edge.points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
return (
<path
key={`edge-${ei}`}
d={d}
fill="none"
stroke="#9ca3af"
strokeWidth={1.5}
markerEnd="url(#cfg-arrow)"
/>
);
})}
{/* Nodes */}
{nodes.map(({ block, lines, x, y, width, height }) => {
const def = BLOCK_DEFS[block.type ?? -1] ?? DEFAULT_DEF;
const rx = x - width / 2;
const ry = y - height / 2;
return (
<g key={block.id} data-cfg-node={block.id}>
<rect
x={rx}
y={ry}
width={width}
height={height}
rx={5}
fill={def.fill}
stroke={def.stroke}
strokeWidth={1.5}
/>
<text
x={x}
y={ry + 13}
textAnchor="middle"
fill="#1e293b"
fontSize={11}
fontFamily="JetBrains Mono, monospace"
fontWeight={600}
>
{block.start}-{block.end}
</text>
{lines.map((l, li) => (
<text
key={li}
x={rx + 8}
y={ry + HEADER_H + li * LINE_H + 10}
textAnchor="start"
fill="#334155"
fontSize={10}
fontFamily="JetBrains Mono, monospace"
>
{l.text}
</text>
))}
<text
x={x}
y={ry + height - 4}
textAnchor="middle"
fill={def.stroke}
fontSize={9}
>
{def.label}
</text>
</g>
);
})}
</g>
</svg>
</div>
{/* Legend */}
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
{Object.entries(BLOCK_DEFS).map(([type, def]) => (
<div key={type} className="flex items-center gap-1.5 text-xs">
<span
className="inline-block h-2.5 w-2.5 rounded-sm border"
style={{
backgroundColor: def.fill,
borderColor: def.stroke,
}}
/>
<span className="text-muted-foreground">{def.label}</span>
</div>
))}
</div>
</div>
);
}

View File

@ -1,16 +1,19 @@
"use client"; "use client";
import { useRef, useCallback } from "react"; import { useRef, useCallback, useMemo, useState } from "react";
import { Copy, Code, ArrowUpRight, ArrowDownRight, Link } from "lucide-react"; import { Copy, Code, ArrowUpRight, ArrowDownRight, Link } from "lucide-react";
import type { FunctionDetailResponse } from "@/api/generated/api.schemas"; import type { FunctionDetailResponse, CommentResponse } from "@/api/generated/api.schemas";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CfgGraph } from "@/features/binary/CfgGraph";
interface CodeViewerProps { interface CodeViewerProps {
func: FunctionDetailResponse | undefined; func: FunctionDetailResponse | undefined;
isLoading?: boolean; isLoading?: boolean;
onSelectFunction: (id: string) => void;
functionNameToId: Record<string, string>;
} }
const FLAG_COLORS: Record<string, string> = { const FLAG_COLORS: Record<string, string> = {
@ -50,10 +53,34 @@ function LabelTypeIcon({ type }: { type: string | undefined }) {
} }
} }
export function CodeViewer({ func, isLoading }: CodeViewerProps) { export function CodeViewer({
func,
isLoading,
onSelectFunction,
functionNameToId,
}: CodeViewerProps) {
const assemblyRef = useRef<HTMLTableElement>(null); const assemblyRef = useRef<HTMLTableElement>(null);
const pseudoRef = useRef<HTMLTableElement>(null); const pseudoRef = useRef<HTMLTableElement>(null);
const [activeTab, setActiveTab] = useState("assembly");
const [highlightedRange, setHighlightedRange] = useState<{
start: string;
end: string;
} | null>(null);
const handleCfgHighlight = useCallback((start: string, end: string) => {
setHighlightedRange((prev) =>
prev?.start === start && prev?.end === end ? null : { start, end }
);
setActiveTab("assembly");
setTimeout(() => {
document.getElementById(`asm-addr-${start}`)?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, 50);
}, []);
const copyActiveTab = useCallback(() => { const copyActiveTab = useCallback(() => {
const activeContent = assemblyRef.current?.textContent ?? pseudoRef.current?.textContent ?? ""; const activeContent = assemblyRef.current?.textContent ?? pseudoRef.current?.textContent ?? "";
navigator.clipboard.writeText(activeContent); navigator.clipboard.writeText(activeContent);
@ -85,16 +112,42 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
addressToLabel[func.address] = func.name; addressToLabel[func.address] = func.name;
} }
// Build comments by address for inline rendering
const commentsByAddress = useMemo(() => {
const map: Record<string, CommentResponse[]> = {};
for (const c of func?.comments ?? []) {
if (!c.address) continue;
const key = c.address.toLowerCase();
if (!map[key]) map[key] = [];
map[key].push(c);
}
return map;
}, [func?.comments]);
// Stack frame table
const stackFrame = func?.stackFrame ?? [];
const hasStackFrame = stackFrame.length > 0;
const cfgBlocks = func?.cfgBlocks ?? [];
const hasCfg = cfgBlocks.length > 0;
const hasPseudocode = !!func?.decompiledCode;
return ( return (
<Tabs defaultValue="assembly" className="flex h-full flex-col"> <Tabs value={activeTab} onValueChange={setActiveTab} className="flex h-full flex-col">
<div className="border-border flex shrink-0 items-center justify-between border-b px-4 py-2"> <div className="border-border flex shrink-0 items-center justify-between border-b px-4 py-2">
<TabsList className="h-8"> <TabsList className="h-8">
<TabsTrigger value="assembly" className="text-xs"> <TabsTrigger value="assembly" className="text-xs">
Assembly Assembly
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="pseudocode" className="text-xs"> {hasCfg && (
Pseudocode <TabsTrigger value="cfg" className="text-xs">
</TabsTrigger> CFG
</TabsTrigger>
)}
{hasPseudocode && (
<TabsTrigger value="pseudocode" className="text-xs">
Pseudocode
</TabsTrigger>
)}
</TabsList> </TabsList>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -129,6 +182,38 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
</div> </div>
</div> </div>
{func?.signature && (
<div className="border-border shrink-0 border-b px-4 py-1">
<span className="text-muted-foreground font-mono text-xs">{func.signature}</span>
</div>
)}
{hasStackFrame && (
<div className="border-border shrink-0 border-b px-4 py-1">
<details>
<summary className="text-muted-foreground cursor-pointer text-xs select-none">
Stack Frame ({stackFrame.length} member{stackFrame.length !== 1 ? "s" : ""})
</summary>
<div className="mt-1 flex flex-col">
<div className="text-muted-foreground flex shrink-0 text-xs">
<span className="w-[60px]">Offset</span>
<span className="w-[50px]">Size</span>
<span>Name</span>
</div>
<div className="max-h-40 overflow-y-auto">
{stackFrame.map((m, i) => (
<div key={i} className="even:bg-muted/30 flex font-mono text-xs">
<span className="w-[60px] shrink-0">{m.offset}</span>
<span className="w-[50px] shrink-0">{m.size}</span>
<span className="truncate">{m.name}</span>
</div>
))}
</div>
</div>
</details>
</div>
)}
<TabsContent value="assembly" className="mt-0 flex-1 overflow-hidden"> <TabsContent value="assembly" className="mt-0 flex-1 overflow-hidden">
{hasData ? ( {hasData ? (
<ScrollArea className="h-full w-full"> <ScrollArea className="h-full w-full">
@ -143,6 +228,10 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
addressToLabel={addressToLabel} addressToLabel={addressToLabel}
labelToAddress={labelToAddress} labelToAddress={labelToAddress}
addressToLabelType={addressToLabelType} addressToLabelType={addressToLabelType}
commentsByAddress={commentsByAddress}
onSelectFunction={onSelectFunction}
functionNameToId={functionNameToId}
highlightedRange={highlightedRange}
/> />
))} ))}
</tbody> </tbody>
@ -164,33 +253,47 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
)} )}
</TabsContent> </TabsContent>
<TabsContent value="pseudocode" className="mt-0 flex-1 overflow-hidden"> {hasCfg && (
{hasData ? ( <TabsContent value="cfg" className="mt-0 flex-1 overflow-hidden">
<ScrollArea className="h-full w-full"> <div className="h-full w-full p-4">
<div className="p-4 font-mono"> <CfgGraph
<table ref={pseudoRef} className="text-sm leading-relaxed"> blocks={cfgBlocks}
<tbody> assembly={func?.assembly}
{pseudoLines.map((line, idx) => ( onHighlightRange={handleCfgHighlight}
<PseudoRow key={idx} line={line} lineNumber={idx + 1} /> />
))}
</tbody>
</table>
</div>
</ScrollArea>
) : (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-3">
<div className="bg-muted flex h-16 w-16 items-center justify-center rounded-full">
<Code className="h-8 w-8" />
</div>
<h3 className="text-foreground font-medium">Code Viewer</h3>
<p className="text-muted-foreground text-sm">
{isLoading
? "Loading function..."
: "Select a function from the list to view its assembly and pseudocode."}
</p>
</div> </div>
)} </TabsContent>
</TabsContent> )}
{hasPseudocode && (
<TabsContent value="pseudocode" className="mt-0 flex-1 overflow-hidden">
{hasData ? (
<ScrollArea className="h-full w-full">
<div className="p-4 font-mono">
<table ref={pseudoRef} className="text-sm leading-relaxed">
<tbody>
{pseudoLines.map((line, idx) => (
<PseudoRow key={idx} line={line} lineNumber={idx + 1} />
))}
</tbody>
</table>
</div>
</ScrollArea>
) : (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-3">
<div className="bg-muted flex h-16 w-16 items-center justify-center rounded-full">
<Code className="h-8 w-8" />
</div>
<h3 className="text-foreground font-medium">Code Viewer</h3>
<p className="text-muted-foreground text-sm">
{isLoading
? "Loading function..."
: "Select a function from the list to view its assembly and pseudocode."}
</p>
</div>
)}
</TabsContent>
)}
</Tabs> </Tabs>
); );
} }
@ -214,12 +317,20 @@ function AssemblyRow({
addressToLabel, addressToLabel,
labelToAddress, labelToAddress,
addressToLabelType, addressToLabelType,
commentsByAddress,
onSelectFunction,
functionNameToId,
highlightedRange,
}: { }: {
line: string; line: string;
lineNumber: number; lineNumber: number;
addressToLabel: Record<string, string>; addressToLabel: Record<string, string>;
labelToAddress: Record<string, string>; labelToAddress: Record<string, string>;
addressToLabelType: Record<string, string>; addressToLabelType: Record<string, string>;
commentsByAddress: Record<string, CommentResponse[]>;
onSelectFunction: (id: string) => void;
functionNameToId: Record<string, string>;
highlightedRange?: { start: string; end: string } | null;
}) { }) {
const trimmed = line.trim(); const trimmed = line.trim();
@ -229,7 +340,7 @@ function AssemblyRow({
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none"> <td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
{lineNumber} {lineNumber}
</td> </td>
<td colSpan={3}>&nbsp;</td> <td colSpan={4}>&nbsp;</td>
</tr> </tr>
); );
} }
@ -250,7 +361,7 @@ function AssemblyRow({
<td className="w-[14ch] whitespace-nowrap"> <td className="w-[14ch] whitespace-nowrap">
<span className="text-[var(--code-function)]">{labelName}:</span> <span className="text-[var(--code-function)]">{labelName}:</span>
</td> </td>
<td colSpan={2}>&nbsp;</td> <td colSpan={3}>&nbsp;</td>
</tr> </tr>
) : null; ) : null;
@ -260,20 +371,47 @@ function AssemblyRow({
</td> </td>
); );
// Inject backend comments anchored to this address — rendered in a dedicated column
const backendComments = address ? commentsByAddress[address.toLowerCase()] : undefined;
const commentsCell =
backendComments && backendComments.length > 0 ? (
<td className="text-xs text-[var(--code-comment)]">
{backendComments.map((c, ci) => {
const body = (c.text ?? "").replace(/^;\s*/, "");
return (
<span key={`ida-cmt-${ci}`}>
{ci > 0 && " "}; {body}
</span>
);
})}
</td>
) : null;
let isHighlighted = false;
if (highlightedRange && address) {
const a = parseInt(address, 16);
const rs = parseInt(highlightedRange.start, 16);
const re = parseInt(highlightedRange.end, 16);
if (!isNaN(a) && !isNaN(rs) && !isNaN(re)) {
isHighlighted = a >= rs && a <= re;
}
}
const highlightClass = isHighlighted ? "bg-primary/10" : "";
const rowId = address ? `asm-addr-${address}` : undefined;
// Label-only line (ends with colon, no mnemonic) // Label-only line (ends with colon, no mnemonic)
const labelMatch = rest.match(/^([a-zA-Z_]\w*):$/); const labelMatch = rest.match(/^([a-zA-Z_]\w*):$/);
if (labelMatch) { if (labelMatch) {
return ( return (
<> <>
{labelDeclRow} {labelDeclRow}
<tr className="hover:bg-muted/50"> <tr id={rowId} className={`hover:bg-muted/50 ${highlightClass}`}>
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none"> <td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
{lineNumber} {lineNumber}
</td> </td>
{addrCell} {addrCell}
<td className="text-[var(--code-function)]" colSpan={2}> <td className="text-[var(--code-function)]">{rest}</td>
{rest} {commentsCell}
</td>
</tr> </tr>
</> </>
); );
@ -284,14 +422,13 @@ function AssemblyRow({
return ( return (
<> <>
{labelDeclRow} {labelDeclRow}
<tr className="hover:bg-muted/50"> <tr id={rowId} className={`hover:bg-muted/50 ${highlightClass}`}>
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none"> <td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
{lineNumber} {lineNumber}
</td> </td>
{addrCell} {addrCell}
<td className="text-[var(--code-comment)]" colSpan={2}> <td className="text-[var(--code-comment)]">{rest}</td>
{rest} {commentsCell}
</td>
</tr> </tr>
</> </>
); );
@ -306,7 +443,7 @@ function AssemblyRow({
return ( return (
<> <>
{labelDeclRow} {labelDeclRow}
<tr className="hover:bg-muted/50"> <tr id={rowId} className={`hover:bg-muted/50 ${highlightClass}`}>
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none"> <td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
{lineNumber} {lineNumber}
</td> </td>
@ -319,8 +456,11 @@ function AssemblyRow({
operands={operands} operands={operands}
labelToAddress={labelToAddress} labelToAddress={labelToAddress}
labelTypeMap={addressToLabelType} labelTypeMap={addressToLabelType}
onSelectFunction={onSelectFunction}
functionNameToId={functionNameToId}
/> />
</td> </td>
{commentsCell}
</tr> </tr>
</> </>
); );
@ -330,14 +470,13 @@ function AssemblyRow({
return ( return (
<> <>
{labelDeclRow} {labelDeclRow}
<tr className="hover:bg-muted/50"> <tr id={rowId} className={`hover:bg-muted/50 ${highlightClass}`}>
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none"> <td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
{lineNumber} {lineNumber}
</td> </td>
{addrCell} {addrCell}
<td className="text-foreground" colSpan={2}> <td className="text-foreground">{rest}</td>
{rest} {commentsCell}
</td>
</tr> </tr>
</> </>
); );
@ -347,10 +486,14 @@ function HighlightedAssembly({
operands, operands,
labelToAddress, labelToAddress,
labelTypeMap, labelTypeMap,
onSelectFunction,
functionNameToId,
}: { }: {
operands: string; operands: string;
labelToAddress: Record<string, string>; labelToAddress: Record<string, string>;
labelTypeMap: Record<string, string>; labelTypeMap: Record<string, string>;
onSelectFunction: (id: string) => void;
functionNameToId: Record<string, string>;
}) { }) {
const jumpToLabel = (name: string) => { const jumpToLabel = (name: string) => {
document.getElementById(`asm-label-${name}`)?.scrollIntoView({ document.getElementById(`asm-label-${name}`)?.scrollIntoView({
@ -359,9 +502,28 @@ function HighlightedAssembly({
}); });
}; };
const parts = operands.split( const handleLabelClick = (label: string) => {
/(\b0x[0-9a-fA-F]+\b|\bloc_\w+\b|\bsub_\w+\b|\bshort\b|\bbyte\b|\bword\b|\bdword\b|\bptr\b|\bnear\b|\bfar\b)/g const addr = labelToAddress[label];
); if (addr) {
const ltype = labelTypeMap[addr];
// CALL / JUMP labels are cross-references to external functions — prefer navigation
if (ltype === "CALL" || ltype === "JUMP") {
const funcId = functionNameToId[label];
if (funcId) {
onSelectFunction(funcId);
return;
}
}
jumpToLabel(label);
return;
}
const funcId = functionNameToId[label];
if (funcId) onSelectFunction(funcId);
};
const KEYWORDS = new Set(["short", "byte", "word", "dword", "ptr", "near", "far"]);
const parts = operands.split(/(\b0x[0-9a-fA-F]+\b|\b[a-zA-Z_]\w*\b)/g);
return ( return (
<span> <span>
@ -373,32 +535,38 @@ function HighlightedAssembly({
</span> </span>
); );
} }
if (/^(loc_\w+|sub_\w+)$/.test(part)) { if (/^[a-zA-Z_]\w*$/.test(part)) {
const addr = labelToAddress[part]; if (KEYWORDS.has(part)) {
const ltype = addr ? labelTypeMap[addr] : undefined; return (
const lcolor = <span key={i} className="text-[var(--code-keyword)]">
ltype === "CALL" {part}
? "text-[var(--code-function)]" </span>
: ltype === "JUMP" );
? "text-[var(--chart-3)]" }
: "text-[var(--code-function)]"; const isLocal = labelToAddress[part];
return ( const isFunc = functionNameToId[part];
<span if (isLocal || isFunc) {
key={i} const ltype = isLocal ? labelTypeMap[labelToAddress[part]] : undefined;
className={`cursor-pointer hover:underline ${lcolor}`} const lcolor =
title={addr ? `${addr}${ltype ? ` (${ltype})` : ""}` : undefined} ltype === "CALL"
onClick={() => jumpToLabel(part)} ? "text-[var(--code-function)]"
> : ltype === "JUMP"
{part} ? "text-[var(--chart-3)]"
</span> : "text-[var(--code-function)]";
); return (
} <span
if (/^(short|byte|word|dword|ptr|near|far)$/.test(part)) { key={i}
return ( className={`cursor-pointer hover:underline ${lcolor}`}
<span key={i} className="text-[var(--code-keyword)]"> title={
{part} isLocal ? `${labelToAddress[part]}${ltype ? ` (${ltype})` : ""}` : `${part}`
</span> }
); onClick={() => handleLabelClick(part)}
>
{part}
</span>
);
}
return <span key={i}>{part}</span>;
} }
return <span key={i}>{part}</span>; return <span key={i}>{part}</span>;
})} })}

View File

@ -0,0 +1,121 @@
import { useState, Fragment } from "react";
import { Search, ChevronRight, ChevronDown } from "lucide-react";
import type { EnumResponse } from "@/api/generated/api.schemas";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface EnumTableProps {
enums: EnumResponse[];
}
export function EnumTable({ enums }: EnumTableProps) {
const [search, setSearch] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const filtered = enums.filter(
(e) => !search || e.name?.toLowerCase().includes(search.toLowerCase())
);
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter enums..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[28px]" />
<TableHead>Name</TableHead>
<TableHead className="w-[80px] text-right">Width</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((e) => {
const hasConstants = e.constants && e.constants.length > 0;
const expanded = !!(e.id && expandedIds.has(e.id));
return (
<Fragment key={e.id}>
<TableRow
className="cursor-pointer"
onClick={() => e.id && hasConstants && toggleExpanded(e.id)}
>
<TableCell className="px-1">
{hasConstants &&
(expanded ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
))}
</TableCell>
<TableCell className="font-mono text-xs">{e.name}</TableCell>
<TableCell className="text-right text-xs">{e.width}</TableCell>
</TableRow>
{expanded && hasConstants && (
<TableRow key={`consts-${e.id}`}>
<TableCell colSpan={3} className="bg-muted/30 p-0">
<div className="flex flex-col">
<div className="text-muted-foreground grid shrink-0 grid-cols-[1fr_100px] px-2 text-xs">
<span>Name</span>
<span className="text-right">Value</span>
</div>
<div className="max-h-40 overflow-y-auto">
{e.constants!.map((c, ci) => (
<div
key={ci}
className="even:bg-muted/30 grid grid-cols-[1fr_100px] px-2 font-mono text-xs"
>
<span className="truncate">{c.name}</span>
<span className="text-right">{c.value}</span>
</div>
))}
</div>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{search ? "No matching enums" : "No enums found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{search ? `${filtered.length} of ${enums.length} enums` : `${enums.length} enums`}
</div>
</div>
);
}

View File

@ -0,0 +1,158 @@
import { useState, Fragment } from "react";
import { Search, ChevronRight, ChevronDown } from "lucide-react";
import type { DataLabelResponse } from "@/api/generated/api.schemas";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface GlobalVarTableProps {
globalVars: DataLabelResponse[];
functionNameToId: Record<string, string>;
onSelectFunction: (functionId: string) => void;
}
export function GlobalVarTable({
globalVars,
functionNameToId,
onSelectFunction,
}: GlobalVarTableProps) {
const [segmentFilter, setSegmentFilter] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const filtered = globalVars.filter(
(gv) => !segmentFilter || gv.segment?.toLowerCase().includes(segmentFilter.toLowerCase())
);
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={segmentFilter}
onChange={(e) => setSegmentFilter(e.target.value)}
placeholder="Filter by segment..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[28px]" />
<TableHead>Name</TableHead>
<TableHead className="w-[110px]">Address</TableHead>
<TableHead className="w-[80px]">Segment</TableHead>
<TableHead className="w-[60px] text-right">Size</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((gv) => {
const hasXrefs = gv.dataXrefs && gv.dataXrefs.length > 0;
const expanded = !!(gv.id && expandedIds.has(gv.id));
return (
<Fragment key={gv.id}>
<TableRow
className="cursor-pointer"
onClick={() => gv.id && hasXrefs && toggleExpanded(gv.id)}
>
<TableCell className="px-1">
{hasXrefs &&
(expanded ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
))}
</TableCell>
<TableCell className="font-mono text-xs">{gv.name}</TableCell>
<TableCell className="font-mono text-xs">{gv.address}</TableCell>
<TableCell className="text-xs">{gv.segment}</TableCell>
<TableCell className="text-right text-xs">{gv.size}</TableCell>
</TableRow>
{expanded && hasXrefs && (
<TableRow key={`xref-${gv.id}`}>
<TableCell colSpan={5} className="bg-muted/30 p-0">
<div className="px-9 py-2">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="text-xs">Function</TableHead>
<TableHead className="w-[110px] text-xs">Address</TableHead>
<TableHead className="w-[80px] text-xs">Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{gv.dataXrefs!.map((xref, xi) => {
const funcId = xref.function
? functionNameToId[xref.function]
: undefined;
return (
<TableRow key={xi}>
<TableCell className="text-xs">
{funcId ? (
<button
type="button"
className="text-primary hover:underline"
onClick={(e) => {
e.stopPropagation();
onSelectFunction(funcId);
}}
>
{xref.function}
</button>
) : (
(xref.function ?? "")
)}
</TableCell>
<TableCell className="font-mono text-xs">
{xref.address}
</TableCell>
<TableCell className="text-xs">{xref.type}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{segmentFilter ? "No matching global vars" : "No global vars found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{segmentFilter
? `${filtered.length} of ${globalVars.length} global vars`
: `${globalVars.length} global vars`}
</div>
</div>
);
}

View File

@ -0,0 +1,75 @@
import { useState } from "react";
import { Search } from "lucide-react";
import type { ImportResponse } from "@/api/generated/api.schemas";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface ImportTableProps {
imports: ImportResponse[];
}
export function ImportTable({ imports }: ImportTableProps) {
const [dllFilter, setDllFilter] = useState("");
const filtered = imports.filter(
(i) => !dllFilter || i.dll?.toLowerCase().includes(dllFilter.toLowerCase())
);
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={dllFilter}
onChange={(e) => setDllFilter(e.target.value)}
placeholder="Filter by DLL..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[130px]">DLL</TableHead>
<TableHead>Function</TableHead>
<TableHead className="w-[110px]">Address</TableHead>
<TableHead className="w-[90px] text-right">Ordinal</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((imp) => (
<TableRow key={imp.id}>
<TableCell className="text-xs">{imp.dll}</TableCell>
<TableCell className="font-mono text-xs">{imp.name}</TableCell>
<TableCell className="font-mono text-xs">{imp.address}</TableCell>
<TableCell className="text-right text-xs">{imp.ordinal}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{dllFilter ? "No matching imports" : "No imports found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{dllFilter
? `${filtered.length} of ${imports.length} imports`
: `${imports.length} imports`}
</div>
</div>
);
}

View File

@ -0,0 +1,110 @@
import { useState } from "react";
import { Search } from "lucide-react";
import type { StringResponse } from "@/api/generated/api.schemas";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface StringTableProps {
strings: StringResponse[];
functionNameToId: Record<string, string>;
onSelectFunction: (functionId: string) => void;
highlightedAddress?: string;
}
export function StringTable({
strings,
functionNameToId,
onSelectFunction,
highlightedAddress,
}: StringTableProps) {
const [search, setSearch] = useState("");
const filtered = strings.filter((s) => s.value?.toLowerCase().includes(search.toLowerCase()));
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter strings..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[110px]">Address</TableHead>
<TableHead>Value</TableHead>
<TableHead className="w-[90px]">Encoding</TableHead>
<TableHead className="w-[140px]">Referenced by</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((s) => (
<TableRow
key={s.id}
className={cn(
highlightedAddress && s.address === highlightedAddress && "bg-primary/10"
)}
>
<TableCell className="font-mono text-xs">{s.address}</TableCell>
<TableCell className="max-w-[200px] truncate font-mono text-xs">
{s.value}
</TableCell>
<TableCell className="text-muted-foreground text-xs">{s.encoding}</TableCell>
<TableCell className="text-xs">
{s.referencedBy
?.split(/\s+/)
.filter(Boolean)
.map((name, i) => {
const funcId = functionNameToId[name];
return (
<span key={name}>
{i > 0 && " "}
{funcId ? (
<button
type="button"
onClick={() => onSelectFunction(funcId)}
className="text-primary hover:underline"
>
{name}
</button>
) : (
name
)}
</span>
);
})}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{search ? "No matching strings" : "No strings found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{search ? `${filtered.length} of ${strings.length} strings` : `${strings.length} strings`}
</div>
</div>
);
}

View File

@ -0,0 +1,136 @@
import { useState, Fragment } from "react";
import { Search, ChevronRight, ChevronDown } from "lucide-react";
import type { StructResponse } from "@/api/generated/api.schemas";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface StructureTableProps {
structures: StructResponse[];
}
export function StructureTable({ structures }: StructureTableProps) {
const [search, setSearch] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const filtered = structures.filter(
(s) => !search || s.name?.toLowerCase().includes(search.toLowerCase())
);
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter structures..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[28px]" />
<TableHead>Name</TableHead>
<TableHead className="w-[80px] text-right">Size</TableHead>
<TableHead className="w-[90px]">Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((s) => {
const expanded = !!(s.id && expandedIds.has(s.id));
return (
<Fragment key={s.id}>
<TableRow className="cursor-pointer" onClick={() => s.id && toggleExpanded(s.id)}>
<TableCell className="px-1">
{expanded ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
)}
</TableCell>
<TableCell className="font-mono text-xs">{s.name}</TableCell>
<TableCell className="text-right font-mono text-xs">{s.size}</TableCell>
<TableCell>
<Badge
variant="outline"
className={
s.isUnion
? "border-purple-500 text-[10px] text-purple-400"
: "border-blue-500 text-[10px] text-blue-400"
}
>
{s.isUnion ? "Union" : "Struct"}
</Badge>
</TableCell>
</TableRow>
{expanded && s.members && s.members.length > 0 && (
<TableRow key={`members-${s.id}`}>
<TableCell colSpan={4} className="bg-muted/30 p-0">
<div className="flex flex-col">
<div className="text-muted-foreground grid shrink-0 grid-cols-[60px_1fr_50px_90px] px-2 text-xs">
<span>Offset</span>
<span>Name</span>
<span>Size</span>
<span>Type</span>
</div>
<div className="max-h-40 overflow-y-auto">
{s.members.map((m, mi) => (
<div
key={mi}
className="even:bg-muted/30 grid grid-cols-[60px_1fr_50px_90px] px-2 font-mono text-xs"
>
<span>{m.offset}</span>
<span className="truncate">{m.name}</span>
<span>{m.size}</span>
<span>{m.type}</span>
</div>
))}
</div>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{search ? "No matching structures" : "No structures found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{search
? `${filtered.length} of ${structures.length} structures`
: `${structures.length} structures`}
</div>
</div>
);
}

View File

@ -1,14 +1,24 @@
"use client"; "use client";
import { ArrowUpRight, ArrowDownRight, Link, Loader2, GitFork, GripHorizontal } from "lucide-react"; import {
ArrowUpRight,
ArrowDownRight,
Link,
Loader2,
GitFork,
GripHorizontal,
Text,
} from "lucide-react";
import { useGetCallers, useGetCallees } from "@/api/generated/function/function.ts"; import { useGetCallers, useGetCallees } from "@/api/generated/function/function.ts";
import type { XrefResponse } from "@/api/generated/api.schemas"; import type { XrefResponse, StringResponse } from "@/api/generated/api.schemas";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
interface XrefPanelProps { interface XrefPanelProps {
functionId?: string; functionId?: string;
onSelectFunction: (functionId: string) => void; onSelectFunction: (functionId: string) => void;
strings?: StringResponse[];
onSelectString?: (address: string) => void;
} }
function XrefTypeBadge({ type }: { type?: string }) { function XrefTypeBadge({ type }: { type?: string }) {
@ -108,7 +118,12 @@ function XrefList({
); );
} }
export function XrefPanel({ functionId, onSelectFunction }: XrefPanelProps) { export function XrefPanel({
functionId,
onSelectFunction,
strings,
onSelectString,
}: XrefPanelProps) {
const { data: callers, isLoading: callersLoading } = useGetCallers(functionId ?? "", { const { data: callers, isLoading: callersLoading } = useGetCallers(functionId ?? "", {
query: { enabled: !!functionId }, query: { enabled: !!functionId },
}); });
@ -146,6 +161,41 @@ export function XrefPanel({ functionId, onSelectFunction }: XrefPanelProps) {
onSelectFunction={onSelectFunction} onSelectFunction={onSelectFunction}
isLoading={calleesLoading} isLoading={calleesLoading}
/> />
{strings && (
<>
<div className="border-border mx-3 border-t" />
<div className="px-3 py-2">
<h4 className="text-muted-foreground mb-1.5 flex items-center gap-1.5 text-xs font-medium uppercase">
<Text className="h-3 w-3" />
Strings
<span className="text-muted-foreground/60 ml-auto">{strings.length}</span>
</h4>
{strings.length === 0 ? (
<p className="text-muted-foreground/50 py-2 text-center text-xs italic">None</p>
) : (
<div className="space-y-0.5">
{strings.map((s, i) => (
<button
key={s.id ?? i}
type="button"
onClick={() => s.address && onSelectString?.(s.address)}
disabled={!s.address || !onSelectString}
className="hover:bg-muted flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors disabled:opacity-50"
>
<span className="text-foreground flex-1 truncate font-mono">{s.value}</span>
{s.address && (
<span className="text-muted-foreground shrink-0 font-mono text-[10px]">
{s.address}
</span>
)}
</button>
))}
</div>
)}
</div>
</>
)}
</ScrollArea> </ScrollArea>
)} )}
</div> </div>

View File

@ -4,8 +4,17 @@ import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useGetBinary, useAnalyzeBinary } from "@/api/generated/binary/binary.ts"; import { useGetBinary, useAnalyzeBinary } from "@/api/generated/binary/binary.ts";
import { useGetProject } from "@/api/generated/project/project.ts"; import { useGetProject } from "@/api/generated/project/project.ts";
import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts"; import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts";
import { useListAnalyses, useListSegments } from "@/api/generated/analysis/analysis.ts"; import {
useListAnalyses,
useListSegments,
useListStrings,
useListImports,
useListGlobalVars,
useListStructures,
useListEnums,
} from "@/api/generated/analysis/analysis.ts";
import { useGetFunction } from "@/api/generated/function/function.ts"; import { useGetFunction } from "@/api/generated/function/function.ts";
import { useListEngines } from "@/api/generated/engine/engine.ts";
import { customInstance } from "@/api/api.ts"; import { customInstance } from "@/api/api.ts";
import type { import type {
FunctionSummaryResponse, FunctionSummaryResponse,
@ -42,7 +51,7 @@ export function useBinaryPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [selectedFunctionId, setSelectedFunctionId] = useState<string>(); const [selectedFunctionId, setSelectedFunctionId] = useState<string>();
const [confirmReanalyzeOpen, setConfirmReanalyzeOpen] = useState(false); const [analyzeDialogOpen, setAnalyzeDialogOpen] = useState(false);
const [selectedSegmentId, setSelectedSegmentId] = useState<string>(); const [selectedSegmentId, setSelectedSegmentId] = useState<string>();
const [selectedFlags, setSelectedFlags] = useState<Set<string>>(new Set()); const [selectedFlags, setSelectedFlags] = useState<Set<string>>(new Set());
@ -75,6 +84,31 @@ export function useBinaryPage() {
query: { enabled: !!analysisId }, query: { enabled: !!analysisId },
}); });
// Fetch strings for the analysis
const { data: strings } = useListStrings(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Fetch imports for the analysis
const { data: imports } = useListImports(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Fetch global vars for the analysis
const { data: globalVars } = useListGlobalVars(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Fetch structures for the analysis
const { data: structures } = useListStructures(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Fetch enums for the analysis
const { data: enums } = useListEnums(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Infinite query for functions pagination — single page with all functions // Infinite query for functions pagination — single page with all functions
const functionsQuery = useInfiniteQuery({ const functionsQuery = useInfiniteQuery({
queryKey: ["functions", analysisId], queryKey: ["functions", analysisId],
@ -96,6 +130,9 @@ export function useBinaryPage() {
{ query: { enabled: !!selectedFunctionId } } { query: { enabled: !!selectedFunctionId } }
); );
// Fetch available engines
const { data: engines, isLoading: enginesLoading } = useListEngines();
// Analyze / re-analyze mutation // Analyze / re-analyze mutation
const analyzeMutation = useAnalyzeBinary({ const analyzeMutation = useAnalyzeBinary({
mutation: { mutation: {
@ -107,17 +144,13 @@ export function useBinaryPage() {
}, },
}); });
const handleAnalyze = () => { const handleOpenAnalyzeDialog = () => {
analyzeMutation.mutate({ binaryId, params: { engine: "IDA5" } }); setAnalyzeDialogOpen(true);
}; };
const handleReanalyzeRequest = () => { const handleAnalyzeConfirm = (engine: string) => {
setConfirmReanalyzeOpen(true); setAnalyzeDialogOpen(false);
}; analyzeMutation.mutate({ binaryId, params: { engine } });
const handleReanalyzeConfirm = () => {
setConfirmReanalyzeOpen(false);
handleAnalyze();
}; };
// Flatten all pages into a single array // Flatten all pages into a single array
@ -138,6 +171,25 @@ export function useBinaryPage() {
return map; return map;
}, [allFunctions, segmentsList]); }, [allFunctions, segmentsList]);
// Build map: function name → id (for resolving referencedBy in strings)
const functionNameToId = useMemo(() => {
const map: Record<string, string> = {};
for (const f of allFunctions) {
if (f.name && f.id) map[f.name] = f.id;
}
return map;
}, [allFunctions]);
// Strings referenced by the currently selected function
const stringsList = useMemo(() => strings ?? [], [strings]);
const functionStrings = useMemo(() => {
if (!selectedFunction?.name) return [];
return stringsList.filter((s) => {
if (!s.referencedBy) return false;
return s.referencedBy.split(/\s+/).includes(selectedFunction.name!);
});
}, [stringsList, selectedFunction]);
// Filter functions by segment, flags, and text search (text search is in FunctionTree) // Filter functions by segment, flags, and text search (text search is in FunctionTree)
const filteredFunctions = useMemo(() => { const filteredFunctions = useMemo(() => {
let result = allFunctions; let result = allFunctions;
@ -214,13 +266,21 @@ export function useBinaryPage() {
isFetchingNextPage: functionsQuery.isFetchingNextPage, isFetchingNextPage: functionsQuery.isFetchingNextPage,
functionsLoading: functionsQuery.isLoading, functionsLoading: functionsQuery.isLoading,
functionsTotal: functionsQuery.data?.pages[0]?.totalElements ?? 0, functionsTotal: functionsQuery.data?.pages[0]?.totalElements ?? 0,
strings: stringsList,
imports: imports ?? [],
globalVars: globalVars ?? [],
structures: structures ?? [],
enums: enums ?? [],
functionNameToId,
functionStrings,
selectedFunction, selectedFunction,
selectedFunctionLoading, selectedFunctionLoading,
analyzeMutation, analyzeMutation,
confirmReanalyzeOpen, analyzeDialogOpen,
setConfirmReanalyzeOpen, setAnalyzeDialogOpen,
handleAnalyze, handleOpenAnalyzeDialog,
handleReanalyzeRequest, handleAnalyzeConfirm,
handleReanalyzeConfirm, engines: engines ?? [],
enginesLoading,
}; };
} }

View File

@ -13,6 +13,8 @@ interface FileUploadProps {
accept?: string; accept?: string;
maxSize?: number; maxSize?: number;
className?: string; className?: string;
autoUpload?: boolean;
onFileSelect?: (file: File) => void;
} }
export const FileUpload = ({ export const FileUpload = ({
@ -23,6 +25,8 @@ export const FileUpload = ({
accept = ".exe,.com,.dll,.bin", accept = ".exe,.com,.dll,.bin",
maxSize = 50 * 1024 * 1024, maxSize = 50 * 1024 * 1024,
className, className,
autoUpload = true,
onFileSelect,
}: FileUploadProps) => { }: FileUploadProps) => {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@ -49,9 +53,13 @@ export const FileUpload = ({
setValidationError(null); setValidationError(null);
setFile(file); setFile(file);
onUpload(file); if (autoUpload) {
onUpload(file);
} else {
onFileSelect?.(file);
}
}, },
[onUpload, maxSize] [onUpload, maxSize, autoUpload, onFileSelect]
); );
const handleDrop = useCallback( const handleDrop = useCallback(

View File

@ -1,3 +1,4 @@
import { useState } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -8,14 +9,19 @@ import {
} from "@/components/ui/dialog.tsx"; } from "@/components/ui/dialog.tsx";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { FileUpload } from "@/features/project/FileUpload.tsx"; import { FileUpload } from "@/features/project/FileUpload.tsx";
import { EngineSelect } from "@/components/EngineSelect";
import { Upload } from "lucide-react";
import type { EngineResponse } from "@/api/generated/api.schemas";
interface UploadBinaryDialogProps { interface UploadBinaryDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onUpload: (file: File) => void; onUpload: (file: File, engine?: string) => void;
isUploading: boolean; isUploading: boolean;
uploadError: string | null; uploadError: string | null;
onReset: () => void; onReset: () => void;
engines: EngineResponse[];
enginesLoading: boolean;
} }
export const UploadBinaryDialog = ({ export const UploadBinaryDialog = ({
@ -25,7 +31,31 @@ export const UploadBinaryDialog = ({
isUploading, isUploading,
uploadError, uploadError,
onReset, onReset,
engines,
enginesLoading,
}: UploadBinaryDialogProps) => { }: UploadBinaryDialogProps) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedEngine, setSelectedEngine] = useState<string>();
const handleFileSelect = (file: File) => {
setSelectedFile(file);
};
const handleEngineChange = (engine: string) => {
setSelectedEngine(engine || undefined);
};
const handleUpload = () => {
if (!selectedFile) return;
onUpload(selectedFile, selectedEngine);
};
const handleReset = () => {
setSelectedFile(null);
setSelectedEngine(undefined);
onReset();
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-lg">
@ -45,19 +75,52 @@ export const UploadBinaryDialog = ({
<span className="text-sm font-medium">Select Binary File</span> <span className="text-sm font-medium">Select Binary File</span>
</div> </div>
<FileUpload <FileUpload
onUpload={onUpload} onUpload={() => {}}
onFileSelect={handleFileSelect}
isUploading={isUploading} isUploading={isUploading}
error={uploadError} error={uploadError}
onReset={onReset} onReset={handleReset}
accept=".exe,.com,.dll,.bin,.elf" accept=".exe,.com,.dll,.bin,.elf"
maxSize={100 * 1024 * 1024} maxSize={100 * 1024 * 1024}
autoUpload={false}
/> />
</div> </div>
{selectedFile && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="bg-muted text-muted-foreground flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium">
2
</div>
<span className="text-sm font-medium">Analysis Engine</span>
<span className="text-muted-foreground text-xs">(optional)</span>
</div>
<EngineSelect
engines={engines}
value={selectedEngine}
onChange={handleEngineChange}
isLoading={enginesLoading}
placeholder="Select engine (optional)"
/>
</div>
)}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isUploading}> <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isUploading}>
Cancel Cancel
</Button> </Button>
{selectedFile && (
<Button onClick={handleUpload} disabled={isUploading}>
{isUploading ? (
<>Uploading...</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload
</>
)}
</Button>
)}
</DialogFooter> </DialogFooter>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -4,6 +4,7 @@ import { useGetProject } from "@/api/generated/project/project.ts";
import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts"; import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts";
import { useDeleteBinary, useGetBinaries, useUploadBinary } from "@/api/generated/binary/binary.ts"; import { useDeleteBinary, useGetBinaries, useUploadBinary } from "@/api/generated/binary/binary.ts";
import { downloadBinary } from "@/api/generated/binary/binary.ts"; import { downloadBinary } from "@/api/generated/binary/binary.ts";
import { useListEngines } from "@/api/generated/engine/engine.ts";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import type { BinaryResponse } from "@/api/generated/api.schemas.ts"; import type { BinaryResponse } from "@/api/generated/api.schemas.ts";
@ -28,6 +29,8 @@ export function useProjectPage(projectId: string) {
{ query: { enabled: !!projectId } } { query: { enabled: !!projectId } }
); );
const { data: engines, isLoading: enginesLoading } = useListEngines();
const { mutate: mutateDeleteBinary } = useDeleteBinary({ const { mutate: mutateDeleteBinary } = useDeleteBinary({
mutation: { mutation: {
onSuccess: () => { onSuccess: () => {
@ -69,8 +72,8 @@ export function useProjectPage(projectId: string) {
setBinaryToDelete(null); setBinaryToDelete(null);
}; };
const handleUploadBinary = (file: File) => { const handleUploadBinary = (file: File, engine?: string) => {
uploadMutation.mutate({ projectId, data: { file } }); uploadMutation.mutate({ projectId, data: { file }, params: engine ? { engine } : undefined });
}; };
const handleDownloadBinary = async (binary: BinaryResponse) => { const handleDownloadBinary = async (binary: BinaryResponse) => {
@ -104,5 +107,7 @@ export function useProjectPage(projectId: string) {
uploadError, uploadError,
handleUploadBinary, handleUploadBinary,
handleDownloadBinary, handleDownloadBinary,
engines: engines ?? [],
enginesLoading,
}; };
} }

View File

@ -1,10 +1,15 @@
import { ArrowLeft } from "lucide-react"; import { useState } from "react";
import { ArrowLeft, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/EmptyState"; import { EmptyState } from "@/components/EmptyState";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { Breadcrumbs } from "@/components/Breadcrumbs.tsx"; import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx"; import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx";
import { FunctionTree } from "@/features/binary/FunctionTree"; import { FunctionTree } from "@/features/binary/FunctionTree";
import { StringTable } from "@/features/binary/StringTable";
import { ImportTable } from "@/features/binary/ImportTable";
import { GlobalVarTable } from "@/features/binary/GlobalVarTable";
import { StructureTable } from "@/features/binary/StructureTable";
import { EnumTable } from "@/features/binary/EnumTable";
import { XrefPanel } from "@/features/binary/XrefPanel"; import { XrefPanel } from "@/features/binary/XrefPanel";
import { import {
ResizableHandle, ResizableHandle,
@ -14,6 +19,15 @@ import {
import { useBinaryPage } from "@/features/binary/hooks/useBinaryPage"; import { useBinaryPage } from "@/features/binary/hooks/useBinaryPage";
import { CodeViewer } from "@/features/binary/CodeViewer"; import { CodeViewer } from "@/features/binary/CodeViewer";
import { AIChatPanel } from "@/features/binary/AIChatPanel"; import { AIChatPanel } from "@/features/binary/AIChatPanel";
import { AnalyzeDialog } from "@/features/binary/AnalyzeDialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
const Binary = () => { const Binary = () => {
const { const {
@ -36,14 +50,22 @@ const Binary = () => {
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
functionsTotal, functionsTotal,
strings,
functionNameToId,
functionStrings,
selectedFunction, selectedFunction,
selectedFunctionLoading, selectedFunctionLoading,
analyzeMutation, analyzeMutation,
confirmReanalyzeOpen, analyzeDialogOpen,
setConfirmReanalyzeOpen, setAnalyzeDialogOpen,
handleAnalyze, handleOpenAnalyzeDialog,
handleReanalyzeRequest, handleAnalyzeConfirm,
handleReanalyzeConfirm, engines,
enginesLoading,
imports,
globalVars,
structures,
enums,
} = useBinaryPage(); } = useBinaryPage();
const navigateToEntryPoint = () => { const navigateToEntryPoint = () => {
@ -52,6 +74,33 @@ const Binary = () => {
if (match) selectFunctionByAddress(match[1]); if (match) selectFunctionByAddress(match[1]);
}; };
const [activeTab, setActiveTab] = useState("functions");
const [highlightedStringAddress, setHighlightedStringAddress] = useState<string>();
const handleSelectStringInXref = (address: string) => {
setActiveTab("strings");
setHighlightedStringAddress(address);
};
const handleTabChange = (value: string) => {
setActiveTab(value);
setHighlightedStringAddress(undefined);
};
// Build data tabs for the "More" dropdown
const dataTabs = [
...(imports && imports.length > 0 ? ([{ value: "imports", label: "Imports" }] as const) : []),
...(globalVars && globalVars.length > 0
? ([{ value: "global-vars", label: "Global Vars" }] as const)
: []),
...(structures && structures.length > 0
? ([{ value: "structures", label: "Structures" }] as const)
: []),
...(enums && enums.length > 0 ? ([{ value: "enums", label: "Enums" }] as const) : []),
];
const hasDataTabs = dataTabs.length > 0;
const isDataTabActive = dataTabs.some((t) => t.value === activeTab);
if (binaryLoading) return null; if (binaryLoading) return null;
if (!binary) { if (!binary) {
@ -91,7 +140,7 @@ const Binary = () => {
binary={binary} binary={binary}
hasAnalysis={binary.staticAnalysisDone} hasAnalysis={binary.staticAnalysisDone}
isAnalyzing={analyzeMutation.isPending} isAnalyzing={analyzeMutation.isPending}
onAnalyze={binary.staticAnalysisDone ? handleReanalyzeRequest : handleAnalyze} onAnalyze={handleOpenAnalyzeDialog}
entryPoint={analysis?.entryPoint} entryPoint={analysis?.entryPoint}
onNavigateToEntryPoint={navigateToEntryPoint} onNavigateToEntryPoint={navigateToEntryPoint}
/> />
@ -103,24 +152,112 @@ const Binary = () => {
<ResizablePanelGroup orientation="vertical" className="h-full min-h-0 flex-col"> <ResizablePanelGroup orientation="vertical" className="h-full min-h-0 flex-col">
<ResizablePanel id="function-tree-panel" defaultSize={100} minSize={30}> <ResizablePanel id="function-tree-panel" defaultSize={100} minSize={30}>
<div className="flex h-full flex-col overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
<div className="border-border flex shrink-0 items-center border-b px-4 py-2"> <Tabs
<span className="text-sm font-medium">Functions</span> value={activeTab}
</div> onValueChange={handleTabChange}
<FunctionTree className="flex min-h-0 flex-1 flex-col"
functions={functions} >
selectedFunctionId={selectedFunctionId} <TabsList className="mx-auto mt-2">
onSelectFunction={(f) => setSelectedFunctionId(f.id)} <TabsTrigger value="functions">Functions</TabsTrigger>
onLoadMore={fetchNextPage} <TabsTrigger value="strings">Strings</TabsTrigger>
hasMore={!!hasNextPage} {hasDataTabs && (
isLoading={isFetchingNextPage} <DropdownMenu>
totalCount={functionsTotal} <DropdownMenuTrigger asChild>
segments={segments} <button
selectedSegmentId={selectedSegmentId} className={cn(
onSelectSegment={setSelectedSegmentId} "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
segmentByAddress={segmentByAddress} isDataTabActive && "bg-background text-foreground shadow-sm"
selectedFlags={selectedFlags} )}
onToggleFlag={toggleFlag} >
/> {isDataTabActive
? dataTabs.find((t) => t.value === activeTab)?.label
: "More"}
<ChevronDown className="ml-1 h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{dataTabs.map((tab) => (
<DropdownMenuItem
key={tab.value}
onClick={() => handleTabChange(tab.value)}
>
<span className={activeTab === tab.value ? "font-medium" : ""}>
{tab.label}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</TabsList>
<TabsContent
value="functions"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<FunctionTree
functions={functions}
selectedFunctionId={selectedFunctionId}
onSelectFunction={(f) => setSelectedFunctionId(f.id)}
onLoadMore={fetchNextPage}
hasMore={!!hasNextPage}
isLoading={isFetchingNextPage}
totalCount={functionsTotal}
segments={segments}
selectedSegmentId={selectedSegmentId}
onSelectSegment={setSelectedSegmentId}
segmentByAddress={segmentByAddress}
selectedFlags={selectedFlags}
onToggleFlag={toggleFlag}
/>
</TabsContent>
<TabsContent
value="strings"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<StringTable
strings={strings ?? []}
functionNameToId={functionNameToId}
onSelectFunction={setSelectedFunctionId}
highlightedAddress={highlightedStringAddress}
/>
</TabsContent>
{imports && imports.length > 0 && (
<TabsContent
value="imports"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<ImportTable imports={imports} />
</TabsContent>
)}
{globalVars && globalVars.length > 0 && (
<TabsContent
value="global-vars"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<GlobalVarTable
globalVars={globalVars}
functionNameToId={functionNameToId}
onSelectFunction={setSelectedFunctionId}
/>
</TabsContent>
)}
{structures && structures.length > 0 && (
<TabsContent
value="structures"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<StructureTable structures={structures} />
</TabsContent>
)}
{enums && enums.length > 0 && (
<TabsContent
value="enums"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<EnumTable enums={enums} />
</TabsContent>
)}
</Tabs>
</div> </div>
</ResizablePanel> </ResizablePanel>
@ -140,6 +277,8 @@ const Binary = () => {
<XrefPanel <XrefPanel
functionId={selectedFunctionId} functionId={selectedFunctionId}
onSelectFunction={setSelectedFunctionId} onSelectFunction={setSelectedFunctionId}
strings={functionStrings}
onSelectString={handleSelectStringInXref}
/> />
</div> </div>
</ResizablePanel> </ResizablePanel>
@ -151,7 +290,12 @@ const Binary = () => {
<ResizablePanel id="code-panel" defaultSize="40%" minSize="30%"> <ResizablePanel id="code-panel" defaultSize="40%" minSize="30%">
<div className="relative flex h-full flex-col overflow-hidden bg-(--code-bg)"> <div className="relative flex h-full flex-col overflow-hidden bg-(--code-bg)">
<CodeViewer func={selectedFunction} isLoading={selectedFunctionLoading} /> <CodeViewer
func={selectedFunction}
isLoading={selectedFunctionLoading}
onSelectFunction={setSelectedFunctionId}
functionNameToId={functionNameToId}
/>
</div> </div>
</ResizablePanel> </ResizablePanel>
@ -165,14 +309,14 @@ const Binary = () => {
</ResizablePanelGroup> </ResizablePanelGroup>
</div> </div>
<ConfirmDialog <AnalyzeDialog
open={confirmReanalyzeOpen} key={analyzeDialogOpen ? "analyze-open" : "analyze-closed"}
onOpenChange={setConfirmReanalyzeOpen} open={analyzeDialogOpen}
title="Re-analyze binary" onOpenChange={setAnalyzeDialogOpen}
description="This will replace all current analysis data. Function names, comments, and decompiled code will be lost. Are you sure?" isReanalyze={!!binary.staticAnalysisDone}
confirmLabel="Re-analyze" engines={engines}
variant="destructive" enginesLoading={enginesLoading}
onConfirm={handleReanalyzeConfirm} onConfirm={handleAnalyzeConfirm}
/> />
</div> </div>
); );

View File

@ -32,6 +32,8 @@ const Project = () => {
uploadError, uploadError,
handleUploadBinary, handleUploadBinary,
handleDownloadBinary, handleDownloadBinary,
engines,
enginesLoading,
} = useProjectPage(projectId!); } = useProjectPage(projectId!);
if (!project || !workspace) { if (!project || !workspace) {
@ -111,12 +113,15 @@ const Project = () => {
)} )}
<UploadBinaryDialog <UploadBinaryDialog
key={uploadDialogOpen ? "upload-open" : "upload-closed"}
open={uploadDialogOpen} open={uploadDialogOpen}
onOpenChange={handleCloseUploadDialog} onOpenChange={handleCloseUploadDialog}
onUpload={handleUploadBinary} onUpload={handleUploadBinary}
isUploading={uploadMutation.isPending} isUploading={uploadMutation.isPending}
uploadError={uploadError} uploadError={uploadError}
onReset={uploadMutation.reset} onReset={uploadMutation.reset}
engines={engines}
enginesLoading={enginesLoading}
/> />
<ConfirmDialog <ConfirmDialog