Give File its own prototype that inherits from Blob.prototype

JA
3mo ago
+426-69

Closes #25422.
Closes #26899.

What does this PR do?

File.prototype was the same object as Blob.prototype, with a comment saying "This is not quite right. But we'll fix it if someone files an issue about it." To keep new Blob() instanceof File false despite that, the File constructor installed a custom [Symbol.hasInstance] that checked a native flag on the underlying cell instead of walking the prototype chain.

That broke every case where instanceof is supposed to consult the prototype chain:

new Proxy(new File([], "f"), {}) instanceof File                          // false, should be true
Object.create(File.prototype) instanceof File                              // false, should be true
new Proxy({}, { getPrototypeOf: () => File.prototype }) instanceof File    // false, should be true
Object.setPrototypeOf(new File([], "f"), null) instanceof File             // true,  should be false

It also meant new File(...).constructor === Blob, Object.prototype.toString.call(file) === "[object Blob]", and File.prototype === Blob.prototype, none of which match browsers or Node.

This gives File a real prototype chain:

  • JSDOMFilePrototype is a new prototype object whose [[Prototype]] is Blob.prototype. It carries constructor and Symbol.toStringTag; everything else is inherited from Blob.prototype.
  • m_JSDOMFileStructure is a cached Structure on the global object for File instances (same JSBlob cell type, different prototype).
  • new File(...), structuredClone(file), FormData file entries, and Bun.Archive file entries all allocate with that structure.
  • The custom hasInstance and its JSDOMFile__hasInstance Rust export are deleted. instanceof File is now ordinary OrdinaryHasInstance.
ExpressionBeforeAfterNode/browsers
File.prototype === Blob.prototypetruefalsefalse
Object.getPrototypeOf(File.prototype) === Blob.prototypen/atruetrue
new File([], "x").constructorBlobFileFile
Object.prototype.toString.call(new File([], "x"))"[object Blob]""[object File]""[object File]"
new Proxy(new File([], "x"), {}) instanceof Filefalsetruetrue
Object.create(File.prototype) instanceof Filefalsetruetrue
Object.create(Blob.prototype) instanceof Filefalsefalsefalse
new Blob() instanceof Filefalsefalsefalse
structuredClone(file).constructorBlobFileFile

Bun.file() is unchanged (it is a Blob, not a File).

How did you verify your code works?

27 new tests in test/js/web/fetch/blob.test.ts covering the prototype chain, constructor, Symbol.toStringTag, Object.getPrototypeOf(File) === Blob, proxies (transparent and with traps), Object.create, Object.setPrototypeOf, subclassing, structuredClone, Worker.postMessage into a fresh realm, FormData append/set with named, empty, and omitted filenames, multipart filename="", WebSocket binaryType="blob", and file.slice() / Response(file).blob() / new Blob([file]) staying plain Blobs. Verified fail-before on the unfixed build and pass-after with bun bd test.

Also ran the existing blob, FormData, structured-clone, Archive, globals, inspect, body, and response suites locally with no regressions.

Rebased twice onto main. The second rebase moved the m_JSDOMFileClassStructure init into main's new lazyClassStructureInits table in ZigGlobalObject.cpp and dropped the stale m_JSDOMFileConstructor entry from lazyObjectInits. The .lut.txt entry follows main's renamed m_generatedLazyClasses neighbours.


no test proof · iteration 11 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/web/fetch/blob.test.ts