Skip to content
Open
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
14 changes: 14 additions & 0 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ pub(crate) trait BlobExt {
fn stat(&self, cx: &mut JsScope<'_>) -> JsResult<JSValue>;
fn get_size(&self) -> f64;
fn resolve_size(&self);
/// `resolve_size`, except a file whose `st_size` is 0 (procfs) keeps its window: that size is not a read budget.
fn resolve_size_for_stream(&self);
fn resolved_size(&self) -> (SizeType, SizeType);
fn constructor(
cx: &mut JsScope<'_>,
Expand Down Expand Up @@ -2064,6 +2066,18 @@ impl BlobExt for Blob {
}
}

fn resolve_size_for_stream(&self) {
let (offset, size) = (self.offset.get(), self.size.get());
self.resolve_size();
if let Some(store) = self.store.get()
&& let store::Data::File(file) = &store.data
&& file.max_size == 0
{
self.offset.set(offset);
self.size.set(size);
}
}

/// Non-mutating variant of [`resolve_size`]: returns the `(offset, size)` that
/// `resolve_size` would assign without touching `self`. For callers
/// (e.g. `ByteBlobLoader::setup`) that need the resolved size of a `Blob`
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/webcore/Body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,7 @@ impl Value {
Value::InternalBlob(_) | Value::Blob(_) | Value::WTFStringImpl(_) => {
// `deinit` must run on every exit incl. `?` paths.
let blob = scopeguard::guard(self.use_(), |mut b| b.deinit());
blob.resolve_size();
blob.resolve_size_for_stream();
let blob_size = blob.size.get();
ReadableStream::from_blob_copy_ref(cx, &blob, blob_size)?
}
Expand Down Expand Up @@ -832,7 +832,7 @@ impl Value {
Value::Blob(_) => {
let stream = {
let blob = scopeguard::guard(self.use_(), |mut b| b.deinit());
blob.resolve_size();
blob.resolve_size_for_stream();
if blob.needs_to_read_file() || blob.is_s3() {
let blob_size = blob.size.get();
let bytes = ReadableStream::from_blob_copy_ref(cx, &blob, blob_size)?;
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/webcore/wasm_streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ fn get_body_stream_or_bytes_for_wasm_streaming(
};
// `defer blob.detach()` — RAII via scopeguard.
let blob = scopeguard::guard(blob, |b: Blob| b.detach());
blob.resolve_size();
blob.resolve_size_for_stream();
let size = blob.size.get();
return ReadableStream::from_blob_copy_ref(&this.js_thread(context), &blob, size);
}
Expand Down
37 changes: 35 additions & 2 deletions test/js/web/fetch/body.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,18 +352,51 @@ for (const { body, fn } of bodyTypes) {
});
});

// A procfs file is a regular file whose st_size is 0. The body getter stats the
// file, and that cached stat must not turn a later read of the Bun.file() into "".
// A procfs file is a regular file whose st_size is 0. A stream that trusts that size
// as a byte budget ends before it reads anything, and the stat the body getter
// caches must not turn a later read of the Bun.file() into "".
describe.skipIf(!isLinux)("made from a procfs Bun.file()", () => {
const path = "/proc/version";

test("the body stream reads the whole file", async () => {
const expected = await Bun.file(path).text();
expect(expected.length).toBeGreaterThan(0);
const drain = async (stream: ReadableStream<Uint8Array>) => {
let text = "";
for await (const chunk of stream) text += Buffer.from(chunk).toString();
return text;
};
expect({
"for await": await drain(fn(Bun.file(path)).body!),
"Bun.readableStreamToText": await Bun.readableStreamToText(fn(Bun.file(path)).body!),
"new Response(body).text()": await new Response(fn(Bun.file(path)).body).text(),
"textStream()": (await Array.fromAsync(fn(Bun.file(path)).textStream())).join(""),
}).toEqual({
"for await": expected,
"Bun.readableStreamToText": expected,
"new Response(body).text()": expected,
"textStream()": expected,
});
});

test("the body getter does not make a later text() on the same Bun.file() empty", async () => {
const expected = await Bun.file(path).text();
expect(expected).not.toBe("");
const file = Bun.file(path);
expect(fn(file).body).toBeInstanceOf(ReadableStream);
expect(await file.text()).toBe(expected);
});

test("clone() after the body getter reads the whole file on both copies", async () => {
const expected = await Bun.file(path).text();
const original = fn(Bun.file(path));
expect(original.body).toBeInstanceOf(ReadableStream);
const clone = original.clone();
expect({ clone: await clone.text(), original: await original.text() }).toEqual({
clone: expected,
original: expected,
});
});
});
});
for (const { string, buffer } of utf8) {
Expand Down
10 changes: 9 additions & 1 deletion test/js/web/fetch/wasm-streaming.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { tmpdirSync } from "harness";
import { isLinux, tmpdirSync } from "harness";

import { ok } from "node:assert/strict";

Expand Down Expand Up @@ -213,6 +213,14 @@ describe("WebAssembly.compileStreaming", () => {
const response = await fetch("data:application/wasm,This is not actually Wasm");
expect(WebAssembly.compileStreaming(response)).rejects.toBeInstanceOf(WebAssembly.CompileError);
});

// /proc/version is a regular file whose st_size is 0, and it is not Wasm. The error shows that
// its bytes reached the compiler: a stream capped at st_size fails as "expected a module of at
// least 8 bytes" instead.
test.skipIf(!isLinux)("reads a procfs Bun.file() response to EOF", async () => {
const response = new Response(Bun.file("/proc/version"), { headers: { "Content-Type": "application/wasm" } });
expect(WebAssembly.compileStreaming(response)).rejects.toThrow("module doesn't start with '\\0asm'");
});
});

describe("WebAssembly.instantiateStreaming", () => {
Expand Down
Loading