[6a3a178] | 1 | 'use strict'
|
---|
| 2 |
|
---|
| 3 | const crypto = require('crypto')
|
---|
| 4 | const MiniPass = require('minipass')
|
---|
| 5 |
|
---|
| 6 | const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512']
|
---|
| 7 |
|
---|
| 8 | // TODO: this should really be a hardcoded list of algorithms we support,
|
---|
| 9 | // rather than [a-z0-9].
|
---|
| 10 | const BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i
|
---|
| 11 | const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/
|
---|
| 12 | const STRICT_SRI_REGEX = /^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/
|
---|
| 13 | const VCHAR_REGEX = /^[\x21-\x7E]+$/
|
---|
| 14 |
|
---|
| 15 | const defaultOpts = {
|
---|
| 16 | algorithms: ['sha512'],
|
---|
| 17 | error: false,
|
---|
| 18 | options: [],
|
---|
| 19 | pickAlgorithm: getPrioritizedHash,
|
---|
| 20 | sep: ' ',
|
---|
| 21 | single: false,
|
---|
| 22 | strict: false
|
---|
| 23 | }
|
---|
| 24 |
|
---|
| 25 | const ssriOpts = (opts = {}) => ({ ...defaultOpts, ...opts })
|
---|
| 26 |
|
---|
| 27 | const getOptString = options => !options || !options.length
|
---|
| 28 | ? ''
|
---|
| 29 | : `?${options.join('?')}`
|
---|
| 30 |
|
---|
| 31 | const _onEnd = Symbol('_onEnd')
|
---|
| 32 | const _getOptions = Symbol('_getOptions')
|
---|
| 33 | class IntegrityStream extends MiniPass {
|
---|
| 34 | constructor (opts) {
|
---|
| 35 | super()
|
---|
| 36 | this.size = 0
|
---|
| 37 | this.opts = opts
|
---|
| 38 |
|
---|
| 39 | // may be overridden later, but set now for class consistency
|
---|
| 40 | this[_getOptions]()
|
---|
| 41 |
|
---|
| 42 | // options used for calculating stream. can't be changed.
|
---|
| 43 | const { algorithms = defaultOpts.algorithms } = opts
|
---|
| 44 | this.algorithms = Array.from(
|
---|
| 45 | new Set(algorithms.concat(this.algorithm ? [this.algorithm] : []))
|
---|
| 46 | )
|
---|
| 47 | this.hashes = this.algorithms.map(crypto.createHash)
|
---|
| 48 | }
|
---|
| 49 |
|
---|
| 50 | [_getOptions] () {
|
---|
| 51 | const {
|
---|
| 52 | integrity,
|
---|
| 53 | size,
|
---|
| 54 | options
|
---|
| 55 | } = { ...defaultOpts, ...this.opts }
|
---|
| 56 |
|
---|
| 57 | // For verification
|
---|
| 58 | this.sri = integrity ? parse(integrity, this.opts) : null
|
---|
| 59 | this.expectedSize = size
|
---|
| 60 | this.goodSri = this.sri ? !!Object.keys(this.sri).length : false
|
---|
| 61 | this.algorithm = this.goodSri ? this.sri.pickAlgorithm(this.opts) : null
|
---|
| 62 | this.digests = this.goodSri ? this.sri[this.algorithm] : null
|
---|
| 63 | this.optString = getOptString(options)
|
---|
| 64 | }
|
---|
| 65 |
|
---|
| 66 | emit (ev, data) {
|
---|
| 67 | if (ev === 'end') this[_onEnd]()
|
---|
| 68 | return super.emit(ev, data)
|
---|
| 69 | }
|
---|
| 70 |
|
---|
| 71 | write (data) {
|
---|
| 72 | this.size += data.length
|
---|
| 73 | this.hashes.forEach(h => h.update(data))
|
---|
| 74 | return super.write(data)
|
---|
| 75 | }
|
---|
| 76 |
|
---|
| 77 | [_onEnd] () {
|
---|
| 78 | if (!this.goodSri) {
|
---|
| 79 | this[_getOptions]()
|
---|
| 80 | }
|
---|
| 81 | const newSri = parse(this.hashes.map((h, i) => {
|
---|
| 82 | return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}`
|
---|
| 83 | }).join(' '), this.opts)
|
---|
| 84 | // Integrity verification mode
|
---|
| 85 | const match = this.goodSri && newSri.match(this.sri, this.opts)
|
---|
| 86 | if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) {
|
---|
| 87 | const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.expectedSize}\n Found: ${this.size}`)
|
---|
| 88 | err.code = 'EBADSIZE'
|
---|
| 89 | err.found = this.size
|
---|
| 90 | err.expected = this.expectedSize
|
---|
| 91 | err.sri = this.sri
|
---|
| 92 | this.emit('error', err)
|
---|
| 93 | } else if (this.sri && !match) {
|
---|
| 94 | const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`)
|
---|
| 95 | err.code = 'EINTEGRITY'
|
---|
| 96 | err.found = newSri
|
---|
| 97 | err.expected = this.digests
|
---|
| 98 | err.algorithm = this.algorithm
|
---|
| 99 | err.sri = this.sri
|
---|
| 100 | this.emit('error', err)
|
---|
| 101 | } else {
|
---|
| 102 | this.emit('size', this.size)
|
---|
| 103 | this.emit('integrity', newSri)
|
---|
| 104 | match && this.emit('verified', match)
|
---|
| 105 | }
|
---|
| 106 | }
|
---|
| 107 | }
|
---|
| 108 |
|
---|
| 109 | class Hash {
|
---|
| 110 | get isHash () { return true }
|
---|
| 111 | constructor (hash, opts) {
|
---|
| 112 | opts = ssriOpts(opts)
|
---|
| 113 | const strict = !!opts.strict
|
---|
| 114 | this.source = hash.trim()
|
---|
| 115 |
|
---|
| 116 | // set default values so that we make V8 happy to
|
---|
| 117 | // always see a familiar object template.
|
---|
| 118 | this.digest = ''
|
---|
| 119 | this.algorithm = ''
|
---|
| 120 | this.options = []
|
---|
| 121 |
|
---|
| 122 | // 3.1. Integrity metadata (called "Hash" by ssri)
|
---|
| 123 | // https://w3c.github.io/webappsec-subresource-integrity/#integrity-metadata-description
|
---|
| 124 | const match = this.source.match(
|
---|
| 125 | strict
|
---|
| 126 | ? STRICT_SRI_REGEX
|
---|
| 127 | : SRI_REGEX
|
---|
| 128 | )
|
---|
| 129 | if (!match) { return }
|
---|
| 130 | if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) { return }
|
---|
| 131 | this.algorithm = match[1]
|
---|
| 132 | this.digest = match[2]
|
---|
| 133 |
|
---|
| 134 | const rawOpts = match[3]
|
---|
| 135 | if (rawOpts) {
|
---|
| 136 | this.options = rawOpts.slice(1).split('?')
|
---|
| 137 | }
|
---|
| 138 | }
|
---|
| 139 |
|
---|
| 140 | hexDigest () {
|
---|
| 141 | return this.digest && Buffer.from(this.digest, 'base64').toString('hex')
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | toJSON () {
|
---|
| 145 | return this.toString()
|
---|
| 146 | }
|
---|
| 147 |
|
---|
| 148 | toString (opts) {
|
---|
| 149 | opts = ssriOpts(opts)
|
---|
| 150 | if (opts.strict) {
|
---|
| 151 | // Strict mode enforces the standard as close to the foot of the
|
---|
| 152 | // letter as it can.
|
---|
| 153 | if (!(
|
---|
| 154 | // The spec has very restricted productions for algorithms.
|
---|
| 155 | // https://www.w3.org/TR/CSP2/#source-list-syntax
|
---|
| 156 | SPEC_ALGORITHMS.some(x => x === this.algorithm) &&
|
---|
| 157 | // Usually, if someone insists on using a "different" base64, we
|
---|
| 158 | // leave it as-is, since there's multiple standards, and the
|
---|
| 159 | // specified is not a URL-safe variant.
|
---|
| 160 | // https://www.w3.org/TR/CSP2/#base64_value
|
---|
| 161 | this.digest.match(BASE64_REGEX) &&
|
---|
| 162 | // Option syntax is strictly visual chars.
|
---|
| 163 | // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-option-expression
|
---|
| 164 | // https://tools.ietf.org/html/rfc5234#appendix-B.1
|
---|
| 165 | this.options.every(opt => opt.match(VCHAR_REGEX))
|
---|
| 166 | )) {
|
---|
| 167 | return ''
|
---|
| 168 | }
|
---|
| 169 | }
|
---|
| 170 | const options = this.options && this.options.length
|
---|
| 171 | ? `?${this.options.join('?')}`
|
---|
| 172 | : ''
|
---|
| 173 | return `${this.algorithm}-${this.digest}${options}`
|
---|
| 174 | }
|
---|
| 175 | }
|
---|
| 176 |
|
---|
| 177 | class Integrity {
|
---|
| 178 | get isIntegrity () { return true }
|
---|
| 179 | toJSON () {
|
---|
| 180 | return this.toString()
|
---|
| 181 | }
|
---|
| 182 |
|
---|
| 183 | isEmpty () {
|
---|
| 184 | return Object.keys(this).length === 0
|
---|
| 185 | }
|
---|
| 186 |
|
---|
| 187 | toString (opts) {
|
---|
| 188 | opts = ssriOpts(opts)
|
---|
| 189 | let sep = opts.sep || ' '
|
---|
| 190 | if (opts.strict) {
|
---|
| 191 | // Entries must be separated by whitespace, according to spec.
|
---|
| 192 | sep = sep.replace(/\S+/g, ' ')
|
---|
| 193 | }
|
---|
| 194 | return Object.keys(this).map(k => {
|
---|
| 195 | return this[k].map(hash => {
|
---|
| 196 | return Hash.prototype.toString.call(hash, opts)
|
---|
| 197 | }).filter(x => x.length).join(sep)
|
---|
| 198 | }).filter(x => x.length).join(sep)
|
---|
| 199 | }
|
---|
| 200 |
|
---|
| 201 | concat (integrity, opts) {
|
---|
| 202 | opts = ssriOpts(opts)
|
---|
| 203 | const other = typeof integrity === 'string'
|
---|
| 204 | ? integrity
|
---|
| 205 | : stringify(integrity, opts)
|
---|
| 206 | return parse(`${this.toString(opts)} ${other}`, opts)
|
---|
| 207 | }
|
---|
| 208 |
|
---|
| 209 | hexDigest () {
|
---|
| 210 | return parse(this, { single: true }).hexDigest()
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | // add additional hashes to an integrity value, but prevent
|
---|
| 214 | // *changing* an existing integrity hash.
|
---|
| 215 | merge (integrity, opts) {
|
---|
| 216 | opts = ssriOpts(opts)
|
---|
| 217 | const other = parse(integrity, opts)
|
---|
| 218 | for (const algo in other) {
|
---|
| 219 | if (this[algo]) {
|
---|
| 220 | if (!this[algo].find(hash =>
|
---|
| 221 | other[algo].find(otherhash =>
|
---|
| 222 | hash.digest === otherhash.digest))) {
|
---|
| 223 | throw new Error('hashes do not match, cannot update integrity')
|
---|
| 224 | }
|
---|
| 225 | } else {
|
---|
| 226 | this[algo] = other[algo]
|
---|
| 227 | }
|
---|
| 228 | }
|
---|
| 229 | }
|
---|
| 230 |
|
---|
| 231 | match (integrity, opts) {
|
---|
| 232 | opts = ssriOpts(opts)
|
---|
| 233 | const other = parse(integrity, opts)
|
---|
| 234 | const algo = other.pickAlgorithm(opts)
|
---|
| 235 | return (
|
---|
| 236 | this[algo] &&
|
---|
| 237 | other[algo] &&
|
---|
| 238 | this[algo].find(hash =>
|
---|
| 239 | other[algo].find(otherhash =>
|
---|
| 240 | hash.digest === otherhash.digest
|
---|
| 241 | )
|
---|
| 242 | )
|
---|
| 243 | ) || false
|
---|
| 244 | }
|
---|
| 245 |
|
---|
| 246 | pickAlgorithm (opts) {
|
---|
| 247 | opts = ssriOpts(opts)
|
---|
| 248 | const pickAlgorithm = opts.pickAlgorithm
|
---|
| 249 | const keys = Object.keys(this)
|
---|
| 250 | return keys.reduce((acc, algo) => {
|
---|
| 251 | return pickAlgorithm(acc, algo) || acc
|
---|
| 252 | })
|
---|
| 253 | }
|
---|
| 254 | }
|
---|
| 255 |
|
---|
| 256 | module.exports.parse = parse
|
---|
| 257 | function parse (sri, opts) {
|
---|
| 258 | if (!sri) return null
|
---|
| 259 | opts = ssriOpts(opts)
|
---|
| 260 | if (typeof sri === 'string') {
|
---|
| 261 | return _parse(sri, opts)
|
---|
| 262 | } else if (sri.algorithm && sri.digest) {
|
---|
| 263 | const fullSri = new Integrity()
|
---|
| 264 | fullSri[sri.algorithm] = [sri]
|
---|
| 265 | return _parse(stringify(fullSri, opts), opts)
|
---|
| 266 | } else {
|
---|
| 267 | return _parse(stringify(sri, opts), opts)
|
---|
| 268 | }
|
---|
| 269 | }
|
---|
| 270 |
|
---|
| 271 | function _parse (integrity, opts) {
|
---|
| 272 | // 3.4.3. Parse metadata
|
---|
| 273 | // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
|
---|
| 274 | if (opts.single) {
|
---|
| 275 | return new Hash(integrity, opts)
|
---|
| 276 | }
|
---|
| 277 | const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => {
|
---|
| 278 | const hash = new Hash(string, opts)
|
---|
| 279 | if (hash.algorithm && hash.digest) {
|
---|
| 280 | const algo = hash.algorithm
|
---|
| 281 | if (!acc[algo]) { acc[algo] = [] }
|
---|
| 282 | acc[algo].push(hash)
|
---|
| 283 | }
|
---|
| 284 | return acc
|
---|
| 285 | }, new Integrity())
|
---|
| 286 | return hashes.isEmpty() ? null : hashes
|
---|
| 287 | }
|
---|
| 288 |
|
---|
| 289 | module.exports.stringify = stringify
|
---|
| 290 | function stringify (obj, opts) {
|
---|
| 291 | opts = ssriOpts(opts)
|
---|
| 292 | if (obj.algorithm && obj.digest) {
|
---|
| 293 | return Hash.prototype.toString.call(obj, opts)
|
---|
| 294 | } else if (typeof obj === 'string') {
|
---|
| 295 | return stringify(parse(obj, opts), opts)
|
---|
| 296 | } else {
|
---|
| 297 | return Integrity.prototype.toString.call(obj, opts)
|
---|
| 298 | }
|
---|
| 299 | }
|
---|
| 300 |
|
---|
| 301 | module.exports.fromHex = fromHex
|
---|
| 302 | function fromHex (hexDigest, algorithm, opts) {
|
---|
| 303 | opts = ssriOpts(opts)
|
---|
| 304 | const optString = getOptString(opts.options)
|
---|
| 305 | return parse(
|
---|
| 306 | `${algorithm}-${
|
---|
| 307 | Buffer.from(hexDigest, 'hex').toString('base64')
|
---|
| 308 | }${optString}`, opts
|
---|
| 309 | )
|
---|
| 310 | }
|
---|
| 311 |
|
---|
| 312 | module.exports.fromData = fromData
|
---|
| 313 | function fromData (data, opts) {
|
---|
| 314 | opts = ssriOpts(opts)
|
---|
| 315 | const algorithms = opts.algorithms
|
---|
| 316 | const optString = getOptString(opts.options)
|
---|
| 317 | return algorithms.reduce((acc, algo) => {
|
---|
| 318 | const digest = crypto.createHash(algo).update(data).digest('base64')
|
---|
| 319 | const hash = new Hash(
|
---|
| 320 | `${algo}-${digest}${optString}`,
|
---|
| 321 | opts
|
---|
| 322 | )
|
---|
| 323 | /* istanbul ignore else - it would be VERY strange if the string we
|
---|
| 324 | * just calculated with an algo did not have an algo or digest.
|
---|
| 325 | */
|
---|
| 326 | if (hash.algorithm && hash.digest) {
|
---|
| 327 | const algo = hash.algorithm
|
---|
| 328 | if (!acc[algo]) { acc[algo] = [] }
|
---|
| 329 | acc[algo].push(hash)
|
---|
| 330 | }
|
---|
| 331 | return acc
|
---|
| 332 | }, new Integrity())
|
---|
| 333 | }
|
---|
| 334 |
|
---|
| 335 | module.exports.fromStream = fromStream
|
---|
| 336 | function fromStream (stream, opts) {
|
---|
| 337 | opts = ssriOpts(opts)
|
---|
| 338 | const istream = integrityStream(opts)
|
---|
| 339 | return new Promise((resolve, reject) => {
|
---|
| 340 | stream.pipe(istream)
|
---|
| 341 | stream.on('error', reject)
|
---|
| 342 | istream.on('error', reject)
|
---|
| 343 | let sri
|
---|
| 344 | istream.on('integrity', s => { sri = s })
|
---|
| 345 | istream.on('end', () => resolve(sri))
|
---|
| 346 | istream.on('data', () => {})
|
---|
| 347 | })
|
---|
| 348 | }
|
---|
| 349 |
|
---|
| 350 | module.exports.checkData = checkData
|
---|
| 351 | function checkData (data, sri, opts) {
|
---|
| 352 | opts = ssriOpts(opts)
|
---|
| 353 | sri = parse(sri, opts)
|
---|
| 354 | if (!sri || !Object.keys(sri).length) {
|
---|
| 355 | if (opts.error) {
|
---|
| 356 | throw Object.assign(
|
---|
| 357 | new Error('No valid integrity hashes to check against'), {
|
---|
| 358 | code: 'EINTEGRITY'
|
---|
| 359 | }
|
---|
| 360 | )
|
---|
| 361 | } else {
|
---|
| 362 | return false
|
---|
| 363 | }
|
---|
| 364 | }
|
---|
| 365 | const algorithm = sri.pickAlgorithm(opts)
|
---|
| 366 | const digest = crypto.createHash(algorithm).update(data).digest('base64')
|
---|
| 367 | const newSri = parse({ algorithm, digest })
|
---|
| 368 | const match = newSri.match(sri, opts)
|
---|
| 369 | if (match || !opts.error) {
|
---|
| 370 | return match
|
---|
| 371 | } else if (typeof opts.size === 'number' && (data.length !== opts.size)) {
|
---|
| 372 | const err = new Error(`data size mismatch when checking ${sri}.\n Wanted: ${opts.size}\n Found: ${data.length}`)
|
---|
| 373 | err.code = 'EBADSIZE'
|
---|
| 374 | err.found = data.length
|
---|
| 375 | err.expected = opts.size
|
---|
| 376 | err.sri = sri
|
---|
| 377 | throw err
|
---|
| 378 | } else {
|
---|
| 379 | const err = new Error(`Integrity checksum failed when using ${algorithm}: Wanted ${sri}, but got ${newSri}. (${data.length} bytes)`)
|
---|
| 380 | err.code = 'EINTEGRITY'
|
---|
| 381 | err.found = newSri
|
---|
| 382 | err.expected = sri
|
---|
| 383 | err.algorithm = algorithm
|
---|
| 384 | err.sri = sri
|
---|
| 385 | throw err
|
---|
| 386 | }
|
---|
| 387 | }
|
---|
| 388 |
|
---|
| 389 | module.exports.checkStream = checkStream
|
---|
| 390 | function checkStream (stream, sri, opts) {
|
---|
| 391 | opts = ssriOpts(opts)
|
---|
| 392 | opts.integrity = sri
|
---|
| 393 | sri = parse(sri, opts)
|
---|
| 394 | if (!sri || !Object.keys(sri).length) {
|
---|
| 395 | return Promise.reject(Object.assign(
|
---|
| 396 | new Error('No valid integrity hashes to check against'), {
|
---|
| 397 | code: 'EINTEGRITY'
|
---|
| 398 | }
|
---|
| 399 | ))
|
---|
| 400 | }
|
---|
| 401 | const checker = integrityStream(opts)
|
---|
| 402 | return new Promise((resolve, reject) => {
|
---|
| 403 | stream.pipe(checker)
|
---|
| 404 | stream.on('error', reject)
|
---|
| 405 | checker.on('error', reject)
|
---|
| 406 | let sri
|
---|
| 407 | checker.on('verified', s => { sri = s })
|
---|
| 408 | checker.on('end', () => resolve(sri))
|
---|
| 409 | checker.on('data', () => {})
|
---|
| 410 | })
|
---|
| 411 | }
|
---|
| 412 |
|
---|
| 413 | module.exports.integrityStream = integrityStream
|
---|
| 414 | function integrityStream (opts = {}) {
|
---|
| 415 | return new IntegrityStream(opts)
|
---|
| 416 | }
|
---|
| 417 |
|
---|
| 418 | module.exports.create = createIntegrity
|
---|
| 419 | function createIntegrity (opts) {
|
---|
| 420 | opts = ssriOpts(opts)
|
---|
| 421 | const algorithms = opts.algorithms
|
---|
| 422 | const optString = getOptString(opts.options)
|
---|
| 423 |
|
---|
| 424 | const hashes = algorithms.map(crypto.createHash)
|
---|
| 425 |
|
---|
| 426 | return {
|
---|
| 427 | update: function (chunk, enc) {
|
---|
| 428 | hashes.forEach(h => h.update(chunk, enc))
|
---|
| 429 | return this
|
---|
| 430 | },
|
---|
| 431 | digest: function (enc) {
|
---|
| 432 | const integrity = algorithms.reduce((acc, algo) => {
|
---|
| 433 | const digest = hashes.shift().digest('base64')
|
---|
| 434 | const hash = new Hash(
|
---|
| 435 | `${algo}-${digest}${optString}`,
|
---|
| 436 | opts
|
---|
| 437 | )
|
---|
| 438 | /* istanbul ignore else - it would be VERY strange if the hash we
|
---|
| 439 | * just calculated with an algo did not have an algo or digest.
|
---|
| 440 | */
|
---|
| 441 | if (hash.algorithm && hash.digest) {
|
---|
| 442 | const algo = hash.algorithm
|
---|
| 443 | if (!acc[algo]) { acc[algo] = [] }
|
---|
| 444 | acc[algo].push(hash)
|
---|
| 445 | }
|
---|
| 446 | return acc
|
---|
| 447 | }, new Integrity())
|
---|
| 448 |
|
---|
| 449 | return integrity
|
---|
| 450 | }
|
---|
| 451 | }
|
---|
| 452 | }
|
---|
| 453 |
|
---|
| 454 | const NODE_HASHES = new Set(crypto.getHashes())
|
---|
| 455 |
|
---|
| 456 | // This is a Best Effort™ at a reasonable priority for hash algos
|
---|
| 457 | const DEFAULT_PRIORITY = [
|
---|
| 458 | 'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
|
---|
| 459 | // TODO - it's unclear _which_ of these Node will actually use as its name
|
---|
| 460 | // for the algorithm, so we guesswork it based on the OpenSSL names.
|
---|
| 461 | 'sha3',
|
---|
| 462 | 'sha3-256', 'sha3-384', 'sha3-512',
|
---|
| 463 | 'sha3_256', 'sha3_384', 'sha3_512'
|
---|
| 464 | ].filter(algo => NODE_HASHES.has(algo))
|
---|
| 465 |
|
---|
| 466 | function getPrioritizedHash (algo1, algo2) {
|
---|
| 467 | return DEFAULT_PRIORITY.indexOf(algo1.toLowerCase()) >= DEFAULT_PRIORITY.indexOf(algo2.toLowerCase())
|
---|
| 468 | ? algo1
|
---|
| 469 | : algo2
|
---|
| 470 | }
|
---|