mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
fix(core): make Bun.file() return BunFile instance instead of Blob
Create a separate BunFile class that extends Blob with file-specific methods (exists, write, unlink, delete, stat, writer, name, lastModified). This ensures Bun.file().constructor.name returns "BunFile" and plain Blob instances don't expose non-standard file methods. Also fixes the File prototype chain so File.prototype properly extends Blob.prototype instead of being the same object. Closes #26967 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
276
src/bun.js/bindings/JSBunFile.cpp
Normal file
276
src/bun.js/bindings/JSBunFile.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
|
||||
#include "root.h"
|
||||
|
||||
#include "ZigGlobalObject.h"
|
||||
#include "ZigGeneratedClasses.h"
|
||||
|
||||
#include "JavaScriptCore/JSType.h"
|
||||
#include "JavaScriptCore/JSObject.h"
|
||||
#include "JavaScriptCore/JSGlobalObject.h"
|
||||
#include <JavaScriptCore/InternalFunction.h>
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
#include <JavaScriptCore/FunctionPrototype.h>
|
||||
#include <JavaScriptCore/GetterSetter.h>
|
||||
#include <JavaScriptCore/JSFunction.h>
|
||||
#include "JavaScriptCore/JSCJSValue.h"
|
||||
#include "ErrorCode.h"
|
||||
|
||||
#include "JSBunFile.h"
|
||||
|
||||
namespace Bun {
|
||||
using namespace JSC;
|
||||
using namespace WebCore;
|
||||
|
||||
// Reuse existing Blob extern functions for BunFile-specific methods
|
||||
extern "C" {
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__getExists(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__doUnlink(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__doWrite(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__getStat(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__getWriter(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__getName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject);
|
||||
SYSV_ABI bool BlobPrototype__setName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue value);
|
||||
SYSV_ABI EncodedJSValue BlobPrototype__getLastModified(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
|
||||
SYSV_ABI bool JSDOMFile__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue);
|
||||
}
|
||||
|
||||
// BunFile constructor - throws when called directly, exists for constructor.name
|
||||
JSC_DECLARE_HOST_FUNCTION(callBunFileConstructor);
|
||||
JSC_DEFINE_HOST_FUNCTION(callBunFileConstructor, (JSGlobalObject * globalObject, CallFrame*))
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
|
||||
throwTypeError(globalObject, scope, "BunFile is not constructable. Use Bun.file() to create a BunFile."_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Forward declarations for host functions
|
||||
JSC_DECLARE_HOST_FUNCTION(functionBunFile_exists);
|
||||
JSC_DECLARE_HOST_FUNCTION(functionBunFile_unlink);
|
||||
JSC_DECLARE_HOST_FUNCTION(functionBunFile_write);
|
||||
JSC_DECLARE_HOST_FUNCTION(functionBunFile_stat);
|
||||
JSC_DECLARE_HOST_FUNCTION(functionBunFile_writer);
|
||||
static JSC_DECLARE_CUSTOM_GETTER(getterBunFile_name);
|
||||
static JSC_DECLARE_CUSTOM_SETTER(setterBunFile_name);
|
||||
static JSC_DECLARE_CUSTOM_GETTER(getterBunFile_lastModified);
|
||||
|
||||
// --- Host function implementations ---
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(functionBunFile_exists, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(callFrame->thisValue());
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
return BlobPrototype__getExists(thisObject->wrapped(), globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(functionBunFile_unlink, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(callFrame->thisValue());
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
return BlobPrototype__doUnlink(thisObject->wrapped(), globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(functionBunFile_write, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(callFrame->thisValue());
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
return BlobPrototype__doWrite(thisObject->wrapped(), globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(functionBunFile_stat, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(callFrame->thisValue());
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
return BlobPrototype__getStat(thisObject->wrapped(), globalObject, callFrame);
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(functionBunFile_writer, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(callFrame->thisValue());
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
return BlobPrototype__getWriter(thisObject->wrapped(), globalObject, callFrame);
|
||||
}
|
||||
|
||||
static JSC_DEFINE_CUSTOM_GETTER(getterBunFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(JSValue::decode(thisValue));
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
return BlobPrototype__getName(thisObject->wrapped(), thisValue, globalObject);
|
||||
}
|
||||
|
||||
static JSC_DEFINE_CUSTOM_SETTER(setterBunFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(JSValue::decode(thisValue));
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return false;
|
||||
}
|
||||
|
||||
return BlobPrototype__setName(thisObject->wrapped(), thisValue, globalObject, value);
|
||||
}
|
||||
|
||||
static JSC_DEFINE_CUSTOM_GETTER(getterBunFile_lastModified, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* thisObject = jsDynamicCast<JSBlob*>(JSValue::decode(thisValue));
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a BunFile instance"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
return BlobPrototype__getLastModified(thisObject->wrapped(), globalObject);
|
||||
}
|
||||
|
||||
// --- BunFile-specific prototype property table ---
|
||||
static const HashTableValue JSBunFilePrototypeTableValues[] = {
|
||||
{ "delete"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_unlink, 0 } },
|
||||
{ "exists"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_exists, 0 } },
|
||||
{ "lastModified"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterBunFile_lastModified, 0 } },
|
||||
{ "name"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterBunFile_name, setterBunFile_name } },
|
||||
{ "stat"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_stat, 0 } },
|
||||
{ "unlink"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_unlink, 0 } },
|
||||
{ "write"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_write, 2 } },
|
||||
{ "writer"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::NativeFunctionType, functionBunFile_writer, 1 } },
|
||||
};
|
||||
|
||||
class JSBunFilePrototype final : public WebCore::JSBlobPrototype {
|
||||
public:
|
||||
using Base = WebCore::JSBlobPrototype;
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
static JSBunFilePrototype* create(
|
||||
JSC::VM& vm,
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSC::Structure* structure)
|
||||
{
|
||||
JSBunFilePrototype* prototype = new (NotNull, JSC::allocateCell<JSBunFilePrototype>(vm)) JSBunFilePrototype(vm, globalObject, structure);
|
||||
prototype->finishCreation(vm, globalObject);
|
||||
return prototype;
|
||||
}
|
||||
|
||||
static JSC::Structure* createStructure(
|
||||
JSC::VM& vm,
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSC::JSValue prototype)
|
||||
{
|
||||
auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
structure->setMayBePrototype(true);
|
||||
return structure;
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
|
||||
template<typename CellType, JSC::SubspaceAccess>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSBunFilePrototype, Base);
|
||||
return &vm.plainObjectSpace();
|
||||
}
|
||||
|
||||
protected:
|
||||
JSBunFilePrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
|
||||
: Base(vm, globalObject, structure)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
Base::finishCreation(vm, globalObject);
|
||||
ASSERT(inherits(info()));
|
||||
reifyStaticProperties(vm, JSBunFile::info(), JSBunFilePrototypeTableValues, *this);
|
||||
|
||||
this->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsOwnedString(vm, "BunFile"_s), 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Implementation of JSBunFile methods
|
||||
void JSBunFile::destroy(JSCell* cell)
|
||||
{
|
||||
static_cast<JSBunFile*>(cell)->JSBunFile::~JSBunFile();
|
||||
}
|
||||
|
||||
JSBunFile::~JSBunFile()
|
||||
{
|
||||
// Base class destructor will be called automatically
|
||||
}
|
||||
|
||||
JSBunFile* JSBunFile::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ptr)
|
||||
{
|
||||
JSBunFile* thisObject = new (NotNull, JSC::allocateCell<JSBunFile>(vm)) JSBunFile(vm, structure, ptr);
|
||||
thisObject->finishCreation(vm);
|
||||
return thisObject;
|
||||
}
|
||||
|
||||
JSC::Structure* JSBunFile::createStructure(JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
|
||||
JSC::JSObject* superPrototype = defaultGlobalObject(globalObject)->JSBlobPrototype();
|
||||
auto* protoStructure = JSBunFilePrototype::createStructure(vm, globalObject, superPrototype);
|
||||
auto* prototype = JSBunFilePrototype::create(vm, globalObject, protoStructure);
|
||||
|
||||
// Create a constructor function named "BunFile" for constructor.name
|
||||
auto* constructor = JSFunction::create(vm, globalObject, 0, "BunFile"_s, callBunFileConstructor, ImplementationVisibility::Public, NoIntrinsic, callBunFileConstructor);
|
||||
constructor->putDirect(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
|
||||
prototype->putDirect(vm, vm.propertyNames->constructor, constructor, static_cast<unsigned>(PropertyAttribute::DontEnum));
|
||||
|
||||
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast<JSC::JSType>(0b11101110), StructureFlags), info(), NonArray);
|
||||
}
|
||||
|
||||
Structure* createJSBunFileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
return JSBunFile::createStructure(globalObject);
|
||||
}
|
||||
|
||||
const JSC::ClassInfo JSBunFilePrototype::s_info = { "BunFile"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBunFilePrototype) };
|
||||
const JSC::ClassInfo JSBunFile::s_info = { "BunFile"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBunFile) };
|
||||
|
||||
extern "C" {
|
||||
SYSV_ABI EncodedJSValue BUN__createJSBunFileUnsafely(JSC::JSGlobalObject* globalObject, void* ptr)
|
||||
{
|
||||
ASSERT(ptr);
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
|
||||
auto* zigGlobal = defaultGlobalObject(globalObject);
|
||||
auto* structure = zigGlobal->m_JSBunFileStructure.getInitializedOnMainThread(globalObject);
|
||||
return JSValue::encode(JSBunFile::create(vm, globalObject, structure, ptr));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
src/bun.js/bindings/JSBunFile.h
Normal file
39
src/bun.js/bindings/JSBunFile.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
namespace Zig {
|
||||
class GlobalObject;
|
||||
}
|
||||
|
||||
namespace Bun {
|
||||
using namespace JSC;
|
||||
|
||||
class JSBunFile : public WebCore::JSBlob {
|
||||
using Base = WebCore::JSBlob;
|
||||
|
||||
public:
|
||||
static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction;
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
JSBunFile(JSC::VM& vm, Structure* structure, void* ptr)
|
||||
: Base(vm, structure, ptr)
|
||||
{
|
||||
}
|
||||
DECLARE_INFO;
|
||||
|
||||
template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
|
||||
return nullptr;
|
||||
return WebCore::JSBlob::subspaceFor<WebCore::JSBlob, mode>(vm);
|
||||
}
|
||||
|
||||
static void destroy(JSCell* cell);
|
||||
~JSBunFile();
|
||||
|
||||
static JSBunFile* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ptr);
|
||||
static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject);
|
||||
};
|
||||
|
||||
Structure* createJSBunFileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject);
|
||||
|
||||
} // namespace Bun
|
||||
@@ -1,16 +1,116 @@
|
||||
#include "root.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include "ZigGeneratedClasses.h"
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
#include <JavaScriptCore/InternalFunction.h>
|
||||
#include <JavaScriptCore/FunctionPrototype.h>
|
||||
#include "JSDOMFile.h"
|
||||
#include "ErrorCode.h"
|
||||
|
||||
using namespace JSC;
|
||||
|
||||
extern "C" SYSV_ABI void* JSDOMFile__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe);
|
||||
extern "C" SYSV_ABI bool JSDOMFile__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue);
|
||||
extern "C" SYSV_ABI EncodedJSValue BlobPrototype__getName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject);
|
||||
extern "C" SYSV_ABI bool BlobPrototype__setName(void* ptr, JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue value);
|
||||
extern "C" SYSV_ABI EncodedJSValue BlobPrototype__getLastModified(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
|
||||
|
||||
// TODO: make this inehrit from JSBlob instead of InternalFunction
|
||||
static JSC_DECLARE_CUSTOM_GETTER(getterDOMFile_name);
|
||||
static JSC_DECLARE_CUSTOM_SETTER(setterDOMFile_name);
|
||||
static JSC_DECLARE_CUSTOM_GETTER(getterDOMFile_lastModified);
|
||||
|
||||
static JSC_DEFINE_CUSTOM_GETTER(getterDOMFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* thisObject = jsDynamicCast<WebCore::JSBlob*>(JSValue::decode(thisValue));
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a File instance"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
return BlobPrototype__getName(thisObject->wrapped(), thisValue, globalObject);
|
||||
}
|
||||
|
||||
static JSC_DEFINE_CUSTOM_SETTER(setterDOMFile_name, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* thisObject = jsDynamicCast<WebCore::JSBlob*>(JSValue::decode(thisValue));
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a File instance"_s);
|
||||
return false;
|
||||
}
|
||||
|
||||
return BlobPrototype__setName(thisObject->wrapped(), thisValue, globalObject, value);
|
||||
}
|
||||
|
||||
static JSC_DEFINE_CUSTOM_GETTER(getterDOMFile_lastModified, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* thisObject = jsDynamicCast<WebCore::JSBlob*>(JSValue::decode(thisValue));
|
||||
if (!thisObject) {
|
||||
Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a File instance"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
return BlobPrototype__getLastModified(thisObject->wrapped(), globalObject);
|
||||
}
|
||||
|
||||
static const HashTableValue JSDOMFilePrototypeTableValues[] = {
|
||||
{ "lastModified"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterDOMFile_lastModified, 0 } },
|
||||
{ "name"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterDOMFile_name, setterDOMFile_name } },
|
||||
};
|
||||
|
||||
class JSDOMFilePrototype final : public JSC::JSNonFinalObject {
|
||||
using Base = JSC::JSNonFinalObject;
|
||||
public:
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
static JSDOMFilePrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
|
||||
{
|
||||
JSDOMFilePrototype* prototype = new (NotNull, JSC::allocateCell<JSDOMFilePrototype>(vm)) JSDOMFilePrototype(vm, structure);
|
||||
prototype->finishCreation(vm, globalObject);
|
||||
return prototype;
|
||||
}
|
||||
|
||||
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
{
|
||||
auto* structure = JSC::Structure::create(vm, globalObject, prototype, TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
structure->setMayBePrototype(true);
|
||||
return structure;
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
|
||||
template<typename CellType, JSC::SubspaceAccess>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
return &vm.plainObjectSpace();
|
||||
}
|
||||
|
||||
private:
|
||||
JSDOMFilePrototype(JSC::VM& vm, JSC::Structure* structure)
|
||||
: Base(vm, structure)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
ASSERT(inherits(info()));
|
||||
reifyStaticProperties(vm, info(), JSDOMFilePrototypeTableValues, *this);
|
||||
this->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsOwnedString(vm, "File"_s), 0);
|
||||
}
|
||||
};
|
||||
|
||||
const JSC::ClassInfo JSDOMFilePrototype::s_info = { "File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDOMFilePrototype) };
|
||||
|
||||
// TODO: make this inherit from JSBlob instead of InternalFunction
|
||||
// That will let us remove this hack for [Symbol.hasInstance] and fix the prototype chain.
|
||||
class JSDOMFile : public JSC::InternalFunction {
|
||||
using Base = JSC::InternalFunction;
|
||||
@@ -47,8 +147,13 @@ public:
|
||||
auto* object = new (NotNull, JSC::allocateCell<JSDOMFile>(vm)) JSDOMFile(vm, structure);
|
||||
object->finishCreation(vm);
|
||||
|
||||
// This is not quite right. But we'll fix it if someone files an issue about it.
|
||||
object->putDirect(vm, vm.propertyNames->prototype, zigGlobal->JSBlobPrototype(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0);
|
||||
// Create a proper File prototype that extends Blob.prototype
|
||||
auto* blobPrototype = zigGlobal->JSBlobPrototype();
|
||||
auto* protoStructure = JSDOMFilePrototype::createStructure(vm, globalObject, blobPrototype);
|
||||
auto* filePrototype = JSDOMFilePrototype::create(vm, globalObject, protoStructure);
|
||||
|
||||
object->putDirect(vm, vm.propertyNames->prototype, filePrototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0);
|
||||
filePrototype->putDirect(vm, vm.propertyNames->constructor, object, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum));
|
||||
|
||||
return object;
|
||||
}
|
||||
@@ -69,7 +174,7 @@ public:
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
JSObject* newTarget = asObject(callFrame->newTarget());
|
||||
auto* constructor = globalObject->JSDOMFileConstructor();
|
||||
Structure* structure = globalObject->JSBlobStructure();
|
||||
Structure* structure = globalObject->m_JSDOMFileStructure.getInitializedOnMainThread(lexicalGlobalObject);
|
||||
if (constructor != newTarget) {
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
@@ -77,7 +182,7 @@ public:
|
||||
// ShadowRealm functions belong to a different global object.
|
||||
getFunctionRealm(lexicalGlobalObject, newTarget));
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->JSBlobStructure());
|
||||
structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->m_JSDOMFileStructure.getInitializedOnMainThread(lexicalGlobalObject));
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
}
|
||||
|
||||
@@ -108,4 +213,15 @@ JSC::JSObject* createJSDOMFileConstructor(JSC::VM& vm, JSC::JSGlobalObject* glob
|
||||
return JSDOMFile::create(vm, globalObject);
|
||||
}
|
||||
|
||||
JSC::Structure* createJSDOMFileInstanceStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto* zigGlobal = defaultGlobalObject(globalObject);
|
||||
// Get the File.prototype from the constructor
|
||||
auto* fileConstructor = zigGlobal->JSDOMFileConstructor();
|
||||
JSValue filePrototype = fileConstructor->getDirect(vm, vm.propertyNames->prototype);
|
||||
ASSERT(filePrototype.isObject());
|
||||
// Create a JSBlob structure that uses File.prototype instead of Blob.prototype
|
||||
return JSC::Structure::create(vm, globalObject, filePrototype, JSC::TypeInfo(static_cast<JSC::JSType>(0b11101110), WebCore::JSBlob::StructureFlags), WebCore::JSBlob::info(), JSC::NonArray);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
namespace Bun {
|
||||
JSC::JSObject* createJSDOMFileConstructor(JSC::VM&, JSC::JSGlobalObject*);
|
||||
JSC::Structure* createJSDOMFileInstanceStructure(JSC::VM&, JSC::JSGlobalObject*);
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
#include "webcore/JSMIMEParams.h"
|
||||
#include "JSNodePerformanceHooksHistogram.h"
|
||||
#include "JSS3File.h"
|
||||
#include "JSBunFile.h"
|
||||
#include "S3Error.h"
|
||||
#include "ProcessBindingBuffer.h"
|
||||
#include "NodeValidator.h"
|
||||
@@ -1838,6 +1839,16 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(result.toObject(init.owner));
|
||||
});
|
||||
|
||||
m_JSBunFileStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::createJSBunFileStructure(init.vm, init.owner));
|
||||
});
|
||||
|
||||
m_JSDOMFileStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::createJSDOMFileInstanceStructure(init.vm, init.owner));
|
||||
});
|
||||
|
||||
m_JSS3FileStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::createJSS3FileStructure(init.vm, init.owner));
|
||||
|
||||
@@ -514,6 +514,8 @@ public:
|
||||
\
|
||||
V(public, LazyPropertyOfGlobalObject<JSObject>, m_processEnvObject) \
|
||||
\
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSBunFileStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSDOMFileStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSS3FileStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_S3ErrorStructure) \
|
||||
\
|
||||
|
||||
@@ -1887,8 +1887,54 @@ pub fn estimatedSize(this: *Blob) usize {
|
||||
|
||||
comptime {
|
||||
_ = JSDOMFile__hasInstance;
|
||||
|
||||
// Export BunFile-specific methods for use by JSBunFile.cpp and JSDOMFile.cpp.
|
||||
// These were previously generated by the code generator as part of Blob's prototype,
|
||||
// but are now only on the BunFile/File prototypes.
|
||||
@export(&bunfile_exports.getExists, .{ .name = "BlobPrototype__getExists" });
|
||||
@export(&bunfile_exports.doUnlink, .{ .name = "BlobPrototype__doUnlink" });
|
||||
@export(&bunfile_exports.doWrite, .{ .name = "BlobPrototype__doWrite" });
|
||||
@export(&bunfile_exports.getStat, .{ .name = "BlobPrototype__getStat" });
|
||||
@export(&bunfile_exports.getWriter, .{ .name = "BlobPrototype__getWriter" });
|
||||
@export(&bunfile_exports.getName, .{ .name = "BlobPrototype__getName" });
|
||||
@export(&bunfile_exports.setName, .{ .name = "BlobPrototype__setName" });
|
||||
@export(&bunfile_exports.getLastModified, .{ .name = "BlobPrototype__getLastModified" });
|
||||
}
|
||||
|
||||
const bunfile_exports = struct {
|
||||
pub fn getExists(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getExists, .{ thisValue, globalObject, callFrame } });
|
||||
}
|
||||
|
||||
pub fn doUnlink(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.doUnlink, .{ thisValue, globalObject, callFrame } });
|
||||
}
|
||||
|
||||
pub fn doWrite(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.doWrite, .{ thisValue, globalObject, callFrame } });
|
||||
}
|
||||
|
||||
pub fn getStat(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getStat, .{ thisValue, globalObject, callFrame } });
|
||||
}
|
||||
|
||||
pub fn getWriter(thisValue: *Blob, globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getWriter, .{ thisValue, globalObject, callFrame } });
|
||||
}
|
||||
|
||||
pub fn getName(this: *Blob, thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, jsc.toJSHostCall, .{ globalObject, @src(), Blob.getName, .{ this, thisValue, globalObject } });
|
||||
}
|
||||
|
||||
pub fn setName(this: *Blob, thisValue: jsc.JSValue, globalObject: *jsc.JSGlobalObject, value: jsc.JSValue) callconv(jsc.conv) bool {
|
||||
return @call(bun.callmod_inline, jsc.host_fn.toJSHostSetterValue, .{ globalObject, @call(bun.callmod_inline, Blob.setName, .{ this, thisValue, globalObject, value }) });
|
||||
}
|
||||
|
||||
pub fn getLastModified(this: *Blob, globalObject: *jsc.JSGlobalObject) callconv(jsc.conv) jsc.JSValue {
|
||||
return @call(bun.callmod_inline, Blob.getLastModified, .{ this, globalObject });
|
||||
}
|
||||
};
|
||||
|
||||
pub fn constructBunFile(
|
||||
globalObject: *jsc.JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
@@ -2998,7 +3044,7 @@ pub fn getName(
|
||||
|
||||
pub fn setName(
|
||||
this: *Blob,
|
||||
jsThis: jsc.JSValue,
|
||||
_: jsc.JSValue,
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
value: JSValue,
|
||||
) JSError!void {
|
||||
@@ -3006,7 +3052,6 @@ pub fn setName(
|
||||
if (value.isEmptyOrUndefinedOrNull()) {
|
||||
this.name.deref();
|
||||
this.name = bun.String.dead;
|
||||
js.nameSetCached(jsThis, globalThis, value);
|
||||
return;
|
||||
}
|
||||
if (value.isString()) {
|
||||
@@ -3015,7 +3060,6 @@ pub fn setName(
|
||||
errdefer this.name = bun.String.empty;
|
||||
this.name = try bun.String.fromJS(value, globalThis);
|
||||
// We don't need to increment the reference count since tryFromJS already did it.
|
||||
js.nameSetCached(jsThis, globalThis, value);
|
||||
old_name.deref();
|
||||
}
|
||||
}
|
||||
@@ -3465,9 +3509,15 @@ pub fn toJS(this: *Blob, globalObject: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
return S3File.toJSUnchecked(globalObject, this);
|
||||
}
|
||||
|
||||
if (this.isBunFile()) {
|
||||
return BUN__createJSBunFileUnsafely(globalObject, this);
|
||||
}
|
||||
|
||||
return js.toJSUnchecked(globalObject, this);
|
||||
}
|
||||
|
||||
extern fn BUN__createJSBunFileUnsafely(*jsc.JSGlobalObject, *Blob) callconv(jsc.conv) jsc.JSValue;
|
||||
|
||||
pub fn deinit(this: *Blob) void {
|
||||
this.detach();
|
||||
this.name.deref();
|
||||
|
||||
@@ -156,7 +156,6 @@ export default [
|
||||
slice: { fn: "getSlice", length: 2 },
|
||||
stream: { fn: "getStream", length: 1 },
|
||||
formData: { fn: "getFormData", async: true },
|
||||
exists: { fn: "getExists", length: 0 },
|
||||
|
||||
// Non-standard, but consistent!
|
||||
bytes: { fn: "getBytes", async: true },
|
||||
@@ -165,34 +164,9 @@ export default [
|
||||
getter: "getType",
|
||||
},
|
||||
|
||||
// TODO: Move this to a separate `File` object or BunFile
|
||||
// This is *not* spec-compliant.
|
||||
name: {
|
||||
this: true,
|
||||
cache: true,
|
||||
getter: "getName",
|
||||
setter: "setName",
|
||||
},
|
||||
|
||||
// TODO: Move this to a separate `File` object or BunFile
|
||||
// This is *not* spec-compliant.
|
||||
lastModified: {
|
||||
getter: "getLastModified",
|
||||
},
|
||||
|
||||
// Non-standard, s3 + BunFile support
|
||||
unlink: { fn: "doUnlink", length: 0 },
|
||||
delete: { fn: "doUnlink", length: 0 },
|
||||
write: { fn: "doWrite", length: 2 },
|
||||
size: {
|
||||
getter: "getSize",
|
||||
},
|
||||
stat: { fn: "getStat", length: 0 },
|
||||
|
||||
writer: {
|
||||
fn: "getWriter",
|
||||
length: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -204,10 +204,11 @@ test("blob: can set name property #10178", () => {
|
||||
blob.name = "logo.svg";
|
||||
// @ts-expect-error
|
||||
expect(blob.name).toBe("logo.svg");
|
||||
// name is now a plain data property on Blob (not a typed setter), so any value is accepted
|
||||
// @ts-expect-error
|
||||
blob.name = 10;
|
||||
// @ts-expect-error
|
||||
expect(blob.name).toBe("logo.svg");
|
||||
expect(blob.name).toBe(10);
|
||||
Object.defineProperty(blob, "name", {
|
||||
value: 42,
|
||||
writable: false,
|
||||
@@ -226,10 +227,11 @@ test("blob: can set name property #10178", () => {
|
||||
const myBlob = new MyBlob([Buffer.from("Hello, World")]);
|
||||
// @ts-expect-error
|
||||
expect(myBlob.name).toBe("logo.svg");
|
||||
// name is now a plain data property on Blob (not a typed setter), so any value is accepted
|
||||
// @ts-expect-error
|
||||
myBlob.name = 10;
|
||||
// @ts-expect-error
|
||||
expect(myBlob.name).toBe("logo.svg");
|
||||
expect(myBlob.name).toBe(10);
|
||||
Object.defineProperty(myBlob, "name", {
|
||||
value: 42,
|
||||
writable: false,
|
||||
|
||||
97
test/regression/issue/26967.test.ts
Normal file
97
test/regression/issue/26967.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("Bun.file() returns BunFile with correct constructor.name", () => {
|
||||
const file = Bun.file("file.txt");
|
||||
expect(file.constructor.name).toBe("BunFile");
|
||||
});
|
||||
|
||||
test("Bun.file() returns BunFile instance that is instanceof Blob", () => {
|
||||
const file = Bun.file("file.txt");
|
||||
expect(file).toBeInstanceOf(Blob);
|
||||
});
|
||||
|
||||
test("Bun.file() instance has BunFile-specific methods", () => {
|
||||
const file = Bun.file("file.txt");
|
||||
expect(typeof file.exists).toBe("function");
|
||||
expect(typeof file.write).toBe("function");
|
||||
expect(typeof file.unlink).toBe("function");
|
||||
expect(typeof file.delete).toBe("function");
|
||||
expect(typeof file.stat).toBe("function");
|
||||
expect(typeof file.writer).toBe("function");
|
||||
expect("name" in file).toBe(true);
|
||||
expect("lastModified" in file).toBe(true);
|
||||
});
|
||||
|
||||
test("Bun.file() instance has Blob standard methods", () => {
|
||||
const file = Bun.file("file.txt");
|
||||
expect(typeof file.text).toBe("function");
|
||||
expect(typeof file.arrayBuffer).toBe("function");
|
||||
expect(typeof file.json).toBe("function");
|
||||
expect(typeof file.slice).toBe("function");
|
||||
expect(typeof file.stream).toBe("function");
|
||||
expect(typeof file.formData).toBe("function");
|
||||
expect(typeof file.bytes).toBe("function");
|
||||
});
|
||||
|
||||
test("Blob.prototype does not have BunFile-specific methods", () => {
|
||||
expect("exists" in Blob.prototype).toBe(false);
|
||||
expect("write" in Blob.prototype).toBe(false);
|
||||
expect("unlink" in Blob.prototype).toBe(false);
|
||||
expect("delete" in Blob.prototype).toBe(false);
|
||||
expect("stat" in Blob.prototype).toBe(false);
|
||||
expect("writer" in Blob.prototype).toBe(false);
|
||||
expect("name" in Blob.prototype).toBe(false);
|
||||
expect("lastModified" in Blob.prototype).toBe(false);
|
||||
});
|
||||
|
||||
test("new Blob() does not have BunFile-specific methods", () => {
|
||||
const blob = new Blob(["hello"]);
|
||||
expect("exists" in blob).toBe(false);
|
||||
expect("write" in blob).toBe(false);
|
||||
expect("unlink" in blob).toBe(false);
|
||||
expect("delete" in blob).toBe(false);
|
||||
expect("stat" in blob).toBe(false);
|
||||
expect("writer" in blob).toBe(false);
|
||||
expect("name" in blob).toBe(false);
|
||||
expect("lastModified" in blob).toBe(false);
|
||||
});
|
||||
|
||||
test("new Blob() has standard Blob methods", () => {
|
||||
const blob = new Blob(["hello"]);
|
||||
expect(blob.constructor.name).toBe("Blob");
|
||||
expect(typeof blob.text).toBe("function");
|
||||
expect(typeof blob.arrayBuffer).toBe("function");
|
||||
expect(typeof blob.slice).toBe("function");
|
||||
expect(typeof blob.stream).toBe("function");
|
||||
});
|
||||
|
||||
test("File has proper prototype chain (not sharing Blob.prototype)", () => {
|
||||
expect(File.prototype).not.toBe(Blob.prototype);
|
||||
expect(Object.getPrototypeOf(File.prototype)).toBe(Blob.prototype);
|
||||
});
|
||||
|
||||
test("new File() has name and lastModified", () => {
|
||||
const file = new File(["x"], "test.txt");
|
||||
expect(file.name).toBe("test.txt");
|
||||
expect(typeof file.lastModified).toBe("number");
|
||||
expect(file.constructor.name).toBe("File");
|
||||
expect(file).toBeInstanceOf(File);
|
||||
expect(file).toBeInstanceOf(Blob);
|
||||
});
|
||||
|
||||
test("BunFile prototype chain is correct", () => {
|
||||
const file = Bun.file("file.txt");
|
||||
const proto = Object.getPrototypeOf(file);
|
||||
|
||||
// BunFile prototype -> Blob.prototype -> Object.prototype
|
||||
expect(proto).not.toBe(Blob.prototype);
|
||||
expect(Object.getPrototypeOf(proto)).toBe(Blob.prototype);
|
||||
|
||||
// Symbol.toStringTag
|
||||
expect(Object.prototype.toString.call(file)).toBe("[object BunFile]");
|
||||
});
|
||||
|
||||
test("BunFile constructor throws when called directly", () => {
|
||||
const file = Bun.file("file.txt");
|
||||
expect(() => new (file.constructor as any)()).toThrow("BunFile is not constructable");
|
||||
});
|
||||
Reference in New Issue
Block a user