Skip to content

Commit fbe7db6

Browse files
committed
Add support for NuGet license files
Signed-off-by: Marius Thesing <marius.thesing@gmail.com>
1 parent 52d96ab commit fbe7db6

2 files changed

Lines changed: 141 additions & 9 deletions

File tree

CycloneDX.Tests/NugetV3ServiceTests.cs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,42 @@ public async Task GetComponent_GitHubLicenseLookup_FromProjectUrl_ReturnsCompone
421421
Assert.Equal("LicenseId", component.Licenses.First().License.Id);
422422
}
423423

424+
[Fact]
425+
public async Task GetComponent_GitHubLicenseLookup_FallsBackToLicenseFile()
426+
{
427+
var nuspecFileContents = @"<?xml version=""1.0"" encoding=""utf-8""?>
428+
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
429+
<metadata>
430+
<id>testpackage</id>
431+
<license type=""file"">subdir/LICENSE.MD</license>
432+
</metadata>
433+
</package>";
434+
435+
byte[] licenseContents = Encoding.UTF8.GetBytes("The license");
436+
437+
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
438+
{
439+
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) },
440+
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\subdir\LICENSE.MD"), new MockFileData(licenseContents) },
441+
});
442+
443+
var mockGitHubService = new Mock<IGithubService>();
444+
445+
var nugetService = new NugetV3Service(null,
446+
mockFileSystem,
447+
new List<string> { XFS.Path(@"c:\nugetcache") },
448+
mockGitHubService.Object,
449+
new NullLogger(), false);
450+
451+
var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(true);
452+
453+
Assert.Single(component.Licenses);
454+
Assert.Equal("testpackage License", component.Licenses.First().License.Name);
455+
Assert.Equal(Convert.ToBase64String(licenseContents), component.Licenses.First().License.Text.Content);
456+
Assert.Equal("base64", component.Licenses.First().License.Text.Encoding);
457+
Assert.Equal("text/markdown", component.Licenses.First().License.Text.ContentType);
458+
}
459+
424460
[Fact]
425461
public async Task GetComponent_GitHubLicenseLookup_FromRepository_WhenLicenseInvalid_ReturnsComponent()
426462
{
@@ -578,7 +614,7 @@ public async Task GetComponent_GitHubLicenseLookup_MaliciousCommitWithFragment_D
578614
}
579615

580616
[Fact]
581-
public async Task GetComponent_WhenGitHubServiceIsNull_UsesLicenseUrl()
617+
public async Task GetComponent_WhenGitHubServiceIsNullAndHasNoLicenseFile_UsesLicenseUrl()
582618
{
583619
var nuspecFileContents = @"<?xml version=""1.0"" encoding=""utf-8""?>
584620
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
@@ -605,5 +641,40 @@ public async Task GetComponent_WhenGitHubServiceIsNull_UsesLicenseUrl()
605641
Assert.Equal("https://not-licence.url", component.Licenses.First().License.Url);
606642
Assert.Equal("Unknown - See URL", component.Licenses.First().License.Name);
607643
}
644+
645+
[Fact]
646+
public async Task GetComponent_WhenGitHubServiceIsNull_UsesLicenseFile()
647+
{
648+
var nuspecFileContents = @"<?xml version=""1.0"" encoding=""utf-8""?>
649+
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
650+
<metadata>
651+
<id>testpackage</id>
652+
<license type=""file"">subdir/LICENSE.MD</license>
653+
<repository url=""https://licence.url"" />
654+
</metadata>
655+
</package>";
656+
657+
byte[] licenseContents = Encoding.UTF8.GetBytes("The license");
658+
659+
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
660+
{
661+
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) },
662+
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\subdir\LICENSE.MD"), new MockFileData(licenseContents) },
663+
});
664+
665+
var nugetService = new NugetV3Service(null,
666+
mockFileSystem,
667+
new List<string> { XFS.Path(@"c:\nugetcache") },
668+
null,
669+
new NullLogger(), false);
670+
671+
var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(true);
672+
673+
Assert.Single(component.Licenses);
674+
Assert.Equal("testpackage License", component.Licenses.First().License.Name);
675+
Assert.Equal(Convert.ToBase64String(licenseContents), component.Licenses.First().License.Text.Content);
676+
Assert.Equal("base64", component.Licenses.First().License.Text.Encoding);
677+
Assert.Equal("text/markdown", component.Licenses.First().License.Text.ContentType);
678+
}
608679
}
609680
}

CycloneDX/Services/NugetV3Service.cs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
using NuGet.Common;
3030
using NuGet.Configuration;
3131
using NuGet.Packaging;
32+
using NuGet.Packaging.Core;
3233
using NuGet.Packaging.Licenses;
3334
using NuGet.Protocol;
3435
using NuGet.Protocol.Core.Types;
@@ -77,25 +78,28 @@ bool disableHashComputation
7778
}
7879

7980
internal string GetCachedNuspecFilename(string name, string version)
81+
{
82+
return GetCachedNupkgFilename(name, version, name.ToLowerInvariant() + _nuspecExtension);
83+
}
84+
85+
internal string GetCachedNupkgFilename(string name, string version, string fileName)
8086
{
8187
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) { return null; }
8288

8389
var lowerName = name.ToLowerInvariant();
8490
var lowerVersion = version.ToLowerInvariant();
85-
string nuspecFilename = null;
8691

8792
foreach (var packageCachePath in _packageCachePaths)
8893
{
8994
var currentDirectory = _fileSystem.Path.Combine(packageCachePath, lowerName, NormalizeVersion(lowerVersion));
90-
var currentFilename = _fileSystem.Path.Combine(currentDirectory, lowerName + _nuspecExtension);
95+
var currentFilename = _fileSystem.Path.Combine(currentDirectory, fileName);
9196
if (_fileSystem.File.Exists(currentFilename))
9297
{
93-
nuspecFilename = currentFilename;
94-
break;
98+
return currentFilename;
9599
}
96100
}
97101

98-
return nuspecFilename;
102+
return null;
99103
}
100104

101105
/// <summary>
@@ -272,9 +276,19 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp
272276
}
273277
else if (_githubService == null)
274278
{
275-
var licenseUrl = nuspecModel.nuspecReader.GetLicenseUrl();
276-
var license = new License { Name = "Unknown - See URL", Url = licenseUrl?.Trim() };
277-
component.Licenses = new List<LicenseChoice> { new LicenseChoice { License = license } };
279+
License license = await TryGetLicenseFileAsync(licenseMetadata, name, version).ConfigureAwait(false);
280+
281+
if (license == null)
282+
{
283+
var licenseUrl = nuspecModel.nuspecReader.GetLicenseUrl();
284+
license = new License { Name = "Unknown - See URL", Url = licenseUrl?.Trim() };
285+
}
286+
287+
if (license != null)
288+
{
289+
component.Licenses ??= new List<LicenseChoice>();
290+
component.Licenses.Add(new LicenseChoice { License = license });
291+
}
278292
}
279293
else
280294
{
@@ -310,6 +324,11 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp
310324
}
311325
}
312326

327+
if (license == null)
328+
{
329+
license = await TryGetLicenseFileAsync(licenseMetadata, name, version).ConfigureAwait(false);
330+
}
331+
313332
if (license != null)
314333
{
315334
component.Licenses = new List<LicenseChoice> { new LicenseChoice { License = license } };
@@ -350,6 +369,48 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp
350369
return component;
351370
}
352371

372+
private async Task<License> TryGetLicenseFileAsync(LicenseMetadata licenseMetadata, string name, string version)
373+
{
374+
if (licenseMetadata == null || licenseMetadata.Type != LicenseType.File || string.IsNullOrEmpty(licenseMetadata.License))
375+
{
376+
return null;
377+
}
378+
379+
string licensePath = GetCachedNupkgFilename(name, version, licenseMetadata.License);
380+
381+
if (licensePath == null)
382+
{
383+
return null;
384+
}
385+
386+
if (!_fileSystem.File.Exists(licensePath))
387+
{
388+
return null;
389+
}
390+
391+
string extension = _fileSystem.Path.GetExtension(licensePath)?.ToLowerInvariant();
392+
393+
string contentType = extension switch
394+
{
395+
".md" => "text/markdown",
396+
".txt" or "" or null => "text/plain",
397+
_ => "appliation/octet-stream",
398+
};
399+
400+
byte[] licenseContent = await _fileSystem.File.ReadAllBytesAsync(licensePath).ConfigureAwait(false);
401+
402+
return new License
403+
{
404+
Name = $"{name} License",
405+
Text = new AttachedText
406+
{
407+
ContentType = contentType,
408+
Encoding = "base64",
409+
Content = Convert.ToBase64String(licenseContent),
410+
}
411+
};
412+
}
413+
353414
private static Component SetupComponentProperties(Component component, NuspecModel nuspecModel)
354415
{
355416
component.Authors = new List<OrganizationalContact> { new OrganizationalContact { Name = nuspecModel.nuspecReader.GetAuthors() } };

0 commit comments

Comments
 (0)