From a8e22baeb631f06a39be2e8f27300049d459163e Mon Sep 17 00:00:00 2001 From: Adrien Schildknecht Date: Wed, 22 Nov 2023 18:42:38 +0100 Subject: [PATCH 1/2] nuget: parse .csproj and packages.config files Add support for 2 manifest files: - packages.config: XML file holding the list of package dependencies; - *.csproj: describe a project with dependencies (NuGet package references); The manifest files do not contain a lot of info, but enough to build a package-url. fixes #2392 Signed-off-by: Adrien Schildknecht --- src/packagedcode/__init__.py | 2 + src/packagedcode/nuget.py | 57 ++++++++++++ .../data/nuget/csproj/gvfs.csproj | 11 +++ .../data/nuget/csproj/gvfs.csproj.expected | 44 ++++++++++ .../nuget/packages-config/packages.config | 5 ++ .../packages-config/packages.config.expected | 86 +++++++++++++++++++ tests/packagedcode/test_nuget.py | 14 +++ 7 files changed, 219 insertions(+) create mode 100644 tests/packagedcode/data/nuget/csproj/gvfs.csproj create mode 100644 tests/packagedcode/data/nuget/csproj/gvfs.csproj.expected create mode 100644 tests/packagedcode/data/nuget/packages-config/packages.config create mode 100644 tests/packagedcode/data/nuget/packages-config/packages.config.expected diff --git a/src/packagedcode/__init__.py b/src/packagedcode/__init__.py index 0887a1859da..0cf25adca00 100644 --- a/src/packagedcode/__init__.py +++ b/src/packagedcode/__init__.py @@ -148,6 +148,8 @@ nuget.NugetNupkgHandler, nuget.NugetNuspecHandler, + nuget.NugetCsprojHandler, + nuget.NugetPackagesConfigHandler, opam.OpamFileHandler, diff --git a/src/packagedcode/nuget.py b/src/packagedcode/nuget.py index dee1afb39d5..aa6bcc6fa0e 100644 --- a/src/packagedcode/nuget.py +++ b/src/packagedcode/nuget.py @@ -178,3 +178,60 @@ def parse(cls, location): **urls, ) +class NugetCsprojHandler(models.DatafileHandler): + datasource_id = 'nuget_csproj' + path_patterns = ('*.csproj',) + default_package_type = 'nuget' + description = 'Csproj package manifest' + documentation_url = 'https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files' + + @classmethod + def parse(cls, location): + with open(location, 'rb') as loc: + parsed = xmltodict.parse(loc) + if not parsed: + return + + pkgs = parsed.get('Project', {}).get('ItemGroup', {}).get('PackageReference') + if pkgs is None: + return + for pkg in pkgs if isinstance(pkgs, list) else [pkgs]: + name = pkg.get('Include') or pkg.get('@Include') + version = pkg.get('Version') or pkg.get('@Version') + urls = get_urls(name, version) + yield models.PackageData( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + name=name, + version=version, + **urls, + ) + +class NugetPackagesConfigHandler(models.DatafileHandler): + datasource_id = 'nuget_packages_config' + path_patterns = ('*/packages.config',) + default_package_type = 'nuget' + description = 'NuGet packages config manifest' + documentation_url = 'https://learn.microsoft.com/en-us/nuget/reference/packages-config' + + @classmethod + def parse(cls, location): + with open(location, 'rb') as loc: + parsed = xmltodict.parse(loc) + if not parsed: + return + + pkgs = parsed.get('packages', {}).get('package') + if pkgs is None: + return + for pkg in pkgs if isinstance(pkgs, list) else [pkgs]: + name = pkg.get('@id') + version = pkg.get('@version') + urls = get_urls(name, version) + yield models.PackageData( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + name=name, + version=version, + **urls, + ) diff --git a/tests/packagedcode/data/nuget/csproj/gvfs.csproj b/tests/packagedcode/data/nuget/csproj/gvfs.csproj new file mode 100644 index 00000000000..33c574523b6 --- /dev/null +++ b/tests/packagedcode/data/nuget/csproj/gvfs.csproj @@ -0,0 +1,11 @@ + + + + net461 + + + + + + + diff --git a/tests/packagedcode/data/nuget/csproj/gvfs.csproj.expected b/tests/packagedcode/data/nuget/csproj/gvfs.csproj.expected new file mode 100644 index 00000000000..3ec525d0aa0 --- /dev/null +++ b/tests/packagedcode/data/nuget/csproj/gvfs.csproj.expected @@ -0,0 +1,44 @@ +[ + { + "type": "nuget", + "namespace": null, + "name": "Newtonsoft.Json", + "version": "13.0.1", + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": null, + "release_date": null, + "parties": [], + "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": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://www.nuget.org/packages/Newtonsoft.Json/13.0.1", + "repository_download_url": "https://www.nuget.org/api/v2/package/Newtonsoft.Json/13.0.1", + "api_data_url": "https://api.nuget.org/v3/registration3/newtonsoft.json/13.0.1.json", + "datasource_id": "nuget_csproj", + "purl": "pkg:nuget/Newtonsoft.Json@13.0.1" + } +] diff --git a/tests/packagedcode/data/nuget/packages-config/packages.config b/tests/packagedcode/data/nuget/packages-config/packages.config new file mode 100644 index 00000000000..71c38ee9108 --- /dev/null +++ b/tests/packagedcode/data/nuget/packages-config/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/packagedcode/data/nuget/packages-config/packages.config.expected b/tests/packagedcode/data/nuget/packages-config/packages.config.expected new file mode 100644 index 00000000000..f2b9e8c0a42 --- /dev/null +++ b/tests/packagedcode/data/nuget/packages-config/packages.config.expected @@ -0,0 +1,86 @@ +[ + { + "type": "nuget", + "namespace": null, + "name": "jQuery", + "version": "3.1.1", + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": null, + "release_date": null, + "parties": [], + "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": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://www.nuget.org/packages/jQuery/3.1.1", + "repository_download_url": "https://www.nuget.org/api/v2/package/jQuery/3.1.1", + "api_data_url": "https://api.nuget.org/v3/registration3/jquery/3.1.1.json", + "datasource_id": "nuget_packages_config", + "purl": "pkg:nuget/jQuery@3.1.1" + }, + { + "type": "nuget", + "namespace": null, + "name": "NLog", + "version": "4.3.10", + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": null, + "release_date": null, + "parties": [], + "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": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://www.nuget.org/packages/NLog/4.3.10", + "repository_download_url": "https://www.nuget.org/api/v2/package/NLog/4.3.10", + "api_data_url": "https://api.nuget.org/v3/registration3/nlog/4.3.10.json", + "datasource_id": "nuget_packages_config", + "purl": "pkg:nuget/NLog@4.3.10" + } +] diff --git a/tests/packagedcode/test_nuget.py b/tests/packagedcode/test_nuget.py index 37298a45c7e..5d9a82dbfa9 100644 --- a/tests/packagedcode/test_nuget.py +++ b/tests/packagedcode/test_nuget.py @@ -56,3 +56,17 @@ def test_parse_as_package(self): package = nuget.NugetNuspecHandler.parse(test_file) expected_loc = self.get_test_loc('nuget/Castle.Core.nuspec.json.expected') self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_parse_csproj(self): + # See: https://github.com/microsoft/VFSForGit/blob/master/GVFS/GVFS.GVFlt/GVFS.GVFlt.csproj + test_file = self.get_test_loc('nuget/csproj/gvfs.csproj') + package = nuget.NugetCsprojHandler.parse(test_file) + expected_loc = self.get_test_loc('nuget/csproj/gvfs.csproj.expected') + self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_parse_packages_config(self): + # See: https://learn.microsoft.com/en-us/nuget/reference/packages-config#examples + test_file = self.get_test_loc('nuget/packages-config/packages.config') + package = nuget.NugetPackagesConfigHandler.parse(test_file) + expected_loc = self.get_test_loc('nuget/packages-config/packages.config.expected') + self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) From 8b0f9855cd8bca4111b4e1149bb38ae11cc908a8 Mon Sep 17 00:00:00 2001 From: Adrien Schildknecht Date: Thu, 23 Nov 2023 20:18:15 +0100 Subject: [PATCH 2/2] nuget: handle multiple item groups in .csproj files Complex csproj files have multiple ItemGroups, make sure we check all of them. Signed-off-by: Adrien Schildknecht --- src/packagedcode/nuget.py | 30 ++++--- .../data/nuget/csproj/multiple_groups.csproj | 24 ++++++ .../csproj/multiple_groups.csproj.expected | 86 +++++++++++++++++++ tests/packagedcode/test_nuget.py | 7 ++ 4 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 tests/packagedcode/data/nuget/csproj/multiple_groups.csproj create mode 100644 tests/packagedcode/data/nuget/csproj/multiple_groups.csproj.expected diff --git a/src/packagedcode/nuget.py b/src/packagedcode/nuget.py index aa6bcc6fa0e..1553eb2fde7 100644 --- a/src/packagedcode/nuget.py +++ b/src/packagedcode/nuget.py @@ -192,20 +192,24 @@ def parse(cls, location): if not parsed: return - pkgs = parsed.get('Project', {}).get('ItemGroup', {}).get('PackageReference') - if pkgs is None: + groups = parsed.get('Project', {}).get('ItemGroup') + if groups is None: return - for pkg in pkgs if isinstance(pkgs, list) else [pkgs]: - name = pkg.get('Include') or pkg.get('@Include') - version = pkg.get('Version') or pkg.get('@Version') - urls = get_urls(name, version) - yield models.PackageData( - datasource_id=cls.datasource_id, - type=cls.default_package_type, - name=name, - version=version, - **urls, - ) + for group in groups if isinstance(groups, list) else [groups]: + pkgs = group.get('PackageReference') + if pkgs is None: + continue + for pkg in pkgs if isinstance(pkgs, list) else [pkgs]: + name = pkg.get('Include') or pkg.get('@Include') + version = pkg.get('Version') or pkg.get('@Version') + urls = get_urls(name, version) + yield models.PackageData( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + name=name, + version=version, + **urls, + ) class NugetPackagesConfigHandler(models.DatafileHandler): datasource_id = 'nuget_packages_config' diff --git a/tests/packagedcode/data/nuget/csproj/multiple_groups.csproj b/tests/packagedcode/data/nuget/csproj/multiple_groups.csproj new file mode 100644 index 00000000000..b11aac9f488 --- /dev/null +++ b/tests/packagedcode/data/nuget/csproj/multiple_groups.csproj @@ -0,0 +1,24 @@ + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + diff --git a/tests/packagedcode/data/nuget/csproj/multiple_groups.csproj.expected b/tests/packagedcode/data/nuget/csproj/multiple_groups.csproj.expected new file mode 100644 index 00000000000..98b328ad2a1 --- /dev/null +++ b/tests/packagedcode/data/nuget/csproj/multiple_groups.csproj.expected @@ -0,0 +1,86 @@ +[ + { + "type": "nuget", + "namespace": null, + "name": "ByteSize", + "version": "2.1.1", + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": null, + "release_date": null, + "parties": [], + "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": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://www.nuget.org/packages/ByteSize/2.1.1", + "repository_download_url": "https://www.nuget.org/api/v2/package/ByteSize/2.1.1", + "api_data_url": "https://api.nuget.org/v3/registration3/bytesize/2.1.1.json", + "datasource_id": "nuget_csproj", + "purl": "pkg:nuget/ByteSize@2.1.1" + }, + { + "type": "nuget", + "namespace": null, + "name": "CommunityToolkit.Mvvm", + "version": "8.2.2", + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": null, + "release_date": null, + "parties": [], + "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": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://www.nuget.org/packages/CommunityToolkit.Mvvm/8.2.2", + "repository_download_url": "https://www.nuget.org/api/v2/package/CommunityToolkit.Mvvm/8.2.2", + "api_data_url": "https://api.nuget.org/v3/registration3/communitytoolkit.mvvm/8.2.2.json", + "datasource_id": "nuget_csproj", + "purl": "pkg:nuget/CommunityToolkit.Mvvm@8.2.2" + } + ] diff --git a/tests/packagedcode/test_nuget.py b/tests/packagedcode/test_nuget.py index 5d9a82dbfa9..3f7d6fb7472 100644 --- a/tests/packagedcode/test_nuget.py +++ b/tests/packagedcode/test_nuget.py @@ -64,6 +64,13 @@ def test_parse_csproj(self): expected_loc = self.get_test_loc('nuget/csproj/gvfs.csproj.expected') self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) + def test_parse_csproj_multiple_groups(self): + # See: https://github.com/files-community/Files/blob/main/src/Files.App/Files.App.csproj + test_file = self.get_test_loc('nuget/csproj/multiple_groups.csproj') + package = nuget.NugetCsprojHandler.parse(test_file) + expected_loc = self.get_test_loc('nuget/csproj/multiple_groups.csproj.expected') + self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) + def test_parse_packages_config(self): # See: https://learn.microsoft.com/en-us/nuget/reference/packages-config#examples test_file = self.get_test_loc('nuget/packages-config/packages.config')