Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add buffer.indexOf, includes and lastIndexOf #1112

Merged
merged 4 commits into from
Aug 20, 2022
Merged
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
191 changes: 159 additions & 32 deletions src/bun.js/bindings/JSBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,33 +231,11 @@ static inline EncodedJSValue constructBufferFromLength(JSGlobalObject* lexicalGl
return jsBufferConstructorFunction_allocUnsafeBody(lexicalGlobalObject, callFrame);
}

static inline JSC::EncodedJSValue constructBufferFromStringAndEncoding(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
static EncodedJSValue constructFromEncoding(JSGlobalObject* lexicalGlobalObject, JSString* str, WebCore::BufferEncodingType encoding)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
uint32_t offset = 0;
uint32_t length = castedThis->length();
WebCore::BufferEncodingType encoding = WebCore::BufferEncodingType::utf8;

auto scope = DECLARE_THROW_SCOPE(vm);

EnsureStillAliveScope arg0 = callFrame->argument(0);
auto* str = arg0.value().toString(lexicalGlobalObject);

EnsureStillAliveScope arg1 = callFrame->argument(1);

if (str->length() == 0)
return constructBufferEmpty(lexicalGlobalObject, callFrame);

if (callFrame->argumentCount() > 1) {
std::optional<BufferEncodingType> encoded = parseEnumeration<BufferEncodingType>(*lexicalGlobalObject, callFrame->argument(1));
if (!encoded) {
throwTypeError(lexicalGlobalObject, scope, "Invalid encoding"_s);
return JSC::JSValue::encode(jsUndefined());
}

encoding = encoded.value();
}

auto view = str->tryGetValue(lexicalGlobalObject);
JSC::EncodedJSValue result;

Expand Down Expand Up @@ -339,6 +317,37 @@ static inline JSC::EncodedJSValue constructBufferFromStringAndEncoding(JSC::JSGl
scope.throwException(lexicalGlobalObject, decoded);
return JSC::JSValue::encode(jsUndefined());
}
return result;
}

static inline JSC::EncodedJSValue constructBufferFromStringAndEncoding(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
uint32_t offset = 0;
uint32_t length = castedThis->length();
WebCore::BufferEncodingType encoding = WebCore::BufferEncodingType::utf8;

auto scope = DECLARE_THROW_SCOPE(vm);

EnsureStillAliveScope arg0 = callFrame->argument(0);
auto* str = arg0.value().toString(lexicalGlobalObject);

EnsureStillAliveScope arg1 = callFrame->argument(1);

if (str->length() == 0)
return constructBufferEmpty(lexicalGlobalObject, callFrame);

if (callFrame->argumentCount() > 1) {
std::optional<BufferEncodingType> encoded = parseEnumeration<BufferEncodingType>(*lexicalGlobalObject, callFrame->argument(1));
if (!encoded) {
throwTypeError(lexicalGlobalObject, scope, "Invalid encoding"_s);
return JSC::JSValue::encode(jsUndefined());
}

encoding = encoded.value();
}

JSC::EncodedJSValue result = constructFromEncoding(lexicalGlobalObject, str, encoding);

RELEASE_AND_RETURN(scope, result);
}
Expand Down Expand Up @@ -995,20 +1004,138 @@ static inline JSC::EncodedJSValue jsBufferPrototypeFunction_fillBody(JSC::JSGlob
}
}

static inline JSC::EncodedJSValue jsBufferPrototypeFunction_includesBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
static int64_t indexOf(const uint8_t* thisPtr, int64_t thisLength, const uint8_t* valuePtr, int64_t valueLength, int64_t byteOffset)
{
if (thisLength < valueLength + byteOffset)
return -1;
auto start = thisPtr + byteOffset;
auto it = static_cast<uint8_t*>(memmem(start, static_cast<size_t>(thisLength - byteOffset), valuePtr, static_cast<size_t>(valueLength)));
if (it != NULL) {
return it - thisPtr;
}
return -1;
}

static int64_t lastIndexOf(const uint8_t* thisPtr, int64_t thisLength, const uint8_t* valuePtr, int64_t valueLength, int64_t byteOffset)
{
auto start = thisPtr;
auto end = thisPtr + std::min(thisLength, byteOffset + valueLength);
auto it = std::find_end(start, end, valuePtr, valuePtr + valueLength);
Jarred-Sumner marked this conversation as resolved.
Show resolved Hide resolved
if (it != end) {
return it - thisPtr;
}
return -1;
}

static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis, bool last)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
return JSC::JSValue::encode(jsUndefined());
auto scope = DECLARE_THROW_SCOPE(vm);
if (callFrame->argumentCount() < 1) {
throwVMError(lexicalGlobalObject, scope, createNotEnoughArgumentsError(lexicalGlobalObject));
return JSValue::encode(jsUndefined());
}

auto value = callFrame->uncheckedArgument(0);
WebCore::BufferEncodingType encoding = WebCore::BufferEncodingType::utf8;

int64_t length = static_cast<int64_t>(castedThis->byteLength());
const uint8_t* typedVector = castedThis->typedVector();

int64_t byteOffset = last ? length - 1 : 0;

if (callFrame->argumentCount() > 1) {
auto byteOffset_ = callFrame->uncheckedArgument(1).toNumber(lexicalGlobalObject);
Jarred-Sumner marked this conversation as resolved.
Show resolved Hide resolved
if (std::isnan(byteOffset_) || std::isinf(byteOffset_)) {
byteOffset = last ? length - 1 : 0;
} else if (byteOffset_ < 0) {
byteOffset = length + static_cast<int64_t>(byteOffset_);
} else {
byteOffset = static_cast<int64_t>(byteOffset_);
}

if (last) {
if (byteOffset < 0) {
return -1;
} else if (byteOffset > length - 1) {
byteOffset = length - 1;
}
} else {
if (byteOffset <= 0) {
byteOffset = 0;
} else if (byteOffset > length - 1) {
return -1;
}
}

if (callFrame->argumentCount() > 2) {
std::optional<BufferEncodingType> encoded = parseEnumeration<BufferEncodingType>(*lexicalGlobalObject, callFrame->uncheckedArgument(2));
if (!encoded) {
throwTypeError(lexicalGlobalObject, scope, "Invalid encoding"_s);
return JSC::JSValue::encode(jsUndefined());
}

encoding = encoded.value();
}
}

if (value.isString()) {
auto* str = value.toString(lexicalGlobalObject);
JSC::EncodedJSValue encodedBuffer = constructFromEncoding(lexicalGlobalObject, str, encoding);
Jarred-Sumner marked this conversation as resolved.
Show resolved Hide resolved
auto* arrayValue = JSC::jsDynamicCast<JSC::JSUint8Array*>(JSC::JSValue::decode(encodedBuffer));
int64_t lengthValue = static_cast<int64_t>(arrayValue->byteLength());
const uint8_t* typedVectorValue = arrayValue->typedVector();
if (last) {
return lastIndexOf(typedVector, length, typedVectorValue, lengthValue, byteOffset);
} else {
return indexOf(typedVector, length, typedVectorValue, lengthValue, byteOffset);
}
} else if (value.isNumber()) {
uint8_t byteValue = static_cast<uint8_t>(value.toNumber(lexicalGlobalObject));
if (last) {
for (int64_t i = byteOffset; i >= 0; --i) {
if (byteValue == typedVector[i]) {
return i;
}
}
} else {
for (int64_t i = byteOffset; i < length; ++i) {
if (byteValue == typedVector[i]) {
return i;
}
}
}
return -1;
} else if (auto* arrayValue = JSC::jsDynamicCast<JSC::JSUint8Array*>(value)) {
size_t lengthValue = arrayValue->byteLength();
const uint8_t* typedVectorValue = arrayValue->typedVector();
if (last) {
return lastIndexOf(typedVector, length, typedVectorValue, lengthValue, byteOffset);
} else {
return indexOf(typedVector, length, typedVectorValue, lengthValue, byteOffset);
}
} else {
throwTypeError(lexicalGlobalObject, scope, "Invalid value type"_s);
return -1;
}

return -1;
}

static inline JSC::EncodedJSValue jsBufferPrototypeFunction_includesBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
{
auto index = indexOf(lexicalGlobalObject, callFrame, castedThis, false);
return JSC::JSValue::encode(jsBoolean(index != -1));
}
static inline JSC::EncodedJSValue jsBufferPrototypeFunction_indexOfBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
return JSC::JSValue::encode(jsUndefined());
auto index = indexOf(lexicalGlobalObject, callFrame, castedThis, false);
return JSC::JSValue::encode(jsNumber(index));
}
static inline JSC::EncodedJSValue jsBufferPrototypeFunction_lastIndexOfBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
return JSC::JSValue::encode(jsUndefined());
auto index = indexOf(lexicalGlobalObject, callFrame, castedThis, true);
return JSC::JSValue::encode(jsNumber(index));
}
static inline JSC::EncodedJSValue jsBufferPrototypeFunction_swap16Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBuffer>::ClassParameter castedThis)
{
Expand Down Expand Up @@ -1447,9 +1574,9 @@ static const HashTableValue JSBufferPrototypeTableValues[]
{ "fill"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_fill), (intptr_t)(4) } },
{ "hexSlice"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { (intptr_t) static_cast<BuiltinGenerator>(jsBufferPrototypeHexSliceCodeGenerator), (intptr_t)(2) } },
{ "hexWrite"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { (intptr_t) static_cast<BuiltinGenerator>(jsBufferPrototypeHexWriteCodeGenerator), (intptr_t)(1) } },
// { "includes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_includes), (intptr_t)(3) } },
// { "indexOf"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_indexOf), (intptr_t)(3) } },
// { "lastIndexOf"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_lastIndexOf), (intptr_t)(3) } },
{ "includes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_includes), (intptr_t)(3) } },
{ "indexOf"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_indexOf), (intptr_t)(3) } },
{ "lastIndexOf"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { (intptr_t) static_cast<RawNativeFunction>(jsBufferPrototypeFunction_lastIndexOf), (intptr_t)(3) } },
{ "latin1Slice"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { (intptr_t) static_cast<BuiltinGenerator>(jsBufferPrototypeLatin1SliceCodeGenerator), (intptr_t)(2) } },
{ "latin1Write"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { (intptr_t) static_cast<BuiltinGenerator>(jsBufferPrototypeLatin1WriteCodeGenerator), (intptr_t)(1) } },
{ "readBigInt64"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { (intptr_t) static_cast<BuiltinGenerator>(jsBufferPrototypeReadBigInt64LECodeGenerator), (intptr_t)(1) } },
Expand Down
78 changes: 78 additions & 0 deletions test/bun.js/buffer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,81 @@ it("read", () => {
expect(buf.readUInt8(0)).toBe(255);
reset();
});

it("includes", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for writing tests!!

const buf = Buffer.from('this is a buffer');

expect(buf.includes('this')).toBe(true);
expect(buf.includes('is')).toBe(true);
expect(buf.includes(Buffer.from('a buffer'))).toBe(true);
expect(buf.includes(97)).toBe(true);
expect(buf.includes(Buffer.from('a buffer example'))).toBe(false);
expect(buf.includes(Buffer.from('a buffer example').slice(0, 8))).toBe(true);
expect(buf.includes('this', 4)).toBe(false);
});

it("indexOf", () => {
const buf = Buffer.from('this is a buffer');

expect(buf.indexOf('this')).toBe(0);
expect(buf.indexOf('is')).toBe(2);
expect(buf.indexOf(Buffer.from('a buffer'))).toBe(8);
expect(buf.indexOf(97)).toBe(8);
expect(buf.indexOf(Buffer.from('a buffer example'))).toBe(-1);
expect(buf.indexOf(Buffer.from('a buffer example').slice(0, 8))).toBe(8);

const utf16Buffer = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'utf16le');

expect(utf16Buffer.indexOf('\u03a3', 0, 'utf16le')).toBe(4);
expect(utf16Buffer.indexOf('\u03a3', -4, 'utf16le')).toBe(6);

const b = Buffer.from('abcdef');

// Passing a value that's a number, but not a valid byte.
// Prints: 2, equivalent to searching for 99 or 'c'.
expect(b.indexOf(99.9)).toBe(2);
expect(b.indexOf(256 + 99)).toBe(2);

// Passing a byteOffset that coerces to NaN or 0.
// Prints: 1, searching the whole buffer.
expect(b.indexOf('b', undefined)).toBe(1);
expect(b.indexOf('b', {})).toBe(1);
expect(b.indexOf('b', null)).toBe(1);
expect(b.indexOf('b', [])).toBe(1);
});

it("lastIndexOf", () => {
const buf = Buffer.from('this buffer is a buffer');

expect(buf.lastIndexOf('this')).toBe(0);
expect(buf.lastIndexOf('this', 0)).toBe(0);
expect(buf.lastIndexOf('this', -1000)).toBe(-1);
expect(buf.lastIndexOf('buffer')).toBe(17);
expect(buf.lastIndexOf(Buffer.from('buffer'))).toBe(17);
expect(buf.lastIndexOf(97)).toBe(15);
expect(buf.lastIndexOf(Buffer.from('yolo'))).toBe(-1);
expect(buf.lastIndexOf('buffer', 5)).toBe(5);
expect(buf.lastIndexOf('buffer', 4)).toBe(-1);

const utf16Buffer = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'utf16le');

expect(utf16Buffer.lastIndexOf('\u03a3', undefined, 'utf16le')).toBe(6);
expect(utf16Buffer.lastIndexOf('\u03a3', -5, 'utf16le')).toBe(4);

const b = Buffer.from('abcdef');

// Passing a value that's a number, but not a valid byte.
// Prints: 2, equivalent to searching for 99 or 'c'.
expect(b.lastIndexOf(99.9)).toBe(2);
expect(b.lastIndexOf(256 + 99)).toBe(2);

// Passing a byteOffset that coerces to NaN or 0.
// Prints: 1, searching the whole buffer.
expect(b.lastIndexOf('b', undefined)).toBe(1);
expect(b.lastIndexOf('b', {})).toBe(1);

// Passing a byteOffset that coerces to 0.
// Prints: -1, equivalent to passing 0.
expect(b.lastIndexOf('b', null)).toBe(-1);
expect(b.lastIndexOf('b', [])).toBe(-1);
});