Skip to content

Commit d186b40

Browse files
committed
fix: add npm name validation and make lower casing names adaptive
1 parent cd1eb4b commit d186b40

File tree

2 files changed

+157
-39
lines changed

2 files changed

+157
-39
lines changed

src/purl-type.js

Lines changed: 138 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
'use strict'
22

3+
const { encodeURIComponent } = require('./encode')
34
const { isNullishOrEmptyString } = require('./lang')
4-
55
const { createHelpersNamespaceObject } = require('./helpers')
6-
76
const {
7+
isNonEmptyString,
88
isSemverString,
99
lowerName,
1010
lowerNamespace,
1111
lowerVersion,
1212
replaceDashesWithUnderscores,
1313
replaceUnderscoresWithDashes
1414
} = require('./strings')
15-
1615
const { validateEmptyByType, validateRequiredByType } = require('./validate')
1716
const { PurlError } = require('./error')
1817

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

20+
const PurlTypNormalizer = (purl) => purl
2121
const PurlTypeValidator = (_purl, _throws) => true
2222

2323
module.exports = {
@@ -104,7 +104,12 @@ module.exports = {
104104
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm
105105
npm(purl) {
106106
lowerNamespace(purl)
107-
lowerName(purl)
107+
// Ignore lowercasing names in cases where it might be a
108+
// legacy name because they could be mixed case.
109+
// https://github.com/npm/validate-npm-package-name/tree/v6.0.0?tab=readme-ov-file#legacy-names
110+
if (isNonEmptyString(purl.namespace)) {
111+
lowerName(purl)
112+
}
108113
return purl
109114
},
110115
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#luarocks
@@ -217,6 +222,119 @@ module.exports = {
217222
throws
218223
)
219224
},
225+
// Validation based on
226+
// https://www.npmjs.com/package/validate-npm-package-name
227+
// ISC License
228+
// Copyright (c) 2015, npm, Inc
229+
npm(purl, throws) {
230+
const { name, namespace: rawNamespace } = purl
231+
const namespace = isNonEmptyString(rawNamespace)
232+
? rawNamespace
233+
: ''
234+
const hasNamespace = namespace.length > 0
235+
const compName = hasNamespace ? 'namespace' : 'name'
236+
const id = `${hasNamespace ? `${namespace}/` : ''}${name}`
237+
const code0 = id.charCodeAt(0)
238+
if (code0 === 46 /*'.'*/) {
239+
if (throws) {
240+
throw new PurlError(
241+
`npm "${compName}" component cannot start with a period`
242+
)
243+
}
244+
return false
245+
}
246+
if (code0 === 95 /*'_'*/) {
247+
if (throws) {
248+
throw new PurlError(
249+
`npm "${compName}" component cannot start with an underscore`
250+
)
251+
}
252+
return false
253+
}
254+
const loweredId = id.toLowerCase()
255+
if (
256+
loweredId === 'node_modules' ||
257+
loweredId === 'favicon.ico'
258+
) {
259+
if (throws) {
260+
throw new PurlError(
261+
`npm "${compName}" component of "${loweredId}" is not allowed`
262+
)
263+
}
264+
return false
265+
}
266+
if (hasNamespace) {
267+
if (code0 !== 64 /*'@'*/) {
268+
throw new PurlError(
269+
`npm "namespace" component must start with an "@" character`
270+
)
271+
}
272+
if (namespace.trim() !== namespace) {
273+
if (throws) {
274+
throw new PurlError(
275+
'npm "namespace" component cannot contain leading or trailing spaces'
276+
)
277+
}
278+
return false
279+
}
280+
const namespaceWithoutAtSign = namespace.slice(1)
281+
if (
282+
encodeURIComponent(namespaceWithoutAtSign) !==
283+
namespaceWithoutAtSign
284+
) {
285+
if (throws) {
286+
throw new PurlError(
287+
`npm "namespace" component can only contain URL-friendly characters`
288+
)
289+
}
290+
return false
291+
}
292+
// The remaining checks in this block are modern name
293+
// restrictions. We apply these checks when a namespace
294+
// is present because legacy names did not have namespaces.
295+
if (id.length > 214) {
296+
if (throws) {
297+
throw new PurlError(
298+
`npm "namespace" and "name" components can not collectively be more than 214 characters`
299+
)
300+
}
301+
return false
302+
}
303+
if (loweredId !== id) {
304+
if (throws) {
305+
throw new PurlError(
306+
`npm "name" component can not contain capital letters`
307+
)
308+
}
309+
return false
310+
}
311+
if (/[~'!()*]/.test(name)) {
312+
if (throws) {
313+
throw new PurlError(
314+
`npm "name" component can not contain special characters ("~\'!()*")`
315+
)
316+
}
317+
return false
318+
}
319+
}
320+
if (name.trim() !== name) {
321+
if (throws) {
322+
throw new PurlError(
323+
'npm "name" component cannot contain leading or trailing spaces'
324+
)
325+
}
326+
return false
327+
}
328+
if (encodeURIComponent(name) !== name) {
329+
if (throws) {
330+
throw new PurlError(
331+
`npm "name" component can only contain URL-friendly characters`
332+
)
333+
}
334+
return false
335+
}
336+
return true
337+
},
220338
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci
221339
oci(purl, throws) {
222340
return validateEmptyByType(
@@ -233,21 +351,21 @@ module.exports = {
233351
const code = name.charCodeAt(i)
234352
// prettier-ignore
235353
if (
236-
!(
237-
(
238-
(code >= 48 && code <= 57) || // 0-9
239-
(code >= 97 && code <= 122) || // a-z
240-
code === 95 // _
241-
)
242-
)
243-
) {
244-
if (throws) {
245-
throw new PurlError(
246-
'pub "name" component may only contain [a-z0-9_] characters'
247-
)
248-
}
249-
return false
250-
}
354+
!(
355+
(
356+
(code >= 48 && code <= 57) || // 0-9
357+
(code >= 97 && code <= 122) || // a-z
358+
code === 95 // _
359+
)
360+
)
361+
) {
362+
if (throws) {
363+
throw new PurlError(
364+
'pub "name" component may only contain [a-z0-9_] characters'
365+
)
366+
}
367+
return false
368+
}
251369
}
252370
return true
253371
},

test/data/contrib-tests.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[
22
{
33
"description": "scheme is lowercased",
4-
"purl": "PkG:npm/foo/[email protected]",
5-
"canonical_purl": "pkg:npm/foo/[email protected]",
6-
"type": "npm",
4+
"purl": "PkG:type/foo/[email protected]",
5+
"canonical_purl": "pkg:type/foo/[email protected]",
6+
"type": "type",
77
"namespace": "foo",
88
"name": "bar",
99
"version": "1.0.0",
@@ -61,22 +61,22 @@
6161
},
6262
{
6363
"description": "namespace can contain special characters",
64-
"purl": "pkg:npm/%40foo%40%3F%23/bar@1.0.0",
65-
"canonical_purl": "pkg:npm/%40foo%40%3F%23/bar@1.0.0",
66-
"type": "npm",
67-
"namespace": "@foo@?#",
68-
"name": "bar",
64+
"purl": "pkg:type/%40namespace%40%3F%23/name@1.0.0",
65+
"canonical_purl": "pkg:type/%40namespace%40%3F%23/name@1.0.0",
66+
"type": "type",
67+
"namespace": "@namespace@?#",
68+
"name": "name",
6969
"version": "1.0.0",
7070
"qualifiers": null,
7171
"subpath": null,
7272
"is_invalid": false
7373
},
7474
{
7575
"description": "name can contain special characters (with namespace)",
76-
"purl": "pkg:npm/%40foo/bar%40%3F%[email protected]",
77-
"canonical_purl": "pkg:npm/%40foo/bar%40%3F%[email protected]",
78-
"type": "npm",
79-
"namespace": "@foo",
76+
"purl": "pkg:type/foo/bar%40%3F%[email protected]",
77+
"canonical_purl": "pkg:type/foo/bar%40%3F%[email protected]",
78+
"type": "type",
79+
"namespace": "foo",
8080
"name": "bar@?#",
8181
"version": "1.0.0",
8282
"qualifiers": null,
@@ -85,9 +85,9 @@
8585
},
8686
{
8787
"description": "name can contain special characters (without namespace)",
88-
"purl": "pkg:npm/bar%40%3F%[email protected]",
89-
"canonical_purl": "pkg:npm/bar%40%3F%[email protected]",
90-
"type": "npm",
88+
"purl": "pkg:type/bar%40%3F%[email protected]",
89+
"canonical_purl": "pkg:type/bar%40%3F%[email protected]",
90+
"type": "type",
9191
"namespace": null,
9292
"name": "bar@?#",
9393
"version": "1.0.0",
@@ -97,10 +97,10 @@
9797
},
9898
{
9999
"description": "version can contain special characters",
100-
"purl": "pkg:npm/%40foo/[email protected]%40%3F%23",
101-
"canonical_purl": "pkg:npm/%40foo/[email protected]%40%3F%23",
102-
"type": "npm",
103-
"namespace": "@foo",
100+
"purl": "pkg:type/foo/[email protected]%40%3F%23",
101+
"canonical_purl": "pkg:type/foo/[email protected]%40%3F%23",
102+
"type": "type",
103+
"namespace": "foo",
104104
"name": "bar",
105105
"version": "1.0.0-@?#",
106106
"qualifiers": null,

0 commit comments

Comments
 (0)