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