Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1e41678

Browse files
committed
1 parent e9cf30e commit 1e41678

File tree

4 files changed

+172
-106
lines changed

4 files changed

+172
-106
lines changed

node_modules/npm-package-arg/lib/npa.js

Lines changed: 164 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
'use strict'
2-
module.exports = npa
3-
module.exports.resolve = resolve
4-
module.exports.toPurl = toPurl
5-
module.exports.Result = Result
62

7-
const { URL } = require('url')
3+
const isWindows = process.platform === 'win32'
4+
5+
const { URL } = require('node:url')
6+
// We need to use path/win32 so that we get consistent results in tests, but this also means we need to manually convert backslashes to forward slashes when generating file: urls with paths.
7+
const path = isWindows ? require('node:path/win32') : require('node:path')
8+
const { homedir } = require('node:os')
89
const HostedGit = require('hosted-git-info')
910
const semver = require('semver')
10-
const path = global.FAKE_WINDOWS ? require('path').win32 : require('path')
1111
const validatePackageName = require('validate-npm-package-name')
12-
const { homedir } = require('os')
1312
const { log } = require('proc-log')
1413

15-
const isWindows = process.platform === 'win32' || global.FAKE_WINDOWS
1614
const hasSlashes = isWindows ? /\\|[/]/ : /[/]/
1715
const isURL = /^(?:git[+])?[a-z]+:/i
1816
const isGit = /^[^@]+@[^:.]+\.[^:]+:.+$/i
19-
const isFilename = /[.](?:tgz|tar.gz|tar)$/i
17+
const isFileType = /[.](?:tgz|tar.gz|tar)$/i
18+
const isPortNumber = /:[0-9]+(\/|$)/i
19+
const isWindowsFile = /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/
20+
const isPosixFile = /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/
21+
const defaultRegistry = 'https://registry.npmjs.org'
2022

2123
function npa (arg, where) {
2224
let name
@@ -30,13 +32,14 @@ function npa (arg, where) {
3032
return npa(arg.raw, where || arg.where)
3133
}
3234
}
33-
const nameEndsAt = arg[0] === '@' ? arg.slice(1).indexOf('@') + 1 : arg.indexOf('@')
35+
const nameEndsAt = arg.indexOf('@', 1) // Skip possible leading @
3436
const namePart = nameEndsAt > 0 ? arg.slice(0, nameEndsAt) : arg
3537
if (isURL.test(arg)) {
3638
spec = arg
3739
} else if (isGit.test(arg)) {
3840
spec = `git+ssh://${arg}`
39-
} else if (namePart[0] !== '@' && (hasSlashes.test(namePart) || isFilename.test(namePart))) {
41+
// eslint-disable-next-line max-len
42+
} else if (!namePart.startsWith('@') && (hasSlashes.test(namePart) || isFileType.test(namePart))) {
4043
spec = arg
4144
} else if (nameEndsAt > 0) {
4245
name = namePart
@@ -53,7 +56,27 @@ function npa (arg, where) {
5356
return resolve(name, spec, where, arg)
5457
}
5558

56-
const isFilespec = isWindows ? /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ : /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/
59+
function isFileSpec (spec) {
60+
if (!spec) {
61+
return false
62+
}
63+
if (spec.toLowerCase().startsWith('file:')) {
64+
return true
65+
}
66+
if (isWindows) {
67+
return isWindowsFile.test(spec)
68+
}
69+
// We never hit this in windows tests, obviously
70+
/* istanbul ignore next */
71+
return isPosixFile.test(spec)
72+
}
73+
74+
function isAliasSpec (spec) {
75+
if (!spec) {
76+
return false
77+
}
78+
return spec.toLowerCase().startsWith('npm:')
79+
}
5780

5881
function resolve (name, spec, where, arg) {
5982
const res = new Result({
@@ -64,12 +87,16 @@ function resolve (name, spec, where, arg) {
6487
})
6588

6689
if (name) {
67-
res.setName(name)
90+
res.name = name
6891
}
6992

70-
if (spec && (isFilespec.test(spec) || /^file:/i.test(spec))) {
93+
if (!where) {
94+
where = process.cwd()
95+
}
96+
97+
if (isFileSpec(spec)) {
7198
return fromFile(res, where)
72-
} else if (spec && /^npm:/i.test(spec)) {
99+
} else if (isAliasSpec(spec)) {
73100
return fromAlias(res, where)
74101
}
75102

@@ -81,15 +108,13 @@ function resolve (name, spec, where, arg) {
81108
return fromHostedGit(res, hosted)
82109
} else if (spec && isURL.test(spec)) {
83110
return fromURL(res)
84-
} else if (spec && (hasSlashes.test(spec) || isFilename.test(spec))) {
111+
} else if (spec && (hasSlashes.test(spec) || isFileType.test(spec))) {
85112
return fromFile(res, where)
86113
} else {
87114
return fromRegistry(res)
88115
}
89116
}
90117

91-
const defaultRegistry = 'https://registry.npmjs.org'
92-
93118
function toPurl (arg, reg = defaultRegistry) {
94119
const res = npa(arg)
95120

@@ -127,60 +152,62 @@ function invalidPurlType (type, raw) {
127152
return err
128153
}
129154

130-
function Result (opts) {
131-
this.type = opts.type
132-
this.registry = opts.registry
133-
this.where = opts.where
134-
if (opts.raw == null) {
135-
this.raw = opts.name ? opts.name + '@' + opts.rawSpec : opts.rawSpec
136-
} else {
137-
this.raw = opts.raw
155+
class Result {
156+
constructor (opts) {
157+
this.type = opts.type
158+
this.registry = opts.registry
159+
this.where = opts.where
160+
if (opts.raw == null) {
161+
this.raw = opts.name ? `${opts.name}@${opts.rawSpec}` : opts.rawSpec
162+
} else {
163+
this.raw = opts.raw
164+
}
165+
this.name = undefined
166+
this.escapedName = undefined
167+
this.scope = undefined
168+
this.rawSpec = opts.rawSpec || ''
169+
this.saveSpec = opts.saveSpec
170+
this.fetchSpec = opts.fetchSpec
171+
if (opts.name) {
172+
this.setName(opts.name)
173+
}
174+
this.gitRange = opts.gitRange
175+
this.gitCommittish = opts.gitCommittish
176+
this.gitSubdir = opts.gitSubdir
177+
this.hosted = opts.hosted
138178
}
139179

140-
this.name = undefined
141-
this.escapedName = undefined
142-
this.scope = undefined
143-
this.rawSpec = opts.rawSpec || ''
144-
this.saveSpec = opts.saveSpec
145-
this.fetchSpec = opts.fetchSpec
146-
if (opts.name) {
147-
this.setName(opts.name)
148-
}
149-
this.gitRange = opts.gitRange
150-
this.gitCommittish = opts.gitCommittish
151-
this.gitSubdir = opts.gitSubdir
152-
this.hosted = opts.hosted
153-
}
180+
// TODO move this to a getter/setter in a semver major
181+
setName (name) {
182+
const valid = validatePackageName(name)
183+
if (!valid.validForOldPackages) {
184+
throw invalidPackageName(name, valid, this.raw)
185+
}
154186

155-
Result.prototype.setName = function (name) {
156-
const valid = validatePackageName(name)
157-
if (!valid.validForOldPackages) {
158-
throw invalidPackageName(name, valid, this.raw)
187+
this.name = name
188+
this.scope = name[0] === '@' ? name.slice(0, name.indexOf('/')) : undefined
189+
// scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
190+
this.escapedName = name.replace('/', '%2f')
191+
return this
159192
}
160193

161-
this.name = name
162-
this.scope = name[0] === '@' ? name.slice(0, name.indexOf('/')) : undefined
163-
// scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
164-
this.escapedName = name.replace('/', '%2f')
165-
return this
166-
}
167-
168-
Result.prototype.toString = function () {
169-
const full = []
170-
if (this.name != null && this.name !== '') {
171-
full.push(this.name)
172-
}
173-
const spec = this.saveSpec || this.fetchSpec || this.rawSpec
174-
if (spec != null && spec !== '') {
175-
full.push(spec)
194+
toString () {
195+
const full = []
196+
if (this.name != null && this.name !== '') {
197+
full.push(this.name)
198+
}
199+
const spec = this.saveSpec || this.fetchSpec || this.rawSpec
200+
if (spec != null && spec !== '') {
201+
full.push(spec)
202+
}
203+
return full.length ? full.join('@') : this.raw
176204
}
177-
return full.length ? full.join('@') : this.raw
178-
}
179205

180-
Result.prototype.toJSON = function () {
181-
const result = Object.assign({}, this)
182-
delete result.hosted
183-
return result
206+
toJSON () {
207+
const result = Object.assign({}, this)
208+
delete result.hosted
209+
return result
210+
}
184211
}
185212

186213
// sets res.gitCommittish, res.gitRange, and res.gitSubdir
@@ -227,25 +254,67 @@ function setGitAttrs (res, committish) {
227254
}
228255
}
229256

230-
function fromFile (res, where) {
231-
if (!where) {
232-
where = process.cwd()
257+
// Taken from: EncodePathChars and lookup_table in src/node_url.cc
258+
// url.pathToFileURL only returns absolute references. We can't use it to encode paths.
259+
// encodeURI mangles windows paths. We can't use it to encode paths.
260+
// Under the hood, url.pathToFileURL does a limited set of encoding, with an extra windows step, and then calls path.resolve.
261+
// The encoding node does without path.resolve is not available outside of the source, so we are recreating it here.
262+
const encodedPathChars = new Map([
263+
['\0', '%00'],
264+
['\t', '%09'],
265+
['\n', '%0A'],
266+
['\r', '%0D'],
267+
[' ', '%20'],
268+
['"', '%22'],
269+
['#', '%23'],
270+
['%', '%25'],
271+
['?', '%3F'],
272+
['[', '%5B'],
273+
['\\', isWindows ? '/' : '%5C'],
274+
[']', '%5D'],
275+
['^', '%5E'],
276+
['|', '%7C'],
277+
['~', '%7E'],
278+
])
279+
280+
function pathToFileURL (str) {
281+
let result = ''
282+
for (let i = 0; i < str.length; i++) {
283+
result = `${result}${encodedPathChars.get(str[i]) ?? str[i]}`
284+
}
285+
if (result.startsWith('file:')) {
286+
return result
233287
}
234-
res.type = isFilename.test(res.rawSpec) ? 'file' : 'directory'
288+
return `file:${result}`
289+
}
290+
291+
function fromFile (res, where) {
292+
res.type = isFileType.test(res.rawSpec) ? 'file' : 'directory'
235293
res.where = where
236294

237-
// always put the '/' on where when resolving urls, or else
238-
// file:foo from /path/to/bar goes to /path/to/foo, when we want
239-
// it to be /path/to/bar/foo
295+
let rawSpec = pathToFileURL(res.rawSpec)
296+
297+
if (rawSpec.startsWith('file:/')) {
298+
// XXX backwards compatibility lack of compliance with RFC 8089
299+
300+
// turn file://path into file:/path
301+
if (/^file:\/\/[^/]/.test(rawSpec)) {
302+
rawSpec = `file:/${rawSpec.slice(5)}`
303+
}
304+
305+
// turn file:/../path into file:../path
306+
// for 1 or 3 leading slashes (2 is already ruled out from handling file:// explicitly above)
307+
if (/^\/{1,3}\.\.?(\/|$)/.test(rawSpec.slice(5))) {
308+
rawSpec = rawSpec.replace(/^file:\/{1,3}/, 'file:')
309+
}
310+
}
240311

241-
let specUrl
242312
let resolvedUrl
243-
const prefix = (!/^file:/.test(res.rawSpec) ? 'file:' : '')
244-
const rawWithPrefix = prefix + res.rawSpec
245-
let rawNoPrefix = rawWithPrefix.replace(/^file:/, '')
313+
let specUrl
246314
try {
247-
resolvedUrl = new URL(rawWithPrefix, `file://${path.resolve(where)}/`)
248-
specUrl = new URL(rawWithPrefix)
315+
// always put the '/' on "where", or else file:foo from /path/to/bar goes to /path/to/foo, when we want it to be /path/to/bar/foo
316+
resolvedUrl = new URL(rawSpec, `${pathToFileURL(path.resolve(where))}/`)
317+
specUrl = new URL(rawSpec)
249318
} catch (originalError) {
250319
const er = new Error('Invalid file: URL, must comply with RFC 8089')
251320
throw Object.assign(er, {
@@ -256,24 +325,6 @@ function fromFile (res, where) {
256325
})
257326
}
258327

259-
// XXX backwards compatibility lack of compliance with RFC 8089
260-
if (resolvedUrl.host && resolvedUrl.host !== 'localhost') {
261-
const rawSpec = res.rawSpec.replace(/^file:\/\//, 'file:///')
262-
resolvedUrl = new URL(rawSpec, `file://${path.resolve(where)}/`)
263-
specUrl = new URL(rawSpec)
264-
rawNoPrefix = rawSpec.replace(/^file:/, '')
265-
}
266-
// turn file:/../foo into file:../foo
267-
// for 1, 2 or 3 leading slashes since we attempted
268-
// in the previous step to make it a file protocol url with a leading slash
269-
if (/^\/{1,3}\.\.?(\/|$)/.test(rawNoPrefix)) {
270-
const rawSpec = res.rawSpec.replace(/^file:\/{1,3}/, 'file:')
271-
resolvedUrl = new URL(rawSpec, `file://${path.resolve(where)}/`)
272-
specUrl = new URL(rawSpec)
273-
rawNoPrefix = rawSpec.replace(/^file:/, '')
274-
}
275-
// XXX end RFC 8089 violation backwards compatibility section
276-
277328
// turn /C:/blah into just C:/blah on windows
278329
let specPath = decodeURIComponent(specUrl.pathname)
279330
let resolvedPath = decodeURIComponent(resolvedUrl.pathname)
@@ -287,13 +338,21 @@ function fromFile (res, where) {
287338
if (/^\/~(\/|$)/.test(specPath)) {
288339
res.saveSpec = `file:${specPath.substr(1)}`
289340
resolvedPath = path.resolve(homedir(), specPath.substr(3))
290-
} else if (!path.isAbsolute(rawNoPrefix)) {
341+
} else if (!path.isAbsolute(rawSpec.slice(5))) {
291342
res.saveSpec = `file:${path.relative(where, resolvedPath)}`
292343
} else {
293344
res.saveSpec = `file:${path.resolve(resolvedPath)}`
294345
}
295346

296347
res.fetchSpec = path.resolve(where, resolvedPath)
348+
// re-normalize the slashes in saveSpec due to node:path/win32 behavior in windows
349+
res.saveSpec = res.saveSpec.split('\\').join('/')
350+
// Ignoring because this only happens in windows
351+
/* istanbul ignore next */
352+
if (res.saveSpec.startsWith('file://')) {
353+
// normalization of \\win32\root paths can cause a double / which we don't want
354+
res.saveSpec = `file:/${res.saveSpec.slice(7)}`
355+
}
297356
return res
298357
}
299358

@@ -324,7 +383,9 @@ function fromURL (res) {
324383
// git+ssh://[email protected]:username/project.git#deadbeef
325384
// ...and various combinations. The username in the beginning is *required*.
326385
const matched = rawSpec.match(/^git\+ssh:\/\/([^:#]+:[^#]+(?:\.git)?)(?:#(.*))?$/i)
327-
if (matched && !matched[1].match(/:[0-9]+\/?.*$/i)) {
386+
// Filter out all-number "usernames" which are really port numbers
387+
// They can either be :1234 :1234/ or :1234/path but not :12abc
388+
if (matched && !matched[1].match(isPortNumber)) {
328389
res.type = 'git'
329390
setGitAttrs(res, matched[2])
330391
res.fetchSpec = matched[1]
@@ -413,3 +474,8 @@ function fromRegistry (res) {
413474
}
414475
return res
415476
}
477+
478+
module.exports = npa
479+
module.exports.resolve = resolve
480+
module.exports.toPurl = toPurl
481+
module.exports.Result = Result

0 commit comments

Comments
 (0)