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
22 changes: 14 additions & 8 deletions src/runtime/api/filesystem_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ impl FileSystemRouter {
let mut origin_str: ZigStringSlice = ZigStringSlice::default();
let mut asset_prefix_slice: ZigStringSlice = ZigStringSlice::default();

let mut out_buf = [0u8; MAX_PATH_BYTES * 2];
// Backs `root_dir_path` when `dir` is relative; read until `root_dir_info` is looked up.
let mut dir_buf = path::path_buffer_pool::get();
if let Some(style_val) = argument.get(global_this, "style")? {
if !(style_val.get_zig_string(global_this)?).eql_comptime("nextjs") {
return Err(global_this.throw_invalid_arguments(format_args!(
Expand All @@ -160,14 +161,19 @@ impl FileSystemRouter {
if path::Platform::AUTO.is_absolute(path_) {
root_dir_path = root_dir_path_;
} else {
let parts: [&[u8]; 1] = [path_];
root_dir_path = ZigStringSlice::from_utf8_never_free(
path::resolve_path::join_abs_string_buf::<path::platform::Auto>(
let Some(joined) =
path::resolve_path::join_abs_string_buf_checked::<path::platform::Auto>(
Fs::FileSystem::instance().top_level_dir,
&mut out_buf,
&parts,
),
);
&mut dir_buf[..],
&[path_],
)
else {
return Err(global_this.throw_invalid_arguments(format_args!(
"Expected dir to resolve to a path of at most {} bytes",
MAX_PATH_BYTES
)));
};
root_dir_path = ZigStringSlice::from_utf8_never_free(joined);
}
}
} else {
Expand Down
63 changes: 63 additions & 0 deletions test/js/bun/util/filesystem_router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,69 @@ it("dir should be validated", async () => {
}).toThrow("Expected dir to be a string");
});

it("throws instead of aborting when a relative dir no longer fits in a path buffer once joined with the cwd", async () => {
// A relative `dir` is joined onto the cwd inside a MAX_PATH_BYTES buffer
// (src/bun_core/util.rs). The constructor used to write past the end of that
// buffer and abort the process, so run the cases in a subprocess. An absolute
// `dir` never touches the buffer and is reported as a missing directory.
const maxPathBytes = isWindows ? 32767 * 3 + 1 : isMacOS ? 1024 : 4096;
const tooLong = `TypeError: Expected dir to resolve to a path of at most ${maxPathBytes} bytes`;
using dir = tempDir("fsr-long-relative-dir", {
"pages/index.tsx": "export default 1;\n",
});

const code = /* ts */ `
import path from "path";
function construct(dir: string) {
try {
const router = new Bun.FileSystemRouter({ dir, style: "nextjs", fileExtensions: [".tsx"] });
return Object.keys(router.routes);
} catch (e: any) {
return e.name + ": " + e.message.replace(dir, "<dir>").replace(process.cwd(), "<cwd>");
}
}
// The joined path is cwd + separator + dir.
const resolvingTo = (bytes: number) => Buffer.alloc(bytes - Buffer.byteLength(process.cwd()) - 1, "a").toString();
// Longer than MAX_PATH_BYTES on every platform, and than the 2 * MAX_PATH_BYTES
// buffer the constructor used to overflow (196604 bytes on Windows).
const longDir = Buffer.alloc(250_000, "a").toString();
console.log(JSON.stringify({
relative: construct(longDir),
absolute: construct(path.parse(process.cwd()).root + longDir),
atLimit: construct(resolvingTo(${maxPathBytes})),
oneByteOverLimit: construct(resolvingTo(${maxPathBytes} + 1)),
afterwards: construct("pages"),
}));
`;

await using proc = Bun.spawn({
cmd: [bunExe(), "-e", code],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// An aborted subprocess prints nothing to stdout; keep it as-is so the panic in stderr is what the diff shows.
expect({
stdout: stdout === "" ? stdout : JSON.parse(stdout),
stderr,
exitCode,
signalCode: proc.signalCode,
}).toEqual({
stdout: {
relative: tooLong,
absolute: "Error: Unable to find directory: <dir>",
atLimit: `Error: Unable to find directory: <cwd>${path.sep}<dir>`,
oneByteOverLimit: tooLong,
afterwards: ["/"],
},
stderr: "",
exitCode: 0,
signalCode: null,
});
});

it("origin should be validated", async () => {
const { dir } = make(["posts.tsx"]);

Expand Down
Loading