diff --git a/src/Methods.cs b/src/Methods.cs index 785d1476..a6bac80c 100644 --- a/src/Methods.cs +++ b/src/Methods.cs @@ -13,32 +13,42 @@ using Newtonsoft.Json; using static NugetUtility.Utilties; -namespace NugetUtility { - public class Methods { +namespace NugetUtility +{ + public class Methods + { private const string fallbackPackageUrl = "https://www.nuget.org/api/v2/package/{0}/{1}"; private const string nugetUrl = "https://api.nuget.org/v3-flatcontainer/"; - private static readonly Dictionary, Package> _requestCache = new Dictionary, Package> (); - private static readonly Dictionary, string> _licenseFileCache = new Dictionary, string> (); + private const string deprecateNugetLicense = "https://aka.ms/deprecateLicenseUrl"; + private static readonly Dictionary, Package> _requestCache = new Dictionary, Package>(); + private static readonly Dictionary, string> _licenseFileCache = new Dictionary, string>(); /// /// See https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ /// private static HttpClient _httpClient; + private const int maxRedirects = 5; // HTTP client max number of redirects allowed + private const int timeout = 10; // HTTP client timeout in seconds private readonly IReadOnlyDictionary _licenseMappings; private readonly PackageOptions _packageOptions; private readonly XmlSerializer _serializer; - public Methods (PackageOptions packageOptions) { - if (_httpClient is null) { - _httpClient = new HttpClient (new HttpClientHandler { + public Methods(PackageOptions packageOptions) + { + if (_httpClient is null) + { + _httpClient = new HttpClient(new HttpClientHandler + { AllowAutoRedirect = true, - MaxAutomaticRedirections = 20, - }); + MaxAutomaticRedirections = maxRedirects + }) + { + BaseAddress = new Uri(nugetUrl), + Timeout = TimeSpan.FromSeconds(timeout) + }; - _httpClient.BaseAddress = new Uri (nugetUrl); - _httpClient.Timeout = TimeSpan.FromSeconds (10); } - _serializer = new XmlSerializer (typeof (Package)); + _serializer = new XmlSerializer(typeof(Package)); _packageOptions = packageOptions; _licenseMappings = packageOptions.LicenseToUrlMappingsDictionary; } @@ -49,15 +59,20 @@ public Methods (PackageOptions packageOptions) { /// project name /// List of projects /// - public async Task GetNugetInformationAsync (string project, IEnumerable packages) { - WriteOutput (Environment.NewLine + "project:" + project + Environment.NewLine, logLevel : LogLevel.Information); - var licenses = new PackageList (); - foreach (var packageWithVersion in packages) { - var versions = packageWithVersion.Version.Trim (new char[] { '[', ']', '(', ')' }).Split (","); - foreach (var version in versions) { - try { - if (string.IsNullOrWhiteSpace (packageWithVersion.Name) || string.IsNullOrWhiteSpace (version)) { - WriteOutput ($"Skipping invalid entry {packageWithVersion}", logLevel : LogLevel.Verbose); + public async Task GetNugetInformationAsync(string project, IEnumerable packages) + { + WriteOutput(Environment.NewLine + "project:" + project + Environment.NewLine, logLevel: LogLevel.Information); + var licenses = new PackageList(); + foreach (var packageWithVersion in packages) + { + var versions = packageWithVersion.Version.Trim(new char[] { '[', ']', '(', ')' }).Split(","); + foreach (var version in versions) + { + try + { + if (string.IsNullOrWhiteSpace(packageWithVersion.Name) || string.IsNullOrWhiteSpace(version)) + { + WriteOutput($"Skipping invalid entry {packageWithVersion}", logLevel: LogLevel.Verbose); continue; } @@ -67,59 +82,76 @@ public async Task GetNugetInformationAsync (string project, IEnumer continue; } - var lookupKey = Tuple.Create (packageWithVersion.Name, version); + var lookupKey = Tuple.Create(packageWithVersion.Name, version); - if (_requestCache.TryGetValue (lookupKey, out var package)) { - WriteOutput (packageWithVersion + " obtained from request cache.", logLevel : LogLevel.Information); - licenses.TryAdd ($"{packageWithVersion.Name},{version}", package); + if (_requestCache.TryGetValue(lookupKey, out var package)) + { + WriteOutput(packageWithVersion + " obtained from request cache.", logLevel: LogLevel.Information); + licenses.TryAdd($"{packageWithVersion.Name},{version}", package); continue; } // Search nuspec in local cache - string userDir = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile); - var nuspecPath = Path.Combine (userDir, ".nuget", "packages", packageWithVersion.Name, version, packageWithVersion.Name + ".nuspec"); - if (File.Exists (nuspecPath)) { - try { - using (var textReader = new StreamReader (nuspecPath)) { - await ReadNuspecFile (project, licenses, packageWithVersion.Name, version, lookupKey, textReader); + string userDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var nuspecPath = Path.Combine(userDir, ".nuget", "packages", packageWithVersion.Name, version, packageWithVersion.Name + ".nuspec"); + if (File.Exists(nuspecPath)) + { + try + { + using (var textReader = new StreamReader(nuspecPath)) + { + await ReadNuspecFile(project, licenses, packageWithVersion.Name, version, lookupKey, textReader); } continue; - } catch { + } + catch + { // Ignore errors in local cache, try online call } } // Try dowload nuspec - using (var request = new HttpRequestMessage (HttpMethod.Get, $"{packageWithVersion.Name}/{version}/{packageWithVersion.Name}.nuspec")) - using (var response = await _httpClient.SendAsync (request)) { - if (!response.IsSuccessStatusCode) { - WriteOutput ($"{request.RequestUri} failed due to {response.StatusCode}!", logLevel : LogLevel.Warning); - var fallbackResult = await GetNuGetPackageFileResult (packageWithVersion.Name, version, $"{packageWithVersion.Name}.nuspec"); - if (fallbackResult is Package) { - licenses.Add ($"{packageWithVersion.Name},{version}", fallbackResult); - await this.AddTransitivePackages (project, licenses, fallbackResult); + using (var request = new HttpRequestMessage(HttpMethod.Get, $"{packageWithVersion.Name}/{version}/{packageWithVersion.Name}.nuspec")) + using (var response = await _httpClient.SendAsync(request)) + { + if (!response.IsSuccessStatusCode) + { + WriteOutput($"{request.RequestUri} failed due to {response.StatusCode}!", logLevel: LogLevel.Warning); + var fallbackResult = await GetNuGetPackageFileResult(packageWithVersion.Name, version, $"{packageWithVersion.Name}.nuspec"); + if (fallbackResult is Package) + { + licenses.Add($"{packageWithVersion.Name},{version}", fallbackResult); + await this.AddTransitivePackages(project, licenses, fallbackResult); _requestCache[lookupKey] = fallbackResult; - await HandleLicensing (fallbackResult); - } else { - licenses.Add ($"{packageWithVersion.Name},{version}", new Package { Metadata = new Metadata { Version = version, Id = packageWithVersion.Name } }); + await HandleLicensing(fallbackResult); + } + else + { + licenses.Add($"{packageWithVersion.Name},{version}", new Package { Metadata = new Metadata { Version = version, Id = packageWithVersion.Name } }); } continue; } - WriteOutput ($"Successfully received {request.RequestUri}", logLevel : LogLevel.Information); - using (var responseText = await response.Content.ReadAsStreamAsync ()) - using (var textReader = new StreamReader (responseText)) { - try { - await ReadNuspecFile (project, licenses, packageWithVersion.Name, version, lookupKey, textReader); - } catch (Exception e) { - WriteOutput (e.Message, e, LogLevel.Error); + WriteOutput($"Successfully received {request.RequestUri}", logLevel: LogLevel.Information); + using (var responseText = await response.Content.ReadAsStreamAsync()) + using (var textReader = new StreamReader(responseText)) + { + try + { + await ReadNuspecFile(project, licenses, packageWithVersion.Name, version, lookupKey, textReader); + } + catch (Exception e) + { + WriteOutput(e.Message, e, LogLevel.Error); throw; } } } - } catch (Exception ex) { - WriteOutput (ex.Message, ex, LogLevel.Error); + } + catch (Exception ex) + { + WriteOutput(ex.Message, ex, LogLevel.Error); } } } @@ -127,85 +159,100 @@ public async Task GetNugetInformationAsync (string project, IEnumer return licenses; } - private async Task ReadNuspecFile (string project, PackageList licenses, string package, string version, Tuple lookupKey, StreamReader textReader) { - if (_serializer.Deserialize (new NamespaceIgnorantXmlTextReader (textReader)) is Package result) { - licenses.Add ($"{package},{version}", result); - await this.AddTransitivePackages (project, licenses, result); + private async Task ReadNuspecFile(string project, PackageList licenses, string package, string version, Tuple lookupKey, StreamReader textReader) + { + if (_serializer.Deserialize(new NamespaceIgnorantXmlTextReader(textReader)) is Package result) + { + licenses.Add($"{package},{version}", result); + await this.AddTransitivePackages(project, licenses, result); _requestCache[lookupKey] = result; - await HandleLicensing (result); + await HandleLicensing(result); } } - private async Task AddTransitivePackages (string project, PackageList licenses, Package result) { + private async Task AddTransitivePackages(string project, PackageList licenses, Package result) + { var groups = result.Metadata?.Dependencies?.Group; - if (_packageOptions.IncludeTransitive && groups != null) { - foreach (var group in groups) { + if (_packageOptions.IncludeTransitive && groups != null) + { + foreach (var group in groups) + { var dependant = group .Dependency - .Where (e => !licenses.Keys.Contains ($"{e.Id},{e.Version}")) - .Select (e => new PackageNameAndVersion { Name = e.Id, Version = e.Version }); - - var dependantPackages = await GetNugetInformationAsync (project, dependant); - foreach (var dependantPackage in dependantPackages) { - if (!licenses.ContainsKey (dependantPackage.Key)) { - licenses.Add (dependantPackage.Key, dependantPackage.Value); + .Where(e => !licenses.Keys.Contains($"{e.Id},{e.Version}")) + .Select(e => new PackageNameAndVersion { Name = e.Id, Version = e.Version }); + + var dependantPackages = await GetNugetInformationAsync(project, dependant); + foreach (var dependantPackage in dependantPackages) + { + if (!licenses.ContainsKey(dependantPackage.Key)) + { + licenses.Add(dependantPackage.Key, dependantPackage.Value); } } } } } - public async Task> GetPackages () { - WriteOutput (() => $"Starting {nameof(GetPackages)}...", logLevel : LogLevel.Verbose); - var licenses = new Dictionary (); - var projectFiles = await GetValidProjects (_packageOptions.ProjectDirectory); - foreach (var projectFile in projectFiles) { - var references = this.GetProjectReferences (projectFile); - var referencedPackages = references.Select ((package) => { - var split = package.Split (','); + public async Task> GetPackages() + { + WriteOutput(() => $"Starting {nameof(GetPackages)}...", logLevel: LogLevel.Verbose); + var licenses = new Dictionary(); + var projectFiles = await GetValidProjects(_packageOptions.ProjectDirectory); + foreach (var projectFile in projectFiles) + { + var references = this.GetProjectReferences(projectFile); + var referencedPackages = references.Select((package) => + { + var split = package.Split(','); return new PackageNameAndVersion { Name = split[0], Version = split[1] }; }); - var currentProjectLicenses = await this.GetNugetInformationAsync (projectFile, referencedPackages); + var currentProjectLicenses = await this.GetNugetInformationAsync(projectFile, referencedPackages); licenses[projectFile] = currentProjectLicenses; } return licenses; } - public string[] GetProjectExtensions (bool withWildcard = false) => + public string[] GetProjectExtensions(bool withWildcard = false) => withWildcard ? - new [] { "*.csproj", "*.fsproj" } : - new [] { ".csproj", ".fsproj" }; + new[] { "*.csproj", "*.fsproj" } : + new[] { ".csproj", ".fsproj" }; /// /// Retreive the project references from csproj or fsproj file /// /// The Project Path /// - public IEnumerable GetProjectReferences (string projectPath) { - WriteOutput (() => $"Starting {nameof(GetProjectReferences)}...", logLevel : LogLevel.Verbose); - if (string.IsNullOrWhiteSpace (projectPath)) { - throw new ArgumentNullException (projectPath); + public IEnumerable GetProjectReferences(string projectPath) + { + WriteOutput(() => $"Starting {nameof(GetProjectReferences)}...", logLevel: LogLevel.Verbose); + if (string.IsNullOrWhiteSpace(projectPath)) + { + throw new ArgumentNullException(projectPath); } - if (!GetProjectExtensions ().Any (projExt => projectPath.EndsWith (projExt))) { - projectPath = GetValidProjects (projectPath).GetAwaiter ().GetResult ().FirstOrDefault (); + if (!GetProjectExtensions().Any(projExt => projectPath.EndsWith(projExt))) + { + projectPath = GetValidProjects(projectPath).GetAwaiter().GetResult().FirstOrDefault(); } - if (projectPath is null) { - throw new FileNotFoundException (); + if (projectPath is null) + { + throw new FileNotFoundException(); } // First try to get references from new project file format - var references = GetProjectReferencesFromNewProjectFile (projectPath); + var references = GetProjectReferencesFromNewProjectFile(projectPath); // Then if needed from old packages.config - if (!references.Any ()) { - references = GetProjectReferencesFromPackagesConfig (projectPath); + if (!references.Any()) + { + references = GetProjectReferencesFromPackagesConfig(projectPath); } - return references ?? Array.Empty (); + return references ?? Array.Empty(); } /// @@ -213,161 +260,121 @@ public IEnumerable GetProjectReferences (string projectPath) { /// /// /// - public List MapPackagesToLibraryInfo (Dictionary packages) { - WriteOutput (() => $"Starting {nameof(MapPackagesToLibraryInfo)}...", logLevel : LogLevel.Verbose); - var libraryInfos = new List (256); - foreach (var packageList in packages) { - foreach (var item in packageList.Value.Select (p => p.Value)) { - var info = MapPackageToLibraryInfo (item, packageList.Key); - libraryInfos.Add (info); + public List MapPackagesToLibraryInfo(Dictionary packages) + { + WriteOutput(() => $"Starting {nameof(MapPackagesToLibraryInfo)}...", logLevel: LogLevel.Verbose); + var libraryInfos = new List(256); + foreach (var packageList in packages) + { + foreach (var item in packageList.Value.Select(p => p.Value)) + { + var info = MapPackageToLibraryInfo(item, packageList.Key); + libraryInfos.Add(info); } } // merge in missing manual items where there wasn't a package - var missedManualItems = _packageOptions.ManualInformation.Except (libraryInfos, LibraryNameAndVersionComparer.Default); - foreach (var missed in missedManualItems) { - libraryInfos.Add (missed); + var missedManualItems = _packageOptions.ManualInformation.Except(libraryInfos, LibraryNameAndVersionComparer.Default); + foreach (var missed in missedManualItems) + { + libraryInfos.Add(missed); } - if (_packageOptions.UniqueOnly) { + if (_packageOptions.UniqueOnly) + { libraryInfos = libraryInfos - .GroupBy (x => new { x.PackageName, x.PackageVersion }) - .Select (g => { - var first = g.First (); - return new LibraryInfo { + .GroupBy(x => new { x.PackageName, x.PackageVersion }) + .Select(g => + { + var first = g.First(); + return new LibraryInfo + { PackageName = first.PackageName, - PackageVersion = first.PackageVersion, - PackageUrl = first.PackageUrl, - Copyright = first.Copyright, - Authors = first.Authors, - Description = first.Description, - LicenseType = first.LicenseType, - LicenseUrl = first.LicenseUrl, - Projects = _packageOptions.IncludeProjectFile ? string.Join (";", g.Select (p => p.Projects)) : null + PackageVersion = first.PackageVersion, + PackageUrl = first.PackageUrl, + Copyright = first.Copyright, + Authors = first.Authors, + Description = first.Description, + LicenseType = first.LicenseType, + LicenseUrl = first.LicenseUrl, + Projects = _packageOptions.IncludeProjectFile ? string.Join(";", g.Select(p => p.Projects)) : null }; }) - .ToList (); + .ToList(); } return libraryInfos - .OrderBy (p => p.PackageName) - .ToList (); + .OrderBy(p => p.PackageName) + .ToList(); } - private LibraryInfo MapPackageToLibraryInfo (Package item, string projectFile) { + private LibraryInfo MapPackageToLibraryInfo(Package item, string projectFile) + { string licenseType = item.Metadata.License?.Text ?? null; string licenseUrl = item.Metadata.LicenseUrl ?? null; - if (licenseUrl is string && string.IsNullOrWhiteSpace (licenseType)) { - if (_licenseMappings.TryGetValue (licenseUrl, out var license)) { + if (licenseUrl is string && string.IsNullOrWhiteSpace(licenseType)) + { + if (_licenseMappings.TryGetValue(licenseUrl, out var license)) + { licenseType = license; } } var manual = _packageOptions.ManualInformation - .FirstOrDefault (f => f.PackageName == item.Metadata.Id && f.PackageVersion == item.Metadata.Version); + .FirstOrDefault(f => f.PackageName == item.Metadata.Id && f.PackageVersion == item.Metadata.Version); - return new LibraryInfo { + return new LibraryInfo + { PackageName = item.Metadata.Id ?? string.Empty, - PackageVersion = item.Metadata.Version ?? string.Empty, - PackageUrl = !string.IsNullOrWhiteSpace (manual?.PackageUrl) ? + PackageVersion = item.Metadata.Version ?? string.Empty, + PackageUrl = !string.IsNullOrWhiteSpace(manual?.PackageUrl) ? manual.PackageUrl : item.Metadata.ProjectUrl ?? string.Empty, - Copyright = item.Metadata.Copyright ?? string.Empty, - Authors = manual?.Authors ?? item.Metadata.Authors?.Split (',') ?? new string[] { }, - Description = !string.IsNullOrWhiteSpace (manual?.Description) ? + Copyright = item.Metadata.Copyright ?? string.Empty, + Authors = manual?.Authors ?? item.Metadata.Authors?.Split(',') ?? new string[] { }, + Description = !string.IsNullOrWhiteSpace(manual?.Description) ? manual.Description : item.Metadata.Description ?? string.Empty, - LicenseType = manual?.LicenseType ?? licenseType ?? string.Empty, - LicenseUrl = manual?.LicenseUrl ?? licenseUrl ?? string.Empty, - Projects = _packageOptions.IncludeProjectFile ? projectFile : null - }; - } - - public void PrintLicenses (List libraries) { - if (libraries is null) { throw new ArgumentNullException (nameof (libraries)); } - if (!libraries.Any ()) { return; } - - WriteOutput (Environment.NewLine + "References:", logLevel : LogLevel.Always); - WriteOutput (libraries.ToStringTable (new [] { "Reference", "Version", "License Type", "License" }, - a => a.PackageName ?? "---", - a => a.PackageVersion ?? "---", - a => a.LicenseType ?? "---", - a => a.LicenseUrl ?? "---"), logLevel : LogLevel.Always); - } - - public void SaveAsJson (List libraries) { - if (!libraries.Any () || !_packageOptions.JsonOutput) { return; } - JsonSerializerSettings jsonSettings = new JsonSerializerSettings { - NullValueHandling = _packageOptions.IncludeProjectFile ? NullValueHandling.Include : NullValueHandling.Ignore + LicenseType = manual?.LicenseType ?? licenseType ?? string.Empty, + LicenseUrl = manual?.LicenseUrl ?? licenseUrl ?? string.Empty, + Projects = _packageOptions.IncludeProjectFile ? projectFile : null }; - - using (var fileStream = new FileStream (GetOutputFilename ("licenses.json"), FileMode.Create)) - using (var streamWriter = new StreamWriter (fileStream)) { - streamWriter.Write (JsonConvert.SerializeObject (libraries, jsonSettings)); - streamWriter.Flush (); - } - } - - public void SaveAsTextFile (List libraries) { - if (!libraries.Any () || !_packageOptions.TextOutput) { return; } - StringBuilder sb = new StringBuilder (256); - foreach (var lib in libraries) { - sb.Append (new string ('#', 100)); - sb.AppendLine (); - sb.Append ("Package:"); - sb.Append (lib.PackageName); - sb.AppendLine (); - sb.Append ("Version:"); - sb.Append (lib.PackageVersion); - sb.AppendLine (); - sb.Append ("project URL:"); - sb.Append (lib.PackageUrl); - sb.AppendLine (); - sb.Append ("Description:"); - sb.Append (lib.Description); - sb.AppendLine (); - sb.Append ("licenseUrl:"); - sb.Append (lib.LicenseUrl); - sb.AppendLine (); - sb.Append ("license Type:"); - sb.Append (lib.LicenseType); - sb.AppendLine (); - if (_packageOptions.IncludeProjectFile) { - sb.Append ("Project:"); - sb.Append (lib.Projects); - sb.AppendLine (); - } - sb.AppendLine (); - } - - File.WriteAllText (GetOutputFilename ("licenses.txt"), sb.ToString ()); } - public IValidationResult> ValidateLicenses (Dictionary projectPackages) { - if (_packageOptions.AllowedLicenseType.Count == 0) { + public IValidationResult> ValidateLicenses(Dictionary projectPackages) + { + if (_packageOptions.AllowedLicenseType.Count == 0) + { return new ValidationResult> { IsValid = true }; } - WriteOutput (() => $"Starting {nameof(ValidateLicenses)}...", logLevel : LogLevel.Verbose); + WriteOutput(() => $"Starting {nameof(ValidateLicenses)}...", logLevel: LogLevel.Verbose); var invalidPackages = projectPackages - .SelectMany (kvp => kvp.Value.Select (p => new KeyValuePair (kvp.Key, p.Value))) - .Where (p => !_packageOptions.AllowedLicenseType.Any (allowed => { - if (p.Value.Metadata.LicenseUrl is string licenseUrl) { - if (_licenseMappings.TryGetValue (licenseUrl, out var license)) { + .SelectMany(kvp => kvp.Value.Select(p => new KeyValuePair(kvp.Key, p.Value))) + .Where(p => !_packageOptions.AllowedLicenseType.Any(allowed => + { + if (p.Value.Metadata.LicenseUrl is string licenseUrl) + { + if (_licenseMappings.TryGetValue(licenseUrl, out var license)) + { return allowed == license; } - if (p.Value.Metadata.LicenseUrl?.Contains (allowed, StringComparison.OrdinalIgnoreCase) == true) { + if (p.Value.Metadata.LicenseUrl?.Contains(allowed, StringComparison.OrdinalIgnoreCase) == true) + { return true; } } - if (p.Value.Metadata.License.IsLicenseFile ()) { - var key = Tuple.Create (p.Value.Metadata.Id, p.Value.Metadata.Version); + if (p.Value.Metadata.License.IsLicenseFile()) + { + var key = Tuple.Create(p.Value.Metadata.Id, p.Value.Metadata.Version); - if (_licenseFileCache.TryGetValue (key, out var licenseText)) { - if (licenseText.Contains (allowed, StringComparison.OrdinalIgnoreCase)) { + if (_licenseFileCache.TryGetValue(key, out var licenseText)) + { + if (licenseText.Contains(allowed, StringComparison.OrdinalIgnoreCase)) + { p.Value.Metadata.License.Text = allowed; return true; } @@ -376,108 +383,130 @@ public IValidationResult> ValidateLicenses (Dictio return allowed == p.Value.Metadata.License?.Text; })) - .ToList (); + .ToList(); return new ValidationResult> { IsValid = invalidPackages.Count == 0, InvalidPackages = invalidPackages }; } - public ValidationResult ValidateLicenses (List projectPackages) { - if (_packageOptions.AllowedLicenseType.Count == 0) { + public ValidationResult ValidateLicenses(List projectPackages) + { + if (_packageOptions.AllowedLicenseType.Count == 0) + { return new ValidationResult { IsValid = true }; } - WriteOutput (() => $"Starting {nameof(ValidateLicenses)}...", logLevel : LogLevel.Verbose); + WriteOutput(() => $"Starting {nameof(ValidateLicenses)}...", logLevel: LogLevel.Verbose); var invalidPackages = projectPackages - .Where (p => !_packageOptions.AllowedLicenseType.Any (allowed => { - if (p.LicenseUrl is string licenseUrl) { - if (_licenseMappings.TryGetValue (licenseUrl, out var license)) { + .Where(p => !_packageOptions.AllowedLicenseType.Any(allowed => + { + if (p.LicenseUrl is string licenseUrl) + { + if (_licenseMappings.TryGetValue(licenseUrl, out var license)) + { return allowed == license; } - if (p.LicenseUrl?.Contains (allowed, StringComparison.OrdinalIgnoreCase) == true) { + if (p.LicenseUrl?.Contains(allowed, StringComparison.OrdinalIgnoreCase) == true) + { return true; } } return allowed == p.LicenseType; })) - .ToList (); + .ToList(); return new ValidationResult { IsValid = invalidPackages.Count == 0, InvalidPackages = invalidPackages }; } - private async Task GetNuGetPackageFileResult (string packageName, string versionNumber, string fileInPackage) - where T : class { - if (string.IsNullOrWhiteSpace (packageName) || string.IsNullOrWhiteSpace (versionNumber)) { return await Task.FromResult (null); } - var fallbackEndpoint = new Uri (string.Format (fallbackPackageUrl, packageName, versionNumber)); - WriteOutput (() => "Attempting to download: " + fallbackEndpoint.ToString (), logLevel : LogLevel.Verbose); - using (var packageRequest = new HttpRequestMessage (HttpMethod.Get, fallbackEndpoint)) - using (var packageResponse = await _httpClient.SendAsync (packageRequest, CancellationToken.None)) { - if (!packageResponse.IsSuccessStatusCode) { - WriteOutput ($"{packageRequest.RequestUri} failed due to {packageResponse.StatusCode}!", logLevel : LogLevel.Warning); + private async Task GetNuGetPackageFileResult(string packageName, string versionNumber, string fileInPackage) + where T : class + { + if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(versionNumber)) { return await Task.FromResult(null); } + var fallbackEndpoint = new Uri(string.Format(fallbackPackageUrl, packageName, versionNumber)); + WriteOutput(() => "Attempting to download: " + fallbackEndpoint.ToString(), logLevel: LogLevel.Verbose); + using (var packageRequest = new HttpRequestMessage(HttpMethod.Get, fallbackEndpoint)) + using (var packageResponse = await _httpClient.SendAsync(packageRequest, CancellationToken.None)) + { + if (!packageResponse.IsSuccessStatusCode) + { + WriteOutput($"{packageRequest.RequestUri} failed due to {packageResponse.StatusCode}!", logLevel: LogLevel.Warning); return null; } - using (var fileStream = new MemoryStream ()) { - await packageResponse.Content.CopyToAsync (fileStream); + using (var fileStream = new MemoryStream()) + { + await packageResponse.Content.CopyToAsync(fileStream); - using (var archive = new ZipArchive (fileStream, ZipArchiveMode.Read)) { - var entry = archive.GetEntry (fileInPackage); - if (entry is null) { - WriteOutput (() => $"{fileInPackage} was not found in NuGet Package: ", logLevel : LogLevel.Verbose); + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Read)) + { + var entry = archive.GetEntry(fileInPackage); + if (entry is null) + { + WriteOutput(() => $"{fileInPackage} was not found in NuGet Package: ", logLevel: LogLevel.Verbose); return null; } - WriteOutput (() => "Attempting to read: " + fileInPackage, logLevel : LogLevel.Verbose); - using (var entryStream = entry.Open ()) - using (var textReader = new StreamReader (entryStream)) { - var typeT = typeof (T); - if (typeT == typeof (Package)) { - if (_serializer.Deserialize (new NamespaceIgnorantXmlTextReader (textReader)) is T result) { - return (T) result; + WriteOutput(() => "Attempting to read: " + fileInPackage, logLevel: LogLevel.Verbose); + using (var entryStream = entry.Open()) + using (var textReader = new StreamReader(entryStream)) + { + var typeT = typeof(T); + if (typeT == typeof(Package)) + { + if (_serializer.Deserialize(new NamespaceIgnorantXmlTextReader(textReader)) is T result) + { + return (T)result; } - } else if (typeT == typeof (string)) { - return await textReader.ReadToEndAsync () as T; + } + else if (typeT == typeof(string)) + { + return await textReader.ReadToEndAsync() as T; } - throw new ArgumentException ($"{typeT.FullName} isn't supported!"); + throw new ArgumentException($"{typeT.FullName} isn't supported!"); } } } } } - private IEnumerable GetFilteredProjects (IEnumerable projects) { - if (_packageOptions.ProjectFilter.Count == 0) { + private IEnumerable GetFilteredProjects(IEnumerable projects) + { + if (_packageOptions.ProjectFilter.Count == 0) + { return projects; } - var filteredProjects = projects.Where (project => !_packageOptions.ProjectFilter - .Any (projectToSkip => - project.Contains (projectToSkip, StringComparison.OrdinalIgnoreCase) - )).ToList (); + var filteredProjects = projects.Where(project => !_packageOptions.ProjectFilter + .Any(projectToSkip => + project.Contains(projectToSkip, StringComparison.OrdinalIgnoreCase) + )).ToList(); - WriteOutput (() => "Filtered Project Files" + Environment.NewLine, logLevel : LogLevel.Verbose); - WriteOutput (() => string.Join (Environment.NewLine, filteredProjects.ToArray ()), logLevel : LogLevel.Verbose); + WriteOutput(() => "Filtered Project Files" + Environment.NewLine, logLevel: LogLevel.Verbose); + WriteOutput(() => string.Join(Environment.NewLine, filteredProjects.ToArray()), logLevel: LogLevel.Verbose); return filteredProjects; } - private async Task HandleLicensing (Package package) { + private async Task HandleLicensing(Package package) + { if (package?.Metadata is null) { return; } if (package.Metadata.LicenseUrl is string licenseUrl && - package.Metadata.License?.Text is null) { - if (_licenseMappings.TryGetValue (licenseUrl, out var mappedLicense)) { + package.Metadata.License?.Text is null) + { + if (_licenseMappings.TryGetValue(licenseUrl, out var mappedLicense)) + { package.Metadata.License = new License { Text = mappedLicense }; } } - if (!package.Metadata.License.IsLicenseFile () || _packageOptions.AllowedLicenseType.Count == 0) { return; } + if (!package.Metadata.License.IsLicenseFile() || _packageOptions.AllowedLicenseType.Count == 0) { return; } - var key = Tuple.Create (package.Metadata.Id, package.Metadata.Version); + var key = Tuple.Create(package.Metadata.Id, package.Metadata.Version); - if (_licenseFileCache.TryGetValue (key, out _)) { return; } + if (_licenseFileCache.TryGetValue(key, out _)) { return; } - _licenseFileCache[key] = await GetNuGetPackageFileResult (package.Metadata.Id, package.Metadata.Version, package.Metadata.License.Text); + _licenseFileCache[key] = await GetNuGetPackageFileResult(package.Metadata.Id, package.Metadata.Version, package.Metadata.License.Text); } private string GetOutputFilename (string defaultName) { @@ -488,7 +517,7 @@ private string GetOutputFilename (string defaultName) { Path.Combine (outputDir, _packageOptions.OutputFileName); } - private string GetExportDirectory () { + public string GetExportDirectory () { string outputDirectory = string.Empty; if (!string.IsNullOrWhiteSpace (_packageOptions.OutputDirectory)) { if (_packageOptions.OutputDirectory.EndsWith ('/')) { @@ -512,27 +541,29 @@ private string GetExportDirectory () { /// /// The Project Path /// - private IEnumerable GetProjectReferencesFromNewProjectFile (string projectPath) { - var projDefinition = XDocument.Load (projectPath); + private IEnumerable GetProjectReferencesFromNewProjectFile(string projectPath) + { + var projDefinition = XDocument.Load(projectPath); // Uses an XPath instead of direct navigation (using Elements("…")) as the project file may use xml namespaces - return projDefinition ? - .XPathSelectElements ("/*[local-name()='Project']/*[local-name()='ItemGroup']/*[local-name()='PackageReference']") ? - .Select (refElem => GetProjectReferenceFromElement (refElem)) ?? - Array.Empty (); + return projDefinition? + .XPathSelectElements("/*[local-name()='Project']/*[local-name()='ItemGroup']/*[local-name()='PackageReference']")? + .Select(refElem => GetProjectReferenceFromElement(refElem)) ?? + Array.Empty(); } - private string GetProjectReferenceFromElement (XElement refElem) { - string version, package = refElem.Attribute ("Include")?.Value ?? ""; + private string GetProjectReferenceFromElement(XElement refElem) + { + string version, package = refElem.Attribute("Include")?.Value ?? ""; - var versionAttribute = refElem.Attribute ("Version"); + var versionAttribute = refElem.Attribute("Version"); if (versionAttribute != null) version = versionAttribute.Value; else // no version attribute, look for child element - version = refElem.Elements () - .Where (elem => elem.Name.LocalName == "Version") - .FirstOrDefault ()?.Value ?? ""; + version = refElem.Elements() + .Where(elem => elem.Name.LocalName == "Version") + .FirstOrDefault()?.Value ?? ""; return $"{package},{version}"; } @@ -542,32 +573,36 @@ private string GetProjectReferenceFromElement (XElement refElem) { /// /// The Project Path /// - private IEnumerable GetProjectReferencesFromPackagesConfig (string projectPath) { - var dir = Path.GetDirectoryName (projectPath); - var packagesFile = Path.Join (dir, "packages.config"); - - if (File.Exists (packagesFile)) { - var packagesConfig = XDocument.Load (packagesFile); - - return packagesConfig ? - .Element ("packages") ? - .Elements ("package") ? - .Select (refElem => (refElem.Attribute ("id")?.Value ?? "") + "," + (refElem.Attribute ("version")?.Value ?? "")); + private IEnumerable GetProjectReferencesFromPackagesConfig(string projectPath) + { + var dir = Path.GetDirectoryName(projectPath); + var packagesFile = Path.Join(dir, "packages.config"); + + if (File.Exists(packagesFile)) + { + var packagesConfig = XDocument.Load(packagesFile); + + return packagesConfig? + .Element("packages")? + .Elements("package")? + .Select(refElem => (refElem.Attribute("id")?.Value ?? "") + "," + (refElem.Attribute("version")?.Value ?? "")); } - return Array.Empty (); + return Array.Empty(); } - private async Task> GetValidProjects (string projectPath) { - var pathInfo = new FileInfo (projectPath); - var extensions = GetProjectExtensions (); + private async Task> GetValidProjects(string projectPath) + { + var pathInfo = new FileInfo(projectPath); + var extensions = GetProjectExtensions(); IEnumerable validProjects; - switch (pathInfo.Extension) { + switch (pathInfo.Extension) + { case ".sln": - validProjects = (await ParseSolution (pathInfo.FullName)) - .Select (p => new FileInfo (Path.Combine (pathInfo.Directory.FullName, p))) - .Where (p => p.Exists && extensions.Contains (p.Extension)) - .Select (p => p.FullName); + validProjects = (await ParseSolution(pathInfo.FullName)) + .Select(p => new FileInfo(Path.Combine(pathInfo.Directory.FullName, p))) + .Where(p => p.Exists && extensions.Contains(p.Extension)) + .Select(p => p.FullName); break; case ".csproj": validProjects = new string[] { projectPath }; @@ -576,95 +611,194 @@ private async Task> GetValidProjects (string projectPath) { validProjects = new string[] { projectPath }; break; case ".json": - validProjects = ReadListFromFile (projectPath) - .Select (x => x.EnsureCorrectPathCharacter ()) - .ToList (); + validProjects = ReadListFromFile(projectPath) + .Select(x => x.EnsureCorrectPathCharacter()) + .ToList(); break; default: validProjects = - GetProjectExtensions (withWildcard: true) - .SelectMany (wildcardExtension => - Directory.EnumerateFiles (projectPath, wildcardExtension, SearchOption.AllDirectories) + GetProjectExtensions(withWildcard: true) + .SelectMany(wildcardExtension => + Directory.EnumerateFiles(projectPath, wildcardExtension, SearchOption.AllDirectories) ); break; } - WriteOutput (() => "Discovered Project Files" + Environment.NewLine, logLevel : LogLevel.Verbose); - WriteOutput (() => string.Join (Environment.NewLine, validProjects.ToArray ()), logLevel : LogLevel.Verbose); + WriteOutput(() => "Discovered Project Files" + Environment.NewLine, logLevel: LogLevel.Verbose); + WriteOutput(() => string.Join(Environment.NewLine, validProjects.ToArray()), logLevel: LogLevel.Verbose); - return GetFilteredProjects (validProjects); + return GetFilteredProjects(validProjects); } - private async Task> ParseSolution (string fullName) { - var solutionFile = new FileInfo (fullName); - if (!solutionFile.Exists) { throw new FileNotFoundException (fullName); } - var projectFiles = new List (250); - - using (var fileStream = solutionFile.OpenRead ()) - using (var streamReader = new StreamReader (fileStream)) { - while (await streamReader.ReadLineAsync () is string line) { - if (!line.StartsWith ("Project")) { continue; } - var segments = line.Split (','); + private async Task> ParseSolution(string fullName) + { + var solutionFile = new FileInfo(fullName); + if (!solutionFile.Exists) { throw new FileNotFoundException(fullName); } + var projectFiles = new List(250); + + using (var fileStream = solutionFile.OpenRead()) + using (var streamReader = new StreamReader(fileStream)) + { + while (await streamReader.ReadLineAsync() is string line) + { + if (!line.StartsWith("Project")) { continue; } + var segments = line.Split(','); if (segments.Length < 2) { continue; } - projectFiles.Add (segments[1].EnsureCorrectPathCharacter ().Trim ('"')); + projectFiles.Add(segments[1].EnsureCorrectPathCharacter().Trim('"')); } } return projectFiles; } - private void WriteOutput (Func line, Exception exception = null, LogLevel logLevel = LogLevel.Information) { - if ((int) logLevel < (int) _packageOptions.LogLevelThreshold) { - return; - } - Console.WriteLine (line.Invoke ()); + /// + /// Downloads the nuget package file and read the licence file + /// + /// + /// + /// + /// + public async Task GetLicenceFromNpkgFile(string package, string licenseFile, string version) + { + bool result = false; + var nupkgEndpoint = new Uri(string.Format(fallbackPackageUrl, package, version)); + WriteOutput(() => "Attempting to download: " + nupkgEndpoint.ToString(), logLevel: LogLevel.Verbose); + using (var packageRequest = new HttpRequestMessage(HttpMethod.Get, nupkgEndpoint)) + using (var packageResponse = await _httpClient.SendAsync(packageRequest, CancellationToken.None)) + { + + if (!packageResponse.IsSuccessStatusCode) + { + WriteOutput($"{packageRequest.RequestUri} failed due to {packageResponse.StatusCode}!", logLevel: LogLevel.Warning); + return false; + } + + var directory = GetExportDirectory(); + var outpath = Path.Combine(directory, package + "_" + version + ".nupkg.zip"); + + using (var fileStream = File.OpenWrite(outpath)) + { + try + { + await packageResponse.Content.CopyToAsync(fileStream); + } + catch (Exception) + { + return false; + } + + } + + using (ZipArchive archive = ZipFile.OpenRead(outpath)) + { + var sample = archive.GetEntry(licenseFile); + if (sample != null) + { + var t = sample.Open(); + if (t != null && t.CanRead) + { + var libTxt = outpath.Replace(".nupkg.zip", ".txt"); + using (var fileStream = File.OpenWrite(libTxt)) + { + try + { + await t.CopyToAsync(fileStream); + result = true; + } + catch (Exception) + { + return false; + } - if (exception is object) { - Console.WriteLine (exception.ToString ()); + } + } + } + } + + File.Delete(outpath); + return result; } } - private void WriteOutput (string line, Exception exception = null, LogLevel logLevel = LogLevel.Information) => WriteOutput (() => line, exception, logLevel); + /// + /// HandleMSFTLicenses handle deprecate MSFT nuget licenses + /// + /// List + /// A List of LibraryInfo + public List HandleDeprecateMSFTLicense(List libraries) + { + List result = libraries; + + foreach (var item in result) + { + if (item.LicenseUrl == deprecateNugetLicense) + { + item.LicenseUrl = string.Format("https://www.nuget.org/packages/{0}/{1}/License", item.PackageName, item.PackageVersion); + } + } + return result; + } public async Task ExportLicenseTexts (List infos) { var directory = GetExportDirectory (); foreach (var info in infos.Where (i => !string.IsNullOrEmpty (i.LicenseUrl))) { var source = info.LicenseUrl; - var outpath = Path.Combine (directory, info.PackageName + info.PackageVersion + ".txt"); - if (File.Exists (outpath)) { + var outpath = Path.Combine(directory, info.PackageName + "_" + info.PackageVersion + ".txt"); + if (File.Exists(outpath)) + { continue; } + if (source == deprecateNugetLicense) + { + if (await GetLicenceFromNpkgFile(info.PackageName, info.LicenseType, info.PackageVersion)) + continue; + } - // Correct some uris - if (source.StartsWith ("https://github.com", StringComparison.Ordinal) && source.Contains ("/blob/", StringComparison.Ordinal)) { - source = source.Replace ("/blob/", "/raw/", StringComparison.Ordinal); + if (source == "http://go.microsoft.com/fwlink/?LinkId=329770" || source == "https://dotnet.microsoft.com/en/dotnet_library_license.htm") + { + if (await GetLicenceFromNpkgFile(info.PackageName, "dotnet_library_license.txt", info.PackageVersion)) + continue; } - if (source.StartsWith ("https://github.com", StringComparison.Ordinal) && source.Contains ("/dotnet/corefx/", StringComparison.Ordinal)) { - source = source.Replace ("/dotnet/corefx/", "/dotnet/runtime/", StringComparison.Ordinal); + + if (source.StartsWith("https://licenses.nuget.org")) + { + if (await GetLicenceFromNpkgFile(info.PackageName, "License.txt", info.PackageVersion)) + continue; } - do { - WriteOutput (() => $"Attempting to download {source} to {outpath}", logLevel : LogLevel.Verbose); - using (var request = new HttpRequestMessage (HttpMethod.Get, source)) - using (var response = await _httpClient.SendAsync (request)) { - if (!response.IsSuccessStatusCode) { - WriteOutput ($"{request.RequestUri} failed due to {response.StatusCode}!", logLevel : LogLevel.Error); + do + { + WriteOutput(() => $"Attempting to download {source} to {outpath}", logLevel: LogLevel.Verbose); + using (var request = new HttpRequestMessage(HttpMethod.Get, source)) + using (var response = await _httpClient.SendAsync(request)) + { + if (!response.IsSuccessStatusCode) + { + WriteOutput($"{request.RequestUri} failed due to {response.StatusCode}!", logLevel: LogLevel.Error); break; } - // Somebody redirected us to github. Correct the uri and try again - var realRequestUri = response.RequestMessage.RequestUri.AbsoluteUri; - if ( - IsGithub (realRequestUri) && - !IsGithub (source)) { - WriteOutput (() => " Redirect detected", logLevel : LogLevel.Verbose); - source = CorrectUri (realRequestUri); + // Detect a redirect 302 + if (response.RequestMessage.RequestUri.AbsoluteUri != source) + { + WriteOutput(() => " Redirect detected", logLevel: LogLevel.Verbose); + source = response.RequestMessage.RequestUri.AbsoluteUri; continue; } - using (var fileStream = File.OpenWrite (outpath)) { - await response.Content.CopyToAsync (fileStream); + // Modify the URL if required + if (CorrectUri(source) != source) + { + WriteOutput(() => " Fixing URL", logLevel: LogLevel.Verbose); + source = CorrectUri(source); + continue; + } + + + using (var fileStream = File.OpenWrite(outpath)) + { + await response.Content.CopyToAsync(fileStream); } break; } @@ -672,23 +806,118 @@ public async Task ExportLicenseTexts (List infos) { } } - private bool IsGithub (string uri) { - return uri.StartsWith ("https://github.com", StringComparison.Ordinal); + private bool IsGithub(string uri) + { + return uri.StartsWith("https://github.com", StringComparison.Ordinal); } - private string CorrectUri (string uri) { - if (!IsGithub (uri)) { + /// + /// make the appropriate changes to the URI to get the raw text of the license. + /// + /// URI + /// Returns the raw URL to get the raw text of the library + private string CorrectUri(string uri) + { + if (!IsGithub(uri)) + { return uri; } - if (uri.Contains ("/blob/", StringComparison.Ordinal)) { - uri = uri.Replace ("/blob/", "/raw/", StringComparison.Ordinal); - } - if (uri.Contains ("/dotnet/corefx/", StringComparison.Ordinal)) { - uri = uri.Replace ("/dotnet/corefx/", "/dotnet/runtime/", StringComparison.Ordinal); + if (uri.Contains("/blob/", StringComparison.Ordinal)) + { + uri = uri.Replace("/blob/", "/raw/", StringComparison.Ordinal); } + /* if (uri.Contains("/dotnet/corefx/", StringComparison.Ordinal)) + { + uri = uri.Replace("/dotnet/corefx/", "/dotnet/runtime/", StringComparison.Ordinal); + }*/ + return uri; } + + public void PrintLicenses(List libraries) + { + if (libraries is null) { throw new ArgumentNullException(nameof(libraries)); } + if (!libraries.Any()) { return; } + + WriteOutput(Environment.NewLine + "References:", logLevel: LogLevel.Always); + WriteOutput(libraries.ToStringTable(new[] { "Reference", "Version", "License Type", "License" }, + a => a.PackageName ?? "---", + a => a.PackageVersion ?? "---", + a => a.LicenseType ?? "---", + a => a.LicenseUrl ?? "---"), logLevel: LogLevel.Always); + } + + public void SaveAsJson(List libraries) + { + if (!libraries.Any() || !_packageOptions.JsonOutput) { return; } + JsonSerializerSettings jsonSettings = new JsonSerializerSettings + { + NullValueHandling = _packageOptions.IncludeProjectFile ? NullValueHandling.Include : NullValueHandling.Ignore + }; + + using (var fileStream = new FileStream(GetOutputFilename("licenses.json"), FileMode.Create)) + using (var streamWriter = new StreamWriter(fileStream)) + { + streamWriter.Write(JsonConvert.SerializeObject(libraries, jsonSettings)); + streamWriter.Flush(); + } + } + + public void SaveAsTextFile(List libraries) + { + if (!libraries.Any() || !_packageOptions.TextOutput) { return; } + StringBuilder sb = new StringBuilder(256); + foreach (var lib in libraries) + { + sb.Append(new string('#', 100)); + sb.AppendLine(); + sb.Append("Package:"); + sb.Append(lib.PackageName); + sb.AppendLine(); + sb.Append("Version:"); + sb.Append(lib.PackageVersion); + sb.AppendLine(); + sb.Append("project URL:"); + sb.Append(lib.PackageUrl); + sb.AppendLine(); + sb.Append("Description:"); + sb.Append(lib.Description); + sb.AppendLine(); + sb.Append("licenseUrl:"); + sb.Append(lib.LicenseUrl); + sb.AppendLine(); + sb.Append("license Type:"); + sb.Append(lib.LicenseType); + sb.AppendLine(); + if (_packageOptions.IncludeProjectFile) + { + sb.Append("Project:"); + sb.Append(lib.Projects); + sb.AppendLine(); + } + sb.AppendLine(); + } + + File.WriteAllText(GetOutputFilename("licenses.txt"), sb.ToString()); + } + + private void WriteOutput(Func line, Exception exception = null, LogLevel logLevel = LogLevel.Information) + { + if ((int)logLevel < (int)_packageOptions.LogLevelThreshold) + { + return; + } + + Console.WriteLine(line.Invoke()); + + if (exception is object) + { + Console.WriteLine(exception.ToString()); + } + } + + private void WriteOutput(string line, Exception exception = null, LogLevel logLevel = LogLevel.Information) => WriteOutput(() => line, exception, logLevel); } } \ No newline at end of file diff --git a/src/NugetUtility.csproj b/src/NugetUtility.csproj index df873cd6..201ecac9 100644 --- a/src/NugetUtility.csproj +++ b/src/NugetUtility.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ git dotnet-project-licenses dotnet-project-licenses - 2.2.8 + 2.2.10 Tom Chavakis - .NET Core Tool to print a list of the licenses of a projects @@ -20,6 +20,7 @@ + diff --git a/src/PackageOptions.cs b/src/PackageOptions.cs index fd9ee403..cfc53a86 100644 --- a/src/PackageOptions.cs +++ b/src/PackageOptions.cs @@ -7,121 +7,142 @@ using CommandLine.Text; using static NugetUtility.Utilties; -namespace NugetUtility { - public class PackageOptions { - private readonly Regex UserRegexRegex = new Regex ("^\\/(.+)\\/$"); - - private ICollection _allowedLicenseTypes = new Collection (); - private ICollection _manualInformation = new Collection (); - private ICollection _projectFilter = new Collection (); - private ICollection _packagesFilter = new Collection (); - private Dictionary _customLicenseToUrlMappings = new Dictionary (); - - [Option ("allowed-license-types", Default = null, HelpText = "Simple json file of a text array of allowable licenses, if no file is given, all are assumed allowed")] +namespace NugetUtility +{ + public class PackageOptions + { + private readonly Regex UserRegexRegex = new Regex("^\\/(.+)\\/$"); + + private ICollection _allowedLicenseTypes = new Collection(); + private ICollection _manualInformation = new Collection(); + private ICollection _projectFilter = new Collection(); + private ICollection _packagesFilter = new Collection(); + private Dictionary _customLicenseToUrlMappings = new Dictionary(); + + [Option("allowed-license-types", Default = null, HelpText = "Simple json file of a text array of allowable licenses, if no file is given, all are assumed allowed")] public string AllowedLicenseTypesOption { get; set; } - [Option ('j', "json", Default = false, HelpText = "Saves licenses list in a json file (licenses.json)")] - public bool JsonOutput { get; set; } - - [Option ("include-project-file", Default = false, HelpText = "Adds project file path to information when enabled.")] + [Option("include-project-file", Default = false, HelpText = "Adds project file path to information when enabled.")] public bool IncludeProjectFile { get; set; } - [Option ('l', "log-level", Default = LogLevel.Error, HelpText = "Sets log level for output display. Options: Error|Warning|Information|Verbose.")] + [Option('l', "log-level", Default = LogLevel.Error, HelpText = "Sets log level for output display. Options: Error|Warning|Information|Verbose.")] public LogLevel LogLevelThreshold { get; set; } - [Option ("manual-package-information", Default = null, HelpText = "Simple json file of an array of LibraryInfo objects for manually determined packages.")] + [Option("manual-package-information", Default = null, HelpText = "Simple json file of an array of LibraryInfo objects for manually determined packages.")] public string ManualInformationOption { get; set; } - [Option ("licenseurl-to-license-mappings", Default = null, HelpText = "Simple json file of Dictinary to override default mappings")] + [Option("licenseurl-to-license-mappings", Default = null, HelpText = "Simple json file of Dictinary to override default mappings")] public string LicenseToUrlMappingsOption { get; set; } - [Option ('o', "output", Default = false, HelpText = "Saves as text file (licenses.txt)")] + [Option('o', "output", Default = false, HelpText = "Saves as text file (licenses.txt)")] public bool TextOutput { get; set; } - [Option ("outfile", Default = null, HelpText = "Output filename")] + [Option("outfile", Default = null, HelpText = "Output filename")] public string OutputFileName { get; set; } - [Option ('f', "output-directory", Default = null, HelpText = "Output Directory")] + [Option('f', "output-directory", Default = null, HelpText = "Output Directory")] public string OutputDirectory { get; set; } - [Option ('i', "input", HelpText = "The projects in which to search for used nuget packages. This can either be a folder, a project file, a solution file or a json file containing a list of projects.")] + [Option('i', "input", HelpText = "The projects in which to search for used nuget packages. This can either be a folder, a project file, a solution file or a json file containing a list of projects.")] public string ProjectDirectory { get; set; } - [Option ("projects-filter", Default = null, HelpText = "Simple json file of a text array of projects to skip. Supports Ends with matching such as 'Tests.csproj'")] + [Option("projects-filter", Default = null, HelpText = "Simple json file of a text array of projects to skip. Supports Ends with matching such as 'Tests.csproj'")] public string ProjectsFilterOption { get; set; } - [Option ("packages-filter", Default = null, HelpText = "Simple json file of a text array of packages to skip, or a regular expression defined between two forward slashes.")] + [Option("packages-filter", Default = null, HelpText = "Simple json file of a text array of packages to skip, or a regular expression defined between two forward slashes.")] public string PackagesFilterOption { get; set; } - [Option ('u', "unique", Default = false, HelpText = "Unique licenses list by Id/Version")] + [Option('u', "unique", Default = false, HelpText = "Unique licenses list by Id/Version")] public bool UniqueOnly { get; set; } - [Option ('p', "print", Default = true, HelpText = "Print licenses.")] + [Option('p', "print", Default = true, HelpText = "Print licenses.")] public bool? Print { get; set; } - [Option ("export-license-texts", Default = false, HelpText = "Exports the raw license texts")] + [Option('j', "json", Default = false, HelpText = "Saves licenses list in a json file (licenses.json)")] + public bool JsonOutput { get; set; } + + [Option('e', "export-license-texts", Default = false, HelpText = "Exports the raw license texts")] public bool ExportLicenseTexts { get; set; } - [Option ("include-transitive", Default = false, HelpText = "Include distinct transitive package licenses per project file.")] + [Option('t', "include-transitive", Default = false, HelpText = "Include distinct transitive package licenses per project file.")] public bool IncludeTransitive { get; set; } - [Usage (ApplicationAlias = "dotnet-project-licenses")] - public static IEnumerable Examples { - get { - return new List () { + [Usage(ApplicationAlias = "dotnet-project-licenses")] + public static IEnumerable Examples + { + get + { + return new List() { new Example ("Simple", new PackageOptions { ProjectDirectory = "~/Projects/test-project" }), new Example ("VS Solution", new PackageOptions { ProjectDirectory = "~/Projects/test-project/project.sln" }), new Example ("Unique VS Solution to Custom JSON File", new PackageOptions { - ProjectDirectory = "~/Projects/test-project/project.sln", - UniqueOnly = true, - JsonOutput = true, - OutputFileName = @"~/Projects/another-folder/licenses.json" + ProjectDirectory = "~/Projects/test-project/project.sln", + UniqueOnly = true, + JsonOutput = true, + OutputFileName = @"~/Projects/another-folder/licenses.json" + }), + new Example("Export all license texts in a specific directory with verbose log", new PackageOptions + { + LogLevelThreshold = LogLevel.Verbose, + OutputDirectory = "~/Projects/exports", + ExportLicenseTexts = true, }), }; } } - public ICollection AllowedLicenseType { - get { - if (_allowedLicenseTypes.Any ()) { return _allowedLicenseTypes; } + public ICollection AllowedLicenseType + { + get + { + if (_allowedLicenseTypes.Any()) { return _allowedLicenseTypes; } - return _allowedLicenseTypes = ReadListFromFile (AllowedLicenseTypesOption); + return _allowedLicenseTypes = ReadListFromFile(AllowedLicenseTypesOption); } } - public ICollection ManualInformation { - get { - if (_manualInformation.Any ()) { return _manualInformation; } + public ICollection ManualInformation + { + get + { + if (_manualInformation.Any()) { return _manualInformation; } - return _manualInformation = ReadListFromFile (ManualInformationOption); + return _manualInformation = ReadListFromFile(ManualInformationOption); } } - public ICollection ProjectFilter { - get { - if (_projectFilter.Any ()) { return _projectFilter; } + public ICollection ProjectFilter + { + get + { + if (_projectFilter.Any()) { return _projectFilter; } - return _projectFilter = ReadListFromFile (ProjectsFilterOption) - .Select (x => x.EnsureCorrectPathCharacter ()) - .ToList (); + return _projectFilter = ReadListFromFile(ProjectsFilterOption) + .Select(x => x.EnsureCorrectPathCharacter()) + .ToList(); } } - public Regex? PackageRegex { - get { + public Regex? PackageRegex + { + get + { if (PackagesFilterOption == null) return null; // Check if the input is a regular expression that is defined between two forward slashes '/'; - if (UserRegexRegex.IsMatch (PackagesFilterOption)) { - var userRegexString = UserRegexRegex.Replace (PackagesFilterOption, "$1"); + if (UserRegexRegex.IsMatch(PackagesFilterOption)) + { + var userRegexString = UserRegexRegex.Replace(PackagesFilterOption, "$1"); // Try parse regular expression between forward slashes - try { - var parsedExpression = new Regex (userRegexString, RegexOptions.IgnoreCase); + try + { + var parsedExpression = new Regex(userRegexString, RegexOptions.IgnoreCase); return parsedExpression; } // Catch and suppress Argument exception thrown when pattern is invalid - catch (ArgumentException e) { - throw new ArgumentException ($"Cannot parse regex '{userRegexString}'", e); + catch (ArgumentException e) + { + throw new ArgumentException($"Cannot parse regex '{userRegexString}'", e); } } @@ -129,24 +150,29 @@ public Regex? PackageRegex { } } - public ICollection PackageFilter { - get { + public ICollection PackageFilter + { + get + { // If we've already found package filters, or the user input is a regular expression, // Return the packagesFilter - if (_packagesFilter.Any () || - (PackagesFilterOption != null && UserRegexRegex.IsMatch (PackagesFilterOption))) { + if (_packagesFilter.Any() || + (PackagesFilterOption != null && UserRegexRegex.IsMatch(PackagesFilterOption))) + { return _packagesFilter; } - return _packagesFilter = ReadListFromFile (PackagesFilterOption); + return _packagesFilter = ReadListFromFile(PackagesFilterOption); } } - public IReadOnlyDictionary LicenseToUrlMappingsDictionary { - get { - if (_customLicenseToUrlMappings.Any ()) { return _customLicenseToUrlMappings; } + public IReadOnlyDictionary LicenseToUrlMappingsDictionary + { + get + { + if (_customLicenseToUrlMappings.Any()) { return _customLicenseToUrlMappings; } - return _customLicenseToUrlMappings = ReadDictionaryFromFile (LicenseToUrlMappingsOption, LicenseToUrlMappings.Default); + return _customLicenseToUrlMappings = ReadDictionaryFromFile(LicenseToUrlMappingsOption, LicenseToUrlMappings.Default); } } } diff --git a/src/Program.cs b/src/Program.cs index 23ca99ea..8b66efbd 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -37,6 +37,8 @@ private static async Task Execute(PackageOptions options) await methods.ExportLicenseTexts(mappedLibraryInfo); } + mappedLibraryInfo = methods.HandleDeprecateMSFTLicense(mappedLibraryInfo); + if (options.Print == true) { Console.WriteLine(); @@ -62,6 +64,8 @@ private static async Task Execute(PackageOptions options) } } + + private static void HandleInvalidLicenses(Methods methods, List libraries, ICollection allowedLicenseType) { var invalidPackages = methods.ValidateLicenses(libraries); diff --git a/tests/NugetUtility.Tests/MethodsTests.cs b/tests/NugetUtility.Tests/MethodsTests.cs index b1a096b7..e4b519a2 100644 --- a/tests/NugetUtility.Tests/MethodsTests.cs +++ b/tests/NugetUtility.Tests/MethodsTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -166,7 +167,7 @@ public async Task GetPackages_AllowedLicenses_Should_Throw_On_MIT () { result.Should ().HaveCount (1); validationResult.IsValid.Should ().BeFalse (); - validationResult.InvalidPackages.Count.Should ().Be (2); + validationResult.InvalidPackages.Count.Should ().Be (3); } [Test] @@ -181,7 +182,46 @@ public async Task GetPackages_InputJson_Should_OnlyParseGivenProjects () { result.Should ().HaveCount (1); validationResult.IsValid.Should ().BeFalse (); - validationResult.InvalidPackages.Count.Should ().Be (2); + validationResult.InvalidPackages.Count.Should ().Be (3); + } + + + [TestCase("BenchmarkDotNet", "0.12.1", "https://licenses.nuget.org/MIT", "MIT")] + [TestCase("BCrypt.Net-Next", "2.1.3", "https://github.com/BcryptNet/bcrypt.net/blob/master/licence.txt", "")] + [TestCase("System.Memory", "4.5.4", "https://github.com/dotnet/corefx/blob/master/LICENSE.TXT", "MIT")] + [TestCase("System.Text.RegularExpressions", "4.3.0", "http://go.microsoft.com/fwlink/?LinkId=329770", "MS-EULA")] + [Test] + public async Task ExportLicenseTexts_Should_Export_File(string packageName, string packageVersion, string licenseUrl, string licenseType) + { + var methods = new Methods(new PackageOptions + { + ExportLicenseTexts = true, + }); + List infos = new List(); + infos.Add(new LibraryInfo() + { + PackageName = packageName, + PackageVersion = packageVersion, + LicenseUrl = licenseUrl, + LicenseType = licenseType, + }); + await methods.ExportLicenseTexts(infos); + var directory = methods.GetExportDirectory(); + var outpath = Path.Combine(directory, packageName + "_" + packageVersion + ".txt"); + Assert.That(File.Exists(outpath)); + } + + [TestCase("BenchmarkDotNet", "License.txt", "10.12.1")] + [Test] + public async Task GetLicenceFromNpkgFile_Should_Return_False(string packageName, string licenseFile, string packageVersion) + { + var methods = new Methods(new PackageOptions + { + ExportLicenseTexts = true, + }); + + var result = await methods.GetLicenceFromNpkgFile(packageName, licenseFile, packageVersion); + Assert.False(result); } } } \ No newline at end of file