From 2b240560d75d118456ab2119f839416eff1fb44a Mon Sep 17 00:00:00 2001 From: Jayant Date: Mon, 2 Feb 2026 16:11:35 +0530 Subject: [PATCH] Implement NuGet license file logic - Add license_file_references field to PackageData model - Implement get_license_details() helper in nuget.py - Update NugetNuspecHandler to capture file references - Add test case for file-based license - Update existing expectations with minimal surgical changes Fixes #4609 Signed-off-by: Jayant --- src/packagedcode/models.py | 7 +- src/packagedcode/nuget.py | 29 ++++++- ...tle.Core.nuspec-package-only.json.expected | 1 + .../nuget/Castle.Core.nuspec.json.expected | 1 + .../EntityFramework.nuspec.json.expected | 1 + .../Microsoft.AspNet.Mvc.nuspec.json.expected | 1 + .../Microsoft.Net.Http.nuspec.json.expected | 1 + .../data/nuget/bootstrap.nuspec.json.expected | 1 + .../jQuery.UI.Combined.nuspec.json.expected | 1 + .../data/nuget/license_file.nuspec | 11 +++ .../nuget/license_file.nuspec.json.expected | 87 +++++++++++++++++++ tests/packagedcode/test_nuget.py | 6 ++ 12 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 tests/packagedcode/data/nuget/license_file.nuspec create mode 100644 tests/packagedcode/data/nuget/license_file.nuspec.json.expected diff --git a/src/packagedcode/models.py b/src/packagedcode/models.py index 0c4ffb9e56e..39e767060c3 100644 --- a/src/packagedcode/models.py +++ b/src/packagedcode/models.py @@ -672,6 +672,12 @@ class PackageData(IdentifiablePackageData): 'package manifest and extracted. This can be a string, a list or dict of ' 'strings possibly nested, as found originally in the manifest.') + license_file_references = List( + item_type=str, + label='license file references', + help='A list of license file path references as found in a package manifest.' + ) + notice_text = String( label='notice text', help='A notice text for this package.') @@ -881,7 +887,6 @@ def to_dict(self, with_details=True, **kwargs): mapping = super().to_dict(with_details=with_details, **kwargs) if not with_details: # these are not used in the Package subclass - mapping.pop('file_references', None) mapping.pop('dependencies', None) mapping.pop('datasource_id', None) diff --git a/src/packagedcode/nuget.py b/src/packagedcode/nuget.py index d0d7e110f2f..e165a1f39c6 100644 --- a/src/packagedcode/nuget.py +++ b/src/packagedcode/nuget.py @@ -90,6 +90,29 @@ def get_urls(name, version, **kwargs): ) + +def get_license_details(nuspec): + license_info = nuspec.get('license') + if not license_info: + return None, [] + + license_type = None + license_text = None + if isinstance(license_info, dict): + license_type = (license_info.get('@type') or '').lower() + license_text = license_info.get('#text') or '' + if not license_text: + license_text = license_info.get('@value') or '' + else: + license_text = license_info + + if license_type == 'file': + license_text = license_text or None + return license_text, [license_text] if license_text else [] + + return license_text or None, [] + + class NugetNupkgHandler(models.NonAssemblableDatafileHandler): datasource_id = 'nuget_nupkg' path_patterns = ('*.nupkg',) @@ -156,10 +179,13 @@ def parse(cls, location, package_only=False): urls = get_urls(name, version) extracted_license_statement = None + license_file_references = [] + + # See https://docs.microsoft.com/en-us/nuget/reference/nuspec#license # This is a SPDX license expression if 'license' in nuspec: - extracted_license_statement = nuspec.get('license') + extracted_license_statement, license_file_references = get_license_details(nuspec) # Deprecated and not a license expression, just a URL elif 'licenseUrl' in nuspec: extracted_license_statement = nuspec.get('licenseUrl') @@ -174,6 +200,7 @@ def parse(cls, location, package_only=False): parties=parties, dependencies=list(get_dependencies(nuspec)), extracted_license_statement=extracted_license_statement, + license_file_references=license_file_references, copyright=nuspec.get('copyright') or None, vcs_url=vcs_url, **urls, diff --git a/tests/packagedcode/data/nuget/Castle.Core.nuspec-package-only.json.expected b/tests/packagedcode/data/nuget/Castle.Core.nuspec-package-only.json.expected index 23b1b625caa..158cd381a52 100644 --- a/tests/packagedcode/data/nuget/Castle.Core.nuspec-package-only.json.expected +++ b/tests/packagedcode/data/nuget/Castle.Core.nuspec-package-only.json.expected @@ -45,6 +45,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "http://www.apache.org/licenses/LICENSE-2.0.html", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/Castle.Core.nuspec.json.expected b/tests/packagedcode/data/nuget/Castle.Core.nuspec.json.expected index 512a0994a35..150a6ed9867 100644 --- a/tests/packagedcode/data/nuget/Castle.Core.nuspec.json.expected +++ b/tests/packagedcode/data/nuget/Castle.Core.nuspec.json.expected @@ -68,6 +68,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "http://www.apache.org/licenses/LICENSE-2.0.html", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/EntityFramework.nuspec.json.expected b/tests/packagedcode/data/nuget/EntityFramework.nuspec.json.expected index 5e4133bd82d..d03bb762a37 100644 --- a/tests/packagedcode/data/nuget/EntityFramework.nuspec.json.expected +++ b/tests/packagedcode/data/nuget/EntityFramework.nuspec.json.expected @@ -68,6 +68,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "http://go.microsoft.com/fwlink/?LinkID=320539", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/Microsoft.AspNet.Mvc.nuspec.json.expected b/tests/packagedcode/data/nuget/Microsoft.AspNet.Mvc.nuspec.json.expected index f5333801bb2..679ecb04b7d 100644 --- a/tests/packagedcode/data/nuget/Microsoft.AspNet.Mvc.nuspec.json.expected +++ b/tests/packagedcode/data/nuget/Microsoft.AspNet.Mvc.nuspec.json.expected @@ -68,6 +68,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "http://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/Microsoft.Net.Http.nuspec.json.expected b/tests/packagedcode/data/nuget/Microsoft.Net.Http.nuspec.json.expected index 1dc124edce4..03d6e292fd1 100644 --- a/tests/packagedcode/data/nuget/Microsoft.Net.Http.nuspec.json.expected +++ b/tests/packagedcode/data/nuget/Microsoft.Net.Http.nuspec.json.expected @@ -68,6 +68,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "http://go.microsoft.com/fwlink/?LinkId=329770", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/bootstrap.nuspec.json.expected b/tests/packagedcode/data/nuget/bootstrap.nuspec.json.expected index e189074c5e1..ed92ba88a5e 100644 --- a/tests/packagedcode/data/nuget/bootstrap.nuspec.json.expected +++ b/tests/packagedcode/data/nuget/bootstrap.nuspec.json.expected @@ -68,6 +68,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "https://github.com/twbs/bootstrap/blob/master/LICENSE", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/jQuery.UI.Combined.nuspec.json.expected b/tests/packagedcode/data/nuget/jQuery.UI.Combined.nuspec.json.expected index 9c766ebb03d..50b7d37670d 100644 --- a/tests/packagedcode/data/nuget/jQuery.UI.Combined.nuspec.json.expected +++ b/tests/packagedcode/data/nuget/jQuery.UI.Combined.nuspec.json.expected @@ -68,6 +68,7 @@ "other_license_expression_spdx": null, "other_license_detections": [], "extracted_license_statement": "http://jquery.org/license", + "license_file_references": [], "notice_text": null, "source_packages": [], "file_references": [], diff --git a/tests/packagedcode/data/nuget/license_file.nuspec b/tests/packagedcode/data/nuget/license_file.nuspec new file mode 100644 index 00000000000..fc8abe7c391 --- /dev/null +++ b/tests/packagedcode/data/nuget/license_file.nuspec @@ -0,0 +1,11 @@ + + + + FileLicense + 1.0.0 + Example Org + Example Org + Sample package with file-based license reference. + LICENSE.txt + + diff --git a/tests/packagedcode/data/nuget/license_file.nuspec.json.expected b/tests/packagedcode/data/nuget/license_file.nuspec.json.expected new file mode 100644 index 00000000000..ebdec600d57 --- /dev/null +++ b/tests/packagedcode/data/nuget/license_file.nuspec.json.expected @@ -0,0 +1,87 @@ +[ + { + "type": "nuget", + "namespace": null, + "name": "FileLicense", + "version": "1.0.0", + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": "Sample package with file-based license reference.", + "release_date": null, + "parties": [ + { + "type": null, + "role": "author", + "name": "Example Org", + "email": null, + "url": null + }, + { + "type": null, + "role": "owner", + "name": "Example Org", + "email": null, + "url": null + } + ], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": "unknown-license-reference", + "declared_license_expression_spdx": "LicenseRef-scancode-unknown-license-reference", + "license_detections": [ + { + "license_expression": "unknown-license-reference", + "license_expression_spdx": "LicenseRef-scancode-unknown-license-reference", + "matches": [ + { + "license_expression": "unknown-license-reference", + "license_expression_spdx": "LicenseRef-scancode-unknown-license-reference", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-hash", + "score": 90.0, + "matched_length": 3, + "match_coverage": 100.0, + "rule_relevance": 90, + "rule_identifier": "unknown-license-reference_339.RULE", + "rule_url": "https://github.com/nexB/scancode-toolkit/tree/develop/src/licensedcode/data/rules/unknown-license-reference_339.RULE", + "matched_text": "license LICENSE.txt" + } + ], + "identifier": "unknown_license_reference-bf47ad8c-fa3f-0e58-e5e5-d0aef2c37b43" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "LICENSE.txt", + "license_file_references": [ + "LICENSE.txt" + ], + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://www.nuget.org/packages/FileLicense/1.0.0", + "repository_download_url": "https://www.nuget.org/api/v2/package/FileLicense/1.0.0", + "api_data_url": "https://api.nuget.org/v3/registration3/filelicense/1.0.0.json", + "datasource_id": "nuget_nupsec", + "purl": "pkg:nuget/FileLicense@1.0.0" + } +] \ No newline at end of file diff --git a/tests/packagedcode/test_nuget.py b/tests/packagedcode/test_nuget.py index d3af13ab51b..9bbc77fce16 100644 --- a/tests/packagedcode/test_nuget.py +++ b/tests/packagedcode/test_nuget.py @@ -21,6 +21,12 @@ def test_nuspec_is_package_data_file(self): test_file = self.get_test_loc('nuget/bootstrap.nuspec') assert nuget.NugetNuspecHandler.is_datafile(test_file) + def test_parse_nuspec_license_file_reference(self): + test_file = self.get_test_loc('nuget/license_file.nuspec') + package = nuget.NugetNuspecHandler.parse(test_file) + expected_loc = self.get_test_loc('nuget/license_file.nuspec.json.expected') + self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) + def test_parse_creates_package_from_nuspec_bootstrap(self): test_file = self.get_test_loc('nuget/bootstrap.nuspec') package = nuget.NugetNuspecHandler.parse(test_file)