Skip to content

Commit

Permalink
fix: error on decode with meaningful message
Browse files Browse the repository at this point in the history
  • Loading branch information
jdalton committed Aug 28, 2024
1 parent 07b818b commit f7dccd6
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 150 deletions.
12 changes: 7 additions & 5 deletions src/decode.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use strict'

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

function decodeURIComponent(encodedURIComponent) {
const { decodeURIComponent } = globalThis

function decodePurlComponent(comp, encodedURIComponent) {
try {
return decodeURIComponent_(encodedURIComponent)
return decodeURIComponent(encodedURIComponent)
} catch {}
return encodedURIComponent
throw new PurlError(`unable to decode "${comp}" component`)
}

module.exports = {
decodeURIComponent
decodePurlComponent
}
34 changes: 34 additions & 0 deletions src/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict'

function formatPurlErrorMessage(message = '') {
const { length } = message
let formatted = ''
if (length) {
// Lower case start of message.
const code0 = message.charCodeAt(0)
formatted =
code0 >= 65 /*'A'*/ || code0 <= 90 /*'Z'*/
? `${message[0].toLowerCase()}${message.slice(1)}`
: message
// Remove period from end of message.
if (
length > 1 &&
message.charCodeAt(length - 1) === 46 /*'.'*/ &&
message.charCodeAt(length - 2) !== 46
) {
formatted = formatted.slice(0, -1)
}
}
return `Invalid purl: ${formatted}`
}

class PurlError extends Error {
constructor(message) {
super(formatPurlErrorMessage(message))
}
}

module.exports = {
formatPurlErrorMessage,
PurlError
}
34 changes: 20 additions & 14 deletions src/package-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ SOFTWARE.
*/
'use strict'

const { decodeURIComponent } = require('./decode')
const { decodePurlComponent } = require('./decode')
const { isObject, recursiveFreeze } = require('./objects')
const { isBlank, isNonEmptyString, trimLeadingSlashes } = require('./strings')

const { PurlComponent } = require('./purl-component')
const { PurlQualifierNames } = require('./purl-qualifier-names')
const { PurlType } = require('./purl-type')
const { PurlError } = require('./error')

class PackageURL {
static Component = recursiveFreeze(PurlComponent)
Expand Down Expand Up @@ -149,31 +150,28 @@ class PackageURL {
? url
: new URL(purlStr)
} catch (e) {
throw new Error('Invalid purl: failed to parse as URL', {
throw new PurlError('failed to parse as URL', {
cause: e
})
}
}
// The scheme is a constant with the value "pkg".
if (url?.protocol !== 'pkg:') {
throw new Error(
'Invalid purl: missing required "pkg" scheme component'
)
throw new PurlError('missing required "pkg" scheme component')
}
// A purl must NOT contain a URL Authority i.e. there is no support for
// username, password, host and port components.
if (
maybeUrlWithAuth.username !== '' ||
maybeUrlWithAuth.password !== ''
) {
throw new Error(
'Invalid purl: cannot contain a "user:pass@host:port"'
)
throw new PurlError('cannot contain a "user:pass@host:port"')
}

const { pathname } = url
const firstSlashIndex = pathname.indexOf('/')
const rawType = decodeURIComponent(
const rawType = decodePurlComponent(
'type',
firstSlashIndex === -1
? pathname
: pathname.slice(0, firstSlashIndex)
Expand Down Expand Up @@ -206,29 +204,37 @@ class PackageURL {
)
if (atSignIndex !== -1) {
// Split the remainder once from right on '@'.
rawVersion = decodeURIComponent(pathname.slice(atSignIndex + 1))
rawVersion = decodePurlComponent(
'version',
pathname.slice(atSignIndex + 1)
)
}

let rawNamespace
let rawName
const lastSlashIndex = beforeVersion.lastIndexOf('/')
if (lastSlashIndex === -1) {
// Split the remainder once from right on '/'.
rawName = decodeURIComponent(beforeVersion)
rawName = decodePurlComponent('name', beforeVersion)
} else {
// Split the remainder once from right on '/'.
rawName = decodeURIComponent(
rawName = decodePurlComponent(
'name',
beforeVersion.slice(lastSlashIndex + 1)
)
// Split the remainder on '/'.
rawNamespace = decodeURIComponent(
rawNamespace = decodePurlComponent(
'namespace',
beforeVersion.slice(0, lastSlashIndex)
)
}

let rawQualifiers
const { searchParams } = url
if (searchParams.size !== 0) {
searchParams.forEach((value) =>
decodePurlComponent('qualifiers', value)
)
// Split the remainder once from right on '?'.
rawQualifiers = searchParams
}
Expand All @@ -237,7 +243,7 @@ class PackageURL {
const { hash } = url
if (hash.length !== 0) {
// Split the purl string once from right on '#'.
rawSubpath = decodeURIComponent(hash.slice(1))
rawSubpath = decodePurlComponent('subpath', hash.slice(1))
}

return [
Expand Down
17 changes: 9 additions & 8 deletions src/purl-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
} = require('./strings')

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

const PurlTypNormalizer = (purl) => purl

Expand Down Expand Up @@ -149,16 +150,16 @@ module.exports = {
if (isNullishOrEmptyString(purl.namespace)) {
if (purl.qualifiers?.channel) {
if (throws) {
throw new Error(
'Invalid purl: conan requires a "namespace" field when a "channel" qualifier is present.'
throw new PurlError(
'conan requires a "namespace" component when a "channel" qualifier is present'
)
}
return false
}
} else if (isNullishOrEmptyString(purl.qualifiers)) {
if (throws) {
throw new Error(
'Invalid purl: conan requires a "qualifiers" field when a namespace is present.'
throw new PurlError(
'conan requires a "qualifiers" component when a namespace is present'
)
}
return false
Expand Down Expand Up @@ -190,8 +191,8 @@ module.exports = {
!isSemverString(version.slice(1))
) {
if (throws) {
throw new Error(
'Invalid purl: golang "version" field starting with a "v" must be followed by a valid semver version'
throw new PurlError(
'golang "version" component starting with a "v" must be followed by a valid semver version'
)
}
return false
Expand Down Expand Up @@ -241,8 +242,8 @@ module.exports = {
)
) {
if (throws) {
throw new Error(
'Invalid purl: pub "name" field may only contain [a-z0-9_] characters'
throw new PurlError(
'pub "name" component may only contain [a-z0-9_] characters'
)
}
return false
Expand Down
27 changes: 12 additions & 15 deletions src/validate.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
'use strict'

const { PurlError } = require('./error')
const { isNullishOrEmptyString } = require('./lang')
const { isNonEmptyString } = require('./strings')

function validateEmptyByType(type, name, value, throws) {
if (!isNullishOrEmptyString(value)) {
if (throws) {
throw new Error(
`Invalid purl: ${type} "${name}" field must be empty.`
)
throw new PurlError(`${type} "${name}" component must be empty`)
}
return false
}
Expand All @@ -32,9 +31,7 @@ function validateQualifiers(qualifiers, throws) {
}
if (typeof qualifiers !== 'object') {
if (throws) {
throw new Error(
'Invalid purl: "qualifiers" argument must be an object.'
)
throw new PurlError('"qualifiers" must be an object')
}
return false
}
Expand Down Expand Up @@ -74,8 +71,8 @@ function validateQualifierKey(key, throws) {
)
) {
if (throws) {
throw new Error(
`Invalid purl: qualifier "${key}" contains an illegal character.`
throw new PurlError(
`qualifier "${key}" contains an illegal character`
)
}
return false
Expand All @@ -87,7 +84,7 @@ function validateQualifierKey(key, throws) {
function validateRequired(name, value, throws) {
if (isNullishOrEmptyString(value)) {
if (throws) {
throw new Error(`Invalid purl: "${name}" is a required field.`)
throw new PurlError(`"${name}" is a required component`)
}
return false
}
Expand All @@ -97,7 +94,7 @@ function validateRequired(name, value, throws) {
function validateRequiredByType(type, name, value, throws) {
if (isNullishOrEmptyString(value)) {
if (throws) {
throw new Error(`Invalid purl: ${type} requires a "${name}" field.`)
throw new PurlError(`${type} requires a "${name}" component`)
}
return false
}
Expand All @@ -109,8 +106,8 @@ function validateStartsWithoutNumber(name, value, throws) {
const code = value.charCodeAt(0)
if (code >= 48 /*'0'*/ && code <= 57 /*'9'*/) {
if (throws) {
throw new Error(
`Invalid purl: ${name} "${value}" cannot start with a number.`
throw new PurlError(
`${name} "${value}" cannot start with a number`
)
}
return false
Expand All @@ -124,7 +121,7 @@ function validateStrings(name, value, throws) {
return true
}
if (throws) {
throw new Error(`Invalid purl: "'${name}" argument must be a string.`)
throw new PurlError(`"'${name}" must be a string`)
}
return false
}
Expand Down Expand Up @@ -160,8 +157,8 @@ function validateType(type, throws) {
)
) {
if (throws) {
throw new Error(
`Invalid purl: type "${type}" contains an illegal character.`
throw new PurlError(
`type "${type}" contains an illegal character`
)
}
return false
Expand Down
72 changes: 0 additions & 72 deletions test/data/contrib-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,6 @@
"subpath": null,
"is_invalid": true
},
{
"description": "improperly encoded version string",
"purl": "pkg:maven/org.apache.commons/[email protected]$@",
"canonical_purl": "pkg:maven/org.apache.commons/[email protected]$@",
"type": null,
"namespace": null,
"name": "io",
"version": "1.4.0-$@",
"qualifiers": null,
"subpath": null,
"is_invalid": true
},
{
"description": "leading and trailing slashes '/' are not significant and should be stripped in the canonical form",
"purl": "pkg:golang//github.com///ll////[email protected]",
Expand All @@ -142,65 +130,5 @@
"qualifiers": null,
"subpath": null,
"is_invalid": false
},
{
"description": "percent encoded namespace",
"purl": "pkg:type/100%/name",
"canonical_purl": "pkg:type/100%25/name",
"type": "type",
"namespace": "100%",
"name": "name",
"version": null,
"qualifiers": null,
"subpath": null,
"is_invalid": false
},
{
"description": "percent encoded name",
"purl": "pkg:type/namespace/100%",
"canonical_purl": "pkg:type/namespace/100%25",
"type": "type",
"namespace": "namespace",
"name": "100%",
"version": null,
"qualifiers": null,
"subpath": null,
"is_invalid": false
},
{
"description": "percent encoded version",
"purl": "pkg:type/namespace/name@100%",
"canonical_purl": "pkg:type/namespace/name@100%25",
"type": "type",
"namespace": "namespace",
"name": "name",
"version": "100%",
"qualifiers": null,
"subpath": null,
"is_invalid": false
},
{
"description": "percent encoded qualifiers",
"purl": "pkg:type/namespace/[email protected]?a=100%",
"canonical_purl": "pkg:type/namespace/[email protected]?a=100%25",
"type": "type",
"namespace": "namespace",
"name": "name",
"version": "1.0",
"qualifiers": { "a": "100%" },
"subpath": null,
"is_invalid": false
},
{
"description": "percent encoded subpath",
"purl": "pkg:type/namespace/[email protected]#100%",
"canonical_purl": "pkg:type/namespace/[email protected]#100%25",
"type": "type",
"namespace": "namespace",
"name": "name",
"version": "1.0",
"qualifiers": null,
"subpath": "100%",
"is_invalid": false
}
]
Loading

0 comments on commit f7dccd6

Please sign in to comment.