Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions src/jsc/webcore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ pub struct Blob {
pub ref_count: bun_ptr::RawRefCount,
pub global_this: Cell<*const JSGlobalObject>,
pub last_modified: Cell<f64>,
/// Only used by `<input type="file">` / `File` (issue #10178).
/// This Blob's own name; `Dead` means the store's [`Self::get_file_name`] applies.
pub name: bun_core::OwnedStringCell,
}

Expand Down Expand Up @@ -434,8 +434,7 @@ impl Blob {
matches!(self.store.get().as_deref(), Some(s) if matches!(s.data, store::Data::File(_)))
}

/// `Blob.getFileName()` — the user-visible name: `Bytes.stored_name`,
/// the file path, or the S3 key. `None` for fd-backed or unnamed blobs.
/// The store's own name (`stored_name`, path, or S3 key); `None` when fd-backed or unnamed.
pub fn get_file_name(&self) -> Option<&[u8]> {
match &self.store.get().as_deref()?.data {
store::Data::Bytes(bytes) => {
Expand Down Expand Up @@ -611,8 +610,7 @@ pub mod store {
pub len: SizeType,
pub cap: SizeType,
pub allocator: bun_alloc::StdAllocator,
/// Used by standalone module graph and the `File` constructor.
/// Heap-owned (or empty); freed by `Bytes`'s `Drop`.
/// Set only at store creation (names given later go in `Blob::name`); heap-owned or empty.
pub stored_name: Box<[u8]>,
}

Expand Down Expand Up @@ -713,14 +711,6 @@ pub mod store {
}
}

#[inline]
pub fn init_empty_with_name(name: Box<[u8]>) -> Bytes {
Bytes {
stored_name: name,
..Default::default()
}
}

#[inline]
pub fn allocator(&self) -> bun_alloc::StdAllocator {
self.allocator
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3857,8 +3857,8 @@ where
// 1. Bun.file("foo")
// 2. The content-disposition header is not present
if !has_content_disposition && content_type.category.autoset_filename() {
if let Some(filename) = blob.get_file_name() {
let basename = bun_paths::basename(filename);
if let Some(filename) = blob.get_name_utf8() {
let basename = bun_paths::basename(filename.slice());
if !basename.is_empty() {
let mut filename_buf = [0u8; 1024];
let truncated = &basename[..basename.len().min(1024 - 32)];
Expand Down
79 changes: 28 additions & 51 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ pub trait BlobExt {
fn get_mime_type_or_content_type(&self) -> Option<MimeType>;
fn get_type(&self, global_this: &JSGlobalObject) -> JSValue;
fn get_name_string(&self) -> Option<BunString>;
fn get_name_utf8(&self) -> Option<ZigStringSlice>;
fn get_name(&self, _: JSValue, global_this: &JSGlobalObject) -> JsResult<JSValue>;
fn set_name(
&self,
Expand Down Expand Up @@ -2054,6 +2055,15 @@ impl BlobExt for Blob {
None
}

/// [`Self::get_name_string`] as UTF-8; `None` when there is no name or it is empty.
fn get_name_utf8(&self) -> Option<ZigStringSlice> {
let name = self.get_name_string()?;
if name.is_empty() {
return None;
}
Some(name.to_utf8())
}

// TODO: Move this to a separate `File` object or BunFile
fn get_name(&self, _: JSValue, global_this: &JSGlobalObject) -> JsResult<JSValue> {
Ok(match self.get_name_string() {
Expand Down Expand Up @@ -2087,8 +2097,8 @@ impl BlobExt for Blob {

fn get_loader(&self, jsc_vm: &VirtualMachine) -> Option<bun_ast::Loader> {
use bun_resolver::fs::PathResolverExt as _;
if let Some(filename) = self.get_file_name() {
let current_path = bun_resolver::fs::Path::init(filename);
if let Some(filename) = self.get_name_utf8() {
let current_path = bun_resolver::fs::Path::init(filename.slice());
return Some(
current_path
.loader(&jsc_vm.transpiler.options.loaders)
Expand Down Expand Up @@ -4239,19 +4249,12 @@ pub(crate) extern "C" fn Blob__dupeFromJS(value: JSValue) -> Option<NonNull<Blob
)
}

/// blob.cpp `toJS`: names the FormData entry's Blob; empty means no filename was given.
#[unsafe(no_mangle)]
pub(crate) extern "C" fn Blob__setAsFile(this: &mut Blob, path_str: &mut BunString) {
this.is_jsdom_file.set(true);

// This is not 100% correct...
if let Some(store) = this.store() {
if let store::Data::Bytes(bytes) = &mut store.data_mut() {
if bytes.stored_name.is_empty() {
// Owned heap slice
// owned by `stored_name` (`Box<[u8]>`) and freed by `Bytes::Drop`.
bytes.stored_name = path_str.to_owned_slice().into_boxed_slice();
}
}
if !path_str.is_empty() {
this.name.set(path_str.dupe_ref());
}
}

Expand All @@ -4260,12 +4263,10 @@ pub(crate) extern "C" fn Blob__dupe(this: &Blob) -> *mut Blob {
Blob::new(this.dupe_with_content_type(true))
}

/// Borrowed: JSDOMFormData.cpp refs it via `toWTFString` and never derefs it.
#[unsafe(no_mangle)]
pub(crate) extern "C" fn Blob__getFileNameString(this: &Blob) -> BunString {
if let Some(filename) = this.get_file_name() {
return BunString::from_bytes(filename);
}
BunString::empty()
this.get_name_string().unwrap_or_else(BunString::empty)
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -5525,47 +5526,23 @@ pub(crate) fn jsdom_file_construct(
callframe: &CallFrame,
) -> JsResult<*mut Blob> {
jsc::mark_binding();
let blob: Blob;
let args = callframe.arguments();

if args.len() < 2 {
return Err(global_this.throw_invalid_arguments(format_args!(
"new File(bits, name) expects at least 2 arguments"
)));
}
{
use bun_jsc::StringJsc as _;
// +1 WTF ref; `OwnedString` releases it at scope exit.
// Every consumer below either
// copies bytes (`to_owned_slice`) or takes its own ref (`dupe_ref`).
let name_value_str = OwnedString::new(BunString::from_js(args[1], global_this)?);

blob = Blob::get::<false, true>(global_this, args[0])?;
if let Some(store_) = blob.store.get() {
match store_.data_mut() {
store::Data::Bytes(bytes) => {
// `get::<_, true>` on a single-Blob sequence returns
// `dupe()` (a shared StoreRef), so this `Bytes` may already
// carry an owned `stored_name` from the source blob; the
// assignment drops (frees) the previous `Box<[u8]>`.
bytes.stored_name = name_value_str.to_owned_slice().into_boxed_slice();
}
store::Data::S3(_) | store::Data::File(_) => {
blob.name.set(name_value_str.dupe_ref());
}
}
} else if !name_value_str.is_empty() {
// not store but we have a name so we need a store
blob.store.set(Some(StoreRef::from(Store::new(Store {
data: store::Data::Bytes(store::Bytes::init_empty_with_name(
name_value_str.to_owned_slice().into_boxed_slice(),
)),
ref_count: bun_ptr::ThreadSafeRefCount::init(),
mime_type: bun_http_types::MimeType::NONE,
is_all_ascii: None,
}))));
}

let mut name = OwnedString::new(BunString::from_js(args[1], global_this)?);
if name.is_utf16() {
// USVString: the UTF-8 round trip replaces lone surrogates with U+FFFD.
let utf8 = name.to_utf8();
name = OwnedString::new(BunString::clone_utf8(utf8.slice()));
}
let blob = Blob::get::<false, true>(global_this, args[0])?;
// Not into the store: a single-Blob `bits` shares the source's store.
blob.name.set(name.into_inner());

let mut set_last_modified = false;

Expand Down Expand Up @@ -6345,9 +6322,9 @@ impl Any {
}
}

pub(crate) fn get_file_name(&self) -> Option<&[u8]> {
pub(crate) fn get_name_utf8(&self) -> Option<ZigStringSlice> {
match self {
Any::Blob(b) => b.get_file_name(),
Any::Blob(b) => b.get_name_utf8(),
Any::WTFStringImpl(_) | Any::InternalBlob(_) => None,
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/runtime/webcore/ObjectURLRegistry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,20 @@ const _: fn() = || {
impl Entry {
pub(crate) fn init(blob: &Blob) -> Box<Entry> {
Box::new(Entry {
blob: blob.dupe_with_content_type(true),
blob: dupe_with_private_name(blob),
})
}
}

/// A name impl reachable from JS may become that thread's atom, which no other thread may release.
fn dupe_with_private_name(blob: &Blob) -> Blob {
let copy = blob.dupe_with_content_type(true);
let mut name = copy.name.replace(bun_core::String::dead()).into_inner();
name.to_thread_safe();
copy.name.set(name);
copy
}

impl Drop for Entry {
fn drop(&mut self) {
self.blob.deinit();
Expand All @@ -70,7 +79,7 @@ impl ObjectURLRegistry {
let uuid = uuid_from_pathname(pathname)?;
let map = self.map.lock();
map.get(&uuid.bytes)
.map(|e| e.blob.dupe_with_content_type(true))
.map(|e| dupe_with_private_name(&e.blob))
}

pub(crate) fn resolve_and_dupe_to_js(
Expand Down
Loading
Loading