Skip to content

Commit 4e20b50

Browse files
authored
Populate Version.Integrity for pub, julia and nuget (#23)
pub.dev returns archive_sha256 on each version entry, julia's Versions.toml already gives us git-tree-sha1, and nuget's catalog leaf carries packageHash/packageHashAlgorithm (sha512, base64). The nuget hash is not in the registration index so this fetches the catalog leaf per version, same pattern hex already uses. deno is left out: the apiland /v2/modules endpoint only returns bare version strings with no per-version detail endpoint, so there is no checksum to read without changing data source. Related to ecosyste-ms/packages#1630.
1 parent b5d3b76 commit 4e20b50

6 files changed

Lines changed: 90 additions & 13 deletions

File tree

internal/julia/julia.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,13 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi
148148
versions := make([]core.Version, 0, len(versionNumbers))
149149
for _, v := range versionNumbers {
150150
info := versionMap[v]
151+
var integrity string
152+
if info.gitTreeSha1 != "" {
153+
integrity = "sha1-" + info.gitTreeSha1
154+
}
151155
versions = append(versions, core.Version{
152-
Number: v,
156+
Number: v,
157+
Integrity: integrity,
153158
Metadata: map[string]any{
154159
"git-tree-sha1": info.gitTreeSha1,
155160
},

internal/julia/julia_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ func TestFetchVersions(t *testing.T) {
8989
if versions[0].Metadata["git-tree-sha1"] != "3043b8e5c7c7f4b6f6f5e3b4b4c5d6e7f8a9b0c1" {
9090
t.Errorf("unexpected git-tree-sha1: %v", versions[0].Metadata["git-tree-sha1"])
9191
}
92+
if versions[0].Integrity != "sha1-3043b8e5c7c7f4b6f6f5e3b4b4c5d6e7f8a9b0c1" {
93+
t.Errorf("unexpected integrity: %q", versions[0].Integrity)
94+
}
9295
}
9396

9497
func TestFetchDependencies(t *testing.T) {

internal/nuget/nuget.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type registrationLeaf struct {
6161
}
6262

6363
type catalogEntry struct {
64+
CatalogURL string `json:"@id"`
6465
ID string `json:"id"`
6566
Version string `json:"version"`
6667
Description string `json:"description"`
@@ -92,6 +93,11 @@ type dependency struct {
9293
Range string `json:"range"`
9394
}
9495

96+
type catalogLeaf struct {
97+
PackageHash string `json:"packageHash"`
98+
PackageHashAlgorithm string `json:"packageHashAlgorithm"`
99+
}
100+
95101
func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package, error) {
96102
// NuGet IDs are case-insensitive, lowercase for URL
97103
lowerName := strings.ToLower(name)
@@ -191,6 +197,7 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi
191197
Number: entry.Version,
192198
PublishedAt: publishedAt,
193199
Licenses: licenses,
200+
Integrity: r.fetchIntegrity(ctx, entry.CatalogURL),
194201
Status: status,
195202
Metadata: map[string]any{
196203
"listed": entry.Listed,
@@ -203,6 +210,27 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi
203210
return versions, nil
204211
}
205212

213+
// fetchIntegrity fetches the catalog leaf to extract the package hash.
214+
// The registration index does not include packageHash, only the full
215+
// catalog leaf does. Errors are swallowed since integrity is best-effort.
216+
func (r *Registry) fetchIntegrity(ctx context.Context, catalogURL string) string {
217+
if catalogURL == "" {
218+
return ""
219+
}
220+
var leaf catalogLeaf
221+
if err := r.client.GetJSON(ctx, catalogURL, &leaf); err != nil {
222+
return ""
223+
}
224+
if leaf.PackageHash == "" {
225+
return ""
226+
}
227+
algo := strings.ToLower(leaf.PackageHashAlgorithm)
228+
if algo == "" {
229+
algo = "sha512"
230+
}
231+
return algo + "-" + leaf.PackageHash
232+
}
233+
206234
func (r *Registry) FetchDependencies(ctx context.Context, name, version string) ([]core.Dependency, error) {
207235
lowerName := strings.ToLower(name)
208236
url := fmt.Sprintf("%s/registration5-semver1/%s/index.json", r.baseURL, lowerName)

internal/nuget/nuget_test.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,39 @@ func TestFetchPackageWithGitHubRepository(t *testing.T) {
9595

9696
func TestFetchVersions(t *testing.T) {
9797
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98+
switch r.URL.Path {
99+
case "/catalog/xunit.2.6.0.json":
100+
_ = json.NewEncoder(w).Encode(catalogLeaf{
101+
PackageHash: "ExN13Ybp8c12hwOzvHnrVJeb+I9sR2YgdHUGmIQvU3h+2AHGEU0JieFQ0c8TECX4BgZ/+6aSMAe5ea8LMLykeg==",
102+
PackageHashAlgorithm: "SHA512",
103+
})
104+
return
105+
case "/catalog/xunit.2.5.0.json":
106+
w.WriteHeader(404)
107+
return
108+
}
109+
110+
base := "http://" + r.Host
98111
resp := registrationResponse{
99112
Items: []registrationPage{
100113
{
101114
Items: []registrationLeaf{
102115
{
103116
CatalogEntry: catalogEntry{
104-
ID: "xunit",
105-
Version: "2.6.0",
106-
Published: "2023-10-15T12:00:00Z",
107-
Listed: true,
117+
CatalogURL: base + "/catalog/xunit.2.6.0.json",
118+
ID: "xunit",
119+
Version: "2.6.0",
120+
Published: "2023-10-15T12:00:00Z",
121+
Listed: true,
108122
},
109123
},
110124
{
111125
CatalogEntry: catalogEntry{
112-
ID: "xunit",
113-
Version: "2.5.0",
114-
Published: "2023-07-01T12:00:00Z",
115-
Listed: false,
126+
CatalogURL: base + "/catalog/xunit.2.5.0.json",
127+
ID: "xunit",
128+
Version: "2.5.0",
129+
Published: "2023-07-01T12:00:00Z",
130+
Listed: false,
116131
},
117132
},
118133
{
@@ -160,6 +175,20 @@ func TestFetchVersions(t *testing.T) {
160175
if statusMap["2.4.0"] != core.StatusDeprecated {
161176
t.Errorf("expected deprecated status for 2.4.0, got %q", statusMap["2.4.0"])
162177
}
178+
179+
integrityMap := make(map[string]string)
180+
for _, v := range versions {
181+
integrityMap[v.Number] = v.Integrity
182+
}
183+
if integrityMap["2.6.0"] != "sha512-ExN13Ybp8c12hwOzvHnrVJeb+I9sR2YgdHUGmIQvU3h+2AHGEU0JieFQ0c8TECX4BgZ/+6aSMAe5ea8LMLykeg==" {
184+
t.Errorf("unexpected integrity for 2.6.0: %q", integrityMap["2.6.0"])
185+
}
186+
if integrityMap["2.5.0"] != "" {
187+
t.Errorf("expected empty integrity for 2.5.0 (catalog 404), got %q", integrityMap["2.5.0"])
188+
}
189+
if integrityMap["2.4.0"] != "" {
190+
t.Errorf("expected empty integrity for 2.4.0 (no @id), got %q", integrityMap["2.4.0"])
191+
}
163192
}
164193

165194
func TestFetchDependencies(t *testing.T) {

internal/pub/pub.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ type packageResponse struct {
5555
}
5656

5757
type versionInfo struct {
58-
Version string `json:"version"`
59-
Published time.Time `json:"published"`
60-
Pubspec pubspec `json:"pubspec"`
58+
Version string `json:"version"`
59+
Published time.Time `json:"published"`
60+
ArchiveSHA256 string `json:"archive_sha256"`
61+
Pubspec pubspec `json:"pubspec"`
6162
}
6263

6364
type pubspec struct {
@@ -111,10 +112,15 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi
111112

112113
versions := make([]core.Version, len(resp.Versions))
113114
for i, v := range resp.Versions {
115+
var integrity string
116+
if v.ArchiveSHA256 != "" {
117+
integrity = "sha256-" + v.ArchiveSHA256
118+
}
114119
versions[i] = core.Version{
115120
Number: v.Version,
116121
PublishedAt: v.Published,
117122
Licenses: v.Pubspec.License,
123+
Integrity: integrity,
118124
}
119125
}
120126

internal/pub/pub_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestFetchVersions(t *testing.T) {
5959
resp := packageResponse{
6060
Name: "provider",
6161
Versions: []versionInfo{
62-
{Version: "6.1.0", Pubspec: pubspec{License: "MIT"}},
62+
{Version: "6.1.0", ArchiveSHA256: "cc4fedb40b3c48d1a7558fcfe8d0f479046f017337d7b6b883208d65dbf8724d", Pubspec: pubspec{License: "MIT"}},
6363
{Version: "6.0.0", Pubspec: pubspec{License: "MIT"}},
6464
{Version: "5.0.0", Pubspec: pubspec{License: "MIT"}},
6565
},
@@ -84,6 +84,12 @@ func TestFetchVersions(t *testing.T) {
8484
if versions[0].Licenses != "MIT" {
8585
t.Errorf("expected MIT license, got %q", versions[0].Licenses)
8686
}
87+
if versions[0].Integrity != "sha256-cc4fedb40b3c48d1a7558fcfe8d0f479046f017337d7b6b883208d65dbf8724d" {
88+
t.Errorf("unexpected integrity: %q", versions[0].Integrity)
89+
}
90+
if versions[1].Integrity != "" {
91+
t.Errorf("expected empty integrity when archive_sha256 absent, got %q", versions[1].Integrity)
92+
}
8793
}
8894

8995
func TestFetchDependencies(t *testing.T) {

0 commit comments

Comments
 (0)