diff --git a/pyproject.toml b/pyproject.toml index 875799e0..7d63f887 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,8 @@ dependencies = [ "requests>=2.32", "loguru>=0.7", "pydantic", - "revengai>=4.20.0", - "libbs>=2.16.5", + "revengai>=4.72.2", + "libbs>=3.8.1", ] [build-system] diff --git a/reai_toolkit/app/components/dialogs/matching_dialog.py b/reai_toolkit/app/components/dialogs/matching_dialog.py index 1e121d84..34cf5d33 100644 --- a/reai_toolkit/app/components/dialogs/matching_dialog.py +++ b/reai_toolkit/app/components/dialogs/matching_dialog.py @@ -4,7 +4,7 @@ from loguru import logger from revengai.models import ( - BinarySearchResult, + BinarySearchResultBody, CollectionListItemBody, FunctionMatch, MatchedFunction, @@ -178,7 +178,7 @@ def __init__( self._initCollectionsHeader() # ----------------- Binaries state ----------------- - self._selected_binaries: dict[str, BinarySearchResult] = {} + self._selected_binaries: dict[str, BinarySearchResultBody] = {} self._binariesDebounce = QtCore.QTimer(self) self._binariesDebounce.setSingleShot(True) self._binariesDebounce.timeout.connect(self._performBinariesSearch) @@ -557,7 +557,7 @@ def _performBinariesSearch(self): text = getattr(self.ui, "editBinaries", None) query = text.text().strip() if text else "" - response: GenericApiReturn[List[BinarySearchResult]] = ( + response: GenericApiReturn[List[BinarySearchResultBody]] = ( self.matching_service.search_binaries(text_input=query) ) @@ -593,7 +593,7 @@ def _initBinariesHeader(self): else: hdr.setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch) - def _fillBinariesPopup(self, query: str, rows: list[BinarySearchResult]): + def _fillBinariesPopup(self, query: str, rows: list[BinarySearchResultBody]): if not hasattr(self.ui, "binariesPopupView"): return view = self.ui.binariesPopupView diff --git a/reai_toolkit/app/core/utils.py b/reai_toolkit/app/core/utils.py index 54facec0..6fa7ac62 100644 --- a/reai_toolkit/app/core/utils.py +++ b/reai_toolkit/app/core/utils.py @@ -2,6 +2,8 @@ import json from pathlib import Path +from loguru import logger + from libbs.decompilers.ida.compat import execute_read, execute_write import ida_funcs @@ -14,6 +16,10 @@ from revengai.models.symbols import Symbols +MAX_MANGLED_NAME_LENGTH = 4096 +MAX_FUNCTION_BOUNDARIES = 200000 + + def parse_exception(exception: ApiException) -> BaseResponse | None: """ If the exception body is JSON and matches BaseResponse, return it. @@ -67,10 +73,38 @@ def demangle(mangled_name: str, attr: int = idc.INF_SHORT_DN) -> str: return demangled_name if demangled_name else mangled_name +def build_function_boundary( + mangled_name: str | None, start_address: int, end_address: int +) -> FunctionBoundary | None: + if not mangled_name: + return None + + if start_address < 0 or end_address < 0: + return None + + return FunctionBoundary( + mangled_name=mangled_name[:MAX_MANGLED_NAME_LENGTH], + start_address=start_address, + end_address=end_address, + ) + + +def build_symbols(base_address: int, funcs: list[FunctionBoundary]) -> Symbols: + if len(funcs) > MAX_FUNCTION_BOUNDARIES: + logger.warning( + f"RevEng.AI: Truncating function boundaries from {len(funcs)} to " + f"{MAX_FUNCTION_BOUNDARIES}" + ) + funcs = funcs[:MAX_FUNCTION_BOUNDARIES] + + return Symbols(base_address=max(int(base_address), 0), function_boundaries=funcs) + + @execute_read def collect_symbols_from_ida(inclusive_end: bool = False) -> Symbols | None: base: int = idaapi.get_imagebase() or 0 funcs: list[FunctionBoundary] = [] + skipped: int = 0 plt_section: idaapi.segment_t | None = idaapi.get_segm_by_name(".plt") @@ -86,14 +120,19 @@ def collect_symbols_from_ida(inclusive_end: bool = False) -> Symbols | None: end_ea: int = f.end_ea - 1 if inclusive_end else f.end_ea mangled_name: str = idc.get_func_name(start_ea) - funcs.append( - FunctionBoundary( - mangled_name=mangled_name, - start_address=start_ea, - end_address=end_ea, - ) + boundary: FunctionBoundary | None = build_function_boundary( + mangled_name, start_ea, end_ea ) + if boundary is None: + skipped += 1 + continue + + funcs.append(boundary) - symbols = Symbols(base_address=int(base), function_boundaries=funcs) + if skipped: + logger.warning( + f"RevEng.AI: Skipped {skipped} function boundaries with an empty name " + "or a negative address" + ) - return symbols + return build_symbols(base, funcs) diff --git a/reai_toolkit/app/services/ai_decomp/ai_decomp_service.py b/reai_toolkit/app/services/ai_decomp/ai_decomp_service.py index decd4400..f81ceacb 100644 --- a/reai_toolkit/app/services/ai_decomp/ai_decomp_service.py +++ b/reai_toolkit/app/services/ai_decomp/ai_decomp_service.py @@ -19,10 +19,8 @@ from revengai.models.summary_data import SummaryData from revengai.models.task_status import TaskStatus from revengai.models.token import Token -from revengai.models.upsert_ai_decomplation_rating_request import ( - UpsertAiDecomplationRatingRequest, -) from revengai.models.upsert_overrides_input_body import UpsertOverridesInputBody +from revengai.models.upsert_rating_input_body import UpsertRatingInputBody from revengai.models.workflow_progress import WorkflowProgress from reai_toolkit.app.core.netstore_service import SimpleNetStore @@ -239,9 +237,9 @@ def _run_rate_decomp( ) -> None: try: with self.yield_api_client(sdk_config=self.sdk_config) as api_client: - FunctionsAIDecompilationApi(api_client).upsert_ai_decompilation_rating( + FunctionsAIDecompilationApi(api_client).v3_upsert_ai_decompilation_rating( function_id=function_id, - upsert_ai_decomplation_rating_request=UpsertAiDecomplationRatingRequest( + upsert_rating_input_body=UpsertRatingInputBody( rating=rating, reason=None ), ) diff --git a/reai_toolkit/app/services/ai_decomp/stream.py b/reai_toolkit/app/services/ai_decomp/stream.py index 1c650d2a..1e106c0b 100644 --- a/reai_toolkit/app/services/ai_decomp/stream.py +++ b/reai_toolkit/app/services/ai_decomp/stream.py @@ -15,6 +15,7 @@ EVENT_RENAME_APPLIED = "rename_applied" EVENT_SOURCE_DELTA = "source_delta" EVENT_SOURCE_RESET = "source_reset" +EVENT_TYPES_APPLIED = "types_applied" EVENT_TYPES_SUGGESTED = "types_suggested" EVENT_WARNING = "warning" @@ -41,6 +42,7 @@ class DecompEvent: applied: int = 0 types: int = 0 members: int = 0 + skipped: int = 0 def _as_int(value) -> int: @@ -71,6 +73,7 @@ def normalize_event(event_name: Optional[str], payload: dict) -> Optional[Decomp applied=_as_int(payload.get("applied")), types=_as_int(payload.get("types")), members=_as_int(payload.get("members")), + skipped=_as_int(payload.get("skipped")), ) @@ -110,6 +113,8 @@ class StreamState: names_applied: int = 0 types_suggested: int = 0 members_suggested: int = 0 + types_applied: int = 0 + types_skipped: int = 0 def reset_attempt(self, attempt: int) -> None: self.attempt = attempt @@ -139,6 +144,9 @@ def apply(self, event: DecompEvent) -> None: elif event.type == EVENT_TYPES_SUGGESTED: self.types_suggested = event.types self.members_suggested = event.members + elif event.type == EVENT_TYPES_APPLIED: + self.types_applied = event.types + self.types_skipped = event.skipped elif event.type == EVENT_DECOMP_FINISHED: self.decomp_finished = True elif event.type == EVENT_ATTEMPT_FAILED: diff --git a/reai_toolkit/app/services/analysis_status/analysis_status.py b/reai_toolkit/app/services/analysis_status/analysis_status.py index 0c03c83b..fcb1e062 100644 --- a/reai_toolkit/app/services/analysis_status/analysis_status.py +++ b/reai_toolkit/app/services/analysis_status/analysis_status.py @@ -5,10 +5,10 @@ from loguru import logger from revengai import ( AnalysesCoreApi, - BaseResponseStatus, Configuration, + CreateMetadata, GetAnalysisLogsOutputBody, - StatusOutput, + OperationCreateMetadataCreateResult, ) from reai_toolkit.app.core.netstore_service import SimpleNetStore @@ -37,7 +37,7 @@ def start_polling(self, analysis_id: int, thread_callback: Callable[..., Any]) - args=(analysis_id,), ) - def _api_get_status(self, analysis_id: int) -> StatusOutput | None: + def _api_get_status(self, analysis_id: int) -> CreateMetadata | None: """ Calls the API to get the analysis status. Returns GenericApiReturn with status string on success. @@ -45,12 +45,14 @@ def _api_get_status(self, analysis_id: int) -> StatusOutput | None: with self.yield_api_client(sdk_config=self.sdk_config) as api_client: analyses_client = AnalysesCoreApi(api_client) - analysis_status: BaseResponseStatus = analyses_client.get_analysis_status(analysis_id=analysis_id) - status: StatusOutput | None = analysis_status.data - if status: - self.netstore_service.put_analysis_status(status.analysis_status) + operation: OperationCreateMetadataCreateResult = ( + analyses_client.v3_get_analysis_operation(analysis_id=analysis_id) + ) + metadata: CreateMetadata | None = operation.metadata + if metadata: + self.netstore_service.put_analysis_status(metadata.status) - return analysis_status.data + return metadata def _api_get_logs(self, analysis_id: int) -> GetAnalysisLogsOutputBody | None: with self.yield_api_client(sdk_config=self.sdk_config) as api_client: @@ -83,7 +85,7 @@ def _poll_analysis_status(self, stop_event: threading.Event, analysis_id: int) - for entry in (logs.entries or []): logger.debug(f"RevEng.AI Remote Analysis - {entry.text}") - if get_status_response.data and (status := get_status_response.data.analysis_status): + if get_status_response.data and (status := get_status_response.data.status): logger.info(f"RevEng.AI: Status - {status}") if status == "Complete": self.call_callback( diff --git a/reai_toolkit/app/services/analysis_sync/analysis_sync.py b/reai_toolkit/app/services/analysis_sync/analysis_sync.py index ef508572..82bdfb40 100644 --- a/reai_toolkit/app/services/analysis_sync/analysis_sync.py +++ b/reai_toolkit/app/services/analysis_sync/analysis_sync.py @@ -12,8 +12,9 @@ from revengai import ( AnalysesCoreApi, AnalysisBasicInfoOutputBody, - BaseResponseAnalysisFunctionMapping, Configuration, + FunctionsCoreApi, + GetFunctionMapsOutputBody, ) from reai_toolkit.app.core.netstore_service import SimpleNetStore @@ -59,7 +60,7 @@ def start_syncing(self, func_map: FunctionMapping, callback: Callable[[GenericAp args=(func_map, callback), ) - def get_function_matches(self, callback: Callable[[FunctionMapping], None]) -> None: + def get_function_matches(self, callback: Callable[[GetFunctionMapsOutputBody], None]) -> None: analysis_id: int | None = self.netstore_service.get_analysis_id() if analysis_id is None: return @@ -72,7 +73,7 @@ def get_function_matches(self, callback: Callable[[FunctionMapping], None]) -> N logger.error("failed to retrieve model id") return - response: GenericApiReturn[FunctionMapping] = self.api_request_returning( + response: GenericApiReturn[GetFunctionMapsOutputBody] = self.api_request_returning( fn=lambda: self._fetch_function_map(analysis_id=analysis_id) ) @@ -118,17 +119,15 @@ def _get_current_base_address(self) -> int: def _rebase_program(self, base_address_delta: int) -> None: idaapi.rebase_program(base_address_delta, idaapi.MSF_FIXONCE) - def _fetch_function_map(self, analysis_id: int) -> FunctionMapping: + def _fetch_function_map(self, analysis_id: int) -> GetFunctionMapsOutputBody: """ Fetches the function map for the given analysis ID. """ with self.yield_api_client(sdk_config=self.sdk_config) as api_client: - analyses_client = AnalysesCoreApi(api_client) + functions_client = FunctionsCoreApi(api_client) - function_map: BaseResponseAnalysisFunctionMapping = analyses_client.get_analysis_function_map(analysis_id=analysis_id) - if function_map.data: - func_map: FunctionMapping = function_map.data.function_maps - self.netstore_service.put_function_mapping(func_map) + func_map: GetFunctionMapsOutputBody = functions_client.v3_get_analysis_func_maps(analysis_id=analysis_id) + self.netstore_service.put_function_mapping(func_map) return func_map diff --git a/reai_toolkit/app/services/auth/auth_service.py b/reai_toolkit/app/services/auth/auth_service.py index 071b5aba..22039eef 100644 --- a/reai_toolkit/app/services/auth/auth_service.py +++ b/reai_toolkit/app/services/auth/auth_service.py @@ -85,7 +85,7 @@ def verify(self) -> Tuple[bool, str]: if hasattr(self.sdk_config, "user_agent"): api_client.user_agent = self.sdk_config.user_agent try: - ConfigApi(api_client).get_config() + ConfigApi(api_client).v3_get_config() logger.info("RevEng.AI: User authenticated successfully.") self._is_authed = True self.get_me(force_refresh=True) diff --git a/reai_toolkit/app/services/existing_analyses/existing_analyses_service.py b/reai_toolkit/app/services/existing_analyses/existing_analyses_service.py index 0d381323..cd8fa634 100644 --- a/reai_toolkit/app/services/existing_analyses/existing_analyses_service.py +++ b/reai_toolkit/app/services/existing_analyses/existing_analyses_service.py @@ -2,13 +2,7 @@ from typing import List, Optional from revengai import AnalysesCoreApi, Configuration -from revengai.models import ( - AnalysisRecord, - AppApiRestV2AnalysesEnumsOrderBy, - Order, - StatusInput, - Workspace, -) +from revengai.models import AnalysisRecordBody from reai_toolkit.app.core.netstore_service import SimpleNetStore from reai_toolkit.app.core.shared_schema import GenericApiReturn @@ -22,28 +16,28 @@ class ExistingAnalysesService(IThreadService): def __init__(self, netstore_service: SimpleNetStore, sdk_config: Configuration): super().__init__(netstore_service=netstore_service, sdk_config=sdk_config) - def _fetch_analysis_records(self, sha256_hash: str) -> List[AnalysisRecord]: + def _fetch_analysis_records(self, sha256_hash: str) -> List[AnalysisRecordBody]: """ Helper method to fetch analysis records by SHA256 hash. - Returns a list of AnalysisRecord. + Returns a list of AnalysisRecordBody. """ with self.yield_api_client(sdk_config=self.sdk_config) as api_client: analyses_client = AnalysesCoreApi(api_client) - result = analyses_client.list_analyses( - workspace=[Workspace.PERSONAL], - status=[StatusInput.COMPLETE], + result = analyses_client.v3_list_analyses( + analysis_scope=["PRIVATE"], + status=["Complete"], sha256_hash=sha256_hash, - order_by=AppApiRestV2AnalysesEnumsOrderBy.CREATED, - order=Order.DESC, + order_by="created", + order="DESC", ) - return result.data.results + return result.results or [] def fetch_analyses_same_hash( self, file_path: str - ) -> GenericApiReturn[List[AnalysisRecord]]: + ) -> GenericApiReturn[List[AnalysisRecordBody]]: """ Fetches analyses with the same hash as the given file. Returns a list of analysis summaries. diff --git a/reai_toolkit/app/services/matching/matching_service.py b/reai_toolkit/app/services/matching/matching_service.py index e04fd9c1..7d065ae6 100644 --- a/reai_toolkit/app/services/matching/matching_service.py +++ b/reai_toolkit/app/services/matching/matching_service.py @@ -9,7 +9,8 @@ from loguru import logger from revengai import ( - BinarySearchResult, + BinariesApi, + BinarySearchResultBody, CollectionListItemBody, CollectionsApi, Configuration, @@ -20,7 +21,6 @@ GetMatchesStatusOutputBody, MatchFilters, ProgressMessage, - SearchApi, StartMatchingForFunctionsInputBody, TaskStatus, ) @@ -84,42 +84,42 @@ def search_collections( ) return response - def _search_binaries(self, text_input: str) -> List[BinarySearchResult]: + def _search_binaries(self, text_input: str) -> List[BinarySearchResultBody]: with self.yield_api_client(sdk_config=self.sdk_config) as api_client: - search_client = SearchApi(api_client) + binaries_client = BinariesApi(api_client) model_name: str | None = self.netstore_service.get_model_name() current_binary_id: int | None = self.netstore_service.get_binary_id() - return_list: List[BinarySearchResult] = [] + return_list: List[BinarySearchResultBody] = [] - response = search_client.search_binaries( + response = binaries_client.v3_search_binaries( partial_name=text_input, partial_sha256=None, - page_size=5, - page=1, + limit=5, + offset=0, model_name=model_name, exclude_binary_id=current_binary_id, ) - return_list.extend(response.data.results) + return_list.extend(response.results or []) - response = search_client.search_binaries( + response = binaries_client.v3_search_binaries( partial_name=None, partial_sha256=text_input, - page_size=5, - page=1, + limit=5, + offset=0, model_name=model_name, exclude_binary_id=current_binary_id, ) - return_list.extend(response.data.results) + return_list.extend(response.results or []) return return_list def search_binaries( self, text_input: str - ) -> GenericApiReturn[List[BinarySearchResult]]: + ) -> GenericApiReturn[List[BinarySearchResultBody]]: response = self.api_request_returning(lambda: self._search_binaries(text_input)) # If the search fails, return an empty list instead of an error if not response.success: diff --git a/reai_toolkit/app/services/upload/upload_service.py b/reai_toolkit/app/services/upload/upload_service.py index 1c06f35c..205c092c 100644 --- a/reai_toolkit/app/services/upload/upload_service.py +++ b/reai_toolkit/app/services/upload/upload_service.py @@ -4,7 +4,8 @@ from typing import Optional, Tuple from loguru import logger -from revengai import AnalysesCoreApi, BaseResponseConfigResponse, Configuration, Symbols +from revengai import AnalysesCoreApi, Configuration, GetConfigOutputBody, Symbols +from revengai.api.binaries_api import BinariesApi from revengai.api.config_api import ConfigApi from revengai.models.analysis_config import AnalysisConfig @@ -13,7 +14,7 @@ from revengai.models.analysis_scope import AnalysisScope from revengai.models.tag import Tag from revengai.models.upload_file_type import UploadFileType -from revengai.models.base_response_upload_response import BaseResponseUploadResponse +from revengai.models.upload_output_body import UploadOutputBody from reai_toolkit.app.core.netstore_service import SimpleNetStore from reai_toolkit.app.core.shared_schema import GenericApiReturn @@ -90,7 +91,7 @@ def analyse_file( debug_sha256 = sha256_file(dp) # First, upload the file - upload_response: GenericApiReturn[BaseResponseUploadResponse] = self.upload_user_file( + upload_response: GenericApiReturn[UploadOutputBody] = self.upload_user_file( file_path=file_path, upload_file_type=UploadFileType.BINARY, # must match server UploadFileType force_overwrite=True, @@ -131,9 +132,9 @@ def analyse_file( def _get_max_upload_size(self) -> int: with self.yield_api_client(sdk_config=self.sdk_config) as api_client: config_client: ConfigApi = ConfigApi(api_client) - response: BaseResponseConfigResponse = config_client.get_config() - if response.data: - return response.data.max_file_size_bytes + response: GetConfigOutputBody = config_client.v3_get_config() + if response.max_file_size_bytes: + return response.max_file_size_bytes return MAX_DEFAULT_FILE_SIZE_BYTES @@ -141,15 +142,13 @@ def _upload_file_req( self, upload_file_type: UploadFileType, file: Tuple[str, bytes], - packed_password: Optional[str] = None, force_overwrite: bool = False, - ) -> BaseResponseUploadResponse: + ) -> UploadOutputBody: with self.yield_api_client(sdk_config=self.sdk_config) as api_client: - analyses_client = AnalysesCoreApi(api_client) - return analyses_client.upload_file( - upload_file_type=UploadFileType(upload_file_type), + binaries_client = BinariesApi(api_client) + return binaries_client.v3_upload_file( + upload_file_type=UploadFileType(upload_file_type).value, force_overwrite=force_overwrite, - packed_password=packed_password, file=file, ) @@ -158,9 +157,8 @@ def upload_user_file( file_path: str, *, upload_file_type: UploadFileType, - packed_password: Optional[str] = None, force_overwrite: bool = False, - ) -> GenericApiReturn[BaseResponseUploadResponse]: + ) -> GenericApiReturn[UploadOutputBody]: p = Path(file_path) if not p.is_file(): return GenericApiReturn(success=False, error_message="File does not exist.") @@ -176,9 +174,9 @@ def upload_user_file( max_upload_size_mb: float = max_upload_size_bytes / (1024 * 1024) return GenericApiReturn(success=False, error_message=f"Failed to upload binary due to it exceeding maximum size limit of {max_upload_size_mb}MiB") - response: GenericApiReturn[BaseResponseUploadResponse] = self.api_request_returning( + response: GenericApiReturn[UploadOutputBody] = self.api_request_returning( lambda: self._upload_file_req( - upload_file_type, (p.name, blob), packed_password, force_overwrite + upload_file_type, (p.name, blob), force_overwrite ) ) diff --git a/scripts/build_vendor.sh b/scripts/build_vendor.sh index 56fbe22b..cda0d977 100755 --- a/scripts/build_vendor.sh +++ b/scripts/build_vendor.sh @@ -4,20 +4,28 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." VENDOR="reai_toolkit/vendor" +REQ="$(mktemp -t reai-vendor-req.XXXXXX)" +trap 'rm -f "$REQ"' EXIT if ! command -v uv >/dev/null 2>&1; then echo "uv is not on PATH; see https://docs.astral.sh/uv/getting-started/install/" >&2 exit 1 fi +echo "==> Exporting locked runtime dependencies from uv.lock" +uv export --frozen --no-dev --no-emit-project --no-hashes -o "$REQ" >/dev/null + echo "==> Rebuilding vendor tree at $VENDOR" rm -rf "$VENDOR" mkdir -p "$VENDOR" -uv pip install . --target "$VENDOR" --no-cache-dir +uv pip install -r "$REQ" --target "$VENDOR" --no-cache-dir echo "==> Dropping the self-install so the checkout is not shadowed" rm -rf "$VENDOR/reai_toolkit" "$VENDOR"/reai_toolkit-*.dist-info +echo "==> Verifying the vendor tree matches uv.lock" +uv run python scripts/check_vendor.py + echo "==> Vendored (this is what the plugin and the tests import)" for dist in "$VENDOR"/*.dist-info; do [ -d "$dist" ] || continue diff --git a/scripts/check_vendor.py b/scripts/check_vendor.py new file mode 100644 index 00000000..2ca3e0b1 --- /dev/null +++ b/scripts/check_vendor.py @@ -0,0 +1,79 @@ +import re +import subprocess +import sys +from pathlib import Path + +from packaging.markers import Marker + +ROOT = Path(__file__).resolve().parent.parent +VENDOR = ROOT / "reai_toolkit" / "vendor" +PIN = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==([^\s;]+)\s*(?:;\s*(.+))?$") + + +def normalise(name): + return re.sub(r"[-_.]+", "-", name).lower() + + +def locked(): + out = subprocess.run( + ["uv", "export", "--frozen", "--no-dev", "--no-emit-project", "--no-hashes"], + cwd=ROOT, + capture_output=True, + text=True, + check=True, + ).stdout + pinned = {} + for line in out.splitlines(): + line = line.strip() + if line.startswith("#") or not line: + continue + match = PIN.match(line) + if not match: + continue + name, version, marker = match.groups() + if marker and not Marker(marker).evaluate(): + continue + pinned[normalise(name)] = version + return pinned + + +def vendored(): + found = {} + for dist in VENDOR.glob("*.dist-info"): + name, _, version = dist.name[: -len(".dist-info")].rpartition("-") + if name: + found[normalise(name)] = version + return found + + +def main(): + if not VENDOR.is_dir(): + print(f"vendor tree missing at {VENDOR}; run scripts/build_vendor.sh", file=sys.stderr) + return 1 + + want = locked() + have = vendored() + problems = [] + + for name, version in sorted(want.items()): + if name not in have: + problems.append(f" missing: {name}=={version}") + elif have[name] != version: + problems.append(f" mismatch: {name} vendored {have[name]}, lock wants {version}") + + for name, version in sorted(have.items()): + if name not in want: + problems.append(f" unexpected: {name}=={version} is vendored but not in uv.lock") + + if problems: + print("vendor tree does not match uv.lock:", file=sys.stderr) + print("\n".join(problems), file=sys.stderr) + print("\nrun scripts/build_vendor.sh to rebuild", file=sys.stderr) + return 1 + + print(f"vendor tree matches uv.lock ({len(want)} packages)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/ai_decomp/test_sdk_schemas.py b/tests/unit/ai_decomp/test_sdk_schemas.py index 2fd51a50..fd36d3ff 100644 --- a/tests/unit/ai_decomp/test_sdk_schemas.py +++ b/tests/unit/ai_decomp/test_sdk_schemas.py @@ -5,9 +5,12 @@ import inspect from revengai import FunctionsAIDecompilationApi, FunctionsCoreApi +from revengai.models.ai_decompilation_rating import AiDecompilationRating from revengai.models.comments_data import CommentsData from revengai.models.create_ai_decomp_output_body import CreateAIDecompOutputBody from revengai.models.disassembly_output_body import DisassemblyOutputBody +from revengai.models.event_types_applied import EventTypesApplied +from revengai.models.event_types_suggested import EventTypesSuggested from revengai.models.line_attributions_data import LineAttributionsData from revengai.models.get_tokens_response import GetTokensResponse from revengai.models.inline_comment import InlineComment @@ -18,8 +21,14 @@ from revengai.models.token import Token from revengai.models.upsert_overrides_data import UpsertOverridesData from revengai.models.upsert_overrides_input_body import UpsertOverridesInputBody +from revengai.models.upsert_rating_input_body import UpsertRatingInputBody from revengai.models.workflow_progress import WorkflowProgress +from reai_toolkit.app.services.ai_decomp.stream import ( + EVENT_TYPES_APPLIED, + EVENT_TYPES_SUGGESTED, +) + def test_ai_decomp_api_exposes_the_methods_the_plugin_calls(): for method in ( @@ -34,7 +43,7 @@ def test_ai_decomp_api_exposes_the_methods_the_plugin_calls(): "regenerate_ai_decompilation_inline_comments", "patch_ai_decompilation_inline_comment", "delete_ai_decompilation_inline_comment", - "upsert_ai_decompilation_rating", + "v3_upsert_ai_decompilation_rating", "v3_get_ai_decompilation_tokens", "v3_upsert_ai_decompilation_overrides", "v3_get_ai_decompilation_line_attributions", @@ -79,6 +88,56 @@ def test_v3_token_methods_accept_plugin_kwargs(): assert {"function_id", "upsert_overrides_input_body"} <= set(overrides) +def test_v3_rating_method_accepts_plugin_kwargs(): + rating = inspect.signature( + FunctionsAIDecompilationApi.v3_upsert_ai_decompilation_rating + ).parameters + assert {"function_id", "upsert_rating_input_body"} <= set(rating) + + serialize = inspect.getsource( + FunctionsAIDecompilationApi._v3_upsert_ai_decompilation_rating_serialize + ) + assert "/v3/functions/{function_id}/ai-decompilation/rating" in serialize + + +def test_v3_rating_returns_nothing_so_success_is_the_absence_of_an_error(): + assert ( + inspect.signature( + FunctionsAIDecompilationApi.v3_upsert_ai_decompilation_rating + ).return_annotation + is None + ) + + +def test_upsert_rating_input_body_carries_rating_and_optional_reason(): + assert {"rating", "reason"} <= set(UpsertRatingInputBody.model_fields) + + body = UpsertRatingInputBody.from_json('{"rating": "POSITIVE"}') + assert body.rating == AiDecompilationRating.POSITIVE + assert body.reason is None + + +def test_upsert_rating_input_body_takes_the_rating_enum_the_plugin_holds(): + body = UpsertRatingInputBody(rating=AiDecompilationRating.NEGATIVE, reason=None) + assert body.to_dict() == {"rating": "NEGATIVE"} + + +def test_the_sse_type_events_carry_the_names_the_stream_parser_keys_off(): + suggested = EventTypesSuggested.from_json( + '{"event": "types_suggested", "data": {"attempt": 1, "seq": 4,' + ' "type": "types_suggested", "types": 2, "members": 7}}' + ) + assert suggested.event == EVENT_TYPES_SUGGESTED + assert (suggested.data.types, suggested.data.members) == (2, 7) + + applied = EventTypesApplied.from_json( + '{"event": "types_applied", "data": {"attempt": 1, "seq": 5,' + ' "type": "types_applied", "types": 2, "skipped": 1}}' + ) + assert applied.event == EVENT_TYPES_APPLIED + assert (applied.data.types, applied.data.skipped) == (2, 1) + + def test_workflow_progress_has_expected_fields(): fields = set(WorkflowProgress.model_fields.keys()) assert {"status", "step", "step_index", "steps_total", "messages"} <= fields diff --git a/tests/unit/ai_decomp/test_service.py b/tests/unit/ai_decomp/test_service.py index 44c6b367..c7f75267 100644 --- a/tests/unit/ai_decomp/test_service.py +++ b/tests/unit/ai_decomp/test_service.py @@ -630,7 +630,7 @@ def test_mutation_unknown_function_id_reports_failure(service, sdk, netstore): def test_rate_decomp_calls_upsert_rating(service, sdk): - sdk.upsert_ai_decompilation_rating.return_value = MagicMock() + sdk.v3_upsert_ai_decompilation_rating.return_value = None on_result = MagicMock() service.rate_decomp( @@ -638,16 +638,17 @@ def test_rate_decomp_calls_upsert_rating(service, sdk): ) _wait_mock(on_result) - _, kwargs = sdk.upsert_ai_decompilation_rating.call_args + _, kwargs = sdk.v3_upsert_ai_decompilation_rating.call_args assert kwargs["function_id"] == 42 - body = kwargs["upsert_ai_decomplation_rating_request"] + body = kwargs["upsert_rating_input_body"] assert body.rating == AiDecompilationRating.POSITIVE assert body.reason is None assert on_result.call_args[0][0].success is True + assert on_result.call_args[0][0].data is True def test_rate_decomp_api_error_surfaces(service, sdk): - sdk.upsert_ai_decompilation_rating.side_effect = ApiException(status=422) + sdk.v3_upsert_ai_decompilation_rating.side_effect = ApiException(status=422) on_result = MagicMock() service.rate_decomp( @@ -668,7 +669,7 @@ def test_rate_decomp_unknown_function_id_reports_failure(service, sdk, netstore) on_result.assert_called_once() assert on_result.call_args[0][0].success is False - sdk.upsert_ai_decompilation_rating.assert_not_called() + sdk.v3_upsert_ai_decompilation_rating.assert_not_called() def test_invalidate_clears_all_caches_and_inflight(service): diff --git a/tests/unit/ai_decomp/test_stream.py b/tests/unit/ai_decomp/test_stream.py index 226128f9..cb2c1cfa 100644 --- a/tests/unit/ai_decomp/test_stream.py +++ b/tests/unit/ai_decomp/test_stream.py @@ -12,6 +12,7 @@ EVENT_RENAME_APPLIED, EVENT_SOURCE_DELTA, EVENT_SOURCE_RESET, + EVENT_TYPES_APPLIED, EVENT_TYPES_SUGGESTED, EVENT_WARNING, DecompEvent, @@ -237,6 +238,28 @@ def test_renames_warnings_and_type_suggestions_are_collected(): assert (state.types_suggested, state.members_suggested) == (2, 7) +def test_types_applied_records_the_applied_and_skipped_counts(): + state = reduce_stream( + [ + _ev(EVENT_TYPES_SUGGESTED, types=3, members=9), + _ev(EVENT_TYPES_APPLIED, types=2, skipped=1), + ] + ) + + assert (state.types_applied, state.types_skipped) == (2, 1) + assert (state.types_suggested, state.members_suggested) == (3, 9) + + +def test_types_applied_frames_survive_the_parser(): + chunks = [_frame(EVENT_TYPES_APPLIED, types=2, skipped=1)] + + events = list(iter_stream_events(chunks)) + + assert [(e.type, e.types, e.skipped) for e in events] == [ + (EVENT_TYPES_APPLIED, 2, 1) + ] + + def test_a_warning_with_no_message_falls_back_to_its_kind(): state = reduce_stream([_ev(EVENT_WARNING, warning_kind="TRUNCATED")]) diff --git a/tests/unit/analysis_status/test_sdk_schemas.py b/tests/unit/analysis_status/test_sdk_schemas.py index c9ef268c..273b8b2e 100644 --- a/tests/unit/analysis_status/test_sdk_schemas.py +++ b/tests/unit/analysis_status/test_sdk_schemas.py @@ -1,13 +1,15 @@ +import typing + from revengai import ( AnalysisLogEntry, - BaseResponseStatus, + CreateMetadata, GetAnalysisLogsOutputBody, - StatusOutput, + OperationCreateMetadataCreateResult, ) -def test_status_output_fields(): - assert {"analysis_id", "analysis_status"} <= set(StatusOutput.model_fields) +def test_create_metadata_fields(): + assert {"analysis_id", "binary_id", "status"} <= set(CreateMetadata.model_fields) def test_logs_output_carries_entries(): @@ -18,5 +20,12 @@ def test_log_entry_fields(): assert {"text", "level", "timestamp"} <= set(AnalysisLogEntry.model_fields) -def test_base_response_status_envelope(): - assert {"status", "data", "errors"} <= set(BaseResponseStatus.model_fields) +def test_operation_fields(): + assert {"done", "error", "metadata", "name", "response"} <= set( + OperationCreateMetadataCreateResult.model_fields + ) + + +def test_operation_metadata_is_optional_create_metadata(): + annotation = OperationCreateMetadataCreateResult.model_fields["metadata"].annotation + assert set(typing.get_args(annotation)) == {CreateMetadata, type(None)} diff --git a/tests/unit/analysis_status/test_service.py b/tests/unit/analysis_status/test_service.py index 8cbdb23d..114a8b09 100644 --- a/tests/unit/analysis_status/test_service.py +++ b/tests/unit/analysis_status/test_service.py @@ -3,8 +3,11 @@ import pytest from revengai import ApiException +from revengai.models.create_metadata import CreateMetadata from revengai.models.get_analysis_logs_output_body import GetAnalysisLogsOutputBody -from revengai.models.status_output import StatusOutput +from revengai.models.operation_create_metadata_create_result import ( + OperationCreateMetadataCreateResult, +) from reai_toolkit.app.services.analysis_status import analysis_status as svc_mod from reai_toolkit.app.services.analysis_status.analysis_status import ( @@ -12,6 +15,7 @@ ) ANALYSIS_ID = 4242 +BINARY_ID = 77 @pytest.fixture(autouse=True) @@ -41,9 +45,21 @@ def sdk(mocker): return api_inst -def _status(value: str): - return MagicMock( - data=StatusOutput.model_construct(analysis_id=ANALYSIS_ID, analysis_status=value) +def _operation(status: str): + return OperationCreateMetadataCreateResult( + done=status in ("Complete", "Error"), + name=f"operations/analyses/{ANALYSIS_ID}", + metadata=CreateMetadata( + analysis_id=ANALYSIS_ID, binary_id=BINARY_ID, status=status + ), + ) + + +def _operation_without_metadata(): + return OperationCreateMetadataCreateResult( + done=False, + name=f"operations/analyses/{ANALYSIS_ID}", + metadata=None, ) @@ -55,7 +71,7 @@ def _poll(service): def test_complete_status_reports_analysis_id(service, sdk, netstore): - sdk.get_analysis_status.return_value = _status("Complete") + sdk.v3_get_analysis_operation.return_value = _operation("Complete") cb = _poll(service) @@ -66,8 +82,16 @@ def test_complete_status_reports_analysis_id(service, sdk, netstore): netstore.put_analysis_status.assert_called_with("Complete") +def test_status_is_read_from_operation_metadata(service, sdk): + sdk.v3_get_analysis_operation.return_value = _operation("Complete") + + _poll(service) + + sdk.v3_get_analysis_operation.assert_called_with(analysis_id=ANALYSIS_ID) + + def test_error_status_reports_failure(service, sdk): - sdk.get_analysis_status.return_value = _status("Error") + sdk.v3_get_analysis_operation.return_value = _operation("Error") cb = _poll(service) @@ -77,7 +101,7 @@ def test_error_status_reports_failure(service, sdk): def test_status_request_failure_surfaces_error(service, sdk): - sdk.get_analysis_status.side_effect = ApiException(status=500, reason="boom") + sdk.v3_get_analysis_operation.side_effect = ApiException(status=500, reason="boom") cb = _poll(service) @@ -86,10 +110,30 @@ def test_status_request_failure_surfaces_error(service, sdk): sdk.v3_get_analysis_logs.assert_not_called() -def test_in_progress_status_keeps_polling_until_complete(service, sdk): - sdk.get_analysis_status.side_effect = [_status("Processing"), _status("Complete")] +def test_in_progress_status_keeps_polling_until_complete(service, sdk, netstore): + sdk.v3_get_analysis_operation.side_effect = [ + _operation("Processing"), + _operation("Complete"), + ] + + cb = _poll(service) + + assert sdk.v3_get_analysis_operation.call_count == 2 + assert cb.call_args[0][0].success is True + assert [call.args[0] for call in netstore.put_analysis_status.call_args_list] == [ + "Processing", + "Complete", + ] + + +def test_missing_metadata_keeps_polling_without_recording_status(service, sdk, netstore): + sdk.v3_get_analysis_operation.side_effect = [ + _operation_without_metadata(), + _operation("Complete"), + ] cb = _poll(service) - assert sdk.get_analysis_status.call_count == 2 + assert sdk.v3_get_analysis_operation.call_count == 2 assert cb.call_args[0][0].success is True + netstore.put_analysis_status.assert_called_once_with("Complete") diff --git a/tests/unit/analysis_sync/test_sdk_schemas.py b/tests/unit/analysis_sync/test_sdk_schemas.py index 8e172b82..33fb4602 100644 --- a/tests/unit/analysis_sync/test_sdk_schemas.py +++ b/tests/unit/analysis_sync/test_sdk_schemas.py @@ -1,9 +1,8 @@ from revengai import ( AnalysisBasicInfoOutputBody, - BaseResponseAnalysisFunctionMapping, - FunctionMapping, + FunctionsCoreApi, + GetFunctionMapsOutputBody, ) -from revengai.models.analysis_function_mapping import AnalysisFunctionMapping def test_basic_info_carries_model_and_base_address(): @@ -12,12 +11,13 @@ def test_basic_info_carries_model_and_base_address(): ) -def test_function_mapping_envelope_carries_function_maps(): - assert {"status", "data"} <= set(BaseResponseAnalysisFunctionMapping.model_fields) - assert "function_maps" in AnalysisFunctionMapping.model_fields - - -def test_function_mapping_fields(): +def test_func_maps_output_is_flat(): assert {"function_map", "inverse_function_map", "name_map"} <= set( - FunctionMapping.model_fields + GetFunctionMapsOutputBody.model_fields ) + assert "data" not in GetFunctionMapsOutputBody.model_fields + assert "function_maps" not in GetFunctionMapsOutputBody.model_fields + + +def test_func_maps_lives_on_functions_core_api(): + assert hasattr(FunctionsCoreApi, "v3_get_analysis_func_maps") diff --git a/tests/unit/analysis_sync/test_service.py b/tests/unit/analysis_sync/test_service.py index 35c2f0ee..d5134e07 100644 --- a/tests/unit/analysis_sync/test_service.py +++ b/tests/unit/analysis_sync/test_service.py @@ -5,6 +5,7 @@ AnalysisBasicInfoOutputBody, ) from revengai.models.function_mapping import FunctionMapping +from revengai.models.get_function_maps_output_body import GetFunctionMapsOutputBody from reai_toolkit.app.services.analysis_sync import analysis_sync as svc_mod from reai_toolkit.app.services.analysis_sync.analysis_sync import AnalysisSyncService @@ -266,3 +267,26 @@ def test_fetch_model_id_skips_rebase_when_base_address_unknown( service._fetch_model_id(1234) rebase.assert_not_called() + + +@pytest.fixture +def functions_api(mocker): + mocker.patch.object(AnalysisSyncService, "yield_api_client") + api_class = mocker.patch.object(svc_mod, "FunctionsCoreApi") + api = MagicMock() + api_class.return_value = api + return api + + +def test_fetch_function_map_reads_v3_func_maps(service, functions_api, netstore): + body = GetFunctionMapsOutputBody.model_construct( + function_map={"1": 0x1000}, + inverse_function_map={"4096": 1}, + name_map={"4096": "fn"}, + ) + functions_api.v3_get_analysis_func_maps.return_value = body + + assert service._fetch_function_map(1234) is body + + functions_api.v3_get_analysis_func_maps.assert_called_once_with(analysis_id=1234) + netstore.put_function_mapping.assert_called_once_with(body) diff --git a/tests/unit/auth/test_endpoint_auth.py b/tests/unit/auth/test_endpoint_auth.py index a12294e8..9259846c 100644 --- a/tests/unit/auth/test_endpoint_auth.py +++ b/tests/unit/auth/test_endpoint_auth.py @@ -83,7 +83,7 @@ def test_the_call_discovery_actually_finds_the_plugins_endpoints(): calls = _api_calls() assert "get_me" in calls.get("IAMUsersApi", set()) - assert "get_config" in calls.get("ConfigApi", set()) + assert "v3_get_config" in calls.get("ConfigApi", set()) assert sum(len(v) for v in calls.values()) > 40 diff --git a/tests/unit/auth/test_sdk_schemas.py b/tests/unit/auth/test_sdk_schemas.py index e437b641..9beae518 100644 --- a/tests/unit/auth/test_sdk_schemas.py +++ b/tests/unit/auth/test_sdk_schemas.py @@ -1,8 +1,12 @@ -from revengai import ConfigApi, IAMUsersApi, User +from revengai import ConfigApi, GetConfigOutputBody, IAMUsersApi, User def test_auth_verification_method_exists(): - assert callable(getattr(ConfigApi, "get_config", None)) + assert callable(getattr(ConfigApi, "v3_get_config", None)) + + +def test_config_output_body_exposes_max_file_size_bytes(): + assert "max_file_size_bytes" in GetConfigOutputBody.model_fields def test_iam_get_me_method_exists(): diff --git a/tests/unit/auth/test_service.py b/tests/unit/auth/test_service.py index 886f1fc0..35c908e7 100644 --- a/tests/unit/auth/test_service.py +++ b/tests/unit/auth/test_service.py @@ -67,7 +67,7 @@ def test_get_sdk_config_builds_once_and_mutates_in_place(service): def test_verify_success(service, config_api): - config_api.get_config.return_value = MagicMock() + config_api.v3_get_config.return_value = MagicMock() ok, msg = service.verify() @@ -77,7 +77,7 @@ def test_verify_success(service, config_api): def test_verify_api_exception_with_parsed_errors(service, config_api, mocker): - config_api.get_config.side_effect = ApiException(status=401) + config_api.v3_get_config.side_effect = ApiException(status=401) mocker.patch.object( svc_mod, "parse_exception", @@ -92,7 +92,7 @@ def test_verify_api_exception_with_parsed_errors(service, config_api, mocker): def test_verify_api_exception_without_parsed_body(service, config_api, mocker): - config_api.get_config.side_effect = ApiException(status=500, reason="boom") + config_api.v3_get_config.side_effect = ApiException(status=500, reason="boom") mocker.patch.object(svc_mod, "parse_exception", return_value=None) ok, msg = service.verify() @@ -103,7 +103,7 @@ def test_verify_api_exception_without_parsed_body(service, config_api, mocker): def test_verify_unexpected_exception(service, config_api): - config_api.get_config.side_effect = RuntimeError("network down") + config_api.v3_get_config.side_effect = RuntimeError("network down") ok, msg = service.verify() @@ -163,7 +163,7 @@ def test_is_enthusiast_false_when_user_unavailable(service, iam_api): def test_verify_success_warms_user_cache(service, config_api, iam_api): - config_api.get_config.return_value = MagicMock() + config_api.v3_get_config.return_value = MagicMock() iam_api.get_me.return_value = _user("ENTHUSIAST") ok, _ = service.verify() diff --git a/tests/unit/core/test_utils.py b/tests/unit/core/test_utils.py index dd5c37ea..e18fc108 100644 --- a/tests/unit/core/test_utils.py +++ b/tests/unit/core/test_utils.py @@ -1,8 +1,26 @@ import hashlib +from unittest.mock import MagicMock + +import pytest from revengai import ApiException, BaseResponse -from reai_toolkit.app.core.utils import parse_exception, sha256_file +from reai_toolkit.app.core import utils +from reai_toolkit.app.core.utils import ( + MAX_FUNCTION_BOUNDARIES, + MAX_MANGLED_NAME_LENGTH, + build_function_boundary, + build_symbols, + parse_exception, + sha256_file, +) + + +@pytest.fixture +def fake_logger(monkeypatch): + logger = MagicMock() + monkeypatch.setattr(utils, "logger", logger) + return logger def test_parse_exception_returns_base_response_for_json_body(): @@ -43,3 +61,52 @@ def test_sha256_file_handles_multichunk(tmp_path): f.write_bytes(payload) assert sha256_file(f, chunk_size=1024) == hashlib.sha256(payload).hexdigest() + + +def test_build_function_boundary_skips_empty_mangled_name(): + assert build_function_boundary("", 0x1000, 0x1010) is None + assert build_function_boundary(None, 0x1000, 0x1010) is None + + +def test_build_function_boundary_truncates_overlong_mangled_name(): + boundary = build_function_boundary("_Z" + "a" * 8192, 0x1000, 0x1010) + + assert boundary is not None + assert len(boundary.mangled_name) == MAX_MANGLED_NAME_LENGTH + assert boundary.mangled_name.startswith("_Za") + + +def test_build_function_boundary_skips_negative_addresses(): + assert build_function_boundary("main", -1, 0x10) is None + assert build_function_boundary("main", 0x10, -1) is None + + +def test_build_function_boundary_keeps_valid_entry(): + boundary = build_function_boundary("main", 0x1000, 0x1010) + + assert boundary.mangled_name == "main" + assert boundary.start_address == 0x1000 + assert boundary.end_address == 0x1010 + + +def test_build_symbols_caps_function_boundaries(fake_logger): + boundary = build_function_boundary("main", 0x1000, 0x1010) + + symbols = build_symbols(0x400000, [boundary] * (MAX_FUNCTION_BOUNDARIES + 25)) + + assert len(symbols.function_boundaries) == MAX_FUNCTION_BOUNDARIES + fake_logger.warning.assert_called_once() + + +def test_build_symbols_keeps_list_under_cap(fake_logger): + boundary = build_function_boundary("main", 0x1000, 0x1010) + + symbols = build_symbols(0x400000, [boundary]) + + assert symbols.base_address == 0x400000 + assert len(symbols.function_boundaries) == 1 + fake_logger.warning.assert_not_called() + + +def test_build_symbols_clamps_negative_base_address(): + assert build_symbols(-1, []).base_address == 0 diff --git a/tests/unit/existing_analyses/test_sdk_schemas.py b/tests/unit/existing_analyses/test_sdk_schemas.py index 14669363..ae2640f5 100644 --- a/tests/unit/existing_analyses/test_sdk_schemas.py +++ b/tests/unit/existing_analyses/test_sdk_schemas.py @@ -1,20 +1,24 @@ -from revengai.models import ( - AppApiRestV2AnalysesEnumsOrderBy, - Order, - StatusInput, - Workspace, -) -from revengai.models.analysis_record import AnalysisRecord +import inspect +from revengai import AnalysesCoreApi +from revengai.models.analysis_record_body import AnalysisRecordBody +from revengai.models.list_analyses_output_body import ListAnalysesOutputBody -def test_analysis_record_fields(): + +def test_analysis_record_body_fields(): assert {"analysis_id", "status", "base_address", "sha_256_hash"} <= set( - AnalysisRecord.model_fields + AnalysisRecordBody.model_fields + ) + + +def test_list_analyses_output_is_flat(): + assert {"results", "page_size", "next_page_token"} <= set( + ListAnalysesOutputBody.model_fields ) + assert "data" not in ListAnalysesOutputBody.model_fields -def test_list_analyses_enums_have_expected_members(): - assert "PERSONAL" in Workspace.__members__ - assert "COMPLETE" in StatusInput.__members__ - assert "DESC" in Order.__members__ - assert "CREATED" in AppApiRestV2AnalysesEnumsOrderBy.__members__ +def test_v3_list_analyses_accepts_expected_filters(): + params = inspect.signature(AnalysesCoreApi.v3_list_analyses).parameters + assert {"analysis_scope", "status", "sha256_hash", "order_by", "order"} <= set(params) + assert "workspace" not in params diff --git a/tests/unit/existing_analyses/test_service.py b/tests/unit/existing_analyses/test_service.py index 3e4c4f4b..7bc6873d 100644 --- a/tests/unit/existing_analyses/test_service.py +++ b/tests/unit/existing_analyses/test_service.py @@ -2,13 +2,7 @@ import pytest from revengai import ApiException -from revengai.models import ( - AppApiRestV2AnalysesEnumsOrderBy, - Order, - StatusInput, - Workspace, -) -from revengai.models.analysis_record import AnalysisRecord +from revengai.models.analysis_record_body import AnalysisRecordBody from reai_toolkit.app.services.existing_analyses import ( existing_analyses_service as svc_mod, @@ -33,28 +27,39 @@ def sdk(mocker): return api_inst -def _record(analysis_id: int) -> AnalysisRecord: - return AnalysisRecord.model_construct(analysis_id=analysis_id, status="Complete") +def _record(analysis_id: int) -> AnalysisRecordBody: + return AnalysisRecordBody.model_construct( + analysis_id=analysis_id, status="Complete" + ) def test_returns_records_and_passes_expected_filters(service, sdk): records = [_record(1), _record(2)] - sdk.list_analyses.return_value = MagicMock(data=MagicMock(results=records)) + sdk.v3_list_analyses.return_value = MagicMock(results=records) result = service.fetch_analyses_same_hash("/path/to/binary.elf") assert result.success is True assert result.data == records - kwargs = sdk.list_analyses.call_args.kwargs + kwargs = sdk.v3_list_analyses.call_args.kwargs assert kwargs["sha256_hash"] == "deadbeef" - assert kwargs["workspace"] == [Workspace.PERSONAL] - assert kwargs["status"] == [StatusInput.COMPLETE] - assert kwargs["order_by"] == AppApiRestV2AnalysesEnumsOrderBy.CREATED - assert kwargs["order"] == Order.DESC + assert kwargs["analysis_scope"] == ["PRIVATE"] + assert kwargs["status"] == ["Complete"] + assert kwargs["order_by"] == "created" + assert kwargs["order"] == "DESC" + + +def test_empty_page_returns_empty_list(service, sdk): + sdk.v3_list_analyses.return_value = MagicMock(results=None) + + result = service.fetch_analyses_same_hash("/path/to/binary.elf") + + assert result.success is True + assert result.data == [] def test_api_failure_is_passed_through(service, sdk): - sdk.list_analyses.side_effect = ApiException(status=500, reason="boom") + sdk.v3_list_analyses.side_effect = ApiException(status=500, reason="boom") result = service.fetch_analyses_same_hash("/path/to/binary.elf") diff --git a/tests/unit/matching/test_matching_service.py b/tests/unit/matching/test_matching_service.py index d5c88dbb..a352d94d 100644 --- a/tests/unit/matching/test_matching_service.py +++ b/tests/unit/matching/test_matching_service.py @@ -37,9 +37,9 @@ def service(netstore): @pytest.fixture -def search_api(mocker): +def binaries_api(mocker): mocker.patch.object(MatchingService, "yield_api_client") - api_class = mocker.patch.object(svc_mod, "SearchApi") + api_class = mocker.patch.object(svc_mod, "BinariesApi") api_inst = MagicMock() api_class.return_value = api_inst return api_inst @@ -109,19 +109,19 @@ def test_search_collections_failure_returns_empty_success(service, collections_a assert out.data == [] -def test_search_binaries_queries_name_and_sha_excluding_current(service, search_api): - search_api.search_binaries.return_value = MagicMock( - data=MagicMock(results=[MagicMock()]) - ) +def test_search_binaries_queries_name_and_sha_excluding_current(service, binaries_api): + binaries_api.v3_search_binaries.return_value = MagicMock(results=[MagicMock()]) out = service.search_binaries("abc") assert out.success is True assert len(out.data) == 2 # one per call (name + sha) - assert search_api.search_binaries.call_count == 2 - for call in search_api.search_binaries.call_args_list: + assert binaries_api.v3_search_binaries.call_count == 2 + for call in binaries_api.v3_search_binaries.call_args_list: assert call.kwargs["exclude_binary_id"] == 7 assert call.kwargs["model_name"] == "binnet-0.1" + assert call.kwargs["limit"] == 5 + assert call.kwargs["offset"] == 0 def test_perform_matching_emits_start_batch_summary_and_filters_results(service, core_api): diff --git a/tests/unit/matching/test_sdk_schemas.py b/tests/unit/matching/test_sdk_schemas.py index ba929b29..07aa5b9e 100644 --- a/tests/unit/matching/test_sdk_schemas.py +++ b/tests/unit/matching/test_sdk_schemas.py @@ -1,5 +1,5 @@ from revengai import ( - BinarySearchResult, + BinarySearchResultBody, CollectionListItemBody, FunctionMapping, FunctionMatch, @@ -61,7 +61,7 @@ def test_matched_function_fields(): def test_search_result_fields(): assert {"binary_id", "binary_name", "sha_256_hash"} <= set( - BinarySearchResult.model_fields + BinarySearchResultBody.model_fields ) assert {"collection_id", "collection_name"} <= set( CollectionListItemBody.model_fields diff --git a/tests/unit/upload/test_sdk_schemas.py b/tests/unit/upload/test_sdk_schemas.py new file mode 100644 index 00000000..0fc995ab --- /dev/null +++ b/tests/unit/upload/test_sdk_schemas.py @@ -0,0 +1,27 @@ +import inspect + +from revengai import BinariesApi, UploadOutputBody + + +def test_upload_output_body_is_flat(): + assert { + "sha_256_hash", + "file_type", + "filename", + "mime", + "is_archive", + "can_sandbox", + "can_extract", + "analysis_requirements", + } <= set(UploadOutputBody.model_fields) + assert "data" not in UploadOutputBody.model_fields + + +def test_upload_lives_on_binaries_api(): + assert hasattr(BinariesApi, "v3_upload_file") + + +def test_v3_upload_file_has_no_packed_password(): + params = inspect.signature(BinariesApi.v3_upload_file).parameters + assert {"file", "upload_file_type", "force_overwrite"} <= set(params) + assert "packed_password" not in params diff --git a/tests/unit/upload/test_service.py b/tests/unit/upload/test_service.py index 7522a937..110f781a 100644 --- a/tests/unit/upload/test_service.py +++ b/tests/unit/upload/test_service.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock import pytest +from revengai import GetConfigOutputBody, UploadOutputBody from revengai.models.function_boundary import FunctionBoundary from revengai.models.symbols import Symbols from revengai.models.tag import Tag @@ -20,7 +21,25 @@ def sdk(mocker): mocker.patch.object(UploadService, "yield_api_client") api_class = mocker.patch.object(svc_mod, "AnalysesCoreApi") api_inst = MagicMock() - api_inst.upload_file.return_value = "parsed-upload-response" + api_class.return_value = api_inst + return api_inst + + +@pytest.fixture +def binaries(mocker): + mocker.patch.object(UploadService, "yield_api_client") + api_class = mocker.patch.object(svc_mod, "BinariesApi") + api_inst = MagicMock() + api_inst.v3_upload_file.return_value = "parsed-upload-response" + api_class.return_value = api_inst + return api_inst + + +@pytest.fixture +def config(mocker): + mocker.patch.object(UploadService, "yield_api_client") + api_class = mocker.patch.object(svc_mod, "ConfigApi") + api_inst = MagicMock() api_class.return_value = api_inst return api_inst @@ -35,39 +54,66 @@ def symbols(): ) -def test_upload_file_passes_tuple_to_sdk(service, sdk): +def test_upload_file_passes_tuple_to_v3_sdk(service, binaries): result = service._upload_file_req( upload_file_type=UploadFileType.BINARY, file=("binary.elf", b"\x7fELF\x02\x01"), force_overwrite=True, ) - sdk.upload_file.assert_called_once_with( - upload_file_type=UploadFileType.BINARY, + binaries.v3_upload_file.assert_called_once_with( + upload_file_type="BINARY", force_overwrite=True, - packed_password=None, file=("binary.elf", b"\x7fELF\x02\x01"), ) assert result == "parsed-upload-response" -def test_upload_file_forwards_packed_password(service, sdk): +def test_upload_file_sends_plain_string_file_type(service, binaries): service._upload_file_req( - upload_file_type=UploadFileType.BINARY, + upload_file_type=UploadFileType.DEBUG, file=("x", b""), - packed_password="hunter2", ) - assert sdk.upload_file.call_args.kwargs["packed_password"] == "hunter2" + sent = binaries.v3_upload_file.call_args.kwargs["upload_file_type"] + assert sent == "DEBUG" + assert type(sent) is str -def test_upload_file_defaults_force_overwrite_false(service, sdk): +def test_upload_file_defaults_force_overwrite_false(service, binaries): service._upload_file_req( upload_file_type=UploadFileType.BINARY, file=("x", b""), ) - assert sdk.upload_file.call_args.kwargs["force_overwrite"] is False + assert binaries.v3_upload_file.call_args.kwargs["force_overwrite"] is False + + +def test_upload_user_file_returns_flat_v3_body(service, binaries, mocker, tmp_path): + mocker.patch.object(UploadService, "_get_max_upload_size", return_value=1024) + binaries.v3_upload_file.return_value = UploadOutputBody( + analysis_requirements=[], + can_extract=False, + can_sandbox=False, + file_type="BINARY", + filename="binary.elf", + is_archive=False, + mime="application/x-executable", + sha_256_hash="deadbeef", + ) + target = tmp_path / "binary.elf" + target.write_bytes(b"\x7fELF") + + response = service.upload_user_file( + str(target), + upload_file_type=UploadFileType.BINARY, + force_overwrite=True, + ) + + assert response.success + assert response.data.sha_256_hash == "deadbeef" + assert response.data.file_type == "BINARY" + assert not hasattr(response.data, "data") @pytest.mark.parametrize( @@ -113,3 +159,22 @@ def test_analyse_defaults_no_cache_false(service, sdk, symbols): request = sdk.create_analysis.call_args.kwargs["analysis_create_request"] assert request.analysis_config.no_cache is False + + +def test_max_upload_size_reads_flat_v3_config(service, config): + config.v3_get_config.return_value = GetConfigOutputBody( + ai_decompiler_unsupported_languages=[], + max_file_size_bytes=42 * 1024 * 1024, + ) + + assert service._get_max_upload_size() == 42 * 1024 * 1024 + config.v3_get_config.assert_called_once_with() + + +def test_max_upload_size_falls_back_to_default(service, config): + config.v3_get_config.return_value = GetConfigOutputBody( + ai_decompiler_unsupported_languages=[], + max_file_size_bytes=0, + ) + + assert service._get_max_upload_size() == svc_mod.MAX_DEFAULT_FILE_SIZE_BYTES diff --git a/uv.lock b/uv.lock index daa53010..38feec74 100644 --- a/uv.lock +++ b/uv.lock @@ -152,6 +152,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, ] +[[package]] +name = "declib" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "platformdirs" }, + { name = "ply" }, + { name = "prompt-toolkit" }, + { name = "psutil" }, + { name = "pycparser" }, + { name = "pyghidra" }, + { name = "setuptools" }, + { name = "toml" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/06/f0494429f882ee4d5c4ca94c21e11abc4cee79fcd4939f4133887d919528/declib-4.5.0.tar.gz", hash = "sha256:333dd38992639eb7eb498fe18c26d88eaaa76b20ace504c59f92e2eb0bcb5a07", size = 280134, upload-time = "2026-08-04T01:38:58.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/fe/1a435da0d3b7f6dd64b1bc1851b57064fc54f8c551506bbc2edbf8e29e10/declib-4.5.0-py3-none-any.whl", hash = "sha256:f320a6daca1c76b68d6d1fe60932afb9817c95f167e1976a4fe2ceab9debee2f", size = 253331, upload-time = "2026-08-04T01:38:57.307Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -173,18 +196,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] -[[package]] -name = "ghidra-bridge" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jfx-bridge" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a4/b4/adc97c3222fd4d48903b8155e17f9c6fadc94b5d18daca69d9c98fa6ac21/ghidra_bridge-1.0.0.tar.gz", hash = "sha256:3a7bdf9c8c1dc78acd3e8cfe9649d0e9554e47e147435730104efee634b01c9d", size = 22384, upload-time = "2023-01-30T03:52:22.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/27/68da14f96a9df404acd800969fb12058274f717668ca876be2a2b4d98387/ghidra_bridge-1.0.0-py2.py3-none-any.whl", hash = "sha256:d73d363b99ad893b04dcae4455120b7b44f83899fde14194fcf476cd1656141e", size = 20848, upload-time = "2023-01-30T03:52:20.741Z" }, -] - [[package]] name = "idapro" version = "0.0.6" @@ -212,47 +223,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "jfx-bridge" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/13/796445dd00e517bc9551261cb86e9b0d8cff873b37f92421a3e0badc61bc/jfx_bridge-1.0.0.tar.gz", hash = "sha256:e291b51f510bd2587619434f94c89f0c805936c492924c0d905e9222fa290729", size = 42624, upload-time = "2023-01-30T03:42:09.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/8b/6b054a1dd51f71da6324554eceb30e0bd4c8254c1d7c2744b9791fb0547c/jfx_bridge-1.0.0-py2.py3-none-any.whl", hash = "sha256:c7beda4776e219fdca60c42cbce31a97df392c164474c9b5420ed033f346ad30", size = 37606, upload-time = "2023-01-30T03:42:08.372Z" }, -] - [[package]] name = "jpype1" -version = "1.6.0" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/49/6090a131d84b22c6aae13b1853092028b060fd17da1af87c0e42ad69d50f/jpype1-1.6.0.tar.gz", hash = "sha256:2d46b2a14f8f0e6f17d8aa22b4fc3a64b2790851ebf1409ad79a37c698fd6e9a", size = 1057888, upload-time = "2025-07-07T14:04:11.244Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/68/47fa634cbd0418cbca86355e9421425f5892ee994f7338106327e49f9117/jpype1-1.5.2.tar.gz", hash = "sha256:74a42eccf21d30394c1832aec3985a14965fa5320da087b65029d172c0cec43b", size = 1011421, upload-time = "2025-01-27T10:33:16.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/95/524c67e2aafa301d6ffd21747847c5e9c9785ff8b39d90275ead2877dd0c/jpype1-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:533cf7ced58a4b74272f3d1e962fe33f305184d2fcb3856ca79867d5b6f0fb8b", size = 583402, upload-time = "2025-07-07T13:50:13.487Z" }, - { url = "https://files.pythonhosted.org/packages/13/61/88288a341292d3b76967587e59b674b5445678dcf60e8c921507bd70baa6/jpype1-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:597063698c074e5bf34e696626423a528cdf099989e22a837e889a0d671fd9e4", size = 468810, upload-time = "2025-07-07T13:50:16.967Z" }, - { url = "https://files.pythonhosted.org/packages/08/ed/18a16c6ae0ac5978fe93ad5c265719a311a613ba3dd4d96bd89e93fd2444/jpype1-1.6.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2f0cd698d160ba825952393b4d87911e7eedcbf5af381bb6438126de863f66b6", size = 512880, upload-time = "2025-07-07T13:50:20.451Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/9854e41f4b301a52210254d24d5d62792e0700527f7940445d763d0d64c6/jpype1-1.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fcabb8cce3be16528bd26e4b73e41d7b8c778111f14de52c33c25e2a9d4c9a9f", size = 496509, upload-time = "2025-07-07T13:50:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/74/85/2975be78a88678c9048d39141f39f1ced1b55e937e7854bfd824282ee176/jpype1-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f02c419d6cdd45ed5576d95f0ab4732371c760ba4b01ea9bd646b98b3c21a16f", size = 356591, upload-time = "2025-07-07T13:50:26.994Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ff/688b8ecbce3c4fa8ee34bc364d447f372d2dcc9597742e7331a328b07f20/jpype1-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40b523e11b22398a8ddb777f1e6e8d55108a631311f35b48b0ed0f4c9198d025", size = 583485, upload-time = "2025-07-07T13:50:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/997c0381028998522ef9103a1730be40eee2c8eb5ca244a295f9d5c6a6e2/jpype1-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ae5927e441894d41b65e08390a0ea6a6512fe222c07aa33fbab623512092fdbd", size = 468977, upload-time = "2025-07-07T13:50:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f9/e3b0a467b3ac2c4eeac5ca57764bf49a859b10f08f43e5179e1a283d2a9b/jpype1-1.6.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9d85c2077172a32dd8ffde8711cb57c5bc351378ceb44dd8c3bdd80f27fa8caf", size = 512946, upload-time = "2025-07-07T13:50:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b4/1167be1572704fc365f31038a862afae26dda5006c7f4178f44c27779e37/jpype1-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5a02cbba4022a0aa47ee617bc12349457988c653491484a988dc8f4e6269dfc", size = 496609, upload-time = "2025-07-07T13:50:35.631Z" }, - { url = "https://files.pythonhosted.org/packages/25/a9/3ed33c24c86d234cb6fd5c6701b64f0a39ed4fd63fd5bd45ad8ce6ac72d9/jpype1-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:966daa892e6cd9ec3b2b92fd7a6d572687b29021e6d28fb73995a62ddd0a11eb", size = 356597, upload-time = "2025-07-07T13:50:37.54Z" }, - { url = "https://files.pythonhosted.org/packages/38/88/2156a27251492169f947850293d9870d49d784f1355fad7e920095636721/jpype1-1.6.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5454322887505097c46f41521becf31cda2303f54e35cb42e20f1a180055a558", size = 582187, upload-time = "2025-07-07T13:50:39.034Z" }, - { url = "https://files.pythonhosted.org/packages/6b/5d/51bde8695cc3eabcc465779f6dc23b149194bbcdcb8d221a9572422053f4/jpype1-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf646459023a0dd71b3cc3aeeb977225a93f94c97839bb4e6146977b166c7caf", size = 468341, upload-time = "2025-07-07T13:50:40.739Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/50cdc2bf9e51c0828faabec6fad50d61aada5a57d0bc435f4db0c16eb2ca/jpype1-1.6.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7a98b34bc68a117881382dffa9b02d5e1d0d94325f85d7f564c33cc9e9d2916b", size = 511724, upload-time = "2025-07-07T13:50:42.479Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a4/6038228e44cbcdb8f96ff2518cd6c577730f31fb62e9f46de7efdfabc2de/jpype1-1.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56d7b662ce353c7f876e9ff3d4e775918348340795801e1f00c3b6241006264", size = 495897, upload-time = "2025-07-07T13:50:44.018Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e4/af69d43779da0e49d37d7fd8a4a01bede03f6a3bab450eab1c10cf48e20e/jpype1-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf41134bc59c28de2721614ed00a34f76f69f2b525583a5c42fee5c7bae05d40", size = 355702, upload-time = "2025-07-07T13:50:45.368Z" }, - { url = "https://files.pythonhosted.org/packages/3f/1b/7167cfb702cfbcb17b0973150608f9cb92c3f409ebaaff655f7389617766/jpype1-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ce1da9ff01010a4529de194aecc053bfc8ab25440ea6b63579ef8c8d381da21d", size = 582384, upload-time = "2025-07-07T13:50:47.418Z" }, - { url = "https://files.pythonhosted.org/packages/c0/48/8ae425d8842bc76eb51052bbea5941dac230c8f6b58614ef2b7cb2da1a74/jpype1-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a387430cd4f3f68761013dca7f71b79d8d64b47dfd56eedcbd44c894d98d7f2", size = 468237, upload-time = "2025-07-07T13:50:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/ef/2f/dbdc15aa99404dda0315a9883add7422c9ac7acce28ce24b66c89052c2fb/jpype1-1.6.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6c9d99f41a5d05a8a2b6aeda796f646904c1c2b68aed0cc9a3bbc165483d9e1e", size = 511905, upload-time = "2025-07-07T13:50:50.87Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/50fcdd6d3bb42a87cbda8b04fc72283a9c6d955ada0d011190ce31e0e027/jpype1-1.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55ba7257988a69bee84f7cd1444131725d5447999149fdafdba25bf46e4b1a3f", size = 496001, upload-time = "2025-07-07T13:50:52.711Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b8/25214a9ebecbd31447b685500dca5fe2ac070df38358d6823f591a8ce52e/jpype1-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:ef8dd72442d91cde7e5eefef24ab71323cdd4137283a59f79d265a6620f7d0ab", size = 355727, upload-time = "2025-07-07T13:51:00.402Z" }, - { url = "https://files.pythonhosted.org/packages/29/a0/b2347572998020de8ce3ee0a285a1b84b052b522e8d452ee9ad330570918/jpype1-1.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc0c1c8b989b9857f87648980a746084b323858b0450225eab3642bb674645d6", size = 471150, upload-time = "2025-07-07T13:50:54.254Z" }, - { url = "https://files.pythonhosted.org/packages/7c/62/5e0f64a185f37303d0dd07e7b7a927cd5237fdc3c8176add949ed8cafaa2/jpype1-1.6.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cbbba16381ccd23530ad4ef09f7b2b1b12bd7fd0f6ca9fe4c7b4e3da8fe4cf63", size = 514015, upload-time = "2025-07-07T13:50:56.299Z" }, - { url = "https://files.pythonhosted.org/packages/72/68/84050ed2679e2d2ee2030a60d3e5a59a29d0533cd03fd8d9891e36592b0f/jpype1-1.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1a3814e4f65d67e36bdb03b8851d5ece8d7a408aa3a24251ea0609bb8fba77dd", size = 497229, upload-time = "2025-07-07T13:50:57.842Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/b2efcad1ea5a541f125218e4eb1529ebb8ca18941264c879f3e89a36dc35/jpype1-1.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7b2da98c142812ca40a18a735b33e47c6511b03debf1e979630f4cf473b68a87", size = 584511, upload-time = "2025-01-27T10:32:48.817Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c6/63538d160c17e837f62d29ba4163bc444cef08c29cd3f3b8090691c1869c/jpype1-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcfc5c1d45d6b108800d172ea817bda585db7f1646d6a98d14da9aca66e0eb44", size = 467488, upload-time = "2025-01-27T10:32:52.141Z" }, + { url = "https://files.pythonhosted.org/packages/91/69/655d73a64ae6731706eab73d3f63fd2d48e36704da09710fbc0155c14402/jpype1-1.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:156f469a976cc61f695a2455db6de7334774388e7d53c3da8a0a23d9c062d1b2", size = 510064, upload-time = "2025-01-27T10:32:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/97/0a/cbe03759331c640aa5862f974028122a862b08935a0b11b8fa6f6e46c26b/jpype1-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca93cc74f8db1f604d2ea6adb764dec4dec68528f1ee68308fa3d524095739", size = 494097, upload-time = "2025-01-27T10:32:59.875Z" }, + { url = "https://files.pythonhosted.org/packages/22/18/0a51845ca890ffdc72f4d71a0c2be334b887c5bb6812207efe5ad45afcb3/jpype1-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:924b0a0cf93d3dddb3f79286fbe40f8c901c78ed61216edbe108666234df43e0", size = 356655, upload-time = "2025-01-27T10:33:02.702Z" }, + { url = "https://files.pythonhosted.org/packages/35/a0/638186a75026a02286041e4a0449b1dff799a3914dc1c0716ef9b9367b73/jpype1-1.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9f6ab8dd284c16e2617a697d54c3d0304b08020a37386ed96103a129391a2d9", size = 584474, upload-time = "2025-01-27T10:33:04.674Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/95db2eb3c8a7311ee08a2c237cea24828859db6a6cb5e901971d3f5e49da/jpype1-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a54a771ee56260f98e5b9a77455084e4a48061967de13dabf628bdba9c8122e0", size = 467508, upload-time = "2025-01-27T10:33:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/84/07/f4ed08bf1f65526d59a07efa86a79a2cc37263fea9a91efeb7dfd2b81bcb/jpype1-1.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05dc6d2759111483a9c808a50e67f556efe494e999585c7d7e7d6d8a8ee58525", size = 510256, upload-time = "2025-01-27T10:33:09.465Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7d/9fdbbc1a574be43f9820735ca8df0caf8b159856201d9b21fd73932342bc/jpype1-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b900e154826a076118d074166596f1d817e113e07084bf0c9c43d8064a86ab77", size = 494076, upload-time = "2025-01-27T10:33:11.864Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b9/4dfb38a7f4efb21f71df7344944a8d9a23e30d0503574e455af6ce4f1a56/jpype1-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:0a0d18d4384b3df2e55282545737dfcf18c604504f1382ad14f880bef960f265", size = 356587, upload-time = "2025-01-27T10:33:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e4/0c27352e8222dcc0e3ce44b298015072d2057d08dd353541c980a31d26c9/jpype1-1.5.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1e1db9ac909ad2ae0e40b04c2aa88cb14250d5245d69715561507681f2b08b2f", size = 583445, upload-time = "2025-01-27T10:34:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4c/e0200a6e3fed5cda79e926c2a8a610676f04948f89d7e38d93c7d4b21be9/jpype1-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994fb7b319b453f77ad4b6aff01e0dd4180ea74a6fe5a031e4e9db92dbe95376", size = 466485, upload-time = "2025-01-27T10:34:30.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ad/d85926b2f0104ded953fa53ff95fe71c96935cd742fdadb888f1145ffe79/jpype1-1.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec2f1009d7221fb3443decfb8f326039febc93578aadedfb3e052dab0afbf5a", size = 509372, upload-time = "2025-01-27T10:34:31.94Z" }, + { url = "https://files.pythonhosted.org/packages/74/f3/1cd4332076ed0421e703412f47f15f43af170809435c57ba3162edc80d4b/jpype1-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b1fb2b430a50f081ea0ee24d19232ae0d03dbfe3dd076ec5f8ae42b30a656f", size = 493520, upload-time = "2025-01-27T10:34:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/74/dd/7408d4beae755de6fcd07c76b2f0bacabc0461b43fba83811c1f7c22440e/jpype1-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b1c2d76d211cab60be16505d32a6b3c9fffc51ce79c68e81a3d48e5effff2d", size = 356149, upload-time = "2025-01-27T10:34:38.135Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/b37005bec457b94eaaf637a663073b7c5df70113fd4ae4865f6e386c612f/jpype1-1.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4acb098cb1698b14b6e5c79e275f4c70dcc01b0fb93425f206d0a5e380e43c66", size = 583600, upload-time = "2025-01-27T10:34:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/a3/00a265d424f7d47e0dc547df2320225ce0143fec671faf710def41404b8c/jpype1-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c08480c7d18125664a12bf0a244b96b49c05105306b65937dbefeb05ab4b2847", size = 466426, upload-time = "2025-01-27T10:34:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cd/890d9ed43d7e1366e151c0ed7046e59f6c1cb7ecfc8ecfefe9e5e79f8420/jpype1-1.5.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6819f231c651ef876ffb23158083ea498ff80b57c46da537148412aa22235a13", size = 509513, upload-time = "2025-01-27T10:34:44.538Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d0/191db2e9ab6ae7029368a488c9d88235966843b185aba7925e54aa0c0013/jpype1-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42fe8db66ad4e5c66f637f5c4de82fca880ba696104e1f4a7e575885923dead8", size = 493474, upload-time = "2025-01-27T10:34:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b7/e1787633b41d609320b41d0dd87fe3118598210609e4e3f6cef93cfcef40/jpype1-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b96365f1302df2fb3c6ad73117d6fe450a55b7550fd7fecadac3cec5bc7117c", size = 356150, upload-time = "2025-01-27T10:34:56.58Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c8/76541ffefa6fc4ee7a3a8c7e6d9e376f5ac8980a47ff31a8c330ffbb5dcf/jpype1-1.5.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ea5001f6a7a42be6f5f500dcb20dad5738fef6a7d19c86dbcf482b803f01cab", size = 468412, upload-time = "2025-01-27T10:34:49.155Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/827ca43aaa5ea6b773aa90b405acec22bd152d1284aa40f7c0c02bc1657b/jpype1-1.5.2-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220d33305998ff7a7cbc9c0b5eea54f2691fdc21e60d52ad56276ea13fd5bd4c", size = 511322, upload-time = "2025-01-27T10:34:52.832Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/f1396d7b66f9b6867a279db510e625ba51c8a4b4397f59a6c2b20fb55548/jpype1-1.5.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f1ed152fdd5200067dc0cdcd4683530b87a6be3987b98596cdf5a7d3ac4c679", size = 494714, upload-time = "2025-01-27T10:34:54.785Z" }, ] [[package]] @@ -266,27 +268,14 @@ wheels = [ [[package]] name = "libbs" -version = "2.16.5" +version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, - { name = "ghidra-bridge" }, - { name = "jfx-bridge" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "platformdirs" }, - { name = "ply" }, - { name = "prompt-toolkit" }, - { name = "psutil" }, - { name = "pycparser" }, - { name = "pyhidra" }, - { name = "setuptools" }, - { name = "toml" }, - { name = "tqdm" }, + { name = "declib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/e2/3652680339a08a9dfb2a11345e546b603f101c1ac1e511bcdab89d3a2a0d/libbs-2.16.5.tar.gz", hash = "sha256:b47b7f0fb6e271ddb177c4b70fee42cd9323a46c904c8015ec54695c470f5a62", size = 132805, upload-time = "2025-10-13T12:43:23.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/c0/35aebcc1ed3e75ddc8c07f7b900580ad3f81946703b3b00457f091af84ec/libbs-3.8.1.tar.gz", hash = "sha256:8605a519bf3a2611a9c59896768fe52d3cb1906e43407fba43e858f2b78df445", size = 3189, upload-time = "2026-06-03T17:12:32.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/8a/51f5c1f256829ebdc639260c4c05a79e0dba5d264a88a8666f0ff23b358d/libbs-2.16.5-py3-none-any.whl", hash = "sha256:5046e0e8a2e8bb39fdd336864039e4b06e2f9838392784aa1c3ca968cb581766", size = 141803, upload-time = "2025-10-13T12:43:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/78/69/a1ead5e2459c67274bec8875a08ea1cddf89d29ef1bb55068e0c8bed5e46/libbs-3.8.1-py3-none-any.whl", hash = "sha256:73b6b9651677895b60c1227609cf140adc94eb40727474ec115ff1d85561296a", size = 3331, upload-time = "2026-06-03T17:12:31.121Z" }, ] [[package]] @@ -403,11 +392,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, ] [[package]] @@ -540,25 +529,25 @@ wheels = [ ] [[package]] -name = "pygments" -version = "2.19.2" +name = "pyghidra" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +dependencies = [ + { name = "jpype1" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/eb/842a540cf3a8328a76636de17b0f5b3b11ac75c50a89660260f27021412a/pyghidra-3.1.0.tar.gz", hash = "sha256:2106ac131eb9a4990a79ee84dc2d39a792cf7b2d0de5eaaf1b0e6d7d2d290bb6", size = 53509, upload-time = "2026-05-14T10:00:11.403Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/1d8f9a790be5c6d3f95a8c1428016269c0c049ae70463bc3edbc76f2bb04/pyghidra-3.1.0-py3-none-any.whl", hash = "sha256:62d76e4c42352fc38cb3a9526ba7e25681d21388c16e59a2d17e99aeaf47ac94", size = 51595, upload-time = "2026-05-14T09:59:59.323Z" }, ] [[package]] -name = "pyhidra" -version = "1.3.0" +name = "pygments" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jpype1" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/7f/1de83b2299371f013196d044cfc95d48116a4067f4ff9d5197eb509dc9f5/pyhidra-1.3.0.tar.gz", hash = "sha256:1fae93db37000b7b12d8011500d9367a904fafb09476190e519f61b013d440e4", size = 47568, upload-time = "2024-10-04T16:25:19.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/71/42b696a1e5a81f12a27898b1f83f16b5109f3702e884248d75238e4ab92e/pyhidra-1.3.0-py3-none-any.whl", hash = "sha256:768b5199cf94bafe33bfa64c836380c976dad68db937e1feefb5b5e8f8035c73", size = 56999, upload-time = "2024-10-04T16:25:17.754Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -816,11 +805,11 @@ dev = [ [package.metadata] requires-dist = [ - { name = "libbs", specifier = ">=2.16.5" }, + { name = "libbs", specifier = ">=3.8.1" }, { name = "loguru", specifier = ">=0.7" }, { name = "pydantic" }, { name = "requests", specifier = ">=2.32" }, - { name = "revengai", specifier = ">=4.20.0" }, + { name = "revengai", specifier = ">=4.72.2" }, ] [package.metadata.requires-dev] @@ -855,7 +844,7 @@ wheels = [ [[package]] name = "revengai" -version = "4.20.0" +version = "4.72.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lazy-imports" }, @@ -864,9 +853,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/29/9617fe378a9d191d84c62d1f4ad96411dc55ad516c5470b5d95ef27dc5c3/revengai-4.20.0.tar.gz", hash = "sha256:84886a55baf878c7a31f38eeb7199e8b29fa10077378c33c30bc84d64aa646c3", size = 426999, upload-time = "2026-09-02T14:37:02.389Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/0c/f6841e1f8450c57350e081ffdfc3d65b432ba2430bc3254abdb4ba258722/revengai-4.72.2.tar.gz", hash = "sha256:e49fba4fc2ee664e8bc556fcabedd6a3e9c8be9f33ced430f2916e50356811d4", size = 561742, upload-time = "2026-10-12T07:43:42.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ef/5731372d613239278216f5c152060cfb9091cdc0e5ddebd6d2ee5cf9475d/revengai-4.20.0-py3-none-any.whl", hash = "sha256:a6c362fb691748e1fa8b82a82d232c43a703c4112808a8ab2a98b93b3d24898f", size = 1098020, upload-time = "2026-09-02T14:37:01.028Z" }, + { url = "https://files.pythonhosted.org/packages/47/25/d5e9872cb3e2747dc265c8dca1219128edc9b438013e03efc62c2ac41b7d/revengai-4.72.2-py3-none-any.whl", hash = "sha256:4edd14ada08a185d124f303e926618a8192c8e519cd62d06bb7916eb1dd1afb2", size = 1496400, upload-time = "2026-10-12T07:43:41.044Z" }, ] [[package]]