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
29 changes: 29 additions & 0 deletions src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,11 @@ impl AnyRoute {
"To mount a directory, make sure the path ends in `/*`"
)));
}
if strings::contains_char(relative_root, 0) {
return Err(global.throw_invalid_arguments(format_args!(
"\"dir\" must not contain null bytes"
)));
}

let style_js = argument.get(global, b"style")?;
if style_js.is_none() {
Expand Down Expand Up @@ -835,6 +840,30 @@ impl AnyRoute {
FrameworkRouter::Style::from_js(style_js.unwrap(), global)?;
// Style impls Drop; `?` drops it on the error path.

{
use bun_sys_jsc::ErrorJsc;
let mut buf = paths::path_buffer_pool::get();
let Some(abs_root) =
paths::resolve_path::join_abs_string_buf_checked::<paths::platform::Auto>(
paths::fs::FileSystem::instance().top_level_dir(),
&mut buf[..],
&[relative_root],
)
else {
let err = sys::Error::from_code(sys::E::ENAMETOOLONG, sys::Tag::open)
.with_path(relative_root);
return Err(global.throw_value(err.to_js(global)?));
};
match sys::open_a(
Comment thread
robobun marked this conversation as resolved.
abs_root,
sys::O::DIRECTORY | sys::O::CLOEXEC | sys::O::RDONLY,
0,
) {
Ok(fd) => drop(sys::File::from_fd(fd)),
Err(err) => return Err(global.throw_value(err.to_js(global)?)),
}
}

// trim the /*
// NOTE: `FileSystemRouterType` fields are `Cow<'static,[u8]>`.
// Rather
Expand Down
42 changes: 42 additions & 0 deletions test/js/bun/http/serve-directory-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,48 @@ describe("Bun.serve() directory routes", () => {
).toThrow(expect.objectContaining({ code: "ENOENT" }));
});

it("throws if the directory of a { dir, style } mount does not exist", () => {
using dir = tempDir("serve-dir-style-enoent", {});
expect(() =>
serve({
port: 0,
development: true,
routes: { "/*": { dir: join(String(dir), "does-not-exist"), style: "nextjs-pages" } },
}),
).toThrow(expect.objectContaining({ code: "ENOENT" }));
});

it("throws if the directory of a { dir, style } mount is a file", () => {
using dir = tempDir("serve-dir-style-enotdir", { pages: "not a directory" });
expect(() =>
serve({
port: 0,
development: true,
routes: { "/*": { dir: join(String(dir), "pages"), style: "nextjs-pages" } },
}),
).toThrow(expect.objectContaining({ code: "ENOTDIR" }));
});

it("rejects a dir with an embedded null byte", () => {
using dir = tempDir("serve-dir-nul", { "public/x.txt": "x" });
const withNul = join(String(dir), "public") + "\0ignored";
expect(() => serve({ port: 0, routes: { "/*": { dir: withNul } } })).toThrow(/null bytes/);
expect(() =>
serve({ port: 0, development: true, routes: { "/*": { dir: withNul, style: "nextjs-pages" } } }),
).toThrow(/null bytes/);
});

it("throws if the directory of a { dir, style } mount is too long for a path", () => {
const dir = Buffer.alloc(100_000, "a").toString();
expect(() =>
serve({
port: 0,
development: true,
routes: { "/*": { dir, style: "nextjs-pages" } },
}),
).toThrow(expect.objectContaining({ code: "ENAMETOOLONG" }));
});

it("serves correctly with more unique paths than stat-cache slots", async () => {
const N = 300; // > STAT_CACHE_SLOTS (256)
const files: Record<string, string> = {};
Expand Down
Loading