diff --git a/benchmark/buffers/buffer-bytelength-string.js b/benchmark/buffers/buffer-bytelength-string.js index 557bc8d6fe38d6..65d92cb6f1cbba 100644 --- a/benchmark/buffers/buffer-bytelength-string.js +++ b/benchmark/buffers/buffer-bytelength-string.js @@ -4,7 +4,7 @@ const common = require('../common'); const bench = common.createBenchmark(main, { type: ['one_byte', 'two_bytes', 'three_bytes', 'four_bytes', 'latin1'], - encoding: ['utf8', 'base64'], + encoding: ['utf8', 'base64', 'latin1', 'hex'], repeat: [1, 2, 16, 256], // x16 n: [4e6], }); diff --git a/benchmark/buffers/buffer-copy-bytes-from.js b/benchmark/buffers/buffer-copy-bytes-from.js new file mode 100644 index 00000000000000..508092264f3a43 --- /dev/null +++ b/benchmark/buffers/buffer-copy-bytes-from.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + type: ['Uint8Array', 'Uint16Array', 'Uint32Array', 'Float64Array'], + len: [64, 256, 2048], + partial: ['none', 'offset', 'offset-length'], + n: [6e5], +}); + +function main({ n, len, type, partial }) { + const TypedArrayCtor = globalThis[type]; + const src = new TypedArrayCtor(len); + for (let i = 0; i < len; i++) src[i] = i; + + let offset; + let length; + if (partial === 'offset') { + offset = len >>> 2; + } else if (partial === 'offset-length') { + offset = len >>> 2; + length = len >>> 1; + } + + bench.start(); + for (let i = 0; i < n; i++) { + Buffer.copyBytesFrom(src, offset, length); + } + bench.end(n); +} diff --git a/benchmark/buffers/buffer-fill.js b/benchmark/buffers/buffer-fill.js index f1ae896843eee0..95e584e246af73 100644 --- a/benchmark/buffers/buffer-fill.js +++ b/benchmark/buffers/buffer-fill.js @@ -10,6 +10,7 @@ const bench = common.createBenchmark(main, { 'fill("t")', 'fill("test")', 'fill("t", "utf8")', + 'fill("t", "ascii")', 'fill("t", 0, "utf8")', 'fill("t", 0)', 'fill(Buffer.alloc(1), 0)', diff --git a/benchmark/buffers/buffer-indexof.js b/benchmark/buffers/buffer-indexof.js index 52cc95ccb9cf7e..ac5dffdd6d9103 100644 --- a/benchmark/buffers/buffer-indexof.js +++ b/benchmark/buffers/buffer-indexof.js @@ -19,7 +19,7 @@ const searchStrings = [ const bench = common.createBenchmark(main, { search: searchStrings, - encoding: ['undefined', 'utf8', 'ucs2'], + encoding: ['undefined', 'utf8', 'ascii', 'latin1', 'ucs2'], type: ['buffer', 'string'], n: [5e4], }, { diff --git a/benchmark/buffers/buffer-tostring.js b/benchmark/buffers/buffer-tostring.js index 0638dc996b39ac..64d2373d49980d 100644 --- a/benchmark/buffers/buffer-tostring.js +++ b/benchmark/buffers/buffer-tostring.js @@ -3,7 +3,7 @@ const common = require('../common.js'); const bench = common.createBenchmark(main, { - encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'UCS-2'], + encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'base64', 'base64url', 'UCS-2'], args: [0, 1, 3], len: [1, 64, 1024], n: [1e6], diff --git a/lib/buffer.js b/lib/buffer.js index 93cf26387cc762..be2bdd4334e93d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -50,7 +50,6 @@ const { TypedArrayPrototypeGetByteOffset, TypedArrayPrototypeGetLength, TypedArrayPrototypeSet, - TypedArrayPrototypeSlice, Uint8Array, } = primordials; @@ -382,6 +381,8 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) { return new FastBuffer(); } + const byteLength = TypedArrayPrototypeGetByteLength(view); + if (offset !== undefined || length !== undefined) { if (offset !== undefined) { validateInteger(offset, 'offset', 0); @@ -389,21 +390,33 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) { } else { offset = 0; } + let end; if (length !== undefined) { validateInteger(length, 'length', 0); - end = offset + length; + // The old code used TypedArrayPrototypeSlice which clamps internally. + end = MathMin(offset + length, viewLength); } else { end = viewLength; } - view = TypedArrayPrototypeSlice(view, offset, end); + if (end <= offset) return new FastBuffer(); + + const elementSize = byteLength / viewLength; + const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) + + offset * elementSize; + const srcByteLength = (end - offset) * elementSize; + + return fromArrayLike(new Uint8Array( + TypedArrayPrototypeGetBuffer(view), + srcByteOffset, + srcByteLength)); } return fromArrayLike(new Uint8Array( TypedArrayPrototypeGetBuffer(view), TypedArrayPrototypeGetByteOffset(view), - TypedArrayPrototypeGetByteLength(view))); + byteLength)); }; // Identical to the built-in %TypedArray%.of(), but avoids using the deprecated @@ -550,14 +563,15 @@ function fromArrayBuffer(obj, byteOffset, length) { } function fromArrayLike(obj) { - if (obj.length <= 0) + const len = obj.length; + if (len <= 0) return new FastBuffer(); - if (obj.length < (Buffer.poolSize >>> 1)) { - if (obj.length > (poolSize - poolOffset)) + if (len < (Buffer.poolSize >>> 1)) { + if (len > (poolSize - poolOffset)) createPool(); - const b = new FastBuffer(allocPool, poolOffset, obj.length); + const b = new FastBuffer(allocPool, poolOffset, len); TypedArrayPrototypeSet(b, obj, 0); - poolOffset += obj.length; + poolOffset += len; alignPool(); return b; } @@ -701,11 +715,7 @@ const encodingOps = { write: asciiWrite, slice: asciiSlice, indexOf: (buf, val, byteOffset, dir) => - indexOfBuffer(buf, - fromStringFast(val, encodingOps.ascii), - byteOffset, - encodingsMap.ascii, - dir), + indexOfString(buf, val, byteOffset, encodingsMap.ascii, dir), }, base64: { encoding: 'base64', @@ -1087,7 +1097,9 @@ function _fill(buf, value, offset, end, encoding) { value = 0; } else if (value.length === 1) { // Fast path: If `value` fits into a single byte, use that numeric value. - if (normalizedEncoding === 'utf8') { + // ASCII shares this branch with utf8 since code < 128 covers the full + // ASCII range; anything outside falls through to C++ bindingFill. + if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') { const code = StringPrototypeCharCodeAt(value, 0); if (code < 128) { value = code; @@ -1137,21 +1149,22 @@ function _fill(buf, value, offset, end, encoding) { } Buffer.prototype.write = function write(string, offset, length, encoding) { + const len = this.length; // Buffer#write(string); if (offset === undefined) { - return utf8Write(this, string, 0, this.length); + return utf8Write(this, string, 0, len); } // Buffer#write(string, encoding) if (length === undefined && typeof offset === 'string') { encoding = offset; - length = this.length; + length = len; offset = 0; // Buffer#write(string, offset[, length][, encoding]) } else { - validateOffset(offset, 'offset', 0, this.length); + validateOffset(offset, 'offset', 0, len); - const remaining = this.length - offset; + const remaining = len - offset; if (length === undefined) { length = remaining; @@ -1159,7 +1172,7 @@ Buffer.prototype.write = function write(string, offset, length, encoding) { encoding = length; length = remaining; } else { - validateOffset(length, 'length', 0, this.length); + validateOffset(length, 'length', 0, len); if (length > remaining) length = remaining; } @@ -1177,9 +1190,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) { }; Buffer.prototype.toJSON = function toJSON() { - if (this.length > 0) { - const data = new Array(this.length); - for (let i = 0; i < this.length; ++i) + const len = this.length; + if (len > 0) { + const data = new Array(len); + for (let i = 0; i < len; ++i) data[i] = this[i]; return { type: 'Buffer', data }; } @@ -1233,7 +1247,8 @@ Buffer.prototype.swap16 = function swap16() { swap(this, i, i + 1); return this; } - return _swap16(this); + _swap16(this); + return this; }; Buffer.prototype.swap32 = function swap32() { @@ -1250,7 +1265,8 @@ Buffer.prototype.swap32 = function swap32() { } return this; } - return _swap32(this); + _swap32(this); + return this; }; Buffer.prototype.swap64 = function swap64() { @@ -1269,7 +1285,8 @@ Buffer.prototype.swap64 = function swap64() { } return this; } - return _swap64(this); + _swap64(this); + return this; }; Buffer.prototype.toLocaleString = Buffer.prototype.toString; diff --git a/src/node_buffer.cc b/src/node_buffer.cc index d4a63cf610ca7f..6e06a29bb46ace 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1050,7 +1050,7 @@ void IndexOfString(const FunctionCallbackInfo& args) { needle_length, offset, is_forward); - } else if (enc == LATIN1) { + } else if (enc == ASCII || enc == LATIN1) { uint8_t* needle_data = node::UncheckedMalloc(needle_length); if (needle_data == nullptr) { return args.GetReturnValue().Set(-1); @@ -1200,31 +1200,56 @@ int32_t FastIndexOfNumber(Local, static CFunction fast_index_of_number(CFunction::Make(FastIndexOfNumber)); void Swap16(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); SPREAD_BUFFER_ARG(args[0], ts_obj); CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length)); - args.GetReturnValue().Set(args[0]); } +void FastSwap16(Local receiver, + Local buffer_obj, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("buffer.swap16"); + HandleScope scope(options.isolate); + SPREAD_BUFFER_ARG(buffer_obj, ts_obj); + CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length)); +} + +static CFunction fast_swap16(CFunction::Make(FastSwap16)); void Swap32(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); SPREAD_BUFFER_ARG(args[0], ts_obj); CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length)); - args.GetReturnValue().Set(args[0]); } +void FastSwap32(Local receiver, + Local buffer_obj, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("buffer.swap32"); + HandleScope scope(options.isolate); + SPREAD_BUFFER_ARG(buffer_obj, ts_obj); + CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length)); +} + +static CFunction fast_swap32(CFunction::Make(FastSwap32)); void Swap64(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]); SPREAD_BUFFER_ARG(args[0], ts_obj); CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length)); - args.GetReturnValue().Set(args[0]); } +void FastSwap64(Local receiver, + Local buffer_obj, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("buffer.swap64"); + HandleScope scope(options.isolate); + SPREAD_BUFFER_ARG(buffer_obj, ts_obj); + CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length)); +} + +static CFunction fast_swap64(CFunction::Make(FastSwap64)); + static void IsUtf8(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 1); @@ -1622,9 +1647,9 @@ void Initialize(Local target, SetMethodNoSideEffect( context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer); - SetMethod(context, target, "swap16", Swap16); - SetMethod(context, target, "swap32", Swap32); - SetMethod(context, target, "swap64", Swap64); + SetFastMethod(context, target, "swap16", Swap16, &fast_swap16); + SetFastMethod(context, target, "swap32", Swap32, &fast_swap32); + SetFastMethod(context, target, "swap64", Swap64, &fast_swap64); SetMethodNoSideEffect(context, target, "isUtf8", IsUtf8); SetMethodNoSideEffect(context, target, "isAscii", IsAscii); @@ -1693,8 +1718,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(IndexOfString); registry->Register(Swap16); + registry->Register(fast_swap16); registry->Register(Swap32); + registry->Register(fast_swap32); registry->Register(Swap64); + registry->Register(fast_swap64); registry->Register(IsUtf8); registry->Register(IsAscii); diff --git a/test/parallel/test-buffer-from.js b/test/parallel/test-buffer-from.js index 6c08b973e6ab41..b8f4a5d2a2a67b 100644 --- a/test/parallel/test-buffer-from.js +++ b/test/parallel/test-buffer-from.js @@ -140,5 +140,44 @@ assert.throws(() => { }) ); +// copyBytesFrom: length exceeds view (should clamp, not throw) +{ + const u8 = new Uint8Array([1, 2, 3, 4, 5]); + const b = Buffer.copyBytesFrom(u8, 2, 100); + assert.strictEqual(b.length, 3); + assert.deepStrictEqual([...b], [3, 4, 5]); +} + +// copyBytesFrom: length 0 returns empty buffer +{ + const u8 = new Uint8Array([1, 2, 3]); + const b = Buffer.copyBytesFrom(u8, 1, 0); + assert.strictEqual(b.length, 0); +} + +// copyBytesFrom: offset past end returns empty buffer +{ + const u8 = new Uint8Array([1, 2, 3]); + const b = Buffer.copyBytesFrom(u8, 10); + assert.strictEqual(b.length, 0); +} + +// copyBytesFrom: Float64Array with offset and length +{ + const f64 = new Float64Array([1.0, 2.0, 3.0, 4.0]); + const b = Buffer.copyBytesFrom(f64, 1, 2); + assert.strictEqual(b.length, 16); + const view = new Float64Array(b.buffer, b.byteOffset, 2); + assert.strictEqual(view[0], 2.0); + assert.strictEqual(view[1], 3.0); +} + +// copyBytesFrom: empty typed array returns empty buffer +{ + const empty = new Uint8Array(0); + const b = Buffer.copyBytesFrom(empty); + assert.strictEqual(b.length, 0); +} + // Invalid encoding is allowed Buffer.from('asd', 1); diff --git a/test/parallel/test-buffer-swap-fast.js b/test/parallel/test-buffer-swap-fast.js new file mode 100644 index 00000000000000..755edb6d598892 --- /dev/null +++ b/test/parallel/test-buffer-swap-fast.js @@ -0,0 +1,55 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +function testFastSwap16() { + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]); + const expected = Buffer.from([0x02, 0x01, 0x04, 0x03]); + const padded = Buffer.alloc(256); + buf.copy(padded); + padded.swap16(); + assert.deepStrictEqual(padded.subarray(0, 4), expected); +} + +function testFastSwap32() { + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]); + const expected = Buffer.from([0x04, 0x03, 0x02, 0x01]); + const padded = Buffer.alloc(256); + buf.copy(padded); + padded.swap32(); + assert.deepStrictEqual(padded.subarray(0, 4), expected); +} + +function testFastSwap64() { + const buf = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + const expected = Buffer.from([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]); + const padded = Buffer.alloc(256); + buf.copy(padded); + padded.swap64(); + assert.deepStrictEqual(padded.subarray(0, 8), expected); +} + +eval('%PrepareFunctionForOptimization(Buffer.prototype.swap16)'); +testFastSwap16(); +eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap16)'); +testFastSwap16(); + +eval('%PrepareFunctionForOptimization(Buffer.prototype.swap32)'); +testFastSwap32(); +eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap32)'); +testFastSwap32(); + +eval('%PrepareFunctionForOptimization(Buffer.prototype.swap64)'); +testFastSwap64(); +eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap64)'); +testFastSwap64(); + +if (common.isDebug) { + const { internalBinding } = require('internal/test/binding'); + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('buffer.swap16'), 1); + assert.strictEqual(getV8FastApiCallCount('buffer.swap32'), 1); + assert.strictEqual(getV8FastApiCallCount('buffer.swap64'), 1); +} diff --git a/test/parallel/test-buffer-tostring-range.js b/test/parallel/test-buffer-tostring-range.js index f4adf64c8d9129..00803bc35d7f4e 100644 --- a/test/parallel/test-buffer-tostring-range.js +++ b/test/parallel/test-buffer-tostring-range.js @@ -78,6 +78,46 @@ assert.strictEqual(rangeBuffer.toString('ascii', 0, '-1.99'), ''); assert.strictEqual(rangeBuffer.toString('ascii', 0, 1.99), 'a'); assert.strictEqual(rangeBuffer.toString('ascii', 0, true), 'a'); +// Test hex/base64/base64url partial range encoding (exercises V8 builtin path) +{ + const buf = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]); + assert.strictEqual(buf.toString('hex'), 'deadbeefcafe'); + assert.strictEqual(buf.toString('hex', 0, 3), 'deadbe'); + assert.strictEqual(buf.toString('hex', 2, 5), 'beefca'); + assert.strictEqual(buf.toString('hex', 4), 'cafe'); + assert.strictEqual(buf.toString('hex', 6), ''); + assert.strictEqual(buf.toString('hex', 0, 0), ''); +} + +{ + const buf = Buffer.from('Hello, World!'); + assert.strictEqual(buf.toString('base64'), 'SGVsbG8sIFdvcmxkIQ=='); + assert.strictEqual(buf.toString('base64', 0, 5), 'SGVsbG8='); + assert.strictEqual(buf.toString('base64', 7), 'V29ybGQh'); + assert.strictEqual(buf.toString('base64', 0, 0), ''); +} + +{ + const buf = Buffer.from('Hello, World!'); + assert.strictEqual(buf.toString('base64url'), 'SGVsbG8sIFdvcmxkIQ'); + assert.strictEqual(buf.toString('base64url', 0, 5), 'SGVsbG8'); + assert.strictEqual(buf.toString('base64url', 7), 'V29ybGQh'); + assert.strictEqual(buf.toString('base64url', 0, 0), ''); +} + +// Test with pool-allocated buffer (has non-zero byteOffset) +{ + const poolBuf = Buffer.from('test data for hex encoding'); + assert.strictEqual( + poolBuf.toString('hex'), + Buffer.from('test data for hex encoding').toString('hex') + ); + assert.strictEqual( + poolBuf.toString('hex', 5, 9), + Buffer.from('data').toString('hex') + ); +} + // Try toString() with an object as an encoding assert.strictEqual(rangeBuffer.toString({ toString: function() { return 'ascii';