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
1 change: 1 addition & 0 deletions src/runtime/bake/DevServer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ pub(crate) fn init(options: Options) -> JsResult<Box<DevServer>> {

types.push(framework_router::Type {
abs_root: strings::without_trailing_slash(entry.abs_path).into(),
prefix: fsr.prefix.as_ref().into(),
ignore_underscores: fsr.ignore_underscores,
ignore_dirs: fsr
.ignore_dirs
Expand Down
95 changes: 81 additions & 14 deletions src/runtime/bake/FrameworkRouter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ pub type RouteIndex = bun_core::GenericIndex<u32, RouteMarker>;
/// Native code for `FrameworkFileSystemRouterType`
pub struct Type {
pub(crate) abs_root: Box<[u8]>,
/// URL path of the mount point. `scan` puts its segments in front of every pattern.
pub(crate) prefix: Box<[u8]>,
pub(crate) ignore_underscores: bool,
pub(crate) ignore_dirs: Box<[Box<[u8]>]>,
pub(crate) extensions: Box<[Box<[u8]>]>,
Expand All @@ -150,6 +152,7 @@ impl Default for Type {
fn default() -> Self {
Self {
abs_root: Box::default(),
prefix: Box::default(),
ignore_underscores: false,
ignore_dirs: Box::new([
Box::<[u8]>::from(b".git".as_slice()),
Expand All @@ -169,6 +172,32 @@ impl Type {
pub(crate) fn root_route_index(type_index: TypeIndex) -> RouteIndex {
RouteIndex::init(type_index.get() as u32)
}

/// Says why `prefix` is not a fixed, normalized URL path that a request can match.
pub(crate) fn validate_prefix(prefix: &[u8]) -> Result<(), &'static str> {
if prefix.first() != Some(&b'/') {
return Err("must start with \"/\"");
}
if prefix.len() >= MAX_PATH_BYTES {
return Err("is too long");
}
// A client sends `" < > ^ \` { }` percent-encoded, so they never match.
if !prefix.iter().all(u8::is_ascii_graphic)
|| strings::index_of_any(prefix, b"?#\\:*\"<>^`{}").is_some()
{
return Err(
"can only contain printable ASCII characters, and none of ? # \\ : * \" < > ^ ` { }",
);
}
if strings::tokenize(prefix, b"/").any(|segment| segment == b"." || segment == b"..") {
return Err("cannot contain a \".\" or \"..\" segment");
}
// `/_bun` holds the dev server's own routes and the build's assets.
if strings::tokenize(prefix, b"/").next() == Some(b"_bun".as_slice()) {
return Err("cannot be under \"/_bun\", which Bun reserves");
}
Ok(())
}
}

pub enum TypeMarker {}
Expand Down Expand Up @@ -1132,18 +1161,17 @@ impl FrameworkRouter {

let file_id = ctx.get_file_id_for_router(file_path, new_route_index, file_kind)?;

let new_route = self.route_ptr_mut(new_route_index);
if let Some(existing) = *new_route.file_ptr(file_kind) {
if let Some(existing) = *self.route_ptr_mut(new_route_index).file_ptr(file_kind) {
if existing == file_id {
return Ok(()); // exact match already exists. Hot-reloading code hits this
}
*out_colliding_file_id = existing;
return Err(InsertError::RouteCollision);
}
*new_route.file_ptr(file_kind) = Some(file_id);

if file_kind == FileKind::Page {
match pattern {
// A different route can still serve the same URLs (see `dynamic_routes`).
let aliased_route = match pattern {
InsertPattern::Static(p) => {
let key: &[u8] = if p.route_path().is_empty() {
b"/"
Expand All @@ -1152,19 +1180,32 @@ impl FrameworkRouter {
};
let gop = self.static_routes.get_or_put(key)?;
if gop.found_existing {
panic!("TODO: propagate aliased route error");
Some(*gop.value_ptr)
} else {
*gop.value_ptr = new_route_index;
None
}
*gop.value_ptr = new_route_index;
}
InsertPattern::Dynamic(p) => {
let gop = self.dynamic_routes.get_or_put(p)?;
if gop.found_existing {
panic!("TODO: propagate aliased route error");
Some(*gop.value_ptr)
} else {
*gop.value_ptr = new_route_index;
None
}
*gop.value_ptr = new_route_index;
}
};
if let Some(aliased_route) = aliased_route {
*out_colliding_file_id = self
.route_ptr(aliased_route)
.file_page
.expect("routes in the url maps have a page");
return Err(InsertError::RouteCollision);
}
}

*self.route_ptr_mut(new_route_index).file_ptr(file_kind) = Some(file_id);
Ok(())
}
}
Expand Down Expand Up @@ -1609,6 +1650,16 @@ impl FrameworkRouter {
};

let mut log = TinyLog::empty();
// `effective_url_hash` and `PatternBuffer` size their buffers for a file path.
if t.prefix.len() + rel_path.len() >= MAX_PATH_BYTES {
log.fail(
format_args!("The URL of this route is too long"),
0,
full_rel_path.len(),
);
ctx.on_router_syntax_error(full_rel_path, log)?;
continue 'outer;
}
// The arena is reset at the end of every arm via
// `reset_retain_with_limit(8M)` — keep the `mi_heap`
// warm between directory entries instead of paying
Expand Down Expand Up @@ -1639,9 +1690,14 @@ impl FrameworkRouter {
continue 'outer;
}

let mut parts: ArenaVec<'_, Part<'_>> = ArenaVec::new_in(arena_state);
parts.extend(strings::tokenize(&t.prefix, b"/").map(Part::Text));
parts.extend_from_slice(parsed.parts);
let parts: &[Part<'_>] = parts.into_bump_slice();
Comment thread
robobun marked this conversation as resolved.

let mut static_total_len: usize = 0;
let mut param_count: usize = 0;
for part in parsed.parts {
for part in parts {
match part {
Part::Text(data) => static_total_len += 1 + data.len(),
Part::Param(_) | Part::CatchAll(_) | Part::CatchAllOptional(_) => {
Expand Down Expand Up @@ -1669,10 +1725,8 @@ impl FrameworkRouter {
};

let result = if param_count > 0 {
let pattern = EncodedPattern::init_from_parts(
parsed.parts,
&self.pattern_string_arena,
)?;
let pattern =
EncodedPattern::init_from_parts(parts, &self.pattern_string_arena)?;
self.insert(
t_index,
InsertPattern::Dynamic(pattern),
Expand All @@ -1686,7 +1740,7 @@ impl FrameworkRouter {
.pattern_string_arena
.alloc_slice_fill_default::<u8>(static_total_len);
let mut pos = 0usize;
for part in parsed.parts {
for part in parts {
match part {
Part::Text(data) => {
allocation[pos] = b'/';
Expand Down Expand Up @@ -1791,6 +1845,18 @@ impl JSFrameworkRouter {
_ => return Err(global.throw_invalid_arguments(format_args!("Missing options.root"))),
};

let prefix: Box<[u8]> = match opts.get_optional_slice(global, b"prefix")? {
Some(prefix) => {
if let Err(reason) = Type::validate_prefix(&prefix) {
return Err(
global.throw_invalid_arguments(format_args!("options.prefix {reason}"))
);
}
prefix.slice().into()
}
None => Box::default(),
};

let style = Style::from_js(
opts.get(global, "style")?.unwrap_or(JSValue::UNDEFINED),
global,
Expand All @@ -1809,6 +1875,7 @@ impl JSFrameworkRouter {

let types: Box<[Type]> = Box::new([Type {
abs_root: abs_root.clone(),
prefix,
ignore_underscores: false,
extensions: Box::new([
b".tsx".as_slice().into(),
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/bake/bake.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ declare module "bun" {
*/
root: string;
/**
* The prefix to serve this directory on.
* The URL path to mount this directory on. With `/docs`, `about.tsx` is served at `/docs/about`.
* @default "/"
*/
prefix?: string | undefined;
Expand Down
19 changes: 12 additions & 7 deletions src/runtime/bake/bake_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,11 @@ impl Framework {
get_optional_string(fsr_opts, global, b"clientEntryPoint", refs)?;
let prefix =
get_optional_string(fsr_opts, global, b"prefix", refs)?.unwrap_or(b"/");
if let Err(reason) = framework_router::Type::validate_prefix(prefix) {
return Err(global.throw_invalid_arguments(format_args!(
"'fileSystemRouterTypes[{i}].prefix' {reason}"
)));
}
let ignore_underscores = fsr_opts
.get_boolean_strict(global, "ignoreUnderscores")?
.unwrap_or(false);
Expand Down Expand Up @@ -984,24 +989,24 @@ impl Framework {
]
};

let ignore_dirs: &'static [&'static [u8]] = if let Some(exts_js) =
let ignore_dirs: &'static [&'static [u8]] = if let Some(dirs_js) =
fsr_opts.get(global, "ignoreDirs")?
{
'exts: {
if exts_js.is_array() {
let mut it_2 = array.array_iterator(global)?;
'dirs: {
if dirs_js.is_array() {
let mut it_2 = dirs_js.array_iterator(global)?;
let mut dirs = bun_alloc::ArenaVec::<&'static [u8]>::with_capacity_in(
len as usize,
dirs_js.get_length(global)? as usize,
arena,
);
while let Some(array_item) = it_2.next()? {
dirs.push(refs.track(array_item.to_utf8(global)?));
}
break 'exts arena_erase(dirs.into_bump_slice());
break 'dirs arena_erase(dirs.into_bump_slice());
}

return Err(global.throw_invalid_arguments(format_args!(
"'ignoreDirs' must be an array of strings or \"*\" for all extensions"
"'ignoreDirs' must be an array of strings"
)));
}
} else {
Expand Down
1 change: 1 addition & 0 deletions src/runtime/bake/production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result<
abs_root: Box::from(strings::paths::without_trailing_slash_windows_path(
entry.abs_path,
)),
prefix: Box::from(fsr.prefix),
ignore_underscores: fsr.ignore_underscores,
ignore_dirs: fsr
.ignore_dirs
Expand Down
27 changes: 17 additions & 10 deletions src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,18 +800,19 @@ impl AnyRoute {
)));
}

if strings::index_of_char(path, b':').is_some() {
return Err(global.throw_invalid_arguments(format_args!(
"Directory routes do not support :parameters; use a fixed prefix ending in `/*`"
)));
}
if strings::contains(path, b"//") {
return Err(global.throw_invalid_arguments(format_args!(
"Directory route paths cannot contain empty segments"
)));
}

let style_js = argument.get(global, b"style")?;
if style_js.is_none() {
if strings::index_of_char(path, b':').is_some() {
return Err(global.throw_invalid_arguments(format_args!(
"Directory routes do not support :parameters; use a fixed prefix ending in `/*`"
)));
}
if strings::contains(path, b"//") {
return Err(global.throw_invalid_arguments(format_args!(
"Directory route paths cannot contain empty segments"
)));
}
// `{ dir }` without `style` serves the directory tree
// verbatim; `{ dir, style }` opts into framework routing.
let url_prefix: &[u8] = if path.len() == 2 {
Expand Down Expand Up @@ -848,6 +849,12 @@ impl AnyRoute {
} else {
Cow::Owned(path[..path.len() - 2].to_vec())
};
if let Err(reason) = FrameworkRouter::Type::validate_prefix(&prefix) {
return Err(global.throw_invalid_arguments(format_args!(
"Invalid route {}. The path before `/*` {reason}",
bun_fmt::quote(path),
)));
}
init_ctx
.framework_router_list
.push(bake::FileSystemRouterType {
Expand Down
Loading
Loading