Skip to content

Commit 26f3d0b

Browse files
authored
fix: use hosted-git-info to parse registry urls (#5761)
Previously this was using `new URL` which would fail on some urls that `hosted-git-info` is able to parse. But if we still get a url that can't be parsed, we now set it to be removed from the tree instead of erroring. Fixes: #5278
1 parent 292156c commit 26f3d0b

File tree

9 files changed

+229
-135
lines changed

9 files changed

+229
-135
lines changed

DEPENDENCIES.md

+2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ graph LR;
149149
npm-registry-fetch-->proc-log;
150150
npmcli-arborist-->bin-links;
151151
npmcli-arborist-->cacache;
152+
npmcli-arborist-->hosted-git-info;
152153
npmcli-arborist-->nopt;
153154
npmcli-arborist-->npm-install-checks;
154155
npmcli-arborist-->npm-package-arg;
@@ -578,6 +579,7 @@ graph LR;
578579
npmcli-arborist-->cacache;
579580
npmcli-arborist-->chalk;
580581
npmcli-arborist-->common-ancestor-path;
582+
npmcli-arborist-->hosted-git-info;
581583
npmcli-arborist-->isaacs-string-locale-compare["@isaacs/string-locale-compare"];
582584
npmcli-arborist-->json-parse-even-better-errors;
583585
npmcli-arborist-->json-stringify-nice;
+28-116
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
11
'use strict'
2-
const url = require('url')
32
const gitHosts = require('./git-host-info.js')
43
const GitHost = module.exports = require('./git-host.js')
54
const LRU = require('lru-cache')
6-
const cache = new LRU({ max: 1000 })
7-
8-
const protocolToRepresentationMap = {
9-
'git+ssh:': 'sshurl',
10-
'git+https:': 'https',
11-
'ssh:': 'sshurl',
12-
'git:': 'git',
13-
}
5+
const parseUrl = require('./parse-url.js')
146

15-
function protocolToRepresentation (protocol) {
16-
return protocolToRepresentationMap[protocol] || protocol.slice(0, -1)
17-
}
7+
const cache = new LRU({ max: 1000 })
188

19-
const authProtocols = {
20-
'git:': true,
21-
'https:': true,
22-
'git+https:': true,
23-
'http:': true,
24-
'git+http:': true,
9+
const protocols = {
10+
'git+ssh:': { name: 'sshurl' },
11+
'ssh:': { name: 'sshurl' },
12+
'git+https:': { name: 'https', auth: true },
13+
'git:': { auth: true },
14+
'http:': { auth: true },
15+
'https:': { auth: true },
16+
'git+http:': { auth: true },
17+
...Object.keys(gitHosts.byShortcut).reduce((acc, key) => {
18+
acc[key] = { name: gitHosts.byShortcut[key] }
19+
return acc
20+
}, {}),
2521
}
2622

27-
const knownProtocols = Object.keys(gitHosts.byShortcut)
28-
.concat(['http:', 'https:', 'git:', 'git+ssh:', 'git+https:', 'ssh:'])
29-
3023
module.exports.fromUrl = function (giturl, opts) {
3124
if (typeof giturl !== 'string') {
3225
return
@@ -41,30 +34,34 @@ module.exports.fromUrl = function (giturl, opts) {
4134
return cache.get(key)
4235
}
4336

37+
module.exports.parseUrl = parseUrl
38+
4439
function fromUrl (giturl, opts) {
4540
if (!giturl) {
4641
return
4742
}
4843

49-
const correctedUrl = isGitHubShorthand(giturl) ? 'github:' + giturl : correctProtocol(giturl)
50-
const parsed = parseGitUrl(correctedUrl)
44+
const correctedUrl = isGitHubShorthand(giturl) ? `github:${giturl}` : giturl
45+
const parsed = parseUrl(correctedUrl, protocols)
5146
if (!parsed) {
52-
return parsed
47+
return
5348
}
5449

5550
const gitHostShortcut = gitHosts.byShortcut[parsed.protocol]
56-
const gitHostDomain =
57-
gitHosts.byDomain[parsed.hostname.startsWith('www.') ?
58-
parsed.hostname.slice(4) :
59-
parsed.hostname]
51+
const gitHostDomain = gitHosts.byDomain[parsed.hostname.startsWith('www.')
52+
? parsed.hostname.slice(4)
53+
: parsed.hostname]
6054
const gitHostName = gitHostShortcut || gitHostDomain
6155
if (!gitHostName) {
6256
return
6357
}
6458

6559
const gitHostInfo = gitHosts[gitHostShortcut || gitHostDomain]
6660
let auth = null
67-
if (authProtocols[parsed.protocol] && (parsed.username || parsed.password)) {
61+
if (protocols[parsed.protocol] &&
62+
protocols[parsed.protocol].auth &&
63+
(parsed.username || parsed.password)
64+
) {
6865
auth = `${parsed.username}${parsed.password ? ':' + parsed.password : ''}`
6966
}
7067

@@ -116,7 +113,8 @@ function fromUrl (giturl, opts) {
116113
user = segments.user && decodeURIComponent(segments.user)
117114
project = decodeURIComponent(segments.project)
118115
committish = decodeURIComponent(segments.committish)
119-
defaultRepresentation = protocolToRepresentation(parsed.protocol)
116+
defaultRepresentation = (protocols[parsed.protocol] && protocols[parsed.protocol].name)
117+
|| parsed.protocol.slice(0, -1)
120118
}
121119
} catch (err) {
122120
/* istanbul ignore else */
@@ -130,31 +128,6 @@ function fromUrl (giturl, opts) {
130128
return new GitHost(gitHostName, user, auth, project, committish, defaultRepresentation, opts)
131129
}
132130

133-
// accepts input like git:github.com:user/repo and inserts the // after the first :
134-
const correctProtocol = (arg) => {
135-
const firstColon = arg.indexOf(':')
136-
const proto = arg.slice(0, firstColon + 1)
137-
if (knownProtocols.includes(proto)) {
138-
return arg
139-
}
140-
141-
const firstAt = arg.indexOf('@')
142-
if (firstAt > -1) {
143-
if (firstAt > firstColon) {
144-
return `git+ssh://${arg}`
145-
} else {
146-
return arg
147-
}
148-
}
149-
150-
const doubleSlash = arg.indexOf('//')
151-
if (doubleSlash === firstColon + 1) {
152-
return arg
153-
}
154-
155-
return arg.slice(0, firstColon + 1) + '//' + arg.slice(firstColon + 1)
156-
}
157-
158131
// look for github shorthand inputs, such as npm/cli
159132
const isGitHubShorthand = (arg) => {
160133
// it cannot contain whitespace before the first #
@@ -185,64 +158,3 @@ const isGitHubShorthand = (arg) => {
185158
doesNotStartWithDot && atOnlyAfterHash && colonOnlyAfterHash &&
186159
secondSlashOnlyAfterHash
187160
}
188-
189-
// attempt to correct an scp style url so that it will parse with `new URL()`
190-
const correctUrl = (giturl) => {
191-
const firstAt = giturl.indexOf('@')
192-
const lastHash = giturl.lastIndexOf('#')
193-
let firstColon = giturl.indexOf(':')
194-
let lastColon = giturl.lastIndexOf(':', lastHash > -1 ? lastHash : Infinity)
195-
196-
let corrected
197-
if (lastColon > firstAt) {
198-
// the last : comes after the first @ (or there is no @)
199-
// like it would in:
200-
// proto://hostname.com:user/repo
201-
// username@hostname.com:user/repo
202-
// :password@hostname.com:user/repo
203-
// username:password@hostname.com:user/repo
204-
// proto://username@hostname.com:user/repo
205-
// proto://:password@hostname.com:user/repo
206-
// proto://username:password@hostname.com:user/repo
207-
// then we replace the last : with a / to create a valid path
208-
corrected = giturl.slice(0, lastColon) + '/' + giturl.slice(lastColon + 1)
209-
// // and we find our new : positions
210-
firstColon = corrected.indexOf(':')
211-
lastColon = corrected.lastIndexOf(':')
212-
}
213-
214-
if (firstColon === -1 && giturl.indexOf('//') === -1) {
215-
// we have no : at all
216-
// as it would be in:
217-
// username@hostname.com/user/repo
218-
// then we prepend a protocol
219-
corrected = `git+ssh://${corrected}`
220-
}
221-
222-
return corrected
223-
}
224-
225-
// try to parse the url as its given to us, if that throws
226-
// then we try to clean the url and parse that result instead
227-
// THIS FUNCTION SHOULD NEVER THROW
228-
const parseGitUrl = (giturl) => {
229-
let result
230-
try {
231-
result = new url.URL(giturl)
232-
} catch {
233-
// this fn should never throw
234-
}
235-
236-
if (result) {
237-
return result
238-
}
239-
240-
const correctedUrl = correctUrl(giturl)
241-
try {
242-
result = new url.URL(correctedUrl)
243-
} catch {
244-
// this fn should never throw
245-
}
246-
247-
return result
248-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const url = require('url')
2+
3+
const lastIndexOfBefore = (str, char, beforeChar) => {
4+
const startPosition = str.indexOf(beforeChar)
5+
return str.lastIndexOf(char, startPosition > -1 ? startPosition : Infinity)
6+
}
7+
8+
const safeUrl = (u) => {
9+
try {
10+
return new url.URL(u)
11+
} catch {
12+
// this fn should never throw
13+
}
14+
}
15+
16+
// accepts input like git:github.com:user/repo and inserts the // after the first :
17+
const correctProtocol = (arg, protocols) => {
18+
const firstColon = arg.indexOf(':')
19+
const proto = arg.slice(0, firstColon + 1)
20+
if (Object.prototype.hasOwnProperty.call(protocols, proto)) {
21+
return arg
22+
}
23+
24+
const firstAt = arg.indexOf('@')
25+
if (firstAt > -1) {
26+
if (firstAt > firstColon) {
27+
return `git+ssh://${arg}`
28+
} else {
29+
return arg
30+
}
31+
}
32+
33+
const doubleSlash = arg.indexOf('//')
34+
if (doubleSlash === firstColon + 1) {
35+
return arg
36+
}
37+
38+
return `${arg.slice(0, firstColon + 1)}//${arg.slice(firstColon + 1)}`
39+
}
40+
41+
// attempt to correct an scp style url so that it will parse with `new URL()`
42+
const correctUrl = (giturl) => {
43+
// ignore @ that come after the first hash since the denotes the start
44+
// of a committish which can contain @ characters
45+
const firstAt = lastIndexOfBefore(giturl, '@', '#')
46+
// ignore colons that come after the hash since that could include colons such as:
47+
// git@github.com:user/package-2#semver:^1.0.0
48+
const lastColonBeforeHash = lastIndexOfBefore(giturl, ':', '#')
49+
50+
if (lastColonBeforeHash > firstAt) {
51+
// the last : comes after the first @ (or there is no @)
52+
// like it would in:
53+
// proto://hostname.com:user/repo
54+
// username@hostname.com:user/repo
55+
// :password@hostname.com:user/repo
56+
// username:password@hostname.com:user/repo
57+
// proto://username@hostname.com:user/repo
58+
// proto://:password@hostname.com:user/repo
59+
// proto://username:password@hostname.com:user/repo
60+
// then we replace the last : with a / to create a valid path
61+
giturl = giturl.slice(0, lastColonBeforeHash) + '/' + giturl.slice(lastColonBeforeHash + 1)
62+
}
63+
64+
if (lastIndexOfBefore(giturl, ':', '#') === -1 && giturl.indexOf('//') === -1) {
65+
// we have no : at all
66+
// as it would be in:
67+
// username@hostname.com/user/repo
68+
// then we prepend a protocol
69+
giturl = `git+ssh://${giturl}`
70+
}
71+
72+
return giturl
73+
}
74+
75+
module.exports = (giturl, protocols) => {
76+
const withProtocol = protocols ? correctProtocol(giturl, protocols) : giturl
77+
return safeUrl(withProtocol) || safeUrl(correctUrl(withProtocol))
78+
}

node_modules/hosted-git-info/package.json

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hosted-git-info",
3-
"version": "5.1.0",
3+
"version": "5.2.1",
44
"description": "Provides metadata and conversions from repository urls for GitHub, Bitbucket and GitLab",
55
"main": "./lib/index.js",
66
"repository": {
@@ -21,9 +21,6 @@
2121
"homepage": "https://github.com/npm/hosted-git-info",
2222
"scripts": {
2323
"posttest": "npm run lint",
24-
"postversion": "npm publish",
25-
"prepublishOnly": "git push origin --follow-tags",
26-
"preversion": "npm test",
2724
"snap": "tap",
2825
"test": "tap",
2926
"test:coverage": "tap --coverage-report=html",
@@ -37,7 +34,7 @@
3734
},
3835
"devDependencies": {
3936
"@npmcli/eslint-config": "^3.0.1",
40-
"@npmcli/template-oss": "3.5.0",
37+
"@npmcli/template-oss": "4.7.1",
4138
"tap": "^16.0.1"
4239
},
4340
"files": [
@@ -49,10 +46,22 @@
4946
},
5047
"tap": {
5148
"color": 1,
52-
"coverage": true
49+
"coverage": true,
50+
"nyc-arg": [
51+
"--exclude",
52+
"tap-snapshots/**"
53+
]
5354
},
5455
"templateOSS": {
5556
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
56-
"version": "3.5.0"
57+
"version": "4.7.1",
58+
"ciVersions": [
59+
"12.13.0",
60+
"12.x",
61+
"14.15.0",
62+
"14.x",
63+
"16.0.0",
64+
"16.x"
65+
]
5766
}
5867
}

package-lock.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
"fastest-levenshtein": "^1.0.12",
110110
"glob": "^8.0.1",
111111
"graceful-fs": "^4.2.10",
112-
"hosted-git-info": "^5.1.0",
112+
"hosted-git-info": "^5.2.1",
113113
"ini": "^3.0.1",
114114
"init-package-json": "^3.0.2",
115115
"is-cidr": "^4.0.2",
@@ -5955,9 +5955,9 @@
59555955
}
59565956
},
59575957
"node_modules/hosted-git-info": {
5958-
"version": "5.1.0",
5959-
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.1.0.tgz",
5960-
"integrity": "sha512-Ek+QmMEqZF8XrbFdwoDjSbm7rT23pCgEMOJmz6GPk/s4yH//RQfNPArhIxbguNxROq/+5lNBwCDHMhA903Kx1Q==",
5958+
"version": "5.2.1",
5959+
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz",
5960+
"integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==",
59615961
"inBundle": true,
59625962
"dependencies": {
59635963
"lru-cache": "^7.5.1"
@@ -13880,6 +13880,7 @@
1388013880
"bin-links": "^3.0.3",
1388113881
"cacache": "^16.1.3",
1388213882
"common-ancestor-path": "^1.0.1",
13883+
"hosted-git-info": "^5.2.1",
1388313884
"json-parse-even-better-errors": "^2.3.1",
1388413885
"json-stringify-nice": "^1.1.4",
1388513886
"minimatch": "^5.1.0",

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"fastest-levenshtein": "^1.0.12",
7575
"glob": "^8.0.1",
7676
"graceful-fs": "^4.2.10",
77-
"hosted-git-info": "^5.1.0",
77+
"hosted-git-info": "^5.2.1",
7878
"ini": "^3.0.1",
7979
"init-package-json": "^3.0.2",
8080
"is-cidr": "^4.0.2",

0 commit comments

Comments
 (0)