Skip to content
This repository was archived by the owner on Jan 20, 2022. It is now read-only.

Commit cddfe03

Browse files
committed
fix: make Node.children() a case-insensitive Map
In other words, do not allow a node to have both `FOO` and `foo` in a parent node's `children` collection. In order to maintain proper semantics for package resolution, this also means that Node.edgesOut has to be case-insensitive as well, meaning that a package may not *depend* on both `FOO` and `foo`. Lastly, do not add removed dependency folders to the trashlist. There is no need, since they are retired and then not un-retired, and removing the package folder itself (which was always a rimraf of a non-existent path) could remove something we want, if you are replacing `FOO` with `foo`. This has the effect of making Arborist.buildIdealTree() and Arborist.reify() properly collide packages that differ only in case, thus generating trees that are safe for use across a variety of file systems. This is an improvement on the previous commit's "non-directory detected during sparse tree creation" check, because it will avoid cases where the package-lock.json does not match what we actually end up creating on disk, and sidestep the collisions altogether. Note that it _is_ still possible to end up with collisions on Windows systems, if for example you have a package.json like this: ```json { "dependencies": { "longer-than-eight-char-name": "file:/some/arbitrary/path", "LONGER~1": "npm:arbitrary-write@*" } } ``` In that case, the sparse dir check in the previous commit will be triggered, since the folder's non-directory-ness will be detected while creating the sparse folder tree. The user will end up with a mismatched lockfile, but importantly, they *won't* have arbitrary writes being sent through a symbolic link. On case _sensitive_ systems, this means that the `node_modules` folder may not contain case-variant packages, and any projects currently in this state will need to be reinstalled or updated. As this is extremely rare (since uppercase and non-url-safe characters have not been allowed in package names on the public registry for many years), it is unlikely to affect any users in the wild.
1 parent ec98d96 commit cddfe03

File tree

8 files changed

+316
-58
lines changed

8 files changed

+316
-58
lines changed

lib/arborist/reify.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const _addOmitsToTrashList = Symbol('addOmitsToTrashList')
7979
const _packageLockOnly = Symbol('packageLockOnly')
8080
const _dryRun = Symbol('dryRun')
8181
const _validatePath = Symbol('validatePath')
82-
const _reifyPackages = Symbol('reifyPackages')
82+
const _reifyPackages = Symbol.for('reifyPackages')
8383

8484
const _omitDev = Symbol('omitDev')
8585
const _omitOptional = Symbol('omitOptional')
@@ -330,12 +330,13 @@ module.exports = cls => class Reifier extends cls {
330330
ideal: this.idealTree,
331331
})
332332

333-
for (const node of this.diff.removed) {
334-
// a node in a dep bundle will only be removed if its bundling dep
335-
// is removed as well. in which case, we don't have to delete it!
336-
if (!node.inDepBundle)
337-
this[_addNodeToTrashList](node)
338-
}
333+
// we don't have to add 'removed' folders to the trashlist, because
334+
// they'll be moved aside to a retirement folder, and then the retired
335+
// folder will be deleted at the end. This is important when we have
336+
// a folder like FOO being "removed" in favor of a folder like "foo",
337+
// because if we remove node_modules/FOO on case-insensitive systems,
338+
// it will remove the dep that we *want* at node_modules/foo.
339+
339340
process.emit('timeEnd', 'reify:diffTrees')
340341
}
341342

lib/edge.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class Edge {
7777
}
7878

7979
satisfiedBy (node) {
80-
return depValid(node, this.spec, this.accept, this.from)
80+
return node.name === this.name && depValid(node, this.spec, this.accept, this.from)
8181
}
8282

8383
explain (seen = []) {
@@ -167,7 +167,7 @@ class Edge {
167167
[_loadError] () {
168168
return !this[_to] ? (this.optional ? null : 'MISSING')
169169
: this.peer && this.from === this.to.parent && !this.from.isTop ? 'PEER LOCAL'
170-
: !depValid(this.to, this.spec, this.accept, this.from) ? 'INVALID'
170+
: !this.satisfiedBy(this.to) ? 'INVALID'
171171
: 'OK'
172172
}
173173

lib/node.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const relpath = require('./relpath.js')
6666
const consistentResolve = require('./consistent-resolve.js')
6767

6868
const printableTree = require('./printable.js')
69+
const CaseInsensitiveMap = require('./case-insensitive-map.js')
6970

7071
class Node {
7172
constructor (options) {
@@ -148,7 +149,7 @@ class Node {
148149
this.hasShrinkwrap = hasShrinkwrap || pkg._hasShrinkwrap || false
149150
this.legacyPeerDeps = legacyPeerDeps
150151

151-
this.children = new Map()
152+
this.children = new CaseInsensitiveMap()
152153
this.fsChildren = new Set()
153154
this.inventory = new Inventory({})
154155
this.tops = new Set()
@@ -181,7 +182,7 @@ class Node {
181182
}
182183

183184
this.edgesIn = new Set()
184-
this.edgesOut = new Map()
185+
this.edgesOut = new CaseInsensitiveMap()
185186

186187
// have to set the internal package ref before assigning the parent,
187188
// because this.package is read when adding to inventory
@@ -1014,8 +1015,20 @@ class Node {
10141015

10151016
replace (node) {
10161017
this[_delistFromMeta]()
1017-
this.path = node.path
1018-
this.name = node.name
1018+
1019+
// if the name matches, but is not identical, we are intending to clobber
1020+
// something case-insensitively, so merely setting name and path won't
1021+
// have the desired effect. just set the path so it'll collide in the
1022+
// parent's children map, and leave it at that.
1023+
const nameMatch = node.parent &&
1024+
node.parent.children.get(this.name) === node
1025+
if (nameMatch)
1026+
this.path = resolve(node.parent.path, 'node_modules', this.name)
1027+
else {
1028+
this.path = node.path
1029+
this.name = node.name
1030+
}
1031+
10191032
if (!this.isLink)
10201033
this.realpath = this.path
10211034
this[_refreshLocation]()

tap-snapshots/test/arborist/reify.js.test.cjs

Lines changed: 96 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,100 @@ ArboristNode {
19121912
}
19131913
`
19141914

1915+
exports[`test/arborist/reify.js TAP collide case-variant dep names > tree 1 1`] = `
1916+
ArboristNode {
1917+
"children": Map {
1918+
"abbrev" => ArboristNode {
1919+
"edgesIn": Set {
1920+
EdgeIn {
1921+
"from": "",
1922+
"name": "abbrev",
1923+
"spec": "1",
1924+
"type": "prod",
1925+
},
1926+
},
1927+
"location": "node_modules/abbrev",
1928+
"name": "abbrev",
1929+
"path": "{CWD}/test/arborist/tap-testdir-reify-collide-case-variant-dep-names/node_modules/abbrev",
1930+
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
1931+
"version": "1.1.1",
1932+
},
1933+
},
1934+
"edgesOut": Map {
1935+
"abbrev" => EdgeOut {
1936+
"name": "abbrev",
1937+
"spec": "1",
1938+
"to": "node_modules/abbrev",
1939+
"type": "prod",
1940+
},
1941+
},
1942+
"fsChildren": Set {
1943+
ArboristNode {
1944+
"dev": true,
1945+
"extraneous": true,
1946+
"location": "target",
1947+
"name": "target",
1948+
"optional": true,
1949+
"packageName": "ABBREV",
1950+
"path": "{CWD}/test/arborist/tap-testdir-reify-collide-case-variant-dep-names/target",
1951+
"peer": true,
1952+
"version": "1.0.0",
1953+
},
1954+
},
1955+
"isProjectRoot": true,
1956+
"location": "",
1957+
"name": "tap-testdir-reify-collide-case-variant-dep-names",
1958+
"path": "{CWD}/test/arborist/tap-testdir-reify-collide-case-variant-dep-names",
1959+
}
1960+
`
1961+
1962+
exports[`test/arborist/reify.js TAP collide case-variant dep names > tree 2 1`] = `
1963+
ArboristNode {
1964+
"children": Map {
1965+
"abbrev" => ArboristNode {
1966+
"edgesIn": Set {
1967+
EdgeIn {
1968+
"from": "",
1969+
"name": "abbrev",
1970+
"spec": "^1.1.1",
1971+
"type": "prod",
1972+
},
1973+
},
1974+
"location": "node_modules/abbrev",
1975+
"name": "abbrev",
1976+
"path": "{CWD}/test/arborist/tap-testdir-reify-collide-case-variant-dep-names/node_modules/abbrev",
1977+
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
1978+
"version": "1.1.1",
1979+
},
1980+
},
1981+
"edgesOut": Map {
1982+
"abbrev" => EdgeOut {
1983+
"name": "abbrev",
1984+
"spec": "^1.1.1",
1985+
"to": "node_modules/abbrev",
1986+
"type": "prod",
1987+
},
1988+
},
1989+
"fsChildren": Set {
1990+
ArboristNode {
1991+
"dev": true,
1992+
"extraneous": true,
1993+
"location": "target",
1994+
"name": "target",
1995+
"optional": true,
1996+
"packageName": "ABBREV",
1997+
"path": "{CWD}/test/arborist/tap-testdir-reify-collide-case-variant-dep-names/target",
1998+
"peer": true,
1999+
"version": "1.0.0",
2000+
},
2001+
},
2002+
"isProjectRoot": true,
2003+
"location": "",
2004+
"name": "tap-testdir-reify-collide-case-variant-dep-names",
2005+
"path": "{CWD}/test/arborist/tap-testdir-reify-collide-case-variant-dep-names",
2006+
}
2007+
`
2008+
19152009
exports[`test/arborist/reify.js TAP create link deps > expect resolving Promise 1`] = `
19162010
ArboristNode {
19172011
"children": Map {
@@ -4540,7 +4634,7 @@ ArboristNode {
45404634
EdgeIn {
45414635
"from": "",
45424636
"name": "abbrev",
4543-
"spec": "1",
4637+
"spec": "latest",
45444638
"type": "prod",
45454639
},
45464640
},
@@ -4550,48 +4644,14 @@ ArboristNode {
45504644
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
45514645
"version": "1.1.1",
45524646
},
4553-
"ABBREV" => ArboristLink {
4554-
"edgesIn": Set {
4555-
EdgeIn {
4556-
"from": "",
4557-
"name": "ABBREV",
4558-
"spec": "file:target",
4559-
"type": "prod",
4560-
},
4561-
},
4562-
"location": "node_modules/ABBREV",
4563-
"name": "ABBREV",
4564-
"path": "{CWD}/test/arborist/tap-testdir-reify-move-aside-symlink-clutter/node_modules/ABBREV",
4565-
"realpath": "{CWD}/test/arborist/tap-testdir-reify-move-aside-symlink-clutter/target",
4566-
"resolved": "file:../target",
4567-
"target": ArboristNode {
4568-
"location": "target",
4569-
},
4570-
"version": "1.0.0",
4571-
},
45724647
},
45734648
"edgesOut": Map {
45744649
"abbrev" => EdgeOut {
45754650
"name": "abbrev",
4576-
"spec": "1",
4651+
"spec": "latest",
45774652
"to": "node_modules/abbrev",
45784653
"type": "prod",
45794654
},
4580-
"ABBREV" => EdgeOut {
4581-
"name": "ABBREV",
4582-
"spec": "file:target",
4583-
"to": "node_modules/ABBREV",
4584-
"type": "prod",
4585-
},
4586-
},
4587-
"fsChildren": Set {
4588-
ArboristNode {
4589-
"location": "target",
4590-
"name": "target",
4591-
"packageName": "ABBREV",
4592-
"path": "{CWD}/test/arborist/tap-testdir-reify-move-aside-symlink-clutter/target",
4593-
"version": "1.0.0",
4594-
},
45954655
},
45964656
"isProjectRoot": true,
45974657
"location": "",

0 commit comments

Comments
 (0)