Skip to content

Commit 6b5f214

Browse files
committed
Update implementation for spec changes
1 parent 67e16d8 commit 6b5f214

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+7330
-837
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"@biomejs/biome": "2.1.4",
6565
"@eslint/compat": "1.3.2",
6666
"@eslint/js": "9.32.0",
67-
"@socketsecurity/registry": "1.0.265",
67+
"@socketsecurity/registry": "1.0.266",
6868
"all-the-package-names": "2.0.0",
6969
"all-the-package-names-v1.3905.0": "npm:[email protected]",
7070
"custompatch": "1.1.8",

src/encode.js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,31 @@ const { encodeURIComponent: encodeComponent } = globalThis
1212

1313
function encodeName(name) {
1414
return isNonEmptyString(name)
15-
? encodeComponent(name).replace(/%3A/g, ':')
15+
? encodeComponent(name).replaceAll('%3A', ':')
1616
: ''
1717
}
1818

1919
function encodeNamespace(namespace) {
2020
return isNonEmptyString(namespace)
21-
? encodeComponent(namespace).replace(/%3A/g, ':').replace(/%2F/g, '/')
21+
? encodeComponent(namespace).replaceAll('%3A', ':').replaceAll('%2F', '/')
2222
: ''
2323
}
2424

2525
function encodeQualifierParam(param) {
2626
if (isNonEmptyString(param)) {
27+
// Replace spaces with %20's so they don't get converted to plus signs.
28+
const value = String(param).replaceAll(' ', '%20')
29+
// Use URLSearchParams#set to preserve plus signs.
30+
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
31+
REUSED_SEARCH_PARAMS.set(REUSED_SEARCH_PARAMS_KEY, value)
2732
// Param key and value are encoded with `percentEncodeSet` of
2833
// 'application/x-www-form-urlencoded' and `spaceAsPlus` of `true`.
2934
// https://url.spec.whatwg.org/#urlencoded-serializing
30-
REUSED_SEARCH_PARAMS.set(REUSED_SEARCH_PARAMS_KEY, param)
31-
return replacePlusSignWithPercentEncodedSpace(
32-
REUSED_SEARCH_PARAMS.toString().slice(REUSED_SEARCH_PARAMS_OFFSET)
33-
)
35+
const search = REUSED_SEARCH_PARAMS.toString()
36+
return search
37+
.slice(REUSED_SEARCH_PARAMS_OFFSET)
38+
.replaceAll('%2520', '%20')
39+
.replaceAll('+', '%2B')
3440
}
3541
return ''
3642
}
@@ -42,30 +48,32 @@ function encodeQualifiers(qualifiers) {
4248
const searchParams = new URLSearchParams()
4349
for (let i = 0, { length } = qualifiersKeys; i < length; i += 1) {
4450
const key = qualifiersKeys[i]
45-
searchParams.set(key, qualifiers[key])
51+
// Replace spaces with %20's so they don't get converted to plus signs.
52+
const value = String(qualifiers[key]).replaceAll(' ', '%20')
53+
// Use URLSearchParams#set to preserve plus signs.
54+
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
55+
searchParams.set(key, value)
4656
}
47-
return replacePlusSignWithPercentEncodedSpace(searchParams.toString())
57+
return searchParams
58+
.toString()
59+
.replaceAll('%2520', '%20')
60+
.replaceAll('+', '%2B')
4861
}
4962
return ''
5063
}
5164

5265
function encodeSubpath(subpath) {
5366
return isNonEmptyString(subpath)
54-
? encodeComponent(subpath).replace(/%2F/g, '/')
67+
? encodeComponent(subpath).replaceAll('%2F', '/')
5568
: ''
5669
}
5770

5871
function encodeVersion(version) {
5972
return isNonEmptyString(version)
60-
? encodeComponent(version).replace(/%3A/g, ':').replace(/%2B/g, '+')
73+
? encodeComponent(version).replaceAll('%3A', ':')
6174
: ''
6275
}
6376

64-
function replacePlusSignWithPercentEncodedSpace(str) {
65-
// Convert plus signs to %20 for better portability.
66-
return str.replace(/\+/g, '%20')
67-
}
68-
6977
module.exports = {
7078
encodeComponent,
7179
encodeName,

src/normalize.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function normalizePath(pathname, callback) {
5050

5151
function normalizeQualifiers(rawQualifiers) {
5252
let qualifiers
53+
// Use for-of to work with entries iterators.
5354
for (const { 0: key, 1: value } of qualifiersToEntries(rawQualifiers)) {
5455
const strValue = typeof value === 'string' ? value : String(value)
5556
const trimmed = strValue.trim()
@@ -85,7 +86,8 @@ function normalizeVersion(rawVersion) {
8586

8687
function qualifiersToEntries(rawQualifiers) {
8788
if (isObject(rawQualifiers)) {
88-
return rawQualifiers instanceof URLSearchParams
89+
// URLSearchParams instances have an "entries" method that returns an iterator.
90+
return typeof rawQualifiers.entries === 'function'
8991
? rawQualifiers.entries()
9092
: Object.entries(rawQualifiers)
9193
}

src/package-url.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,17 @@ class PackageURL {
214214
}
215215

216216
let rawQualifiers
217-
const { searchParams } = url
218-
if (searchParams.size !== 0) {
219-
searchParams.forEach(value => decodePurlComponent('qualifiers', value))
217+
if (url.searchParams.size !== 0) {
218+
const search = url.search.slice(1)
219+
const searchParams = new URLSearchParams()
220+
const entries = search.split('&')
221+
for (let i = 0, { length } = entries; i < length; i += 1) {
222+
const pairs = entries[i].split('=')
223+
const value = decodePurlComponent('qualifiers', pairs.at(1) ?? '')
224+
// Use URLSearchParams#append to preserve plus signs.
225+
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
226+
searchParams.append(pairs[0], value)
227+
}
220228
// Split the remainder once from right on '?'.
221229
rawQualifiers = searchParams
222230
}

src/validate.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ function validateQualifiers(qualifiers, throws) {
3636
return false
3737
}
3838
const keysIterable =
39-
// URL searchParams have an "keys" method that returns an iterator.
39+
// URLSearchParams instances have a "keys" method that returns an iterator.
4040
typeof qualifiers.keys === 'function'
4141
? qualifiers.keys()
4242
: Object.keys(qualifiers)
43+
// Use for-of to work with URLSearchParams#keys iterators.
4344
for (const key of keysIterable) {
4445
if (!validateQualifierKey(key, throws)) {
4546
return false
@@ -136,7 +137,7 @@ function validateType(type, throws) {
136137
return false
137138
}
138139
// The package type is composed only of ASCII letters and numbers,
139-
// '.', '+' and '-' (period, plus, and dash)
140+
// '.' (period), and '-' (dash).
140141
for (let i = 0, { length } = type; i < length; i += 1) {
141142
const code = type.charCodeAt(i)
142143
// biome-ignore format: newlines
@@ -147,7 +148,6 @@ function validateType(type, throws) {
147148
(code >= 65 && code <= 90) || // A-Z
148149
(code >= 97 && code <= 122) || // a-z
149150
code === 46 || // .
150-
code === 43 || // +
151151
code === 45
152152
) // -
153153
)

test/benchmark.test.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
11
'use strict'
22

33
const assert = require('node:assert/strict')
4+
const path = require('node:path')
45
const { describe, it } = require('node:test')
56

6-
const TEST_FILE = require('./data/test-suite-data.json')
7+
const { glob } = require('fast-glob')
8+
9+
const { readJson } = require('@socketsecurity/registry/lib/fs')
10+
const { isObject } = require('@socketsecurity/registry/lib/objects')
11+
712
const { PackageURL } = require('../src/package-url')
813

914
describe('PackageURL', () => {
10-
it('Benchmarking the library', () => {
15+
it('Benchmarking the library', async () => {
16+
const TEST_FILES = (
17+
await Promise.all(
18+
(
19+
await glob(['**/**.json'], {
20+
absolute: true,
21+
cwd: path.join(__dirname, 'data')
22+
})
23+
).map(p => readJson(p))
24+
)
25+
)
26+
.filter(Boolean)
27+
.flatMap(o => o.tests ?? [])
28+
1129
const iterations = 10000
12-
const data = TEST_FILE.filter(obj => !obj.is_invalid)
30+
const data = TEST_FILES.filter(obj => isObject(obj.expected_output))
1331
const { length: dataLength } = data
1432
const objects = []
1533
for (let i = 0; i < iterations; i += dataLength) {
@@ -23,13 +41,14 @@ describe('PackageURL', () => {
2341
const start = Date.now()
2442
for (let i = 0; i < iterations; i += 1) {
2543
const obj = objects[i]
44+
const { expected_output } = obj
2645
const purl = new PackageURL(
27-
obj.type,
28-
obj.namespace,
29-
obj.name,
30-
obj.version,
31-
obj.qualifiers,
32-
obj.subpath
46+
expected_output.type,
47+
expected_output.namespace,
48+
expected_output.name,
49+
expected_output.version,
50+
expected_output.qualifiers,
51+
expected_output.subpath
3352
)
3453
PackageURL.fromString(purl.toString())
3554
}

0 commit comments

Comments
 (0)