commit 195c58648cc736ffe9e4be0dcfd235d96b2a5e66
parent d0775fae165d66ed22f21a688b00a4d62508022c
Author: Florian Dold <dold@taler.net>
Date: Tue, 21 Jul 2026 11:05:13 +0200
util: validate currencies up front in sub, normalize in divide
sub() returned early on underflow, so a mismatched currency in a later
operand was never reported; divide() was the only arithmetic entry point
not going through jsonifyAmount, and never normalized its dividend.
Diffstat:
1 file changed, 19 insertions(+), 9 deletions(-)
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
@@ -447,11 +447,17 @@ export class Amounts {
let value = aJ.value + Math.floor(aJ.fraction / amountFractionalBase);
let fraction = aJ.fraction % amountFractionalBase;
- for (const b of rest) {
- const bJ = Amounts.jsonifyAmount(b);
+ // Validate every operand before subtracting any of them, as add() does.
+ // Checking inside the loop made a mismatched currency invisible once an
+ // earlier operand had already saturated the result.
+ const restJ = rest.map((b) => Amounts.jsonifyAmount(b));
+ for (const bJ of restJ) {
if (bJ.currency.toUpperCase() !== aJ.currency.toUpperCase()) {
throw Error(`Mismatched currency: ${bJ.currency} and ${currency}`);
}
+ }
+
+ for (const bJ of restJ) {
if (fraction < bJ.fraction) {
if (value < 1) {
return {
@@ -512,23 +518,27 @@ export class Amounts {
/**
* Divide an amount. Throws on division by zero.
*/
- static divide(a: AmountJson, n: number): AmountJson {
+ static divide(a: AmountLike, n: number): AmountJson {
if (!Number.isInteger(n) || n <= 0) {
throw Error(`Amount can only be divided by a positive integer`);
}
+ const aJ = Amounts.jsonifyAmount(a);
+ // Normalize the dividend before dividing, as the C reference does; an
+ // out-of-range fraction would otherwise survive into the quotient.
+ const value = aJ.value + Math.floor(aJ.fraction / amountFractionalBase);
+ const fraction = aJ.fraction % amountFractionalBase;
if (n === 1) {
- return { value: a.value, fraction: a.fraction, currency: a.currency };
+ return { value, fraction, currency: aJ.currency };
}
- const r = a.value % n;
+ const r = value % n;
// Scale in BigInt: r can be as large as n-1, so r * amountFractionalBase
// exceeds 2^53 and float64 would round the fraction's low bits away.
const scaled =
- (BigInt(r) * BigInt(amountFractionalBase) + BigInt(a.fraction)) /
- BigInt(n);
+ (BigInt(r) * BigInt(amountFractionalBase) + BigInt(fraction)) / BigInt(n);
return {
- currency: a.currency,
+ currency: aJ.currency,
fraction: Number(scaled),
- value: Math.floor(a.value / n),
+ value: Math.floor(value / n),
};
}