Skip to content

Commit 1384cee

Browse files
authored
fix(NODE-5546): decimal 128 fromString performs inexact rounding (#613)
1 parent 06f1774 commit 1384cee

File tree

4 files changed

+81
-72
lines changed

4 files changed

+81
-72
lines changed

Diff for: src/decimal128.ts

+35-67
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export class Decimal128 extends BSONValue {
160160
static fromString(representation: string): Decimal128 {
161161
// Parse state tracking
162162
let isNegative = false;
163+
let sawSign = false;
163164
let sawRadix = false;
164165
let foundNonZero = false;
165166

@@ -180,15 +181,11 @@ export class Decimal128 extends BSONValue {
180181
let nDigitsStored = 0;
181182
// Insertion pointer for digits
182183
let digitsInsert = 0;
183-
// The index of the first non-zero digit
184-
let firstDigit = 0;
185184
// The index of the last digit
186185
let lastDigit = 0;
187186

188187
// Exponent
189188
let exponent = 0;
190-
// loop index over array
191-
let i = 0;
192189
// The high 17 digits of the significand
193190
let significandHigh = new Long(0, 0);
194191
// The low 17 digits of the significand
@@ -241,6 +238,7 @@ export class Decimal128 extends BSONValue {
241238

242239
// Get the negative or positive sign
243240
if (representation[index] === '+' || representation[index] === '-') {
241+
sawSign = true;
244242
isNegative = representation[index++] === '-';
245243
}
246244

@@ -263,7 +261,7 @@ export class Decimal128 extends BSONValue {
263261
continue;
264262
}
265263

266-
if (nDigitsStored < 34) {
264+
if (nDigitsStored < MAX_DIGITS) {
267265
if (representation[index] !== '0' || foundNonZero) {
268266
if (!foundNonZero) {
269267
firstNonZero = nDigitsRead;
@@ -307,11 +305,7 @@ export class Decimal128 extends BSONValue {
307305

308306
// Done reading input
309307
// Find first non-zero digit in digits
310-
firstDigit = 0;
311-
312308
if (!nDigitsStored) {
313-
firstDigit = 0;
314-
lastDigit = 0;
315309
digits[0] = 0;
316310
nDigits = 1;
317311
nDigitsStored = 1;
@@ -320,7 +314,11 @@ export class Decimal128 extends BSONValue {
320314
lastDigit = nDigitsStored - 1;
321315
significantDigits = nDigits;
322316
if (significantDigits !== 1) {
323-
while (digits[firstNonZero + significantDigits - 1] === 0) {
317+
while (
318+
representation[
319+
firstNonZero + significantDigits - 1 + Number(sawSign) + Number(sawRadix)
320+
] === '0'
321+
) {
324322
significantDigits = significantDigits - 1;
325323
}
326324
}
@@ -331,7 +329,7 @@ export class Decimal128 extends BSONValue {
331329
// to represent user input
332330

333331
// Overflow prevention
334-
if (exponent <= radixPosition && radixPosition - exponent > 1 << 14) {
332+
if (exponent <= radixPosition && radixPosition > exponent + (1 << 14)) {
335333
exponent = EXPONENT_MIN;
336334
} else {
337335
exponent = exponent - radixPosition;
@@ -341,11 +339,9 @@ export class Decimal128 extends BSONValue {
341339
while (exponent > EXPONENT_MAX) {
342340
// Shift exponent to significand and decrease
343341
lastDigit = lastDigit + 1;
344-
345-
if (lastDigit - firstDigit > MAX_DIGITS) {
342+
if (lastDigit >= MAX_DIGITS) {
346343
// Check if we have a zero then just hard clamp, otherwise fail
347-
const digitsString = digits.join('');
348-
if (digitsString.match(/^0+$/)) {
344+
if (significantDigits === 0) {
349345
exponent = EXPONENT_MAX;
350346
break;
351347
}
@@ -357,85 +353,57 @@ export class Decimal128 extends BSONValue {
357353

358354
while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) {
359355
// Shift last digit. can only do this if < significant digits than # stored.
360-
if (lastDigit === 0 && significantDigits < nDigitsStored) {
361-
exponent = EXPONENT_MIN;
362-
significantDigits = 0;
363-
break;
356+
if (lastDigit === 0) {
357+
if (significantDigits === 0) {
358+
exponent = EXPONENT_MIN;
359+
break;
360+
}
361+
362+
invalidErr(representation, 'exponent underflow');
364363
}
365364

366365
if (nDigitsStored < nDigits) {
366+
if (
367+
representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' &&
368+
significantDigits !== 0
369+
) {
370+
invalidErr(representation, 'inexact rounding');
371+
}
367372
// adjust to match digits not stored
368373
nDigits = nDigits - 1;
369374
} else {
375+
if (digits[lastDigit] !== 0) {
376+
invalidErr(representation, 'inexact rounding');
377+
}
370378
// adjust to round
371379
lastDigit = lastDigit - 1;
372380
}
373381

374382
if (exponent < EXPONENT_MAX) {
375383
exponent = exponent + 1;
376384
} else {
377-
// Check if we have a zero then just hard clamp, otherwise fail
378-
const digitsString = digits.join('');
379-
if (digitsString.match(/^0+$/)) {
380-
exponent = EXPONENT_MAX;
381-
break;
382-
}
383385
invalidErr(representation, 'overflow');
384386
}
385387
}
386388

387389
// Round
388390
// We've normalized the exponent, but might still need to round.
389-
if (lastDigit - firstDigit + 1 < significantDigits) {
390-
let endOfString = nDigitsRead;
391-
391+
if (lastDigit + 1 < significantDigits) {
392392
// If we have seen a radix point, 'string' is 1 longer than we have
393393
// documented with ndigits_read, so inc the position of the first nonzero
394394
// digit and the position that digits are read to.
395395
if (sawRadix) {
396396
firstNonZero = firstNonZero + 1;
397-
endOfString = endOfString + 1;
398397
}
399-
// if negative, we need to increment again to account for - sign at start.
400-
if (isNegative) {
398+
// if saw sign, we need to increment again to account for - or + sign at start.
399+
if (sawSign) {
401400
firstNonZero = firstNonZero + 1;
402-
endOfString = endOfString + 1;
403401
}
404402

405403
const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10);
406-
let roundBit = 0;
407-
408-
if (roundDigit >= 5) {
409-
roundBit = 1;
410-
if (roundDigit === 5) {
411-
roundBit = digits[lastDigit] % 2 === 1 ? 1 : 0;
412-
for (i = firstNonZero + lastDigit + 2; i < endOfString; i++) {
413-
if (parseInt(representation[i], 10)) {
414-
roundBit = 1;
415-
break;
416-
}
417-
}
418-
}
419-
}
420404

421-
if (roundBit) {
422-
let dIdx = lastDigit;
423-
424-
for (; dIdx >= 0; dIdx--) {
425-
if (++digits[dIdx] > 9) {
426-
digits[dIdx] = 0;
427-
428-
// overflowed most significant digit
429-
if (dIdx === 0) {
430-
if (exponent < EXPONENT_MAX) {
431-
exponent = exponent + 1;
432-
digits[dIdx] = 1;
433-
} else {
434-
return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER);
435-
}
436-
}
437-
}
438-
}
405+
if (roundDigit !== 0) {
406+
invalidErr(representation, 'inexact rounding');
439407
}
440408
}
441409

@@ -449,8 +417,8 @@ export class Decimal128 extends BSONValue {
449417
if (significantDigits === 0) {
450418
significandHigh = Long.fromNumber(0);
451419
significandLow = Long.fromNumber(0);
452-
} else if (lastDigit - firstDigit < 17) {
453-
let dIdx = firstDigit;
420+
} else if (lastDigit < 17) {
421+
let dIdx = 0;
454422
significandLow = Long.fromNumber(digits[dIdx++]);
455423
significandHigh = new Long(0, 0);
456424

@@ -459,7 +427,7 @@ export class Decimal128 extends BSONValue {
459427
significandLow = significandLow.add(Long.fromNumber(digits[dIdx]));
460428
}
461429
} else {
462-
let dIdx = firstDigit;
430+
let dIdx = 0;
463431
significandHigh = Long.fromNumber(digits[dIdx++]);
464432

465433
for (; dIdx <= lastDigit - 17; dIdx++) {

Diff for: test/node/bson_corpus.spec.test.js

-5
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,8 @@ function normalize(cEJ) {
5555

5656
const parseErrorForDecimal128 = scenario => {
5757
// TODO(NODE-3637): remove regex of skipped tests and and add errors to d128 parsing
58-
const skipRegex = /dqbsr|Inexact/;
5958
for (const parseError of scenario.parseErrors) {
6059
it(parseError.description, function () {
61-
if (skipRegex.test(parseError.description)) {
62-
this.skip();
63-
}
64-
6560
expect(
6661
() => BSON.Decimal128.fromString(parseError.string),
6762
`Decimal.fromString('${parseError.string}') should throw`

Diff for: test/node/decimal128_tests.js

+22
Original file line numberDiff line numberDiff line change
@@ -1211,4 +1211,26 @@ describe('Decimal128', function () {
12111211
expect(() => new Decimal128(Buffer.alloc(16))).to.not.throw();
12121212
expect(() => new Decimal128(new Uint8Array(16))).to.not.throw();
12131213
});
1214+
1215+
context('fromString()', function () {
1216+
context("when input has leading '+' and has more than 34 significant digits", function () {
1217+
it('throws BSON error on inexact rounding', function () {
1218+
expect(() =>
1219+
Decimal128.fromString('+100000000000000000000000000000000000000000000001')
1220+
).to.throw(BSON.BSONError, /inexact rounding/);
1221+
});
1222+
});
1223+
1224+
context(
1225+
'when input has 1 significant digits, 34 total digits and an exponent greater than exponent_max',
1226+
function () {
1227+
it('throws BSON error reporting overflow', function () {
1228+
expect(() => Decimal128.fromString('1000000000000000000000000000000000e6112')).to.throw(
1229+
BSON.BSONError,
1230+
/overflow/
1231+
);
1232+
});
1233+
}
1234+
);
1235+
});
12141236
});

Diff for: test/node/specs/bson-corpus/decimal128-1.json

+24
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,30 @@
312312
"canonical_bson": "18000000136400000000000a5bc138938d44c64d31cc3700",
313313
"degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}}",
314314
"canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"1.000000000000000000000000000000000E+999\"}}"
315+
},
316+
{
317+
"description": "Clamped zeros with a large positive exponent",
318+
"canonical_bson": "180000001364000000000000000000000000000000FE5F00",
319+
"degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E+2147483647\"}}",
320+
"canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E+6111\"}}"
321+
},
322+
{
323+
"description": "Clamped zeros with a large negative exponent",
324+
"canonical_bson": "180000001364000000000000000000000000000000000000",
325+
"degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E-2147483647\"}}",
326+
"canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0E-6176\"}}"
327+
},
328+
{
329+
"description": "Clamped negative zeros with a large positive exponent",
330+
"canonical_bson": "180000001364000000000000000000000000000000FEDF00",
331+
"degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E+2147483647\"}}",
332+
"canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E+6111\"}}"
333+
},
334+
{
335+
"description": "Clamped negative zeros with a large negative exponent",
336+
"canonical_bson": "180000001364000000000000000000000000000000008000",
337+
"degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E-2147483647\"}}",
338+
"canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0E-6176\"}}"
315339
}
316340
]
317341
}

0 commit comments

Comments
 (0)