From 090b08770b0402c9368128dd3a08b5a563ed0e50 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 25 Sep 2026 18:58:46 +0000 Subject: [PATCH] buffer: add isByteString Implements a fast check to determine if a string is a valid byte string (only chars <= 0xff). Can be up to 58x faster than the equivalent regex check. Signed-off-by: James M Snell --- benchmark/buffers/buffer-isbytestring.js | 57 +++++++++++++ doc/api/buffer.md | 43 ++++++++++ lib/buffer.js | 7 ++ src/node_buffer.cc | 23 +++++ test/parallel/test-buffer-isbytestring.js | 85 +++++++++++++++++++ .../test-buffer-isutf8-isascii-fast.js | 13 ++- 6 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 benchmark/buffers/buffer-isbytestring.js create mode 100644 test/parallel/test-buffer-isbytestring.js diff --git a/benchmark/buffers/buffer-isbytestring.js b/benchmark/buffers/buffer-isbytestring.js new file mode 100644 index 00000000000..3e08d7042c2 --- /dev/null +++ b/benchmark/buffers/buffer-isbytestring.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common.js'); +const buffer = require('node:buffer'); +const assert = require('node:assert'); + +const bench = common.createBenchmark(main, { + n: [1e7], + length: ['short', 'long'], + // onebyte: one-byte representation (O(1) check) + // twobyte: two-byte representation containing only code units <= 0xFF + // invalid: ends with a code unit > 0xFF + input: ['onebyte', 'twobyte', 'invalid'], + method: ['isByteString', 'loop', 'regex'], +}); + +function loop(str) { + for (let i = 0; i < str.length; i++) { + if (str.charCodeAt(i) > 255) return false; + } + return true; +} + +const notByteStringRe = /[\u0100-\uffff]/; +function regex(str) { + return !notByteStringRe.test(str); +} + +const methods = { isByteString: buffer.isByteString, loop, regex }; + +function main({ n, length, input, method }) { + const base = length === 'short' ? 'hello w\u00f6rld' : 'hello w\u00f6rld'.repeat(200); + let str; + switch (input) { + case 'onebyte': + str = base; + break; + case 'twobyte': + // Slicing a two-byte string keeps the two-byte representation. + str = ('\u0100' + base).slice(1); + break; + case 'invalid': + str = base + '\u0100'; + break; + } + const expected = input !== 'invalid'; + const fn = methods[method]; + assert.strictEqual(fn(str), expected); + + bench.start(); + let result; + for (let i = 0; i < n; ++i) { + result = fn(str); + } + bench.end(n); + assert.strictEqual(result, expected); +} diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 7a574e3c4dd..d7d7b89540e 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5414,6 +5414,46 @@ including the case in which `input` is empty. A detached `ArrayBuffer`, or a `TypedArray` backed by one, is treated as empty. +### `buffer.isByteString(input)` + + + +* `input` {string} The string to validate. +* Returns: {boolean} + +This function returns `true` if `input` is a valid [WebIDL `ByteString`][], +that is, if every UTF-16 code unit of `input` is less than or equal to `0xFF`, +including the case in which `input` is empty. Such a string can be losslessly +encoded using the `'latin1'` encoding. + +Despite its name, a `ByteString` is a JavaScript string, not binary data. +Unlike [`buffer.isAscii()`][] and [`buffer.isUtf8()`][], this function +therefore validates a string rather than a `Buffer`, `TypedArray`, or +`ArrayBuffer`. Every byte sequence would trivially be valid, since every byte +maps to a code unit less than or equal to `0xFF`. + +```mjs +import { isByteString } from 'node:buffer'; + +isByteString('hello'); // true +isByteString('café'); // true +isByteString('\u00ff'); // true +isByteString('\u0100'); // false +isByteString('€'); // false +``` + +```cjs +const { isByteString } = require('node:buffer'); + +isByteString('hello'); // true +isByteString('café'); // true +isByteString('\u00ff'); // true +isByteString('\u0100'); // false +isByteString('€'); // false +``` + ### `buffer.isUtf8(input)`