Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ internal static List<UvPackage> ParsePackagesFromModel(object? model)
uvPackage.Source = source;
}

if (pkgTable.TryGetValue("sdist", out var sdistObj) && sdistObj is TomlTable sdistTable)
{
uvPackage.DownloadUrl = TryGetUrlFromArtifactTable(sdistTable);
}

if (uvPackage.DownloadUrl == null &&
pkgTable.TryGetValue("wheels", out var wheelsObj) && wheelsObj is TomlArray wheelsArray)
{
uvPackage.DownloadUrl = TryGetFirstWheelUrl(wheelsArray);
}

return uvPackage;
}

Expand Down Expand Up @@ -143,4 +154,26 @@ internal static void ParseMetadata(TomlTable? metadataTable, UvPackage uvPackage
}
}
}

private static string? TryGetUrlFromArtifactTable(TomlTable artifactTable)
{
return artifactTable.TryGetValue("url", out var urlObj) && urlObj is string url ? url : null;
}
Comment on lines +158 to +161

private static string? TryGetFirstWheelUrl(TomlArray wheelsArray)
{
foreach (var wheelObj in wheelsArray)
{
if (wheelObj is TomlTable wheelTable)
{
var url = TryGetUrlFromArtifactTable(wheelTable);
if (!string.IsNullOrWhiteSpace(url))
{
return url;
}
}
}

return null;
}
}
11 changes: 10 additions & 1 deletion src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ internal class UvPackage
// Source property for uv.lock
public UvSource? Source { get; set; }

// Preferred artifact URL (sdist first, then wheel fallback) for provenance.
public string? DownloadUrl { get; set; }

public TypedComponent ToTypedComponent()
{
if (this.Source?.Git != null)
Expand All @@ -29,7 +32,13 @@ public TypedComponent ToTypedComponent()
return new GitComponent(repoUrl, commitHash);
}

return new PipComponent(this.Name, this.Version);
var component = new PipComponent(this.Name, this.Version);
if (Uri.TryCreate(this.DownloadUrl, UriKind.Absolute, out var downloadUri))
{
component.DownloadUrl = downloadUri;
}

return component;
Comment on lines +35 to +41
}

private static (Uri RepositoryUrl, string CommitHash) ParseGitUrl(string gitUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,4 +611,37 @@ public async Task TestUvLockDetector_RecursiveDependency_DoesNotHangOrThrowAsync
graph.GetDependenciesForComponent(aId).Should().BeEquivalentTo([bId]);
graph.GetDependenciesForComponent(bId).Should().BeEquivalentTo([aId]);
}

[TestMethod]
public async Task TestUvLockDetector_PipPackage_UsesSdistDownloadUrlAsync()
{
var uvLock = @"[[package]]
name = 'myproject'
version = '0.1.0'
source = { virtual = '.' }
dependencies = [
{ name = 'requests' },
]
[package.metadata]
requires-dist = [
{ name = 'requests' },
]
[[package]]
name = 'requests'
version = '2.32.0'
source = { registry = 'https://pypi.org/simple' }
sdist = { url = 'https://files.pythonhosted.org/packages/source/r/requests/requests-2.32.0.tar.gz' }
";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("uv.lock", uvLock)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
var detectedComponent = componentRecorder.GetDetectedComponents().Single().Component;
detectedComponent.Should().BeOfType<PipComponent>();

var pipComponent = (PipComponent)detectedComponent;
pipComponent.DownloadUrl.Should().Be(new Uri("https://files.pythonhosted.org/packages/source/r/requests/requests-2.32.0.tar.gz"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -392,4 +392,39 @@ public void ParsePackage_ParsesSource_Missing()
var pkg = uvLock.Packages.First();
pkg.Source.Should().BeNull();
}

[TestMethod]
public void ParsePackage_ParsesDownloadUrl_FromSdist()
{
var toml = """
[[package]]
name = 'foo'
version = '1.0.0'
sdist = { url = 'https://files.example.com/foo-1.0.0.tar.gz', hash = 'sha256:abc' }
""";

using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml));
var uvLock = UvLock.Parse(ms);
uvLock.Packages.Should().ContainSingle();
uvLock.Packages.First().DownloadUrl.Should().Be("https://files.example.com/foo-1.0.0.tar.gz");
}

[TestMethod]
public void ParsePackage_ParsesDownloadUrl_FromWheels_WhenSdistMissing()
{
var toml = """
[[package]]
name = 'foo'
version = '1.0.0'
wheels = [
{ hash = 'sha256:missing-url' },
{ url = 'https://files.example.com/foo-1.0.0-py3-none-any.whl', hash = 'sha256:def' },
]
""";

using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml));
var uvLock = UvLock.Parse(ms);
uvLock.Packages.Should().ContainSingle();
uvLock.Packages.First().DownloadUrl.Should().Be("https://files.example.com/foo-1.0.0-py3-none-any.whl");
}
}
Loading