From b1c6e2bb6b7dcec3225557cfa51af8de56beda67 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 16 Sep 2026 14:52:00 +0200 Subject: [PATCH] buffer: add buffer.stringLength() Add `buffer.stringLength(input[, encoding])`, the counterpart of `Buffer.byteLength()`: it returns the number of UTF-16 code units that `buf.toString(encoding)` would produce, without decoding. For UTF-8 the count is computed with simdutf. Invalid input is counted with the same maximal-subpart replacement that the decoder applies, so the result always matches `toString().length`. The other encodings are computed from `byteLength` alone. This lets code that accumulates streamed input check the result against `buffer.constants.MAX_STRING_LENGTH` and size its memory budget before decoding. Refs: https://github.com/nodejs/node/issues/66062 Signed-off-by: Matteo Collina --- benchmark/buffers/buffer-stringlength.js | 28 +++++ doc/api/buffer.md | 53 +++++++++ lib/buffer.js | 29 +++++ src/node_buffer.cc | 95 ++++++++++++++++ test/parallel/test-buffer-stringlength.js | 133 ++++++++++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 benchmark/buffers/buffer-stringlength.js create mode 100644 test/parallel/test-buffer-stringlength.js diff --git a/benchmark/buffers/buffer-stringlength.js b/benchmark/buffers/buffer-stringlength.js new file mode 100644 index 000000000000..df2a8507b285 --- /dev/null +++ b/benchmark/buffers/buffer-stringlength.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common.js'); +const { stringLength } = require('node:buffer'); +const assert = require('node:assert'); + +const bench = common.createBenchmark(main, { + n: [1e6], + encoding: ['utf8', 'latin1', 'base64'], + len: [32, 4096, 1048576], + input: ['ascii', 'multibyte', 'invalid'], +}); + +function main({ n, encoding, len, input }) { + let buf; + if (input === 'ascii') { + buf = Buffer.alloc(len, 'a'); + } else { + buf = Buffer.alloc(len - (len % 3), '€'); + if (input === 'invalid') buf = Buffer.concat([buf, Buffer.from([0xE2, 0x82])]); + } + const expected = buf.toString(encoding).length; + bench.start(); + for (let i = 0; i < n; ++i) { + assert.strictEqual(stringLength(buf, encoding), expected); + } + bench.end(n); +} diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 7d4a70a37877..b04d085a5db1 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5425,6 +5425,58 @@ changes: Resolves a `'blob:nodedata:...'` an associated {Blob} object registered using a prior call to `URL.createObjectURL()`. +### `buffer.stringLength(input[, encoding])` + + + +* `input` {Buffer | ArrayBuffer | TypedArray} The bytes that would be decoded. +* `encoding` {string} The character encoding `input` would be decoded with. + **Default:** `'utf8'`. +* Returns: {integer} + +Returns the length, in UTF-16 code units, of the string that +`buf.toString(encoding)` would produce for the same bytes, without decoding +them. This is the counterpart of [`Buffer.byteLength()`][]. + +For `'utf8'`, invalid byte sequences are counted as they would be decoded: +each maximal invalid subsequence becomes one `U+FFFD` replacement character. +For every other encoding the result is computed from `input.byteLength` alone. + +A detached `ArrayBuffer`, or a `TypedArray` backed by one, is treated as empty. + +The result is not capped: compare it with +[`buffer.constants.MAX_STRING_LENGTH`][] before decoding to know whether the +decode can succeed at all. A string of `n` code units occupies between `n` and +`2 * n` bytes of memory. + +```mjs +import { Buffer, stringLength, constants } from 'node:buffer'; + +const buf = Buffer.from('€ 100', 'utf8'); + +console.log(stringLength(buf)); +// Prints: 5 +console.log(stringLength(buf, 'hex')); +// Prints: 14 +console.log(stringLength(buf) <= constants.MAX_STRING_LENGTH); +// Prints: true +``` + +```cjs +const { Buffer, stringLength, constants } = require('node:buffer'); + +const buf = Buffer.from('€ 100', 'utf8'); + +console.log(stringLength(buf)); +// Prints: 5 +console.log(stringLength(buf, 'hex')); +// Prints: 14 +console.log(stringLength(buf) <= constants.MAX_STRING_LENGTH); +// Prints: true +``` + ### `buffer.transcode(source, fromEnc, toEnc)`