From 1ebd95c7ca6af1d90a88f7d1ee088bba824048e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Wed, 18 Mar 2026 14:10:21 +0100 Subject: [PATCH 1/3] Refactor Vint encoding Update Vint encoding to use BigInt instead of Long. --- lib/types/duration.js | 11 ++++----- lib/utils.js | 57 ++++++++++++++++++------------------------- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/lib/types/duration.js b/lib/types/duration.js index 9871ce5bc..589902af2 100644 --- a/lib/types/duration.js +++ b/lib/types/duration.js @@ -156,17 +156,16 @@ class Duration { * @returns {Buffer} */ toBuffer() { - let nanoseconds = bigintToLong(this.#nanoseconds); const lengthMonths = utils.VIntCoding.writeVInt( - Long.fromNumber(this.#months), + BigInt(this.#months), reusableBuffers.months, ); const lengthDays = utils.VIntCoding.writeVInt( - Long.fromNumber(this.#days), + BigInt(this.#days), reusableBuffers.days, ); const lengthNanoseconds = utils.VIntCoding.writeVInt( - nanoseconds, + this.#nanoseconds, reusableBuffers.nanoseconds, ); const buffer = utils.allocBufferUnsafe( @@ -231,8 +230,8 @@ class Duration { */ static fromBuffer(buffer) { const offset = { value: 0 }; - const months = utils.VIntCoding.readVInt(buffer, offset).toNumber(); - const days = utils.VIntCoding.readVInt(buffer, offset).toNumber(); + const months = Number(utils.VIntCoding.readVInt(buffer, offset)); + const days = Number(utils.VIntCoding.readVInt(buffer, offset)); const nanoseconds = utils.VIntCoding.readVInt(buffer, offset); return new Duration(months, days, nanoseconds); } diff --git a/lib/utils.js b/lib/utils.js index ec78f0003..a813435fc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,7 +1,6 @@ "use strict"; const util = require("util"); -const Long = require("long"); const net = require("net"); const { EventEmitter } = require("events"); @@ -1044,20 +1043,20 @@ function whilst(condition, fn, callback) { * Exposes only 2 internal methods, the rest are hidden. */ const VIntCoding = (function () { - /** @param {Long} n */ + /** @param {bigint} n */ function encodeZigZag64(n) { // (n << 1) ^ (n >> 63); - return n.toUnsigned().shiftLeft(1).xor(n.shiftRight(63)); + return BigInt.asUintN(64, (n << 1n) ^ (n >> 63n)); } - /** @param {Long} n */ + /** @param {bigint} n */ function decodeZigZag64(n) { // (n >>> 1) ^ -(n & 1); - return n.shiftRightUnsigned(1).xor(n.and(Long.ONE).negate()); + return BigInt.asIntN(64, (n >> 1n) ^ -(n & 1n)); } /** - * @param {Long} value + * @param {bigint} value * @param {Buffer} buffer * @returns {Number} */ @@ -1066,14 +1065,14 @@ const VIntCoding = (function () { } /** - * @param {Long} value + * @param {bigint} value * @param {Buffer} buffer * @returns {number} */ function writeUnsignedVInt(value, buffer) { const size = computeUnsignedVIntSize(value); if (size === 1) { - buffer[0] = value.getLowBits(); + buffer[0] = Number(value); return 1; } encodeVInt(value, size, buffer); @@ -1081,49 +1080,42 @@ const VIntCoding = (function () { } /** - * @param {Long} value + * @param {bigint} value * @returns {number} */ function computeUnsignedVIntSize(value) { - const magnitude = numberOfLeadingZeros(value.or(Long.ONE)); + const magnitude = numberOfLeadingZeros(value | 1n); return (639 - magnitude * 9) >> 6; } /** - * @param {Long} value + * @param {bigint} value * @param {Number} size * @param {Buffer} buffer */ function encodeVInt(value, size, buffer) { const extraBytes = size - 1; - let intValue = value.getLowBits(); - let i; - let intBytes = 4; - for (i = extraBytes; i >= 0 && intBytes-- > 0; i--) { - buffer[i] = 0xff & intValue; - intValue >>= 8; - } - intValue = value.getHighBits(); - for (; i >= 0; i--) { - buffer[i] = 0xff & intValue; - intValue >>= 8; + let v = value; + for (let i = extraBytes; i >= 0; i--) { + buffer[i] = Number(v & 0xffn); + v >>= 8n; } buffer[0] |= encodeExtraBytesToRead(extraBytes); } /** * Returns the number of zero bits preceding the highest-order one-bit in the binary representation of the value. - * @param {Long} value + * @param {bigint} value * @returns {Number} */ function numberOfLeadingZeros(value) { - if (value.equals(Long.ZERO)) { + if (value === 0n) { return 64; } let n = 1; - let x = value.getHighBits(); + let x = Number(value >> 32n); if (x === 0) { n += 32; - x = value.getLowBits(); + x = Number(value & 0xffffffffn); } if (x >>> 16 === 0) { n += 16; @@ -1152,30 +1144,29 @@ const VIntCoding = (function () { /** * @param {Buffer} buffer * @param {{value: number}} offset - * @returns {Long} + * @returns {bigint} */ function readVInt(buffer, offset) { return decodeZigZag64(readUnsignedVInt(buffer, offset)); } /** - * uvint_unpack * @param {Buffer} input * @param {{ value: number}} offset - * @returns {Long} + * @returns {bigint} */ function readUnsignedVInt(input, offset) { const firstByte = input[offset.value++]; if ((firstByte & 0x80) === 0) { - return Long.fromInt(firstByte); + return BigInt(firstByte); } const sByteInt = fromSignedByteToInt(firstByte); const size = numberOfExtraBytesToRead(sByteInt); - let result = Long.fromInt(sByteInt & firstByteValueMask(size)); + let result = BigInt(sByteInt & firstByteValueMask(size)); for (let ii = 0; ii < size; ii++) { - const b = Long.fromInt(input[offset.value++]); + const b = BigInt(input[offset.value++]); // (result << 8) | b - result = result.shiftLeft(8).or(b); + result = (result << 8n) | b; } return result; } From b3d42fe77f4e4220240e511c921b89f451be298c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Wed, 18 Mar 2026 14:31:31 +0100 Subject: [PATCH 2/3] Refactor duration.js Replace internal uses of Long to BigInt. This does not change the API of the class. Replace util.format() to "`${}`" syntax. --- lib/types/duration.js | 144 +++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 94 deletions(-) diff --git a/lib/types/duration.js b/lib/types/duration.js index 589902af2..8dd55dae2 100644 --- a/lib/types/duration.js +++ b/lib/types/duration.js @@ -1,6 +1,5 @@ "use strict"; const Long = require("long"); -const util = require("util"); const utils = require("../utils"); const { bigintToLong, @@ -18,12 +17,12 @@ const reusableBuffers = { }; const maxInt32 = 0x7fffffff; -const longOneThousand = Long.fromInt(1000); -const nanosPerMicro = longOneThousand; -const nanosPerMilli = longOneThousand.multiply(nanosPerMicro); -const nanosPerSecond = longOneThousand.multiply(nanosPerMilli); -const nanosPerMinute = Long.fromInt(60).multiply(nanosPerSecond); -const nanosPerHour = Long.fromInt(60).multiply(nanosPerMinute); +const maxInt64 = 0x7fffffffffffffffn; +const nanosPerMicro = 1000n; +const nanosPerMilli = 1000n * nanosPerMicro; +const nanosPerSecond = 1000n * nanosPerMilli; +const nanosPerMinute = 60n * nanosPerSecond; +const nanosPerHour = 60n * nanosPerMinute; const daysPerWeek = 7; const monthsPerYear = 12; const standardRegex = /(\d+)(y|mo|w|d|h|s|ms|us|µs|ns|m)/gi; @@ -97,7 +96,7 @@ class Duration { return ( this.months === other.months && this.days === other.days && - this.nanoseconds.compare(other.nanoseconds) === 0 + this.#nanoseconds === other.#nanoseconds ); } @@ -184,7 +183,6 @@ class Duration { * @return {string} */ toString() { - let nanoseconds = bigintToLong(this.#nanoseconds); let value = ""; function append(dividend, divisor, unit) { if (dividend === 0 || dividend < divisor) { @@ -195,30 +193,29 @@ class Duration { return dividend % divisor; } function append64(dividend, divisor, unit) { - if (dividend.equals(Long.ZERO) || dividend.lessThan(divisor)) { + if (dividend === 0n || dividend < divisor) { return dividend; } // string concatenation is supposed to be faster than join() - value += dividend.divide(divisor).toString() + unit; - return dividend.modulo(divisor); + value += (dividend / divisor).toString() + unit; + return dividend % divisor; } - if (this.#months < 0 || this.#days < 0 || nanoseconds.isNegative()) { + if (this.#months < 0 || this.#days < 0 || this.#nanoseconds < 0) { value = "-"; } let remainder = append(Math.abs(this.#months), monthsPerYear, "y"); append(remainder, 1, "mo"); append(Math.abs(this.#days), 1, "d"); - if (!nanoseconds.equals(Long.ZERO)) { - const nanos = nanoseconds.isNegative() - ? nanoseconds.negate() - : nanoseconds; + if (this.#nanoseconds !== 0n) { + const nanos = + this.#nanoseconds < 0n ? -this.#nanoseconds : this.#nanoseconds; remainder = append64(nanos, nanosPerHour, "h"); remainder = append64(remainder, nanosPerMinute, "m"); remainder = append64(remainder, nanosPerSecond, "s"); remainder = append64(remainder, nanosPerMilli, "ms"); remainder = append64(remainder, nanosPerMicro, "us"); - append64(remainder, Long.ONE, "ns"); + append64(remainder, 1n, "ns"); } return value; } @@ -303,9 +300,7 @@ function parseStandardFormat(isNegative, source) { function parseIso8601Format(isNegative, source) { const matches = iso8601Regex.exec(source); if (!matches || matches[0] !== source) { - throw new TypeError( - util.format("Unable to convert '%s' to a duration", source), - ); + throw new TypeError(`Unable to convert '${source}' to a duration`); } const builder = new Builder(isNegative); if (matches[1]) { @@ -340,9 +335,7 @@ function parseIso8601Format(isNegative, source) { function parseIso8601WeekFormat(isNegative, source) { const matches = iso8601WeekRegex.exec(source); if (!matches || matches[0] !== source) { - throw new TypeError( - util.format("Unable to convert '%s' to a duration", source), - ); + throw new TypeError(`Unable to convert '${source}' to a duration`); } return new Builder(isNegative).addWeeks(matches[1]).build(); } @@ -356,9 +349,7 @@ function parseIso8601WeekFormat(isNegative, source) { function parseIso8601AlternativeFormat(isNegative, source) { const matches = iso8601AlternateRegex.exec(source); if (!matches || matches[0] !== source) { - throw new TypeError( - util.format("Unable to convert '%s' to a duration", source), - ); + throw new TypeError(`Unable to convert '${source}' to a duration`); } return new Builder(isNegative) .addYears(matches[1]) @@ -381,7 +372,7 @@ class Builder { this._unitIndex = 0; this._months = 0; this._days = 0; - this._nanoseconds = Long.ZERO; + this._nanoseconds = 0n; this._addMethods = { y: this.addYears, mo: this.addMonths, @@ -413,20 +404,13 @@ class Builder { #validateOrder(unitIndex) { if (unitIndex === this._unitIndex) { throw new TypeError( - util.format( - "Invalid duration. The %s are specified multiple times", - this.#getUnitName(unitIndex), - ), + `Invalid duration. The ${this.#getUnitName(unitIndex)} are specified multiple times`, ); } if (unitIndex <= this._unitIndex) { throw new TypeError( - util.format( - "Invalid duration. The %s should be after %s", - this.#getUnitName(this._unitIndex), - this.#getUnitName(unitIndex), - ), + `Invalid duration. The ${this.#getUnitName(this._unitIndex)} should be after ${this.#getUnitName(unitIndex)}`, ); } this._unitIndex = unitIndex; @@ -450,13 +434,13 @@ class Builder { this.#validate32(units, (maxInt32 - this._days) / daysPerUnit, "days"); } /** - * @param {Long} units - * @param {Long} nanosPerUnit + * @param {bigint} units + * @param {bigint} nanosPerUnit */ #validateNanos(units, nanosPerUnit) { this.#validate64( units, - Long.MAX_VALUE.subtract(this._nanoseconds).divide(nanosPerUnit), + (maxInt64 - this._nanoseconds) / nanosPerUnit, "nanoseconds", ); } @@ -468,27 +452,19 @@ class Builder { #validate32(units, limit, unitName) { if (units > limit) { throw new TypeError( - util.format( - "Invalid duration. The total number of %s must be less or equal to %s", - unitName, - maxInt32, - ), + `Invalid duration. The total number of ${unitName} must be less or equal to ${maxInt32}`, ); } } /** - * @param {Long} units - * @param {Long} limit + * @param {bigint} units + * @param {bigint} limit * @param {String} unitName */ #validate64(units, limit, unitName) { - if (units.greaterThan(limit)) { + if (units > limit) { throw new TypeError( - util.format( - "Invalid duration. The total number of %s must be less or equal to %s", - unitName, - Long.MAX_VALUE.toString(), - ), + `Invalid duration. The total number of ${unitName} must be less or equal to ${maxInt64}`, ); } } @@ -502,9 +478,7 @@ class Builder { add(textValue, symbol) { const addMethod = this._addMethods[symbol.toLowerCase()]; if (!addMethod) { - throw new TypeError( - util.format("Unknown duration symbol '%s'", symbol), - ); + throw new TypeError(`Unknown duration symbol '${symbol}'`); } return addMethod.call(this, textValue); } @@ -553,93 +527,75 @@ class Builder { return this; } /** - * @param {String|Long} hours + * @param {String|Number|BigInt} hours * @return {Builder} */ addHours(hours) { - const value = - typeof hours === "string" ? Long.fromString(hours) : hours; + const value = BigInt(hours); this.#validateOrder(5); this.#validateNanos(value, nanosPerHour); - this._nanoseconds = this._nanoseconds.add(value.multiply(nanosPerHour)); + this._nanoseconds += value * nanosPerHour; return this; } /** - * @param {String|Long} minutes + * @param {String|Number|BigInt} minutes * @return {Builder} */ addMinutes(minutes) { - const value = - typeof minutes === "string" ? Long.fromString(minutes) : minutes; + const value = BigInt(minutes); this.#validateOrder(6); this.#validateNanos(value, nanosPerMinute); - this._nanoseconds = this._nanoseconds.add( - value.multiply(nanosPerMinute), - ); + this._nanoseconds += value * nanosPerMinute; return this; } /** - * @param {String|Long} seconds + * @param {String|Number|BigInt} seconds * @return {Builder} */ addSeconds(seconds) { - const value = - typeof seconds === "string" ? Long.fromString(seconds) : seconds; + const value = BigInt(seconds); this.#validateOrder(7); this.#validateNanos(value, nanosPerSecond); - this._nanoseconds = this._nanoseconds.add( - value.multiply(nanosPerSecond), - ); + this._nanoseconds += value * nanosPerSecond; return this; } /** - * @param {String|Long} millis + * @param {String|Number|BigInt} millis * @return {Builder} */ addMillis(millis) { - const value = - typeof millis === "string" ? Long.fromString(millis) : millis; + const value = BigInt(millis); this.#validateOrder(8); this.#validateNanos(value, nanosPerMilli); - this._nanoseconds = this._nanoseconds.add( - value.multiply(nanosPerMilli), - ); + this._nanoseconds += value * nanosPerMilli; return this; } /** - * @param {String|Long} micros + * @param {String|Number|BigInt} micros * @return {Builder} */ addMicros(micros) { - const value = - typeof micros === "string" ? Long.fromString(micros) : micros; + const value = BigInt(micros); this.#validateOrder(9); this.#validateNanos(value, nanosPerMicro); - this._nanoseconds = this._nanoseconds.add( - value.multiply(nanosPerMicro), - ); + this._nanoseconds += value * nanosPerMicro; return this; } /** - * @param {String|Long} nanos + * @param {String|Number|BigInt} nanos * @return {Builder} */ addNanos(nanos) { - const value = - typeof nanos === "string" ? Long.fromString(nanos) : nanos; + const value = BigInt(nanos); this.#validateOrder(10); - this.#validateNanos(value, Long.ONE); - this._nanoseconds = this._nanoseconds.add(value); + this.#validateNanos(value, 1n); + this._nanoseconds += value; return this; } /** @return {Duration} */ build() { return this._isNegative - ? new Duration( - -this._months, - -this._days, - this._nanoseconds.negate(), - ) + ? new Duration(-this._months, -this._days, -this._nanoseconds) : new Duration(this._months, this._days, this._nanoseconds); } } From 6b324768df5b2afe94f84d83cef1cd3aa9a30ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Wed, 18 Mar 2026 15:31:36 +0100 Subject: [PATCH 3/3] Fix bug in duration parsing In the DSx code, when addNanos received ABS(MIN_INT64) [ex. when parsing MIN_INT64 ns duration], it pared it into 64 bit type, which quietly converted it into MIN_INT64, rather than the expected ABS(MIN_INT64). This then allowed it to pass the check in `validateNanos`, as we were comparing negative value instead of the expected positive value. With the replacement of Long in favour of BigInt, we now correctly parse ABS(MIN_INT64) when creating a BigInt. This leads to incorrectly trigger the validate64 error check when parsing MIN_INT64 ns duration. This commit adds a check that take into account the difference between ABS(MIN_INT64) and MAX_INT64, by accepting ABS(MIN_INT64) only for negative durations. --- lib/types/duration.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/types/duration.js b/lib/types/duration.js index 8dd55dae2..e6f549187 100644 --- a/lib/types/duration.js +++ b/lib/types/duration.js @@ -189,7 +189,7 @@ class Duration { return dividend; } // string concatenation is supposed to be faster than join() - value += (dividend / divisor).toFixed(0) + unit; + value += Math.floor(dividend / divisor) + unit; return dividend % divisor; } function append64(dividend, divisor, unit) { @@ -420,9 +420,10 @@ class Builder { * @param {Number} monthsPerUnit */ #validateMonths(units, monthsPerUnit) { + const maxMonths = maxInt32 + (this._isNegative ? 1 : 0); this.#validate32( units, - (maxInt32 - this._months) / monthsPerUnit, + (maxMonths - this._months) / monthsPerUnit, "months", ); } @@ -431,16 +432,18 @@ class Builder { * @param {Number} daysPerUnit */ #validateDays(units, daysPerUnit) { - this.#validate32(units, (maxInt32 - this._days) / daysPerUnit, "days"); + const maxDays = maxInt32 + (this._isNegative ? 1 : 0); + this.#validate32(units, (maxDays - this._days) / daysPerUnit, "days"); } /** * @param {bigint} units * @param {bigint} nanosPerUnit */ #validateNanos(units, nanosPerUnit) { + const maxNanos = maxInt64 + (this._isNegative ? 1n : 0n); this.#validate64( units, - (maxInt64 - this._nanoseconds) / nanosPerUnit, + (maxNanos - this._nanoseconds) / nanosPerUnit, "nanoseconds", ); } @@ -452,7 +455,7 @@ class Builder { #validate32(units, limit, unitName) { if (units > limit) { throw new TypeError( - `Invalid duration. The total number of ${unitName} must be less or equal to ${maxInt32}`, + `Invalid duration. The total number of ${unitName} must fit in a 32 bit signed integer.`, ); } } @@ -464,7 +467,7 @@ class Builder { #validate64(units, limit, unitName) { if (units > limit) { throw new TypeError( - `Invalid duration. The total number of ${unitName} must be less or equal to ${maxInt64}`, + `Invalid duration. The total number of ${unitName} must fit in a 64 bit signed integer.`, ); } }