From 555debaaab3f5f6c676c6682ccdcc6376f995e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 24 Dec 2025 15:01:05 -0300 Subject: [PATCH 01/11] buffer: create copy static method Fixes: https://github.com/nodejs/node/issues/53700 --- lib/buffer.js | 95 +++-- src/node_buffer.cc | 87 ++++ test/parallel/test-buffer-copy-within.js | 509 +++++++++++++++++++++++ 3 files changed, 653 insertions(+), 38 deletions(-) create mode 100644 test/parallel/test-buffer-copy-within.js diff --git a/lib/buffer.js b/lib/buffer.js index dc189712fda29f..d17a66e974818c 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -23,7 +23,6 @@ const { Array, - ArrayBufferIsView, ArrayIsArray, ArrayPrototypeForEach, MathFloor, @@ -65,6 +64,7 @@ const { indexOfBuffer, indexOfNumber, indexOfString, + staticCopy, swap16: _swap16, swap32: _swap32, swap64: _swap64, @@ -203,42 +203,6 @@ function toInteger(n, defaultVal) { return defaultVal; } -function copyImpl(source, target, targetStart, sourceStart, sourceEnd) { - if (!ArrayBufferIsView(source)) - throw new ERR_INVALID_ARG_TYPE('source', ['Buffer', 'Uint8Array'], source); - if (!ArrayBufferIsView(target)) - throw new ERR_INVALID_ARG_TYPE('target', ['Buffer', 'Uint8Array'], target); - - if (targetStart === undefined) { - targetStart = 0; - } else { - targetStart = NumberIsInteger(targetStart) ? targetStart : toInteger(targetStart, 0); - if (targetStart < 0) - throw new ERR_OUT_OF_RANGE('targetStart', '>= 0', targetStart); - } - - if (sourceStart === undefined) { - sourceStart = 0; - } else { - sourceStart = NumberIsInteger(sourceStart) ? sourceStart : toInteger(sourceStart, 0); - if (sourceStart < 0 || sourceStart > source.byteLength) - throw new ERR_OUT_OF_RANGE('sourceStart', `>= 0 && <= ${source.byteLength}`, sourceStart); - } - - if (sourceEnd === undefined) { - sourceEnd = source.byteLength; - } else { - sourceEnd = NumberIsInteger(sourceEnd) ? sourceEnd : toInteger(sourceEnd, 0); - if (sourceEnd < 0) - throw new ERR_OUT_OF_RANGE('sourceEnd', '>= 0', sourceEnd); - } - - if (targetStart >= target.byteLength || sourceStart >= sourceEnd) - return 0; - - return _copyActual(source, target, targetStart, sourceStart, sourceEnd); -} - function _copyActual(source, target, targetStart, sourceStart, sourceEnd, isUint8Copy = false) { if (sourceEnd - sourceStart > target.byteLength - targetStart) sourceEnd = sourceStart + target.byteLength - targetStart; @@ -618,6 +582,61 @@ Buffer.concat = function concat(list, length) { return buffer; }; +Buffer.copy = function copy(source, target, targetStart, sourceStart, sourceEnd) { + if (!isAnyArrayBuffer(source) && !isArrayBufferView(source)) { + throw new ERR_INVALID_ARG_TYPE('source', ['ArrayBuffer', 'SharedArrayBuffer', 'TypedArray'], source); + } + + if (!isAnyArrayBuffer(target) && !isArrayBufferView(target)) { + throw new ERR_INVALID_ARG_TYPE('target', ['Buffer', 'ArrayBuffer', 'SharedArrayBuffer', 'TypedArray'], target); + } + + if (targetStart === undefined) { + targetStart = 0; + } else { + targetStart = NumberIsInteger(targetStart) ? targetStart : toInteger(targetStart, 0); + if (targetStart < 0) { + throw new ERR_OUT_OF_RANGE('targetStart', '>= 0', targetStart); + } + } + + const sourceByteLengthValue = source.byteLength; + + if (sourceStart === undefined) { + sourceStart = 0; + } else { + sourceStart = NumberIsInteger(sourceStart) ? sourceStart : toInteger(sourceStart, 0); + if (sourceStart < 0 || sourceStart > sourceByteLengthValue) { + throw new ERR_OUT_OF_RANGE('sourceStart', `>= 0 && <= ${sourceByteLengthValue}`, sourceStart); + } + } + + if (sourceEnd === undefined) { + sourceEnd = sourceByteLengthValue; + } else { + sourceEnd = NumberIsInteger(sourceEnd) ? sourceEnd : toInteger(sourceEnd, 0); + if (sourceEnd < 0) { + throw new ERR_OUT_OF_RANGE('sourceEnd', '>= 0', sourceEnd); + } + + if (sourceEnd > sourceByteLengthValue) { + sourceEnd = sourceByteLengthValue; + } + } + + if (sourceStart >= sourceEnd) { + return 0; + } + + const targetByteLengthValue = target.byteLength; + + if (targetStart >= targetByteLengthValue) { + return 0; + } + + return staticCopy(source, target, targetStart, sourceStart, sourceEnd); +}; + function base64ByteLength(str, bytes) { // Handle padding if (StringPrototypeCharCodeAt(str, bytes - 1) === 0x3D) @@ -827,7 +846,7 @@ ObjectDefineProperty(Buffer.prototype, 'offset', { Buffer.prototype.copy = function copy(target, targetStart, sourceStart, sourceEnd) { - return copyImpl(this, target, targetStart, sourceStart, sourceEnd); + return Buffer.copy(this, target, targetStart, sourceStart, sourceEnd); }; // No need to verify that "buf.length <= MAX_UINT32" since it's a read-only diff --git a/src/node_buffer.cc b/src/node_buffer.cc index cc418017f76803..e6be9c325ff557 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1449,6 +1449,91 @@ void CopyArrayBuffer(const FunctionCallbackInfo& args) { memcpy(dest, src, bytes_to_copy); } +std::pair DecomposeSourceToParts(Local source) { + void* pointer; + size_t byte_length; + + if (source->IsArrayBufferView()) { + Local view = source.As(); + Local buffer = view->Buffer(); + + // Handle potential detached buffer case + if (buffer.IsEmpty() || buffer->Data() == nullptr) { + pointer = nullptr; + byte_length = 0; + } else { + pointer = static_cast(buffer->Data()) + view->ByteOffset(); + byte_length = view->ByteLength(); + } + } else if (source->IsArrayBuffer()) { + Local ab = source.As(); + pointer = ab->Data(); + byte_length = ab->ByteLength(); + } else if (source->IsSharedArrayBuffer()) { + Local ab = source.As(); + pointer = ab->Data(); + byte_length = ab->ByteLength(); + } else { + UNREACHABLE(); // Caller must validate. + } + + return {pointer, byte_length}; +} + +void StaticCopy(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Local source = args[0]; + Local target = args[1]; + + void* source_data; + size_t source_byte_length; + std::tie(source_data, source_byte_length) = DecomposeSourceToParts(source); + + void* target_data; + size_t target_byte_length; + std::tie(target_data, target_byte_length) = DecomposeSourceToParts(target); + + size_t target_start = static_cast(args[2].As()->Value()); + size_t source_start = static_cast(args[3].As()->Value()); + size_t source_end = static_cast(args[4].As()->Value()); + + if (source_data == nullptr || target_data == nullptr) { + args.GetReturnValue().Set(0); + return; + } + + if (target_start >= target_byte_length) { + return THROW_ERR_OUT_OF_RANGE( + env, "targetStart is out of bounds"); + } + + if (source_start > source_byte_length || source_end > source_byte_length) { + return THROW_ERR_OUT_OF_RANGE( + env, "sourceStart or sourceEnd is out of bounds"); + } + + if (source_start >= source_end) { + args.GetReturnValue().Set(0); + return; + } + + size_t bytes_to_copy = source_end - source_start; + size_t target_remaining = target_byte_length - target_start; + + if (bytes_to_copy > target_remaining) { + bytes_to_copy = target_remaining; + } + + if (bytes_to_copy > 0) { + uint8_t* dest = static_cast(target_data) + target_start; + uint8_t* src = static_cast(source_data) + source_start; + memmove(dest, src, bytes_to_copy); + } + + args.GetReturnValue().Set(static_cast(bytes_to_copy)); +} + template uint32_t WriteOneByteString(const char* src, uint32_t src_len, @@ -1576,6 +1661,7 @@ void Initialize(Local target, SetMethodNoSideEffect(context, target, "indexOfString", IndexOfString); SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer); + SetMethod(context, target, "staticCopy", StaticCopy); SetMethod(context, target, "swap16", Swap16); SetMethod(context, target, "swap32", Swap32); @@ -1646,6 +1732,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(SlowIndexOfNumber); registry->Register(fast_index_of_number); registry->Register(IndexOfString); + registry->Register(StaticCopy); registry->Register(Swap16); registry->Register(Swap32); diff --git a/test/parallel/test-buffer-copy-within.js b/test/parallel/test-buffer-copy-within.js new file mode 100644 index 00000000000000..2a3faa6e3777d8 --- /dev/null +++ b/test/parallel/test-buffer-copy-within.js @@ -0,0 +1,509 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { describe, test } = require('node:test'); + +describe('Buffer.copy', () => { + describe('Buffer to Buffer', () => { + test('should copy entire source buffer to target buffer', () => { + const src = Buffer.from([1, 2, 3, 4]); + const dst = Buffer.alloc(4); + + Buffer.copy(src, dst, 0); + + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3, 4])); + }); + + test('should copy source buffer to target buffer at offset', () => { + const src = Buffer.from([1, 2, 3]); + const dst = Buffer.from([0, 0, 0, 0, 0]); + + Buffer.copy(src, dst, 2); + + assert.deepStrictEqual(dst, Buffer.from([0, 0, 1, 2, 3])); + }); + + test('should return number of bytes copied', () => { + const src = Buffer.from([1, 2, 3, 4]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0); + + assert.strictEqual(bytesCopied, 4); + }); + }); + + describe('ArrayBuffer to Buffer', () => { + test('should copy ArrayBuffer to Buffer', () => { + const src = new Uint8Array([5, 6, 7, 8]).buffer; + const dst = Buffer.alloc(4); + + Buffer.copy(src, dst, 0); + + assert.deepStrictEqual(dst, Buffer.from([5, 6, 7, 8])); + }); + + test('should copy ArrayBuffer to Buffer at offset', () => { + const src = new Uint8Array([5, 6]).buffer; + const dst = Buffer.from([0, 0, 0, 0]); + + Buffer.copy(src, dst, 1); + + assert.deepStrictEqual(dst, Buffer.from([0, 5, 6, 0])); + }); + + test('should handle overlapping memory in the same ArrayBuffer', () => { + const ab = new Uint8Array([1, 2, 3, 4, 5, 0, 0]).buffer; + const src = new Uint8Array(ab, 0, 5); + const dst = new Uint8Array(ab, 2, 5); + + Buffer.copy(src, dst, 0); + assert.deepStrictEqual(new Uint8Array(ab), new Uint8Array([1, 2, 1, 2, 3, 4, 5])); + }); + }); + + describe('DataView to Buffer', () => { + test('should copy DataView to Buffer', () => { + const ab = new Uint8Array([9, 10, 11]).buffer; + const src = new DataView(ab); + const dst = Buffer.alloc(3); + + Buffer.copy(src, dst, 0); + + assert.deepStrictEqual(dst, Buffer.from([9, 10, 11])); + }); + + test('should copy DataView to Buffer at offset', () => { + const ab = new Uint8Array([9, 10]).buffer; + const src = new DataView(ab); + const dst = Buffer.from([0, 0, 0, 0]); + + Buffer.copy(src, dst, 2); + + assert.deepStrictEqual(dst, Buffer.from([0, 0, 9, 10])); + }); + }); + + describe('Buffer to ArrayBuffer', () => { + test('should copy Buffer to ArrayBuffer', () => { + const src = Buffer.from([1, 2, 3]); + const dst = new Uint8Array(3).buffer; + + Buffer.copy(src, dst, 0); + + assert.deepStrictEqual(new Uint8Array(dst), new Uint8Array([1, 2, 3])); + }); + + test('should copy Buffer to ArrayBuffer at offset', () => { + const src = Buffer.from([1, 2]); + const dst = new Uint8Array([0, 0, 0, 0]).buffer; + + Buffer.copy(src, dst, 1); + + assert.deepStrictEqual(new Uint8Array(dst), new Uint8Array([0, 1, 2, 0])); + }); + }); + + describe('Buffer to DataView', () => { + test('should copy Buffer to DataView', () => { + const src = Buffer.from([1, 2, 3]); + const ab = new Uint8Array(3).buffer; + const dst = new DataView(ab); + + Buffer.copy(src, dst, 0); + + assert.deepStrictEqual(new Uint8Array(ab), new Uint8Array([1, 2, 3])); + }); + + test('should copy Buffer to DataView at offset', () => { + const src = Buffer.from([1, 2]); + const ab = new Uint8Array([0, 0, 0, 0]).buffer; + const dst = new DataView(ab); + + Buffer.copy(src, dst, 2); + + assert.deepStrictEqual(new Uint8Array(ab), new Uint8Array([0, 0, 1, 2])); + }); + }); + + describe('Partial copies', () => { + test('should copy only available space in target', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(3); + + const bytesCopied = Buffer.copy(src, dst, 0); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3])); + }); + + test('should copy partial data when offset reduces available space', () => { + const src = Buffer.from([1, 2, 3, 4]); + const dst = Buffer.alloc(5); + + const bytesCopied = Buffer.copy(src, dst, 3); + + assert.strictEqual(bytesCopied, 2); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 1, 2])); + }); + }); + + describe('sourceStart and sourceEnd parameters', () => { + describe('sourceStart parameter', () => { + test('should copy from sourceStart position', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 2); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([3, 4, 5, 0])); + }); + + test('should copy from sourceStart with target offset', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.from([0, 0, 0, 0, 0]); + + const bytesCopied = Buffer.copy(src, dst, 1, 2); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([0, 3, 4, 5, 0])); + }); + + test('should handle sourceStart at end of source', () => { + const src = Buffer.from([1, 2, 3, 4]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 4); + + assert.strictEqual(bytesCopied, 0); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0])); + }); + + test('should throw RangeError for sourceStart beyond source length', () => { + const src = Buffer.from([1, 2, 3, 4]); + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(src, dst, 0, 10); + }, { + name: 'RangeError', + message: /The value of "sourceStart" is out of range/ + }); + }); + }); + + describe('sourceEnd parameter', () => { + test('should copy up to sourceEnd position', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 0, 3); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3, 0])); + }); + + test('should copy partial range with sourceEnd', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.from([0, 0, 0, 0, 0]); + + const bytesCopied = Buffer.copy(src, dst, 1, 0, 2); + + assert.strictEqual(bytesCopied, 2); + assert.deepStrictEqual(dst, Buffer.from([0, 1, 2, 0, 0])); + }); + + test('should clamp sourceEnd beyond source length', () => { + const src = Buffer.from([1, 2, 3, 4]); + const dst = Buffer.alloc(6); + + const bytesCopied = Buffer.copy(src, dst, 0, 0, 10); + + assert.strictEqual(bytesCopied, 4); + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3, 4, 0, 0])); + }); + }); + + describe('sourceStart and sourceEnd together', () => { + test('should copy range from sourceStart to sourceEnd', () => { + const src = Buffer.from([1, 2, 3, 4, 5, 6]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 2, 5); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([3, 4, 5, 0])); + }); + + test('should copy range with target offset', () => { + const src = Buffer.from([1, 2, 3, 4, 5, 6]); + const dst = Buffer.from([0, 0, 0, 0, 0, 0]); + + const bytesCopied = Buffer.copy(src, dst, 2, 1, 4); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 2, 3, 4, 0])); + }); + + test('should handle sourceStart >= sourceEnd (copy zero bytes)', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 3, 3); + + assert.strictEqual(bytesCopied, 0); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0])); + }); + + test('should handle sourceStart > sourceEnd (copy zero bytes)', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 4, 2); + + assert.strictEqual(bytesCopied, 0); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0])); + }); + }); + + describe('sourceStart/sourceEnd validation', () => { + test('should throw RangeError for negative sourceStart', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(src, dst, 0, -1); + }, { + name: 'RangeError', + message: /The value of "sourceStart" is out of range/ + }); + }); + + test('should throw RangeError for negative sourceEnd', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(src, dst, 0, 0, -1); + }, { + name: 'RangeError', + message: /The value of "sourceEnd" is out of range/ + }); + }); + + test('should coerce string sourceStart to number', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, '2'); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([3, 4, 5, 0])); + }); + + test('should coerce string sourceEnd to number', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, 0, '3'); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3, 0])); + }); + + test('should treat NaN sourceStart as 0', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 0, NaN); + + assert.strictEqual(bytesCopied, 4); + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3, 4])); + }); + + test('should treat NaN sourceEnd as 0', () => { + const src = Buffer.from([1, 2, 3, 4, 5]); + const dst = Buffer.alloc(6); + + const bytesCopied = Buffer.copy(src, dst, 0, 0, NaN); + + assert.strictEqual(bytesCopied, 0); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0, 0, 0])); + }); + }); + }); + + describe('Input validation', () => { + describe('Null and undefined arguments', () => { + test('should throw TypeError for null source', () => { + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(null, dst, 0); + }, { + name: 'TypeError', + message: /The "source" argument must be an instance of ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received null/ + }); + }); + + test('should throw TypeError for undefined source', () => { + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(undefined, dst, 0); + }, { + name: 'TypeError', + message: /The "source" argument must be an instance of ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received undefined/ + }); + }); + + test('should throw TypeError for null target', () => { + const src = Buffer.from([1, 2, 3]); + + assert.throws(() => { + Buffer.copy(src, null, 0); + }, { + name: 'TypeError', + message: /The "target" argument must be an instance of Buffer, ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received null/ + }); + }); + + test('should throw TypeError for undefined target', () => { + const src = Buffer.from([1, 2, 3]); + + assert.throws(() => { + Buffer.copy(src, undefined, 0); + }, { + name: 'TypeError', + message: /The "target" argument must be an instance of Buffer, ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received undefined/ + }); + }); + }); + + describe('Invalid type arguments', () => { + test('should throw TypeError for string source', () => { + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy('hello', dst, 0); + }, { + name: 'TypeError', + message: /The "source" argument must be an instance of ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received type string \('hello'\)/ + }); + }); + + test('should throw TypeError for number source', () => { + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(123, dst, 0); + }, { + name: 'TypeError', + message: /The "source" argument must be an instance of ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received type number \(123\)/ + }); + }); + + test('should throw TypeError for plain object source', () => { + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy({ length: 4 }, dst, 0); + }, { + name: 'TypeError', + message: /The "source" argument must be an instance of ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received an instance of Object/ + }); + }); + + test('should throw TypeError for array source', () => { + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy([1, 2, 3, 4], dst, 0); + }, { + name: 'TypeError', + message: /The "source" argument must be an instance of ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received an instance of Array/ + }); + }); + + test('should throw TypeError for string target', () => { + const src = Buffer.from([1, 2, 3]); + + assert.throws(() => { + Buffer.copy(src, 'hello', 0); + }, { + name: 'TypeError', + message: /The "target" argument must be an instance of Buffer, ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received type string \('hello'\)/ + }); + }); + + test('should throw TypeError for number target', () => { + const src = Buffer.from([1, 2, 3]); + + assert.throws(() => { + Buffer.copy(src, 123, 0); + }, { + name: 'TypeError', + message: /The "target" argument must be an instance of Buffer, ArrayBuffer, SharedArrayBuffer, or TypedArray\. Received type number \(123\)/ + }); + }); + }); + + describe('Offset validation', () => { + test('should throw RangeError for negative offset', () => { + const src = Buffer.from([1, 2, 3]); + const dst = Buffer.alloc(4); + + assert.throws(() => { + Buffer.copy(src, dst, -1); + }, { + name: 'RangeError', + message: /The value of "targetStart" is out of range\. It must be >= 0\. Received -1/ + }); + }); + + test('should coerce non-number offset to number', () => { + const src = Buffer.from([1, 2, 3]); + const dst = Buffer.from([0, 0, 0, 0]); + + // String '2' should be coerced to number 2 + const bytesCopied = Buffer.copy(src, dst, '2'); + + assert.strictEqual(bytesCopied, 2); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 1, 2])); + }); + + test('should treat NaN offset as 0', () => { + const src = Buffer.from([1, 2, 3]); + const dst = Buffer.from([0, 0, 0, 0]); + + // NaN should be coerced to 0 + const bytesCopied = Buffer.copy(src, dst, NaN); + + assert.strictEqual(bytesCopied, 3); + assert.deepStrictEqual(dst, Buffer.from([1, 2, 3, 0])); + }); + + test('should return 0 for offset beyond target length', () => { + const src = Buffer.from([1, 2, 3]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 5); + + assert.strictEqual(bytesCopied, 0); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0])); + }); + + test('should return 0 for offset equal to target length', () => { + const src = Buffer.from([1, 2, 3]); + const dst = Buffer.alloc(4); + + const bytesCopied = Buffer.copy(src, dst, 4); + + assert.strictEqual(bytesCopied, 0); + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0])); + }); + }); + }); +}); From 917e8166cbd6048582649aeca1eb7d7604d9ea1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 24 Dec 2025 15:12:06 -0300 Subject: [PATCH 02/11] buffer: rename helper method --- src/node_buffer.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node_buffer.cc b/src/node_buffer.cc index e6be9c325ff557..9609e204b7bd5f 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1449,7 +1449,7 @@ void CopyArrayBuffer(const FunctionCallbackInfo& args) { memcpy(dest, src, bytes_to_copy); } -std::pair DecomposeSourceToParts(Local source) { +std::pair DecomposeBufferToParts(Local source) { void* pointer; size_t byte_length; @@ -1488,11 +1488,11 @@ void StaticCopy(const FunctionCallbackInfo& args) { void* source_data; size_t source_byte_length; - std::tie(source_data, source_byte_length) = DecomposeSourceToParts(source); + std::tie(source_data, source_byte_length) = DecomposeBufferToParts(source); void* target_data; size_t target_byte_length; - std::tie(target_data, target_byte_length) = DecomposeSourceToParts(target); + std::tie(target_data, target_byte_length) = DecomposeBufferToParts(target); size_t target_start = static_cast(args[2].As()->Value()); size_t source_start = static_cast(args[3].As()->Value()); From 67335df4d33f582783bd2eebeeec3586d3d666d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 24 Dec 2025 15:14:44 -0300 Subject: [PATCH 03/11] buffer: update error message --- test/parallel/test-buffer-alloc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-buffer-alloc.js b/test/parallel/test-buffer-alloc.js index 382cfbc93a6510..78ad15e143b719 100644 --- a/test/parallel/test-buffer-alloc.js +++ b/test/parallel/test-buffer-alloc.js @@ -1086,8 +1086,8 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "target" argument must be an instance of Buffer or ' + - 'Uint8Array. Received undefined' + message: 'The "target" argument must be an instance of Buffer, ' + + 'ArrayBuffer, SharedArrayBuffer, or TypedArray. Received undefined' }); assert.throws(() => Buffer.from(), { From 1080519dd8b2593a81d1f3ff3da697c1dfbd4e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 24 Dec 2025 15:19:55 -0300 Subject: [PATCH 04/11] buffer: format code --- src/node_buffer.cc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 9609e204b7bd5f..4c1824ab4baa7f 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1504,13 +1504,11 @@ void StaticCopy(const FunctionCallbackInfo& args) { } if (target_start >= target_byte_length) { - return THROW_ERR_OUT_OF_RANGE( - env, "targetStart is out of bounds"); + return THROW_ERR_OUT_OF_RANGE(env, "targetStart is out of bounds"); } if (source_start > source_byte_length || source_end > source_byte_length) { - return THROW_ERR_OUT_OF_RANGE( - env, "sourceStart or sourceEnd is out of bounds"); + return THROW_ERR_OUT_OF_RANGE(env, "sourceStart or sourceEnd is out of bounds"); } if (source_start >= source_end) { From 6386d3f43c1b2851b00f67451fbe28fe21d37799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 25 Dec 2025 12:32:23 -0300 Subject: [PATCH 05/11] buffer: remove repeated method --- src/node_buffer.cc | 46 ++++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 4c1824ab4baa7f..1a7aae1d5ce8b7 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1399,7 +1399,20 @@ namespace { std::pair DecomposeBufferToParts(Local buffer) { void* pointer; size_t byte_length; - if (buffer->IsArrayBuffer()) { + + if (buffer->IsArrayBufferView()) { + Local view = buffer.As(); + Local buffer = view->Buffer(); + + // Handle potential detached buffer case + if (buffer.IsEmpty() || buffer->Data() == nullptr) { + pointer = nullptr; + byte_length = 0; + } else { + pointer = static_cast(buffer->Data()) + view->ByteOffset(); + byte_length = view->ByteLength(); + } + } else if (buffer->IsArrayBuffer()) { Local ab = buffer.As(); pointer = ab->Data(); byte_length = ab->ByteLength(); @@ -1449,37 +1462,6 @@ void CopyArrayBuffer(const FunctionCallbackInfo& args) { memcpy(dest, src, bytes_to_copy); } -std::pair DecomposeBufferToParts(Local source) { - void* pointer; - size_t byte_length; - - if (source->IsArrayBufferView()) { - Local view = source.As(); - Local buffer = view->Buffer(); - - // Handle potential detached buffer case - if (buffer.IsEmpty() || buffer->Data() == nullptr) { - pointer = nullptr; - byte_length = 0; - } else { - pointer = static_cast(buffer->Data()) + view->ByteOffset(); - byte_length = view->ByteLength(); - } - } else if (source->IsArrayBuffer()) { - Local ab = source.As(); - pointer = ab->Data(); - byte_length = ab->ByteLength(); - } else if (source->IsSharedArrayBuffer()) { - Local ab = source.As(); - pointer = ab->Data(); - byte_length = ab->ByteLength(); - } else { - UNREACHABLE(); // Caller must validate. - } - - return {pointer, byte_length}; -} - void StaticCopy(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); From fa634742db9f4e1864962f16db687cb2c837d061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 25 Dec 2025 12:38:42 -0300 Subject: [PATCH 06/11] buffer: handle detached instances --- src/node_buffer.cc | 10 ++-------- test/parallel/test-buffer-copy-within.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 1a7aae1d5ce8b7..522c6bc68ea27c 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1404,14 +1404,8 @@ std::pair DecomposeBufferToParts(Local buffer) { Local view = buffer.As(); Local buffer = view->Buffer(); - // Handle potential detached buffer case - if (buffer.IsEmpty() || buffer->Data() == nullptr) { - pointer = nullptr; - byte_length = 0; - } else { - pointer = static_cast(buffer->Data()) + view->ByteOffset(); - byte_length = view->ByteLength(); - } + pointer = static_cast(buffer->Data()) + view->ByteOffset(); + byte_length = view->ByteLength(); } else if (buffer->IsArrayBuffer()) { Local ab = buffer.As(); pointer = ab->Data(); diff --git a/test/parallel/test-buffer-copy-within.js b/test/parallel/test-buffer-copy-within.js index 2a3faa6e3777d8..910c44ad3b1e19 100644 --- a/test/parallel/test-buffer-copy-within.js +++ b/test/parallel/test-buffer-copy-within.js @@ -61,6 +61,16 @@ describe('Buffer.copy', () => { Buffer.copy(src, dst, 0); assert.deepStrictEqual(new Uint8Array(ab), new Uint8Array([1, 2, 1, 2, 3, 4, 5])); }); + + test("should handle detached instances", () => { + const src = new Uint8Array([5, 6]).buffer; + src.transfer() + const dst = Buffer.from([0, 0, 0, 0]); + + Buffer.copy(src, dst, 1); + + assert.deepStrictEqual(dst, Buffer.from([0, 0, 0, 0])); + }) }); describe('DataView to Buffer', () => { From 23aca2ffc99db13fdae6ae1f69cfe124bf3ee3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Thu, 25 Dec 2025 12:47:03 -0300 Subject: [PATCH 07/11] buffer: create buffer.copy doc --- doc/api/buffer.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 981c053ac40f0c..56ac7487c4bdc9 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -919,6 +919,97 @@ socket.on('readable', () => { A `TypeError` will be thrown if `size` is not a number. +### Static method: `Buffer.copy(source, target, targetStart[, sourceStart[, sourceEnd]])` + + + +* `source` {Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer} The source + to copy data from. +* `target` {Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer} The target + to copy data to. +* `targetStart` {integer} The offset within `target` at which to begin writing. + **Default:** `0`. +* `sourceStart` {integer} The offset within `source` from which to begin copying. + **Default:** `0`. +* `sourceEnd` {integer} The offset within `source` at which to stop copying + (exclusive). **Default:** `source.byteLength`. +* Returns: {integer} The number of bytes copied. + +Copies data from `source` to `target`. This is a method that can copy data between different types of binary data structures, including `Buffer`, `TypedArray`, `DataView`, `ArrayBuffer`, and `SharedArrayBuffer` instances. + +```mjs +import { Buffer } from 'node:buffer'; + +const src = Buffer.from([1, 2, 3, 4]); +const dst = Buffer.alloc(4); + +const bytesCopied = Buffer.copy(src, dst, 0); +console.log(bytesCopied); // 4 +console.log(dst); // +``` + +```cjs +const { Buffer } = require('node:buffer'); + +const src = Buffer.from([1, 2, 3, 4]); +const dst = Buffer.alloc(4); + +const bytesCopied = Buffer.copy(src, dst, 0); +console.log(bytesCopied); // 4 +console.log(dst); // +``` + +The method can also copy between different types: + +```mjs +import { Buffer } from 'node:buffer'; + +// Copy from ArrayBuffer to Buffer +const ab = new Uint8Array([5, 6, 7, 8]).buffer; +const buf = Buffer.alloc(4); + +Buffer.copy(ab, buf, 0); +console.log(buf); // + +// Copy from Buffer to DataView +const src = Buffer.from([1, 2, 3]); +const targetAB = new ArrayBuffer(5); +const dv = new DataView(targetAB); + +Buffer.copy(src, dv, 2); +console.log(new Uint8Array(targetAB)); // Uint8Array(5) [ 0, 0, 1, 2, 3 ] +``` + +```cjs +const { Buffer } = require('node:buffer'); + +// Copy from ArrayBuffer to Buffer +const ab = new Uint8Array([5, 6, 7, 8]).buffer; +const buf = Buffer.alloc(4); + +Buffer.copy(ab, buf, 0); +console.log(buf); // + +// Copy from Buffer to DataView +const src = Buffer.from([1, 2, 3]); +const targetAB = new ArrayBuffer(5); +const dv = new DataView(targetAB); + +Buffer.copy(src, dv, 2); +console.log(new Uint8Array(targetAB)); // Uint8Array(5) [ 0, 0, 1, 2, 3 ] +``` + +If `targetStart` is negative or beyond the length of `target`, a [`RangeError`][] +is thrown. If `sourceStart` is negative or beyond the length of `source`, a +[`RangeError`][] is thrown. If `sourceEnd` is negative, a [`RangeError`][] is +thrown. Values that exceed the respective buffer lengths are clamped to the +appropriate limits. + +If `sourceStart` is greater than or equal to `sourceEnd`, zero bytes are copied +and the method returns `0`. + ### Static method: `Buffer.byteLength(string[, encoding])`