1// Do not rely on package._fields, so that we don't throw 2// false failures if a tree is generated by other clients. 3// Only relies on child.resolved, which MAY come from 4// client-specific package.json meta _fields, but most of 5// the time will be pulled out of a lockfile 6 7const semver = require('semver') 8const npa = require('npm-package-arg') 9const { relative } = require('path') 10const fromPath = require('./from-path.js') 11 12const depValid = (child, requested, requestor) => { 13 // NB: we don't do much to verify 'tag' type requests. 14 // Just verify that we got a remote resolution. Presumably, it 15 // came from a registry and was tagged at some point. 16 17 if (typeof requested === 'string') { 18 try { 19 // tarball/dir must have resolved to the same tgz on disk, but for 20 // file: deps that depend on other files/dirs, we must resolve the 21 // location based on the *requestor* file/dir, not where it ends up. 22 // '' is equivalent to '*' 23 requested = npa.resolve(child.name, requested || '*', fromPath(requestor, requestor.edgesOut.get(child.name))) 24 } catch (er) { 25 // Not invalid because the child doesn't match, but because 26 // the spec itself is not supported. Nothing would match, 27 // so the edge is definitely not valid and never can be. 28 er.dependency = child.name 29 er.requested = requested 30 requestor.errors.push(er) 31 return false 32 } 33 } 34 35 // if the lockfile is super old, or hand-modified, 36 // then it's possible to hit this state. 37 if (!requested) { 38 const er = new Error('Invalid dependency specifier') 39 er.dependency = child.name 40 er.requested = requested 41 requestor.errors.push(er) 42 return false 43 } 44 45 switch (requested.type) { 46 case 'range': 47 if (requested.fetchSpec === '*') { 48 return true 49 } 50 // fallthrough 51 case 'version': 52 // if it's a version or a range other than '*', semver it 53 return semver.satisfies(child.version, requested.fetchSpec, true) 54 55 case 'directory': 56 return linkValid(child, requested, requestor) 57 58 case 'file': 59 return tarballValid(child, requested, requestor) 60 61 case 'alias': 62 // check that the alias target is valid 63 return depValid(child, requested.subSpec, requestor) 64 65 case 'tag': 66 // if it's a tag, we just verify that it has a tarball resolution 67 // presumably, it came from the registry and was tagged at some point 68 return child.resolved && npa(child.resolved).type === 'remote' 69 70 case 'remote': 71 // verify that we got it from the desired location 72 return child.resolved === requested.fetchSpec 73 74 case 'git': { 75 // if it's a git type, verify that they're the same repo 76 // 77 // if it specifies a definite commit, then it must have the 78 // same commit to be considered the same repo 79 // 80 // if it has a #semver:<range> specifier, verify that the 81 // version in the package is in the semver range 82 const resRepo = npa(child.resolved || '') 83 const resHost = resRepo.hosted 84 const reqHost = requested.hosted 85 const reqCommit = /^[a-fA-F0-9]{40}$/.test(requested.gitCommittish || '') 86 const nc = { noCommittish: !reqCommit } 87 if (!resHost) { 88 if (resRepo.fetchSpec !== requested.fetchSpec) { 89 return false 90 } 91 } else { 92 if (reqHost?.ssh(nc) !== resHost.ssh(nc)) { 93 return false 94 } 95 } 96 if (!requested.gitRange) { 97 return true 98 } 99 return semver.satisfies(child.package.version, requested.gitRange, { 100 loose: true, 101 }) 102 } 103 104 default: // unpossible, just being cautious 105 break 106 } 107 108 const er = new Error('Unsupported dependency type') 109 er.dependency = child.name 110 er.requested = requested 111 requestor.errors.push(er) 112 return false 113} 114 115const linkValid = (child, requested, requestor) => { 116 const isLink = !!child.isLink 117 // if we're installing links and the node is a link, then it's invalid because we want 118 // a real node to be there. Except for workspaces. They are always links. 119 if (requestor.installLinks && !child.isWorkspace) { 120 return !isLink 121 } 122 123 // directory must be a link to the specified folder 124 return isLink && relative(child.realpath, requested.fetchSpec) === '' 125} 126 127const tarballValid = (child, requested, requestor) => { 128 if (child.isLink) { 129 return false 130 } 131 132 if (child.resolved) { 133 return child.resolved.replace(/\\/g, '/') === `file:${requested.fetchSpec.replace(/\\/g, '/')}` 134 } 135 136 // if we have a legacy mutated package.json file. we can't be 100% 137 // sure that it resolved to the same file, but if it was the same 138 // request, that's a pretty good indicator of sameness. 139 if (child.package._requested) { 140 return child.package._requested.saveSpec === requested.saveSpec 141 } 142 143 // ok, we're probably dealing with some legacy cruft here, not much 144 // we can do at this point unfortunately. 145 return false 146} 147 148module.exports = (child, requested, accept, requestor) => 149 depValid(child, requested, requestor) || 150 (typeof accept === 'string' ? depValid(child, accept, requestor) : false) 151