feat/ida66 #3
16
package-lock.json
generated
16
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 };
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
120
src/api/generated/engine/engine.ts
Normal file
120
src/api/generated/engine/engine.ts
Normal 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 };
|
||||||
|
}
|
||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
57
src/components/EngineSelect.tsx
Normal file
57
src/components/EngineSelect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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}
|
||||||
|
|||||||
96
src/features/binary/AnalyzeDialog.tsx
Normal file
96
src/features/binary/AnalyzeDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
354
src/features/binary/CfgGraph.tsx
Normal file
354
src/features/binary/CfgGraph.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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}> </td>
|
<td colSpan={4}> </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}> </td>
|
<td colSpan={3}> </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>;
|
||||||
})}
|
})}
|
||||||
|
|||||||
121
src/features/binary/EnumTable.tsx
Normal file
121
src/features/binary/EnumTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
src/features/binary/GlobalVarTable.tsx
Normal file
158
src/features/binary/GlobalVarTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/features/binary/ImportTable.tsx
Normal file
75
src/features/binary/ImportTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/features/binary/StringTable.tsx
Normal file
110
src/features/binary/StringTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
136
src/features/binary/StructureTable.tsx
Normal file
136
src/features/binary/StructureTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user