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(@"(?