Skip to content

Commit f622fff

Browse files
committed
buffer: add end parameter
To limit how far into the buffer we search without allocating an unnecessary subarray.
1 parent 35fed19 commit f622fff

File tree

4 files changed

+107
-29
lines changed

4 files changed

+107
-29
lines changed

doc/api/buffer.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,11 +2069,14 @@ console.log(buf.fill('zz', 'hex'));
20692069
// Throws an exception.
20702070
```
20712071

2072-
### `buf.includes(value[, byteOffset][, encoding])`
2072+
### `buf.includes(value[, byteOffset[, end]][, encoding])`
20732073

20742074
<!-- YAML
20752075
added: v5.3.0
20762076
changes:
2077+
- version: REPLACEME
2078+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
2079+
description: Added the `end` parameter.
20772080
- version:
20782081
- v25.5.0
20792082
- v24.13.1
@@ -2084,6 +2087,8 @@ changes:
20842087
* `value` {string|Buffer|Uint8Array|integer} What to search for.
20852088
* `byteOffset` {integer} Where to begin searching in `buf`. If negative, then
20862089
offset is calculated from the end of `buf`. **Default:** `0`.
2090+
* `end` {integer} Where to stop searching in `buf` (exclusive). **Default:**
2091+
`buf.length`.
20872092
* `encoding` {string} If `value` is a string, this is its encoding.
20882093
**Default:** `'utf8'`.
20892094
* Returns: {boolean} `true` if `value` was found in `buf`, `false` otherwise.
@@ -2132,11 +2137,14 @@ console.log(buf.includes('this', 4));
21322137
// Prints: false
21332138
```
21342139

2135-
### `buf.indexOf(value[, byteOffset][, encoding])`
2140+
### `buf.indexOf(value[, start[, end]][, encoding])`
21362141

21372142
<!-- YAML
21382143
added: v1.5.0
21392144
changes:
2145+
- version: REPLACEME
2146+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
2147+
description: Added the `end` parameter.
21402148
- version: v8.0.0
21412149
pr-url: https://github.com/nodejs/node/pull/10236
21422150
description: The `value` can now be a `Uint8Array`.
@@ -2149,8 +2157,10 @@ changes:
21492157
-->
21502158

21512159
* `value` {string|Buffer|Uint8Array|integer} What to search for.
2152-
* `byteOffset` {integer} Where to begin searching in `buf`. If negative, then
2160+
* `start` {integer} Where to begin searching in `buf`. If negative, then
21532161
offset is calculated from the end of `buf`. **Default:** `0`.
2162+
* `end` {integer} Where to stop searching in `buf` (exclusive). **Default:**
2163+
`buf.length`.
21542164
* `encoding` {string} If `value` is a string, this is the encoding used to
21552165
determine the binary representation of the string that will be searched for in
21562166
`buf`. **Default:** `'utf8'`.
@@ -2310,20 +2320,25 @@ for (const key of buf.keys()) {
23102320
// 5
23112321
```
23122322

2313-
### `buf.lastIndexOf(value[, byteOffset][, encoding])`
2323+
### `buf.lastIndexOf(value[, start[, end]][, encoding])`
23142324

23152325
<!-- YAML
23162326
added: v6.0.0
23172327
changes:
2328+
- version: REPLACEME
2329+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
2330+
description: Added the `end` parameter.
23182331
- version: v8.0.0
23192332
pr-url: https://github.com/nodejs/node/pull/10236
23202333
description: The `value` can now be a `Uint8Array`.
23212334
-->
23222335

23232336
* `value` {string|Buffer|Uint8Array|integer} What to search for.
2324-
* `byteOffset` {integer} Where to begin searching in `buf`. If negative, then
2337+
* `start` {integer} Where to begin searching in `buf`. If negative, then
23252338
offset is calculated from the end of `buf`. **Default:**
23262339
`buf.length - 1`.
2340+
* `end` {integer} Where to stop searching in `buf` (exclusive). **Default:**
2341+
`buf.length`.
23272342
* `encoding` {string} If `value` is a string, this is the encoding used to
23282343
determine the binary representation of the string that will be searched for in
23292344
`buf`. **Default:** `'utf8'`.

lib/buffer.js

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -731,51 +731,55 @@ const encodingOps = {
731731
byteLength: (string) => string.length,
732732
write: asciiWrite,
733733
slice: asciiSlice,
734-
indexOf: (buf, val, byteOffset, dir) =>
734+
indexOf: (buf, val, byteOffset, dir, byteLength = buf.byteLength - byteOffset) =>
735735
indexOfBuffer(buf,
736736
fromStringFast(val, encodingOps.ascii),
737737
byteOffset,
738738
encodingsMap.ascii,
739-
dir),
739+
dir,
740+
byteLength),
740741
},
741742
base64: {
742743
encoding: 'base64',
743744
encodingVal: encodingsMap.base64,
744745
byteLength: (string) => base64ByteLength(string, string.length),
745746
write: base64Write,
746747
slice: base64Slice,
747-
indexOf: (buf, val, byteOffset, dir) =>
748+
indexOf: (buf, val, byteOffset, dir, byteLength = buf.byteLength - byteOffset) =>
748749
indexOfBuffer(buf,
749750
fromStringFast(val, encodingOps.base64),
750751
byteOffset,
751752
encodingsMap.base64,
752-
dir),
753+
dir,
754+
byteLength),
753755
},
754756
base64url: {
755757
encoding: 'base64url',
756758
encodingVal: encodingsMap.base64url,
757759
byteLength: (string) => base64ByteLength(string, string.length),
758760
write: base64urlWrite,
759761
slice: base64urlSlice,
760-
indexOf: (buf, val, byteOffset, dir) =>
762+
indexOf: (buf, val, byteOffset, dir, byteLength = buf.byteLength - byteOffset) =>
761763
indexOfBuffer(buf,
762764
fromStringFast(val, encodingOps.base64url),
763765
byteOffset,
764766
encodingsMap.base64url,
765-
dir),
767+
dir,
768+
byteLength),
766769
},
767770
hex: {
768771
encoding: 'hex',
769772
encodingVal: encodingsMap.hex,
770773
byteLength: (string) => string.length >>> 1,
771774
write: hexWrite,
772775
slice: hexSlice,
773-
indexOf: (buf, val, byteOffset, dir) =>
776+
indexOf: (buf, val, byteOffset, dir, byteLength = buf.byteLength - byteOffset) =>
774777
indexOfBuffer(buf,
775778
fromStringFast(val, encodingOps.hex),
776779
byteOffset,
777780
encodingsMap.hex,
778-
dir),
781+
dir,
782+
byteLength),
779783
},
780784
};
781785
function getEncodingOps(encoding) {
@@ -1031,7 +1035,7 @@ Buffer.prototype.compare = function compare(target,
10311035
// - byteOffset - an index into `buffer`; will be clamped to an int32
10321036
// - encoding - an optional encoding, relevant if val is a string
10331037
// - dir - true for indexOf, false for lastIndexOf
1034-
function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) {
1038+
function bidirectionalIndexOf(buffer, val, byteOffset, byteLength, encoding, dir) {
10351039
validateBuffer(buffer);
10361040

10371041
if (typeof byteOffset === 'string') {
@@ -1051,7 +1055,7 @@ function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) {
10511055
dir = !!dir; // Cast to bool.
10521056

10531057
if (typeof val === 'number')
1054-
return indexOfNumber(buffer, val >>> 0, byteOffset, dir);
1058+
return indexOfNumber(buffer, val >>> 0, byteOffset, dir, byteLength);
10551059

10561060
let ops;
10571061
if (encoding === undefined)
@@ -1062,30 +1066,42 @@ function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) {
10621066
if (typeof val === 'string') {
10631067
if (ops === undefined)
10641068
throw new ERR_UNKNOWN_ENCODING(encoding);
1065-
return ops.indexOf(buffer, val, byteOffset, dir);
1069+
return ops.indexOf(buffer, val, byteOffset, dir, byteLength);
10661070
}
10671071

10681072
if (isUint8Array(val)) {
10691073
const encodingVal =
10701074
(ops === undefined ? encodingsMap.utf8 : ops.encodingVal);
1071-
return indexOfBuffer(buffer, val, byteOffset, encodingVal, dir);
1075+
return indexOfBuffer(buffer, val, byteOffset, encodingVal, dir, byteLength);
10721076
}
10731077

10741078
throw new ERR_INVALID_ARG_TYPE(
10751079
'value', ['number', 'string', 'Buffer', 'Uint8Array'], val,
10761080
);
10771081
}
10781082

1079-
Buffer.prototype.indexOf = function indexOf(val, byteOffset, encoding) {
1080-
return bidirectionalIndexOf(this, val, byteOffset, encoding, true);
1083+
Buffer.prototype.indexOf = function indexOf(val, offset, end, encoding) {
1084+
if (typeof end === 'string') {
1085+
encoding = end;
1086+
end = val.byteLength;
1087+
}
1088+
return bidirectionalIndexOf(this, val, offset, end - offset, encoding, true);
10811089
};
10821090

1083-
Buffer.prototype.lastIndexOf = function lastIndexOf(val, byteOffset, encoding) {
1084-
return bidirectionalIndexOf(this, val, byteOffset, encoding, false);
1091+
Buffer.prototype.lastIndexOf = function lastIndexOf(val, offset, byteLength, encoding) {
1092+
if (typeof end === 'string') {
1093+
encoding = end;
1094+
end = val.byteLength;
1095+
}
1096+
return bidirectionalIndexOf(this, val, offset, end - offset, encoding, false);
10851097
};
10861098

1087-
Buffer.prototype.includes = function includes(val, byteOffset, encoding) {
1088-
return bidirectionalIndexOf(this, val, byteOffset, encoding, true) !== -1;
1099+
Buffer.prototype.includes = function includes(val, offset, end, encoding) {
1100+
if (typeof end === 'string') {
1101+
encoding = end;
1102+
end = val.byteLength;
1103+
}
1104+
return bidirectionalIndexOf(this, val, offset, end - offset, encoding, true) !== -1;
10891105
};
10901106

10911107
// Usage:

src/node_buffer.cc

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ void IndexOfBuffer(const FunctionCallbackInfo<Value>& args) {
10791079
CHECK(args[2]->IsNumber());
10801080
CHECK(args[3]->IsInt32());
10811081
CHECK(args[4]->IsBoolean());
1082+
CHECK(args[5]->IsNumber());
10821083

10831084
enum encoding enc = static_cast<enum encoding>(args[3].As<Int32>()->Value());
10841085

@@ -1089,13 +1090,14 @@ void IndexOfBuffer(const FunctionCallbackInfo<Value>& args) {
10891090
ArrayBufferViewContents<char> needle_contents(args[1]);
10901091
int64_t offset_i64 = args[2].As<Integer>()->Value();
10911092
bool is_forward = args[4]->IsTrue();
1093+
int64_t end_i64 = args[5].As<Integer>()->Value();
10921094

10931095
const char* haystack = haystack_contents.data();
10941096
const size_t haystack_length = haystack_contents.length();
10951097
const char* needle = needle_contents.data();
10961098
const size_t needle_length = needle_contents.length();
10971099

1098-
int64_t opt_offset = IndexOfOffset(haystack_length,
1100+
int64_t opt_offset = IndexOfOffset(std::min(end_i64 - offset_i64, haystack_length),
10991101
offset_i64,
11001102
needle_length,
11011103
is_forward);
@@ -1149,6 +1151,7 @@ void IndexOfBuffer(const FunctionCallbackInfo<Value>& args) {
11491151
int32_t IndexOfNumberImpl(Local<Value> buffer_obj,
11501152
const uint32_t needle,
11511153
const int64_t offset_i64,
1154+
const int64_t end_i64,
11521155
const bool is_forward) {
11531156
ArrayBufferViewContents<uint8_t> buffer(buffer_obj);
11541157
const uint8_t* buffer_data = buffer.data();
@@ -1158,13 +1161,14 @@ int32_t IndexOfNumberImpl(Local<Value> buffer_obj,
11581161
return -1;
11591162
}
11601163
size_t offset = static_cast<size_t>(opt_offset);
1161-
CHECK_LT(offset, buffer_length);
1164+
size_t length = std::min(static_cast<size_t>(end_i64), offset + buffer_length);
1165+
CHECK_LT(offset, length);
11621166

11631167
const void* ptr;
11641168
if (is_forward) {
1165-
ptr = memchr(buffer_data + offset, needle, buffer_length - offset);
1169+
ptr = memchr(buffer_data + offset, needle, length - offset);
11661170
} else {
1167-
ptr = nbytes::stringsearch::MemrchrFill(buffer_data, needle, offset + 1);
1171+
ptr = nbytes::stringsearch::MemrchrFill(buffer_data, needle, length);
11681172
}
11691173
const uint8_t* ptr_uint8 = static_cast<const uint8_t*>(ptr);
11701174
return ptr != nullptr ? static_cast<int32_t>(ptr_uint8 - buffer_data) : -1;
@@ -1174,27 +1178,30 @@ void SlowIndexOfNumber(const FunctionCallbackInfo<Value>& args) {
11741178
CHECK(args[1]->IsUint32());
11751179
CHECK(args[2]->IsNumber());
11761180
CHECK(args[3]->IsBoolean());
1181+
CHECK(args[4]->IsUint32());
11771182

11781183
THROW_AND_RETURN_UNLESS_BUFFER(Environment::GetCurrent(args), args[0]);
11791184

11801185
Local<Value> buffer_obj = args[0];
11811186
uint32_t needle = args[1].As<Uint32>()->Value();
11821187
int64_t offset_i64 = args[2].As<Integer>()->Value();
11831188
bool is_forward = args[3]->IsTrue();
1189+
int64_t end_int64 = args[4].As<Integer>()->Value();
11841190

11851191
args.GetReturnValue().Set(
1186-
IndexOfNumberImpl(buffer_obj, needle, offset_i64, is_forward));
1192+
IndexOfNumberImpl(buffer_obj, needle, offset_i64, end_int64, is_forward));
11871193
}
11881194

11891195
int32_t FastIndexOfNumber(Local<Value>,
11901196
Local<Value> buffer_obj,
11911197
uint32_t needle,
11921198
int64_t offset_i64,
1199+
int64_t end_i64,
11931200
bool is_forward,
11941201
// NOLINTNEXTLINE(runtime/references)
11951202
FastApiCallbackOptions& options) {
11961203
HandleScope scope(options.isolate);
1197-
return IndexOfNumberImpl(buffer_obj, needle, offset_i64, is_forward);
1204+
return IndexOfNumberImpl(buffer_obj, needle, offset_i64, end_i64, is_forward);
11981205
}
11991206

12001207
static CFunction fast_index_of_number(CFunction::Make(FastIndexOfNumber));

test/parallel/test-buffer-indexof.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,3 +633,43 @@ assert.strictEqual(reallyLong.lastIndexOf(pattern), 0);
633633
'Received an instance of lastIndexOf'
634634
});
635635
}
636+
637+
// Test the new end parameter for indexOf, lastIndexOf, and includes
638+
{
639+
const buf = Buffer.from('abcabc');
640+
641+
// indexOf with end parameter limits the search range
642+
assert.strictEqual(buf.indexOf('c', 0, 3), 2);
643+
assert.strictEqual(buf.indexOf('c', 0, 2), -1); // 'c' is at index 2, excluded when end=2
644+
assert.strictEqual(buf.indexOf('a', 1, 4), 3);
645+
assert.strictEqual(buf.indexOf('a', 0, 1), 0);
646+
assert.strictEqual(buf.indexOf('a', 0, 0), -1); // empty range
647+
assert.strictEqual(buf.indexOf('abc', 0, 3), 0);
648+
assert.strictEqual(buf.indexOf('abc', 0, 2), -1); // not enough bytes in range
649+
650+
// indexOf with end parameter and Buffer value
651+
assert.strictEqual(buf.indexOf(Buffer.from('bc'), 0, 3), 1);
652+
assert.strictEqual(buf.indexOf(Buffer.from('bc'), 0, 2), -1);
653+
654+
// indexOf with end parameter and encoding (end is a string → encoding)
655+
assert.strictEqual(buf.indexOf('a', 0, 'utf8'), 0);
656+
assert.strictEqual(buf.indexOf('abc', 0, 'utf8'), 0);
657+
658+
// lastIndexOf with end parameter limits the search range
659+
assert.strictEqual(buf.lastIndexOf('a', 5, 4), 3);
660+
assert.strictEqual(buf.lastIndexOf('a', 5, 3), 0); // 'a' at index 3 excluded, finds index 0
661+
assert.strictEqual(buf.lastIndexOf('c', 5, 3), 2);
662+
assert.strictEqual(buf.lastIndexOf('c', 5, buf.length), 5);
663+
664+
// lastIndexOf with end as encoding string
665+
assert.strictEqual(buf.lastIndexOf('a', 5, 'utf8'), 3);
666+
667+
// includes with end parameter
668+
assert.strictEqual(buf.includes('c', 0, 3), true);
669+
assert.strictEqual(buf.includes('c', 0, 2), false);
670+
assert.strictEqual(buf.includes('abc', 0, 3), true);
671+
assert.strictEqual(buf.includes('abc', 0, 2), false);
672+
673+
// includes with end as encoding string
674+
assert.strictEqual(buf.includes('a', 0, 'utf8'), true);
675+
}

0 commit comments

Comments
 (0)