From 7560a7265d1073ac9ececad5b6f68b58d6b19657 Mon Sep 17 00:00:00 2001 From: Simon Ensslen Date: Wed, 13 Mar 2024 23:01:42 +0100 Subject: [PATCH] Add basic support for license expressions --- .github/workflows/action.yml | 8 +- .../allowedLicenses.json | 1 + .../ignorePackages.json | 1 + .../overwritePackageInformation.json | 2 + .../projectsToCheck.json | 1 + .../urlToLicenseMapping.json | 2 + NuGetUtility.sln | 48 ++- ...eferenceContainingLicenseExpression.csproj | 12 + src/NuGetLicenseCore/NuGetLicenseCore.csproj | 1 + .../LicenseValidator/LicenseValidator.cs | 12 +- .../LicenseValidator/UrlToLicenseMapping.cs | 56 +-- src/NuGetUtility/NuGetUtility.csproj | 6 +- .../SpdxAndExpression.cs | 53 +++ .../SpdxExpression.cs | 42 ++ .../SpdxExpressionException.cs | 22 ++ .../SpdxExpressionParser.cs | 373 ++++++++++++++++++ .../SpdxLicenseExpression.cs | 56 +++ .../SpdxLicenseReference.cs | 47 +++ .../SpdxOrExpression.cs | 53 +++ .../SpdxParsingOptions.cs | 29 ++ .../SpdxScopedExpression.cs | 46 +++ .../SpdxWithExpression.cs | 53 +++ .../Tethys.SPDX.ExpressionParser.csproj | 11 + src/Tethys.SPDX.ExpressionParser/Token.cs | 51 +++ src/Tethys.SPDX.ExpressionParser/TokenType.cs | 56 +++ .../LicenseValidator/LicenseValidatorTest.cs | 187 +++++++++ ...cted_License_847892e5f3e913b2.verified.txt | 21 + ...ReferencedPackagesReaderIntegrationTest.cs | 2 +- .../PackageReferenceProject.csproj | 20 +- .../ProjectWithTransitiveNuget.csproj | 20 +- .../ProjectWithTransitiveReferences.csproj | 20 +- .../ProjectWithoutNugetReferences.csproj | 14 +- 32 files changed, 1253 insertions(+), 73 deletions(-) create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json create mode 100644 .github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json create mode 100644 integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj create mode 100644 src/Tethys.SPDX.ExpressionParser/Token.cs create mode 100644 src/Tethys.SPDX.ExpressionParser/TokenType.cs create mode 100644 tests/NuGetUtility.Test/LicenseValidator/UrlToLicenseMappingTest.License_Should_Be_Available_And_Match_Expected_License_847892e5f3e913b2.verified.txt diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index a7863408..ed1945ca 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -87,7 +87,7 @@ jobs: strategy: matrix: targetFramework: [net6.0, net7.0, net8.0] - project: [App, Tests] + project: [App, Tests, ProjectWithReferenceContainingLicenseExpression] include: - targetFramework: net6.0 @@ -110,7 +110,7 @@ jobs: dotnet-version: ${{ matrix.dotnetVersion }} - name: restore - run: dotnet restore -p:TargetFramework=${{ matrix.targetFramework }} + run: dotnet restore NuGetUtility.sln - name: build run: dotnet publish ./src/NuGetLicenseCore/NuGetLicenseCore.csproj --configuration Release -o ./release -f ${{ matrix.targetFramework }} --no-restore @@ -131,7 +131,7 @@ jobs: runs-on: windows-latest strategy: matrix: - project: [App, Tests] + project: [App, Tests, ProjectWithReferenceContainingLicenseExpression] steps: - uses: actions/checkout@v4 @@ -195,7 +195,7 @@ jobs: dotnet-version: ${{ matrix.dotnetVersion }} - name: restore - run: dotnet restore -p:TargetFramework=${{ matrix.targetFramework }} + run: dotnet restore NuGetUtility.sln - uses: paulhatch/semantic-version@v5.4.0 id: version diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json new file mode 100644 index 00000000..790c6abc --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/allowedLicenses.json @@ -0,0 +1 @@ +["MIT","Apache-2.0","MS-EULA"] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json new file mode 100644 index 00000000..6628f6d6 --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/ignorePackages.json @@ -0,0 +1 @@ +["NETStandard.Library"] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json new file mode 100644 index 00000000..0d4f101c --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/overwritePackageInformation.json @@ -0,0 +1,2 @@ +[ +] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json new file mode 100644 index 00000000..3201b624 --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/projectsToCheck.json @@ -0,0 +1 @@ +["./integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj"] diff --git a/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/.github/workflows/assets/ProjectWithReferenceContainingLicenseExpression/urlToLicenseMapping.json @@ -0,0 +1,2 @@ +{ +} diff --git a/NuGetUtility.sln b/NuGetUtility.sln index 910955a0..0745b04f 100644 --- a/NuGetUtility.sln +++ b/NuGetUtility.sln @@ -31,9 +31,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SimpleCppProject", "tests\targets\SimpleCppProject\SimpleCppProject.vcxproj", "{380FBD90-2CF0-4F83-A58E-EB98CE2EAE15}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetLicenseCore", "src\NuGetLicenseCore\NuGetLicenseCore.csproj", "{FBA6622A-C9E3-4250-AB79-35F02CAD2419}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NuGetLicenseCore", "src\NuGetLicenseCore\NuGetLicenseCore.csproj", "{FBA6622A-C9E3-4250-AB79-35F02CAD2419}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetLicenseFramework", "src\NuGetLicenseFramework\NuGetLicenseFramework.csproj", "{DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NuGetLicenseFramework", "src\NuGetLicenseFramework\NuGetLicenseFramework.csproj", "{DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tethys.SPDX.ExpressionParser", "src\Tethys.SPDX.ExpressionParser\Tethys.SPDX.ExpressionParser.csproj", "{408005E7-D628-477C-A816-59AA4AD3E40C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{FFB2826C-17A3-4C18-8FFE-A34AB51592F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectWithReferenceContainingLicenseExpression", "integration\ProjectWithReferenceContainingLicenseExpression\ProjectWithReferenceContainingLicenseExpression.csproj", "{01704839-219A-4CE2-93F4-DB3CB8773DCE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -224,6 +230,42 @@ Global {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}.TestWindows|x64.Build.0 = Debug|Any CPU {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}.TestWindows|x86.ActiveCfg = Debug|Any CPU {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F}.TestWindows|x86.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x64.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x64.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x86.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Debug|x86.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|Any CPU.Build.0 = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x64.ActiveCfg = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x64.Build.0 = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x86.ActiveCfg = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.Release|x86.Build.0 = Release|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|Any CPU.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|Any CPU.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x64.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x64.Build.0 = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x86.ActiveCfg = Debug|Any CPU + {408005E7-D628-477C-A816-59AA4AD3E40C}.TestWindows|x86.Build.0 = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Debug|x64.ActiveCfg = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Debug|x64.Build.0 = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Debug|x86.ActiveCfg = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Debug|x86.Build.0 = Debug|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Release|Any CPU.Build.0 = Release|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Release|x64.ActiveCfg = Release|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Release|x64.Build.0 = Release|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Release|x86.ActiveCfg = Release|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.Release|x86.Build.0 = Release|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.TestWindows|Any CPU.ActiveCfg = TestWindows|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.TestWindows|Any CPU.Build.0 = TestWindows|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.TestWindows|x64.ActiveCfg = TestWindows|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.TestWindows|x64.Build.0 = TestWindows|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.TestWindows|x86.ActiveCfg = TestWindows|Any CPU + {01704839-219A-4CE2-93F4-DB3CB8773DCE}.TestWindows|x86.Build.0 = TestWindows|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -240,6 +282,8 @@ Global {380FBD90-2CF0-4F83-A58E-EB98CE2EAE15} = {FA92392F-D895-4D1E-A5ED-E6DC3C08223E} {FBA6622A-C9E3-4250-AB79-35F02CAD2419} = {D2AB2D00-1F48-487D-BFE0-99FDB4E071CC} {DE079B9C-B6BA-4D53-8B83-03D3CBD4027F} = {D2AB2D00-1F48-487D-BFE0-99FDB4E071CC} + {408005E7-D628-477C-A816-59AA4AD3E40C} = {D2AB2D00-1F48-487D-BFE0-99FDB4E071CC} + {01704839-219A-4CE2-93F4-DB3CB8773DCE} = {FFB2826C-17A3-4C18-8FFE-A34AB51592F7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70887D40-0182-4C32-BFA1-B5A02E405F11} diff --git a/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj b/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj new file mode 100644 index 00000000..b00b2ac1 --- /dev/null +++ b/integration/ProjectWithReferenceContainingLicenseExpression/ProjectWithReferenceContainingLicenseExpression.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + Debug;Release;TestWindows + + + + + + + diff --git a/src/NuGetLicenseCore/NuGetLicenseCore.csproj b/src/NuGetLicenseCore/NuGetLicenseCore.csproj index fcacf09c..eb14ac45 100644 --- a/src/NuGetLicenseCore/NuGetLicenseCore.csproj +++ b/src/NuGetLicenseCore/NuGetLicenseCore.csproj @@ -24,6 +24,7 @@ AnyCPU README.md A .net tool to print and validate the licenses of .net code. This tool supports .NET (Core), .NET Standard and .NET Framework projects. + NuGet;License diff --git a/src/NuGetUtility/LicenseValidator/LicenseValidator.cs b/src/NuGetUtility/LicenseValidator/LicenseValidator.cs index c2e57447..467efba0 100644 --- a/src/NuGetUtility/LicenseValidator/LicenseValidator.cs +++ b/src/NuGetUtility/LicenseValidator/LicenseValidator.cs @@ -9,6 +9,7 @@ using NuGetUtility.Wrapper.NuGetWrapper.Packaging; using NuGetUtility.Wrapper.NuGetWrapper.Packaging.Core; using NuGetUtility.Wrapper.NuGetWrapper.Versioning; +using Tethys.SPDX.ExpressionParser; namespace NuGetUtility.LicenseValidator { @@ -126,7 +127,8 @@ private void ValidateLicenseByMetadata(IPackageMetadata info, case LicenseType.Expression: case LicenseType.Overwrite: string licenseId = info.LicenseMetadata!.License; - if (IsLicenseValid(licenseId)) + SpdxExpression? licenseExpression = SpdxExpressionParser.Parse(licenseId, _ => true, _ => true); + if (IsValidLicenseExpression(licenseExpression)) { AddOrUpdateLicense(result, info, @@ -154,6 +156,14 @@ private void ValidateLicenseByMetadata(IPackageMetadata info, } } + private bool IsValidLicenseExpression(SpdxExpression? expression) => expression switch + { + SpdxAndExpression and => IsValidLicenseExpression(and.Left) && IsValidLicenseExpression(and.Right), + SpdxOrExpression or => IsValidLicenseExpression(or.Left) || IsValidLicenseExpression(or.Right), + SpdxWithExpression or SpdxLicenseExpression or SpdxLicenseReference => IsLicenseValid(expression.ToString()), + _ => false, + }; + private async Task ValidateLicenseByUrl(IPackageMetadata info, string context, ConcurrentDictionary result, diff --git a/src/NuGetUtility/LicenseValidator/UrlToLicenseMapping.cs b/src/NuGetUtility/LicenseValidator/UrlToLicenseMapping.cs index 5f88c80a..b9d613fe 100644 --- a/src/NuGetUtility/LicenseValidator/UrlToLicenseMapping.cs +++ b/src/NuGetUtility/LicenseValidator/UrlToLicenseMapping.cs @@ -17,37 +17,39 @@ public static class UrlToLicenseMapping public static IImmutableDictionary Default { get; } = ImmutableDictionary.CreateRange( new[] { - new KeyValuePair( new Uri("http://www.apache.org/licenses/LICENSE-2.0.html"), Apache20 ), - new KeyValuePair( new Uri("http://www.apache.org/licenses/LICENSE-2.0"), Apache20 ), - new KeyValuePair( new Uri("http://aws.amazon.com/apache2.0/"), Apache20 ), - new KeyValuePair( new Uri("https://github.com/owin-contrib/owin-hosting/blob/master/LICENSE.txt"), Apache20 ), - new KeyValuePair( new Uri("https://raw.githubusercontent.com/aspnet/Home/2.0.0/LICENSE.txt"), Apache20 ), - new KeyValuePair( - new Uri("https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream/blob/master/LICENSE"), Mit + new KeyValuePair(new Uri("http://www.apache.org/licenses/LICENSE-2.0.html"), Apache20), + new KeyValuePair(new Uri("http://www.apache.org/licenses/LICENSE-2.0"), Apache20), + new KeyValuePair(new Uri("http://aws.amazon.com/apache2.0/"), Apache20), + new KeyValuePair(new Uri("https://github.com/owin-contrib/owin-hosting/blob/master/LICENSE.txt"), Apache20), + new KeyValuePair(new Uri("https://raw.githubusercontent.com/aspnet/Home/2.0.0/LICENSE.txt"), Apache20), + new KeyValuePair( + new Uri("https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream/blob/master/LICENSE"), + Mit ), - new KeyValuePair( new Uri("https://github.com/AutoMapper/AutoMapper/blob/master/LICENSE.txt"), Mit ), - new KeyValuePair( new Uri("https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE"), Mit ), - new KeyValuePair( new Uri("https://raw.githubusercontent.com/hey-red/markdownsharp/master/LICENSE"), Mit ), - new KeyValuePair( new Uri("https://raw.github.com/JamesNK/Newtonsoft.Json/master/LICENSE.md"), Mit ), - new KeyValuePair( new Uri("https://licenses.nuget.org/MIT"), Mit ), - new KeyValuePair( new Uri("http://max.mit-license.org/"), Mit ), - new KeyValuePair( new Uri("https://github.com/dotnet/corefx/blob/master/LICENSE.TXT"), Mit ), - new KeyValuePair( new Uri("https://go.microsoft.com/fwlink/?linkid=868514"), Mit ), - new KeyValuePair( new Uri("http://go.microsoft.com/fwlink/?linkid=833178"), Mit ), - new KeyValuePair( new Uri("http://www.gnu.org/licenses/old-licenses/gpl-2.0.html"), Gpl20 ), - new KeyValuePair( new Uri("https://raw.githubusercontent.com/AArnott/Validation/8377954d86/LICENSE.txt"), MsPl ), - new KeyValuePair( new Uri("https://www.microsoft.com/web/webpi/eula/aspnetmvc3update-eula.htm"), MsEula ), - new KeyValuePair( new Uri("http://go.microsoft.com/fwlink/?LinkID=214339"), MsEula ), - new KeyValuePair( new Uri("https://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm"), MsEula ), - new KeyValuePair( new Uri("http://go.microsoft.com/fwlink/?LinkId=329770"), MsEula ), - new KeyValuePair( new Uri("http://go.microsoft.com/fwlink/?LinkId=529443"), MsEula ), - new KeyValuePair( + new KeyValuePair(new Uri("https://github.com/AutoMapper/AutoMapper/blob/master/LICENSE.txt"), Mit), + new KeyValuePair(new Uri("https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE"), Mit), + new KeyValuePair(new Uri("https://raw.githubusercontent.com/hey-red/markdownsharp/master/LICENSE"), Mit), + new KeyValuePair(new Uri("https://raw.github.com/JamesNK/Newtonsoft.Json/master/LICENSE.md"), Mit), + new KeyValuePair(new Uri("https://licenses.nuget.org/MIT"), Mit), + new KeyValuePair(new Uri("http://max.mit-license.org/"), Mit), + new KeyValuePair(new Uri("https://github.com/dotnet/corefx/blob/master/LICENSE.TXT"), Mit), + new KeyValuePair(new Uri("https://go.microsoft.com/fwlink/?linkid=868514"), Mit), + new KeyValuePair(new Uri("http://go.microsoft.com/fwlink/?linkid=833178"), Mit), + new KeyValuePair(new Uri("http://www.gnu.org/licenses/old-licenses/gpl-2.0.html"), Gpl20), + new KeyValuePair(new Uri("https://raw.githubusercontent.com/AArnott/Validation/8377954d86/LICENSE.txt"), MsPl), + new KeyValuePair(new Uri("https://www.microsoft.com/web/webpi/eula/aspnetmvc3update-eula.htm"), MsEula), + new KeyValuePair(new Uri("http://go.microsoft.com/fwlink/?LinkID=214339"), MsEula), + new KeyValuePair(new Uri("https://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm"), MsEula), + new KeyValuePair(new Uri("http://go.microsoft.com/fwlink/?LinkId=329770"), MsEula), + new KeyValuePair(new Uri("http://go.microsoft.com/fwlink/?LinkId=529443"), MsEula), + new KeyValuePair( new Uri("https://www.microsoft.com/web/webpi/eula/dotnet_library_license_non_redistributable.htm"), MsEulaNonRedistributable ), - new KeyValuePair( new Uri("http://go.microsoft.com/fwlink/?LinkId=529444"), MsEulaNonRedistributable ), - new KeyValuePair( new Uri(" http://opensource.org/licenses/mit-license.php"), Mit ), - new KeyValuePair( new Uri("https://raw.githubusercontent.com/bchavez/Bogus/master/LICENSE"), Mit) + new KeyValuePair(new Uri("http://go.microsoft.com/fwlink/?LinkId=529444"), MsEulaNonRedistributable), + new KeyValuePair(new Uri(" http://opensource.org/licenses/mit-license.php"), Mit), + new KeyValuePair(new Uri("https://raw.githubusercontent.com/bchavez/Bogus/master/LICENSE"), Mit), + new KeyValuePair(new Uri("https://github.com/Microsoft/dotnet/blob/master/LICENSE"), Mit) } ); } diff --git a/src/NuGetUtility/NuGetUtility.csproj b/src/NuGetUtility/NuGetUtility.csproj index e45108f7..08e50f5c 100644 --- a/src/NuGetUtility/NuGetUtility.csproj +++ b/src/NuGetUtility/NuGetUtility.csproj @@ -1,4 +1,4 @@ - + net472;net6.0;net7.0;net8.0 @@ -60,4 +60,8 @@ + + + + \ No newline at end of file diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs new file mode 100644 index 00000000..968bf0fd --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxAndExpression.cs @@ -0,0 +1,53 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxAndExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// compound-expression "AND" compound-expression + + #region PUBLIC PROPERTIES + /// + /// Gets the left side of the expression. + /// + public SpdxExpression Left { get; } + + /// + /// Gets the right side of the expression. + /// + public SpdxExpression Right { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The left. + /// The right. + public SpdxAndExpression(SpdxExpression? left, SpdxExpression? right) + { + Left = left ?? throw new ArgumentNullException(nameof(left)); + Right = right ?? throw new ArgumentNullException(nameof(right)); + } // SpdxAndExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Left.ToString()} AND {Right.ToString()}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxAndExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs new file mode 100644 index 00000000..6dc9d1e0 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxExpression.cs @@ -0,0 +1,42 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +namespace Tethys.SPDX.ExpressionParser +{ + /************************************************************************* + * SPDX Expressions + * ---------------- + * idstring = 1*(ALPHA / DIGIT / "-" / "." ) + * + * license-id = + * + * license-exception-id = + * + * license-ref = ["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring) + * + * simple-expression = license-id / license-id"+" / license-ref + * + * compound-expression = (simple-expression / + * simple-expression "WITH" license-exception-id / + * compound-expression "AND" compound-expression / + * compound-expression "OR" compound-expression / + * "(" compound-expression ")" ) + * + * license-expression = (simple-expression / compound-expression) + * + ************************************************************************/ + + /// + /// Represents an SPDX expression. + /// + public abstract class SpdxExpression + { + /// + /// Converts an to a string. + /// + /// + /// A that represents this instance. + /// + public new abstract string ToString(); + } // SpdxExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs new file mode 100644 index 00000000..3097db1b --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionException.cs @@ -0,0 +1,22 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxExpressionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public SpdxExpressionException(string message) + : base(message) + { + } // SpdxExpressionException() + } // SpdxExpressionException +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs new file mode 100644 index 00000000..8e7f1132 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxExpressionParser.cs @@ -0,0 +1,373 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /************************************************************************* + * SPDX Expressions + * ---------------- + * idstring = 1*(ALPHA / DIGIT / "-" / "." ) + * + * license-id = + * + * license-exception-id = + * + * license-ref = ["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring) + * + * simple-expression = license-id / license-id"+" / license-ref + * + * compound-expression = (simple-expression / + * simple-expression "WITH" license-exception-id / + * compound-expression "AND" compound-expression / + * compound-expression "OR" compound-expression / + * "(" compound-expression ")" ) + * + * license-expression = (simple-expression / compound-expression) + * + ************************************************************************/ + + /// + /// Represents an SPDX expression. + /// + public class SpdxExpressionParser + { + #region PRIVATE PROPERTIES + private readonly string[] _tokens; + private int _position = 0; + private readonly Func _isSpdxIdentifier; + private readonly Func _isSpdxException; + private readonly SpdxParsingOptions _options; + #endregion // PRIVATE PROPERTIES + + //// --------------------------------------------------------------------- + + /// + /// Parses a SPDX expression. + /// + /// The expression. + /// The is identifier. + /// The is exception. + /// The options. + /// + /// A . + /// + /// + /// Exception for parsing problems. + /// + public static SpdxExpression? Parse( + string expression, + Func? isIdentifier, + Func? isException, + SpdxParsingOptions parsingOptions = SpdxParsingOptions.Default) + { + if (string.IsNullOrEmpty(expression)) + { + throw new ArgumentNullException(nameof(expression)); + } // if + + // ensure that we detect all parenthesis + expression = expression.Replace("(", " ( "); + expression = expression.Replace(")", " ) "); + + var parser = new SpdxExpressionParser(expression, + isIdentifier ?? throw new ArgumentNullException(nameof(isIdentifier)), + isException ?? throw new ArgumentNullException(nameof(isException)), + parsingOptions); + + return parser.Parse(); + } // Parse() + + private SpdxExpressionParser(string expression, + Func isIdentifier, + Func isException, + SpdxParsingOptions parsingOptions) + { + _isSpdxIdentifier = isIdentifier; + _isSpdxException = isException; + _options = parsingOptions; + + // very much simplified ... + _tokens = expression.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + _position = -1; + } + + private SpdxExpression? Parse() + { + _ = GetNextToken() ?? throw new SpdxExpressionException(string.Empty); + SpdxExpression? expr = ParseAnd(); + + return expr; + } + + /// + /// Parses an "and" expression. + /// + /// A . + private SpdxExpression? ParseAnd() + { + SpdxExpression? expression = ParseOr(); + Token? currentToken = GetCurrentToken(); + while (currentToken?.Type == TokenType.And) + { + GetNextToken(); + expression = new SpdxAndExpression(expression, ParseOr()); + currentToken = GetCurrentToken(); + } // while + + return expression; + } // ParseAnd() + + /// + /// Parses an "or" expression. + /// + /// A . + private SpdxExpression? ParseOr() + { + SpdxExpression? expression; + Token? currentToken = GetCurrentToken(); + if (currentToken?.Type == TokenType.Left) + { + expression = ParseScopedExpression(); + } + else + { + expression = ParseLicense(); + } // if + + currentToken = GetCurrentToken(); + while (currentToken?.Type == TokenType.Or) + { + GetNextToken(); + expression = new SpdxOrExpression(expression, ParseOr()); + currentToken = GetCurrentToken(); + } // while + + return expression; + } // ParseOr() + + /// + /// Parses a scoped expression. + /// + /// A . + private SpdxExpression ParseScopedExpression() + { + GetNextToken(); + SpdxExpression? expression = ParseAnd(); + if (GetCurrentToken()?.Type != TokenType.Right) + { + throw new SpdxExpressionException("Unexpected end of expression."); + } // if + + GetNextToken(); + + return new SpdxScopedExpression(expression); + } // ParseScopedExpression() + + /// + /// Determines whether this expression contains invalid characters. + /// Allowed are (ALPHA / DIGIT / "-" / "." ). + /// + /// The expression. + /// + /// true if this expression contains invalid characters; otherwise, false. + /// + private static bool ContainsInvalidCharacters(string expression) + { + foreach (char c in expression) + { + if (c != '+' && c != '.' && c != '-' && c != '(' && c != ')' && !char.IsDigit(c) && !char.IsLetter(c)) + { + return true; + } // if + } // foreach + + return false; + } // ContainsInvalidCharacters() + + /// + /// Parses a license. + /// + /// A . + private SpdxExpression? ParseLicense() + { + Token? token = GetCurrentToken(); + if (token?.Type == TokenType.LicenseId) + { + if ((_options & SpdxParsingOptions.AllowUnknownLicenses) == 0 + && !_isSpdxIdentifier(token.Value.TrimEnd('+'))) + { + throw new SpdxExpressionException("Invalid/unknown SPDX license id"); + } // if + + Token? tokenNext = PeekNextToken(); + if (tokenNext?.Type == TokenType.With) + { + Token? t2 = PeekNextNextToken(); + if (t2?.Type == TokenType.Exception) + { + GetNextToken(); + GetNextToken(); + GetNextToken(); + + return new SpdxWithExpression( + GetLicenseExpression(token.Value), + t2.Value); + } // if + } // if + + GetNextToken(); + + return GetLicenseExpression(token.Value); + } // if + + if (token?.Type == TokenType.LicenseRef) + { + GetNextToken(); + return new SpdxLicenseReference(token.Value); + } // if + + return null; + } // ParseLicense() + + /// + /// Gets a license expression from the given string. + /// + /// The expression. + /// A . + private static SpdxLicenseExpression GetLicenseExpression(string expression) + { + if (expression.EndsWith("+")) + { + return new SpdxLicenseExpression(expression.TrimEnd('+'), true); + } // if + + return new SpdxLicenseExpression(expression, false); + } // GetLicenseExpression() + + /// + /// Gets a token from the given text. + /// + /// The text. + /// A . + private Token GetToken(string text) + { + string textCompare = text.Trim().ToLower(); + if (textCompare == "(") + { + return new Token(TokenType.Left, string.Empty); + } // if + + if (textCompare == ")") + { + return new Token(TokenType.Right, string.Empty); + } // if + + if (textCompare == "and") + { + return new Token(TokenType.And, "AND"); + } // if + + if (textCompare == "or") + { + return new Token(TokenType.Or, "OR"); + } // if + + if (textCompare.Contains("with")) + { + return new Token(TokenType.With, text); + } // if + + if (textCompare.StartsWith("licenseref")) + { + return new Token(TokenType.LicenseRef, text); + } // if + + if (textCompare.EndsWith("+")) + { + return new Token(TokenType.LicenseId, text); + } // if + + if (_isSpdxIdentifier(textCompare)) + { + return new Token(TokenType.LicenseId, text); + } // if + + if (_isSpdxException(textCompare)) + { + return new Token(TokenType.Exception, text); + } // if + + if (ContainsInvalidCharacters(textCompare)) + { + throw new SpdxExpressionException("Invalid characters found"); + } // if + + if ((_options & SpdxParsingOptions.AllowUnknownExceptions) != 0) + { + return new Token(TokenType.Exception, text); + } // if + + throw new SpdxExpressionException($"Unknown token: {text}"); + } // GetToken() + + /// + /// Gets the current token. + /// + /// A . + private Token? GetCurrentToken() + { + if (_position < _tokens.Length) + { + return GetToken(_tokens[_position]); + } // if + + return null; + } + + /// + /// Gets the next token. + /// + /// A or null. + private Token? GetNextToken() + { + if (_position < _tokens.Length - 1) + { + return GetToken(_tokens[++_position]); + } // if + + return null; + } // GetNextToken() + + /// + /// Peeks the next token. + /// + /// A or null. + private Token? PeekNextToken() + { + if (_position < _tokens.Length - 1) + { + return GetToken(_tokens[_position + 1]); + } // if + + return null; + } // PeekNextToken() + + /// + /// Peeks the next token. + /// + /// + /// A or null. + /// + private Token? PeekNextNextToken() + { + if (_position < _tokens.Length - 2) + { + return GetToken(_tokens[_position + 2]); + } // if + + return null; + } // PeekNextNextToken() + } // SpdxExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs new file mode 100644 index 00000000..ca2c9fed --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseExpression.cs @@ -0,0 +1,56 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxLicenseExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// simple-expression = license-id / license-id"+" / license-ref + //// we replace this with + //// simple-expression = SpdxLicenseExpression / SpdxLicenseReference + + #region PUBLIC PROPERTIES + /// + /// Gets the license ID. + /// + public string Id { get; } + + /// + /// Gets a value indicating whether or not later versions of the license is accepted. + /// + public bool OrLater { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The identifier. + /// if set to true [or later]. + public SpdxLicenseExpression(string id, bool orLater) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + OrLater = orLater; + } // SpdxLicenseExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + string plus = OrLater ? "+" : string.Empty; + return $"{Id}{plus}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxLicenseExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs new file mode 100644 index 00000000..6684adb9 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxLicenseReference.cs @@ -0,0 +1,47 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxLicenseReference : SpdxExpression + { + //// Annex D SPDX license expressions + //// LicenseRef-"(idstring) + + #region PUBLIC PROPERTIES + /// + /// Gets the license reference. + /// + public string LicenseRef { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The license reference. + public SpdxLicenseReference(string licenseRef) + { + LicenseRef = licenseRef ?? throw new ArgumentNullException(); + } // SpdxLicenseReference() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + string text = $"{LicenseRef}"; + return text; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxLicenseReference +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs new file mode 100644 index 00000000..e76e86a1 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxOrExpression.cs @@ -0,0 +1,53 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxOrExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// compound-expression "OR" compound-expression + + #region PUBLIC PROPERTIES + /// + /// Gets the left side of the expression. + /// + public SpdxExpression Left { get; } + + /// + /// Gets the right side of the expression. + /// + public SpdxExpression Right { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The left. + /// The right. + public SpdxOrExpression(SpdxExpression? left, SpdxExpression? right) + { + Left = left ?? throw new ArgumentNullException(nameof(left)); + Right = right ?? throw new ArgumentNullException(nameof(right)); + } // SpdxOrExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Left.ToString()} OR {Right.ToString()}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxOrExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs b/src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs new file mode 100644 index 00000000..4a330cc9 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxParsingOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// SPDX parsing options. + /// + [Flags] + public enum SpdxParsingOptions + { + /// + /// Default (= no) option. + /// + Default = 0x00, + + /// + /// Allow unknown licenses. + /// + AllowUnknownLicenses = 0x01, + + /// + /// Allow unknown exceptions. + /// + AllowUnknownExceptions = 0x02, + } // SpdxParsingOptions +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs new file mode 100644 index 00000000..cc8cafcf --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxScopedExpression.cs @@ -0,0 +1,46 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression enclosed by parenthesis. + /// + public class SpdxScopedExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// "(" compound-expression ")" + + #region PUBLIC PROPERTIES + /// + /// Gets the expression node. + /// + public SpdxExpression Expression { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The expression node. + public SpdxScopedExpression(SpdxExpression? expression) + { + Expression = expression ?? throw new ArgumentNullException(nameof(expression)); + } // SpdxScopedExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"({Expression.ToString()})"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxScopedExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs b/src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs new file mode 100644 index 00000000..91b887f6 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/SpdxWithExpression.cs @@ -0,0 +1,53 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Represents an SPDX expression. + /// + public class SpdxWithExpression : SpdxExpression + { + //// Annex D SPDX license expressions + //// simple-expression "WITH" license-exception-id + + #region PUBLIC PROPERTIES + /// + /// Gets the expression node. + /// + public SpdxExpression Expression { get; } + + /// + /// Gets the license exception node. + /// + public string Exception { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The expression node. + /// The license exception node. + public SpdxWithExpression(SpdxExpression expression, string exception) + { + Expression = expression ?? throw new ArgumentNullException(nameof(expression)); + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } // SpdxWithExpression() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Expression.ToString()} WITH {Exception}"; + } // ToString() + #endregion // PUBLIC METHODS + } // SpdxExpression +} diff --git a/src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj b/src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj new file mode 100644 index 00000000..f2b5fad5 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/Tethys.SPDX.ExpressionParser.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + enable + Debug;Release;TestWindows + AnyCPU + 9.0 + + + diff --git a/src/Tethys.SPDX.ExpressionParser/Token.cs b/src/Tethys.SPDX.ExpressionParser/Token.cs new file mode 100644 index 00000000..378e037b --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/Token.cs @@ -0,0 +1,51 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +using System; + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// Implements a token. + /// + internal class Token + { + #region PUBLIC PROPERTIES + + /// + /// Gets or sets the type. + /// + public TokenType Type { get; set; } + + /// + /// Gets the value. + /// + public string Value { get; } + #endregion // PUBLIC PROPERTIES + + //// --------------------------------------------------------------------- + + #region CONSTRUCTION + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The value. + public Token(TokenType type, string value) + { + Type = type; + Value = value ?? throw new ArgumentNullException(nameof(value)); + } // Token() + #endregion // CONSTRUCTION + + //// --------------------------------------------------------------------- + + #region PUBLIC METHODS + /// + public override string ToString() + { + return $"{Type}: {Value}"; + } // ToString() + #endregion // PUBLIC METHODS + } // Token +} diff --git a/src/Tethys.SPDX.ExpressionParser/TokenType.cs b/src/Tethys.SPDX.ExpressionParser/TokenType.cs new file mode 100644 index 00000000..a6274815 --- /dev/null +++ b/src/Tethys.SPDX.ExpressionParser/TokenType.cs @@ -0,0 +1,56 @@ +// Licensed to the projects contributors. +// The license conditions are provided in the LICENSE file located in the project root + +namespace Tethys.SPDX.ExpressionParser +{ + /// + /// The token types. + /// + internal enum TokenType + { + /// + /// A license identifier like MIT, Apache-2.0 or GPL-2.0. + /// + LicenseId, + + /// + /// A license reference like LicenseRed-someorg-somename. + /// + LicenseRef, + + /// + /// A license exception like Autoconf-exception-2.0. + /// + Exception, + + /// + /// A trailing plus sign to indicate "or later". + /// + Plus, + + /// + /// A left parenthesis. + /// + Left, + + /// + /// A right parenthesis. + /// + Right, + + /// + /// The license exception combination keyword.. + /// + With, + + /// + /// The license conjunction keyword. + /// + And, + + /// + /// The license disjunction keyword. + /// + Or, + } // TokenType +} diff --git a/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs b/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs index 22636dc4..5f5eefc6 100644 --- a/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs +++ b/tests/NuGetUtility.Test/LicenseValidator/LicenseValidatorTest.cs @@ -307,6 +307,68 @@ public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_Give .Using(new LicenseValidationResultValueEqualityComparer())); } + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectValidatedLicenseList_When_Or_Expression( + string packageId, + INuGetVersion packageVersion, + string license1, + string license2) + { + _uut = new NuGetUtility.LicenseValidator.LicenseValidator(_licenseMapping, + Array.Empty(), + _fileDownloader, + _ignoredLicenses); + + string expression = $"{license1} OR {license2}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectValidatedLicenseList_When_And_Expression( + string packageId, + INuGetVersion packageVersion, + string license1, + string license2) + { + _uut = new NuGetUtility.LicenseValidator.LicenseValidator(_licenseMapping, + Array.Empty(), + _fileDownloader, + _ignoredLicenses); + + string expression = $"{license1} AND {license2}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + [Test] [ExtendedAutoData(typeof(NuGetVersionBuilder))] public async Task ValidatingLicensesWithOverwriteLicenseInformation_Should_GiveCorrectValidatedLicenseList( @@ -502,6 +564,80 @@ public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_Give .Using(new LicenseValidationResultValueEqualityComparer())); } + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithOrExpression_If_NoneAllowed( + string packageId, + INuGetVersion packageVersion, + string[] licenses) + { + string expression = licenses.Length switch + { + 0 => string.Empty, + 1 => licenses[0], + 2 => $"{licenses[0]} OR {licenses[1]}", + _ => licenses.Skip(2).Aggregate($"{licenses[0]} OR {licenses[1]}", (expression, newLicense) => $"{newLicense} OR ({expression})") + }; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression, + new List + { + new ValidationError($"License {expression} not found in list of supported licenses", + _context) + }) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithAndExpression_If_OneNotAllowed( + string packageId, + INuGetVersion packageVersion, + string unallowedLicense) + { + string[] licenses = _allowedLicenses.Shuffle(135643).Append(unallowedLicense).ToArray(); + + string expression = licenses.Length switch + { + 0 => string.Empty, + 1 => licenses[0], + 2 => $"{licenses[0]} AND {licenses[1]}", + _ => licenses.Skip(2).Aggregate($"{licenses[0]} AND {licenses[1]}", (expression, newLicense) => $"{newLicense} AND ({expression})") + }; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression, + new List + { + new ValidationError($"License {expression} not found in list of supported licenses", + _context) + }) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + [Test] [ExtendedAutoData(typeof(NuGetVersionBuilder))] public async Task ValidatingLicensesWithOverwriteLicenseInformation_Should_GiveCorrectResult_If_NotAllowed( @@ -555,6 +691,57 @@ public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_Give .Using(new LicenseValidationResultValueEqualityComparer())); } + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithOrExpression_If_OneAllowed( + string packageId, + INuGetVersion packageVersion, + string unallowedLicense) + { + string expression = $"{_allowedLicenses.Shuffle(13563).First()} OR {unallowedLicense}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + + [Test] + [ExtendedAutoData(typeof(NuGetVersionBuilder))] + public async Task ValidatingLicensesWithExpressionLicenseInformation_Should_GiveCorrectResult_WithAndExpression_If_AllAllowed( + string packageId, + INuGetVersion packageVersion) + { + string[] licenses = _allowedLicenses.Shuffle(135643).Take(2).ToArray(); + + string expression = $"{licenses[0]} AND {licenses[1]}"; + + IPackageMetadata package = SetupPackageWithExpressionLicenseInformation(packageId, packageVersion, expression); + + IEnumerable result = await _uut.Validate(CreateInput(package, _context), _token.Token); + Assert.That(result, + Is.EquivalentTo(new[] + { + new LicenseValidationResult(packageId, + packageVersion, + _projectUrl.ToString(), + expression, + null, + LicenseInformationOrigin.Expression) + }) + .Using(new LicenseValidationResultValueEqualityComparer())); + } + [Test] [ExtendedAutoData(typeof(NuGetVersionBuilder))] public async Task ValidatingLicensesWithOverwriteLicenseInformation_Should_GiveCorrectResult_If_Allowed( diff --git a/tests/NuGetUtility.Test/LicenseValidator/UrlToLicenseMappingTest.License_Should_Be_Available_And_Match_Expected_License_847892e5f3e913b2.verified.txt b/tests/NuGetUtility.Test/LicenseValidator/UrlToLicenseMappingTest.License_Should_Be_Available_And_Match_Expected_License_847892e5f3e913b2.verified.txt new file mode 100644 index 00000000..c477d6b4 --- /dev/null +++ b/tests/NuGetUtility.Test/LicenseValidator/UrlToLicenseMappingTest.License_Should_Be_Available_And_Match_Expected_License_847892e5f3e913b2.verified.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs b/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs index bd9e21df..0e4be0d3 100644 --- a/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs +++ b/tests/NuGetUtility.Test/ReferencedPackagesReader/ReferencedPackagesReaderIntegrationTest.cs @@ -62,7 +62,7 @@ public void GetInstalledPackagesShould_ReturnTransitiveNuGet() } [Test] - public void GetInstalledPackagesShould_ReturnEmptyEnumerableForProjectsWithoutPackages() + public void GetInstalledPackagesShould_ReturnEmptyEnumerable_For_ProjectsWithoutPackages() { string path = Path.GetFullPath( "../../../../targets/ProjectWithoutNugetReferences/ProjectWithoutNugetReferences.csproj"); diff --git a/tests/targets/PackageReferenceProject/PackageReferenceProject.csproj b/tests/targets/PackageReferenceProject/PackageReferenceProject.csproj index a491bb3c..98c05136 100644 --- a/tests/targets/PackageReferenceProject/PackageReferenceProject.csproj +++ b/tests/targets/PackageReferenceProject/PackageReferenceProject.csproj @@ -1,15 +1,15 @@ - - net6.0 - enable - enable - Debug;Release;TestWindows - AnyCPU - + + net8.0 + enable + enable + Debug;Release;TestWindows + AnyCPU + - - - + + + diff --git a/tests/targets/ProjectWithTransitiveNuget/ProjectWithTransitiveNuget.csproj b/tests/targets/ProjectWithTransitiveNuget/ProjectWithTransitiveNuget.csproj index ea408f22..1a50e697 100644 --- a/tests/targets/ProjectWithTransitiveNuget/ProjectWithTransitiveNuget.csproj +++ b/tests/targets/ProjectWithTransitiveNuget/ProjectWithTransitiveNuget.csproj @@ -1,15 +1,15 @@ - - net6.0 - enable - enable - Debug;Release;TestWindows - AnyCPU - + + net8.0 + enable + enable + Debug;Release;TestWindows + AnyCPU + - - - + + + diff --git a/tests/targets/ProjectWithTransitiveReferences/ProjectWithTransitiveReferences.csproj b/tests/targets/ProjectWithTransitiveReferences/ProjectWithTransitiveReferences.csproj index 07c5d304..362e3647 100644 --- a/tests/targets/ProjectWithTransitiveReferences/ProjectWithTransitiveReferences.csproj +++ b/tests/targets/ProjectWithTransitiveReferences/ProjectWithTransitiveReferences.csproj @@ -1,15 +1,15 @@ - - net6.0 - enable - enable - Debug;Release;TestWindows - AnyCPU - + + net8.0 + enable + enable + Debug;Release;TestWindows + AnyCPU + - - - + + + diff --git a/tests/targets/ProjectWithoutNugetReferences/ProjectWithoutNugetReferences.csproj b/tests/targets/ProjectWithoutNugetReferences/ProjectWithoutNugetReferences.csproj index c1319926..d04a946b 100644 --- a/tests/targets/ProjectWithoutNugetReferences/ProjectWithoutNugetReferences.csproj +++ b/tests/targets/ProjectWithoutNugetReferences/ProjectWithoutNugetReferences.csproj @@ -1,11 +1,11 @@ - - net6.0 - enable - enable - Debug;Release;TestWindows - AnyCPU - + + net8.0 + enable + enable + Debug;Release;TestWindows + AnyCPU +