Skip to content

Move name and lastModified from Blob.prototype to File.prototype - #32434

Open
robobun wants to merge 6 commits into
mainfrom
farm/0605fa29/blob-name-prototype
Open

Move name and lastModified from Blob.prototype to File.prototype#32434
robobun wants to merge 6 commits into
mainfrom
farm/0605fa29/blob-name-prototype

Conversation

@robobun

@robobun robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Repro

"name" in new Blob()                                    // true   (expected false)
"lastModified" in new Blob()                            // true   (expected false)
File.prototype === Blob.prototype                       // true   (expected false)
Object.getPrototypeOf(File.prototype) === Blob.prototype // false  (expected true)

Per the W3C File API, name and lastModified belong on File.prototype, not Blob.prototype. Node, Deno, Firefox and Chrome all return false for "name" in new Blob().

Cause

File did not have its own prototype. JSDOMFile.cpp pointed File.prototype directly at the JSBlobPrototype object and used JSBlobStructure() for new File():

// 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(), ...);

Because there was no separate File.prototype, the name and lastModified accessors were placed on Blob.prototype in response.classes.ts.

Fix

Add JSDOMFilePrototype ([[Prototype]] = Blob.prototype) carrying the name/lastModified accessors plus Symbol.toStringTag = "File", and a cached m_JSDOMFileStructure on ZigGlobalObject that points instances at it. The File constructor's own [[Prototype]] is now the Blob constructor and File.prototype.constructor === File. The Symbol.hasInstance override on File is removed since the prototype chain now makes default instanceof work.

All File-producing paths route through the new structure: new File() and subclasses, Bun.file(), structuredClone of Files, FormData file entries (WebCore::toJS/toJSNewlyCreated). S3File.prototype now chains through File.prototype so name/lastModified remain reachable there. Instances remain WebCore::JSBlob cells; only the structure/prototype differs.

The m_name cached WriteBarrier stays on JSBlob (via the values list in the class definition) so the Rust-side name cache continues to work.

Verification

$ bun bd test test/js/web/fetch/blob.test.ts -t "name and lastModified"
(pass) name and lastModified live on File.prototype, not Blob.prototype
$ USE_SYSTEM_BUN=1 bun test test/js/web/fetch/blob.test.ts -t "name and lastModified"
(fail) name and lastModified live on File.prototype, not Blob.prototype
$ bun bd test test/js/web/fetch/blob.test.ts -t "can set lastModified"
(pass) blob: can set lastModified property
$ USE_SYSTEM_BUN=1 bun test test/js/web/fetch/blob.test.ts -t "can set lastModified"
(fail) blob: can set lastModified property

blob: can set lastModified property is the exact scenario from #14257 (assigning lastModified on a Blob and in a Blob subclass constructor now creates an own data property, as in Node; File.prototype.lastModified stays read-only).

Existing tests in blob.test.ts, globals.test.js, FormData.test.ts, structured-clone.test.ts, blob-file-name-ownership.test.ts pass.

Fixes #20700
Fixes #14257

Relationship to #30328

#30328 is the PR landing the prototype split itself (File.prototype distinct from and inheriting Blob.prototype, Symbol.toStringTag, constructor, ordinary instanceof; it resolves #14102 and #25422). #32430 and #31656 were closed in its favor. #30328 keeps name and lastModified on Blob.prototype (while also defining them on File.prototype), so this PR stays open for removing them from Blob.prototype (#20700, #14257). The prototype plumbing here duplicates #30328 and will be dropped once that lands; what remains is the response.classes.ts change plus the question below. #32425 (a setter on Blob.prototype.lastModified) was closed as the narrower alternative to this.

Point for maintainers: once the accessors leave Blob.prototype, something has to keep the documented Bun.file().name / .lastModified working. This PR does that by having Bun.file(), Bun.stdin/stdout/stderr, embedded files and S3File inherit from File.prototype, which also makes Bun.file(path) instanceof File true (it is false today, and #30328 leaves it false). An alternative would be a separate BunFile prototype carrying those two accessors.


no test proof · iteration 13 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_compile.test.ts test/js/bun/util/inspect.test.js test/js/web/fetch/blob.test.ts test/js/web/html/FormData-multipart-serialization.test.ts test/js/web/structured-clone-blob-file.test.ts

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces the previous JSDOMFile InternalFunction + Symbol.hasInstance implementation with a split JSDOMFilePrototype/JSDOMFileConstructor pair registered via LazyClassStructure. Moves name and lastModified from Blob.prototype to File.prototype, rewires all C++ and Rust FFI bindings accordingly, updates S3File's prototype chain, and adjusts tests to match the new behavior.

Changes

JSDOMFile prototype/constructor split and Blob property migration

Layer / File(s) Summary
JSDOMFile C++ prototype/constructor and ABI entry points
src/jsc/bindings/JSDOMFile.h, src/jsc/bindings/JSDOMFile.cpp
Declares new Zig ABI functions for name/lastModified, implements JSDOMFilePrototype with custom property getters/setters, implements JSDOMFileConstructor using JSDOMFileStructure(), adds initJSDOMFileClassStructure and BUN__createJSDOMFileUnsafely, removes createJSDOMFileConstructor and JSDOMFile__hasInstance. Header replaces createJSDOMFileConstructor declaration with initJSDOMFileClassStructure.
GlobalObject lazy class structure wiring
src/jsc/bindings/ZigGlobalObject.h, src/jsc/bindings/ZigGlobalObject.cpp, src/jsc/bindings/ZigGlobalObject.lut.txt
Replaces the m_JSDOMFileConstructor GC lazy member with m_JSDOMFileClassStructure (LazyClassStructure), updates JSDOMFileConstructor() accessor to derive from the class structure, adds JSDOMFileStructure() accessor, replaces the initLater call, and switches the lut.txt File entry from CellProperty to ClassStructure.
Rust/C++ FFI rewiring and Blob-to-JS routing
src/jsc/bindings/blob.cpp, src/runtime/webcore/Blob.rs
Renames the extern-C binding from Blob__create to BUN__createJSDOMFileUnsafely in blob.cpp and updates both call sites. In Blob.rs, introduces BUN__createJSDOMFileUnsafely FFI, dom_file_to_js_unchecked helper, host ABI functions for File.prototype.name/lastModified, early routing in to_js for is_jsdom_file/is_bun_file blobs, clears is_jsdom_file on slice, removes the JSDOMFile__hasInstance export, and updates Bun.file() to use dom_file_to_js_unchecked.
Blob class definition and S3File prototype chain
src/runtime/webcore/response.classes.ts, src/jsc/bindings/JSS3File.cpp
Adds "name" to Blob's values list for instance caching and removes name/lastModified proto entries from Blob. JSS3File::createStructure switches superPrototype from JSBlobPrototype() to JSDOMFileStructure()->storedPrototypeObject().
FormData, Archive, BunObject, and Body integration
src/jsc/bindings/DOMFormData.cpp, src/runtime/api/Archive.rs, src/runtime/api/BunObject.rs, src/runtime/webcore/Body.rs
Updates DOMFormData::append and DOMFormData::set to normalize null filenames as emptyString and apply unpaired-surrogate replacement only for non-null filenames. Updates Archive.files() FilesContext, BunObject.get_embedded_files, and stdio_stores.make_blob to route through dom_file_to_js_unchecked for proper File wrapper creation. Updates Body.resolve and Body.get_blob_with_this_value to clear is_jsdom_file so converted blobs are treated as plain Blobs.
Test updates for File/Blob property split
test/js/bun/util/inspect.test.js, test/js/web/fetch/blob.test.ts
Removes Blob name field assertions from inspect test. Updates two existing assertions to expect numeric own-property assignment on Blob.name. Adds tests verifying name/lastModified live on File.prototype not Blob.prototype, slice returns plain Blob, and Bun.file().constructor resolves to File before the File global is otherwise touched.

Possibly related PRs

  • oven-sh/bun#31579: Both PRs touch src/runtime/webcore/Blob.rs in the jsdom/file wrapping path—main PR adds/rewires dom_file_to_js_unchecked and File.prototype accessors, while the other PR refactors how File/Blob filename data (stored_name/PathLike::String) is represented after deleting PathString—so they are directly connected at the code level.

Suggested reviewers

  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely states the primary change: moving name and lastModified from Blob.prototype to File.prototype.
Description check ✅ Passed The description explains the problem, cause, implementation, verification results, related issues, and test limitations, although it uses different headings from the template.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:03 PM PT - Aug 16th, 2026

@robobun, your commit ac2df5c8956c87bd3087a66c930146204aa92d98 passed in Build #99613! 🎉


🧪   To try this PR locally:

bunx bun-pr 32434

That installs a local version of the PR into your bun-32434 executable, so you can run:

bun-32434 --bun

@robobun robobun mentioned this pull request Jun 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. BunFile should extend Web File #16850 - PR makes Bun.file() return instanceof File with proper File.prototype chain, directly addressing the request for BunFile to extend Web File
  2. api to check if value is an instance of BunFile #10481 - PR enables instanceof File checks for BunFile instances, providing the requested API for checking if a value is a BunFile

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #16850
Fixes #10481

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Give File its own prototype with Symbol.toStringTag "File" #32430 - Same architectural fix (separate JSDOMFilePrototype inheriting from Blob.prototype), fixes File symbol is Blob instead of File #14102; explicitly superseded by this PR
  2. fix(File): set Symbol.toStringTag to "File" #31656 - Fixes the same issue (File symbol is Blob instead of File #14102) by setting Symbol.toStringTag = "File" per-instance rather than via prototype
  3. Give File its own prototype that inherits from Blob.prototype #30328 - Fixes instanceof File behavior via patched customHasInstance; this PR's prototype restructuring makes that workaround unnecessary

🤖 Generated with Claude Code

@robobun

robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Ready for review at 0e67f6c (rebased onto current main, MERGEABLE, all 54 review threads resolved).

One substantive addition since the last green run: Blob SERIALIZATION_VERSION is bumped to 5 so that bun:jsc payloads of a Bun.file() written by older Bun versions (which serialized is_jsdom_file=0) still deserialize as a File with .name/.lastModified, while v5 payloads keep the byte authoritative so Bun.file().slice() round-trips as a plain Blob. Both directions are covered in structured-clone-blob-file.test.ts. The rebased build is running now.

Deferred as follow-ups per review discussion: the File.prototype.name brand check accepts plain Blob receivers (no distinct File ClassInfo to key on), and FormData defaults a nameless Blob entry to "" rather than the spec "blob" (needs the Blob/File bit threaded into DOMFormData).

Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/jsc/bindings/JSDOMFile.cpp (2)

177-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release the ThrowScope on the throwing tail call.

throwTypeError leaves a pending exception; return through RELEASE_AND_RETURN so exception-scope validation sees the intentional throw.

Proposed fix
         auto scope = DECLARE_THROW_SCOPE(lexicalGlobalObject->vm());
         throwTypeError(lexicalGlobalObject, scope, "Class constructor File cannot be invoked without 'new'"_s);
-        return {};
+        RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));

As per coding guidelines, "**/*.cpp: Exception checks required after every call that can throw or run user code" and throwing tail calls should use the checked propagation path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jsc/bindings/JSDOMFile.cpp` around lines 177 - 181, In the static call
method of JSDOMFile.cpp, the throwTypeError function call leaves a pending
exception but the function returns with a simple return statement. Replace the
return {}; with RELEASE_AND_RETURN(scope, JSValue()); to properly release the
ThrowScope and use the checked propagation path for the throwing tail call,
ensuring exception-scope validation sees the intentional throw as required by
the coding guidelines.

Source: Coding guidelines


149-175: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Check for constructor exceptions before wrapping ptr.

JSDOMFile__construct parses/coerces constructor arguments and can throw or run JS. The returned native pointer is wrapped without RETURN_IF_EXCEPTION, which can trip JSC exception validation or return a value with a pending exception.

Proposed fix
         auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
         auto& vm = JSC::getVM(globalObject);
+        auto throwScope = DECLARE_THROW_SCOPE(vm);
         JSObject* newTarget = asObject(callFrame->newTarget());
         auto* constructor = globalObject->JSDOMFileConstructor();
         Structure* structure = globalObject->JSDOMFileStructure();
         if (constructor != newTarget) {
-            auto scope = DECLARE_THROW_SCOPE(vm);
-
             auto* functionGlobalObject = defaultGlobalObject(
                 // ShadowRealm functions belong to a different global object.
                 getFunctionRealm(lexicalGlobalObject, newTarget));
-            RETURN_IF_EXCEPTION(scope, {});
+            RETURN_IF_EXCEPTION(throwScope, {});
             structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->JSDOMFileStructure());
-            RETURN_IF_EXCEPTION(scope, {});
+            RETURN_IF_EXCEPTION(throwScope, {});
         }
 
         void* ptr = JSDOMFile__construct(lexicalGlobalObject, callFrame);
+        RETURN_IF_EXCEPTION(throwScope, {});
 
         if (!ptr) [[unlikely]] {
-            return JSValue::encode(JSC::jsUndefined());
+            RELEASE_AND_RETURN(throwScope, JSValue::encode(JSC::jsUndefined()));
         }
 
-        return JSValue::encode(
-            WebCore::JSBlob::create(vm, globalObject, structure, ptr));
+        RELEASE_AND_RETURN(throwScope, JSValue::encode(
+            WebCore::JSBlob::create(vm, globalObject, structure, ptr)));

As per coding guidelines, "**/*.cpp: Exception checks required after every call that can throw or run user code; use RETURN_IF_EXCEPTION or JSError propagation before using results".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jsc/bindings/JSDOMFile.cpp` around lines 149 - 175, The construct method
in JSDOMFile.cpp calls JSDOMFile__construct which can throw or run user code,
but the returned ptr value is wrapped in WebCore::JSBlob::create without
checking for exceptions. Add a RETURN_IF_EXCEPTION check after the
JSDOMFile__construct call and before the JSBlob::create wrapping, using the
existing scope variable that was declared in the constructor subclass path. This
ensures that if an exception occurs during construction, the method returns
early rather than attempting to wrap a potentially invalid pointer.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/JSDOMFile.cpp`:
- Around line 15-17: The File.name property currently has a writable setter
(JSDOMFile__setName) which allows mutation of file metadata, while lastModified
is readonly. To align with the W3C File API specification, remove the
JSDOMFile__setName function declaration and its associated installation at line
69, then apply the ReadOnly attribute to the name property descriptor to match
the immutable pattern used for lastModified.
- Around line 50-51: The JSDOMFile__setName function can throw exceptions during
value coercion or storage, but the code does not validate whether an exception
occurred before using the returned bool result. After the JSDOMFile__setName
call and before the RELEASE_AND_RETURN statement, add an exception check using
RETURN_IF_EXCEPTION or appropriate JSError propagation to ensure any pending
exceptions are properly handled before the result is returned.

In `@src/runtime/webcore/Blob.rs`:
- Around line 2014-2016: The slice() method currently only clears is_jsdom_file
but retains the file-backed store from dupe(), allowing is_bun_file() to promote
it back to a File when to_js() is called later. To fix this and ensure
Bun.file().slice() returns a plain Blob as per the File API spec, either route
the slice result through a plain JSBlob wrapper that strips the file-backed
store entirely, or add an explicit state flag (like is_plain_blob_from_slice)
that prevents the is_bun_file() check in the to_js path from promoting sliced
blobs back to File objects.

---

Outside diff comments:
In `@src/jsc/bindings/JSDOMFile.cpp`:
- Around line 177-181: In the static call method of JSDOMFile.cpp, the
throwTypeError function call leaves a pending exception but the function returns
with a simple return statement. Replace the return {}; with
RELEASE_AND_RETURN(scope, JSValue()); to properly release the ThrowScope and use
the checked propagation path for the throwing tail call, ensuring
exception-scope validation sees the intentional throw as required by the coding
guidelines.
- Around line 149-175: The construct method in JSDOMFile.cpp calls
JSDOMFile__construct which can throw or run user code, but the returned ptr
value is wrapped in WebCore::JSBlob::create without checking for exceptions. Add
a RETURN_IF_EXCEPTION check after the JSDOMFile__construct call and before the
JSBlob::create wrapping, using the existing scope variable that was declared in
the constructor subclass path. This ensures that if an exception occurs during
construction, the method returns early rather than attempting to wrap a
potentially invalid pointer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76e87ba3-fff3-4c49-96c2-0bd053015954

📥 Commits

Reviewing files that changed from the base of the PR and between 91ce3b4 and 4d7252d.

📒 Files selected for processing (11)
  • src/jsc/bindings/JSDOMFile.cpp
  • src/jsc/bindings/JSDOMFile.h
  • src/jsc/bindings/JSS3File.cpp
  • src/jsc/bindings/ZigGlobalObject.cpp
  • src/jsc/bindings/ZigGlobalObject.h
  • src/jsc/bindings/ZigGlobalObject.lut.txt
  • src/jsc/bindings/blob.cpp
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/response.classes.ts
  • test/js/bun/util/inspect.test.js
  • test/js/web/fetch/blob.test.ts
💤 Files with no reviewable changes (1)
  • test/js/bun/util/inspect.test.js

Comment thread src/jsc/bindings/JSDOMFile.cpp
Comment thread src/jsc/bindings/JSDOMFile.cpp
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/jsc/bindings/blob.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/webcore/Blob.rs`:
- Around line 4338-4340: Remove the is_bun_file() check from the condition on
line 4338 that determines whether to deserialize as a File. Instead, ensure that
Bun.file() payloads are explicitly marked with a File indicator during
serialization before wrapping. Update the deserialization logic to only promote
to File based on this explicit marker (likely through is_jsdom_file or a similar
explicit flag), not through the is_bun_file() method check. This prevents slices
of Bun.file() blobs (which clear is_jsdom_file but retain the file store) from
being incorrectly promoted back to File, preserving the slice invariant where a
slice of a Blob remains a Blob.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2de364e9-4cc5-440a-9e10-82c3ec69d4e4

📥 Commits

Reviewing files that changed from the base of the PR and between 4d7252d and 8b03308.

📒 Files selected for processing (2)
  • src/runtime/webcore/Blob.rs
  • test/js/web/fetch/blob.test.ts

Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/jsc/bindings/blob.cpp
Comment thread src/runtime/webcore/response.classes.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/DOMFormData.cpp`:
- Around line 160-163: The `set` method that takes a name, blob, and filename
parameters is not applying the same filename sanitization that the `append`
method uses. Instead of directly passing the filename to `blob->setFileName()`,
apply the `replaceUnpairedSurrogatesWithReplacementCharacter()` function to the
filename (handling both null and non-null cases) before calling setFileName,
ensuring consistent behavior between the `set` and `append` methods for filename
sanitization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0777294-c33a-4acf-9612-11879b781f6e

📥 Commits

Reviewing files that changed from the base of the PR and between 8b03308 and 1f3ea85.

📒 Files selected for processing (3)
  • src/jsc/bindings/DOMFormData.cpp
  • src/jsc/bindings/blob.cpp
  • src/runtime/api/Archive.rs

Comment thread src/jsc/bindings/DOMFormData.cpp
Comment thread src/runtime/webcore/response.classes.ts Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/jsc/bindings/JSDOMFile.cpp
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/api/BunObject.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/api/BunObject.rs Outdated
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/jsc/bindings/JSDOMFile.cpp
Comment thread test/js/web/fetch/blob.test.ts Outdated
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Do you need to make it so toJS on Blob when is_jsdom_file creates the File object instead of the Blob ojbect?

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Yes, done in f329ef6. to_js() now branches on is_jsdom_file (after the existing is_s3() check) and routes to the File structure, so is_jsdom_file is the single source of truth for which prototype a wrapper gets:

if self.is_s3() { return s3_file::to_js_unchecked(...); }
if self.is_jsdom_file.get() { return dom_file_to_js_unchecked(...); }
js::to_js_unchecked(...)

To make that hold everywhere, construct_bun_file and stdio_stores::make_blob now set is_jsdom_file = true on creation (previously they only called dom_file_to_js_unchecked directly, so the JS prototype was right but the Rust flag was still false). With the flag set at the source, structuredClone / resolveObjectURL / Bun.inspect all see the same state the wrapper has, and the per-call-site is_jsdom_file || is_bun_file() branches in deserialize and ObjectURLRegistry are gone.

I did not fold is_bun_file() into the to_js() check because Response(Bun.file(path)).blob() returns a blob that still shares the Data::File store (so is_bun_file() is true) but must be Blob.prototype per the Fetch spec; keying on is_jsdom_file alone and clearing it in Body.blob() keeps that path correct.

One knock-on: now that Bun.file(fd) and new File([], "") are File-prototyped and is_jsdom_file=true, File.prototype.name returns "" instead of undefined when there is no backing path (File.name is a USVString in the spec, so this also brings new File([], "").name in line with Node and browsers). Updated the two FormData assertions that checked for undefined on a File-wrapped entry.

@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (273 commits of drift) and squashed the 17 WIP commits into one. Head is now 5fb5f36; the net diff is unchanged apart from one conflict resolution.

The only conflict was src/jsc/bindings/ZigGlobalObject.lut.txt. It looked like a whole-file conflict because main converted the file from CRLF to LF, so every line showed as changed. I took main's version and re-applied my single line, moving the File entry out of the CellProperty block and into the ClassStructure block where it now belongs:

-  File                            GlobalObject::m_JSDOMFileConstructor                 CellProperty
   crypto                          GlobalObject::m_cryptoObject                         CellProperty
 ...
   Crypto                          GlobalObject::m_JSCrypto                             ClassStructure
+  File                            GlobalObject::m_JSDOMFileClassStructure              ClassStructure

main also moved under ZigGlobalObject.cpp/h, Blob.rs, Body.rs, BunObject.rs, Archive.rs and three of the test files; those all merged cleanly and I verified each of my hunks is still present and correct against the new base.

Re-verified on the rebased tree:

  • blob.test.ts, inspect.test.js, FormData.test.ts, FormData-multipart-serialization.test.ts: 237 pass, 0 fail
  • body.test.ts, structured-clone.test.ts, structured-clone-blob-file.test.ts, bun-file.test.ts, archive.test.ts, structuredClone-classes.test.ts: 762 pass, 0 fail
  • compile/Bun.embeddedFiles: pass
  • cargo clippy -p bun_runtime: clean
  • USE_SYSTEM_BUN=1 still fails the new name and lastModified live on File.prototype test, as expected

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (511caaa) and re-verified. Every case from the File/Blob prototype report now matches Node:

File.prototype === Blob.prototype           false
f.constructor.name                          File
"name" in new Blob(["b"])                   false
"lastModified" in new Blob(["b"])           false
File.prototype.x = 7; new Blob(["b"]).x     undefined
structuredClone(f).constructor.name         File
new Blob(["b"]) instanceof File             false
Object.getPrototypeOf(File.prototype) === Blob.prototype   true
Object.prototype.toString.call(f)           [object File]
f.slice().constructor.name                  Blob

Added explicit assertions for file.constructor, Object.prototype.toString.call(file), and the prototype-patch isolation case to test/js/web/fetch/blob.test.ts.

Comment thread src/runtime/webcore/Body.rs Outdated
Comment thread src/runtime/webcore/Body.rs Outdated
Comment thread src/runtime/webcore/ObjectURLRegistry.rs Outdated
Comment thread src/runtime/webcore/response.classes.ts Outdated
Comment thread src/runtime/webcore/response.classes.ts Outdated
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Triage update: #30328 is the PR landing the File prototype split (#32430 and #31656 were closed in its favor), and #32425 was closed as the narrower alternative to this one. This PR stays open for the part #30328 does not do, removing name and lastModified from Blob.prototype (#20700, #14257); once #30328 lands it should be reworked on top of it. Pushed 906d671 adding the #14257 scenario as a test (assigning lastModified on a Blob and in a Blob subclass constructor; fails on the current release, passes with this branch). The description now has a "Relationship to #30328" section, including the open question of how Bun.file().name / .lastModified should keep working (this branch makes Bun.file() inherit from File.prototype, which #30328 deliberately does not do). The branch also currently conflicts with main in src/runtime/webcore/Blob.rs.

Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
@robobun
robobun force-pushed the farm/0605fa29/blob-name-prototype branch from 889434c to fc7cd7d Compare August 13, 2026 03:11
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the latest push (55c31cf switching the is_bun_file() inspect arm to !name.is_empty(), plus autofix) and this pass found nothing new — that commit resolves the last inline nit about structuredClone(Bun.file(fd)) printing a spurious name: "".

I'm not auto-approving because the open design question in the description (whether Bun.file() / Bun.stdin etc. should inherit from File.prototype and become instanceof File, vs. a separate BunFile prototype) and the overlap with #30328 are maintainer calls, and the change spans JSC structure/prototype plumbing across 19 files.

What was reviewed

  • show_name now keys on !name.is_empty() and compares against the header's get_file_name(), so both the fd-clone and the new File([Bun.file(p)], "custom.txt") cases render correctly (both covered in inspect.test.js).
  • LazyClassStructure wiring: m_JSDOMFileClassStructure is in FOR_EACH_GLOBALOBJECT_GC_MEMBER so it's visited; init.setConstructor sets File.prototype.constructor before any global lookup (covered by the "constructor is set before the File global is touched" test).
  • is_jsdom_file reset on every path that yields a plain Blob from a File source: slice(), new Blob([file]), body.blob() (both arms), and the v1-only structured-clone fallback.
Extended reasoning...

Overview

This PR splits File.prototype from Blob.prototype and moves the name/lastModified accessors onto the new prototype. It touches 19 files: hand-written JSC C++ (JSDOMFile.cpp gains a JSDOMFilePrototype/JSDOMFileConstructor pair and a LazyClassStructure initializer; ZigGlobalObject.{h,cpp} and the .lut.txt swap the File global from a CellProperty to a ClassStructure; blob.cpp and DOMFormData.cpp route File-flavoured wrappers through the new structure; JSS3File.cpp re-parents the S3 prototype), Rust runtime (Blob.rs gains dom_file_to_js_unchecked plus three new host ABI exports and threads is_jsdom_file through to_js, slice(), the Blob constructor's single-part dupe path, structured-clone deserialize and construct_bun_file; Body.rs, BunObject.rs, standalone_graph_jsc.rs, ObjectURLRegistry.rs follow), the response.classes.ts codegen input, and five test files.

Since my last inline comment (03:42Z), 55c31cf changed the is_bun_file() arm of show_name from name.tag() != Tag::Dead to !name.is_empty(), and added a regression assertion (Bun.inspect(structuredClone(Bun.file(123))) equals the un-cloned output). That was the fix I suggested, and it also composes correctly with the earlier new File([Bun.file(p)], "custom.txt") case because get_file_name() returns the store path, not blob.name, so the equality check only suppresses the line when they genuinely match.

Security risks

None identified. No untrusted input parsing was added; the FormData change only substitutes emptyString() for a null WTF::String before the existing surrogate-replacement call. The new BUN__createJSDOMFileUnsafely mirrors the generated Blob__create (same JSBlob::create + reportExtraMemoryAllocated), taking ownership of the passed pointer the same way. The m_name WriteBarrier remains on JSBlob via the values: ["stream", "name"] list, so the cached JS name string is still visited by visitChildrenImpl.

Level of scrutiny

High. This is a user-visible API-shape change (removing two accessors from Blob.prototype, changing Bun.file() instanceof File from false to true, re-parenting S3File.prototype) implemented across hand-written JSC prototype/structure code, a LazyClassStructure on ZigGlobalObject, generated-class inputs, and Rust FFI. The PR description explicitly flags a design decision for maintainers — whether Bun.file()/stdio/embedded files should sit on File.prototype (this branch) or a dedicated BunFile prototype — and notes that the prototype-split plumbing overlaps #30328 and will need reworking on top of it. Those are exactly the kind of choices a human maintainer should sign off on.

Other factors

The three earlier deferred nits (WebIDL brand check accepting a plain Blob receiver on the extracted File.prototype.name getter; FormData defaulting a nameless Blob entry to "" instead of the spec's "blob"; both explicitly acknowledged as follow-ups) remain open but are strict improvements over pre-PR behavior and don't block. Test coverage for the new shape is thorough (prototype chain, Symbol.toStringTag, slice()/body.blob()/new Blob([file])/structuredClone staying Blob, embedded-file File-ness in bundler_compile.test.ts, the #14257 subclass-assignment scenario, and the lazy-init constructor test). The earlier robobun note about a merge conflict in Blob.rs may still apply.

alii added a commit that referenced this pull request Aug 15, 2026
### Problem
- mordant's `reimplemented_helper` finding baselined for
`src/jsc/webcore_types.rs`: `Blob::is_bun_file` and
`Blob::needs_to_read_file` had the same signature and the same body (the
store is `Some` and its data is `Store::File`), so a change to one would
have silently missed the other.
- The duplication is inherited, not a porting mistake: `Blob.zig`
already had `isBunFile` and `needsToReadFile` with the same body, and
neither side has changed since the port (#30412). Nothing relies on them
differing.

### Fix
- Delete `is_bun_file` and keep `needs_to_read_file`, which is what the
~40 other call sites (and the `blob_needs_to_read_file` hook that
`bun_sql_jsc` goes through) already use.
- Point the two `is_bun_file` callers at it: `Response.rs`
(`getCompleteWebRequestOrResponseBodyValueAsArrayBuffer` returns
`undefined` for a file-backed body) and `CryptoHasher.rs` (the
synchronous hashers reject `Bun.file()` input). Same predicate, so no
behavior change.
- Remove the `reimplemented_helper:src/jsc/webcore_types.rs` entry from
`mordant-baseline.toml`, and the two stale comments in `webcore/Blob.rs`
that named the deleted method. The entry is removed by hand rather than
by regenerating the file: a full `bun run rust:mordant:baseline` on
Linux also drops two unrelated entries (`always_unwrapped_option` in
`PackageInstall.rs`, `narrowed_two_ways` in `node_crypto_binding.rs`)
that this PR has no business touching.
- No new test: there is no observable behavior to pin (the two
predicates were byte-for-byte the same), so no test can distinguish
before from after. The `Bun.file()` rejection at the `CryptoHasher` call
site is already covered by `bun-cryptohasher.test.ts` ("Bun.file in
CryptoHasher is not supported yet"), and the mordant run below is the
check for the finding itself. Same shape as #39124, #39116 and #39135
from this batch.
- Verified:
- `bun run rust:mordant` (full workspace) on this branch: clean, no
`target/mordant/over-baseline.txt`. With main's `webcore_types.rs`
restored on top of the trimmed baseline it reports `reimplemented_helper
... over the mordant baseline (0 recorded for src/jsc/webcore_types.rs)`
and `1 finding(s) over the baseline in bun_jsc`, so the entry was this
site and the lint no longer fires.
- The `mordant` workflow also runs on this PR since the baseline
changed; with the entry removed it fails if the finding is still
reported.
- `bun bd test test/js/bun/util/bun-cryptohasher.test.ts` (covers the
`CryptoHasher` call site: `Bun.file()` input still throws): 402 pass.
- `bun bd test` on
`test/js/web/fetch/{blob,blob-write,body,blob-file-name-ownership,response}.test.ts`,
`test/js/web/structured-clone-blob-file.test.ts` and
`test/js/bun/io/bun-write.test.js` (`--timeout 60000`, since several of
these spawn a debug+ASAN child and exceed the 5s default): everything
passes except `blob.test.ts` "Bun.file(path).slice(start, end) streams
only the slice", which fails identically on a clean checkout of main
(88a6398) and is being handled separately; it is a `FileReader`
streaming bug unrelated to this predicate.
- Two open PRs add `is_bun_file()` calls in `Blob.rs` (#32434, #33659);
whichever lands after this one needs those calls renamed to
`needs_to_read_file()`. That is a compile error, not a silent change.

### Background
- `Blob` (`src/jsc/webcore_types.rs`) is a view (offset + size) onto a
refcounted `Store`, whose `data` is one of `Bytes` (in memory), `File`
(a path or fd; what `Bun.file()` creates) or `S3`.
`needs_to_read_file()` asks whether the blob is `File`-backed, i.e.
whether its bytes have to be read off disk before anything in memory can
look at them; synchronous consumers use it to bail out.
- mordant is the advisory Rust lint pack the `rust-lints` workflow runs
(`bun run rust:mordant`). `mordant-baseline.toml` holds the per-(lint,
file) counts of findings that predate the job, so CI only fails when a
count goes up; once a baselined finding is fixed its entry is deleted.

Co-authored-by: Alistair Smith <hi@alistair.sh>
`"name" in new Blob()` returned true because `File.prototype` and
`Blob.prototype` were the same object: `File` was an InternalFunction
with a patched `Symbol.hasInstance`, not a real class with its own
prototype.

Give `File` a real `JSDOMFilePrototype` / `JSDOMFileConstructor` pair
registered through `LazyClassStructure`, so
`Object.getPrototypeOf(File.prototype) === Blob.prototype` and `name` /
`lastModified` live only on `File.prototype`.

`is_jsdom_file` is now the single source of truth for which prototype a
Blob's JS wrapper gets: `Blob::to_js()` routes to the File structure when
it is set (alongside the existing `is_s3()` branch), so call sites no
longer pick the structure themselves. `Bun.file()`, `Bun.stdin/stdout/
stderr`, embedded assets and archive entries set the flag at creation;
`slice()`, `new Blob()` and `Body.blob()` clear it. Structured-clone
deserialize honors the serialized flag for every payload version that
carries it.

Side effects:
- `File.prototype.name` returns "" instead of undefined when there is no
  backing path (`Bun.file(fd)`, `new File([], "")`). File.name is a
  USVString, so this matches Node and browsers and makes
  `structuredClone(Bun.file(fd)).name` round-trip.
- `Bun.inspect` no longer prints a redundant `name:` line for a FileRef
  header that already shows the path; a File constructed over a BunFile
  with its own name still shows it.

Fixes #20700
Fixes #14257
Fixes #14102
@robobun
robobun force-pushed the farm/0605fa29/blob-name-prototype branch from 3c80700 to cf3ed26 Compare August 16, 2026 19:33
Comment thread src/runtime/webcore/Blob.rs Outdated
@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main again (194 commits); head is 47b8442.

Two small conflicts, both in src/runtime/webcore/Blob.rs and both from main touching the same lines this PR does:

  • main (Blob: drop is_bun_file, a duplicate of needs_to_read_file #39158) removed Blob::is_bun_file() in favour of the identical needs_to_read_file(). Updated the two uses this PR adds (the Bun.inspect name line and the v1 structured-clone fallback).
  • main respelled the to_js returns in construct_bun_file and the structured-clone deserializer as BlobExt::to_js(&*ptr, ..) to avoid a method-resolution ambiguity. Kept main's form in both places; in the deserializer the version gate now sits just above into_inner, so main's return block is unchanged context.

Archive.rs no longer appears in the diff: main already sets is_jsdom_file and calls to_js() there, so the to_js() routing in this PR covers archive entries without a local change (archive.test.ts passes).

Re-verified on the rebased tree: blob, inspect, FormData, FormData-multipart-serialization, archive (444 pass), compile/Bun.embeddedFiles, and the headline test still fails against the released binary.

Comment thread src/runtime/webcore/Blob.rs Outdated
… File

Bun.file() now serializes is_jsdom_file=1, so a 0 on a file-backed
store means slice()/body.blob(). Payloads written before this change
wrote 0 for Bun.file() itself; without a version bump they would
deserialize as plain Blobs and lose .name/.lastModified. Promote
file-backed payloads older than v5 and keep the byte authoritative
from v5 on.
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs
The slice is a plain Blob at runtime (no name/lastModified), matching
File.prototype.slice() in the File API.
@robobun
robobun requested a review from alii as a code owner August 16, 2026 21:18

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the bug-hunting pass found no new issues — every finding from the prior ~17 review rounds has been addressed. Given the scope (new File.prototype + structure on ZigGlobalObject, serialization format bump to v5, Bun.file()/Bun.stdin/S3File now inheriting File.prototype) and the open design question the description explicitly flags for maintainers — whether Bun.file() instanceof File should become true or whether a separate BunFile prototype is preferable — a human sign-off is still worthwhile.

What was reviewed: prototype-chain wiring (JSDOMFilePrototype/LazyClassStructure, File.prototype.constructor set via init.setConstructor, S3File chained through File); every File-producing path routes through is_jsdom_fileto_js() (Bun.file, stdio, embedded files, structuredClone, FormData, resolveObjectURL, Archive); every Blob-producing path clears the flag (slice(), new Blob([file]), Body.blob()); m_name WriteBarrier kept on JSBlob via values: and used by the hand-written getter; BUN__createJSDOMFileUnsafely reports extra memory like Blob__create; the v5 gate keeps pre-v5 file-backed payloads File-typed while v5 slices stay Blob.

Extended reasoning...

Overview

This PR splits File.prototype from Blob.prototype and moves the name/lastModified accessors onto File.prototype. It touches 21 files: a new JSDOMFilePrototype + JSDOMFileConstructor + LazyClassStructure on ZigGlobalObject (JSDOMFile.cpp/h, ZigGlobalObject.cpp/h/.lut.txt), the generated-class definition (response.classes.ts drops name/lastModified from Blob's proto but keeps name in values: for the WriteBarrier cache), blob.cpp/DOMFormData.cpp (toJS now routes on null-vs-set fileName), JSS3File.cpp (S3File.prototype now chains through File.prototype), Rust-side plumbing in Blob.rs/Body.rs/BunObject.rs/standalone_graph_jsc.rs/ObjectURLRegistry.rs to set/clear is_jsdom_file at every producer, a serialization format bump (v4→v5) with a back-compat gate, and bun.d.ts (BunFile.slice() now returns Blob). Six test files are updated/extended.

Security risks

None identified. No auth/crypto/permissions surface. The structured-clone deserializer change is a version-gated read of an already-trusted byte; no new untrusted-length arithmetic. The new C++ getters do dynamicDowncast<JSBlob> before dereferencing wrapped() and use RETURN_IF_EXCEPTION.

Level of scrutiny

High. This is user-facing Web API surface (File/Blob prototype identity, instanceof, Symbol.toStringTag), it changes what Bun.file() is an instance of (a documented Bun API), it bumps a persisted serialization format, and it adds a new cached structure to ZigGlobalObject. It has been through many review rounds with a dozen-plus resolved findings (slice/Body.blob/new Blob([file]) File-leak, WebSocket blob promotion, FormData entry wrapping, resolveObjectURL, embeddedFiles/compiled-asset flag consistency, reportExtraMemoryAllocated, DOMAttribute brand check, inspect show_name edge cases, structured-clone version gating, .d.ts drift), all now addressed with tests.

Other factors

The description explicitly leaves a design decision to maintainers: routing Bun.file()/stdio/embedded/S3File through File.prototype (so Bun.file(p) instanceof File === true, a behavior change) versus a separate BunFile prototype. It also notes overlap with #30328 (the prototype-split PR), with the plumbing here to be reworked on top of that once it lands. Jarred asked a design question on the thread earlier. Those are exactly the kinds of calls a human should make; the code itself looks correct for the approach taken.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blob has name Blob LastModified incompatibility with Node File symbol is Blob instead of File

2 participants