Skip to content

Commit

Permalink
(chocolatey#3000) Add new rule service to handle custom rules
Browse files Browse the repository at this point in the history
This commit adds a new rule service together with some base types
that will be used by the service itself, and any implementing
rule.
This service takes a path to a `nupkg` file or to a `nuspec` file
and passes along the appropriate values necessary
to validate the metadata that is being used for the
package.
  • Loading branch information
AdmiringWorm committed Jan 27, 2023
1 parent 2cbac08 commit fbe4c0f
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace chocolatey.tests.infrastructure.app.services
using chocolatey.infrastructure.app.configuration;
using chocolatey.infrastructure.app.domain;
using chocolatey.infrastructure.app.services;
using chocolatey.infrastructure.services;
using Moq;
using NuGet.Common;
using NuGet.Packaging;
Expand All @@ -40,6 +41,7 @@ public abstract class NugetServiceSpecsBase : TinySpec
protected Mock<IFilesService> filesService = new Mock<IFilesService>();
protected Mock<IPackageMetadata> package = new Mock<IPackageMetadata>();
protected Mock<IPackageDownloader> packageDownloader = new Mock<IPackageDownloader>();
protected Mock<IRuleService> ruleService = new Mock<IRuleService>();

public override void Context()
{
Expand All @@ -49,7 +51,7 @@ public override void Context()
filesService.ResetCalls();
package.ResetCalls();

service = new NugetService(fileSystem.Object, nugetLogger.Object, packageInfoService.Object, filesService.Object);
service = new NugetService(fileSystem.Object, nugetLogger.Object, packageInfoService.Object, filesService.Object, ruleService.Object);
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/chocolatey/chocolatey.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.3.3.3\build\Microsoft.CodeAnalysis.BannedApiAnalyzers.props" Condition="Exists('..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.3.3.3\build\Microsoft.CodeAnalysis.BannedApiAnalyzers.props')" />
<PropertyGroup>
Expand Down Expand Up @@ -215,6 +215,8 @@
<Compile Include="infrastructure.app\nuget\ChocolateyNuGetProjectContext.cs" />
<Compile Include="infrastructure.app\nuget\ChocolateyNuGetSettings.cs" />
<Compile Include="infrastructure.app\nuget\ChocolateySourceCacheContext.cs" />
<Compile Include="infrastructure.app\rules\MetadataRuleBase.cs" />
<Compile Include="infrastructure.app\services\RuleService.cs" />
<Compile Include="infrastructure\cryptography\DefaultEncryptionUtility.cs" />
<Compile Include="infrastructure\adapters\IEncryptionUtility.cs" />
<Compile Include="infrastructure.app\validations\GlobalConfigurationValidation.cs" />
Expand Down Expand Up @@ -251,6 +253,10 @@
<Compile Include="infrastructure\logging\LogMessage.cs" />
<Compile Include="infrastructure\logging\LogSinkLog.cs" />
<Compile Include="infrastructure\registration\AssemblyResolution.cs" />
<Compile Include="infrastructure\rules\IMetadataRule.cs" />
<Compile Include="infrastructure\rules\RuleResult.cs" />
<Compile Include="infrastructure\rules\RuleType.cs" />
<Compile Include="infrastructure\services\IRuleService.cs" />
<Compile Include="infrastructure\synchronization\GlobalMutex.cs" />
<Compile Include="infrastructure\logging\TraceLog.cs" />
<Compile Include="infrastructure\registration\SecurityProtocol.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace chocolatey.infrastructure.app.registration
using NuGet.Common;
using NuGet.PackageManagement;
using NuGet.Packaging;
using chocolatey.infrastructure.rules;
using chocolatey.infrastructure.app.rules;
using System.Linq;

internal class ChocolateyRegistrationModule : IExtensionModule
{
Expand Down Expand Up @@ -78,6 +81,16 @@ public void register_dependencies(IContainerRegistrator registrator, ChocolateyC
registrator.register_service<IValidation>(
typeof(GlobalConfigurationValidation),
typeof(SystemStateValidation));

// Rule registrations
registrator.register_service<IRuleService, RuleService>();

var availableRules = GetType().Assembly
.GetTypes()
.Where(t => !t.IsInterface && !t.IsAbstract && typeof(IMetadataRule).IsAssignableFrom(t))
.ToArray();

registrator.register_service<IMetadataRule>(availableRules);
}
}
}
55 changes: 55 additions & 0 deletions src/chocolatey/infrastructure.app/rules/MetadataRuleBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright © 2023-Present Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.app.rules
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using chocolatey.infrastructure.rules;
using NuGet.Packaging;

public abstract class MetadataRuleBase : IMetadataRule
{
public abstract IEnumerable<RuleResult> validate(NuspecReader reader);

protected static bool has_element(NuspecReader reader, string name)
{
var metadataNode = reader.Xml.Root.Elements().FirstOrDefault(e => StringComparer.Ordinal.Equals(e.Name.LocalName, "metadata"));

return !(metadataNode is null || metadataNode.Elements(XName.Get(name, metadataNode.GetDefaultNamespace().NamespaceName)).FirstOrDefault() is null);
}

protected static string get_element_value(NuspecReader reader, string name)
{
var metadataNode = reader.Xml.Root.Elements().FirstOrDefault(e => StringComparer.Ordinal.Equals(e.Name.LocalName, "metadata"));

if (metadataNode is null)
{
return null;
}

var element = metadataNode.Elements(XName.Get(name, metadataNode.GetDefaultNamespace().NamespaceName)).FirstOrDefault();

if (element is null)
{
return null;
}

return element.Value;
}
}
}
49 changes: 47 additions & 2 deletions src/chocolatey/infrastructure.app/services/NugetService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2017 - 2022 Chocolatey Software, Inc
// Copyright © 2017 - 2023 Chocolatey Software, Inc
// Copyright © 2011 - 2017 RealDimensions Software, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -52,6 +52,7 @@ namespace chocolatey.infrastructure.app.services
using NuGet.Versioning;
using System.Xml.Linq;
using infrastructure.configuration;
using chocolatey.infrastructure.services;

//todo: #2575 - this monolith is too large. Refactor once test coverage is up.

Expand All @@ -61,6 +62,7 @@ public class NugetService : INugetService
private readonly ILogger _nugetLogger;
private readonly IChocolateyPackageInformationService _packageInfoService;
private readonly IFilesService _filesService;
private readonly IRuleService _ruleService;
//private readonly PackageDownloader _packageDownloader;
private readonly Lazy<IDateTime> datetime_initializer = new Lazy<IDateTime>(() => new DateTime());

Expand All @@ -76,12 +78,18 @@ private IDateTime DateTime
/// <param name="nugetLogger">The nuget logger</param>
/// <param name="packageInfoService">Package information service</param>
/// <param name="filesService">The files service</param>
public NugetService(IFileSystem fileSystem, ILogger nugetLogger, IChocolateyPackageInformationService packageInfoService, IFilesService filesService)
public NugetService(
IFileSystem fileSystem,
ILogger nugetLogger,
IChocolateyPackageInformationService packageInfoService,
IFilesService filesService,
IRuleService ruleService)
{
_fileSystem = fileSystem;
_nugetLogger = nugetLogger;
_packageInfoService = packageInfoService;
_filesService = filesService;
_ruleService = ruleService;
}

public string SourceType
Expand Down Expand Up @@ -1573,6 +1581,43 @@ protected virtual ChocolateyConfiguration set_package_config_for_upgrade(Chocola
return originalConfig;
}

private void validate_nuspec(string nuspecFilePath)
{
var results = _ruleService.validate_rules(nuspecFilePath);

if (!config.PackCommand.PackThrowOnUnsupportedElements)
{
results = results.Where(r => !r.Message.contains("not supported"));
}

var hasErrors = false;

foreach (var rule in results)
{
switch (rule.Severity)
{
case infrastructure.rules.RuleType.Error:
this.Log().Error("ERROR: " + rule.Message);
hasErrors = true;
break;

case infrastructure.rules.RuleType.Warning:
this.Log().Warn("WARNING: " + rule.Message);
break;

case infrastructure.rules.RuleType.Information:
this.Log().Info("INFO: " + rule.Message);
break;
}
}

if (hasErrors)
{
this.Log().Info(string.Empty);
throw new InvalidDataException("One or more issues found with {0}, please fix all validation items above listed as errors.".format_with(nuspecFilePath));
}
}

private string get_install_directory(ChocolateyConfiguration config, IPackageMetadata installedPackage)
{

Expand Down
85 changes: 85 additions & 0 deletions src/chocolatey/infrastructure.app/services/RuleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright © 2023-Present Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.app.services
{
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using chocolatey.infrastructure.guards;
using chocolatey.infrastructure.rules;
using chocolatey.infrastructure.services;
using NuGet.Configuration;
using NuGet.Packaging;

public sealed class RuleService : IRuleService
{
private readonly IMetadataRule[] _rules;

public RuleService(IMetadataRule[] rules)
{
_rules = rules;
}

public IEnumerable<RuleResult> validate_rules(string filePath)
{
Ensure.that(() => filePath)
.is_not_null_or_whitespace()
.has_any_extension(NuGetConstants.PackageExtension, NuGetConstants.ManifestExtension);

var rules = filePath.EndsWith(NuGetConstants.PackageExtension)
? get_rules_from_package_async(filePath).GetAwaiter().GetResult()
: get_rules_from_metadata(filePath);

return rules
.OrderBy(r => r.Severity)
.ThenBy(r => r.Message);
}

private async Task<IEnumerable<RuleResult>> get_rules_from_package_async(string filePath, CancellationToken token = default)
{
using (var packageReader = new PackageArchiveReader(filePath))
{
var nuspecReader = await packageReader.GetNuspecReaderAsync(token);

// We add ToList here to ensure that the package
// reader hasn't been disposed of before we return
// any results.
return validate_nuspec(nuspecReader, _rules).ToList();
}
}

private IEnumerable<RuleResult> get_rules_from_metadata(string filePath)
{
var nuspecReader = new NuspecReader(filePath);

return validate_nuspec(nuspecReader, _rules);
}

private static IEnumerable<RuleResult> validate_nuspec(NuspecReader reader, IMetadataRule[] rules)
{
foreach (var rule in rules)
{
var validationResults = rule.validate(reader);

foreach (var result in validationResults.Where(v => v.Severity != RuleType.None))
{
yield return result;
}
}
}
}
}
27 changes: 27 additions & 0 deletions src/chocolatey/infrastructure/rules/IMetadataRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright © 2023-Present Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.rules
{
using System.Collections.Generic;
using chocolatey.infrastructure.app.attributes;
using NuGet.Packaging;

[MultiService]
public interface IMetadataRule
{
IEnumerable<RuleResult> validate(NuspecReader reader);
}
}
32 changes: 32 additions & 0 deletions src/chocolatey/infrastructure/rules/RuleResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright © 2023-Present Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.rules
{
public readonly struct RuleResult
{
public static readonly RuleResult Success = new RuleResult(RuleType.None, string.Empty);

public RuleResult(RuleType severity, string message)
{
Severity = severity;
Message = message;
}

public readonly RuleType Severity;

public readonly string Message;
}
}
25 changes: 25 additions & 0 deletions src/chocolatey/infrastructure/rules/RuleType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright © 2023-Present Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.rules
{
public enum RuleType
{
None = 0,
Error,
Warning,
Information
}
}
Loading

0 comments on commit fbe4c0f

Please sign in to comment.