From 9ddd12fbc585d8dfe2380e093e29ea8e1bd8fdb5 Mon Sep 17 00:00:00 2001 From: JB Date: Fri, 18 Sep 2026 23:54:52 -0400 Subject: [PATCH] fix: bound money conversion after cents scaling --- src/lib/domain/money.ts | 17 ++++++++++------- tests/money-formatting.test.ts | 7 +++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/lib/domain/money.ts b/src/lib/domain/money.ts index a96087b..7a09f4f 100644 --- a/src/lib/domain/money.ts +++ b/src/lib/domain/money.ts @@ -7,8 +7,9 @@ export type IskLike = string | bigint | number; const INTEGER_TEXT = /^-?\d+$/; -// The final lookahead makes the end assertion strict: JavaScript's `$` alone -// would also accept a match immediately before one trailing newline. +// The final lookahead makes the end assertion strict. This intentionally +// tightens the old ESI regex, because JavaScript's `$` alone also accepts a +// match immediately before one trailing newline. const DECIMAL_TEXT = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$(?![\s\S])/; /** Resource bounds for signed decimal ingestion. These reject excess input; no truncation is performed. */ @@ -68,18 +69,20 @@ export function decimalIskToCents(value: string): string { } const exponent = exponentText === undefined ? 0n : parseDecimalExponent(exponentText); - const decimalPlaces = BigInt(fraction.length) - exponent; - if (decimalPlaces < -MAX_DECIMAL_SHIFT || decimalPlaces > MAX_DECIMAL_SHIFT) { + // Convert the decimal coefficient to cents before applying the resource + // bound: value * 10^(exponent - fractionDigits) * 100. + const centsShift = exponent + 2n - BigInt(fraction.length); + if (centsShift < -MAX_DECIMAL_SHIFT || centsShift > MAX_DECIMAL_SHIFT) { throw new RangeError('ISK decimal shift exceeds resource bounds'); } const digits = `${whole}${fraction}`.replace(/^0+(?=\d)/, ''); const significand = BigInt(digits); let magnitude: bigint; - if (decimalPlaces <= 2n) { - magnitude = significand * 10n ** (2n - decimalPlaces); + if (centsShift >= 0n) { + magnitude = significand * 10n ** centsShift; } else { - const divisor = 10n ** (decimalPlaces - 2n); + const divisor = 10n ** -centsShift; const quotient = significand / divisor; const remainder = significand % divisor; // Half-away-from-zero is half-up for this non-negative magnitude. diff --git a/tests/money-formatting.test.ts b/tests/money-formatting.test.ts index a197785..bacde32 100644 --- a/tests/money-formatting.test.ts +++ b/tests/money-formatting.test.ts @@ -37,9 +37,12 @@ describe('exact decimal ISK conversion', () => { expect(() => decimalIskToCents(`1e${'0'.repeat(8191)}`)).toThrow('input exceeds resource bounds'); expect(() => decimalIskToCents('1e1001')).toThrow('exponent is outside bounds'); expect(() => decimalIskToCents('1e-1001')).toThrow('exponent is outside bounds'); - expect(decimalIskToCents('1e1000')).toBe(`1${'0'.repeat(1002)}`); + expect(decimalIskToCents('1e998')).toBe(`1${'0'.repeat(1000)}`); + expect(() => decimalIskToCents('1e999')).toThrow('shift exceeds resource bounds'); + expect(() => decimalIskToCents('1e1000')).toThrow('shift exceeds resource bounds'); + expect(decimalIskToCents(`1.${'0'.repeat(1002)}`)).toBe('100'); + expect(() => decimalIskToCents(`1.${'0'.repeat(1003)}`)).toThrow('shift exceeds resource bounds'); expect(decimalIskToCents(`0.${'0'.repeat(999)}1`)).toBe('0'); - expect(() => decimalIskToCents(`0.${'0'.repeat(1000)}1`)).toThrow('shift exceeds resource bounds'); expect(decimalIskToCents('1'.repeat(4096))).toBe(`${BigInt('1'.repeat(4096)) * 100n}`); expect(() => decimalIskToCents('1'.repeat(4097))).toThrow('coefficient exceeds resource bounds'); });