Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions src/jsc/webcore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,8 @@ pub mod store {
pub max_size: SizeType,
/// Milliseconds since ECMAScript epoch.
pub last_modified: crate::JSTimeType,
/// `st_mode` as a body's `clone()` saw it, kept apart from what the `Bun.file()` sharing this store answers from.
pub mode_seen_by_clone: Option<bun_sys::Mode>,
}

impl Default for File {
Expand All @@ -803,6 +805,7 @@ pub mod store {
seekable: None,
max_size: MAX_SIZE,
last_modified: crate::INIT_TIMESTAMP,
mode_seen_by_clone: None,
}
}
}
Expand Down
52 changes: 48 additions & 4 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,8 +1254,13 @@ impl BlobExt for Blob {
}

fn get_exists_sync(&self) -> JSValue {
if self.size.get() == MAX_SIZE {
self.resolve_size();
match self.store.get() {
_ if self.size.get() == MAX_SIZE => self.resolve_size(),
// A failed stat is not an answer to keep: the file may exist by now.
Some(store) if matches!(&store.data, store::Data::File(file) if file.seekable.is_none()) => {
resolve_file_stat(store)
}
_ => {}
}

// If there's no store that means it's empty and we just return true
Expand Down Expand Up @@ -1996,15 +2001,26 @@ impl BlobExt for Blob {
}

fn get_size_for_bindings(&self) -> u64 {
if self.size.get() == MAX_SIZE {
let has_size = self.size.get() != MAX_SIZE;
if !has_size {
self.resolve_size();
}

// If the file doesn't exist or is not seekable
// signal that the size is unknown.
if let Some(store) = self.store.get() {
if let store::Data::File(file) = &store.data {
if !file.seekable.unwrap_or(false) {
// A slice has its size without a stat, and `clone()` learns the mode without filling `seekable`.
let mode_seen_by_clone = file.mode_seen_by_clone.filter(|_| has_size);
if !file
.seekable
.or_else(|| mode_seen_by_clone.map(bun_sys::S::ISREG))
.unwrap_or(false)
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
{
// Printing is not to leave the 0 of a failed stat behind: the body would read as empty.
if !has_size && file.seekable.is_none() {
self.size.set(MAX_SIZE);
}
return u64::MAX;
}
}
Expand Down Expand Up @@ -5836,6 +5852,34 @@ pub(crate) fn store_reads_repeatably(store: &RefPtr<Store>) -> bool {
}
}

/// Whether two Blobs over `store` would compete for its bytes (an fd, a pipe, a terminal). A directory or a closed fd has none: each Blob fails when read.
pub(crate) fn store_yields_bytes_once(store: &RefPtr<Store>) -> bool {
if !matches!(store.data, store::Data::File(_)) {
return false;
}
let is_fd = Store::data_mut(store).as_file().pathlike.is_fd();
// What the tee of an fd does next, so this costs no extra `fstat`.
if is_fd && Store::data_mut(store).as_file().seekable.is_none() {
resolve_file_stat(store);
}
let file = Store::data_mut(store).as_file_mut();
let mode = if file.seekable.is_some() {
Some(file.mode)
} else if let PathOrFileDescriptor::Path(path) = &file.pathlike {
// Not `resolve_file_stat`: the `Bun.file()` sharing `store` answers from what that caches.
if file.mode_seen_by_clone.is_none() {
let mut buffer = bun_paths::path_buffer_pool::get();
if let bun_sys::Result::Ok(stat) = bun_sys::stat(path.slice_z(&mut buffer)) {
file.mode_seen_by_clone = Some(stat.st_mode as bun_sys::Mode);
}
}
file.mode_seen_by_clone
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
} else {
None
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
mode.is_some_and(|mode| !bun_sys::S::ISDIR(mode) && (is_fd || !bun_sys::S::ISREG(mode)))
}

// ──────────────────────────────────────────────────────────────────────────
// toStringWithBytes / toString / toJSON / toFormData / toArrayBuffer{View}
// ──────────────────────────────────────────────────────────────────────────
Expand Down
16 changes: 11 additions & 5 deletions src/runtime/webcore/Body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ impl Body {
unsafe { self.value.get_mut() }
}

pub(crate) fn len(&self) -> blob::SizeType {
self.value_mut().size()
pub(crate) fn known_len(&self) -> Option<usize> {
self.value_mut().known_size()
}
}

Expand Down Expand Up @@ -767,6 +767,14 @@ impl Value {
}
}

/// `None` for a file that does not exist or is not seekable.
pub(crate) fn known_size(&mut self) -> Option<usize> {
match self.size() {
u64::MAX => None,
size => Some(size as usize),
}
}

pub(crate) fn memory_cost(&self) -> usize {
match self {
Value::InternalBlob(b) => b.memory_cost(),
Expand Down Expand Up @@ -1571,9 +1579,7 @@ impl Value {
}

if let Value::Blob(b) = self {
if b.store()
.is_some_and(|store| !blob::store_reads_repeatably(store))
{
if b.store().is_some_and(blob::store_yields_bytes_once) {
// A pipe or other fd yields its bytes once: read it as one
// stream and tee that.
self.to_readable_stream(cx)?;
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
Expand Down
15 changes: 9 additions & 6 deletions src/runtime/webcore/Request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,15 @@ impl Request {
} else {
"BunRequest"
};
writeln!(
writer,
"{} ({}) {{",
class_label,
bun_fmt::size(self.body_value_mut().size() as usize, Default::default())
)?;
match self.body_value_mut().known_size() {
Some(size) => writeln!(
writer,
"{} ({}) {{",
class_label,
bun_fmt::size(size, Default::default())
)?,
None => writeln!(writer, "{} {{", class_label)?,
}
{
// RAII guard restores indent on every exit incl. `?` error paths.
// Shadows `formatter` for the block; auto-derefs to `&mut F`.
Expand Down
17 changes: 8 additions & 9 deletions src/runtime/webcore/Response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,6 @@ impl Response {

impl Response {
#[inline]
pub(crate) fn get_body_len(&self) -> usize {
self.body.get().len() as usize
}

pub(crate) fn get_form_data_encoding(
&self,
) -> JsResult<Option<Box<bun_core::form_data::AsyncFormData>>> {
Expand Down Expand Up @@ -646,11 +642,14 @@ impl Response {
// `fmt::Error`.
let js_err = |_: JsError| core::fmt::Error;

writeln!(
writer,
"Response ({}) {{",
bun_core::fmt::size(self.get_body_len(), Default::default())
)?;
match self.body.get().known_len() {
Some(len) => writeln!(
writer,
"Response ({}) {{",
bun_core::fmt::size(len, Default::default())
)?,
None => writeln!(writer, "Response {{")?,
}

{
let mut formatter = formatter.indented();
Expand Down
15 changes: 14 additions & 1 deletion test/js/bun/util/bun-file-exists.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { write } from "bun";
import { expect, test } from "bun:test";
import { unlinkSync } from "fs";
import { unlinkSync, writeFileSync } from "fs";
import { tempDir } from "harness";
import { tmpdir } from "os";
import { join } from "path";
test("bun-file-exists", async () => {
Expand All @@ -18,3 +19,15 @@ test("bun-file-exists", async () => {
unlinkSync(temp);
expect(await Bun.file(temp).exists()).toBeFalse();
});

test.each([
["exists()", file => file.exists()],
["size", file => file.size],
])("one Bun.file() sees a file that is created after %s found none", async (_, look) => {
using dir = tempDir("bun-file-exists-later", {});
const file = Bun.file(join(String(dir), "later.txt"));
await look(file);
expect(await file.exists()).toBeFalse();
writeFileSync(file.name, "boop");
expect(await file.exists()).toBeTrue();
});
28 changes: 28 additions & 0 deletions test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,34 @@ it("Blob inspect", () => {
}`);
});

it("Response and Request print no size for a Bun.file() body whose size is unknown", () => {
using dir = tempDir("inspect-unknown-size", {});
const firstLines = file => [
Bun.inspect(new Response(file)).split("\n")[0],
Bun.inspect(new Request("http://example.com/", { method: "POST", body: file })).split("\n")[0],
];
expect({
missing: firstLines(Bun.file(join(String(dir), "missing.txt"))),
directory: firstLines(Bun.file(String(dir))),
}).toEqual({
missing: ["Response {", "Request {"],
directory: ["Response {", "Request {"],
});
});

it("printing a Response over a Bun.file() that does not exist yet leaves its body as it is", async () => {
using dir = tempDir("inspect-missing-then-created", {});
const path = join(String(dir), "later.txt");
const response = new Response(Bun.file(path));
const before = Bun.inspect(response).split("\n")[0];
await Bun.write(path, "created");
expect({ before, after: Bun.inspect(response).split("\n")[0], text: await response.text() }).toEqual({
before: "Response {",
after: "Response (7 bytes) {",
text: "created",
});
});

it("utf16 property name", () => {
var { Database } = require("bun:sqlite");
const db = Database.open(":memory:");
Expand Down
Loading
Loading