Skip to content

Commit ff590d2

Browse files
committed
feat: encode qualifiers with URLSearchParams
1 parent 09d8ebf commit ff590d2

File tree

4 files changed

+59
-36
lines changed

4 files changed

+59
-36
lines changed

src/encode.js

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
'use strict'
22

3+
const { isObject } = require('./objects')
4+
const { isNonEmptyString } = require('./strings')
5+
6+
const reusedSearchParams = new URLSearchParams()
7+
38
const { encodeURIComponent } = globalThis
49

510
function encodeWithColonAndForwardSlash(str) {
@@ -15,40 +20,54 @@ function encodeWithForwardSlash(str) {
1520
}
1621

1722
function encodeNamespace(namespace) {
18-
return typeof namespace === 'string' && namespace.length
23+
return isNonEmptyString(namespace)
1924
? encodeWithColonAndForwardSlash(namespace)
2025
: ''
2126
}
2227

2328
function encodeVersion(version) {
24-
return typeof version === 'string' && version.length
25-
? encodeWithColonAndPlusSign(version)
26-
: ''
29+
return isNonEmptyString(version) ? encodeWithColonAndPlusSign(version) : ''
2730
}
2831

2932
function encodeQualifiers(qualifiers) {
30-
let query = ''
31-
if (qualifiers !== null && typeof qualifiers === 'object') {
33+
if (isObject(qualifiers)) {
3234
// Sort this list of qualifier strings lexicographically.
3335
const qualifiersKeys = Object.keys(qualifiers).sort()
36+
const searchParams = new URLSearchParams()
3437
for (let i = 0, { length } = qualifiersKeys; i < length; i += 1) {
3538
const key = qualifiersKeys[i]
36-
query = `${query}${i === 0 ? '' : '&'}${key}=${encodeQualifierValue(qualifiers[key])}`
39+
searchParams.set(key, qualifiers[key])
3740
}
41+
return replacePlusSignWithPercentEncodedSpace(searchParams.toString())
3842
}
39-
return query
43+
return ''
4044
}
4145

42-
function encodeQualifierValue(qualifierValue) {
43-
return typeof qualifierValue === 'string' && qualifierValue.length
44-
? encodeWithColonAndForwardSlash(qualifierValue)
46+
function encodeQualifierParam(qualifierValue) {
47+
return isNonEmptyString
48+
? encodeURLSearchParamWithPercentEncodedSpace(param)
4549
: ''
4650
}
4751

4852
function encodeSubpath(subpath) {
49-
return typeof subpath === 'string' && subpath.length
50-
? encodeWithForwardSlash(subpath)
51-
: ''
53+
return isNonEmptyString(subpath) ? encodeWithForwardSlash(subpath) : ''
54+
}
55+
56+
function encodeURLSearchParam(param) {
57+
// Param key and value are encoded with `percentEncodeSet` of
58+
// 'application/x-www-form-urlencoded' and `spaceAsPlus` of `true`.
59+
// https://url.spec.whatwg.org/#urlencoded-serializing
60+
reusedSearchParams.set('_', qualifierValue)
61+
return reusedSearchParams.toString().slice(2)
62+
}
63+
64+
function encodeURLSearchParamWithPercentEncodedSpace(str) {
65+
return replacePlusSignWithPercentEncodedSpace(encodeURLSearchParam(str))
66+
}
67+
68+
function replacePlusSignWithPercentEncodedSpace(str) {
69+
// Convert plus signs to %20 for better portability.
70+
return str.replace(/\+/g, '%20')
5271
}
5372

5473
module.exports = {
@@ -58,7 +77,9 @@ module.exports = {
5877
encodeNamespace,
5978
encodeVersion,
6079
encodeQualifiers,
61-
encodeQualifierValue,
80+
encodeQualifierParam,
6281
encodeSubpath,
63-
encodeURIComponent
82+
encodeURIComponent,
83+
encodeURLSearchParam,
84+
encodeURLSearchParamWithPercentEncodedSpace
6485
}

src/purl-component.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
encodeNamespace,
55
encodeVersion,
66
encodeQualifiers,
7+
encodeQualifierKey,
78
encodeQualifierValue,
89
encodeSubpath,
910
encodeURIComponent
@@ -69,6 +70,7 @@ module.exports = {
6970
namespace: encodeNamespace,
7071
version: encodeVersion,
7172
qualifiers: encodeQualifiers,
73+
qualifierKey: encodeQualifierKey,
7274
qualifierValue: encodeQualifierValue,
7375
subpath: encodeSubpath
7476
},

test/data/contrib-tests.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@
107107
"subpath": null,
108108
"is_invalid": false
109109
},
110+
{
111+
"description": "maven requires a namespace",
112+
"purl": "pkg:maven/[email protected]",
113+
"canonical_purl": "pkg:maven/[email protected]",
114+
"type": "maven",
115+
"namespace": null,
116+
"name": null,
117+
"version": null,
118+
"qualifiers": null,
119+
"subpath": null,
120+
"is_invalid": true
121+
},
110122
{
111123
"description": "improperly encoded version string",
112124
"purl": "pkg:maven/org.apache.commons/[email protected]$@",
@@ -120,8 +132,8 @@
120132
"is_invalid": true
121133
},
122134
{
123-
"description": "In namespace, leading and trailing slashes '/' are not significant and should be stripped in the canonical form",
124-
"purl": "pkg:golang//github.com/ll/[email protected]",
135+
"description": "leading and trailing slashes '/' are not significant and should be stripped in the canonical form",
136+
"purl": "pkg:golang//github.com///ll////[email protected]",
125137
"canonical_purl": "pkg:golang/github.com/ll/[email protected]",
126138
"type": "golang",
127139
"namespace": "github.com/ll",

test/data/test-suite-data.json

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
{
111111
"description": "maven often uses qualifiers",
112112
"purl": "pkg:Maven/org.apache.xmlgraphics/[email protected]?classifier=sources&repositorY_url=repo.spring.io/release",
113-
"canonical_purl": "pkg:maven/org.apache.xmlgraphics/[email protected]?classifier=sources&repository_url=repo.spring.io/release",
113+
"canonical_purl": "pkg:maven/org.apache.xmlgraphics/[email protected]?classifier=sources&repository_url=repo.spring.io%2Frelease",
114114
"type": "maven",
115115
"namespace": "org.apache.xmlgraphics",
116116
"name": "batik-anim",
@@ -122,7 +122,7 @@
122122
{
123123
"description": "maven pom reference",
124124
"purl": "pkg:Maven/org.apache.xmlgraphics/[email protected]?extension=pom&repositorY_url=repo.spring.io/release",
125-
"canonical_purl": "pkg:maven/org.apache.xmlgraphics/[email protected]?extension=pom&repository_url=repo.spring.io/release",
125+
"canonical_purl": "pkg:maven/org.apache.xmlgraphics/[email protected]?extension=pom&repository_url=repo.spring.io%2Frelease",
126126
"type": "maven",
127127
"namespace": "org.apache.xmlgraphics",
128128
"name": "batik-anim",
@@ -143,18 +143,6 @@
143143
"subpath": null,
144144
"is_invalid": false
145145
},
146-
{
147-
"description": "maven requires a namespace",
148-
"purl": "pkg:maven/[email protected]",
149-
"canonical_purl": "pkg:maven/[email protected]",
150-
"type": "maven",
151-
"namespace": null,
152-
"name": null,
153-
"version": null,
154-
"qualifiers": null,
155-
"subpath": null,
156-
"is_invalid": true
157-
},
158146
{
159147
"description": "npm can be scoped",
160148
"purl": "pkg:npm/%40angular/[email protected]",
@@ -494,7 +482,7 @@
494482
{
495483
"description": "Hugging Face model with staging endpoint",
496484
"purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co",
497-
"canonical_purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co",
485+
"canonical_purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https%3A%2F%2Fhub-ci.huggingface.co",
498486
"type": "huggingface",
499487
"namespace": "microsoft",
500488
"name": "deberta-v3-base",
@@ -518,7 +506,7 @@
518506
{
519507
"description": "MLflow model tracked in Azure Databricks (case insensitive)",
520508
"purl": "pkg:mlflow/CreditFraud@3?repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow",
521-
"canonical_purl": "pkg:mlflow/creditfraud@3?repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow",
509+
"canonical_purl": "pkg:mlflow/creditfraud@3?repository_url=https%3A%2F%2Fadb-5245952564735461.0.azuredatabricks.net%2Fapi%2F2.0%2Fmlflow",
522510
"type": "mlflow",
523511
"namespace": null,
524512
"name": "creditfraud",
@@ -530,7 +518,7 @@
530518
{
531519
"description": "MLflow model tracked in Azure ML (case sensitive)",
532520
"purl": "pkg:mlflow/CreditFraud@3?repository_url=https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace",
533-
"canonical_purl": "pkg:mlflow/CreditFraud@3?repository_url=https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace",
521+
"canonical_purl": "pkg:mlflow/CreditFraud@3?repository_url=https%3A%2F%2Fwestus2.api.azureml.ms%2Fmlflow%2Fv1.0%2Fsubscriptions%2Fa50f2011-fab8-4164-af23-c62881ef8c95%2FresourceGroups%2FTestResourceGroup%2Fproviders%2FMicrosoft.MachineLearningServices%2Fworkspaces%2FTestWorkspace",
534522
"type": "mlflow",
535523
"namespace": null,
536524
"name": "CreditFraud",
@@ -542,7 +530,7 @@
542530
{
543531
"description": "MLflow model with unique identifiers",
544532
"purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a&repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow",
545-
"canonical_purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a",
533+
"canonical_purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&repository_url=https%3A%2F%2Fadb-5245952564735461.0.azuredatabricks.net%2Fapi%2F2.0%2Fmlflow&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a",
546534
"type": "mlflow",
547535
"namespace": null,
548536
"name": "trafficsigns",

0 commit comments

Comments
 (0)