diff --git a/packages/loot-core/src/server/budget/actions.js b/packages/loot-core/src/server/budget/actions.js index fac045a..10f28e7 100644 --- a/packages/loot-core/src/server/budget/actions.js +++ b/packages/loot-core/src/server/budget/actions.js @@ -3,10 +3,11 @@ import * as db from '../db'; import * as prefs from '../prefs'; import * as sheet from '../sheet'; import { batchMessages } from '../sync'; +import { safeNumber } from './util'; async function getSheetValue(sheetName, cell) { const node = await sheet.getCell(sheetName, cell); - return typeof node.value === 'number' ? node.value : 0; + return safeNumber(typeof node.value === 'number' ? node.value : 0); } // We want to only allow the positive movement of money back and @@ -71,9 +72,7 @@ export function getBudget({ category, month }) { } export function setBudget({ category, month, amount }) { - if (typeof amount !== 'number') { - amount = 0; - } + amount = safeNumber(typeof amount === 'number' ? amount : 0); const table = getBudgetTable(); let existing = db.firstSync( diff --git a/packages/loot-core/src/server/budget/rollover.js b/packages/loot-core/src/server/budget/rollover.js index 1df8260..4a119aa 100644 --- a/packages/loot-core/src/server/budget/rollover.js +++ b/packages/loot-core/src/server/budget/rollover.js @@ -1,6 +1,6 @@ import * as monthUtils from '../../shared/months'; import * as sheet from '../sheet'; -import { number, sumAmounts, flatten2, unflatten2 } from './util'; +import { number, sumAmounts, flatten2, unflatten2, safeNumber } from './util'; const { resolveName } = require('../spreadsheet/util'); diff --git a/packages/loot-core/src/server/budget/util.js b/packages/loot-core/src/server/budget/util.js index ae1a288..e4d3051 100644 --- a/packages/loot-core/src/server/budget/util.js +++ b/packages/loot-core/src/server/budget/util.js @@ -19,3 +19,23 @@ export function unflatten2(arr) { } return res; } + +// Note that we don't restrict values to `Number.MIN_SAFE_INTEGER <= value <= Number.MAX_SAFE_INTEGER` +// where `Number.MAX_SAFE_INTEGER == 2^53 - 1` but a smaller range over `-(2^43-1) <= value <= 2^43 - 1`. +// This ensure that the number is accurate not just for the integer component but for 3 decimal places also. +// +// This gives us the guarantee that can use `safeNumber` on number whether they are unscaled user inputs +// or they have been converted to integers (using `amountToInteger`). + +const MAX_SAFE_NUMBER = 2 ** 43 - 1; +const MIN_SAFE_NUMBER = -MAX_SAFE_NUMBER; + +export function safeNumber(value) { + value = number(value); + if (value > MAX_SAFE_NUMBER || value < MIN_SAFE_NUMBER) { + throw new Error( + "Can't safely perform arithmetic with number: " + JSON.stringify(value) + ); + } + return value; +}