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/runtime/server/ServerConfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,7 @@ impl ServerConfig {

if let Some(value) = arg.get(global, "idleTimeout")? {
if !value.is_undefined_or_null() {
if !value.is_any_int() {
if !value.is_integer() {
return Err(global.throw_invalid_arguments(format_args!(
"Bun.serve expects idleTimeout to be an integer",
)));
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/server/WebSocketServerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub(crate) fn on_create(

if let Some(value) = object.get(global_object, "maxPayloadLength")? {
if !value.is_undefined_or_null() {
if !value.is_any_int() {
if !value.is_integer() {
return Err(global_object.throw_invalid_arguments(format_args!(
"websocket expects maxPayloadLength to be an integer"
)));
Expand All @@ -312,7 +312,7 @@ pub(crate) fn on_create(

if let Some(value) = object.get(global_object, "idleTimeout")? {
if !value.is_undefined_or_null() {
if !value.is_any_int() {
if !value.is_integer() {
return Err(global_object.throw_invalid_arguments(format_args!(
"websocket expects idleTimeout to be an integer"
)));
Expand All @@ -336,7 +336,7 @@ pub(crate) fn on_create(
}
if let Some(value) = object.get(global_object, "backpressureLimit")? {
if !value.is_undefined_or_null() {
if !value.is_any_int() {
if !value.is_integer() {
return Err(global_object.throw_invalid_arguments(format_args!(
"websocket expects backpressureLimit to be an integer"
)));
Expand Down
88 changes: 52 additions & 36 deletions src/runtime/socket/socket_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3005,50 +3005,20 @@ impl<const SSL: bool> NewSocket<SSL> {
if offset_value.is_undefined() {
break 'brk 0;
}
if !offset_value.is_any_int() {
let _ = global.throw_invalid_argument_type(label, "byteOffset", "integer");
let Some(offset) = byte_count_arg(global, offset_value, label, "byteOffset") else {
return WriteResult::Fail;
}
let i = offset_value.to_int64();
if i < 0 {
let _ = global.throw_range_error(
i,
jsc::RangeErrorOptions {
field_name: b"byteOffset",
min: 0,
max: jsc::MAX_SAFE_INTEGER,
msg: b"",
},
);
return WriteResult::Fail;
}
usize::try_from(i).expect("int cast")
};
offset
};

let byte_length: usize = 'brk: {
if length_value.is_undefined() {
break 'brk buffer.slice().len();
}
if !length_value.is_any_int() {
let _ = global.throw_invalid_argument_type(label, "byteLength", "integer");
return WriteResult::Fail;
}

let l = length_value.to_int64();

if l < 0 {
let _ = global.throw_range_error(
l,
jsc::RangeErrorOptions {
field_name: b"byteLength",
min: 0,
max: jsc::MAX_SAFE_INTEGER,
msg: b"",
},
);
let Some(length) = byte_count_arg(global, length_value, label, "byteLength") else {
return WriteResult::Fail;
}
usize::try_from(l).expect("int cast")
};
length
};

let mut bytes = buffer.slice();
Expand Down Expand Up @@ -4180,6 +4150,52 @@ enum WriteResult {
Success { wrote: i32, total: usize },
}

/// `byteOffset` / `byteLength` of `write()` and `end()`, read by value; `None` means it threw.
#[inline]
fn byte_count_arg(
global: &JSGlobalObject,
value: JSValue,
label: &'static str,
field: &'static str,
) -> Option<usize> {
if value.is_int32() {
if let Ok(count) = usize::try_from(value.as_int32()) {
return Some(count);
}
} else if let Some(number) = value.get_number() {
if number >= 0.0 && number <= jsc::MAX_SAFE_INTEGER as f64 && number.trunc() == number {
return Some(number as usize);
}
}
throw_byte_count_arg(global, value, label, field);
None
}

#[cold]
#[inline(never)]
fn throw_byte_count_arg(
global: &JSGlobalObject,
value: JSValue,
label: &'static str,
field: &'static str,
) {
let _ = match value
.get_number()
.filter(|n| n.is_finite() && n.trunc() == *n)
{
Some(number) => global.throw_range_error(
number,
jsc::RangeErrorOptions {
field_name: field.as_bytes(),
min: 0,
max: jsc::MAX_SAFE_INTEGER,
msg: b"",
},
),
None => global.throw_invalid_argument_type(label, field, "integer"),
};
}

pub(crate) struct StoredVerifyError {
pub code: Box<[u8]>,
pub reason: Box<[u8]>,
Expand Down
28 changes: 28 additions & 0 deletions src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3037,6 +3037,34 @@ pub(crate) mod mock {
Ok(value.get(global_this, "value")?.unwrap_or(JSValue::UNDEFINED))
}

/// Jest's `ensureExpectedIsNonNegativeInteger` for `toHaveBeenCalledTimes` / `toHaveReturnedTimes`.
pub(crate) fn expected_count(
global: &JSGlobalObject,
arguments: &[JSValue],
matcher_name: &'static str,
) -> JsResult<i64> {
if let Some(&expected) = arguments.first() {
if expected.is_int32() {
if expected.as_int32() >= 0 {
return Ok(i64::from(expected.as_int32()));
}
} else if let Some(count) = expected.get_number() {
if count >= 0.0 && count <= bun_jsc::MAX_SAFE_INTEGER as f64 && count.trunc() == count {
return Ok(count as i64);
}
}
}
Err(expected_count_error(global, matcher_name))
}

#[cold]
#[inline(never)]
fn expected_count_error(global: &JSGlobalObject, matcher_name: &'static str) -> JsError {
global.throw_invalid_arguments(format_args!(
"{matcher_name}() requires 1 non-negative integer argument"
))
}

// split lifetimes — `&'a mut Formatter<'a>` is the invariant-borrow trap
// (forces the &mut to live as long as the Formatter's own param, which outlives the
// local). `'g` tracks the JSGlobalObject borrow inside Formatter; `'a` is the short
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/test_runner/expect/toBeArrayOfSize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub(crate) fn to_be_array_of_size(
let size = arguments[0];
size.ensure_still_alive();

if !size.is_any_int() {
if !size.is_integer() {
return Err(global.throw(format_args!("toBeArrayOfSize() requires the first argument to be a number")));
}

Expand Down
8 changes: 1 addition & 7 deletions src/runtime/test_runner/expect/toHaveBeenCalledTimes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,7 @@ pub(crate) fn to_have_been_called_times(
super::mock::MockKind::Calls,
)?;

if arguments.len() < 1 || !arguments[0].is_uint32_as_any_int() {
return Err(global.throw_invalid_arguments(format_args!(
"toHaveBeenCalledTimes() requires 1 non-negative integer argument"
)));
}

let times = arguments[0].to_int64();
let times = super::mock::expected_count(global, arguments, "toHaveBeenCalledTimes")?;

let mut pass = calls.get_length(global)? as i64 == times;

Expand Down
13 changes: 3 additions & 10 deletions src/runtime/test_runner/expect/toHaveReturned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn to_have_returned_times_fn(
)?;
let mut returns = returns_arr.array_iterator(global)?;

let expected_success_count: i32 = if mode == Mode::ToHaveReturned {
let expected_success_count: i64 = if mode == Mode::ToHaveReturned {
if arguments.len() > 0 && !arguments[0].is_undefined() {
return Err(global.throw_invalid_arguments(format_args!(
"{}() must not have an argument",
Expand All @@ -48,19 +48,12 @@ fn to_have_returned_times_fn(
}
1
} else {
if arguments.len() < 1 || !arguments[0].is_uint32_as_any_int() {
return Err(global.throw_invalid_arguments(format_args!(
"{}() requires 1 non-negative integer argument",
mode.tag_name()
)));
}

arguments[0].coerce::<i32>(global)?
mock::expected_count(global, arguments, mode.tag_name())?
};

let mut pass;

let mut actual_success_count: i32 = 0;
let mut actual_success_count: i64 = 0;
let mut total_call_count: i32 = 0;
while let Some(item) = returns.next()? {
match mock::jest_mock_return_object_type(global, item)? {
Expand Down
70 changes: 70 additions & 0 deletions test/js/bun/http/bun-serve-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,76 @@ describe("Bun.serve websocket options", () => {
);
});

describe("Bun.serve integer options are read by value", () => {
// -0 is an integer that ordinary arithmetic produces, and it equals 0.
const negativeZero = Math.round(-0.2);
const fetch = () => new Response("ok");
const websocket = (option: string) => (value: unknown) => ({ websocket: { message() {}, [option]: value } });

// `max` is the largest value the option takes. The two options without one clamp at 2^32 - 1.
const integerOptions: [name: string, expects: string, max: number | null, withValue: (value: unknown) => object][] = [
["idleTimeout", "Bun.serve expects idleTimeout", 255, idleTimeout => ({ idleTimeout })],
["websocket.idleTimeout", "websocket expects idleTimeout", 960, websocket("idleTimeout")],
["websocket.maxPayloadLength", "websocket expects maxPayloadLength", null, websocket("maxPayloadLength")],
["websocket.backpressureLimit", "websocket expects backpressureLimit", null, websocket("backpressureLimit")],
];

describe.each(integerOptions)("%s", (_name, expects, max, withValue) => {
const notAnInteger = { name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message: `${expects} to be an integer` };
const aboveMax = max === null ? "accepted" : { ...notAnInteger, message: `${expects} to be ${max} or less` };
const limit = max ?? 2 ** 32 - 1;

function outcome(value: unknown) {
try {
serve({ port: 0, fetch, ...withValue(value) }).stop(true);
return "accepted";
} catch (e: any) {
return { name: e.name, code: e.code, message: e.message };
}
}

test("takes -0 and every other whole number, and refuses what is not one", () => {
const cases: [value: unknown, expected: unknown][] = [
[0, "accepted"],
[negativeZero, "accepted"],
[limit, "accepted"],
// A negative value means 0, whatever its size.
[-1, "accepted"],
[-(2 ** 53), "accepted"],
[limit + 1, aboveMax],
[2 ** 51, aboveMax],
[2 ** 53, aboveMax],
[1e300, aboveMax],
[1.5, notAnInteger],
[NaN, notAnInteger],
[Infinity, notAnInteger],
[-Infinity, notAnInteger],
["5", notAnInteger],
[true, notAnInteger],
[5n, notAnInteger],
[{}, notAnInteger],
];
expect(cases.map(([value]) => [value, outcome(value)])).toEqual(cases);
});
});

test("-0 for every option in one call, and again in reload()", () => {
const all = () => ({
idleTimeout: negativeZero,
websocket: {
message() {},
idleTimeout: negativeZero,
maxPayloadLength: negativeZero,
backpressureLimit: negativeZero,
},
fetch,
});
using server = serve({ port: 0, ...all() });
server.reload(all());
expect(server.port).toBeGreaterThan(0);
});
});

describe("Bun.serve development options", () => {
test("development mode", () => {
using server = serve({
Expand Down
Loading
Loading