feat/ida66 #3
@ -20,11 +20,13 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
AnalysisResponse,
|
AnalysisResponse,
|
||||||
DataLabelResponse,
|
DataLabelResponse,
|
||||||
|
EnumResponse,
|
||||||
ImportResponse,
|
ImportResponse,
|
||||||
ListFunctionsParams,
|
ListFunctionsParams,
|
||||||
PageFunctionSummaryResponse,
|
PageFunctionSummaryResponse,
|
||||||
SegmentResponse,
|
SegmentResponse,
|
||||||
StringResponse,
|
StringResponse,
|
||||||
|
StructResponse,
|
||||||
} from "../api.schemas";
|
} from "../api.schemas";
|
||||||
|
|
||||||
import { customInstance } from "../../api";
|
import { customInstance } from "../../api";
|
||||||
@ -251,6 +253,132 @@ 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.
|
* Retrieve all strings extracted during a static analysis.
|
||||||
* @summary List strings in an analysis
|
* @summary List strings in an analysis
|
||||||
@ -839,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 };
|
||||||
|
}
|
||||||
|
|||||||
@ -186,6 +186,21 @@ 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 {
|
export interface StringResponse {
|
||||||
id?: string;
|
id?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
@ -270,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)
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,6 +10,8 @@ import {
|
|||||||
useListStrings,
|
useListStrings,
|
||||||
useListImports,
|
useListImports,
|
||||||
useListGlobalVars,
|
useListGlobalVars,
|
||||||
|
useListStructures,
|
||||||
|
useListEnums,
|
||||||
} from "@/api/generated/analysis/analysis.ts";
|
} 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 { useListEngines } from "@/api/generated/engine/engine.ts";
|
||||||
@ -97,6 +99,16 @@ export function useBinaryPage() {
|
|||||||
query: { enabled: !!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],
|
||||||
@ -257,6 +269,8 @@ export function useBinaryPage() {
|
|||||||
strings: stringsList,
|
strings: stringsList,
|
||||||
imports: imports ?? [],
|
imports: imports ?? [],
|
||||||
globalVars: globalVars ?? [],
|
globalVars: globalVars ?? [],
|
||||||
|
structures: structures ?? [],
|
||||||
|
enums: enums ?? [],
|
||||||
functionNameToId,
|
functionNameToId,
|
||||||
functionStrings,
|
functionStrings,
|
||||||
selectedFunction,
|
selectedFunction,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ArrowLeft } from "lucide-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 { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
|
import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
|
||||||
@ -8,6 +8,8 @@ import { FunctionTree } from "@/features/binary/FunctionTree";
|
|||||||
import { StringTable } from "@/features/binary/StringTable";
|
import { StringTable } from "@/features/binary/StringTable";
|
||||||
import { ImportTable } from "@/features/binary/ImportTable";
|
import { ImportTable } from "@/features/binary/ImportTable";
|
||||||
import { GlobalVarTable } from "@/features/binary/GlobalVarTable";
|
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,
|
||||||
@ -19,6 +21,13 @@ 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 { AnalyzeDialog } from "@/features/binary/AnalyzeDialog";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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 {
|
||||||
@ -55,6 +64,8 @@ const Binary = () => {
|
|||||||
enginesLoading,
|
enginesLoading,
|
||||||
imports,
|
imports,
|
||||||
globalVars,
|
globalVars,
|
||||||
|
structures,
|
||||||
|
enums,
|
||||||
} = useBinaryPage();
|
} = useBinaryPage();
|
||||||
|
|
||||||
const navigateToEntryPoint = () => {
|
const navigateToEntryPoint = () => {
|
||||||
@ -76,6 +87,20 @@ const Binary = () => {
|
|||||||
setHighlightedStringAddress(undefined);
|
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) {
|
||||||
@ -132,14 +157,37 @@ const Binary = () => {
|
|||||||
onValueChange={handleTabChange}
|
onValueChange={handleTabChange}
|
||||||
className="flex min-h-0 flex-1 flex-col"
|
className="flex min-h-0 flex-1 flex-col"
|
||||||
>
|
>
|
||||||
<TabsList className="mx-3 mt-2 shrink-0">
|
<TabsList className="mx-auto mt-2">
|
||||||
<TabsTrigger value="functions">Functions</TabsTrigger>
|
<TabsTrigger value="functions">Functions</TabsTrigger>
|
||||||
<TabsTrigger value="strings">Strings</TabsTrigger>
|
<TabsTrigger value="strings">Strings</TabsTrigger>
|
||||||
{imports && imports.length > 0 && (
|
{hasDataTabs && (
|
||||||
<TabsTrigger value="imports">Imports</TabsTrigger>
|
<DropdownMenu>
|
||||||
)}
|
<DropdownMenuTrigger asChild>
|
||||||
{globalVars && globalVars.length > 0 && (
|
<button
|
||||||
<TabsTrigger value="global-vars">Global Vars</TabsTrigger>
|
className={cn(
|
||||||
|
"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",
|
||||||
|
isDataTabActive && "bg-background text-foreground shadow-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
@ -193,6 +241,22 @@ const Binary = () => {
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user