codekingpro/portable-devtools
114k
1'use strict'
2
3const parser = require('postcss-selector-parser')
4
5const arrayDelimiter = Symbol('arrayDelimiter')
6
7const escapeSlashes = str =>
8 str.replace(/\//g, '\\/')
9
10const unescapeSlashes = str =>
11 str.replace(/\\\//g, '/')
12
13// recursively fixes up any :attr pseudo-class found
14const fixupAttr = astNode => {
15 const properties = []
16 const matcher = {}
17 for (const selectorAstNode of astNode.nodes) {
18 const [firstAstNode] = selectorAstNode.nodes
19 if (firstAstNode.type === 'tag') {
20 properties.push(firstAstNode.value)
21 }
22 }
23
24 const lastSelectorAstNode = astNode.nodes.pop()
25 const [attributeAstNode] = lastSelectorAstNode.nodes
26
27 if (attributeAstNode.value === ':attr') {
28 const appendParts = fixupAttr(attributeAstNode)
29 properties.push(arrayDelimiter, ...appendParts.lookupProperties)
30 matcher.qualifiedAttribute = appendParts.attributeMatcher.qualifiedAttribute
31 matcher.operator = appendParts.attributeMatcher.operator
32 matcher.value = appendParts.attributeMatcher.value
33
34 // backwards compatibility
35 matcher.attribute = appendParts.attributeMatcher.attribute
36
37 if (appendParts.attributeMatcher.insensitive) {
38 matcher.insensitive = true
39 }
40 } else {
41 if (attributeAstNode.type !== 'attribute') {
42 throw Object.assign(
43 new Error('`:attr` pseudo-class expects an attribute matcher as the last value'),
44 { code: 'EQUERYATTR' }
45 )
46 }
47
48 matcher.qualifiedAttribute = unescapeSlashes(attributeAstNode.qualifiedAttribute)
49 matcher.operator = attributeAstNode.operator
50 matcher.value = attributeAstNode.value
51
52 // backwards compatibility
53 matcher.attribute = matcher.qualifiedAttribute
54
55 if (attributeAstNode.insensitive) {
56 matcher.insensitive = true
57 }
58 }
59
60 astNode.lookupProperties = properties
61 astNode.attributeMatcher = matcher
62 astNode.nodes.length = 0
63 return astNode
64}
65
66// fixed up nested pseudo nodes will have their internal selectors moved
67// to a new root node that will be referenced by the `nestedNode` property,
68// this tweak makes it simpler to reuse `retrieveNodesFromParsedAst` to
69// recursively parse and extract results from the internal selectors
70const fixupNestedPseudo = astNode => {
71 // create a new ast root node and relocate any children
72 // selectors of the current ast node to this new root
73 const newRootNode = parser.root()
74 astNode.nestedNode = newRootNode
75 newRootNode.nodes = [...astNode.nodes]
76
77 // clean up the ast by removing the children nodes from the
78 // current ast node while also cleaning up their `parent` refs
79 astNode.nodes.length = 0
80 for (const currAstNode of newRootNode.nodes) {
81 currAstNode.parent = newRootNode
82 }
83
84 // recursively fixup nodes of any nested selector
85 transformAst(newRootNode)
86}
87
88// :semver(<version|range|selector>, [version|range|selector], [function])
89// note: the first or second parameter must be a static version or range
90const fixupSemverSpecs = astNode => {
91 // if we have three nodes, the last is the semver function to use, pull that out first
92 if (astNode.nodes.length === 3) {
93 const funcNode = astNode.nodes.pop().nodes[0]
94 if (funcNode.type === 'tag') {
95 astNode.semverFunc = funcNode.value
96 } else if (funcNode.type === 'string') {
97 // a string is always in some type of quotes, we don't want those so slice them off
98 astNode.semverFunc = funcNode.value.slice(1, -1)
99 } else {
100 // anything that isn't a tag or a string isn't a function name
101 throw Object.assign(
102 new Error('`:semver` pseudo-class expects a function name as last value'),
103 { code: 'ESEMVERFUNC' }
104 )
105 }
106 }
107
108 // now if we have 1 node, it's a static value
109 // istanbul ignore else
110 if (astNode.nodes.length === 1) {
111 const semverNode = astNode.nodes.pop()
112 astNode.semverValue = semverNode.nodes.reduce((res, next) => `${res}${String(next)}`, '')
113 } else if (astNode.nodes.length === 2) {
114 // and if we have two nodes, one of them is a static value and we need to determine which it is
115 for (let i = 0; i < astNode.nodes.length; ++i) {
116 const type = astNode.nodes[i].nodes[0].type
117 // the type of the first child may be combinator for ranges, such as >14
118 if (type === 'tag' || type === 'combinator') {
119 const semverNode = astNode.nodes.splice(i, 1)[0]
120 astNode.semverValue = semverNode.nodes.reduce((res, next) => `${res}${String(next)}`, '')
121 astNode.semverPosition = i
122 break
123 }
124 }
125
126 if (typeof astNode.semverValue === 'undefined') {
127 throw Object.assign(
128 new Error('`:semver` pseudo-class expects a static value in the first or second position'),
129 { code: 'ESEMVERVALUE' }
130 )
131 }
132 }
133
134 // if we got here, the last remaining child should be attribute selector
135 if (astNode.nodes.length === 1) {
136 fixupAttr(astNode)
137 } else {
138 // if we don't have a selector, we default to `[version]`
139 astNode.attributeMatcher = {
140 insensitive: false,
141 attribute: 'version',
142 qualifiedAttribute: 'version',
143 }
144 astNode.lookupProperties = []
145 }
146
147 astNode.nodes.length = 0
148}
149
150const fixupTypes = astNode => {
151 const [valueAstNode] = astNode.nodes[0].nodes
152 const { value } = valueAstNode || {}
153 astNode.typeValue = value
154 astNode.nodes.length = 0
155}
156
157const fixupPaths = astNode => {
158 astNode.pathValue = unescapeSlashes(String(astNode.nodes[0]))
159 astNode.nodes.length = 0
160}
161
162const fixupOutdated = astNode => {
163 if (astNode.nodes.length) {
164 astNode.outdatedKind = String(astNode.nodes[0])
165 astNode.nodes.length = 0
166 }
167}
168
169const fixupVuln = astNode => {
170 const vulns = []
171 if (astNode.nodes.length) {
172 for (const selector of astNode.nodes) {
173 const vuln = {}
174 for (const node of selector.nodes) {
175 if (node.type !== 'attribute') {
176 throw Object.assign(
177 new Error(':vuln pseudo-class only accepts attribute matchers or "cwe" tag'),
178 { code: 'EQUERYATTR' }
179 )
180 }
181 if (!['severity', 'cwe'].includes(node._attribute)) {
182 throw Object.assign(
183 new Error(':vuln pseudo-class only matches "severity" and "cwe" attributes'),
184 { code: 'EQUERYATTR' }
185 )
186 }
187 if (!node.operator) {
188 node.operator = '='
189 node.value = '*'
190 }
191 if (node.operator !== '=') {
192 throw Object.assign(
193 new Error(':vuln pseudo-class attribute selector only accepts "=" operator', node),
194 { code: 'EQUERYATTR' }
195 )
196 }
197 if (!vuln[node._attribute]) {
198 vuln[node._attribute] = []
199 }
200 vuln[node._attribute].push(node._value)
201 }
202 vulns.push(vuln)
203 }
204 astNode.vulns = vulns
205 astNode.nodes.length = 0
206 }
207}
208
209// a few of the supported ast nodes need to be tweaked in order to properly be
210// interpreted as proper arborist query selectors, namely semver ranges from
211// both ids and :semver pseudo-class selectors need to be translated from what
212// are usually multiple ast nodes, such as: tag:1, class:.0, class:.0 to a
213// single `1.0.0` value, other pseudo-class selectors also get preprocessed in
214// order to make it simpler to execute later when traversing each ast node
215// using rootNode.walk(), such as :path, :type, etc. transformAst handles all
216// these modifications to the parsed ast by doing an extra, initial traversal
217// of the parsed ast from the query and modifying the parsed nodes accordingly
218const transformAst = selector => {
219 selector.walk((nextAstNode) => {
220 switch (nextAstNode.value) {
221 case ':attr':
222 return fixupAttr(nextAstNode)
223 case ':is':
224 case ':has':
225 case ':not':
226 return fixupNestedPseudo(nextAstNode)
227 case ':path':
228 return fixupPaths(nextAstNode)
229 case ':semver':
230 return fixupSemverSpecs(nextAstNode)
231 case ':type':
232 return fixupTypes(nextAstNode)
233 case ':outdated':
234 return fixupOutdated(nextAstNode)
235 case ':vuln':
236 return fixupVuln(nextAstNode)
237 }
238 })
239}
240
241const queryParser = (query) => {
242 // if query is an empty string or any falsy
243 // value, just returns an empty result
244 if (!query) {
245 return []
246 }
247
248 return parser(transformAst)
249 .astSync(escapeSlashes(query), { lossless: false })
250}
251
252module.exports = {
253 parser: queryParser,
254 arrayDelimiter,
255}
256 