Skip to content

Commit f7dccd6

Browse files
committed
fix: error on decode with meaningful message
1 parent 07b818b commit f7dccd6

File tree

7 files changed

+128
-150
lines changed

7 files changed

+128
-150
lines changed

src/decode.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
'use strict'
22

3-
const { decodeURIComponent: decodeURIComponent_ } = globalThis
3+
const { PurlError } = require('./error')
44

5-
function decodeURIComponent(encodedURIComponent) {
5+
const { decodeURIComponent } = globalThis
6+
7+
function decodePurlComponent(comp, encodedURIComponent) {
68
try {
7-
return decodeURIComponent_(encodedURIComponent)
9+
return decodeURIComponent(encodedURIComponent)
810
} catch {}
9-
return encodedURIComponent
11+
throw new PurlError(`unable to decode "${comp}" component`)
1012
}
1113

1214
module.exports = {
13-
decodeURIComponent
15+
decodePurlComponent
1416
}

src/error.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict'
2+
3+
function formatPurlErrorMessage(message = '') {
4+
const { length } = message
5+
let formatted = ''
6+
if (length) {
7+
// Lower case start of message.
8+
const code0 = message.charCodeAt(0)
9+
formatted =
10+
code0 >= 65 /*'A'*/ || code0 <= 90 /*'Z'*/
11+
? `${message[0].toLowerCase()}${message.slice(1)}`
12+
: message
13+
// Remove period from end of message.
14+
if (
15+
length > 1 &&
16+
message.charCodeAt(length - 1) === 46 /*'.'*/ &&
17+
message.charCodeAt(length - 2) !== 46
18+
) {
19+
formatted = formatted.slice(0, -1)
20+
}
21+
}
22+
return `Invalid purl: ${formatted}`
23+
}
24+
25+
class PurlError extends Error {
26+
constructor(message) {
27+
super(formatPurlErrorMessage(message))
28+
}
29+
}
30+
31+
module.exports = {
32+
formatPurlErrorMessage,
33+
PurlError
34+
}

src/package-url.js

+20-14
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ SOFTWARE.
2121
*/
2222
'use strict'
2323

24-
const { decodeURIComponent } = require('./decode')
24+
const { decodePurlComponent } = require('./decode')
2525
const { isObject, recursiveFreeze } = require('./objects')
2626
const { isBlank, isNonEmptyString, trimLeadingSlashes } = require('./strings')
2727

2828
const { PurlComponent } = require('./purl-component')
2929
const { PurlQualifierNames } = require('./purl-qualifier-names')
3030
const { PurlType } = require('./purl-type')
31+
const { PurlError } = require('./error')
3132

3233
class PackageURL {
3334
static Component = recursiveFreeze(PurlComponent)
@@ -149,31 +150,28 @@ class PackageURL {
149150
? url
150151
: new URL(purlStr)
151152
} catch (e) {
152-
throw new Error('Invalid purl: failed to parse as URL', {
153+
throw new PurlError('failed to parse as URL', {
153154
cause: e
154155
})
155156
}
156157
}
157158
// The scheme is a constant with the value "pkg".
158159
if (url?.protocol !== 'pkg:') {
159-
throw new Error(
160-
'Invalid purl: missing required "pkg" scheme component'
161-
)
160+
throw new PurlError('missing required "pkg" scheme component')
162161
}
163162
// A purl must NOT contain a URL Authority i.e. there is no support for
164163
// username, password, host and port components.
165164
if (
166165
maybeUrlWithAuth.username !== '' ||
167166
maybeUrlWithAuth.password !== ''
168167
) {
169-
throw new Error(
170-
'Invalid purl: cannot contain a "user:pass@host:port"'
171-
)
168+
throw new PurlError('cannot contain a "user:pass@host:port"')
172169
}
173170

174171
const { pathname } = url
175172
const firstSlashIndex = pathname.indexOf('/')
176-
const rawType = decodeURIComponent(
173+
const rawType = decodePurlComponent(
174+
'type',
177175
firstSlashIndex === -1
178176
? pathname
179177
: pathname.slice(0, firstSlashIndex)
@@ -206,29 +204,37 @@ class PackageURL {
206204
)
207205
if (atSignIndex !== -1) {
208206
// Split the remainder once from right on '@'.
209-
rawVersion = decodeURIComponent(pathname.slice(atSignIndex + 1))
207+
rawVersion = decodePurlComponent(
208+
'version',
209+
pathname.slice(atSignIndex + 1)
210+
)
210211
}
211212

212213
let rawNamespace
213214
let rawName
214215
const lastSlashIndex = beforeVersion.lastIndexOf('/')
215216
if (lastSlashIndex === -1) {
216217
// Split the remainder once from right on '/'.
217-
rawName = decodeURIComponent(beforeVersion)
218+
rawName = decodePurlComponent('name', beforeVersion)
218219
} else {
219220
// Split the remainder once from right on '/'.
220-
rawName = decodeURIComponent(
221+
rawName = decodePurlComponent(
222+
'name',
221223
beforeVersion.slice(lastSlashIndex + 1)
222224
)
223225
// Split the remainder on '/'.
224-
rawNamespace = decodeURIComponent(
226+
rawNamespace = decodePurlComponent(
227+
'namespace',
225228
beforeVersion.slice(0, lastSlashIndex)
226229
)
227230
}
228231

229232
let rawQualifiers
230233
const { searchParams } = url
231234
if (searchParams.size !== 0) {
235+
searchParams.forEach((value) =>
236+
decodePurlComponent('qualifiers', value)
237+
)
232238
// Split the remainder once from right on '?'.
233239
rawQualifiers = searchParams
234240
}
@@ -237,7 +243,7 @@ class PackageURL {
237243
const { hash } = url
238244
if (hash.length !== 0) {
239245
// Split the purl string once from right on '#'.
240-
rawSubpath = decodeURIComponent(hash.slice(1))
246+
rawSubpath = decodePurlComponent('subpath', hash.slice(1))
241247
}
242248

243249
return [

src/purl-type.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
} = require('./strings')
1515

1616
const { validateEmptyByType, validateRequiredByType } = require('./validate')
17+
const { PurlError } = require('./error')
1718

1819
const PurlTypNormalizer = (purl) => purl
1920

@@ -149,16 +150,16 @@ module.exports = {
149150
if (isNullishOrEmptyString(purl.namespace)) {
150151
if (purl.qualifiers?.channel) {
151152
if (throws) {
152-
throw new Error(
153-
'Invalid purl: conan requires a "namespace" field when a "channel" qualifier is present.'
153+
throw new PurlError(
154+
'conan requires a "namespace" component when a "channel" qualifier is present'
154155
)
155156
}
156157
return false
157158
}
158159
} else if (isNullishOrEmptyString(purl.qualifiers)) {
159160
if (throws) {
160-
throw new Error(
161-
'Invalid purl: conan requires a "qualifiers" field when a namespace is present.'
161+
throw new PurlError(
162+
'conan requires a "qualifiers" component when a namespace is present'
162163
)
163164
}
164165
return false
@@ -190,8 +191,8 @@ module.exports = {
190191
!isSemverString(version.slice(1))
191192
) {
192193
if (throws) {
193-
throw new Error(
194-
'Invalid purl: golang "version" field starting with a "v" must be followed by a valid semver version'
194+
throw new PurlError(
195+
'golang "version" component starting with a "v" must be followed by a valid semver version'
195196
)
196197
}
197198
return false
@@ -241,8 +242,8 @@ module.exports = {
241242
)
242243
) {
243244
if (throws) {
244-
throw new Error(
245-
'Invalid purl: pub "name" field may only contain [a-z0-9_] characters'
245+
throw new PurlError(
246+
'pub "name" component may only contain [a-z0-9_] characters'
246247
)
247248
}
248249
return false

src/validate.js

+12-15
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
'use strict'
22

3+
const { PurlError } = require('./error')
34
const { isNullishOrEmptyString } = require('./lang')
45
const { isNonEmptyString } = require('./strings')
56

67
function validateEmptyByType(type, name, value, throws) {
78
if (!isNullishOrEmptyString(value)) {
89
if (throws) {
9-
throw new Error(
10-
`Invalid purl: ${type} "${name}" field must be empty.`
11-
)
10+
throw new PurlError(`${type} "${name}" component must be empty`)
1211
}
1312
return false
1413
}
@@ -32,9 +31,7 @@ function validateQualifiers(qualifiers, throws) {
3231
}
3332
if (typeof qualifiers !== 'object') {
3433
if (throws) {
35-
throw new Error(
36-
'Invalid purl: "qualifiers" argument must be an object.'
37-
)
34+
throw new PurlError('"qualifiers" must be an object')
3835
}
3936
return false
4037
}
@@ -74,8 +71,8 @@ function validateQualifierKey(key, throws) {
7471
)
7572
) {
7673
if (throws) {
77-
throw new Error(
78-
`Invalid purl: qualifier "${key}" contains an illegal character.`
74+
throw new PurlError(
75+
`qualifier "${key}" contains an illegal character`
7976
)
8077
}
8178
return false
@@ -87,7 +84,7 @@ function validateQualifierKey(key, throws) {
8784
function validateRequired(name, value, throws) {
8885
if (isNullishOrEmptyString(value)) {
8986
if (throws) {
90-
throw new Error(`Invalid purl: "${name}" is a required field.`)
87+
throw new PurlError(`"${name}" is a required component`)
9188
}
9289
return false
9390
}
@@ -97,7 +94,7 @@ function validateRequired(name, value, throws) {
9794
function validateRequiredByType(type, name, value, throws) {
9895
if (isNullishOrEmptyString(value)) {
9996
if (throws) {
100-
throw new Error(`Invalid purl: ${type} requires a "${name}" field.`)
97+
throw new PurlError(`${type} requires a "${name}" component`)
10198
}
10299
return false
103100
}
@@ -109,8 +106,8 @@ function validateStartsWithoutNumber(name, value, throws) {
109106
const code = value.charCodeAt(0)
110107
if (code >= 48 /*'0'*/ && code <= 57 /*'9'*/) {
111108
if (throws) {
112-
throw new Error(
113-
`Invalid purl: ${name} "${value}" cannot start with a number.`
109+
throw new PurlError(
110+
`${name} "${value}" cannot start with a number`
114111
)
115112
}
116113
return false
@@ -124,7 +121,7 @@ function validateStrings(name, value, throws) {
124121
return true
125122
}
126123
if (throws) {
127-
throw new Error(`Invalid purl: "'${name}" argument must be a string.`)
124+
throw new PurlError(`"'${name}" must be a string`)
128125
}
129126
return false
130127
}
@@ -160,8 +157,8 @@ function validateType(type, throws) {
160157
)
161158
) {
162159
if (throws) {
163-
throw new Error(
164-
`Invalid purl: type "${type}" contains an illegal character.`
160+
throw new PurlError(
161+
`type "${type}" contains an illegal character`
165162
)
166163
}
167164
return false

test/data/contrib-tests.json

-72
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,6 @@
119119
"subpath": null,
120120
"is_invalid": true
121121
},
122-
{
123-
"description": "improperly encoded version string",
124-
"purl": "pkg:maven/org.apache.commons/[email protected]$@",
125-
"canonical_purl": "pkg:maven/org.apache.commons/[email protected]$@",
126-
"type": null,
127-
"namespace": null,
128-
"name": "io",
129-
"version": "1.4.0-$@",
130-
"qualifiers": null,
131-
"subpath": null,
132-
"is_invalid": true
133-
},
134122
{
135123
"description": "leading and trailing slashes '/' are not significant and should be stripped in the canonical form",
136124
"purl": "pkg:golang//github.com///ll////[email protected]",
@@ -142,65 +130,5 @@
142130
"qualifiers": null,
143131
"subpath": null,
144132
"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/[email protected]?a=100%",
185-
"canonical_purl": "pkg:type/namespace/[email protected]?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/[email protected]#100%",
197-
"canonical_purl": "pkg:type/namespace/[email protected]#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
205133
}
206134
]

0 commit comments

Comments
 (0)