Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions benchmark/buffers/buffer-stringlength.js
Original file line number Diff line number Diff line change
@@ -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);
}
53 changes: 53 additions & 0 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])`

<!-- YAML
added: REPLACEME
-->

* `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)`

<!-- YAML
Expand Down Expand Up @@ -5715,6 +5767,7 @@ or after startup, if the alignment has to hold at run time.
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
[`Buffer.byteLength()`]: #static-method-bufferbytelengthstring-encoding
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
[`Buffer.from(array)`]: #static-method-bufferfromarray
Expand Down
29 changes: 29 additions & 0 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
ArrayBufferIsView,
ArrayIsArray,
ArrayPrototypeForEach,
MathCeil,
MathFloor,
MathMin,
MathTrunc,
Expand Down Expand Up @@ -62,6 +63,7 @@ const {
fill: bindingFill,
isAscii: bindingIsAscii,
isUtf8: bindingIsUtf8,
stringLengthUtf8: bindingStringLengthUtf8,
indexOfBuffer,
indexOfNumber,
indexOfString,
Expand Down Expand Up @@ -1494,11 +1496,38 @@ function isAscii(input) {
throw new ERR_INVALID_ARG_TYPE('input', ['ArrayBuffer', 'Buffer', 'TypedArray'], input);
}

function stringLength(input, encoding = 'utf8') {
if (!isTypedArray(input) && !isAnyArrayBuffer(input)) {
throw new ERR_INVALID_ARG_TYPE('input', ['ArrayBuffer', 'Buffer', 'TypedArray'], input);
}
validateString(encoding, 'encoding');
const ops = getEncodingOps(encoding);
if (ops === undefined) {
throw new ERR_UNKNOWN_ENCODING(encoding);
}
const length = input.byteLength;
switch (ops.encodingVal) {
case encodingsMap.utf8:
return length === 0 ? 0 : bindingStringLengthUtf8(input);
case encodingsMap.utf16le:
return MathFloor(length / 2);
case encodingsMap.hex:
return length * 2;
case encodingsMap.base64:
return MathCeil(length / 3) * 4;
case encodingsMap.base64url:
return MathCeil(length * 4 / 3);
default: // latin1, ascii
return length;
}
}

module.exports = {
Buffer,
transcode,
isUtf8,
isAscii,
stringLength,

// Legacy
kMaxLength,
Expand Down
95 changes: 95 additions & 0 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,94 @@ static bool FastIsAscii(Local<Value> receiver,

static CFunction fast_is_ascii(CFunction::Make(FastIsAscii));

// Number of UTF-16 code units produced by decoding [p, end) as UTF-8 with
// WHATWG "maximal subpart" U+FFFD replacement, matching the fallback that
// StringBytes::Encode takes for invalid input (v8::String::NewFromUtf8).
static size_t Utf16LengthFromInvalidUtf8(const uint8_t* p, const uint8_t* end) {
size_t units = 0;
while (p < end) {
const uint8_t lead = *p;
if (lead < 0x80) {
p++;
units++;
continue;
}
size_t len;
uint8_t lo = 0x80;
uint8_t hi = 0xBF;
if (lead >= 0xC2 && lead <= 0xDF) {
len = 2;
} else if (lead >= 0xE0 && lead <= 0xEF) {
len = 3;
if (lead == 0xE0) lo = 0xA0;
if (lead == 0xED) hi = 0x9F;
} else if (lead >= 0xF0 && lead <= 0xF4) {
len = 4;
if (lead == 0xF0) lo = 0x90;
if (lead == 0xF4) hi = 0x8F;
} else {
// Invalid lead byte: one replacement character.
p++;
units++;
continue;
}
size_t i = 1;
for (; i < len && p + i < end; i++) {
const uint8_t c = p[i];
if (i == 1 ? (c < lo || c > hi) : (c < 0x80 || c > 0xBF)) break;
}
if (i == len) {
p += len;
units += (len == 4) ? 2 : 1;
} else {
// The lead byte plus the valid continuation bytes seen so far form the
// maximal subpart and become one replacement character; the byte that
// failed is decoded again on the next iteration.
p += i;
units++;
}
}
return units;
}

static double StringLengthUtf8Impl(Local<Value> value) {
ArrayBufferViewContents<uint8_t> abv(value);
const uint8_t* data = abv.data();
const size_t length = abv.length();
if (length == 0) return 0;
const simdutf::result r = simdutf::validate_utf8_with_errors(
reinterpret_cast<const char*>(data), length);
if (r.error == simdutf::error_code::SUCCESS) {
return static_cast<double>(simdutf::utf16_length_from_utf8(
reinterpret_cast<const char*>(data), length));
}
// r.count is the offset of the first invalid sequence; everything before it
// is valid UTF-8.
const size_t valid = simdutf::utf16_length_from_utf8(
reinterpret_cast<const char*>(data), r.count);
return static_cast<double>(
valid + Utf16LengthFromInvalidUtf8(data + r.count, data + length));
}

static void StringLengthUtf8(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsTypedArray() || args[0]->IsArrayBuffer() ||
args[0]->IsSharedArrayBuffer());

args.GetReturnValue().Set(StringLengthUtf8Impl(args[0]));
}

static double FastStringLengthUtf8(Local<Value> receiver,
Local<Value> value,
// NOLINTNEXTLINE(runtime/references)
FastApiCallbackOptions& options) {
TRACK_V8_FAST_API_CALL("buffer.stringLengthUtf8");
HandleScope scope(options.isolate);
return StringLengthUtf8Impl(value);
}

static CFunction fast_string_length_utf8(CFunction::Make(FastStringLengthUtf8));

void SetBufferPrototype(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);

Expand Down Expand Up @@ -1839,6 +1927,11 @@ void Initialize(Local<Object> target,
SetFastMethodNoSideEffect(context, target, "isUtf8", IsUtf8, &fast_is_utf8);
SetFastMethodNoSideEffect(
context, target, "isAscii", IsAscii, &fast_is_ascii);
SetFastMethodNoSideEffect(context,
target,
"stringLengthUtf8",
StringLengthUtf8,
&fast_string_length_utf8);

target
->Set(context,
Expand Down Expand Up @@ -1914,6 +2007,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(fast_is_utf8);
registry->Register(IsAscii);
registry->Register(fast_is_ascii);
registry->Register(StringLengthUtf8);
registry->Register(fast_string_length_utf8);

registry->Register(StringSlice<ASCII>);
registry->Register(StringSlice<BASE64>);
Expand Down
Loading
Loading