diff options
-rw-r--r-- | src/bun.js/bindings/JSBufferList.cpp | 122 | ||||
-rw-r--r-- | src/bun.js/bindings/JSBufferList.h | 31 | ||||
-rw-r--r-- | test/bun.js/bufferlist.test.ts | 38 |
3 files changed, 158 insertions, 33 deletions
diff --git a/src/bun.js/bindings/JSBufferList.cpp b/src/bun.js/bindings/JSBufferList.cpp index e54b433e5..37b14143c 100644 --- a/src/bun.js/bindings/JSBufferList.cpp +++ b/src/bun.js/bindings/JSBufferList.cpp @@ -39,17 +39,26 @@ JSC::JSValue JSBufferList::concat(JSC::VM& vm, JSC::JSGlobalObject* lexicalGloba const size_t len = length(); if (len == 0) { // Buffer.alloc(0) - RELEASE_AND_RETURN(throwScope, JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, 0)); + auto array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, 0); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, array); } auto iter = m_deque.begin(); + size_t offset = m_read; if (len == 1) { auto array = JSC::jsDynamicCast<JSC::JSUint8Array*>(iter->get()); if (UNLIKELY(!array)) { return throwTypeError(lexicalGlobalObject, throwScope, "concat can only be called when all buffers are Uint8Array"_s); } - if (UNLIKELY(array->byteLength() > n)) { + const size_t length = array->byteLength() - offset; + if (UNLIKELY(length > n)) { return throwRangeError(lexicalGlobalObject, throwScope, "specified size too small to fit all buffers"_s); } + if (offset > 0) { + auto buffer = array->possiblySharedBuffer(); + array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, offset, len); + RETURN_IF_EXCEPTION(throwScope, {}); + } RELEASE_AND_RETURN(throwScope, array); } // Buffer.allocUnsafe(n >>> 0) @@ -58,6 +67,7 @@ JSC::JSValue JSBufferList::concat(JSC::VM& vm, JSC::JSGlobalObject* lexicalGloba return throwOutOfMemoryError(lexicalGlobalObject, throwScope); } JSC::JSUint8Array* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, WTFMove(arrayBuffer), 0, n); + RETURN_IF_EXCEPTION(throwScope, {}); size_t i = 0; for (const auto end = m_deque.end(); iter != end; ++iter) { @@ -65,19 +75,26 @@ JSC::JSValue JSBufferList::concat(JSC::VM& vm, JSC::JSGlobalObject* lexicalGloba if (UNLIKELY(!array)) { return throwTypeError(lexicalGlobalObject, throwScope, "concat can only be called when all buffers are Uint8Array"_s); } - const size_t length = array->byteLength(); + const size_t length = array->byteLength() - offset; if (UNLIKELY(i + length > n)) { return throwRangeError(lexicalGlobalObject, throwScope, "specified size too small to fit all buffers"_s); } - if (UNLIKELY(!uint8Array->setFromTypedArray(lexicalGlobalObject, i, array, 0, length, JSC::CopyType::Unobservable))) { + if (UNLIKELY(!uint8Array->setFromTypedArray(lexicalGlobalObject, i, array, offset, length, JSC::CopyType::Unobservable))) { return throwOutOfMemoryError(lexicalGlobalObject, throwScope); } + offset = 0; i += length; } RELEASE_AND_RETURN(throwScope, uint8Array); } +inline size_t slicedLength(JSString* str, size_t offset) +{ + const auto len = str->length(); + return len < offset ? 0 : len - offset; +} + JSC::JSValue JSBufferList::join(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSString* seq) { auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -86,9 +103,16 @@ JSC::JSValue JSBufferList::join(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalO } const bool needSeq = seq->length() != 0; const auto end = m_deque.end(); + size_t offset = m_read; JSRopeString::RopeBuilder<RecordOverflow> ropeBuilder(vm); for (auto iter = m_deque.begin();;) { auto str = iter->get().toString(lexicalGlobalObject); + if (offset > 0) { + const size_t len = slicedLength(str, offset); + str = JSC::jsSubstring(lexicalGlobalObject, str, offset, len); + RETURN_IF_EXCEPTION(throwScope, {}); + offset = 0; + } if (!ropeBuilder.append(str)) return throwOutOfMemoryError(lexicalGlobalObject, throwScope); if (++iter == end) @@ -120,16 +144,22 @@ JSC::JSValue JSBufferList::_getString(JSC::VM& vm, JSC::JSGlobalObject* lexicalG if (UNLIKELY(!str)) { return throwTypeError(lexicalGlobalObject, throwScope, "_getString can only be called when all buffers are string"_s); } - const size_t len = str->length(); + const size_t len = slicedLength(str, m_read); size_t n = total; if (n == len) { m_deque.removeFirst(); + if (m_read > 0) { + str = JSC::jsSubstring(lexicalGlobalObject, str, m_read, len); + RETURN_IF_EXCEPTION(throwScope, {}); + m_read = 0; + } RELEASE_AND_RETURN(throwScope, str); } if (n < len) { - JSString* firstHalf = JSC::jsSubstring(lexicalGlobalObject, str, 0, n); - iter->set(vm, this, JSC::jsSubstring(lexicalGlobalObject, str, n, len - n)); + JSString* firstHalf = JSC::jsSubstring(lexicalGlobalObject, str, m_read, n); + RETURN_IF_EXCEPTION(throwScope, {}); + m_read += n; RELEASE_AND_RETURN(throwScope, firstHalf); } @@ -139,18 +169,25 @@ JSC::JSValue JSBufferList::_getString(JSC::VM& vm, JSC::JSGlobalObject* lexicalG if (UNLIKELY(!str)) { return throwTypeError(lexicalGlobalObject, throwScope, "_getString can only be called when all buffers are string"_s); } - const size_t len = str->length(); + const size_t len = slicedLength(str, m_read); if (n < len) { - JSString* firstHalf = JSC::jsSubstring(lexicalGlobalObject, str, 0, n); + JSString* firstHalf = JSC::jsSubstring(lexicalGlobalObject, str, m_read, n); + RETURN_IF_EXCEPTION(throwScope, {}); + m_read += n; if (!ropeBuilder.append(firstHalf)) return throwOutOfMemoryError(lexicalGlobalObject, throwScope); - iter->set(vm, this, JSC::jsSubstring(lexicalGlobalObject, str, n, len - n)); break; } + if (m_read > 0) { + str = JSC::jsSubstring(lexicalGlobalObject, str, m_read, len); + RETURN_IF_EXCEPTION(throwScope, {}); + m_read = 0; + } if (!ropeBuilder.append(str)) return throwOutOfMemoryError(lexicalGlobalObject, throwScope); m_deque.removeFirst(); - if (n == len) break; + if (n == len) + break; n -= len; } RELEASE_AND_RETURN(throwScope, ropeBuilder.release()); @@ -162,7 +199,9 @@ JSC::JSValue JSBufferList::_getBuffer(JSC::VM& vm, JSC::JSGlobalObject* lexicalG auto* subclassStructure = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject)->JSBufferSubclassStructure(); if (total <= 0 || length() == 0) { // Buffer.alloc(0) - RELEASE_AND_RETURN(throwScope, JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, 0)); + JSC::JSUint8Array* array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, 0); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, array); } auto iter = m_deque.begin(); @@ -170,19 +209,25 @@ JSC::JSValue JSBufferList::_getBuffer(JSC::VM& vm, JSC::JSGlobalObject* lexicalG if (UNLIKELY(!array)) { return throwTypeError(lexicalGlobalObject, throwScope, "_getBuffer can only be called when all buffers are Uint8Array"_s); } - const size_t len = array->byteLength(); + const size_t len = array->byteLength() - m_read; size_t n = total; if (n == len) { m_deque.removeFirst(); + if (m_read > 0) { + auto buffer = array->possiblySharedBuffer(); + array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, m_read, len); + RETURN_IF_EXCEPTION(throwScope, {}); + m_read = 0; + } RELEASE_AND_RETURN(throwScope, array); } if (n < len) { auto buffer = array->possiblySharedBuffer(); - JSC::JSUint8Array* retArray = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, 0, n); - JSC::JSUint8Array* newArray = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, n, len - n); - iter->set(vm, this, newArray); - RELEASE_AND_RETURN(throwScope, retArray); + JSC::JSUint8Array* head = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, m_read, n); + RETURN_IF_EXCEPTION(throwScope, {}); + m_read += n; + RELEASE_AND_RETURN(throwScope, head); } // Buffer.allocUnsafe(n >>> 0) @@ -191,33 +236,56 @@ JSC::JSValue JSBufferList::_getBuffer(JSC::VM& vm, JSC::JSGlobalObject* lexicalG return throwOutOfMemoryError(lexicalGlobalObject, throwScope); } JSC::JSUint8Array* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, WTFMove(arrayBuffer), 0, n); + RETURN_IF_EXCEPTION(throwScope, {}); size_t offset = 0; for (const auto end = m_deque.end(); iter != end; ++iter) { JSC::JSUint8Array* array = JSC::jsDynamicCast<JSC::JSUint8Array*>(iter->get()); if (UNLIKELY(!array)) { return throwTypeError(lexicalGlobalObject, throwScope, "_getBuffer can only be called when all buffers are Uint8Array"_s); } - const size_t len = array->byteLength(); + const size_t len = array->byteLength() - m_read; if (n < len) { - if (UNLIKELY(!uint8Array->setFromTypedArray(lexicalGlobalObject, offset, array, 0, n, JSC::CopyType::Unobservable))) { + if (UNLIKELY(!uint8Array->setFromTypedArray(lexicalGlobalObject, offset, array, m_read, n, JSC::CopyType::Unobservable))) { return throwOutOfMemoryError(lexicalGlobalObject, throwScope); } - auto buffer = array->possiblySharedBuffer(); - JSC::JSUint8Array* newArray = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, n, len - n); - iter->set(vm, this, newArray); + m_read += n; break; } - if (UNLIKELY(!uint8Array->setFromTypedArray(lexicalGlobalObject, offset, array, 0, len, JSC::CopyType::Unobservable))) { + if (UNLIKELY(!uint8Array->setFromTypedArray(lexicalGlobalObject, offset, array, m_read, len, JSC::CopyType::Unobservable))) { return throwOutOfMemoryError(lexicalGlobalObject, throwScope); } + m_read = 0; m_deque.removeFirst(); - if (n == len) break; + if (n == len) + break; n -= len; offset += len; } RELEASE_AND_RETURN(throwScope, uint8Array); } +// assumes `length() > 0` and `m_read > 0` +inline JSC::JSValue JSBufferList::trim(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue value) +{ + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSC::JSString* str = JSC::jsDynamicCast<JSC::JSString*>(value); + if (str) { + const size_t len = slicedLength(str, m_read); + str = JSC::jsSubstring(lexicalGlobalObject, str, m_read, len); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, str); + } + JSC::JSUint8Array* array = JSC::jsDynamicCast<JSC::JSUint8Array*>(value); + if (UNLIKELY(!array)) + return throwTypeError(lexicalGlobalObject, throwScope, "expected string or Uint8Array"_s); + auto* subclassStructure = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject)->JSBufferSubclassStructure(); + const size_t len = array->byteLength() - m_read; + auto buffer = array->possiblySharedBuffer(); + JSC::JSUint8Array* head = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, buffer, m_read, len); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, head); +} + const JSC::ClassInfo JSBufferList::s_info = { "BufferList"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBufferList) }; JSC::GCClient::IsoSubspace* JSBufferList::subspaceForImpl(JSC::VM& vm) @@ -266,14 +334,14 @@ static inline JSC::EncodedJSValue jsBufferListPrototypeFunction_unshiftBody(JSC: } auto v = callFrame->uncheckedArgument(0); - castedThis->unshift(vm, v); + castedThis->unshift(vm, lexicalGlobalObject, v); RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(JSC::jsUndefined())); } static inline JSC::EncodedJSValue jsBufferListPrototypeFunction_shiftBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBufferList>::ClassParameter castedThis) { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(castedThis->shift())); + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(castedThis->shift(vm, lexicalGlobalObject))); } static inline JSC::EncodedJSValue jsBufferListPrototypeFunction_clearBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBufferList>::ClassParameter castedThis) { @@ -286,7 +354,7 @@ static inline JSC::EncodedJSValue jsBufferListPrototypeFunction_firstBody(JSC::J { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(castedThis->first())); + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(castedThis->first(vm, lexicalGlobalObject))); } static inline JSC::EncodedJSValue jsBufferListPrototypeFunction_concatBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSBufferList>::ClassParameter castedThis) { diff --git a/src/bun.js/bindings/JSBufferList.h b/src/bun.js/bindings/JSBufferList.h index a9227e981..c34a0ee95 100644 --- a/src/bun.js/bindings/JSBufferList.h +++ b/src/bun.js/bindings/JSBufferList.h @@ -52,28 +52,43 @@ public: m_deque.append(WriteBarrier<Unknown>()); m_deque.last().set(vm, this, v); } - void unshift(JSC::VM& vm, JSC::JSValue v) + void unshift(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue v) { + if (m_read > 0) { + m_deque.first().set(vm, this, trim(vm, lexicalGlobalObject, m_deque.first().get())); + m_read = 0; + } m_deque.prepend(WriteBarrier<Unknown>()); m_deque.first().set(vm, this, v); } - JSC::JSValue shift() + JSC::JSValue shift(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject) { if (UNLIKELY(length() == 0)) return JSC::jsUndefined(); - auto v = m_deque.first().get(); + auto value = m_deque.first().get(); + if (m_read > 0) { + value = trim(vm, lexicalGlobalObject, value); + m_read = 0; + } m_deque.removeFirst(); - return v; + return value; } void clear() { m_deque.clear(); + m_read = 0; } - JSC::JSValue first() + JSC::JSValue first(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject) { if (UNLIKELY(length() == 0)) return JSC::jsUndefined(); - return JSC::JSValue(m_deque.first().get()); + auto value = m_deque.first().get(); + if (m_read > 0) { + value = trim(vm, lexicalGlobalObject, value); + m_deque.first().set(vm, this, value); + m_read = 0; + } + return value; } JSC::JSValue concat(JSC::VM&, JSC::JSGlobalObject*, int32_t); @@ -84,6 +99,9 @@ public: private: Deque<WriteBarrier<Unknown>> m_deque; + size_t m_read = 0; + + JSC::JSValue trim(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue value); }; class JSBufferListPrototype : public JSC::JSNonFinalObject { @@ -134,6 +152,7 @@ public: // Must be defined for each specialization class. static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*); DECLARE_EXPORT_INFO; + private: JSBufferListConstructor(JSC::VM& vm, JSC::Structure* structure, JSC::NativeFunction nativeFunction) : Base(vm, structure, nativeFunction, nativeFunction) diff --git a/test/bun.js/bufferlist.test.ts b/test/bun.js/bufferlist.test.ts index e344d49b4..3aa20845b 100644 --- a/test/bun.js/bufferlist.test.ts +++ b/test/bun.js/bufferlist.test.ts @@ -185,3 +185,41 @@ it("should work with .unshift()", () => { expect(list.shift()).toBe(item2); expect(list.shift()).toBeUndefined(); }); + +it("should work with partial .consume() followed by .first()", () => { + const list = new Readable().readableBuffer; + expect(list.length).toBe(0); + expect(list.push("foo")).toBeUndefined(); + expect(list.push("bar")).toBeUndefined(); + expect(list.length).toBe(2); + expect(list.consume(4, true)).toEqual("foob"); + expect(list.length).toBe(1); + expect(list.first()).toEqual("ar"); + expect(list.length).toBe(1); +}); + +it("should work with partial .consume() followed by .shift()", () => { + const list = new Readable().readableBuffer; + expect(list.length).toBe(0); + expect(list.push(makeUint8Array("foo"))).toBeUndefined(); + expect(list.push(makeUint8Array("bar"))).toBeUndefined(); + expect(list.length).toBe(2); + expect(list.consume(4, false)).toEqual(makeUint8Array("foob")); + expect(list.length).toBe(1); + expect(list.shift()).toEqual(makeUint8Array("ar")); + expect(list.length).toBe(0); +}); + +it("should work with partial .consume() followed by .unshift()", () => { + const list = new Readable().readableBuffer; + expect(list.length).toBe(0); + expect(list.push(makeUint8Array("foo"))).toBeUndefined(); + expect(list.push(makeUint8Array("bar"))).toBeUndefined(); + expect(list.length).toBe(2); + expect(list.consume(4, false)).toEqual(makeUint8Array("foob")); + expect(list.length).toBe(1); + expect(list.unshift(makeUint8Array("baz"))).toBeUndefined(); + expect(list.length).toBe(2); + expect(list.consume(5, false)).toEqual(makeUint8Array("bazar")); + expect(list.length).toBe(0); +}); |