Skip to content
2 changes: 1 addition & 1 deletion benchmark/buffers/buffer-bytelength-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
});
Expand Down
31 changes: 31 additions & 0 deletions benchmark/buffers/buffer-copy-bytes-from.js
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions benchmark/buffers/buffer-fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down
2 changes: 1 addition & 1 deletion benchmark/buffers/buffer-indexof.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const searchStrings = [

const bench = common.createBenchmark(main, {
search: searchStrings,
encoding: ['undefined', 'utf8', 'ucs2', 'latin1'],
encoding: ['undefined', 'utf8', 'ascii', 'latin1', 'ucs2'],
type: ['buffer', 'string'],
n: [5e4],
}, {
Expand Down
2 changes: 1 addition & 1 deletion benchmark/buffers/buffer-tostring.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
121 changes: 67 additions & 54 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const {
TypedArrayPrototypeGetByteOffset,
TypedArrayPrototypeGetLength,
TypedArrayPrototypeSet,
TypedArrayPrototypeSlice,
TypedArrayPrototypeSubarray,
Uint8Array,
} = primordials;
Expand Down Expand Up @@ -383,28 +382,33 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) {
return new FastBuffer();
}

if (offset !== undefined || length !== undefined) {
if (offset !== undefined) {
validateInteger(offset, 'offset', 0);
if (offset >= viewLength) return new FastBuffer();
} else {
offset = 0;
}
let end;
if (length !== undefined) {
validateInteger(length, 'length', 0);
end = offset + length;
} else {
end = viewLength;
}
let start = 0;
let end = viewLength;

view = TypedArrayPrototypeSlice(view, offset, end);
if (offset !== undefined) {
validateInteger(offset, 'offset', 0);
if (offset >= viewLength) return new FastBuffer();
start = offset;
}

if (length !== undefined) {
validateInteger(length, 'length', 0);
// The old code used TypedArrayPrototypeSlice which clamps internally.
end = MathMin(start + length, viewLength);
}

if (end <= start) return new FastBuffer();

const viewByteLength = TypedArrayPrototypeGetByteLength(view);
const elementSize = viewByteLength / viewLength;
const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) +
start * elementSize;
const srcByteLength = (end - start) * elementSize;

return fromArrayLike(new Uint8Array(
TypedArrayPrototypeGetBuffer(view),
TypedArrayPrototypeGetByteOffset(view),
TypedArrayPrototypeGetByteLength(view)));
srcByteOffset,
srcByteLength));
};

// Identical to the built-in %TypedArray%.of(), but avoids using the deprecated
Expand Down Expand Up @@ -551,14 +555,15 @@ function fromArrayBuffer(obj, byteOffset, length) {
}

function fromArrayLike(obj) {
if (obj.length <= 0)
const { length } = obj;
if (length <= 0)
return new FastBuffer();
if (obj.length < (Buffer.poolSize >>> 1)) {
if (obj.length > (poolSize - poolOffset))
if (length < (Buffer.poolSize >>> 1)) {
if (length > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolOffset, obj.length);
const b = new FastBuffer(allocPool, poolOffset, length);
TypedArrayPrototypeSet(b, obj, 0);
poolOffset += obj.length;
poolOffset += length;
alignPool();
return b;
}
Expand Down Expand Up @@ -732,11 +737,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',
Expand Down Expand Up @@ -897,17 +898,17 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
return utf8Slice(this, 0, this.length);
}

const len = this.length;
const bufferLength = TypedArrayPrototypeGetLength(this);

if (start <= 0)
start = 0;
else if (start >= len)
else if (start >= bufferLength)
return '';
else
start = MathTrunc(start) || 0;

if (end === undefined || end > len)
end = len;
if (end === undefined || end > bufferLength)
end = bufferLength;
else
end = MathTrunc(end) || 0;

Expand Down Expand Up @@ -1118,7 +1119,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') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, ascii behaves exactly like latin1
Unsure if by design or accidentally

Copy link
Contributor Author

@thisalihassan thisalihassan Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is safe by design I am just extending the existing single byte numeric optimization to cover ASCII, since the guard already constrains it to the valid ASCII range.

const code = StringPrototypeCharCodeAt(value, 0);
if (code < 128) {
value = code;
Expand Down Expand Up @@ -1168,29 +1171,30 @@ function _fill(buf, value, offset, end, encoding) {
}

Buffer.prototype.write = function write(string, offset, length, encoding) {
const bufferLength = TypedArrayPrototypeGetLength(this);
// Buffer#write(string);
if (offset === undefined) {
return utf8Write(this, string, 0, this.length);
return utf8Write(this, string, 0, bufferLength);
}
// Buffer#write(string, encoding)
if (length === undefined && typeof offset === 'string') {
encoding = offset;
length = this.length;
length = bufferLength;
offset = 0;

// Buffer#write(string, offset[, length][, encoding])
} else {
validateOffset(offset, 'offset', 0, this.length);
validateOffset(offset, 'offset', 0, bufferLength);

const remaining = this.length - offset;
const remaining = bufferLength - offset;

if (length === undefined) {
length = remaining;
} else if (typeof length === 'string') {
encoding = length;
length = remaining;
} else {
validateOffset(length, 'length', 0, this.length);
validateOffset(length, 'length', 0, bufferLength);
if (length > remaining)
length = remaining;
}
Expand All @@ -1208,9 +1212,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 bufferLength = TypedArrayPrototypeGetLength(this);
if (bufferLength > 0) {
const data = new Array(bufferLength);
for (let i = 0; i < bufferLength; ++i)
data[i] = this[i];
return { type: 'Buffer', data };
}
Expand All @@ -1235,7 +1240,7 @@ function adjustOffset(offset, length) {
}

Buffer.prototype.subarray = function subarray(start, end) {
const srcLength = this.length;
const srcLength = TypedArrayPrototypeGetLength(this);
start = adjustOffset(start, srcLength);
end = end !== undefined ? adjustOffset(end, srcLength) : srcLength;
const newLength = end > start ? end - start : 0;
Expand All @@ -1253,45 +1258,52 @@ function swap(b, n, m) {
}

Buffer.prototype.swap16 = function swap16() {
// For Buffer.length < 128, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length <= 32, it's generally faster to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a reference link to this pull-request above this line

// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
const len = TypedArrayPrototypeGetLength(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this should provide a viable defense against crashing if this isn't a typed array, the error is going to be somewhat non-obvious...

Welcome to Node.js v26.0.0-pre.
Type ".help" for more information.
> const { swap16 } = Buffer.prototype
undefined
> swap16(1)
Uncaught:
TypeError: Method get TypedArray.prototype.length called on incompatible receiver undefined
    at get length (<anonymous>)
    at swap16 (node:buffer:1265:15)
> 

Having this instead be a ERR_INVALID_THIS error with a more specific error message would be nicer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would have a performance impact wouldn't it?

Copy link
Contributor Author

@thisalihassan thisalihassan Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree about the error have discussed it with @aduh95 in this thread

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are used so infrequently that I'm not concerned about the performance impact of an additional isArrayBufferView() type check

Copy link
Contributor Author

@thisalihassan thisalihassan Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added isArrayBufferView guard with ERR_INVALID_THIS to all three swap methods. Here's the behavior comparison:

swap16

Receiver main branch This PR
Buffer(4) OK OK
Uint8Array(4) OK OK
Float32Array(4) OK OK
Int16Array(4) OK OK
Array(4) OK ERR_INVALID_THIS
Array(200) ERR_INVALID_ARG_TYPE ERR_INVALID_THIS
number(1) ERR_INVALID_BUFFER_SIZE ERR_INVALID_THIS
string("ab") TypeError (read-only property) ERR_INVALID_THIS
plain object OK (silent garbage) ERR_INVALID_THIS
null TypeError ERR_INVALID_THIS
undefined TypeError ERR_INVALID_THIS

I don't think any of this is now considered breaking changes, some used to work but that shouldn't have worked realistically @jasnell
cc: @aduh95

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are used so infrequently that I'm not concerned about the error being not as helpful as it could be, especially since we don't support passing an arbitrary this. If V8 can get away with it, we can too.

if (len % 2 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('16-bits');
if (len < 128) {
if (len <= 32) {
for (let i = 0; i < len; i += 2)
swap(this, i, i + 1);
return this;
}
return _swap16(this);
_swap16(this);
Copy link
Member

@ChALkeR ChALkeR Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would happen if it's called on not a TypeArrayView now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. Buffer.prototype.swap16.apply(new Array(128))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I will add a JS side guard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would that be good approach @ChALkeR ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It changes the method to not being callable on a typedarray that is not an uint8array, but the previous behavior was inconsistent in that case anyway as the js part swapped elements and the src part swapped bytes

So I think explicitly blocking non-uint8arr should be fine and not a semver-major?
(perhaps cc @nodejs/lts just in case)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right about that, I was pointing about the inconsistency about the different paths. as JS swapped the elements but alright I will work on the feedback
Thanks

Copy link
Contributor

@aduh95 aduh95 Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against changing the current state of things, but IMO that's a separate concern and should be discussed separately. Currently Buffer.prototype.swap16.call(new Float32Array(128)) does not throw, so if we want to make it throw let's open a separate semver-major PR.
My opinion is we should rely on TypedArrayPrototypeGetByteLength doing the identity check for us in this PR, better for performance and not worse than the status quo for DX/UX.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's pretty valid @aduh95, I have used TypedArrayPrototypeGetByteLength I will open a seperate issue on this.

Copy link
Member

@ChALkeR ChALkeR Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently Buffer.prototype.swap16.call(new Float32Array(128)) does not throw,

It does not throw but returns garbage (as in, result is not predictable / does not follow any logic)
So anything can be done with it I think

Making it swap bytes is an option. Making it throw is another option.

The current behavior is length-dependent:

  • 129 (516 bytes) throws and complains about non-even number of bytes
  • 128 (512 bytes) does not throw (as you pointed out) and swaps bytes
  • 127 (508 bytes) throws and complains about non-even number of bytes
  • 126 (504 bytes) does not throw and swaps elements (groups of 4 bytes)

That is a mere oversight and not a proper api, and, more significantly, no one should be using that realistically as it doesn't work.
I don't think that making that a hard error is a semver-major.

Copy link
Contributor

@aduh95 aduh95 Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a non-Buffer this is not documented, so it implicitly is undefined behavior.
Garbage out is IMO a good tradeoff for a more performant happy path, although I didn't run any benchmark.

no one should be using that realistically as it doesn't work.

100% agreed – and I would bet that absolutely no one is passing Float32Arrays to that API. Still, I don't think it's worth changing in this PR, which is about improving performance.

return this;
};

Buffer.prototype.swap32 = function swap32() {
// For Buffer.length < 192, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length <= 32, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
const len = TypedArrayPrototypeGetLength(this);
if (len % 4 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('32-bits');
if (len < 192) {
if (len <= 32) {
for (let i = 0; i < len; i += 4) {
swap(this, i, i + 3);
swap(this, i + 1, i + 2);
}
return this;
}
return _swap32(this);
_swap32(this);
return this;
};

Buffer.prototype.swap64 = function swap64() {
// For Buffer.length < 192, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length < 48, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
// Threshold differs from swap16/swap32 (<=32) because swap64's
// crossover is between 40 and 48 (native wins at 48, loses at 40).
const len = TypedArrayPrototypeGetLength(this);
if (len % 8 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('64-bits');
if (len < 192) {
if (len < 48) {
for (let i = 0; i < len; i += 8) {
swap(this, i, i + 7);
swap(this, i + 1, i + 6);
Expand All @@ -1300,7 +1312,8 @@ Buffer.prototype.swap64 = function swap64() {
}
return this;
}
return _swap64(this);
_swap64(this);
return this;
};

Buffer.prototype.toLocaleString = Buffer.prototype.toString;
Expand Down
Loading
Loading