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
2 changes: 1 addition & 1 deletion src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2792,7 +2792,7 @@ impl JSValue {
}

// ── Length introspection. ──────────────────────────
/// `JSValue.getLengthIfPropertyExistsInternal` — returns `f64::MAX` when
/// `JSValue.getLengthIfPropertyExistsInternal` — returns `+Infinity` when
/// no `length`-ish property exists. Do not call directly; prefer
/// [`JSValue::get_length`].
pub fn get_length_if_property_exists_internal(self, global: &JSGlobalObject) -> JsResult<f64> {
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2988,8 +2988,9 @@ double JSC__JSValue__getLengthIfPropertyExistsInternal(JSC::EncodedJSValue value

if (auto* blob = dynamicDowncast<Bun::Bindgen::Generated::JSBlob>(cell)) {
uint64_t size = Bun__Blob__getSizeForBindings(blob->wrapped());
// The size is unknown. NaN is not a number that a caller can compare or print as a length.
if (size == std::numeric_limits<uint64_t>::max())
return std::numeric_limits<double>::max();
return PNaN;
return static_cast<double>(size);
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,18 @@ impl Expect {
Ok((this, value, not))
}

/// For `toHaveLength()` and `toBeEmpty()`: the length probe answers NaN for a Blob whose size is unknown.
pub(crate) fn throw_nan_length(global: &JSGlobalObject, value: JSValue) -> JsError {
if crate::webcore::blob::js::from_js(value).is_none() {
return global.throw(format_args!("Received value has non-number length property: NaN"));
}
let mut formatter = make_formatter(global);
global.throw(format_args!(
"Received value is a Blob whose size is not known: {}",
value.to_fmt(&mut formatter),
Comment on lines +1640 to +1647

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.

🔴 bun:test users whose suites call .not.toBeEmpty() or .not.toHaveLength(n) on a slice of an existing regular file now get a thrown error where the base passes. The helper at src/runtime/test_runner/expect.rs:1640 treats every NaN from the length probe as an unknown size, but get_size_for_bindings (src/runtime/webcore/Blob.rs:1925) also answers u64::MAX for Bun.file(path).slice(a, b) of a regular file that nothing has stat'd yet. Fix: before reporting unknown size, resolve seekability for a file-backed slice whose seekable is unset (one stat on the store) so a regular-file slice keeps its known length, and only truly unsizable stores (missing path, directory, FIFO) throw. [also at: src/jsc/bindings/bindings.cpp:2993 - Test authors who slice a regular Bun.file() before anything stats it now get a throw from toHaveLength()/toBeEmpty() where the negated forms passed before.]

Why this was flagged

A test does const s = Bun.file("data.bin").slice(0, 10); expect(s).not.toBeEmpty(); without reading .size on the parent file first. Bun__Blob__getSizeForBindings goes to get_size_for_bindings at src/runtime/webcore/Blob.rs:1913; has_size is true for a slice, file.seekable is None and mode_seen_by_clone is None, so src/runtime/webcore/Blob.rs:1934 returns u64::MAX. The C++ probe at src/jsc/bindings/bindings.cpp:2993 now returns PNaN instead of DBL_MAX. toBeEmpty at src/runtime/test_runner/expect/toBeEmpty.rs:78 and toHaveLength at src/runtime/test_runner/expect/toHaveLength.rs:61 call Expect::throw_nan_length, which finds the value is a Blob and throws "Received value is a Blob whose size is not known: FileRef ("data.bin")". On the base branch the length was DBL_MAX, so .not.toBeEmpty() and .not.toHaveLength(10) passed (and were correct). The toHaveLength test block at test/js/bun/test/expect.test.js:2667 only covers slices of a missing path and a directory, not of a regular file, so nothing pins this case.

Verification: The C++ probe (src/jsc/bindings/bindings.cpp:2990-2993) now maps that u64::MAX to PNaN instead of DBL_MAX; toBeEmpty.rs:77-78 and toHaveLength.rs:60-61 then call Expect::throw_nan_length (expect.rs:1640-1649), which, since from_js(value) is a Blob, throws "Received value is a Blob whose size is not known: FileRef (...)" — before not is applied.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The regression is real: on main .not.toBeEmpty() passes on such a slice, and with this PR it throws. It is now the first line of the description, as a choice for a maintainer. No code change in this push.

I built the suggested stat before I opened the PR, and removed it. A slice that exists before the stat is not cut to the file, so the stat gives the slice its window, not the bytes that are in the file. First use of Bun.file(path).slice(a, b), release builds:

slice bytes that a read returns toHaveLength(10) with the stat this PR
slice(0, 10) of 20 bytes 10 passes throws
slice(0, 10) of an empty file 0 passes throws
slice(0, 10) of 5 bytes 5 passes throws
slice(30, 40) of 20 bytes 0 passes throws

Three of the four are silent passes of a wrong length. Main gives the same answers when file.size runs after slice(), and the right ones when it runs before. The stat also kept its result on the store that a slice shares with its Bun.file(): a Bun.file(fd) reported size 20 after the file grew to 100.

A right answer needs the window cut to st_size. That decides what size a file slice has, and #44504 left that open on purpose: an empty file and a procfs file give the same stat. #45859 and #39763 change the same state. So this PR reports what the Blob layer knows about such a slice, which is nothing. console.log of a Response over it prints no size for the same reason.

A test has two exits: expect(slice.size).toBe(n), or a read of file.size before the matcher. If a maintainer prefers the stat, it is a small change on top of this one.

))
}

pub(crate) fn unreachable(cx: &mut JsScope<'_>, message: JsLocal<'_>) -> JsResult<()> {
let global_this = cx.global();
let arg = message.to_js();
Expand Down
5 changes: 1 addition & 4 deletions src/runtime/test_runner/expect/toBeEmpty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ impl Expect {
);
}
} else if actual_length.is_nan() {
return Err(global.throw(format_args!(
"Received value has non-number length property: {}",
actual_length
)));
return Err(Expect::throw_nan_length(global, value));
} else {
pass = actual_length == 0.0;
}
Expand Down
5 changes: 1 addition & 4 deletions src/runtime/test_runner/expect/toHaveLength.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ impl Expect {
value.to_fmt(&mut fmt),
)));
} else if actual_length.is_nan() {
return Err(global.throw(format_args!(
"Received value has non-number length property: {}",
actual_length,
)));
return Err(Expect::throw_nan_length(global, value));
}

if actual_length == expected_length {
Expand Down
61 changes: 61 additions & 0 deletions test/js/bun/test/expect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2627,6 +2627,67 @@ describe("expect()", () => {
// WeakMap
expect(new WeakMap([[globalThis, 1]])).toHaveLength(1);
});

// The length of a Blob is its size. A file that does not exist, a directory and a pipe have none.
describe("toHaveLength() and toBeEmpty() on a Blob of unknown size", () => {
const { isWindows, tempDir } = require("harness");
const { join } = require("path");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const unknownSize = "Received value is a Blob whose size is not known: FileRef";

test.each(
/** @type {[string, (dir: string) => string][]} */ ([
["a path that does not exist", dir => join(dir, "missing")],
["a directory", dir => dir],
...(isWindows
? []
: [
[
"a FIFO",
dir => {
const fifo = join(dir, "fifo");
require("mkfifo").mkfifo(fifo);
return fifo;
},
],
]),
]),
)("Bun.file() of %s", async (_kind, pathIn) => {
using dir = tempDir("expect-unknown-size", {});
const file = Bun.file(pathIn(String(dir)));

expect(() => expect(file).toHaveLength(0)).toThrow(unknownSize);
expect(() => expect(file).toHaveLength(100)).toThrow(unknownSize);
expect(() => expect(file).toHaveLength(Number.MAX_VALUE)).toThrow(unknownSize);
expect(() => expect(file).not.toHaveLength(0)).toThrow(unknownSize);
expect(() => expect(file).toBeEmpty()).toThrow(unknownSize);
expect(() => expect(file).not.toBeEmpty()).toThrow(unknownSize);
await expectFailure(() => expect(Promise.resolve(file)).resolves.not.toHaveLength(0)).toThrow(unknownSize);
});

test.each(
/** @type {[string, (dir: string) => string][]} */ ([
["a path that does not exist", dir => join(dir, "missing")],
["a directory", dir => dir],
]),
)("a slice of the Bun.file() of %s", (_kind, pathIn) => {
using dir = tempDir("expect-unknown-size", {});
const slice = Bun.file(pathIn(String(dir))).slice(0, 10);

expect(() => expect(slice).toHaveLength(10)).toThrow(unknownSize);
expect(() => expect(slice).not.toHaveLength(10)).toThrow(unknownSize);
expect(() => expect(slice).toBeEmpty()).toThrow(unknownSize);
expect(() => expect(slice).not.toBeEmpty()).toThrow(unknownSize);
});

test("a length that is a number is not taken for an unknown size", () => {
expect({ length: Number.MAX_VALUE }).toHaveLength(Number.MAX_VALUE);
expect({ length: Number.MAX_VALUE }).not.toBeEmpty();
expect(() => expect({ length: NaN }).toHaveLength(0)).toThrow(
"Received value has non-number length property: NaN",
);
expect(() => expect({ length: NaN }).toBeEmpty()).toThrow("Received value has non-number length property: NaN");
});
});
}

test.each([
Expand Down
Loading