diff --git a/src/bun.js/bindings/InternalForTesting.cpp b/src/bun.js/bindings/InternalForTesting.cpp index 9e1afc3c0630..6f36f8d97153 100644 --- a/src/bun.js/bindings/InternalForTesting.cpp +++ b/src/bun.js/bindings/InternalForTesting.cpp @@ -42,4 +42,35 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck, (JSC::JSGlobalObject * glob return encodedJSUndefined(); } +extern "C" bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t min, int64_t max); +extern "C" bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t min, uint64_t max); + +// For testing JSC__isBigIntInInt64Range / JSC__isBigIntInUInt64Range from JS. +// args: (bigint, min: number|bigint, max: number|bigint, unsigned: boolean) +JSC_DEFINE_HOST_FUNCTION(jsFunction_isBigIntInRange, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue value = callFrame->argument(0); + JSValue minArg = callFrame->argument(1); + JSValue maxArg = callFrame->argument(2); + bool isUnsigned = callFrame->argument(3).toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + if (isUnsigned) { + uint64_t min = minArg.toBigUInt64(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + uint64_t max = maxArg.toBigUInt64(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsBoolean(JSC__isBigIntInUInt64Range(JSValue::encode(value), min, max))); + } + + int64_t min = minArg.toBigInt64(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + int64_t max = maxArg.toBigInt64(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsBoolean(JSC__isBigIntInInt64Range(JSValue::encode(value), min, max))); +} + } diff --git a/src/bun.js/bindings/InternalForTesting.h b/src/bun.js/bindings/InternalForTesting.h index 52e1b4a76bf0..4311017a80f2 100644 --- a/src/bun.js/bindings/InternalForTesting.h +++ b/src/bun.js/bindings/InternalForTesting.h @@ -8,5 +8,6 @@ namespace Bun { JSC_DECLARE_HOST_FUNCTION(jsFunction_arrayBufferViewHasBuffer); JSC_DECLARE_HOST_FUNCTION(jsFunction_hasReifiedStatic); JSC_DECLARE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck); +JSC_DECLARE_HOST_FUNCTION(jsFunction_isBigIntInRange); } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 8af5f71b18e8..085100d7522f 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5265,34 +5265,32 @@ extern "C" void JSC__JSValue__forEachPropertyNonIndexed(JSC::EncodedJSValue JSVa JSC__JSValue__forEachPropertyImpl(JSValue0, globalObject, arg2, iter); } -extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t max, uint64_t min) +extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t min, uint64_t max) { JSValue jsValue = JSValue::decode(value); if (!jsValue.isHeapBigInt()) return false; JSC::JSBigInt* bigInt = jsValue.asHeapBigInt(); - auto result = bigInt->compare(bigInt, min); - if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) { - return true; - } - result = bigInt->compare(bigInt, max); - return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal; + auto result = JSBigInt::compare(bigInt, min); + if (result == JSBigInt::ComparisonResult::LessThan) + return false; + result = JSBigInt::compare(bigInt, max); + return result != JSBigInt::ComparisonResult::GreaterThan; } -extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t max, int64_t min) +extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t min, int64_t max) { JSValue jsValue = JSValue::decode(value); if (!jsValue.isHeapBigInt()) return false; JSC::JSBigInt* bigInt = jsValue.asHeapBigInt(); - auto result = bigInt->compare(bigInt, min); - if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) { - return true; - } - result = bigInt->compare(bigInt, max); - return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal; + auto result = JSBigInt::compare(bigInt, min); + if (result == JSBigInt::ComparisonResult::LessThan) + return false; + result = JSBigInt::compare(bigInt, max); + return result != JSBigInt::ComparisonResult::GreaterThan; } [[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 35b7cd091664..17ad753d8cd4 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -194,6 +194,12 @@ export const getDevServerDeinitCount = $bindgenFn("DevServer.bind.ts", "getDeini export const getCounters = $newZigFunction("Counters.zig", "createCountersObject", 0); export const hasNonReifiedStatic = $newCppFunction("InternalForTesting.cpp", "jsFunction_hasReifiedStatic", 1); +export const isBigIntInRange: (value: bigint, min: bigint, max: bigint, unsigned: boolean) => boolean = $newCppFunction( + "InternalForTesting.cpp", + "jsFunction_isBigIntInRange", + 4, +); + interface setSocketOptionsFn { (socket: Bun.Socket, sendBuffer: 1, size: number): void; (socket: Bun.Socket, recvBuffer: 2, size: number): void; diff --git a/test/js/bun/jsc/bigint-range-check.test.ts b/test/js/bun/jsc/bigint-range-check.test.ts new file mode 100644 index 000000000000..2ef36d138ebd --- /dev/null +++ b/test/js/bun/jsc/bigint-range-check.test.ts @@ -0,0 +1,85 @@ +import { isBigIntInRange } from "bun:internal-for-testing"; +import { describe, expect, test } from "bun:test"; + +// Regression test for JSC__isBigIntInInt64Range / JSC__isBigIntInUInt64Range. +// Previously the C++ implementation had swapped (min, max) parameter names AND +// used OR-short-circuit logic, causing the function to return true when the +// value was OUTSIDE the range and false when it was INSIDE. +// This affected MySQL BigInt parameter binding (all in-range values were +// rejected with ERR_OUT_OF_RANGE). + +const I64_MIN = -9223372036854775808n; +const I64_MAX = 9223372036854775807n; +const U64_MAX = 18446744073709551615n; + +describe("isBigIntInInt64Range (signed)", () => { + test("100n is in [i64_min, i64_max]", () => { + expect(isBigIntInRange(100n, I64_MIN, I64_MAX, false)).toBe(true); + }); + + test("0n is in [i64_min, i64_max]", () => { + expect(isBigIntInRange(0n, I64_MIN, I64_MAX, false)).toBe(true); + }); + + test("-100n is in [i64_min, i64_max]", () => { + expect(isBigIntInRange(-100n, I64_MIN, I64_MAX, false)).toBe(true); + }); + + test("i64_max boundary is in range", () => { + expect(isBigIntInRange(I64_MAX, I64_MIN, I64_MAX, false)).toBe(true); + }); + + test("i64_min boundary is in range", () => { + expect(isBigIntInRange(I64_MIN, I64_MIN, I64_MAX, false)).toBe(true); + }); + + test("i64_max + 1 is out of range", () => { + expect(isBigIntInRange(I64_MAX + 1n, I64_MIN, I64_MAX, false)).toBe(false); + }); + + test("i64_min - 1 is out of range", () => { + expect(isBigIntInRange(I64_MIN - 1n, I64_MIN, I64_MAX, false)).toBe(false); + }); + + test("very large positive bigint is out of range", () => { + expect(isBigIntInRange(2n ** 128n, I64_MIN, I64_MAX, false)).toBe(false); + }); + + test("very large negative bigint is out of range", () => { + expect(isBigIntInRange(-(2n ** 128n), I64_MIN, I64_MAX, false)).toBe(false); + }); + + test("narrow range [0, 100]", () => { + expect(isBigIntInRange(50n, 0n, 100n, false)).toBe(true); + expect(isBigIntInRange(0n, 0n, 100n, false)).toBe(true); + expect(isBigIntInRange(100n, 0n, 100n, false)).toBe(true); + expect(isBigIntInRange(-1n, 0n, 100n, false)).toBe(false); + expect(isBigIntInRange(101n, 0n, 100n, false)).toBe(false); + }); +}); + +describe("isBigIntInUInt64Range (unsigned)", () => { + test("100n is in [0, u64_max]", () => { + expect(isBigIntInRange(100n, 0n, U64_MAX, true)).toBe(true); + }); + + test("0n is in [0, u64_max]", () => { + expect(isBigIntInRange(0n, 0n, U64_MAX, true)).toBe(true); + }); + + test("u64_max boundary is in range", () => { + expect(isBigIntInRange(U64_MAX, 0n, U64_MAX, true)).toBe(true); + }); + + test("u64_max + 1 is out of range", () => { + expect(isBigIntInRange(U64_MAX + 1n, 0n, U64_MAX, true)).toBe(false); + }); + + test("-1n is out of [0, u64_max]", () => { + expect(isBigIntInRange(-1n, 0n, U64_MAX, true)).toBe(false); + }); + + test("very large bigint is out of range", () => { + expect(isBigIntInRange(2n ** 128n, 0n, U64_MAX, true)).toBe(false); + }); +}); diff --git a/test/js/sql/sql-mysql.test.ts b/test/js/sql/sql-mysql.test.ts index 0228dcf82d4d..42841e475b55 100644 --- a/test/js/sql/sql-mysql.test.ts +++ b/test/js/sql/sql-mysql.test.ts @@ -483,9 +483,7 @@ if (isDockerEnabled()) { test("Binary", async () => { const random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase(); await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a binary(1), b varbinary(1), c blob)`; - const values = [ - { a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) }, - ]; + const values = [{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) }]; await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`; const results = await sql`select * from ${sql(random_name)}`; // return buffers @@ -497,7 +495,7 @@ if (isDockerEnabled()) { expect(results2[0].a).toEqual(Buffer.from([1])); expect(results2[0].b).toEqual(Buffer.from([2])); expect(results2[0].c).toEqual(Buffer.from([3])); - }) + }); test("bulk insert nested sql()", async () => { await using sql = new SQL({ ...getOptions(), max: 1 }); @@ -966,6 +964,51 @@ if (isDockerEnabled()) { expect((await sql`select 9223372036854777 as x`)[0].x).toBe(9223372036854777n); }); + // Regression: previously any in-range BigInt parameter would throw + // ERR_OUT_OF_RANGE due to inverted logic in JSC__isBigIntInInt64Range. + describe("bigint parameter binding", () => { + test("binds BigInt values into BIGINT columns without ERR_OUT_OF_RANGE", async () => { + await using sql = new SQL({ ...getOptions(), bigint: true }); + const table = "t_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(table)} (s BIGINT, u BIGINT UNSIGNED)`; + + // These should NOT throw ERR_OUT_OF_RANGE (the bug was that they all did) + await sql`INSERT INTO ${sql(table)} (s) VALUES (${100n})`; + await sql`INSERT INTO ${sql(table)} (s) VALUES (${0n})`; + await sql`INSERT INTO ${sql(table)} (s) VALUES (${-1n})`; + await sql`INSERT INTO ${sql(table)} (s) VALUES (${9223372036854775807n})`; + await sql`INSERT INTO ${sql(table)} (s) VALUES (${-9223372036854775808n})`; + await sql`INSERT INTO ${sql(table)} (u) VALUES (${0n})`; + await sql`INSERT INTO ${sql(table)} (u) VALUES (${9223372036854775808n})`; + await sql`INSERT INTO ${sql(table)} (u) VALUES (${18446744073709551615n})`; + + // Verify i64 boundary values round-trip correctly as BigInt + const s = (await sql`SELECT s FROM ${sql(table)} WHERE s IS NOT NULL ORDER BY s`).map(r => r.s); + expect(s).toHaveLength(5); + expect(s[0]).toBe(-9223372036854775808n); + expect(s[s.length - 1]).toBe(9223372036854775807n); + + // Verify u64 boundary values round-trip correctly + const u = (await sql`SELECT u FROM ${sql(table)} WHERE u IS NOT NULL ORDER BY u`).map(r => r.u); + expect(u).toHaveLength(3); + expect(u[u.length - 1]).toBe(18446744073709551615n); + }); + + test("throws ERR_OUT_OF_RANGE for BigInt exceeding u64", async () => { + await using sql = new SQL({ ...getOptions() }); + await expect(sql`select ${18446744073709551616n} as x`).rejects.toMatchObject({ + code: "ERR_OUT_OF_RANGE", + }); + }); + + test("throws ERR_OUT_OF_RANGE for BigInt below i64 min", async () => { + await using sql = new SQL({ ...getOptions() }); + await expect(sql`select ${-9223372036854775809n} as x`).rejects.toMatchObject({ + code: "ERR_OUT_OF_RANGE", + }); + }); + }); + test("int is returned as Number", async () => { expect((await sql`select CAST(123 AS SIGNED) as x`)[0].x).toBe(123); });