Skip to content

Commit

Permalink
fix: add npm name validation and make lower casing names adaptive
Browse files Browse the repository at this point in the history
  • Loading branch information
jdalton committed Nov 15, 2024
1 parent cd1eb4b commit d186b40
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 39 deletions.
158 changes: 138 additions & 20 deletions src/purl-type.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
'use strict'

const { encodeURIComponent } = require('./encode')
const { isNullishOrEmptyString } = require('./lang')

const { createHelpersNamespaceObject } = require('./helpers')

const {
isNonEmptyString,
isSemverString,
lowerName,
lowerNamespace,
lowerVersion,
replaceDashesWithUnderscores,
replaceUnderscoresWithDashes
} = require('./strings')

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

const PurlTypNormalizer = (purl) => purl
const scopedPackagePattern = /^(?:@([^\/]+?)[\/])?([^\/]+?)$/

const PurlTypNormalizer = (purl) => purl
const PurlTypeValidator = (_purl, _throws) => true

module.exports = {
Expand Down Expand Up @@ -104,7 +104,12 @@ module.exports = {
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm
npm(purl) {
lowerNamespace(purl)
lowerName(purl)
// Ignore lowercasing names in cases where it might be a
// legacy name because they could be mixed case.
// https://github.com/npm/validate-npm-package-name/tree/v6.0.0?tab=readme-ov-file#legacy-names
if (isNonEmptyString(purl.namespace)) {
lowerName(purl)
}
return purl
},
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#luarocks
Expand Down Expand Up @@ -217,6 +222,119 @@ module.exports = {
throws
)
},
// Validation based on
// https://www.npmjs.com/package/validate-npm-package-name
// ISC License
// Copyright (c) 2015, npm, Inc
npm(purl, throws) {
const { name, namespace: rawNamespace } = purl
const namespace = isNonEmptyString(rawNamespace)
? rawNamespace
: ''
const hasNamespace = namespace.length > 0
const compName = hasNamespace ? 'namespace' : 'name'
const id = `${hasNamespace ? `${namespace}/` : ''}${name}`
const code0 = id.charCodeAt(0)
if (code0 === 46 /*'.'*/) {
if (throws) {
throw new PurlError(
`npm "${compName}" component cannot start with a period`
)
}
return false
}
if (code0 === 95 /*'_'*/) {
if (throws) {
throw new PurlError(
`npm "${compName}" component cannot start with an underscore`
)
}
return false
}
const loweredId = id.toLowerCase()
if (
loweredId === 'node_modules' ||
loweredId === 'favicon.ico'
) {
if (throws) {
throw new PurlError(
`npm "${compName}" component of "${loweredId}" is not allowed`
)
}
return false
}
if (hasNamespace) {
if (code0 !== 64 /*'@'*/) {
throw new PurlError(
`npm "namespace" component must start with an "@" character`
)
}
if (namespace.trim() !== namespace) {
if (throws) {
throw new PurlError(
'npm "namespace" component cannot contain leading or trailing spaces'
)
}
return false
}
const namespaceWithoutAtSign = namespace.slice(1)
if (
encodeURIComponent(namespaceWithoutAtSign) !==
namespaceWithoutAtSign
) {
if (throws) {
throw new PurlError(
`npm "namespace" component can only contain URL-friendly characters`
)
}
return false
}
// The remaining checks in this block are modern name
// restrictions. We apply these checks when a namespace
// is present because legacy names did not have namespaces.
if (id.length > 214) {
if (throws) {
throw new PurlError(
`npm "namespace" and "name" components can not collectively be more than 214 characters`
)
}
return false
}
if (loweredId !== id) {
if (throws) {
throw new PurlError(
`npm "name" component can not contain capital letters`
)
}
return false
}
if (/[~'!()*]/.test(name)) {
if (throws) {
throw new PurlError(
`npm "name" component can not contain special characters ("~\'!()*")`
)
}
return false
}
}
if (name.trim() !== name) {
if (throws) {
throw new PurlError(
'npm "name" component cannot contain leading or trailing spaces'
)
}
return false
}
if (encodeURIComponent(name) !== name) {
if (throws) {
throw new PurlError(
`npm "name" component can only contain URL-friendly characters`
)
}
return false
}
return true
},
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci
oci(purl, throws) {
return validateEmptyByType(
Expand All @@ -233,21 +351,21 @@ module.exports = {
const code = name.charCodeAt(i)
// prettier-ignore
if (
!(
(
(code >= 48 && code <= 57) || // 0-9
(code >= 97 && code <= 122) || // a-z
code === 95 // _
)
)
) {
if (throws) {
throw new PurlError(
'pub "name" component may only contain [a-z0-9_] characters'
)
}
return false
}
!(
(
(code >= 48 && code <= 57) || // 0-9
(code >= 97 && code <= 122) || // a-z
code === 95 // _
)
)
) {
if (throws) {
throw new PurlError(
'pub "name" component may only contain [a-z0-9_] characters'
)
}
return false
}
}
return true
},
Expand Down
38 changes: 19 additions & 19 deletions test/data/contrib-tests.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"description": "scheme is lowercased",
"purl": "PkG:npm/foo/[email protected]",
"canonical_purl": "pkg:npm/foo/[email protected]",
"type": "npm",
"purl": "PkG:type/foo/[email protected]",
"canonical_purl": "pkg:type/foo/[email protected]",
"type": "type",
"namespace": "foo",
"name": "bar",
"version": "1.0.0",
Expand Down Expand Up @@ -61,22 +61,22 @@
},
{
"description": "namespace can contain special characters",
"purl": "pkg:npm/%40foo%40%3F%23/bar@1.0.0",
"canonical_purl": "pkg:npm/%40foo%40%3F%23/bar@1.0.0",
"type": "npm",
"namespace": "@foo@?#",
"name": "bar",
"purl": "pkg:type/%40namespace%40%3F%23/name@1.0.0",
"canonical_purl": "pkg:type/%40namespace%40%3F%23/name@1.0.0",
"type": "type",
"namespace": "@namespace@?#",
"name": "name",
"version": "1.0.0",
"qualifiers": null,
"subpath": null,
"is_invalid": false
},
{
"description": "name can contain special characters (with namespace)",
"purl": "pkg:npm/%40foo/bar%40%3F%[email protected]",
"canonical_purl": "pkg:npm/%40foo/bar%40%3F%[email protected]",
"type": "npm",
"namespace": "@foo",
"purl": "pkg:type/foo/bar%40%3F%[email protected]",
"canonical_purl": "pkg:type/foo/bar%40%3F%[email protected]",
"type": "type",
"namespace": "foo",
"name": "bar@?#",
"version": "1.0.0",
"qualifiers": null,
Expand All @@ -85,9 +85,9 @@
},
{
"description": "name can contain special characters (without namespace)",
"purl": "pkg:npm/bar%40%3F%[email protected]",
"canonical_purl": "pkg:npm/bar%40%3F%[email protected]",
"type": "npm",
"purl": "pkg:type/bar%40%3F%[email protected]",
"canonical_purl": "pkg:type/bar%40%3F%[email protected]",
"type": "type",
"namespace": null,
"name": "bar@?#",
"version": "1.0.0",
Expand All @@ -97,10 +97,10 @@
},
{
"description": "version can contain special characters",
"purl": "pkg:npm/%40foo/[email protected]%40%3F%23",
"canonical_purl": "pkg:npm/%40foo/[email protected]%40%3F%23",
"type": "npm",
"namespace": "@foo",
"purl": "pkg:type/foo/[email protected]%40%3F%23",
"canonical_purl": "pkg:type/foo/[email protected]%40%3F%23",
"type": "type",
"namespace": "foo",
"name": "bar",
"version": "1.0.0-@?#",
"qualifiers": null,
Expand Down

0 comments on commit d186b40

Please sign in to comment.