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:
Claude Bot
2026-02-12 14:40:49 +00:00
parent 50e478dcdc
commit eb3d38dbde
10 changed files with 604 additions and 36 deletions

View 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));
}
}
}

View 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

View File

@@ -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);
}
}

View File

@@ -4,4 +4,5 @@
namespace Bun {
JSC::JSObject* createJSDOMFileConstructor(JSC::VM&, JSC::JSGlobalObject*);
JSC::Structure* createJSDOMFileInstanceStructure(JSC::VM&, JSC::JSGlobalObject*);
}

View File

@@ -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));

View File

@@ -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) \
\

View File

@@ -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();

View File

@@ -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,
},
},
}),
];

View File

@@ -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,

View 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");
});