Skip to content

Commit 07b818b

Browse files
committed
fix: only decode in parseString
1 parent c2f576f commit 07b818b

File tree

7 files changed

+150
-24
lines changed

7 files changed

+150
-24
lines changed

src/constants.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
const LOOP_SENTINEL = 1_000_000
44

5+
const REUSED_SEARCH_PARAMS = new URLSearchParams()
6+
7+
const REUSED_SEARCH_PARAMS_KEY = '_'
8+
9+
const REUSED_SEARCH_PARAMS_OFFSET = 2 // '_='.length
10+
511
module.exports = {
6-
LOOP_SENTINEL
12+
LOOP_SENTINEL,
13+
REUSED_SEARCH_PARAMS,
14+
REUSED_SEARCH_PARAMS_KEY,
15+
REUSED_SEARCH_PARAMS_OFFSET
716
}

src/decode.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
const { decodeURIComponent: decodeURIComponent_ } = globalThis
4+
5+
function decodeURIComponent(encodedURIComponent) {
6+
try {
7+
return decodeURIComponent_(encodedURIComponent)
8+
} catch {}
9+
return encodedURIComponent
10+
}
11+
12+
module.exports = {
13+
decodeURIComponent
14+
}

src/encode.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict'
22

3+
const {
4+
REUSED_SEARCH_PARAMS,
5+
REUSED_SEARCH_PARAMS_KEY,
6+
REUSED_SEARCH_PARAMS_OFFSET
7+
} = require('./constants')
38
const { isObject } = require('./objects')
49
const { isNonEmptyString } = require('./strings')
510

6-
const reusedSearchParams = new URLSearchParams()
7-
const reusedSearchParamKey = '_'
8-
const reusedSearchParamOffset = 2 // '_='.length
9-
1011
const { encodeURIComponent } = globalThis
1112

1213
function encodeNamespace(namespace) {
@@ -22,9 +23,9 @@ function encodeQualifierParam(param) {
2223
// Param key and value are encoded with `percentEncodeSet` of
2324
// 'application/x-www-form-urlencoded' and `spaceAsPlus` of `true`.
2425
// https://url.spec.whatwg.org/#urlencoded-serializing
25-
reusedSearchParams.set(reusedSearchParamKey, param)
26+
REUSED_SEARCH_PARAMS.set(REUSED_SEARCH_PARAMS_KEY, param)
2627
return replacePlusSignWithPercentEncodedSpace(
27-
reusedSearchParams.toString().slice(reusedSearchParamOffset)
28+
REUSED_SEARCH_PARAMS.toString().slice(REUSED_SEARCH_PARAMS_OFFSET)
2829
)
2930
}
3031
return ''

src/normalize.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@
33
const { isObject } = require('./objects')
44
const { isBlank } = require('./strings')
55

6-
const { decodeURIComponent } = globalThis
7-
86
function normalizeName(rawName) {
9-
return typeof rawName === 'string'
10-
? decodeURIComponent(rawName).trim()
11-
: undefined
7+
return typeof rawName === 'string' ? rawName.trim() : undefined
128
}
139

1410
function normalizeNamespace(rawNamespace) {
1511
return typeof rawNamespace === 'string'
16-
? normalizePath(decodeURIComponent(rawNamespace))
12+
? normalizePath(rawNamespace)
1713
: undefined
1814
}
1915

@@ -74,22 +70,20 @@ function normalizeQualifiers(rawQualifiers) {
7470

7571
function normalizeSubpath(rawSubpath) {
7672
return typeof rawSubpath === 'string'
77-
? normalizePath(decodeURIComponent(rawSubpath), subpathFilter)
73+
? normalizePath(rawSubpath, subpathFilter)
7874
: undefined
7975
}
8076

8177
function normalizeType(rawType) {
8278
// The type must NOT be percent-encoded.
8379
// The type is case insensitive. The canonical form is lowercase.
8480
return typeof rawType === 'string'
85-
? decodeURIComponent(rawType).trim().toLowerCase()
81+
? rawType.trim().toLowerCase()
8682
: undefined
8783
}
8884

8985
function normalizeVersion(rawVersion) {
90-
return typeof rawVersion === 'string'
91-
? decodeURIComponent(rawVersion).trim()
92-
: undefined
86+
return typeof rawVersion === 'string' ? rawVersion.trim() : undefined
9387
}
9488

9589
function qualifiersToEntries(rawQualifiers) {

src/package-url.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ SOFTWARE.
2121
*/
2222
'use strict'
2323

24+
const { decodeURIComponent } = require('./decode')
2425
const { isObject, recursiveFreeze } = require('./objects')
2526
const { isBlank, isNonEmptyString, trimLeadingSlashes } = require('./strings')
2627

@@ -172,10 +173,11 @@ class PackageURL {
172173

173174
const { pathname } = url
174175
const firstSlashIndex = pathname.indexOf('/')
175-
const rawType =
176+
const rawType = decodeURIComponent(
176177
firstSlashIndex === -1
177178
? pathname
178179
: pathname.slice(0, firstSlashIndex)
180+
)
179181
if (firstSlashIndex < 1) {
180182
return [
181183
rawType,
@@ -204,20 +206,24 @@ class PackageURL {
204206
)
205207
if (atSignIndex !== -1) {
206208
// Split the remainder once from right on '@'.
207-
rawVersion = pathname.slice(atSignIndex + 1)
209+
rawVersion = decodeURIComponent(pathname.slice(atSignIndex + 1))
208210
}
209211

210212
let rawNamespace
211213
let rawName
212214
const lastSlashIndex = beforeVersion.lastIndexOf('/')
213215
if (lastSlashIndex === -1) {
214216
// Split the remainder once from right on '/'.
215-
rawName = beforeVersion
217+
rawName = decodeURIComponent(beforeVersion)
216218
} else {
217219
// Split the remainder once from right on '/'.
218-
rawName = beforeVersion.slice(lastSlashIndex + 1)
220+
rawName = decodeURIComponent(
221+
beforeVersion.slice(lastSlashIndex + 1)
222+
)
219223
// Split the remainder on '/'.
220-
rawNamespace = beforeVersion.slice(0, lastSlashIndex)
224+
rawNamespace = decodeURIComponent(
225+
beforeVersion.slice(0, lastSlashIndex)
226+
)
221227
}
222228

223229
let rawQualifiers
@@ -231,7 +237,7 @@ class PackageURL {
231237
const { hash } = url
232238
if (hash.length !== 0) {
233239
// Split the purl string once from right on '#'.
234-
rawSubpath = hash.slice(1)
240+
rawSubpath = decodeURIComponent(hash.slice(1))
235241
}
236242

237243
return [

test/data/contrib-tests.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,65 @@
142142
"qualifiers": null,
143143
"subpath": null,
144144
"is_invalid": false
145+
},
146+
{
147+
"description": "percent encoded namespace",
148+
"purl": "pkg:type/100%/name",
149+
"canonical_purl": "pkg:type/100%25/name",
150+
"type": "type",
151+
"namespace": "100%",
152+
"name": "name",
153+
"version": null,
154+
"qualifiers": null,
155+
"subpath": null,
156+
"is_invalid": false
157+
},
158+
{
159+
"description": "percent encoded name",
160+
"purl": "pkg:type/namespace/100%",
161+
"canonical_purl": "pkg:type/namespace/100%25",
162+
"type": "type",
163+
"namespace": "namespace",
164+
"name": "100%",
165+
"version": null,
166+
"qualifiers": null,
167+
"subpath": null,
168+
"is_invalid": false
169+
},
170+
{
171+
"description": "percent encoded version",
172+
"purl": "pkg:type/namespace/name@100%",
173+
"canonical_purl": "pkg:type/namespace/name@100%25",
174+
"type": "type",
175+
"namespace": "namespace",
176+
"name": "name",
177+
"version": "100%",
178+
"qualifiers": null,
179+
"subpath": null,
180+
"is_invalid": false
181+
},
182+
{
183+
"description": "percent encoded qualifiers",
184+
"purl": "pkg:type/namespace/name@1.0?a=100%",
185+
"canonical_purl": "pkg:type/namespace/name@1.0?a=100%25",
186+
"type": "type",
187+
"namespace": "namespace",
188+
"name": "name",
189+
"version": "1.0",
190+
"qualifiers": { "a": "100%" },
191+
"subpath": null,
192+
"is_invalid": false
193+
},
194+
{
195+
"description": "percent encoded subpath",
196+
"purl": "pkg:type/namespace/name@1.0#100%",
197+
"canonical_purl": "pkg:type/namespace/name@1.0#100%25",
198+
"type": "type",
199+
"namespace": "namespace",
200+
"name": "name",
201+
"version": "1.0",
202+
"qualifiers": null,
203+
"subpath": "100%",
204+
"is_invalid": false
145205
}
146206
]

test/package-url.spec.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,48 @@ describe('PackageURL', function () {
169169
testInvalid(paramName)
170170
})
171171
})
172+
173+
it('should not decode params', () => {
174+
assert.strictEqual(
175+
new PackageURL('type', '%21', 'name').toString(),
176+
'pkg:type/%2521/name'
177+
)
178+
assert.strictEqual(
179+
new PackageURL('type', 'namespace', '%21').toString(),
180+
'pkg:type/namespace/%2521'
181+
)
182+
assert.strictEqual(
183+
new PackageURL('type', 'namespace', 'name', '%21').toString(),
184+
'pkg:type/namespace/name@%2521'
185+
)
186+
assert.strictEqual(
187+
new PackageURL('type', 'namespace', 'name', '1.0', {
188+
a: '%21'
189+
}).toString(),
190+
'pkg:type/namespace/name@1.0?a=%2521'
191+
)
192+
assert.strictEqual(
193+
new PackageURL(
194+
'type',
195+
'namespace',
196+
'name',
197+
'1.0',
198+
'a=%2521'
199+
).toString(),
200+
'pkg:type/namespace/name@1.0?a=%2521'
201+
)
202+
assert.strictEqual(
203+
new PackageURL(
204+
'type',
205+
'namespace',
206+
'name',
207+
'1.0',
208+
null,
209+
'%21'
210+
).toString(),
211+
'pkg:type/namespace/name@1.0#%2521'
212+
)
213+
})
172214
})
173215

174216
describe('toString()', function () {

0 commit comments

Comments
 (0)