diff --git a/README.md b/README.md
index a9de450..0c55226 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# dotnet-thirdpartynotices
-[![NuGet](https://img.shields.io/nuget/v/DotnetThirdPartyNotices)](https://www.nuget.org/packages/DotnetThirdPartyNotices/)
+[![NuGet](https://img.shields.io/nuget/v/DotnetThirdPartyNotices.svg)](https://www.nuget.org/packages/DotnetThirdPartyNotices/)
+[![NuGet downloads](https://img.shields.io/nuget/dt/SharpIppNext.svg)](https://www.nuget.org/packages/SharpIppNext)
+[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/danielklecha/SharpIppNext/blob/master/LICENSE.txt)
![bcs](https://i.giphy.com/media/giFr1HNq8p5gOQ3nCv/200.gif)
diff --git a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj b/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj
index d9070d4..89a8aa6 100644
--- a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj
+++ b/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj
@@ -10,7 +10,7 @@
dotnet-thirdpartynotices
DotnetThirdPartyNotices
A .NET tool to generate file with third party legal notices
- 0.2.9
+ 0.3.0
MIT
git
https://github.com/bugproof/DotnetThirdPartyNotices
diff --git a/src/DotnetThirdPartyNotices/Properties/launchSettings.json b/src/DotnetThirdPartyNotices/Properties/launchSettings.json
new file mode 100644
index 0000000..6263a72
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "DotnetThirdPartyNotices": {
+ "commandName": "Project",
+ "commandLineArgs": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/LicenseService.cs b/src/DotnetThirdPartyNotices/Services/LicenseService.cs
index 30086f4..a31c43b 100644
--- a/src/DotnetThirdPartyNotices/Services/LicenseService.cs
+++ b/src/DotnetThirdPartyNotices/Services/LicenseService.cs
@@ -1,14 +1,9 @@
using DotnetThirdPartyNotices.Models;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
+using System.Text.RegularExpressions;
namespace DotnetThirdPartyNotices.Services;
-internal class LicenseService(IEnumerable licenseUriLicenseResolvers, IEnumerable projectUriLicenseResolvers, IEnumerable repositoryUriLicenseResolvers, IEnumerable fileVersionInfoLicenseResolvers, IHttpClientFactory httpClientFactory) : ILicenseService
+internal partial class LicenseService(IEnumerable licenseUriLicenseResolvers, IEnumerable projectUriLicenseResolvers, IEnumerable repositoryUriLicenseResolvers, IEnumerable fileVersionInfoLicenseResolvers, IHttpClientFactory httpClientFactory) : ILicenseService
{
private static readonly Dictionary LicenseCache = [];
@@ -29,20 +24,48 @@ internal class LicenseService(IEnumerable licenseUri
?? (await ResolveFromProjectUrlAsync(resolvedFileInfo, true));
}
+ private static string? UnifyLicense(string? license)
+ {
+ if (string.IsNullOrWhiteSpace(license))
+ return null;
+ // remove empty lines from the beggining and at the end of the license
+ license = license.Trim('\r', '\n');
+ // unify new line characters
+ license = SingleLineFeedRegex().Replace(license, "\r\n");
+ license = SingleCarriageReturnRegex().Replace(license, "\r\n");
+ //remove control characters
+ license = ControlCharacterRegex().Replace(license, string.Empty);
+ //remove leading spaces in the whole license
+ var lines = license.Split('\n');
+ var leadingSpacesCount = lines.Aggregate((int?)null, (current, line) =>
+ {
+ if (current == 0 || string.IsNullOrWhiteSpace(line))
+ return current;
+ int spacesCount = line.TakeWhile(char.IsWhiteSpace).Count();
+ return current == null ? spacesCount : Math.Min(spacesCount, current.Value);
+ });
+ if (leadingSpacesCount > 0)
+ license = string.Join('\n', lines.Select(x => x.Length == 0 ? x : x[Math.Min(x.Length, leadingSpacesCount.Value)..]));
+ return license;
+ }
+
private async Task ResolveFromPackagePathAsync(ResolvedFileInfo resolvedFileInfo)
{
if (string.IsNullOrEmpty(resolvedFileInfo.PackagePath))
return null;
if (LicenseCache.TryGetValue(resolvedFileInfo.PackagePath, out string? value))
return value;
- var licensePath = Directory.EnumerateFiles(resolvedFileInfo.PackagePath, "license.*", new EnumerationOptions
+ var licensePath = Directory.EnumerateFiles(resolvedFileInfo.PackagePath, "*.*", new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = false
- }).FirstOrDefault(x => x.EndsWith("\\license.txt", StringComparison.OrdinalIgnoreCase) || x.EndsWith("\\license.md", StringComparison.OrdinalIgnoreCase));
+ }).FirstOrDefault(LicenseFileRegex().IsMatch);
if (licensePath == null)
return null;
var license = await File.ReadAllTextAsync(licensePath);
+ license = UnifyLicense(license);
+ if (license == null)
+ return null;
if (resolvedFileInfo.NuSpec != null)
LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
LicenseCache[resolvedFileInfo.PackagePath] = license;
@@ -53,20 +76,25 @@ internal class LicenseService(IEnumerable licenseUri
{
if (string.IsNullOrEmpty(resolvedFileInfo.SourcePath))
return null;
- var assemblyPath = Path.GetDirectoryName(resolvedFileInfo.SourcePath);
- if(assemblyPath == null)
- return null;
- if (LicenseCache.TryGetValue(assemblyPath, out string? value))
+ if (LicenseCache.TryGetValue(resolvedFileInfo.SourcePath, out string? value))
return value;
- var licensePath = Directory.EnumerateFiles(assemblyPath, "license.*", new EnumerationOptions
+ var directoryPath = Path.GetDirectoryName(resolvedFileInfo.SourcePath);
+ if(directoryPath == null)
+ return null;
+ var fileName = Path.GetFileNameWithoutExtension(resolvedFileInfo.SourcePath) ?? string.Empty;
+ Regex regex = new($"\\\\(|{Regex.Escape(fileName)}[+-_])license\\.(txt|md)$", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(300));
+ var licensePath = Directory.EnumerateFiles(directoryPath, "*.*", new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = false
- }).FirstOrDefault(x => x.EndsWith("\\license.txt", StringComparison.OrdinalIgnoreCase) || x.EndsWith("\\license.md", StringComparison.OrdinalIgnoreCase));
+ }).FirstOrDefault(regex.IsMatch);
if (licensePath == null)
return null;
var license = await File.ReadAllTextAsync(licensePath);
- LicenseCache[assemblyPath] = license;
+ license = UnifyLicense(license);
+ if (license == null)
+ return null;
+ LicenseCache[directoryPath] = license;
return license;
}
@@ -81,6 +109,7 @@ internal class LicenseService(IEnumerable licenseUri
if (!resolver.CanResolve(resolvedFileInfo.VersionInfo))
continue;
var license = await resolver.Resolve(resolvedFileInfo.VersionInfo);
+ license = UnifyLicense(license);
if (license != null)
{
LicenseCache[resolvedFileInfo.VersionInfo.FileName] = license;
@@ -100,8 +129,10 @@ internal class LicenseService(IEnumerable licenseUri
if (!licenseFullPath.EndsWith(".txt") && !licenseFullPath.EndsWith(".md") || !File.Exists(licenseFullPath))
return null;
var license = await File.ReadAllTextAsync(licenseFullPath);
- if (string.IsNullOrEmpty(license))
+ license = UnifyLicense(license);
+ if (license == null)
return null;
+ license = license.Trim();
LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
LicenseCache[licenseFullPath] = license;
return license;
@@ -118,11 +149,11 @@ internal class LicenseService(IEnumerable licenseUri
var license = useFinalUrl
? await ResolveFromFinalUrlAsync(uri, projectUriLicenseResolvers)
: await ResolveFromUrlAsync(uri, projectUriLicenseResolvers);
- if (license != null)
- {
- LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
- LicenseCache[resolvedFileInfo.NuSpec.ProjectUrl] = license;
- }
+ license = UnifyLicense(license);
+ if (license == null)
+ return null;
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[resolvedFileInfo.NuSpec.ProjectUrl] = license;
return license;
}
@@ -133,6 +164,7 @@ internal class LicenseService(IEnumerable licenseUri
if (!resolver.CanResolve(uri))
continue;
var license = await resolver.Resolve(uri);
+ license = UnifyLicense(license);
if (license != null)
return license;
}
@@ -159,6 +191,7 @@ internal class LicenseService(IEnumerable licenseUri
if (!resolver.CanResolve(httpResponseMessage.RequestMessage.RequestUri))
continue;
var license = await resolver.Resolve(httpResponseMessage.RequestMessage.RequestUri);
+ license = UnifyLicense(license);
if (license != null)
return license;
}
@@ -166,7 +199,8 @@ internal class LicenseService(IEnumerable licenseUri
// Finally, if no license uri can be found despite all the redirects, try to blindly get it
if (httpResponseMessage.Content.Headers.ContentType?.MediaType != "text/plain")
return null;
- return await httpResponseMessage.Content.ReadAsStringAsync();
+ var license2 = await httpResponseMessage.Content.ReadAsStringAsync();
+ return UnifyLicense(license2);
}
private async Task ResolveFromRepositoryUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl)
@@ -180,6 +214,7 @@ internal class LicenseService(IEnumerable licenseUri
var license = useFinalUrl
? await ResolveFromFinalUrlAsync(uri, repositoryUriLicenseResolvers)
: await ResolveFromUrlAsync(uri, repositoryUriLicenseResolvers);
+ license = UnifyLicense(license);
if (license != null)
{
LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
@@ -199,11 +234,19 @@ internal class LicenseService(IEnumerable licenseUri
var license = useFinalUrl
? await ResolveFromFinalUrlAsync(uri, licenseUriLicenseResolvers)
: await ResolveFromUrlAsync(uri, licenseUriLicenseResolvers);
- if (license != null)
- {
- LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
- LicenseCache[resolvedFileInfo.NuSpec.LicenseUrl] = license;
- }
+ license = UnifyLicense(license);
+ if (license == null)
+ return null;
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[resolvedFileInfo.NuSpec.LicenseUrl] = license;
return license;
}
+ [GeneratedRegex(@"\\license\.(txt|md)$", RegexOptions.IgnoreCase, 300)]
+ private static partial Regex LicenseFileRegex();
+ [GeneratedRegex(@"(\f|\uFEFF|\u200B)", RegexOptions.None, 300)]
+ private static partial Regex ControlCharacterRegex();
+ [GeneratedRegex(@"(?