From 1ac92c7e81f9e2deb3fc6160f9bc3c67ef67a4a5 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 23 Jun 2025 20:27:01 +0200 Subject: [PATCH 1/4] purl: Implement `addRegistryUrl()` fn --- app/utils/purl.js | 22 +++++++++++++ tests/utils/purl-test.js | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 app/utils/purl.js create mode 100644 tests/utils/purl-test.js diff --git a/app/utils/purl.js b/app/utils/purl.js new file mode 100644 index 00000000000..bf07b5bf173 --- /dev/null +++ b/app/utils/purl.js @@ -0,0 +1,22 @@ +import window from 'ember-window-mock'; + +/** + * Adds a repository_url query parameter to a PURL string based on the host. + * + * @param {string} purl - The base PURL string (e.g., "pkg:cargo/serde@1.0.0") + * @param {string} [host] - The host to use for repository URL. Defaults to current window location host. + * @returns {string} The PURL with repository_url parameter added, or unchanged if host is crates.io + */ +export function addRegistryUrl(purl) { + let host = window.location.host; + + // Don't add repository_url for the main crates.io registry + if (host === 'crates.io') { + return purl; + } + + // Add repository_url query parameter + const repositoryUrl = `https://${host}/`; + const separator = purl.includes('?') ? '&' : '?'; + return `${purl}${separator}repository_url=${encodeURIComponent(repositoryUrl)}`; +} diff --git a/tests/utils/purl-test.js b/tests/utils/purl-test.js new file mode 100644 index 00000000000..4f5f340c2e9 --- /dev/null +++ b/tests/utils/purl-test.js @@ -0,0 +1,69 @@ +import { module, test } from 'qunit'; + +import window from 'ember-window-mock'; +import { setupWindowMock } from 'ember-window-mock/test-support'; + +import { addRegistryUrl } from 'crates-io/utils/purl'; + +module('Utils | purl', function (hooks) { + setupWindowMock(hooks); + + module('addRegistryUrl()', function () { + test('returns PURL unchanged for crates.io host', function (assert) { + window.location = 'https://crates.io'; + + let purl = 'pkg:cargo/serde@1.0.0'; + let result = addRegistryUrl(purl); + + assert.strictEqual(result, purl); + }); + + test('adds repository_url parameter for non-crates.io hosts', function (assert) { + window.location = 'https://staging.crates.io'; + + let purl = 'pkg:cargo/serde@1.0.0'; + let result = addRegistryUrl(purl); + + assert.strictEqual(result, 'pkg:cargo/serde@1.0.0?repository_url=https%3A%2F%2Fstaging.crates.io%2F'); + }); + + test('adds repository_url parameter for custom registry hosts', function (assert) { + window.location = 'https://my-registry.example.com'; + + let purl = 'pkg:cargo/my-crate@2.5.0'; + let result = addRegistryUrl(purl); + + assert.strictEqual(result, 'pkg:cargo/my-crate@2.5.0?repository_url=https%3A%2F%2Fmy-registry.example.com%2F'); + }); + + test('appends repository_url parameter when PURL already has query parameters', function (assert) { + window.location = 'https://staging.crates.io'; + + let purl = 'pkg:cargo/serde@1.0.0?arch=x86_64'; + let result = addRegistryUrl(purl); + + assert.strictEqual(result, 'pkg:cargo/serde@1.0.0?arch=x86_64&repository_url=https%3A%2F%2Fstaging.crates.io%2F'); + }); + + test('properly URL encodes the repository URL', function (assert) { + window.location = 'https://registry.example.com:8080'; + + let purl = 'pkg:cargo/test@1.0.0'; + let result = addRegistryUrl(purl); + + assert.strictEqual(result, 'pkg:cargo/test@1.0.0?repository_url=https%3A%2F%2Fregistry.example.com%3A8080%2F'); + }); + + test('handles PURL with complex qualifiers', function (assert) { + window.location = 'https://private.registry.co'; + + let purl = 'pkg:cargo/complex@1.0.0?os=linux&arch=amd64'; + let result = addRegistryUrl(purl); + + assert.strictEqual( + result, + 'pkg:cargo/complex@1.0.0?os=linux&arch=amd64&repository_url=https%3A%2F%2Fprivate.registry.co%2F', + ); + }); + }); +}); From 702b1baa0657f537cf8ab3334830fbc2e929e3a5 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 23 Jun 2025 20:42:31 +0200 Subject: [PATCH 2/4] models/version: Add `purl` property --- app/models/version.js | 10 ++++++++++ tests/models/version-test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/app/models/version.js b/app/models/version.js index c07e781e1ed..6220c3a72db 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -9,6 +9,7 @@ import { alias } from 'macro-decorators'; import semverParse from 'semver/functions/parse'; import ajax from '../utils/ajax'; +import { addRegistryUrl } from '../utils/purl'; const EIGHT_DAYS = 8 * 24 * 60 * 60 * 1000; @@ -52,6 +53,15 @@ export default class Version extends Model { return this.belongsTo('crate').id(); } + /** + * Returns the Package URL (PURL) for this version. + * @type {string} + */ + get purl() { + let basePurl = `pkg:cargo/${this.crateName}@${this.num}`; + return addRegistryUrl(basePurl); + } + get editionMsrv() { if (this.edition === '2018') { return '1.31.0'; diff --git a/tests/models/version-test.js b/tests/models/version-test.js index a24b7873f17..ef6e4d52611 100644 --- a/tests/models/version-test.js +++ b/tests/models/version-test.js @@ -1,6 +1,8 @@ import { module, test } from 'qunit'; import { calculateReleaseTracks } from '@crates-io/msw/utils/release-tracks'; +import window from 'ember-window-mock'; +import { setupWindowMock } from 'ember-window-mock/test-support'; import { setupTest } from 'crates-io/tests/helpers'; import setupMsw from 'crates-io/tests/helpers/setup-msw'; @@ -8,6 +10,7 @@ import setupMsw from 'crates-io/tests/helpers/setup-msw'; module('Model | Version', function (hooks) { setupTest(hooks); setupMsw(hooks); + setupWindowMock(hooks); hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); @@ -345,4 +348,36 @@ module('Model | Version', function (hooks) { assert.ok(version.published_by); assert.strictEqual(version.published_by.name, 'JD'); }); + + module('purl', function () { + test('generates PURL for crates.io version', async function (assert) { + let { db, store } = this; + + window.location = 'https://crates.io'; + + let crate = db.crate.create({ name: 'serde' }); + db.version.create({ crate, num: '1.0.136' }); + + let crateRecord = await store.findRecord('crate', crate.name); + let versions = (await crateRecord.versions).slice(); + let version = versions[0]; + + assert.strictEqual(version.purl, 'pkg:cargo/serde@1.0.136'); + }); + + test('generates PURL with registry URL for non-crates.io hosts', async function (assert) { + let { db, store } = this; + + window.location = 'https://staging.crates.io'; + + let crate = db.crate.create({ name: 'test-crate' }); + db.version.create({ crate, num: '2.5.0' }); + + let crateRecord = await store.findRecord('crate', crate.name); + let versions = (await crateRecord.versions).slice(); + let version = versions[0]; + + assert.strictEqual(version.purl, 'pkg:cargo/test-crate@2.5.0?repository_url=https%3A%2F%2Fstaging.crates.io%2F'); + }); + }); }); From 04fd86eb46eb0baa759a53fc5e6ccb9ea8f9a417 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 23 Jun 2025 21:07:01 +0200 Subject: [PATCH 3/4] CrateSidebar: Show Package URL in "Metadata" section --- app/components/crate-sidebar.hbs | 12 ++++++++ app/components/crate-sidebar.js | 12 ++++++++ app/components/crate-sidebar.module.css | 37 ++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/components/crate-sidebar.hbs b/app/components/crate-sidebar.hbs index 6d62d7b95fc..7af09b88499 100644 --- a/app/components/crate-sidebar.hbs +++ b/app/components/crate-sidebar.hbs @@ -6,6 +6,18 @@

Metadata

+
+ {{svg-jar "link"}} + + {{@version.purl}} + Package URL: {{@version.purl}} (click to copy) + +
+