mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
fix(web): give File a distinct prototype from Blob
Previously `File.prototype === Blob.prototype` was `true`, causing `new File(...).constructor.name` to return `"Blob"` instead of `"File"`. This creates a separate `FilePrototype` that inherits from `Blob.prototype`, fixing the prototype chain per the spec. Closes #26899 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,56 @@ 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);
|
||||
|
||||
// TODO: make this inehrit from JSBlob instead of InternalFunction
|
||||
// That will let us remove this hack for [Symbol.hasInstance] and fix the prototype chain.
|
||||
// File.prototype inherits from Blob.prototype per the spec.
|
||||
// This gives File instances all Blob methods while having a distinct prototype
|
||||
// with constructor === File and [Symbol.toStringTag] === "File".
|
||||
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;
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
|
||||
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
{
|
||||
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
structure->setMayBePrototype(true);
|
||||
return structure;
|
||||
}
|
||||
|
||||
template<typename CellType, JSC::SubspaceAccess>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSDOMFilePrototype, Base);
|
||||
return &vm.plainObjectSpace();
|
||||
}
|
||||
|
||||
protected:
|
||||
JSDOMFilePrototype(JSC::VM& vm, JSC::Structure* structure)
|
||||
: Base(vm, structure)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
// Set [Symbol.toStringTag] = "File" so Object.prototype.toString.call(file) === "[object File]"
|
||||
this->putDirectWithoutTransition(vm, vm.propertyNames->toStringTagSymbol,
|
||||
jsNontrivialString(vm, "File"_s),
|
||||
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly);
|
||||
}
|
||||
};
|
||||
|
||||
const JSC::ClassInfo JSDOMFilePrototype::s_info = { "File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDOMFilePrototype) };
|
||||
|
||||
class JSDOMFile : public JSC::InternalFunction {
|
||||
using Base = JSC::InternalFunction;
|
||||
|
||||
@@ -40,15 +88,20 @@ public:
|
||||
Base::finishCreation(vm, 2, "File"_s);
|
||||
}
|
||||
|
||||
static JSDOMFile* create(JSC::VM& vm, JSGlobalObject* globalObject)
|
||||
static JSDOMFile* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::JSObject* filePrototype)
|
||||
{
|
||||
auto* zigGlobal = defaultGlobalObject(globalObject);
|
||||
auto structure = createStructure(vm, globalObject, zigGlobal->functionPrototype());
|
||||
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);
|
||||
// Set File.prototype to the distinct FilePrototype object (which inherits from Blob.prototype).
|
||||
object->putDirect(vm, vm.propertyNames->prototype, filePrototype,
|
||||
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly);
|
||||
|
||||
// Set FilePrototype.constructor = File
|
||||
filePrototype->putDirect(vm, vm.propertyNames->constructor, object,
|
||||
static_cast<unsigned>(JSC::PropertyAttribute::DontEnum));
|
||||
|
||||
return object;
|
||||
}
|
||||
@@ -69,7 +122,7 @@ public:
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
JSObject* newTarget = asObject(callFrame->newTarget());
|
||||
auto* constructor = globalObject->JSDOMFileConstructor();
|
||||
Structure* structure = globalObject->JSBlobStructure();
|
||||
Structure* structure = globalObject->JSFileStructure();
|
||||
if (constructor != newTarget) {
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
@@ -77,7 +130,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->JSFileStructure());
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
}
|
||||
|
||||
@@ -103,9 +156,30 @@ const JSC::ClassInfo JSDOMFile::s_info = { "File"_s, &Base::s_info, nullptr, nul
|
||||
|
||||
namespace Bun {
|
||||
|
||||
JSC::Structure* createJSFileStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto* zigGlobal = defaultGlobalObject(globalObject);
|
||||
JSC::JSObject* blobPrototype = zigGlobal->JSBlobPrototype();
|
||||
|
||||
// Create FilePrototype with [[Prototype]] = Blob.prototype
|
||||
auto* protoStructure = JSDOMFilePrototype::createStructure(vm, globalObject, blobPrototype);
|
||||
auto* filePrototype = JSDOMFilePrototype::create(vm, globalObject, protoStructure);
|
||||
|
||||
// Create the structure for File instances: [[Prototype]] = FilePrototype
|
||||
return JSC::Structure::create(vm, globalObject, filePrototype,
|
||||
JSC::TypeInfo(static_cast<JSC::JSType>(0b11101110), WebCore::JSBlob::StructureFlags),
|
||||
WebCore::JSBlob::info(), NonArray);
|
||||
}
|
||||
|
||||
JSC::JSObject* createJSDOMFileConstructor(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
return JSDOMFile::create(vm, globalObject);
|
||||
auto* zigGlobal = defaultGlobalObject(globalObject);
|
||||
|
||||
// Get the File instance structure - its prototype is the FilePrototype we need
|
||||
auto* fileStructure = zigGlobal->JSFileStructure();
|
||||
auto* filePrototype = fileStructure->storedPrototypeObject();
|
||||
|
||||
return JSDOMFile::create(vm, globalObject, filePrototype);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
namespace Bun {
|
||||
JSC::JSObject* createJSDOMFileConstructor(JSC::VM&, JSC::JSGlobalObject*);
|
||||
JSC::Structure* createJSFileStructure(JSC::VM&, JSC::JSGlobalObject*);
|
||||
}
|
||||
|
||||
@@ -1805,6 +1805,11 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(CustomGetterSetter::create(init.vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter));
|
||||
});
|
||||
|
||||
m_JSFileStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::createJSFileStructure(init.vm, init.owner));
|
||||
});
|
||||
|
||||
m_JSDOMFileConstructor.initLater(
|
||||
[](const Initializer<JSObject>& init) {
|
||||
JSObject* fileConstructor = Bun::createJSDOMFileConstructor(init.vm, init.owner);
|
||||
|
||||
@@ -610,6 +610,7 @@ public:
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_importMetaBakeObjectStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_asyncBoundFunctionStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSC::JSObject>, m_JSDOMFileConstructor) \
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSFileStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSC::JSObject>, m_JSMIMEParamsConstructor) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSC::JSObject>, m_JSMIMETypeConstructor) \
|
||||
\
|
||||
@@ -712,6 +713,7 @@ public:
|
||||
|
||||
JSObject* cryptoObject() const { return m_cryptoObject.getInitializedOnMainThread(this); }
|
||||
JSObject* JSDOMFileConstructor() const { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); }
|
||||
JSC::Structure* JSFileStructure() const { return m_JSFileStructure.getInitializedOnMainThread(this); }
|
||||
|
||||
JSMap* nodeWorkerEnvironmentData() { return m_nodeWorkerEnvironmentData.get(); }
|
||||
void setNodeWorkerEnvironmentData(JSMap* data);
|
||||
|
||||
67
test/regression/issue/26899.test.ts
Normal file
67
test/regression/issue/26899.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26899
|
||||
// File.prototype should be distinct from Blob.prototype
|
||||
|
||||
test("File.prototype !== Blob.prototype", () => {
|
||||
expect(File.prototype).not.toBe(Blob.prototype);
|
||||
});
|
||||
|
||||
test("File.prototype inherits from Blob.prototype", () => {
|
||||
expect(Object.getPrototypeOf(File.prototype)).toBe(Blob.prototype);
|
||||
});
|
||||
|
||||
test("new File(...).constructor.name === 'File'", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(file.constructor.name).toBe("File");
|
||||
});
|
||||
|
||||
test("new File(...).constructor === File", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(file.constructor).toBe(File);
|
||||
});
|
||||
|
||||
test("new File(...).constructor !== Blob", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(file.constructor).not.toBe(Blob);
|
||||
});
|
||||
|
||||
test("Object.prototype.toString.call(file) === '[object File]'", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(Object.prototype.toString.call(file)).toBe("[object File]");
|
||||
});
|
||||
|
||||
test("file instanceof File", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(file instanceof File).toBe(true);
|
||||
});
|
||||
|
||||
test("file instanceof Blob", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(file instanceof Blob).toBe(true);
|
||||
});
|
||||
|
||||
test("blob is not instanceof File", () => {
|
||||
const blob = new Blob(["hello"]);
|
||||
expect(blob instanceof File).toBe(false);
|
||||
});
|
||||
|
||||
test("File instances have Blob methods", () => {
|
||||
const file = new File(["hello"], "hello.txt");
|
||||
expect(typeof file.text).toBe("function");
|
||||
expect(typeof file.arrayBuffer).toBe("function");
|
||||
expect(typeof file.slice).toBe("function");
|
||||
expect(typeof file.stream).toBe("function");
|
||||
});
|
||||
|
||||
test("File name and lastModified work", () => {
|
||||
const file = new File(["hello"], "hello.txt", { lastModified: 12345 });
|
||||
expect(file.name).toBe("hello.txt");
|
||||
expect(file.lastModified).toBe(12345);
|
||||
});
|
||||
|
||||
test("File.prototype has correct Symbol.toStringTag", () => {
|
||||
const desc = Object.getOwnPropertyDescriptor(File.prototype, Symbol.toStringTag);
|
||||
expect(desc).toBeDefined();
|
||||
expect(desc!.value).toBe("File");
|
||||
});
|
||||
Reference in New Issue
Block a user