Files
bare-operating-system/packages/bare-os-booter/lib/shell/shell-arithmetic.js
T
2026-08-18 18:11:28 -04:00

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))
}