From 9e37c329746dd2c4fc0d05f7315cb278dc6aed50 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 20 Jul 2026 16:04:06 -0700 Subject: [PATCH 1/4] Make native-only loaders optional on Emscripten --- cle/backends/__init__.py | 10 ++++++++-- cle/backends/pe/pe.py | 10 +++++++++- pyproject.toml | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/cle/backends/__init__.py b/cle/backends/__init__.py index 05a330e4..ad6ea88c 100644 --- a/cle/backends/__init__.py +++ b/cle/backends/__init__.py @@ -20,10 +20,14 @@ from .static_archive import StaticArchive from .symbol import Symbol, SymbolSubType, SymbolType from .te import TE -from .uefi_firmware import UefiFirmware from .universal2 import Universal2 from .xbe import XBE +try: + from .uefi_firmware import UefiFirmware +except ImportError: + UefiFirmware = None + # BinjaBin is not imported by default since importing it is too slow # you may manually import it by running `from cle.backends.binja import BinjaBin` @@ -60,8 +64,10 @@ "Symbol", "SymbolType", "SymbolSubType", - "UefiFirmware", "TE", "Universal2", "CARTFile", ] + +if UefiFirmware is not None: + __all__.append("UefiFirmware") diff --git a/cle/backends/pe/pe.py b/cle/backends/pe/pe.py index f9b72b03..25b7d57f 100644 --- a/cle/backends/pe/pe.py +++ b/cle/backends/pe/pe.py @@ -9,7 +9,11 @@ import archinfo import pefile -import pyxdia + +try: + import pyxdia +except ImportError: + pyxdia = None from cle.address_translator import AT from cle.backends.backend import Backend, FunctionHint, FunctionHintSource, register_backend @@ -215,6 +219,10 @@ def load_symbols_from_pdb(self, pdb_path): """ Load available symbols from PDB at `pdb_path` """ + if pyxdia is None: + log.warning("PDB support is unavailable because pyxdia is not installed") + return + log.debug("Loading symbols from %s", pdb_path) try: pdb = pyxdia.PDB(pdb_path) diff --git a/pyproject.toml b/pyproject.toml index 54985f3e..0e3f8921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,9 +25,9 @@ dependencies = [ "pyelftools>=0.29", "pyvex==9.3.1.dev0", "pyxbe~=1.0.3", - "pyxdia~=0.1", + "pyxdia~=0.1; sys_platform != 'emscripten'", "sortedcontainers>=2.0", - "uefi-firmware~=1.16", + "uefi-firmware~=1.16; sys_platform != 'emscripten'", ] [project.readme] From 45fe394cb1b401cba6f33af49913f4aa4a18b2b1 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 21 Jul 2026 12:47:03 -0700 Subject: [PATCH 2/4] Keep optional UEFI handling in its backend --- cle/backends/__init__.py | 10 ++-------- cle/backends/uefi_firmware.py | 31 +++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/cle/backends/__init__.py b/cle/backends/__init__.py index ad6ea88c..05a330e4 100644 --- a/cle/backends/__init__.py +++ b/cle/backends/__init__.py @@ -20,14 +20,10 @@ from .static_archive import StaticArchive from .symbol import Symbol, SymbolSubType, SymbolType from .te import TE +from .uefi_firmware import UefiFirmware from .universal2 import Universal2 from .xbe import XBE -try: - from .uefi_firmware import UefiFirmware -except ImportError: - UefiFirmware = None - # BinjaBin is not imported by default since importing it is too slow # you may manually import it by running `from cle.backends.binja import BinjaBin` @@ -64,10 +60,8 @@ "Symbol", "SymbolType", "SymbolSubType", + "UefiFirmware", "TE", "Universal2", "CARTFile", ] - -if UefiFirmware is not None: - __all__.append("UefiFirmware") diff --git a/cle/backends/uefi_firmware.py b/cle/backends/uefi_firmware.py index c93525d2..da8ce58d 100644 --- a/cle/backends/uefi_firmware.py +++ b/cle/backends/uefi_firmware.py @@ -8,7 +8,11 @@ from uuid import UUID import archinfo -import uefi_firmware + +try: + import uefi_firmware +except ImportError: + uefi_firmware = None from cle.errors import CLEUnknownFormatError @@ -50,11 +54,15 @@ def _to_bytes(cls, fileobj: io.IOBase): @classmethod def is_compatible(cls, stream): + if uefi_firmware is None: + return False buffer = cls._to_bytes(stream) parser = uefi_firmware.AutoParser(buffer) return parser.type() != "unknown" def __init__(self, *args, **kwargs) -> None: + if uefi_firmware is None: + raise ImportError("The UEFI backend requires the uefi-firmware package") super().__init__(*args, **kwargs) # hack: we are using a loader internal method in a non-kosher way which will cause our children to be @@ -97,11 +105,10 @@ def __init__(self, *args, **kwargs) -> None: self.loader._main_object = None @singledispatchmethod - def _load(self, uefi_obj): # pylint: disable=no-self-use + def _load(self, uefi_obj): raise CLEUnknownFormatError(f"Can't load firmware object: {uefi_obj}") - @_load.register - def _load_generic(self, uefi_obj: uefi_firmware.FirmwareObject): + def _load_generic(self, uefi_obj): for obj in uefi_obj.objects: self._load(obj) @@ -109,8 +116,7 @@ def _load_generic(self, uefi_obj: uefi_firmware.FirmwareObject): def _load_none(self, uefi_obj: None): pass - @_load.register - def _load_firmwarefile(self, uefi_obj: uefi_firmware.uefi.FirmwareFile): + def _load_firmwarefile(self, uefi_obj): old_uuid = self._current_file if uefi_obj.type == 7: # driver uuid = UUID(bytes=uefi_obj.guid) @@ -119,8 +125,7 @@ def _load_firmwarefile(self, uefi_obj: uefi_firmware.uefi.FirmwareFile): self._load_generic(uefi_obj) self._current_file = old_uuid - @_load.register - def _load_firmwarefilesection(self, uefi_obj: uefi_firmware.uefi.FirmwareFileSystemSection): + def _load_firmwarefilesection(self, uefi_obj): pending = self._drivers_pending.get(self._current_file, None) if pending is not None: if uefi_obj.type == 16: # pe32 image @@ -176,7 +181,7 @@ def __init__(self, *args, guid: UUID, name: str | None, **kwargs): def __repr__(self): return ( f"<{type(self).__name__} Object " - f'{self.guid}{f" {self.user_interface_name}" if self.user_interface_name else ""}, ' + f"{self.guid}{f' {self.user_interface_name}' if self.user_interface_name else ''}, " f"maps [{self.min_addr:#x}:{self.max_addr:#x}]>" ) @@ -193,4 +198,10 @@ class UefiTE(UefiModuleMixin, TE): """ -register_backend("uefi", UefiFirmware) +if uefi_firmware is not None: + # singledispatchmethod exposes register on the descriptor; pylint sees only the bound method here. + # pylint: disable=no-member + UefiFirmware._load.register(uefi_firmware.FirmwareObject)(UefiFirmware._load_generic) + UefiFirmware._load.register(uefi_firmware.uefi.FirmwareFile)(UefiFirmware._load_firmwarefile) + UefiFirmware._load.register(uefi_firmware.uefi.FirmwareFileSystemSection)(UefiFirmware._load_firmwarefilesection) + register_backend("uefi", UefiFirmware) From 7a69573c502732ec5cc31da152671d5769387f6b Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 21 Jul 2026 13:01:18 -0700 Subject: [PATCH 3/4] Satisfy optional no-self-use lint --- cle/backends/uefi_firmware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cle/backends/uefi_firmware.py b/cle/backends/uefi_firmware.py index da8ce58d..0897145c 100644 --- a/cle/backends/uefi_firmware.py +++ b/cle/backends/uefi_firmware.py @@ -106,7 +106,7 @@ def __init__(self, *args, **kwargs) -> None: @singledispatchmethod def _load(self, uefi_obj): - raise CLEUnknownFormatError(f"Can't load firmware object: {uefi_obj}") + raise CLEUnknownFormatError(f"{type(self).__name__} can't load firmware object: {uefi_obj}") def _load_generic(self, uefi_obj): for obj in uefi_obj.objects: From f01c699249127e92871b622aab7fc1ec228e56cd Mon Sep 17 00:00:00 2001 From: Kevin Phoenix Date: Tue, 21 Jul 2026 16:25:19 -0700 Subject: [PATCH 4/4] Revert llm lint fixes --- cle/backends/uefi_firmware.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/cle/backends/uefi_firmware.py b/cle/backends/uefi_firmware.py index 0897145c..8301ab92 100644 --- a/cle/backends/uefi_firmware.py +++ b/cle/backends/uefi_firmware.py @@ -105,10 +105,11 @@ def __init__(self, *args, **kwargs) -> None: self.loader._main_object = None @singledispatchmethod - def _load(self, uefi_obj): - raise CLEUnknownFormatError(f"{type(self).__name__} can't load firmware object: {uefi_obj}") + def _load(self, uefi_obj): # pylint: disable=no-self-use + raise CLEUnknownFormatError(f"Can't load firmware object: {uefi_obj}") - def _load_generic(self, uefi_obj): + @_load.register + def _load_generic(self, uefi_obj: uefi_firmware.FirmwareObject): for obj in uefi_obj.objects: self._load(obj) @@ -116,7 +117,8 @@ def _load_generic(self, uefi_obj): def _load_none(self, uefi_obj: None): pass - def _load_firmwarefile(self, uefi_obj): + @_load.register + def _load_firmwarefile(self, uefi_obj: uefi_firmware.uefi.FirmwareFile): old_uuid = self._current_file if uefi_obj.type == 7: # driver uuid = UUID(bytes=uefi_obj.guid) @@ -125,7 +127,8 @@ def _load_firmwarefile(self, uefi_obj): self._load_generic(uefi_obj) self._current_file = old_uuid - def _load_firmwarefilesection(self, uefi_obj): + @_load.register + def _load_firmwarefilesection(self, uefi_obj: uefi_firmware.uefi.FirmwareFileSystemSection): pending = self._drivers_pending.get(self._current_file, None) if pending is not None: if uefi_obj.type == 16: # pe32 image @@ -181,7 +184,7 @@ def __init__(self, *args, guid: UUID, name: str | None, **kwargs): def __repr__(self): return ( f"<{type(self).__name__} Object " - f"{self.guid}{f' {self.user_interface_name}' if self.user_interface_name else ''}, " + f'{self.guid}{f" {self.user_interface_name}" if self.user_interface_name else ""}, ' f"maps [{self.min_addr:#x}:{self.max_addr:#x}]>" ) @@ -198,10 +201,4 @@ class UefiTE(UefiModuleMixin, TE): """ -if uefi_firmware is not None: - # singledispatchmethod exposes register on the descriptor; pylint sees only the bound method here. - # pylint: disable=no-member - UefiFirmware._load.register(uefi_firmware.FirmwareObject)(UefiFirmware._load_generic) - UefiFirmware._load.register(uefi_firmware.uefi.FirmwareFile)(UefiFirmware._load_firmwarefile) - UefiFirmware._load.register(uefi_firmware.uefi.FirmwareFileSystemSection)(UefiFirmware._load_firmwarefilesection) - register_backend("uefi", UefiFirmware) +register_backend("uefi", UefiFirmware)