feat/ida66 #3
@ -20,11 +20,13 @@ import type {
|
||||
import type {
|
||||
AnalysisResponse,
|
||||
DataLabelResponse,
|
||||
EnumResponse,
|
||||
ImportResponse,
|
||||
ListFunctionsParams,
|
||||
PageFunctionSummaryResponse,
|
||||
SegmentResponse,
|
||||
StringResponse,
|
||||
StructResponse,
|
||||
} from "../api.schemas";
|
||||
|
||||
import { customInstance } from "../../api";
|
||||
@ -251,6 +253,132 @@ export function useGetAnalysis<TData = Awaited<ReturnType<typeof getAnalysis>>,
|
||||
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
|
||||
@ -839,3 +967,113 @@ export function useListFunctions<
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
@ -270,6 +285,18 @@ export interface PageFunctionSummaryResponse {
|
||||
empty?: boolean;
|
||||
}
|
||||
|
||||
export interface EnumConstResponse {
|
||||
name?: string;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export interface EnumResponse {
|
||||
id?: string;
|
||||
name?: string;
|
||||
width?: number;
|
||||
constants?: EnumConstResponse[];
|
||||
}
|
||||
|
||||
export type GetWorkspacesParams = {
|
||||
/**
|
||||
* 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,
|
||||
useListImports,
|
||||
useListGlobalVars,
|
||||
useListStructures,
|
||||
useListEnums,
|
||||
} from "@/api/generated/analysis/analysis.ts";
|
||||
import { useGetFunction } from "@/api/generated/function/function.ts";
|
||||
import { useListEngines } from "@/api/generated/engine/engine.ts";
|
||||
@ -97,6 +99,16 @@ export function useBinaryPage() {
|
||||
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
|
||||
const functionsQuery = useInfiniteQuery({
|
||||
queryKey: ["functions", analysisId],
|
||||
@ -257,6 +269,8 @@ export function useBinaryPage() {
|
||||
strings: stringsList,
|
||||
imports: imports ?? [],
|
||||
globalVars: globalVars ?? [],
|
||||
structures: structures ?? [],
|
||||
enums: enums ?? [],
|
||||
functionNameToId,
|
||||
functionStrings,
|
||||
selectedFunction,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { ArrowLeft, ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
|
||||
@ -8,6 +8,8 @@ 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 {
|
||||
ResizableHandle,
|
||||
@ -19,6 +21,13 @@ import { CodeViewer } from "@/features/binary/CodeViewer";
|
||||
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 {
|
||||
@ -55,6 +64,8 @@ const Binary = () => {
|
||||
enginesLoading,
|
||||
imports,
|
||||
globalVars,
|
||||
structures,
|
||||
enums,
|
||||
} = useBinaryPage();
|
||||
|
||||
const navigateToEntryPoint = () => {
|
||||
@ -76,6 +87,20 @@ const Binary = () => {
|
||||
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 (!binary) {
|
||||
@ -132,14 +157,37 @@ const Binary = () => {
|
||||
onValueChange={handleTabChange}
|
||||
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="strings">Strings</TabsTrigger>
|
||||
{imports && imports.length > 0 && (
|
||||
<TabsTrigger value="imports">Imports</TabsTrigger>
|
||||
)}
|
||||
{globalVars && globalVars.length > 0 && (
|
||||
<TabsTrigger value="global-vars">Global Vars</TabsTrigger>
|
||||
{hasDataTabs && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
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>
|
||||
<TabsContent
|
||||
@ -193,6 +241,22 @@ const Binary = () => {
|
||||
/>
|
||||
</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>
|
||||
</ResizablePanel>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user