Repository navigation
Blob: read file-backed parts in multi-part new Blob([...]) #33600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2f30b03
fe3305e
b00b6db
80b64bd
0832a6d
14a5bbd
ff530e7
3184042
dd1127f
a148e68
df1e85b
735123a
ecbd8ec
c38f6b3
e170412
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3033,18 +3033,23 @@ impl BlobExt for Blob { | |
| if let Some(blob) = item.as_class_ref::<Blob>() { | ||
| could_have_non_ascii = could_have_non_ascii | ||
| || blob.charset.get() != strings::AsciiStatus::AllAscii; | ||
| let view = blob.shared_view(); | ||
| if view.is_empty() { | ||
| if blob.needs_to_read_file() || blob.is_s3() { | ||
| push_file_part(blob, &mut joiner, global)?; | ||
| } | ||
| continue; | ||
|
robobun marked this conversation as resolved.
Resolved
|
||
| } | ||
| // A later part may run user JS that drops the | ||
| // last ref to this Blob's Store before `done()`. | ||
| if parts_can_run_js { | ||
| joiner.push_cloned(blob.shared_view()); | ||
| joiner.push_cloned(view); | ||
| } else { | ||
| // SAFETY: the prescan above proved no | ||
| // remaining part can run user JS, so this | ||
| // Blob (rooted via `_keep`/`arg`) keeps its | ||
| // Store alive until `joiner.done()` below. | ||
| joiner.push(unsafe { | ||
| bun_ptr::detach_lifetime(blob.shared_view()) | ||
| }); | ||
| joiner.push(unsafe { bun_ptr::detach_lifetime(view) }); | ||
| } | ||
| continue; | ||
| } else { | ||
|
|
@@ -3072,7 +3077,12 @@ impl BlobExt for Blob { | |
| // This arm only handles entries deferred onto the walk | ||
| // stack; other pending entries may still run user JS and | ||
| // free this Blob's Store before `done()`, so always copy. | ||
| joiner.push_cloned(blob.shared_view()); | ||
| let view = blob.shared_view(); | ||
| if !view.is_empty() { | ||
| joiner.push_cloned(view); | ||
| } else if blob.needs_to_read_file() || blob.is_s3() { | ||
| push_file_part(blob, &mut joiner, global)?; | ||
| } | ||
|
Comment on lines
+3080
to
+3085
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 nit (optional): maintainers get a second copy of the file-part dispatch in an arm no input can reach, so it is untested and will drift from the live one. Nothing ever pushes onto Why this was flaggedThe loop at Blob.rs:2948 only sees Verification: The Blob branch of the top-level
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. This arm is dead on main too: nothing pushes onto The next push to this branch puts the line of main back here ( |
||
| } else { | ||
| let utf8 = current.to_utf8(global)?; | ||
| could_have_non_ascii = could_have_non_ascii || utf8.is_owned(); | ||
|
|
@@ -3266,6 +3276,159 @@ impl BlobExt for Blob { | |
| } | ||
| } | ||
|
|
||
| /// The null device has no bytes, and a read of it cannot block. | ||
| fn is_null_device(stat: &bun_sys::Stat) -> bool { | ||
| if !bun_sys::S::ISCHR(stat.st_mode as _) { | ||
| return false; | ||
| } | ||
| // Linux fixes the null device at major 1, minor 3, so a sandbox with no /dev still finds it. | ||
| #[cfg(any(target_os = "linux", target_os = "android"))] | ||
| { | ||
| stat.st_rdev == libc::makedev(1, 3) | ||
| } | ||
| #[cfg(not(any(target_os = "linux", target_os = "android")))] | ||
| { | ||
| #[cfg(windows)] | ||
| let null_device = bun_core::zstr!("\\\\.\\NUL"); | ||
| #[cfg(not(windows))] | ||
| let null_device = bun_core::zstr!("/dev/null"); | ||
| bun_sys::stat(null_device).is_ok_and(|null| null.st_rdev == stat.st_rdev) | ||
| } | ||
| } | ||
|
|
||
| /// Reads a file-backed part of a multi-part `new Blob([...])`: `shared_view()` is empty for it. | ||
| #[cold] | ||
| #[inline(never)] | ||
| fn push_file_part( | ||
| blob: &Blob, | ||
| joiner: &mut bun_core::string_joiner::StringJoiner, | ||
| global: &JSGlobalObject, | ||
| ) -> JsResult<()> { | ||
| let Some(store) = blob.store.get() else { | ||
| return Ok(()); | ||
| }; | ||
| let file = match &store.data { | ||
| store::Data::Bytes(_) => return Ok(()), | ||
| store::Data::S3(_) => { | ||
| return Err(global.throw_invalid_arguments(format_args!( | ||
| "Blob parts backed by S3 cannot be read synchronously; await .bytes() or .arrayBuffer() first" | ||
| ))); | ||
|
robobun marked this conversation as resolved.
Resolved
|
||
| } | ||
| store::Data::File(file) => file, | ||
| }; | ||
| let throw = |err: bun_sys::Error| { | ||
| let err = match &file.pathlike { | ||
| PathOrFileDescriptor::Path(path) => err.with_path(path.slice()), | ||
| PathOrFileDescriptor::Fd(fd) => err.with_fd(*fd), | ||
| }; | ||
| global.throw_value(err.to_js(global)) | ||
| }; | ||
|
|
||
| let no_bytes_or_refused = |stat: &bun_sys::Stat| -> JsResult<()> { | ||
| if is_null_device(stat) { | ||
| return Ok(()); | ||
| } | ||
| Err(global.throw_invalid_arguments(format_args!( | ||
| "Blob parts backed by a pipe, socket or device cannot be read synchronously; await .bytes() or .arrayBuffer() first" | ||
| ))) | ||
| }; | ||
| let is_file_or_dir = |mode| bun_sys::S::ISREG(mode) || bun_sys::S::ISDIR(mode); | ||
|
|
||
| let (fd, _close_on_drop) = match &file.pathlike { | ||
| PathOrFileDescriptor::Fd(fd) => (*fd, None), | ||
| PathOrFileDescriptor::Path(path) => { | ||
| let mut path_buf = bun_paths::path_buffer_pool::get(); | ||
| let path = path.slice_z(&mut path_buf); | ||
| // open(2) of a FIFO wakes the writer that waits for a reader, so refuse it unopened. | ||
| match bun_sys::stat(path) { | ||
| Ok(stat) if !is_file_or_dir(stat.st_mode as _) => { | ||
| return no_bytes_or_refused(&stat); | ||
| } | ||
| _ => {} | ||
| } | ||
| // O_NONBLOCK: if the path is a FIFO by now, open(2) must not block the JS thread. | ||
| let flags = bun_sys::O::RDONLY | ||
| | bun_sys::O::NONBLOCK | ||
| | bun_sys::O::CLOEXEC | ||
| | bun_sys::O::NOCTTY; | ||
| match bun_sys::File::open(path, flags, 0) { | ||
| Ok(opened) => (opened.handle(), Some(opened)), | ||
| Err(err) => return Err(throw(err)), | ||
| } | ||
| } | ||
| }; | ||
| let stat = match bun_sys::fstat(fd) { | ||
| Ok(stat) => stat, | ||
| Err(err) => return Err(throw(err)), | ||
| }; | ||
| if bun_sys::S::ISDIR(stat.st_mode as _) { | ||
| return Err(throw(bun_sys::Error::from_code( | ||
| bun_sys::E::EISDIR, | ||
| bun_sys::Tag::read, | ||
| ))); | ||
| } | ||
| if !bun_sys::S::ISREG(stat.st_mode as _) { | ||
| return no_bytes_or_refused(&stat); | ||
| } | ||
|
|
||
| let offset = blob.offset.get(); | ||
| let size = blob.size.get(); | ||
| let file_len = stat.st_size.max(0) as SizeType; | ||
| // st_size==0 may be a virtual file (procfs): grow-until-EOF instead of capping at 0. | ||
| let avail = if file_len > 0 { | ||
| file_len.saturating_sub(offset) as usize | ||
| } else { | ||
| usize::MAX | ||
| }; | ||
| let cap = if size != MAX_SIZE { | ||
| (size as usize).min(avail) | ||
| } else { | ||
| avail | ||
| }; | ||
| if cap == 0 { | ||
| return Ok(()); | ||
| } | ||
| // The join holds a part twice (this buffer, then the joined one), so half the RAM is the most that fits. | ||
| if file_len > 0 && cap >= bun_core::get_total_memory_size() / 2 { | ||
| return Err(throw(bun_sys::Error::oom())); | ||
| } | ||
|
robobun marked this conversation as resolved.
Resolved
|
||
| let initial = if file_len > 0 { cap } else { 8192.min(cap) }.min(8 << 30); | ||
| let mut buf: Vec<u8> = Vec::new(); | ||
| if buf.try_reserve_exact(initial).is_err() { | ||
| return Err(throw(bun_sys::Error::oom())); | ||
| } | ||
| buf.resize(initial, 0); | ||
|
robobun marked this conversation as resolved.
Resolved
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| let mut total = 0usize; | ||
| loop { | ||
| if total == buf.len() { | ||
| if total == cap { | ||
| break; | ||
| } | ||
| let new_len = buf.len().saturating_mul(2).min(cap); | ||
| if buf.try_reserve(new_len - buf.len()).is_err() { | ||
| return Err(throw(bun_sys::Error::oom())); | ||
| } | ||
| buf.resize(new_len, 0); | ||
| } | ||
| // pread, not read: the cursor of a caller's fd must not decide what a repeated part reads. | ||
| let n = match bun_sys::pread( | ||
| fd, | ||
| &mut buf[total..], | ||
| (offset as i64).saturating_add(total as i64), | ||
| ) { | ||
| Ok(n) => n, | ||
| Err(err) => return Err(throw(err)), | ||
| }; | ||
| if n == 0 { | ||
| break; | ||
| } | ||
| total += n; | ||
| } | ||
| buf.truncate(total); | ||
| joiner.push_owned(buf.into_boxed_slice()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| // ────────────────────────────────────────────────────────────────────────── | ||
| // Basic accessors | ||
| // ────────────────────────────────────────────────────────────────────────── | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.