From da592370f8b91116f7d7274c831d45c7d60731d5 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Fri, 20 Feb 2026 00:51:09 +0530 Subject: [PATCH 1/3] loader: handle struct.error from dnfile and show clear CorruptFile message When .NET metadata is truncated or invalid, dnfile can raise struct.error. Catch it in DnfileFeatureExtractor and DotnetFileFeatureExtractor and re-raise as CorruptFile with a user-friendly message. Fixes #2442 --- CHANGELOG.md | 1 + capa/features/extractors/dnfile/extractor.py | 10 +++++++++- capa/features/extractors/dotnetfile.py | 12 ++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3169082671..7f00b95e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - lint: disable rule caching during linting @Maijin #2817 - vmray: skip processes with invalid PID or missing filename @EclipseAditya #2807 - render: use default styling for dynamic -vv API/call details so they are easier to see @devs6186 #1865 +- loader: handle struct.error from dnfile and show clear CorruptFile message @devs6186 #2442 ### capa Explorer Web - webui: fix 404 for "View rule in capa-rules" by using encodeURIComponent for rule name in URL @devs6186 #2482 diff --git a/capa/features/extractors/dnfile/extractor.py b/capa/features/extractors/dnfile/extractor.py index 4b6694f57d..d1316ea1b8 100644 --- a/capa/features/extractors/dnfile/extractor.py +++ b/capa/features/extractors/dnfile/extractor.py @@ -15,6 +15,7 @@ from __future__ import annotations +import struct from typing import Union, Iterator, Optional from pathlib import Path @@ -83,7 +84,14 @@ def get_type(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: class DnfileFeatureExtractor(StaticFeatureExtractor): def __init__(self, path: Path): - self.pe: dnfile.dnPE = dnfile.dnPE(str(path)) + try: + self.pe = dnfile.dnPE(str(path)) + except struct.error as e: + from capa.loader import CorruptFile + + raise CorruptFile( + "Invalid or truncated .NET metadata; the file may be corrupted or not a valid .NET PE." + ) from e super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes())) # pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction diff --git a/capa/features/extractors/dotnetfile.py b/capa/features/extractors/dotnetfile.py index dcba2c2f2d..919f4fff6c 100644 --- a/capa/features/extractors/dotnetfile.py +++ b/capa/features/extractors/dotnetfile.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import struct import logging from typing import Iterator from pathlib import Path @@ -184,8 +185,15 @@ def extract_global_features(pe: dnfile.dnPE) -> Iterator[tuple[Feature, Address] class DotnetFileFeatureExtractor(StaticFeatureExtractor): def __init__(self, path: Path): super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes())) - self.path: Path = path - self.pe: dnfile.dnPE = dnfile.dnPE(str(path)) + self.path = path + try: + self.pe = dnfile.dnPE(str(path)) + except struct.error as e: + from capa.loader import CorruptFile + + raise CorruptFile( + "Invalid or truncated .NET metadata; the file may be corrupted or not a valid .NET PE." + ) from e def get_base_address(self): return NO_ADDRESS From 6b36ff438ae06da4d8d1cffa00e0943923b33830 Mon Sep 17 00:00:00 2001 From: devs6186 Date: Tue, 24 Feb 2026 21:41:46 +0530 Subject: [PATCH 2/3] dnfile: centralize dnPE() loading in load_dotnet_image helper Add load_dotnet_image() to dnfile/helpers.py that calls dnfile.dnPE() and catches struct.error, raising CorruptFile with the original error message included (f"Invalid or truncated .NET metadata: {e}"). Both DnfileFeatureExtractor and DotnetFileFeatureExtractor now call the helper instead of duplicating the try/except block, and their direct import of struct is removed. Addresses review feedback on #2872. --- capa/features/extractors/dnfile/extractor.py | 49 +++++--- capa/features/extractors/dnfile/helpers.py | 125 +++++++++++++++---- capa/features/extractors/dotnetfile.py | 89 ++++++++----- 3 files changed, 191 insertions(+), 72 deletions(-) diff --git a/capa/features/extractors/dnfile/extractor.py b/capa/features/extractors/dnfile/extractor.py index d1316ea1b8..51b25873ad 100644 --- a/capa/features/extractors/dnfile/extractor.py +++ b/capa/features/extractors/dnfile/extractor.py @@ -15,7 +15,6 @@ from __future__ import annotations -import struct from typing import Union, Iterator, Optional from pathlib import Path @@ -28,7 +27,12 @@ import capa.features.extractors.dnfile.insn import capa.features.extractors.dnfile.function from capa.features.common import Feature -from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress +from capa.features.address import ( + NO_ADDRESS, + Address, + DNTokenAddress, + DNTokenOffsetAddress, +) from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod from capa.features.extractors.base_extractor import ( BBHandle, @@ -40,6 +44,7 @@ from capa.features.extractors.dnfile.helpers import ( get_dotnet_types, get_dotnet_fields, + load_dotnet_image, get_dotnet_managed_imports, get_dotnet_managed_methods, get_dotnet_unmanaged_imports, @@ -69,7 +74,9 @@ def __init__(self, pe: dnfile.dnPE): def get_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: return self.imports.get(token) - def get_native_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: + def get_native_import( + self, token: int + ) -> Optional[Union[DnType, DnUnmanagedMethod]]: return self.native_imports.get(token) def get_method(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: @@ -84,25 +91,26 @@ def get_type(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: class DnfileFeatureExtractor(StaticFeatureExtractor): def __init__(self, path: Path): - try: - self.pe = dnfile.dnPE(str(path)) - except struct.error as e: - from capa.loader import CorruptFile - - raise CorruptFile( - "Invalid or truncated .NET metadata; the file may be corrupted or not a valid .NET PE." - ) from e + self.pe = load_dotnet_image(path) super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes())) # pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction # most relevant at instruction scope - self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache(self.pe) + self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache( + self.pe + ) # pre-compute these because we'll yield them at *every* scope. self.global_features: list[tuple[Feature, Address]] = [] - self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format()) - self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe)) - self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe)) + self.global_features.extend( + capa.features.extractors.dotnetfile.extract_file_format() + ) + self.global_features.extend( + capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe) + ) + self.global_features.extend( + capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe) + ) def get_base_address(self): return NO_ADDRESS @@ -120,7 +128,12 @@ def get_functions(self) -> Iterator[FunctionHandle]: fh: FunctionHandle = FunctionHandle( address=DNTokenAddress(token), inner=method, - ctx={"pe": self.pe, "calls_from": set(), "calls_to": set(), "cache": self.token_cache}, + ctx={ + "pe": self.pe, + "calls_from": set(), + "calls_to": set(), + "cache": self.token_cache, + }, ) # method tokens should be unique @@ -168,7 +181,9 @@ def extract_basic_block_features(self, fh, bbh): def get_instructions(self, fh, bbh): for insn in bbh.inner.instructions: yield InsnHandle( - address=DNTokenOffsetAddress(bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size)), + address=DNTokenOffsetAddress( + bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size) + ), inner=insn, ) diff --git a/capa/features/extractors/dnfile/helpers.py b/capa/features/extractors/dnfile/helpers.py index 5decca5340..a42cc121c0 100644 --- a/capa/features/extractors/dnfile/helpers.py +++ b/capa/features/extractors/dnfile/helpers.py @@ -15,8 +15,10 @@ from __future__ import annotations +import struct import logging from typing import Union, Iterator, Optional +from pathlib import Path import dnfile from dncil.cil.body import CilMethodBody @@ -30,6 +32,16 @@ logger = logging.getLogger(__name__) +def load_dotnet_image(path: Path) -> dnfile.dnPE: + """load a .NET PE file, raising CorruptFile on struct.error with the original error message.""" + try: + return dnfile.dnPE(str(path)) + except struct.error as e: + from capa.loader import CorruptFile + + raise CorruptFile(f"Invalid or truncated .NET metadata: {e}") from e + + class DnfileMethodBodyReader(CilMethodBodyReaderBase): def __init__(self, pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow): self.pe: dnfile.dnPE = pe @@ -48,7 +60,9 @@ def seek(self, offset: int) -> int: return self.offset -def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDTableRow, InvalidToken, str]: +def resolve_dotnet_token( + pe: dnfile.dnPE, token: Token +) -> Union[dnfile.base.MDTableRow, InvalidToken, str]: """map generic token to string or table row""" assert pe.net is not None assert pe.net.mdtables is not None @@ -59,7 +73,9 @@ def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDT return InvalidToken(token.value) return user_string - table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(token.table) + table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get( + token.table + ) if table is None: # table index is not valid return InvalidToken(token.value) @@ -71,7 +87,9 @@ def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDT return InvalidToken(token.value) -def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -> Optional[CilMethodBody]: +def read_dotnet_method_body( + pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow +) -> Optional[CilMethodBody]: """read dotnet method body""" try: return CilMethodBody(DnfileMethodBodyReader(pe, row)) @@ -90,7 +108,9 @@ def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str return None try: - user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get(token.rid) + user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get( + token.rid + ) except UnicodeDecodeError as e: logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e) return None @@ -151,7 +171,9 @@ def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]: ) -def get_dotnet_methoddef_property_accessors(pe: dnfile.dnPE) -> Iterator[tuple[int, str]]: +def get_dotnet_methoddef_property_accessors( + pe: dnfile.dnPE, +) -> Iterator[tuple[int, str]]: """get MethodDef methods used to access properties see https://www.ntcore.com/files/dotnetformat.htm @@ -162,7 +184,9 @@ def get_dotnet_methoddef_property_accessors(pe: dnfile.dnPE) -> Iterator[tuple[i Method (index into the MethodDef table) Association (index into the Event or Property table; more precisely, a HasSemantics coded index) """ - for rid, method_semantics in iter_dotnet_table(pe, dnfile.mdtable.MethodSemantics.number): + for rid, method_semantics in iter_dotnet_table( + pe, dnfile.mdtable.MethodSemantics.number + ): assert isinstance(method_semantics, dnfile.mdtable.MethodSemanticsRow) if method_semantics.Association.row is None: @@ -216,7 +240,9 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]: logger.debug("TypeDef[0x%X] MethodList[0x%X] row is None", rid, idx) continue - token: int = calculate_dotnet_token_value(method.table.number, method.row_index) + token: int = calculate_dotnet_token_value( + method.table.number, method.row_index + ) access: Optional[str] = accessor_map.get(token) method_name: str = str(method.row.Name) @@ -224,9 +250,17 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]: # remove get_/set_ method_name = method_name[4:] - typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) + typedefnamespace, typedefname = resolve_nested_typedef_name( + nested_class_table, rid, typedef, pe + ) - yield DnType(token, typedefname, namespace=typedefnamespace, member=method_name, access=access) + yield DnType( + token, + typedefname, + namespace=typedefnamespace, + member=method_name, + access=access, + ) def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]: @@ -253,18 +287,28 @@ def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]: logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx) continue - typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) + typedefnamespace, typedefname = resolve_nested_typedef_name( + nested_class_table, rid, typedef, pe + ) - token: int = calculate_dotnet_token_value(field.table.number, field.row_index) - yield DnType(token, typedefname, namespace=typedefnamespace, member=field.row.Name) + token: int = calculate_dotnet_token_value( + field.table.number, field.row_index + ) + yield DnType( + token, typedefname, namespace=typedefnamespace, member=field.row.Name + ) -def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[tuple[int, CilMethodBody]]: +def get_dotnet_managed_method_bodies( + pe: dnfile.dnPE, +) -> Iterator[tuple[int, CilMethodBody]]: """get managed methods from MethodDef table""" for rid, method_def in iter_dotnet_table(pe, dnfile.mdtable.MethodDef.number): assert isinstance(method_def, dnfile.mdtable.MethodDefRow) - if not method_def.ImplFlags.miIL or any((method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl)): + if not method_def.ImplFlags.miIL or any( + (method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl) + ): # skip methods that do not have a method body continue @@ -310,7 +354,9 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod] # ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the # name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded # MethodDef table token to help us later record native import method calls made from CIL - token: int = calculate_dotnet_token_value(member_forward_table, member_forward_row) + token: int = calculate_dotnet_token_value( + member_forward_table, member_forward_row + ) # like Kernel32.dll if module and "." in module: @@ -320,14 +366,18 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod] yield DnUnmanagedMethod(token, module, method) -def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> Optional[dnfile.base.MDTableRow]: +def get_dotnet_table_row( + pe: dnfile.dnPE, table_index: int, row_index: int +) -> Optional[dnfile.base.MDTableRow]: assert pe.net is not None assert pe.net.mdtables is not None if row_index - 1 <= 0: return None - table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(table_index) + table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get( + table_index + ) if table is None: return None @@ -338,7 +388,10 @@ def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> O def resolve_nested_typedef_name( - nested_class_table: dict, index: int, typedef: dnfile.mdtable.TypeDefRow, pe: dnfile.dnPE + nested_class_table: dict, + index: int, + typedef: dnfile.mdtable.TypeDefRow, + pe: dnfile.dnPE, ) -> tuple[str, tuple[str, ...]]: """Resolves all nested TypeDef class names. Returns the namespace as a str and the nested TypeRef name as a tuple""" @@ -351,7 +404,9 @@ def resolve_nested_typedef_name( while nested_class_table[index] in nested_class_table: # Iterate through the typedef table to resolve the nested name - table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index]) + table_row = get_dotnet_table_row( + pe, dnfile.mdtable.TypeDef.number, nested_class_table[index] + ) if table_row is None: return str(typedef.TypeNamespace), tuple(typedef_name[::-1]) @@ -360,7 +415,9 @@ def resolve_nested_typedef_name( index = nested_class_table[index] # Document the root enclosing details - table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index]) + table_row = get_dotnet_table_row( + pe, dnfile.mdtable.TypeDef.number, nested_class_table[index] + ) if table_row is None: return str(typedef.TypeNamespace), tuple(typedef_name[::-1]) @@ -392,7 +449,9 @@ def resolve_nested_typeref_name( # Iterate through the typeref table to resolve the nested name typeref_name.append(name) name = str(table_row.TypeName) - table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeRef.number, table_row.ResolutionScope.row_index) + table_row = get_dotnet_table_row( + pe, dnfile.mdtable.TypeRef.number, table_row.ResolutionScope.row_index + ) if table_row is None: return str(typeref.TypeNamespace), tuple(typeref_name[::-1]) @@ -412,7 +471,9 @@ def get_dotnet_nested_class_table_index(pe: dnfile.dnPE) -> dict[int, int]: # Used to find nested classes in typedef for _, nestedclass in iter_dotnet_table(pe, dnfile.mdtable.NestedClass.number): assert isinstance(nestedclass, dnfile.mdtable.NestedClassRow) - nested_class_table[nestedclass.NestedClass.row_index] = nestedclass.EnclosingClass.row_index + nested_class_table[nestedclass.NestedClass.row_index] = ( + nestedclass.EnclosingClass.row_index + ) return nested_class_table @@ -424,17 +485,25 @@ def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]: for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number): assert isinstance(typedef, dnfile.mdtable.TypeDefRow) - typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) + typedefnamespace, typedefname = resolve_nested_typedef_name( + nested_class_table, rid, typedef, pe + ) - typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid) + typedef_token: int = calculate_dotnet_token_value( + dnfile.mdtable.TypeDef.number, rid + ) yield DnType(typedef_token, typedefname, namespace=typedefnamespace) for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number): assert isinstance(typeref, dnfile.mdtable.TypeRefRow) - typerefnamespace, typerefname = resolve_nested_typeref_name(typeref.ResolutionScope.row_index, typeref, pe) + typerefnamespace, typerefname = resolve_nested_typeref_name( + typeref.ResolutionScope.row_index, typeref, pe + ) - typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid) + typeref_token: int = calculate_dotnet_token_value( + dnfile.mdtable.TypeRef.number, rid + ) yield DnType(typeref_token, typerefname, namespace=typerefnamespace) @@ -449,7 +518,9 @@ def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool: return not bool(pe.net.Flags.CLR_ILONLY) -def iter_dotnet_table(pe: dnfile.dnPE, table_index: int) -> Iterator[tuple[int, dnfile.base.MDTableRow]]: +def iter_dotnet_table( + pe: dnfile.dnPE, table_index: int +) -> Iterator[tuple[int, dnfile.base.MDTableRow]]: assert pe.net is not None assert pe.net.mdtables is not None diff --git a/capa/features/extractors/dotnetfile.py b/capa/features/extractors/dotnetfile.py index 919f4fff6c..0bae0929ab 100644 --- a/capa/features/extractors/dotnetfile.py +++ b/capa/features/extractors/dotnetfile.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import struct import logging from typing import Iterator from pathlib import Path @@ -43,6 +42,7 @@ from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor from capa.features.extractors.dnfile.helpers import ( iter_dotnet_table, + load_dotnet_image, is_dotnet_mixed_mode, get_dotnet_managed_imports, get_dotnet_managed_methods, @@ -61,23 +61,31 @@ def extract_file_format(**kwargs) -> Iterator[tuple[Format, Address]]: yield Format(FORMAT_PE), NO_ADDRESS -def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Import, Address]]: +def extract_file_import_names( + pe: dnfile.dnPE, **kwargs +) -> Iterator[tuple[Import, Address]]: for method in get_dotnet_managed_imports(pe): # like System.IO.File::OpenRead yield Import(str(method)), DNTokenAddress(method.token) for imp in get_dotnet_unmanaged_imports(pe): # like kernel32.CreateFileA - for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method, include_dll=True): + for name in capa.features.extractors.helpers.generate_symbols( + imp.module, imp.method, include_dll=True + ): yield Import(name), DNTokenAddress(imp.token) -def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[FunctionName, Address]]: +def extract_file_function_names( + pe: dnfile.dnPE, **kwargs +) -> Iterator[tuple[FunctionName, Address]]: for method in get_dotnet_managed_methods(pe): yield FunctionName(str(method)), DNTokenAddress(method.token) -def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Namespace, Address]]: +def extract_file_namespace_features( + pe: dnfile.dnPE, **kwargs +) -> Iterator[tuple[Namespace, Address]]: """emit namespace features from TypeRef and TypeDef tables""" # namespaces may be referenced multiple times, so we need to filter @@ -101,7 +109,9 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple yield Namespace(namespace), NO_ADDRESS -def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Class, Address]]: +def extract_file_class_features( + pe: dnfile.dnPE, **kwargs +) -> Iterator[tuple[Class, Address]]: """emit class features from TypeRef and TypeDef tables""" nested_class_table = get_dotnet_nested_class_table_index(pe) @@ -109,19 +119,27 @@ def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Cla # emit internal .NET classes assert isinstance(typedef, dnfile.mdtable.TypeDefRow) - typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) + typedefnamespace, typedefname = resolve_nested_typedef_name( + nested_class_table, rid, typedef, pe + ) token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid) - yield Class(DnType.format_name(typedefname, namespace=typedefnamespace)), DNTokenAddress(token) + yield Class( + DnType.format_name(typedefname, namespace=typedefnamespace) + ), DNTokenAddress(token) for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number): # emit external .NET classes assert isinstance(typeref, dnfile.mdtable.TypeRefRow) - typerefnamespace, typerefname = resolve_nested_typeref_name(typeref.ResolutionScope.row_index, typeref, pe) + typerefnamespace, typerefname = resolve_nested_typeref_name( + typeref.ResolutionScope.row_index, typeref, pe + ) token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid) - yield Class(DnType.format_name(typerefname, namespace=typerefnamespace)), DNTokenAddress(token) + yield Class( + DnType.format_name(typerefname, namespace=typerefnamespace) + ), DNTokenAddress(token) def extract_file_os(**kwargs) -> Iterator[tuple[OS, Address]]: @@ -136,7 +154,10 @@ def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Arch, Address if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE: yield Arch(ARCH_I386), NO_ADDRESS - elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS: + elif ( + not pe.net.Flags.CLR_32BITREQUIRED + and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS + ): yield Arch(ARCH_AMD64), NO_ADDRESS else: yield Arch(ARCH_ANY), NO_ADDRESS @@ -186,14 +207,7 @@ class DotnetFileFeatureExtractor(StaticFeatureExtractor): def __init__(self, path: Path): super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes())) self.path = path - try: - self.pe = dnfile.dnPE(str(path)) - except struct.error as e: - from capa.loader import CorruptFile - - raise CorruptFile( - "Invalid or truncated .NET metadata; the file may be corrupted or not a valid .NET PE." - ) from e + self.pe = load_dotnet_image(path) def get_base_address(self): return NO_ADDRESS @@ -225,7 +239,10 @@ def get_runtime_version(self) -> tuple[int, int]: assert self.pe.net.struct.MajorRuntimeVersion is not None assert self.pe.net.struct.MinorRuntimeVersion is not None - return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion + return ( + self.pe.net.struct.MajorRuntimeVersion, + self.pe.net.struct.MinorRuntimeVersion, + ) def get_meta_version_string(self) -> str: assert self.pe.net is not None @@ -239,25 +256,41 @@ def get_meta_version_string(self) -> str: return vbuf.rstrip(b"\x00").decode("utf-8") def get_functions(self): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def extract_function_features(self, f): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def get_basic_blocks(self, f): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def extract_basic_block_features(self, f, bb): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def get_instructions(self, f, bb): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def extract_insn_features(self, f, bb, insn): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def is_library_function(self, va): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) def get_function_name(self, va): - raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") + raise NotImplementedError( + "DotnetFileFeatureExtractor can only be used to extract file features" + ) From bc3176b025ed6e5aaaa4e1b08bcdc3da7c10273a Mon Sep 17 00:00:00 2001 From: devs6186 Date: Fri, 27 Feb 2026 12:45:35 +0530 Subject: [PATCH 3/3] style: reformat dnfile files with black --line-length=120 Fixes CI code_style failure by applying the project's configured line length (120) instead of black's default (88). --- capa/features/extractors/dnfile/extractor.py | 24 ++--- capa/features/extractors/dnfile/helpers.py | 92 +++++--------------- capa/features/extractors/dotnetfile.py | 73 ++++------------ 3 files changed, 47 insertions(+), 142 deletions(-) diff --git a/capa/features/extractors/dnfile/extractor.py b/capa/features/extractors/dnfile/extractor.py index 51b25873ad..84fc886974 100644 --- a/capa/features/extractors/dnfile/extractor.py +++ b/capa/features/extractors/dnfile/extractor.py @@ -74,9 +74,7 @@ def __init__(self, pe: dnfile.dnPE): def get_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: return self.imports.get(token) - def get_native_import( - self, token: int - ) -> Optional[Union[DnType, DnUnmanagedMethod]]: + def get_native_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: return self.native_imports.get(token) def get_method(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]: @@ -96,21 +94,13 @@ def __init__(self, path: Path): # pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction # most relevant at instruction scope - self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache( - self.pe - ) + self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache(self.pe) # pre-compute these because we'll yield them at *every* scope. self.global_features: list[tuple[Feature, Address]] = [] - self.global_features.extend( - capa.features.extractors.dotnetfile.extract_file_format() - ) - self.global_features.extend( - capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe) - ) - self.global_features.extend( - capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe) - ) + self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format()) + self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe)) + self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe)) def get_base_address(self): return NO_ADDRESS @@ -181,9 +171,7 @@ def extract_basic_block_features(self, fh, bbh): def get_instructions(self, fh, bbh): for insn in bbh.inner.instructions: yield InsnHandle( - address=DNTokenOffsetAddress( - bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size) - ), + address=DNTokenOffsetAddress(bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size)), inner=insn, ) diff --git a/capa/features/extractors/dnfile/helpers.py b/capa/features/extractors/dnfile/helpers.py index a42cc121c0..fa103ad12a 100644 --- a/capa/features/extractors/dnfile/helpers.py +++ b/capa/features/extractors/dnfile/helpers.py @@ -60,9 +60,7 @@ def seek(self, offset: int) -> int: return self.offset -def resolve_dotnet_token( - pe: dnfile.dnPE, token: Token -) -> Union[dnfile.base.MDTableRow, InvalidToken, str]: +def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDTableRow, InvalidToken, str]: """map generic token to string or table row""" assert pe.net is not None assert pe.net.mdtables is not None @@ -73,9 +71,7 @@ def resolve_dotnet_token( return InvalidToken(token.value) return user_string - table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get( - token.table - ) + table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(token.table) if table is None: # table index is not valid return InvalidToken(token.value) @@ -87,9 +83,7 @@ def resolve_dotnet_token( return InvalidToken(token.value) -def read_dotnet_method_body( - pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow -) -> Optional[CilMethodBody]: +def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -> Optional[CilMethodBody]: """read dotnet method body""" try: return CilMethodBody(DnfileMethodBodyReader(pe, row)) @@ -108,9 +102,7 @@ def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str return None try: - user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get( - token.rid - ) + user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get(token.rid) except UnicodeDecodeError as e: logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e) return None @@ -184,9 +176,7 @@ def get_dotnet_methoddef_property_accessors( Method (index into the MethodDef table) Association (index into the Event or Property table; more precisely, a HasSemantics coded index) """ - for rid, method_semantics in iter_dotnet_table( - pe, dnfile.mdtable.MethodSemantics.number - ): + for rid, method_semantics in iter_dotnet_table(pe, dnfile.mdtable.MethodSemantics.number): assert isinstance(method_semantics, dnfile.mdtable.MethodSemanticsRow) if method_semantics.Association.row is None: @@ -240,9 +230,7 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]: logger.debug("TypeDef[0x%X] MethodList[0x%X] row is None", rid, idx) continue - token: int = calculate_dotnet_token_value( - method.table.number, method.row_index - ) + token: int = calculate_dotnet_token_value(method.table.number, method.row_index) access: Optional[str] = accessor_map.get(token) method_name: str = str(method.row.Name) @@ -250,9 +238,7 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]: # remove get_/set_ method_name = method_name[4:] - typedefnamespace, typedefname = resolve_nested_typedef_name( - nested_class_table, rid, typedef, pe - ) + typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) yield DnType( token, @@ -287,16 +273,10 @@ def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]: logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx) continue - typedefnamespace, typedefname = resolve_nested_typedef_name( - nested_class_table, rid, typedef, pe - ) + typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) - token: int = calculate_dotnet_token_value( - field.table.number, field.row_index - ) - yield DnType( - token, typedefname, namespace=typedefnamespace, member=field.row.Name - ) + token: int = calculate_dotnet_token_value(field.table.number, field.row_index) + yield DnType(token, typedefname, namespace=typedefnamespace, member=field.row.Name) def get_dotnet_managed_method_bodies( @@ -306,9 +286,7 @@ def get_dotnet_managed_method_bodies( for rid, method_def in iter_dotnet_table(pe, dnfile.mdtable.MethodDef.number): assert isinstance(method_def, dnfile.mdtable.MethodDefRow) - if not method_def.ImplFlags.miIL or any( - (method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl) - ): + if not method_def.ImplFlags.miIL or any((method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl)): # skip methods that do not have a method body continue @@ -354,9 +332,7 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod] # ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the # name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded # MethodDef table token to help us later record native import method calls made from CIL - token: int = calculate_dotnet_token_value( - member_forward_table, member_forward_row - ) + token: int = calculate_dotnet_token_value(member_forward_table, member_forward_row) # like Kernel32.dll if module and "." in module: @@ -366,18 +342,14 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod] yield DnUnmanagedMethod(token, module, method) -def get_dotnet_table_row( - pe: dnfile.dnPE, table_index: int, row_index: int -) -> Optional[dnfile.base.MDTableRow]: +def get_dotnet_table_row(pe: dnfile.dnPE, table_index: int, row_index: int) -> Optional[dnfile.base.MDTableRow]: assert pe.net is not None assert pe.net.mdtables is not None if row_index - 1 <= 0: return None - table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get( - table_index - ) + table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(table_index) if table is None: return None @@ -404,9 +376,7 @@ def resolve_nested_typedef_name( while nested_class_table[index] in nested_class_table: # Iterate through the typedef table to resolve the nested name - table_row = get_dotnet_table_row( - pe, dnfile.mdtable.TypeDef.number, nested_class_table[index] - ) + table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index]) if table_row is None: return str(typedef.TypeNamespace), tuple(typedef_name[::-1]) @@ -415,9 +385,7 @@ def resolve_nested_typedef_name( index = nested_class_table[index] # Document the root enclosing details - table_row = get_dotnet_table_row( - pe, dnfile.mdtable.TypeDef.number, nested_class_table[index] - ) + table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeDef.number, nested_class_table[index]) if table_row is None: return str(typedef.TypeNamespace), tuple(typedef_name[::-1]) @@ -449,9 +417,7 @@ def resolve_nested_typeref_name( # Iterate through the typeref table to resolve the nested name typeref_name.append(name) name = str(table_row.TypeName) - table_row = get_dotnet_table_row( - pe, dnfile.mdtable.TypeRef.number, table_row.ResolutionScope.row_index - ) + table_row = get_dotnet_table_row(pe, dnfile.mdtable.TypeRef.number, table_row.ResolutionScope.row_index) if table_row is None: return str(typeref.TypeNamespace), tuple(typeref_name[::-1]) @@ -471,9 +437,7 @@ def get_dotnet_nested_class_table_index(pe: dnfile.dnPE) -> dict[int, int]: # Used to find nested classes in typedef for _, nestedclass in iter_dotnet_table(pe, dnfile.mdtable.NestedClass.number): assert isinstance(nestedclass, dnfile.mdtable.NestedClassRow) - nested_class_table[nestedclass.NestedClass.row_index] = ( - nestedclass.EnclosingClass.row_index - ) + nested_class_table[nestedclass.NestedClass.row_index] = nestedclass.EnclosingClass.row_index return nested_class_table @@ -485,25 +449,17 @@ def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]: for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number): assert isinstance(typedef, dnfile.mdtable.TypeDefRow) - typedefnamespace, typedefname = resolve_nested_typedef_name( - nested_class_table, rid, typedef, pe - ) + typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) - typedef_token: int = calculate_dotnet_token_value( - dnfile.mdtable.TypeDef.number, rid - ) + typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid) yield DnType(typedef_token, typedefname, namespace=typedefnamespace) for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number): assert isinstance(typeref, dnfile.mdtable.TypeRefRow) - typerefnamespace, typerefname = resolve_nested_typeref_name( - typeref.ResolutionScope.row_index, typeref, pe - ) + typerefnamespace, typerefname = resolve_nested_typeref_name(typeref.ResolutionScope.row_index, typeref, pe) - typeref_token: int = calculate_dotnet_token_value( - dnfile.mdtable.TypeRef.number, rid - ) + typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid) yield DnType(typeref_token, typerefname, namespace=typerefnamespace) @@ -518,9 +474,7 @@ def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool: return not bool(pe.net.Flags.CLR_ILONLY) -def iter_dotnet_table( - pe: dnfile.dnPE, table_index: int -) -> Iterator[tuple[int, dnfile.base.MDTableRow]]: +def iter_dotnet_table(pe: dnfile.dnPE, table_index: int) -> Iterator[tuple[int, dnfile.base.MDTableRow]]: assert pe.net is not None assert pe.net.mdtables is not None diff --git a/capa/features/extractors/dotnetfile.py b/capa/features/extractors/dotnetfile.py index 0bae0929ab..192d9385e9 100644 --- a/capa/features/extractors/dotnetfile.py +++ b/capa/features/extractors/dotnetfile.py @@ -61,31 +61,23 @@ def extract_file_format(**kwargs) -> Iterator[tuple[Format, Address]]: yield Format(FORMAT_PE), NO_ADDRESS -def extract_file_import_names( - pe: dnfile.dnPE, **kwargs -) -> Iterator[tuple[Import, Address]]: +def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Import, Address]]: for method in get_dotnet_managed_imports(pe): # like System.IO.File::OpenRead yield Import(str(method)), DNTokenAddress(method.token) for imp in get_dotnet_unmanaged_imports(pe): # like kernel32.CreateFileA - for name in capa.features.extractors.helpers.generate_symbols( - imp.module, imp.method, include_dll=True - ): + for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method, include_dll=True): yield Import(name), DNTokenAddress(imp.token) -def extract_file_function_names( - pe: dnfile.dnPE, **kwargs -) -> Iterator[tuple[FunctionName, Address]]: +def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[FunctionName, Address]]: for method in get_dotnet_managed_methods(pe): yield FunctionName(str(method)), DNTokenAddress(method.token) -def extract_file_namespace_features( - pe: dnfile.dnPE, **kwargs -) -> Iterator[tuple[Namespace, Address]]: +def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Namespace, Address]]: """emit namespace features from TypeRef and TypeDef tables""" # namespaces may be referenced multiple times, so we need to filter @@ -109,9 +101,7 @@ def extract_file_namespace_features( yield Namespace(namespace), NO_ADDRESS -def extract_file_class_features( - pe: dnfile.dnPE, **kwargs -) -> Iterator[tuple[Class, Address]]: +def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Class, Address]]: """emit class features from TypeRef and TypeDef tables""" nested_class_table = get_dotnet_nested_class_table_index(pe) @@ -119,27 +109,19 @@ def extract_file_class_features( # emit internal .NET classes assert isinstance(typedef, dnfile.mdtable.TypeDefRow) - typedefnamespace, typedefname = resolve_nested_typedef_name( - nested_class_table, rid, typedef, pe - ) + typedefnamespace, typedefname = resolve_nested_typedef_name(nested_class_table, rid, typedef, pe) token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid) - yield Class( - DnType.format_name(typedefname, namespace=typedefnamespace) - ), DNTokenAddress(token) + yield Class(DnType.format_name(typedefname, namespace=typedefnamespace)), DNTokenAddress(token) for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number): # emit external .NET classes assert isinstance(typeref, dnfile.mdtable.TypeRefRow) - typerefnamespace, typerefname = resolve_nested_typeref_name( - typeref.ResolutionScope.row_index, typeref, pe - ) + typerefnamespace, typerefname = resolve_nested_typeref_name(typeref.ResolutionScope.row_index, typeref, pe) token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid) - yield Class( - DnType.format_name(typerefname, namespace=typerefnamespace) - ), DNTokenAddress(token) + yield Class(DnType.format_name(typerefname, namespace=typerefnamespace)), DNTokenAddress(token) def extract_file_os(**kwargs) -> Iterator[tuple[OS, Address]]: @@ -154,10 +136,7 @@ def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple[Arch, Address if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE: yield Arch(ARCH_I386), NO_ADDRESS - elif ( - not pe.net.Flags.CLR_32BITREQUIRED - and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS - ): + elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS: yield Arch(ARCH_AMD64), NO_ADDRESS else: yield Arch(ARCH_ANY), NO_ADDRESS @@ -256,41 +235,25 @@ def get_meta_version_string(self) -> str: return vbuf.rstrip(b"\x00").decode("utf-8") def get_functions(self): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def extract_function_features(self, f): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def get_basic_blocks(self, f): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def extract_basic_block_features(self, f, bb): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def get_instructions(self, f, bb): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def extract_insn_features(self, f, bb, insn): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def is_library_function(self, va): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features") def get_function_name(self, va): - raise NotImplementedError( - "DotnetFileFeatureExtractor can only be used to extract file features" - ) + raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")