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
57 changes: 57 additions & 0 deletions benchmark/buffers/buffer-isbytestring.js
Original file line number Diff line number Diff line change
@@ -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);
}
43 changes: 43 additions & 0 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

<!-- YAML
added: REPLACEME
-->

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

<!-- YAML
Expand Down Expand Up @@ -5775,6 +5815,7 @@ or after startup, if the alignment has to hold at run time.
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
[WebIDL `ByteString`]: https://webidl.spec.whatwg.org/#idl-ByteString
[`--build-snapshot`]: cli.md#--build-snapshot
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
Expand Down Expand Up @@ -5814,6 +5855,8 @@ or after startup, if the alignment has to hold at run time.
[`buf.values()`]: #bufvalues
[`buffer.constants.MAX_LENGTH`]: #bufferconstantsmax_length
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
[`buffer.isAscii()`]: #bufferisasciiinput
[`buffer.isUtf8()`]: #bufferisutf8input
[`buffer.kMaxLength`]: #bufferkmaxlength
[`util.inspect()`]: util.md#utilinspectobject-options
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
Expand Down
7 changes: 7 additions & 0 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const {
copy: _copy,
fill: bindingFill,
isAscii: bindingIsAscii,
isByteString: bindingIsByteString,
isUtf8: bindingIsUtf8,
stringLengthUtf8: bindingStringLengthUtf8,
indexOfBuffer,
Expand Down Expand Up @@ -1498,6 +1499,11 @@ function isAscii(input) {
throw new ERR_INVALID_ARG_TYPE('input', ['ArrayBuffer', 'Buffer', 'TypedArray'], input);
}

function isByteString(input) {
validateString(input, 'input');
return bindingIsByteString(input);
}

function stringLength(input, encoding = 'utf8') {
if (!isTypedArray(input) && !isAnyArrayBuffer(input)) {
throw new ERR_INVALID_ARG_TYPE('input', ['ArrayBuffer', 'Buffer', 'TypedArray'], input);
Expand Down Expand Up @@ -1529,6 +1535,7 @@ module.exports = {
transcode,
isUtf8,
isAscii,
isByteString,

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

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

// Returns true if every UTF-16 code unit of the string is <= 0xFF, i.e. the
// string is a valid WebIDL ByteString. ContainsOnlyOneByte() is O(1) for
// strings with a one-byte representation, uses SIMD for flat two-byte
// strings, and traverses cons strings without flattening (no allocation),
// which makes it safe to call from a fast API call.
static void IsByteString(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsString());
args.GetReturnValue().Set(args[0].As<String>()->ContainsOnlyOneByte());
}

static bool FastIsByteString(Local<Value> receiver, Local<Value> value) {
TRACK_V8_FAST_API_CALL("buffer.isByteString");
CHECK(value->IsString());
return value.As<String>()->ContainsOnlyOneByte();
}

static CFunction fast_is_byte_string(CFunction::Make(FastIsByteString));

// 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).
Expand Down Expand Up @@ -1928,6 +1947,8 @@ void Initialize(Local<Object> target,
SetFastMethodNoSideEffect(context, target, "isUtf8", IsUtf8, &fast_is_utf8);
SetFastMethodNoSideEffect(
context, target, "isAscii", IsAscii, &fast_is_ascii);
SetFastMethodNoSideEffect(
context, target, "isByteString", IsByteString, &fast_is_byte_string);
SetFastMethodNoSideEffect(context,
target,
"stringLengthUtf8",
Expand Down Expand Up @@ -2008,6 +2029,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(fast_is_utf8);
registry->Register(IsAscii);
registry->Register(fast_is_ascii);
registry->Register(IsByteString);
registry->Register(fast_is_byte_string);
registry->Register(StringLengthUtf8);
registry->Register(fast_string_length_utf8);

Expand Down
85 changes: 85 additions & 0 deletions test/parallel/test-buffer-isbytestring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

require('../common');
const assert = require('assert');
const { isByteString } = require('buffer');

function reference(str) {
for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 0xFF) return false;
}
return true;
}

// Basic cases.
assert.strictEqual(isByteString(''), true);
assert.strictEqual(isByteString('hello'), true);
assert.strictEqual(isByteString('\x00'), true);
assert.strictEqual(isByteString('\x7f\x80'), true);
assert.strictEqual(isByteString('caf\u00e9'), true);
assert.strictEqual(isByteString('\u00ff'), true);
assert.strictEqual(isByteString('\u0100'), false);
assert.strictEqual(isByteString('\u20ac'), false);
assert.strictEqual(isByteString('\uffff'), false);
// Surrogate pairs and lone surrogates are > 0xFF.
assert.strictEqual(isByteString('\ud83d\ude00'), false);
assert.strictEqual(isByteString('\ud800'), false);
assert.strictEqual(isByteString('\udfff'), false);

// Position of the offending code unit must not matter, and long strings must
// exercise the vectorized paths.
for (const len of [1, 7, 8, 15, 16, 31, 32, 33, 63, 64, 65, 1000, 4099]) {
const base = 'a\u00ff'.repeat(len).slice(0, len);
assert.strictEqual(isByteString(base), true);
for (const pos of [0, len >> 1, len - 1]) {
for (const ch of ['\u0100', '\u1234', '\ud800', '\uffff']) {
const str = base.slice(0, pos) + ch + base.slice(pos + 1);
assert.strictEqual(isByteString(str), false, `len=${len} pos=${pos}`);
}
}
}

// Strings stored with a two-byte representation that only contain code units
// <= 0xFF must still be reported as ByteStrings.
{
const twoByte = '\u0100' + 'abc\u00e9\u00ff'.repeat(100);
const sliced = twoByte.slice(1);
assert.strictEqual(isByteString(twoByte), false);
assert.strictEqual(isByteString(sliced), true);
assert.strictEqual(isByteString(twoByte.substring(1, 20)), true);
}

// Cons strings (results of concatenation) with mixed representations.
{
let cons = '';
for (let i = 0; i < 100; i++) cons += `x${i}\u00e9`;
assert.strictEqual(isByteString(cons), true);
assert.strictEqual(isByteString(cons + '\u0100'), false);
assert.strictEqual(isByteString('\u0100' + cons), false);
assert.strictEqual(isByteString(cons + '\u0100'.slice(1) + cons), true);
}

// Randomized comparison with the reference implementation.
for (let i = 0; i < 1000; i++) {
const len = Math.floor(Math.random() * 100);
const max = Math.random() < 0.5 ? 0x100 : 0x10000;
let str = '';
for (let j = 0; j < len; j++) {
// Keep the probability of producing a code unit > 0xFF low so that
// both outcomes are exercised.
const code = Math.random() < 0.98 ?
Math.floor(Math.random() * 0x100) :
Math.floor(Math.random() * max);
str += String.fromCharCode(code);
}
assert.strictEqual(isByteString(str), reference(str), JSON.stringify(str));
}

// Invalid argument types.
[
undefined, null, 1, 1n, true, {}, [], Symbol('a'),
Buffer.from('a'), new Uint8Array(1), new ArrayBuffer(1),
new String('a'),
].forEach((input) => {
assert.throws(() => isByteString(input), { code: 'ERR_INVALID_ARG_TYPE' });
});
13 changes: 12 additions & 1 deletion test/parallel/test-buffer-isutf8-isascii-fast.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

const common = require('../common');
const assert = require('assert');
const { Buffer, isAscii, isUtf8 } = require('buffer');
const { Buffer, isAscii, isByteString, isUtf8 } = require('buffer');

const ascii = Buffer.from('hello');
const utf8 = Buffer.from('hello \xc4\x9f');
const byteString = 'hello \u00e9';

function testFastIsAscii() {
assert.strictEqual(isAscii(ascii), true);
Expand All @@ -16,6 +17,10 @@ function testFastIsUtf8() {
assert.strictEqual(isUtf8(utf8), true);
}

function testFastIsByteString() {
assert.strictEqual(isByteString(byteString), true);
}

eval('%PrepareFunctionForOptimization(isAscii)');
testFastIsAscii();
eval('%OptimizeFunctionOnNextCall(isAscii)');
Expand All @@ -26,9 +31,15 @@ testFastIsUtf8();
eval('%OptimizeFunctionOnNextCall(isUtf8)');
testFastIsUtf8();

eval('%PrepareFunctionForOptimization(isByteString)');
testFastIsByteString();
eval('%OptimizeFunctionOnNextCall(isByteString)');
testFastIsByteString();

if (common.isDebug) {
const { internalBinding } = require('internal/test/binding');
const { getV8FastApiCallCount } = internalBinding('debug');
assert.strictEqual(getV8FastApiCallCount('buffer.isAscii'), 1);
assert.strictEqual(getV8FastApiCallCount('buffer.isUtf8'), 1);
assert.strictEqual(getV8FastApiCallCount('buffer.isByteString'), 1);
}
Loading