Skip to content

Commit 31b183d

Browse files
committed
refactor(ejson): perform non-lossy direct Long comparison for safe limit check
Compare the Long object directly rather than calling toNumber() first, preventing precision loss for boundary values (e.g. MAX_SAFE_INTEGER + 2) that round back to safe limits as floats. Add clarifying JSDoc documentation about the prototype recursion constraint.
1 parent 1f618fb commit 31b183d

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

src/helpers/ejson.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { EJSON, Long } from "bson";
44
* Pre-processes an object by converting BSON Long values into EJSON format
55
* if they exceed the JavaScript safe integer limits. Safe Long values are
66
* converted to standard JavaScript numbers to maintain readability.
7+
*
8+
* Note: Recursion is restricted to plain objects (proto is Object.prototype or null)
9+
* and arrays to avoid traversing and corrupting other BSON wrapper types (e.g. ObjectId, Binary).
710
*/
811
export function serializeSafeLongs(obj: unknown): unknown {
912
if (obj === null || obj === undefined) {
@@ -15,9 +18,11 @@ export function serializeSafeLongs(obj: unknown): unknown {
1518
(typeof obj === "object" && "_bsontype" in obj && (obj as Record<string, unknown>)._bsontype === "Long")
1619
) {
1720
const longObj = obj as unknown as Long;
18-
const num = longObj.toNumber();
19-
if (num >= Number.MIN_SAFE_INTEGER && num <= Number.MAX_SAFE_INTEGER) {
20-
return num;
21+
const isSafe =
22+
longObj.lessThanOrEqual(Long.fromNumber(Number.MAX_SAFE_INTEGER)) &&
23+
longObj.greaterThanOrEqual(Long.fromNumber(Number.MIN_SAFE_INTEGER));
24+
if (isSafe) {
25+
return longObj.toNumber();
2126
}
2227
return { $numberLong: longObj.toString() };
2328
}

tests/unit/helpers/ejson.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ describe("serializeSafeLongs", () => {
2929
expect(serializeSafeLongs(unsafeLong)).toEqual({ $numberLong: "-7583362298413593073" });
3030
});
3131

32+
it("handles boundary values around safe integer limits without float lossiness", () => {
33+
// MAX_SAFE_INTEGER is 9007199254740991
34+
// 9007199254740993 is unsafe, but its float representation rounds to 9007199254740992,
35+
// which could trick a simple float-based safety check.
36+
const unsafeBoundary = Long.fromString("9007199254740993");
37+
expect(serializeSafeLongs(unsafeBoundary)).toEqual({ $numberLong: "9007199254740993" });
38+
39+
const safeBoundary = Long.fromString("9007199254740991");
40+
expect(serializeSafeLongs(safeBoundary)).toBe(9007199254740991);
41+
});
42+
3243
it("handles nested objects and arrays", () => {
3344
const input = {
3445
a: Long.fromNumber(100),

0 commit comments

Comments
 (0)