Repository: denoland/deno
Pull request: fix(ext/node): Buffer.fill compatibility fixes

Enables `parallel/test-buffer-fill.js`

Towards https://github.com/denoland/deno/issues/29589

Changed files:
filename: ext/node/polyfills/internal/buffer.mjs
patch: @@ -85,7 +85,10 @@ import {
   validateBuffer,
   validateInteger,
 } from "ext:deno_node/internal/validators.mjs";
-import { isUint8Array } from "ext:deno_node/internal/util/types.ts";
+import {
+  isArrayBufferView,
+  isUint8Array,
+} from "ext:deno_node/internal/util/types.ts";
 import {
   ERR_INVALID_ARG_TYPE,
   ERR_INVALID_STATE,
@@ -1877,10 +1880,10 @@ Buffer.prototype.fill = function fill(val, start, end, encoding) {
       end = this.length;
     }
     if (encoding !== void 0 && typeof encoding !== "string") {
-      throw new TypeError("encoding must be a string");
+      throw new codes.ERR_INVALID_ARG_TYPE("encoding", "string", encoding);
     }
     if (typeof encoding === "string" && !BufferIsEncoding(encoding)) {
-      throw new TypeError("Unknown encoding: " + encoding);
+      throw new codes.ERR_UNKNOWN_ENCODING(encoding);
     }
     if (val.length === 1) {
       const code = StringPrototypeCharCodeAt(val, 0);
@@ -1893,6 +1896,19 @@ Buffer.prototype.fill = function fill(val, start, end, encoding) {
   } else if (typeof val === "boolean") {
     val = Number(val);
   }
+
+  if (typeof start === "string") {
+    encoding = start;
+    start = 0;
+    end = this.length;
+  }
+  if (start !== undefined) {
+    validateNumber(start, "start", 0, kMaxLength);
+    if (end !== undefined) {
+      validateNumber(end, "end", 0, this.length);
+    }
+  }
+
   if (start < 0 || this.length < start || this.length < end) {
     throw new RangeError("Out of range index");
   }
@@ -1906,10 +1922,24 @@ Buffer.prototype.fill = function fill(val, start, end, encoding) {
   }
   let i;
   if (typeof val === "number") {
+    // OOB check
+    const byteLen = TypedArrayPrototypeGetByteLength(this);
+    const fillLength = end - start;
+    if (start > end || fillLength + start > byteLen) {
+      throw new codes.ERR_BUFFER_OUT_OF_BOUNDS();
+    }
+
     for (i = start; i < end; ++i) {
       this[i] = val;
     }
   } else {
+    if (typeof val !== "string" && !isArrayBufferView(val)) {
+      val = Number(val) & 255;
+      for (i = start; i < end; ++i) {
+        this[i] = val;
+      }
+      return this;
+    }
     const bytes = BufferIsBuffer(val) ? val : BufferFrom(val, encoding);
     const len = bytes.length;
     if (len === 0) {

filename: ext/node/polyfills/internal_binding/buffer.ts
patch: @@ -5,6 +5,15 @@
 
 import { Encodings } from "ext:deno_node/internal_binding/_node.ts";
 
+export function fill(
+  buffer,
+  value,
+  start,
+  end,
+) {
+  return buffer.fill(value, start, end);
+}
+
 export function indexOfNeedle(
   source: Uint8Array,
   needle: Uint8Array,

filename: tests/node_compat/config.jsonc
patch: @@ -288,6 +288,7 @@
       "test-buffer-equals.js",
       "test-buffer-failed-alloc-typed-arrays.js",
       "test-buffer-fakes.js",
+      "test-buffer-fill.js",
       "test-buffer-from.js",
       "test-buffer-includes.js",
       "test-buffer-indexof.js",

filename: tests/node_compat/runner/TODO.md
patch: @@ -283,7 +283,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
 - [parallel/test-bootstrap-modules.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-bootstrap-modules.js)
 - [parallel/test-broadcastchannel-custom-inspect.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-broadcastchannel-custom-inspect.js)
 - [parallel/test-buffer-constructor-node-modules-paths.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-buffer-constructor-node-modules-paths.js)
-- [parallel/test-buffer-fill.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-buffer-fill.js)
 - [parallel/test-buffer-inspect.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-buffer-inspect.js)
 - [parallel/test-buffer-prototype-inspect.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-buffer-prototype-inspect.js)
 - [parallel/test-c-ares.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-c-ares.js)

filename: tests/node_compat/test/parallel/test-buffer-fill.js
patch: @@ -0,0 +1,453 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 23.9.0
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+// Flags: --expose-internals
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const { codes: { ERR_OUT_OF_RANGE } } = require('internal/errors');
+const { internalBinding } = require('internal/test/binding');
+const SIZE = 28;
+
+const buf1 = Buffer.allocUnsafe(SIZE);
+const buf2 = Buffer.allocUnsafe(SIZE);
+
+// Default encoding
+testBufs('abc');
+testBufs('\u0222aa');
+testBufs('a\u0234b\u0235c\u0236');
+testBufs('abc', 4);
+testBufs('abc', 5);
+testBufs('abc', SIZE);
+testBufs('\u0222aa', 2);
+testBufs('\u0222aa', 8);
+testBufs('a\u0234b\u0235c\u0236', 4);
+testBufs('a\u0234b\u0235c\u0236', 12);
+testBufs('abc', 4, 1);
+testBufs('abc', 5, 1);
+testBufs('\u0222aa', 8, 1);
+testBufs('a\u0234b\u0235c\u0236', 4, 1);
+testBufs('a\u0234b\u0235c\u0236', 12, 1);
+
+// UTF8
+testBufs('abc', 'utf8');
+testBufs('\u0222aa', 'utf8');
+testBufs('a\u0234b\u0235c\u0236', 'utf8');
+testBufs('abc', 4, 'utf8');
+testBufs('abc', 5, 'utf8');
+testBufs('abc', SIZE, 'utf8');
+testBufs('\u0222aa', 2, 'utf8');
+testBufs('\u0222aa', 8, 'utf8');
+testBufs('a\u0234b\u0235c\u0236', 4, 'utf8');
+testBufs('a\u0234b\u0235c\u0236', 12, 'utf8');
+testBufs('abc', 4, 1, 'utf8');
+testBufs('abc', 5, 1, 'utf8');
+testBufs('\u0222aa', 8, 1, 'utf8');
+testBufs('a\u0234b\u0235c\u0236', 4, 1, 'utf8');
+testBufs('a\u0234b\u0235c\u0236', 12, 1, 'utf8');
+assert.strictEqual(Buffer.allocUnsafe(1).fill(0).fill('\u0222')[0], 0xc8);
+
+// BINARY
+testBufs('abc', 'binary');
+testBufs('\u0222aa', 'binary');
+testBufs('a\u0234b\u0235c\u0236', 'binary');
+testBufs('abc', 4, 'binary');
+testBufs('abc', 5, 'binary');
+testBufs('abc', SIZE, 'binary');
+testBufs('\u0222aa', 2, 'binary');
+testBufs('\u0222aa', 8, 'binary');
+testBufs('a\u0234b\u0235c\u0236', 4, 'binary');
+testBufs('a\u0234b\u0235c\u0236', 12, 'binary');
+testBufs('abc', 4, 1, 'binary');
+testBufs('abc', 5, 1, 'binary');
+testBufs('\u0222aa', 8, 1, 'binary');
+testBufs('a\u0234b\u0235c\u0236', 4, 1, 'binary');
+testBufs('a\u0234b\u0235c\u0236', 12, 1, 'binary');
+
+// LATIN1
+testBufs('abc', 'latin1');
+testBufs('\u0222aa', 'latin1');
+testBufs('a\u0234b\u0235c\u0236', 'latin1');
+testBufs('abc', 4, 'latin1');
+testBufs('abc', 5, 'latin1');
+testBufs('abc', SIZE, 'latin1');
+testBufs('\u0222aa', 2, 'latin1');
+testBufs('\u0222aa', 8, 'latin1');
+testBufs('a\u0234b\u0235c\u0236', 4, 'latin1');
+testBufs('a\u0234b\u0235c\u0236', 12, 'latin1');
+testBufs('abc', 4, 1, 'latin1');
+testBufs('abc', 5, 1, 'latin1');
+testBufs('\u0222aa', 8, 1, 'latin1');
+testBufs('a\u0234b\u0235c\u0236', 4, 1, 'latin1');
+testBufs('a\u0234b\u0235c\u0236', 12, 1, 'latin1');
+
+// UCS2
+testBufs('abc', 'ucs2');
+testBufs('\u0222aa', 'ucs2');
+testBufs('a\u0234b\u0235c\u0236', 'ucs2');
+testBufs('abc', 4, 'ucs2');
+testBufs('abc', SIZE, 'ucs2');
+testBufs('\u0222aa', 2, 'ucs2');
+testBufs('\u0222aa', 8, 'ucs2');
+testBufs('a\u0234b\u0235c\u0236', 4, 'ucs2');
+testBufs('a\u0234b\u0235c\u0236', 12, 'ucs2');
+testBufs('abc', 4, 1, 'ucs2');
+testBufs('abc', 5, 1, 'ucs2');
+testBufs('\u0222aa', 8, 1, 'ucs2');
+testBufs('a\u0234b\u0235c\u0236', 4, 1, 'ucs2');
+testBufs('a\u0234b\u0235c\u0236', 12, 1, 'ucs2');
+assert.strictEqual(Buffer.allocUnsafe(1).fill('\u0222', 'ucs2')[0], 0x22);
+
+// HEX
+testBufs('616263', 'hex');
+testBufs('c8a26161', 'hex');
+testBufs('61c8b462c8b563c8b6', 'hex');
+testBufs('616263', 4, 'hex');
+testBufs('616263', 5, 'hex');
+testBufs('616263', SIZE, 'hex');
+testBufs('c8a26161', 2, 'hex');
+testBufs('c8a26161', 8, 'hex');
+testBufs('61c8b462c8b563c8b6', 4, 'hex');
+testBufs('61c8b462c8b563c8b6', 12, 'hex');
+testBufs('616263', 4, 1, 'hex');
+testBufs('616263', 5, 1, 'hex');
+testBufs('c8a26161', 8, 1, 'hex');
+testBufs('61c8b462c8b563c8b6', 4, 1, 'hex');
+testBufs('61c8b462c8b563c8b6', 12, 1, 'hex');
+
+assert.throws(() => {
+  const buf = Buffer.allocUnsafe(SIZE);
+
+  buf.fill('yKJh', 'hex');
+}, {
+  code: 'ERR_INVALID_ARG_VALUE',
+  name: 'TypeError'
+});
+
+assert.throws(() => {
+  const buf = Buffer.allocUnsafe(SIZE);
+
+  buf.fill('\u0222', 'hex');
+}, {
+  code: 'ERR_INVALID_ARG_VALUE',
+  name: 'TypeError'
+});
+
+// BASE64
+testBufs('YWJj', 'base64');
+testBufs('yKJhYQ==', 'base64');
+testBufs('Yci0Ysi1Y8i2', 'base64');
+testBufs('YWJj', 4, 'base64');
+testBufs('YWJj', SIZE, 'base64');
+testBufs('yKJhYQ==', 2, 'base64');
+testBufs('yKJhYQ==', 8, 'base64');
+testBufs('Yci0Ysi1Y8i2', 4, 'base64');
+testBufs('Yci0Ysi1Y8i2', 12, 'base64');
+testBufs('YWJj', 4, 1, 'base64');
+testBufs('YWJj', 5, 1, 'base64');
+testBufs('yKJhYQ==', 8, 1, 'base64');
+testBufs('Yci0Ysi1Y8i2', 4, 1, 'base64');
+testBufs('Yci0Ysi1Y8i2', 12, 1, 'base64');
+
+// BASE64URL
+testBufs('YWJj', 'base64url');
+testBufs('yKJhYQ', 'base64url');
+testBufs('Yci0Ysi1Y8i2', 'base64url');
+testBufs('YWJj', 4, 'base64url');
+testBufs('YWJj', SIZE, 'base64url');
+testBufs('yKJhYQ', 2, 'base64url');
+testBufs('yKJhYQ', 8, 'base64url');
+testBufs('Yci0Ysi1Y8i2', 4, 'base64url');
+testBufs('Yci0Ysi1Y8i2', 12, 'base64url');
+testBufs('YWJj', 4, 1, 'base64url');
+testBufs('YWJj', 5, 1, 'base64url');
+testBufs('yKJhYQ', 8, 1, 'base64url');
+testBufs('Yci0Ysi1Y8i2', 4, 1, 'base64url');
+testBufs('Yci0Ysi1Y8i2', 12, 1, 'base64url');
+
+// Buffer
+function deepStrictEqualValues(buf, arr) {
+  for (const [index, value] of buf.entries()) {
+    assert.deepStrictEqual(value, arr[index]);
+  }
+}
+
+const buf2Fill = Buffer.allocUnsafe(1).fill(2);
+deepStrictEqualValues(genBuffer(4, [buf2Fill]), [2, 2, 2, 2]);
+deepStrictEqualValues(genBuffer(4, [buf2Fill, 1]), [0, 2, 2, 2]);
+deepStrictEqualValues(genBuffer(4, [buf2Fill, 1, 3]), [0, 2, 2, 0]);
+deepStrictEqualValues(genBuffer(4, [buf2Fill, 1, 1]), [0, 0, 0, 0]);
+const hexBufFill = Buffer.allocUnsafe(2).fill(0).fill('0102', 'hex');
+deepStrictEqualValues(genBuffer(4, [hexBufFill]), [1, 2, 1, 2]);
+deepStrictEqualValues(genBuffer(4, [hexBufFill, 1]), [0, 1, 2, 1]);
+deepStrictEqualValues(genBuffer(4, [hexBufFill, 1, 3]), [0, 1, 2, 0]);
+deepStrictEqualValues(genBuffer(4, [hexBufFill, 1, 1]), [0, 0, 0, 0]);
+
+// Check exceptions
+[
+  [0, -1],
+  [0, 0, buf1.length + 1],
+  ['', -1],
+  ['', 0, buf1.length + 1],
+  ['', 1, -1],
+].forEach((args) => {
+  assert.throws(
+    () => buf1.fill(...args),
+    { code: 'ERR_OUT_OF_RANGE' }
+  );
+});
+
+assert.throws(
+  () => buf1.fill('a', 0, buf1.length, 'node rocks!'),
+  {
+    code: 'ERR_UNKNOWN_ENCODING',
+    name: 'TypeError',
+    message: 'Unknown encoding: node rocks!'
+  }
+);
+
+[
+  ['a', 0, 0, NaN],
+  ['a', 0, 0, false],
+].forEach((args) => {
+  assert.throws(
+    () => buf1.fill(...args),
+    {
+      code: 'ERR_INVALID_ARG_TYPE',
+      message: 'The "encoding" argument must be of type ' +
+      `string.${common.invalidArgTypeHelper(args[3])}`
+    }
+  );
+});
+
+assert.throws(
+  () => buf1.fill('a', 0, 0, 'foo'),
+  {
+    code: 'ERR_UNKNOWN_ENCODING',
+    name: 'TypeError',
+    message: 'Unknown encoding: foo'
+  }
+);
+
+function genBuffer(size, args) {
+  const b = Buffer.allocUnsafe(size);
+  return b.fill(0).fill.apply(b, args);
+}
+
+function bufReset() {
+  buf1.fill(0);
+  buf2.fill(0);
+}
+
+// This is mostly accurate. Except write() won't write partial bytes to the
+// string while fill() blindly copies bytes into memory. To account for that an
+// error will be thrown if not all the data can be written, and the SIZE has
+// been massaged to work with the input characters.
+function writeToFill(string, offset, end, encoding) {
+  if (typeof offset === 'string') {
+    encoding = offset;
+    offset = 0;
+    end = buf2.length;
+  } else if (typeof end === 'string') {
+    encoding = end;
+    end = buf2.length;
+  } else if (end === undefined) {
+    end = buf2.length;
+  }
+
+  // Should never be reached.
+  if (offset < 0 || end > buf2.length)
+    throw new ERR_OUT_OF_RANGE();
+
+  if (end <= offset)
+    return buf2;
+
+  offset >>>= 0;
+  end >>>= 0;
+  assert(offset <= buf2.length);
+
+  // Convert "end" to "length" (which write understands).
+  const length = end - offset < 0 ? 0 : end - offset;
+
+  let wasZero = false;
+  do {
+    const written = buf2.write(string, offset, length, encoding);
+    offset += written;
+    // Safety check in case write falls into infinite loop.
+    if (written === 0) {
+      if (wasZero)
+        throw new Error('Could not write all data to Buffer');
+      else
+        wasZero = true;
+    }
+  } while (offset < buf2.length);
+
+  return buf2;
+}
+
+function testBufs(string, offset, length, encoding) {
+  bufReset();
+  buf1.fill.apply(buf1, arguments);
+  // Swap bytes on BE archs for ucs2 encoding.
+  assert.deepStrictEqual(buf1.fill.apply(buf1, arguments),
+                         writeToFill.apply(null, arguments));
+}
+
+// Make sure these throw.
+assert.throws(
+  () => Buffer.allocUnsafe(8).fill('a', -1),
+  { code: 'ERR_OUT_OF_RANGE' });
+assert.throws(
+  () => Buffer.allocUnsafe(8).fill('a', 0, 9),
+  { code: 'ERR_OUT_OF_RANGE' });
+
+// Make sure this doesn't hang indefinitely.
+Buffer.allocUnsafe(8).fill('');
+Buffer.alloc(8, '');
+
+{
+  const buf = Buffer.alloc(64, 10);
+  for (let i = 0; i < buf.length; i++)
+    assert.strictEqual(buf[i], 10);
+
+  buf.fill(11, 0, buf.length >> 1);
+  for (let i = 0; i < buf.length >> 1; i++)
+    assert.strictEqual(buf[i], 11);
+  for (let i = (buf.length >> 1) + 1; i < buf.length; i++)
+    assert.strictEqual(buf[i], 10);
+
+  buf.fill('h');
+  for (let i = 0; i < buf.length; i++)
+    assert.strictEqual(buf[i], 'h'.charCodeAt(0));
+
+  buf.fill(0);
+  for (let i = 0; i < buf.length; i++)
+    assert.strictEqual(buf[i], 0);
+
+  buf.fill(null);
+  for (let i = 0; i < buf.length; i++)
+    assert.strictEqual(buf[i], 0);
+
+  buf.fill(1, 16, 32);
+  for (let i = 0; i < 16; i++)
+    assert.strictEqual(buf[i], 0);
+  for (let i = 16; i < 32; i++)
+    assert.strictEqual(buf[i], 1);
+  for (let i = 32; i < buf.length; i++)
+    assert.strictEqual(buf[i], 0);
+}
+
+{
+  const buf = Buffer.alloc(10, 'abc');
+  assert.strictEqual(buf.toString(), 'abcabcabca');
+  buf.fill('է');
+  assert.strictEqual(buf.toString(), 'էէէէէ');
+}
+
+// Testing process.binding. Make sure "start" is properly checked for range
+// errors.
+assert.throws(
+  () => { internalBinding('buffer').fill(Buffer.alloc(1), 1, -1, 0, 1); },
+  { code: 'ERR_OUT_OF_RANGE' }
+);
+
+// Make sure "end" is properly checked, even if it's magically mangled using
+// Symbol.toPrimitive.
+{
+  assert.throws(() => {
+    const end = {
+      [Symbol.toPrimitive]() {
+        return 1;
+      }
+    };
+    Buffer.alloc(1).fill(Buffer.alloc(1), 0, end);
+  }, {
+    code: 'ERR_INVALID_ARG_TYPE',
+    message: 'The "end" argument must be of type number. Received an ' +
+             'instance of Object'
+  });
+}
+
+// Testing process.binding. Make sure "end" is properly checked for range
+// errors.
+assert.throws(
+  () => { internalBinding('buffer').fill(Buffer.alloc(1), 1, 1, -2, 1); },
+  { code: 'ERR_OUT_OF_RANGE' }
+);
+
+// Test that bypassing 'length' won't cause an abort.
+assert.throws(() => {
+  const buf = Buffer.from('w00t');
+  Object.defineProperty(buf, 'length', {
+    value: 1337,
+    enumerable: true
+  });
+  buf.fill('');
+}, {
+  code: 'ERR_BUFFER_OUT_OF_BOUNDS',
+  name: 'RangeError',
+  message: 'Attempt to access memory outside buffer bounds'
+});
+
+assert.deepStrictEqual(
+  Buffer.allocUnsafeSlow(16).fill('ab', 'utf16le'),
+  Buffer.from('61006200610062006100620061006200', 'hex'));
+
+assert.deepStrictEqual(
+  Buffer.allocUnsafeSlow(15).fill('ab', 'utf16le'),
+  Buffer.from('610062006100620061006200610062', 'hex'));
+
+assert.deepStrictEqual(
+  Buffer.allocUnsafeSlow(16).fill('ab', 'utf16le'),
+  Buffer.from('61006200610062006100620061006200', 'hex'));
+assert.deepStrictEqual(
+  Buffer.allocUnsafeSlow(16).fill('a', 'utf16le'),
+  Buffer.from('61006100610061006100610061006100', 'hex'));
+
+assert.strictEqual(
+  Buffer.allocUnsafeSlow(16).fill('a', 'utf16le').toString('utf16le'),
+  'a'.repeat(8));
+assert.strictEqual(
+  Buffer.allocUnsafeSlow(16).fill('a', 'latin1').toString('latin1'),
+  'a'.repeat(16));
+assert.strictEqual(
+  Buffer.allocUnsafeSlow(16).fill('a', 'utf8').toString('utf8'),
+  'a'.repeat(16));
+
+assert.strictEqual(
+  Buffer.allocUnsafeSlow(16).fill('Љ', 'utf16le').toString('utf16le'),
+  'Љ'.repeat(8));
+assert.strictEqual(
+  Buffer.allocUnsafeSlow(16).fill('Љ', 'latin1').toString('latin1'),
+  '\t'.repeat(16));
+assert.strictEqual(
+  Buffer.allocUnsafeSlow(16).fill('Љ', 'utf8').toString('utf8'),
+  'Љ'.repeat(8));
+
+assert.throws(() => {
+  const buf = Buffer.from('a'.repeat(1000));
+
+  buf.fill('This is not correctly encoded', 'hex');
+}, {
+  code: 'ERR_INVALID_ARG_VALUE',
+  name: 'TypeError'
+});
+
+
+{
+  const bufEmptyString = Buffer.alloc(5, '');
+  assert.strictEqual(bufEmptyString.toString(), '\x00\x00\x00\x00\x00');
+
+  const bufEmptyArray = Buffer.alloc(5, []);
+  assert.strictEqual(bufEmptyArray.toString(), '\x00\x00\x00\x00\x00');
+
+  const bufEmptyBuffer = Buffer.alloc(5, Buffer.alloc(5));
+  assert.strictEqual(bufEmptyBuffer.toString(), '\x00\x00\x00\x00\x00');
+
+  const bufZero = Buffer.alloc(5, 0);
+  assert.strictEqual(bufZero.toString(), '\x00\x00\x00\x00\x00');
+}