101 lines
2.9 KiB
JavaScript
101 lines
2.9 KiB
JavaScript
/**
|
|
* POSIX-ish $(( )) arithmetic used by the guest shell.
|
|
*/
|
|
import { shellCheckUnboundParam } from './shell-nounset.js'
|
|
|
|
/**
|
|
* Expand `$VAR`, `${VAR}`, and `$N` inside arithmetic before identifier substitution.
|
|
* @param {string} inner
|
|
* @param {Record<string, string>} env
|
|
*/
|
|
export function bareOsArithmeticExpandDollarForms(inner, env) {
|
|
const s = String(inner || '')
|
|
if (s.length > 4096) throw new Error('shell: arithmetic: expression too long')
|
|
/** @type {string[]} */
|
|
const parts = []
|
|
let i = 0
|
|
let replacements = 0
|
|
const maxRep = 256
|
|
while (i < s.length) {
|
|
if (s[i] !== '$') {
|
|
parts.push(s[i++])
|
|
continue
|
|
}
|
|
if (++replacements > maxRep)
|
|
throw new Error('shell: arithmetic: expansion limit exceeded')
|
|
if (i + 1 >= s.length) {
|
|
parts.push('$')
|
|
i++
|
|
continue
|
|
}
|
|
if (s[i + 1] === '{') {
|
|
const end = s.indexOf('}', i + 2)
|
|
if (end === -1) {
|
|
parts.push('$')
|
|
i++
|
|
continue
|
|
}
|
|
const name = s.slice(i + 2, end).trim()
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
throw new Error('shell: arithmetic: invalid ${} in expression')
|
|
shellCheckUnboundParam(name, env)
|
|
const raw = env[name]
|
|
const n = Number.parseInt(String(raw ?? '0').trim(), 10)
|
|
parts.push(String(Number.isFinite(n) ? n : 0))
|
|
i = end + 1
|
|
continue
|
|
}
|
|
const c1 = s[i + 1]
|
|
if (/[0-9]/.test(c1)) {
|
|
shellCheckUnboundParam(c1, env)
|
|
const raw = env[c1]
|
|
const n = Number.parseInt(String(raw ?? '0').trim(), 10)
|
|
parts.push(String(Number.isFinite(n) ? n : 0))
|
|
i += 2
|
|
continue
|
|
}
|
|
if (!/[A-Za-z_]/.test(c1)) {
|
|
parts.push('$')
|
|
i++
|
|
continue
|
|
}
|
|
let k = i + 2
|
|
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
|
|
const name = s.slice(i + 1, k)
|
|
shellCheckUnboundParam(name, env)
|
|
const raw = env[name]
|
|
const n = Number.parseInt(String(raw ?? '0').trim(), 10)
|
|
parts.push(String(Number.isFinite(n) ? n : 0))
|
|
i = k
|
|
}
|
|
return parts.join('')
|
|
}
|
|
|
|
/**
|
|
* @param {string} expr
|
|
* @param {Record<string, string>} env
|
|
*/
|
|
export function bareOsEvalArithmeticExpr(expr, env) {
|
|
const pre = bareOsArithmeticExpandDollarForms(String(expr || ''), env)
|
|
const src = pre.trim()
|
|
if (!src) return '0'
|
|
if (!/^[A-Za-z0-9_+\-*/%()\s]+$/.test(src)) {
|
|
throw new Error('shell: arithmetic: invalid token')
|
|
}
|
|
const replaced = src.replace(/\b[A-Za-z_][A-Za-z0-9_]*\b/g, (name) => {
|
|
shellCheckUnboundParam(name, env)
|
|
const raw = env[name]
|
|
const n = Number.parseInt(String(raw ?? '0').trim(), 10)
|
|
return String(Number.isFinite(n) ? n : 0)
|
|
})
|
|
let out = 0
|
|
try {
|
|
out = Function('"use strict"; return (' + replaced + ');')()
|
|
} catch {
|
|
throw new Error('shell: arithmetic: invalid expression')
|
|
}
|
|
const num = Number(out)
|
|
if (!Number.isFinite(num)) throw new Error('shell: arithmetic: non-finite result')
|
|
return String(Math.trunc(num))
|
|
}
|