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