From 9fb70a2e5ee052ae53eeec10769e6f7b2275a57a Mon Sep 17 00:00:00 2001 From: OctavioValdiviaMendoza Date: Tue, 21 Jul 2026 12:15:42 -0700 Subject: [PATCH 1/7] Initial Implementation of Linux kernel module package parser --- src/packagedcode/lkm.py | 160 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/packagedcode/lkm.py diff --git a/src/packagedcode/lkm.py b/src/packagedcode/lkm.py new file mode 100644 index 0000000000..76a731aa92 --- /dev/null +++ b/src/packagedcode/lkm.py @@ -0,0 +1,160 @@ +import re +from typing import Iterator, Dict, Any, List, Optional +from packagedcode.models import DatafileHandler, PackageData, DependentPackage, Party +import os + +from elftools.common.exceptions import ELFError +from elftools.elf.elffile import ELFFile + + +# Inside ScanCode, we would import the official classes: +# from packagedcode.models import Package, Party, Dependency +# from packagedcode import DatafileHandler + +class LinuxKernelModuleHandler(DatafileHandler): + """ + DatafileHandler for compiled Linux Kernel Modules (.ko binaries). + Extracts the .modinfo section and maps it to ScanCode's standard PackageData. + """ + + datasource_id = 'linux_kernel_module' + datasource_type = 'sys' + supported_oses = ('linux',) + default_package_type = 'linux-kernel-module' + path_patterns = ('*.ko',) + description = 'Linux Kernel Module' + documentation_url = "..." + + @classmethod + def parse(cls, location: str, package_only=False) -> Iterator[PackageData]: + #Main entry point called by ScanCode when scanning directories. + raw_metadata = cls.extract_modinfo(location) + if not raw_metadata: + return + + #Ensures no empty or invalid package data is yielded + + yield cls.build_package( + metadata=raw_metadata, + location=location, + package_only=package_only, + ) + + + @staticmethod + def extract_modinfo(location: str) -> Dict[str, List[str]]: + """ + Reads the .modinfo byte section from the ELF file in-memory. + Uses pyelftools (which is already a ScanCode dependency). + """ + + metadata: Dict[str, List[str]] = {} + + try: + with open(location, 'rb') as module_file: + elffile = ELFFile(module_file) + + # Locate the specific .modinfo section in the ELF structure + modinfo_sec = elffile.get_section_by_name('.modinfo') + if modinfo_sec is None: + return {} + + # Extract the raw binary block + raw_bytes = modinfo_sec.data() + + except (ELFError, OSError): + return {} + # Split null-terminated bytes on \x00 + for raw_entry in raw_bytes.split(b'\x00'): + if not raw_entry: + continue + + entry = raw_entry.decode('utf-8', errors='replace') + + if '=' not in entry: + continue + + key, value = entry.split('=', 1) + + if not key: + continue + + metadata.setdefault(key, []).append(value) + + return metadata + + @staticmethod + def get_first(metadata: Dict[str, List[str]], key: str) -> Optional[str]: + values = metadata.get(key) or [] + return values[0] if values else None + + @staticmethod + def get_dependencies(metadata: Dict[str, List[str]]) -> List[str]: + dependencies = [] + + for depends_entry in metadata.get('depends', []): + dependencies.extend( + dependency.strip() + for dependency in depends_entry.split(',') + if dependency.strip() + ) + + return dependencies + + @classmethod + def build_package(cls, metadata: Dict[str, Any], location: str, package_only: bool = False) -> PackageData: + """ + Maps raw .modinfo dictionary keys into ScanCode's standard PackageData models. + """ + # 1. Map Authors to standard 'Party' objects + + parties = [] + for author in metadata.get('author', []): + author = author.strip() + + if author: + parties.append( + Party( + type='person', + role='author', + name=author, + ) + ) + + + + filename = os.path.basename(location) + name = filename[:-3] if filename.endswith('.ko') else filename + + + normalized_keys = { + 'author', + 'description', + 'license', + 'version', + } + + extra_data = { + "srcversion": cls.get_first(metadata, "srcversion"), + "aliases": metadata.get("alias", []), + "intree": cls.get_first(metadata, "intree"), + "vermagic": cls.get_first(metadata, "vermagic"), + "Depends" : cls.get_dependencies(metadata) + } + + package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + name=name, + version=cls.get_first(metadata, 'version'), + description=cls.get_first(metadata, 'description'), + extracted_license_statement=cls.get_first(metadata,'license'), + parties=parties, + extra_data=extra_data, + ) + + return PackageData.from_data( + package_data=package_data, + package_only=package_only, + ) + \ No newline at end of file From 92c64664ae1feec530bcca5edf4ddc69f624ee13 Mon Sep 17 00:00:00 2001 From: OctavioValdiviaMendoza Date: Wed, 22 Jul 2026 11:34:22 -0700 Subject: [PATCH 2/7] Add Linux kernel module package handler issue 625 Parsesthrough metadata using .modinfo section found in ELF files to yield the module macros --- src/packagedcode/lkm.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/packagedcode/lkm.py b/src/packagedcode/lkm.py index 76a731aa92..2f05fbf928 100644 --- a/src/packagedcode/lkm.py +++ b/src/packagedcode/lkm.py @@ -23,7 +23,7 @@ class LinuxKernelModuleHandler(DatafileHandler): default_package_type = 'linux-kernel-module' path_patterns = ('*.ko',) description = 'Linux Kernel Module' - documentation_url = "..." + documentation_url = "https://docs.kernel.org/kbuild/modules.html" @classmethod def parse(cls, location: str, package_only=False) -> Iterator[PackageData]: @@ -135,13 +135,16 @@ def build_package(cls, metadata: Dict[str, Any], location: str, package_only: bo } extra_data = { - "srcversion": cls.get_first(metadata, "srcversion"), - "aliases": metadata.get("alias", []), - "intree": cls.get_first(metadata, "intree"), - "vermagic": cls.get_first(metadata, "vermagic"), - "Depends" : cls.get_dependencies(metadata) + key: values + for key, values in metadata.items() + if key not in normalized_keys } + dependency_names = cls.get_dependency_names(metadata) + + if dependency_names: + extra_data['depends'] = dependency_names + package_data = dict( datasource_id=cls.datasource_id, type=cls.default_package_type, From 347cba9fb66f41fa7bf20d41aff2745349852dd9 Mon Sep 17 00:00:00 2001 From: diana-galeana <162429082+diana-galeana@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:37:06 +0000 Subject: [PATCH 3/7] Tests added for lkm.py --- tests/packagedcode/test_lkm.py | 116 +++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/packagedcode/test_lkm.py diff --git a/tests/packagedcode/test_lkm.py b/tests/packagedcode/test_lkm.py new file mode 100644 index 0000000000..b6ab818183 --- /dev/null +++ b/tests/packagedcode/test_lkm.py @@ -0,0 +1,116 @@ +import shutil +from pathlib import Path + +from packagedcode import lkm + + +class TestLinuxKernelModule: + + test_data_dir = Path(__file__).resolve().parents[2] / "tests" / "licensedcode" / "data" / "query" + + def test_is_linux_kernel_module(self): + test_file = self.test_data_dir / "eeepc_acpi.ko" + + assert lkm.LinuxKernelModuleHandler.is_datafile( + str(test_file) + ) + + def test_extract_modinfo(self): + test_file = self.test_data_dir / "eeepc_acpi.ko" + + metadata = lkm.LinuxKernelModuleHandler.extract_modinfo( + str(test_file) + ) + + assert "license" in metadata + assert "description" in metadata + assert "author" in metadata + + def test_get_first(self): + metadata = { + "license": [ + "GPL", + "MIT", + ] + } + + result = lkm.LinuxKernelModuleHandler.get_first( + metadata, + "license", + ) + + assert result == "GPL" + + def test_get_dependencies(self): + metadata = { + "depends": [ + "usbcore,cfg80211,mac80211" + ] + } + + result = lkm.LinuxKernelModuleHandler.get_dependencies( + metadata + ) + + assert result == [ + "usbcore", + "cfg80211", + "mac80211", + ] + + def test_get_dependencies_handles_multiple_entries(self): + metadata = { + "depends": [ + "usbcore,cfg80211", + "mac80211,netdev", + ] + } + + result = lkm.LinuxKernelModuleHandler.get_dependencies( + metadata + ) + + assert result == [ + "usbcore", + "cfg80211", + "mac80211", + "netdev", + ] + + def test_get_first_missing_optional_value_returns_none(self): + metadata = { + "license": [ + "GPL", + ] + } + + result = lkm.LinuxKernelModuleHandler.get_first( + metadata, + "version", + ) + + assert result is None + + def test_parse_elf_without_modinfo_section_returns_empty(self, tmp_path): + test_file = tmp_path / "no-modinfo.ko" + shutil.copyfile("/bin/ls", test_file) + + package = list( + lkm.LinuxKernelModuleHandler.parse( + str(test_file) + ) + ) + + assert package == [] + + def test_parse_invalid_lkm(self, tmp_path): + test_file = tmp_path / "fake.ko" + test_file.write_bytes(b"not an elf file") + + package = list( + lkm.LinuxKernelModuleHandler.parse( + str(test_file) + ) + ) + + assert package == [] \ No newline at end of file From d81ebbd000a753f213a877d433bdc45ac51cf825 Mon Sep 17 00:00:00 2001 From: OctavioValdiviaMendoza Date: Fri, 24 Jul 2026 03:58:28 -0700 Subject: [PATCH 4/7] Add Linux kernel module package parser, Added DependentPackage objects that are now passed to PackageData --- src/packagedcode/lkm.py | 114 ++++++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 40 deletions(-) diff --git a/src/packagedcode/lkm.py b/src/packagedcode/lkm.py index 2f05fbf928..1f0966c2a5 100644 --- a/src/packagedcode/lkm.py +++ b/src/packagedcode/lkm.py @@ -1,70 +1,78 @@ -import re -from typing import Iterator, Dict, Any, List, Optional -from packagedcode.models import DatafileHandler, PackageData, DependentPackage, Party +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# ScanCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/nexB/scancode-toolkit for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + import os +from typing import Iterator +from typing import Dict +from typing import List +from typing import Optional + from elftools.common.exceptions import ELFError from elftools.elf.elffile import ELFFile +from packageurl import PackageURL - -# Inside ScanCode, we would import the official classes: -# from packagedcode.models import Package, Party, Dependency -# from packagedcode import DatafileHandler +from packagedcode.models import DatafileHandler +from packagedcode.models import DependentPackage +from packagedcode.models import PackageData +from packagedcode.models import Party class LinuxKernelModuleHandler(DatafileHandler): """ - DatafileHandler for compiled Linux Kernel Modules (.ko binaries). - Extracts the .modinfo section and maps it to ScanCode's standard PackageData. + Extract package metadata from compiled Linux Kernel Module ELF files. """ - datasource_id = 'linux_kernel_module' datasource_type = 'sys' supported_oses = ('linux',) default_package_type = 'linux-kernel-module' path_patterns = ('*.ko',) description = 'Linux Kernel Module' - documentation_url = "https://docs.kernel.org/kbuild/modules.html" + documentation_url = 'https://docs.kernel.org/kbuild/modules.html' @classmethod def parse(cls, location: str, package_only=False) -> Iterator[PackageData]: - #Main entry point called by ScanCode when scanning directories. - raw_metadata = cls.extract_modinfo(location) - if not raw_metadata: + metadata = cls.extract_modinfo(location) + + if not metadata: return - #Ensures no empty or invalid package data is yielded - - yield cls.build_package( - metadata=raw_metadata, - location=location, - package_only=package_only, + yield cls.build_package_data( + metadata=metadata, + location=location, + package_only=package_only, ) @staticmethod def extract_modinfo(location: str) -> Dict[str, List[str]]: """ - Reads the .modinfo byte section from the ELF file in-memory. - Uses pyelftools (which is already a ScanCode dependency). + Reads the .modinfo byte section from the ELF file in-memory using pyelftools. + Returns a dictionary of metadata where keys are the .modinfo keys and values are lists of strings. """ - metadata: Dict[str, List[str]] = {} try: with open(location, 'rb') as module_file: - elffile = ELFFile(module_file) + elf_file = ELFFile(module_file) - # Locate the specific .modinfo section in the ELF structure - modinfo_sec = elffile.get_section_by_name('.modinfo') - if modinfo_sec is None: + # Restrict parsing to .modinfo instead of searching arbitrary binary data. + modinfo_section = elf_file.get_section_by_name('.modinfo') + if modinfo_section is None: return {} # Extract the raw binary block - raw_bytes = modinfo_sec.data() + raw_bytes = modinfo_section.data() except (ELFError, OSError): return {} - # Split null-terminated bytes on \x00 + + # Entries in .modinfo are NULL-terminated key=value strings. for raw_entry in raw_bytes.split(b'\x00'): if not raw_entry: continue @@ -89,9 +97,12 @@ def get_first(metadata: Dict[str, List[str]], key: str) -> Optional[str]: return values[0] if values else None @staticmethod - def get_dependencies(metadata: Dict[str, List[str]]) -> List[str]: + def get_dependency_names( + metadata: Dict[str, List[str]] + ) -> List[str]: dependencies = [] + # The depends field lists modules required when this module is loaded. for depends_entry in metadata.get('depends', []): dependencies.extend( dependency.strip() @@ -100,14 +111,40 @@ def get_dependencies(metadata: Dict[str, List[str]]) -> List[str]: ) return dependencies + + @classmethod + def get_dependent_packages( + cls, + metadata: Dict[str, List[str]], + ) -> List[DependentPackage]: + dependency_names = cls.get_dependency_names(metadata) + + return [ + DependentPackage( + purl=PackageURL( + type=cls.default_package_type, + name=dependency_name, + ).to_string(), + extracted_requirement=None, + scope='runtime', + is_runtime=True, + is_optional=False, + ) + for dependency_name in dependency_names + ] @classmethod - def build_package(cls, metadata: Dict[str, Any], location: str, package_only: bool = False) -> PackageData: + def build_package_data( + cls, + metadata: Dict[str, List[str]], + location: str, + package_only: bool = False + ) -> PackageData: """ Maps raw .modinfo dictionary keys into ScanCode's standard PackageData models. """ - # 1. Map Authors to standard 'Party' objects + # Represent every declared author as a package party. parties = [] for author in metadata.get('author', []): author = author.strip() @@ -120,30 +157,26 @@ def build_package(cls, metadata: Dict[str, Any], location: str, package_only: bo name=author, ) ) - - filename = os.path.basename(location) name = filename[:-3] if filename.endswith('.ko') else filename - normalized_keys = { 'author', 'description', 'license', 'version', + 'depends', } + #Preserve additional metadata that is not mapped to PackageData fields. extra_data = { key: values for key, values in metadata.items() if key not in normalized_keys } - dependency_names = cls.get_dependency_names(metadata) - - if dependency_names: - extra_data['depends'] = dependency_names + dependencies = cls.get_dependent_packages(metadata) package_data = dict( datasource_id=cls.datasource_id, @@ -151,8 +184,9 @@ def build_package(cls, metadata: Dict[str, Any], location: str, package_only: bo name=name, version=cls.get_first(metadata, 'version'), description=cls.get_first(metadata, 'description'), - extracted_license_statement=cls.get_first(metadata,'license'), + extracted_license_statement=cls.get_first(metadata, 'license'), parties=parties, + dependencies=dependencies, extra_data=extra_data, ) From ed5a38afd86aaddc48ebb9fe694263bdf7552a6a Mon Sep 17 00:00:00 2001 From: OctavioValdiviaMendoza Date: Fri, 24 Jul 2026 04:46:37 -0700 Subject: [PATCH 5/7] Added test cases to ensure all data gets returned.Added test to verify DependecyPackage and PAckageData are structured correctly --- tests/packagedcode/test_lkm.py | 135 +++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/tests/packagedcode/test_lkm.py b/tests/packagedcode/test_lkm.py index b6ab818183..0d223a92d7 100644 --- a/tests/packagedcode/test_lkm.py +++ b/tests/packagedcode/test_lkm.py @@ -1,7 +1,14 @@ import shutil +import os from pathlib import Path +import pytest + from packagedcode import lkm +from packagedcode.models import DatafileHandler +from packagedcode.models import PackageData +from packagedcode.models import Party +from packagedcode.models import DependentPackage class TestLinuxKernelModule: @@ -41,14 +48,14 @@ def test_get_first(self): assert result == "GPL" - def test_get_dependencies(self): + def test_get_dependency_names(self): metadata = { "depends": [ "usbcore,cfg80211,mac80211" ] } - result = lkm.LinuxKernelModuleHandler.get_dependencies( + result = lkm.LinuxKernelModuleHandler.get_dependency_names( metadata ) @@ -58,7 +65,7 @@ def test_get_dependencies(self): "mac80211", ] - def test_get_dependencies_handles_multiple_entries(self): + def test_get_dependency_names_handles_multiple_entries(self): metadata = { "depends": [ "usbcore,cfg80211", @@ -66,7 +73,7 @@ def test_get_dependencies_handles_multiple_entries(self): ] } - result = lkm.LinuxKernelModuleHandler.get_dependencies( + result = lkm.LinuxKernelModuleHandler.get_dependency_names( metadata ) @@ -92,6 +99,8 @@ def test_get_first_missing_optional_value_returns_none(self): assert result is None def test_parse_elf_without_modinfo_section_returns_empty(self, tmp_path): + if os.name == 'nt': + pytest.skip("Test currently depends on the Unix /bin/ls ELF binary'") test_file = tmp_path / "no-modinfo.ko" shutil.copyfile("/bin/ls", test_file) @@ -102,7 +111,7 @@ def test_parse_elf_without_modinfo_section_returns_empty(self, tmp_path): ) assert package == [] - + def test_parse_invalid_lkm(self, tmp_path): test_file = tmp_path / "fake.ko" test_file.write_bytes(b"not an elf file") @@ -113,4 +122,118 @@ def test_parse_invalid_lkm(self, tmp_path): ) ) - assert package == [] \ No newline at end of file + assert package == [] + + def test_parse_linux_kernel_module(self): + test_file = self.test_data_dir / 'eeepc_acpi.ko' + + packages = list( + lkm.LinuxKernelModuleHandler.parse( + str(test_file) + ) + ) + + assert len(packages) == 1 + + package = packages[0] + + assert package.datasource_id == 'linux_kernel_module' + assert package.type == 'linux-kernel-module' + assert package.name == 'eeepc_acpi' + assert package.extracted_license_statement + assert package.description + assert package.parties + + def test_get_dependent_packages(self): + metadata = { + 'depends': [ + 'usbcore,cfg80211', + ] + } + + dependencies = ( + lkm.LinuxKernelModuleHandler.get_dependent_packages(metadata) + ) + + assert len(dependencies) == 2 + + assert dependencies[0].purl == ( + 'pkg:linux-kernel-module/usbcore' + ) + assert dependencies[0].scope == 'runtime' + assert dependencies[0].is_runtime is True + assert dependencies[0].is_optional is False + assert dependencies[0].extracted_requirement is None + + assert dependencies[1].purl == ( + 'pkg:linux-kernel-module/cfg80211' + ) + + def test_build_package_data_includes_dependencies(self): + metadata = { + 'license': ['GPL'], + 'depends': ['usbcore,cfg80211'], + } + + package_data = ( + lkm.LinuxKernelModuleHandler.build_package_data( + metadata=metadata, + location='/tmp/example.ko', + ) + ) + + assert package_data.name == 'example' + assert len(package_data.dependencies) == 2 + assert package_data.dependencies[0].purl == ( + 'pkg:linux-kernel-module/usbcore' + ) + assert package_data.dependencies[1].purl == ( + 'pkg:linux-kernel-module/cfg80211' + ) + + def test_build_package_data_preserves_unknown_metadata(self): + metadata = { + 'license': ['GPL'], + 'vermagic': ['6.8.0 SMP preempt mod_unload'], + 'srcversion': ['ABC123'], + 'firmware': ['example.bin'], + } + + package_data = ( + lkm.LinuxKernelModuleHandler.build_package_data( + metadata=metadata, + location='/tmp/example.ko', + ) + ) + + assert package_data.extra_data["vermagic"] == [ + "6.8.0 SMP preempt mod_unload" + ] + + assert package_data.extra_data["srcversion"] == [ + "ABC123" + ] + + assert package_data.extra_data["firmware"] == [ + "example.bin" + ] + + def test_build_package_data_handles_multiple_authors(self): + metadata = { + 'author': [ + 'Alice', + 'Bob', + ], + } + + package_data = ( + lkm.LinuxKernelModuleHandler.build_package_data( + metadata=metadata, + location='/tmp/example.ko', + ) + ) + + assert [party.name for party in package_data.parties] == [ + 'Alice', + 'Bob', + ] From 08531c3e8d8d6889aeea3dde04be438bf85c5e9f Mon Sep 17 00:00:00 2001 From: OctavioValdiviaMendoza Date: Sun, 26 Jul 2026 16:49:17 -0700 Subject: [PATCH 6/7] Signed-off-by: OctavioValdiviaMendoza Add Linux Kernel Module package parser Add support for parsing Linux Kernel Module (.ko) files. Add pyelftools as a runtime dependency for reading ELF .modinfo sections, register the new handler, and add unit tests. --- AUTHORS.rst | 1 + CHANGELOG.rst | 3 +++ pyproject-scancode-toolkit.toml | 1 + requirements.txt | 1 + src/packagedcode/__init__.py | 5 ++++- src/packagedcode/lkm.py | 31 +++++++++++++++++++++---------- tests/packagedcode/test_lkm.py | 9 +++++++++ 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 9a8224dc7d..38de31aefc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -61,6 +61,7 @@ The following organizations or individuals have contributed to ScanCode: - Nisha Kumar @nishakm - Nishchith Shetty @inishchith - Nitish Sharma @nitish81299 +- Octavio Valdivia Mendoza @OctavioValdiviaMendoza - Paul Gier @pgier - Philippe Ombredanne @pombredanne - Pi Delport @PiDelport diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9a5a6b402..67ccd1e5f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,9 @@ Next release ``licensedcode-data``. https://github.com/aboutcode-org/scancode-toolkit/pull/5056 +- Add a package data handler for compiled Linux Kernel Module (``.ko``) + files that extracts metadata from the ELF ``.modinfo`` section. + v33.0.0rc1 - 2026-05-14 ------------------------ diff --git a/pyproject-scancode-toolkit.toml b/pyproject-scancode-toolkit.toml index 407d65b9c4..37f90b388e 100644 --- a/pyproject-scancode-toolkit.toml +++ b/pyproject-scancode-toolkit.toml @@ -88,6 +88,7 @@ dependencies = [ "plugincode >= 32.0.0", "publicsuffix2", "pyahocorasick >= 2.3.0", + "pyelftools >= 0.32", "pygmars >= 1.0.0", "pygments >= 1.0.0", "pymaven_patch >= 0.2.8", diff --git a/requirements.txt b/requirements.txt index e534652d52..05175e3b58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,6 +63,7 @@ publicsuffix2==2.20191221 py==1.11.0 pyahocorasick==2.3.1 pycparser==2.23 +pyelftools==0.32 pygmars==1.0.0 Pygments==2.13.0 pymaven-patch==0.3.2 diff --git a/src/packagedcode/__init__.py b/src/packagedcode/__init__.py index fc1e490eef..0c8e75c6cd 100644 --- a/src/packagedcode/__init__.py +++ b/src/packagedcode/__init__.py @@ -26,6 +26,7 @@ from packagedcode import godeps from packagedcode import golang from packagedcode import haxe +from packagedcode import lkm from packagedcode import maven from packagedcode import misc from packagedcode import npm @@ -233,8 +234,10 @@ debian.DebianInstalledMd5sumFilelistHandler, debian.DebianInstalledStatusDatabaseHandler, + lkm.LinuxKernelModuleHandler, + rpm.RpmLicenseFilesHandler, - rpm.RpmMarinerContainerManifestHandler + rpm.RpmMarinerContainerManifestHandler, ] if on_linux: diff --git a/src/packagedcode/lkm.py b/src/packagedcode/lkm.py index 1f0966c2a5..6204e745ec 100644 --- a/src/packagedcode/lkm.py +++ b/src/packagedcode/lkm.py @@ -9,8 +9,8 @@ import os -from typing import Iterator from typing import Dict +from typing import Iterator from typing import List from typing import Optional @@ -23,6 +23,7 @@ from packagedcode.models import PackageData from packagedcode.models import Party + class LinuxKernelModuleHandler(DatafileHandler): """ Extract package metadata from compiled Linux Kernel Module ELF files. @@ -52,9 +53,12 @@ def parse(cls, location: str, package_only=False) -> Iterator[PackageData]: @staticmethod def extract_modinfo(location: str) -> Dict[str, List[str]]: """ - Reads the .modinfo byte section from the ELF file in-memory using pyelftools. - Returns a dictionary of metadata where keys are the .modinfo keys and values are lists of strings. + Return .modinfo metadata as a mapping of keys to lists of values. + + Multiple values are preserved because fields such as 'author', 'alias', + and 'firmware' may appear more than once. """ + metadata: Dict[str, List[str]] = {} try: @@ -66,13 +70,13 @@ def extract_modinfo(location: str) -> Dict[str, List[str]]: if modinfo_section is None: return {} - # Extract the raw binary block + # Read only the raw bytes stored in the ELF .modinfo section. raw_bytes = modinfo_section.data() except (ELFError, OSError): return {} - # Entries in .modinfo are NULL-terminated key=value strings. + # Entries in .modinfo are NUL-terminated key=value strings. for raw_entry in raw_bytes.split(b'\x00'): if not raw_entry: continue @@ -135,13 +139,13 @@ def get_dependent_packages( @classmethod def build_package_data( - cls, - metadata: Dict[str, List[str]], - location: str, - package_only: bool = False + cls, + metadata: Dict[str, List[str]], + location: str, + package_only: bool = False, ) -> PackageData: """ - Maps raw .modinfo dictionary keys into ScanCode's standard PackageData models. + Return PackageData built from raw .modinfo metadata. """ # Represent every declared author as a package party. @@ -176,6 +180,13 @@ def build_package_data( if key not in normalized_keys } + # PackageData stores only the first value for these normalized fields. + # Preserve all values in extra_data when a field occurs more than once. + for key in ('description', 'license', 'version'): + values = metadata.get(key, []) + if len(values) > 1: + extra_data[key] = values + dependencies = cls.get_dependent_packages(metadata) package_data = dict( diff --git a/tests/packagedcode/test_lkm.py b/tests/packagedcode/test_lkm.py index 0d223a92d7..70dd3ffd69 100644 --- a/tests/packagedcode/test_lkm.py +++ b/tests/packagedcode/test_lkm.py @@ -1,6 +1,7 @@ import shutil import os from pathlib import Path +from xml.sax import handler import pytest @@ -9,6 +10,7 @@ from packagedcode.models import PackageData from packagedcode.models import Party from packagedcode.models import DependentPackage +from packagedcode import HANDLER_BY_DATASOURCE_ID class TestLinuxKernelModule: @@ -237,3 +239,10 @@ def test_build_package_data_handles_multiple_authors(self): 'Alice', 'Bob', ] + + def test_datasource_id_is_registered(self): + handler = HANDLER_BY_DATASOURCE_ID[ + lkm.LinuxKernelModuleHandler.datasource_id + ] + + assert handler is lkm.LinuxKernelModuleHandler \ No newline at end of file From a936c9eddd0e0a1ed104f0e9caa7fb1ee0d63c99 Mon Sep 17 00:00:00 2001 From: diana-galeana <162429082+diana-galeana@users.noreply.github.com> Date: Mon, 27 Jul 2026 05:56:15 +0000 Subject: [PATCH 7/7] Add tests for alias pass-through and multi-value metadata fields in lkm.py Add self to AUTHORS.rst Signed-off-by: diana-galeana <162429082+diana-galeana@users.noreply.github.com> --- AUTHORS.rst | 1 + tests/packagedcode/test_lkm.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 38de31aefc..d84a325506 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,6 +27,7 @@ The following organizations or individuals have contributed to ScanCode: - Daniel Eder @daniel-eder - Dan Kegel @dankegel - Dennis Clark @DennisClark +- Diana Galeana @diana-galeana - Divyansh Sharma @Divyansh2512 - Duncan Howe @Duncan-Howe - Felix Kauselmann @selmf diff --git a/tests/packagedcode/test_lkm.py b/tests/packagedcode/test_lkm.py index 70dd3ffd69..f3f6db3d78 100644 --- a/tests/packagedcode/test_lkm.py +++ b/tests/packagedcode/test_lkm.py @@ -245,4 +245,18 @@ def test_datasource_id_is_registered(self): lkm.LinuxKernelModuleHandler.datasource_id ] - assert handler is lkm.LinuxKernelModuleHandler \ No newline at end of file + assert handler is lkm.LinuxKernelModuleHandler + + def test_build_package_data_multiple_alias_entries(self): + metadata = { + 'license': ['GPL'], + 'alias': ['usbcore', 'cfg80211'], + } + + package_data = lkm.LinuxKernelModuleHandler.build_package_data( + metadata=metadata, + location='/tmp/example.ko', + ) + + assert package_data.extra_data['alias'] == ['usbcore', 'cfg80211'] + assert 'depends' not in package_data.extra_data \ No newline at end of file